feat(slice-4f): wire Admin section pages to existing /api endpoints#40
Conversation
… to existing endpoints Admin: - PageTokens fetches GET /api/tokens (x-aqa-org), replaces the local fixture with the server payload. Existing 3-row mock stays as fallback for mock-data mode. - PageOrg fetches GET /api/orgs and joins the live org list into the page subtitle (falls back to "padosoft" / "No orgs configured" depending on response). - PageAdminAudit shares the same /api/audit wire as PageAudit (slice 4e) with admin-view subtitle copy. Schema normalizer + unmount-safe cancellation guard applied (same lessons as 4e). Scope note: Users / Roles / SSO are intentionally deferred — no server scaffolding exists for them yet (would require new schemas + routes). Slice 4f ships what's wirable against the current route surface. E2E: 4 new tests in admin-section.e2e.ts (tokens live, orgs live, orgs fallback, audit-admin live). Admin e2e total: 132/133 (+4, 1 skipped). Typecheck + lint clean.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe1aa79fd0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (cancelled || !res.ok) return; | ||
| const body = await res.json(); | ||
| if (cancelled) return; | ||
| if (Array.isArray(body?.tokens)) setTokens(body.tokens); |
There was a problem hiding this comment.
Normalize /api/tokens records before storing in UI state
GET /api/tokens returns ApiToken objects (display_name, last_used_at, prefix, etc.), but this code stores body.tokens directly and later renders fields like t.name, t.kind, and t.last_used; with real server data those columns become blank/incorrect (and the “live” view silently degrades) even though tests pass with a fixture-shaped mock. Map the server payload into the table’s expected view model before setTokens.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Wires the Admin section pages in @aqa/admin to existing server endpoints (/api/tokens, /api/orgs, /api/audit) while keeping fixture-mode fallbacks, and adds Playwright E2E coverage to validate the wiring.
Changes:
- Added live-fetch wiring for Org & project, API tokens, and Admin audit pages with graceful fallback behavior.
- Updated Admin audit rendering to normalize server event shapes into the existing
AuditChainViewerdemo format. - Added new Playwright E2E tests for the Admin section live/fallback behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/admin/src/app.tsx | Adds live fetch() calls for /api/orgs, /api/tokens, /api/audit and updates page subtitles/viewer input based on live data. |
| packages/admin/test/e2e/admin-section.e2e.ts | Adds E2E wiring tests for Admin pages (tokens/orgs/audit-admin). |
| const res = await fetch(apiUrl('/api/tokens'), { | ||
| headers: { 'x-aqa-org': 'padosoft' }, | ||
| }); | ||
| if (cancelled || !res.ok) return; | ||
| const body = await res.json(); | ||
| if (cancelled) return; | ||
| if (Array.isArray(body?.tokens)) setTokens(body.tokens); | ||
| } catch { |
| // hashed token records (no raw secret); the create flow (POST | ||
| // /api/tokens) returns the freshly-issued raw token exactly once. |
| name: 'Live token from server', | ||
| kind: 'service', | ||
| last_used: '2026-05-19T12:00:00Z', | ||
| scopes: ['runs:write'], |
| const sub = | ||
| orgs === null | ||
| ? 'padosoft' | ||
| : orgs.length === 0 | ||
| ? 'No orgs configured · using mock org "padosoft"' | ||
| : orgs.map((o) => o.slug ?? o.name ?? '?').join(', '); |
…t/UX fixes Copilot iter-1 (5): - PageTokens was storing the raw /api/tokens payload (@aqa/schemas ApiToken: display_name/prefix/owner/last_used_at/scopes) directly, but the page renders fixture-style fields (name/kind/last_used). Added an adapter that maps display_name→name, last_used_at→ last_used, and heuristically derives kind from owner (svc_/bot_/ ci_/service_ prefix → service, otherwise user). A future server schema can expose `kind` directly to remove the heuristic. - Comment about POST /api/tokens issuing a raw token "exactly once" was inaccurate (current server schema only defines prefix). Updated the comment. - E2E mock rewritten to a schema-conforming ApiToken payload so the test exercises the adapter and would catch a real contract drift. - Org subtitle copy aligned with the PR description: when the live list is empty, just "No orgs configured" (was "No orgs configured · using mock org \"padosoft\""). Tests: admin-section e2e 4/4 green.
| { slug: 'padosoft', name: 'Padosoft' }, | ||
| { slug: 'acme', name: 'Acme Corp' }, |
| }); | ||
|
|
||
| test('Audit (admin) page fetches /api/audit and renders the live count', async ({ page }) => { | ||
| await page.route('**/api/audit**', async (route) => { |
…route stub
Copilot iter-2 (2):
- Org mock payload now matches @aqa/schemas Tenancy.Org
(schema_version/slug/display_name/created_at) instead of the
truncated {slug, name}. Page consumes slug for the subtitle
which still works, but the test now exercises the actual
contract.
- Audit-admin route stub gated on GET so a future admin
POST/DELETE on /api/audit isn't silently hijacked.
Tests: admin-section e2e 4/4 green.
| () => [ | ||
| { | ||
| id: 'tok_ci', | ||
| name: 'CI · GitHub Actions', | ||
| kind: 'service', | ||
| last_used: '2026-05-18T13:48:00Z', | ||
| scopes: ['runs:write', 'findings:read'], | ||
| created_at: '2026-04-12T08:00:00Z', | ||
| }, | ||
| { | ||
| id: 'tok_sara', | ||
| name: 'Sara · CLI laptop', | ||
| kind: 'user', | ||
| last_used: '2026-05-18T11:14:00Z', | ||
| scopes: ['*'], | ||
| created_at: '2026-04-02T09:00:00Z', | ||
| }, |
… enum
Copilot iter-3 (1):
- Fallback fixture used pre-schema scopes ('runs:write', '*') that
don't match ApiTokenScope. Switched to schema-valid values
('runs:create', 'findings:read', 'admin:everything',
'audit:read'). Future token-creation/revocation flows will
validate cleanly against the same enum the server uses.
Tests: admin-section e2e 4/4 green.
| // PR #40 Copilot iter 3: fallback scope values use the actual | ||
| // ApiTokenScope enum (runs:create / findings:edit / | ||
| // admin:everything / audit:read), not the pre-schema strings | ||
| // ("runs:write" / "*") that wouldn't validate against the server. | ||
| const FALLBACK_TOKENS = React.useMemo( |
| : 'user', | ||
| last_used: t.last_used_at ?? t.last_used ?? null, | ||
| scopes: Array.isArray(t.scopes) ? t.scopes : [], | ||
| created_at: t.created_at ?? new Date().toISOString(), |
| <AuditChainViewer | ||
| demoGood={ | ||
| liveEvents !== null | ||
| ? liveEvents.map((ev) => ({ | ||
| at: ev.ts ?? ev.at ?? '', | ||
| actor: | ||
| typeof ev.actor === 'string' | ||
| ? ev.actor | ||
| : ev.actor?.id || ev.actor?.type || 'system', | ||
| kind: ev.kind ?? 'event', | ||
| payload: ev.payload ?? {}, | ||
| prev_hash: ev.prev_hash ?? '0'.repeat(64), | ||
| hash: ev.hash ?? '0'.repeat(64), | ||
| })) | ||
| : AUDIT_EVENTS_GOOD |
… audit normalizer
Copilot iter-4 (3):
- Create-token modal chips listed pre-schema scopes
('runs:write', 'findings:write', 'packs:install', 'admin'). Now
mirrors @aqa/schemas ApiTokenScope (runs:read/create,
findings:read/edit, audit:read, admin:everything) so a future
POST /api/tokens can pass them straight through.
- Adapter defaulted `created_at` to `new Date().toISOString()` when
the server omitted it — a misleading "just-created" date for
whatever record lost the timestamp in transit. Now left null;
downstream formatters can render a placeholder.
- Extracted normalizeAuditEventsForViewer to a top-level helper
used by both PageAudit and PageAdminAudit. Same normalization
(ts → at, actor: {type,id} → string, prev_hash null → zero
sha256, …) in one place — no more divergence as the Event
schema evolves.
Tests: admin-section + operations e2e green.
| // PR #40 Copilot iter 4: leave created_at null when the | ||
| // server omits it — defaulting to "now" was misleading | ||
| // (showed a fake "just created" date for any record where | ||
| // the timestamp was lost in transit). | ||
| created_at: t.created_at ?? null, |
Copilot iter-5 (1): - fmtDate/fmtDateTime didn't guard against null — a token with null created_at (from iter 4) would render as '1970-01-01'. Both formatters now return an em-dash for null/undefined/ invalid dates, matching the existing fmtRelative null-safety pattern. Tests: admin e2e green.
- docs/PROGRESS.md: added a 2026-05-20 entry summarizing every v1.7 slice 4 PR (#29..#40), the architecture lessons that carried across them, and the final tag step. - README.md Roadmap table: added v1.4/v1.5/v1.6/v1.7 rows matching the actual shipped surface. Status header updated to "v1.7 current". No code changes — closing-step docs only.
Summary
v1.7 slice 4f — Admin section pages (API tokens, Org & project, Audit-admin) now read from the existing `/api/tokens` / `/api/orgs` / `/api/audit` endpoints with graceful fixture fallback.
Admin
Scope note
Users / Roles / SSO are intentionally deferred — no server scaffolding exists for those (would require new schemas + routes). Slice 4f ships what's wirable against the current route surface.
E2E
Test plan
🤖 Generated with Claude Code