forge-openclaw-plugin 0.3.5 → 0.3.6

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 (155) hide show
  1. package/dist/assets/{action-bar-DBZ38L_6.js → action-bar-CsDQF9d4.js} +1 -1
  2. package/dist/assets/{activity-page-BJxUR9bD.js → activity-page-CACf1Sd5.js} +1 -1
  3. package/dist/assets/{ai-surface-workspace-ehzBjTsW.js → ai-surface-workspace-Vn5fqCAT.js} +1 -1
  4. package/dist/assets/{atlas-panel-BFUeVT69.js → atlas-panel-GdK1oyxo.js} +1 -1
  5. package/dist/assets/{board-CLOHbg6t.js → board-CuxQRKPJ.js} +1 -1
  6. package/dist/assets/{calendar-page-Bg8W-YQ-.js → calendar-page-CcVGbvfY.js} +1 -1
  7. package/dist/assets/{calendar-rules-9XQLoI96.js → calendar-rules-Dlq0KT93.js} +1 -1
  8. package/dist/assets/{calendar-week-toolbar-BeYnx1pE.js → calendar-week-toolbar-BVFb1_2G.js} +1 -1
  9. package/dist/assets/{charts-DwTguE_x.js → charts-BzT4pUPg.js} +1 -1
  10. package/dist/assets/{companion-sync-lab-page-BRKYkIrs.js → companion-sync-lab-page-Dgvtdc4i.js} +1 -1
  11. package/dist/assets/{daily-metrics-dashboard-Jt0nAHU5.js → daily-metrics-dashboard-ChQVbNJI.js} +1 -1
  12. package/dist/assets/date-keys-BnZV4PNO.js +1 -0
  13. package/dist/assets/{define-workbench-box-5h-dyvJY.js → define-workbench-box-DWAqqjvH.js} +1 -1
  14. package/dist/assets/{entity-link-multiselect-Bb4De_e3.js → entity-link-multiselect-Do5DTju7.js} +1 -1
  15. package/dist/assets/{entity-note-count-link-BzWnsOmI.js → entity-note-count-link-CV3eOv66.js} +1 -1
  16. package/dist/assets/{entity-notes-surface-CagOwdng.js → entity-notes-surface-CiUgAVl7.js} +1 -1
  17. package/dist/assets/{execution-board-DTmAtmqZ.js → execution-board-ClaXVQ0H.js} +1 -1
  18. package/dist/assets/{faceted-token-search-DJPRm9AY.js → faceted-token-search-DoSsb7qT.js} +1 -1
  19. package/dist/assets/{flagship-signal-deck-CJbGZF7I.js → flagship-signal-deck-B2s7TrIN.js} +1 -1
  20. package/dist/assets/{floating-action-menu-K77jh8XZ.js → floating-action-menu-DGkHCmvI.js} +1 -1
  21. package/dist/assets/{forms-C5d5hTf2.js → forms-D1qJ3oOP.js} +1 -1
  22. package/dist/assets/{generic-node-view-p5ePSFuG.js → generic-node-view-CgLkbts-.js} +1 -1
  23. package/dist/assets/{goal-detail-page-DGu_khEK.js → goal-detail-page-JRcEYpeA.js} +1 -1
  24. package/dist/assets/{goal-dialog-BWD7cOv9.js → goal-dialog-6R2uYWPQ.js} +1 -1
  25. package/dist/assets/{goals-page-Bgj3sj0f.js → goals-page-CYestXkp.js} +1 -1
  26. package/dist/assets/{graph-Cd5WF3lw.js → graph-BF4IsheG.js} +1 -1
  27. package/dist/assets/{habits-page-Ctut1tuX.js → habits-page-B27lnyKu.js} +1 -1
  28. package/dist/assets/{health-boxes-Ie3horXx.js → health-boxes-CeqSGyZ3.js} +1 -1
  29. package/dist/assets/index-DGYIFHgo.js +2 -0
  30. package/dist/assets/{index-CEgIwgk9.css → index-UzsVTD1W.css} +1 -1
  31. package/dist/assets/{inline-note-fields-DE6WM9uM.js → inline-note-fields-CJ9ukqFp.js} +1 -1
  32. package/dist/assets/{insight-flow-dialog-XL8I74eJ.js → insight-flow-dialog-BgaJRYGX.js} +1 -1
  33. package/dist/assets/{insights-page-Bz1FLbp5.js → insights-page-Dc5bilGE.js} +1 -1
  34. package/dist/assets/{kanban-boxes-BBziel7P.js → kanban-boxes-qIwmQlRq.js} +1 -1
  35. package/dist/assets/{kanban-page-DdQP73I3.js → kanban-page-BGnVLHeN.js} +1 -1
  36. package/dist/assets/{knowledge-graph-page-DdHWj4Dj.js → knowledge-graph-page-B7IsVVO_.js} +1 -1
  37. package/dist/assets/{life-force-page-BRN-93cI.js → life-force-page-B5N4DknK.js} +1 -1
  38. package/dist/assets/{life-force-workspace-BowvP5R4.js → life-force-workspace-Da-dmoRX.js} +1 -1
  39. package/dist/assets/{maps-D0Mm6WPG.js → maps-BTVHALP8.js} +1 -1
  40. package/dist/assets/{metric-tile-B6aJueRo.js → metric-tile-BVuj7mc3.js} +1 -1
  41. package/dist/assets/{motion-DwjmC9aq.js → motion-DcgUnXhY.js} +1 -1
  42. package/dist/assets/{movement-boxes-5cjWjIdR.js → movement-boxes-BZrdIh8b.js} +1 -1
  43. package/dist/assets/{movement-page-Dy53RWtB.js → movement-page-CY2XLgp_.js} +1 -1
  44. package/dist/assets/{note-markdown-CvEQCfoi.js → note-markdown-BZHBOkEd.js} +1 -1
  45. package/dist/assets/{note-tags-input-Cg5zBXos.js → note-tags-input-DucocvNH.js} +1 -1
  46. package/dist/assets/{notes-boxes-CgZyf7mV.js → notes-boxes-DAQ6KBkJ.js} +1 -1
  47. package/dist/assets/{notes-page-Cbdcv0Ej.js → notes-page-CSCfdDJl.js} +1 -1
  48. package/dist/assets/{open-in-graph-button-D31ZOEhA.js → open-in-graph-button-ObO3m8yd.js} +1 -1
  49. package/dist/assets/{orbit-map-8eNDWek8.js → orbit-map-Bteg-ola.js} +1 -1
  50. package/dist/assets/{overview-page-CZ6q52Qj.js → overview-page-ckfN_sLZ.js} +1 -1
  51. package/dist/assets/{page-hero-C0MpI3MM.js → page-hero-BkrRTu-t.js} +1 -1
  52. package/dist/assets/pill-cluster-COzv8VgR.js +1 -0
  53. package/dist/assets/{preference-entity-handoff-button-CJhr5AXl.js → preference-entity-handoff-button-BpVAeFmY.js} +1 -1
  54. package/dist/assets/{preferences-page-8vszDNFX.js → preferences-page-DseOh9AP.js} +1 -1
  55. package/dist/assets/{project-collections-sC7eAAhS.js → project-collections-C7yU5PSi.js} +1 -1
  56. package/dist/assets/{project-detail-page-BKdnMjJy.js → project-detail-page-Crt46y_7.js} +1 -1
  57. package/dist/assets/{project-dialog-BiHZpOo1.js → project-dialog-YYxtlqg8.js} +1 -1
  58. package/dist/assets/{project-management-hierarchy-page-VW-hykAI.js → project-management-hierarchy-page-DnojKHzy.js} +1 -1
  59. package/dist/assets/{project-management-section-nav-BEZ5zihs.js → project-management-section-nav-Ctjd348W.js} +1 -1
  60. package/dist/assets/{projects-boxes-Ca6rpYeE.js → projects-boxes-D8lLFGBC.js} +1 -1
  61. package/dist/assets/{projects-page-D86pjpTf.js → projects-page-CV_2em8d.js} +1 -1
  62. package/dist/assets/{psyche-behaviors-page-YLJB6CRU.js → psyche-behaviors-page-CYCNk0rz.js} +1 -1
  63. package/dist/assets/{psyche-flashcards-page-D3gdHLUw.js → psyche-flashcards-page-DwmjBixN.js} +1 -1
  64. package/dist/assets/{psyche-goal-map-page-DJiqSiCx.js → psyche-goal-map-page-CdMhyBKh.js} +1 -1
  65. package/dist/assets/{psyche-graph-Tke0qFdt.js → psyche-graph-BcrZcJrq.js} +1 -1
  66. package/dist/assets/{psyche-metrics-page-DoLnvmC2.js → psyche-metrics-page-D5bX1mNX.js} +1 -1
  67. package/dist/assets/{psyche-mode-guide-page-_Zbvg_HL.js → psyche-mode-guide-page-BmX9EOOB.js} +1 -1
  68. package/dist/assets/{psyche-modes-page-eAkaAzJc.js → psyche-modes-page-CTHFDyLE.js} +1 -1
  69. package/dist/assets/{psyche-page-CwmuBVjA.js → psyche-page-DRR3Fjv-.js} +1 -1
  70. package/dist/assets/{psyche-patterns-page-D7B4Ykjq.js → psyche-patterns-page-BAS38xYf.js} +1 -1
  71. package/dist/assets/{psyche-questionnaire-builder-page-CHJlvCzX.js → psyche-questionnaire-builder-page-DpSi-zg3.js} +1 -1
  72. package/dist/assets/{psyche-questionnaire-detail-page-Bg3ll-D4.js → psyche-questionnaire-detail-page-BcXIiV4E.js} +1 -1
  73. package/dist/assets/{psyche-questionnaire-run-detail-page-CIEerczF.js → psyche-questionnaire-run-detail-page-DvccwzWO.js} +1 -1
  74. package/dist/assets/{psyche-questionnaire-run-page-DyxIk6Qx.js → psyche-questionnaire-run-page-vcyon8IZ.js} +1 -1
  75. package/dist/assets/{psyche-questionnaires-page-ZlqUxuIl.js → psyche-questionnaires-page-B0DDTner.js} +1 -1
  76. package/dist/assets/{psyche-report-detail-page-B2jgYnQ5.js → psyche-report-detail-page-BgBA3xIO.js} +1 -1
  77. package/dist/assets/{psyche-reports-page-DlXr52yr.js → psyche-reports-page-Ce1vEEqW.js} +1 -1
  78. package/dist/assets/{psyche-schemas-Dtskzvv1.js → psyche-schemas-DDol0j-g.js} +1 -1
  79. package/dist/assets/{psyche-schemas-beliefs-page-BhrTZN9B.js → psyche-schemas-beliefs-page-D-IHHohY.js} +1 -1
  80. package/dist/assets/{psyche-screen-time-page-WNGcdusx.js → psyche-screen-time-page-DOKW3m8B.js} +1 -1
  81. package/dist/assets/{psyche-self-observation-page-XsCOglo4.js → psyche-self-observation-page-j7Sk6Ns1.js} +1 -1
  82. package/dist/assets/{psyche-values-page-NvHI6zWK.js → psyche-values-page-NfzdWUyb.js} +1 -1
  83. package/dist/assets/{question-flow-dialog-Bj4PxpjS.js → question-flow-dialog-CLHVmFON.js} +1 -1
  84. package/dist/assets/{report-chain-fields-mWFikZzT.js → report-chain-fields-C9MuyBha.js} +1 -1
  85. package/dist/assets/{rewards-page-DyjwXsQN.js → rewards-page-CVxOBP6m.js} +1 -1
  86. package/dist/assets/{scheduling-rules-editor-C6FEYOxd.js → scheduling-rules-editor-BmbOHH_R.js} +1 -1
  87. package/dist/assets/{schema-badge-CCe4zkSN.js → schema-badge-BAiS_OAp.js} +1 -1
  88. package/dist/assets/{schemas-Cjwn6ooR.js → schemas-B0AXfuOr.js} +1 -1
  89. package/dist/assets/{select-menu-DeJhCsd8.js → select-menu-Bl5MILOj.js} +1 -1
  90. package/dist/assets/{settings-agents-page-DYDxyKu-.js → settings-agents-page-BIRP02mt.js} +1 -1
  91. package/dist/assets/{settings-bin-page-BWLfKVW1.js → settings-bin-page-DxJKeM28.js} +1 -1
  92. package/dist/assets/{settings-calendar-page-Ci-wXhCv.js → settings-calendar-page-aMBtwvO_.js} +1 -1
  93. package/dist/assets/{settings-data-page-DxIw-fWn.js → settings-data-page-nHNergsh.js} +1 -1
  94. package/dist/assets/{settings-logs-page-inpyIdu6.js → settings-logs-page-CFotPoFy.js} +1 -1
  95. package/dist/assets/{settings-mobile-page-BcJGLax8.js → settings-mobile-page-Bf_1D-bN.js} +1 -1
  96. package/dist/assets/{settings-models-page-OWTRhfkj.js → settings-models-page-BbFNmnik.js} +1 -1
  97. package/dist/assets/{settings-page-DXxF3qK2.js → settings-page-CalbNbcg.js} +1 -1
  98. package/dist/assets/{settings-rewards-page-CCXin1n_.js → settings-rewards-page-ByDfCzP5.js} +1 -1
  99. package/dist/assets/{settings-section-nav-CwSDNC3W.js → settings-section-nav-x_JXfzL9.js} +1 -1
  100. package/dist/assets/{settings-users-page-B4NEACwR.js → settings-users-page-N1jl9hWV.js} +1 -1
  101. package/dist/assets/{settings-wiki-page-BNRK1SYc.js → settings-wiki-page-Bn2Qr94L.js} +1 -1
  102. package/dist/assets/{sleep-page-Bara54nB.js → sleep-page-DyU635jb.js} +1 -1
  103. package/dist/assets/{sports-page-BawekFKD.js → sports-page-DbK4yGrR.js} +1 -1
  104. package/dist/assets/{state-BtwEvpO6.js → state-Bpe5dF3T.js} +1 -1
  105. package/dist/assets/{strategies-page-BEl3NGAU.js → strategies-page-AUBlFCZ9.js} +1 -1
  106. package/dist/assets/{strategy-detail-page-DPPI_8Ub.js → strategy-detail-page-DCF8mVaL.js} +1 -1
  107. package/dist/assets/{strategy-dialog-DU6wbBkQ.js → strategy-dialog-50xKlqcz.js} +1 -1
  108. package/dist/assets/{surface-BVYp-Wq9.js → surface-BjT1dIAF.js} +1 -1
  109. package/dist/assets/{table-BuONJH1s.js → table-U7otr5go.js} +1 -1
  110. package/dist/assets/{task-detail-page-BsMVAsbb.js → task-detail-page-Dsll2LCX.js} +1 -1
  111. package/dist/assets/{task-dialog-BGzPc6TW.js → task-dialog-BfPkbIPE.js} +1 -1
  112. package/dist/assets/{timebox-planning-dialog-4_XWuqkw.js → timebox-planning-dialog-BTKUS6Jj.js} +1 -1
  113. package/dist/assets/{today-boxes-5pCvh5zS.js → today-boxes-BPi9bDHM.js} +1 -1
  114. package/dist/assets/{today-page-BGurICpl.js → today-page-BSXSA-Ts.js} +1 -1
  115. package/dist/assets/{training-load-page-BkoYKZ9_.js → training-load-page-BD0s8-Zm.js} +1 -1
  116. package/dist/assets/{ui-B9O-eUim.js → ui-B9TWEtCx.js} +1 -1
  117. package/dist/assets/{use-anchored-overlay-position-BrQ4cqKn.js → use-anchored-overlay-position-BY4kNzPj.js} +1 -1
  118. package/dist/assets/{use-psyche-focus-target-Cuxni3SK.js → use-psyche-focus-target-BhNedCZB.js} +1 -1
  119. package/dist/assets/{user-badge-DfDv87j7.js → user-badge-ap1PvOxd.js} +1 -1
  120. package/dist/assets/{user-select-field-BNqv8-wd.js → user-select-field-fx129Uh6.js} +1 -1
  121. package/dist/assets/{utility-widgets-CbYj32he.js → utility-widgets-Cq508sqJ.js} +1 -1
  122. package/dist/assets/{vendor-Cpmju3nw.js → vendor-BwL6m4SE.js} +216 -211
  123. package/dist/assets/{vitals-page-DlxMk-L7.js → vitals-page-BnXk8OzN.js} +1 -1
  124. package/dist/assets/{weekly-review-page-VrEvAl1T.js → weekly-review-page-CnmSGnEC.js} +1 -1
  125. package/dist/assets/weight-loss-page-Bn6lAXNf.js +5 -0
  126. package/dist/assets/{wiki-article-markdown-DxTkiQRy.js → wiki-article-markdown-BDnkdNDC.js} +1 -1
  127. package/dist/assets/{wiki-editor-page-Beu92YnZ.js → wiki-editor-page-CHEETB0G.js} +1 -1
  128. package/dist/assets/{wiki-ingest-history-page-Cq_soygm.js → wiki-ingest-history-page-CisvtAzm.js} +1 -1
  129. package/dist/assets/{wiki-ingest-modal-D6xs2sEn.js → wiki-ingest-modal-C2faIjzj.js} +1 -1
  130. package/dist/assets/{wiki-page-yF3Flgtg.js → wiki-page-CELe_6qL.js} +1 -1
  131. package/dist/assets/{workbench-flow-page-CZZGg3a8.js → workbench-flow-page-JBVim3L0.js} +1 -1
  132. package/dist/assets/{workbench-page-DKENZ5Nz.js → workbench-page-q7Z3sQtz.js} +1 -1
  133. package/dist/assets/{workout-detail-page-CeN4QiYQ.js → workout-detail-page-9I_3DPYU.js} +2 -2
  134. package/dist/index.html +8 -8
  135. package/dist/server/server/migrations/070_health_mobile_sync_completion_index.sql +16 -0
  136. package/dist/server/server/src/app.js +135 -13
  137. package/dist/server/server/src/health-weight-loss.js +48 -6
  138. package/dist/server/server/src/health.js +301 -93
  139. package/dist/server/server/src/openapi.js +31 -0
  140. package/dist/server/server/src/repositories/gamification.js +25 -0
  141. package/dist/server/server/src/repositories/tasks.js +82 -17
  142. package/dist/server/server/src/services/dashboard.js +5 -4
  143. package/dist/server/server/src/services/gamification.js +47 -19
  144. package/dist/server/server/src/services/life-force.js +99 -0
  145. package/dist/server/src/lib/api.js +4 -0
  146. package/dist/server/src/lib/snapshot-normalizer.js +42 -23
  147. package/openclaw.plugin.json +1 -1
  148. package/package.json +1 -1
  149. package/server/migrations/070_health_mobile_sync_completion_index.sql +16 -0
  150. package/skills/forge-openclaw/SKILL.md +1 -0
  151. package/skills/forge-openclaw/psyche_entity_playbooks.md +8 -0
  152. package/dist/assets/date-keys-Cj1G3TOn.js +0 -1
  153. package/dist/assets/index-BaiwtAgo.js +0 -2
  154. package/dist/assets/pill-cluster-DGwKZQKF.js +0 -1
  155. package/dist/assets/weight-loss-page-DJDnjxKF.js +0 -5
