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
| File | Size | Purpose |
|---|---|---|
frontend/index.html | 227 lines | Page shell, tab structure, all form markup |
frontend/app.js | 523 lines | All logic: API calls, charting, tab management, validation |
frontend/style.css | ~9 KB | Design system, layout, typography |
Page Structure
Initialisation Sequence
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.
BP Trend Chart
The chart displays up to 30 sessions, reversed to chronological order. Four metric views are available via toggle buttons:
| Button | Datasets | Colour |
|---|---|---|
| BP | Systolic + Diastolic | Red + Blue |
| Pulse | Heart rate (bpm) | Purple |
| MAP | Mean arterial pressure | Teal |
| Pulse pressure | PP (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:
| State | Badge Class | Condition |
|---|---|---|
| Active | sync-badge ok | status.last_sync present |
| Re-auth needed | sync-badge error | status.needs_reauth === true |
| Pending first sync | sync-badge warn | No last_sync, no reauth needed |
CPAP Trend Chart
Four metric views toggled by buttons:
| Button | Key | Transform | Colour |
|---|---|---|---|
| Score | myair_score | None | Purple |
| AHI | ahi | None | Red |
| Usage | usage_minutes | ÷ 60 → hours | Blue |
| Mask leak | mask_leak_pct | None | Orange |
Correlation Chart (Mixed type)
A combined Chart.js chart using two y-axes:
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:
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)
| Library | Version | Usage |
|---|---|---|
| Chart.js | 4.x | All charts |
| Google Fonts | — | DM Mono, Fraunces |
No bundler, no transpilation, no npm packages in the frontend — everything runs natively in the browser.