Repository navigation
fix(dashboard): three pages read fields the API does not return - #75
ExposureGuard wants to merge 2 commits into
Conversation
Three faults stacked on each other, each hiding the next:
* the store had no way to remove an endpoint at all;
* `DELETE /v1/webhooks/<id>` did not exist, though the dashboard's Delete
button has always called it — the button 404'd and the row stayed;
* `/v1/admin/overview` never set the `webhooks.webhooks` key the page
renders from, so the page said "No webhooks registered" however many
were registered.
And the table read `id` / `event` / `deliveries` — names no surface has ever
returned — so even once the list arrived, every cell but the URL would have
rendered empty.
Fixed in all four places, with the rule the rest of the webhook surface
follows: a miss is the same 404 whether the id belongs to another tenant or
to nobody, so the route cannot be used as an existence oracle. Delivery
history is kept on purpose — it records what was sent, not what is
configured.
The list is read straight from the table rather than the manager's in-memory
registry: production runs two workers, and a webhook registered on one is not
in the other's list.
The contract test parses `dashboard.js`'s real template and asserts every
field it reads is one the overview returns. The two drifted silently once;
that drift was the whole bug.
`openapi.json` regenerated for the new route.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The webhooks fix was one instance of a class. Sweeping the other loaders against the real payloads found two more: **Approvals.** The table read `r.session_id`, `r.requested_by` and `r.requested_at`. An approval row carries `agent_id`, `tool`, `action`, `amount`, `reason` and `created_at` — and neither of the first two exists, so half of every row rendered blank and "Requested at" showed nothing at all. The headers now say what the columns are. **Compliance.** `next_due` is per schedule and the response carries no top-level one, so reading `schedules.next_due` left "Next pack due" showing "—" forever, however many schedules there were. The soonest is now taken from the rows. The rest were checked and are correct: account, quotas, sessions, audit and settings all read fields the API returns. So does the older `/dashboard` (dashboard.html), which is a separate self-contained implementation — the newer page is the one with the mismatches. `tests/test_dashboard_contract.py` generalises the webhook contract test to all three, parsing the real template in dashboard.js and resolving each `x.field` access against the live payload. Checked in both directions: with the old field names restored, the approvals test fails. Its session is minted through the Gate rather than `POST /v1/sessions` — that route enforces the tenant's agent cap, which a full-suite run has usually reached, and that is exactly how this test failed the first time it ran in the suite. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Second pass added (commit Approvals was broken the same way. The table read Compliance's "Next pack due" was permanently "—". The rest were checked and are clean — account, quotas, sessions, audit and settings all read real fields. So does the older
|
Independent of #73 and #74 — branched from
main.What was wrong
The dashboard's Webhooks page has never worked. Three faults stacked, each hiding the next:
WebhookManagerhadregister,fire,list_webhooks,rotate_secret— and no way to remove an endpoint.DELETE /v1/webhooks/<id>did not exist — but the page's Delete button has always called it, so it 404'd and the row stayed./v1/admin/overviewnever set thewebhooks.webhookskey the page renders from._webhooks()returns counts (registered_count,deliveries_24h), not a list, sorowswas always[]and the page said "No webhooks registered" however many there were.And underneath all three: the table read
w.id/w.event/w.deliveries, while the API answerswebhook_id/events/fire_count. Even once the list arrived, every cell but the URL would have been empty.The fix
WebhookManager.unregister(webhook_id, tenant_id)— tenant-scoped; refreshes the in-memory registry so the next event doesn't go to a receiver the operator just removed. Deliberately does not rely on the DELETE's row count: psycopg2'sexecute()returnsNonewhere sqlite3 returns a cursor, so a successful delete would read as a miss on Postgres.DELETE /v1/webhooks/<int:webhook_id>—webhooks:write, audit-logged, and a miss is the same 404 whether the id belongs to another tenant or to nobody. Status codes must not answer "does tenant B have webhook id 7".haldir_admin._webhooks()now carries the endpoint list, read straight from the table rather than the manager's in-memory registry — production runs two workers, and a webhook registered on one is not in the other's list.success_rateisNone, not1.0, for an endpoint nothing has ever reached.dashboard.jsreads the real field names.Delivery history is kept on purpose —
webhook_deliveriesrecords what was sent, not what is configured.Tests —
tests/test_webhook_delete.py, 8 newStore removal, tenant scoping (both directions), the route end-to-end, delete-twice-is-404, the overview payload, and a contract test that parses
dashboard.js's actual template (function loadWebhooks…// ── Approvals page) and asserts everyw.<field>it reads is a field the overview returns. That drift was the bug; it now fails in CI instead of emptying a table.Verification
openapi.jsonregenerated for the new route (its guard caught the drift, as designed)Note for whoever merges #73 first: both branches touch
openapi.json— a generated file. Whichever lands second needs a regen; the committed-spec test will say so.🤖 Generated with Claude Code