Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ jobs:
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium

# Playwright serves dist/ via `npm run preview` when CI is set, so the
# app must be built first. Without this the preview server has nothing
# to serve and every test times out waiting for the app to render.
- name: Build
run: npm run build

- name: Run Playwright tests
run: npx playwright test --project=chromium --grep-invert @visual

Expand All @@ -65,3 +71,48 @@ jobs:
path: playwright-report/
if-no-files-found: ignore

deploy:
runs-on: ubuntu-latest
needs: [test, e2e]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

steps:
- name: Check Cloudflare credentials
id: check_credentials
run: |
if [ -n "$CLOUDFLARE_API_TOKEN" ]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
echo "Cloudflare credentials detected; proceeding with deployment."
else
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "Deploy skipped: no Cloudflare credentials configured."
fi

- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Build
if: steps.check_credentials.outputs.configured == 'true'
run: npm run build

- name: Delete sourcemaps
if: steps.check_credentials.outputs.configured == 'true'
run: find dist -name '*.map' -delete

- name: Deploy to Cloudflare Pages
if: steps.check_credentials.outputs.configured == 'true'
run: npx wrangler pages deploy dist --project-name dream --branch main


1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ e2e/screenshots/
.codex/
.playwright-mcp/
skills-lock.json
.claude/
124 changes: 124 additions & 0 deletions HANDOVER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Handover & Succession Inventory

This document provides a comprehensive inventory of services, secrets, external dependencies, security considerations, and operational continuity procedures for the DREAM repository. It is written to ensure that a successor maintainer or the hospital department can maintain, operate, and migrate the application without reliance on the original author.

---

## Service & Asset Inventory

| Asset / Service | Current Owner | Required for Clinical Function? | Description & Risks |
| :--- | :--- | :--- | :--- |
| **GitHub Repository** (`monchee/dream`) | `monchee` (Personal Account) | **No** (needed for CI/CD & maintenance) | Source code, issues, and GitHub Actions workflows. Planned migration to an institutional organisation. |
| **Cloudflare Pages** (`dream` project) | `monchee` (Personal Account) | **Yes** (for hosting live PWA) | Static hosting platform serving the web application. |
| **Custom Domain** (`dream.yuson.au`) | `monchee` (Personal Domain) | **No** (fallback URL available) | Custom DNS domain. **Succession risk:** personal domain ownership. |
| **Sentry** (`monchee` / `dream`) | `monchee` (Personal Account) | **No** (optional monitoring) | Error monitoring and performance tracing. No-ops if DSN is omitted. |
| **Supabase** (`research_submissions`) | `monchee` (Personal Account) | **No** (optional research storage) | Database for deidentified research submissions only. Core app is local-first. |
| **Access PIN** (`PasswordGate.tsx`) | Hardcoded in repository | **Yes** (deterrent gate) | Shoulder-surfing deterrent PIN (`2050`). Not a security boundary. |

---

## Detailed Component Analysis & Rotation Runbooks

### 1. GitHub Repository
- **Current State**: Hosted under personal GitHub account `monchee/dream`.
- **Functionality**: Houses source code, issue tracking, and GitHub Actions CI workflows.
- **Succession / Transfer Procedure**:
1. In repository settings, navigate to **Settings** > **General** > **Danger Zone** > **Transfer ownership**.
2. Specify the target GitHub Organisation (e.g. hospital/department organisation) or successor account.
3. Ensure repository secrets (`CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`) are migrated or reconfigured under organisation/repository secrets.

---

### 2. Cloudflare Pages & Deployment Secrets
- **Current State**: Hosted on Cloudflare Pages under project `dream`.
- **Functionality**: Serves static web assets (`dist/`) over global CDN with automated HTTPS and caching headers (`public/_headers`).
- **Secrets**:
- `CLOUDFLARE_API_TOKEN`: Cloudflare API token with `Cloudflare Pages: Edit` permissions.
- `CLOUDFLARE_ACCOUNT_ID`: Cloudflare account ID.
- Configured in GitHub repository secrets (**Settings** > **Secrets and variables** > **Actions**).
- **Rotation / Replacement Procedure**:
1. In Cloudflare Dashboard, go to **My Profile** > **API Tokens** > **Create Token**.
2. Use the **Edit Cloudflare Workers/Pages** template (or create custom token with `Account.Cloudflare Pages:Edit` permissions).
3. Copy Account ID from the Cloudflare Pages overview sidebar.
4. Update `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` in GitHub repository secrets.
5. *Fail-safe note:* If `CLOUDFLARE_API_TOKEN` is unset or empty, the `Check Cloudflare credentials` guard step in the CI `deploy` job detects its absence and skips the build, sourcemap deletion, and deployment steps cleanly without failing builds.

