Final sweep: Crucible fixes, Path Tracer polish, forge progress, tests green.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Align dashboard subtitle default and UpsertAgent tests with fleet label behavior; WebSocket coalesce and PathForge hardening; Crucible expanded ops and visual DV fixes; Vitest 610/610 and full test-suite pass; trim PROBLEMS.md to open items only.
This commit is contained in:
@@ -153,6 +153,12 @@ type BuildArtifactFile struct {
|
||||
FilePath string `json:"file_path,omitempty"`
|
||||
}
|
||||
|
||||
// BuildProgress is returned by GET /builder/progress/{token} while a forge is running.
|
||||
type BuildProgress struct {
|
||||
Stage string `json:"stage"`
|
||||
Pct int `json:"pct"`
|
||||
}
|
||||
|
||||
func buildExtraFilesFromArtifacts(arts []BuildArtifactFile) []models.BuildExtraFile {
|
||||
if len(arts) == 0 {
|
||||
return nil
|
||||
@@ -181,6 +187,11 @@ type Handler struct {
|
||||
// can abort an in-progress compile via DELETE /api/v1/builder/cancel/{token}.
|
||||
activeCancelsMu sync.Mutex
|
||||
activeCancels map[string]context.CancelFunc
|
||||
|
||||
// Real-time build progress — maps cancel_token → current stage so the frontend
|
||||
// can poll GET /api/v1/builder/progress/{token} instead of running a fake timer.
|
||||
activeProgressMu sync.RWMutex
|
||||
activeProgress map[string]BuildProgress
|
||||
}
|
||||
|
||||
// SetFleetSecret stores the fleet secret so it is baked into every forged binary.
|
||||
@@ -231,6 +242,46 @@ func (h *Handler) unregisterCancel(token string) {
|
||||
h.activeCancelsMu.Unlock()
|
||||
}
|
||||
|
||||
// setProgress records the current forge stage so the frontend can poll it.
|
||||
func (h *Handler) setProgress(token, stage string, pct int) {
|
||||
if token == "" {
|
||||
return
|
||||
}
|
||||
h.activeProgressMu.Lock()
|
||||
if h.activeProgress == nil {
|
||||
h.activeProgress = make(map[string]BuildProgress)
|
||||
}
|
||||
h.activeProgress[token] = BuildProgress{Stage: stage, Pct: pct}
|
||||
h.activeProgressMu.Unlock()
|
||||
}
|
||||
|
||||
func (h *Handler) clearProgress(token string) {
|
||||
if token == "" {
|
||||
return
|
||||
}
|
||||
h.activeProgressMu.Lock()
|
||||
delete(h.activeProgress, token)
|
||||
h.activeProgressMu.Unlock()
|
||||
}
|
||||
|
||||
// ServeProgress returns the current build stage for a running forge identified by its cancel token.
|
||||
// The frontend polls this every second to drive a real progress bar instead of a client-side simulation.
|
||||
func (h *Handler) ServeProgress(w http.ResponseWriter, r *http.Request) {
|
||||
token := chi.URLParam(r, "token")
|
||||
if token == "" {
|
||||
http.Error(w, "token required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h.activeProgressMu.RLock()
|
||||
prog, ok := h.activeProgress[token]
|
||||
h.activeProgressMu.RUnlock()
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, BuildProgress{Stage: "", Pct: 0})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, prog)
|
||||
}
|
||||
|
||||
type SignPolicy struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CertThumbprint string `json:"cert_thumbprint"`
|
||||
@@ -346,6 +397,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancelFn = context.WithCancel(ctx)
|
||||
h.registerCancel(req.CancelToken, cancelFn)
|
||||
defer h.unregisterCancel(req.CancelToken)
|
||||
defer h.clearProgress(req.CancelToken)
|
||||
}
|
||||
|
||||
// FusionOutputName will be derived from the payload filename if not set
|
||||
@@ -539,12 +591,14 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
return BuildResponse{Success: false, Error: "Failed to create build directory"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
h.setProgress(req.CancelToken, "Copying source files", 5)
|
||||
if err := h.copyAgentSource(agentDir); err != nil {
|
||||
cleanupBuild()
|
||||
log.Printf("Failed to copy agent source: %v", err)
|
||||
return BuildResponse{Success: false, Error: "Failed to prepare agent source: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
h.setProgress(req.CancelToken, "Configuring build", 14)
|
||||
configDir := filepath.Join(agentDir, "config")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
cleanupBuild()
|
||||
@@ -553,18 +607,21 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
|
||||
platforms := platformsForRequest(req)
|
||||
p := platforms[0]
|
||||
h.setProgress(req.CancelToken, "Compiling agent", 20)
|
||||
outputPath, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
|
||||
if err != nil {
|
||||
cleanupBuild()
|
||||
log.Printf("Build failed: %v", err)
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
h.setProgress(req.CancelToken, "Compiled — linking output", 72)
|
||||
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
|
||||
workerName := filepath.Base(outputPath)
|
||||
finalPath := outputPath
|
||||
finalName := workerName
|
||||
var fusionEnabled bool
|
||||
|
||||
h.setProgress(req.CancelToken, "Writing scripts", 76)
|
||||
uninstallName, uninstallPath, err := h.writeUninstallScript(buildDir, buildID, req)
|
||||
if err != nil {
|
||||
cleanupBuild()
|
||||
@@ -577,12 +634,14 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
if req.FusionPayloadKind == "" {
|
||||
req.FusionPayloadKind = detectFusionPayloadKind(prepPath)
|
||||
}
|
||||
h.setProgress(req.CancelToken, "Building fusion bundle", 80)
|
||||
var err error
|
||||
fusionRes, err = h.buildFusionFromRequest(ctx, buildDir, prepPath, outputPath, req)
|
||||
if err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
h.setProgress(req.CancelToken, "Fusion bundle ready", 88)
|
||||
finalPath = fusionRes.LauncherPath
|
||||
finalName = filepath.Base(finalPath)
|
||||
fusionEnabled = true
|
||||
@@ -658,6 +717,7 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
}
|
||||
bundleDownloadURL = fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, bundleFileName)
|
||||
} else {
|
||||
h.setProgress(req.CancelToken, "Publishing build", 91)
|
||||
var err error
|
||||
exportPath, err = h.publishRootExecutable(finalPath, finalName)
|
||||
if err != nil {
|
||||
@@ -680,6 +740,7 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
|
||||
signed := false
|
||||
if h.shouldSignBuild(req) {
|
||||
h.setProgress(req.CancelToken, "Signing binary", 95)
|
||||
if err := h.signExecutable(finalPath); err != nil {
|
||||
return BuildResponse{Success: false, Error: "Build succeeded but signing failed: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
@@ -692,6 +753,7 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
scrambled := false
|
||||
fingerprint := ""
|
||||
if shouldSigilScramble(req) {
|
||||
h.setProgress(req.CancelToken, "Scrambling sigil", 97)
|
||||
fp, err := ApplySigilScramble(finalPath, buildID)
|
||||
if err != nil {
|
||||
log.Printf("[Forge] sigil scramble: %v", err)
|
||||
@@ -750,6 +812,7 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
PoolTLS: req.PoolTLS,
|
||||
PoolPass: req.PoolPass,
|
||||
}
|
||||
h.setProgress(req.CancelToken, "Saving to database", 99)
|
||||
if err := h.db.InsertBuild(buildRecord); err != nil {
|
||||
log.Printf("Failed to record build: %v", err)
|
||||
return BuildResponse{Success: false, Error: "Failed to record build in database"}, http.StatusInternalServerError, ""
|
||||
|
||||
Reference in New Issue
Block a user