Harden worker federation and operator UI

This commit is contained in:
2026-07-29 13:30:55 +04:00
parent 95a96d87a5
commit 1ca9d64e89
35 changed files with 1195 additions and 581 deletions
+17 -2
View File
@@ -136,7 +136,9 @@ func (s Server) detail(ctx context.Context, id string) (TaskDetail, error) {
if !ok {
return TaskDetail{}, domain.ErrNotFound
}
d := TaskDetail{Task: t, Actions: actions(t)}
// Keep collection fields as JSON arrays for browser clients, including
// older task records that genuinely have no retained events.
d := TaskDetail{Task: t, Events: []domain.Event{}, Actions: actions(t)}
for _, e := range s.Store.Events(0) {
if e.TaskID != id {
continue
@@ -165,9 +167,14 @@ func (s Server) detail(ctx context.Context, id string) (TaskDetail, error) {
session.Blocker = "capture unavailable: " + err.Error()
}
d.Session = session
} else if t.State == domain.StateBlocked {
// A blocked task has no active lease by definition, but must retain its
// last observed pane evidence instead of rendering an unexplained void.
d.Session = &Session{PaneID: t.LastPaneID, HarnessID: t.LastHarness, AgentStatus: t.PaneState, Blocker: t.Blocker}
}
return d, nil
}
// captureRevision identifies *what the operator saw*, not when they saw it.
// B20: this was UnixNano, so it changed on every read and said nothing about
// whether the pane had changed. A content hash changes if and only if the
@@ -206,7 +213,15 @@ func actions(t domain.Task) []Action {
return []Action{{ID: "handoff", Enabled: active, Reason: "requires a live leased session"}, {ID: "release", Enabled: active, Needs: []string{"reason or handoff_ref"}}, {ID: "block", Enabled: active, Needs: []string{"blocker"}}, {ID: "complete", Enabled: active, Needs: []string{"report_ref", "receipt"}}}
}
func (s Server) Overview(ctx context.Context) Overview {
out := Overview{Tasks: s.Store.Tasks(), UpdatedAt: time.Now().UTC()}
// JSON null is not an empty collection to browser clients. In particular,
// the shell renders the number of active sessions before any page-level
// loading state, so a nil Sessions slice made an otherwise healthy empty
// worker pool crash the entire SPA on `sessions.length`.
tasks := s.Store.Tasks()
if tasks == nil {
tasks = []domain.Task{}
}
out := Overview{Tasks: tasks, Workers: []federation.Worker{}, Sessions: []Session{}, UpdatedAt: time.Now().UTC()}
if s.Workers != nil {
out.Workers = s.Workers.Snapshot()
}