Commit Graph
44 Commits
Author SHA1 Message Date
tiennm99 ca8739dc01 feat(ux): doctor ergonomics — call-next, live-queue indicator, guards
Implements the deferred NEXT items from the clinician UX review, which
targeted the doctor's core loop:

- call_next_patient RPC (advisory-locked so two staff can't double-call) with
  a prominent per-shift button and an Alt+N shortcut that lands straight on
  the checkup screen
- realtime queue indicator: live/disconnected as colour + icon + text plus a
  last-updated time, so a frozen queue is never trusted silently
- unsaved-changes guard on the checkup form (beforeunload + in-app marker)
- diagnosis quick-pick sourced from recent diagnoses, and Vietnamese dose
  presets on every prescription row, so the doctor types almost nothing

typecheck/lint/build green; 97 unit tests still passing.
2026-07-25 15:27:44 +07:00
tiennm99 345238b20b docs: README reflects feature-complete state, tests, and runtime-verification caveat 2026-07-25 15:18:21 +07:00
tiennm99 5ddf11ac4a test: add Vitest unit + Playwright E2E suites (PLAN §7)
The repo had zero tests despite PLAN §7 mandating them.

- 97 unit tests over the pure logic: checkup/customer/catalog/template Zod
  schemas, parseNum, computeAge, and invoice math
- extracted lib/billing/totals.ts (sumLineTotals/formatVnd) so invoice math is
  testable, and refactored the invoice route + dashboard to use it
- 16 Playwright smoke tests that need no database: auth gates redirect to
  sign-in, VI-default rendering, password-reveal toggle, /en locale, 404
- playwright.config passes placeholder env inline via webServer.env so no
  env file is ever needed; pnpm test wired into CI (E2E stays local)

tests/e2e/README.md documents the seeded-data prerequisites for the full
queue -> checkup -> prescription -> paid -> invoice happy path, which is
blocked on a provisioned Supabase project rather than faked with skipped tests.
2026-07-25 15:16:48 +07:00
tiennm99 d91515c36e feat(queue): manual counter control + apply checkup templates
Closes the last two original-app feature gaps:

- set_queue_counter RPC (admin/receptionist, advisory-locked, VN-local day)
  plus per-shift counter display for all staff and a set/reset form for
  admin/receptionist (orig SetCounterRequest/GetCounterRequest)
- checkup templates are now actually applied: pick a template on the checkup
  screen (filtered to the patient's gender), its fields render as inputs and
  persist to the new checkups.template_values jsonb; the ultrasound report PDF
  already reads the template title

daily_queue_counters gains a SELECT policy (reads only) — writes stay inside
the DEFINER RPCs.
2026-07-25 15:05:20 +07:00
tiennm99 86046ace4f feat(reports): prescription & ultrasound PDFs, wider Excel exports, print CSS
Restores the original's full print/report surface (it had three JasperReports
templates; only the invoice existed):

- prescription PDF: dosage-prominent, no prices, doctor + signature line
- ultrasound/imaging report PDF: template title, diagnosis/conclusion, up to
  4 embedded images (downloaded server-side via signed URL)
- Excel exports added for patient roster, medicine+service catalog (2 sheets),
  and monthly paid-revenue with a grand-total row
- browser print CSS (@page A4, chrome hidden via data-print-hidden)

All PDFs use the bundled Be Vietnam Pro family so diacritics render.
2026-07-25 14:57:23 +07:00
tiennm99 2521374c66 feat(patients): patient history, recently-seen, checkup soft-delete
Closes three original-app gaps found by the implemented-vs-original audit:

- patient detail page with checkup history (orig GetPatientHistoryRequest /
  HistoryViewDialog): resolved address, status badges, doctor, diagnosis;
  patient names in the list now link to it
- "recently seen" patients (orig GetRecentPatientRequest), de-duped by most
  recent visit
- checkup soft-delete with confirm (orig DeleteCheckupRequest), audit-logged

Also files the gap-audit report under plans/.
2026-07-25 14:45:11 +07:00
tiennm99 7f0bf31f5c chore(deps): resolve 16 of 17 dependency advisories
- next 16.2.6 -> 16.2.11 (4 advisories)
- xlsx: npm's newest (0.18.5) is unpatched; switch to SheetJS's official
  0.20.3 distribution, which the advisories require (same API, no code change)
- override floors for postcss, js-yaml, esbuild, @babel/core, sharp
- brace-expansion deliberately NOT overridden: its 1.x line (minimatch 3.x
  inside eslint) has no patched release and forcing 5.x crashes eslint's
  config-array. Residual advisory is dev-tooling only, no runtime exposure.

pnpm audit: 17 -> 1. typecheck/lint/build green on Next 16.2.11.
2026-07-25 14:38:21 +07:00
tiennm99 f22ab66b00 Merge: BSK clinic system — phases 0–8 + hardening + review fixes
Educational Next.js 16 rewrite of the original Java/Swing BSK clinic app,
built to source-grounded feature parity across all planned phases.

- Auth + RBAC: allowlist-gated admin bootstrap, rate limiting, audit log
- Master data: doctors, medicines, services, checkup templates, clinic
  settings, staff management (race-safe last-admin guard)
- Patients: VN geo lookup + accent-insensitive search
- Queue + checkup workflow with Supabase Realtime; atomic queue numbers
- Prescriptions + billing: server-authoritative VND totals, paid-invoice lock,
  cashier mark-paid
- Imaging: webcam/file capture, downscale+compress to <=200KB, signed URLs,
  barcode; nightly retention sweep
- Reports: Vietnamese PDF invoice, Excel export, revenue dashboard
- Recheck reminders (daily cron + in-app list)
- Polish: error boundary, loading state, README + operator setup

All commits typecheck/lint/next-build green. Infra-dependent paths (realtime,
storage, PDF/Excel with live data, SQL RPCs, cron) are compile-verified and
need a live Supabase project to smoke-test.
2026-07-25 14:09:52 +07:00
tiennm99 7dc0bb556c fix: address code-review findings
- imaging: downscale to 1280px before JPEG quality-stepping so real camera
  photos fit the 200KB cap (was quality-only → rejected typical photos)
- billing: save_prescription/save_checkup_services refuse to modify a PAID
  invoice, so recorded payment can't diverge from the invoice total
- staff: set_staff_role/remove_staff RPCs hold an advisory lock while enforcing
  the last-admin invariant — race-safe vs the prior check-then-act
- imaging delete: remove the object at the row's stored path (DB lookup), not a
  client-supplied path
- invoice PDF: filter deleted=false like every other checkup view
- sign-in: skip rate limit when no client IP is resolvable instead of bucketing
  all requests under a shared "unknown" key
- reminders: bound to a [today-30d, today+7d] window so stale overdue rows
  don't accumulate and bury upcoming ones
2026-07-25 13:44:02 +07:00
tiennm99 299b27417a feat(polish): error boundary, loading state, README (Phase 8)
- locale-subtree error boundary with retry (no error details leaked)
- app-subtree loading spinner (reduced-motion aware)
- README: built-feature overview + first-run operator setup steps
2026-07-25 13:25:41 +07:00
tiennm99 9f6eb24fb4 feat(reminders): recheck reminders + nightly media sweep (Phase 7)
- in-app reminders page: patients with recheck_date due/overdue or within 7
  days (clinical-gated, VN-local); sidebar nav + vi/en
- nightly Vercel Cron (/api/cron/nightly, CRON_SECRET-gated) enforces the
  7-day image retention window: removes stale Storage objects + soft-deletes
  rows, keeping the free-tier storage budget bounded
- CRON_SECRET added to env schema + .env.example; vercel.json cron 01:00 VN
2026-07-25 13:23:24 +07:00
tiennm99 f4840b8000 feat(reports): Excel export + revenue dashboard (Phase 6)
- visits Excel export route (admin/cashier, Node runtime, xlsx): month range,
  per-visit totals + payment status
- dashboard: today waiting/completed tiles + 7-day paid-revenue chart
  (recharts, admin/cashier only); VN-local dates, server-summed VND
- admin reports page (month -> Excel); sidebar nav; vi/en
2026-07-25 13:19:54 +07:00
tiennm99 540f3e0fa6 feat(reports): Vietnamese-capable PDF invoice (Phase 6 slice)
- react-pdf invoice route (Node runtime) — clinic header, medicines + services
  tables, server-summed VND total, payment status
- bundled Be Vietnam Pro (Regular+Bold) so diacritics render; fonts registered
  once per process
- "Print invoice" link on the prescription page (locale-prefix aware)
- reports i18n namespace (vi/en); xlsx + recharts deps added for next slices
2026-07-25 13:15:24 +07:00
tiennm99 e718547332 feat(imaging): checkup media capture, gallery & barcode (Phase 5)
- private bsk-checkup-media bucket + checkup_images table; RLS: enrolled read,
  clinical write; storage.objects policies scoped to the bucket
- webcam (getUserMedia) + file capture, client canvas JPEG compression to
  <=200KB; upload via browser client, metadata recorded server-side with a
  path-ownership guard; audit-logged
- gallery with 1h signed URLs + soft-delete; code128 barcode of the checkup id
  (no PII); linked from the checkup page; vi/en; bwip-js added
2026-07-25 12:36:49 +07:00
tiennm99 354f5fd4a7 feat(billing): prescriptions & invoicing (Phase 4b)
- order_items + checkup_services + medicine_orders (payment per checkup);
  integer-VND, server-authoritative totals
- save_prescription / save_checkup_services RPCs snapshot catalog prices and
  compute line_total server-side (clinical-gated, SECURITY DEFINER)
- mark_order_paid RPC gated to admin/cashier; clinical roles cannot flip payment
- prescription composer: dynamic medicine + service rows, live total, save;
  payment status + mark-paid; linked from the checkup page; vi/en
2026-07-25 12:19:59 +07:00
tiennm99 835b5237ab feat(catalog): medicine & service catalogs (Phase 4 slice)
- bsk.medicines + bsk.services; integer-VND money (no floats); RLS enrolled
  read, admin write; soft-delete
- medicines: list + new/edit form (name, unit, sale/cost price, company, route)
- services: inline list + add form (name, price)
- admin-gated, audit-logged; sidebar nav + vi/en
2026-07-25 12:03:04 +07:00
tiennm99 1dc1e946a4 feat(queue): queue & checkup workflow with realtime (Phase 3)
- shifts + daily_queue_counters + checkups (status enum, vitals, diagnosis,
  conclusion, recheck); VN-local checkup_date so the daily queue rolls over
  correctly
- register_checkup RPC assigns queue numbers atomically (concurrency-safe);
  role-gated to clinical staff
- queue page: register form + today's list by shift + call-patient, with
  Supabase Realtime live refresh (RLS-authorized, bsk:queue channel)
- checkup form: single keyboard-tabbable screen (vitals -> diagnosis ->
  conclusion -> recheck -> status), audit-logged
- clinical-role route gates; nav + vi/en; Phase 3-8 roadmap under plans/
2026-07-25 11:56:02 +07:00
tiennm99 481259c08e feat(staff): staff management — roles + access removal (Phase 2 slice)
- admin staff list (email + role) over bsk.app_users; invite stays the
  create path
- change role / remove access via admin-client Server Actions (audit-logged)
- guards: no self role-change or self-removal; the last admin cannot be
  demoted or removed
- sidebar nav + vi/en; completes Phase 2 core entities
2026-07-25 11:45:23 +07:00
tiennm99 46d5ff9bd1 feat(templates): checkup template management (Phase 2 slice)
- bsk.checkup_templates (gender + jsonb field layout, soft-delete); RLS:
  enrolled read, admin write
- create/edit/deactivate Server Actions (admin-gated, audit-logged); field
  layout edited one-label-per-line, serialized to jsonb
- admin templates list + new + edit pages; sidebar nav; vi/en
2026-07-25 11:41:55 +07:00
tiennm99 4273cba2bb feat(patients): patient management with VN geo + accent search (Phase 2 slice)
- bsk.customers (soft-delete) + provinces/wards geo tables; RLS: enrolled
  read, clinical-role write; geo read-only
- accent-insensitive search via unaccent wrapper + search_customers RPC
  (SECURITY INVOKER so RLS applies); name/phone contains
- patients list+search, create, edit; shared form with cascading
  province -> ward dropdown; clinical-role route gate; nav + vi/en
- audit-logged mutations; seed-geo script (db:seed-geo) for the geo dataset
2026-07-25 11:33:40 +07:00
tiennm99 b9ff387a6d feat(settings): clinic information management (Phase 2 slice)
- bsk.clinic_settings singleton (boolean PK pinned true) with name,
  address, phone, barcode prefix; seeded blank row
- RLS: enrolled staff read, admin write; user-client upsert so RLS gates
- admin settings page + form; audit-logged; sidebar nav entry; vi/en
2026-07-25 02:30:42 +07:00
tiennm99 e2fb601b2a feat(doctors): admin doctor management (Phase 2 slice)
- bsk.doctors table (soft-delete) with RLS: reads for enrolled staff,
  writes admin-only; user-client writes so RLS is the enforcement point
- add / edit / deactivate Server Actions — admin-gated, Zod-validated,
  audit-logged via log_audit, revalidate the list
- doctors admin page (RSC list + inline edit + deactivate) and add form
- sidebar nav entry + vi/en strings

Establishes the Phase 2 CRUD pattern (RLS gate + defense-in-depth role
check + Zod + audit + revalidate) for the remaining core entities.
2026-07-25 01:57:01 +07:00
tiennm99 695a9a6bec docs: design guidelines, source-grounded parity, hardening plan + reports
- design-guidelines.md: tokens, density, focus/keyboard, status, i18n rules
- PLAN.md: name granular original features per phase from a source audit
  (geo-lookup, gender templates, accent search, queue counter, batch sync);
  record chat + emergency alerts as non-goals
- supabase-shared-config.md: admin allowlist seeding + manual-enroll note
- plans/: hardening+UX plan and agent review reports
2026-07-25 01:51:50 +07:00
tiennm99 653aca1d1b feat(ui): clinician-focused UX pass
- responsive app shell: persistent sidebar at md+, accessible off-canvas
  drawer (focus trap, Esc, dialog semantics) + hamburger below md
- active-route highlight, localized role labels, greet by name
- visible focus rings, AA-contrast muted text, 44px primary actions
- sign-in: autofocus, show/hide password, always-enabled submit
- Be Vietnam Pro via next/font; prefers-reduced-motion guard; skip-link
2026-07-25 01:51:50 +07:00
tiennm99 164c6125c2 feat(auth): allowlist-gated admin bootstrap, rate limiting, audit log
- claim_first_admin: no-arg, inserts auth.uid(), gated on an email allowlist
  table so an arbitrary shared-pool principal can no longer claim admin
- revoke direct writes on app_users from authenticated (least privilege)
- rate-limit sign-in (by platform IP) and invite (by admin id); fail open
  on Redis outage so an outage cannot lock staff out
- audit_log table + SECURITY DEFINER log_audit writer, admin-only reads
- invite: map existing-email to a clear error, roll back orphaned auth row
- session: read role + full_name in one own-row query
2026-07-25 01:51:35 +07:00
tiennm99 1d38c262af chore: ignore local Claude Code settings 2026-07-25 01:51:35 +07:00
tiennm99 95a2d50846 fix: pin patched postcss resolution 2026-05-27 23:28:00 +07:00
tiennm99 d578e71266 ci: use package manager pnpm version 2026-05-27 23:19:44 +07:00
tiennm99 8b337cd27d docs: archive agent review reports
Preserves the multi-agent review trail produced across the session:
- code-reviewer-phase0-scaffold + code-reviewer-phase0-fixes
- brainstormer-architecture-redteam + brainstormer-fixes-closure-check
- researcher-plan-vs-impl-alignment
- planner-phase-1
- code-reviewer-phase-1-full

Useful as context for future phases (decisions, accepted residual
risks, original rationale).
2026-05-25 18:05:57 +07:00
tiennm99 9afd68a741 feat(phase-1): role-gated app shell + dashboard placeholder
- app/[locale]/(app)/layout.tsx: Server gate — getServerSession()
  redirects unauth users (defense in depth) and signs out + redirects
  the authed-but-no-role edge case. Renders AppShell with user/role/locale.
- app/[locale]/(app)/admin/layout.tsx: second gate via requireRole;
  non-admin → /[locale]/dashboard (not 404 — avoids confirming routes).
- app/[locale]/(app)/dashboard/page.tsx: placeholder showing email +
  role badge.
- lib/auth/role-menu.ts: ROLE_MENU mapping per AppRole → MenuItem[]
  with href + i18n labelKey + lucide icon.
- lib/auth/require-role.ts: server helper for the admin gate.
- components/app-shell/{app-shell,sidebar,sign-out-button,locale-switcher}.tsx:
  Server-rendered shell + sidebar that reads ROLE_MENU[role]; client
  locale switcher (native <select> fallback) and sign-out (<form action>
  with useFormStatus pending UX).
- components/ui/{badge,separator}.tsx: shadcn primitives.
- messages/{vi,en}.json: nav.*, app.*, dashboard.* keys (parity).

Gating chain: proxy.ts redirects unauth → /sign-in for /dashboard +
/admin prefixes; (app) layout enforces session+role; (app)/admin layout
additionally enforces role === 'admin'.
2026-05-25 17:53:44 +07:00
tiennm99 eb1af9013e feat(phase-1): admin enrollment (first-admin claim + invite flow)
- supabase/migrations/20260525163400_bsk_admin.sql:
  bsk.claim_first_admin(uuid) -> boolean, VOLATILE SECURITY DEFINER.
  Advisory lock keyed by hashtext('bsk:claim_first_admin')::bigint
  serializes concurrent first-sign-ins; EXISTS-guarded INSERT means
  only the first caller wins.
- types/supabase-bsk.ts: added claim_first_admin to bsk.Functions.
- lib/auth/invite-schema.ts: InviteUserSchema (Zod v4: email + role
  enum derived from appRoles) + InviteUserState discriminated union.
- app/[locale]/(app)/admin/invite/{actions,page,form}.tsx: admin-only
  invite Server Action + page + RHF/useActionState client form.
  Caller-role check via getServerSession() (defense in depth; the
  (app)/admin layout in phase 06 will gate at the route level).
  Insert uses createSupabaseAdminClient() because app_users has no
  INSERT RLS policy by design.
- app/[locale]/(auth)/sign-in/actions.ts: extended enrollment-check
  branch — when no row AND count == 0, calls claim_first_admin RPC.
  On true, re-fetches enrollment row and proceeds; on false (race
  lost) or count > 0, falls through to existing sign-out + generic
  error (enumeration defense preserved).
- messages/{vi,en}.json: admin.invite.* keys (parity).
- docs/runbooks/first-admin-setup.md: happy path + manual psql
  fallback bootstrap procedure.

No audit_log refs — trimmed plan respected.
2026-05-25 17:47:47 +07:00
tiennm99 8265942101 feat(phase-1): sign-in page + form (RHF + useActionState + shadcn)
- app/[locale]/(auth)/layout.tsx: centered card layout for unauth routes
- app/[locale]/(auth)/sign-in/page.tsx: Server Component, redirects
  authed users to dashboard, renders SignInForm
- app/[locale]/(auth)/sign-in/sign-in-form.tsx: Client form with RHF
  (zodResolver, mode onBlur) + useActionState(signInAction). Form root
  is <form action={dispatchAction}>. Server fieldErrors sync into RHF
  via useEffect for consistent inline UX. aria-invalid/aria-describedby
  set for accessibility.
- app/[locale]/layout.tsx: mounts <Toaster richColors position="top-right" />
  inside NextIntlClientProvider
- components/ui/{button,input,label,sonner}.tsx: shadcn primitives
  installed via shadcn CLI v4 (Tailwind v4 CSS-first)
- messages/{vi,en}.json: title + subtitle keys under auth.signIn

Scope honored: no next= plumbing, no rate-limit i18n keys (both cut from
phase 03), no unenrolledError key (action uses invalidCredentials for
both wrong-password and unenrolled paths per enumeration defense).
2026-05-25 17:42:13 +07:00
tiennm99 e514d710c4 feat(phase-1): sign-in/sign-out server actions + zod schema
- lib/auth/schemas.ts: SignInSchema (Zod v4, email + password 8-72),
  SignInState discriminated union (idle | error), parseSignIn helper.
  Framework-agnostic — no 'use server' so client RHF resolver can import.
- app/[locale]/(auth)/sign-in/actions.ts: signInAction (RHF/useActionState
  shape) + signOutAction. Flow: safeParse → signInWithPassword → enrollment
  check via bsk.app_users → redirect to /[locale]/dashboard. Wrong-password
  and unenrolled paths return the SAME generic error (auth.users
  enumeration defense). redirect() called outside try/catch per
  React 19 / Next 16 semantics.
- messages/{vi,en}.json: auth.signIn.* keys in parity.

Scope honored: no rate limit, no next= guard, no IP extraction.
2026-05-25 17:36:09 +07:00
tiennm99 129cbb7bf0 feat(phase-1): auth session wiring (proxy + layout + helpers)
- proxy.ts: composes Supabase session refresh + next-intl middleware
  into a single NextResponse via copyCookies helper. Coarse auth gate
  on /dashboard + /admin prefixes redirects unauth users to
  /[locale]/sign-in (no ?next= per trimmed plan).
- lib/supabase/session.ts: implements updateSupabaseSession() returning
  { response, user }. Cookies written onto both request.cookies (for
  downstream reads) and response.cookies (for browser). PROTECTED_PATH_PREFIXES
  exported as the gate list.
- lib/proxy/copy-cookies.ts: small helper that ports Set-Cookie entries
  between two NextResponses.
- lib/auth/get-server-session.ts: getServerSession() returning
  { user, role } | null. Derives User type from the factory's return
  type so @supabase/supabase-js stays out of allow-listed lib/auth/*
  per ESLint no-restricted-imports.
- lib/auth/session-provider.tsx: client-side context exposing user to
  client components via useSession() — populated once per request in
  the locale layout.
- app/[locale]/layout.tsx: reads user via getUser() outside any
  'use cache' scope; wraps children in SessionProvider; explicit
  'use cache' warning comment.
2026-05-25 17:30:41 +07:00
tiennm99 0a08f80450 feat(phase-1): db schema init (bsk_init migration + roles)
- supabase/migrations/20260525163300_bsk_init.sql: bsk schema, app_role
  enum, app_users enrollment table, current_role() SECURITY DEFINER
  STABLE helper, RLS enabled with two SELECT policies (own row + admin)
- types/supabase-bsk.ts: hand-written placeholder mirroring canonical
  supabase gen output; regenerate via pnpm db:gen-types after migration
  applied
- lib/db/roles.ts: appRoles tuple + AppRole union + satisfies guard
  against generated enum + isAppRole type guard
- package.json: db:gen-types script
- README: one-line note on regenerating types after db:push
- plans/: include phase 02/04/06 detail files alongside the existing
  01/03/05 (planner output that wasn't committed with the trim)
2026-05-25 17:23:08 +07:00
tiennm99 2d5e4e92f9 docs(plan): trim phase 1 to features present in original
Drop audit-log table + write helper, Playwright E2E, sign-in rate
limiting, and the `next=` open-redirect guard — none are present in
the original lds217/BSK project (Java/Swing/SQLite, no tests,
LAN-only).

Resolves D1-D7 from the planner's open-questions list:
- D1 magic link: defer
- D2 first-admin: advisory lock (Strategy A)
- D3 audit log: cut
- D4 sign-in route: [locale]/(auth)/sign-in
- D5 Playwright: defer
- D6 E2E target project: moot
- D7 rate-limit keying: cut

Generic-error-on-unenrolled is kept — it defends against the shared
auth.users enumeration vector, which is platform-introduced, not a
new feature.
2026-05-25 17:18:00 +07:00
tiennm99 eeda68c34a chore(ci): add NEXT_PUBLIC_*=sb_secret_* leak guard
Defense-in-depth check that fails the build (and the local pre-push
workflow) if a server secret value is assigned to a NEXT_PUBLIC_*
variable — those get bundled into the browser by Next.js.

- scripts/check-no-secret-leak.mjs: git grep for the assignment shape,
  excluding lockfiles and the script itself
- package.json: pnpm check:no-secret-leak
- .github/workflows/ci.yml: run the guard right after install, before
  format/lint/typecheck/build
- docs/threat-model.md: close the last Unresolved item
2026-05-25 16:19:43 +07:00
tiennm99 699686253c docs: record phase-5/7 + key-rotation decisions
- Phase 5 imaging: keep, pin numbers — 200 KB/image, 1h signed-URL TTL,
  7-day retention window (PLAN.md §4 Phase 5)
- Phase 7 reminders: keep QStash (free tier 1000 msgs/day covers
  clinic-scale); document signature + Zod + DB-invariant validation
  plan in threat-model R8
- sb_secret_* / sb_publishable_* rotation: event-driven only for the
  current educational scope (solo author, no real users); switch to
  quarterly when any real user exists
- threat-model Unresolved: drop the three items above; CI grep for
  sb_secret_ in NEXT_PUBLIC_* lines remains the only open question
2026-05-25 16:16:38 +07:00
tiennm99 0393ddf6ec docs(infra): add shared-supabase config, restore runbook, threat model, db preflight
- docs/supabase-shared-config.md: registry of project-wide Supabase
  settings (auth, JWT, email, keys, storage, realtime) shared across
  sibling apps; change-coordination protocol
- docs/runbooks/restore-from-bad-migration.md: per-schema recovery via
  pg_dump --schema=bsk that avoids project-wide PITR (which would also
  wipe sibling apps)
- docs/threat-model.md: trust model, sibling-app invite-only policy
  (mitigates auth.users blast radius), R1-R8 residual risks, unresolved
  questions for Phase 1
- scripts/preflight-supabase.ts: refuses 'supabase db push' unless the
  linked project ref is in ALLOWED_PROJECT_REFS; wired via pnpm db:push
- package.json: tsx devDep, db:preflight + db:push scripts
- pnpm-workspace.yaml: allowBuilds esbuild=true (required for tsx)
2026-05-25 15:57:10 +07:00
tiennm99 10a3693f1b fix(scaffold): apply phase-0 review findings
- env: cross-check VERCEL_ENV against NEXT_PUBLIC_APP_ENV at boot so prod
  credentials cannot silently write into a dev keyspace
- upstash: tighten cache-key regex (kebab + colon only); split SCAN
  patterns into their own validator so glob '*' is allowed only there
- eslint: forbid raw @upstash/redis, @upstash/ratelimit, @supabase/supabase-js
  imports outside the named factory files
- supabase/admin: harmonize 'use cache' guidance with CONTRIBUTING.md
  (safe inside cache; partition key on identity for user-specific reads)
- app/layout: clarify global-error.tsx vs error.tsx shell requirements
  given the passthrough root layout
- readme: Next.js 15 -> 16 (matches scaffolded version)
2026-05-25 15:56:54 +07:00
tiennm99 b88147059e feat: scaffold Phase 0 foundation (Next.js 16 + Tailwind v4 + Supabase/Upstash)
Initial code drop for the BSK educational rewrite. Repo previously held only
docs (PLAN.md, README, NOTICE, LICENSE, RESEARCH_REPORT). This commit lands
the App Router shell, i18n route group, and shared-infra factories per the
Phase 0 plan.

Scaffold:
- Next.js 16 + React 19 + TypeScript 5.9, App Router, Turbopack defaults
- Tailwind v4 via @tailwindcss/postcss with CSS-first @theme block
- shadcn/ui CLI v4 (components.json + cn helper); components install lazily
- next-intl v4 with vi default + en fallback; async-params-aware routing
- proxy.ts (Next 16's renamed middleware) wired to next-intl
- lib/supabase/{server,client,admin,session}.ts on @supabase/ssr, schema-scoped
  to 'bsk', async cookies(), server factory unsafe inside 'use cache'
- lib/upstash.ts: prefixed cache helpers and Ratelimit v2, QStash signature
  verifier; future code cannot write unprefixed Redis keys
- lib/env/{client,server}.ts split so the secret key types stay server-side
- ESLint flat config (eslint-config-next/core-web-vitals + typescript +
  prettier), Prettier with tailwindcss plugin, .npmrc + pnpm-workspace.yaml
  for pnpm 11 native-build approval
- CI runs format:check, lint, typecheck, build on PR with dummy env

PLAN.md updates:
- §1 reconciled to TypeScript 5.9 (TS 6 is GA but lacks ecosystem support)
- §3.1 notes middleware → proxy file rename and removal of `next lint`

All four gates pass locally: format:check, lint, typecheck, build (SSG for
/vi and /en, Proxy detected). Code-reviewer findings applied: env split,
session helper renamed and docstring fixed, cache.set/del types tightened,
prettierignore scope reduced, bilingual GlobalNotFound, explanatory comments
on no-op layouts and duplicate setRequestLocale.

Deferred to Phase 1: wiring updateSupabaseSession into proxy.ts (needs auth
flow first), schema migrations, sign-in form.
2026-05-25 10:58:04 +07:00
tiennm99 cbb232a92f docs(plan): pin stack to Next.js 16 + Tailwind v4 era; harden shared-infra rules
Repin §1 to latest-stable versions as of 2026-05 (Next 16, React 19, TS 6,
Tailwind v4 + shadcn CLI v4, Zod v4 + useActionState, TanStack Table v8,
@react-pdf v4, next-intl v4, Vitest + Playwright) and add §3.1 capturing
the Next 16 cross-cutting rules (async params, 'use cache' constraints,
Supabase/Realtime interaction, new sb_publishable_*/sb_secret_* keys).

