Meltdown Minder — Current Architecture & Capabilities¶
Last updated: 2026-08-12
Tags: category:app tier:0
Product Purpose¶
Meltdown Minder detects rising emotional dysregulation in neurodivergent children early enough for caregivers and therapists to intervene before a meltdown escalates. It pairs biometric data from the WHOOP 5 strap with clinician-labeled emotional states (Zones of Regulation + ABC events) to train per-child arousal-detection models.
Phase progress: - Phase 1 — Event logging (current): After-school check-ins, event timeline, trigger analysis, weekly summaries - Phase 2 — Tantrum prediction + coaching (in progress): WHOOP BLE ingestion, arousal detection, real-time alerts, parenting-style-aware coaching prompts - Phase 3 — Advanced (future): Population models, therapist dashboard, multi-wearable support, AI summaries
Architecture Overview¶
Three Azure Container Apps within a single ACA Environment:
meltdownminder.com
│
┌─────┴──────────────────────────────────┐
│ Azure Container Apps Environment │
│ Static IP: 4.153.57.168 │
│ Region: eastus2 │
│ │
│ ┌────────────────────┐ │
│ │ meltdownminder- │ HTTPS :80 │
│ │ prod-frontend │←───────── │
│ │ nginx → proxy_pass │ │
│ └────────┬───────────┘ │
│ │ http://<app>:8000 │
│ ┌────────┴───────────┐ gRPC TLS :443│
│ │ meltdownminder- │───────────────→│
│ │ prod-backend │ │
│ │ FastAPI + SQLite │ │
│ └────────────────────┘ │
│ │ gRPC (internal) │
│ ┌────────┴───────────┐ │
│ │ meltdownminder- │ │
│ │ prod-ingestor │ │
│ │ Rust + DuckDB │ │
│ └────────────────────┘ │
└────────────────────────────────────────┘
| Container App | Technology | Port | CPU/RAM | Replicas |
|---|---|---|---|---|
| Frontend | nginx + React PWA | 80 | 0.25 / 0.5Gi | 1 |
| Backend | FastAPI + SQLAlchemy | 8000 | 0.5 / 1.0Gi | 1 |
| Ingestor | Rust + DuckDB (gRPC) | 50051 | 0.25 / 0.5Gi | 1 |
Frontend (frontend/)¶
Stack: React 19, Vite 8, TypeScript 6, Tailwind 4 (CSS-first), shadcn/ui, TanStack React Query 5, React Router 7, Playwright e2e
Routes¶
Public pages (wrapped in PublicLayout):
| Route | Page | Purpose |
|---|---|---|
| /login | LoginPage | Email/password login, demo hint |
| /register | RegisterPage | Registration with auto-login |
| /about | AboutPage | Mission, features overview |
| /privacy | PrivacyPage | Full privacy policy (HIPAA, CCPA) |
| /terms | TermsPage | Terms of Service |
| /research | ResearchPage | Evidence: studies, methodology, clinical glossary |
| /help/contact | ContactPage | Contact form → POST /api/notifications/contact |
| /hipaa, /accessibility, /impact, /careers, /press | Static pages |
Parent pages (wrapped in AppShell with 6-tab bottom nav):
| Route | Page | Purpose |
|---|---|---|
| / | Dashboard | Child cards, zone badges, regulation sparklines, alert banners |
| /monitor | Monitor | Real-time ZoneGauge, HR/metrics, alert list with AI suggestions |
| /recap | DailyRecap | Event timeline, zone events, patterns |
| /child | ChildDetails | CRUD child profiles (diagnoses, triggers, sensory profiles) |
| /insights | Insights | Weekly trigger analysis, trend charts |
| /calmspace | CalmSpace | Grounding exercises, breathing tools, SMS enrollment |
| /parenting-style | ParentingStyle | Select style (authoritative/gentle/positive discipline/attachment) |
| /pair-device | PairDevice | WHOOP band pairing codes |
| /profile | ProfilePage | User profile, email management, password change |
| /whoop-ble | WhoopBLETest | Dev: Web Bluetooth WHOOP 5 connection (raw frame viewer) |
| /whoop-data | WhoopDataView | Debug: raw WHOOP readings from DuckDB |
Clinician pages:
| Route | Page | Purpose |
|---|---|---|
| /clinician | ClinicianDashboard | Patient list, labeling sessions, ABC event review |
| /clinician/child/:id | ChildView | Per-child diagnoses, strategies, session history |
| /labeling | LabelingTool | ABC Antecedent-Behavior-Consequence labeling tool |
Auth Flow¶
- No
<ProtectedRoute>wrapper — pages calluseAuth()and self-check AuthContext.tsxprovides{ user, loading, login, register, logout, refreshUser }- Tokens (access + refresh) stored in
localStorage api.tsintercepts 401s, queues refresh requests, retries- Demo mode: auto-attempts
parent@demo.com/demo1234, shows "Demo" badge in header
Key Components¶
- AppShell.tsx — Sticky header (branding, demo badge, avatar dropdown) + 6-tab sub-nav
- ZoneGauge.tsx — Circular SVG gauge (0-100, color by zone: blue < 25, green < 50, yellow < 75, red >= 75)
- TrendSparkline.tsx — SVG polyline sparkline for regulation score history
- SuggestedResponseSheet.tsx — Bottom sheet with parenting-style-aware response cards
- Footer.tsx — 6-section footer, demo data loader, PWA install prompt
- 22 shadcn/ui components in
src/components/ui/
WHOOP BLE Web Worker¶
src/workers/whoopWorker.ts (288 lines, @ts-nocheck):
- Parses WHOOP 5 BLE frames (packet types: K10, K16/K17/K18/K24, R22, T40/T47)
- Computes pseudo-RMSSD from rolling HR buffer
- Flushes accumulated readings every 500ms via POST /api/whoop/data/batch
- Max batch size: 200 readings
- Communicates with main thread via postMessage
PWA Features¶
- Service worker: network-first for app shell, cache-first for static assets, bypasses
/apiand/ws - Web manifest: standalone display, SVG icons, warm-bg theme
beforeinstallprompthandler (useInstallPrompt)- Deep-link handler via
__tcDeepLink
Backend (backend/)¶
Stack: FastAPI async, SQLAlchemy async, Alembic, Python 3.13, Pydantic v2
All API Routes (by prefix)¶
POST /api/auth/* — Auth¶
| Method | Endpoint | Rate Limit | Auth |
|---|---|---|---|
| POST | /register |
100/min | None |
| POST | /login |
10/min | None (5-fail lockout, 15min) |
| POST | /refresh |
— | None (refresh token) |
| POST | /logout |
— | Required |
| GET | /me |
— | Required |
| PUT | /profile |
— | Required |
| POST | /change-password |
— | Required |
| POST | /emails |
— | Required |
| DELETE | /emails/{id} |
— | Required |
| PUT | /emails/{id}/primary |
— | Required |
| POST | /delete-account |
3/hour | Required |
GET/POST/PUT/DELETE /api/children* — Children + Readings + Trends¶
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/children/status |
Dashboard card statuses (zone, score, trend) |
| GET | /api/children |
List children |
| POST | /api/children |
Create child |
| GET/PUT/DELETE | /api/children/{id} |
CRUD single child |
| GET | /api/children/{id}/readings |
List sensor readings (filtered by start/end/limit) |
| POST | /api/children/{id}/readings |
Create reading (triggers alert + notification) |
| GET | /api/children/{id}/trends |
Aggregated trends (days param, 1-90) |
| GET | /api/children/{id}/recap |
Daily recap (alerts, readings, zone counts) |
GET/PATCH /api/alerts — Alerts + Interventions¶
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/alerts |
List alerts (filtered by child_id/status) |
| GET | /api/alerts/{id} |
Single alert |
| PATCH | /{id}/acknowledge |
Acknowledge |
| PATCH | /{id}/snooze |
Snooze |
| PATCH | /{id}/dismiss |
Dismiss (sets resolved_at) |
| PATCH | /{id}/resolve |
Resolve (sets resolved_at) |
| POST | /api/alerts/interventions |
Log intervention |
| GET | /api/alerts/interventions |
List interventions |
GET/POST/DELETE /api/devices — Device Pairing¶
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/devices/{child_id} |
List paired devices |
| POST | /api/devices/{child_id} |
Register device |
| DELETE | /api/devices/{child_id}/{device_id} |
Remove device |
| POST | /api/devices/{child_id}/generate-token |
Generate 5-min pairing token |
| POST | /api/devices/claim-token |
Claim device via token |
| PATCH | /api/devices/{child_id}/{device_id}/heartbeat |
Update last_seen_at |
POST/GET /api/labeling/* — Therapy Labeling¶
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/labeling/sessions |
Start session |
| GET | /api/labeling/sessions |
List sessions |
| GET | /api/labeling/sessions/{id} |
Single session |
| PATCH | /api/labeling/sessions/{id}/end |
End session |
| POST | /api/labeling/zone-events |
Record zone event |
| POST | /api/labeling/abc-events |
Record ABC event |
| GET | /api/labeling/sessions/{id}/review |
Full session review |
POST /api/ai/* — AI Suggestions¶
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/ai/suggest |
Get parenting suggestion (MD5-based pool) |
| POST | /api/ai/regenerate |
Get adjacent suggestion |
POST /api/notifications/* — Notifications¶
| Method | Endpoint | Rate Limit | Auth |
|---|---|---|---|
| POST | /api/notifications/sms |
— | Required |
| POST | /api/notifications/email |
— | Required |
| POST | /api/notifications/inbound |
— | None (Telnyx webhook, Ed25519 verified) |
| GET | /api/notifications/messages |
— | Required |
| POST | /api/notifications/contact |
10/min | None (public) |
| POST | /api/notifications/subscribe |
— | None (public) |
POST /api/whoop/* — WHOOP Ingestion¶
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/whoop/data |
Single reading (→ gRPC + SensorReading) |
| POST | /api/whoop/data/batch |
Batch readings (500ms flush, 200 max) |
| GET | /api/whoop/raw/{child_id} |
Raw DuckDB data (for ML export) |
POST /api/seed/demo — Demo Data (dev only)¶
- Creates 2 users, 2 children, 72h sensor readings, 30 days of alerts
POST /api/sim/* — Simulation (dev only)¶
- Start/stop/speed/reset/trigger simulated sensor data
GET /api/stakeholder/* — Clinician Access¶
- List shared children, read/create clinical notes
GET /api/ml/export — ML Training Export¶
- Export intervention data with preceding sensor readings
Unauthenticated¶
| Endpoint | Purpose |
|---|---|
GET /health |
Health check |
GET /api/whoop/ingestor-health |
Ingestor connectivity + uptime |
WebSocket /ws |
Real-time events (auth via first message: JWT) |
Database Models (15 tables)¶
| Table | Key Columns | Purpose |
|---|---|---|
users |
id, email, password_hash, role, phone_number, failed_login_attempts, locked_until | Parent/clinician accounts |
children |
id, parent_id, name, date_of_birth, diagnoses, triggers, sensory_profile, preferred_strategies, alert_sensitivity | Child profiles |
sensor_readings |
id, child_id, timestamp, heart_rate, hrv, eda, movement, skin_temp, regulation_score, data_quality | Aggregated biometric snapshots |
alerts |
id, child_id, level (BLUE/GREEN/YELLOW/RED), regulation_score, predicted_meltdown_type, ai_suggestion, status | Dysregulation alerts |
interventions |
id, alert_id, parent_id, intervention_used, outcome_rating, time_to_resolution_min | Caregiver interventions |
labeling_sessions |
id, child_id, therapist_id, status, start_time, end_time | Therapy labeling sessions |
zone_events |
id, session_id, zone, trigger | Zones of Regulation events |
abc_events |
id, session_id, antecedent, behavior, consequence, severity, recovery_minutes | ABC behavioral events |
clinical_notes |
id, stakeholder_id, child_id, note_text, recommended_strategies | Clinician notes (PHI) |
stakeholder_access |
id, stakeholder_id, child_id, granted_by, access_level | Cross-user data sharing |
devices |
id, child_id, device_name, device_type, device_identifier | WHOOP/pairing device registry |
audit_logs |
id, timestamp, actor_id, action, resource_type, resource_id, child_id, summary, ip_address | HIPAA audit trail |
revoked_tokens |
id, jti, token_type, expires_at | JWT revocation |
user_emails |
id, user_id, email, is_primary | Multi-email support |
inbound_messages |
id, from_number, body, received_at, raw_payload | Inbound SMS storage |
email_subscriptions |
id, email, source, subscribed_at, unsubscribed_at | Newsletter/waitlist |
opt_outs |
id, phone_number, opted_out | SMS opt-out registry |
Key Services¶
| Service | File | Purpose |
|---|---|---|
| arousal_service | app/services/arousal_service.py |
Heuristic regulation score (hr=0.3, hrv=0.4, motion=0.15, temp=0.15), zone classification, alert creation |
| ml_service | app/services/ml_service.py |
XGBoost ONNX inference → regulation score (falls back to heuristic) |
| whoop_ingestor | app/services/whoop_ingestor.py |
gRPC client to Rust sidecar (ingest_batch, query_readings, ping) |
| notification_service | app/services/notification_service.py |
SMS (Telnyx) + email (Azure ACS) alert dispatch |
| sms_processor | app/services/sms_processor.py |
Parse inbound SMS: zone keywords, ABC format, severity, recovery time |
| alert_followup | app/services/alert_followup.py |
5-min delayed follow-up SMS (30-min debounce) |
| audit_logger | app/services/audit_logger.py |
HIPAA e-PHI access logging |
| seed | app/services/seed.py |
Demo data generator (2 users, 2 children, 72h readings) |
app/services/email.py |
Azure Communication Services email sender | |
| sensor_registry | app/services/sensor_registry.py |
Pluggable sensor adapters |
Middleware & Auth¶
- AuthDep (
app/middleware/auth.py): JWT Bearer token →AuthContext(user_id, role, email, name, token_jti). OptionalAuthOptDepfor unauthenticated access. Role-based guard viarequire_role("PARENT", "STAKEHOLDER"). - AuditMiddleware (
app/middleware/audit.py): Logs all POST/PUT/PATCH/DELETE and 4xx/5xx responses. SetsX-Audit-Idheader. - Rate Limiting (
app/rate_limit.py): slowapi in-memory limiter./login= 10/min,/register= 100/min,/delete-account= 3/hour,/contact= 10/min.
Background Tasks¶
- _promote_loop() (every 60s): Backfills
regulation_scorefor SensorReading rows that missed it (processes 50 at a time)
WebSocket¶
/wsendpoint: First message must contain{"token": "..."}(JWT). Sends real-time"alert"and"whoop_data"events viaConnectionManager.
Ingestor — Rust gRPC Sidecar (backend/rust/)¶
Stack: Rust + tonic 0.12 + prost 0.13 + DuckDB 1.x (bundled, ~7min build)
gRPC Endpoints (4 RPCs)¶
| RPC | Purpose |
|---|---|
IngestBatch |
Accepts Reading list → writes to DuckDB whoop_raw (deduplicated by timestamp + packet_type + k_value) |
QueryReadings |
Queries DuckDB by child_id + since_unix + limit (used by ML pipeline and promote loop) |
RefreshChildStatus |
Materializes 24h aggregation into child_status table |
Ping |
Health check (status, uptime_secs, total_readings) |
DuckDB Schema: whoop_raw (29 fields)¶
Includes: recorded_at, device_id, heart_rate_bpm, heart_rate_source, rmssd_ms, rr_intervals (INT[]), accel_x/y/z_g, gyro_x/y/z_dps, motion_intensity, optical_sample_count/min/max/flags, temperature_celsius, temperature_source, respiratory_rate, ppg_channel, ppg_waveform (INT[]), packet_type, k_value, body_length, raw_frame_hex, child_id, session_id, zone_label, ingested_at
Indexed on (child_id, recorded_at).
Derived Computations (done in Rust)¶
- RMSSD:
sqrt(mean(squared(RR_diff))) / 10from raw RR intervals - Motion Intensity:
(abs(accel_x) + abs(accel_y) + abs(accel_z)) / 3, clamped to 1.0
Android Phone App (PhoneApp/)¶
Stack: Kotlin, Gradle, Android 10+ (API 29+), foreground service
Purpose: BLE bridge from WHOOP 5 strap to the cloud. Displays live status dashboard.
BLE Flow¶
- Login (email/password → JWT via
/api/auth/login) - Select child from
/api/children - Scan BLE for "WHOOP" prefix, connect GATT (MTU 247, 2M PHY)
- Enable notifications on 3 WHOOP V5 characteristics
- Deframer syncs on
0xAA, validates CRC16 header + CRC32 payload - PacketProcessor dispatches type-40 (HR+RR), type-47 r22 (105fps accel), V18 (gravity+temp), V26 (PPG)
- Uploader flushes every 500ms or 200 readings via
POST /api/whoop/data/batch - Auto-reconnect with exponential backoff, auto-restart on boot
Historical Data Offload¶
State machine: Idle → DisablingR22 → Offloading → ReEnablingR22 → Complete. Sends WHOOP commands to dump historical V18/V26 data using cursor-based pagination.
ML Pipeline (ml/)¶
Stack: Python, XGBoost, Optuna, ONNX
Independence: Runs locally, pulls data from production API (not DuckDB directly)
Pipeline Steps¶
export.py— Pulls raw WHOOP data fromGET /api/whoop/raw/{child_id}→ CSVfeature_engineering.py— 30s sliding windows (10s stride) → 41 features across 7 groups → Parquettrain.py— Optuna-tuned XGBoost (30 trials, 5-fold CV, ROC-AUC objective). Falls back to Isolation Forest if no labels.evaluate.py— Leave-one-session-out CV, lead-time metric, feature ablationmodel.py— ONNX export →backend/models/arousal_model.onnx
Feature Groups (41 total)¶
| Group | Features | Count |
|---|---|---|
| HR | mean, std, min, max, range, trend, percentiles, rate of change, onset/recovery | 11 |
| HRV | rmssd_mean, sdnn, pnn50, mean_rr, delta/min, DFA alpha1, sample entropy | 7 |
| Motion | acc_mag stats, axis ranges, jerk, energy, entropy | 12 |
| PPG | pulse amp mean/std, upstroke slope | 3 |
| Temp | mean, std, slope | 3 |
| Resp | rate mean/std | 2 |
| Coherence | motion-HR coherence, dissociation index | 2 |
Reported Performance (Daniel's model)¶
- Test AUC: 0.9872, Accuracy: 93%
- Missed arousals: 1
- HR dominates ablation (AUC drop 0.138), HRV (0.0037), Motion (0.0050)
Database Architecture¶
Current State¶
| Database | Engine | Data | Volume | Deployment |
|---|---|---|---|---|
| Main app | SQLite (via aiosqlite) | Users, children, alerts, interventions, labeling sessions, clinical notes, sensor readings | Trial: 5 children × 25 sessions | Azure Files persistent volume mount at /data/meltdownminder.db |
| WHOOP raw | DuckDB (Rust sidecar) | Raw 29-field WHOOP frames including RR intervals, PPG waveforms, accelerometer | High-velocity time-series | Sidecar container, file at /data/meltdownminder.duckdb |
| MCP server | SQLite (separate) | mcp.db — agent tools data |
Small | Local file in backend container |
Write Path (CQRS)¶
WHOOP BLE frame → FastAPI /api/whoop/data/batch
├── gRPC IngestBatch → Rust sidecar → DuckDB (whoop_raw) ← Hot path, high fidelity
└── Direct INSERT → SQLite sensor_readings ← Immediate, aggregated
Scaling Consideration¶
SQLite is appropriate for the trial phase but cannot support multiple backend replicas (Azure Files + WAL does not handle concurrent writers). PostgreSQL is the natural upgrade path and is well prepared for by the SQLAlchemy async + Alembic abstraction. The .env.example still references MSSQL (design intent), but production uses SQLite after the Azure SQL server was decommissioned.
Deployment¶
Manual — no CI/CD pipeline.
Initial Deploy¶
powershell -File backend/deploy.ps1
Incremental Updates¶
Per-service: docker build → docker push → az containerapp update (see docs/deployment_runbook.md).
Pre-deploy Checks¶
- Backend:
ruff check app/→mypy app/ - Frontend:
pnpm tsc --noEmit→pnpm build→pnpm test
Security¶
In-App Controls¶
- JWT auth: 15-min access token, 7-day refresh token, revocation on logout
- Rate limiting on auth endpoints
- Account lockout: 5 failed attempts = 15 min
AuditMiddlewarelogs all PHI access (who, what, when, IP)- WebSocket auth via first-message JWT
- All PHI endpoints require
AuthDep+ ownership scoping - HIPAA compliance docs:
docs/Compliance/
Known Gaps (development phase)¶
proxy_ssl_verify offin nginx- Secrets pass through Key Vault but set as plain env vars (not runtime SDK fetch)
- HS256 symmetric JWT signing (no RS256)
localStoragetoken storage (XSS-vulnerable, accepted residual risk)- No VNet isolation
- No automated backup policy
- All Defender plans disabled (cost)
Gotchas¶
| Gotcha | Detail |
|---|---|
| Tailwind v4 | CSS-first — no postcss.config.js, no tailwind.config.*. Theme in globals.css @theme block |
| Auth hooks | AuthContext.tsx is the only live auth. Legacy useAuth.ts deleted (0 imports) |
| Demo mode | Auto-attempts demo login, shows "Demo" badge, seeded PRNG (mulberry32) |
| JWT expiration | config.py defaults 15min, .env.example overrides to 1440min (24h) |
| Rust build | ~7 minutes (DuckDB C lib compiled from source via bundled feature) |
Nginx envsubst |
${BACKEND_URL} required at container start or nginx refuses to boot |
| Docker tags | Use unique timestamps (20260624-210147) — ACA may cache :latest |
| No route guards | Pages call useAuth() and self-check. No <ProtectedRoute> wrapper |
| ignoreDeprecations | frontend/tsconfig.json has "ignoreDeprecations": "6.0" for TS 6 |
| dark mode | next-themes installed but no UI toggle wired |
| WebSocket unused | Frontend has no WebSocket consumer despite proxy and nginx config |