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.
- package/dist/assets/{action-bar-DBZ38L_6.js → action-bar-CsDQF9d4.js} +1 -1
- package/dist/assets/{activity-page-BJxUR9bD.js → activity-page-CACf1Sd5.js} +1 -1
- package/dist/assets/{ai-surface-workspace-ehzBjTsW.js → ai-surface-workspace-Vn5fqCAT.js} +1 -1
- package/dist/assets/{atlas-panel-BFUeVT69.js → atlas-panel-GdK1oyxo.js} +1 -1
- package/dist/assets/{board-CLOHbg6t.js → board-CuxQRKPJ.js} +1 -1
- package/dist/assets/{calendar-page-Bg8W-YQ-.js → calendar-page-CcVGbvfY.js} +1 -1
- package/dist/assets/{calendar-rules-9XQLoI96.js → calendar-rules-Dlq0KT93.js} +1 -1
- package/dist/assets/{calendar-week-toolbar-BeYnx1pE.js → calendar-week-toolbar-BVFb1_2G.js} +1 -1
- package/dist/assets/{charts-DwTguE_x.js → charts-BzT4pUPg.js} +1 -1
- package/dist/assets/{companion-sync-lab-page-BRKYkIrs.js → companion-sync-lab-page-Dgvtdc4i.js} +1 -1
- package/dist/assets/{daily-metrics-dashboard-Jt0nAHU5.js → daily-metrics-dashboard-ChQVbNJI.js} +1 -1
- package/dist/assets/date-keys-BnZV4PNO.js +1 -0
- package/dist/assets/{define-workbench-box-5h-dyvJY.js → define-workbench-box-DWAqqjvH.js} +1 -1
- package/dist/assets/{entity-link-multiselect-Bb4De_e3.js → entity-link-multiselect-Do5DTju7.js} +1 -1
- package/dist/assets/{entity-note-count-link-BzWnsOmI.js → entity-note-count-link-CV3eOv66.js} +1 -1
- package/dist/assets/{entity-notes-surface-CagOwdng.js → entity-notes-surface-CiUgAVl7.js} +1 -1
- package/dist/assets/{execution-board-DTmAtmqZ.js → execution-board-ClaXVQ0H.js} +1 -1
- package/dist/assets/{faceted-token-search-DJPRm9AY.js → faceted-token-search-DoSsb7qT.js} +1 -1
- package/dist/assets/{flagship-signal-deck-CJbGZF7I.js → flagship-signal-deck-B2s7TrIN.js} +1 -1
- package/dist/assets/{floating-action-menu-K77jh8XZ.js → floating-action-menu-DGkHCmvI.js} +1 -1
- package/dist/assets/{forms-C5d5hTf2.js → forms-D1qJ3oOP.js} +1 -1
- package/dist/assets/{generic-node-view-p5ePSFuG.js → generic-node-view-CgLkbts-.js} +1 -1
- package/dist/assets/{goal-detail-page-DGu_khEK.js → goal-detail-page-JRcEYpeA.js} +1 -1
- package/dist/assets/{goal-dialog-BWD7cOv9.js → goal-dialog-6R2uYWPQ.js} +1 -1
- package/dist/assets/{goals-page-Bgj3sj0f.js → goals-page-CYestXkp.js} +1 -1
- package/dist/assets/{graph-Cd5WF3lw.js → graph-BF4IsheG.js} +1 -1
- package/dist/assets/{habits-page-Ctut1tuX.js → habits-page-B27lnyKu.js} +1 -1
- package/dist/assets/{health-boxes-Ie3horXx.js → health-boxes-CeqSGyZ3.js} +1 -1
- package/dist/assets/index-DGYIFHgo.js +2 -0
- package/dist/assets/{index-CEgIwgk9.css → index-UzsVTD1W.css} +1 -1
- package/dist/assets/{inline-note-fields-DE6WM9uM.js → inline-note-fields-CJ9ukqFp.js} +1 -1
- package/dist/assets/{insight-flow-dialog-XL8I74eJ.js → insight-flow-dialog-BgaJRYGX.js} +1 -1
- package/dist/assets/{insights-page-Bz1FLbp5.js → insights-page-Dc5bilGE.js} +1 -1
- package/dist/assets/{kanban-boxes-BBziel7P.js → kanban-boxes-qIwmQlRq.js} +1 -1
- package/dist/assets/{kanban-page-DdQP73I3.js → kanban-page-BGnVLHeN.js} +1 -1
- package/dist/assets/{knowledge-graph-page-DdHWj4Dj.js → knowledge-graph-page-B7IsVVO_.js} +1 -1
- package/dist/assets/{life-force-page-BRN-93cI.js → life-force-page-B5N4DknK.js} +1 -1
- package/dist/assets/{life-force-workspace-BowvP5R4.js → life-force-workspace-Da-dmoRX.js} +1 -1
- package/dist/assets/{maps-D0Mm6WPG.js → maps-BTVHALP8.js} +1 -1
- package/dist/assets/{metric-tile-B6aJueRo.js → metric-tile-BVuj7mc3.js} +1 -1
- package/dist/assets/{motion-DwjmC9aq.js → motion-DcgUnXhY.js} +1 -1
- package/dist/assets/{movement-boxes-5cjWjIdR.js → movement-boxes-BZrdIh8b.js} +1 -1
- package/dist/assets/{movement-page-Dy53RWtB.js → movement-page-CY2XLgp_.js} +1 -1
- package/dist/assets/{note-markdown-CvEQCfoi.js → note-markdown-BZHBOkEd.js} +1 -1
- package/dist/assets/{note-tags-input-Cg5zBXos.js → note-tags-input-DucocvNH.js} +1 -1
- package/dist/assets/{notes-boxes-CgZyf7mV.js → notes-boxes-DAQ6KBkJ.js} +1 -1
- package/dist/assets/{notes-page-Cbdcv0Ej.js → notes-page-CSCfdDJl.js} +1 -1
- package/dist/assets/{open-in-graph-button-D31ZOEhA.js → open-in-graph-button-ObO3m8yd.js} +1 -1
- package/dist/assets/{orbit-map-8eNDWek8.js → orbit-map-Bteg-ola.js} +1 -1
- package/dist/assets/{overview-page-CZ6q52Qj.js → overview-page-ckfN_sLZ.js} +1 -1
- package/dist/assets/{page-hero-C0MpI3MM.js → page-hero-BkrRTu-t.js} +1 -1
- package/dist/assets/pill-cluster-COzv8VgR.js +1 -0
- package/dist/assets/{preference-entity-handoff-button-CJhr5AXl.js → preference-entity-handoff-button-BpVAeFmY.js} +1 -1
- package/dist/assets/{preferences-page-8vszDNFX.js → preferences-page-DseOh9AP.js} +1 -1
- package/dist/assets/{project-collections-sC7eAAhS.js → project-collections-C7yU5PSi.js} +1 -1
- package/dist/assets/{project-detail-page-BKdnMjJy.js → project-detail-page-Crt46y_7.js} +1 -1
- package/dist/assets/{project-dialog-BiHZpOo1.js → project-dialog-YYxtlqg8.js} +1 -1
- package/dist/assets/{project-management-hierarchy-page-VW-hykAI.js → project-management-hierarchy-page-DnojKHzy.js} +1 -1
- package/dist/assets/{project-management-section-nav-BEZ5zihs.js → project-management-section-nav-Ctjd348W.js} +1 -1
- package/dist/assets/{projects-boxes-Ca6rpYeE.js → projects-boxes-D8lLFGBC.js} +1 -1
- package/dist/assets/{projects-page-D86pjpTf.js → projects-page-CV_2em8d.js} +1 -1
- package/dist/assets/{psyche-behaviors-page-YLJB6CRU.js → psyche-behaviors-page-CYCNk0rz.js} +1 -1
- package/dist/assets/{psyche-flashcards-page-D3gdHLUw.js → psyche-flashcards-page-DwmjBixN.js} +1 -1
- package/dist/assets/{psyche-goal-map-page-DJiqSiCx.js → psyche-goal-map-page-CdMhyBKh.js} +1 -1
- package/dist/assets/{psyche-graph-Tke0qFdt.js → psyche-graph-BcrZcJrq.js} +1 -1
- package/dist/assets/{psyche-metrics-page-DoLnvmC2.js → psyche-metrics-page-D5bX1mNX.js} +1 -1
- package/dist/assets/{psyche-mode-guide-page-_Zbvg_HL.js → psyche-mode-guide-page-BmX9EOOB.js} +1 -1
- package/dist/assets/{psyche-modes-page-eAkaAzJc.js → psyche-modes-page-CTHFDyLE.js} +1 -1
- package/dist/assets/{psyche-page-CwmuBVjA.js → psyche-page-DRR3Fjv-.js} +1 -1
- package/dist/assets/{psyche-patterns-page-D7B4Ykjq.js → psyche-patterns-page-BAS38xYf.js} +1 -1
- package/dist/assets/{psyche-questionnaire-builder-page-CHJlvCzX.js → psyche-questionnaire-builder-page-DpSi-zg3.js} +1 -1
- package/dist/assets/{psyche-questionnaire-detail-page-Bg3ll-D4.js → psyche-questionnaire-detail-page-BcXIiV4E.js} +1 -1
- package/dist/assets/{psyche-questionnaire-run-detail-page-CIEerczF.js → psyche-questionnaire-run-detail-page-DvccwzWO.js} +1 -1
- package/dist/assets/{psyche-questionnaire-run-page-DyxIk6Qx.js → psyche-questionnaire-run-page-vcyon8IZ.js} +1 -1
- package/dist/assets/{psyche-questionnaires-page-ZlqUxuIl.js → psyche-questionnaires-page-B0DDTner.js} +1 -1
- package/dist/assets/{psyche-report-detail-page-B2jgYnQ5.js → psyche-report-detail-page-BgBA3xIO.js} +1 -1
- package/dist/assets/{psyche-reports-page-DlXr52yr.js → psyche-reports-page-Ce1vEEqW.js} +1 -1
- package/dist/assets/{psyche-schemas-Dtskzvv1.js → psyche-schemas-DDol0j-g.js} +1 -1
- package/dist/assets/{psyche-schemas-beliefs-page-BhrTZN9B.js → psyche-schemas-beliefs-page-D-IHHohY.js} +1 -1
- package/dist/assets/{psyche-screen-time-page-WNGcdusx.js → psyche-screen-time-page-DOKW3m8B.js} +1 -1
- package/dist/assets/{psyche-self-observation-page-XsCOglo4.js → psyche-self-observation-page-j7Sk6Ns1.js} +1 -1
- package/dist/assets/{psyche-values-page-NvHI6zWK.js → psyche-values-page-NfzdWUyb.js} +1 -1
- package/dist/assets/{question-flow-dialog-Bj4PxpjS.js → question-flow-dialog-CLHVmFON.js} +1 -1
- package/dist/assets/{report-chain-fields-mWFikZzT.js → report-chain-fields-C9MuyBha.js} +1 -1
- package/dist/assets/{rewards-page-DyjwXsQN.js → rewards-page-CVxOBP6m.js} +1 -1
- package/dist/assets/{scheduling-rules-editor-C6FEYOxd.js → scheduling-rules-editor-BmbOHH_R.js} +1 -1
- package/dist/assets/{schema-badge-CCe4zkSN.js → schema-badge-BAiS_OAp.js} +1 -1
- package/dist/assets/{schemas-Cjwn6ooR.js → schemas-B0AXfuOr.js} +1 -1
- package/dist/assets/{select-menu-DeJhCsd8.js → select-menu-Bl5MILOj.js} +1 -1
- package/dist/assets/{settings-agents-page-DYDxyKu-.js → settings-agents-page-BIRP02mt.js} +1 -1
- package/dist/assets/{settings-bin-page-BWLfKVW1.js → settings-bin-page-DxJKeM28.js} +1 -1
- package/dist/assets/{settings-calendar-page-Ci-wXhCv.js → settings-calendar-page-aMBtwvO_.js} +1 -1
- package/dist/assets/{settings-data-page-DxIw-fWn.js → settings-data-page-nHNergsh.js} +1 -1
- package/dist/assets/{settings-logs-page-inpyIdu6.js → settings-logs-page-CFotPoFy.js} +1 -1
- package/dist/assets/{settings-mobile-page-BcJGLax8.js → settings-mobile-page-Bf_1D-bN.js} +1 -1
- package/dist/assets/{settings-models-page-OWTRhfkj.js → settings-models-page-BbFNmnik.js} +1 -1
- package/dist/assets/{settings-page-DXxF3qK2.js → settings-page-CalbNbcg.js} +1 -1
- package/dist/assets/{settings-rewards-page-CCXin1n_.js → settings-rewards-page-ByDfCzP5.js} +1 -1
- package/dist/assets/{settings-section-nav-CwSDNC3W.js → settings-section-nav-x_JXfzL9.js} +1 -1
- package/dist/assets/{settings-users-page-B4NEACwR.js → settings-users-page-N1jl9hWV.js} +1 -1
- package/dist/assets/{settings-wiki-page-BNRK1SYc.js → settings-wiki-page-Bn2Qr94L.js} +1 -1
- package/dist/assets/{sleep-page-Bara54nB.js → sleep-page-DyU635jb.js} +1 -1
- package/dist/assets/{sports-page-BawekFKD.js → sports-page-DbK4yGrR.js} +1 -1
- package/dist/assets/{state-BtwEvpO6.js → state-Bpe5dF3T.js} +1 -1
- package/dist/assets/{strategies-page-BEl3NGAU.js → strategies-page-AUBlFCZ9.js} +1 -1
- package/dist/assets/{strategy-detail-page-DPPI_8Ub.js → strategy-detail-page-DCF8mVaL.js} +1 -1
- package/dist/assets/{strategy-dialog-DU6wbBkQ.js → strategy-dialog-50xKlqcz.js} +1 -1
- package/dist/assets/{surface-BVYp-Wq9.js → surface-BjT1dIAF.js} +1 -1
- package/dist/assets/{table-BuONJH1s.js → table-U7otr5go.js} +1 -1
- package/dist/assets/{task-detail-page-BsMVAsbb.js → task-detail-page-Dsll2LCX.js} +1 -1
- package/dist/assets/{task-dialog-BGzPc6TW.js → task-dialog-BfPkbIPE.js} +1 -1
- package/dist/assets/{timebox-planning-dialog-4_XWuqkw.js → timebox-planning-dialog-BTKUS6Jj.js} +1 -1
- package/dist/assets/{today-boxes-5pCvh5zS.js → today-boxes-BPi9bDHM.js} +1 -1
- package/dist/assets/{today-page-BGurICpl.js → today-page-BSXSA-Ts.js} +1 -1
- package/dist/assets/{training-load-page-BkoYKZ9_.js → training-load-page-BD0s8-Zm.js} +1 -1
- package/dist/assets/{ui-B9O-eUim.js → ui-B9TWEtCx.js} +1 -1
- package/dist/assets/{use-anchored-overlay-position-BrQ4cqKn.js → use-anchored-overlay-position-BY4kNzPj.js} +1 -1
- package/dist/assets/{use-psyche-focus-target-Cuxni3SK.js → use-psyche-focus-target-BhNedCZB.js} +1 -1
- package/dist/assets/{user-badge-DfDv87j7.js → user-badge-ap1PvOxd.js} +1 -1
- package/dist/assets/{user-select-field-BNqv8-wd.js → user-select-field-fx129Uh6.js} +1 -1
- package/dist/assets/{utility-widgets-CbYj32he.js → utility-widgets-Cq508sqJ.js} +1 -1
- package/dist/assets/{vendor-Cpmju3nw.js → vendor-BwL6m4SE.js} +216 -211
- package/dist/assets/{vitals-page-DlxMk-L7.js → vitals-page-BnXk8OzN.js} +1 -1
- package/dist/assets/{weekly-review-page-VrEvAl1T.js → weekly-review-page-CnmSGnEC.js} +1 -1
- package/dist/assets/weight-loss-page-Bn6lAXNf.js +5 -0
- package/dist/assets/{wiki-article-markdown-DxTkiQRy.js → wiki-article-markdown-BDnkdNDC.js} +1 -1
- package/dist/assets/{wiki-editor-page-Beu92YnZ.js → wiki-editor-page-CHEETB0G.js} +1 -1
- package/dist/assets/{wiki-ingest-history-page-Cq_soygm.js → wiki-ingest-history-page-CisvtAzm.js} +1 -1
- package/dist/assets/{wiki-ingest-modal-D6xs2sEn.js → wiki-ingest-modal-C2faIjzj.js} +1 -1
- package/dist/assets/{wiki-page-yF3Flgtg.js → wiki-page-CELe_6qL.js} +1 -1
- package/dist/assets/{workbench-flow-page-CZZGg3a8.js → workbench-flow-page-JBVim3L0.js} +1 -1
- package/dist/assets/{workbench-page-DKENZ5Nz.js → workbench-page-q7Z3sQtz.js} +1 -1
- package/dist/assets/{workout-detail-page-CeN4QiYQ.js → workout-detail-page-9I_3DPYU.js} +2 -2
- package/dist/index.html +8 -8
- package/dist/server/server/migrations/070_health_mobile_sync_completion_index.sql +16 -0
- package/dist/server/server/src/app.js +135 -13
- package/dist/server/server/src/health-weight-loss.js +48 -6
- package/dist/server/server/src/health.js +301 -93
- package/dist/server/server/src/openapi.js +31 -0
- package/dist/server/server/src/repositories/gamification.js +25 -0
- package/dist/server/server/src/repositories/tasks.js +82 -17
- package/dist/server/server/src/services/dashboard.js +5 -4
- package/dist/server/server/src/services/gamification.js +47 -19
- package/dist/server/server/src/services/life-force.js +99 -0
- package/dist/server/src/lib/api.js +4 -0
- package/dist/server/src/lib/snapshot-normalizer.js +42 -23
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/070_health_mobile_sync_completion_index.sql +16 -0
- package/skills/forge-openclaw/SKILL.md +1 -0
- package/skills/forge-openclaw/psyche_entity_playbooks.md +8 -0
- package/dist/assets/date-keys-Cj1G3TOn.js +0 -1
- package/dist/assets/index-BaiwtAgo.js +0 -2
- package/dist/assets/pill-cluster-DGwKZQKF.js +0 -1
- 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
|
|
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
|
|
28
|
-
FROM
|
|
29
|
-
WHERE
|
|
30
|
-
ORDER BY
|
|
31
|
-
.all(
|
|
32
|
-
|
|
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
|
|
102
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
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
|
-
})
|
|
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 =
|
|
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
|
|
276
|
-
? `
|
|
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
|
-
? [
|
|
283
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
}
|