Repository navigation
feat: add dashboard types and mock data from the API contract - #7
Conversation
devinder-dev
left a comment
There was a problem hiding this comment.
Thanks, this is really useful. Writing down the exact shapes is what showed that our full contract only lived in my notes and never made it into the repo. That's on me, not you. I've now opened #8 with docs/api.md, and it adopts two of your ideas:
- nested
event/endpointobjects in delivery rows; attempt_logfor the history of tries. Good catch on theattemptsname clash.
Requesting changes so this PR matches docs/api.md once #8 is merged:
- Remove the tool attribution lines: the
Co-Authored-Bytrailer in the commit message (git commit --amend, thengit push --force-with-lease) and the last line of the PR description. I've switched squash merges to use the PR title and description, so commit trailers can't reachmainanymore, but the commit itself is still shown on the PR. - Replay mock:
attemptsgoes back to 0 after a replay. It's the retry budget: with 8, a replayed delivery would get one try and be dead again. The old tries stay inattempt_log, and the next one becomes number 9. - Endpoints: add
status: "active" | "failing" | "disabled"(for the "Not responding" badge).GET /v1/endpointsreturns{ data: Endpoint[] }with no paging (max 20 per owner). GET /v1/me:{ owner: { id, name }, key: { prefix } }. The top bar needs the prefix.- Attempt errors as codes:
timeout | connect_failed | blocked_address | redirect | tls, andnullwhen the receiver answered with a status code.last_errorcopies the last try's error. ApiError: add the optionaldetails: { path, message }[]that comes with a 422, so the Add Endpoint form can show which field is wrong.- Small things:
- Make the mock timeline add up: each wait starts when the previous try ended; after 3 failed tries the next one is about 5 min away; the dead delivery's last try comes before the replay; the list is newest first.
- Use
example.comURLs and an obviously fake secret likewhsec_MOCK…. The Slack-style URL and the real-looking secret could trip secret scanners. - Point the header comment to
docs/api.mdinstead of PR #6.
The easiest route is probably to copy the types block from docs/api.md into types.ts and adjust the mocks to it. Happy to pair on the replay part if that helps.
7016525 to
0665956
Compare
Adds `docs/api.md`: the API v1 contract with every request and response
shape, so the API and the dashboard build against one file instead of
two descriptions that drift apart. This moves the planned "API contract"
step earlier, because the dashboard needs it now.
## What's in it
- Conventions: auth, JSON only, 256 KiB limit, UUIDv7 ids, ISO times,
`X-Request-Id`, 404 for other owners' ids, CORS.
- Errors: the one error shape, `details` on 422, and the full list of
status codes and `code` values.
- Cursor pagination: `{ data, next_cursor }`, newest first.
- TypeScript types the dashboard can copy into `src/types.ts`.
- Every route with its body rules, answer and errors.
## Adopted from PR #7
- **Nested** `event` and `endpoint` objects in delivery rows, instead of
flat `event_type` / `endpoint_name`. It matches the detail answer and
reads better in the dashboard. The API maps database rows to this shape
in its service layer.
- **`attempt_log`** for the history of tries. The earlier draft used
`attempts` for both the counter and the list.
## Clarified
- Replay resets `attempts` to 0. The history (`attempt_log`) is never
deleted, and tries keep their numbers.
- Endpoints have a `status` (`active` / `failing` / `disabled`), worked
out when the list is read.
- `GET /v1/me` returns the owner and the key prefix.
- Attempt errors are codes (`timeout`, `connect_failed`,
`blocked_address`, `redirect`, `tls`); `null` when the receiver answered
with a status code.
- `next_attempt_at` is `null` unless the delivery is pending.
`docs/CONTEXT.md` now points to the new file.
## How to review
Read `docs/api.md` top to bottom and check that the dashboard screens
can be built from these shapes alone. Once merged, PR #7 can align
`dashboard/src/types.ts` to it.
types.ts is a copy of the types block in docs/api.md; mocks.ts is fake data shaped to it, for building the four screens before the API exists. The mocks cover every state the screens have to render: a delivery mid-retry, one that gave up after eight tries, two that went through, three endpoints with one of each status, and the three error bodies (401, and both ways replay can be refused). Timelines are arithmetic, not decoration: every gap in an attempt log starts when the previous try ended and follows the retry schedule, so the detail screen shows a history that could actually have happened.
0665956 to
2657274
Compare
|
This is ready for another look when you have a moment — I pushed the changes but Now at Your seven points
Plus the #10 contract, which landed after you reviewed:
I diffed every type and field against the block in Not blocking, but it's holding up #9, which is based on this branch. Once this |
devinder-dev
left a comment
There was a problem hiding this comment.
Checked against docs/api.md on main, after #10. The types match the contract now: EndpointStatus is active | failing, DeliveryError includes endpoint_deleted, the nested event/endpoint and attempt_log are right, and they pass tsc --strict. Two small things before I approve:
-
The #10 cases are missing from the mocks. The error fixtures (
mocks.tsaround line 304) only coverunauthorized,already_deliveredandin_flight. Please add:- the two 409s from #10,
endpoint_limit_reached(creating a 21st endpoint) andendpoint_deleted(replaying a delivery whose endpoint is gone); - one soft-deleted example: a
deaddelivery withlast_error: "endpoint_deleted", whose endpoint is missing from the endpoint list but still nested in the delivery.
The screens in #9 are built from these mocks, so they're the cases the dashboard will otherwise never show. (DELETE itself answers 204 with no body, so no fixture is needed for it.)
- the two 409s from #10,
-
The Replay comment on
deadDeliverysays it's "the only row where Replay does anything". The contract also lets you replay a pending delivery to retry it now (it's 409in_flightonly while a worker is sending it). If the comment stays, the dashboard might disable "retry now" on pending rows. Something like "the exhausted-budget example; a pending delivery can be retried too" would fix it.
Small, optional: the PR description still lists active | failing | disabled, and a validation_failed example with details would help when you build the endpoint form.
I'll approve as soon as 1 and 2 are in. Then #9 can be rebased onto it.
|
Both points are in, at 746f44f. 1. The #10 cases. 2. The Replay comment on Both optional ones are done as well: the description no longer says Ready for another look. Once this merges I'll retarget #9 to main. |
devinder-dev
left a comment
There was a problem hiding this comment.
Checked 746f44f against both points. The two 409s from #10 are in, the soft-deleted example is right (dead, last_error endpoint_deleted, its endpoint nested in the row and the detail but absent from the endpoint list), and the Replay comment now allows retrying a pending delivery. The optional ones are in too, and types.ts plus mocks.ts pass tsc --strict. Approving. Retarget #9 to main once this merges.
The four screens from the wireframe, running on the mock data from #7. Vite + React + TypeScript + Tailwind v4 + React Router, per ADR 0002. Based on #7 so the diff stays clean — GitHub retargets this to main once that merges. ## Screens | Screen | What works | |---|---| | Connect | key check, error on a bad key, key kept in sessionStorage | | Delivery log | status filter, event type and endpoint name per row, link to detail | | Delivery detail | payload, full attempt history, replay | | Endpoints | list with status, add form with validation, signing secret shown once | ## Three things worth a look **The detail screen reloads every 2 s while a delivery is pending**, and only then — nothing starts for one that is delivered or dead, and the timer is cleared when the screen is left. ADR 0002 asks for this, and it is what makes retries visible during the demo. **Replay takes four fields, not the whole answer.** The replay endpoint returns a `Delivery`, whose `event` carries no payload. Spreading it over a `DeliveryDetail` silently emptied the payload section. TypeScript caught it, which is a decent argument for copying the contract types exactly. **The key goes in sessionStorage, not localStorage**, so it is gone when the tab closes rather than waiting for the next person on a shared machine. ## Styling Palette and fonts lifted from `docs/design/relay-wireframe.html` as CSS variables with the same light and dark values. Status colours live in one `StatusPill` component. Two small departures from the sketch: times show as clock time rather than the ISO string, since the log is scanned for "which row is mine"; and a pending row carries its next attempt time, because "pending" on its own says nothing. ## Dockerfile Two stages: Node builds the app, then unprivileged nginx serves the result. No Node and no `node_modules` in the final image — 82 MB. Two details worth knowing: - **Port 8080, not 80.** The unprivileged image runs as uid 101, which cannot bind a port below 1024. The k8s Service will have to target 8080. - **`VITE_API_URL` is baked in at build time**, not read at startup. Changing the API address means rebuilding the image; a Kubernetes env var will not do it. `try_files $uri $uri/ /index.html` is what makes reloading `/deliveries/:id` work. Assets under `/assets/` are excluded from the fallback, so a missing script returns a clean 404 instead of HTML that fails to parse. ## Checked `npm run build` passes, `tsc --noEmit` clean, no console errors. Every screen and both themes verified in a browser — filters, the add form, and replay (`dead → pending`, `attempts → 0`, payload and attempt log untouched). The image was built and run locally: serves `/`, serves `/deliveries/abc` as index.html, returns 404 for a missing asset, runs as uid 101, and sets `no-cache` on index.html and a one-year immutable cache on hashed assets. ## Next The k8s manifest is written and validated against the cluster with a server-side dry run, but it points at an image that is not in GHCR yet, so it comes as a separate infra PR. Swapping `src/mocks.ts` for a real `src/api.ts` is phase 5.2. --------- Co-authored-by: Abdikhani Abdirashid <Abdikhani_abdirashd@hotmail.com>
Updated to match
docs/api.md(#8), as requested.dashboard/src/types.ts— the types block fromdocs/api.md, copied over. Not a proposal any more.dashboard/src/mocks.ts— fake data shaped to those types, for building the four screens before the API exists.Your review points
attemptsto 0;attempt_logkeeps its eight rowsstatus: active | failingon endpoints;GET /v1/endpointsreturns{ data }with no pagingmeis{ owner: { id, name }, key: { prefix } }AttemptErrorcodes,nullwhen the receiver answeredApiError.detailsadded, with 401 and both 409 bodies as fixturesexample.comURLs,whsec_MOCK_not_a_real_secret, header points atdocs/api.mdOn the timelines
Every gap in an attempt log starts when the previous try ended (
started_at + duration_ms) and follows the retry schedule. The dead delivery's eight tries are exact to the millisecond, and the pending delivery's three tries total 10.350 s, which is what makes itsnext_attempt_atland five minutes after try 3.What the mocks cover
attempts > 1can still end wellEndpoint
statusis derived the waydocs/api.mdsays: n8n isfailingbecause its latest delivery is pending after a failed try; Slack isactivebecause its latest went through.Checked
tsc --noEmitpasses. All 27 ids are valid UUIDv7 with valid variant bits.Not covered on purpose
No
EventAcceptedfixture — no screen posts events. No pagination fixture — four rows is under the default limit of 50.