@@ -5354,6 +5354,10 @@ export function buildOpenApiDocument() {
5354
5354
  required: ["items"],
5355
5355
  properties: {
5356
5356
  loggedAt: { type: "string", format: "date-time" },
5357
+ timeZone: {
5358
+ type: "string",
5359
+ description: "IANA timezone used to derive the local dayKey when dayKey is omitted."
5360
+ },
5357
5361
  mealLabel: nullable({ type: "string" }),
5358
5362
  source: {
5359
5363
  type: "string",
@@ -5889,6 +5893,29 @@ export function buildOpenApiDocument() {
5889
5893
  get: {
5890
5894
  tags: ["Health"],
5891
5895
  summary: "Read the Forge nutrition, weight-loss, food-effect, and body insight surface",
5896
+ parameters: [
5897
+ {
5898
+ name: "dateKey",
5899
+ in: "query",
5900
+ schema: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" }
5901
+ },
5902
+ {
5903
+ name: "dayStartAt",
5904
+ in: "query",
5905
+ schema: { type: "string", format: "date-time" }
5906
+ },
5907
+ {
5908
+ name: "dayEndAt",
5909
+ in: "query",
5910
+ schema: { type: "string", format: "date-time" }
5911
+ },
5912
+ {
5913
+ name: "timeZone",
5914
+ in: "query",
5915
+ schema: { type: "string" },
5916
+ description: "IANA timezone used for default local-day calculation when dateKey is omitted."
5917
+ }
5918
+ ],
5892
5919
  responses: {
5893
5920
  "200": jsonResponse({
5894
5921
  type: "object",
@@ -5940,6 +5967,10 @@ export function buildOpenApiDocument() {
5940
5967
  type: "string",
5941
5968
  pattern: "^\\d{4}-\\d{2}-\\d{2}$"
5942
5969
  },
5970
+ timeZone: {
5971
+ type: "string",
5972
+ description: "IANA timezone used to derive the local dayKey when dayKey is omitted."
5973
+ },
5943
5974
  activeCaloriesKcal: {
5944
5975
  type: ["number", "null"],
5945
5976
  minimum: 0
@@ -14,7 +14,32 @@ function mapCelebration(row) {
14
14
  seenAt: row.seen_at
15
15
  });
16
16
  }
17
+ function dailyActivityKey(row) {
18
+ return `${row.dateKey}\0${row.timezone}`;
19
+ }
20
+ function dailyActivityRowsEqual(existingRows, nextRows) {
21
+ if (existingRows.length !== nextRows.length) {
22
+ return false;
23
+ }
24
+ const existingByKey = new Map(existingRows.map((row) => [dailyActivityKey(row), row]));
25
+ for (const next of nextRows) {
26
+ const existing = existingByKey.get(dailyActivityKey(next));
27
+ if (!existing ||
28
+ existing.userId !== next.userId ||
29
+ existing.qualifyingXp !== next.qualifyingXp ||
30
+ existing.eventCount !== next.eventCount ||
31
+ existing.firstRewardEventId !== next.firstRewardEventId ||
32
+ existing.lastRewardEventId !== next.lastRewardEventId) {
33
+ return false;
34
+ }
35
+ }
36
+ return true;
37
+ }
17
38
  export function replaceGamificationDailyActivity(userId, rows) {
39
+ const existingRows = listGamificationDailyActivity(userId);
40
+ if (dailyActivityRowsEqual(existingRows, rows)) {
41
+ return;
42
+ }
18
43
  const database = getDatabase();
19
44
  const deleteRows = database.prepare(`DELETE FROM gamification_daily_activity WHERE user_id = ?`);
20
45
  const insertRow = database.prepare(`INSERT INTO gamification_daily_activity (
@@ -3,8 +3,8 @@ import { getDatabase } from "../db.js";
3
3
  import { runInTransaction } from "../db.js";
4
4
  import { HttpError } from "../errors.js";
5
5
  import { recordActivityEvent } from "./activity-events.js";
6
- import { decorateOwnedEntity, inferFirstOwnedUserId, replaceEntityAssignees, setEntityOwner } from "./entity-ownership.js";
7
- import { filterDeletedEntities, filterDeletedIds, isEntityDeleted } from "./deleted-entities.js";
6
+ import { decorateOwnedEntities, decorateOwnedEntity, inferFirstOwnedUserId, replaceEntityAssignees, setEntityOwner } from "./entity-ownership.js";
7
+ import { filterDeletedEntities, filterDeletedIds, getDeletedEntityIdSet, isEntityDeleted } from "./deleted-entities.js";
8
8
  import { getGoalById } from "./goals.js";
9
9
  import { createLinkedNotes } from "./notes.js";
10
10
  import { ensureDefaultProjectForGoal, getProjectById } from "./projects.js";
@@ -13,7 +13,7 @@ import { awardTaskCompletionReward, reverseLatestTaskCompletionReward } from "./
13
13
  import { findUserByLabel, getDefaultUser, getUserById, resolveUserForMutation } from "./users.js";
14
14
  import { assertTaskRelations } from "../services/relations.js";
15
15
  import { computeWorkTime, emptyTaskTimeSummary } from "../services/work-time.js";
16
- import { buildTaskLifeForceFields, getTaskCompletionRequirement, upsertTaskActionProfile } from "../services/life-force.js";
16
+ import { buildTasksLifeForceFields, buildTaskLifeForceFields, getTaskCompletionRequirement, upsertTaskActionProfile } from "../services/life-force.js";
17
17
  import { createWorkAdjustment } from "./work-adjustments.js";
18
18
  import { calendarSchedulingRulesSchema, createTaskSchema, taskSchema } from "../types.js";
19
19
  function readTaskTagIds(taskId) {
@@ -22,14 +22,32 @@ function readTaskTagIds(taskId) {
22
22
  .all(taskId);
23
23
  return filterDeletedIds("tag", rows.map((row) => row.tag_id));
24
24
  }
25
- function readWorkItemGitRefs(taskId) {
25
+ function readTaskTagIdsIndex(taskIds) {
26
+ const index = new Map(taskIds.map((taskId) => [taskId, []]));
27
+ if (taskIds.length === 0) {
28
+ return index;
29
+ }
30
+ const placeholders = taskIds.map(() => "?").join(", ");
26
31
  const rows = getDatabase()
27
- .prepare(`SELECT id, work_item_id, ref_type, provider, repository, ref_value, url, display_title, created_at, updated_at
28
- FROM work_item_git_refs
29
- WHERE work_item_id = ?
30
- ORDER BY created_at DESC`)
31
- .all(taskId);
32
- return rows.map((row) => ({
32
+ .prepare(`SELECT task_id, tag_id
33
+ FROM task_tags
34
+ WHERE task_id IN (${placeholders})
35
+ ORDER BY task_id, tag_id`)
36
+ .all(...taskIds);
37
+ const deletedTagIds = getDeletedEntityIdSet("tag");
38
+ for (const row of rows) {
39
+ if (deletedTagIds.has(row.tag_id)) {
40
+ continue;
41
+ }
42
+ const tagIds = index.get(row.task_id);
43
+ if (tagIds) {
44
+ tagIds.push(row.tag_id);
45
+ }
46
+ }
47
+ return index;
48
+ }
49
+ function mapWorkItemGitRef(row) {
50
+ return {
33
51
  id: row.id,
34
52
  workItemId: row.work_item_id,
35
53
  refType: row.ref_type === "commit" ||
@@ -44,7 +62,36 @@ function readWorkItemGitRefs(taskId) {
44
62
  displayTitle: row.display_title,
45
63
  createdAt: row.created_at,
46
64
  updatedAt: row.updated_at
47
- }));
65
+ };
66
+ }
67
+ function readWorkItemGitRefs(taskId) {
68
+ const rows = getDatabase()
69
+ .prepare(`SELECT id, work_item_id, ref_type, provider, repository, ref_value, url, display_title, created_at, updated_at
70
+ FROM work_item_git_refs
71
+ WHERE work_item_id = ?
72
+ ORDER BY created_at DESC`)
73
+ .all(taskId);
74
+ return rows.map(mapWorkItemGitRef);
75
+ }
76
+ function readWorkItemGitRefsIndex(taskIds) {
77
+ const index = new Map(taskIds.map((taskId) => [taskId, []]));
78
+ if (taskIds.length === 0) {
79
+ return index;
80
+ }
81
+ const placeholders = taskIds.map(() => "?").join(", ");
82
+ const rows = getDatabase()
83
+ .prepare(`SELECT id, work_item_id, ref_type, provider, repository, ref_value, url, display_title, created_at, updated_at
84
+ FROM work_item_git_refs
85
+ WHERE work_item_id IN (${placeholders})
86
+ ORDER BY work_item_id, created_at DESC`)
87
+ .all(...taskIds);
88
+ for (const row of rows) {
89
+ const refs = index.get(row.work_item_id);
90
+ if (refs) {
91
+ refs.push(mapWorkItemGitRef(row));
92
+ }
93
+ }
94
+ return index;
48
95
  }
49
96
  function replaceWorkItemGitRefs(taskId, refs) {
50
97
  if (refs === undefined) {
@@ -98,8 +145,8 @@ function assertWorkItemHierarchy(options) {
98
145
  throw new HttpError(409, "subtask_parent_invalid", "Subtasks can only live under tasks");
99
146
  }
100
147
  }
101
- function mapTask(row, time = emptyTaskTimeSummary()) {
102
- const task = taskSchema.parse(decorateOwnedEntity("task", {
148
+ function mapTaskBase(row, time, relations) {
149
+ return {
103
150
  id: row.id,
104
151
  title: row.title,
105
152
  description: row.description,
@@ -134,18 +181,24 @@ function mapTask(row, time = emptyTaskTimeSummary()) {
134
181
  completionReport: row.completion_report_json === null
135
182
  ? null
136
183
  : JSON.parse(row.completion_report_json),
137
- gitRefs: readWorkItemGitRefs(row.id),
184
+ gitRefs: relations?.gitRefs ?? readWorkItemGitRefs(row.id),
138
185
  completedAt: row.completed_at,
139
186
  createdAt: row.created_at,
140
187
  updatedAt: row.updated_at,
141
- tagIds: readTaskTagIds(row.id),
188
+ tagIds: relations?.tagIds ?? readTaskTagIds(row.id),
142
189
  time
143
- }));
190
+ };
191
+ }
192
+ function finalizeTask(taskInput) {
193
+ const task = taskSchema.parse(taskInput);
144
194
  return {
145
195
  ...task,
146
196
  ...buildTaskLifeForceFields(task, task.userId ?? undefined)
147
197
  };
148
198
  }
199
+ function mapTask(row, time = emptyTaskTimeSummary()) {
200
+ return finalizeTask(decorateOwnedEntity("task", mapTaskBase(row, time)));
201
+ }
149
202
  function replaceTaskTags(taskId, tagIds) {
150
203
  const database = getDatabase();
151
204
  database.prepare(`DELETE FROM task_tags WHERE task_id = ?`).run(taskId);
@@ -662,8 +715,20 @@ export function listTasks(filters = {}) {
662
715
  created_at
663
716
  ${limitSql}`)
664
717
  .all(...params);
718
+ const taskIds = rows.map((row) => row.id);
719
+ const tagIdsByTaskId = readTaskTagIdsIndex(taskIds);
720
+ const gitRefsByTaskId = readWorkItemGitRefsIndex(taskIds);
665
721
  const workTime = computeWorkTime();
666
- return filterDeletedEntities("task", rows.map((row) => mapTask(row, workTime.taskSummaries.get(row.id) ?? emptyTaskTimeSummary())));
722
+ const tasks = decorateOwnedEntities("task", rows.map((row) => taskSchema.parse(mapTaskBase(row, workTime.taskSummaries.get(row.id) ?? emptyTaskTimeSummary(), {
723
+ tagIds: tagIdsByTaskId.get(row.id) ?? [],
724
+ gitRefs: gitRefsByTaskId.get(row.id) ?? []
725
+ }))));
726
+ const lifeForceFieldsByTaskId = buildTasksLifeForceFields(tasks);
727
+ return filterDeletedEntities("task", tasks.map((task) => ({
728
+ ...task,
729
+ ...(lifeForceFieldsByTaskId.get(task.id) ??
730
+ buildTaskLifeForceFields(task, task.userId ?? undefined))
731
+ })));
667
732
  }
668
733
  export function getTaskById(taskId) {
669
734
  if (isEntityDeleted("task", taskId)) {
@@ -116,9 +116,10 @@ function buildGoalSummary(tasks, goalId) {
116
116
  return { progress, totalTasks, completedTasks, earnedPoints, momentumLabel };
117
117
  }
118
118
  export function getDashboard(options = {}) {
119
- const goals = filterOwnedEntities("goal", listGoals(), options.userIds);
120
- const tasks = filterOwnedEntities("task", listTasks(), options.userIds);
121
- const habits = filterOwnedEntities("habit", listHabits(), options.userIds);
119
+ const goals = options.goals ?? filterOwnedEntities("goal", listGoals(), options.userIds);
120
+ const tasks = options.tasks ?? filterOwnedEntities("task", listTasks(), options.userIds);
121
+ const habits = options.habits ??
122
+ filterOwnedEntities("habit", listHabits(), options.userIds);
122
123
  const tags = listTags();
123
124
  const now = new Date();
124
125
  const weekStart = startOfWeek(now).toISOString();
@@ -149,7 +150,7 @@ export function getDashboard(options = {}) {
149
150
  tags: listTagsByIds(goal.tagIds)
150
151
  };
151
152
  });
152
- const projects = listProjectSummaries({ userIds: options.userIds });
153
+ const projects = options.projects ?? listProjectSummaries({ userIds: options.userIds });
153
154
  const suggestedTags = tags.filter((tag) => ["value", "execution"].includes(tag.kind)).slice(0, 6);
154
155
  const owners = [...new Set(tasks.map((task) => task.owner).filter(Boolean))].sort((left, right) => left.localeCompare(right));
155
156
  const executionBuckets = buildExecutionBuckets(tasks, todayIso, weekEndIso);
@@ -3,7 +3,7 @@ import { enqueueGamificationCelebration, getGamificationEquipment, insertGamific
3
3
  import { getDailyAmbientXp, listRewardRules, recordEntityCreationReward } from "../repositories/rewards.js";
4
4
  import { getDefaultUser, listUsers, listUsersByIds } from "../repositories/users.js";
5
5
  import { GAMIFICATION_CATALOG, GAMIFICATION_STREAK_AWAY_DAY_KEYS, GAMIFICATION_STREAK_POWER_DAY_KEYS } from "../../../src/lib/gamification-catalog.js";
6
- import { achievementSignalSchema, gamificationCatalogPayloadSchema, gamificationProfileSchema, milestoneRewardSchema, rewardLedgerEventSchema } from "../types.js";
6
+ import { achievementSignalSchema, gamificationCatalogPayloadSchema, gamificationProfileSchema, milestoneRewardSchema } from "../types.js";
7
7
  const XP_CURVE_VERSION = "smith-forge";
8
8
  const ENTITY_CREATION_REWARD_SOURCES = [
9
9
  { entityType: "goal", tableName: "goals", titleColumn: "title" },
@@ -102,14 +102,24 @@ function resolveTimezone() {
102
102
  Intl.DateTimeFormat().resolvedOptions().timeZone ||
103
103
  "UTC");
104
104
  }
105
- function dateKeyInTimezone(value, timezone) {
106
- const date = typeof value === "string" ? new Date(value) : value;
107
- const parts = new Intl.DateTimeFormat("en-CA", {
105
+ const dateKeyFormattersByTimezone = new Map();
106
+ function getDateKeyFormatter(timezone) {
107
+ const existing = dateKeyFormattersByTimezone.get(timezone);
108
+ if (existing) {
109
+ return existing;
110
+ }
111
+ const formatter = new Intl.DateTimeFormat("en-CA", {
108
112
  timeZone: timezone,
109
113
  year: "numeric",
110
114
  month: "2-digit",
111
115
  day: "2-digit"
112
- }).formatToParts(date);
116
+ });
117
+ dateKeyFormattersByTimezone.set(timezone, formatter);
118
+ return formatter;
119
+ }
120
+ function dateKeyInTimezone(value, timezone) {
121
+ const date = typeof value === "string" ? new Date(value) : value;
122
+ const parts = getDateKeyFormatter(timezone).formatToParts(date);
113
123
  const year = parts.find((part) => part.type === "year")?.value ?? "1970";
114
124
  const month = parts.find((part) => part.type === "month")?.value ?? "01";
115
125
  const day = parts.find((part) => part.type === "day")?.value ?? "01";
@@ -237,7 +247,7 @@ function loadScopedRewardEvents(scope) {
237
247
  const resolveOwner = buildOwnerResolver(defaultUserId);
238
248
  return rows
239
249
  .map((row) => {
240
- const event = rewardLedgerEventSchema.parse({
250
+ const event = {
241
251
  id: row.id,
242
252
  ruleId: row.rule_id,
243
253
  eventLogId: row.event_log_id,
@@ -252,7 +262,7 @@ function loadScopedRewardEvents(scope) {
252
262
  reversedByRewardId: row.reversed_by_reward_id,
253
263
  metadata: parseMetadata(row.metadata_json),
254
264
  createdAt: row.created_at
255
- });
265
+ };
256
266
  return {
257
267
  ...event,
258
268
  ownerUserId: resolveOwner(event),
@@ -272,15 +282,20 @@ function syncEntityCreationRewards(scope) {
272
282
  const scopeUserIds = [...new Set(scope.userIds)];
273
283
  const scopePlaceholders = scopeUserIds.map(() => "?").join(", ");
274
284
  for (const source of ENTITY_CREATION_REWARD_SOURCES) {
275
- const scopedWhere = scopeUserIds.length > 0
276
- ? `WHERE (
285
+ const scopedPredicate = scopeUserIds.length > 0
286
+ ? `AND (
277
287
  entity_owners.user_id IN (${scopePlaceholders})
278
288
  OR (entity_owners.user_id IS NULL AND ? IS NOT NULL)
279
289
  )`
280
290
  : "";
281
291
  const params = scopeUserIds.length > 0
282
- ? [source.entityType, ...scopeUserIds, scopeUserIds[0] ?? null]
283
- : [source.entityType];
292
+ ? [
293
+ source.entityType,
294
+ source.entityType,
295
+ ...scopeUserIds,
296
+ scopeUserIds[0] ?? null
297
+ ]
298
+ : [source.entityType, source.entityType];
284
299
  const rows = database
285
300
  .prepare(`SELECT
286
301
  ${source.tableName}.id AS id,
@@ -290,7 +305,10 @@ function syncEntityCreationRewards(scope) {
290
305
  LEFT JOIN entity_owners
291
306
  ON entity_owners.entity_type = ?
292
307
  AND entity_owners.entity_id = ${source.tableName}.id
293
- ${scopedWhere}`)
308
+ LEFT JOIN reward_ledger existing_reward
309
+ ON existing_reward.reversible_group = ('entity_created:' || ? || ':' || ${source.tableName}.id)
310
+ WHERE existing_reward.id IS NULL
311
+ ${scopedPredicate}`)
294
312
  .all(...params);
295
313
  for (const row of rows) {
296
314
  recordEntityCreationReward({
@@ -732,17 +750,30 @@ function syncCatalog(input) {
732
750
  ? listGamificationUnlocks(userId).filter((unlock) => catalogItemIds.has(unlock.itemId))
733
751
  : [];
734
752
  const isInitialBackfill = existingUnlocks.length === 0;
753
+ const unlocksByItemId = new Map(existingUnlocks.map((unlock) => [unlock.itemId, unlock]));
754
+ const evaluationsByItemId = new Map(GAMIFICATION_CATALOG.map((item) => [item.id, evaluateCatalogItem(item, input.metricValues)]));
735
755
  for (const item of GAMIFICATION_CATALOG) {
736
- const evaluation = evaluateCatalogItem(item, input.metricValues);
737
- if (userId && evaluation.met) {
756
+ const evaluation = evaluationsByItemId.get(item.id);
757
+ if (userId && evaluation.met && !unlocksByItemId.has(item.id)) {
758
+ const celebrationSeenAt = isInitialBackfill ? nowIso : null;
738
759
  const inserted = insertGamificationUnlock({
739
760
  userId,
740
761
  itemId: item.id,
741
762
  unlockedAt: nowIso,
742
763
  sourceMetric: evaluation.sourceMetric,
743
764
  sourceValue: evaluation.current,
744
- celebrationSeenAt: isInitialBackfill ? nowIso : null
765
+ celebrationSeenAt
745
766
  });
767
+ if (inserted) {
768
+ unlocksByItemId.set(item.id, {
769
+ userId,
770
+ itemId: item.id,
771
+ unlockedAt: nowIso,
772
+ sourceMetric: evaluation.sourceMetric,
773
+ sourceValue: evaluation.current,
774
+ celebrationSeenAt
775
+ });
776
+ }
746
777
  if (inserted && !isInitialBackfill) {
747
778
  enqueueGamificationCelebration({
748
779
  id: `gce_${userId}_${item.id}`,
@@ -794,11 +825,8 @@ function syncCatalog(input) {
794
825
  createdAt: nowIso
795
826
  });
796
827
  }
797
- const unlocksByItemId = new Map((userId ? listGamificationUnlocks(userId) : existingUnlocks)
798
- .filter((unlock) => catalogItemIds.has(unlock.itemId))
799
- .map((unlock) => [unlock.itemId, unlock]));
800
828
  const entries = GAMIFICATION_CATALOG.map((item) => {
801
- const evaluation = evaluateCatalogItem(item, input.metricValues);
829
+ const evaluation = evaluationsByItemId.get(item.id);
802
830
  const unlock = unlocksByItemId.get(item.id);
803
831
  const readOnlyAggregateUnlock = !userId && evaluation.met;
804
832
  return {
@@ -836,6 +836,19 @@ function readEntityActionProfileRow(entityType, entityId) {
836
836
  WHERE entity_type = ? AND entity_id = ?`)
837
837
  .get(entityType, entityId);
838
838
  }
839
+ function readEntityActionProfileRowsIndex(entityType, entityIds) {
840
+ if (entityIds.length === 0) {
841
+ return new Map();
842
+ }
843
+ const placeholders = entityIds.map(() => "?").join(", ");
844
+ const rows = getDatabase()
845
+ .prepare(`SELECT id, entity_type, entity_id, profile_json, created_at, updated_at
846
+ FROM entity_action_profiles
847
+ WHERE entity_type = ?
848
+ AND entity_id IN (${placeholders})`)
849
+ .all(entityType, ...entityIds);
850
+ return new Map(rows.map((row) => [row.entity_id, row]));
851
+ }
839
852
  export function readEntityActionProfile(entityType, entityId, fallback) {
840
853
  const row = readEntityActionProfileRow(entityType, entityId);
841
854
  return row ? mapEntityProfileRow(row, fallback) : null;
@@ -1239,6 +1252,9 @@ function getOrCreateDaySnapshot(userId, date) {
1239
1252
  }
1240
1253
  export function resolveTaskActionProfile(task, lifeForceProfile) {
1241
1254
  const row = readEntityActionProfileRow("task", task.id);
1255
+ return resolveTaskActionProfileFromRow(task, lifeForceProfile, row);
1256
+ }
1257
+ function resolveTaskActionProfileFromRow(task, lifeForceProfile, row) {
1242
1258
  const baseProfile = !row
1243
1259
  ? buildDefaultTaskActionProfile({
1244
1260
  id: `profile_task_${task.id}`,
@@ -1354,6 +1370,33 @@ function readActiveTaskRunProjectionRows(taskId) {
1354
1370
  AND status = 'active'`)
1355
1371
  .all(taskId);
1356
1372
  }
1373
+ function readActiveTaskRunProjectionRowsIndex(taskIds) {
1374
+ const index = new Map(taskIds.map((taskId) => [taskId, []]));
1375
+ if (taskIds.length === 0) {
1376
+ return index;
1377
+ }
1378
+ const placeholders = taskIds.map(() => "?").join(", ");
1379
+ const rows = getDatabase()
1380
+ .prepare(`SELECT
1381
+ id,
1382
+ task_id,
1383
+ timer_mode,
1384
+ planned_duration_seconds,
1385
+ claimed_at,
1386
+ lease_expires_at,
1387
+ status
1388
+ FROM task_runs
1389
+ WHERE task_id IN (${placeholders})
1390
+ AND status = 'active'`)
1391
+ .all(...taskIds);
1392
+ for (const row of rows) {
1393
+ const taskRows = index.get(row.task_id);
1394
+ if (taskRows) {
1395
+ taskRows.push(row);
1396
+ }
1397
+ }
1398
+ return index;
1399
+ }
1357
1400
  function computeProjectedRemainingSeconds(row, now) {
1358
1401
  if (row.timer_mode !== "planned" || row.planned_duration_seconds === null) {
1359
1402
  return 0;
@@ -2601,6 +2644,62 @@ export function buildTaskLifeForceFields(task, userId) {
2601
2644
  })
2602
2645
  };
2603
2646
  }
2647
+ export function buildTasksLifeForceFields(tasks) {
2648
+ const fieldsByTaskId = new Map();
2649
+ if (tasks.length === 0) {
2650
+ return fieldsByTaskId;
2651
+ }
2652
+ const now = new Date();
2653
+ const range = buildDayRange(now);
2654
+ const taskIds = tasks.map((task) => task.id);
2655
+ const defaultUserId = getDefaultUser().id;
2656
+ const profileRowsByTaskId = readEntityActionProfileRowsIndex("task", taskIds);
2657
+ const activeProjectionRowsByTaskId = readActiveTaskRunProjectionRowsIndex(taskIds);
2658
+ const tasksByUserId = new Map();
2659
+ for (const task of tasks) {
2660
+ const effectiveUserId = task.userId ?? defaultUserId;
2661
+ const userTasks = tasksByUserId.get(effectiveUserId) ?? [];
2662
+ userTasks.push(task);
2663
+ tasksByUserId.set(effectiveUserId, userTasks);
2664
+ }
2665
+ for (const [userId, userTasks] of tasksByUserId.entries()) {
2666
+ const taskIdSet = new Set(userTasks.map((task) => task.id));
2667
+ const lifeForceProfile = ensureLifeForceProfile(userId);
2668
+ const todayRunSecondsByTaskId = new Map();
2669
+ for (const row of readTaskRunRows(range, userId)) {
2670
+ if (!taskIdSet.has(row.task_id)) {
2671
+ continue;
2672
+ }
2673
+ todayRunSecondsByTaskId.set(row.task_id, (todayRunSecondsByTaskId.get(row.task_id) ?? 0) +
2674
+ overlapSeconds(range, row, now));
2675
+ }
2676
+ const todayAdjustmentSecondsByTaskId = readTodayAdjustmentSecondsByTaskId(userId, range);
2677
+ for (const task of userTasks) {
2678
+ const profile = resolveTaskActionProfileFromRow(task, lifeForceProfile, profileRowsByTaskId.get(task.id));
2679
+ const todayCreditedSeconds = (todayRunSecondsByTaskId.get(task.id) ?? 0) +
2680
+ (todayAdjustmentSecondsByTaskId.get(task.id) ?? 0);
2681
+ const spentTodayAp = (todayCreditedSeconds / 3600) * profile.sustainRateApPerHour;
2682
+ const spentTotalAp = (task.time.totalCreditedSeconds / 3600) *
2683
+ profile.sustainRateApPerHour;
2684
+ const projectedTotalSeconds = task.time.totalCreditedSeconds +
2685
+ (activeProjectionRowsByTaskId.get(task.id) ?? []).reduce((sum, row) => sum + computeProjectedRemainingSeconds(row, now), 0);
2686
+ fieldsByTaskId.set(task.id, {
2687
+ actionPointSummary: buildTaskActionPointSummary({
2688
+ plannedDurationSeconds: task.plannedDurationSeconds,
2689
+ totalCostAp: profile.totalCostAp,
2690
+ spentTodayAp,
2691
+ spentTotalAp
2692
+ }),
2693
+ splitSuggestion: buildTaskSplitSuggestion({
2694
+ plannedDurationSeconds: task.plannedDurationSeconds,
2695
+ totalTrackedSeconds: task.time.totalCreditedSeconds,
2696
+ projectedTotalSeconds
2697
+ })
2698
+ });
2699
+ }
2700
+ }
2701
+ return fieldsByTaskId;
2702
+ }
2604
2703
  export function getTaskCompletionRequirement(task, userId) {
2605
2704
  const effectiveUserId = userId ?? task.userId ?? getDefaultUser().id;
2606
2705
  const runtime = buildTaskLifeForceRuntime(task, effectiveUserId);
@@ -300,6 +300,7 @@ export function revokeOperatorSession() {
300
300
  }
301
301
  export function getForgeSnapshot(userIds) {
302
302
  const search = new URLSearchParams();
303
+ search.set("profile", "shell");
303
304
  appendUserIds(search, coerceUserIds(userIds));
304
305
  const suffix = search.size > 0 ? `?${search.toString()}` : "";
305
306
  return request(`/api/v1/context${suffix}`).then(normalizeForgeSnapshot);
@@ -1771,6 +1772,9 @@ export function getWeightLossView(userIds, options) {
1771
1772
  if (options?.dayEndAt) {
1772
1773
  search.set("dayEndAt", options.dayEndAt);
1773
1774
  }
1775
+ if (options?.timeZone) {
1776
+ search.set("timeZone", options.timeZone);
1777
+ }
1774
1778
  const suffix = search.size > 0 ? `?${search.toString()}` : "";
1775
1779
  return request(`/api/v1/health/weight-loss${suffix}`);
1776
1780
  }