Split out of #4's "Not yet shipped" list so it can be worked as one self-contained change. #4 stays open as the umbrella.
Goal
Show the user's own Gitea repos in the atlas, the same way open issues already appear on stage 03.
The pattern to mirror — already in this repo
The issues slice (b2761a4) is the template. Follow it structurally rather than inventing a new approach:
Layer
Existing (issues)
To add (repos)
Pure parser
internal/atlas/issues.go → IssueNodes()
RepoNodes() — Gitea repo list → stage nodes
API client
internal/gitea/client.go → MyIssues()
MyRepos() — authenticated GET, GITEA_TOKEN
Wiring
internal/web/handler.go → liveOverlay()
same function, onto the appropriate stage
Config
generate:"gitea-issues" in atlas.json
an equivalent generate: key for repos
Acceptance criteria
RepoNodes() is a pure function over an already-decoded API response, unit-tested table-driven — no network in the test. Match issues.go's test style.
Each node carries the repo name, a short description or tag, and a clickable html_url (the frontend already renders any node with a url as <a class="node">).
MyRepos() returns an error when GITEA_TOKEN is unset, and the overlay skips gracefully — the atlas must still render with its authored placeholder nodes, exactly like the issues path does today. This is load-bearing: GITEA_TOKEN is still not provisioned in prod (#4), so the unset path is the one that runs live right now.
Archived repos are excluded.
task check green (lint, vet, race tests).
Out of scope
PRs panel and the per-repo Actions-runs generalization — separate bullets on #4, don't pull them in here.
Anything under mathias/infra, including the GITEA_TOKEN ExternalSecret. That needs a real PAT and a human; do not attempt it, and do not work around its absence with a hardcoded token.
Notes
Read-only only. The token is Mathias's own PAT, so Gitea's own visibility rules already scope what can be seen — do not add any filtering that assumes otherwise.
Split out of #4's "Not yet shipped" list so it can be worked as one self-contained change. #4 stays open as the umbrella.
## Goal
Show the user's own Gitea repos in the atlas, the same way open issues already appear on stage 03.
## The pattern to mirror — already in this repo
The issues slice (b2761a4) is the template. Follow it structurally rather than inventing a new approach:
| Layer | Existing (issues) | To add (repos) |
|---|---|---|
| Pure parser | `internal/atlas/issues.go` → `IssueNodes()` | `RepoNodes()` — Gitea repo list → stage nodes |
| API client | `internal/gitea/client.go` → `MyIssues()` | `MyRepos()` — authenticated GET, `GITEA_TOKEN` |
| Wiring | `internal/web/handler.go` → `liveOverlay()` | same function, onto the appropriate stage |
| Config | `generate:"gitea-issues"` in `atlas.json` | an equivalent `generate:` key for repos |
## Acceptance criteria
- [ ] `RepoNodes()` is a pure function over an already-decoded API response, unit-tested table-driven — no network in the test. Match `issues.go`'s test style.
- [ ] Each node carries the repo name, a short description or tag, and a clickable `html_url` (the frontend already renders any node with a `url` as `<a class="node">`).
- [ ] `MyRepos()` returns an error when `GITEA_TOKEN` is unset, and the overlay **skips gracefully** — the atlas must still render with its authored placeholder nodes, exactly like the issues path does today. This is load-bearing: `GITEA_TOKEN` is still not provisioned in prod (#4), so the unset path is the one that runs live right now.
- [ ] Archived repos are excluded.
- [ ] `task check` green (lint, vet, race tests).
## Out of scope
- PRs panel and the per-repo Actions-runs generalization — separate bullets on #4, don't pull them in here.
- Anything under `mathias/infra`, including the `GITEA_TOKEN` ExternalSecret. That needs a real PAT and a human; do not attempt it, and do not work around its absence with a hardcoded token.
## Notes
Read-only only. The token is Mathias's own PAT, so Gitea's own visibility rules already scope what can be seen — do not add any filtering that assumes otherwise.
Dispatched 2026-08-18 12:47 and again at 12:58 (status:redispatch). Both runs failed in ~3 seconds, before turn 1 — nothing to do with this issue's scope.
Root cause is dispatch-wide and now tracked in dispatch#33: the Anthropic account has no credit (Credit balance is too low from the CLI in the dispatch image, while the API key itself is valid — /v1/models returns 200). Every dispatched run since 2026-07-03 has failed this way.
Labels cleared (status:dispatch, status:redispatch removed) so this issue is not stuck re-firing every minute. status:dispatched left on to record that it was attempted.
To pick this up once credit is topped up: apply status:redispatch — plain status:dispatch will not re-trigger, because the bot comments above already mark it handled.
The scope in the issue body is unchanged and still accurate.
Dispatched 2026-08-18 12:47 and again at 12:58 (`status:redispatch`). Both runs failed in ~3 seconds, before turn 1 — nothing to do with this issue's scope.
Root cause is dispatch-wide and now tracked in dispatch#33: the Anthropic account has no credit (`Credit balance is too low` from the CLI in the dispatch image, while the API key itself is valid — `/v1/models` returns 200). Every dispatched run since 2026-07-03 has failed this way.
Labels cleared (`status:dispatch`, `status:redispatch` removed) so this issue is not stuck re-firing every minute. `status:dispatched` left on to record that it was attempted.
To pick this up once credit is topped up: apply `status:redispatch` — plain `status:dispatch` will not re-trigger, because the bot comments above already mark it handled.
The scope in the issue body is unchanged and still accurate.
Shipped in 097d874, released as v0.8.0. CI green on main (run #71) and on the tag (run #72).
Acceptance criteria
RepoNodes() is a pure function over an already-decoded API response, unit-tested, no network in the test.internal/atlas/repos.go, tests in repos_test.go — five cases, matching issues_test.go's style.
Each node carries the repo name, a short description or tag, and a clickable html_url. Title = repo name, Desc = description, Tags = ["live · Gitea", <language>], URL = html_url.
MyRepos() errors when GITEA_TOKEN is unset, and the overlay skips gracefully. Both halves tested: client_test.go covers the error, and TestPrependGenerated_NoLiveNodesLeavesAuthoredIntact covers the authored nodes surviving an empty overlay. Verified live: with no token the atlas returns 200 with stage 05's authored nodes untouched.
Archived repos are excluded.TestRepoNodes_ExcludesArchived.
task check green — golangci-lint 0 issues, vet, -race.
Out-of-scope items respected: no PRs panel, no per-repo Actions generalization, nothing touched under mathias/infra, no hardcoded token.
One design decision that wasn't in the ticket
The issue said "onto the appropriate stage". Stage 05 is the right home — repos are what the execution stage acts on — but #11 landed first and claimed stage 05's generate key for stuck-badges, and a stage could only name one source.
Rather than displace #11 or park repos on a semantically wrong free stage (00/01/02 were the only ones left), generate now accepts either spelling:
Bare strings stay valid and marshal back as bare strings, so no authored stage was rewritten and the frontend contract is unchanged. Stages 04 and 08 already shared stuck-badges between them — this is the same growth one level in. Verified live: stage 05 now serves both overlays at once (stuck badges → 8 live repos → 8 authored nodes).
Incidental fix
The overlay merge moved out of handler.go into atlas.PrependGenerated, which is unit-testable where the inline loop was not — it sat behind a network call. That fixed a latent aliasing bug in the issues path: the old append(liveNodes, authored...) wrote into the live slice's backing array, so two stages sharing a generate key would corrupt each other. Not hypothetical any more, now that stuck-badges spans three stages.
Not fixed here
/user/repos?limit=8 returns the alphabetically first 8 repos (agentsquad … brain-weekly-ingest), which is arbitrary for a live view. Sorting by recent update would be more useful — one query param, but it's a behaviour change the ticket didn't ask for, so left alone.
Filed #13: CI installs templ@latest while go.mod pins v0.3.1020.
GITEA_TOKEN is still unprovisioned in prod, so this stays inert there until #4's ExternalSecret lands.
Shipped in `097d874`, released as **v0.8.0**. CI green on `main` ([run #71](https://git.d-ma.be/mathias/cad-atlas/actions/runs/71)) and on the tag ([run #72](https://git.d-ma.be/mathias/cad-atlas/actions/runs/72)).
## Acceptance criteria
- [x] **`RepoNodes()` is a pure function over an already-decoded API response, unit-tested, no network in the test.** `internal/atlas/repos.go`, tests in `repos_test.go` — five cases, matching `issues_test.go`'s style.
- [x] **Each node carries the repo name, a short description or tag, and a clickable `html_url`.** Title = repo name, `Desc` = description, `Tags` = `["live · Gitea", <language>]`, `URL` = `html_url`.
- [x] **`MyRepos()` errors when `GITEA_TOKEN` is unset, and the overlay skips gracefully.** Both halves tested: `client_test.go` covers the error, and `TestPrependGenerated_NoLiveNodesLeavesAuthoredIntact` covers the authored nodes surviving an empty overlay. Verified live: with no token the atlas returns 200 with stage 05's authored nodes untouched.
- [x] **Archived repos are excluded.** `TestRepoNodes_ExcludesArchived`.
- [x] **`task check` green** — golangci-lint 0 issues, vet, `-race`.
Out-of-scope items respected: no PRs panel, no per-repo Actions generalization, nothing touched under `mathias/infra`, no hardcoded token.
## One design decision that wasn't in the ticket
The issue said "onto the appropriate stage". Stage 05 is the right home — repos are what the execution stage acts on — but #11 landed first and claimed stage 05's `generate` key for `stuck-badges`, and a stage could only name one source.
Rather than displace #11 or park repos on a semantically wrong free stage (00/01/02 were the only ones left), `generate` now accepts either spelling:
```json
"generate": "ci-jobs"
"generate": ["stuck-badges", "gitea-repos"]
```
Bare strings stay valid and marshal back as bare strings, so no authored stage was rewritten and the frontend contract is unchanged. Stages 04 and 08 already shared `stuck-badges` between them — this is the same growth one level in. Verified live: stage 05 now serves both overlays at once (stuck badges → 8 live repos → 8 authored nodes).
## Incidental fix
The overlay merge moved out of `handler.go` into `atlas.PrependGenerated`, which is unit-testable where the inline loop was not — it sat behind a network call. That fixed a latent aliasing bug in the issues path: the old `append(liveNodes, authored...)` wrote into the live slice's backing array, so two stages sharing a `generate` key would corrupt each other. Not hypothetical any more, now that `stuck-badges` spans three stages.
## Not fixed here
- `/user/repos?limit=8` returns the **alphabetically** first 8 repos (`agentsquad` … `brain-weekly-ingest`), which is arbitrary for a live view. Sorting by recent update would be more useful — one query param, but it's a behaviour change the ticket didn't ask for, so left alone.
- Filed #13: CI installs `templ@latest` while `go.mod` pins `v0.3.1020`.
- `GITEA_TOKEN` is still unprovisioned in prod, so this stays inert there until #4's ExternalSecret lands.
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Split out of #4's "Not yet shipped" list so it can be worked as one self-contained change. #4 stays open as the umbrella.
Goal
Show the user's own Gitea repos in the atlas, the same way open issues already appear on stage 03.
The pattern to mirror — already in this repo
The issues slice (
b2761a4) is the template. Follow it structurally rather than inventing a new approach:internal/atlas/issues.go→IssueNodes()RepoNodes()— Gitea repo list → stage nodesinternal/gitea/client.go→MyIssues()MyRepos()— authenticated GET,GITEA_TOKENinternal/web/handler.go→liveOverlay()generate:"gitea-issues"inatlas.jsongenerate:key for reposAcceptance criteria
RepoNodes()is a pure function over an already-decoded API response, unit-tested table-driven — no network in the test. Matchissues.go's test style.html_url(the frontend already renders any node with aurlas<a class="node">).MyRepos()returns an error whenGITEA_TOKENis unset, and the overlay skips gracefully — the atlas must still render with its authored placeholder nodes, exactly like the issues path does today. This is load-bearing:GITEA_TOKENis still not provisioned in prod (#4), so the unset path is the one that runs live right now.task checkgreen (lint, vet, race tests).Out of scope
mathias/infra, including theGITEA_TOKENExternalSecret. That needs a real PAT and a human; do not attempt it, and do not work around its absence with a hardcoded token.Notes
Read-only only. The token is Mathias's own PAT, so Gitea's own visibility rules already scope what can be seen — do not add any filtering that assumes otherwise.
🤖 dispatch: routing: executor=claude (default — no executor: label) — starting headless run
🤖 dispatch: executor claude failed: runner error: agent run ended in error (see stderr)
🤖 dispatch: routing: executor=claude (default — no executor: label) — starting headless run
🤖 dispatch: executor claude failed: runner error: agent run ended in error (see stderr)
🤖 dispatch: routing: executor=claude (default — no executor: label) — starting headless run
🤖 dispatch: executor claude failed: runner error: agent run ended in error (see stderr)
🤖 dispatch: routing: executor=claude (default — no executor: label) — starting headless run
🤖 dispatch: executor claude failed: runner error: agent run ended in error (see stderr)
Dispatched 2026-08-18 12:47 and again at 12:58 (
status:redispatch). Both runs failed in ~3 seconds, before turn 1 — nothing to do with this issue's scope.Root cause is dispatch-wide and now tracked in dispatch#33: the Anthropic account has no credit (
Credit balance is too lowfrom the CLI in the dispatch image, while the API key itself is valid —/v1/modelsreturns 200). Every dispatched run since 2026-07-03 has failed this way.Labels cleared (
status:dispatch,status:redispatchremoved) so this issue is not stuck re-firing every minute.status:dispatchedleft on to record that it was attempted.To pick this up once credit is topped up: apply
status:redispatch— plainstatus:dispatchwill not re-trigger, because the bot comments above already mark it handled.The scope in the issue body is unchanged and still accurate.
Shipped in
097d874, released as v0.8.0. CI green onmain(run #71) and on the tag (run #72).Acceptance criteria
RepoNodes()is a pure function over an already-decoded API response, unit-tested, no network in the test.internal/atlas/repos.go, tests inrepos_test.go— five cases, matchingissues_test.go's style.html_url. Title = repo name,Desc= description,Tags=["live · Gitea", <language>],URL=html_url.MyRepos()errors whenGITEA_TOKENis unset, and the overlay skips gracefully. Both halves tested:client_test.gocovers the error, andTestPrependGenerated_NoLiveNodesLeavesAuthoredIntactcovers the authored nodes surviving an empty overlay. Verified live: with no token the atlas returns 200 with stage 05's authored nodes untouched.TestRepoNodes_ExcludesArchived.task checkgreen — golangci-lint 0 issues, vet,-race.Out-of-scope items respected: no PRs panel, no per-repo Actions generalization, nothing touched under
mathias/infra, no hardcoded token.One design decision that wasn't in the ticket
The issue said "onto the appropriate stage". Stage 05 is the right home — repos are what the execution stage acts on — but #11 landed first and claimed stage 05's
generatekey forstuck-badges, and a stage could only name one source.Rather than displace #11 or park repos on a semantically wrong free stage (00/01/02 were the only ones left),
generatenow accepts either spelling:Bare strings stay valid and marshal back as bare strings, so no authored stage was rewritten and the frontend contract is unchanged. Stages 04 and 08 already shared
stuck-badgesbetween them — this is the same growth one level in. Verified live: stage 05 now serves both overlays at once (stuck badges → 8 live repos → 8 authored nodes).Incidental fix
The overlay merge moved out of
handler.gointoatlas.PrependGenerated, which is unit-testable where the inline loop was not — it sat behind a network call. That fixed a latent aliasing bug in the issues path: the oldappend(liveNodes, authored...)wrote into the live slice's backing array, so two stages sharing ageneratekey would corrupt each other. Not hypothetical any more, now thatstuck-badgesspans three stages.Not fixed here
/user/repos?limit=8returns the alphabetically first 8 repos (agentsquad…brain-weekly-ingest), which is arbitrary for a live view. Sorting by recent update would be more useful — one query param, but it's a behaviour change the ticket didn't ask for, so left alone.templ@latestwhilego.modpinsv0.3.1020.GITEA_TOKENis still unprovisioned in prod, so this stays inert there until #4's ExternalSecret lands.