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.

Cari pesakit secara manual dalam DB

Biasanya staf mencari pesakit melalui carian dalam aplikasi (halaman Senarai Pesakit). Cara manual di bawah hanya untuk pemilik/pembangun semasa troubleshooting β€” ia read-only (SELECT sahaja) dan mesti guna --remote untuk data produksi (tanpanya ia kena DB lokal β€” lihat gotcha (b) di atas).

Padanan nama ikut huruf awal Nama disimpan dalam satu kolum patients.nama (cth MUHAMMAD RIZAL BIN SHAHUJI). Untuk "nama pertama mula dengan M, nama kedua mula dengan R" guna nama LIKE 'M% R%'. Tak pasti nama bapa selepas BIN/BINTI? Jangan tapis bahagian itu langsung β€” biarkan terbuka.
# Nama pertama 'M', nama kedua 'R', lelaki (bin) β€” data PRODUKSI
npx wrangler d1 execute omfs-database --remote --json \
  --command "SELECT nama, ic_passport, umur, daerah, negeri
             FROM patients
             WHERE nama LIKE 'M% R%' AND nama LIKE '%bin%'
             ORDER BY nama;"

Output JSON mentah menyusahkan dibaca β€” salurkan ke Python untuk senarai ringkas:

... --json --command "SELECT nama, ic_passport, umur FROM patients WHERE nama LIKE 'M% R%';" \
  | python -c "import sys,json; d=json.load(sys.stdin); [print(r['nama'],'|',r['ic_passport'],'|',r['umur']) for r in d[0]['results']]"
Privasi & keselamatan Nama/IC pesakit ialah PII β€” jangan tampal hasil ke chat awam, isu GitHub, atau prompt AI. Guna SELECT sahaja untuk siasatan; jangan UPDATE/DELETE manual pada patients/visits (cascade β€” lihat amaran di atas). Pindaan data sebenar mesti melalui UI aplikasi supaya ada audit trail.
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/.