Frontend Application

The frontend is a zero-build, single-page application served as static files from the frontend/ directory. It uses no JavaScript framework and requires no compilation step — HTML, CSS, and vanilla JS are deployed as-is via Wrangler.

File Overview

FileSizePurpose
frontend/index.html227 linesPage shell, tab structure, all form markup
frontend/app.js523 linesAll logic: API calls, charting, tab management, validation
frontend/style.css~9 KBDesign system, layout, typography

Page Structure

graph TD HTML["index.html"] HTML --> Header["Header · App title"] HTML --> TabBar["Tab bar\n[BP] [Sleep] [Weight] [Settings]"] HTML --> BPPanel["tab-bp\nBlood Pressure panel"] HTML --> SleepPanel["tab-cpap\nSleep / CPAP panel"] HTML --> WeightPanel["tab-weight\nWeight panel"] HTML --> SettingsPanel["tab-settings\nSettings panel"] BPPanel --> EntryCard["Entry form\n3-reading grid"] BPPanel --> ChartCard["Trend chart\n(BP/Pulse/MAP/PP)"] BPPanel --> HistoryCard["History table\n(paginated, 7/page)"] SleepPanel --> StatusCard["Sync status badge"] SleepPanel --> CPAPChart["CPAP trend chart\n(Score/AHI/Usage/Leak)"] SleepPanel --> CorrChart["Correlation chart\n(Sleep score vs BP)"] SleepPanel --> CPAPTable["CPAP history table"] WeightPanel --> WeightEntry["Weight entry form"] WeightPanel --> WeightChart["Weight trend chart"] WeightPanel --> WeightTable["Weight history table"] SettingsPanel --> AuthCard["MyAir auth flow"] SettingsPanel --> ProfileCard["Height input"] SettingsPanel --> RefCard["BP reference table"]

Initialisation Sequence

sequenceDiagram participant Page as Page Load participant API as Worker API participant CPAP as CPAP Worker Page->>API: GET /api/blood-pressure?limit=30 Page->>API: GET /api/blood-pressure?limit=7&offset=0 Page->>API: GET /api/weight?limit=30 Page->>API: GET /api/profile Page->>CPAP: GET /api/auth/status Note over Page: All fired in parallel via Promise.all() API-->>Page: Sessions data → BP chart + history table API-->>Page: Weight entries → chart + table API-->>Page: Profile → height field pre-fill CPAP-->>Page: Auth status → sync badge

On page load, Promise.all([loadBpChart(), loadHistory(), loadWeight(), loadProfile()]) fires all initial data requests concurrently. CPAP status is loaded independently.

Tab Management

Tabs use a CSS-class toggle pattern — no routing library:

document.querySelectorAll('.tab').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
    document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
  });
});

Switching to the CPAP tab triggers lazy loading of all CPAP data: loadCpapStatus(), loadCpapChart(), loadCpapTable(), loadCorrelation().

API Client

A thin wrapper around fetch provides consistent request patterns:

const api = {
  get:    (path)       => fetch(path).then(r => r.json()),
  post:   (path, body) => fetch(path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  }).then(r => r.json()),
  put:    (path, body) => fetch(path, { method: 'PUT', ... }).then(r => r.json()),
  delete: (path)       => fetch(path, { method: 'DELETE' }).then(r => r.json()),
};

Blood Pressure Tab

Reading Entry Form

A 3×3 grid accepts systolic, diastolic, and pulse for each of the three required readings.

flowchart TD Form["User fills 3-reading grid\n(sys / dia / pulse × 3)"] Submit["Click 'Save readings'"] ClientVal["validateClient(readings)\n- All fields present\n- diastolic < systolic"] ClientFail["Show error in #status-msg"] APIPost["api.post('/api/blood-pressure', {readings, context, notes})"] APIFail["Show API error"] Success["Show best reading + category\nClear form\nReload chart + history"] Form --> Submit Submit --> ClientVal ClientVal -->|Fails| ClientFail ClientVal -->|Passes| APIPost APIPost -->|data.error| APIFail APIPost -->|Success| Success

BP Trend Chart

