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.
Files changed (227) hide show
  1. package/dist/assets/activity-page-Dv6X5ZCV.js +1 -0
  2. package/dist/assets/ai-surface-workspace-CutiG6uS.js +1 -0
  3. package/dist/assets/atlas-panel-jgMRyaHn.js +1 -0
  4. package/dist/assets/{board-BkDRaMp6.js → board-dIX6etHh.js} +1 -1
  5. package/dist/assets/calendar-page-q7Nm5E2U.js +1 -0
  6. package/dist/assets/calendar-rules-DPFsfiRl.js +1 -0
  7. package/dist/assets/calendar-week-toolbar-nVhgB-0s.js +1 -0
  8. package/dist/assets/{charts-P7EVhIog.js → charts-DFnEuIMB.js} +8 -8
  9. package/dist/assets/companion-sync-lab-page-CJ8UTij8.js +1 -0
  10. package/dist/assets/daily-metrics-dashboard-CiJkkkd1.js +1 -0
  11. package/dist/assets/entity-note-count-link-RzBB6ujx.js +1 -0
  12. package/dist/assets/entity-notes-surface-Djz50HvD.js +1 -0
  13. package/dist/assets/execution-board-CtPhI-58.js +1 -0
  14. package/dist/assets/faceted-token-search-DEC6ANa9.js +1 -0
  15. package/dist/assets/flagship-signal-deck-CtHC3mql.js +1 -0
  16. package/dist/assets/floating-action-menu-CKQRhff9.js +1 -0
  17. package/dist/assets/{forms-BFlTgZ3W.js → forms-hB0SqEh-.js} +1 -1
  18. package/dist/assets/goal-detail-page-CF5fNQeR.js +1 -0
  19. package/dist/assets/goals-page-fsY8NzGB.js +1 -0
  20. package/dist/assets/{graph-D6JLqDbD.js → graph-DDUZNRsO.js} +14 -14
  21. package/dist/assets/habits-page-COoGz4kj.js +1 -0
  22. package/dist/assets/index-BAXYM89v.css +1 -0
  23. package/dist/assets/index-EqQsXoat.js +19 -0
  24. package/dist/assets/insight-flow-dialog-DtX_5W7g.js +1 -0
  25. package/dist/assets/insights-page-cBd74ObU.js +8 -0
  26. package/dist/assets/kanban-page-CYav9THw.js +1 -0
  27. package/dist/assets/knowledge-graph-page-D2A-W1jE.js +1 -0
  28. package/dist/assets/{life-force-page-Dy0JTS2G.js → life-force-page-CACGlNhq.js} +1 -1
  29. package/dist/assets/life-force-workspace-BJ4N1I78.js +1 -0
  30. package/dist/assets/{maps-ClgJoCjz.js → maps-C-yOWiDN.js} +1 -1
  31. package/dist/assets/metric-tile-CMadwnGz.js +1 -0
  32. package/dist/assets/{motion-BeD44FeG.js → motion-Lt5B1XuE.js} +1 -1
  33. package/dist/assets/movement-page-D5VqFd2q.js +1 -0
  34. package/dist/assets/note-markdown-BzK2Qlgr.js +3 -0
  35. package/dist/assets/note-tags-input-CZzqJMLc.js +1 -0
  36. package/dist/assets/notes-page-D3Hsh90C.js +1 -0
  37. package/dist/assets/{open-in-graph-button-IXe9SGth.js → open-in-graph-button-BFPKfyK3.js} +1 -1
  38. package/dist/assets/orbit-map-CnyfSmOG.js +1 -0
  39. package/dist/assets/overview-page-CXdWrOV1.js +1 -0
  40. package/dist/assets/page-hero-ffKzgyW3.js +1 -0
  41. package/dist/assets/pill-cluster-C2D0h3lx.js +1 -0
  42. package/dist/assets/{preference-entity-handoff-button-5PzUn42S.js → preference-entity-handoff-button-Dj3V6VxL.js} +1 -1
  43. package/dist/assets/preferences-page-Jo8Gw386.js +1 -0
  44. package/dist/assets/project-collections-qSqp90HN.js +1 -0
  45. package/dist/assets/{project-detail-page-BXK5-4xW.js → project-detail-page-BifhiLQX.js} +1 -1
  46. package/dist/assets/project-management-hierarchy-page-DZ_9klIc.js +1 -0
  47. package/dist/assets/project-management-section-nav-BImLCVvf.js +1 -0
  48. package/dist/assets/projects-page-ptcx6H38.js +1 -0
  49. package/dist/assets/psyche-behaviors-page-BDwXyDta.js +5 -0
  50. package/dist/assets/psyche-flashcards-page-DL1BP1jX.js +1 -0
  51. package/dist/assets/psyche-goal-map-page-ovcpisx1.js +1 -0
  52. package/dist/assets/psyche-graph-EP5GL612.js +1 -0
  53. package/dist/assets/{psyche-metrics-page-CuR9oqEy.js → psyche-metrics-page-Bcu813Rg.js} +1 -1
  54. package/dist/assets/psyche-mode-guide-page-C0g27Xpt.js +1 -0
  55. package/dist/assets/psyche-modes-page-BjKjX5MR.js +1 -0
  56. package/dist/assets/psyche-page-F4tF2W70.js +1 -0
  57. package/dist/assets/psyche-patterns-page-B-14hukK.js +5 -0
  58. package/dist/assets/psyche-questionnaire-builder-page-BdXmoHvK.js +1 -0
  59. package/dist/assets/psyche-questionnaire-detail-page-Cu5uwlJu.js +1 -0
  60. package/dist/assets/psyche-questionnaire-run-detail-page-C3R4PClg.js +1 -0
  61. package/dist/assets/psyche-questionnaire-run-page-Div3iDdt.js +1 -0
  62. package/dist/assets/psyche-questionnaires-page-DpqAPQCp.js +1 -0
  63. package/dist/assets/psyche-report-detail-page-BvWVDKP3.js +3 -0
  64. package/dist/assets/psyche-reports-page-BrbWUlAq.js +1 -0
  65. package/dist/assets/{psyche-schemas-HFmg37Wj.js → psyche-schemas-Dxj554nU.js} +1 -1
  66. package/dist/assets/psyche-schemas-beliefs-page-DYKvAtSD.js +9 -0
  67. package/dist/assets/psyche-screen-time-page-CAOKCyQw.js +1 -0
  68. package/dist/assets/psyche-self-observation-page-F0MVA0UH.js +1 -0
  69. package/dist/assets/psyche-values-page-DyvX-d0o.js +5 -0
  70. package/dist/assets/report-chain-fields-CeC1cJFS.js +1 -0
  71. package/dist/assets/rewards-page-C2loyODo.js +1 -0
  72. package/dist/assets/scheduling-rules-editor-D40AC2jR.js +1 -0
  73. package/dist/assets/schema-badge-FY7818qB.js +1 -0
  74. package/dist/assets/schema-visuals-CvC9a3i6.js +1 -0
  75. package/dist/assets/select-menu-DJsCG6rM.js +1 -0
  76. package/dist/assets/settings-agents-page-BFkJ5AAD.js +6 -0
  77. package/dist/assets/settings-bin-page-D3-Ab-iA.js +1 -0
  78. package/dist/assets/settings-calendar-page-ly_mSTAD.js +5 -0
  79. package/dist/assets/settings-data-page-D7QOqNf-.js +1 -0
  80. package/dist/assets/settings-logs-page-Ca87HEUx.js +1 -0
  81. package/dist/assets/settings-mobile-page-D0jejZot.js +1 -0
  82. package/dist/assets/settings-models-page-DPDsZjSc.js +1 -0
  83. package/dist/assets/settings-page-1cArlSPM.js +1 -0
  84. package/dist/assets/settings-rewards-page-Db4BV1DC.js +1 -0
  85. package/dist/assets/{settings-section-nav-DSOuht_F.js → settings-section-nav-DP9o4peU.js} +1 -1
  86. package/dist/assets/settings-users-page-BHFyDSsd.js +1 -0
  87. package/dist/assets/settings-wiki-page-BNaiupBm.js +1 -0
  88. package/dist/assets/sleep-page-BmOPF0yD.js +1 -0
  89. package/dist/assets/sports-page-BldIiclr.js +1 -0
  90. package/dist/assets/{state-B-4sS1xO.js → state-vCcAT5Hq.js} +1 -1
  91. package/dist/assets/strategies-page-CaTN99qj.js +1 -0
  92. package/dist/assets/strategy-detail-page-DsgFTd-U.js +1 -0
  93. package/dist/assets/strategy-dialog-7X7peRzu.js +1 -0
  94. package/dist/assets/surface-CJI17F3n.js +1 -0
  95. package/dist/assets/{table-WfAPUppN.js → table-BNqMG3_S.js} +1 -1
  96. package/dist/assets/task-detail-page-M1sIIPA8.js +1 -0
  97. package/dist/assets/timebox-planning-dialog-ByojN0AN.js +1 -0
  98. package/dist/assets/today-page-BjDijtn8.js +1 -0
  99. package/dist/assets/training-load-page-BPRmaWmF.js +1 -0
  100. package/dist/assets/{ui-C13Nbgas.js → ui-C1iwpj2-.js} +4 -4
  101. package/dist/assets/use-psyche-focus-target-Ct-acS9G.js +1 -0
  102. package/dist/assets/vendor-Dnkkx2co.js +1067 -0
  103. package/dist/assets/{vitals-page-qre17Nw8.js → vitals-page-CE9zdGLF.js} +1 -1
  104. package/dist/assets/weekly-review-page-CzhTv90n.js +1 -0
  105. package/dist/assets/weight-loss-page-Bp_cIk78.js +5 -0
  106. package/dist/assets/wiki-article-markdown-Dok2uy2p.js +4 -0
  107. package/dist/assets/wiki-editor-page-BuW32Y3f.js +26 -0
  108. package/dist/assets/wiki-ingest-history-page-DZKSBNHV.js +1 -0
  109. package/dist/assets/wiki-ingest-modal-DyqBEZcC.js +1 -0
  110. package/dist/assets/wiki-page-DqxlbLKG.js +1 -0
  111. package/dist/assets/workbench-flow-page-C4nc9jUg.js +5 -0
  112. package/dist/assets/workbench-page-BnyR7SL0.js +1 -0
  113. package/dist/assets/workout-detail-page-DT-c5cHL.js +2 -0
  114. package/dist/index.html +9 -9
  115. package/dist/openclaw/local-runtime.js +41 -14
  116. package/dist/server/server/migrations/067_weight_loss_daily_active_overrides.sql +13 -0
  117. package/dist/server/server/src/app.js +125 -33
  118. package/dist/server/server/src/health-weight-loss.js +554 -77
  119. package/dist/server/server/src/health.js +12 -4
  120. package/dist/server/server/src/movement.js +84 -1
  121. package/dist/server/server/src/openapi.js +123 -18
  122. package/dist/server/server/src/repositories/model-settings.js +21 -12
  123. package/dist/server/server/src/repositories/settings.js +19 -5
  124. package/dist/server/src/components/ui/info-tooltip.js +6 -6
  125. package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +13 -5
  126. package/dist/server/src/lib/api.js +24 -5
  127. package/dist/server/src/lib/theme-system.js +8 -0
  128. package/openclaw.plugin.json +1 -1
  129. package/package.json +3 -3
  130. package/server/migrations/067_weight_loss_daily_active_overrides.sql +13 -0
  131. package/skills/forge-openclaw/SKILL.md +13 -0
  132. package/skills/forge-openclaw/entity_conversation_playbooks.md +7 -1
  133. package/dist/assets/activity-page-CpjuNSHw.js +0 -1
  134. package/dist/assets/ai-surface-workspace-DEAFZruS.js +0 -1
  135. package/dist/assets/atlas-panel-CdVNPotj.js +0 -1
  136. package/dist/assets/calendar-page-DNNt6lfz.js +0 -1
  137. package/dist/assets/calendar-rules-DNJFNsxi.js +0 -1
  138. package/dist/assets/calendar-week-toolbar-BbPwYeN0.js +0 -1
  139. package/dist/assets/companion-sync-lab-page-KxEDigM6.js +0 -1
  140. package/dist/assets/daily-metrics-dashboard-B3cqJgDt.js +0 -1
  141. package/dist/assets/entity-note-count-link-DrhjJZ4i.js +0 -1
  142. package/dist/assets/entity-notes-surface-CkcRsKJQ.js +0 -1
  143. package/dist/assets/execution-board-D07gOocB.js +0 -1
  144. package/dist/assets/faceted-token-search-BxRRcM3q.js +0 -1
  145. package/dist/assets/flagship-signal-deck-cmy82b8_.js +0 -1
  146. package/dist/assets/floating-action-menu-Fs_ZiUMo.js +0 -1
  147. package/dist/assets/goal-detail-page-CqLiNz4f.js +0 -1
  148. package/dist/assets/goals-page-BTk7mg_T.js +0 -1
  149. package/dist/assets/habits-page-BJxagdzx.js +0 -1
  150. package/dist/assets/index-CF4J4R9L.js +0 -19
  151. package/dist/assets/index-CZbuZQjw.css +0 -1
  152. package/dist/assets/insight-flow-dialog-8f3D0GuC.js +0 -1
  153. package/dist/assets/insights-page-D6rOa7uk.js +0 -8
  154. package/dist/assets/kanban-page-XQ7Se6dH.js +0 -1
  155. package/dist/assets/knowledge-graph-page-BtAg8iv3.js +0 -1
  156. package/dist/assets/life-force-workspace-OfyB9HJM.js +0 -1
  157. package/dist/assets/metric-tile-DKpo-8xw.js +0 -1
  158. package/dist/assets/movement-page-Bg_T_Stx.js +0 -1
  159. package/dist/assets/note-markdown-N-uxD3Xt.js +0 -3
  160. package/dist/assets/note-tags-input-Cdu7wiw6.js +0 -1
  161. package/dist/assets/notes-page-CKXnF_KU.js +0 -1
  162. package/dist/assets/orbit-map-Dzi6KliQ.js +0 -1
  163. package/dist/assets/overview-page-1miYqaVS.js +0 -1
  164. package/dist/assets/page-hero-DRy5b2MU.js +0 -1
  165. package/dist/assets/pill-cluster-C9QczVJ2.js +0 -1
  166. package/dist/assets/preferences-page-DtNaF5Q3.js +0 -1
  167. package/dist/assets/project-collections-xPz2mlRr.js +0 -1
  168. package/dist/assets/project-management-hierarchy-page-DtRpMABw.js +0 -1
  169. package/dist/assets/project-management-section-nav-P3ixzPa-.js +0 -1
  170. package/dist/assets/projects-page-C5ViRuf4.js +0 -1
  171. package/dist/assets/psyche-behaviors-page-Dco46sC4.js +0 -5
  172. package/dist/assets/psyche-flashcards-page-ZcoEB8gV.js +0 -1
  173. package/dist/assets/psyche-goal-map-page-CLBAQOI0.js +0 -1
  174. package/dist/assets/psyche-graph-k4tX2tdp.js +0 -1
  175. package/dist/assets/psyche-mode-guide-page-hIVXcCnE.js +0 -1
  176. package/dist/assets/psyche-modes-page-0lYtBlhO.js +0 -1
  177. package/dist/assets/psyche-page-VZ9k9ISp.js +0 -1
  178. package/dist/assets/psyche-patterns-page-gx5nmdGq.js +0 -5
  179. package/dist/assets/psyche-questionnaire-builder-page-Bn0TOISd.js +0 -1
  180. package/dist/assets/psyche-questionnaire-detail-page-CmVzSd_s.js +0 -1
  181. package/dist/assets/psyche-questionnaire-run-detail-page-BsMbmXCG.js +0 -1
  182. package/dist/assets/psyche-questionnaire-run-page-CgkRL2vi.js +0 -1
  183. package/dist/assets/psyche-questionnaires-page-D7V8uLXM.js +0 -1
  184. package/dist/assets/psyche-report-detail-page-OlFq57eL.js +0 -3
  185. package/dist/assets/psyche-reports-page-dVUZjna1.js +0 -1
  186. package/dist/assets/psyche-schemas-beliefs-page-BCgc8FUd.js +0 -9
  187. package/dist/assets/psyche-screen-time-page-B_6BT_WN.js +0 -1
  188. package/dist/assets/psyche-self-observation-page-CEG5mluK.js +0 -1
  189. package/dist/assets/psyche-values-page-DRbRfEd6.js +0 -5
  190. package/dist/assets/report-chain-fields-CALCV3V5.js +0 -1
  191. package/dist/assets/rewards-page-DmC4R_Ps.js +0 -1
  192. package/dist/assets/scheduling-rules-editor-D02s70hr.js +0 -1
  193. package/dist/assets/schema-badge-BZO-qNhO.js +0 -1
  194. package/dist/assets/schema-visuals-D6nxjbYC.js +0 -1
  195. package/dist/assets/select-menu-fYyreSdQ.js +0 -1
  196. package/dist/assets/settings-agents-page-C_v_hMJF.js +0 -6
  197. package/dist/assets/settings-bin-page-DY5bg81n.js +0 -1
  198. package/dist/assets/settings-calendar-page-D1CzE6cg.js +0 -5
  199. package/dist/assets/settings-data-page-CHRQFU9H.js +0 -1
  200. package/dist/assets/settings-logs-page-B04pUwEv.js +0 -1
  201. package/dist/assets/settings-mobile-page-D9kTlYDS.js +0 -1
  202. package/dist/assets/settings-models-page-D26270R2.js +0 -1
  203. package/dist/assets/settings-page-DYDTFlnv.js +0 -1
  204. package/dist/assets/settings-rewards-page-cl4vqqO_.js +0 -1
  205. package/dist/assets/settings-users-page-BU79JB_T.js +0 -1
  206. package/dist/assets/settings-wiki-page-DwAUlyA3.js +0 -1
  207. package/dist/assets/sleep-page-D8NbdhyS.js +0 -1
  208. package/dist/assets/sports-page-CV4Cnzwn.js +0 -1
  209. package/dist/assets/strategies-page-C4qvXnql.js +0 -1
  210. package/dist/assets/strategy-detail-page-DJLo5rfy.js +0 -1
  211. package/dist/assets/strategy-dialog-D3AuUlVz.js +0 -1
  212. package/dist/assets/task-detail-page-z-9u9rF0.js +0 -1
  213. package/dist/assets/timebox-planning-dialog-DB6FLqmI.js +0 -1
  214. package/dist/assets/today-page-BKlu6gx5.js +0 -1
  215. package/dist/assets/training-load-page-CyJQqo_3.js +0 -1
  216. package/dist/assets/use-psyche-focus-target-C1C_XjYG.js +0 -1
  217. package/dist/assets/vendor-DHkYh85p.js +0 -1052
  218. package/dist/assets/weekly-review-page-Cz4vkRcx.js +0 -1
  219. package/dist/assets/weight-loss-page-BQrnOI0y.js +0 -1
  220. package/dist/assets/wiki-article-markdown-DdiR2TJE.js +0 -4
  221. package/dist/assets/wiki-editor-page-DqwoqVFb.js +0 -26
  222. package/dist/assets/wiki-ingest-history-page--evBLbOw.js +0 -1
  223. package/dist/assets/wiki-ingest-modal--ohzFnj2.js +0 -1
  224. package/dist/assets/wiki-page-B_VJFBPA.js +0 -1
  225. package/dist/assets/workbench-flow-page-Du62mtJU.js +0 -5
  226. package/dist/assets/workbench-page-4MKr3iRm.js +0 -1
  227. 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.enum(["planned", "running", "complete", "paused"]).default("planned"),
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.omit({ userId: true }).partial();
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: row.serving_grams,
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: parseJson(row.nutrients_json, {}),
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, dayKey(loggedAt), jsonString(parsed.imageRefs), jsonString(parsed.parserProvenance), jsonString(parsed.links), now, now);
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, dayKey(nextLoggedAt), jsonString(parsed.imageRefs ?? existing.imageRefs), jsonString(parsed.parserProvenance ?? existing.parserProvenance), jsonString(parsed.links ?? existing.links), now, logId);
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 ? parsed.baselineEnd : existing.baseline_end, parsed.interventionStart !== 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 today = new Date().toISOString().slice(0, 10);
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: today,
1106
+ dateKey,
757
1107
  meals: todayLogs,
