Panduan Developer Β· OMFS
🏠 Laman utama
Rujukan Teknikal Penuh  Β·  Sistem OMFS

Panduan Developer

Untuk seseorang yang akan membaca & mengubah kod ini. Padat, tepat, dan mengandaikan anda selesa dengan TypeScript, HTTP, dan SQL. Seni bina, lapisan, kontrak, auth, database, deploy, ujian β€” dengan graf modul interaktif.

βš›οΈ React 18 + Vite + TS ☁️ Cloudflare Worker (Hono) πŸ—„οΈ D1 (SQLite) πŸ”’ jose JWT + bcryptjs πŸ§ͺ Vitest (D1 sebenar) + Playwright
00 Mukadimah

Mukadimah & sumber kebenaran

Dokumen ini ialah rujukan teknikal Sistem OMFS untuk developer. Ia ringkasan; bila ragu, kod dan CLAUDE.md menang.

Mula baca kod dari sini (untuk manusia)

Turutan disyorkan untuk developer baru yang mahu faham aliran sebenar dengan pantas. Ini fail kod β€” bukan fail peraturan AI.

#FailKenapa
1worker/index.tsPintu masuk: middleware, laluan awam, mount routes.
2worker/routes/hospital.tsContoh route ringkas + lengkap (GET awam, PUT dipagari).
3worker/services/HospitalConfigService.tsCorak service (env, actorId) + audit.
4worker/contracts/hospital.contracts.tsBagaimana Zod jadi sumber type.
5shared/types.tsType teras (Patient, Visit, UserRole) FE+BE kongsi.

Untuk ejen AI (Claude / Cursor / Aider)

Jika anda membrief ejen AI, fail-fail ini ialah taklimat projek yang dibaca mesin β€” apa yang ejen patut baca dahulu (CLAUDE.md sendiri ditulis untuk ejen, bukan manusia). Sebagai manusia, anda boleh langkau ini β€” jadual kod di atas sudah memadai.

CLAUDE.md + REFACTOR_PROGRESS.md

Peraturan projek + peta fail terkini, dan satu-satunya pelan hidup (keadaan kerja semasa).

graphify-out/GRAPH_REPORT.md

Peta struktur kod (~3950 nodes). God Nodes = abstraksi teras; Communities = fail berkaitan. Baca dulu sebelum grep penuh-repo.

.claude/rules/*.md

api.md (auth/SQL/roles), database.md (cascade), frontend.md (UI), services.md (bila extract).

Untuk orang bukan-coder Versi mesra orang bukan-coder ada di Panduan Lengkap (bukan coder) β€” analogi, gambar rajah, dan Panduan Penyelenggara. Dokumen ini pula mengandaikan anda baca kod.
01 Seni Bina

Seni bina sistem

Satu Cloudflare Worker menyajikan kedua-dua aset SPA (HTML/JS/CSS terbina) dan API /api/*. Tiada server berasingan, tiada SSR. Database ialah D1 (SQLite) terikat (binding DB) pada Worker yang sama.

Setiap hospital = satu salinan Worker + satu D1 dalam akaun Cloudflare percuma mereka sendiri (lihat Β§09). Identiti per-instance hidup dalam jadual singleton hospital_config β€” tiada nama hospital ditanam keras.

Graf di bawah ialah seni bina lapisan secara interaktif. Tarik nod, hover untuk serlahkan kebergantungannya, scroll untuk zum.

Drag nod Β· hover = serlah Β· scroll = zum Β· seret latar = pan.

πŸ“Š (Graf seni bina interaktif β€” buka dalam pelayar.)

02 Repo

Susun atur repo & graf modul

Struktur tegas: src/ (frontend) Β· worker/ (backend) Β· shared/ (type kongsi) Β· migrations/ (skema D1).

omfs-system/ β”œβ”€ src/ # Frontend SPA β€” React 18 + TS + Tailwind β”‚ β”œβ”€ pages/ # satu folder ikut role: admin doctor oncall registrar superadmin shared β”‚ β”œβ”€ components/ # boleh guna semula + ui/ (shadcn/radix), patient/, oncall/ β”‚ β”œβ”€ contexts/ # HospitalConfigProvider β†’ useHospitalConfig() β”‚ β”œβ”€ lib/ # oncall-api, ics-utils, xlsx-utils, letter-utils … β”‚ β”œβ”€ config/ # support.ts (channel pemilik, BUKAN per-hospital) β”‚ └─ main.tsx # router (react-router-dom v7) + bootstrap β”œβ”€ worker/ # Backend Worker (Hono) β”‚ β”œβ”€ index.ts # entry: middleware, PUBLIC_PATHS, onError, cron, mount β”‚ β”œβ”€ user-routes.ts # DIBEKUKAN β€” barrel 49 LOC; route baru JANGAN sini β”‚ β”œβ”€ routes/ # API ikut domain: hospital, oncall, patients, visits/ … β”‚ β”œβ”€ services/ # akses D1: PatientService, OncallService … (env, actorId) β”‚ β”œβ”€ contracts/ # skema Zod (SDD): *.contracts.ts β”‚ β”œβ”€ middleware/ # auth.ts β†’ requireRole() β”‚ β”œβ”€ oncall/ # enjin keadilan: fairness.ts, swaps.ts, types.ts (HSR_CONFIG) β”‚ β”œβ”€ observability/ # alert.ts β†’ Discord (PII-scrubbed) β”‚ β”œβ”€ db/ # transactions.ts (withTx) β”‚ β”œβ”€ core-utils.ts # signJwt / verifyJwt / isValidUuid (jose) β”‚ └─ __tests__/ # vitest β€” Miniflare D1 sebenar (TIADA mock) β”œβ”€ shared/ # type FE+BE: types.ts, hospital-identity.ts, hospital-defaults.ts β”œβ”€ migrations/ # 0001..0046 β€” D1 jejak ikut NAMA fail β”œβ”€ scripts/ # provision-hospital.mjs, release-all.mjs, backup-instance.mjs … β”œβ”€ e2e/ # Playwright specs β”œβ”€ public/ # templat PG201/PG211 xlsx, aset oncall β”œβ”€ wrangler.jsonc # config HSR (committed); wrangler.<slug>.jsonc per-instance β”œβ”€ wrangler.template.jsonc # templat provisioning (__PLACEHOLDER__) └─ vite.config.ts # plugin @cloudflare/vite (configPath ← OMFS_WRANGLER_CONFIG)

Graf modul di bawah memetakan hubungan sebenar dalam kod β€” routes β†’ services β†’ contracts, enjin on-call, dan jadual D1. ~44 nod terpilih (bukan keseluruhan ~3950 β€” elak hairball).

Drag nod Β· hover = serlah kebergantungan Β· scroll = zum Β· seret latar = pan.

πŸ“Š (Graf modul interaktif β€” buka dalam pelayar.)

Selepas perubahan struktur Tambah/buang/namakan semula fail? Jalankan python scripts/graphify-rebuild.py (saat, terskop) untuk kemas kini peta β€” BUKAN graphify generate (tiada arahan itu) atau watch helper (imbas node_modules, hang ~13 min).
03 Stack

Stack, persekitaran & arahan

React 18 + Vite + TypeScript + Tailwind di depan; Hono di atas Cloudflare Worker; D1 (SQLite) sebagai database; Wrangler untuk deploy + migrasi.

Arahan harian

ArahanGuna
npm run devDev tempatan (vite, port 3000).
npm run buildJana ikon PWA β†’ vite build (produksi).
npx tsc --noEmitType check (lihat gotcha Β§11 β€” gate ini hampir no-op).
npm testVitest (D1 Miniflare sebenar).
npm run lint:checkESLint.
npm run seed:e2eApply migrasi lokal + seed seed-e2e.sql untuk Playwright.
npm run db:migrate:remoteApply migrasi ke D1 produksi.
git push origin mainDeploy normal β†’ GitHub Actions (CI β†’ wrangler deploy).
Rahsia tidak masuk transkrip Jangan baca/print .env, .dev.vars, *.key/*.pem. Wrangler memuatkan .dev.vars sendiri semasa npm run dev. Dikuatkuasakan oleh deny-list Read + hook validate-command.js.
04 Request

Kitaran request & lapisan backend

Setiap permintaan masuk melalui worker/index.ts. Auth diperiksa mengikut path, kemudian role, kemudian handler. Handler ikut SDD: parse Zod β†’ service β†’ envelope { success, data }.

worker/routes/hospital.ts β€” corak rasmi
// 1. Kontrak Zod dulu (SDD) β€” wujud sebelum handler, jadi type FE juga
app.put('/api/hospital/config', requireRole('superadmin'), async (c) => {
  const body = UpdateHospitalConfigRequest.parse(await c.req.json());  // 400 jika salah bentuk
  const jwt = c.get('jwtPayload')!;                              // diisi oleh middleware
  const cfg = await new HospitalConfigService(c.env, jwt.sub).update(body);
  return ok(c, cfg);                                              // { success:true, data }
});
LangkahDi manaApa berlaku
1index.tsPadan PUBLIC_PATHS / PUBLIC_GET_PATHS β€” laluan awam langkau auth.
2middleware/auth.tsverifyJwt (jose) β†’ isi jwtPayload; gagal β†’ 401.
3requireRole(...)Role tak cukup β†’ 403. Role disimpan dalam Bahasa Melayu.
4routes/{domain}.tsZod .parse() β†’ salah β†’ 400 (ZodError).
5services/*.tsSemua sentuhan D1; constructor (env, actorId); audit via logAction.
6ok(c, data)Envelope seragam β†’ 200.

Budget saiz fail (dikuatkuasakan CI)

LapisanLokasiSoft / Hard LOC
Routeworker/routes/*.ts400 / 600
Serviceworker/services/*.ts300 / 500
Contractworker/contracts/*.ts200 / 400
Middlewareworker/middleware/*.ts100 / 200
React pagesrc/pages/**250 / 400
React leafsrc/components/**150 / 300

Hard-limit breach = CI gagal (node scripts/check-file-sizes.mjs). Nak sentuh fail yang sudah lepas hard limit β†’ extract dulu (no-behaviour-change), commit, baru edit. Extract service bila: query sama di 3+ handler, mutasi perlu 2+ statement (withTx), atau peraturan sama dikuatkuasakan di >1 tempat.

05 Auth

Auth & model JWT

Login berbilang-pintu di worker/routes/auth.ts; token ditandatangani dengan jose; kata laluan di-hash dengan bcryptjs.

Pintu: POST /api/login (role biasa), /api/login/superadmin, dan /api/login/oncall (pintu berasingan; role oncall dipagari hanya ke /api/oncall/*). signJwt/verifyJwt di core-utils.ts; hashPassword/verifyPassword di auth.ts (BUKAN core-utils). JWT_SECRET unik-rawak setiap instance β€” kunci dikongsi = pemalsuan token silang-instance.

Role (nilai DB)Orang sebenarSkop
pendaftarKaunterDaftar pesakit, bayaran, senarai.
doktorDoktorDiagnosis & rawatan, rekod sendiri.
pentadbirStaf klinikLaporan, lihat semua pesakit (BUKAN urus on-call).
superadminPemilik/developerTetapan hospital, urus pengguna, kawalan penuh.
oncallPengurus jadualPintu berasingan; jana/kunci jadual. Mgr juga = superadmin ATAU flag can_manage_oncall.
Jangan guna nama role Inggeris 'admin'/'doctor'/'registrar' tidak akan padan nilai DB. Sentiasa guna jenis UserRole dari shared/types.ts (nilai Melayu).
06 Database

Database, migrasi & gotcha

D1 (SQLite). Migrasi migrations/NNNN_nama.sql dijejak D1 ikut nama fail β€” maka mengedit migrasi yang sudah diapply adalah selamat (DB live tak re-run; hanya install baru dapat kandungan baru).

Jadual: patients Β· visits Β· visit_doctors Β· diagnoses Β· managements Β· users Β· audit_logs Β· settings Β· appointments Β· messages Β· system_kv Β· hospital_config (singleton id=1).

Tiga gotcha D1 yang memusnahkan data (a) Cascade visits: SQLite buat DELETE FROM visits implisit sebelum DROP TABLE β†’ cascade padam visit_doctors/diagnoses/managements. Backup jadual anak DULU (lihat migrations/0026); PRAGMA defer_foreign_keys TIDAK menyelamatkan. (b) Flag --remote: wrangler d1 execute tanpa --remote kena DB lokal β€” sentiasa tambah --remote untuk produksi. (c) Kiraan binding INSERT: senaraikan kolum, ?, dan .bind(...) selari β€” beza = D1_ERROR: N values for M columns yang mematahkan pendaftaran pesakit.

Pemulihan: D1 menyimpan PITR (time-travel) 7 hari β€” wrangler d1 time-travel info/restore. Export safety dulu (wrangler d1 export --remote), restore, re-apply migrasi selepas bookmark.

07 Frontend

Frontend mendalam

React 18 + Vite + TypeScript + Tailwind (SPA, tiada SSR). UI = shadcn/ui di atas Radix (src/components/ui/).

Routing: react-router-dom v7 (src/main.tsx). State: zustand + React context (HospitalConfigProvider β†’ useHospitalConfig(), dimuat dari GET /api/hospital/config yang awam β€” branding load sebelum auth). Borang: react-hook-form + zod. Halaman ikut role di src/pages/{admin,doctor,oncall,registrar,superadmin,shared}/.

Pecahkan komponen sebelum tambah ciri bila: >400 LOC (page) / >300 (leaf), >5 useState, >2 dialog, atau >3 domain. Helper yang di-extract pergi ke fail .ts (bukan sebelah komponen .tsx β€” peraturan react-refresh).

PWA registerType kekal 'prompt' (jangan 'autoUpdate'). Jangan guna updateSW(true) untuk reload β€” guna SKIP_WAITING terus dalam PwaUpdatePrompt. Kemas kini mid-session beratur dan dipakai pada navigasi route seterusnya.
08 Deploy

Build, deploy & CI

Deploy normal = push ke main β†’ GitHub Actions. Push ialah deploy; jangan wrangler deploy manual sebagai aliran biasa (ia memintas gate CI).

.github/workflows/deploy.yml: job ci (lint β†’ typecheck β†’ build β†’ test β†’ file-size) β†’ job deploy (wrangler deploy, 3 retry untuk API 10013). CI merah = tiada deploy. Sahkan hijau (gh run watch) sebelum lapor "live".

Dua nota deploy semasa (a) Actions billing rosak (sejak 2026-06-13): sehingga dibaiki, deploy terus β€” npm run build && npx wrangler deploy β€” tetapi semua gate lokal MESTI lulus dulu. (b) Gotcha build per-instance: JANGAN wrangler deploy -c <instance>.jsonc β€” ia memintas build vite-plugin β†’ gagal "assets missing directory". Betul: build dengan env OMFS_WRANGLER_CONFIG=wrangler.<slug>.jsonc dahulu, kemudian wrangler deploy TANPA -c (ikut redirect .wrangler/deploy).
09 Fleet

Multi-hospital, provisioning & fleet

Setiap hospital berjalan dalam akaun Cloudflare percuma mereka sendiri (kos sifar, skala linear β€” BUKAN SaaS pusat). Hanya orkestrasi yang berpusat.

TugasAlat
Install pertama (instance baru)scripts/provision-hospital.mjs
Templat config per-instancewrangler.template.jsonc
Kemas kini SEMUA instance (satu arahan)scripts/release-all.mjs
Backup D1 per-instancescripts/backup-instance.mjs
Daftar instance (gitignored)instances.registry.json
Token scoped per-akaun.env.fleet
Semakan versi liveGET /api/version

Provisioning β‰  release. Provisioning = install pertama (cipta D1, migrate, set JWT_SECRET, putar kata laluan superadmin/admin yang di-seed dengan hash kongsi β†’ bcrypt rawak baru). Release = kemas kini (tak sentuh secret; migrasi idempoten). Runbook: docs/PROVISIONING.md, docs/FLEET-OPERATIONS.md. Wizard pasang kali pertama di /setup dipagari flag settings.setup_completed.

10 Ops & Ujian

Observability & ujian

Amaran ralat ke Discord (PII-scrubbed); ujian guna D1 sebenar, tiada mock.

Pemerhatian & ralat

worker/observability/alert.ts (sendAlert/dispatchAlert) hantar ke Discord β€” PII di-scrub + dedupe. Diwayar dalam worker/index.ts (app.onError, 4 cron catch, /api/client-errors). Opt-in melalui secret DISCORD_WEBHOOK_URL (no-op bila tak diset); env HOSPITAL_LABEL menanda instance.

Ujian

Vitest dengan @cloudflare/vitest-pool-workers β€” D1 Miniflare sebenar, TIADA mock D1 (sebab pernah berlaku: ujian mock lulus tapi migrasi prod gagal). E2E: Playwright (e2e/), seed via scripts/seed-e2e.sql (set setup_completed=1). Metodologi: SDD (Zod dulu) + TDD (service) + characterization (sebelum refactor legacy).

gate sebelum commit
npx tsc --noEmit       # type check
npm run build          # esbuild (tangkap decl pendua yang tsc terlepas)
npm test               # vitest (D1 sebenar)
npm run lint:check     # eslint
11 Konvensyen

Konvensyen, corak terlarang & gotchas

Perkara yang review akan tolak, dan gotcha yang sudah pernah memakan masa.

Corak terlarang any (guna unknown + narrow) Β· @ts-ignore tanpa isu+tarikh Β· .catch(()=>{}) senyap Β· JWT inline Β· env.DB.prepare dalam handler Β· nama role Inggeris Β· route baru dalam user-routes.ts Β· mock D1 dalam ujian Β· baca/print fail rahsia.

Gotchas (footguns yang disahkan)

GotchaNota
Cascade visitsBackup jadual anak sebelum DROP (Β§06a).
--remote hilangexecute tanpa flag kena DB lokal (Β§06b).
Kiraan binding INSERTkolum/?/bind mesti selari (Β§06c).
Build per-instanceguna OMFS_WRANGLER_CONFIG, bukan -c (Β§08b).
typecheck β‰ˆ no-opsolution tsconfig periksa hampir tiada; e2e/runtime yang tangkap import hilang.
LEFT JOIN filterfilter jadual kanan dalam ON tak buang baris kiri β€” guna COUNT(CASE WHEN…).
Auth path-onlyroute awam-untuk-GET perlu PUBLIC_GET_PATHS, jika tidak PUT langkau auth lalu 401.
Auto-push senyapsemak git log origin/main..HEAD sebelum lapor "pushed".
12 Praktik

Resipi perubahan β€” dari mudah ke sukar

Boleh ke saya betul-betul coding selepas baca panduan ini? Membaca = faham. Membuat resipi di bawah = mula boleh. Setiap resipi senaraikan fail sebenar yang disentuh dan cara mengesahkannya.

Jawapan jujur Tahap 🟒 mudah & 🟑 sederhana: ya β€” anda boleh ikut resipi ini terus. Tahap 🟠 sukar ke atas: resipi memberi peta, tetapi anda perlu baca fail sebenar yang dirujuk + faham domain dulu. Selepas setiap perubahan, jalankan gate yang sama setiap kali: npx tsc --noEmit && npm run build && npm test.

🟒 Sangat mudah β€” tukar teks / label pada skrin

Contoh: tukar perkataan pada butang, tajuk, atau mesej.

Fail: komponen .tsx berkaitan dalam src/pages/ atau src/components/. Cari teks sedia ada dengan carian editor (Ctrl+Shift+F).

Langkah: cari string β†’ ubah β†’ simpan.  Uji: npm run dev, lihat skrin. Tiada migrasi, tiada API.

🟒 Mudah β€” tukar warna / tema

Fail: token warna di src/index.css (pemboleh ubah --color-* / tema Tailwind v4). Untuk subpokok bertema, override --color-* pada pembungkus .theme-X (bukan triple --primary mentah).

Uji: npm run dev; semak kontras (lihat pelajaran kontras: teks gelap atas latar gelap = tak nampak).

🟑 Sederhana β€” tambah medan baru pada borang + simpan ke DB

Ikut aliran data β€” lima tempat selari:

  1. Migrasi: migrations/NNNN_nama.sql β†’ ALTER TABLE ... ADD COLUMN (jalankan /migrate).
  2. Kontrak Zod: worker/contracts/*.contracts.ts β†’ tambah medan pada schema.
  3. Service: worker/services/*Service.ts β†’ masukkan dalam INSERT/UPDATE. Ingat kiraan binding (kolum = ? = .bind).
  4. Type kongsi: shared/types.ts β†’ tambah medan pada interface.
  5. UI: komponen borang .tsx (react-hook-form + zod resolver).

Berikut diff sebenar untuk satu medan β€” katakan medan teks pilihan alergi pada pesakit. Perhatikan kelima-lima fail bergerak selari, dan kiraan kolum / ? / .bind kekal sama (langkah 3):

diff β€” medan `alergi`, 5 fail selari
1 ─ migrations/0047_patients_alergi.sql   (fail baharu β€” jalankan /migrate)
+ ALTER TABLE patients ADD COLUMN alergi TEXT;

2 ─ worker/contracts/patients.contracts.ts
  export const CreatePatientRequest = z.object({
    nama: z.string().min(1),
+   alergi: z.string().optional(),
  });

3 ─ worker/services/PatientService.ts   ← kolum = ? = .bind mesti kira bertiga
- INSERT INTO patients (nama)         VALUES (?)
+ INSERT INTO patients (nama, alergi)  VALUES (?, ?)
  ...
-   .bind(p.nama)
+   .bind(p.nama, p.alergi ?? null)

4 ─ shared/types.ts
  export interface Patient {
    nama: string;
+   alergi?: string;
  }

5 ─ src/pages/registrar/PatientRegistrationPage.tsx   (react-hook-form)
+ <FormField name="alergi" control={form.control} render={({ field }) => (
+   <Input placeholder="Alergi (jika ada)" {...field} />
+ )} />
Gotcha paling kerap Lupa salah satu daripada tiga di langkah 3 β†’ D1_ERROR: N values for M columns yang mematahkan pendaftaran pesakit. Tambah alergi pada senarai kolum TAPI lupa ? atau .bind = beza kiraan. Kira ketiga-tiganya dengan jari sebelum deploy.

Uji: npm run db:migrate:local β†’ npm run dev β†’ isi borang β†’ semak DB (wrangler d1 execute DB --local --command "SELECT ...").

🟑 Sederhana β€” tambah route API baru

Corak SDD (lihat Β§04): kontrak dulu, kemudian handler, kemudian service.

  1. Kontrak: tambah schema Request/Response di worker/contracts/{domain}.contracts.ts.
  2. Handler: dalam worker/routes/{domain}.ts β†’ app.post(path, requireRole('...'), …); parse Zod; panggil service; return ok(c, data). Jangan tulis route dalam user-routes.ts (beku).
  3. Service: logik + akses D1 di worker/services/{Domain}Service.ts; constructor (env, actorId).
  4. Jika route awam-untuk-GET β†’ daftar dalam PUBLIC_GET_PATHS (worker/index.ts).

Uji: tulis ujian di worker/__tests__/ (D1 sebenar) + curl ke npm run dev.

🟠 Sukar β€” tambah jadual D1 baru (hujung ke hujung)

Gabungan semua di atas: migrations/NNNN (CREATE TABLE, indeks, FK) β†’ {Domain}Service.ts baru β†’ {domain}.contracts.ts baru β†’ routes/{domain}.ts baru (daftar mount dalam worker/index.ts) β†’ type dalam shared/types.ts β†’ halaman/komponen UI.

Bila extract jadi service: query sama di 3+ tempat, mutasi 2+ statement (withTx), atau peraturan dikuatkuasakan di >1 tempat. Uji: ujian service (TDD) + e2e satu laluan.

πŸ”΄ Sangat sukar β€” ubah algoritma keadilan on-call

Sentuh enjin: worker/oncall/fairness.ts, swaps.ts, dan types.ts (HSR_CONFIG + pemberat). Logik berkait rapat dengan slot, kekosongan, cuti, kredit, dan self-picks (yang dipelihara merentas generate()).

Wajib: baca fail-fail itu sepenuhnya + ujian sedia ada di worker/__tests__/oncall/ dahulu. Tambah ujian yang mengunci tingkah laku sebelum mengubah (characterization). Satu pemberat salah boleh menjadikan jadual tak adil tanpa ralat.

Sebelum sebarang perubahan DB Baca gotcha Β§06: cascade visits (backup jadual anak dulu), flag --remote, dan kiraan binding INSERT. Untuk produksi, wrangler d1 export --remote dahulu.
Jadi β€” boleh ke? Untuk 🟒/🟑: ya, dari panduan ini sahaja anda boleh buat perubahan sebenar & selamat. Untuk 🟠/πŸ”΄: panduan ini bawa anda ke fail yang betul dan beri urutan yang betul β€” tetapi kebolehan sebenar datang daripada membaca kod yang dirujuk + membuat 🟒/🟑 dahulu. Tiada jalan pintas; tetapi ada jalan yang jelas.
13 Modul

Modul ciri β€” peta fail & route

Rujukan padat per ciri: di mana kod hidup, route/entry-pointnya, dan gotcha utamanya. Untuk versi naratif bahasa-mudah, lihat Panduan Kod 03. Laluan disahkan 2026-06-18.

CiriFail terasRoute / entry
Laporan PG201/PG211src/lib/xlsx-utils.ts (barrel) β†’ xlsx-pg201.ts Β· xlsx-pg211.ts Β· xlsx-patient-report.ts Β· xlsx-pg101b.ts Β· xlsx-lampiran-c.ts Β· xlsx-styles.ts Β· xlsx-template-helpers.ts; public/templates/PG201.xlsx Β· PG211.xlsxReportingPage.tsx β†’ generate*Excel*()
Eksport & muat turunsrc/pages/admin/ReportingPage.tsx Β· src/pages/shared/MuatTurunPage.tsx Β· MuatTurunRegistrar.tsxbrowser-side; reportFacilityLine ← hospital_config
Hantar e-melworker/email.ts β†’ sendReportEmail()worker/routes/registrar/email.ts; secrets RESEND_API_KEY / RESEND_FROM_EMAIL / RESEND_PG101B_TO
Error handlingsrc/lib/swallow.ts Β· src/lib/errorReporter.ts Β· worker/db/rows.tsPOST /api/client-errors (queue, dedup, max 10)
Pembantu AIsrc/components/panduan/AiChatAssistant.tsx Β· ai-chat/useAiChat.ts Β· session.ts Β· prompt.ts Β· image.tssesi di localStorage; MAX_SESSIONS=20, HISTORY_TTL
Keadilan on-callworker/oncall/fairness.ts (pure) Β· swaps.ts Β· types.ts (HSR_CONFIG)worker/routes/oncall.ts β†’ generate(); lihat Β§12 resipi πŸ”΄

Nota tajam per ciri

PG201/PG211 (M7): isDoNotFill() menguatkuasakan 4 peraturan MOH (R1 ibu≤6, R2 referral-ULANGAN, R3 bersekolah 7–19, R4 pesara≥30). Lajur D mesti ditulis literal 0 (bukan '') atau pengesah AJ/AK jadi FALSE. Gabungan multi-helaian guna createTemplateSheets() model-clone β€” salinan sel-demi-sel lama merosakkan styles.xml. Entry-side hard-block: src/lib/age-utils.ts + StatusSection.tsx/schema.ts.

E-mel: Resend via fetch dengan AbortSignal.timeout(10s). Tiada RESEND_API_KEY β†’ pulang { ok:false } dengan mesej BM "sila muat turun" (graceful fallback, bukan throw). 403 Resend = domain ujian resend.dev hanya hantar ke alamat sendiri β†’ set RESEND_FROM_EMAIL ke domain disahkan. Badan respons TIDAK dilog penuh (boleh ada PII).

Error handling (corak terlarang Β§11): tiada .catch(() => {}) senyap β€” guna swallow('sebab') (no-op eksplisit + console.warn dalam DEV) ATAU errorReporter.report() ATAU toast. Selepas query D1, guna rows<T>(result) bukan as any[] (larangan any).

Fairness: enjin tulen (tiada DB/req β†’ unit-testable). Greedy kronologi; skor = beban tertimbang + penalti silang-bulan + tie-break mulberry32 berbenih (deterministik). Tapisan keras R16: tak-tersedia / kerja hari sebelum / maxDutiesPerMonth / leaveCreditMinDays. Slot tanpa calon sah β†’ kekal kosong + direkod konflik (tak paksa pilihan tak selamat). Self-picks dipelihara merentas generate() via worker/oncall/self-picks.ts. Ujian: worker/__tests__/oncall/.