The chart displays up to 30 sessions, reversed to chronological order. Four metric views are available via toggle buttons:

ButtonDatasetsColour
BPSystolic + DiastolicRed + Blue
PulseHeart rate (bpm)Purple
MAPMean arterial pressureTeal
Pulse pressurePP (mmHg)Orange

Chart instances are destroyed and recreated on each reload (if (bpChart) bpChart.destroy()).

History Table

Paginated at 7 sessions per page. Each row shows: date, best reading (sys/dia), pulse, MAP, AHA category badge, context. A delete button fires a confirmation prompt then calls DELETE /api/blood-pressure/:id.

Category badges use a dynamically generated light background colour:

function hexToLight(hex) {
  const r = parseInt(hex.slice(1,3),16), g = parseInt(hex.slice(3,5),16), b = parseInt(hex.slice(5,7),16);
  return `rgba(${r},${g},${b},0.12)`;
}

Sleep / CPAP Tab

Sync Status Badge

Reads GET /api/auth/status from the external CPAP worker. Displays one of three states:

StateBadge ClassCondition
Activesync-badge okstatus.last_sync present
Re-auth neededsync-badge errorstatus.needs_reauth === true
Pending first syncsync-badge warnNo last_sync, no reauth needed

CPAP Trend Chart

Four metric views toggled by buttons:

ButtonKeyTransformColour
Scoremyair_scoreNonePurple
AHIahiNoneRed
Usageusage_minutes÷ 60 → hoursBlue
Mask leakmask_leak_pctNoneOrange

Correlation Chart (Mixed type)

A combined Chart.js chart using two y-axes:

graph LR Y1["Y-axis left: Sleep score (bar)\n(0–100)"] Y2["Y-axis right: BP mmHg (line)\nSystolic · Diastolic"] X["X-axis: Dates"]

Data comes from GET /api/cpap-correlation on the CPAP worker, which joins CPAP nights with the following morning’s first blood pressure reading.

Weight Tab

Simple entry → chart → table flow. No pagination — the 30 most recent entries are loaded. Weight is displayed in kg with an optional notes column.

Settings Tab

MyAir Authentication

Two-step flow managed entirely in the UI:

stateDiagram-v2 [*] --> Idle Idle --> SendingCode : Click "Send code" SendingCode --> CodeSent : POST /api/auth/start → mfa_required SendingCode --> Authenticated : POST /api/auth/start → no MFA needed CodeSent --> Verifying : User enters code, clicks "Verify" Verifying --> Authenticated : POST /api/auth/verify → success Verifying --> CodeSent : POST /api/auth/verify → error Authenticated --> [*]

Step 2 (code entry) is hidden via display: none until mfa_required is returned.

Profile

Height entry (cm) persisted via PUT /api/profile. Pre-populated from GET /api/profile on load.

BP Reference Table

A static reference table of AHA/ACC 2017 categories rendered in the Settings tab for user reference.

Design System

The CSS is organised around custom properties (CSS variables):

--ink:         /* Primary text colour */
--ink-muted:   /* Secondary / placeholder text */
--surface:     /* Card/panel background */
--border:      /* Subtle borders */
--accent:      /* Interactive elements */

/* Typography */
--font-mono:   'DM Mono', monospace   /* Numbers, data, code */
--font-body:   'Fraunces', serif      /* Body text, headings */

/* Spacing */
--space-*:     /* 4px scale */

/* Shadows */
--shadow-*:    /* Layered box-shadow tokens */

Chart containers have a fixed height of 260px to ensure consistent layout across viewport sizes. All charts are responsive: true, maintainAspectRatio: false.

Status Messages

A shared setStatus(id, message, type) utility updates any status element:

function setStatus(id, msg, type = '') {
  const el = document.getElementById(id);
  if (!el) return;
  el.textContent = msg;
  el.className = type;  // 'ok' | 'err' | ''
}

Elements with class ok render in green; err renders in red.

External Dependencies (CDN)

LibraryVersionUsage
Chart.js4.xAll charts
Google FontsDM Mono, Fraunces

No bundler, no transpilation, no npm packages in the frontend — everything runs natively in the browser.