Skip to content

feat(slice-4e): wire admin Audit/Cost/Queue/Notifications to existing endpoints#39

Merged
lopadova merged 9 commits into
mainfrom
task/v1.7-slice-4e-operations
May 20, 2026
Merged

feat(slice-4e): wire admin Audit/Cost/Queue/Notifications to existing endpoints#39
lopadova merged 9 commits into
mainfrom
task/v1.7-slice-4e-operations

Conversation

@lopadova
Copy link
Copy Markdown
Contributor

Summary

v1.7 slice 4e — Operations admin pages (Audit/Cost/Queue/Notifications) now read from the existing server endpoints when reachable, with a graceful fallback to local fixtures.

Admin wires

  • `PageAudit` → GET `/api/audit` (x-aqa-org). Subtitle reads "N events · live from /api/audit" when server responds; AuditChainViewer renders live events.
  • `PageQueue` → GET `/api/queue`. KPIs + table both refresh from server payload.
  • `PageCost` → GET `/api/cost/summary` (tenant headers). Uses server's `total_usd` as MTD when available.
  • `PageNotifications` → GET `/api/notifications` (x-aqa-org).
  • All four fall back to existing local fixtures on error so mock-data mode keeps working.

E2E

  • 5 new tests in `operations.e2e.ts` asserting requests fire, headers are set, live count rendering (audit), and fixture fallback (audit error path).

Test plan

  • `bun run --filter @aqa/admin test:e2e -- --workers=2` (128 passed, 1 skipped)
  • `bun run typecheck`
  • `bun run lint` (4 preexisting warnings)

🤖 Generated with Claude Code

… endpoints

User asked for slice 4e to audit each Operations admin page against
the server route surface and wire any missing reads. The endpoints
(/api/audit, /api/cost/summary, /api/queue, /api/notifications) all
existed already; this slice adds the client-side fetches with
graceful fallback to the local fixtures so mock-data mode keeps
working.

Admin:
- PageAudit fetches /api/audit (x-aqa-org). When the server
  responds, the page subtitle reads "N events · live from
  /api/audit" and the AuditChainViewer renders the live events
  (the fixture demoBad is suppressed in live mode to avoid mixing
  concerns).
- PageQueue fetches /api/queue and replaces QUEUE_JOBS with the
  live payload (KPIs + table both refresh).
- PageCost fetches /api/cost/summary with tenant headers
  (x-aqa-org, x-aqa-project) and uses the server's total_usd as
  the MTD figure when available.
- PageNotifications fetches /api/notifications (x-aqa-org).

E2E: 5 new tests in operations.e2e.ts asserting the request
fires, the headers are set, and either the live count renders
(audit) or the fixture sub-header survives (audit fallback).

Tests: admin e2e 128/129 (+5, 1 skipped). Lint preexisting only.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4133fc3d30

ℹ️ 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".

Comment thread packages/admin/src/app.tsx Outdated
const res = await fetch(apiUrl('/api/queue'));
if (cancelled || !res.ok) return;
const body = await res.json();
if (Array.isArray(body?.jobs)) setJobs(body.jobs);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize queue API jobs before using queue KPIs

This assigns body.jobs directly to jobs, but GET /api/queue returns ctx.queue.snapshot() entries (EnqueuedJob in packages/server/src/runner-queue.ts) that expose status/leased_until/payload, not the UI fields used here (leased_by, attempts, payload_summary, stuck). In live mode that makes queue metrics and table cells misleading (for example, in-flight jobs are counted as pending because leased_by is undefined). Please map the API payload to the UI contract (or update the UI logic to the queue schema) before calling setJobs.

Useful? React with 👍 / 👎.

Comment thread packages/admin/src/app.tsx Outdated
});
if (cancelled || !res.ok) return;
const body = await res.json();
if (Array.isArray(body?.notifications)) setItems(body.notifications);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Adapt notifications API payload to the UI notification shape

The code stores body.notifications as-is, but the server notification schema (packages/schemas/src/notification.ts) uses fields like summary and read_by, while this page renders title, body, and unread plus fixture-specific kinds. With real API data, rows render incomplete content and unread/filter behavior becomes incorrect. Add a mapping layer from API notifications to the UI model (including derived unread state) before setting state.

Useful? React with 👍 / 👎.

