forge-openclaw-plugin 0.2.83 → 0.2.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-BNvUaA6y.js +91 -0
- package/dist/assets/{index-Chc9CWm5.css → index-NqIbz_lv.css} +1 -1
- package/dist/index.html +2 -2
- package/dist/server/server/src/health.js +176 -100
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/assets/index-DEoJdpz5.js +0 -91
package/dist/index.html
CHANGED
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
/>
|
|
14
14
|
<link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
15
15
|
<link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
16
|
-
<script type="module" crossorigin src="/forge/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/forge/assets/index-BNvUaA6y.js"></script>
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-BVU0cZC9.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="/forge/assets/board-DKxKOwax.js">
|
|
19
19
|
<link rel="modulepreload" crossorigin href="/forge/assets/ui-3Wd4pVaA.js">
|
|
20
20
|
<link rel="modulepreload" crossorigin href="/forge/assets/motion-CM4AfIqo.js">
|
|
21
21
|
<link rel="modulepreload" crossorigin href="/forge/assets/table-BUeQ9wzR.js">
|
|
22
22
|
<link rel="stylesheet" crossorigin href="/forge/assets/vendor-B-Lq_OG3.css">
|
|
23
|
-
<link rel="stylesheet" crossorigin href="/forge/assets/index-
|
|
23
|
+
<link rel="stylesheet" crossorigin href="/forge/assets/index-NqIbz_lv.css">
|
|
24
24
|
</head>
|
|
25
25
|
<body class="bg-canvas text-ink antialiased">
|
|
26
26
|
<div id="root"></div>
|
|
@@ -41,7 +41,12 @@ const generatedHealthEventTemplateSchema = z.object({
|
|
|
41
41
|
enabled: z.boolean().default(false),
|
|
42
42
|
workoutType: z.string().trim().min(1).default("workout"),
|
|
43
43
|
title: z.string().trim().default(""),
|
|
44
|
-
durationMinutes: z
|
|
44
|
+
durationMinutes: z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.positive()
|
|
48
|
+
.max(24 * 60)
|
|
49
|
+
.default(45),
|
|
45
50
|
xpReward: z.number().int().min(0).max(500).default(0),
|
|
46
51
|
tags: z.array(z.string().trim()).default([]),
|
|
47
52
|
links: z.array(healthLinkSchema).default([]),
|
|
@@ -110,7 +115,12 @@ const companionSourceStatesSchema = z.object({
|
|
|
110
115
|
export const createCompanionPairingSessionSchema = z.object({
|
|
111
116
|
label: z.string().trim().default("Forge Companion"),
|
|
112
117
|
userId: z.string().trim().nullable().optional(),
|
|
113
|
-
expiresInMinutes: z.coerce
|
|
118
|
+
expiresInMinutes: z.coerce
|
|
119
|
+
.number()
|
|
120
|
+
.int()
|
|
121
|
+
.min(5)
|
|
122
|
+
.max(24 * 60)
|
|
123
|
+
.default(30),
|
|
114
124
|
transportMode: z
|
|
115
125
|
.enum(["iroh", "manual-http"])
|
|
116
126
|
.default("iroh")
|
|
@@ -221,7 +231,11 @@ export const mobileHealthSyncSchema = z.object({
|
|
|
221
231
|
endedAt: z.string().datetime(),
|
|
222
232
|
sourceTimezone: z.string().trim().min(1).default("UTC"),
|
|
223
233
|
localDateKey: z.string().trim().min(1),
|
|
224
|
-
providerRecordType: z
|
|
234
|
+
providerRecordType: z
|
|
235
|
+
.string()
|
|
236
|
+
.trim()
|
|
237
|
+
.min(1)
|
|
238
|
+
.default("healthkit_sleep_sample"),
|
|
225
239
|
rawStage: z.string().trim().min(1),
|
|
226
240
|
rawValue: z.number().int().nullable().optional(),
|
|
227
241
|
payload: z.record(z.string(), z.unknown()).default({}),
|
|
@@ -294,14 +308,13 @@ export const mobileHealthSyncSessionStartSchema = mobileHealthSyncSchema
|
|
|
294
308
|
sourceStates: true
|
|
295
309
|
})
|
|
296
310
|
.extend({
|
|
297
|
-
schemaVersion: z
|
|
298
|
-
.string()
|
|
299
|
-
.trim()
|
|
300
|
-
.default(HEALTH_MOBILE_SYNC_SCHEMA_VERSION),
|
|
311
|
+
schemaVersion: z.string().trim().default(HEALTH_MOBILE_SYNC_SCHEMA_VERSION),
|
|
301
312
|
requestedFamilies: z
|
|
302
313
|
.array(mobileHealthSyncFamilySchema)
|
|
303
314
|
.default(defaultMobileHealthSyncFamilies),
|
|
304
|
-
expectedCounts: z
|
|
315
|
+
expectedCounts: z
|
|
316
|
+
.record(z.string(), z.number().int().nonnegative())
|
|
317
|
+
.default({}),
|
|
305
318
|
metadata: z.record(z.string(), z.unknown()).default({})
|
|
306
319
|
});
|
|
307
320
|
const workoutTimeSeriesChunkPayloadSchema = z.object({
|
|
@@ -355,7 +368,9 @@ export const mobileHealthSyncChunkSchema = z.object({
|
|
|
355
368
|
});
|
|
356
369
|
export const mobileHealthSyncSessionCompleteSchema = z.object({
|
|
357
370
|
finalCursor: z.record(z.string(), z.unknown()).default({}),
|
|
358
|
-
expectedCounts: z
|
|
371
|
+
expectedCounts: z
|
|
372
|
+
.record(z.string(), z.number().int().nonnegative())
|
|
373
|
+
.default({})
|
|
359
374
|
});
|
|
360
375
|
export const verifyCompanionPairingSchema = z.object({
|
|
361
376
|
sessionId: z.string().trim().min(1),
|
|
@@ -535,9 +550,9 @@ function upsertPairingSourceState(pairing, source, patch) {
|
|
|
535
550
|
...patch.metadata
|
|
536
551
|
}
|
|
537
552
|
: safeJsonParse(current.metadata_json, {});
|
|
538
|
-
const nextDesiredEnabled = patch.desiredEnabled ??
|
|
539
|
-
const nextAppliedEnabled = patch.appliedEnabled ??
|
|
540
|
-
const nextSyncEligible = patch.syncEligible ??
|
|
553
|
+
const nextDesiredEnabled = patch.desiredEnabled ?? current.desired_enabled === 1;
|
|
554
|
+
const nextAppliedEnabled = patch.appliedEnabled ?? current.applied_enabled === 1;
|
|
555
|
+
const nextSyncEligible = patch.syncEligible ?? current.sync_eligible === 1;
|
|
541
556
|
const nextUpdatedAt = nowIso();
|
|
542
557
|
getDatabase()
|
|
543
558
|
.prepare(`UPDATE companion_pairing_source_states
|
|
@@ -681,9 +696,7 @@ function computeSleepDerivedMetrics(input) {
|
|
|
681
696
|
})
|
|
682
697
|
.reduce((total, stage) => total + stage.seconds, 0);
|
|
683
698
|
const restorativeShare = input.asleepSeconds > 0 ? restorativeSeconds / input.asleepSeconds : 0;
|
|
684
|
-
const sleepDebtHours = input.asleepSeconds > 0
|
|
685
|
-
? Math.max(0, 8 - input.asleepSeconds / 3600)
|
|
686
|
-
: 8;
|
|
699
|
+
const sleepDebtHours = input.asleepSeconds > 0 ? Math.max(0, 8 - input.asleepSeconds / 3600) : 8;
|
|
687
700
|
return {
|
|
688
701
|
durationHours: round(input.asleepSeconds / 3600, 2),
|
|
689
702
|
efficiency: round(efficiency, 3),
|
|
@@ -888,10 +901,13 @@ function mapSleepRawLog(row) {
|
|
|
888
901
|
createdAt: row.created_at
|
|
889
902
|
};
|
|
890
903
|
}
|
|
891
|
-
function mapWorkoutSession(row) {
|
|
904
|
+
function mapWorkoutSession(row, options = {}) {
|
|
892
905
|
const provenance = safeJsonParse(row.provenance_json, {});
|
|
893
906
|
const derived = safeJsonParse(row.derived_json, {});
|
|
894
|
-
const
|
|
907
|
+
const includeAnalytics = options.includeAnalytics ?? true;
|
|
908
|
+
const analytics = includeAnalytics
|
|
909
|
+
? getStoredWorkoutAnalytics(row)
|
|
910
|
+
: undefined;
|
|
895
911
|
const presentation = buildWorkoutSessionPresentation({
|
|
896
912
|
source: row.source,
|
|
897
913
|
sourceType: row.source_type,
|
|
@@ -937,7 +953,7 @@ function mapWorkoutSession(row) {
|
|
|
937
953
|
annotations: safeJsonParse(row.annotations_json, {}),
|
|
938
954
|
provenance,
|
|
939
955
|
derived,
|
|
940
|
-
analytics,
|
|
956
|
+
...(analytics ? { analytics } : {}),
|
|
941
957
|
generatedFromHabitId: row.generated_from_habit_id,
|
|
942
958
|
generatedFromCheckInId: row.generated_from_check_in_id,
|
|
943
959
|
reconciliationStatus: row.reconciliation_status,
|
|
@@ -1113,7 +1129,10 @@ function inferHistoricalRawStage(payload) {
|
|
|
1113
1129
|
return "asleep_unspecified";
|
|
1114
1130
|
}
|
|
1115
1131
|
function inferHistoricalSleepBucket(stage) {
|
|
1116
|
-
const normalized = stage
|
|
1132
|
+
const normalized = stage
|
|
1133
|
+
.trim()
|
|
1134
|
+
.toLowerCase()
|
|
1135
|
+
.replace(/[\s-]+/g, "_");
|
|
1117
1136
|
if (normalized.includes("bed")) {
|
|
1118
1137
|
return "in_bed";
|
|
1119
1138
|
}
|
|
@@ -1135,7 +1154,7 @@ export function listSleepSessions(userIds) {
|
|
|
1135
1154
|
return listSleepRows(userIds).map(mapSleepSession);
|
|
1136
1155
|
}
|
|
1137
1156
|
export function listWorkoutSessions(userIds) {
|
|
1138
|
-
return listWorkoutRows(userIds).map(mapWorkoutSession);
|
|
1157
|
+
return listWorkoutRows(userIds).map((row) => mapWorkoutSession(row));
|
|
1139
1158
|
}
|
|
1140
1159
|
export function getSleepSessionById(sleepId) {
|
|
1141
1160
|
ensureLegacyAppleSleepHistoryRepaired();
|
|
@@ -1155,22 +1174,24 @@ function sleepHasReflection(session) {
|
|
|
1155
1174
|
tags.length > 0);
|
|
1156
1175
|
}
|
|
1157
1176
|
function sleepEfficiency(session) {
|
|
1158
|
-
return typeof session.derived.efficiency ===
|
|
1177
|
+
return typeof session.derived.efficiency ===
|
|
1178
|
+
"number"
|
|
1159
1179
|
? session.derived.efficiency
|
|
1160
1180
|
: session.timeInBedSeconds > 0
|
|
1161
1181
|
? session.asleepSeconds / session.timeInBedSeconds
|
|
1162
1182
|
: 0;
|
|
1163
1183
|
}
|
|
1164
1184
|
function sleepRestorativeShare(session) {
|
|
1165
|
-
return typeof session.derived
|
|
1166
|
-
"number"
|
|
1185
|
+
return typeof session.derived
|
|
1186
|
+
.restorativeShare === "number"
|
|
1167
1187
|
? session.derived.restorativeShare
|
|
1168
1188
|
: 0;
|
|
1169
1189
|
}
|
|
1170
1190
|
function sleepRecoveryState(session) {
|
|
1171
1191
|
return typeof session.derived.recoveryState ===
|
|
1172
1192
|
"string"
|
|
1173
|
-
?
|
|
1193
|
+
? session.derived.recoveryState ||
|
|
1194
|
+
null
|
|
1174
1195
|
: null;
|
|
1175
1196
|
}
|
|
1176
1197
|
function sleepQualitativeState(session) {
|
|
@@ -1211,7 +1232,9 @@ function sleepStageShare(session) {
|
|
|
1211
1232
|
return session.stageBreakdown.map((stage) => ({
|
|
1212
1233
|
stage: stage.stage,
|
|
1213
1234
|
seconds: stage.seconds,
|
|
1214
|
-
percentage: session.asleepSeconds > 0
|
|
1235
|
+
percentage: session.asleepSeconds > 0
|
|
1236
|
+
? round(stage.seconds / session.asleepSeconds, 3)
|
|
1237
|
+
: 0
|
|
1215
1238
|
}));
|
|
1216
1239
|
}
|
|
1217
1240
|
function buildSleepSurfaceNight(session, baselineAverageSleepSeconds) {
|
|
@@ -1239,8 +1262,8 @@ function buildSleepSurfaceNight(session, baselineAverageSleepSeconds) {
|
|
|
1239
1262
|
hasRawSegments: session.rawSegmentCount > 0,
|
|
1240
1263
|
qualitySummary: typeof session.annotations.qualitySummary ===
|
|
1241
1264
|
"string"
|
|
1242
|
-
?
|
|
1243
|
-
null
|
|
1265
|
+
? session.annotations
|
|
1266
|
+
.qualitySummary || null
|
|
1244
1267
|
: null,
|
|
1245
1268
|
stageBreakdown: sleepStageShare(session)
|
|
1246
1269
|
};
|
|
@@ -1289,7 +1312,10 @@ function pickDisplaySleepSessions(sessions) {
|
|
|
1289
1312
|
return [...byDateKey.values()].sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt));
|
|
1290
1313
|
}
|
|
1291
1314
|
function normalizeTimelineStage(stage, bucket) {
|
|
1292
|
-
const normalized = stage
|
|
1315
|
+
const normalized = stage
|
|
1316
|
+
.trim()
|
|
1317
|
+
.toLowerCase()
|
|
1318
|
+
.replace(/[\s-]+/g, "_");
|
|
1293
1319
|
if (bucket === "in_bed" || normalized.includes("bed")) {
|
|
1294
1320
|
return "in_bed";
|
|
1295
1321
|
}
|
|
@@ -1560,8 +1586,7 @@ export function revokeCompanionPairingSession(pairingSessionId, activity) {
|
|
|
1560
1586
|
}
|
|
1561
1587
|
export function revokeAllCompanionPairingSessions(input, activity) {
|
|
1562
1588
|
const parsed = revokeAllCompanionPairingSessionsSchema.parse(input ?? {});
|
|
1563
|
-
const rows = listPairingRows(parsed.userIds.length > 0 ? parsed.userIds : undefined)
|
|
1564
|
-
.filter((row) => parsed.includeRevoked || row.status !== "revoked");
|
|
1589
|
+
const rows = listPairingRows(parsed.userIds.length > 0 ? parsed.userIds : undefined).filter((row) => parsed.includeRevoked || row.status !== "revoked");
|
|
1565
1590
|
const sessions = revokePairingRows(rows, {
|
|
1566
1591
|
actor: activity?.actor ?? null,
|
|
1567
1592
|
source: activity?.source ?? "ui",
|
|
@@ -1794,7 +1819,8 @@ function normalizeSleepSegmentInput(input) {
|
|
|
1794
1819
|
return {
|
|
1795
1820
|
...input,
|
|
1796
1821
|
sourceTimezone,
|
|
1797
|
-
localDateKey: input.localDateKey ||
|
|
1822
|
+
localDateKey: input.localDateKey ||
|
|
1823
|
+
localDateKeyForTimezone(input.endedAt, sourceTimezone)
|
|
1798
1824
|
};
|
|
1799
1825
|
}
|
|
1800
1826
|
function normalizeSleepRawRecordInput(input) {
|
|
@@ -1802,7 +1828,8 @@ function normalizeSleepRawRecordInput(input) {
|
|
|
1802
1828
|
return {
|
|
1803
1829
|
...input,
|
|
1804
1830
|
sourceTimezone,
|
|
1805
|
-
localDateKey: input.localDateKey ||
|
|
1831
|
+
localDateKey: input.localDateKey ||
|
|
1832
|
+
localDateKeyForTimezone(input.endedAt, sourceTimezone)
|
|
1806
1833
|
};
|
|
1807
1834
|
}
|
|
1808
1835
|
function listNormalizedSleepNights(payload) {
|
|
@@ -2018,8 +2045,7 @@ function upsertVitalDaySummary(userId, input) {
|
|
|
2018
2045
|
}
|
|
2019
2046
|
function summarizeUserHealthDay(userId, dateKeyValue) {
|
|
2020
2047
|
const sleeps = listSleepRows([userId]).filter((row) => sleepSessionDateKey(row) === dateKeyValue ||
|
|
2021
|
-
localDateKeyForTimezone(row.started_at, resolveTimeZone(row.source_timezone)) ===
|
|
2022
|
-
dateKeyValue);
|
|
2048
|
+
localDateKeyForTimezone(row.started_at, resolveTimeZone(row.source_timezone)) === dateKeyValue);
|
|
2023
2049
|
const workouts = listWorkoutRows([userId]).filter((row) => dayKey(row.started_at) === dateKeyValue);
|
|
2024
2050
|
const totalSleepSeconds = sleeps.reduce((sum, row) => sum + row.asleep_seconds, 0);
|
|
2025
2051
|
const totalWorkoutSeconds = workouts.reduce((sum, row) => sum + row.duration_seconds, 0);
|
|
@@ -2147,12 +2173,16 @@ function insertOrUpdateWorkoutSession(pairing, input) {
|
|
|
2147
2173
|
(typeof existingProvenance.sourceProductType === "string"
|
|
2148
2174
|
? existingProvenance.sourceProductType
|
|
2149
2175
|
: null),
|
|
2150
|
-
activity: input.activity ??
|
|
2176
|
+
activity: input.activity ??
|
|
2177
|
+
existingDerived.activity ??
|
|
2178
|
+
existingProvenance.activity,
|
|
2151
2179
|
details: input.details ?? existingDerived.details ?? existingProvenance.details
|
|
2152
2180
|
});
|
|
2153
2181
|
const mergedLinks = mergeHealthLinks(existingLinks, input.links, annotations.links);
|
|
2154
2182
|
const mergedTags = mergeStringLists(existingTags, annotations.tags);
|
|
2155
|
-
const nextSubjectiveEffort = matchedGenerated.subjective_effort ??
|
|
2183
|
+
const nextSubjectiveEffort = matchedGenerated.subjective_effort ??
|
|
2184
|
+
annotations.subjectiveEffort ??
|
|
2185
|
+
null;
|
|
2156
2186
|
const nextMoodBefore = matchedGenerated.mood_before || annotations.moodBefore;
|
|
2157
2187
|
const nextMoodAfter = matchedGenerated.mood_after || annotations.moodAfter;
|
|
2158
2188
|
const nextMeaningText = matchedGenerated.meaning_text || annotations.meaningText;
|
|
@@ -2201,7 +2231,8 @@ function insertOrUpdateWorkoutSession(pairing, input) {
|
|
|
2201
2231
|
}), matchedGenerated.generated_from_habit_id ? "merged" : "standalone", now, matchedGenerated.id);
|
|
2202
2232
|
persistWorkoutEvidenceForInput(matchedGenerated.id, pairing.user_id, input);
|
|
2203
2233
|
return {
|
|
2204
|
-
mode: matchedGenerated.generated_from_habit_id ||
|
|
2234
|
+
mode: matchedGenerated.generated_from_habit_id ||
|
|
2235
|
+
matchedGenerated.source !== "apple_health"
|
|
2205
2236
|
? "merged"
|
|
2206
2237
|
: "updated",
|
|
2207
2238
|
id: matchedGenerated.id
|
|
@@ -2364,13 +2395,14 @@ function backfillHistoricalSleepEvidence() {
|
|
|
2364
2395
|
)
|
|
2365
2396
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
2366
2397
|
.run(sourceRecordId, rawLog.import_run_id, rawLog.pairing_session_id, session.id, rawLog.user_id, "apple_health", "historical_import_interval", rawLog.external_uid ?? rawLog.id, session.source_device, sourceTimezone, localDateKey, rawLog.started_at ?? session.started_at, rawLog.ended_at ?? session.ended_at, inferHistoricalRawStage(payload), null, "historical_import", JSON.stringify(payload), JSON.stringify({
|
|
2367
|
-
...
|
|
2398
|
+
...safeJsonParse(rawLog.metadata_json, {}),
|
|
2368
2399
|
migratedFromRawLogId: rawLog.id
|
|
2369
2400
|
}), rawLog.created_at);
|
|
2370
2401
|
}
|
|
2371
2402
|
}
|
|
2372
2403
|
if (existingSegmentCount.count === 0 && rawLogs.length > 0) {
|
|
2373
|
-
const authoritativeLogs = selectAuthoritativeSleepRows(rawLogs.filter((row) => typeof row.started_at === "string" &&
|
|
2404
|
+
const authoritativeLogs = selectAuthoritativeSleepRows(rawLogs.filter((row) => typeof row.started_at === "string" &&
|
|
2405
|
+
typeof row.ended_at === "string"));
|
|
2374
2406
|
for (const rawLog of authoritativeLogs) {
|
|
2375
2407
|
const payload = safeJsonParse(rawLog.payload_json, {});
|
|
2376
2408
|
const inferredStage = inferHistoricalRawStage(payload);
|
|
@@ -2499,7 +2531,9 @@ function ensureLegacyAppleSleepHistoryRepaired() {
|
|
|
2499
2531
|
for (const cluster of clusters) {
|
|
2500
2532
|
const rowsForNight = selectAuthoritativeSleepRows(cluster);
|
|
2501
2533
|
const startedAt = rowsForNight[0].started_at;
|
|
2502
|
-
const endedAt = rowsForNight.reduce((latest, row) => Date.parse(row.ended_at) > Date.parse(latest)
|
|
2534
|
+
const endedAt = rowsForNight.reduce((latest, row) => Date.parse(row.ended_at) > Date.parse(latest)
|
|
2535
|
+
? row.ended_at
|
|
2536
|
+
: latest, rowsForNight[0].ended_at);
|
|
2503
2537
|
const sourceTimezone = inferHistoricalSleepTimeZone(cluster.map((row) => row.source_timezone));
|
|
2504
2538
|
const localDateKey = localDateKeyForTimezone(endedAt, sourceTimezone);
|
|
2505
2539
|
const asleepSeconds = unionDurationSeconds(rowsForNight.map((row) => ({
|
|
@@ -2897,7 +2931,7 @@ function mobileSyncSessionPairing(session) {
|
|
|
2897
2931
|
function mobileSyncSessionMetadata(session) {
|
|
2898
2932
|
return safeJsonParse(session.source_metadata_json, {});
|
|
2899
2933
|
}
|
|
2900
|
-
function
|
|
2934
|
+
function applyWorkoutChunkImmediately(session, family, workouts) {
|
|
2901
2935
|
if (workouts.length === 0) {
|
|
2902
2936
|
return;
|
|
2903
2937
|
}
|
|
@@ -2951,6 +2985,7 @@ function applyWorkoutSummaryChunkImmediately(session, workouts) {
|
|
|
2951
2985
|
.run(device.sourceDevice, JSON.stringify({
|
|
2952
2986
|
progressiveChunk: true,
|
|
2953
2987
|
syncSessionId: session.id,
|
|
2988
|
+
family,
|
|
2954
2989
|
workouts: workouts.length
|
|
2955
2990
|
}), workouts.length, createdCount, updatedCount, mergedCount, now, now, runId);
|
|
2956
2991
|
});
|
|
@@ -2999,24 +3034,34 @@ export function startMobileHealthSyncSession(payload) {
|
|
|
2999
3034
|
if (existing &&
|
|
3000
3035
|
existing.pairing_session_id === pairing.id &&
|
|
3001
3036
|
existing.status === "running") {
|
|
3002
|
-
const
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3037
|
+
const existingFamilies = safeJsonParse(existing.requested_families_json, []);
|
|
3038
|
+
const canResume = existing.schema_version === HEALTH_MOBILE_SYNC_SCHEMA_VERSION &&
|
|
3039
|
+
parsed.requestedFamilies.every((family) => existingFamilies.includes(family));
|
|
3040
|
+
if (canResume) {
|
|
3041
|
+
const receivedChunkIds = getDatabase()
|
|
3042
|
+
.prepare(`SELECT chunk_id
|
|
3043
|
+
FROM health_mobile_sync_chunks
|
|
3044
|
+
WHERE sync_session_id = ?
|
|
3045
|
+
ORDER BY sequence ASC`)
|
|
3046
|
+
.all(resumeSyncSessionId)
|
|
3047
|
+
.map((row) => row.chunk_id);
|
|
3048
|
+
return {
|
|
3049
|
+
syncSessionId: resumeSyncSessionId,
|
|
3050
|
+
schemaVersion: HEALTH_MOBILE_SYNC_SCHEMA_VERSION,
|
|
3051
|
+
chunkTargetBytes: HEALTH_MOBILE_SYNC_CHUNK_TARGET_BYTES,
|
|
3052
|
+
chunkMaxBytes: HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES,
|
|
3053
|
+
chunkPayloadEncoding: HEALTH_MOBILE_SYNC_CHUNK_PAYLOAD_ENCODING,
|
|
3054
|
+
acceptedPayloadEncodings: HEALTH_MOBILE_SYNC_ACCEPTED_CHUNK_PAYLOAD_ENCODINGS,
|
|
3055
|
+
supportsCompression: true,
|
|
3056
|
+
acceptedFamilies: existingFamilies,
|
|
3057
|
+
receivedChunkIds
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
getDatabase()
|
|
3061
|
+
.prepare(`UPDATE health_mobile_sync_sessions
|
|
3062
|
+
SET status = 'aborted', aborted_at = ?, updated_at = ?
|
|
3063
|
+
WHERE id = ?`)
|
|
3064
|
+
.run(nowIso(), nowIso(), existing.id);
|
|
3020
3065
|
}
|
|
3021
3066
|
}
|
|
3022
3067
|
const now = nowIso();
|
|
@@ -3082,7 +3127,8 @@ export function ingestMobileHealthSyncChunk(syncSessionId, payload, rawPayloadJs
|
|
|
3082
3127
|
actualBytes: actualByteCount
|
|
3083
3128
|
});
|
|
3084
3129
|
}
|
|
3085
|
-
if (wirePayload.mode !== "legacy_payload_object" &&
|
|
3130
|
+
if (wirePayload.mode !== "legacy_payload_object" &&
|
|
3131
|
+
parsed.byteCount !== actualByteCount) {
|
|
3086
3132
|
console.warn("[healthkit-sync] chunk byte count mismatch", {
|
|
3087
3133
|
syncSessionId,
|
|
3088
3134
|
chunkId: parsed.chunkId,
|
|
@@ -3159,8 +3205,9 @@ export function ingestMobileHealthSyncChunk(syncSessionId, payload, rawPayloadJs
|
|
|
3159
3205
|
serverChecksum,
|
|
3160
3206
|
mode: wirePayload.mode
|
|
3161
3207
|
}), now, now, now, now);
|
|
3162
|
-
if (parsed.family === "workout_summaries"
|
|
3163
|
-
|
|
3208
|
+
if (parsed.family === "workout_summaries" ||
|
|
3209
|
+
parsed.family === "workout_archive") {
|
|
3210
|
+
applyWorkoutChunkImmediately(session, parsed.family, wirePayload.payload.workouts ?? []);
|
|
3164
3211
|
}
|
|
3165
3212
|
const progress = updateMobileSyncSessionProgress(syncSessionId);
|
|
3166
3213
|
return {
|
|
@@ -3478,7 +3525,8 @@ export function ingestMobileHealthSync(payload) {
|
|
|
3478
3525
|
for (const sleep of normalizedSleepNights) {
|
|
3479
3526
|
const targetSessionId = sleepSessionsByLocalDate
|
|
3480
3527
|
.get(sleep.localDateKey)
|
|
3481
|
-
?.find((session) => session.startedAt === sleep.startedAt &&
|
|
3528
|
+
?.find((session) => session.startedAt === sleep.startedAt &&
|
|
3529
|
+
session.endedAt === sleep.endedAt)?.id ?? null;
|
|
3482
3530
|
if (targetSessionId) {
|
|
3483
3531
|
replaceHistoricalSleepSessionsForDate(pairing.user_id, sleep.localDateKey, targetSessionId);
|
|
3484
3532
|
}
|
|
@@ -3566,7 +3614,9 @@ export function ingestMobileHealthSync(payload) {
|
|
|
3566
3614
|
screenTimeDaySummaries: parsed.screenTime.daySummaries.length,
|
|
3567
3615
|
screenTimeHourlySegments: parsed.screenTime.hourlySegments.length,
|
|
3568
3616
|
createdCount: createdCount + movementSync.createdCount,
|
|
3569
|
-
updatedCount: updatedCount +
|
|
3617
|
+
updatedCount: updatedCount +
|
|
3618
|
+
movementSync.updatedCount +
|
|
3619
|
+
screenTimeSync.updatedCount,
|
|
3570
3620
|
mergedCount
|
|
3571
3621
|
}
|
|
3572
3622
|
});
|
|
@@ -3582,8 +3632,12 @@ export function ingestMobileHealthSync(payload) {
|
|
|
3582
3632
|
workouts: parsed.workouts.length,
|
|
3583
3633
|
vitalsDaySummaries: parsed.vitals.daySummaries.length,
|
|
3584
3634
|
vitalsMetricEntries: vitalMetricEntries,
|
|
3585
|
-
createdCount: createdCount +
|
|
3586
|
-
|
|
3635
|
+
createdCount: createdCount +
|
|
3636
|
+
movementSync.createdCount +
|
|
3637
|
+
screenTimeSync.createdCount,
|
|
3638
|
+
updatedCount: updatedCount +
|
|
3639
|
+
movementSync.updatedCount +
|
|
3640
|
+
screenTimeSync.updatedCount,
|
|
3587
3641
|
mergedCount,
|
|
3588
3642
|
movementStays: parsed.movement.stays.length,
|
|
3589
3643
|
movementTrips: parsed.movement.trips.length,
|
|
@@ -3620,7 +3674,7 @@ export function getCompanionOverview(userIds) {
|
|
|
3620
3674
|
WHERE user_id IN (${userIds.map(() => "?").join(",")})`
|
|
3621
3675
|
: `SELECT COUNT(*) AS count FROM health_sleep_raw_logs`)
|
|
3622
3676
|
.get(...(userIds ?? []));
|
|
3623
|
-
const workouts = listWorkoutRows(userIds).map(mapWorkoutSession);
|
|
3677
|
+
const workouts = listWorkoutRows(userIds).map((row) => mapWorkoutSession(row));
|
|
3624
3678
|
const vitalsRows = listDailySummaryRows("vitals", userIds);
|
|
3625
3679
|
const vitalsMetricEntries = vitalsRows.reduce((sum, row) => {
|
|
3626
3680
|
const metrics = safeJsonParse(row.metrics_json, {});
|
|
@@ -3677,10 +3731,12 @@ export function getCompanionOverview(userIds) {
|
|
|
3677
3731
|
return (session.links.length > 0 ||
|
|
3678
3732
|
(typeof annotations.qualitySummary === "string" &&
|
|
3679
3733
|
annotations.qualitySummary.length > 0) ||
|
|
3680
|
-
(typeof annotations.notes === "string" &&
|
|
3734
|
+
(typeof annotations.notes === "string" &&
|
|
3735
|
+
annotations.notes.length > 0) ||
|
|
3681
3736
|
tags.length > 0);
|
|
3682
3737
|
}).length,
|
|
3683
|
-
linkedWorkouts: workouts.filter((session) => session.links.length > 0)
|
|
3738
|
+
linkedWorkouts: workouts.filter((session) => session.links.length > 0)
|
|
3739
|
+
.length,
|
|
3684
3740
|
habitGeneratedWorkouts: workouts.filter((session) => session.sourceType === "habit_generated").length,
|
|
3685
3741
|
reconciledWorkouts: workouts.filter((session) => session.reconciliationStatus === "merged").length,
|
|
3686
3742
|
vitalsDaySummaries: vitalsRows.length,
|
|
@@ -3740,9 +3796,9 @@ export function getSleepViewData(userIds) {
|
|
|
3740
3796
|
? session.asleepSeconds / session.timeInBedSeconds
|
|
3741
3797
|
: 0)), 2),
|
|
3742
3798
|
averageRestorativeShare: round(average(weekly.map((session) => sleepRestorativeShare(session))), 2),
|
|
3743
|
-
reflectiveNightCount: weekly.filter((session) => sleepHasReflection(session))
|
|
3799
|
+
reflectiveNightCount: weekly.filter((session) => sleepHasReflection(session)).length,
|
|
3800
|
+
linkedNightCount: weekly.filter((session) => session.links.length > 0)
|
|
3744
3801
|
.length,
|
|
3745
|
-
linkedNightCount: weekly.filter((session) => session.links.length > 0).length,
|
|
3746
3802
|
averageBedtimeConsistencyMinutes: Math.round(average(weekly
|
|
3747
3803
|
.map((session) => session.bedtimeConsistencyMinutes)
|
|
3748
3804
|
.filter((value) => value !== null))),
|
|
@@ -3771,8 +3827,10 @@ export function getSleepViewData(userIds) {
|
|
|
3771
3827
|
.map((session) => ({
|
|
3772
3828
|
id: session.id,
|
|
3773
3829
|
dateKey: session.localDateKey,
|
|
3774
|
-
onsetHour: getTimeZoneParts(session.startedAt, session.sourceTimezone)
|
|
3775
|
-
|
|
3830
|
+
onsetHour: getTimeZoneParts(session.startedAt, session.sourceTimezone)
|
|
3831
|
+
.hour,
|
|
3832
|
+
wakeHour: getTimeZoneParts(session.endedAt, session.sourceTimezone)
|
|
3833
|
+
.hour,
|
|
3776
3834
|
sleepHours: Number((session.asleepSeconds / 3600).toFixed(2))
|
|
3777
3835
|
}))
|
|
3778
3836
|
.reverse(),
|
|
@@ -3844,9 +3902,17 @@ function buildFitnessVitalsTrend(rows) {
|
|
|
3844
3902
|
}));
|
|
3845
3903
|
}
|
|
3846
3904
|
export function getFitnessViewData(userIds) {
|
|
3847
|
-
const
|
|
3905
|
+
const workoutRows = listWorkoutRows(userIds);
|
|
3906
|
+
const recent = workoutRows
|
|
3907
|
+
.slice(0, 40)
|
|
3908
|
+
.map((row) => mapWorkoutSession(row, { includeAnalytics: true }));
|
|
3909
|
+
const browserSessions = workoutRows
|
|
3910
|
+
.slice(0, 2000)
|
|
3911
|
+
.map((row, index) => mapWorkoutSession(row, { includeAnalytics: index < 40 }));
|
|
3912
|
+
const analysisSessions = workoutRows
|
|
3913
|
+
.slice(0, 500)
|
|
3914
|
+
.map((row) => mapWorkoutSession(row, { includeAnalytics: true }));
|
|
3848
3915
|
const vitalsTrend = buildFitnessVitalsTrend(listDailySummaryRows("vitals", userIds));
|
|
3849
|
-
const recent = workouts.slice(0, 40);
|
|
3850
3916
|
const weekly = recent.filter((session) => Date.now() - Date.parse(session.startedAt) <= 7 * 24 * 60 * 60 * 1000);
|
|
3851
3917
|
const weeklyVolumeSeconds = weekly.reduce((sum, session) => sum + session.durationSeconds, 0);
|
|
3852
3918
|
const exerciseMinutes = weekly.reduce((sum, session) => sum + (session.exerciseMinutes ?? session.durationSeconds / 60), 0);
|
|
@@ -3914,14 +3980,14 @@ export function getFitnessViewData(userIds) {
|
|
|
3914
3980
|
averageEffort: round(average(recent
|
|
3915
3981
|
.map((session) => session.subjectiveEffort)
|
|
3916
3982
|
.filter((value) => value !== null)), 1),
|
|
3917
|
-
linkedSessionCount: recent.filter((session) => session.links.length > 0)
|
|
3983
|
+
linkedSessionCount: recent.filter((session) => session.links.length > 0)
|
|
3984
|
+
.length,
|
|
3918
3985
|
plannedSessionCount: recent.filter((session) => session.plannedContext.trim().length > 0).length,
|
|
3919
3986
|
importedSessionCount: recent.filter((session) => session.source === "apple_health").length,
|
|
3920
3987
|
habitGeneratedSessionCount: recent.filter((session) => session.sourceType === "habit_generated").length,
|
|
3921
3988
|
reconciledSessionCount: recent.filter((session) => session.reconciliationStatus === "merged").length,
|
|
3922
3989
|
topWorkoutType: orderedWorkoutTypes[0]?.[0] ?? null,
|
|
3923
|
-
topWorkoutTypeLabel: recent.find((session) => session.workoutType === orderedWorkoutTypes[0]?.[0])
|
|
3924
|
-
?.workoutTypeLabel ?? null,
|
|
3990
|
+
topWorkoutTypeLabel: recent.find((session) => session.workoutType === orderedWorkoutTypes[0]?.[0])?.workoutTypeLabel ?? null,
|
|
3925
3991
|
streakDays: Array.from(new Set(weekly.map((session) => dayKey(session.startedAt)))).length,
|
|
3926
3992
|
averageHeartRateCoverage: heartRateCoverageCount > 0
|
|
3927
3993
|
? Number((heartRateCoverageSum / heartRateCoverageCount).toFixed(3))
|
|
@@ -3942,17 +4008,16 @@ export function getFitnessViewData(userIds) {
|
|
|
3942
4008
|
energyKcal: Math.round(session.totalEnergyKcal ?? session.activeEnergyKcal ?? 0),
|
|
3943
4009
|
zoneDurations: session.analytics
|
|
3944
4010
|
?.zoneDurations ?? [],
|
|
3945
|
-
trainingLoad:
|
|
3946
|
-
|
|
3947
|
-
heartRateCoverage: (session.analytics?.dataQuality?.sampleCoverage ?? 0)
|
|
4011
|
+
trainingLoad: session.analytics?.load?.trimp ?? null,
|
|
4012
|
+
heartRateCoverage: session.analytics?.dataQuality?.sampleCoverage ?? 0
|
|
3948
4013
|
}))
|
|
3949
4014
|
.reverse(),
|
|
3950
4015
|
typeBreakdown: orderedWorkoutTypes.map(([workoutType, metrics]) => ({
|
|
3951
4016
|
workoutType,
|
|
3952
|
-
workoutTypeLabel: recent.find((session) => session.workoutType === workoutType)
|
|
3953
|
-
workoutType,
|
|
3954
|
-
activityFamily: recent.find((session) => session.workoutType === workoutType)
|
|
3955
|
-
"other",
|
|
4017
|
+
workoutTypeLabel: recent.find((session) => session.workoutType === workoutType)
|
|
4018
|
+
?.workoutTypeLabel ?? workoutType,
|
|
4019
|
+
activityFamily: recent.find((session) => session.workoutType === workoutType)
|
|
4020
|
+
?.activityFamily ?? "other",
|
|
3956
4021
|
activityFamilyLabel: recent.find((session) => session.workoutType === workoutType)
|
|
3957
4022
|
?.activityFamilyLabel ?? "Other",
|
|
3958
4023
|
sessionCount: metrics.sessionCount,
|
|
@@ -3960,8 +4025,8 @@ export function getFitnessViewData(userIds) {
|
|
|
3960
4025
|
energyKcal: metrics.energyKcal
|
|
3961
4026
|
})),
|
|
3962
4027
|
vitalsTrend,
|
|
3963
|
-
analysisSessions
|
|
3964
|
-
sessions:
|
|
4028
|
+
analysisSessions,
|
|
4029
|
+
sessions: browserSessions
|
|
3965
4030
|
};
|
|
3966
4031
|
}
|
|
3967
4032
|
function averageNullable(values) {
|
|
@@ -3970,7 +4035,9 @@ function averageNullable(values) {
|
|
|
3970
4035
|
}
|
|
3971
4036
|
function sumNullable(values) {
|
|
3972
4037
|
const present = values.filter((value) => value != null);
|
|
3973
|
-
return present.length > 0
|
|
4038
|
+
return present.length > 0
|
|
4039
|
+
? present.reduce((sum, value) => sum + value, 0)
|
|
4040
|
+
: null;
|
|
3974
4041
|
}
|
|
3975
4042
|
function maxNullable(values) {
|
|
3976
4043
|
const present = values.filter((value) => value != null);
|
|
@@ -4047,7 +4114,9 @@ export function getVitalsViewData(userIds) {
|
|
|
4047
4114
|
}))
|
|
4048
4115
|
.filter((value) => value != null);
|
|
4049
4116
|
const baselineValues = recentValues.slice(Math.max(0, recentValues.length - 8), recentValues.length - 1);
|
|
4050
|
-
const baselineValue = baselineValues.length > 0
|
|
4117
|
+
const baselineValue = baselineValues.length > 0
|
|
4118
|
+
? average(baselineValues)
|
|
4119
|
+
: (recentValues.at(-2) ?? null);
|
|
4051
4120
|
const latestValue = latestDay
|
|
4052
4121
|
? vitalMetricPrimaryValue({
|
|
4053
4122
|
aggregation: bucket.aggregation,
|
|
@@ -4063,9 +4132,13 @@ export function getVitalsViewData(userIds) {
|
|
|
4063
4132
|
category: bucket.category,
|
|
4064
4133
|
unit: bucket.displayUnit,
|
|
4065
4134
|
aggregation: bucket.aggregation,
|
|
4066
|
-
latestValue: latestValue == null
|
|
4135
|
+
latestValue: latestValue == null
|
|
4136
|
+
? null
|
|
4137
|
+
: round(latestValue, bucket.aggregation === "cumulative" ? 0 : 1),
|
|
4067
4138
|
latestDateKey: latestDay?.dateKey ?? null,
|
|
4068
|
-
baselineValue: baselineValue == null
|
|
4139
|
+
baselineValue: baselineValue == null
|
|
4140
|
+
? null
|
|
4141
|
+
: round(baselineValue, bucket.aggregation === "cumulative" ? 0 : 1),
|
|
4069
4142
|
deltaValue: latestValue != null && baselineValue != null
|
|
4070
4143
|
? round(latestValue - baselineValue, bucket.aggregation === "cumulative" ? 0 : 1)
|
|
4071
4144
|
: null,
|
|
@@ -4079,7 +4152,9 @@ export function getVitalsViewData(userIds) {
|
|
|
4079
4152
|
}
|
|
4080
4153
|
return left.category.localeCompare(right.category);
|
|
4081
4154
|
});
|
|
4082
|
-
const categoryBreakdown = [
|
|
4155
|
+
const categoryBreakdown = [
|
|
4156
|
+
...new Set(metrics.map((metric) => metric.category))
|
|
4157
|
+
]
|
|
4083
4158
|
.map((category) => {
|
|
4084
4159
|
const categoryMetrics = metrics.filter((metric) => metric.category === category);
|
|
4085
4160
|
return {
|
|
@@ -4094,8 +4169,7 @@ export function getVitalsViewData(userIds) {
|
|
|
4094
4169
|
trackedDays: dayCount,
|
|
4095
4170
|
metricCount: metrics.length,
|
|
4096
4171
|
latestDateKey: rows[0]?.date_key ?? null,
|
|
4097
|
-
latestMetricCount: metrics.filter((metric) => metric.latestDateKey === (rows[0]?.date_key ?? null))
|
|
4098
|
-
.length,
|
|
4172
|
+
latestMetricCount: metrics.filter((metric) => metric.latestDateKey === (rows[0]?.date_key ?? null)).length,
|
|
4099
4173
|
categoryBreakdown
|
|
4100
4174
|
},
|
|
4101
4175
|
metrics
|
|
@@ -4147,7 +4221,8 @@ export function createSleepSession(input, activity) {
|
|
|
4147
4221
|
provenance_json, derived_json, created_at, updated_at
|
|
4148
4222
|
)
|
|
4149
4223
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
4150
|
-
.run(id, externalUid, null, userId, parsed.source, parsed.sourceType, parsed.sourceDevice, sourceTimezone, parsed.localDateKey ??
|
|
4224
|
+
.run(id, externalUid, null, userId, parsed.source, parsed.sourceType, parsed.sourceDevice, sourceTimezone, parsed.localDateKey ??
|
|
4225
|
+
localDateKeyForTimezone(parsed.endedAt, sourceTimezone), parsed.startedAt, parsed.endedAt, timeInBedSeconds, asleepSeconds, awakeSeconds, sleepScore, timingMetrics.regularityScore, timingMetrics.bedtimeConsistencyMinutes, timingMetrics.wakeConsistencyMinutes, parsed.rawSegmentCount, JSON.stringify(parsed.stageBreakdown), JSON.stringify(parsed.recoveryMetrics), JSON.stringify(parsed.sourceMetrics), JSON.stringify(parsed.links), JSON.stringify(annotations), JSON.stringify({
|
|
4151
4226
|
manualEntry: true,
|
|
4152
4227
|
entryMode: "local",
|
|
4153
4228
|
source: parsed.source,
|
|
@@ -4157,7 +4232,8 @@ export function createSleepSession(input, activity) {
|
|
|
4157
4232
|
createdAt: now,
|
|
4158
4233
|
...parsed.provenance
|
|
4159
4234
|
}), JSON.stringify(derived), now, now);
|
|
4160
|
-
summarizeUserHealthDay(userId, parsed.localDateKey ??
|
|
4235
|
+
summarizeUserHealthDay(userId, parsed.localDateKey ??
|
|
4236
|
+
localDateKeyForTimezone(parsed.endedAt, sourceTimezone));
|
|
4161
4237
|
recordActivityEvent({
|
|
4162
4238
|
entityType: "sleep_session",
|
|
4163
4239
|
entityId: id,
|
|
@@ -4374,9 +4450,7 @@ export function updateWorkoutSession(workoutId, patch, activity) {
|
|
|
4374
4450
|
? durationSecondsBetween(startedAt, endedAt)
|
|
4375
4451
|
: current.duration_seconds;
|
|
4376
4452
|
const currentAnnotations = safeJsonParse(current.annotations_json, {});
|
|
4377
|
-
const tags = parsed.tags ??
|
|
4378
|
-
safeJsonParse(current.tags_json, []) ??
|
|
4379
|
-
[];
|
|
4453
|
+
const tags = parsed.tags ?? safeJsonParse(current.tags_json, []) ?? [];
|
|
4380
4454
|
const links = parsed.links ??
|
|
4381
4455
|
safeJsonParse(current.links_json, []);
|
|
4382
4456
|
const annotations = {
|
|
@@ -4512,7 +4586,9 @@ export function updateWorkoutMetadata(workoutId, patch, activity) {
|
|
|
4512
4586
|
...(parsed.subjectiveEffort !== undefined
|
|
4513
4587
|
? { subjectiveEffort: parsed.subjectiveEffort }
|
|
4514
4588
|
: {}),
|
|
4515
|
-
...(parsed.moodBefore !== undefined
|
|
4589
|
+
...(parsed.moodBefore !== undefined
|
|
4590
|
+
? { moodBefore: parsed.moodBefore }
|
|
4591
|
+
: {}),
|
|
4516
4592
|
...(parsed.moodAfter !== undefined ? { moodAfter: parsed.moodAfter } : {}),
|
|
4517
4593
|
...(parsed.meaningText !== undefined
|
|
4518
4594
|
? { meaningText: parsed.meaningText }
|