diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index aefe63c..c6bffad 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -13,3 +13,4 @@ {"id":"int-b0e97dffd303b9e4f164830ebcbe2085","kind":"field_change","created_at":"2026-08-20T15:56:04.581867751Z","actor":"Anthony Merlo","issue_id":"nutrition-mcp-aea","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Hermes skill created at ~/.hermes/skills/productivity/nutrition-tracker/SKILL.md with workflow guidance, meal auto-detection, portion handling, protein estimation guide, and gout/hypertension health context."}} {"id":"int-c69e98d9ab404dbe46f1b088e3453677","kind":"field_change","created_at":"2026-08-20T16:25:47.847889101Z","actor":"Anthony Merlo","issue_id":"nutrition-mcp-45t","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"25 integration tests added, all passing. Covers: DB init, goals seeding, FTS5 cache search, upsert, portion scaling, sum_nutriments (with no-data handling), meal detection, date format, log entry round trips, goals update, weight log + upsert. Also fixed sum_nutriments display key bug (was using OFF field names instead of display names) and weight_log UNIQUE constraint."}} {"id":"int-80ae42f606a6a22aef6d8883d3462f72","kind":"field_change","created_at":"2026-08-20T21:57:03.081085913Z","actor":"Anthony Merlo","issue_id":"nutrition-mcp-aav","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"POST /ingest endpoint implemented in src/ingest.rs. Accepts array of health samples (weight, BP, HR, steps, sleep, SpO2). Auth via Bearer token. Tested: 4 samples ingested (weight, BP, steps, HR), 401 on missing auth. 31 tests passing. Server restarted and live. Dependency on 3nl was for vitals tables — ingest uses a generic vitals table with JSON value column, no need to wait for individual table tooling."}} +{"id":"int-d36257ef0a5cb2afb18ece95e6b92bc7","kind":"field_change","created_at":"2026-08-22T21:35:47.560297Z","actor":"Anthony Merlo","issue_id":"hm-zy2.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed 3 API bugs in health-sync.js. Server side verified working. Needs on-device test to fully confirm."}} diff --git a/scripts/health-sync.js b/scripts/health-sync.js index b896f04..6ef8e2a 100644 --- a/scripts/health-sync.js +++ b/scripts/health-sync.js @@ -1,16 +1,25 @@ -// Health Sync — reads Apple Health and POSTs to nutrition-mcp server +// Health Sync — reads Apple Health via Scripting app and POSTs to health-mcp server // Run via Scripting app automation at 9:30am daily // Server: http://chronos:9432/ingest // -// Uses Scripting app's Health API: Health.queryQuantitySamples() with HealthUnit. +// Uses Scripting app's Health API: +// Health.queryQuantitySamples(type, options) — returns samples +// sample.quantityValue(unit) — extracts numeric value +// HealthUnit / HealthMetricPrefix — unit construction +// +// Docs: https://scriptingapp.github.io/guide/Device%20Capabilities/Health/ + +// Variables used by Scripting app. These must be at the very top of the file. +// icon-color: deep-blue; icon-glyph: heart; const SERVER_URL = "http://chronos:9432/ingest"; const API_KEY = "333f09ce904e9cbbe3b578fda393be92869122c7222dbccd"; // --- Units for each quantity type --- +// HealthUnit API: https://scriptingapp.github.io/guide/Device%20Capabilities/Health/HealthUnit const U = { - kg: HealthUnit.gram(HealthMetricPrefix.kilo), - mmHg: HealthUnit.millimeterOfMercury(), + kg: HealthUnit.gramUnit(HealthMetricPrefix.kilo), + mmHg: HealthUnit.fromString("mmHg"), bpm: HealthUnit.count().divided(HealthUnit.minute()), count: HealthUnit.count(), percent: HealthUnit.percent() @@ -153,22 +162,22 @@ if (samples.length === 0) { await notify("No health data found today"); } else { -const req = new Request(SERVER_URL); -req.method = "POST"; -req.headers = { - "Authorization": `Bearer ${API_KEY}`, - "Content-Type": "application/json" -}; -req.body = JSON.stringify({ samples: samples }); + const req = new Request(SERVER_URL); + req.method = "POST"; + req.headers = { + "Authorization": `Bearer ${API_KEY}`, + "Content-Type": "application/json" + }; + req.body = JSON.stringify({ samples: samples }); -try { - const res = await req.loadJSON(); - console.log(`Server response: ${JSON.stringify(res)}`); - await notify(`Sent ${res.ingested || samples.length} samples`); -} catch (e) { - console.log(`POST error: ${e}`); - await notify(`Error: ${e.message || e}`); -} + try { + const res = await req.loadJSON(); + console.log(`Server response: ${JSON.stringify(res)}`); + await notify(`Sent ${res.ingested || samples.length} samples`); + } catch (e) { + console.log(`POST error: ${e}`); + await notify(`Error: ${e.message || e}`); + } } // end else console.log("=== Health Sync done ==="); \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 167051c..64f9432 100644 --- a/src/main.rs +++ b/src/main.rs @@ -108,14 +108,15 @@ async fn health_check() -> &'static str { "OK" } -/// Serve the Scriptable health-sync.js file for easy phone download. +/// Serve the health-sync.js script for easy phone access. /// No auth — it's just a script, no sensitive data. +/// Served as inline text so Safari displays it for copy-paste into Scripting app. async fn serve_scriptable_script() -> impl axum::response::IntoResponse { let script = include_str!("../scripts/health-sync.js"); ( [ ("content-type", "text/javascript; charset=utf-8"), - ("content-disposition", "attachment; filename=\"health-sync.js\""), + ("content-disposition", "inline"), ], script, )