Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 42 additions & 10 deletions ARCHITECTURE_ESSENTIALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,15 @@ land and consume `appointmentKeys.list(profileId)`.
**preview**, not production: the free-tier backend spins down after 15
minutes idle (~1 minute cold-start delay on the next request), and its
Postgres expires 2026-10-14 unless renewed — data is disposable.
- Router is `HashRouter`, not `BrowserRouter` — GitHub Pages has no
server-side rewrite for client-side routes, so a deep link like `/login`
would 404 on refresh under history-API routing. Switch back to
`BrowserRouter` if this ever deploys somewhere with a real rewrite rule
(e.g. behind `docforum-core` itself, or Vercel/Netlify).
- Routing is a `createHashRouter` data router (`src/router.tsx`), not
`BrowserRouter` — GitHub Pages has no server-side rewrite for
client-side routes, so a deep link like `/login` would 404 on refresh
under history-API routing. Route modules are lazy, loaders prefetch
React Query data (`ensureQueryData` with the same keys/shapes the pages
use), guards are loaders that await session restore, and each section
has a route-level `RouteError`. Switch back to `createBrowserRouter` if
this ever deploys somewhere with a real rewrite rule (e.g. behind
`docforum-core` itself, or Vercel/Netlify).
- `vite.config.ts`'s `base` is conditional on `GITHUB_PAGES=true` (set by
`npm run build:pages`) — plain `npm run build` still serves from `/`.

Expand Down Expand Up @@ -153,11 +157,13 @@ envelope, `fullName` is a real column, `role` restricted to `patient`/
real backend's refresh cookie is `SameSite=None; Secure`, cross-site
between this repo's `github.io` domain and Render's, so it's required for
the cookie to be sent/received at all). One gap remains:
- **`POST /auth/refresh` exists on the backend but nothing here calls
it.** `auth-store.ts` still has no silent-reauth-on-reload flow — a
page refresh loses the session even though the refresh cookie would
still be valid. Tracked, not built (Phase W1 was scoped to
signup/login only).
- **`POST /auth/refresh` silent re-auth is implemented** by the W7
data-router migration (#112): `restoreSession()` in `auth-store.ts`
calls it once (single-flight) and every loader guard awaits that
promise, so a deep link no longer bounces through `/login`. The
response is assumed to be the same `{ user, accessToken }` envelope as
login; issue #9 stays open for an end-to-end check against the real
backend.
- Local-dev-only caveat inherited from the backend: its refresh cookie is
`sameSite: 'lax'`/`secure: false`, which may not survive true
cross-origin (different ports) `fetch` + `credentials: 'include'` in
Expand Down Expand Up @@ -731,6 +737,32 @@ localStorage/sessionStorage); EXIF is never read or displayed here (the
server strips it). The full thread UX (optimistic send, live updates,
per-side layout) is #22's scope — `MessageThread` here is a minimal shared
shell so the attachment composer/preview have a real home.
### Phase W7 — Platform quality (issues #112–#115)

- **Session restore contract (#112, depends on #9):** `POST /auth/refresh`
(no body; the httpOnly cookie carries the token, sent because
`apiFetch` uses `credentials: 'include'`) returns the same
`{user: User, accessToken: string}` envelope as `POST /auth/login`. A
failure is treated as "signed out", not as an app error. The frontend
never stores the refresh token.
- **Upload contract (#115):** `POST /uploads` (overridable via the
component's `endpoint`/`uploadFile` props) sends `multipart/form-data`
with the file under `file` (plus optional text `fields`), bearer auth,
`credentials: 'include'`, and returns `{ id?, url?, ... }`. Progress and
cancellation use XHR because `fetch` exposes no upload-progress event.
Client-side type validation sniffs magic bytes (JPEG/PNG/GIF/WebP/PDF/
ZIP + text); the server must re-check. Assumed pending a real endpoint
in docforum-core.
- **Picker semantics (#114):** dates are plain `YYYY-MM-DD` calendar
strings, never instants. The optional `timeZone` (IANA) only decides
what "today" is and what is shown as a hint; formatting anchors at UTC
noon so a date never shifts. Times are `HH:mm` 24-hour strings.
- **Toast/optimistic rule (#113):** `useOptimisticMutation` and
`useDeferredAction` must never be used for money movements (payment
intent fund/release/refund) — those always show the server's result.
Toast messages complement, never replace, inline form errors. Undo
uses a delayed commit; if the component unmounts before the window
elapses, the optimistic change is reverted rather than left uncommitted.

## Roadmap / status
See `ROADMAP.md` in this repo, and Phase 7 in `docforum-core`'s
Expand Down
14 changes: 10 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@

**Status: Phase W1 built and connected to a live backend.** The deployed
Pages preview (https://docforum.github.io/docforum-web/) points at
`docforum-core`'s Render deployment; signup/login work end-to-end. One
gap remains — see `ARCHITECTURE_ESSENTIALS.md` "Known gaps against the
real API" (no refresh-on-reload flow).
`docforum-core`'s Render deployment; signup/login work end-to-end. The
original W1 gap — no refresh-on-reload flow — is closed behaviourally by
the W7 data-router migration (#112), whose guards await the single-flight
`POST /auth/refresh` restore before deciding; issue #9 stays open only
for a real-backend end-to-end check.

## Phase W1 — Shell & auth
- [x] Vite app wired up — real `main.tsx`/`App.tsx`, React Router, React
Expand Down Expand Up @@ -140,7 +142,7 @@ Admin-only screens beyond payments. Everything goes through `docforum-core`'s ad
- [ ] Doctor, facility and admin journeys with mocked API. Tracked as [issue #94](https://github.com/DocForum/docforum-web/issues/94). Queue start, consultation sign-off, fulfillment attribution, role guards and exact payment mutation counts are covered. Doctor order issuance UI has not been built yet.
- [x] Mock API for local development (MSW). Tracked as [issue #95](https://github.com/DocForum/docforum-web/issues/95) (High, 200 pts) — `npm run dev:mock` serves every endpoint the app uses through the normal service layer via `src/mocks/`: seeded obviously-fake fixtures, a dev-panel role switcher (patient/doctor/facility/admin), scenario toggles (empty states, forced errors), a latency slider, and a fixture reset. 26 handler tests drive the real `src/services/` layer against MSW; CI fails if mock code ever enters a production bundle (`npm run check:dist`). Documented in `CONTRIBUTING.md`.
- [ ] Generate MSW handlers and fixtures from the backend OpenAPI spec. Tracked as [issue #96](https://github.com/DocForum/docforum-web/issues/96) (High, 200 pts) — **blocked**: depends on [docforum-core#112](https://github.com/DocForum/docforum-core/issues/112) (OpenAPI 3.1 spec generated from the Zod schemas), which is still open — there is no authoritative `openapi.json` to generate from yet. Do not fabricate one; see that issue.
- [ ] Storybook component library with interaction and accessibility tests. Tracked as [issue #97](https://github.com/DocForum/docforum-web/issues/97) (High, 200 pts).
- [ ] Storybook component library with interaction and accessibility tests. Tracked as [issue #97](https://github.com/DocForum/docforum-web/issues/97) (High, 200 pts). CSF story files for `DatePicker`, `TimePicker` and `Upload` already exist; #97 is the wiring/config half.
- [ ] Performance budget: route-level code splitting, lazy GSAP, and Lighthouse CI. Tracked as [issue #99](https://github.com/DocForum/docforum-web/issues/99) (High, 200 pts).
- [x] React Query key factory and cache invalidation conventions. Tracked as [issue #122](https://github.com/DocForum/docforum-web/issues/122) (High, 200 pts) — `src/query-keys.ts` (typed factory per resource, profile-scoped keys where the API scopes by profile, one invalidation root each) and `src/invalidations.ts` (the mutation → keys map, refreshed through `invalidateFor`); every query and mutation hook migrated off hand-written key arrays. Payment actions now refresh the affected intent and the facility ledgers, a confirmed visit action refreshes the dashboard appointment list and queue (the stand-in for booking until #2/#3 land and consume `appointmentKeys.list(profileId)`), and health writes stay scoped to the acting profile's one section. New tests: `query-keys.test.ts`, `invalidations.test.ts`, plus a hook-level invalidation case in `useQueue.test.tsx`. Conventions documented in `ARCHITECTURE_ESSENTIALS.md` "Query keys & cache invalidation".
- [x] API client: request cancellation, de-duplication, timeouts and retry policy. Tracked as [issue #123](https://github.com/DocForum/docforum-web/issues/123) (High, 200 pts) — `src/services/api-client.ts` now owns the whole HTTP lifecycle: every request has a timeout (`DEFAULT_TIMEOUT_MS` 60 s, deliberately longer than a cold start so the `useSlowRequestHint` explanation isn't turned into a fake failure; slow endpoints — data-export create/download, admin job run — pass `SLOW_REQUEST_TIMEOUT_MS` 120 s) failing with a typed `ApiTimeoutError` that is distinct from `ApiError` and from an abort; identical concurrent GETs share one network request and one parsed result (keyed by URL, bearer token and timeout), with **independent cancellation per caller** — one caller walking away never fails the others, and the shared request is aborted only when its last caller does; mutations are never de-duplicated. Every service function used by a query takes an optional `AbortSignal` and every `queryFn` passes React Query's signal through, so navigating away actually stops the request; catch-all fallbacks rethrow aborts/timeouts instead of caching placeholder data (`useCareThreads`, `useFacilityLedger`). The shared `QueryClient`'s retry policy retries transient failures once and never retries a cancellation (`isAbortError`).
Expand All @@ -150,6 +152,10 @@ Admin-only screens beyond payments. Everything goes through `docforum-core`'s ad
- [x] Privacy-preserving product analytics (no PHI, no third-party tracking). Tracked as [issue #118](https://github.com/DocForum/docforum-web/issues/118) (High, 200 pts) — `src/utils/analytics.ts`: cookieless, self-hostable, disabled unless `VITE_ANALYTICS_ENDPOINT` is set. Explicit `ALLOWED_EVENTS` allowlist with no PHI. `scrubUrl` strips query strings and ids. DNT/GPC respected. `useAnalyticsPageview` hook. `/privacy` page. Env vars in `.env.example`.
- [x] ADR + migration: configurable router (BrowserRouter / HashRouter). Tracked as [issue #119](https://github.com/DocForum/docforum-web/issues/119) (High, 200 pts) — `docs/adr/0001-routing-strategy.md` compares hosting options. `main.tsx` selects `BrowserRouter` by default, `HashRouter` when `VITE_USE_HASH_ROUTER=true`. `deploy-pages.yml` sets that flag for GitHub Pages. `HashRedirectShim` converts old `#/path` links to clean paths transparently.
- [x] **PR conflict notifier** — `.github/workflows/pr-conflict-notifier.yml` + `.github/scripts/pr-conflict-notifier.cjs`. On every push to `main` (and every 3h) it labels open PRs that conflict with `main` as `needs-rebase` and @mentions the author once per push with rebase steps; the label clears itself when the conflict is resolved. Runs from `main` only and never checks out PR code.
- [x] Data routers with loaders and route-level error elements. Tracked as [issue #112](https://github.com/DocForum/docforum-web/issues/112) (High, 200 pts) — `src/router.tsx`, a configurable data router (`createBrowserRouter` by default, `createHashRouter` for GitHub Pages), every lazily imported route, loaders prefetching the same React Query keys/shapes the pages read, `RouteError` per section, `RouteLoading` as the hydration fallback, and no `RequireAuth` component left. Route guards await the single-flight `POST /auth/refresh` restore: signed-out deep links redirect to `/login`; wrong-role deep links redirect to the user's own dashboard.
- [x] Mutation feedback framework: toasts, optimistic updates and undo. Tracked as [issue #113](https://github.com/DocForum/docforum-web/issues/113) (High, 200 pts) — `src/store/toast-store.ts` + `Toast`/`Toaster` (live region, dismissible, auto-timeout paused on hover/focus), `useOptimisticMutation` (snapshot rollback, error toast, optional refetch) applied to message mark-read and invite revoke, and `useDeferredAction` (delayed commit + Undo toast + rollback on failure or unmount) applied to the doctor queue's start/complete. Payments stay free of optimistic updates by rule.
- [x] Accessible date and time pickers. Tracked as [issue #114](https://github.com/DocForum/docforum-web/issues/114) (High, 200 pts) — `DatePicker`/`TimePicker` following the WAI-ARIA date picker dialog and listbox patterns: keyboard grid and list navigation, typed entry (ISO and locale order / 12- or 24-hour), min/max and disabled dates, locale-aware formatting, explicit IANA time zone, 44px touch targets. Replaces every native date input (dependent date of birth, audit `from`/`to`, insights custom range).
- [x] Shared file upload component. Tracked as [issue #115](https://github.com/DocForum/docforum-web/issues/115) (High, 200 pts) — `Upload` (drag-and-drop plus button, mobile camera capture, per-file progress, cancel and retry, accessible announcements), client-side signature-based type sniffing plus size limits in `upload-utils.ts`, `downscaleImage` with a quality setting, and an XHR multipart uploader with credentials/progress/abort in `api-client.ts` (`apiUpload`). File contents stay in component state, never in a global store.
- [x] Notification preferences and quiet hours. Tracked as [issue #108](https://github.com/DocForum/docforum-web/issues/108) (High, 200 pts) — `src/features/notifications/`, `NotificationSettingsPage` at `/settings/notifications`. One toggle per notification type × channel (email/SMS/in-app), role-appropriate defaults, safety-critical lab/security rows locked on in both UI and mock API, and quiet hours with an explicit IANA zone. Optimistic saves roll back to the previous snapshot on failure; invalid same-start/end windows are refused before sending. **Built against an assumed shape:** docforum-core#99 hasn't landed. See `ARCHITECTURE_ESSENTIALS.md` "Known assumptions — Phase W7".
- [x] Shared SSE client for real-time updates. Tracked as [issue #109](https://github.com/DocForum/docforum-web/issues/109) (High, 200 pts) — `src/services/event-stream-service.ts`, `src/hooks/useServerEvents.ts`, `src/hooks/useRealtimeCacheSync.ts`. One fetch-based connection per tab (Authorization header + `Last-Event-ID`, which `EventSource` can't send), typed subscriptions, incremental parser, bounded exponential backoff, one auth-refresh retry on `401`/`auth.expired`, pause after 60 s hidden and reconnect on return, and React Query invalidation for notification/queue/message/admin events. The replay cursor is cleared when the account changes. **Built against an assumed shape:** docforum-core#97 hasn't landed. See `ARCHITECTURE_ESSENTIALS.md` "Known assumptions — Phase W7".
- [x] App-wide time-zone-correct date/time display. Tracked as [issue #110](https://github.com/DocForum/docforum-web/issues/110) (High, 200 pts) — `src/i18n/time-zone.ts`, `TimeZoneProvider`, `ZonedTime`/`DualZoneTime`, and a settings picker under Account settings. All date/time renderers now take an explicit IANA zone; storage is `localStorage['docforum-time-zone']`, with the browser zone as fallback. Tests cover DST gaps/overlaps and cross-midnight zone differences. `CareTimeline` shows the doctor's zone alongside the viewer's when they differ (from step metadata); the booking/appointment screens named in the issue don't exist in this repo yet, so dual-zone display could not be wired into them. No API dependency; see `ARCHITECTURE_ESSENTIALS.md` "Known assumptions — Phase W7".
Expand Down
21 changes: 21 additions & 0 deletions changelog.d/112-115-data-routers-feedback-pickers-upload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Phase W7: data routers, mutation feedback, accessible pickers and shared
file upload
([issue #112](https://github.com/DocForum/docforum-web/issues/112),
[#113](https://github.com/DocForum/docforum-web/issues/113),
[#114](https://github.com/DocForum/docforum-web/issues/114),
[#115](https://github.com/DocForum/docforum-web/issues/115)).

`src/router.tsx` replaces the component tree and `RequireAuth` with a
configurable data router (`createBrowserRouter` by default, `createHashRouter`
when `VITE_USE_HASH_ROUTER=true`), lazy route modules, loaders that prefetch
the exact React Query keys/shapes pages read, per-section `RouteError`, and
guards that await the single-flight `POST /auth/refresh` restore. Toasts,
`useOptimisticMutation` and `useDeferredAction` now back message mark-read,
invite revoke and queue start/complete (payments excluded). `DatePicker` and
`TimePicker` replace every native date input with keyboard/screen-reader
support, typed entry, min/max, disabled dates and explicit time zones.
`Upload` adds drag-and-drop, mobile camera capture, progress/cancel/retry,
signature-based type sniffing, size limits and image downscaling through a new
`apiUploadFile` XHR helper. CSF story files for the three components are in
place ahead of Storybook (#97); new assumed shapes (refresh, upload) are
recorded in `ARCHITECTURE_ESSENTIALS.md`.
14 changes: 14 additions & 0 deletions changelog.d/161-merge-upstream-main.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
PR #161: merged `upstream/main` again after the W7 platform-quality and
API-client/PWA/i18n work landed there (#151, #157), and resolved the
conflicts without dropping either side.

Kept this branch's data router, `DatePicker`/`TimePicker`, `Upload` and the
toast-backed mutation feedback, and adopted upstream's pieces on top of them:
`I18nProvider` plus the app-level `ErrorBoundary` and `UpdatePromptContainer`
around `RouterProvider`, `IntlProvider` in the test render helpers, the
queue's locale-aware labels and live-region announcements, and the
`@docforum/api-client` transport — `apiUploadFile` is now the only addition
on top of it. The queue keeps the shared toast undo rather than the inline
undo bar, the patient hub keeps its own route file with upstream's
translations, and the custom date range keeps upstream's labelled fieldset
around the branch's pickers.
Loading
Loading