forge-openclaw-plugin 0.2.101 → 0.2.103
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/activity-page-Dv6X5ZCV.js +1 -0
- package/dist/assets/ai-surface-workspace-CutiG6uS.js +1 -0
- package/dist/assets/atlas-panel-jgMRyaHn.js +1 -0
- package/dist/assets/{board-BkDRaMp6.js → board-dIX6etHh.js} +1 -1
- package/dist/assets/calendar-page-q7Nm5E2U.js +1 -0
- package/dist/assets/calendar-rules-DPFsfiRl.js +1 -0
- package/dist/assets/calendar-week-toolbar-nVhgB-0s.js +1 -0
- package/dist/assets/{charts-P7EVhIog.js → charts-DFnEuIMB.js} +8 -8
- package/dist/assets/companion-sync-lab-page-CJ8UTij8.js +1 -0
- package/dist/assets/daily-metrics-dashboard-CiJkkkd1.js +1 -0
- package/dist/assets/entity-note-count-link-RzBB6ujx.js +1 -0
- package/dist/assets/entity-notes-surface-Djz50HvD.js +1 -0
- package/dist/assets/execution-board-CtPhI-58.js +1 -0
- package/dist/assets/faceted-token-search-DEC6ANa9.js +1 -0
- package/dist/assets/flagship-signal-deck-CtHC3mql.js +1 -0
- package/dist/assets/floating-action-menu-CKQRhff9.js +1 -0
- package/dist/assets/{forms-BFlTgZ3W.js → forms-hB0SqEh-.js} +1 -1
- package/dist/assets/goal-detail-page-CF5fNQeR.js +1 -0
- package/dist/assets/goals-page-fsY8NzGB.js +1 -0
- package/dist/assets/{graph-D6JLqDbD.js → graph-DDUZNRsO.js} +14 -14
- package/dist/assets/habits-page-COoGz4kj.js +1 -0
- package/dist/assets/index-BAXYM89v.css +1 -0
- package/dist/assets/index-EqQsXoat.js +19 -0
- package/dist/assets/insight-flow-dialog-DtX_5W7g.js +1 -0
- package/dist/assets/insights-page-cBd74ObU.js +8 -0
- package/dist/assets/kanban-page-CYav9THw.js +1 -0
- package/dist/assets/knowledge-graph-page-D2A-W1jE.js +1 -0
- package/dist/assets/{life-force-page-Dy0JTS2G.js → life-force-page-CACGlNhq.js} +1 -1
- package/dist/assets/life-force-workspace-BJ4N1I78.js +1 -0
- package/dist/assets/{maps-ClgJoCjz.js → maps-C-yOWiDN.js} +1 -1
- package/dist/assets/metric-tile-CMadwnGz.js +1 -0
- package/dist/assets/{motion-BeD44FeG.js → motion-Lt5B1XuE.js} +1 -1
- package/dist/assets/movement-page-D5VqFd2q.js +1 -0
- package/dist/assets/note-markdown-BzK2Qlgr.js +3 -0
- package/dist/assets/note-tags-input-CZzqJMLc.js +1 -0
- package/dist/assets/notes-page-D3Hsh90C.js +1 -0
- package/dist/assets/{open-in-graph-button-IXe9SGth.js → open-in-graph-button-BFPKfyK3.js} +1 -1
- package/dist/assets/orbit-map-CnyfSmOG.js +1 -0
- package/dist/assets/overview-page-CXdWrOV1.js +1 -0
- package/dist/assets/page-hero-ffKzgyW3.js +1 -0
- package/dist/assets/pill-cluster-C2D0h3lx.js +1 -0
- package/dist/assets/{preference-entity-handoff-button-5PzUn42S.js → preference-entity-handoff-button-Dj3V6VxL.js} +1 -1
- package/dist/assets/preferences-page-Jo8Gw386.js +1 -0
- package/dist/assets/project-collections-qSqp90HN.js +1 -0
- package/dist/assets/{project-detail-page-BXK5-4xW.js → project-detail-page-BifhiLQX.js} +1 -1
- package/dist/assets/project-management-hierarchy-page-DZ_9klIc.js +1 -0
- package/dist/assets/project-management-section-nav-BImLCVvf.js +1 -0
- package/dist/assets/projects-page-ptcx6H38.js +1 -0
- package/dist/assets/psyche-behaviors-page-BDwXyDta.js +5 -0
- package/dist/assets/psyche-flashcards-page-DL1BP1jX.js +1 -0
- package/dist/assets/psyche-goal-map-page-ovcpisx1.js +1 -0
- package/dist/assets/psyche-graph-EP5GL612.js +1 -0
- package/dist/assets/{psyche-metrics-page-CuR9oqEy.js → psyche-metrics-page-Bcu813Rg.js} +1 -1
- package/dist/assets/psyche-mode-guide-page-C0g27Xpt.js +1 -0
- package/dist/assets/psyche-modes-page-BjKjX5MR.js +1 -0
- package/dist/assets/psyche-page-F4tF2W70.js +1 -0
- package/dist/assets/psyche-patterns-page-B-14hukK.js +5 -0
- package/dist/assets/psyche-questionnaire-builder-page-BdXmoHvK.js +1 -0
- package/dist/assets/psyche-questionnaire-detail-page-Cu5uwlJu.js +1 -0
- package/dist/assets/psyche-questionnaire-run-detail-page-C3R4PClg.js +1 -0
- package/dist/assets/psyche-questionnaire-run-page-Div3iDdt.js +1 -0
- package/dist/assets/psyche-questionnaires-page-DpqAPQCp.js +1 -0
- package/dist/assets/psyche-report-detail-page-BvWVDKP3.js +3 -0
- package/dist/assets/psyche-reports-page-BrbWUlAq.js +1 -0
- package/dist/assets/{psyche-schemas-HFmg37Wj.js → psyche-schemas-Dxj554nU.js} +1 -1
- package/dist/assets/psyche-schemas-beliefs-page-DYKvAtSD.js +9 -0
- package/dist/assets/psyche-screen-time-page-CAOKCyQw.js +1 -0
- package/dist/assets/psyche-self-observation-page-F0MVA0UH.js +1 -0
- package/dist/assets/psyche-values-page-DyvX-d0o.js +5 -0
- package/dist/assets/report-chain-fields-CeC1cJFS.js +1 -0
- package/dist/assets/rewards-page-C2loyODo.js +1 -0
- package/dist/assets/scheduling-rules-editor-D40AC2jR.js +1 -0
- package/dist/assets/schema-badge-FY7818qB.js +1 -0
- package/dist/assets/schema-visuals-CvC9a3i6.js +1 -0
- package/dist/assets/select-menu-DJsCG6rM.js +1 -0
- package/dist/assets/settings-agents-page-BFkJ5AAD.js +6 -0
- package/dist/assets/settings-bin-page-D3-Ab-iA.js +1 -0
- package/dist/assets/settings-calendar-page-ly_mSTAD.js +5 -0
- package/dist/assets/settings-data-page-D7QOqNf-.js +1 -0
- package/dist/assets/settings-logs-page-Ca87HEUx.js +1 -0
- package/dist/assets/settings-mobile-page-D0jejZot.js +1 -0
- package/dist/assets/settings-models-page-DPDsZjSc.js +1 -0
- package/dist/assets/settings-page-1cArlSPM.js +1 -0
- package/dist/assets/settings-rewards-page-Db4BV1DC.js +1 -0
- package/dist/assets/{settings-section-nav-DSOuht_F.js → settings-section-nav-DP9o4peU.js} +1 -1
- package/dist/assets/settings-users-page-BHFyDSsd.js +1 -0
- package/dist/assets/settings-wiki-page-BNaiupBm.js +1 -0
- package/dist/assets/sleep-page-BmOPF0yD.js +1 -0
- package/dist/assets/sports-page-BldIiclr.js +1 -0
- package/dist/assets/{state-B-4sS1xO.js → state-vCcAT5Hq.js} +1 -1
- package/dist/assets/strategies-page-CaTN99qj.js +1 -0
- package/dist/assets/strategy-detail-page-DsgFTd-U.js +1 -0
- package/dist/assets/strategy-dialog-7X7peRzu.js +1 -0
- package/dist/assets/surface-CJI17F3n.js +1 -0
- package/dist/assets/{table-WfAPUppN.js → table-BNqMG3_S.js} +1 -1
- package/dist/assets/task-detail-page-M1sIIPA8.js +1 -0
- package/dist/assets/timebox-planning-dialog-ByojN0AN.js +1 -0
- package/dist/assets/today-page-BjDijtn8.js +1 -0
- package/dist/assets/training-load-page-BPRmaWmF.js +1 -0
- package/dist/assets/{ui-C13Nbgas.js → ui-C1iwpj2-.js} +4 -4
- package/dist/assets/use-psyche-focus-target-Ct-acS9G.js +1 -0
- package/dist/assets/vendor-Dnkkx2co.js +1067 -0
- package/dist/assets/{vitals-page-qre17Nw8.js → vitals-page-CE9zdGLF.js} +1 -1
- package/dist/assets/weekly-review-page-CzhTv90n.js +1 -0
- package/dist/assets/weight-loss-page-Bp_cIk78.js +5 -0
- package/dist/assets/wiki-article-markdown-Dok2uy2p.js +4 -0
- package/dist/assets/wiki-editor-page-BuW32Y3f.js +26 -0
- package/dist/assets/wiki-ingest-history-page-DZKSBNHV.js +1 -0
- package/dist/assets/wiki-ingest-modal-DyqBEZcC.js +1 -0
- package/dist/assets/wiki-page-DqxlbLKG.js +1 -0
- package/dist/assets/workbench-flow-page-C4nc9jUg.js +5 -0
- package/dist/assets/workbench-page-BnyR7SL0.js +1 -0
- package/dist/assets/workout-detail-page-DT-c5cHL.js +2 -0
- package/dist/index.html +9 -9
- package/dist/openclaw/local-runtime.js +41 -14
- package/dist/server/server/migrations/067_weight_loss_daily_active_overrides.sql +13 -0
- package/dist/server/server/src/app.js +125 -33
- package/dist/server/server/src/health-weight-loss.js +554 -77
- package/dist/server/server/src/health.js +12 -4
- package/dist/server/server/src/movement.js +84 -1
- package/dist/server/server/src/openapi.js +123 -18
- package/dist/server/server/src/repositories/model-settings.js +21 -12
- package/dist/server/server/src/repositories/settings.js +19 -5
- package/dist/server/src/components/ui/info-tooltip.js +6 -6
- package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +13 -5
- package/dist/server/src/lib/api.js +24 -5
- package/dist/server/src/lib/theme-system.js +8 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -3
- package/server/migrations/067_weight_loss_daily_active_overrides.sql +13 -0
- package/skills/forge-openclaw/SKILL.md +13 -0
- package/skills/forge-openclaw/entity_conversation_playbooks.md +7 -1
- package/dist/assets/activity-page-CpjuNSHw.js +0 -1
- package/dist/assets/ai-surface-workspace-DEAFZruS.js +0 -1
- package/dist/assets/atlas-panel-CdVNPotj.js +0 -1
- package/dist/assets/calendar-page-DNNt6lfz.js +0 -1
- package/dist/assets/calendar-rules-DNJFNsxi.js +0 -1
- package/dist/assets/calendar-week-toolbar-BbPwYeN0.js +0 -1
- package/dist/assets/companion-sync-lab-page-KxEDigM6.js +0 -1
- package/dist/assets/daily-metrics-dashboard-B3cqJgDt.js +0 -1
- package/dist/assets/entity-note-count-link-DrhjJZ4i.js +0 -1
- package/dist/assets/entity-notes-surface-CkcRsKJQ.js +0 -1
- package/dist/assets/execution-board-D07gOocB.js +0 -1
- package/dist/assets/faceted-token-search-BxRRcM3q.js +0 -1
- package/dist/assets/flagship-signal-deck-cmy82b8_.js +0 -1
- package/dist/assets/floating-action-menu-Fs_ZiUMo.js +0 -1
- package/dist/assets/goal-detail-page-CqLiNz4f.js +0 -1
- package/dist/assets/goals-page-BTk7mg_T.js +0 -1
- package/dist/assets/habits-page-BJxagdzx.js +0 -1
- package/dist/assets/index-CF4J4R9L.js +0 -19
- package/dist/assets/index-CZbuZQjw.css +0 -1
- package/dist/assets/insight-flow-dialog-8f3D0GuC.js +0 -1
- package/dist/assets/insights-page-D6rOa7uk.js +0 -8
- package/dist/assets/kanban-page-XQ7Se6dH.js +0 -1
- package/dist/assets/knowledge-graph-page-BtAg8iv3.js +0 -1
- package/dist/assets/life-force-workspace-OfyB9HJM.js +0 -1
- package/dist/assets/metric-tile-DKpo-8xw.js +0 -1
- package/dist/assets/movement-page-Bg_T_Stx.js +0 -1
- package/dist/assets/note-markdown-N-uxD3Xt.js +0 -3
- package/dist/assets/note-tags-input-Cdu7wiw6.js +0 -1
- package/dist/assets/notes-page-CKXnF_KU.js +0 -1
- package/dist/assets/orbit-map-Dzi6KliQ.js +0 -1
- package/dist/assets/overview-page-1miYqaVS.js +0 -1
- package/dist/assets/page-hero-DRy5b2MU.js +0 -1
- package/dist/assets/pill-cluster-C9QczVJ2.js +0 -1
- package/dist/assets/preferences-page-DtNaF5Q3.js +0 -1
- package/dist/assets/project-collections-xPz2mlRr.js +0 -1
- package/dist/assets/project-management-hierarchy-page-DtRpMABw.js +0 -1
- package/dist/assets/project-management-section-nav-P3ixzPa-.js +0 -1
- package/dist/assets/projects-page-C5ViRuf4.js +0 -1
- package/dist/assets/psyche-behaviors-page-Dco46sC4.js +0 -5
- package/dist/assets/psyche-flashcards-page-ZcoEB8gV.js +0 -1
- package/dist/assets/psyche-goal-map-page-CLBAQOI0.js +0 -1
- package/dist/assets/psyche-graph-k4tX2tdp.js +0 -1
- package/dist/assets/psyche-mode-guide-page-hIVXcCnE.js +0 -1
- package/dist/assets/psyche-modes-page-0lYtBlhO.js +0 -1
- package/dist/assets/psyche-page-VZ9k9ISp.js +0 -1
- package/dist/assets/psyche-patterns-page-gx5nmdGq.js +0 -5
- package/dist/assets/psyche-questionnaire-builder-page-Bn0TOISd.js +0 -1
- package/dist/assets/psyche-questionnaire-detail-page-CmVzSd_s.js +0 -1
- package/dist/assets/psyche-questionnaire-run-detail-page-BsMbmXCG.js +0 -1
- package/dist/assets/psyche-questionnaire-run-page-CgkRL2vi.js +0 -1
- package/dist/assets/psyche-questionnaires-page-D7V8uLXM.js +0 -1
- package/dist/assets/psyche-report-detail-page-OlFq57eL.js +0 -3
- package/dist/assets/psyche-reports-page-dVUZjna1.js +0 -1
- package/dist/assets/psyche-schemas-beliefs-page-BCgc8FUd.js +0 -9
- package/dist/assets/psyche-screen-time-page-B_6BT_WN.js +0 -1
- package/dist/assets/psyche-self-observation-page-CEG5mluK.js +0 -1
- package/dist/assets/psyche-values-page-DRbRfEd6.js +0 -5
- package/dist/assets/report-chain-fields-CALCV3V5.js +0 -1
- package/dist/assets/rewards-page-DmC4R_Ps.js +0 -1
- package/dist/assets/scheduling-rules-editor-D02s70hr.js +0 -1
- package/dist/assets/schema-badge-BZO-qNhO.js +0 -1
- package/dist/assets/schema-visuals-D6nxjbYC.js +0 -1
- package/dist/assets/select-menu-fYyreSdQ.js +0 -1
- package/dist/assets/settings-agents-page-C_v_hMJF.js +0 -6
- package/dist/assets/settings-bin-page-DY5bg81n.js +0 -1
- package/dist/assets/settings-calendar-page-D1CzE6cg.js +0 -5
- package/dist/assets/settings-data-page-CHRQFU9H.js +0 -1
- package/dist/assets/settings-logs-page-B04pUwEv.js +0 -1
- package/dist/assets/settings-mobile-page-D9kTlYDS.js +0 -1
- package/dist/assets/settings-models-page-D26270R2.js +0 -1
- package/dist/assets/settings-page-DYDTFlnv.js +0 -1
- package/dist/assets/settings-rewards-page-cl4vqqO_.js +0 -1
- package/dist/assets/settings-users-page-BU79JB_T.js +0 -1
- package/dist/assets/settings-wiki-page-DwAUlyA3.js +0 -1
- package/dist/assets/sleep-page-D8NbdhyS.js +0 -1
- package/dist/assets/sports-page-CV4Cnzwn.js +0 -1
- package/dist/assets/strategies-page-C4qvXnql.js +0 -1
- package/dist/assets/strategy-detail-page-DJLo5rfy.js +0 -1
- package/dist/assets/strategy-dialog-D3AuUlVz.js +0 -1
- package/dist/assets/task-detail-page-z-9u9rF0.js +0 -1
- package/dist/assets/timebox-planning-dialog-DB6FLqmI.js +0 -1
- package/dist/assets/today-page-BKlu6gx5.js +0 -1
- package/dist/assets/training-load-page-CyJQqo_3.js +0 -1
- package/dist/assets/use-psyche-focus-target-C1C_XjYG.js +0 -1
- package/dist/assets/vendor-DHkYh85p.js +0 -1052
- package/dist/assets/weekly-review-page-Cz4vkRcx.js +0 -1
- package/dist/assets/weight-loss-page-BQrnOI0y.js +0 -1
- package/dist/assets/wiki-article-markdown-DdiR2TJE.js +0 -4
- package/dist/assets/wiki-editor-page-DqwoqVFb.js +0 -26
- package/dist/assets/wiki-ingest-history-page--evBLbOw.js +0 -1
- package/dist/assets/wiki-ingest-modal--ohzFnj2.js +0 -1
- package/dist/assets/wiki-page-B_VJFBPA.js +0 -1
- package/dist/assets/workbench-flow-page-Du62mtJU.js +0 -5
- package/dist/assets/workbench-page-4MKr3iRm.js +0 -1
- package/dist/assets/workout-detail-page-DfUbYYw1.js +0 -2
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { getDatabase, runInTransaction } from "./db.js";
|
|
4
4
|
import { getSettings } from "./repositories/settings.js";
|
|
5
5
|
import { getDefaultUser, resolveUserForMutation } from "./repositories/users.js";
|
|
6
|
+
import { readEncryptedSecret } from "./repositories/calendar.js";
|
|
6
7
|
const optionalNumberSchema = z
|
|
7
8
|
.union([z.coerce.number().finite(), z.null()])
|
|
8
9
|
.optional();
|
|
@@ -74,9 +75,25 @@ export const nutritionTargetUpdateSchema = z.object({
|
|
|
74
75
|
bodyGoal: z.string().trim().default(""),
|
|
75
76
|
notes: z.string().trim().default("")
|
|
76
77
|
});
|
|
78
|
+
export const nutritionDailyActiveCaloriesUpdateSchema = z.object({
|
|
79
|
+
userId: z.string().trim().min(1).optional(),
|
|
80
|
+
dayKey: z
|
|
81
|
+
.string()
|
|
82
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
83
|
+
.optional(),
|
|
84
|
+
activeCaloriesKcal: z
|
|
85
|
+
.union([z.null(), z.coerce.number().finite().min(0)])
|
|
86
|
+
.optional(),
|
|
87
|
+
notes: z.string().trim().default("")
|
|
88
|
+
});
|
|
77
89
|
export const nutritionFoodLogCreateSchema = z.object({
|
|
78
90
|
userId: z.string().trim().min(1).optional(),
|
|
79
91
|
loggedAt: z.string().datetime().optional(),
|
|
92
|
+
dayKey: z
|
|
93
|
+
.string()
|
|
94
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
95
|
+
.nullable()
|
|
96
|
+
.optional(),
|
|
80
97
|
mealLabel: z.string().trim().default(""),
|
|
81
98
|
source: z
|
|
82
99
|
.enum(["manual", "search", "barcode", "chatgpt", "photo", "saved_meal"])
|
|
@@ -167,7 +184,9 @@ export const nutritionExperimentCreateSchema = z.object({
|
|
|
167
184
|
userId: z.string().trim().min(1).optional(),
|
|
168
185
|
hypothesisId: z.string().trim().min(1).nullable().optional(),
|
|
169
186
|
title: z.string().trim().min(1),
|
|
170
|
-
status: z
|
|
187
|
+
status: z
|
|
188
|
+
.enum(["planned", "running", "complete", "paused"])
|
|
189
|
+
.default("planned"),
|
|
171
190
|
baselineStart: z.string().trim().nullable().optional(),
|
|
172
191
|
baselineEnd: z.string().trim().nullable().optional(),
|
|
173
192
|
interventionStart: z.string().trim().nullable().optional(),
|
|
@@ -177,7 +196,9 @@ export const nutritionExperimentCreateSchema = z.object({
|
|
|
177
196
|
adherence: z.record(z.string(), z.unknown()).default({}),
|
|
178
197
|
resultSummary: z.string().trim().default("")
|
|
179
198
|
});
|
|
180
|
-
export const nutritionExperimentPatchSchema = nutritionExperimentCreateSchema
|
|
199
|
+
export const nutritionExperimentPatchSchema = nutritionExperimentCreateSchema
|
|
200
|
+
.omit({ userId: true })
|
|
201
|
+
.partial();
|
|
181
202
|
export const nutritionParseRequestSchema = z.object({
|
|
182
203
|
text: z.string().trim().min(1),
|
|
183
204
|
mealTime: z.string().datetime().optional(),
|
|
@@ -195,6 +216,9 @@ function newId(prefix) {
|
|
|
195
216
|
function dayKey(value) {
|
|
196
217
|
return value.slice(0, 10);
|
|
197
218
|
}
|
|
219
|
+
function normalizeDayKey(value, fallbackIso) {
|
|
220
|
+
return value ?? dayKey(fallbackIso);
|
|
221
|
+
}
|
|
198
222
|
function jsonString(value) {
|
|
199
223
|
return JSON.stringify(value ?? null);
|
|
200
224
|
}
|
|
@@ -221,6 +245,280 @@ function average(values) {
|
|
|
221
245
|
}
|
|
222
246
|
return real.reduce((sum, value) => sum + value, 0) / real.length;
|
|
223
247
|
}
|
|
248
|
+
function metricTotal(metrics, key) {
|
|
249
|
+
const metric = metrics[key];
|
|
250
|
+
if (!metric || typeof metric !== "object") {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const record = metric;
|
|
254
|
+
for (const field of ["total", "average", "latest"]) {
|
|
255
|
+
const value = record[field];
|
|
256
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
function parsePlanNoteNumber(notes, key) {
|
|
263
|
+
if (!notes) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
267
|
+
const match = notes.match(new RegExp(`${escapedKey}=([^;]+)`));
|
|
268
|
+
const parsed = Number(match?.[1]?.trim());
|
|
269
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
270
|
+
}
|
|
271
|
+
function estimateStepActiveCaloriesKcal(input) {
|
|
272
|
+
if (input.stepCount == null ||
|
|
273
|
+
input.stepCount <= 0 ||
|
|
274
|
+
input.weightKg == null ||
|
|
275
|
+
input.weightKg <= 0) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const estimatedKilometers = (input.stepCount * 0.762) / 1000;
|
|
279
|
+
return estimatedKilometers * input.weightKg * 0.57;
|
|
280
|
+
}
|
|
281
|
+
function mapDailyEnergyOverride(row) {
|
|
282
|
+
if (!row) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
id: row.id,
|
|
287
|
+
userId: row.user_id,
|
|
288
|
+
dayKey: row.day_key,
|
|
289
|
+
activeCaloriesKcal: row.active_calories_kcal,
|
|
290
|
+
notes: row.notes,
|
|
291
|
+
createdAt: row.created_at,
|
|
292
|
+
updatedAt: row.updated_at
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function getDailyEnergyOverride(userId, dateKey) {
|
|
296
|
+
const row = getDatabase()
|
|
297
|
+
.prepare(`SELECT *
|
|
298
|
+
FROM nutrition_daily_energy_overrides
|
|
299
|
+
WHERE user_id = ?
|
|
300
|
+
AND day_key = ?`)
|
|
301
|
+
.get(userId, dateKey);
|
|
302
|
+
return mapDailyEnergyOverride(row);
|
|
303
|
+
}
|
|
304
|
+
function buildStoredEnergyModel(input) {
|
|
305
|
+
const todayKey = input.dateKey;
|
|
306
|
+
const start = new Date(`${todayKey}T00:00:00.000Z`);
|
|
307
|
+
start.setUTCDate(start.getUTCDate() - 6);
|
|
308
|
+
const startKey = start.toISOString().slice(0, 10);
|
|
309
|
+
const dailySummaryRows = getDatabase()
|
|
310
|
+
.prepare(`SELECT date_key, metrics_json
|
|
311
|
+
FROM health_daily_summaries
|
|
312
|
+
WHERE user_id = ?
|
|
313
|
+
AND summary_type = 'vitals'
|
|
314
|
+
AND date_key >= ?
|
|
315
|
+
ORDER BY date_key DESC`)
|
|
316
|
+
.all(input.userId, startKey);
|
|
317
|
+
const dailyHealthKit = dailySummaryRows.map((row) => {
|
|
318
|
+
const metrics = parseJson(row.metrics_json, {});
|
|
319
|
+
return {
|
|
320
|
+
dateKey: row.date_key,
|
|
321
|
+
activeEnergyKcal: metricTotal(metrics, "activeEnergyBurned"),
|
|
322
|
+
restingEnergyKcal: metricTotal(metrics, "basalEnergyBurned"),
|
|
323
|
+
exerciseMinutes: metricTotal(metrics, "appleExerciseTime"),
|
|
324
|
+
stepCount: metricTotal(metrics, "stepCount")
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
const workoutRows = getDatabase()
|
|
328
|
+
.prepare(`SELECT date(started_at) AS date_key,
|
|
329
|
+
SUM(active_energy_kcal) AS active_energy_kcal,
|
|
330
|
+
SUM(total_energy_kcal) AS total_energy_kcal,
|
|
331
|
+
NULL AS movement_calories_kcal
|
|
332
|
+
FROM health_workout_sessions
|
|
333
|
+
WHERE user_id = ?
|
|
334
|
+
AND date(started_at) >= ?
|
|
335
|
+
GROUP BY date(started_at)`)
|
|
336
|
+
.all(input.userId, startKey);
|
|
337
|
+
const movementRows = getDatabase()
|
|
338
|
+
.prepare(`SELECT date(started_at) AS date_key,
|
|
339
|
+
NULL AS active_energy_kcal,
|
|
340
|
+
NULL AS total_energy_kcal,
|
|
341
|
+
SUM(calories_kcal) AS movement_calories_kcal
|
|
342
|
+
FROM movement_trips
|
|
343
|
+
WHERE user_id = ?
|
|
344
|
+
AND date(started_at) >= ?
|
|
345
|
+
GROUP BY date(started_at)`)
|
|
346
|
+
.all(input.userId, startKey);
|
|
347
|
+
const workoutByDay = new Map(workoutRows.map((row) => [
|
|
348
|
+
row.date_key,
|
|
349
|
+
n(row.active_energy_kcal) || n(row.total_energy_kcal) || null
|
|
350
|
+
]));
|
|
351
|
+
const movementByDay = new Map(movementRows.map((row) => [
|
|
352
|
+
row.date_key,
|
|
353
|
+
n(row.movement_calories_kcal) || null
|
|
354
|
+
]));
|
|
355
|
+
const todayWorkoutEnergyFromRange = input.dayStartAt && input.dayEndAt
|
|
356
|
+
? getDatabase()
|
|
357
|
+
.prepare(`SELECT SUM(active_energy_kcal) AS active_energy_kcal,
|
|
358
|
+
SUM(total_energy_kcal) AS total_energy_kcal
|
|
359
|
+
FROM health_workout_sessions
|
|
360
|
+
WHERE user_id = ?
|
|
361
|
+
AND started_at >= ?
|
|
362
|
+
AND started_at < ?`)
|
|
363
|
+
.get(input.userId, input.dayStartAt, input.dayEndAt)
|
|
364
|
+
: null;
|
|
365
|
+
const todayMovementCaloriesFromRange = input.dayStartAt && input.dayEndAt
|
|
366
|
+
? getDatabase()
|
|
367
|
+
.prepare(`SELECT SUM(calories_kcal) AS movement_calories_kcal
|
|
368
|
+
FROM movement_trips
|
|
369
|
+
WHERE user_id = ?
|
|
370
|
+
AND started_at >= ?
|
|
371
|
+
AND started_at < ?`)
|
|
372
|
+
.get(input.userId, input.dayStartAt, input.dayEndAt)
|
|
373
|
+
: null;
|
|
374
|
+
const activeEnergyAverage = average(dailyHealthKit.map((day) => day.activeEnergyKcal));
|
|
375
|
+
const restingEnergyAverage = average(dailyHealthKit.map((day) => day.restingEnergyKcal));
|
|
376
|
+
const workoutEnergyAverage = average([...workoutByDay.values()]);
|
|
377
|
+
const movementCaloriesAverage = average([...movementByDay.values()]);
|
|
378
|
+
const fallbackActiveBurn = workoutEnergyAverage != null || movementCaloriesAverage != null
|
|
379
|
+
? n(workoutEnergyAverage) + n(movementCaloriesAverage)
|
|
380
|
+
: null;
|
|
381
|
+
const activeBurnKcal = activeEnergyAverage ?? fallbackActiveBurn;
|
|
382
|
+
const baselineActiveCalories = input.defaultActiveCalories ?? activeBurnKcal ?? 0;
|
|
383
|
+
const todayHealthKitActive = dailyHealthKit.find((day) => day.dateKey === todayKey)?.activeEnergyKcal ??
|
|
384
|
+
null;
|
|
385
|
+
const todayStepCount = dailyHealthKit.find((day) => day.dateKey === todayKey)?.stepCount ?? null;
|
|
386
|
+
const todayWorkoutEnergy = todayWorkoutEnergyFromRange != null
|
|
387
|
+
? n(todayWorkoutEnergyFromRange.active_energy_kcal) ||
|
|
388
|
+
n(todayWorkoutEnergyFromRange.total_energy_kcal) ||
|
|
389
|
+
null
|
|
390
|
+
: (workoutByDay.get(todayKey) ?? null);
|
|
391
|
+
const todayMovementCalories = todayMovementCaloriesFromRange != null
|
|
392
|
+
? n(todayMovementCaloriesFromRange.movement_calories_kcal) || null
|
|
393
|
+
: (movementByDay.get(todayKey) ?? null);
|
|
394
|
+
const todayWorkoutMovementCalories = todayWorkoutEnergy != null || todayMovementCalories != null
|
|
395
|
+
? n(todayWorkoutEnergy) + n(todayMovementCalories)
|
|
396
|
+
: null;
|
|
397
|
+
const todayStepEstimatedCalories = estimateStepActiveCaloriesKcal({
|
|
398
|
+
stepCount: todayStepCount,
|
|
399
|
+
weightKg: input.latestWeightKg
|
|
400
|
+
});
|
|
401
|
+
const todayStepCaloriesForActiveBudget = todayStepEstimatedCalories != null &&
|
|
402
|
+
(todayWorkoutEnergy != null ||
|
|
403
|
+
todayMovementCalories != null ||
|
|
404
|
+
todayStepEstimatedCalories > baselineActiveCalories)
|
|
405
|
+
? todayStepEstimatedCalories
|
|
406
|
+
: null;
|
|
407
|
+
const todayFallbackPartCount = [
|
|
408
|
+
todayWorkoutEnergy,
|
|
409
|
+
todayMovementCalories,
|
|
410
|
+
todayStepCaloriesForActiveBudget
|
|
411
|
+
].filter((value) => value != null).length;
|
|
412
|
+
const todayFallbackActiveCalories = todayFallbackPartCount > 0
|
|
413
|
+
? n(todayWorkoutEnergy) +
|
|
414
|
+
n(todayMovementCalories) +
|
|
415
|
+
n(todayStepCaloriesForActiveBudget)
|
|
416
|
+
: null;
|
|
417
|
+
const todayObservedActiveCalories = todayHealthKitActive ?? todayFallbackActiveCalories;
|
|
418
|
+
const todayFallbackSource = (() => {
|
|
419
|
+
if (todayWorkoutEnergy != null &&
|
|
420
|
+
todayMovementCalories != null &&
|
|
421
|
+
todayStepCaloriesForActiveBudget != null) {
|
|
422
|
+
return "today_workout_movement_step_energy";
|
|
423
|
+
}
|
|
424
|
+
if (todayWorkoutEnergy != null &&
|
|
425
|
+
todayStepCaloriesForActiveBudget != null) {
|
|
426
|
+
return "today_workout_step_energy";
|
|
427
|
+
}
|
|
428
|
+
if (todayMovementCalories != null &&
|
|
429
|
+
todayStepCaloriesForActiveBudget != null) {
|
|
430
|
+
return "today_movement_step_energy";
|
|
431
|
+
}
|
|
432
|
+
if (todayWorkoutEnergy != null && todayMovementCalories != null) {
|
|
433
|
+
return "today_workout_movement_energy";
|
|
434
|
+
}
|
|
435
|
+
if (todayWorkoutEnergy != null) {
|
|
436
|
+
return "today_workout_energy";
|
|
437
|
+
}
|
|
438
|
+
if (todayMovementCalories != null) {
|
|
439
|
+
return "today_movement_trip_calories";
|
|
440
|
+
}
|
|
441
|
+
if (todayStepCaloriesForActiveBudget != null) {
|
|
442
|
+
return "today_step_estimate";
|
|
443
|
+
}
|
|
444
|
+
return "default_active_calories";
|
|
445
|
+
})();
|
|
446
|
+
const todayActiveSource = input.dailyActiveOverride != null
|
|
447
|
+
? "user_override"
|
|
448
|
+
: todayHealthKitActive != null
|
|
449
|
+
? "today_healthkit_active_energy"
|
|
450
|
+
: todayFallbackActiveCalories != null
|
|
451
|
+
? todayFallbackSource
|
|
452
|
+
: "default_active_calories";
|
|
453
|
+
const todayActiveCalories = input.dailyActiveOverride?.activeCaloriesKcal ??
|
|
454
|
+
todayObservedActiveCalories ??
|
|
455
|
+
baselineActiveCalories;
|
|
456
|
+
const todayTargetAdjustmentKcal = todayActiveCalories - baselineActiveCalories;
|
|
457
|
+
const estimatedTdeeKcal = activeBurnKcal != null && restingEnergyAverage != null
|
|
458
|
+
? round(activeBurnKcal + restingEnergyAverage, 0)
|
|
459
|
+
: input.inferredTdee;
|
|
460
|
+
const hasHealthKitDailyActiveEnergy = activeEnergyAverage != null;
|
|
461
|
+
const hasHealthKitEnergy = hasHealthKitDailyActiveEnergy ||
|
|
462
|
+
restingEnergyAverage != null ||
|
|
463
|
+
workoutEnergyAverage != null;
|
|
464
|
+
const hasMovementEnergy = movementCaloriesAverage != null;
|
|
465
|
+
const sourceConfidence = activeEnergyAverage != null
|
|
466
|
+
? "healthkit_daily_active_energy"
|
|
467
|
+
: fallbackActiveBurn != null
|
|
468
|
+
? "workout_movement_fallback"
|
|
469
|
+
: "target_inference_only";
|
|
470
|
+
return {
|
|
471
|
+
activeEnergyCalories: activeEnergyAverage != null ? round(activeEnergyAverage, 0) : null,
|
|
472
|
+
restingEnergyCalories: restingEnergyAverage != null ? round(restingEnergyAverage, 0) : null,
|
|
473
|
+
wearableConfidence: hasHealthKitEnergy
|
|
474
|
+
? "measured_directional"
|
|
475
|
+
: "directional",
|
|
476
|
+
inferredTdee: input.inferredTdee,
|
|
477
|
+
estimatedTdeeKcal,
|
|
478
|
+
activeBurnKcal: activeBurnKcal != null ? round(activeBurnKcal, 0) : null,
|
|
479
|
+
baselineActiveCaloriesKcal: round(baselineActiveCalories, 0),
|
|
480
|
+
todayActiveCaloriesKcal: round(todayActiveCalories, 0),
|
|
481
|
+
todayObservedActiveCaloriesKcal: todayObservedActiveCalories != null
|
|
482
|
+
? round(todayObservedActiveCalories, 0)
|
|
483
|
+
: null,
|
|
484
|
+
todayActiveCaloriesSource: todayActiveSource,
|
|
485
|
+
todayTargetAdjustmentKcal: round(todayTargetAdjustmentKcal, 0),
|
|
486
|
+
todayWorkoutEnergyKcal: todayWorkoutEnergy != null ? round(todayWorkoutEnergy, 0) : null,
|
|
487
|
+
todayMovementCaloriesKcal: todayMovementCalories != null ? round(todayMovementCalories, 0) : null,
|
|
488
|
+
todayHealthKitActiveCaloriesKcal: todayHealthKitActive != null ? round(todayHealthKitActive, 0) : null,
|
|
489
|
+
todayStepCount: todayStepCount != null ? round(todayStepCount, 0) : null,
|
|
490
|
+
todayStepEstimatedCaloriesKcal: todayStepEstimatedCalories != null
|
|
491
|
+
? round(todayStepEstimatedCalories, 0)
|
|
492
|
+
: null,
|
|
493
|
+
todayActiveOverride: input.dailyActiveOverride,
|
|
494
|
+
movementCaloriesKcal: movementCaloriesAverage != null
|
|
495
|
+
? round(movementCaloriesAverage, 0)
|
|
496
|
+
: null,
|
|
497
|
+
workoutEnergyKcal: workoutEnergyAverage != null ? round(workoutEnergyAverage, 0) : null,
|
|
498
|
+
averageCalorieIntake: input.averageCalories,
|
|
499
|
+
recentFoodLogCount: input.recentFoodLogCount,
|
|
500
|
+
recentFoodLogDayCount: input.recentFoodLogDayCount,
|
|
501
|
+
currentDeficitEstimate: estimatedTdeeKcal != null
|
|
502
|
+
? round(input.averageCalories - estimatedTdeeKcal, 0)
|
|
503
|
+
: null,
|
|
504
|
+
estimatedDailyEnergyBalanceKcal: estimatedTdeeKcal != null
|
|
505
|
+
? round(input.averageCalories - estimatedTdeeKcal, 0)
|
|
506
|
+
: null,
|
|
507
|
+
energySourceConfidence: sourceConfidence,
|
|
508
|
+
evidenceDays: new Set([
|
|
509
|
+
...dailyHealthKit.map((day) => day.dateKey),
|
|
510
|
+
...workoutRows.map((row) => row.date_key),
|
|
511
|
+
...movementRows.map((row) => row.date_key)
|
|
512
|
+
]).size,
|
|
513
|
+
exerciseMinutesAverage: average(dailyHealthKit.map((day) => day.exerciseMinutes)),
|
|
514
|
+
stepCountAverage: average(dailyHealthKit.map((day) => day.stepCount)),
|
|
515
|
+
sourceAvailability: {
|
|
516
|
+
healthKitDailyEnergy: hasHealthKitDailyActiveEnergy,
|
|
517
|
+
movementTripCalories: hasMovementEnergy,
|
|
518
|
+
workoutEnergy: workoutEnergyAverage != null
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
224
522
|
function resolveWriteUser(userId) {
|
|
225
523
|
return resolveUserForMutation(userId ?? null).id;
|
|
226
524
|
}
|
|
@@ -228,6 +526,27 @@ function resolveReadUser(userIds) {
|
|
|
228
526
|
return userIds?.[0] ?? getDefaultUser().id;
|
|
229
527
|
}
|
|
230
528
|
function mapFood(row) {
|
|
529
|
+
const nutrients = parseJson(row.nutrients_json, {});
|
|
530
|
+
const servingGrams = row.serving_grams ??
|
|
531
|
+
(row.source === "open_food_facts"
|
|
532
|
+
? parseGramQuantity(row.serving_label)
|
|
533
|
+
: null);
|
|
534
|
+
const openFoodFactsNutrient = (currentValue, per100gKey, perServingKey, multiplier = 1) => {
|
|
535
|
+
if (row.source !== "open_food_facts") {
|
|
536
|
+
return currentValue;
|
|
537
|
+
}
|
|
538
|
+
const perServing = nutrients[perServingKey];
|
|
539
|
+
if (typeof perServing === "number" && Number.isFinite(perServing)) {
|
|
540
|
+
return round(perServing * multiplier, multiplier === 1 ? 1 : 0);
|
|
541
|
+
}
|
|
542
|
+
const per100g = nutrients[per100gKey];
|
|
543
|
+
if (typeof per100g === "number" &&
|
|
544
|
+
Number.isFinite(per100g) &&
|
|
545
|
+
servingGrams != null) {
|
|
546
|
+
return round((per100g * servingGrams * multiplier) / 100, multiplier === 1 ? 1 : 0);
|
|
547
|
+
}
|
|
548
|
+
return currentValue;
|
|
549
|
+
};
|
|
231
550
|
return {
|
|
232
551
|
id: row.id,
|
|
233
552
|
source: row.source,
|
|
@@ -236,21 +555,21 @@ function mapFood(row) {
|
|
|
236
555
|
name: row.name,
|
|
237
556
|
brand: row.brand,
|
|
238
557
|
servingLabel: row.serving_label,
|
|
239
|
-
servingGrams
|
|
240
|
-
calories: row.calories,
|
|
241
|
-
proteinGrams: row.protein_grams,
|
|
242
|
-
carbohydrateGrams: row.carbohydrate_grams,
|
|
243
|
-
fatGrams: row.fat_grams,
|
|
244
|
-
fiberGrams: row.fiber_grams,
|
|
245
|
-
sugarGrams: row.sugar_grams,
|
|
246
|
-
sodiumMg: row.sodium_mg,
|
|
558
|
+
servingGrams,
|
|
559
|
+
calories: openFoodFactsNutrient(row.calories, "energy-kcal_100g", "energy-kcal_serving"),
|
|
560
|
+
proteinGrams: openFoodFactsNutrient(row.protein_grams, "proteins_100g", "proteins_serving"),
|
|
561
|
+
carbohydrateGrams: openFoodFactsNutrient(row.carbohydrate_grams, "carbohydrates_100g", "carbohydrates_serving"),
|
|
562
|
+
fatGrams: openFoodFactsNutrient(row.fat_grams, "fat_100g", "fat_serving"),
|
|
563
|
+
fiberGrams: openFoodFactsNutrient(row.fiber_grams, "fiber_100g", "fiber_serving"),
|
|
564
|
+
sugarGrams: openFoodFactsNutrient(row.sugar_grams, "sugars_100g", "sugars_serving"),
|
|
565
|
+
sodiumMg: openFoodFactsNutrient(row.sodium_mg, "sodium_100g", "sodium_serving", 1000),
|
|
247
566
|
potassiumMg: row.potassium_mg,
|
|
248
567
|
caffeineMg: row.caffeine_mg,
|
|
249
568
|
alcoholGrams: row.alcohol_grams,
|
|
250
569
|
novaGroup: row.nova_group,
|
|
251
570
|
nutriScore: row.nutri_score,
|
|
252
571
|
tags: parseJson(row.tags_json, []),
|
|
253
|
-
nutrients
|
|
572
|
+
nutrients,
|
|
254
573
|
confidence: row.confidence,
|
|
255
574
|
createdAt: row.created_at,
|
|
256
575
|
updatedAt: row.updated_at
|
|
@@ -382,6 +701,7 @@ export function createNutritionFoodLog(input) {
|
|
|
382
701
|
const parsed = nutritionFoodLogCreateSchema.parse(input);
|
|
383
702
|
const userId = resolveWriteUser(parsed.userId);
|
|
384
703
|
const loggedAt = parsed.loggedAt ?? nowIso();
|
|
704
|
+
const logDayKey = normalizeDayKey(parsed.dayKey, loggedAt);
|
|
385
705
|
const id = newId("meal");
|
|
386
706
|
const now = nowIso();
|
|
387
707
|
runInTransaction(() => {
|
|
@@ -391,7 +711,7 @@ export function createNutritionFoodLog(input) {
|
|
|
391
711
|
place_id, stay_id, workout_id, sleep_id, day_key, image_refs_json,
|
|
392
712
|
parser_provenance_json, links_json, created_at, updated_at
|
|
393
713
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
394
|
-
.run(id, userId, loggedAt, parsed.mealLabel, parsed.source, parsed.confirmationState, parsed.notes, parsed.placeId ?? null, parsed.stayId ?? null, parsed.workoutId ?? null, parsed.sleepId ?? null,
|
|
714
|
+
.run(id, userId, loggedAt, parsed.mealLabel, parsed.source, parsed.confirmationState, parsed.notes, parsed.placeId ?? null, parsed.stayId ?? null, parsed.workoutId ?? null, parsed.sleepId ?? null, logDayKey, jsonString(parsed.imageRefs), jsonString(parsed.parserProvenance), jsonString(parsed.links), now, now);
|
|
395
715
|
for (const item of parsed.items) {
|
|
396
716
|
insertMealItem(id, item);
|
|
397
717
|
}
|
|
@@ -405,6 +725,7 @@ export function patchNutritionFoodLog(logId, input) {
|
|
|
405
725
|
return null;
|
|
406
726
|
}
|
|
407
727
|
const nextLoggedAt = parsed.loggedAt ?? existing.loggedAt;
|
|
728
|
+
const nextDayKey = normalizeDayKey(parsed.dayKey, nextLoggedAt);
|
|
408
729
|
const now = nowIso();
|
|
409
730
|
runInTransaction(() => {
|
|
410
731
|
getDatabase()
|
|
@@ -414,7 +735,7 @@ export function patchNutritionFoodLog(logId, input) {
|
|
|
414
735
|
day_key = ?, image_refs_json = ?, parser_provenance_json = ?,
|
|
415
736
|
links_json = ?, updated_at = ?
|
|
416
737
|
WHERE id = ?`)
|
|
417
|
-
.run(nextLoggedAt, parsed.mealLabel ?? existing.mealLabel, parsed.source ?? existing.source, parsed.confirmationState ?? existing.confirmationState, parsed.notes ?? existing.notes, parsed.placeId !== undefined ? parsed.placeId : existing.placeId, parsed.stayId !== undefined ? parsed.stayId : existing.stayId, parsed.workoutId !== undefined ? parsed.workoutId : existing.workoutId, parsed.sleepId !== undefined ? parsed.sleepId : existing.sleepId,
|
|
738
|
+
.run(nextLoggedAt, parsed.mealLabel ?? existing.mealLabel, parsed.source ?? existing.source, parsed.confirmationState ?? existing.confirmationState, parsed.notes ?? existing.notes, parsed.placeId !== undefined ? parsed.placeId : existing.placeId, parsed.stayId !== undefined ? parsed.stayId : existing.stayId, parsed.workoutId !== undefined ? parsed.workoutId : existing.workoutId, parsed.sleepId !== undefined ? parsed.sleepId : existing.sleepId, nextDayKey, jsonString(parsed.imageRefs ?? existing.imageRefs), jsonString(parsed.parserProvenance ?? existing.parserProvenance), jsonString(parsed.links ?? existing.links), now, logId);
|
|
418
739
|
if (parsed.items) {
|
|
419
740
|
getDatabase()
|
|
420
741
|
.prepare(`DELETE FROM nutrition_meal_items WHERE log_id = ?`)
|
|
@@ -479,6 +800,34 @@ export function updateNutritionTarget(input) {
|
|
|
479
800
|
.prepare(`SELECT * FROM nutrition_targets WHERE user_id = ?`)
|
|
480
801
|
.get(userId), userId);
|
|
481
802
|
}
|
|
803
|
+
export function updateNutritionDailyActiveCalories(input) {
|
|
804
|
+
const parsed = nutritionDailyActiveCaloriesUpdateSchema.parse(input);
|
|
805
|
+
const userId = resolveWriteUser(parsed.userId);
|
|
806
|
+
const dateKey = parsed.dayKey ?? new Date().toISOString().slice(0, 10);
|
|
807
|
+
const now = nowIso();
|
|
808
|
+
if (parsed.activeCaloriesKcal == null) {
|
|
809
|
+
getDatabase()
|
|
810
|
+
.prepare(`DELETE FROM nutrition_daily_energy_overrides
|
|
811
|
+
WHERE user_id = ?
|
|
812
|
+
AND day_key = ?`)
|
|
813
|
+
.run(userId, dateKey);
|
|
814
|
+
return { override: null, dayKey: dateKey };
|
|
815
|
+
}
|
|
816
|
+
const id = newId("daily_energy");
|
|
817
|
+
getDatabase()
|
|
818
|
+
.prepare(`INSERT INTO nutrition_daily_energy_overrides (
|
|
819
|
+
id, user_id, day_key, active_calories_kcal, notes, created_at, updated_at
|
|
820
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
821
|
+
ON CONFLICT(user_id, day_key) DO UPDATE SET
|
|
822
|
+
active_calories_kcal = excluded.active_calories_kcal,
|
|
823
|
+
notes = excluded.notes,
|
|
824
|
+
updated_at = excluded.updated_at`)
|
|
825
|
+
.run(id, userId, dateKey, parsed.activeCaloriesKcal, parsed.notes, now, now);
|
|
826
|
+
return {
|
|
827
|
+
override: getDailyEnergyOverride(userId, dateKey),
|
|
828
|
+
dayKey: dateKey
|
|
829
|
+
};
|
|
830
|
+
}
|
|
482
831
|
export function createNutritionBodyCheckin(input) {
|
|
483
832
|
const parsed = nutritionBodyCheckinCreateSchema.parse(input);
|
|
484
833
|
const userId = resolveWriteUser(parsed.userId);
|
|
@@ -572,7 +921,9 @@ export function patchNutritionExperiment(experimentId, input) {
|
|
|
572
921
|
? parsed.hypothesisId
|
|
573
922
|
: existing.hypothesis_id, parsed.title ?? existing.title, parsed.status ?? existing.status, parsed.baselineStart !== undefined
|
|
574
923
|
? parsed.baselineStart
|
|
575
|
-
: existing.baseline_start, parsed.baselineEnd !== undefined
|
|
924
|
+
: existing.baseline_start, parsed.baselineEnd !== undefined
|
|
925
|
+
? parsed.baselineEnd
|
|
926
|
+
: existing.baseline_end, parsed.interventionStart !== undefined
|
|
576
927
|
? parsed.interventionStart
|
|
577
928
|
: existing.intervention_start, parsed.interventionEnd !== undefined
|
|
578
929
|
? parsed.interventionEnd
|
|
@@ -730,9 +1081,8 @@ function listExperiments(userId, limit = 20) {
|
|
|
730
1081
|
updatedAt: row.updated_at
|
|
731
1082
|
}));
|
|
732
1083
|
}
|
|
733
|
-
function buildTodayLedger(logs, target) {
|
|
734
|
-
const
|
|
735
|
-
const todayLogs = logs.filter((log) => log.dayKey === today);
|
|
1084
|
+
function buildTodayLedger(logs, target, dateKey, dynamicTargetCalories, activeAdjustmentCalories, activeCaloriesSource) {
|
|
1085
|
+
const todayLogs = logs.filter((log) => log.dayKey === dateKey);
|
|
736
1086
|
const totals = todayLogs.reduce((acc, log) => ({
|
|
737
1087
|
calories: acc.calories + log.totals.calories,
|
|
738
1088
|
proteinGrams: acc.proteinGrams + log.totals.proteinGrams,
|
|
@@ -753,11 +1103,14 @@ function buildTodayLedger(logs, target) {
|
|
|
753
1103
|
alcoholGrams: 0
|
|
754
1104
|
});
|
|
755
1105
|
return {
|
|
756
|
-
dateKey
|
|
1106
|
+
dateKey,
|
|
757
1107
|
meals: todayLogs,
|
|
758
1108
|
totals,
|
|
759
|
-
|
|
760
|
-
|
|
1109
|
+
plannedTargetCalories: target.calorieTarget,
|
|
1110
|
+
targetCalories: dynamicTargetCalories,
|
|
1111
|
+
activeAdjustmentCalories,
|
|
1112
|
+
activeCaloriesSource,
|
|
1113
|
+
calorieDelta: round(totals.calories - dynamicTargetCalories, 0),
|
|
761
1114
|
proteinCoverage: n(target.proteinGramsTarget) > 0
|
|
762
1115
|
? round(totals.proteinGrams / n(target.proteinGramsTarget), 2)
|
|
763
1116
|
: null,
|
|
@@ -767,15 +1120,37 @@ function buildTodayLedger(logs, target) {
|
|
|
767
1120
|
unconfirmedCount: todayLogs.filter((log) => log.confirmationState !== "confirmed").length
|
|
768
1121
|
};
|
|
769
1122
|
}
|
|
770
|
-
function
|
|
1123
|
+
function latestHealthKitBodyMass(userId) {
|
|
1124
|
+
const rows = getDatabase()
|
|
1125
|
+
.prepare(`SELECT date_key, metrics_json
|
|
1126
|
+
FROM health_daily_summaries
|
|
1127
|
+
WHERE user_id = ?
|
|
1128
|
+
AND summary_type = 'vitals'
|
|
1129
|
+
ORDER BY date_key DESC
|
|
1130
|
+
LIMIT 30`)
|
|
1131
|
+
.all(userId);
|
|
1132
|
+
for (const row of rows) {
|
|
1133
|
+
const metrics = parseJson(row.metrics_json, {});
|
|
1134
|
+
const bodyMassKg = metricTotal(metrics, "bodyMass");
|
|
1135
|
+
if (bodyMassKg != null && bodyMassKg > 0) {
|
|
1136
|
+
return {
|
|
1137
|
+
weightKg: round(bodyMassKg, 2),
|
|
1138
|
+
checkedAt: `${row.date_key}T12:00:00.000Z`
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
function buildWeightTrend(userId, body) {
|
|
771
1145
|
const withWeight = body
|
|
772
1146
|
.filter((entry) => typeof entry.weightKg === "number")
|
|
773
1147
|
.slice()
|
|
774
1148
|
.reverse();
|
|
775
1149
|
const latest = withWeight.at(-1) ?? null;
|
|
1150
|
+
const healthKitFallback = latest ? null : latestHealthKitBodyMass(userId);
|
|
776
1151
|
const previous = withWeight.length > 1 ? withWeight.at(-2) : null;
|
|
777
1152
|
const first = withWeight[0] ?? null;
|
|
778
|
-
const latestWeight = latest?.weightKg ?? null;
|
|
1153
|
+
const latestWeight = latest?.weightKg ?? healthKitFallback?.weightKg ?? null;
|
|
779
1154
|
const deltaFromPrevious = latestWeight != null && previous?.weightKg != null
|
|
780
1155
|
? round(latestWeight - previous.weightKg, 2)
|
|
781
1156
|
: null;
|
|
@@ -784,17 +1159,28 @@ function buildWeightTrend(body) {
|
|
|
784
1159
|
: null;
|
|
785
1160
|
return {
|
|
786
1161
|
latestWeightKg: latestWeight,
|
|
787
|
-
latestCheckedAt: latest?.checkedAt ?? null,
|
|
1162
|
+
latestCheckedAt: latest?.checkedAt ?? healthKitFallback?.checkedAt ?? null,
|
|
1163
|
+
latestWeightSource: latest
|
|
1164
|
+
? "nutrition_body_checkin"
|
|
1165
|
+
: healthKitFallback
|
|
1166
|
+
? "healthkit_body_mass"
|
|
1167
|
+
: null,
|
|
788
1168
|
deltaFromPreviousKg: deltaFromPrevious,
|
|
789
1169
|
deltaFromFirstKg: deltaFromFirst,
|
|
790
1170
|
trendWeightKg: withWeight.length > 0
|
|
791
1171
|
? round(average(withWeight.slice(-7).map((entry) => entry.weightKg)) ?? 0, 2)
|
|
792
|
-
: null,
|
|
1172
|
+
: (healthKitFallback?.weightKg ?? null),
|
|
793
1173
|
weeklyRateKg: withWeight.length >= 2 && first && latest
|
|
794
|
-
? round((
|
|
1174
|
+
? round((latest.weightKg - first.weightKg) /
|
|
795
1175
|
Math.max(1, (new Date(latest.checkedAt).getTime() -
|
|
796
1176
|
new Date(first.checkedAt).getTime()) /
|
|
797
|
-
(7 * 24 * 60 * 60 * 1000))
|
|
1177
|
+
(7 * 24 * 60 * 60 * 1000)), 2)
|
|
1178
|
+
: null,
|
|
1179
|
+
sevenDayRateKg: withWeight.length >= 2 && first && latest
|
|
1180
|
+
? round((latest.weightKg - first.weightKg) /
|
|
1181
|
+
Math.max(1, (new Date(latest.checkedAt).getTime() -
|
|
1182
|
+
new Date(first.checkedAt).getTime()) /
|
|
1183
|
+
(7 * 24 * 60 * 60 * 1000)), 2)
|
|
798
1184
|
: null,
|
|
799
1185
|
waistToHeightRatio: null
|
|
800
1186
|
};
|
|
@@ -812,8 +1198,7 @@ function buildFoodQuality(logs) {
|
|
|
812
1198
|
const calorieBase = Math.max(1, totals.calories / 1000);
|
|
813
1199
|
const highProteinShare = round((tagCounts.get("high_protein") ?? 0) / total, 2);
|
|
814
1200
|
const highFiberShare = round((tagCounts.get("high_fiber") ?? 0) / total, 2);
|
|
815
|
-
const ultraProcessedShare = round(((tagCounts.get("ultra_processed") ?? 0) +
|
|
816
|
-
(tagCounts.get("nova_4") ?? 0)) /
|
|
1201
|
+
const ultraProcessedShare = round(((tagCounts.get("ultra_processed") ?? 0) + (tagCounts.get("nova_4") ?? 0)) /
|
|
817
1202
|
total, 2);
|
|
818
1203
|
return {
|
|
819
1204
|
itemCount: items.length,
|
|
@@ -862,7 +1247,9 @@ function buildGutSummary(checkins) {
|
|
|
862
1247
|
averageBloating,
|
|
863
1248
|
averageReflux,
|
|
864
1249
|
averageAbdominalPain,
|
|
865
|
-
gutComfortScore: discomfortAverage == null
|
|
1250
|
+
gutComfortScore: discomfortAverage == null
|
|
1251
|
+
? null
|
|
1252
|
+
: round(Math.max(0, 10 - discomfortAverage), 1),
|
|
866
1253
|
bristolDistribution: [1, 2, 3, 4, 5, 6, 7].map((type) => ({
|
|
867
1254
|
type,
|
|
868
1255
|
count: checkins.filter((entry) => entry.bristolStoolType === type).length
|
|
@@ -874,7 +1261,9 @@ function buildGeneratedHypotheses(logs, subjective, gut, appearance) {
|
|
|
874
1261
|
const cards = [];
|
|
875
1262
|
const lateMeals = logs.filter((log) => new Date(log.loggedAt).getHours() >= 21);
|
|
876
1263
|
const lowEnergy = subjective.filter((entry) => typeof entry.energy === "number" && entry.energy <= 4);
|
|
877
|
-
const gutSymptoms = gut.filter((entry) => n(entry.bloating) >= 6 ||
|
|
1264
|
+
const gutSymptoms = gut.filter((entry) => n(entry.bloating) >= 6 ||
|
|
1265
|
+
n(entry.reflux) >= 6 ||
|
|
1266
|
+
n(entry.abdominalPain) >= 6);
|
|
878
1267
|
const puffiness = appearance.filter((entry) => typeof entry.facePuffiness === "number" && entry.facePuffiness >= 6);
|
|
879
1268
|
if (lateMeals.length >= 2 && puffiness.length >= 1) {
|
|
880
1269
|
cards.push({
|
|
@@ -923,9 +1312,10 @@ function buildGeneratedHypotheses(logs, subjective, gut, appearance) {
|
|
|
923
1312
|
}
|
|
924
1313
|
return cards;
|
|
925
1314
|
}
|
|
926
|
-
export function getWeightLossViewData(userIds) {
|
|
1315
|
+
export function getWeightLossViewData(userIds, options = {}) {
|
|
927
1316
|
const generatedAt = new Date().toISOString();
|
|
928
1317
|
const userId = resolveReadUser(userIds);
|
|
1318
|
+
const todayKey = options.dateKey ?? generatedAt.slice(0, 10);
|
|
929
1319
|
const targetRow = getDatabase()
|
|
930
1320
|
.prepare(`SELECT * FROM nutrition_targets WHERE user_id = ?`)
|
|
931
1321
|
.get(userId);
|
|
@@ -938,14 +1328,34 @@ export function getWeightLossViewData(userIds) {
|
|
|
938
1328
|
const storedHypotheses = listHypotheses(userId);
|
|
939
1329
|
const generatedHypotheses = buildGeneratedHypotheses(logs, subjective, gut, appearance);
|
|
940
1330
|
const experiments = listExperiments(userId);
|
|
941
|
-
const
|
|
1331
|
+
const weightTrend = buildWeightTrend(userId, body);
|
|
942
1332
|
const recentLogs = logs.slice(0, 14);
|
|
943
1333
|
const recentTotals = sumItems(recentLogs.flatMap((log) => log.items));
|
|
944
1334
|
const trackedDays = new Set(logs.map((log) => log.dayKey)).size;
|
|
945
|
-
const
|
|
1335
|
+
const recentTrackedDays = new Set(recentLogs.map((log) => log.dayKey)).size;
|
|
1336
|
+
const averageCalories = recentTrackedDays > 0
|
|
1337
|
+
? round(recentTotals.calories / recentTrackedDays, 0)
|
|
1338
|
+
: 0;
|
|
946
1339
|
const inferredTdee = target.calorieTarget != null
|
|
947
1340
|
? round(target.calorieTarget + Math.abs(n(target.weeklyRateGoalKg)) * 1100, 0)
|
|
948
1341
|
: null;
|
|
1342
|
+
const defaultActiveCalories = parsePlanNoteNumber(target.notes, "activity_kcal");
|
|
1343
|
+
const dailyActiveOverride = getDailyEnergyOverride(userId, todayKey);
|
|
1344
|
+
const energyModel = buildStoredEnergyModel({
|
|
1345
|
+
userId,
|
|
1346
|
+
dateKey: todayKey,
|
|
1347
|
+
dayStartAt: options.dayStartAt,
|
|
1348
|
+
dayEndAt: options.dayEndAt,
|
|
1349
|
+
inferredTdee,
|
|
1350
|
+
averageCalories,
|
|
1351
|
+
recentFoodLogCount: recentLogs.length,
|
|
1352
|
+
recentFoodLogDayCount: recentTrackedDays,
|
|
1353
|
+
defaultActiveCalories,
|
|
1354
|
+
latestWeightKg: weightTrend.latestWeightKg,
|
|
1355
|
+
dailyActiveOverride
|
|
1356
|
+
});
|
|
1357
|
+
const todayTargetCalories = Math.max(0, round(target.calorieTarget + energyModel.todayTargetAdjustmentKcal, 0));
|
|
1358
|
+
const todayLedger = buildTodayLedger(logs, target, todayKey, todayTargetCalories, energyModel.todayTargetAdjustmentKcal, energyModel.todayActiveCaloriesSource);
|
|
949
1359
|
return {
|
|
950
1360
|
generatedAt,
|
|
951
1361
|
userId,
|
|
@@ -954,14 +1364,13 @@ export function getWeightLossViewData(userIds) {
|
|
|
954
1364
|
loggedMealCount: logs.length,
|
|
955
1365
|
trackedDays,
|
|
956
1366
|
todayCalories: todayLedger.totals.calories,
|
|
957
|
-
targetCalories:
|
|
1367
|
+
targetCalories: todayLedger.targetCalories,
|
|
958
1368
|
todayCalorieDelta: todayLedger.calorieDelta,
|
|
959
1369
|
averageCalories,
|
|
960
1370
|
inferredTdee,
|
|
961
1371
|
proteinCoverage: todayLedger.proteinCoverage,
|
|
962
1372
|
fiberCoverage: todayLedger.fiberCoverage,
|
|
963
|
-
unconfirmedCount: logs.filter((log) => log.confirmationState !== "confirmed")
|
|
964
|
-
.length,
|
|
1373
|
+
unconfirmedCount: logs.filter((log) => log.confirmationState !== "confirmed").length,
|
|
965
1374
|
hypothesisCount: storedHypotheses.length + generatedHypotheses.length,
|
|
966
1375
|
dataQualityScore: round(Math.min(1, trackedDays / 7 +
|
|
967
1376
|
Math.min(0.2, body.length * 0.04) +
|
|
@@ -970,19 +1379,8 @@ export function getWeightLossViewData(userIds) {
|
|
|
970
1379
|
},
|
|
971
1380
|
todayLedger,
|
|
972
1381
|
recentMeals: logs.slice(0, 30),
|
|
973
|
-
energyModel
|
|
974
|
-
|
|
975
|
-
restingEnergyCalories: null,
|
|
976
|
-
wearableConfidence: "directional",
|
|
977
|
-
inferredTdee,
|
|
978
|
-
estimatedTdeeKcal: inferredTdee,
|
|
979
|
-
activeBurnKcal: null,
|
|
980
|
-
movementCaloriesKcal: null,
|
|
981
|
-
averageCalorieIntake: averageCalories,
|
|
982
|
-
currentDeficitEstimate: inferredTdee != null ? round(averageCalories - inferredTdee, 0) : null,
|
|
983
|
-
estimatedDailyEnergyBalanceKcal: inferredTdee != null ? round(averageCalories - inferredTdee, 0) : null
|
|
984
|
-
},
|
|
985
|
-
weightTrend: buildWeightTrend(body),
|
|
1382
|
+
energyModel,
|
|
1383
|
+
weightTrend,
|
|
986
1384
|
bodyCheckins: body,
|
|
987
1385
|
appearanceCheckins: appearance,
|
|
988
1386
|
foodQuality: buildFoodQuality(logs),
|
|
@@ -1069,6 +1467,51 @@ function readOffNutrient(product, key) {
|
|
|
1069
1467
|
const value = nutriments?.[key];
|
|
1070
1468
|
return typeof value === "number" ? value : null;
|
|
1071
1469
|
}
|
|
1470
|
+
function parseGramQuantity(value) {
|
|
1471
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
1472
|
+
return value;
|
|
1473
|
+
}
|
|
1474
|
+
if (typeof value !== "string") {
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
const match = value
|
|
1478
|
+
.toLowerCase()
|
|
1479
|
+
.replace(",", ".")
|
|
1480
|
+
.match(/(\d+(?:\.\d+)?)\s*(kg|g|gram|grams|ml|l)\b/);
|
|
1481
|
+
if (!match) {
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
const quantity = Number(match[1]);
|
|
1485
|
+
if (!Number.isFinite(quantity) || quantity <= 0) {
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
const unit = match[2];
|
|
1489
|
+
return unit === "kg" || unit === "l" ? quantity * 1000 : quantity;
|
|
1490
|
+
}
|
|
1491
|
+
function openFoodFactsServingGrams(product) {
|
|
1492
|
+
const servingSizeGrams = parseGramQuantity(product.serving_size);
|
|
1493
|
+
if (servingSizeGrams != null) {
|
|
1494
|
+
return servingSizeGrams;
|
|
1495
|
+
}
|
|
1496
|
+
const servingQuantity = parseGramQuantity(product.serving_quantity);
|
|
1497
|
+
if (servingQuantity != null) {
|
|
1498
|
+
return servingQuantity;
|
|
1499
|
+
}
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
function openFoodFactsServingNutrient(product, per100gKey, perServingKey, servingGrams) {
|
|
1503
|
+
const perServing = readOffNutrient(product, perServingKey);
|
|
1504
|
+
if (perServing != null) {
|
|
1505
|
+
return perServing;
|
|
1506
|
+
}
|
|
1507
|
+
const per100g = readOffNutrient(product, per100gKey);
|
|
1508
|
+
if (per100g == null) {
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
return servingGrams != null
|
|
1512
|
+
? round((per100g * servingGrams) / 100, 1)
|
|
1513
|
+
: per100g;
|
|
1514
|
+
}
|
|
1072
1515
|
function mapOpenFoodFactsProduct(product) {
|
|
1073
1516
|
const code = typeof product.code === "string" ? product.code : "";
|
|
1074
1517
|
const name = typeof product.product_name === "string" && product.product_name.trim()
|
|
@@ -1084,22 +1527,30 @@ function mapOpenFoodFactsProduct(product) {
|
|
|
1084
1527
|
nova === 4 ? "ultra_processed" : null,
|
|
1085
1528
|
nova ? `nova_${nova}` : null
|
|
1086
1529
|
].filter((tag) => Boolean(tag));
|
|
1530
|
+
const servingGrams = openFoodFactsServingGrams(product);
|
|
1531
|
+
const servingLabel = typeof product.serving_size === "string" && product.serving_size.trim()
|
|
1532
|
+
? product.serving_size.trim()
|
|
1533
|
+
: servingGrams != null
|
|
1534
|
+
? `${servingGrams} g`
|
|
1535
|
+
: "100 g";
|
|
1536
|
+
const sodiumServingGrams = openFoodFactsServingNutrient(product, "sodium_100g", "sodium_serving", servingGrams);
|
|
1087
1537
|
return cacheFood({
|
|
1088
1538
|
source: "open_food_facts",
|
|
1089
1539
|
sourceId: code,
|
|
1090
1540
|
barcode: code,
|
|
1091
1541
|
name,
|
|
1092
|
-
brand: typeof product.brands === "string"
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1542
|
+
brand: typeof product.brands === "string"
|
|
1543
|
+
? product.brands.split(",")[0].trim()
|
|
1544
|
+
: "",
|
|
1545
|
+
servingLabel,
|
|
1546
|
+
servingGrams: servingGrams ?? 100,
|
|
1547
|
+
calories: openFoodFactsServingNutrient(product, "energy-kcal_100g", "energy-kcal_serving", servingGrams),
|
|
1548
|
+
proteinGrams: openFoodFactsServingNutrient(product, "proteins_100g", "proteins_serving", servingGrams),
|
|
1549
|
+
carbohydrateGrams: openFoodFactsServingNutrient(product, "carbohydrates_100g", "carbohydrates_serving", servingGrams),
|
|
1550
|
+
fatGrams: openFoodFactsServingNutrient(product, "fat_100g", "fat_serving", servingGrams),
|
|
1551
|
+
fiberGrams: openFoodFactsServingNutrient(product, "fiber_100g", "fiber_serving", servingGrams),
|
|
1552
|
+
sugarGrams: openFoodFactsServingNutrient(product, "sugars_100g", "sugars_serving", servingGrams),
|
|
1553
|
+
sodiumMg: sodiumServingGrams != null ? round(sodiumServingGrams * 1000, 0) : null,
|
|
1103
1554
|
novaGroup: nova,
|
|
1104
1555
|
nutriScore: typeof product.nutriscore_grade === "string"
|
|
1105
1556
|
? product.nutriscore_grade
|
|
@@ -1133,11 +1584,15 @@ async function lookupOpenFoodFactsBarcode(barcode) {
|
|
|
1133
1584
|
return [];
|
|
1134
1585
|
}
|
|
1135
1586
|
const payload = (await response.json());
|
|
1136
|
-
const food = payload.product
|
|
1587
|
+
const food = payload.product
|
|
1588
|
+
? mapOpenFoodFactsProduct(payload.product)
|
|
1589
|
+
: null;
|
|
1137
1590
|
return food ? [food] : [];
|
|
1138
1591
|
}
|
|
1139
1592
|
function mapFdcFood(food) {
|
|
1140
|
-
const sourceId = typeof food.fdcId === "number"
|
|
1593
|
+
const sourceId = typeof food.fdcId === "number"
|
|
1594
|
+
? String(food.fdcId)
|
|
1595
|
+
: String(food.fdcId ?? "");
|
|
1141
1596
|
const name = typeof food.description === "string" ? food.description.trim() : "";
|
|
1142
1597
|
if (!sourceId || !name) {
|
|
1143
1598
|
return null;
|
|
@@ -1239,25 +1694,13 @@ function extractJsonObject(text) {
|
|
|
1239
1694
|
}
|
|
1240
1695
|
return trimmed;
|
|
1241
1696
|
}
|
|
1242
|
-
function
|
|
1243
|
-
const settings = getSettings();
|
|
1244
|
-
const selectedConnectionId = connectionId?.trim() ||
|
|
1245
|
-
settings.modelSettings.forgeAgent.basicChat.connectionId ||
|
|
1246
|
-
settings.modelSettings.forgeAgent.wiki.connectionId ||
|
|
1247
|
-
"";
|
|
1248
|
-
if (!selectedConnectionId) {
|
|
1249
|
-
return null;
|
|
1250
|
-
}
|
|
1251
|
-
const row = getDatabase()
|
|
1252
|
-
.prepare(`SELECT provider, auth_mode, base_url, model, secret_id, enabled
|
|
1253
|
-
FROM ai_model_connections
|
|
1254
|
-
WHERE id = ?`)
|
|
1255
|
-
.get(selectedConnectionId);
|
|
1697
|
+
function mapNutritionCodexProfileRow(row) {
|
|
1256
1698
|
if (!row ||
|
|
1257
1699
|
row.enabled !== 1 ||
|
|
1258
1700
|
row.provider !== "openai-codex" ||
|
|
1259
1701
|
row.auth_mode !== "oauth" ||
|
|
1260
|
-
!row.secret_id
|
|
1702
|
+
!row.secret_id ||
|
|
1703
|
+
!readEncryptedSecret(row.secret_id)) {
|
|
1261
1704
|
return null;
|
|
1262
1705
|
}
|
|
1263
1706
|
return {
|
|
@@ -1269,6 +1712,40 @@ function getNutritionCodexProfile(connectionId) {
|
|
|
1269
1712
|
metadata: {}
|
|
1270
1713
|
};
|
|
1271
1714
|
}
|
|
1715
|
+
function getNutritionCodexProfile(connectionId) {
|
|
1716
|
+
const settings = getSettings();
|
|
1717
|
+
const selectedConnectionIds = [
|
|
1718
|
+
connectionId?.trim(),
|
|
1719
|
+
settings.modelSettings.forgeAgent.basicChat.connectionId,
|
|
1720
|
+
settings.modelSettings.forgeAgent.wiki.connectionId
|
|
1721
|
+
].filter((id) => Boolean(id));
|
|
1722
|
+
for (const selectedConnectionId of new Set(selectedConnectionIds)) {
|
|
1723
|
+
const row = getDatabase()
|
|
1724
|
+
.prepare(`SELECT provider, auth_mode, base_url, model, secret_id, enabled
|
|
1725
|
+
FROM ai_model_connections
|
|
1726
|
+
WHERE id = ?`)
|
|
1727
|
+
.get(selectedConnectionId);
|
|
1728
|
+
const profile = mapNutritionCodexProfileRow(row);
|
|
1729
|
+
if (profile) {
|
|
1730
|
+
return profile;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
const fallbackRows = getDatabase()
|
|
1734
|
+
.prepare(`SELECT provider, auth_mode, base_url, model, secret_id, enabled
|
|
1735
|
+
FROM ai_model_connections
|
|
1736
|
+
WHERE provider = 'openai-codex'
|
|
1737
|
+
AND auth_mode = 'oauth'
|
|
1738
|
+
AND enabled = 1
|
|
1739
|
+
ORDER BY updated_at DESC`)
|
|
1740
|
+
.all();
|
|
1741
|
+
for (const row of fallbackRows) {
|
|
1742
|
+
const profile = mapNutritionCodexProfileRow(row);
|
|
1743
|
+
if (profile) {
|
|
1744
|
+
return profile;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1272
1749
|
const parsedMealItemSchema = z.object({
|
|
1273
1750
|
name: z.string().trim().min(1),
|
|
1274
1751
|
quantity: z.coerce.number().positive().default(1),
|