Data and call flow
Trace a loaded review through an edit, an accepted save, and its next appearance in a list.
Open a page
GET /dashboard root layout → getServerSession → session/appearance/notification providers protected layout → getPageActor → capability check where needed dashboard page → queryInbox(actor, query) → initial rows Inbox reuses matching initial data; later changes fetch /api/inbox GET /reviews/<UUID> validate UUID → getPageActor → loadReviewForActor(actor, key) reviewStore.load(key) → latest snapshot → assertReviewAccess touchReview(actor, key, version) → opened/read/touch state ReviewProvider(initial) → ReviewWorkspace
Opening a review updates read/touch state but does not append a review snapshot or create a missing demo case. Received copies of facts are captured by intake, reset, and workflow commands, not by GET.
Edit and save
Input change
ReviewProvider.perform(command)
commandSchema.parse
CommandDraft.stage or execute → local review preview
queue command; mark unsaved
Save / save-before-navigation / optional autosave
flush → POST /api/reviews/<UUID>
{ version, requestId, commands: [{ id, command }] }
route parses body, adds the session actor and server time
saveCommands
load current + findRequest, in parallel
check access for every command actor
return accepted request on retry, or reject stale version
execute each command with shared domain rules
appendReview → reviewStore.append(result, previous)
reuses the loaded version for news, no second read
one transaction: snapshot + index + derived news + staff touch
response: StoredReview → provider updates saved version/status
The POST accepts 1–1000 commands. Repeated edits of a draft-compatible field can become
one command. Computation drafts are separate: Save column turns one draft into a
scenario command; Apply Final Results runs selectFinal on saved
scenarios. Saving other commands does not save unsaved computation columns.
Failures and retries
| Result | Behavior |
|---|---|
| 401 / 403 | Authentication or access rejection; inspect the server actor and role/state rule. |
| 404 | Invalid UUID or missing review. Legacy URL formats do not resolve. |
| 400 | Invalid request or domain rejection; exact messages come from the endpoint. |
| 409 + current | Stale review. The provider clears queued changes, undo, and computation drafts, then installs the server copy. |
| 503 / network failure | Storage/load failure or failed request. The provider retains the failed save batch for retry with the same requestId. |
The unique snapshot key (review_key, version) handles concurrent append
races. A request ID belongs to one review; retry paths recheck that identity and access.
Save-before-navigation waits for success. Page-hide/unload attempts are best effort, not a
guarantee against a closed or crashed browser.
Other writes
| Action | Path |
|---|---|
| Flag a row | PATCH catalog/flag → scoped personal touch-row update; no snapshot version. |
| Upload PDF | Upload adapter → validated storage key → document command → snapshot metadata. Blob upload and review save are separate operations. |
| Import / Refresh | POST imports/snap-negative → server parser → findBusiness → UUID allocation or preservation → appendReview. |
| Publish policy | POST policy → role check + schema → locked insert-only policy row. Existing review policy copies stay fixed. |
Downstream reads
The next inbox/catalog query sees the saved index version. Reports read
eligible and variances; export month counts use
transmittalEligible. Export downloads re-read current snapshots and apply the
gates again. Notifications are polled, so they can lag the save.
Source: src/lib/load-review.ts,
src/features/reviews/persistence.ts,
src/app/api/reviews/[reviewKey]/route.ts.