758
1108
  totals,
759
- targetCalories: target.calorieTarget,
760
- calorieDelta: round(totals.calories - n(target.calorieTarget), 0),
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 buildWeightTrend(body) {
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(((latest.weightKg - first.weightKg) /
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))), 2)
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 ? null : round(Math.max(0, 10 - discomfortAverage), 1),
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 || n(entry.reflux) >= 6 || n(entry.abdominalPain) >= 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 todayLedger = buildTodayLedger(logs, target);
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 averageCalories = trackedDays > 0 ? round(recentTotals.calories / trackedDays, 0) : 0;
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: target.calorieTarget,
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
- activeEnergyCalories: null,
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" ? product.brands.split(",")[0].trim() : "",
1093
- servingLabel: typeof product.serving_size === "string" ? product.serving_size : "",
1094
- calories: readOffNutrient(product, "energy-kcal_100g"),
1095
- proteinGrams: readOffNutrient(product, "proteins_100g"),
1096
- carbohydrateGrams: readOffNutrient(product, "carbohydrates_100g"),
1097
- fatGrams: readOffNutrient(product, "fat_100g"),
1098
- fiberGrams: readOffNutrient(product, "fiber_100g"),
1099
- sugarGrams: readOffNutrient(product, "sugars_100g"),
1100
- sodiumMg: readOffNutrient(product, "sodium_100g") != null
1101
- ? readOffNutrient(product, "sodium_100g") * 1000
1102
- : null,
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 ? mapOpenFoodFactsProduct(payload.product) : null;
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" ? String(food.fdcId) : String(food.fdcId ?? "");
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 getNutritionCodexProfile(connectionId) {
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),