generated from mathias/template-go-web
Adds the repos panel from #4's "Not yet shipped" list, following the shipped
issues slice (b2761a4) structurally rather than inventing a second approach:
- internal/atlas/repos.go — RepoNodes(), a pure parser over an already-decoded
/user/repos response. Archived repos are dropped: the atlas shows where work
happens, and an archived repo is by definition somewhere it does not.
- internal/gitea/client.go — MyRepos(), same PAT and same contract as MyIssues:
errors when GITEA_TOKEN is unset so the caller skips the overlay rather than
issuing an unauthenticated request. That unset path is the one running in
prod today (#4), so it is tested, not assumed.
- internal/web/handler.go — wired into liveOverlay().
Stage 05 is the right home — repos are what the execution stage acts on — but
#11 had just claimed its `generate` key for stuck-badges, and a stage could
only name one source. So `generate` now accepts either spelling:
"generate": "ci-jobs"
"generate": ["stuck-badges", "gitea-repos"]
The bare string stays valid and marshals back as a bare string, so no authored
stage is rewritten and the frontend contract is unchanged. Stages 04 and 08
already share stuck-badges between them; this is the same growth one level in.
The overlay merge also moves out of the handler into atlas.PrependGenerated,
which is unit-testable where the inline loop was not — it sat behind a network
call. It covers the issues path too, and fixes a latent aliasing bug there: the
old `append(liveNodes, authored...)` wrote into the live slice's backing array,
so two stages sharing a key would corrupt each other. That shape is no longer
hypothetical now that stuck-badges spans three stages.
Verified end-to-end both ways: without GITEA_TOKEN the atlas returns 200 with
stage 05's authored nodes intact; with a real token stage 05 serves both
overlays at once — stuck badges, then eight live repos, then the authored
nodes — and a repo with no language emits one tag rather than an empty pill.
task check green (golangci-lint 0 issues, vet, -race).
Closes #10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
3.9 KiB
Go
101 lines
3.9 KiB
Go
// stuck.go overlays cad-atlas#11's stuck-state badges onto the pipeline: a
|
|
// count badge per stage (Node entries + Stage.StuckCount/AwaitingCount),
|
|
// sourced from mathias/cad#14's `cad stuck` query rather than re-implemented
|
|
// here. cad's app/domain packages are public Go modules (not internal/) —
|
|
// this reuses the exact tested classification logic instead of duplicating
|
|
// the taxonomy, per ADR-0002 ("a small coupling surface, not a monolith").
|
|
package atlas
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
caddomain "git.d-ma.be/mathias/cad/domain"
|
|
)
|
|
|
|
// stageForCategory maps a cad-stuck taxonomy row onto the existing 9-stage
|
|
// pipeline (atlas.json), following the grouping cad-atlas#11 itself proposes:
|
|
// Trigger→armed-but-never-fired sits at the human dispatch gate (the label
|
|
// that authorizes dispatch is on, nothing has claimed it yet); Claim/Execute
|
|
// collapse into the one Execute stage this pipeline already has (there is no
|
|
// separate Claim stage); Close maps to the loop-back stage, where a human
|
|
// closing the issue is the action that actually completes the loop.
|
|
//
|
|
// CategoryDoneNotClosed is deliberately absent from the "stuck" class — see
|
|
// ApplyStuckOverlay, which reads Class rather than Category to decide the
|
|
// visual treatment. It IS present here for stage placement.
|
|
var stageForCategory = map[caddomain.StuckCategory]string{
|
|
caddomain.CategoryArmedNeverFired: "STAGE 04",
|
|
caddomain.CategoryClaimedNeverRan: "STAGE 05",
|
|
caddomain.CategoryCeilingHit: "STAGE 05",
|
|
caddomain.CategoryReFireLoop: "STAGE 05",
|
|
caddomain.CategoryNearAttemptCap: "STAGE 05",
|
|
caddomain.CategoryDoneNotClosed: "STAGE 08",
|
|
}
|
|
|
|
// stuckNodeCls / awaitingNodeCls are the CSS classes the static shell colours
|
|
// (cad-atlas.html: .node.stuck, .node.awaiting-human) — the two visual
|
|
// classes cad-atlas#11 asks for, kept visually distinct so "awaiting human"
|
|
// (a functioning control) never reads as "stuck" (needs intervention).
|
|
const (
|
|
stuckNodeCls = "stuck"
|
|
awaitingNodeCls = "awaiting-human"
|
|
)
|
|
|
|
// ApplyStuckOverlay assigns each finding to the stage stageForCategory names,
|
|
// and for every stage in stages marked `"generate":"stuck-badges"`, sets its
|
|
// StuckCount/AwaitingCount and prepends one Node per finding assigned there
|
|
// (title, class, repo tag, and a URL to the finding's first locator — this
|
|
// overlay never adds a control, only a link out to Gitea). Findings whose
|
|
// category has no stage mapping are silently omitted from the pipeline view
|
|
// rather than misplaced — cad-atlas#11 is a read of the query, not a second
|
|
// copy of its taxonomy, so an unmapped category is a gap to notice in
|
|
// stageForCategory, not something to guess a stage for.
|
|
//
|
|
// stages is returned unmodified aside from the stuck-badges stages; this is a
|
|
// pure function, no I/O, so it is fixture-testable without a live query.
|
|
func ApplyStuckOverlay(stages []Stage, findings []caddomain.StuckFinding) []Stage {
|
|
byStage := make(map[string][]caddomain.StuckFinding)
|
|
for _, f := range findings {
|
|
stageNo, ok := stageForCategory[f.Category]
|
|
if !ok {
|
|
continue
|
|
}
|
|
byStage[stageNo] = append(byStage[stageNo], f)
|
|
}
|
|
|
|
out := make([]Stage, len(stages))
|
|
copy(out, stages)
|
|
for i := range out {
|
|
if !out[i].Generate.Has("stuck-badges") {
|
|
continue
|
|
}
|
|
assigned := byStage[out[i].No]
|
|
var stuckN, awaitingN int
|
|
nodes := make([]Node, 0, len(assigned))
|
|
for _, f := range assigned {
|
|
cls := stuckNodeCls
|
|
if f.Class == caddomain.ClassAwaitingHuman {
|
|
cls = awaitingNodeCls
|
|
awaitingN++
|
|
} else {
|
|
stuckN++
|
|
}
|
|
var url string
|
|
if len(f.Locators) > 0 {
|
|
url = f.Locators[0]
|
|
}
|
|
nodes = append(nodes, Node{
|
|
Title: fmt.Sprintf("#%d %s", f.Issue, f.Category),
|
|
Desc: f.Detail,
|
|
Cls: cls,
|
|
Tags: []string{"live · cad-stuck", f.Repo},
|
|
URL: url,
|
|
})
|
|
}
|
|
out[i].StuckCount = stuckN
|
|
out[i].AwaitingCount = awaitingN
|
|
out[i].Nodes = append(nodes, out[i].Nodes...)
|
|
}
|
|
return out
|
|
}
|