---

### 3. Custom Domain & DNS Migration
- **Current State**: [dream.yuson.au](https://dream.yuson.au) is mapped via DNS to Cloudflare Pages.
- **Succession Risk**: `yuson.au` is a **personal domain** owned by the original author. If the personal domain lapses or becomes unavailable, the custom URL will stop resolving.
- **Domain Independence**: In `vite.config.ts`, `base: './'` is configured, ensuring all bundle assets and routes use relative paths. The application is completely domain-agnostic and functions identically under any hostname, subdirectory, or port.
- **Fallback URL**: The Cloudflare-provided domain [dream.pages.dev](https://dream.pages.dev) is always available as an immediate fallback.
- **Migration to a New Domain (e.g., Hospital / NSW Health Domain)**:
1. In Cloudflare Pages Dashboard, navigate to **dream** > **Custom domains** > **Set up a custom domain**.
2. Enter the new FQDN (e.g. `dream.allergy.rpah.health.nsw.gov.au` or `dream.yourhospital.org.au`).
3. In the hospital/department DNS provider, create a `CNAME` record:
- **Host / Name**: `dream` (or desired subdomain)
- **Target / Value**: `dream.pages.dev`
4. Cloudflare automatically provisions and renews SSL/TLS certificates for the custom domain.
5. Update clinic bookmarks and PWA shortcuts.

---

### 4. Sentry (Error Tracking & Crash Reporting)
- **Current State**: Integrated via `@sentry/react` in `src/lib/sentry.ts`.
- **Runtime Behavior** (verified in `src/lib/sentry.ts` and `src/lib/env.ts`):
- Enabled only when `VITE_SENTRY_DSN` is set and `VITE_ENVIRONMENT !== 'test'`.
- The Sentry library is dynamically imported (`loadSentry()`) only if enabled.
- If `VITE_SENTRY_DSN` is absent or empty, Sentry remains uninitialized and all logging functions (`initSentry()`, `captureMessage()`, `captureException()`) safely no-op without throwing errors or impacting user experience.
- **PHI Redaction**: `scrubPhiFromEvent()` in `src/lib/sentry.ts` redacts MRNs, record IDs, DOB-shaped date strings, cookies, user IDs, and IP addresses before sending error events.
- **Build Plugin**: `sentryVitePlugin` in `vite.config.ts` only activates if `SENTRY_AUTH_TOKEN` is present in the build environment.
- **Rotation / Replacement**:
- To change Sentry instance, update `VITE_SENTRY_DSN` in build environment variables (or `.env`).
- Update `SENTRY_AUTH_TOKEN` in CI secrets if sourcemap uploading is desired.

---

### 5. Supabase (Research Database)
- **Current State**: Integrated in `src/lib/supabase.ts` and consumed by `src/features/research/services/ResearchService.ts`.
- **Runtime Behavior** (verified in `src/lib/supabase.ts` and `src/features/research/services/ResearchService.ts`):
- Configured via `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY`.
- Exported boolean `isSupabaseConfigured` gates research features.
- **Clinical Workflows are Independent**: All core clinical functions (REDCap import, patient review, testing plan generation, test recording, handouts, clinical reports) run local-first in browser memory and local storage. If Supabase is unconfigured or unavailable, the app remains fully functional for clinic operations; only research submission features are disabled.
- **Known Open Security Issue**:
- **Location**: `supabase/migrations/001_research_submissions.sql`
- **Issue**: The migration script configures the following Row Level Security (RLS) policies:
```sql
CREATE POLICY "anon_insert" ON research_submissions FOR INSERT TO anon WITH CHECK (true);
CREATE POLICY "anon_select" ON research_submissions FOR SELECT TO anon USING (true);
CREATE POLICY "anon_delete" ON research_submissions FOR DELETE TO anon USING (true);
```
- **Impact**: The public `anon` role is granted `INSERT`, `SELECT`, and `DELETE` access to all rows in `research_submissions`. Anyone possessing the public `VITE_SUPABASE_ANON_KEY` can read, modify, or delete every research submission in the database.
- **Remediation Plan (to be implemented separately)**: Replace the permissive `anon_select` and `anon_delete` policies with authenticated access or restricted service-role access (e.g., allow `anon` INSERT only, with SELECT/DELETE restricted to authenticated clinic administrators).
- **Rotation / Replacement**:
- Create a new Supabase project.
- Run database migration scripts in `supabase/migrations/` (with updated RLS policies).
- Set `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` in environment variables.

---

### 6. Screen Lock Access PIN
- **Current State**: Hardcoded constant in `src/core/components/PasswordGate.tsx` line 9:
```typescript
const HARDCODED_PIN = '2050';
```
- **Security Assessment**:
- **Deterrent Only**: This is a front-of-screen shoulder-surfing deterrent intended for shared clinical workstations. It is **NOT** a cryptographic security boundary.
- The PIN is plainly visible in client JavaScript bundles and can be bypassed via browser DevTools or by executing `sessionStorage.setItem('dream_unlocked', 'true')`.
- **How to Change the PIN**:
1. Open `src/core/components/PasswordGate.tsx`.
2. Update line 9: `const HARDCODED_PIN = 'XXXX';` (replace with desired 4-digit string).
3. Rebuild and deploy (`npm run build` or push to `main`).

---

### 7. Degraded Fallback & Offline Independence

DREAM is engineered with zero runtime server requirements for clinical operations:
- **Plain Source in Git**: The entire application is authored in standard TypeScript and React.
- **Static Artifact**: Production builds (`npm run build`) produce standard static assets in `dist/` (HTML, CSS, JS, manifest, and icons).
- **Intranet / Local Hosting**: Even if GitHub, Cloudflare, Sentry, and Supabase accounts lapse or are closed:
- The department can clone or copy the repository.
- The application can be served from an internal hospital server, local intranet, static web server (Nginx, Apache, Caddy), or local machine (`npx serve dist` or `python3 -m http.server`).
- Clinical workflows (REDCap CSV import, plan generation, test execution, report printing) will continue to operate with full functionality.
104 changes: 104 additions & 0 deletions MAINTAINERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Maintainers Runbook

## Overview

DREAM (Drug Reaction Evaluation & Anaesthetic Management) is a private, local-first clinical Progressive Web App designed for the Royal Prince Alfred Hospital (RPAH) Department of Clinical Immunology & Allergy. It guides clinicians through perioperative anaesthetic allergy workups—from REDCap patient record import and tailored testing plan creation to skin prick/intradermal test logging, patient handouts, and eMR-ready clinical reports—all processed locally in the browser without transmitting identifiable patient data. Drug protocol definitions and dilution concentrations are tied to the SCRATCH drug library (repository `monchee/drug-library`, hosted at [scratch.pages.dev](https://scratch.pages.dev)), which serves as the upcoming source of truth for drug protocol data that DREAM consumes as a pinned JSON snapshot.

---

## Local Development

### Prerequisites
- Node.js 20 (`.nvmrc` / GitHub Actions target Node 20)
- npm (bundled with Node)

### Installation
```bash
npm ci
```

### Development Server
```bash
npm run dev
```
Starts the Vite dev server at `http://localhost:3000`.

### Validation & Code Quality
- **Type check**: `npx tsc --noEmit`
- **Lint**: `npm run lint` (runs ESLint with `--report-unused-disable-directives --max-warnings 0`)
- **Lint auto-fix**: `npm run lint:fix`

### Testing
- **Unit & component tests (single run)**: `npm run test:unit`
- **Unit tests (watch mode)**: `npm run test`
- **Unit tests (UI mode)**: `npm run test:ui`
- **Unit test coverage**: `npm run test:coverage`
- **End-to-end tests (Playwright)**: `npm run test:e2e`
- **End-to-end tests (interactive UI)**: `npm run test:e2e:ui`

### Build & Preview
- **Production build**: `npm run build` (runs `prebuild` changelog sync then `vite build`)
- **Preview production build locally**: `npm run preview`
- **Changelog sync (manual)**: `npm run changelog:sync` (runs `scripts/generate-changelog.mjs` to update `src/shared/data/changelog.json` from `CHANGELOG.md`)

---

## Deployment

### Normal Path (Automated CI/CD)
1. Prepare release:
- Update `version` in `package.json`.
- Add release notes under a new version heading in `CHANGELOG.md` (include a `Summary:` line for in-app highlights).
- Run `npm run changelog:sync`.
2. Validate locally with `npx tsc --noEmit && npm run lint && npm run test:unit && npm run build`.
3. Open a pull request or push directly to `main`.
4. The GitHub Actions workflow (`.github/workflows/ci.yml`) runs `test` and `e2e` jobs in parallel.
5. Upon successful completion of both test suites, the `deploy` job builds the application, strips sourcemaps (`find dist -name '*.map' -delete`), and publishes to Cloudflare Pages using `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`.

> **Note**: If `CLOUDFLARE_API_TOKEN` is not configured in GitHub repository secrets, the `deploy` job skips cleanly without failing the CI run.

### Break-Glass Path (Emergency Manual Deployment)
If GitHub Actions is down, credentials fail, or an urgent hotfix must be shipped immediately:

1. Ensure local Wrangler authentication:
```bash
npx wrangler login
```
2. Deploy directly to production (`main` branch on Cloudflare Pages):
```bash
npm run deploy
```
*This script runs `npm run build && find dist -name '*.map' -delete && wrangler pages deploy dist --project-name dream --commit-dirty=true --branch main`.*

3. Deploy to preview branch (optional):
```bash
npm run deploy:preview
```

---

## Release Verification

After a deployment completes, verify the release with the following checklist:

1. **Check CI / Host Status**:
- Confirm the GitHub Actions run on `main` succeeded.
- Verify the deployment status in the Cloudflare Pages dashboard.

2. **Access the Live Application**:
- Production URL: [https://dream.yuson.au](https://dream.yuson.au)
- Cloudflare Pages fallback URL: [https://dream.pages.dev](https://dream.pages.dev)

3. **Check PWA & Version**:
- Trigger a refresh to allow the service worker to fetch the updated bundle.
- Enter the PIN gate (`2050`) to unlock the app.
- Confirm the version badge in the bottom-right corner matches the expected version from `package.json`.

4. **Verify Changelog**:
- Open the "What's New" modal or navigate to `/changelog` to ensure the new release summary and notes are rendered.

5. **Smoke Test Core Features**:
- Load or import a test patient record.
- Navigate to Testing Plans and verify protocol generation.
- Test logging and report generation (Clinical Report, Patient Handout).
- Check browser console (`F12`) for any runtime exceptions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ The deploy script runs a production build and then publishes with:
wrangler pages deploy dist --project-name dream --commit-dirty=true --branch main
```

For automated CI/CD deployment instructions, see [MAINTAINERS.md](MAINTAINERS.md). For service ownership, credentials, domain risks, and fallback procedures, see [HANDOVER.md](HANDOVER.md).

## Project Structure

```text
Expand All @@ -152,7 +154,9 @@ src/

## Known Operational Notes

- GitHub Actions validate pushes and pull requests to `main`.
- GitHub Actions validate pushes and pull requests to `main`, with automated deployment to Cloudflare Pages on push to `main` when `CLOUDFLARE_API_TOKEN` is configured.
- Maintainer runbook and testing scripts are documented in [MAINTAINERS.md](MAINTAINERS.md).
- Succession inventory, credential rotation, and continuity plans are documented in [HANDOVER.md](HANDOVER.md).
- Branch protection is not configured because GitHub reports it is unavailable for this private repository without GitHub Pro.
- Production assets are split into dedicated vendor chunks (React runtime, Supabase, Sentry, Radix UI, forms, and icons) so production builds stay well within chunk-size limits without warnings.
- `/manifest.webmanifest` is generated from the Vite PWA configuration. The Vite config is the manifest source of truth.
Expand Down
4 changes: 4 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export default defineConfig({
baseURL: process.env.CI ? 'http://localhost:4173' : 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
// Pin the browser clock to the clinic's timezone. Tests that assert a
// formatted wall-clock time would otherwise pass only on a machine set to
// Sydney and fail on CI runners, which are UTC.
timezoneId: 'Australia/Sydney',
},
projects: [
{
Expand Down
Loading