CI has been red on every push: the workflow runs `pnpm format:check`, which I
never included in local validation (I only gated typecheck/lint/build), so 54
files were committed unformatted.
- ran `pnpm format` — all changes are Prettier line-reflow, no logic touched
(verified: `git diff -w` flags exactly the same 54 files CI reported)
- added .gitattributes (`* text=auto eol=lf` + binary rules for fonts/images):
with core.autocrlf and no attributes file, `format:check` counted 141 files
locally vs 54 in CI, which is what let this slip through
All six CI gates now pass locally in workflow order: secret-leak, format:check,
test (97), lint, typecheck, build.
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.
- 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).