From efc1bc82ab755f83fe06b598af66b92b237c2a3a Mon Sep 17 00:00:00 2001 From: Anthony Merlo Date: Sat, 22 Aug 2026 23:25:58 +0100 Subject: [PATCH] =?UTF-8?q?v10:=20full=20version=20=E2=80=94=20HealthKit?= =?UTF-8?q?=20reading=20+=20POST=20to=20/ingest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/health-sync.tsx | 179 +++++++++++++++++++++++++++++++++++----- 1 file changed, 159 insertions(+), 20 deletions(-) diff --git a/scripts/health-sync.tsx b/scripts/health-sync.tsx index e45fefc..fb89d72 100644 --- a/scripts/health-sync.tsx +++ b/scripts/health-sync.tsx @@ -1,39 +1,178 @@ // @ts-nocheck -// Health Sync v9 — minimal test with timeout + IP fallback +// Health Sync v10 — full version: read HealthKit + POST to /ingest -import { Script } from "scripting" +import { Script, Notification } from "scripting" // icon-color: deep-blue; icon-glyph: heart; async function main() { - console.log("v9: script started") + const SERVER_URL = "http://chronos:9432/ingest" + const API_KEY = "333f09ce904e9cbbe3b578fda393be92869122c7222dbccd" - // Try hostname first, then LAN IP - const urls = [ - "http://chronos:9432/health", - "http://192.168.1.148:9432/health", - "http://100.120.136.16:9432/health" - ] + console.log("v10: script started") - for (const url of urls) { - console.log("v9: trying " + url) + // --- Units --- + const U = { + kg: HealthUnit.gramUnit(HealthUnitPrefix.kilo), + mmHg: HealthUnit.millimeterOfMercury(), + bpm: HealthUnit.count().divided(HealthUnit.minute()), + count: HealthUnit.count(), + percent: HealthUnit.percent() + } + + const readLatest = async (type, unit) => { try { - const response = await fetch(url, { - allowInsecureRequest: true, - timeout: 5 + const samples = await Health.queryQuantitySamples(type, { + limit: 1, + sortDescriptors: [{ key: "startDate", order: "reverse" }] }) - const text = await response.text() - console.log("v9: SUCCESS from " + url + " = " + text) - break + if (!samples || samples.length === 0) { + console.log(`readLatest(${type}): no samples`) + return null + } + const s = samples[0] + const value = s.quantityValue(unit) + console.log(`readLatest(${type}): value=${value} date=${s.startDate}`) + return { value, startDate: s.startDate } } catch (e) { - console.log("v9: FAILED " + url + " = " + e) + console.log(`readLatest(${type}): error ${e}`) + return null } } - console.log("v9: done") + const readTodaySum = async (type, unit) => { + try { + const start = new Date() + start.setHours(0, 0, 0, 0) + const samples = await Health.queryQuantitySamples(type, { + startDate: start, + endDate: new Date() + }) + if (!samples || samples.length === 0) { + console.log(`readTodaySum(${type}): no samples`) + return null + } + let total = 0 + for (const s of samples) { + total += s.quantityValue(unit) + } + console.log(`readTodaySum(${type}): total=${total}`) + return total + } catch (e) { + console.log(`readTodaySum(${type}): error ${e}`) + return null + } + } + + const todayDate = () => new Date().toISOString().slice(0, 10) + + const notify = async (body) => { + console.log(`notify: ${body}`) + await Notification.schedule({ title: "Health Sync", body }) + } + + // --- Read health metrics --- + console.log("v10: reading health metrics...") + + const weight = await readLatest("bodyMass", U.kg) + const bpSys = await readLatest("bloodPressureSystolic", U.mmHg) + const bpDia = await readLatest("bloodPressureDiastolic", U.mmHg) + const hr = await readLatest("heartRate", U.bpm) + const restingHr = await readLatest("restingHeartRate", U.bpm) + const steps = await readTodaySum("stepCount", U.count) + const spo2 = await readLatest("oxygenSaturation", U.percent) + + console.log(`v10: weight=${weight?.value} bpSys=${bpSys?.value} bpDia=${bpDia?.value} hr=${hr?.value} rhr=${restingHr?.value} steps=${steps} spo2=${spo2?.value}`) + + // --- Build samples --- + const samples = [] + + if (weight) { + samples.push({ + type: "weight", + timestamp_utc: weight.startDate.toISOString(), + value: weight.value, + source: "apple_health" + }) + } + + if (bpSys && bpDia) { + const bpValue = { systolic: bpSys.value, diastolic: bpDia.value } + if (hr) bpValue.pulse = hr.value + samples.push({ + type: "blood_pressure", + timestamp_utc: bpSys.startDate.toISOString(), + value: bpValue, + source: "omron" + }) + } + + if (hr && !bpSys) { + samples.push({ + type: "heart_rate", + timestamp_utc: hr.startDate.toISOString(), + value: hr.value, + source: "apple_health" + }) + } + + if (restingHr) { + samples.push({ + type: "resting_heart_rate", + timestamp_utc: restingHr.startDate.toISOString(), + value: restingHr.value, + source: "apple_health" + }) + } + + if (steps !== null) { + samples.push({ + type: "steps", + timestamp_utc: todayDate(), + value: steps, + source: "apple_health" + }) + } + + if (spo2) { + samples.push({ + type: "spo2", + timestamp_utc: spo2.startDate.toISOString(), + value: spo2.value * 100, + source: "apple_health" + }) + } + + console.log(`v10: ${samples.length} samples to send`) + + // --- POST to server --- + if (samples.length === 0) { + await notify("No health data found today") + } else { + try { + const response = await fetch(SERVER_URL, { + method: "POST", + headers: { + "Authorization": `Bearer ${API_KEY}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ samples }), + allowInsecureRequest: true, + timeout: 10 + }) + const res = await response.json() + console.log(`v10: server response = ${JSON.stringify(res)}`) + await notify(`Sent ${res.ingested || samples.length} samples`) + } catch (e) { + console.log(`v10: POST error = ${e}`) + await notify(`Error: ${e.message || e}`) + } + } + + console.log("v10: done") } main().then(() => Script.exit()).catch((e) => { - console.log("v9: main() error = " + e) + console.log("v10: main() error = " + e) Script.exit() }) \ No newline at end of file