fix: health-sync.js HealthUnit method names + serve as inline text

- HealthUnit.gram(prefix) → HealthUnit.gramUnit(prefix) (Scripting app API)
- HealthUnit.millimeterOfMercury() → HealthUnit.fromString('mmHg')
- Updated header: Scriptable → Scripting app
- content-disposition: inline instead of attachment (fixes download issue)
This commit is contained in:
Anthony Merlo 2026-08-22 22:43:16 +01:00
parent 3319a6b233
commit 067bf7a769
3 changed files with 32 additions and 21 deletions

View file

@ -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."}}

View file

@ -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 ===");

View file

@ -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,
)