(plan) convert original implement plan to mock api

This commit is contained in:
Story Crater Bot
2026-08-19 09:47:14 -07:00
parent c42040319f
commit 0aef2281bc
65 changed files with 307 additions and 627 deletions
-15
View File
@@ -1,15 +0,0 @@
# 0.1 — Resolve GitOps repo ownership
Phase: 0 — Unblock
Depends on: [00-decisions.md](00-decisions.md) decision #1
- [ ] Resolve which GitOps repo owns the portfolio; delete or correct the losing manifest
Cluster evidence (2026-08-18, `kubectl get applications -n argocd`): `portfolio` and `auth-infra` (from this repo's `infra/argocd-apps.yaml`) do not exist in-cluster. `homarr` — also defined in this repo's manifest — does exist, but its live source is 100% `homelab-root` (`github.com/Riotpiaole/riotpiao.homelab.com`, `project: homelab`), not this repo. Points toward homelab-root as authoritative, but decision is still open — needs explicit sign-off, not inferred.
## Verify
```bash
kubectl get application portfolio -n argocd # shows one, correct, source
```
-15
View File
@@ -1,15 +0,0 @@
# 0.2 — Fix NXDOMAIN repoURL/image reference
Phase: 0 — Unblock
Depends on: [0.1](0.1-gitops-repo.md)
- [ ] Point `infra/argocd-apps.yaml` `repoURL` and image reference at real hostnames (fix NXDOMAIN)
Root cause confirmed (2026-08-18): `*.riotpiao.homelab.com` is fully retired, replaced by `*.riotpiao.com`. `infra/argocd-apps.yaml` still points at `forgejo.riotpiao.homelab.com` (dead domain); `forgejo.riotpiao.com` is live and returns 200. Fix is a straight domain swap in `infra/argocd-apps.yaml` (`portfolio` and `auth-infra` Application `spec.source.repoURL`) — not open diagnosis anymore. `portfolio` namespace still does not exist in-cluster until this + [0.1](0.1-gitops-repo.md) land.
## Verify
```bash
kubectl get pods -n portfolio # 2/2 Running
```
-11
View File
@@ -1,11 +0,0 @@
# 0.3 — Replace :latest with commit-SHA tag
Phase: 0 — Unblock
- [ ] Replace `:latest` tag with commit-SHA tag in `infra/portfolio/base/deployment.yaml`; keep `imagePullPolicy: IfNotPresent` (correct once tags are immutable)
## Verify
```bash
# new commit -> new tag -> Argo rolls out automatically, no manual `kubectl set image`
```
-19
View File
@@ -1,19 +0,0 @@
# 0.4 — Diagnose forgejo-gitea stuck Init:0/3
Phase: 0 — Unblock
- [x] Diagnose `forgejo-gitea` stuck `Init:0/3` (3h+) — read init container logs before changing anything
Resolved — `forgejo-gitea` pod Running 1/1 (confirmed via `kubectl get pods -n cicd`, 2026-08-18).
## Command
```bash
kubectl logs -n cicd <forgejo-gitea-pod> -c <init-container-name>
```
## Verify
```bash
kubectl get pods -n cicd # forgejo-gitea 1/1 Running
```
-15
View File
@@ -1,15 +0,0 @@
# 0.5 — Diagnose apex 403
Phase: 0 — Unblock
Depends on: [00-decisions.md](00-decisions.md) decision #2
- [ ] Diagnose apex 403 — check Cloudflare tunnel Public Hostnames list and WAF event log
Partial evidence (2026-08-18): `kubectl get ingress -A` has zero rules for bare host `riotpiao.com` — every ingress is a subdomain (22 hosts, all `*.riotpiao.com` per the `*.riotpiao.homelab.com` retirement). No in-cluster origin exists for the apex today. Leans toward "no origin configured" as the cause, but Cloudflare tunnel Public Hostnames list + WAF event log still needed to confirm the tunnel-side config (not visible from `kubectl`).
## Verify
```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://riotpiao.com # 200
```
-10
View File
@@ -1,10 +0,0 @@
# 0.7 — Triage unrelated cluster issues
Phase: 0 — Unblock
- [ ] Triage unrelated cluster issues: `sms` Application Degraded (`macos-bluebubbles` Pending 3h), `longhorn-config` OutOfSync
- Unrelated to atlas, but delivery tree (Phase 3) will render both red on day one — fix or explicitly accept as known-red
Status (2026-08-18, `kubectl get application <name> -n argocd`):
- `longhorn-config` — Synced / Healthy. Resolved.
- `sms` — Synced / Degraded. Still open.
-45
View File
@@ -1,45 +0,0 @@
# Blocking Decisions
All five must resolve before Phase 1 starts. Source: ADR-0001 review notes.
## 1. Which GitOps repo owns the portfolio?
Two roots exist:
- `homelab-root``[email protected]:Riotpiaole/riotpiao.homelab.com.git`, path `k8s/argocd/apps` (31 child Applications, this is the live one)
- This repo's `infra/argocd-apps.yaml``forgejo.riotpiao.homelab.com` (`portfolio`/`auth-infra` Applications don't exist in cluster)
**Update (2026-08-18):** `*.riotpiao.homelab.com` is fully retired — confirmed NXDOMAIN across the board (`argocd.riotpiao.homelab.com`, `forgejo.riotpiao.homelab.com`, etc.), not just the forgejo subdomain. Live ingress hosts are all `*.riotpiao.com` now (`forgejo.riotpiao.com` → 200, `argocd.riotpiao.com`, `vault.riotpiao.com`, etc. — 22 hosts total). `infra/argocd-apps.yaml`'s `repoURL: https://forgejo.riotpiao.homelab.com/...` in this repo is pointed at the dead domain; the working equivalent is `forgejo.riotpiao.com`. This doesn't answer which repo is authoritative, but it means 0.2's NXDOMAIN fix is now a known, mechanical domain swap rather than an open diagnosis.
**Decision needed:** GitHub or Forgejo. Blocks Phase 0.10.3.
## 2. Apex 403 cause
`riotpiao.com` resolves via Cloudflare (172.67.196.33 / 104.21.60.115) but returns HTTP 403 at the edge, no origin headers.
**Update (2026-08-18):** `kubectl get ingress -A` shows zero ingress objects with host exactly `riotpiao.com` (bare apex) — every rule is a subdomain (`argocd.`, `forgejo.`, `grafana.`, ...). No in-cluster origin is configured for the apex at all, which is consistent with (though not proof of) "no origin configured" as the 403 cause. Still need the Cloudflare tunnel Public Hostnames list / WAF event log to confirm whether the tunnel even has an apex entry, since that's Cloudflare-side config not visible from `kubectl`.
**Decision needed:** tunnel route missing, WAF rule, or no origin configured at all. Check Cloudflare tunnel Public Hostnames list + WAF event log (event log names the blocking rule). Blocks Phase 0.5.
## 3. Is homarr still wanted?
Deployed and healthy, but shipped under the abandoned Homarr+Terraform plan.
**Decision needed:** keep, or decommission via GitOps (remove Application, let Argo prune).
## 4. Does chat (Surface D) stay in v1?
8-slot GPU ceiling (`--max-num-seqs=4` × 2 replicas) means chat queues under real traffic — a visible "please wait" is a worse first impression than no chat.
**Decision needed:** ship B+E+C first and treat D as separate follow-up, or commit to D in v1 with the queue UX as-is.
## 5. Where does atlas live?
This repo (`riotpiao`) or the homelab repo. Follows from decision 1 — whichever repo is authoritative for the portfolio's GitOps should also own atlas's manifests.
---
Once answered, update this file with the decisions taken (date + rationale) before starting Phase 0 tasks in [INDEX.md](INDEX.md).
## Hard requirement: Playwright
Every phase shipping a UI surface (Phase 2, 3, 4, 5) requires a passing Playwright e2e spec before merge — not optional, gated in CI alongside `pnpm test`. Setup lands in Phase 0 task 0.6a.
-12
View File
@@ -1,12 +0,0 @@
# 1.1 — redact_test.go (RED)
Phase: 1 — Atlas Core
Stage: RED
- [ ] Golden test: serialized snapshot contains none of the denied fields, run against a fixture captured from the real cluster
## Verify
```bash
go test ./... -run TestRedact -v
```
-6
View File
@@ -1,6 +0,0 @@
# 1.10 — Container hardening (GREEN)
Phase: 1 — Atlas Core
Stage: GREEN
- [ ] Container hardening: `runAsNonRoot`, read-only root filesystem, all capabilities dropped, `seccompProfile: RuntimeDefault`
-8
View File
@@ -1,8 +0,0 @@
# 1.11 — simplify skill pass (REFACTOR)
Phase: 1 — Atlas Core
Stage: REFACTOR
Depends on: [1.4](1.4-clusterrole.md), [1.5](1.5-informers.md), [1.6](1.6-reducer.md), [1.7](1.7-dto-allowlist.md), [1.8](1.8-redis-publish.md)
- [ ] Run `simplify` skill pass on reducer/DTO code
-16
View File
@@ -1,16 +0,0 @@
# 1.12 — Go error-handling / context hygiene (REFACTOR)
Phase: 1 — Atlas Core
Stage: REFACTOR
Depends on: [1.11](1.11-simplify-pass.md)
- [ ] Confirm no `_ =` on errors, no naked returns, every upstream call carries a `context.Context` (go-error-handling, go-context skills)
## Verify
```bash
go test ./... -run TestRedact -v
go test ./... -run TestRBAC -v
go test ./... -run TestSnapshot -v
```
-13
View File
@@ -1,13 +0,0 @@
# 1.2 — rbac_test.go (RED)
Phase: 1 — Atlas Core
Stage: RED
- [ ] atlas ServiceAccount receives 403 on `get secrets` in every namespace
## Verify
```bash
kubectl auth can-i get secrets --as=system:serviceaccount:portfolio:atlas # no
go test ./... -run TestRBAC -v
```
-12
View File
@@ -1,12 +0,0 @@
# 1.3 — snapshot_test.go (RED)
Phase: 1 — Atlas Core
Stage: RED
- [ ] An informer event produces the expected delta
## Verify
```bash
go test ./... -run TestSnapshot -v
```
-14
View File
@@ -1,14 +0,0 @@
# 1.4 — ClusterRole (GREEN)
Phase: 1 — Atlas Core
Stage: GREEN
Depends on: [1.2](1.2-rbac-test.md)
- [ ] ClusterRole: verbs `get,list,watch` only, explicit resource list — no `secrets`, no `*`, no wildcard apiGroups
## Verify
```bash
kubectl auth can-i get secrets --as=system:serviceaccount:portfolio:atlas # no
```
-14
View File
@@ -1,14 +0,0 @@
# 1.5 — client-go informers (GREEN)
Phase: 1 — Atlas Core
Stage: GREEN
Depends on: [1.3](1.3-snapshot-test.md)
- [ ] client-go informers: kube API (nodes, namespaces, workloads), Argo CD `Application` CRs
## Verify
```bash
go test ./... -run TestSnapshot -v
```
-14
View File
@@ -1,14 +0,0 @@
# 1.6 — Reducer: informer events -> snapshot (GREEN)
Phase: 1 — Atlas Core
Stage: GREEN
Depends on: [1.5](1.5-informers.md)
- [ ] Reducer: informer events -> in-memory snapshot, redacted **at write time**
## Verify
```bash
go test ./... -run TestSnapshot -v
```
-16
View File
@@ -1,16 +0,0 @@
# 1.7 — DTO construction, allowlist only (GREEN)
Phase: 1 — Atlas Core
Stage: GREEN
Depends on: [1.1](1.1-redact-test.md), [1.6](1.6-reducer.md)
- [ ] DTO construction — allowlist only.
- Emitted: name, namespace, kind, phase, ready counts, restart count, age, node name, health status, sync status, sync wave, explicit label subset
- Never emitted: container env/args, image digests/tags, `spec.source.repoURL`, `spec.source.path`, annotations, pod IPs, cluster IPs, Secret names, `status.conditions[].message`, node internal IPs
## Verify
```bash
go test ./... -run TestRedact -v
```
-8
View File
@@ -1,8 +0,0 @@
# 1.8 — Redis publish (GREEN)
Phase: 1 — Atlas Core
Stage: GREEN
Depends on: [1.6](1.6-reducer.md)
- [ ] Redis publish (snapshot deltas -> `kmsvc-redis-master.sqs:6379`)
-6
View File
@@ -1,6 +0,0 @@
# 1.9 — NetworkPolicy on atlas (GREEN)
Phase: 1 — Atlas Core
Stage: GREEN
- [ ] NetworkPolicy on atlas: egress restricted to kube API, `prometheus-operated.monitoring`, `reasoning-predictor.llm-serving`, `kmsvc-redis-master.sqs`; ingress from `ingress-nginx` only
-12
View File
@@ -1,12 +0,0 @@
# 2.1 — stream_test.go (RED)
Phase: 2 — Topology (Surface B)
Stage: RED
- [ ] SSE emits a delta within 5s of a pod state change
## Verify
```bash
go test ./... -run TestStream -v
```
+5 -3
View File
@@ -1,8 +1,10 @@
# 2.4 — GET /api/topology (GREEN)
# 2.4 — GET /api/topology (mock, GREEN)
Phase: 2 — Topology (Surface B)
Stage: GREEN
Depends on: [1.7](1.7-dto-allowlist.md)
- [x] `GET /api/topology` — nodes, namespaces, models; envelope `{"data": {}, "meta": {"snapshotAge", "generation", "truncated"}}`
- [ ] `GET /api/topology` — nodes, namespaces, workload summaries; envelope `{"data": {}, "meta": {"snapshotAge", "generation"}}`; capped 256 KB, `meta.truncated: true` on overflow, never a silent drop
Done — implemented in [app/api/topology/route.ts](../app/api/topology/route.ts), serves fixture data from [lib/clusterMock.ts](../lib/clusterMock.ts). Mock only: no real cluster, no 256 KB cap logic (fixture is small), `truncated` always `false`. See [REQUIREMENTS.md](REQUIREMENTS.md) for the real endpoint's contract.
Remaining: wire [app/cluster/page.tsx](../app/cluster/page.tsx) to fetch this instead of importing `clusterMock` directly.
+4 -4
View File
@@ -1,14 +1,14 @@
# 2.5 — GET /api/stream (GREEN)
# 2.5 — GET /api/stream (mock, GREEN)
Phase: 2 — Topology (Surface B)
Stage: GREEN
Depends on: [2.1](2.1-stream-test.md), [1.8](1.8-redis-publish.md)
- [x] `GET /api/stream` — SSE, `topology` event type, keepalive comment frame every 30s
- [ ] `GET /api/stream` — SSE, session cookie, 2 concurrent/IP, `topology` event type; keepalive comment frame every 30s; `Last-Event-ID` supported for resumable deltas
Done — implemented in [app/api/stream/route.ts](../app/api/stream/route.ts). Emits a synthetic random-namespace delta every 5s plus a keepalive comment every 30s. Mock only: no session cookie, no 2-concurrent/IP cap, no `Last-Event-ID` resume, no real informer. See [REQUIREMENTS.md](REQUIREMENTS.md).
## Verify
```bash
go test ./... -run TestStream -v
curl -N http://localhost:3000/api/stream
```
+3 -2
View File
@@ -6,10 +6,11 @@ Stage: GREEN
Depends on: [2.2](2.2-topology-component-test.md), [2.4](2.4-api-topology.md), [2.5](2.5-api-stream.md)
- [ ] Frontend: React Flow, force layout, node -> namespace -> workload
- [ ] Fetch from [/api/topology](../app/api/topology/route.ts) and [/api/stream](../app/api/stream/route.ts) (mock) instead of importing `clusterMock` directly
## Verify
```bash
# delete a pod, observe graph update in <5s without reloading
kubectl delete pod <name> -n <ns>
# trigger a mock delta via /api/stream, observe graph update in <5s without reloading
curl -N http://localhost:3000/api/stream
```
-6
View File
@@ -1,6 +0,0 @@
# 2.8 — Rate limiting Tier 1 + Tier 2 (GREEN)
Phase: 2 — Topology (Surface B)
Stage: GREEN
- [ ] Rate limiting Tier 1 (Cloudflare edge — WAF, Bot Fight Mode, per-IP rules) + Tier 2 (Kong `rate-limiting`, `policy: redis`, generous profile for topology)
+3 -3
View File
@@ -5,11 +5,11 @@ Stage: REFACTOR
Depends on: [2.5](2.5-api-stream.md), [2.6](2.6-frontend-react-flow.md)
- [ ] `simplify` pass on SSE handler + React Flow wiring
- [ ] `simplify` pass on mock SSE route handler + React Flow wiring
## Verify
```bash
kubectl delete pod <name> -n <ns> # graph updates in <5s without reload
pnpm test:e2e topology.e2e.ts # required, must pass before merge
curl -N http://localhost:3000/api/stream # delta every 5s, keepalive every 30s
pnpm test:e2e topology.e2e.ts # required, must pass before merge
```
-12
View File
@@ -1,12 +0,0 @@
# 3.1 — delivery_test.go (RED)
Phase: 3 — Delivery Tree (Surface E)
Stage: RED
- [ ] Apps group correctly by `sync-wave`; Secret names absent from output (kind+count only); `repoURL` absent from output
## Verify
```bash
go test ./... -run TestDelivery -v
```
+5 -3
View File
@@ -1,8 +1,10 @@
# 3.4 — GET /api/delivery (GREEN)
# 3.4 — GET /api/delivery (mock, GREEN)
Phase: 3 — Delivery Tree (Surface E)
Stage: GREEN
Depends on: [3.1](3.1-delivery-test.md)
- [x] `GET /api/delivery` — apps, wave-grouped (0->8)
- [ ] `GET /api/delivery` — Argo apps, wave-grouped (0->8), resource children lazy; capped 256 KB
Done — implemented in [app/api/delivery/route.ts](../app/api/delivery/route.ts), serves fixture data from [lib/clusterMock.ts](../lib/clusterMock.ts). Mock only: no real Argo watch, no 256 KB cap logic. See [REQUIREMENTS.md](REQUIREMENTS.md).
Remaining: wire [app/cluster/delivery/page.tsx](../app/cluster/delivery/page.tsx) to fetch this instead of importing `clusterMock` directly.
+3 -3
View File
@@ -1,8 +1,8 @@
# 3.5 — GET /api/delivery/{app}/resources (GREEN)
# 3.5 — GET /api/delivery/{app}/resources (mock, GREEN)
Phase: 3 — Delivery Tree (Surface E)
Stage: GREEN
Depends on: [3.4](3.4-api-delivery.md)
- [x] `GET /api/delivery/{app}/resources` — resource kinds + counts per app
- [ ] `GET /api/delivery/{app}/resources` cursor-paginated at 100 items (prometheus alone has 68 resources today)
Done — implemented in [app/api/delivery/[app]/resources/route.ts](<../app/api/delivery/[app]/resources/route.ts>), serves fixture data from `resourceKinds`/`DEFAULT_KINDS` in [lib/clusterMock.ts](../lib/clusterMock.ts). Mock only: no cursor pagination (fixture never exceeds 100 items), `hasMore` always `false`. See [REQUIREMENTS.md](REQUIREMENTS.md).
+1
View File
@@ -6,3 +6,4 @@ Stage: GREEN
Depends on: [3.2](3.2-delivery-component-test.md), [3.4](3.4-api-delivery.md), [3.5](3.5-api-delivery-resources.md)
- [ ] Frontend: React Flow, wave columns left->right from `sync-wave` annotations. Click app -> side panel with `react-arborist` virtualized resource tree, lazy-loaded children
- [ ] Fetch from [/api/delivery](../app/api/delivery/route.ts) and [/api/delivery/{app}/resources](<../app/api/delivery/[app]/resources/route.ts>) (mock) instead of importing `clusterMock` directly
+5 -4
View File
@@ -1,15 +1,16 @@
# 3.7 — Live sync animation (GREEN)
# 3.7 — Live sync animation (mock, GREEN)
Phase: 3 — Delivery Tree (Surface E)
Stage: GREEN
Depends on: [2.5](2.5-api-stream.md), [3.6](3.6-frontend-wave-columns.md)
- [ ] Live sync animation `OutOfSync -> Syncing -> Synced` driven by Application watch (reuse Phase 2 SSE `/api/stream`, add `delivery` event type)
- [ ] Live sync animation `OutOfSync -> Syncing -> Synced` driven by [/api/stream](../app/api/stream/route.ts) `delivery` event type (add this event type alongside the existing `topology` one)
Mock only: `/api/stream` emits synthetic deltas, not a real Argo Application watch. See [REQUIREMENTS.md](REQUIREMENTS.md).
## Verify
```bash
# trigger an Argo sync, observe wave-ordered animation
argocd app sync homelab-root
curl -N http://localhost:3000/api/stream # observe delivery events once event type is added
```
-14
View File
@@ -1,14 +0,0 @@
# 3.8 — Redaction check specific to delivery surface (GREEN)
Phase: 3 — Delivery Tree (Surface E)
Stage: GREEN
Depends on: [3.4](3.4-api-delivery.md)
- [ ] 21 `Secret` resources appear in Argo trees today — render kind+count only, never names (includes `sops-secrets`); `status.conditions[].message` echoes raw errors with internal hostnames — emit condition **type** only
## Verify
```bash
go test ./... -run TestDelivery -v
```
-1
View File
@@ -10,6 +10,5 @@ Depends on: [3.6](3.6-frontend-wave-columns.md), [3.7](3.7-live-sync-animation.m
## Verify
```bash
argocd app sync homelab-root # wave-ordered animation
pnpm test:e2e delivery.e2e.ts # required, must pass before merge
```
-12
View File
@@ -1,12 +0,0 @@
# 4.1 — exec_parse_test.go (RED)
Phase: 4 — Terminal (Surface C)
Stage: RED
- [ ] Fuzz corpus: every non-allowlisted input rejects and performs zero upstream calls
## Verify
```bash
go test ./... -run TestExecParse -v
```
+3 -3
View File
@@ -3,13 +3,13 @@
Phase: 4 — Terminal (Surface C)
Stage: REFACTOR
Depends on: [4.5](4.5-command-set.md), [4.6](4.6-namespace-validation.md)
Depends on: [4.4](4.4-api-exec.md), [4.6](4.6-namespace-validation.md)
- [ ] `simplify` pass on the enum parser
- [ ] `simplify` pass on the enum parser in [app/api/exec/route.ts](../app/api/exec/route.ts)
## Verify
```bash
curl -X POST https://riotpiao.com/api/exec -d '{"cmd":"get pods; rm -rf /"}'
curl -X POST http://localhost:3000/api/exec -d '{"cmd":"get pods; rm -rf /"}'
pnpm test:e2e terminal.e2e.ts # required, must pass before merge
```
-12
View File
@@ -1,12 +0,0 @@
# 4.2 — exec_test.go (RED)
Phase: 4 — Terminal (Surface C)
Stage: RED
- [ ] Unknown namespace rejects on snapshot membership, not regex
## Verify
```bash
go test ./... -run TestExec -v
```
+4 -4
View File
@@ -1,14 +1,14 @@
# 4.4 — POST /api/exec (GREEN)
# 4.4 — POST /api/exec (mock, GREEN)
Phase: 4 — Terminal (Surface C)
Stage: GREEN
Depends on: [4.1](4.1-exec-parse-test.md), [4.2](4.2-exec-test.md)
- [x] `POST /api/exec` — input parses to a closed command enum, anything unmatched rejected before any lookup
- [ ] `POST /api/exec` — session cookie, 20/min/session; input parses to a closed command enum, anything unmatched rejected before any lookup
Done — implemented in [app/api/exec/route.ts](../app/api/exec/route.ts). Command set, snapshot-membership validation, and rejection logging all present. Mock only: no session cookie, no 20/min/session rate limit. See [REQUIREMENTS.md](REQUIREMENTS.md).
## Verify
```bash
curl -X POST https://riotpiao.com/api/exec -d '{"cmd":"get pods; rm -rf /"}'
curl -X POST http://localhost:3000/api/exec -d '{"cmd":"get pods; rm -rf /"}'
```
+3 -9
View File
@@ -1,14 +1,8 @@
# 4.6 — Namespace/resource-name validation by snapshot membership (GREEN)
# 4.6 — Namespace/resource-name validation by fixture membership (GREEN)
Phase: 4 — Terminal (Surface C)
Stage: GREEN
Depends on: [4.2](4.2-exec-test.md), [1.7](1.7-dto-allowlist.md)
- [x] Namespace and resource-name arguments validated by **set membership against the mock fixture** (`lib/clusterMock.ts`), not regex/escaping
- [ ] Namespace and resource-name arguments validated by **set membership against current snapshot**, not regex/escaping
## Verify
```bash
go test ./... -run TestExec -v
```
Done — implemented in [app/api/exec/route.ts](../app/api/exec/route.ts) (`namespaces.find(...)`). Real backend must validate against a live snapshot, not a static fixture — see [REQUIREMENTS.md](REQUIREMENTS.md).
-6
View File
@@ -1,6 +0,0 @@
# 4.7 — No shell, no exec, no kubectl binary in image (GREEN)
Phase: 4 — Terminal (Surface C)
Stage: GREEN
- [ ] No shell, no `exec`, no `kubectl` binary in the container image
+3 -2
View File
@@ -1,8 +1,9 @@
# 4.8 — Frontend: wire InteractiveTerminal to /api/exec (GREEN)
# 4.8 — Frontend: wire terminal page to /api/exec (GREEN)
Phase: 4 — Terminal (Surface C)
Stage: GREEN
Depends on: [4.4](4.4-api-exec.md)
- [ ] Frontend: reuse [components/InteractiveTerminal.tsx](../components/InteractiveTerminal.tsx), wire to `/api/exec`
- [ ] [app/cluster/terminal/page.tsx](../app/cluster/terminal/page.tsx) currently has its own inline `run()` command parser — replace it with a `fetch('/api/exec', { method: 'POST', body: JSON.stringify({ cmd }) })` call so the terminal actually round-trips through the mock API instead of simulating client-side
- [ ] [components/InteractiveTerminal.tsx](../components/InteractiveTerminal.tsx) (the floating Cmd+K terminal) is a separate, older component with its own hardcoded `commands` map (`kubectl`, `terraform`, `argocd`, `kafka` — all backend/infra flavored) — decide whether to retire it, or reduce it to the same closed command set as the Surface C terminal and wire it to `/api/exec` too
+3 -10
View File
@@ -1,15 +1,8 @@
# 4.9 — Structured logging of rejections (GREEN)
# 4.9 — Log rejected /api/exec input (GREEN)
Phase: 4 — Terminal (Surface C)
Stage: GREEN
Depends on: [4.4](4.4-api-exec.md)
- [x] Every rejected `/api/exec` input logged (server console)
- [ ] Structured logging: every rejected `/api/exec` input logged
## Verify
```bash
# attempt injection payloads against /api/exec; all rejected, all logged
curl -X POST https://riotpiao.com/api/exec -d '{"cmd":"get pods; rm -rf /"}'
```
Done — `console.warn` on every reject branch in [app/api/exec/route.ts](../app/api/exec/route.ts). Mock only: plain console output, not the structured/shipped logging pipeline a real backend needs — see [REQUIREMENTS.md](REQUIREMENTS.md).
-14
View File
@@ -1,14 +0,0 @@
# 5.1 — ratelimit_test.go (RED)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: RED
Depends on: [00-decisions.md](00-decisions.md) decision #4
- [ ] 7th concurrent chat queues rather than reaching vLLM
## Verify
```bash
go test ./... -run TestRateLimit -v
```
-14
View File
@@ -1,14 +0,0 @@
# 5.10 — Prompt injection defenses (GREEN)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: GREEN
Depends on: [5.4](5.4-injection-test.md)
- [ ] System prompt is compile-time constant, unreachable by user input; cluster snapshot digest injected in a delimited block explicitly labelled untrusted data; user message always last; **no tool-calling** — model reads pre-built digest, cannot query anything; `max_tokens: 1500` cap
## Verify
```bash
go test ./... -run TestInjection -v
```
-14
View File
@@ -1,14 +0,0 @@
# 5.11 — Context budget (GREEN)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: GREEN
Depends on: [5.5](5.5-context-test.md)
- [ ] Context budget (16384 total): system prompt ~300, snapshot digest capped at 2000, `max_tokens` 1500, ~12500 for history, truncated oldest-first
## Verify
```bash
go test ./... -run TestContext -v
```
+3 -3
View File
@@ -1,8 +1,8 @@
# 5.12 — Chat SSE event types (GREEN)
# 5.12 — Chat SSE event types (mock, GREEN)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: GREEN
Depends on: [5.7](5.7-api-chat.md)
- [x] Chat SSE events: `{"type":"queue"|"reasoning"|"content"|"done"}` implemented; `"error"` not yet emitted (mock never errors)
- [ ] Chat SSE events: `{"type":"reasoning"|"content"|"queue"|"done"|"error"}` render `reasoning_content` in collapsible block (this is the demo)
Done — [app/api/chat/route.ts](../app/api/chat/route.ts). Frontend should render `reasoning` in a collapsible block (this is the demo).
+3 -2
View File
@@ -1,8 +1,9 @@
# 5.13 — Frontend: chat component (GREEN)
# 5.13 — Frontend: wire chat page to /api/chat (GREEN)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: GREEN
Depends on: [5.6](5.6-chat-e2e.md), [5.12](5.12-chat-sse-events.md)
- [ ] Frontend: new chat component — collapsible reasoning block, queue position, streaming tokens
- [ ] [app/cluster/chat/page.tsx](../app/cluster/chat/page.tsx) currently fakes the queue -> stream transition entirely client-side with `setTimeout`/`setInterval` against hardcoded `REPLY`/`REPLY_REASONING` strings — replace with an SSE read from `POST /api/chat` (mock), parsing `queue`/`reasoning`/`content`/`done` events into the existing `Turn` state shape
- [ ] Collapsible reasoning block, queue position, and streaming-token rendering already exist in the component — this task is the data-source swap, not new UI
-8
View File
@@ -1,8 +0,0 @@
# 5.14 — Prometheus metrics (GREEN)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: GREEN
Depends on: [5.8](5.8-rate-limiting-tier3.md)
- [ ] Prometheus metrics: `atlas_chat_concurrent`, `atlas_chat_queue_depth`, `atlas_ratelimit_rejections_total`, `atlas_snapshot_age_seconds`
-8
View File
@@ -1,8 +0,0 @@
# 5.15 — Alertmanager rule (GREEN)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: GREEN
Depends on: [5.14](5.14-prometheus-metrics.md)
- [ ] Alertmanager rule: chat queue saturated > 5 min, snapshot age > 60s
+4 -2
View File
@@ -1,6 +1,8 @@
# 5.16 — pnpm audit + govulncheck CI gate (GREEN)
# 5.16 — pnpm audit CI gate (GREEN)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: GREEN
- [ ] `pnpm audit` + `govulncheck` in CI, fail build on high severity
- [ ] `pnpm audit` in CI, fail build on high severity
`govulncheck` dropped from this task — no Go code in this repo.
+7 -11
View File
@@ -3,23 +3,19 @@
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: REFACTOR
Depends on: [5.8](5.8-rate-limiting-tier3.md), [5.13](5.13-frontend-chat-component.md)
Depends on: [5.7](5.7-api-chat.md), [5.13](5.13-frontend-chat-component.md)
- [ ] `simplify` pass on rate limiter + SSE chat handler
- [ ] `simplify` pass on the mock SSE chat route + frontend chat component
## Verify
```bash
# load test at 20 concurrent clients
# GPU sequence usage never exceeds 6, no upstream 5xx, queue drains
pnpm test:e2e chat.e2e.ts # required, must pass before merge
```
## Done — atlas v1 shipped
## Done — Next.js portfolio surfaces shipped (mock-backed)
Success criteria (from PLAN-atlas.md):
- [ ] Protected services redirect unauthenticated to Authentik (if still applicable post decision #3)
- [ ] Fabricated stats gone, real numbers or removed
- [ ] `riotpiao.com` is the only public hostname (I1 held)
- [ ] Golden redaction test passes against live-cluster fixture
- [ ] Load test: 20 concurrent clients, GPU usage <= 6, no 5xx, queue drains
- [ ] Fabricated stats gone, real numbers or removed ([3.10](3.10-landing-page-cleanup.md))
- [ ] All four cluster surfaces (topology, delivery, terminal, chat) fetch from `/api/*` mock routes, not static `clusterMock` imports
- [ ] `pnpm test` and `pnpm test:e2e` both pass
- [ ] See [REQUIREMENTS.md](REQUIREMENTS.md) for what a real backend needs to implement before these mocks can be swapped for the genuine atlas service
-12
View File
@@ -1,12 +0,0 @@
# 5.2 — disconnect_test.go (RED)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: RED
- [ ] Client abort cancels the upstream request
## Verify
```bash
go test ./... -run TestDisconnect -v
```
-12
View File
@@ -1,12 +0,0 @@
# 5.3 — budget_test.go (RED)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: RED
- [ ] 13th message in 24h returns `429` with `Retry-After`
## Verify
```bash
go test ./... -run TestBudget -v
```
-12
View File
@@ -1,12 +0,0 @@
# 5.4 — injection_test.go (RED)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: RED
- [ ] Snapshot content cannot alter system-prompt behaviour
## Verify
```bash
go test ./... -run TestInjection -v
```
-12
View File
@@ -1,12 +0,0 @@
# 5.5 — context_test.go (RED)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: RED
- [ ] History truncation keeps total tokens under 16384
## Verify
```bash
go test ./... -run TestContext -v
```
+3 -3
View File
@@ -1,8 +1,8 @@
# 5.7 — POST /api/chat (GREEN)
# 5.7 — POST /api/chat (mock, GREEN)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: GREEN
Depends on: [5.1](5.1-ratelimit-test.md), [5.2](5.2-disconnect-test.md), [5.3](5.3-budget-test.md)
- [x] `POST /api/chat` — SSE token stream
- [ ] `POST /api/chat` — session + Turnstile, 12/day/session, 6 global concurrent, SSE token stream
Done — implemented in [app/api/chat/route.ts](../app/api/chat/route.ts). Streams a canned reply through `queue` -> `reasoning` -> `content` -> `done` events. Mock only: no session, no Turnstile, no rate limiting, no real model. See [REQUIREMENTS.md](REQUIREMENTS.md).
-15
View File
@@ -1,15 +0,0 @@
# 5.8 — Rate limiting Tier 3 (atlas) (GREEN)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: GREEN
Depends on: [5.7](5.7-api-chat.md)
- [ ] Global chat semaphore = 6 (2 of 8 GPU slots kept as operator headroom); queue depth 20 then reject `429`; per-session budget 12 msg/24h; per-request timeout 120s hard server-side; disconnect cancels upstream immediately (`req.Context()` threaded to vLLM request)
## Verify
```bash
# load test at 20 concurrent clients
# GPU sequence usage never exceeds 6, no upstream 5xx, queue drains
```
+3 -3
View File
@@ -1,8 +1,8 @@
# 5.9 — Queue position streamed as SSE (GREEN)
# 5.9 — Queue position streamed as SSE (mock, GREEN)
Phase: 5 — Chat + Rate Limiter (Surface D)
Stage: GREEN
Depends on: [5.8](5.8-rate-limiting-tier3.md)
- [x] Queue position streamed as SSE `{"type":"queue","position":N}`
- [ ] Queue position streamed as SSE `{"type":"queue","position":N}`
Done — [app/api/chat/route.ts](../app/api/chat/route.ts) counts down a fixed position (3 -> 1) before replying. Mock only: not driven by a real semaphore/queue depth. See [REQUIREMENTS.md](REQUIREMENTS.md).
+31 -71
View File
@@ -1,74 +1,48 @@
# atlas — Task Board
# riotpiao — Task Board (Next.js portfolio, mock-backed)
This repo is scoped to the Next.js portfolio frontend only. Anything requiring `kubectl`, a real cluster, a Go
backend, or infra-side config (Cloudflare, GitOps, RBAC) has been moved to [REQUIREMENTS.md](REQUIREMENTS.md) —
a handoff spec for whoever builds the real `atlas` backend service. The four cluster-visualization surfaces
(topology, delivery, terminal, chat) run against mock `/api/*` Next.js route handlers backed by
[lib/clusterMock.ts](../lib/clusterMock.ts) fixture data.
Source of truth: [docs/PLAN-atlas.md](../docs/PLAN-atlas.md), [docs/adr/ADR-0001-atlas-cluster-visualization.md](../docs/adr/ADR-0001-atlas-cluster-visualization.md)
Blocking decisions (must answer before Phase 1): [00-decisions.md](00-decisions.md)
Rules carried from the ADR:
- I1: `riotpiao.com` is the only public hostname, ever
- I2: browser never talks to an internal API directly — atlas is the only origin
- I3: redaction is allowlist-only, enforced by DTO construction
- I4: no free-form string reaches an internal system (closed enum, snapshot-membership validation)
- I5: GPU chat concurrency capped at 6 of 8 sequence slots, disconnect cancels upstream immediately
Rules carried from the ADR (frontend-relevant subset):
- I2: browser never talks to an internal API directly — all cluster data flows through this app's own `/api/*` routes (mock today, real atlas later)
- I3: redaction is allowlist-only — the mock fixture already excludes denied fields, keep it that way when replacing with real data
- TDD: RED (tests named before code) -> GREEN (minimal code) -> REFACTOR, per phase
- Playwright e2e is a hard requirement for every UI-shipping phase (2, 3, 4, 5) — no surface merges without a passing spec
## 0 — Unblock
## 0 — Setup
| Task | Description |
|---|---|
| [0.1](0.1-gitops-repo.md) | Resolve GitOps repo ownership |
| [0.2](0.2-fix-nxdomain.md) | Fix NXDOMAIN repoURL/image reference |
| [0.3](0.3-immutable-tags.md) | Replace :latest with commit-SHA tag |
| [0.4](0.4-forgejo-init-stuck.md) | Diagnose forgejo-gitea stuck Init:0/3 — done |
| [0.5](0.5-apex-403.md) | Diagnose apex 403 |
| [0.6](0.6-vitest-setup.md) | Add Vitest + Testing Library + msw |
| [0.6a](0.6a-playwright-setup.md) | Add Playwright (hard requirement) |
| [0.7](0.7-triage-unrelated.md) | Triage unrelated cluster issues |
## 1 — Atlas Core
| Task | Description |
|---|---|
| [1.1](1.1-redact-test.md) | redact_test.go (RED) |
| [1.2](1.2-rbac-test.md) | rbac_test.go (RED) |
| [1.3](1.3-snapshot-test.md) | snapshot_test.go (RED) |
| [1.4](1.4-clusterrole.md) | ClusterRole (GREEN) |
| [1.5](1.5-informers.md) | client-go informers (GREEN) |
| [1.6](1.6-reducer.md) | Reducer: informer events -> snapshot (GREEN) |
| [1.7](1.7-dto-allowlist.md) | DTO construction, allowlist only (GREEN) |
| [1.8](1.8-redis-publish.md) | Redis publish (GREEN) |
| [1.9](1.9-networkpolicy.md) | NetworkPolicy on atlas (GREEN) |
| [1.10](1.10-container-hardening.md) | Container hardening (GREEN) |
| [1.11](1.11-simplify-pass.md) | simplify skill pass (REFACTOR) |
| [1.12](1.12-go-hygiene.md) | Go error-handling / context hygiene (REFACTOR) |
## 2 — Topology (Surface B)
| Task | Description |
|---|---|
| [2.1](2.1-stream-test.md) | stream_test.go (RED) |
| [2.2](2.2-topology-component-test.md) | topology.test.tsx (RED) |
| [2.3](2.3-topology-e2e.md) | topology.e2e.ts (RED, Playwright — hard requirement) |
| [2.4](2.4-api-topology.md) | GET /api/topology (GREEN) |
| [2.5](2.5-api-stream.md) | GET /api/stream (GREEN) |
| [2.4](2.4-api-topology.md) | GET /api/topology (mock, GREEN) — done |
| [2.5](2.5-api-stream.md) | GET /api/stream (mock, GREEN) — done |
| [2.6](2.6-frontend-react-flow.md) | Frontend: React Flow topology graph (GREEN) |
| [2.7](2.7-security-headers.md) | Security headers (GREEN) |
| [2.8](2.8-rate-limiting-tier1-2.md) | Rate limiting Tier 1 + Tier 2 (GREEN) |
| [2.9](2.9-simplify-pass.md) | simplify skill pass (REFACTOR) |
## 3 — Delivery Tree (Surface E)
| Task | Description |
|---|---|
| [3.1](3.1-delivery-test.md) | delivery_test.go (RED) |
| [3.2](3.2-delivery-component-test.md) | delivery.test.tsx (RED) |
| [3.3](3.3-delivery-e2e.md) | delivery.e2e.ts (RED, Playwright — hard requirement) |
| [3.4](3.4-api-delivery.md) | GET /api/delivery (GREEN) |
| [3.5](3.5-api-delivery-resources.md) | GET /api/delivery/{app}/resources (GREEN) |
| [3.4](3.4-api-delivery.md) | GET /api/delivery (mock, GREEN) — done |
| [3.5](3.5-api-delivery-resources.md) | GET /api/delivery/{app}/resources (mock, GREEN) — done |
| [3.6](3.6-frontend-wave-columns.md) | Frontend: wave columns + resource tree (GREEN) |
| [3.7](3.7-live-sync-animation.md) | Live sync animation (GREEN) |
| [3.8](3.8-redaction-check.md) | Redaction check specific to delivery surface (GREEN) |
| [3.7](3.7-live-sync-animation.md) | Live sync animation (mock, GREEN) |
| [3.9](3.9-simplify-pass.md) | simplify skill pass (REFACTOR) |
| [3.10](3.10-landing-page-cleanup.md) | Landing page: remove fabricated stats and dead links (GREEN) |
@@ -76,46 +50,32 @@ Rules carried from the ADR:
| Task | Description |
|---|---|
| [4.1](4.1-exec-parse-test.md) | exec_parse_test.go (RED) |
| [4.2](4.2-exec-test.md) | exec_test.go (RED) |
| [4.3](4.3-terminal-e2e.md) | terminal.e2e.ts (RED, Playwright — hard requirement) |
| [4.4](4.4-api-exec.md) | POST /api/exec (GREEN) |
| [4.4](4.4-api-exec.md) | POST /api/exec (mock, GREEN) — done |
| [4.5](4.5-command-set.md) | Command set (GREEN) |
| [4.6](4.6-namespace-validation.md) | Namespace/resource-name validation by snapshot membership (GREEN) |
| [4.7](4.7-no-shell-in-image.md) | No shell, no exec, no kubectl binary in image (GREEN) |
| [4.8](4.8-frontend-terminal-wiring.md) | Frontend: wire InteractiveTerminal to /api/exec (GREEN) |
| [4.9](4.9-structured-logging.md) | Structured logging of rejections (GREEN) |
| [4.6](4.6-namespace-validation.md) | Namespace/resource-name validation by fixture membership (GREEN) — done |
| [4.8](4.8-frontend-terminal-wiring.md) | Frontend: wire terminal page to /api/exec (GREEN) |
| [4.9](4.9-structured-logging.md) | Log rejected /api/exec input (GREEN) — done |
| [4.10](4.10-simplify-pass.md) | simplify skill pass (REFACTOR) |
## 5 — Chat + Rate Limiter (Surface D)
| Task | Description |
|---|---|
| [5.1](5.1-ratelimit-test.md) | ratelimit_test.go (RED) |
| [5.2](5.2-disconnect-test.md) | disconnect_test.go (RED) |
| [5.3](5.3-budget-test.md) | budget_test.go (RED) |
| [5.4](5.4-injection-test.md) | injection_test.go (RED) |
| [5.5](5.5-context-test.md) | context_test.go (RED) |
| [5.6](5.6-chat-e2e.md) | chat.e2e.ts (RED, Playwright — hard requirement) |
| [5.7](5.7-api-chat.md) | POST /api/chat (GREEN) |
| [5.8](5.8-rate-limiting-tier3.md) | Rate limiting Tier 3 (atlas) (GREEN) |
| [5.9](5.9-queue-position-sse.md) | Queue position streamed as SSE (GREEN) |
| [5.10](5.10-prompt-injection-defenses.md) | Prompt injection defenses (GREEN) |
| [5.11](5.11-context-budget.md) | Context budget (GREEN) |
| [5.12](5.12-chat-sse-events.md) | Chat SSE event types (GREEN) |
| [5.13](5.13-frontend-chat-component.md) | Frontend: chat component (GREEN) |
| [5.14](5.14-prometheus-metrics.md) | Prometheus metrics (GREEN) |
| [5.15](5.15-alertmanager-rule.md) | Alertmanager rule (GREEN) |
| [5.16](5.16-audit-ci-gate.md) | pnpm audit + govulncheck CI gate (GREEN) |
| [5.7](5.7-api-chat.md) | POST /api/chat (mock, GREEN) — done |
| [5.9](5.9-queue-position-sse.md) | Queue position streamed as SSE (mock, GREEN) — done |
| [5.12](5.12-chat-sse-events.md) | Chat SSE event types (mock, GREEN) — done |
| [5.13](5.13-frontend-chat-component.md) | Frontend: wire chat page to /api/chat (GREEN) |
| [5.16](5.16-audit-ci-gate.md) | pnpm audit CI gate (GREEN) |
| [5.17](5.17-simplify-pass.md) | simplify skill pass (REFACTOR) |
## Progress
Not started, Phase 0 partially unblocked. 5 decisions still block Phase 1 — see [00-decisions.md](00-decisions.md).
Status as of 2026-08-19: mock API layer implemented (`app/api/topology`, `/api/stream`, `/api/delivery`,
`/api/delivery/[app]/resources`, `/api/exec`, `/api/chat`) — all `[x]`-marked GREEN tasks above are done.
Remaining work is wiring the four page components to fetch from these routes instead of importing
`clusterMock` directly (2.6, 3.6, 4.8, 5.13), plus the RED (Playwright/Vitest) specs and REFACTOR passes.
Status as of 2026-08-18:
- 0.4 (forgejo-gitea Init stuck) — resolved, pod Running 1/1
- 0.7 (`longhorn-config` OutOfSync) — resolved, now Synced/Healthy; `sms` Degraded still open
- `*.riotpiao.homelab.com` fully retired, replaced by `*.riotpiao.com` — confirmed root cause for 0.2 (dead-domain `repoURL`, not a broken forgejo pod)
- 0.1/0.2 (portfolio GitOps repo, dead-domain repoURL) — still open, `application portfolio` not found in cluster, fix is now a known domain swap
- 0.5 (apex 403) — still open, `curl riotpiao.com` returns 403; no in-cluster ingress exists for bare apex, still need Cloudflare-side confirmation
Everything cluster/backend/infra-related that used to block this board now lives in
[REQUIREMENTS.md](REQUIREMENTS.md) and does not gate any task here.
+16 -16
View File
@@ -1,4 +1,8 @@
# atlas — Task Breakdown
# riotpiao — Task Breakdown
Next.js portfolio frontend. Cluster-visualization surfaces (topology, delivery, terminal, chat) run against
mock `/api/*` route handlers backed by [lib/clusterMock.ts](../lib/clusterMock.ts) — no real cluster, no Go
backend, no `kubectl` in this repo.
Source of truth: [docs/PLAN-atlas.md](../docs/PLAN-atlas.md), [docs/adr/ADR-0001-atlas-cluster-visualization.md](../docs/adr/ADR-0001-atlas-cluster-visualization.md)
@@ -6,29 +10,25 @@ Supersedes: [PLAN.md](../PLAN.md), [IMPLEMENTATION.md](../IMPLEMENTATION.md) (Ho
## Status
Not started, Phase 0 partially unblocked. **5 decisions block Phase 1** — see [00-decisions.md](00-decisions.md).
Mock API layer implemented. Remaining: wire page components to fetch from it, RED specs, REFACTOR passes — see [INDEX.md](INDEX.md).
## Task board
Task board lives in [INDEX.md](INDEX.md) — one row per task, one file per task (`<id>-<slug>.md`), agent-harness format. That file is the source of truth for execution order; this README is a human overview only.
| Phase | Est. |
|---|---|
| 0 — unblock deployment | — |
| 1 — atlas core (RBAC, informers, redaction) | ~4d |
| 2 — Surface B: cluster topology | ~3d |
| 3 — Surface E: delivery tree | ~3d |
| 4 — Surface C: terminal | ~2d |
| 5 — Surface D: chat + rate limiter | ~5d |
Everything cluster/backend/infra (GitOps, DNS, RBAC, the real `atlas` Go service, rate-limiting tiers 1-3, observability) is out of scope for this repo — see [REQUIREMENTS.md](REQUIREMENTS.md), a handoff spec for whoever builds that separately.
**Total: ~17 working days.**
| Phase | Description |
|---|---|
| 0 — setup | Vitest, Playwright |
| 2 — Surface B: cluster topology | mock-backed |
| 3 — Surface E: delivery tree | mock-backed |
| 4 — Surface C: terminal | mock-backed |
| 5 — Surface D: chat | mock-backed |
## Rules carried from the ADR
- I1: `riotpiao.com` is the only public hostname, ever
- I2: browser never talks to an internal API directly — atlas is the only origin
- I3: redaction is allowlist-only, enforced by DTO construction
- I4: no free-form string reaches an internal system (closed enum, snapshot-membership validation)
- I5: GPU chat concurrency capped at 6 of 8 sequence slots, disconnect cancels upstream immediately
- I2: browser never talks to an internal API directly — all cluster data flows through this app's own `/api/*` routes
- I3: redaction is allowlist-only — the mock fixture already excludes denied fields, keep it that way
- TDD: RED (tests named before code) → GREEN (minimal code) → REFACTOR, per phase
- Playwright e2e is a hard requirement for every UI-shipping phase (2, 3, 4, 5) — no surface merges without a passing spec