Skip to content

Issue #71, 72, 73 and 74 resolved - #147

Merged
afeezorobsco-cyber merged 6 commits into
DocForum:mainfrom
Jayking40:main
Oct 19, 2026
Merged

afeezorobsco-cyber merged 6 commits into
DocForum:mainfrom
Jayking40:main

Conversation

@Jayking40

Copy link
Copy Markdown
Contributor

feat(admin): audit and access log viewer (Phase W6)

Summary

PRD §7 requires every state change to be attributable, and docforum-core now records audit events (docforum-core#43) and will record who viewed which clinical record (docforum-core#45). Those records only matter if an admin can search them. This PR adds an admin-only viewer at /admin/audit that answers questions like "who released this payment?", "who verified this doctor?" and "who viewed this patient's record?".

It starts Phase W6 — Admin console in ROADMAP.md. This is PRD §7 (auditability), not one of the PRD §4 core flows.

All data comes from docforum-core's API. Nothing here calls Stellar or the escrow service.

Changes

  • New /admin/audit page with two tabs
    • State changes uses docforum-core's real GET /admin/audit-events. Each row shows the time, action code, entity, from → to state, who did it (with their role), the request ID and the extra details.
    • Record access shows who viewed which record and through which API route. It uses an assumed GET /admin/access-logs (docforum-core#45 isn't merged yet). If that endpoint returns 404, the tab says the logs aren't available yet instead of showing an error.
  • Filters are kept in the URL. Entity type and ID, user ID, action, date range, the active tab and the page position all live in the address bar, so any view can be bookmarked, shared or reloaded.
    • Values read from the URL are validated (allowed characters, length, real calendar dates, start date not after end date). Invalid values are dropped and listed in a notice.
    • Switching tabs keeps the entity, user and date filters, so "who changed this?" becomes "who viewed this?" in one click.
  • Cursor pagination, one page at a time. The viewer never loads everything: one 50-row page per request, with Newer/Older buttons. The cursors for newer pages are stored in browser history, so the back/forward buttons and the in-page buttons stay in sync. An expired cursor offers "Back to newest".
  • Entity links and shortcuts
    • Payment intents link to the payments console, which now opens a specific intent via /admin?intent=<id>. Entity types with no admin screen yet show their ID as plain text.
    • Each row has an "Entity history" link, and clicking a user ID narrows the list to that user's actions.
  • CSV export of the current page, generated in the browser. It contains exactly the rows on screen. Cells that a spreadsheet would treat as a formula are escaped to prevent CSV injection.
  • Admin-only and no PHI in logs. The route is restricted to the admin role (other roles are redirected and nothing is fetched). The viewer never writes to the browser console, and a test checks this. Client errors (4xx) are not retried.
  • Navigation: admins now get "Payments" and "Audit trail" links in the header, and the payments console links to the audit trail.
  • Docs: ROADMAP.md has a new Phase W6 section and changelog entry. The "Known assumptions — Phase W6" section of ARCHITECTURE_ESSENTIALS.md documents the real audit-events contract, the assumed access-log contract, and the missing action filter.

API gap to file in docforum-core

GET /admin/audit-events has no action filter and rejects unknown parameters with a 400. For now the action filter only searches the page that's loaded, and the UI says so. Adding a server-side action filter would let the viewer search every page.

Testing

  • Filter logic: URL round-trip for both tabs, trimming, rejection of invalid input, entity ID requiring an entity type, impossible dates and reversed ranges, malformed cursors, the query sent to the API never containing action, and how action matching works.
  • CSV: quoting, formula escaping, line endings, empty pages, file names; entity links, including that type names like constructor and __proto__ don't produce a link.
  • Viewer (with a real router):
    • URL → form → API request, and form → URL.
    • Entity ID without a type is blocked before any request.
    • Invalid links show a notice and clear back to the default view.
    • Older/Newer paging keeps the cursor in the URL; an expired cursor recovers.
    • Tab switching carries filters over and drops the action filter; arrow keys move between tabs.
    • Access-log 404 message, action filtering within the page, entity and user links, empty state.
    • CSV contains exactly the visible rows; nothing is written to the console.
    • Route guard: admins can open the page; patient, doctor and facility users are redirected and nothing is fetched.

$ npm run typecheck

tsc -b --noEmit

$ npm test
Test Files 27 passed (27)
Tests 153 passed (153)

$ npm run build
✓ built in 3.24s

The >500 kB chunk-size warning from npm run build already appears on main (518 kB there; this branch adds about 6 kB gzipped).

Screenshots

Taken with fake API responses, since the preview backend has no audit data. The fake events endpoint rejects action the same way the real one does, so the filtered screenshot also shows the parameter is never sent.

View Desktop Mobile
State changes screenshots/admin-audit-events-desktop.png screenshots/admin-audit-events-mobile.png
State changes, filtered (entity type + action + date range) screenshots/admin-audit-events-filtered-desktop.png —
Record access screenshots/admin-audit-access-desktop.png screenshots/admin-audit-access-mobile.png

Closes #71

feat(admin): Stuck referrals console — reassign or cancel with a reason

Summary

Referrals that sit pending for days leave patients stuck between doctors. This is the failure DocForum exists to prevent. This PR adds an admin console at /admin/referrals that lists stuck referrals by age, with their target specialty and the reason they're stuck (no doctor, doctor inactive, loop flagged). Each one can be reassigned to an eligible doctor or cancelled, and both actions need a reason, which docforum-core records in the audit trail.

PRD flow: §4 core flow, the referral step (booking → referral → order → fulfillment). This screen is the admin fallback when routing fails.

Depends on: DocForum/docforum-core#68 (admin referral operations) and #65 (loop detection / referral chain). Neither has landed, so the API shapes are documented assumptions (see "Known assumptions — Phase W6" in ARCHITECTURE_ESSENTIALS.md).

Changes

  • Stuck referral queue: lists referrals pending past a chosen threshold (1, 2, 3 or 7 days; 2 days by default), oldest first, with "Load more" paging. Each card shows age (the badge escalates at 3 and 7 days), target specialty, why it's stuck, the referring doctor and the doctor it's waiting on.
  • Filters in the URL: the age threshold and a reason filter (with counts) live in the address bar, so views can be shared. Values from a hand-edited link fall back to safe defaults.
  • Loop context: loop-flagged referrals show their full referral chain, with the hop that returns to an earlier doctor or specialty marked.
  • Reassign dialog:
    • The doctor picker only offers eligible doctors. It asks docforum-core for verified doctors in the target specialty, then checks the result again in the UI: only doctors who are verified, list the referral's target specialty, and are neither the doctor it's stuck with nor the referring doctor can be selected. Anything else is hidden, and the picker says how many were hidden.
    • Each doctor shows open slots in the next 7 days and their next slot; doctors with the soonest slot come first.
    • Doctors already in a flagged loop are marked, because sending the referral back to them could restart the loop.
    • If no doctor is eligible, the dialog suggests cancelling instead.
  • Cancel dialog: says plainly that docforum-core notifies the patient and that the action can't be undone.
  • Reasons: required for both actions, 10–500 characters, with extra whitespace collapsed. The form reminds admins to keep clinical details out, because reasons go into the audit trail.
  • Conflict handling: both actions send the referral version the admin saw (expectedUpdatedAt).
    • 409 / 404 (someone else acted first): the dialog closes, a notice explains what happened, and the queue refreshes.
    • 400 / 422: the dialog stays open with the server's message; 422 also reloads the doctor list.
    • Network failure: the dialog stays open and retrying is safe, because a duplicate request comes back as a conflict instead of being applied twice.
  • After an action: a notice links straight to the referral's history in the audit trail.
  • Navigation: a "Referrals" link in the admin nav, and a link from the admin dashboard.
  • Types: mirror docforum-core's real ReferralStatus and Specialty, and the Referral fields from its ARCHITECTURE.md §4.4. The encrypted snapshotNotes field is deliberately left out, and no patient-identifying data is shown.
  • Docs: ROADMAP.md has the Phase W6 item and a changelog line. ARCHITECTURE_ESSENTIALS.md documents every assumed endpoint and shape.

Testing

  • Unit tests:
    • the eligibility filter: unverified and rejected doctors, wrong specialty, current and referring doctor excluded, duplicate rows, availability ordering
    • URL filter parsing
    • age formatting and escalation
    • loop-hop detection
    • reason validation
    • error classification
  • Component and integration tests:
    • queue order and content, chain shown only for loop-flagged referrals
    • threshold filter refetches; reason filter doesn't send a request
    • paging, empty state, endpoint-not-deployed message
    • reassign: picker restricted to eligible doctors, availability, validation blocks the request, success payload and notice, 409 conflict, 422 refusal, no eligible doctors
    • Escape closes the dialog and focus returns to the button that opened it
    • cancel: patient notice, reason required, success, 404 conflict, network failure then safe retry
    • nothing is written to the browser console
    • only admins can reach the route
  • Screenshots were taken against the production build with docforum-core responses mocked.

npm run typecheck ✓ no errors
npm test Test Files 30 passed (30)
Tests 207 passed (207)
npm run build ✓ built

Screenshots

Desktop Mobile
Queue
Reassign
Cancel

Closes #72

feat(admin): system health page — readiness, background jobs and recent failures

Summary

When the backend misbehaves (a cold start on the preview, a stalled expiry job, a Stellar RPC outage), admins currently have no way to see what's wrong. This adds an admin-only System health page at /admin/system that answers "is it broken?" at a glance and shows the detail underneath.

The three backend dependencies (docforum-core#100 job runner, #104 readiness checks, #94 outbox) haven't landed yet, so the page is built against assumed API shapes, written up in ARCHITECTURE_ESSENTIALS.md under "Known assumptions — Phase W6". Until those land, each section shows "not served yet" or "not exposed" instead of an error.

PRD flow: this isn't one of the PRD §4 core flows. It's admin operations tooling (Phase W6, admin console).

Changes

  • Overview tiles for API readiness, background jobs (count failing, overdue, running) and outbox dead letters, with the pending backlog.
  • Readiness checks: overall status (Ready / Degraded / Not ready / Unreachable / Not served) and a per-check list showing required vs optional, pass/warn/fail, duration and detail. A failing optional check such as Stellar RPC shows as degraded, not down. Slow answers are flagged as likely free-tier cold starts.
  • Background jobs: one card per job with its schedule, last success (with items processed), last failure and next run. Status is Healthy, Last run failed, Overdue (more than 5 minutes past its next run, which is how a stalled job shows up), Running or Never run.
  • Recent failures: the latest failure per job from the last 24 hours, with its error message and whether it was a scheduled or manual run.
  • Run now:
    • A confirmation dialog shows the schedule, last run and next run, and explains that the job does real work. Focus starts on Cancel, so a stray Enter doesn't start it.
    • The dialog can't be closed while the request is in flight.
    • The result appears in the same dialog: finished (items, duration), failed (error), still running, already running on another instance (409), job not found (404), or couldn't confirm (network failure; safe to retry because jobs are idempotent).
    • Every outcome refreshes the page's data.
  • Auto-refresh: every 30 seconds, only while the tab is visible, catching up as soon as the tab is shown again. A failed refresh never leaves a stale "Ready" on screen; the job list stays visible with a "couldn't refresh" banner.
  • Each source fails on its own, so a missing outbox endpoint doesn't hide the jobs or readiness checks.
  • Admin-only route, linked from the admin nav ("System") and the admin dashboard.
  • Docs: ROADMAP.md (W6 item and changelog line) and ARCHITECTURE_ESSENTIALS.md (assumed shapes for /health/ready, /admin/jobs, /admin/jobs/:name/run, /admin/outbox).

Testing

  • npm run typecheck: clean
  • npm test: 33 files, 267/267 passing (60 new)
  • npm run build: green (the >500 kB chunk warning was already there on main)
 Test Files  33 passed (33)
      Tests  267 passed (267)

New tests cover:

  • Each status display: readiness ready / degraded / not ready / unreachable / not served; checks passing / warning / failing (required vs optional); jobs healthy / failing / overdue / running / never run; outbox with and without dead letters, and not exposed.
  • Run now: confirmation with Cancel and Escape (nothing runs, focus returns to the button), a successful run with its result and a refreshed job list, a failed run, a run that outlives the request, 409 already running, network failure, and that the dialog can't be closed mid-request.
  • Auto-refresh: polls while visible, stops while hidden, catches up when the tab is shown again.
  • Stale data: a failed refresh turns readiness into "Unreachable" and keeps the job list with a stale banner.
  • Routing: reachable by admins; patients, doctors and facility users are redirected without any request.
  • Service: /health/ready's 503 body is unwrapped only when it's a real readiness report; job names are URL-encoded.

Screenshots

Captured against stubbed API responses, since the endpoints don't exist yet.

Desktop Mobile
Page screenshots/admin-system-desktop.png screenshots/admin-system-mobile.png
Run now: confirm screenshots/admin-system-run-confirm-desktop.png screenshots/admin-system-run-confirm-mobile.png
Run now: result screenshots/admin-system-run-result-desktop.png screenshots/admin-system-run-result-mobile.png

Unrelated issue found

On main, the homepage's GSAP animation calls getTotalLength() on an SVG that is display: none at mobile widths. Chromium throws and the whole app goes blank, which affects anyone logging in on a phone. It's out of scope here; I'll open a separate issue.

Closes #73

feat(admin): feature flags console

Summary

Staged rollouts (wallet payments, 2FA enforcement, new notification channels) are controlled by docforum-core feature flags. Until now, changing one meant editing the database, with no audit trail the team could see. This PR adds an admin console at /admin/flags. Admins can review flags, search and filter them, and change them through a confirmation step that shows who changed each flag last and when.

docforum-core#132 hasn't landed, so the flag API shape is assumed. It is documented in ARCHITECTURE_ESSENTIALS.md → "Known assumptions — Phase W6". The "last changed" data uses the real audit-events endpoint (docforum-core#43).

This is Phase W6 (admin console) work, not one of the PRD §4 core flows.

Changes

  • Flag list: each flag shows its key, description, state (On / Rolling out / Off), type, a rollout bar, role targeting, allowlist size, a plain-language "who it reaches" sentence, and who changed it last and when. The last-change data comes from one audit-trail request for all flags, with a History link per flag.
  • Search and filter: search by key or description, a state filter with counts, and a type filter. All of it lives in the URL, so filtered views can be shared, and unrecognised URL values fall back to defaults.
  • Confirm before saving: both the quick on/off switch and the full edit form (kill switch, percentage, roles, user allowlist) lead to a review step. It shows each changed setting before and after, who the flag reaches before and after, and the latest audit entry. Turning a flag off is called out explicitly.
  • Validation: the rollout must be a whole number from 0 to 100. Allowlist entries must be user ids (UUIDs), are de-duplicated and capped at 500, and pasted emails are rejected rather than silently dropped. A review with no changes is refused.
  • Optimistic updates with rollback: the list updates the moment a change is confirmed, and the flag shows "Saving…" with its controls disabled. If the save fails, only that flag is rolled back; a save to another flag that's in flight or already finished is never undone. The list and audit data are then re-read from the server.
  • Conflicts: every save carries the updatedAt the admin saw, so concurrent edits come back as 409 and are explained. A flag that no longer exists (404) is reported too. An open dialog notices when the flag changes underneath it, blocks confirming, and offers to start again from the latest values.
  • Navigation: a new admin-only route, a "Flags" nav link and a link from the admin dashboard.
  • Docs: a ROADMAP item and changelog line, and the assumed API shape plus behaviour notes in ARCHITECTURE_ESSENTIALS.

Testing

  • Unit tests for state derivation, the audience wording, URL filter parsing, search, latest-change selection, percentage and allowlist validation, change diffing and the optimistic patch.
  • Service tests for the request path, method, body and error handling.
  • Console integration tests for:
    • the list and the last change taken from the audit trail (including the cases where the history is cut off or can't be loaded)
    • search and filters
    • the confirmation content, and that nothing is sent before confirming
    • the optimistic flip
    • rollback on a failed toggle, with list refetches held so the rollback alone is what's checked
    • rollback that affects only the failed flag while another save is in flight
    • 409 conflict handling, rejection of invalid percentages and allowlist entries, and detecting a flag changed mid-edit
    • focus handling on Escape, and that nothing is logged to the console
    • route guards for non-admin roles

npm run typecheck # clean
npm test # Test Files 37 passed (37) · Tests 335 passed (335)
npm run build # ✓ built (the >500 kB chunk warning was already there on main)

Screenshots

Captured against a mocked API, at desktop (1280px) and mobile (390px) widths:

Screen Desktop Mobile
Flag list screenshots/admin-flags-list-desktop.png screenshots/admin-flags-list-mobile.png
Search + state filter screenshots/admin-flags-filtered-desktop.png screenshots/admin-flags-filtered-mobile.png
Edit with validation error screenshots/admin-flags-edit-desktop.png screenshots/admin-flags-edit-mobile.png
Review and confirm an edit screenshots/admin-flags-confirm-desktop.png screenshots/admin-flags-confirm-mobile.png
Confirm a toggle-off screenshots/admin-flags-toggle-confirm-desktop.png screenshots/admin-flags-toggle-confirm-mobile.png
Failed save rolled back screenshots/admin-flags-rollback-desktop.png screenshots/admin-flags-rollback-mobile.png

Closes #74

Copilot AI balanced review requested due to automatic review settings October 18, 2026 22:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@drips-wave

drips-wave Bot commented Oct 18, 2026

Copy link
Copy Markdown

@Jayking40 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

The merge of upstream main resolved the models.ts conflict by truncating
the Phase W6 stuck-referrals section mid-interface, dropping Referral's
closing fields and the StuckReferral/DoctorAvailability/reassign/cancel
types, which broke typecheck in CI. Both sides only appended sections
after the same shared prefix, so keep both: upstream's Phase W4 sections
followed by the Phase W6 ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@afeezorobsco-cyber afeezorobsco-cyber left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed: all four admin pages sit behind RequireAuth allowedRoles={['admin']}, ids in paths are encodeURIComponent-ed, there's no raw HTML rendering, and every endpoint core doesn't serve yet becomes a neutral "not served yet" on 404 instead of an error. The assumed shapes are well documented in ARCHITECTURE_ESSENTIALS. CI green. LGTM.

Follow-up (non-blocking): the assumed AccessLog shape here ({id, actorUserId, actorRole, entityType, entityId, route, createdAt}, paged as CursorPage) doesn't match what docforum-core#149 currently returns ({items: [...]} with accessedBy / accessedByUser.role / accessedAt, entityType required, take limit, no cursor). One side will need to change when #149 lands.

@afeezorobsco-cyber
afeezorobsco-cyber merged commit eb9e5bb into DocForum:main Oct 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants