Architecture & data
Follow a segment from the camera to the dashboard.
HealthCam separates local capture and inference from cloud ingestion and presentation. The same segment can be processed by multiple analyzers while application context is sampled independently.
Local service lifecycle
booter.py reads enabled service IDs from config.services.json, loads their manifests, allocates ports, and writes a shared registry. Native services run as host processes with separate Python environments; the orchestrator and analyzers run in Docker.
The orchestrator groups registered services by capability: sensor, context, and analyzer. Calling /start activates capture, starts context polling, and launches analysis and dispatch tasks. Booting processes and starting the recording pipeline are separate operations.
| Service | Default port | Main endpoints |
|---|---|---|
| Camera capture | 8000 | POST /start, POST /stop, GET /health, GET /status |
| App tracker | 8002 | GET /context, GET /health, GET /status |
| Orchestrator | 8008 | POST /start, POST /stop, GET /services_status, GET /status, GET /health |
| BPM analyzer | 8009 | POST /analyze, GET /health, GET /status |
| Blink analyzer | 8039 | POST /analyze, GET /health, GET /status |
| Notifications | 8011 | POST /notify, GET /health |
Port assignment avoids duplicates within the enabled set, but does not probe other processes for occupied ports. The generated registry is the source of assigned addresses.
From capture to upload
- FFmpeg writes a video segment; the camera service writes a JSON sidecar with capture configuration and timing.
- The orchestrator finds completed segments and sends the shared local path to enabled analyzers concurrently.
- It combines analysis results with context events whose intervals overlap the segment.
- An in-memory queue separates analysis from cloud dispatch.
- The dispatcher posts JSON to the cloud, then deletes the video and sidecar after a successful response.
The current dispatcher withholds a segment when any analyzer reports an outer error. It retries rejected uploads, but the queue is not durable and ingestion is not idempotent. These are development limitations, not exactly-once delivery guarantees.
Ingestion contract
Send JSON to POST /api/recordings/payloads using X-API-Key. The request has two top-level fields: payload and timezone-aware sent_at.
The payload includes segment metadata, context events, per-analyzer success/error envelopes, and analysis timing. The API validates common structure with Zod, resolves the user from the key, and stores the JSON payload with send/receipt timestamps. Analyzer-specific data remains flexible.
Use the actual schema and valid test fixture when building clients.
| HTTP response | Meaning |
|---|---|
| 201 | Payload persisted |
| 400 | Invalid JSON or contract |
| 401 | Missing or invalid API key |
| 429 | The user has reached the stored-record limit within the current window |
| 500 | Server/database failure |
The current rate limit counts stored records: 30 per user within 60 seconds. It is a basic ingestion guard, not an atomic distributed quota.
Raw telemetry and summaries
PostgreSQL keeps raw JSONB payloads. SQL transformations populate daily BPM summaries by user, day, and app, including sums, counts, extrema, and recorded seconds. Dashboards use sample-weighted averages over those summaries.
The incremental job runs on a five-minute schedule. The daily reconciliation rebuilds a recent window beginning two days before the latest summary date. Older backfills need a deliberately wider rebuild.
Current simplifications matter when interpreting the UI:
- Each segment is assigned to its latest app-context event, rather than divided across app switches.
- Day buckets use send time, with receipt time as a fallback, rather than capture time.
- Per-app charts require more than 30 minutes of recorded duration.
- The distribution chart groups daily app averages weighted by sample count; it is not a raw-measurement histogram.
- Coverage indicators describe the availability of data, not physiological accuracy.
Privacy and design trade-offs
Local inference avoids video uploads and central GPU workloads, but requires managing device permissions, hardware compatibility, native environments, and containers. Application/window context and metadata still leave the device in telemetry.
Keeping raw and derived data supports reprocessing and cheaper dashboard queries, at the cost of duplication and delayed consistency. Independent services allow different runtimes and replaceable analyzers, but add lifecycle, networking, and failure-handling work.
Read the engineering rationale and validation report for the design decisions and experimental limits.