Skip to content

feat(booking): the audit trail is readable — a timeline read model, a staff route, and the operator detail view - #1333

Open
teetangh wants to merge 6 commits into
devfrom
feat/booking-status-history-surface
Open

feat(booking): the audit trail is readable — a timeline read model, a staff route, and the operator detail view#1333
teetangh wants to merge 6 commits into
devfrom
feat/booking-status-history-surface

Conversation

@teetangh

@teetangh teetangh commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

The read side of the booking audit trail (wave-5 PR 8 of #1319, the staff half of #448). #1322 made every CAS transition append a BookingStatusHistory row; nothing displayed it and no staff booking surface existed.

  • Read model lib/data/booking-history.ts: getBookingTimeline(appointmentId, scope) merges the appointment's status-history rows (matched by the appointment's polymorphic ids: request/event/trial id, every slot id, every reschedule-request id, plus appointmentId for rows that carry it) with its RescheduleRequest rows, newest first, actors as { id, name } only. Scope is typed to kind: "all"; an org or personal caller is a compile error and a runtime throw.
  • Route GET /api/staff/appointments/[appointmentId]/timeline behind requirePrivilegedAuth() (identical to the sibling staff appointments route), zod on the param, the platform read limiter under its own slug.
  • UI: the operator appointment detail modal (OperatorAppointmentsClient) gains a Timeline section rendering from → to, entity badge, actor or "system", reason, relative time. Metadata only, per ADR 20: no notes, no chat, no recording links, no org surface.

Finding

BookingStatusHistory.appointmentId is NULL on every row today: no caller of the transition helpers passes meta.appointmentId. The read model does not depend on it. Threading it at the writers is a follow-up registered on #1319.

Verification

Cold tsc clean; eslint zero warnings; booking-algorithm + payments + enterprise 197 suites / 2,159 tests green (4 new). One reflow in OperatorAppointmentsClient.tsx is prettier fixing drift already on dev.

Part of #1319. Part of #448 (staff visibility; the org drill-in stays out by ADR 20).

🤖 Generated with Claude Code

https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7

teetangh and others added 6 commits September 3, 2026 01:00
#1322 gave every guarded transition an append-only BookingStatusHistory
row and nothing ever read one. getBookingTimeline is that reader: it
merges the status log with the reschedule proposals raised against the
same appointment into one newest-first list, which is what #448 asked
for under the name "RescheduleLog". No new table.

The trail cannot be resolved by BookingStatusHistory.appointmentId,
because no caller in lib/booking/transitions.ts's call graph passes
meta.appointmentId, so that column is NULL on every row that exists. The
reader collects the appointment's polymorphic keys instead — its
request/event/trial id, every slot id and every reschedule request id —
and matches them against entityId, which the writers do populate. SLOT
and RESCHEDULE_REQUEST rows surface because of it. The appointmentId arm
stays in the OR so rows written once the meta is wired need no edit here.

Reading it is privileged-only. ADR 20 gives organization roles no
per-session drill-in, so the scope parameter is narrowed to the single
privileged kind at the type level and a non-`all` scope throws for an
untyped caller. Every person the trail names is read through a select
allow-list that stops at id and name (#946).

Part of #1319

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
…[id]/timeline

A thin shell over getBookingTimeline that mirrors the sibling
/api/staff/appointments route: requirePrivilegedAuth is the gate, and
passing it is what earns the `all` scope, so nothing widens it. Zod
rejects a non-uuid param before it reaches the database, a missing
appointment answers 404, and the read is throttled through
participantReadLimiter under its own route slug — no staff route has a
bucket of its own and this is the platform's 30/min-per-user read
profile.

Part of #1319

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
…l modal

The staff and admin appointments pages already share one detail modal,
opened from the row, so the timeline goes there rather than into a new
surface. It mounts with the modal, which means the trail is fetched only
for the appointment an operator actually opened.

One row per event: an entity badge, the `from → to` edge, the actor's
name or "system" for the crons, webhooks and sweeps that write no
actorUserId, a relative timestamp, and the reason when one was recorded.
Reschedule rows add how many times the proposal named and which round it
was. Metadata only — the endpoint returns no note, no chat and no
recording link, so there is none to render.

The unrelated reflow in OperatorAppointmentsClient is prettier fixing
formatting drift that was already on dev.

Part of #1319

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
Four cases over a mocked Prisma: the reschedule proposal interleaves with
the status log by timestamp rather than clustering at either end; the
actor select is exactly { id, name } and never an email; an org or
personal scope throws before a single query runs; and a missing
appointment resolves null without a second round trip. The merge case
also asserts the history WHERE resolves by entityId, which is the only
reason SLOT and RESCHEDULE_REQUEST rows appear at all.

Part of #1319

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
… model

The array annotations already contextually type the mapped literals, so
`as BookingHistoryEntity` and `as RescheduleRequestStatus` asserted what
the compiler had inferred anyway. Removing them takes the now-unused
RescheduleRequestStatus import with them.

Part of #1319

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
…ADME

The wave-5 changelog gains a PR 8 entry covering the read model, why it
resolves the trail through entityId rather than the always-NULL
appointmentId column, the type-level privilege gate that keeps ADR 20
intact, and the staff route and modal that render it. The booking README
gains a short "Reading the audit trail" section pointing at both.

Part of #1319

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
@teetangh teetangh added launch: pre-mvp Gates launch — money, data, or a failure we would not detect booking Booking, scheduling, slots, reschedule, cancellation labels Sep 2, 2026
@netlify

netlify Bot commented Sep 2, 2026

Copy link
Copy Markdown

Deploy Preview for familiarise ready!

Name Link
🔨 Latest commit 8c96f9f
🔍 Latest deploy log https://app.netlify.com/projects/familiarise/deploys/6a987ad5f574f70008ed898b
😎 Deploy Preview https://deploy-preview-1333--familiarise.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 38 (🔴 down 20 from production)
Accessibility: 90 (no change from production)
Best Practices: 83 (no change from production)
SEO: 90 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 40 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 60 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: c7d132f7-6e43-44fe-bd95-a3cee7ac45ec

📥 Commits

Reviewing files that changed from the base of the PR and between a584ca9 and 8c96f9f.

📒 Files selected for processing (7)
  • __tests__/booking-algorithm/booking-timeline-read.test.ts
  • app/api/staff/appointments/[appointmentId]/timeline/route.ts
  • components/dashboard/shared/AppointmentTimeline.tsx
  • components/dashboard/shared/OperatorAppointmentsClient.tsx
  • docs/booking/05-troubleshooting-and-changelog.md
  • docs/booking/README.md
  • lib/data/booking-history.ts

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

@teetangh

teetangh commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your current included review allowance is based on your included PR review attempts over the past 7 days. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 40 minutes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

booking Booking, scheduling, slots, reschedule, cancellation launch: pre-mvp Gates launch — money, data, or a failure we would not detect

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant