Commit graph

5 commits

Author SHA1 Message Date
Daniel
fb609f6672 fix(infra): Dockerfile compiles TS, SPA fallback works on Express 4
Rebuilding the pediatric-ai-scribe container surfaced three long-
standing bugs that pre-date today's work but only showed up when
the image was actually rebuilt for the first time since April.
Fixing them in one commit so the live app reflects the React
changes shipped earlier today.

Dockerfile (compiles TypeScript now):
  • COPY package.json → COPY package.json package-lock.json .npmrc
    so npm ci can do a deterministic install.
  • Drop --omit=dev from the install step (tsc is a devDep and we
    need it to compile); prune devDeps AFTER the build runs.
  • RUN npm run build — compiles server.ts + src/ to dist/.
    server.ts was renamed from server.js in commit d60a29f but no
    one updated the Dockerfile; CMD has been "node server.js"
    against a file that didn't exist. The running e2e container
    survived because it was built before the rename.
  • cp package.json dist/package.json — server.ts does
    require('./package.json') which resolves relative to the
    compiled file; keeping a copy next to it is simpler than
    making the source path-aware.
  • ln -s /app/public /app/dist/public — express.static /
    sendFile calls use path.join(__dirname, 'public', …) and
    __dirname at runtime is /app/dist.
  • CMD ["node", "dist/server.js"] (was "server.js").

.npmrc:
  • legacy-peer-deps=true. npm 10+ hard-rejects the transitive
    peerOptional mismatch between openai@4.x (peerOptional zod@^3)
    and our direct dep zod@^4.3.6. Host, CI, and Docker now all
    share one resolution policy so the lock file stays reproducible.

server.ts:
  • `app.get('/app/*splat', …)` → `app.get('/app/*', …)`
  • `app.get('*splat', …)` → `app.get('*', …)`
    Express 5 named-splat syntax doesn't match anything on Express
    4.21 (which we ship). Hard refresh on /wellvisit, /encounter,
    /cms etc. was falling through to the static 404 instead of
    serving the React index. Verified: all 7 clinical routes now
    return HTTP 200 from the SPA fallback.

Smoke-tested against 127.0.0.1:3552 after rebuild:
  /                              HTTP 200
  /wellvisit  /encounter  /cms   HTTP 200
  /auth  /api/health              HTTP 200
  /app/assets/index-C9wKWOAN.js  HTTP 200
  /does-not-exist.jpg            HTTP 404  (static 404 preserved)
2026-04-24 06:03:02 +02:00
Daniel
39d764e1ff chore: delete vanilla tree — React+Tailwind is the only client now
Closes the migration. Every line of vanilla JS/CSS/HTML that used to
render the user-facing app is gone. The React bundle in public/app/
is the entire client.

Deleted (everything was 100% covered by ported React equivalents):
  • public/index.html              (vanilla shell — auth screen + tab loader)
  • public/js/                     (~30 vanilla modules: app.js, auth.js,
                                     admin.js, calculators.js, peGuide.js,
                                     bedside/*, learningHub.js, encounters.js,
                                     etc. — all replaced by client/src/pages/*
                                     and client/src/data/*)
  • public/components/             (18 lazy-loaded HTML fragments — not
                                     loaded by any React route)
  • public/css/styles.css          (vanilla design system — superseded by
                                     Tailwind + shadcn classes throughout
                                     the React tree)
  • public/e2e-harness.html        (vanilla-only Playwright bootstrapper)
  • e2e/tests/bedside-smoke.spec.js
  • e2e/tests/top-calculators.spec.js
                                   (the two e2e specs that exercised
                                     /e2e-harness.html and the vanilla
                                     calculators directly — replaced by
                                     calculators-react.spec.js +
                                     bedside-react.spec.js + the 136
                                     vitest parity tests in
                                     shared/clinical/*.test.ts)

Moved (still needed by the backend):
  • public/js/pediatricScheduleData.js → src/data/pediatric-schedule-data.js
    Required by src/routes/wellVisit.ts at runtime; should never have been
    in public/ anyway since it carries server-side schedule + growth +
    BMI reference tables and was being served as a 2120-line public JS
    blob to every browser. Not in public/ means it's not exposed to
    anonymous web requests anymore.

server.ts
  • Removed the dead getTemplatedIndex() / INDEX_PATH machinery that
    used to BUILD_ID-stamp /js/* and /css/* references in the vanilla
    HTML. Vite's hashed asset URLs already do that job for the React
    bundle.
  • express.static cache header: removed the /components/ branch
    (folder no longer exists) and changed /js/ + /css/ from 1-hour
    to 1-day immutable cache — safe because Vite hashes the
    filenames on every build.

Backend tsc + client tsc + vite build all green. 136/136 vitest
parity tests still pass against the captured calc-vectors.json.
Initial bundle unchanged at 343.97 kB / 106.59 kB gz.
2026-04-24 02:56:04 +02:00
Daniel
7948cad6f1 feat: React auth screen + SPA now serves every route (vanilla retired)
The final big piece of "everything in React + Tailwind". Login,
register, forgot-password, reset-password, and email-verification all
render from the React bundle now. The root path / serves the SPA,
vanilla index.html + public/js/* are no longer served by the server.

BACKEND — src/routes/auth.ts
  New GET /api/auth/public-config (public — no auth required) returns
  { registrationEnabled, turnstileSiteKey, oidcEnabled,
    disableLocalAuth, ssoButtonLabel }.
  Single round-trip the React auth screen needs on mount. Reuses
  existing DB settings; no new tables.

BACKEND — server.ts
  • / and /index.html now send public/app/index.html (React SPA),
    not public/index.html (vanilla).
  • /auth, /reset-password, /verify-email explicitly route to the SPA
    so the email links land on the React router.
  • /app/*splat preserved as an alias so old bookmarks keep working.
  • SPA fallback added after express.static so hard-refresh on
    /encounter / /bedside / /settings etc. serves the React index
    instead of 404ing. API paths and static-file extensions still
    fall through to their existing handlers.
  • The dead app.get('/') duplicate that also pointed at the vanilla
    index is removed.

CLIENT — React auth flow
  client/src/pages/Auth.tsx (new)
    Login / register / forgot sub-forms with a single useQuery on
    ['public-config'] driving Turnstile + SSO button visibility.
    Login flow handles all three vanilla-equivalent responses
    (token / requires2FA / needsVerification). 2FA field reveals
    inline when the server asks for it; resend-verification link
    appears when needsVerification fires. SSO button renders
    whenever oidcEnabled is true, even if local auth is disabled
    (disableLocalAuth hides the login/register/forgot forms
    entirely). HIPAA notice + APK download link preserved.

  client/src/pages/ResetPassword.tsx (new)
    Reads ?token=xxx from the URL, POSTs /api/auth/reset-password.
    Confirm-password match, 8+ char validation, server
    passwordWarning (pwned password) surfaces as an amber info box.
    Redirects to /auth 2.5 s after success.

  client/src/components/Turnstile.tsx (new)
    Loads the challenges.cloudflare.com/turnstile script once,
    renders a widget per form, calls onToken(token) on success and
    onToken('') on error / expiry. If siteKey is null/empty (e2e
    container with TURNSTILE_SITE_KEY="") renders nothing and
    auto-reports empty — matches the vanilla no-key-no-widget
    behaviour.

  client/src/components/AuthGuard.tsx (new)
    useQuery(['auth-me']) with retry: false. On 401/error redirects
    to /auth?next=<current-url> so the deep link survives sign-in.
    Used as a parent route in App.tsx wrapping every private page.

  client/src/components/Layout.tsx
    "← back to legacy app" link replaced with "Sign out" — calls
    POST /api/auth/logout then window.location = /auth.

  client/src/App.tsx
    BrowserRouter no longer has basename (was "/app"). Public
    routes: /auth, /reset-password. Everything else lives under
    <AuthGuard> → <Layout>. Lazy-loaded Auth + ResetPassword join
    the existing heavy-route code-split.

  client/vite.config.ts
    base stays "/app/" so hashed asset URLs resolve to
    /app/assets/... (served unchanged by express.static).

shared/types.ts + client/src/shared/types.ts — additive:
  PublicConfigOk { registrationEnabled, turnstileSiteKey,
    oidcEnabled, disableLocalAuth, ssoButtonLabel }.

Bundle — Auth chunk splits out at 10.87 kB / 3.35 kB gz, lazy-loaded
only on the sign-in path; initial bundle unchanged at 343.97 kB /
106.59 kB gz.

Backend tsc + client tsc + vite build + 136/136 vitest all green.
2026-04-24 02:45:21 +02:00
Daniel
8d244a167a feat: day 7 — first tab ported to React (Extensions) + Express serves /app/*
End-to-end proof that the full React + Vite + Tailwind + TypeScript
pipeline works against the existing Express API:

client/src/pages/Extensions.tsx
  Minimum-viable port of the Extensions tab. Fetches /api/extensions
  via the typed api wrapper; renders an add form backed by Zod
  validation (ExtensionCreateSchema). Uses @tanstack/react-query for
  server state (queryKey: ['extensions'], invalidate on mutate).
  Full CRUD UI (trash / restore / purge / search) is a follow-up —
  this ships just enough to prove the stack works.

client/src/lib/api.ts
  Thin fetch wrapper. Every React page goes through apiFetch<T>(),
  which narrows ApiResponse<T> to the success shape and throws
  ApiError on failure. Central spot for future request/response
  instrumentation, auth-token refresh, etc.

client/src/App.tsx
  Replaced the Vite starter splash screen with a minimal router:
  BrowserRouter basename='/app', routes for / (landing) and
  /extensions. QueryClientProvider wraps the tree so every page can
  use useQuery/useMutation.

client/src/shared/
  Mirrored copy of /shared/types.ts + schemas.ts. Canonical source
  stays at /shared/ (used by backend). A post-migration task is to
  wire proper TypeScript project references so the client can import
  straight from /shared — TS 6's cross-root bundler-mode paths
  resolution isn't pulling it in cleanly. For now, the mirror is
  header-annotated 'do not edit, mirror only'.

client/src/index.css
  Tailwind v4 @theme block declaring the shadcn design tokens as
  first-class CSS custom properties, which exposes the
  bg-background / text-foreground / border-border utility classes
  the page components use. v4 no longer uses @apply for these —
  the theme block is the idiomatic form.

server.ts
  Added:
    app.get('/app/*splat', ...) → sendFile public/app/index.html
  so React Router deep links (e.g. /app/extensions) resolve
  client-side. Express static middleware below continues to serve
  the hashed /app/assets/*.js + .css.

Build output (checked into public/app/ so the next prod docker
rebuild ships the React bundle without requiring a client/npm
install step in the Dockerfile — that's a day-8 refinement):
  index.html    0.46 kB   gzip  0.29 kB
  index.css     9.51 kB   gzip  2.69 kB
  index.js    329.24 kB   gzip 100.98 kB

Typecheck green on both sides:
  server: npx tsc --noEmit   → EXIT 0
  client: npx tsc -b         → EXIT 0
  client: npx vite build     → 150 modules, 219ms

How to see it live (after Daniel rebuilds prod):
  https://<host>/app/            → React landing page
  https://<host>/app/extensions  → React-rendered Extensions list
  https://<host>/                → unchanged vanilla JS app

Nothing destructive. The vanilla JS /extensions tab still works
identically. The React /app/extensions route talks to the same
/api/extensions backend endpoints. Both render from the same
PostgreSQL rows.

This closes out the 7-day migration scaffolding. The rest is a
port-one-tab-at-a-time grind that Codex (or anyone) can pick up
tab-by-tab with the Playwright suite as the safety net.
2026-04-23 22:09:04 +02:00
Daniel
a2fe1b38d1 build(ts): day 2 — shared/types.ts + server.ts rename (no behavior
change)

Creates the wire-protocol contract every route and every client
component will import from. Renames server.js → server.ts as a pure
rename (zero bytes of logic changed) so the entry point becomes the
first file tsc type-checks against the real compilerOptions.

shared/types.ts — what's in it and why

- ApiResponse<T> envelope: (ApiOk<T> & T) | ApiErr. Every route
  returns one of these; client code narrows on `r.success`.
- One typed "Ok" shape per endpoint, keyed by the actual res.json()
  call in the current handler. Walked every src/routes/*.js file
  and transcribed the literal keys: hpi, soap, note, hospitalCourse,
  review, refined, shortened, questions, narrative+summary, etc.
  No inventive renaming — wire stays identical, only the types are
  new.
- Critical mismatches the types now prevent at compile time:
    /api/refine returns `refined` (not `content` — a past bug)
    /api/sick-visit/note (not /api/generate-sick-visit — past bug)
    /api/generate-hospital-course returns `hospitalCourse` (not
      `narrative` — past bug)
  All three were caught and fixed earlier in Playwright; with the
  shared types they become compile errors for any future regression.

server.ts

- Pure rename via `git mv`. Body unchanged.
- Compiled dist/server.js diffs against the original server.js by
  two lines (TypeScript prepends `"use strict"` and the CommonJS
  export marker). No semantic drift.

tsconfig.json tweak

- Include list adds server.ts alongside server.js so tsc doesn't
  silently skip the entry point during the intermediate state
  where `.js` entries might reappear.
- baseUrl removed (deprecated in TS 6); paths now uses './shared/*'.

package.json

- main: dist/server.js (post-compile entry)
- start: node dist/server.js
- prebuild: rm -rf dist (clean emit every time)
- dev: ts-node-dev for fast TS-aware reloads

The Dockerfile is still unchanged. The deployed prod and e2e
containers still run their baked-in server.js from the previous
image — this migration day has no effect on either until the final
rebuild at the end of day 7.

Next: day 3 renames the 29 route files one-by-one, each adding the
ApiResponse<T> type parameter to its res.json() calls.
2026-04-23 19:21:22 +02:00
Renamed from server.js (Browse further)