Comment thread packages/admin/src/app.tsx Outdated
Comment on lines +11245 to +11247
const res = await fetch(apiUrl('/api/cost/summary'), {
headers: { 'x-aqa-org': 'padosoft', 'x-aqa-project': 'demo' },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Request an explicit month window for MTD cost

This request omits from/to, but GET /api/cost/summary defaults to a rolling 30-day window in packages/server/src/api.ts, while the page labels the KPI as “MTD spend”. Around month boundaries this pulls prior-month cost into the MTD number and misstates budget usage. Pass an explicit month-start to now window (or change the label/logic) before using summary.total_usd as MTD.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the admin “Operations” screens (Audit log, Cost, Queue, Notifications) to attempt to load live data from existing server endpoints, while keeping the existing fixture-based UI as a fallback. It also adds Playwright E2E coverage to assert the wiring (requests fire, headers are sent, and basic live/fallback rendering works).

Changes:

  • Added operations.e2e.ts Playwright suite to verify requests, tenant headers, and an audit fallback path.
  • Wired PageAudit, PageCost, PageQueue, and PageNotifications to call /api/audit, /api/cost/summary, /api/queue, and /api/notifications.
  • Updated the Audit header subtitle to reflect “live from /api/audit” and added a data-testid to the Audit download button.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
packages/admin/src/app.tsx Adds live fetches for Audit/Cost/Queue/Notifications and switches UI state to server payloads when available.
packages/admin/test/e2e/operations.e2e.ts Adds E2E tests asserting endpoint wiring, header propagation, and fixture fallback for audit.

Comment on lines +11184 to +11190
const res = await fetch(apiUrl('/api/audit'), {
headers: { 'x-aqa-org': 'padosoft' },
});
if (cancelled || !res.ok) return;
const body = await res.json();
if (Array.isArray(body?.events)) setLiveEvents(body.events);
} catch {
Comment thread packages/admin/src/app.tsx Outdated
Comment on lines +11226 to +11227
demoGood={liveEvents && liveEvents.length > 0 ? liveEvents : AUDIT_EVENTS_GOOD}
demoBad={liveEvents ? [] : AUDIT_EVENTS_BAD}
Comment on lines +11493 to +11514
const [jobs, setJobs] = React.useState(QUEUE_JOBS);
React.useEffect(() => {
let cancelled = false;
(async () => {
try {
const res = await fetch(apiUrl('/api/queue'));
if (cancelled || !res.ok) return;
const body = await res.json();
if (Array.isArray(body?.jobs)) setJobs(body.jobs);
} catch {
/* mock mode — keep the fixture */
}
})();
return () => {
cancelled = true;
};
}, []);
const pending = jobs.filter((j) => !j.leased_by).length;
const inflight = jobs.filter((j) => j.leased_by).length;
const oldestPending = jobs.find((j) => !j.leased_by);
const onlineRunners = RUNNERS.filter((r) => r.online).length;
const stuck = QUEUE_JOBS.find((j) => j.stuck);
const stuck = jobs.find((j) => j.stuck);
Comment on lines 11774 to 11803
@@ -11699,8 +11799,7 @@ function PageNotifications({ onNavigate }) {
'pack.signed',
'audit.verified',
];
const filtered =
filter === 'all' ? NOTIFICATIONS : NOTIFICATIONS.filter((n) => n.kind === filter);
const filtered = filter === 'all' ? items : items.filter((n) => n.kind === filter);

Comment on lines +14 to +18
async function gotoNav(page: import('@playwright/test').Page, label: string): Promise<void> {
await page.goto('/');
await expect(page.locator('.sidebar')).toBeVisible();
await page
.locator('.nav-item', { hasText: new RegExp(`^${label}`, 'i') })
Comment thread packages/admin/src/app.tsx Outdated
Comment on lines +11245 to +11250
const res = await fetch(apiUrl('/api/cost/summary'), {
headers: { 'x-aqa-org': 'padosoft', 'x-aqa-project': 'demo' },
});
if (cancelled || !res.ok) return;
const body = await res.json();
if (body?.summary) setSummary(body.summary);
…icit MTD bounds

Copilot/Codex iter-1 (5 P1+P2):
- AUDIT: server @aqa/schemas Event uses { ts, actor: { type, id },
  prev_hash }, but AuditChainViewer expects { at, actor: string,
  prev_hash }. Added a normalizer in the live-events branch so
  the viewer's actor.toLowerCase() / ev.at reads don't break.
  Also switched the "loaded" check from `liveEvents.length > 0`
  to `liveEvents !== null` so an empty live response is no longer
  silently masked by the fixture (while the header still claims
  "live").
- QUEUE: server EnqueuedJob is { id, enqueued_at, payload, status,
  leased_until }; UI expects { kind, leased_by, attempts, stuck,
  payload_summary }. Added a mapper: status === 'in_flight' →
  leased_by; expired leased_until → stuck; payload.kind → kind;
  truncated JSON.stringify(payload) → payload_summary.
- NOTIFICATIONS: server Notification uses { summary, actor,
  read_by, … }; UI expects { title, body, unread }. Adapter
  derives `title` from summary (or a humanized kind), `unread`
  from `!read_by.includes(SELF)`, etc.
- COST: server defaults to a rolling 30-day window but the page
  KPI reads "MTD spend"; pass explicit from/to aligned to the
  current calendar month so the total_usd matches the label.
- Test gotoNav now escapes regex metacharacters in `label` to
  match smoke.e2e.ts (a nav like "Audit (admin)" would otherwise
  trip parens).
- Cost e2e now asserts the request carries `from` (1st of current
  month) and `to` query params.

Tests: operations e2e 5/5 green; admin e2e 128/129 (1 skipped);
lint preexisting only.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (1)

packages/admin/src/app.tsx:11290

  • When summary?.total_usd is used as MTD spend, dayCount still comes from COST_DAYS.length (fixture has 18 days). This makes Avg daily and Projected · month-end incorrect in live mode (they’ll be computed using the fixture day count rather than the live from/to window). Derive dayCount (and ideally daysInMonth) from the live time range (e.g., to/from or current UTC date) when summary is present.
  const mtd = summary?.total_usd ?? COST_DAYS.reduce((a, d) => a + d.usd, 0);
  const dayCount = COST_DAYS.length;
  const avgDay = mtd / dayCount;
  const budget = 250;
  const projection = budget * 1.18; // 18% over by month-end
  const daysInMonth = 31;
  const projected = (mtd / dayCount) * daysInMonth;

Comment thread packages/admin/src/app.tsx Outdated
const to = now.toISOString();
const url = `${apiUrl('/api/cost/summary')}?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;
const res = await fetch(url, {
headers: { 'x-aqa-org': 'padosoft', 'x-aqa-project': 'demo' },
Comment thread packages/admin/src/app.tsx Outdated
Comment on lines +11819 to +11823
const SELF = 'usr_self';
const [items, setItems] = React.useState(NOTIFICATIONS);
React.useEffect(() => {
let cancelled = false;
(async () => {
Comment thread packages/admin/src/app.tsx Outdated
Comment on lines 11851 to 11859
@@ -11699,8 +11857,7 @@ function PageNotifications({ onNavigate }) {
'pack.signed',
'audit.verified',
];
Comment on lines +29 to +31
await page.route('**/api/audit', async (route) => {
const req = route.request();
captured = { url: req.url(), org: req.headers()['x-aqa-org'] ?? null };
Comment on lines +37 to +38
{ schema_version: '1', ts: '2026-05-19T10:00:00Z', kind: 'run.started', payload: {} },
{ schema_version: '1', ts: '2026-05-19T10:05:00Z', kind: 'run.finished', payload: {} },
Comment on lines +61 to +75
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
jobs: [
{
id: 'job-live-1',
kind: 'aqa.run',
enqueued_at: '2026-05-19T11:00:00Z',
leased_by: null,
stuck: false,
},
],
}),
});
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ summary: { total_usd: 123.45, by_profile: [], by_model: [] } }),
await gotoNav(page, 'Cost');
await expect(page.locator('h1, .page-title').first()).toContainText(/Cost/i);
expect(captured?.org).toBe('padosoft');
expect(captured?.project).toBe('demo');
…ue test mocks, dynamic kinds

Copilot iter-2 (8):
- Cost summary header had `x-aqa-project: demo` hardcoded but the
  admin selected project is `gescat` (PROJECTS list / TopBar
  switcher). Fixed.
- Notifications unread check compared `read_by` against a hardcoded
  'usr_self' that doesn't exist. Switched to SESSION_USER.id
  (= 'usr_sara') so the unread state resolves correctly against
  live payloads.
- Notifications filter `kinds` list omitted server kinds
  (audit.chain_broken, pack.install_failed, queue.stuck,
  user.invited, …). Derive the union dynamically from a static
  base + whatever's in items[] so live payloads stay filterable.
- E2E route patterns: `**/api/audit**` (was `**/api/audit`) and
  `**/api/queue**` so the matchers stay resilient to query params.
- Audit/Queue/Cost mock responses rewritten to the SERVER schema
  shapes (Event with seq/actor:{type,id}/prev_hash; EnqueuedJob
  with status/leased_until/payload; CostSummary with
  schema_version/org/project/from/to). Tests now exercise the
  PageAudit/PageQueue/PageCost adapters end-to-end.
- Cost test assertion now expects project=gescat (matches the
  fixed header).

Tests: operations e2e 5/5 green.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

packages/admin/src/app.tsx:11295

  • When summary?.total_usd is present, mtd becomes live data but dayCount and all projection inputs still come from the May-2026 fixtures (COST_DAYS). This makes “Avg daily”, “Projected”, and the cumulative chart internally inconsistent (fixture curve won’t sum to live MTD). Consider deriving dayCount/projection from summary.fromsummary.to, or keeping KPI/projection fully fixture-based unless you also fetch per-day data.
  const mtd = summary?.total_usd ?? COST_DAYS.reduce((a, d) => a + d.usd, 0);
  const dayCount = COST_DAYS.length;
  const avgDay = mtd / dayCount;
  const budget = 250;
  const projection = budget * 1.18; // 18% over by month-end
  const daysInMonth = 31;
  const projected = (mtd / dayCount) * daysInMonth;

Comment thread packages/admin/src/app.tsx Outdated
}
sub="Hash-chained, tamper-evident event log · verify in-browser with Web Crypto"
sub={
liveEvents
Comment thread packages/admin/src/app.tsx Outdated
Comment on lines +11533 to +11547
const adapted = body.jobs.map((j) => ({
id: j.id,
kind: (j.payload && typeof j.payload === 'object' ? j.payload.kind : null) ?? 'aqa.run',
enqueued_at: j.enqueued_at ?? new Date().toISOString(),
leased_by: j.status === 'in_flight' ? (j.leased_by ?? 'runner') : null,
attempts: j.attempts ?? 0,
stuck:
typeof j.leased_until === 'string'
? new Date(j.leased_until).getTime() < Date.now()
: false,
payload_summary:
j.payload && typeof j.payload === 'object'
? JSON.stringify(j.payload).slice(0, 80)
: '',
}));
await gotoNav(page, 'Queue');
await expect(page.locator('h1, .page-title').first()).toContainText(/Queue/i);
expect(hit).toBe(true);
// Live job id appears in the table (last 12 chars).
… accuracy

Copilot iter-3 (3 new + many stale repeats):
- Audit subtitle now uses `liveEvents !== null` (matches the
  viewer's loaded signal). The previous truthy check `liveEvents
  ? …` happened to work for [] (arrays are truthy in JS), but the
  explicit form is clearer and aligns with the viewer's branch.
- Queue adapter filters out terminal `done` jobs before mapping
  to the UI shape; otherwise those would land in the pending KPI
  (status='done' but leased_by=null after the map).
- Test comment "(last 12 chars)" was misleading because
  `job-live-1` is shorter than 12 chars; clarified that the
  full id matches in this case.

(Codex/Copilot also re-emitted ~7 already-addressed comments from
iter 1/2; all noted line:null and already fixed.)

Tests: operations e2e 5/5 green.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

packages/admin/src/app.tsx:11297

  • When live summary.total_usd is used for MTD, dayCount is still derived from the fixture COST_DAYS.length. That makes avgDay, projected, and the “days observed” label incorrect in live mode (and can also skew projections). Consider deriving dayCount from the live summary.from/summary.to (or from summary.daily if present) when summary is loaded.
  const mtd = summary?.total_usd ?? COST_DAYS.reduce((a, d) => a + d.usd, 0);
  const dayCount = COST_DAYS.length;
  const avgDay = mtd / dayCount;
  const budget = 250;
  const projection = budget * 1.18; // 18% over by month-end
  const daysInMonth = 31;
  const projected = (mtd / dayCount) * daysInMonth;

Comment on lines +37 to +50
// Schema-conforming Event payload: seq, ts, actor:{type,id},
// prev_hash, hash, run_id.
body: JSON.stringify({
events: [
{
schema_version: '1',
seq: 1,
ts: '2026-05-19T10:00:00Z',
actor: { type: 'system', id: 'runner' },
kind: 'run.started',
payload: { run_id: 'r-1' },
prev_hash: '0'.repeat(64),
hash: 'a'.repeat(64),
},
enqueued_at: '2026-05-19T11:00:00Z',
payload: { kind: 'aqa.run', profile: 'smoke' },
status: 'queued',
leased_until: null,
to: '2026-05-19T23:00:00.000Z',
total_usd: 123.45,
by_profile: [],
currency: 'USD',
Comment thread packages/admin/src/app.tsx Outdated
Comment on lines +11871 to +11883
const kinds = React.useMemo(() => {
const set = new Set([
'finding.critical',
'run.failed',
'run.completed',
'budget.threshold',
'pack.signed',
'audit.verified',
'audit.chain_broken',
'pack.install_failed',
'queue.stuck',
'user.invited',
]);
…ver enum

Copilot iter-4 (4):
- Audit test mock now matches @aqa/schemas Event exactly:
  run_id at top level, EventKind enum values (run_started /
  run_finished, not dotted), prev_hash null on the chain head.
- Queue mock: queued jobs omit `leased_until` (it's only set when
  leased to a runner), matching the server's EnqueuedJob shape.
- Cost mock dropped the non-schema `currency` field; added the
  required `daily` record (date → usd, empty here).
- Notifications kinds filter list rebased on the
  @aqa/schemas NotificationKind enum (the server contract); still
  unioned with items[] so live extras still surface as filter
  options. Previously mixed fixture-only kinds with server kinds
  and presented filters the server could never produce.

Tests: operations e2e 5/5 green.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

packages/admin/src/app.tsx:11297

  • When a live summary is loaded, only mtd switches to summary.total_usd while dayCount, the cumulative/projection series (COST_DAYS), and per-profile/table sections remain based on fixtures. This makes KPIs like “Avg daily / days observed” and the chart inconsistent with the live MTD value. Consider deriving dayCount + series from summary.daily (and using summary.by_profile) when summary is present, or keep all calculations on fixtures until the full page is wired.
  const mtd = summary?.total_usd ?? COST_DAYS.reduce((a, d) => a + d.usd, 0);
  const dayCount = COST_DAYS.length;
  const avgDay = mtd / dayCount;
  const budget = 250;
  const projection = budget * 1.18; // 18% over by month-end
  const daysInMonth = 31;
  const projected = (mtd / dayCount) * daysInMonth;

Comment on lines +11846 to +11852
const adapted = body.notifications.map((n) => ({
id: n.id,
kind: n.kind ?? 'audit.verified',
// Title falls back to a humanized kind so we always render
// something even when the server omits `summary`.
title: n.summary ?? n.title ?? (n.kind ?? 'event').replace(/\./g, ' ').toUpperCase(),
body: n.body ?? '',
// the actual session user so read_by membership checks resolve
// correctly (iter 2 — previously was a hardcoded `usr_self`).
const SELF = SESSION_USER.id;
const [items, setItems] = React.useState(NOTIFICATIONS);
Copilot iter-5 (2):
- Notification adapter fallback `kind ?? 'audit.verified'` used a
  non-schema kind ('audit.verified' isn't in NotificationKind).
  Switched the (defensive-only) fallback to 'run.failed' so a
  malformed payload still satisfies the schema-aligned filter list.
- Header subtitle ("N unread of M") and per-kind chip counts still
  read from the static NOTIFICATIONS fixture, so the counts
  disagreed with the rendered list when the server replaced items.
  Sourced both from `items` instead.

Tests: operations e2e 5/5 green.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

Comment thread packages/admin/src/app.tsx Outdated
Comment on lines +11173 to +11175
// returns chain-verified events; in mock mode we fall back to the
// hardcoded fixtures (AUDIT_EVENTS_GOOD / _BAD) so the demo stays
// usable when no server is reachable. The AuditChainViewer's
headers: { 'x-aqa-org': 'padosoft' },
});
if (cancelled || !res.ok) return;
const body = await res.json();
Comment on lines +11228 to +11232
<AuditChainViewer
// PR #39 Copilot iter 1: server Event has {ts, actor:{type,id},
// prev_hash} but AuditChainViewer's demo shape is {at, actor:
// string, prev_hash}. Normalize the server response before
// handing it to the viewer; otherwise the viewer breaks on
Comment on lines +11291 to 11294
const mtd = summary?.total_usd ?? COST_DAYS.reduce((a, d) => a + d.usd, 0);
const dayCount = COST_DAYS.length;
const avgDay = mtd / dayCount;
const budget = 250;
headers: { 'x-aqa-org': 'padosoft', 'x-aqa-project': 'gescat' },
});
if (cancelled || !res.ok) return;
const body = await res.json();
Comment on lines +11532 to +11535
const res = await fetch(apiUrl('/api/queue'));
if (cancelled || !res.ok) return;
const body = await res.json();
if (!Array.isArray(body?.jobs)) return;
Comment on lines +11546 to +11547
leased_by: j.status === 'in_flight' ? (j.leased_by ?? 'runner') : null,
attempts: j.attempts ?? 0,
Comment on lines +11843 to +11845
if (cancelled || !res.ok) return;
const body = await res.json();
if (!Array.isArray(body?.notifications)) return;
…er window

Copilot iter-6 (subset of 4 actionable):
- AUDIT fetch re-checks `cancelled` after `await res.json()` so an
  unmount mid-parse can't trigger setState on a dead component.
- Comment on the audit wire reworded: the server returns events
  as stored; AuditChainViewer is the one that verifies hashes
  client-side via Web Crypto. (The previous "chain-verified
  events" wording implied server-side verification.)
- COST: dayCount is now derived from summary.from/to when a live
  summary is loaded, so avgDay/projected/cumulative curves stay
  consistent with the server-requested [from,to] window. Falls
  back to COST_DAYS.length in mock mode.

(The 4th comment about AuditChainViewer's auto-load behavior
needs a viewer-API change that's outside this slice; left for
follow-up.)

Tests: operations e2e 5/5 green.
…ot dayCount

When dayCount is derived from a live summary window it can be >
COST_DAYS.length, and COST_DAYS[dayCount-1] would be undefined.
Anchor to the fixture's last realized day instead. Regression
from iter 6.

Tests: operations e2e 5/5 green.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

Comment on lines +11297 to +11301
const dayCount = (() => {
if (summary?.from && summary?.to) {
const fromMs = new Date(summary.from).getTime();
const toMs = new Date(summary.to).getTime();
if (Number.isFinite(fromMs) && Number.isFinite(toMs) && toMs >= fromMs) {
headers: { 'x-aqa-org': 'padosoft', 'x-aqa-project': 'gescat' },
});
if (cancelled || !res.ok) return;
const body = await res.json();
});
await gotoNav(page, 'Notifications');
await expect(page.locator('h1, .page-title').first()).toContainText(/Notifications/i);
expect(captured?.org).toBe('padosoft');
Comment on lines +108 to +112
await gotoNav(page, 'Queue');
await expect(page.locator('h1, .page-title').first()).toContainText(/Queue/i);
expect(hit).toBe(true);
// Live job id appears in the table. The UI renders only the last
// 12 chars in the id column, but `job-live-1` is already <12 so
Comment on lines +176 to +180
await gotoNav(page, 'Cost');
await expect(page.locator('h1, .page-title').first()).toContainText(/Cost/i);
expect(captured?.org).toBe('padosoft');
// PR #39 Copilot iter 2: project is now `gescat` (matches the
// admin's selected project).
Comment on lines +11547 to +11551
const [jobs, setJobs] = React.useState(QUEUE_JOBS);
React.useEffect(() => {
let cancelled = false;
(async () => {
try {
Comment on lines +11855 to +11859
const [items, setItems] = React.useState(NOTIFICATIONS);
React.useEffect(() => {
let cancelled = false;
(async () => {
try {
… strict)

The CI typecheck (stricter than local) flagged  as never because the assignment
happens inside the page.route callback closure, and TS flow
analysis narrows the outer let back to null at access sites.

Switched all four tests to a stable wrapper  and mutate properties — TS treats object-property writes
through closures as opaque.

Tests: operations e2e 5/5 green; typecheck clean.
@lopadova lopadova merged commit 99633d5 into main May 20, 2026
15 checks passed
@lopadova lopadova deleted the task/v1.7-slice-4e-operations branch May 20, 2026 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants