An offline-first Progressive Web App for medical clerks doing hospital rounds.
Replaces Google Sheets for tracking ~10 active patients during a rotation — built to be fast on a phone.
- About
- Features
- Tech Stack
- Getting Started
- Deployment
- Project Structure
- Data & Privacy
- Validating Changes
- Known Limitations
- License
PUHRR is a single-user, personal PWA designed for a medical clerk who needs to:
- Quickly capture patient notes during morning rounds on a phone.
- Generate copy-paste-ready text to share via Viber/WhatsApp to a laptop.
- Paste into Google Docs for the official record.
It is not a shared EHR, team tool, or full EMR — just a fast personal notebook that works offline.
- Bottom nav on mobile — Patients / Patient / Checklist / Settings sticky bar.
- Top nav on desktop — same sections, with focused patient shown as Room – Last name.
- Tap Open on any patient card to jump directly into the focused patient view.
- On mobile, the tab row stays fixed just above the bottom nav when a patient is open.
Each open patient has eight focused tabs:
| Tab | Purpose |
|---|---|
| Profile | Demographics plus case-review notes (clinical summary, chief complaint, HPI, PMH, PE), diagnosis, and clinical details |
| Problems | Ordered, user-defined problem blocks with a title and free-text notes, plus assessment/plan and a per-date checklist |
| Vitals | Temp, BP, HR, RR, O₂ saturation with history |
| Labs | CBC, urinalysis, Blood Chemistry, ABG (with auto-calculated pO2/FiO2 and conditional Desired FiO2 when FiO2 > 21% or pO2 < 60; target PaO2 = 60), and Others (custom label + freeform result); trend comparison applies to structured templates, while Others stays plain |
| Medications | Structured medication list with status tracking and drag-to-reorder support |
| Orders | Doctor's orders — add, edit status, remove in one place |
| Photos | Camera capture or gallery pick, organized by section category |
| Reporting | Profile/problems/vitals/labs/orders/census exports with lab instance selection and comparison support |
- Date picker shows checklist state for the chosen date across active patients only.
- Incomplete items carry forward to future dates; completed items stay on their original completion date.
- Each row shows patient identifier plus created/completed dates (if present), displayed in short format (e.g.,
Feb 10). - Completing a checklist item moves it to the bottom; reopening it moves it before the first completed item.
- Checklist entries can be marked done/pending, edited, removed, and freely reordered from either Problems or Master Checklist view.
- Profile summary follows room/name header, room/ward, admission and referral dates, tag-based Main Service/Referrals,
Dx, and optionalNotesblocks. - Problems summary uses
ROOM - LASTNAME, First — MM-DD-YYYY, includes numbered problem blocks and daily vitals min–max ranges, and preserves problem/checklist order. - Vitals summary supports multi-patient selection and date/time window filtering.
- Labs summary supports arbitrary instance selection per patient; comparison mode runs only when exactly 2 instances of the same non-Others lab template are selected.
- Orders summary supports date/time filtering using order date/time fields and preserves order text exactly as entered.
- All patient exports — choose exactly which active patients to include and reorder them before generating Selected Census or Selected Vitals.
- Text output opens in a popup with full-select and Copy full text button.
- Attach one or multiple photos per upload, categorized by section (Profile, Problems, Vitals, Medications, Labs, Orders).
- Each upload batch uses one shared title + category and appears as one gallery block with a photo-count badge.
- Photos inside the same upload batch preserve the order returned by your picker selection.
- Photo title is auto-prefilled as
Category + date/time; editable before saving. - Tapping a gallery block opens an in-app carousel for that upload set with Previous/Next buttons, keyboard shortcuts (Left/Right + Home/End), and thumbnail jump navigation for large groups.
- Use
@photo-titlementions in long-form notes to link directly to an attached photo. - Compressed copies stored in IndexedDB for offline viewing.
- Deleting a photo removes only the in-app copy — the original phone gallery file is untouched.
- Backup / restore — export all text data as JSON; import replaces text data while keeping current on-device photos.
- Encrypted sync (optional) — link devices with a shared room key, your user name, and distinct device names, then sync encrypted patient, problems, vitals, medication, lab, and order data through the Cloudflare Worker proxy.
- MCP server (
mcp/) — an optional MCP server that exposes your synced data and report templates to MCP-compatible tools, read-only by default. Report assembly and the Master Checklist logic live in shared modules (features/reporting/templateReport.ts,features/checklist/masterChecklist.ts), so the app and the server can't drift apart. Seemcp/README.md. - Change log (no screen) — changes made through the MCP server are recorded as just the fields that changed (old value → new value). The server reports exactly what it changed after every edit and lists earlier ones on request. There is no undo: a change is put back by making a new change.
- Automatic sync — after the first sync, edits are pushed about 5 seconds after you make them, and the room is checked on open, when the app returns to the foreground or comes back online, and every minute while it's visible. Room changes wait while a patient is open and apply when it's closed. Manual Sync still works.
- Sync status panel — view latest room upload time/device and whether this device has local unsynced changes (new, edited, or deleted records).
- Review all photos — open a global photo manager that marks each photo as linked/orphan and supports reassign, delete, and export.
- Clear discharged patients — bulk-remove patients marked as discharged.
- Show onboarding — reopen the Welcome modal and retry the install prompt at any time.
| Layer | Technology |
|---|---|
| Framework | React 19 + TypeScript 5.9 |
| Build tool | Vite 7 |
| PWA | vite-plugin-pwa (Workbox, autoUpdate) |
| Styling | Tailwind CSS v4 (CSS-only config in src/index.css) |
| UI components | shadcn/ui (files in src/components/ui/) |
| Local database | Dexie.js v4 (IndexedDB) |
| Forms | React Hook Form + Zod |
| Icons | lucide-react |
Tailwind v4 note: There is no
tailwind.config.js. All theme tokens (colors, spacing) are declared in the@themeblock insidesrc/index.css.
Before running the app, use this quick mental model:
- What this is: a personal, offline-first rounding notebook for one medical clerk. It is not a shared EHR.
- How it works: React + TypeScript renders the UI, Dexie stores data in IndexedDB, and report builders generate copy/paste-ready text from saved records.
- Data flow to remember: UI actions in
App.tsxwrite/read Dexie tables viadb.ts, shared shapes come fromtypes.ts, and formatted outputs are produced by feature formatters/builders. - Safety baseline: preserve existing patient data by default; avoid destructive schema changes unless explicitly requested.
- Docs drift rule: if workflow/labels/output behavior changes, update both this README and the in-app Settings “How to use” content.
If you only remember one thing: keep the app fast on mobile and offline-first.
- Node.js 20 LTS or later — nodejs.org
- npm (bundled with Node.js)
# 1. Clone the repo
git clone https://github.com/CSfromCS/PortableEletronicHealthRecord.git
cd PortableEletronicHealthRecord
# 2. Install dependencies
npm install
# 3. Start the dev server
npm run devVite starts on http://localhost:5173 by default.
- Run
npm run devin the terminal. - Open the Ports panel in VS Code — port 5173 appears automatically.
- Set visibility to Public.
- Copy the forwarded URL and open it in Chrome on your phone.
- Use Add to Home Screen in Chrome to install the PWA.
- Tailwind CSS IntelliSense (
bradlc.vscode-tailwindcss) — autocomplete for custom tokens likebg-coral-punch.
npm run build
npm run previewPreview serves the production bundle at http://localhost:4173.
After npm run build, deploy the dist/ folder to any static host:
- Netlify
- Cloudflare Pages
- Vercel (static output mode)
- GitHub Pages (see Option C)
The build uses relative asset paths (./) so it works in a subdirectory without extra configuration.
A GitHub Actions workflow (.github/workflows/deploy-pages.yml) deploys automatically on every push to main.
One-time setup:
- Go to Settings → Pages in your fork.
- Under Build and deployment, set Source to GitHub Actions.
- Push to
main(or trigger the workflow manually from the Actions tab).
The live URL appears in the workflow run after the deploy job completes.
PWA manifest: Metadata is the source of truth in
vite.config.ts(VitePWA.manifest).public/manifest.jsonis kept in sync as a static mirror for hosts that fetch manifests directly.
src/
├── App.tsx # Main app — all views, tabs, and UI logic
├── db.ts # Dexie schema, migrations, and DB helper functions
├── types.ts # TypeScript domain types
├── index.css # Tailwind v4 @theme tokens + global styles
├── labFormatters.ts # Lab result formatting utilities
├── main.tsx # React entry point + PWA registration
├── components/
│ └── ui/ # shadcn/ui base components (edit freely)
├── hooks/ # Custom React hooks
└── lib/
└── utils.ts # cn() helper and shared utilities
public/
└── manifest.json # Static PWA manifest mirror (keep in sync with vite.config.ts)
| Store | Contents |
|---|---|
patients |
Demographics, diagnosis, clinical details, status |
dailyUpdates |
Ordered problem notes, assessment/plan, and checklist per patient per day |
vitals |
Vital signs history |
medications |
Medication list entries |
labs |
Lab results |
orders |
Doctor's orders |
photoAttachments |
Compressed photo blobs + metadata |
Schema baseline: The database is named
roundingAppDatabase_v1. Legacy pre-1.0 stores and migration chains are intentionally dropped for clean installs.
- By default, data stays on your device.
- If Sync is enabled, only encrypted sync blobs are sent to the configured sync endpoint.
- No analytics, no telemetry, no external API calls.
- Backups are plain JSON files exported manually from Settings.
- Photo attachments are stored in IndexedDB only — they are excluded from the JSON backup.
- Importing a JSON backup replaces text tables and keeps existing photo attachments already stored on the device.
No automated test suite yet. Use these checks before shipping:
npm run lint
npm run buildThen do a quick manual smoke test:
- Open the app — confirm it loads with title PUHRR.
- Add a patient, enter and reorder problem blocks, then check the generated Problems summary.
- Confirm no errors in the browser console.
- (Optional) Disable network in DevTools → confirm the app still loads and data is accessible.
- (Optional) Install via browser menu → confirm it opens in standalone mode.
- JSON backup/restore covers text data only — photo attachments are not included.
- Import keeps currently stored photos; it does not recreate photos from the backup file.
- Sync covers patients, Problems, vitals, medications, labs, orders, tags, report templates, date/time formats, custom views, and custom actions — not photos.
- Every synced record carries a permanent cross-device
uidand anupdatedAttime, and deletions are kept as tombstones, so edits to existing records and deletions count as unsynced changes (not just newly created records). - If a room still contains a legacy snapshot from an older build, pull may include only
patients+dailyUpdatesuntil an updated device pushes a fresh snapshot. - First sync against an existing room now requires explicit user choice (upload local data or download room data first) to avoid silent overwrite.
- Auto-sync needs the app open and visible: a phone that backgrounds or locks PUHRR pauses it until the app is opened again. Network failures are retried silently on the next trigger.
- When the room has newer data and this device also changed, sync merges the two record by record (matched by each record's
uid; the laterupdatedAtwins; a deletion wins unless the record was edited after it). New entries from both sides are kept. This replaces the old pick-a-side conflict dialog, which now appears only for the first sync with an existing room. - The change log (what the MCP server changed, kept as the changed fields only, with long text shortened) is synced like any other data and pruned after 30 days. Each entry is small, but heavy editing through the MCP server still adds entries for those 30 days.
- Merging is per record, not per field: if the same record (for example one day's checklist) is edited on both devices before syncing, the later edit wins for that whole record. Two devices that each created the same patient's note for the same day are folded into one.
- A push is checked against the room just before uploading and sends
expectedUpdatedAt; if the room changed in between, PUHRR re-reads and merges again (up to 3 times). The Worker can additionally reject a stale push usingexpectedUpdatedAt. - New records get time-ordered ids with random low bits instead of 1, 2, 3, so two devices adding records between syncs don't collide. If two different records ever do share an id, sync stops without changing anything.
- Failed room lookups or remote checks stop sync without uploading; only an explicit room-not-found response permits creation.
- Choosing an older conflict revision restores it locally and uploads it as the room's new latest snapshot.
- Offline support depends on the PWA service worker being registered on first load while online.