Skip to content

feat: add dashboard types and mock data from the API contract - #7

Merged
AbdikhaniAbdirashid merged 4 commits into
mainfrom
feat/dashboard-mock-data
Oct 31, 2026
Merged

AbdikhaniAbdirashid merged 4 commits into
mainfrom
feat/dashboard-mock-data

Conversation

@AbdikhaniAbdirashid

@AbdikhaniAbdirashid AbdikhaniAbdirashid commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Updated to match docs/api.md (#8), as requested.

  • dashboard/src/types.ts — the types block from docs/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

# Done
1 Tool attribution gone from the commit and from this description
2 Replay resets attempts to 0; attempt_log keeps its eight rows
3 status: active | failing on endpoints; GET /v1/endpoints returns { data } with no paging
4 me is { owner: { id, name }, key: { prefix } }
5 Attempt errors are AttemptError codes, null when the receiver answered
6 ApiError.details added, with 401 and both 409 bodies as fixtures
7 Timelines add up, example.com URLs, whsec_MOCK_not_a_real_secret, header points at docs/api.md

On 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 its next_attempt_at land five minutes after try 3.

What the mocks cover

Fixture Why
pending, 3 tries the row the detail screen reloads every 2 s
dead, 8 tries the exhausted-budget example; pending can be replayed too
delivered, 1 try the common case
delivered, 2 tries attempts > 1 can still end well
dead, endpoint_deleted soft-deleted endpoint example; endpoint is missing from the list
3 endpoints two active/failing, one shown on Create form response
401, 409 ×3, 422 Connect (401), Replay (409 in_flight/already_delivered/endpoint_deleted), Create (409 endpoint_limit_reached), Add form (422)

Endpoint status is derived the way docs/api.md says: n8n is failing because its latest delivery is pending after a failed try; Slack is active because its latest went through.

Checked

tsc --noEmit passes. All 27 ids are valid UUIDv7 with valid variant bits.

Not covered on purpose

No EventAccepted fixture — no screen posts events. No pagination fixture — four rows is under the default limit of 50.

@devinder-dev devinder-dev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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 / endpoint objects in delivery rows;
  • attempt_log for the history of tries. Good catch on the attempts name clash.

Requesting changes so this PR matches docs/api.md once #8 is merged:

  1. Remove the tool attribution lines: the Co-Authored-By trailer in the commit message (git commit --amend, then git 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 reach main anymore, but the commit itself is still shown on the PR.
  2. Replay mock: attempts goes 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 in attempt_log, and the next one becomes number 9.
  3. Endpoints: add status: "active" | "failing" | "disabled" (for the "Not responding" badge). GET /v1/endpoints returns { data: Endpoint[] } with no paging (max 20 per owner).
  4. GET /v1/me: { owner: { id, name }, key: { prefix } }. The top bar needs the prefix.
  5. Attempt errors as codes: timeout | connect_failed | blocked_address | redirect | tls, and null when the receiver answered with a status code. last_error copies the last try's error.
  6. ApiError: add the optional details: { path, message }[] that comes with a 422, so the Add Endpoint form can show which field is wrong.
  7. 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.com URLs and an obviously fake secret like whsec_MOCK…. The Slack-style URL and the real-looking secret could trip secret scanners.
    • Point the header comment to docs/api.md instead 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.

@AbdikhaniAbdirashid AbdikhaniAbdirashid changed the title feat: propose API v1 response shapes as dashboard mock data feat: add dashboard types and mock data from the API contract Oct 25, 2026
devinder-dev added a commit that referenced this pull request Oct 26, 2026
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.
Abdikhani Abdirashid added 2 commits October 29, 2026 14:22
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.
@AbdikhaniAbdirashid

Copy link
Copy Markdown
Collaborator Author

This is ready for another look when you have a moment — I pushed the changes but
never said so here, sorry. Your review is still pinned to 7016525, which the
rebase replaced, so GitHub is showing you a commit that no longer exists.

Now at 2657274, rebased onto main so it carries #8 and #10.

Your seven points

  1. Tool attribution — nothing to remove. The only Co-authored-by in the history
    is my own name on 6a9b2eb, which went into main back in Add dashboard wireframe sketch #2.
  2. Replay mock: attempts back to 0, old tries kept in attempt_log.
  3. Endpoint.status added.
  4. GET /v1/me is { owner: { id, name }, key: { prefix } }.
  5. Attempt errors as codes, null when the receiver answered.
  6. ApiError.details added for the 422s.
  7. Timeline adds up, example.com URLs, whsec_MOCK…, and the header comment
    points at docs/api.md instead of docs: record stack decisions (Fastify, dashboard) and update team context #6.

Plus the #10 contract, which landed after you reviewed:

  • EndpointStatus lost disabled. "Legacy billing" is now active — it has no
    deliveries, and the rule's fallback is active.
  • last_error is DeliveryError, the new type being AttemptError | "endpoint_deleted".
    I kept Attempt.error as plain AttemptError, since a single try can't fail
    because its endpoint was deleted. The types now stop us writing that by mistake.

I diffed every type and field against the block in docs/api.md; the only
differences left are whitespace.

Not blocking, but it's holding up #9, which is based on this branch. Once this
merges I'll retarget #9 to main — I won't merge it before this one, as you asked.

@devinder-dev devinder-dev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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:

  1. The #10 cases are missing from the mocks. The error fixtures (mocks.ts around line 304) only cover unauthorized, already_delivered and in_flight. Please add:

    • the two 409s from #10, endpoint_limit_reached (creating a 21st endpoint) and endpoint_deleted (replaying a delivery whose endpoint is gone);
    • one soft-deleted example: a dead delivery with last_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.)

  2. The Replay comment on deadDelivery says it's "the only row where Replay does anything". The contract also lets you replay a pending delivery to retry it now (it's 409 in_flight only 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.

@AbdikhaniAbdirashid

Copy link
Copy Markdown
Collaborator Author

Both points are in, at 746f44f.

1. The #10 cases. errorEndpointLimitReached (409, creating a 21st endpoint) and errorEndpointDeleted (409, replaying a delivery whose endpoint is gone) are in the error fixtures. The soft-deleted example is endpointDeletedDelivery: a dead delivery with last_error: "endpoint_deleted", last_status_code: null, whose endpoint "Old CRM" is nested in the row but absent from endpoints.data. endpointDeletedDetail is the same row for the detail screen — one attempt, then the endpoint went away, so the short attempt_log is the point rather than an omission.

2. The Replay comment on deadDelivery now reads as the exhausted-budget example and says a pending delivery can be retried too, with 409 in_flight only while a worker is sending it. So #9 won't disable "retry now" on pending rows.

Both optional ones are done as well: the description no longer says disabled, and errorValidationFailed carries details for url and name for the Add endpoint form.

Ready for another look. Once this merges I'll retarget #9 to main.

@devinder-dev devinder-dev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

@AbdikhaniAbdirashid
AbdikhaniAbdirashid merged commit a999516 into main Oct 31, 2026
4 checks passed
@AbdikhaniAbdirashid
AbdikhaniAbdirashid deleted the feat/dashboard-mock-data branch October 31, 2026 16:16
AbdikhaniAbdirashid added a commit that referenced this pull request Nov 1, 2026
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>
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