Skip to content

Commit dd952f2

Browse files
committed
docs: sync telemetry docs to long-format rewrite
Update the 3 user-facing docs (MOBILE_SYNC_APP upload payload, INTEGRATIONS_FRAMEWORK telemetry-split helper, PROJECT_STRUCTURE scripts list) + CHANGELOG to match the long-format hypertable. Local skill files (.opencode/skills/, gitignored) also synced on disk for AI accuracy.
1 parent ed92132 commit dd952f2

4 files changed

Lines changed: 15 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
1313
## [Unreleased]
1414

15+
- **Docs: sync skills + docs to the telemetry long-format rewrite.** Updates 4 skill files (the AI's source of truth — `clinical-data` §4.1-4.6 + debugging/recap, `project-overview` §3.4, `backend` model table + `migrate_biomarker_data` row + `sync_active_integrations` row + deleted-scripts note, `integrations` §8.4) and 3 user-facing docs (`MOBILE_SYNC_APP.md` upload payload → long-format `{timestamp, slug, value, unit?, patient_id?}`, `INTEGRATIONS_FRAMEWORK.md` telemetry-split helper, `PROJECT_STRUCTURE.md` scripts count/list) so they no longer describe the dropped `heart_rate`/`steps`/`calories` dedicated columns + JSONB `data` catch-all, the deleted `_METRIC_COLUMNS` alias map, the deleted `scripts/migrate_heart_rate.py`, or the removed `device_id → UserIntegration → user_id → Patient` attribution chain (now `patient_id` is read directly off each telemetry row). Without this, the next AI session touching telemetry would get stale guidance.
16+
1517
- **Change (breaking): telemetry hypertable rewritten to long-format (drops dedicated `heart_rate`/`steps`/`calories` columns + JSONB catch-all).** The `telemetry_data` TimescaleDB hypertable is rebuilt in the canonical long-format shape — one row per `(timestamp, device, slug)` with explicit `value Float` + `unit Text` + `patient_id UUID` columns. Replaces the asymmetric wide+JSONB hybrid (dedicated Float columns for 3 "first-class" metrics + a JSONB `data` catch-all that gave every other telemetry biomarker — SpO2, glucose, sleep stages — second-class storage). **Modularity win:** adding a new telemetry biomarker is now a row-only change — flip `BiomarkerDefinition.is_telemetry = True` and the integration sync starts emitting rows with no DDL and no service-layer branching (previously required editing ~5 files: the `_METRIC_COLUMNS` alias map, `apply_telemetry_split`'s slug→column branch, `analytics_service`'s dual-path SQL, and writing new CAgg DDL). The new `tests/test_telemetry_new_biomarker_regression.py` verifies `spo2` (a brand-new biomarker with no historical dedicated column) flows end-to-end through upload → read → summary → trends with **zero code changes**. **Continuous aggregates are generic:** the two hardcoded CAggs (`AVG(heart_rate), AVG(steps), AVG(calories)`) are replaced by three generic `GROUP BY slug` CAggs (hourly/daily/monthly — the monthly CAgg covers the `last-12-months`/`all-time` analytics buckets that previously hit the raw hypertable). One definition covers every current and future telemetry biomarker. **Patient attribution simplified:** `patient_id` is now persisted on every telemetry row at insert time, killing the fragile `device_id → UserIntegration → user_id → Patient` resolver chain in `migrate_biomarker_data` (~100 lines deleted); telemetry→FHIR migration now reads `tr.patient_id` directly. **Upload contract (breaking):** `TelemetryDataPoint` is now `{timestamp, slug, value, unit?, patient_id?}` — one point per metric/timestamp (was: one point with `heart_rate`/`steps`/`calories`/`data` fields). The integration SDK already produces one Observation per metric, so the mapping is 1:1. `upload_telemetry_data` now uses SQLAlchemy Core bulk insert with `ON CONFLICT DO NOTHING` chunked at 5000 rows. **Hot-path indexes:** `(tenant_id, slug, timestamp)` for analytics + `(tenant_id, device_id, timestamp)` for per-device reads; compression segmented by `tenant_id, device_id, slug`. **Files:** new migration `t1e2l3o4n5g6_telemetry_long_format` + baseline `8ddb7ef7ca4d` updated so fresh installs match; `models/telemetry_model.py`, `schemas/telemetry.py`, `services/telemetry_service.py`, `services/integration_sync_service.py`, `services/analytics_service.py`, `services/import_service.py`, `workers/tasks.py`, `api/v1/endpoints/telemetry.py` rewritten; `scripts/migrate_heart_rate.py` + `scripts/delete_migrated_heart_rate.py` deleted (superseded); 4 test files rewritten + 1 new regression test + 3 collateral test fixes. 2715 backend tests pass; ruff clean. See `dev/plans/telemetry-long-format-2026-07-28.md` for the full plan.
1618

1719
- **Feature: parallel `test`-branch deployment in the Gitea Actions pipeline + parameterised compose container names.** The deploy workflow (`.gitea/workflows/deploy.yml`) now triggers on pushes to `test` as well as `main`/`master`, and a `test` push deploys a **fully separate stack on the same host** — driven by the `TEST_*` Gitea secrets. Branch-aware `env:` selects, per `github.ref_name == 'test'`: `STACK_NAME` (`health-assistant-test` vs `health-assistant`), `DEPLOY_PATH` (`TEST_DEPLOY_PATH` vs `DEPLOY_PATH`), the five host ports (`TEST_BACKEND_PORT`/`TEST_FRONTEND_PORT`/`TEST_FLOWER_PORT`/`TEST_POSTGRES_PORT`/`TEST_REDIS_PORT` vs the unprefixed defaults), and a branch-specific floating image tag (`:test` vs `:latest`, so the two branches no longer thrash the same `:latest` pointer — deploys always pin to `${{ github.sha }}` regardless). Distinct per-branch fallbacks prevent a missing secret from silently reusing a production port. **Critical compose change:** `docker/docker-compose.prod.yml` had hardcoded `container_name:` on all 8 services (`health-assistant-postgres` etc.), which bypass Docker Compose's project-name namespacing — two stacks on one host would have collided at `docker compose up` ("container name already in use"). All eight are now `${STACK_NAME:-health-assistant}-<service>`, so the production stack is **byte-for-byte unchanged** (default prefix = current names) while the test stack gets `health-assistant-test-*`. Named volumes and the bridge network were already isolated by the compose project name (= deploy-dir basename), so only `container_name` needed parameterising. `concurrency: deploy-${{ github.ref }}` already scoped per-branch, so main and test runs don't cancel each other. App-level secrets (SECRET_KEY, POSTGRES_PASSWORD, VAPID, AI keys, SMTP) remain shared across both branches by design — only deployment topology is branched.

docs/INTEGRATIONS_FRAMEWORK.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Whether data is pulled or pushed, the framework expects the integration to retur
6969

7070
**Frequency-based routing (FHIR vs TimescaleDB):** observations linked to a `BiomarkerDefinition` flagged `is_telemetry=True` are routed to the `telemetry_data` TimescaleDB hypertable (heart rate, steps, CGM, etc.); everything else lands in the standard `fhir_observations` table. This routing is the telemetry-split step inside `IntegrationSyncService.run_sync` (`apply_telemetry_split`), which is called by the background task and the manual sync endpoint (both via `run_sync`). The webhook handler inlines the same logic — the only persistence path not currently routed through `run_sync`, but the post-sync notification dispatch is now shared via `post_sync_notifications` (see `integration_sync_service.py`).
7171

72-
The helper stamps the `performer` reference (`Integration/<id>`) on FHIR rows that don't already have one, and routes the long-tail of telemetry slugs (anything without a dedicated `heart_rate`/`steps`/`calories` column) into the row's JSONB `data` payload alongside its unit.
72+
The helper stamps the `performer` reference (`Integration/<id>`) on FHIR rows that don't already have one, and emits one **long-format** `TelemetryDataModel` row per telemetry observation (`slug`/`value`/`unit`/`patient_id` columns) — uniform across every biomarker, no dedicated-column or JSONB-catch-all branching.
7373

7474
### 7. Interactive Documentation Rendering
7575
When a user views an integration's details in the UI, the frontend requests `/api/v1/integrations/{domain}/documentation`. The backend checks the integration's root folder for a `docs/docs-tree.json`. If found, it parses the JSON tree and returns it alongside the requested markdown file. The frontend uses this metadata to dynamically render a sidebar navigation menu, allowing users to browse complex SDK references or setup guides without leaving the platform.

docs/MOBILE_SYNC_APP.md

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,34 +84,28 @@ The mobile app must read the fragmented native data formats (which vary heavily
8484
Requests must include standard Authorization headers (e.g., `Bearer <JWT_TOKEN>`) configured against the user's specific self-hosted tenant.
8585

8686
### Proposed JSON Payload Schema (`WearableSyncPayload`)
87-
The proposed payload is a highly normalized, time-series array of `WearableDataPoint` objects.
87+
The proposed payload is a highly normalized, time-series array of `WearableDataPoint` objects**long-format**: one point per `(timestamp, slug)`.
8888

8989
```json
9090
{
9191
"device_id": "iPhone_15_Pro_Max",
9292
"points": [
93-
{
94-
"timestamp": "2026-06-11T14:30:00Z",
95-
"heart_rate": 72.5,
96-
"steps": 150,
97-
"calories": null,
98-
"data": {
99-
"spo2": 98.0,
100-
"sleep_stage": "deep"
101-
}
102-
}
93+
{ "timestamp": "2026-06-11T14:30:00Z", "slug": "heart-rate", "value": 72.5, "unit": "bpm" },
94+
{ "timestamp": "2026-06-11T14:30:00Z", "slug": "steps", "value": 150 },
95+
{ "timestamp": "2026-06-11T14:30:00Z", "slug": "spo2", "value": 98.0, "unit": "%" },
96+
{ "timestamp": "2026-06-11T14:30:00Z", "slug": "sleep-stage", "value": 3, "unit": "stage" }
10397
]
10498
}
10599
```
106100

107101
#### Field Definitions:
108102
* `device_id` (String): An identifier to track which phone/watch sourced the data.
109-
* `points` (Array): Time-series metrics.
103+
* `points` (Array): Time-series metrics (one point per metric/timestamp).
110104
* `timestamp` (String): ISO 8601 UTC timestamp.
111-
* `heart_rate` (Float, Optional): Beats per minute.
112-
* `steps` (Float, Optional): Step count for that specific timestamp interval.
113-
* `calories` (Float, Optional): Active kilocalories burned.
114-
* `data` (Object, Optional): A dynamic JSON payload to capture unstandardized or advanced metrics (e.g., blood oxygen, HRV, specific sleep metadata) that the backend can parse flexibly.
105+
* `slug` (String): Biomarker slug (e.g. `heart-rate`, `steps`, `spo2`, `sleep-stage`).
106+
* `value` (Float): The numeric measurement.
107+
* `unit` (String, Optional): Unit symbol (e.g. `bpm`, `%`).
108+
* `patient_id` (UUID, Optional): Patient attribution; when omitted the row is attributed later via the device→integration chain.
115109

116110
---
117111

docs/PROJECT_STRUCTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ Health Assistant/
3030
│ ├── data/seeds/ # JSON seed files (10: concepts, diseases, medications, vaccines,
3131
│ │ # clinical_event_types, allergies, anatomy_structures,
3232
│ │ # concept_edges, default_catalog, biomarker_panels)
33-
│ ├── scripts/ # Admin + maintenance scripts (~22: seed_demo, create_system_admin,
34-
│ │ # export_seeds, encrypt_existing_api_keys, migrate_heart_rate, …)
33+
│ ├── scripts/ # Admin + maintenance scripts (~20: seed_demo, create_system_admin,
34+
│ │ # export_seeds, encrypt_existing_api_keys, …)
3535
│ ├── tests/ # pytest suite (1800+ tests)
3636
│ ├── requirements.txt
3737
│ └── pyproject.toml

0 commit comments

Comments
 (0)