Expand §2 with the namespacing surfaces that matter when one Supabase
project + one Upstash DB are shared across multiple Vercel apps:
project-wide API keys (RLS is the only isolation), shared Auth/SMTP/JWT
settings, Realtime channel prefixing, Storage bucket prefixing, QStash
signature-based per-app isolation, and a do/don't operational cheat-sheet.

Phase 0/1/3/5 bullets and §7 risks updated to reflect the new versions.
2026-05-25 10:33:59 +07:00
tiennm99 991ec4b245 docs: add planning, research, and attribution for educational rewrite
Repositions the repo as an educational Next.js + Supabase + Upstash
rewrite of lds217/BSK-All-in-One-Clinic-Management-System.

- RESEARCH_REPORT.md: upstream analysis (Java/Swing/Netty/SQLite,
  171 files, 25+ features, Vietnamese locale, no explicit license).
- PLAN.md: target stack, shared-infra design (schema-per-app on
  Supabase, key-prefixed Upstash), 9-phase roadmap, divergences from
  the original, risks, and open decision on BSK isolation.
- NOTICE: clean-room attribution to @lds217 and the upstream repo.
- README.md: project intent, stack summary, educational-only
  disclaimers, and license stance.

No application code yet.
2026-05-23 22:59:26 +07:00
tiennm99andGitHub 4a3a9a4232 Initial commit 2026-05-23 22:35:19 +07:00