forge-openclaw-plugin 0.2.27 → 0.2.28
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/README.md +1 -0
- package/dist/assets/{board-C6jCchjI.js → board-DPFvZf-D.js} +2 -2
- package/dist/assets/{board-C6jCchjI.js.map → board-DPFvZf-D.js.map} +1 -1
- package/dist/assets/index-Auw3JrdE.css +1 -0
- package/dist/assets/index-D1H7myQH.js +85 -0
- package/dist/assets/index-D1H7myQH.js.map +1 -0
- package/dist/assets/{motion-DFHrH2rd.js → motion-Bvwc85ch.js} +2 -2
- package/dist/assets/{motion-DFHrH2rd.js.map → motion-Bvwc85ch.js.map} +1 -1
- package/dist/assets/{table-ZL7Di_u3.js → table-FJQTJvUR.js} +2 -2
- package/dist/assets/{table-ZL7Di_u3.js.map → table-FJQTJvUR.js.map} +1 -1
- package/dist/assets/{ui-CKNPpz7q.js → ui-GXFcgvSw.js} +2 -2
- package/dist/assets/{ui-CKNPpz7q.js.map → ui-GXFcgvSw.js.map} +1 -1
- package/dist/assets/{vendor-DoNZuFhn.js → vendor-Cwf49UMz.js} +204 -204
- package/dist/assets/vendor-Cwf49UMz.js.map +1 -0
- package/dist/index.html +7 -7
- package/dist/server/server/src/app.js +244 -2
- package/dist/server/server/src/openapi.js +799 -2
- package/dist/server/server/src/repositories/calendar.js +151 -0
- package/dist/server/server/src/services/life-force-model.js +20 -0
- package/dist/server/server/src/services/life-force.js +1333 -97
- package/dist/server/server/src/types.js +21 -1
- package/dist/server/src/lib/snapshot-normalizer.js +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/forge-openclaw/SKILL.md +17 -0
- package/skills/forge-openclaw/entity_conversation_playbooks.md +238 -0
- package/skills/forge-openclaw/psyche_entity_playbooks.md +57 -0
- package/dist/assets/index-DVvS8iiU.css +0 -1
- package/dist/assets/index-zYB-9Dfo.js +0 -85
- package/dist/assets/index-zYB-9Dfo.js.map +0 -1
- package/dist/assets/vendor-DoNZuFhn.js.map +0 -1
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { getDatabase, runInTransaction } from "../db.js";
|
|
3
3
|
import { getDefaultUser, getUserById } from "../repositories/users.js";
|
|
4
4
|
import { actionProfileSchema, fatigueSignalCreateSchema, lifeForcePayloadSchema, lifeForceProfilePatchSchema, lifeForceTemplateUpdateSchema } from "../types.js";
|
|
5
|
-
import {
|
|
5
|
+
import { LIFE_FORCE_BASELINE_DAILY_AP, buildDefaultTaskActionProfile, buildTaskActionPointSummary, buildTaskSplitSuggestion, clamp, computeActionCostModifier, computeLifeForceLevelMultiplier, computeStatCostModifier, interpolateCurveRate, normalizeCurveToBudget, resolveBandTotalCostAp, resolveTaskExpectedDurationSeconds } from "./life-force-model.js";
|
|
6
6
|
import { computeWorkTime } from "./work-time.js";
|
|
7
7
|
const DEFAULT_TEMPLATE_POINTS = [
|
|
8
8
|
{ minuteOfDay: 0, rateApPerHour: 0 },
|
|
@@ -23,6 +23,217 @@ const LIFE_FORCE_STAT_LABELS = {
|
|
|
23
23
|
composure: "Composure",
|
|
24
24
|
flow: "Flow"
|
|
25
25
|
};
|
|
26
|
+
const CALENDAR_ACTIVITY_PRESETS = {
|
|
27
|
+
deep_work: {
|
|
28
|
+
title: "Deep work",
|
|
29
|
+
mode: "container",
|
|
30
|
+
sustainRateApPerHour: 14,
|
|
31
|
+
demandWeights: {
|
|
32
|
+
activation: 0.1,
|
|
33
|
+
focus: 0.55,
|
|
34
|
+
vigor: 0.05,
|
|
35
|
+
composure: 0.05,
|
|
36
|
+
flow: 0.25
|
|
37
|
+
},
|
|
38
|
+
doubleCountPolicy: "container_only",
|
|
39
|
+
costBand: "light",
|
|
40
|
+
recoveryEffect: 0,
|
|
41
|
+
metadata: {
|
|
42
|
+
physicalIntensity: 0.15,
|
|
43
|
+
cognitiveDemand: 0.9,
|
|
44
|
+
socialLoad: 0.1,
|
|
45
|
+
switchingLoad: 0.2
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
admin: {
|
|
49
|
+
title: "Admin and coordination",
|
|
50
|
+
mode: "container",
|
|
51
|
+
sustainRateApPerHour: 9,
|
|
52
|
+
demandWeights: {
|
|
53
|
+
activation: 0.15,
|
|
54
|
+
focus: 0.3,
|
|
55
|
+
vigor: 0.05,
|
|
56
|
+
composure: 0.1,
|
|
57
|
+
flow: 0.4
|
|
58
|
+
},
|
|
59
|
+
doubleCountPolicy: "container_only",
|
|
60
|
+
costBand: "light",
|
|
61
|
+
recoveryEffect: 0,
|
|
62
|
+
metadata: {
|
|
63
|
+
physicalIntensity: 0.1,
|
|
64
|
+
cognitiveDemand: 0.45,
|
|
65
|
+
socialLoad: 0.25,
|
|
66
|
+
switchingLoad: 0.7
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
maintenance: {
|
|
70
|
+
title: "Maintenance and light activity",
|
|
71
|
+
mode: "container",
|
|
72
|
+
sustainRateApPerHour: 6,
|
|
73
|
+
demandWeights: {
|
|
74
|
+
activation: 0.15,
|
|
75
|
+
focus: 0.2,
|
|
76
|
+
vigor: 0.1,
|
|
77
|
+
composure: 0.05,
|
|
78
|
+
flow: 0.5
|
|
79
|
+
},
|
|
80
|
+
doubleCountPolicy: "container_only",
|
|
81
|
+
costBand: "light",
|
|
82
|
+
recoveryEffect: 0,
|
|
83
|
+
metadata: {
|
|
84
|
+
physicalIntensity: 0.2,
|
|
85
|
+
cognitiveDemand: 0.25,
|
|
86
|
+
socialLoad: 0.1,
|
|
87
|
+
switchingLoad: 0.5
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
meeting: {
|
|
91
|
+
title: "Meeting or social commitment",
|
|
92
|
+
mode: "container",
|
|
93
|
+
sustainRateApPerHour: 13,
|
|
94
|
+
demandWeights: {
|
|
95
|
+
activation: 0.05,
|
|
96
|
+
focus: 0.25,
|
|
97
|
+
vigor: 0.05,
|
|
98
|
+
composure: 0.45,
|
|
99
|
+
flow: 0.2
|
|
100
|
+
},
|
|
101
|
+
doubleCountPolicy: "container_only",
|
|
102
|
+
costBand: "standard",
|
|
103
|
+
recoveryEffect: 0,
|
|
104
|
+
metadata: {
|
|
105
|
+
physicalIntensity: 0.1,
|
|
106
|
+
cognitiveDemand: 0.45,
|
|
107
|
+
socialLoad: 0.85,
|
|
108
|
+
switchingLoad: 0.35
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
recovery_break: {
|
|
112
|
+
title: "Rest and recovery",
|
|
113
|
+
mode: "recovery",
|
|
114
|
+
sustainRateApPerHour: 3,
|
|
115
|
+
demandWeights: {
|
|
116
|
+
activation: 0.05,
|
|
117
|
+
focus: 0.05,
|
|
118
|
+
vigor: 0.15,
|
|
119
|
+
composure: 0.15,
|
|
120
|
+
flow: 0.1
|
|
121
|
+
},
|
|
122
|
+
doubleCountPolicy: "container_only",
|
|
123
|
+
costBand: "tiny",
|
|
124
|
+
recoveryEffect: 4,
|
|
125
|
+
metadata: {
|
|
126
|
+
physicalIntensity: 0.1,
|
|
127
|
+
cognitiveDemand: 0.05,
|
|
128
|
+
socialLoad: 0.1,
|
|
129
|
+
switchingLoad: 0.05
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
holiday_leisure: {
|
|
133
|
+
title: "Holiday or leisure time",
|
|
134
|
+
mode: "container",
|
|
135
|
+
sustainRateApPerHour: 4,
|
|
136
|
+
demandWeights: {
|
|
137
|
+
activation: 0.05,
|
|
138
|
+
focus: 0.05,
|
|
139
|
+
vigor: 0.15,
|
|
140
|
+
composure: 0.25,
|
|
141
|
+
flow: 0.1
|
|
142
|
+
},
|
|
143
|
+
doubleCountPolicy: "container_only",
|
|
144
|
+
costBand: "tiny",
|
|
145
|
+
recoveryEffect: 2,
|
|
146
|
+
metadata: {
|
|
147
|
+
physicalIntensity: 0.15,
|
|
148
|
+
cognitiveDemand: 0.1,
|
|
149
|
+
socialLoad: 0.35,
|
|
150
|
+
switchingLoad: 0.1
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
light_context: {
|
|
154
|
+
title: "Light context",
|
|
155
|
+
mode: "container",
|
|
156
|
+
sustainRateApPerHour: 2,
|
|
157
|
+
demandWeights: {
|
|
158
|
+
activation: 0.05,
|
|
159
|
+
focus: 0.1,
|
|
160
|
+
vigor: 0.05,
|
|
161
|
+
composure: 0.15,
|
|
162
|
+
flow: 0.15
|
|
163
|
+
},
|
|
164
|
+
doubleCountPolicy: "container_only",
|
|
165
|
+
costBand: "tiny",
|
|
166
|
+
recoveryEffect: 0,
|
|
167
|
+
metadata: {
|
|
168
|
+
physicalIntensity: 0.05,
|
|
169
|
+
cognitiveDemand: 0.15,
|
|
170
|
+
socialLoad: 0.2,
|
|
171
|
+
switchingLoad: 0.15
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function buildStatLevels(profile) {
|
|
176
|
+
return {
|
|
177
|
+
life_force: profile.life_force_level,
|
|
178
|
+
activation: profile.activation_level,
|
|
179
|
+
focus: profile.focus_level,
|
|
180
|
+
vigor: profile.vigor_level,
|
|
181
|
+
composure: profile.composure_level,
|
|
182
|
+
flow: profile.flow_level
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function buildEffectiveProfile(profile, lifeForceProfile) {
|
|
186
|
+
const costModifier = computeActionCostModifier(profile.demandWeights, buildStatLevels(lifeForceProfile));
|
|
187
|
+
return {
|
|
188
|
+
...profile,
|
|
189
|
+
startupAp: Number((profile.startupAp * costModifier).toFixed(4)),
|
|
190
|
+
totalCostAp: Number((profile.totalCostAp * costModifier).toFixed(4)),
|
|
191
|
+
sustainRateApPerHour: Number((profile.sustainRateApPerHour * costModifier).toFixed(4)),
|
|
192
|
+
metadata: {
|
|
193
|
+
...profile.metadata,
|
|
194
|
+
costModifier
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function rateToTotalAp(rateApPerHour, durationSeconds) {
|
|
199
|
+
return Number(((durationSeconds / 3600) * rateApPerHour).toFixed(4));
|
|
200
|
+
}
|
|
201
|
+
function overlapsWindow(leftStartIso, leftEndIso, rightStartIso, rightEndIso) {
|
|
202
|
+
return (Date.parse(leftStartIso) < Date.parse(rightEndIso) &&
|
|
203
|
+
Date.parse(leftEndIso) > Date.parse(rightStartIso));
|
|
204
|
+
}
|
|
205
|
+
function clipWindowToRange(window, range) {
|
|
206
|
+
const startMs = Math.max(Date.parse(window.startAt), Date.parse(range.startAt));
|
|
207
|
+
const endMs = Math.min(Date.parse(window.endAt), Date.parse(range.endAt));
|
|
208
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return { startMs, endMs };
|
|
212
|
+
}
|
|
213
|
+
function computeUncoveredSeconds(window, blockingWindows) {
|
|
214
|
+
const clippedBlocks = blockingWindows
|
|
215
|
+
.map((candidate) => clipWindowToRange(candidate, window))
|
|
216
|
+
.filter((candidate) => candidate !== null)
|
|
217
|
+
.sort((left, right) => left.startMs - right.startMs);
|
|
218
|
+
const totalSeconds = Math.max(0, Math.floor((Date.parse(window.endAt) - Date.parse(window.startAt)) / 1000));
|
|
219
|
+
if (totalSeconds <= 0 || clippedBlocks.length === 0) {
|
|
220
|
+
return totalSeconds;
|
|
221
|
+
}
|
|
222
|
+
let coveredMs = 0;
|
|
223
|
+
let activeStartMs = clippedBlocks[0].startMs;
|
|
224
|
+
let activeEndMs = clippedBlocks[0].endMs;
|
|
225
|
+
for (const block of clippedBlocks.slice(1)) {
|
|
226
|
+
if (block.startMs <= activeEndMs) {
|
|
227
|
+
activeEndMs = Math.max(activeEndMs, block.endMs);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
coveredMs += activeEndMs - activeStartMs;
|
|
231
|
+
activeStartMs = block.startMs;
|
|
232
|
+
activeEndMs = block.endMs;
|
|
233
|
+
}
|
|
234
|
+
coveredMs += activeEndMs - activeStartMs;
|
|
235
|
+
return Math.max(0, totalSeconds - Math.floor(coveredMs / 1000));
|
|
236
|
+
}
|
|
26
237
|
function nowIso() {
|
|
27
238
|
return new Date().toISOString();
|
|
28
239
|
}
|
|
@@ -69,6 +280,125 @@ function parseCurvePoints(raw) {
|
|
|
69
280
|
function defaultTemplatePoints() {
|
|
70
281
|
return normalizeCurveToBudget(DEFAULT_TEMPLATE_POINTS, LIFE_FORCE_BASELINE_DAILY_AP);
|
|
71
282
|
}
|
|
283
|
+
function resolveWorkBlockPresetKey(kind) {
|
|
284
|
+
if (kind === "main_activity") {
|
|
285
|
+
return "deep_work";
|
|
286
|
+
}
|
|
287
|
+
if (kind === "secondary_activity") {
|
|
288
|
+
return "admin";
|
|
289
|
+
}
|
|
290
|
+
if (kind === "third_activity" || kind === "custom") {
|
|
291
|
+
return "maintenance";
|
|
292
|
+
}
|
|
293
|
+
if (kind === "holiday") {
|
|
294
|
+
return "holiday_leisure";
|
|
295
|
+
}
|
|
296
|
+
return "recovery_break";
|
|
297
|
+
}
|
|
298
|
+
function resolveCalendarEventPresetKey(input) {
|
|
299
|
+
const searchable = `${input.title} ${input.eventType ?? ""}`.toLowerCase();
|
|
300
|
+
if (searchable.includes("meeting") || searchable.includes("call") || searchable.includes("interview")) {
|
|
301
|
+
return "meeting";
|
|
302
|
+
}
|
|
303
|
+
if (searchable.includes("deep work") || searchable.includes("focus")) {
|
|
304
|
+
return "deep_work";
|
|
305
|
+
}
|
|
306
|
+
if (searchable.includes("admin") || searchable.includes("email") || searchable.includes("inbox")) {
|
|
307
|
+
return "admin";
|
|
308
|
+
}
|
|
309
|
+
if (searchable.includes("lunch") ||
|
|
310
|
+
searchable.includes("break") ||
|
|
311
|
+
searchable.includes("rest")) {
|
|
312
|
+
return "recovery_break";
|
|
313
|
+
}
|
|
314
|
+
if (searchable.includes("holiday") || searchable.includes("vacation")) {
|
|
315
|
+
return "holiday_leisure";
|
|
316
|
+
}
|
|
317
|
+
return input.availability === "busy" ? "meeting" : "light_context";
|
|
318
|
+
}
|
|
319
|
+
function buildCalendarActivityProfile(input) {
|
|
320
|
+
const durationSeconds = Math.max(1, input.expectedDurationSeconds);
|
|
321
|
+
const presetDefinition = input.activityPresetKey === "task_inherited"
|
|
322
|
+
? null
|
|
323
|
+
: CALENDAR_ACTIVITY_PRESETS[input.activityPresetKey];
|
|
324
|
+
const inheritedProfile = input.fallbackProfile;
|
|
325
|
+
const baseProfile = presetDefinition === null
|
|
326
|
+
? inheritedProfile
|
|
327
|
+
: actionProfileSchema.parse({
|
|
328
|
+
id: `profile_${input.entityType}_${input.entityId}`,
|
|
329
|
+
profileKey: `${String(input.entityType)}_${input.entityId}`,
|
|
330
|
+
title: input.title || presetDefinition.title,
|
|
331
|
+
entityType: input.entityType,
|
|
332
|
+
mode: presetDefinition.mode,
|
|
333
|
+
startupAp: 0,
|
|
334
|
+
totalCostAp: rateToTotalAp(presetDefinition.sustainRateApPerHour, durationSeconds),
|
|
335
|
+
expectedDurationSeconds: durationSeconds,
|
|
336
|
+
sustainRateApPerHour: presetDefinition.sustainRateApPerHour,
|
|
337
|
+
demandWeights: presetDefinition.demandWeights,
|
|
338
|
+
doubleCountPolicy: presetDefinition.doubleCountPolicy,
|
|
339
|
+
sourceMethod: input.sourceMethod ?? "inferred",
|
|
340
|
+
costBand: presetDefinition.costBand,
|
|
341
|
+
recoveryEffect: presetDefinition.recoveryEffect,
|
|
342
|
+
metadata: {
|
|
343
|
+
activityPresetKey: input.activityPresetKey,
|
|
344
|
+
...presetDefinition.metadata,
|
|
345
|
+
...(input.metadata ?? {})
|
|
346
|
+
},
|
|
347
|
+
createdAt: nowIso(),
|
|
348
|
+
updatedAt: nowIso()
|
|
349
|
+
});
|
|
350
|
+
const base = baseProfile ??
|
|
351
|
+
actionProfileSchema.parse({
|
|
352
|
+
id: `profile_${input.entityType}_${input.entityId}`,
|
|
353
|
+
profileKey: `${String(input.entityType)}_${input.entityId}`,
|
|
354
|
+
title: input.title || "Activity",
|
|
355
|
+
entityType: input.entityType,
|
|
356
|
+
mode: "container",
|
|
357
|
+
startupAp: 0,
|
|
358
|
+
totalCostAp: rateToTotalAp(8, durationSeconds),
|
|
359
|
+
expectedDurationSeconds: durationSeconds,
|
|
360
|
+
sustainRateApPerHour: 8,
|
|
361
|
+
demandWeights: {
|
|
362
|
+
activation: 0.1,
|
|
363
|
+
focus: 0.3,
|
|
364
|
+
vigor: 0.1,
|
|
365
|
+
composure: 0.1,
|
|
366
|
+
flow: 0.4
|
|
367
|
+
},
|
|
368
|
+
doubleCountPolicy: "container_only",
|
|
369
|
+
sourceMethod: input.sourceMethod ?? "inferred",
|
|
370
|
+
costBand: "light",
|
|
371
|
+
recoveryEffect: 0,
|
|
372
|
+
metadata: {
|
|
373
|
+
activityPresetKey: "maintenance",
|
|
374
|
+
...(input.metadata ?? {})
|
|
375
|
+
},
|
|
376
|
+
createdAt: nowIso(),
|
|
377
|
+
updatedAt: nowIso()
|
|
378
|
+
});
|
|
379
|
+
const sustainRateApPerHour = input.customSustainRateApPerHour ?? base.sustainRateApPerHour;
|
|
380
|
+
return actionProfileSchema.parse({
|
|
381
|
+
...base,
|
|
382
|
+
id: `profile_${input.entityType}_${input.entityId}`,
|
|
383
|
+
profileKey: `${String(input.entityType)}_${input.entityId}`,
|
|
384
|
+
title: input.title || base.title,
|
|
385
|
+
entityType: input.entityType,
|
|
386
|
+
expectedDurationSeconds: durationSeconds,
|
|
387
|
+
totalCostAp: rateToTotalAp(sustainRateApPerHour, durationSeconds),
|
|
388
|
+
sustainRateApPerHour,
|
|
389
|
+
sourceMethod: input.customSustainRateApPerHour !== null &&
|
|
390
|
+
input.customSustainRateApPerHour !== undefined
|
|
391
|
+
? "manual"
|
|
392
|
+
: input.sourceMethod ?? base.sourceMethod,
|
|
393
|
+
metadata: {
|
|
394
|
+
...base.metadata,
|
|
395
|
+
activityPresetKey: input.activityPresetKey,
|
|
396
|
+
customSustainRateApPerHour: input.customSustainRateApPerHour ?? null,
|
|
397
|
+
...(input.metadata ?? {})
|
|
398
|
+
},
|
|
399
|
+
updatedAt: nowIso()
|
|
400
|
+
});
|
|
401
|
+
}
|
|
72
402
|
function seededActionProfiles() {
|
|
73
403
|
const now = nowIso();
|
|
74
404
|
const taskDefault = buildDefaultTaskActionProfile({});
|
|
@@ -198,6 +528,206 @@ function seededActionProfiles() {
|
|
|
198
528
|
metadata: {},
|
|
199
529
|
createdAt: now,
|
|
200
530
|
updatedAt: now
|
|
531
|
+
}),
|
|
532
|
+
actionProfileSchema.parse({
|
|
533
|
+
id: "profile_wake_start",
|
|
534
|
+
profileKey: "wake_start",
|
|
535
|
+
title: "Get out of bed",
|
|
536
|
+
entityType: "sleep_session",
|
|
537
|
+
mode: "impulse",
|
|
538
|
+
startupAp: 5,
|
|
539
|
+
totalCostAp: 5,
|
|
540
|
+
expectedDurationSeconds: null,
|
|
541
|
+
sustainRateApPerHour: 0,
|
|
542
|
+
demandWeights: {
|
|
543
|
+
activation: 0.75,
|
|
544
|
+
focus: 0.05,
|
|
545
|
+
vigor: 0.1,
|
|
546
|
+
composure: 0,
|
|
547
|
+
flow: 0.1
|
|
548
|
+
},
|
|
549
|
+
doubleCountPolicy: "primary_only",
|
|
550
|
+
sourceMethod: "seeded",
|
|
551
|
+
costBand: "light",
|
|
552
|
+
recoveryEffect: 0,
|
|
553
|
+
metadata: {},
|
|
554
|
+
createdAt: now,
|
|
555
|
+
updatedAt: now
|
|
556
|
+
}),
|
|
557
|
+
actionProfileSchema.parse({
|
|
558
|
+
id: "profile_timebox_default",
|
|
559
|
+
profileKey: "task_timebox_default",
|
|
560
|
+
title: "Task timebox",
|
|
561
|
+
entityType: "task_timebox",
|
|
562
|
+
mode: "container",
|
|
563
|
+
startupAp: 0,
|
|
564
|
+
totalCostAp: 12,
|
|
565
|
+
expectedDurationSeconds: 3_600,
|
|
566
|
+
sustainRateApPerHour: 12,
|
|
567
|
+
demandWeights: {
|
|
568
|
+
activation: 0.15,
|
|
569
|
+
focus: 0.45,
|
|
570
|
+
vigor: 0.05,
|
|
571
|
+
composure: 0.05,
|
|
572
|
+
flow: 0.3
|
|
573
|
+
},
|
|
574
|
+
doubleCountPolicy: "container_only",
|
|
575
|
+
sourceMethod: "seeded",
|
|
576
|
+
costBand: "light",
|
|
577
|
+
recoveryEffect: 0,
|
|
578
|
+
metadata: {},
|
|
579
|
+
createdAt: now,
|
|
580
|
+
updatedAt: now
|
|
581
|
+
}),
|
|
582
|
+
actionProfileSchema.parse({
|
|
583
|
+
id: "profile_work_block_main",
|
|
584
|
+
profileKey: "work_block_main",
|
|
585
|
+
title: "Main activity block",
|
|
586
|
+
entityType: "work_block",
|
|
587
|
+
mode: "container",
|
|
588
|
+
startupAp: 0,
|
|
589
|
+
totalCostAp: 14,
|
|
590
|
+
expectedDurationSeconds: 3_600,
|
|
591
|
+
sustainRateApPerHour: 14,
|
|
592
|
+
demandWeights: {
|
|
593
|
+
activation: 0.1,
|
|
594
|
+
focus: 0.5,
|
|
595
|
+
vigor: 0.1,
|
|
596
|
+
composure: 0.05,
|
|
597
|
+
flow: 0.25
|
|
598
|
+
},
|
|
599
|
+
doubleCountPolicy: "container_only",
|
|
600
|
+
sourceMethod: "seeded",
|
|
601
|
+
costBand: "light",
|
|
602
|
+
recoveryEffect: 0,
|
|
603
|
+
metadata: { kind: "main_activity" },
|
|
604
|
+
createdAt: now,
|
|
605
|
+
updatedAt: now
|
|
606
|
+
}),
|
|
607
|
+
actionProfileSchema.parse({
|
|
608
|
+
id: "profile_work_block_secondary",
|
|
609
|
+
profileKey: "work_block_secondary",
|
|
610
|
+
title: "Secondary activity block",
|
|
611
|
+
entityType: "work_block",
|
|
612
|
+
mode: "container",
|
|
613
|
+
startupAp: 0,
|
|
614
|
+
totalCostAp: 9,
|
|
615
|
+
expectedDurationSeconds: 3_600,
|
|
616
|
+
sustainRateApPerHour: 9,
|
|
617
|
+
demandWeights: {
|
|
618
|
+
activation: 0.15,
|
|
619
|
+
focus: 0.3,
|
|
620
|
+
vigor: 0.1,
|
|
621
|
+
composure: 0.05,
|
|
622
|
+
flow: 0.4
|
|
623
|
+
},
|
|
624
|
+
doubleCountPolicy: "container_only",
|
|
625
|
+
sourceMethod: "seeded",
|
|
626
|
+
costBand: "light",
|
|
627
|
+
recoveryEffect: 0,
|
|
628
|
+
metadata: { kind: "secondary_activity" },
|
|
629
|
+
createdAt: now,
|
|
630
|
+
updatedAt: now
|
|
631
|
+
}),
|
|
632
|
+
actionProfileSchema.parse({
|
|
633
|
+
id: "profile_work_block_third",
|
|
634
|
+
profileKey: "work_block_third",
|
|
635
|
+
title: "Third activity block",
|
|
636
|
+
entityType: "work_block",
|
|
637
|
+
mode: "container",
|
|
638
|
+
startupAp: 0,
|
|
639
|
+
totalCostAp: 6,
|
|
640
|
+
expectedDurationSeconds: 3_600,
|
|
641
|
+
sustainRateApPerHour: 6,
|
|
642
|
+
demandWeights: {
|
|
643
|
+
activation: 0.15,
|
|
644
|
+
focus: 0.2,
|
|
645
|
+
vigor: 0.1,
|
|
646
|
+
composure: 0.05,
|
|
647
|
+
flow: 0.5
|
|
648
|
+
},
|
|
649
|
+
doubleCountPolicy: "container_only",
|
|
650
|
+
sourceMethod: "seeded",
|
|
651
|
+
costBand: "light",
|
|
652
|
+
recoveryEffect: 0,
|
|
653
|
+
metadata: { kind: "third_activity" },
|
|
654
|
+
createdAt: now,
|
|
655
|
+
updatedAt: now
|
|
656
|
+
}),
|
|
657
|
+
actionProfileSchema.parse({
|
|
658
|
+
id: "profile_work_block_rest",
|
|
659
|
+
profileKey: "work_block_rest",
|
|
660
|
+
title: "Rest block",
|
|
661
|
+
entityType: "work_block",
|
|
662
|
+
mode: "recovery",
|
|
663
|
+
startupAp: 0,
|
|
664
|
+
totalCostAp: 3,
|
|
665
|
+
expectedDurationSeconds: 3_600,
|
|
666
|
+
sustainRateApPerHour: 3,
|
|
667
|
+
demandWeights: {
|
|
668
|
+
activation: 0.05,
|
|
669
|
+
focus: 0.05,
|
|
670
|
+
vigor: 0.15,
|
|
671
|
+
composure: 0.15,
|
|
672
|
+
flow: 0.1
|
|
673
|
+
},
|
|
674
|
+
doubleCountPolicy: "container_only",
|
|
675
|
+
sourceMethod: "seeded",
|
|
676
|
+
costBand: "tiny",
|
|
677
|
+
recoveryEffect: 4,
|
|
678
|
+
metadata: { kind: "rest", activityPresetKey: "recovery_break" },
|
|
679
|
+
createdAt: now,
|
|
680
|
+
updatedAt: now
|
|
681
|
+
}),
|
|
682
|
+
actionProfileSchema.parse({
|
|
683
|
+
id: "profile_work_block_holiday",
|
|
684
|
+
profileKey: "work_block_holiday",
|
|
685
|
+
title: "Holiday block",
|
|
686
|
+
entityType: "work_block",
|
|
687
|
+
mode: "container",
|
|
688
|
+
startupAp: 0,
|
|
689
|
+
totalCostAp: 4,
|
|
690
|
+
expectedDurationSeconds: 3_600,
|
|
691
|
+
sustainRateApPerHour: 4,
|
|
692
|
+
demandWeights: {
|
|
693
|
+
activation: 0.05,
|
|
694
|
+
focus: 0.05,
|
|
695
|
+
vigor: 0.15,
|
|
696
|
+
composure: 0.25,
|
|
697
|
+
flow: 0.1
|
|
698
|
+
},
|
|
699
|
+
doubleCountPolicy: "container_only",
|
|
700
|
+
sourceMethod: "seeded",
|
|
701
|
+
costBand: "tiny",
|
|
702
|
+
recoveryEffect: 2,
|
|
703
|
+
metadata: { kind: "holiday", activityPresetKey: "holiday_leisure" },
|
|
704
|
+
createdAt: now,
|
|
705
|
+
updatedAt: now
|
|
706
|
+
}),
|
|
707
|
+
actionProfileSchema.parse({
|
|
708
|
+
id: "profile_movement_trip",
|
|
709
|
+
profileKey: "movement_trip_default",
|
|
710
|
+
title: "Movement trip",
|
|
711
|
+
entityType: "movement_trip",
|
|
712
|
+
mode: "rate",
|
|
713
|
+
startupAp: 0,
|
|
714
|
+
totalCostAp: 8,
|
|
715
|
+
expectedDurationSeconds: 3_600,
|
|
716
|
+
sustainRateApPerHour: 8,
|
|
717
|
+
demandWeights: {
|
|
718
|
+
activation: 0.05,
|
|
719
|
+
focus: 0.15,
|
|
720
|
+
vigor: 0.55,
|
|
721
|
+
composure: 0,
|
|
722
|
+
flow: 0.25
|
|
723
|
+
},
|
|
724
|
+
doubleCountPolicy: "primary_only",
|
|
725
|
+
sourceMethod: "seeded",
|
|
726
|
+
costBand: "light",
|
|
727
|
+
recoveryEffect: 0,
|
|
728
|
+
metadata: {},
|
|
729
|
+
createdAt: now,
|
|
730
|
+
updatedAt: now
|
|
201
731
|
})
|
|
202
732
|
];
|
|
203
733
|
}
|
|
@@ -223,6 +753,17 @@ function mapEntityProfileRow(row, fallback) {
|
|
|
223
753
|
...JSON.parse(row.profile_json)
|
|
224
754
|
});
|
|
225
755
|
}
|
|
756
|
+
function readEntityActionProfileRow(entityType, entityId) {
|
|
757
|
+
return getDatabase()
|
|
758
|
+
.prepare(`SELECT id, entity_type, entity_id, profile_json, created_at, updated_at
|
|
759
|
+
FROM entity_action_profiles
|
|
760
|
+
WHERE entity_type = ? AND entity_id = ?`)
|
|
761
|
+
.get(entityType, entityId);
|
|
762
|
+
}
|
|
763
|
+
export function readEntityActionProfile(entityType, entityId, fallback) {
|
|
764
|
+
const row = readEntityActionProfileRow(entityType, entityId);
|
|
765
|
+
return row ? mapEntityProfileRow(row, fallback) : null;
|
|
766
|
+
}
|
|
226
767
|
function ensureActionProfileTemplates() {
|
|
227
768
|
const database = getDatabase();
|
|
228
769
|
const insert = database.prepare(`INSERT OR IGNORE INTO action_profile_templates (
|
|
@@ -290,6 +831,80 @@ export function upsertTaskActionProfile(input) {
|
|
|
290
831
|
metadata: profile.metadata
|
|
291
832
|
}), now, now);
|
|
292
833
|
}
|
|
834
|
+
export function upsertEntityActionProfile(input) {
|
|
835
|
+
const now = nowIso();
|
|
836
|
+
getDatabase()
|
|
837
|
+
.prepare(`INSERT INTO entity_action_profiles (
|
|
838
|
+
id,
|
|
839
|
+
entity_type,
|
|
840
|
+
entity_id,
|
|
841
|
+
profile_json,
|
|
842
|
+
created_at,
|
|
843
|
+
updated_at
|
|
844
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
845
|
+
ON CONFLICT(entity_type, entity_id) DO UPDATE SET
|
|
846
|
+
profile_json = excluded.profile_json,
|
|
847
|
+
updated_at = excluded.updated_at`)
|
|
848
|
+
.run(input.profile.id, input.entityType, input.entityId, JSON.stringify({
|
|
849
|
+
mode: input.profile.mode,
|
|
850
|
+
startupAp: input.profile.startupAp,
|
|
851
|
+
totalCostAp: input.profile.totalCostAp,
|
|
852
|
+
expectedDurationSeconds: input.profile.expectedDurationSeconds,
|
|
853
|
+
sustainRateApPerHour: input.profile.sustainRateApPerHour,
|
|
854
|
+
demandWeights: input.profile.demandWeights,
|
|
855
|
+
doubleCountPolicy: input.profile.doubleCountPolicy,
|
|
856
|
+
sourceMethod: input.profile.sourceMethod,
|
|
857
|
+
costBand: input.profile.costBand,
|
|
858
|
+
recoveryEffect: input.profile.recoveryEffect,
|
|
859
|
+
metadata: input.profile.metadata
|
|
860
|
+
}), now, now);
|
|
861
|
+
}
|
|
862
|
+
export function buildWorkBlockTemplateActionProfile(input) {
|
|
863
|
+
const durationMinutes = input.endMinute > input.startMinute
|
|
864
|
+
? input.endMinute - input.startMinute
|
|
865
|
+
: 24 * 60 - input.startMinute + input.endMinute;
|
|
866
|
+
const activityPresetKey = input.activityPresetKey ??
|
|
867
|
+
resolveWorkBlockPresetKey(input.kind);
|
|
868
|
+
return buildCalendarActivityProfile({
|
|
869
|
+
entityType: "work_block_template",
|
|
870
|
+
entityId: input.templateId,
|
|
871
|
+
title: input.title,
|
|
872
|
+
expectedDurationSeconds: durationMinutes * 60,
|
|
873
|
+
activityPresetKey,
|
|
874
|
+
customSustainRateApPerHour: input.customSustainRateApPerHour,
|
|
875
|
+
sourceMethod: input.customSustainRateApPerHour !== null &&
|
|
876
|
+
input.customSustainRateApPerHour !== undefined
|
|
877
|
+
? "manual"
|
|
878
|
+
: "inferred",
|
|
879
|
+
metadata: {
|
|
880
|
+
kind: input.kind
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
export function buildCalendarEventActionProfile(input) {
|
|
885
|
+
const activityPresetKey = input.activityPresetKey ??
|
|
886
|
+
resolveCalendarEventPresetKey({
|
|
887
|
+
title: input.title,
|
|
888
|
+
eventType: input.eventType,
|
|
889
|
+
availability: input.availability
|
|
890
|
+
});
|
|
891
|
+
return buildCalendarActivityProfile({
|
|
892
|
+
entityType: "calendar_event",
|
|
893
|
+
entityId: input.eventId,
|
|
894
|
+
title: input.title,
|
|
895
|
+
expectedDurationSeconds: Math.max(60, Math.floor((Date.parse(input.endAt) - Date.parse(input.startAt)) / 1000)),
|
|
896
|
+
activityPresetKey,
|
|
897
|
+
customSustainRateApPerHour: input.customSustainRateApPerHour,
|
|
898
|
+
sourceMethod: input.customSustainRateApPerHour !== null &&
|
|
899
|
+
input.customSustainRateApPerHour !== undefined
|
|
900
|
+
? "manual"
|
|
901
|
+
: "inferred",
|
|
902
|
+
metadata: {
|
|
903
|
+
availability: input.availability,
|
|
904
|
+
eventType: input.eventType ?? ""
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
}
|
|
293
908
|
function ensureLifeForceProfile(userId) {
|
|
294
909
|
const database = getDatabase();
|
|
295
910
|
const existing = database
|
|
@@ -404,14 +1019,7 @@ function readStatXpByKey(userId) {
|
|
|
404
1019
|
}
|
|
405
1020
|
function buildStats(profile, userId) {
|
|
406
1021
|
const xpByKey = readStatXpByKey(userId);
|
|
407
|
-
const levelByKey =
|
|
408
|
-
life_force: profile.life_force_level,
|
|
409
|
-
activation: profile.activation_level,
|
|
410
|
-
focus: profile.focus_level,
|
|
411
|
-
vigor: profile.vigor_level,
|
|
412
|
-
composure: profile.composure_level,
|
|
413
|
-
flow: profile.flow_level
|
|
414
|
-
};
|
|
1022
|
+
const levelByKey = buildStatLevels(profile);
|
|
415
1023
|
return Object.keys(levelByKey).map((key) => {
|
|
416
1024
|
const level = levelByKey[key];
|
|
417
1025
|
return {
|
|
@@ -421,16 +1029,43 @@ function buildStats(profile, userId) {
|
|
|
421
1029
|
xp: xpByKey.get(key) ?? 0,
|
|
422
1030
|
xpToNextLevel: level * 100,
|
|
423
1031
|
costModifier: key === "life_force"
|
|
424
|
-
?
|
|
425
|
-
:
|
|
1032
|
+
? Number(computeLifeForceLevelMultiplier(level).toFixed(3))
|
|
1033
|
+
: Number(computeStatCostModifier(level).toFixed(3))
|
|
426
1034
|
};
|
|
427
1035
|
});
|
|
428
1036
|
}
|
|
429
1037
|
function computeLifeForceMultiplier(profile) {
|
|
430
|
-
return
|
|
1038
|
+
return computeLifeForceLevelMultiplier(profile.life_force_level);
|
|
431
1039
|
}
|
|
432
|
-
function
|
|
433
|
-
|
|
1040
|
+
function readPrimarySleepSessionForDate(userId, date) {
|
|
1041
|
+
const range = buildDayRange(date);
|
|
1042
|
+
const lookback = new Date(range.startMs - 18 * 60 * 60 * 1000).toISOString();
|
|
1043
|
+
try {
|
|
1044
|
+
return getDatabase()
|
|
1045
|
+
.prepare(`SELECT id, started_at, ended_at, asleep_seconds, sleep_score
|
|
1046
|
+
FROM health_sleep_sessions
|
|
1047
|
+
WHERE user_id = ?
|
|
1048
|
+
AND ended_at >= ?
|
|
1049
|
+
AND ended_at < ?
|
|
1050
|
+
ORDER BY asleep_seconds DESC, ended_at DESC
|
|
1051
|
+
LIMIT 1`)
|
|
1052
|
+
.get(userId, lookback, range.to);
|
|
1053
|
+
}
|
|
1054
|
+
catch {
|
|
1055
|
+
return undefined;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function computeSleepRecoveryMultiplier(userId, date) {
|
|
1059
|
+
const session = readPrimarySleepSessionForDate(userId, date);
|
|
1060
|
+
if (!session) {
|
|
1061
|
+
return 1;
|
|
1062
|
+
}
|
|
1063
|
+
const sleepHours = session.asleep_seconds / 3600;
|
|
1064
|
+
const durationFactor = clamp(0.82 + ((sleepHours - 4.5) / 4.5) * 0.22, 0.85, 1.1);
|
|
1065
|
+
const scoreFactor = session.sleep_score === null
|
|
1066
|
+
? 1
|
|
1067
|
+
: clamp(0.92 + (session.sleep_score / 100) * 0.16, 0.9, 1.08);
|
|
1068
|
+
return Number((durationFactor * scoreFactor).toFixed(3));
|
|
434
1069
|
}
|
|
435
1070
|
function computeFatigueDebtCarry(userId, date) {
|
|
436
1071
|
const previous = new Date(date);
|
|
@@ -463,7 +1098,7 @@ function getOrCreateDaySnapshot(userId, date) {
|
|
|
463
1098
|
}
|
|
464
1099
|
const profile = ensureLifeForceProfile(userId);
|
|
465
1100
|
const template = readWeekdayTemplate(userId, date.getUTCDay());
|
|
466
|
-
const sleepRecoveryMultiplier = computeSleepRecoveryMultiplier();
|
|
1101
|
+
const sleepRecoveryMultiplier = computeSleepRecoveryMultiplier(userId, date);
|
|
467
1102
|
const fatigueDebtCarry = computeFatigueDebtCarry(userId, date);
|
|
468
1103
|
const readinessMultiplier = profile.readiness_multiplier;
|
|
469
1104
|
const dailyBudgetAp = Math.max(40, Math.round(profile.base_daily_ap *
|
|
@@ -493,27 +1128,50 @@ function getOrCreateDaySnapshot(userId, date) {
|
|
|
493
1128
|
WHERE id = ?`)
|
|
494
1129
|
.get(id);
|
|
495
1130
|
}
|
|
496
|
-
function resolveTaskActionProfile(task) {
|
|
497
|
-
const row =
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
WHERE entity_type = 'task' AND entity_id = ?`)
|
|
501
|
-
.get(task.id);
|
|
502
|
-
if (!row) {
|
|
503
|
-
return buildDefaultTaskActionProfile({
|
|
1131
|
+
export function resolveTaskActionProfile(task, lifeForceProfile) {
|
|
1132
|
+
const row = readEntityActionProfileRow("task", task.id);
|
|
1133
|
+
const baseProfile = !row
|
|
1134
|
+
? buildDefaultTaskActionProfile({
|
|
504
1135
|
id: `profile_task_${task.id}`,
|
|
505
1136
|
expectedDurationSeconds: task.plannedDurationSeconds
|
|
1137
|
+
})
|
|
1138
|
+
: mapEntityProfileRow(row, {
|
|
1139
|
+
profileKey: `task_${task.id}`,
|
|
1140
|
+
title: "Task",
|
|
1141
|
+
entityType: "task"
|
|
506
1142
|
});
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
title: "Task",
|
|
511
|
-
entityType: "task"
|
|
512
|
-
});
|
|
513
|
-
return {
|
|
514
|
-
...profile,
|
|
515
|
-
expectedDurationSeconds: resolveTaskExpectedDurationSeconds(profile.expectedDurationSeconds ?? task.plannedDurationSeconds)
|
|
1143
|
+
const profile = {
|
|
1144
|
+
...baseProfile,
|
|
1145
|
+
expectedDurationSeconds: resolveTaskExpectedDurationSeconds(baseProfile.expectedDurationSeconds ?? task.plannedDurationSeconds)
|
|
516
1146
|
};
|
|
1147
|
+
return lifeForceProfile ? buildEffectiveProfile(profile, lifeForceProfile) : profile;
|
|
1148
|
+
}
|
|
1149
|
+
export function buildTaskTimeboxActionProfile(input) {
|
|
1150
|
+
const durationSeconds = Math.max(60, Math.floor((Date.parse(input.endsAt) - Date.parse(input.startsAt)) / 1000));
|
|
1151
|
+
const fallbackTaskProfile = resolveTaskActionProfile({
|
|
1152
|
+
id: input.taskId,
|
|
1153
|
+
plannedDurationSeconds: input.taskPlannedDurationSeconds ?? null
|
|
1154
|
+
});
|
|
1155
|
+
const activityPresetKey = input.activityPresetKey ??
|
|
1156
|
+
"task_inherited";
|
|
1157
|
+
return buildCalendarActivityProfile({
|
|
1158
|
+
entityType: "task_timebox",
|
|
1159
|
+
entityId: input.timeboxId,
|
|
1160
|
+
title: input.title,
|
|
1161
|
+
expectedDurationSeconds: durationSeconds,
|
|
1162
|
+
activityPresetKey,
|
|
1163
|
+
customSustainRateApPerHour: input.customSustainRateApPerHour,
|
|
1164
|
+
sourceMethod: input.customSustainRateApPerHour !== null &&
|
|
1165
|
+
input.customSustainRateApPerHour !== undefined
|
|
1166
|
+
? "manual"
|
|
1167
|
+
: activityPresetKey === "task_inherited"
|
|
1168
|
+
? "inferred"
|
|
1169
|
+
: "manual",
|
|
1170
|
+
fallbackProfile: fallbackTaskProfile,
|
|
1171
|
+
metadata: {
|
|
1172
|
+
taskId: input.taskId
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
517
1175
|
}
|
|
518
1176
|
function readTodayAdjustmentRows(userId, range) {
|
|
519
1177
|
return getDatabase()
|
|
@@ -538,17 +1196,18 @@ function readTodayAdjustmentRows(userId, range) {
|
|
|
538
1196
|
AND work_adjustments.created_at < ?`)
|
|
539
1197
|
.all(userId, range.from, range.to);
|
|
540
1198
|
}
|
|
541
|
-
function readTodayAdjustmentApByTaskId(userId, range) {
|
|
1199
|
+
function readTodayAdjustmentApByTaskId(userId, range, lifeForceProfile) {
|
|
542
1200
|
const rows = readTodayAdjustmentRows(userId, range);
|
|
543
1201
|
const totals = new Map();
|
|
544
1202
|
for (const row of rows) {
|
|
545
1203
|
if (row.entity_type !== "task") {
|
|
546
1204
|
continue;
|
|
547
1205
|
}
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
row.
|
|
551
|
-
|
|
1206
|
+
const profile = resolveTaskActionProfile({
|
|
1207
|
+
id: row.entity_id,
|
|
1208
|
+
plannedDurationSeconds: row.planned_duration_seconds
|
|
1209
|
+
}, lifeForceProfile);
|
|
1210
|
+
const deltaAp = rateToTotalAp(profile.sustainRateApPerHour, row.applied_delta_minutes * 60);
|
|
552
1211
|
totals.set(row.entity_id, (totals.get(row.entity_id) ?? 0) + deltaAp);
|
|
553
1212
|
}
|
|
554
1213
|
return totals;
|
|
@@ -587,9 +1246,9 @@ function computeProjectedRemainingSeconds(row, now) {
|
|
|
587
1246
|
const elapsedWallSeconds = Math.max(0, Math.floor((endMs - Date.parse(row.claimed_at)) / 1000));
|
|
588
1247
|
return Math.max(0, row.planned_duration_seconds - elapsedWallSeconds);
|
|
589
1248
|
}
|
|
590
|
-
function buildTaskLifeForceRuntime(task, userId, now = new Date()) {
|
|
1249
|
+
function buildTaskLifeForceRuntime(task, userId, now = new Date(), lifeForceProfile = ensureLifeForceProfile(userId)) {
|
|
591
1250
|
const range = buildDayRange(now);
|
|
592
|
-
const profile = resolveTaskActionProfile(task);
|
|
1251
|
+
const profile = resolveTaskActionProfile(task, lifeForceProfile);
|
|
593
1252
|
const todayRunSeconds = readTaskRunRows(range, userId)
|
|
594
1253
|
.filter((row) => row.task_id === task.id)
|
|
595
1254
|
.reduce((sum, row) => sum + overlapSeconds(range, row, now), 0);
|
|
@@ -624,14 +1283,15 @@ function readTaskRunWindowsByTaskId(userId, range, now) {
|
|
|
624
1283
|
}
|
|
625
1284
|
return windows;
|
|
626
1285
|
}
|
|
627
|
-
function buildWorkAdjustmentContributions(userId, range) {
|
|
1286
|
+
function buildWorkAdjustmentContributions(userId, range, lifeForceProfile) {
|
|
628
1287
|
return readTodayAdjustmentRows(userId, range)
|
|
629
1288
|
.filter((row) => row.entity_type === "task")
|
|
630
1289
|
.map((row) => {
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
row.
|
|
634
|
-
|
|
1290
|
+
const profile = resolveTaskActionProfile({
|
|
1291
|
+
id: row.entity_id,
|
|
1292
|
+
plannedDurationSeconds: row.planned_duration_seconds
|
|
1293
|
+
}, lifeForceProfile);
|
|
1294
|
+
const totalAp = rateToTotalAp(profile.sustainRateApPerHour, row.applied_delta_minutes * 60);
|
|
635
1295
|
return {
|
|
636
1296
|
entityType: "task",
|
|
637
1297
|
entityId: row.entity_id,
|
|
@@ -651,7 +1311,7 @@ function buildWorkAdjustmentContributions(userId, range) {
|
|
|
651
1311
|
};
|
|
652
1312
|
});
|
|
653
1313
|
}
|
|
654
|
-
function buildTaskRunContributions(userId, range, now) {
|
|
1314
|
+
function buildTaskRunContributions(userId, range, now, lifeForceProfile) {
|
|
655
1315
|
const contributions = [];
|
|
656
1316
|
const totalsByTaskId = new Map();
|
|
657
1317
|
const activeDrains = [];
|
|
@@ -663,8 +1323,8 @@ function buildTaskRunContributions(userId, range, now) {
|
|
|
663
1323
|
const profile = resolveTaskActionProfile({
|
|
664
1324
|
id: row.task_id,
|
|
665
1325
|
plannedDurationSeconds: row.task_expected_duration_seconds ?? row.planned_duration_seconds
|
|
666
|
-
});
|
|
667
|
-
const totalAp = (
|
|
1326
|
+
}, lifeForceProfile);
|
|
1327
|
+
const totalAp = rateToTotalAp(profile.sustainRateApPerHour, seconds);
|
|
668
1328
|
const startsAt = new Date(Math.max(range.startMs, Date.parse(row.claimed_at))).toISOString();
|
|
669
1329
|
const endsAt = new Date(Math.min(range.endMs, terminalRunMs(row, now))).toISOString();
|
|
670
1330
|
const contribution = {
|
|
@@ -695,8 +1355,9 @@ function buildTaskRunContributions(userId, range, now) {
|
|
|
695
1355
|
}
|
|
696
1356
|
return { contributions, totalsByTaskId, activeDrains };
|
|
697
1357
|
}
|
|
698
|
-
function buildNoteContributions(userId, range, now) {
|
|
1358
|
+
function buildNoteContributions(userId, range, now, lifeForceProfile) {
|
|
699
1359
|
try {
|
|
1360
|
+
const noteProfile = buildEffectiveProfile(seededActionProfiles().find((entry) => entry.profileKey === "note_quick"), lifeForceProfile);
|
|
700
1361
|
const taskRunWindowsByTaskId = readTaskRunWindowsByTaskId(userId, range, now);
|
|
701
1362
|
const rows = getDatabase()
|
|
702
1363
|
.prepare(`SELECT
|
|
@@ -734,7 +1395,7 @@ function buildNoteContributions(userId, range, now) {
|
|
|
734
1395
|
entityId: row.id,
|
|
735
1396
|
eventKind: "note_created",
|
|
736
1397
|
sourceKind: "note",
|
|
737
|
-
totalAp:
|
|
1398
|
+
totalAp: noteProfile.totalCostAp,
|
|
738
1399
|
rateApPerHour: null,
|
|
739
1400
|
title: row.title || "Note",
|
|
740
1401
|
why: "Standalone capture takes a small impulse of activation and focus.",
|
|
@@ -747,8 +1408,9 @@ function buildNoteContributions(userId, range, now) {
|
|
|
747
1408
|
return [];
|
|
748
1409
|
}
|
|
749
1410
|
}
|
|
750
|
-
function buildHabitContributions(userId, range) {
|
|
1411
|
+
function buildHabitContributions(userId, range, lifeForceProfile) {
|
|
751
1412
|
try {
|
|
1413
|
+
const habitProfile = buildEffectiveProfile(seededActionProfiles().find((entry) => entry.profileKey === "habit_default"), lifeForceProfile);
|
|
752
1414
|
const rows = getDatabase()
|
|
753
1415
|
.prepare(`SELECT
|
|
754
1416
|
habits.id,
|
|
@@ -774,7 +1436,7 @@ function buildHabitContributions(userId, range) {
|
|
|
774
1436
|
entityId: row.id,
|
|
775
1437
|
eventKind: "habit_check_in",
|
|
776
1438
|
sourceKind: "habit",
|
|
777
|
-
totalAp:
|
|
1439
|
+
totalAp: habitProfile.totalCostAp,
|
|
778
1440
|
rateApPerHour: null,
|
|
779
1441
|
title: row.title,
|
|
780
1442
|
why: "Habit execution still costs activation even when the action is short.",
|
|
@@ -787,7 +1449,7 @@ function buildHabitContributions(userId, range) {
|
|
|
787
1449
|
return [];
|
|
788
1450
|
}
|
|
789
1451
|
}
|
|
790
|
-
function buildWorkoutContributions(userId, range, now) {
|
|
1452
|
+
function buildWorkoutContributions(userId, range, now, lifeForceProfile) {
|
|
791
1453
|
let rows = [];
|
|
792
1454
|
try {
|
|
793
1455
|
rows = getDatabase()
|
|
@@ -803,6 +1465,7 @@ function buildWorkoutContributions(userId, range, now) {
|
|
|
803
1465
|
}
|
|
804
1466
|
const contributions = [];
|
|
805
1467
|
const activeDrains = [];
|
|
1468
|
+
const workoutProfile = buildEffectiveProfile(seededActionProfiles().find((entry) => entry.profileKey === "workout_default"), lifeForceProfile);
|
|
806
1469
|
for (const row of rows) {
|
|
807
1470
|
const startMs = Math.max(range.startMs, Date.parse(row.started_at));
|
|
808
1471
|
const endMs = Math.min(range.endMs, Date.parse(row.ended_at));
|
|
@@ -813,13 +1476,13 @@ function buildWorkoutContributions(userId, range, now) {
|
|
|
813
1476
|
const effortMultiplier = row.subjective_effort
|
|
814
1477
|
? clamp(row.subjective_effort / 6, 0.8, 1.6)
|
|
815
1478
|
: 1;
|
|
816
|
-
const rateApPerHour =
|
|
1479
|
+
const rateApPerHour = Number((workoutProfile.sustainRateApPerHour * effortMultiplier).toFixed(4));
|
|
817
1480
|
const contribution = {
|
|
818
1481
|
entityType: "workout_session",
|
|
819
1482
|
entityId: row.id,
|
|
820
1483
|
eventKind: "workout_session",
|
|
821
1484
|
sourceKind: "workout",
|
|
822
|
-
totalAp: (seconds
|
|
1485
|
+
totalAp: rateToTotalAp(rateApPerHour, seconds),
|
|
823
1486
|
rateApPerHour,
|
|
824
1487
|
title: row.workout_type,
|
|
825
1488
|
why: "Workout sessions consume real physical capacity and should affect current load.",
|
|
@@ -828,48 +1491,530 @@ function buildWorkoutContributions(userId, range, now) {
|
|
|
828
1491
|
role: "secondary"
|
|
829
1492
|
};
|
|
830
1493
|
contributions.push(contribution);
|
|
831
|
-
if (Date.parse(row.ended_at) > now.getTime()) {
|
|
1494
|
+
if (Date.parse(row.started_at) <= now.getTime() && Date.parse(row.ended_at) > now.getTime()) {
|
|
832
1495
|
activeDrains.push({ ...contribution, totalAp: 0 });
|
|
833
1496
|
}
|
|
834
1497
|
}
|
|
835
1498
|
return { contributions, activeDrains };
|
|
836
1499
|
}
|
|
837
|
-
function
|
|
838
|
-
const
|
|
1500
|
+
function buildWakeImpulseContributions(userId, range, lifeForceProfile) {
|
|
1501
|
+
const primarySleep = readPrimarySleepSessionForDate(userId, new Date(range.startMs));
|
|
1502
|
+
if (!primarySleep) {
|
|
1503
|
+
return [];
|
|
1504
|
+
}
|
|
1505
|
+
const endedAtMs = Date.parse(primarySleep.ended_at);
|
|
1506
|
+
if (endedAtMs < range.startMs || endedAtMs >= range.endMs) {
|
|
1507
|
+
return [];
|
|
1508
|
+
}
|
|
1509
|
+
const wakeProfile = buildEffectiveProfile(seededActionProfiles().find((entry) => entry.profileKey === "wake_start"), lifeForceProfile);
|
|
1510
|
+
return [
|
|
1511
|
+
{
|
|
1512
|
+
entityType: "sleep_session",
|
|
1513
|
+
entityId: primarySleep.id,
|
|
1514
|
+
eventKind: "wake_start",
|
|
1515
|
+
sourceKind: "wake",
|
|
1516
|
+
totalAp: wakeProfile.totalCostAp,
|
|
1517
|
+
rateApPerHour: null,
|
|
1518
|
+
title: "Get out of bed",
|
|
1519
|
+
why: "Starting the day takes real activation and should count as an Action Point impulse.",
|
|
1520
|
+
startsAt: primarySleep.ended_at,
|
|
1521
|
+
endsAt: primarySleep.ended_at,
|
|
1522
|
+
role: "background"
|
|
1523
|
+
}
|
|
1524
|
+
];
|
|
1525
|
+
}
|
|
1526
|
+
function buildMovementTripProfile(trip) {
|
|
1527
|
+
const baseProfile = seededActionProfiles().find((entry) => entry.profileKey === "movement_trip_default");
|
|
1528
|
+
const expectedMet = trip.expected_met ?? 2;
|
|
1529
|
+
const baseRateApPerHour = clamp(expectedMet * 4, 4, 22);
|
|
1530
|
+
const lowerTitle = `${trip.activity_type} ${trip.travel_mode}`.toLowerCase();
|
|
1531
|
+
const vigor = lowerTitle.includes("walk") || lowerTitle.includes("run") || lowerTitle.includes("bike")
|
|
1532
|
+
? 0.65
|
|
1533
|
+
: lowerTitle.includes("drive") || lowerTitle.includes("train")
|
|
1534
|
+
? 0.2
|
|
1535
|
+
: 0.45;
|
|
1536
|
+
const focus = lowerTitle.includes("drive") ? 0.35 : 0.15;
|
|
1537
|
+
const flow = 1 - vigor - focus;
|
|
1538
|
+
return actionProfileSchema.parse({
|
|
1539
|
+
...baseProfile,
|
|
1540
|
+
id: `${baseProfile.id}_${trip.travel_mode}_${trip.activity_type || "travel"}`,
|
|
1541
|
+
profileKey: `${baseProfile.profileKey}_${trip.travel_mode}_${trip.activity_type || "travel"}`,
|
|
1542
|
+
title: trip.activity_type?.trim() || trip.travel_mode || "Movement trip",
|
|
1543
|
+
sustainRateApPerHour: Number(baseRateApPerHour.toFixed(4)),
|
|
1544
|
+
totalCostAp: Number(baseRateApPerHour.toFixed(4)),
|
|
1545
|
+
demandWeights: {
|
|
1546
|
+
activation: 0.05,
|
|
1547
|
+
focus,
|
|
1548
|
+
vigor,
|
|
1549
|
+
composure: 0,
|
|
1550
|
+
flow: Math.max(0.05, Number(flow.toFixed(3)))
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
function buildMovementTripContributions(userId, range, now, lifeForceProfile) {
|
|
1555
|
+
let rows = [];
|
|
839
1556
|
try {
|
|
840
|
-
|
|
841
|
-
.prepare(`SELECT
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1557
|
+
rows = getDatabase()
|
|
1558
|
+
.prepare(`SELECT
|
|
1559
|
+
id,
|
|
1560
|
+
label,
|
|
1561
|
+
status,
|
|
1562
|
+
travel_mode,
|
|
1563
|
+
activity_type,
|
|
1564
|
+
started_at,
|
|
1565
|
+
ended_at,
|
|
1566
|
+
moving_seconds,
|
|
1567
|
+
idle_seconds,
|
|
1568
|
+
expected_met,
|
|
1569
|
+
distance_meters
|
|
1570
|
+
FROM movement_trips
|
|
1571
|
+
WHERE user_id = ?
|
|
1572
|
+
AND started_at < ?
|
|
1573
|
+
AND ended_at >= ?`)
|
|
1574
|
+
.all(userId, range.to, range.from);
|
|
1575
|
+
}
|
|
1576
|
+
catch {
|
|
1577
|
+
rows = [];
|
|
1578
|
+
}
|
|
1579
|
+
const contributions = [];
|
|
1580
|
+
const activeDrains = [];
|
|
1581
|
+
for (const row of rows) {
|
|
1582
|
+
const seconds = Math.max(0, Math.floor((Math.min(range.endMs, Date.parse(row.ended_at)) -
|
|
1583
|
+
Math.max(range.startMs, Date.parse(row.started_at))) /
|
|
1584
|
+
1000));
|
|
1585
|
+
if (seconds <= 0) {
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
const profile = buildEffectiveProfile(buildMovementTripProfile(row), lifeForceProfile);
|
|
1589
|
+
const contribution = {
|
|
1590
|
+
entityType: "movement_trip",
|
|
1591
|
+
entityId: row.id,
|
|
1592
|
+
eventKind: "movement_trip",
|
|
1593
|
+
sourceKind: "movement",
|
|
1594
|
+
totalAp: rateToTotalAp(profile.sustainRateApPerHour, seconds),
|
|
1595
|
+
rateApPerHour: profile.sustainRateApPerHour,
|
|
1596
|
+
title: row.label || row.activity_type || row.travel_mode || "Movement trip",
|
|
1597
|
+
why: "Movement and commuting consume current capacity through physical effort, attention, and switching overhead.",
|
|
1598
|
+
startsAt: new Date(Math.max(range.startMs, Date.parse(row.started_at))).toISOString(),
|
|
1599
|
+
endsAt: new Date(Math.min(range.endMs, Date.parse(row.ended_at))).toISOString(),
|
|
1600
|
+
role: "secondary"
|
|
1601
|
+
};
|
|
1602
|
+
contributions.push(contribution);
|
|
1603
|
+
if (Date.parse(row.started_at) <= now.getTime() && Date.parse(row.ended_at) > now.getTime()) {
|
|
1604
|
+
activeDrains.push({ ...contribution, totalAp: 0 });
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return { contributions, activeDrains };
|
|
1608
|
+
}
|
|
1609
|
+
function readTaskTimeboxLifeForceRows(userId, range) {
|
|
1610
|
+
try {
|
|
1611
|
+
return getDatabase()
|
|
1612
|
+
.prepare(`SELECT
|
|
1613
|
+
task_timeboxes.id,
|
|
1614
|
+
task_timeboxes.task_id,
|
|
1615
|
+
task_timeboxes.linked_task_run_id,
|
|
1616
|
+
task_timeboxes.status,
|
|
1617
|
+
task_timeboxes.source,
|
|
1618
|
+
task_timeboxes.title,
|
|
1619
|
+
task_timeboxes.starts_at,
|
|
1620
|
+
task_timeboxes.ends_at,
|
|
1621
|
+
tasks.planned_duration_seconds AS task_planned_duration_seconds
|
|
1622
|
+
FROM task_timeboxes
|
|
1623
|
+
INNER JOIN tasks ON tasks.id = task_timeboxes.task_id
|
|
845
1624
|
INNER JOIN entity_owners
|
|
846
|
-
ON entity_owners.entity_type =
|
|
847
|
-
AND entity_owners.entity_id =
|
|
1625
|
+
ON entity_owners.entity_type = 'task_timebox'
|
|
1626
|
+
AND entity_owners.entity_id = task_timeboxes.id
|
|
848
1627
|
AND entity_owners.role = 'owner'
|
|
849
1628
|
WHERE entity_owners.user_id = ?
|
|
850
|
-
AND
|
|
851
|
-
AND
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1629
|
+
AND task_timeboxes.ends_at > ?
|
|
1630
|
+
AND task_timeboxes.starts_at < ?
|
|
1631
|
+
ORDER BY task_timeboxes.starts_at ASC`)
|
|
1632
|
+
.all(userId, range.from, range.to);
|
|
1633
|
+
}
|
|
1634
|
+
catch {
|
|
1635
|
+
return [];
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
function readWorkBlockTemplateLifeForceRows(userId) {
|
|
1639
|
+
try {
|
|
1640
|
+
return getDatabase()
|
|
1641
|
+
.prepare(`SELECT
|
|
1642
|
+
work_block_templates.id,
|
|
1643
|
+
work_block_templates.title,
|
|
1644
|
+
work_block_templates.kind,
|
|
1645
|
+
work_block_templates.color,
|
|
1646
|
+
work_block_templates.weekdays_json,
|
|
1647
|
+
work_block_templates.start_minute,
|
|
1648
|
+
work_block_templates.end_minute,
|
|
1649
|
+
work_block_templates.starts_on,
|
|
1650
|
+
work_block_templates.ends_on,
|
|
1651
|
+
work_block_templates.blocking_state,
|
|
1652
|
+
work_block_templates.created_at,
|
|
1653
|
+
work_block_templates.updated_at
|
|
1654
|
+
FROM work_block_templates
|
|
1655
|
+
INNER JOIN entity_owners
|
|
1656
|
+
ON entity_owners.entity_type = 'work_block_template'
|
|
1657
|
+
AND entity_owners.entity_id = work_block_templates.id
|
|
1658
|
+
AND entity_owners.role = 'owner'
|
|
1659
|
+
WHERE entity_owners.user_id = ?`)
|
|
1660
|
+
.all(userId);
|
|
1661
|
+
}
|
|
1662
|
+
catch {
|
|
1663
|
+
return [];
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function parseWeekdaysJson(raw) {
|
|
1667
|
+
try {
|
|
1668
|
+
const parsed = JSON.parse(raw);
|
|
1669
|
+
return Array.isArray(parsed)
|
|
1670
|
+
? parsed.filter((entry) => Number.isInteger(entry))
|
|
1671
|
+
: [];
|
|
868
1672
|
}
|
|
869
1673
|
catch {
|
|
870
1674
|
return [];
|
|
871
1675
|
}
|
|
872
1676
|
}
|
|
1677
|
+
function deriveTodayWorkBlocks(userId, range) {
|
|
1678
|
+
const dayDate = new Date(range.startMs);
|
|
1679
|
+
const dayKey = range.dateKey;
|
|
1680
|
+
return readWorkBlockTemplateLifeForceRows(userId)
|
|
1681
|
+
.filter((template) => {
|
|
1682
|
+
if (template.starts_on && dayKey < template.starts_on) {
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
if (template.ends_on && dayKey > template.ends_on) {
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
return parseWeekdaysJson(template.weekdays_json).includes(dayDate.getUTCDay());
|
|
1689
|
+
})
|
|
1690
|
+
.map((template) => {
|
|
1691
|
+
const startAt = new Date(range.startMs + template.start_minute * 60_000).toISOString();
|
|
1692
|
+
const endAt = new Date(range.startMs + template.end_minute * 60_000).toISOString();
|
|
1693
|
+
return {
|
|
1694
|
+
...template,
|
|
1695
|
+
instance_id: `wbinst_${template.id}_${dayKey}`,
|
|
1696
|
+
start_at: startAt,
|
|
1697
|
+
end_at: endAt
|
|
1698
|
+
};
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
function buildWorkBlockProfile(input) {
|
|
1702
|
+
const storedProfile = readEntityActionProfile("work_block_template", input.templateId, {
|
|
1703
|
+
profileKey: `work_block_template_${input.templateId}`,
|
|
1704
|
+
title: input.title,
|
|
1705
|
+
entityType: "work_block_template"
|
|
1706
|
+
});
|
|
1707
|
+
if (storedProfile) {
|
|
1708
|
+
return storedProfile;
|
|
1709
|
+
}
|
|
1710
|
+
const startMinute = new Date(input.startAt).getUTCHours() * 60 + new Date(input.startAt).getUTCMinutes();
|
|
1711
|
+
const endMinute = new Date(input.endAt).getUTCHours() * 60 + new Date(input.endAt).getUTCMinutes();
|
|
1712
|
+
return buildWorkBlockTemplateActionProfile({
|
|
1713
|
+
templateId: input.templateId,
|
|
1714
|
+
title: input.title,
|
|
1715
|
+
kind: input.kind,
|
|
1716
|
+
startMinute,
|
|
1717
|
+
endMinute
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, activeTaskRunTaskIds, actualSourceWindows) {
|
|
1721
|
+
const actualContributions = [];
|
|
1722
|
+
const plannedDrains = [];
|
|
1723
|
+
const activeDrains = [];
|
|
1724
|
+
const timeboxWindows = [];
|
|
1725
|
+
const workBlockWindows = [];
|
|
1726
|
+
const timeboxes = readTaskTimeboxLifeForceRows(userId, range);
|
|
1727
|
+
for (const row of timeboxes) {
|
|
1728
|
+
if (row.linked_task_run_id || row.status === "cancelled") {
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
timeboxWindows.push({
|
|
1732
|
+
startAt: row.starts_at,
|
|
1733
|
+
endAt: row.ends_at
|
|
1734
|
+
});
|
|
1735
|
+
const storedProfile = readEntityActionProfile("task_timebox", row.id, {
|
|
1736
|
+
profileKey: `task_timebox_${row.id}`,
|
|
1737
|
+
title: row.title,
|
|
1738
|
+
entityType: "task_timebox"
|
|
1739
|
+
});
|
|
1740
|
+
const profile = lifeForceProfile
|
|
1741
|
+
? buildEffectiveProfile(storedProfile ??
|
|
1742
|
+
buildTaskTimeboxActionProfile({
|
|
1743
|
+
timeboxId: row.id,
|
|
1744
|
+
title: row.title,
|
|
1745
|
+
taskId: row.task_id,
|
|
1746
|
+
taskPlannedDurationSeconds: row.task_planned_duration_seconds,
|
|
1747
|
+
startsAt: row.starts_at,
|
|
1748
|
+
endsAt: row.ends_at
|
|
1749
|
+
}), lifeForceProfile)
|
|
1750
|
+
: storedProfile ??
|
|
1751
|
+
buildTaskTimeboxActionProfile({
|
|
1752
|
+
timeboxId: row.id,
|
|
1753
|
+
title: row.title,
|
|
1754
|
+
taskId: row.task_id,
|
|
1755
|
+
taskPlannedDurationSeconds: row.task_planned_duration_seconds,
|
|
1756
|
+
startsAt: row.starts_at,
|
|
1757
|
+
endsAt: row.ends_at
|
|
1758
|
+
});
|
|
1759
|
+
const elapsedWindow = {
|
|
1760
|
+
startAt: row.starts_at,
|
|
1761
|
+
endAt: new Date(Math.min(now.getTime(), Date.parse(row.ends_at))).toISOString()
|
|
1762
|
+
};
|
|
1763
|
+
const elapsedSeconds = computeUncoveredSeconds(elapsedWindow, actualSourceWindows);
|
|
1764
|
+
if (elapsedSeconds > 0) {
|
|
1765
|
+
actualContributions.push({
|
|
1766
|
+
entityType: "task_timebox",
|
|
1767
|
+
entityId: row.id,
|
|
1768
|
+
eventKind: "task_timebox_actual",
|
|
1769
|
+
sourceKind: "task_timebox",
|
|
1770
|
+
totalAp: rateToTotalAp(profile.sustainRateApPerHour, elapsedSeconds),
|
|
1771
|
+
rateApPerHour: profile.sustainRateApPerHour,
|
|
1772
|
+
title: row.title,
|
|
1773
|
+
why: "Elapsed timeboxes count toward today's Action Point spend when no richer timed source covered that window.",
|
|
1774
|
+
startsAt: row.starts_at,
|
|
1775
|
+
endsAt: elapsedWindow.endAt,
|
|
1776
|
+
role: "background"
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
const remainingStartMs = Math.max(now.getTime(), Date.parse(row.starts_at));
|
|
1780
|
+
const remainingEndMs = Math.min(range.endMs, Date.parse(row.ends_at));
|
|
1781
|
+
const remainingSeconds = Math.max(0, Math.floor((remainingEndMs - remainingStartMs) / 1000));
|
|
1782
|
+
if (remainingSeconds > 0) {
|
|
1783
|
+
plannedDrains.push({
|
|
1784
|
+
entityType: "task_timebox",
|
|
1785
|
+
entityId: row.id,
|
|
1786
|
+
eventKind: "task_timebox_plan",
|
|
1787
|
+
sourceKind: "task_timebox",
|
|
1788
|
+
totalAp: rateToTotalAp(profile.sustainRateApPerHour, remainingSeconds),
|
|
1789
|
+
rateApPerHour: profile.sustainRateApPerHour,
|
|
1790
|
+
title: row.title,
|
|
1791
|
+
why: "Planned task timeboxes forecast how much Action Point throughput is still booked today.",
|
|
1792
|
+
startsAt: row.starts_at,
|
|
1793
|
+
endsAt: row.ends_at,
|
|
1794
|
+
role: "secondary"
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
if (Date.parse(row.starts_at) <= now.getTime() &&
|
|
1798
|
+
Date.parse(row.ends_at) > now.getTime() &&
|
|
1799
|
+
!activeTaskRunTaskIds.has(row.task_id) &&
|
|
1800
|
+
!actualSourceWindows.some((window) => overlapsWindow(row.starts_at, row.ends_at, window.startAt, window.endAt))) {
|
|
1801
|
+
activeDrains.push({
|
|
1802
|
+
entityType: "task_timebox",
|
|
1803
|
+
entityId: row.id,
|
|
1804
|
+
eventKind: "task_timebox_context",
|
|
1805
|
+
sourceKind: "task_timebox",
|
|
1806
|
+
totalAp: 0,
|
|
1807
|
+
rateApPerHour: profile.sustainRateApPerHour,
|
|
1808
|
+
title: row.title,
|
|
1809
|
+
why: "An active timebox still occupies current capacity even before live work logging starts.",
|
|
1810
|
+
startsAt: row.starts_at,
|
|
1811
|
+
endsAt: row.ends_at,
|
|
1812
|
+
role: "background"
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
const workBlocks = deriveTodayWorkBlocks(userId, range);
|
|
1817
|
+
for (const block of workBlocks) {
|
|
1818
|
+
workBlockWindows.push({
|
|
1819
|
+
startAt: block.start_at,
|
|
1820
|
+
endAt: block.end_at
|
|
1821
|
+
});
|
|
1822
|
+
const overlapsTimebox = timeboxes.some((timebox) => timebox.status !== "cancelled" &&
|
|
1823
|
+
overlapsWindow(block.start_at, block.end_at, timebox.starts_at, timebox.ends_at));
|
|
1824
|
+
const profile = buildEffectiveProfile(buildWorkBlockProfile({
|
|
1825
|
+
templateId: block.id,
|
|
1826
|
+
title: block.title,
|
|
1827
|
+
kind: block.kind,
|
|
1828
|
+
startAt: block.start_at,
|
|
1829
|
+
endAt: block.end_at
|
|
1830
|
+
}), lifeForceProfile);
|
|
1831
|
+
const elapsedWindow = {
|
|
1832
|
+
startAt: block.start_at,
|
|
1833
|
+
endAt: new Date(Math.min(now.getTime(), Date.parse(block.end_at))).toISOString()
|
|
1834
|
+
};
|
|
1835
|
+
const elapsedSeconds = computeUncoveredSeconds(elapsedWindow, [
|
|
1836
|
+
...actualSourceWindows,
|
|
1837
|
+
...timeboxWindows
|
|
1838
|
+
]);
|
|
1839
|
+
if (elapsedSeconds > 0 && !overlapsTimebox) {
|
|
1840
|
+
actualContributions.push({
|
|
1841
|
+
entityType: "work_block",
|
|
1842
|
+
entityId: block.instance_id,
|
|
1843
|
+
eventKind: "work_block_actual",
|
|
1844
|
+
sourceKind: "work_block",
|
|
1845
|
+
totalAp: rateToTotalAp(profile.sustainRateApPerHour, elapsedSeconds),
|
|
1846
|
+
rateApPerHour: profile.sustainRateApPerHour,
|
|
1847
|
+
title: block.title,
|
|
1848
|
+
why: "Elapsed work blocks count toward today's spend when no specific task run or timebox covered that work window.",
|
|
1849
|
+
startsAt: block.start_at,
|
|
1850
|
+
endsAt: elapsedWindow.endAt,
|
|
1851
|
+
role: "background",
|
|
1852
|
+
metadata: {
|
|
1853
|
+
templateId: block.id,
|
|
1854
|
+
kind: block.kind
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
const remainingStartMs = Math.max(now.getTime(), Date.parse(block.start_at));
|
|
1859
|
+
const remainingEndMs = Math.min(range.endMs, Date.parse(block.end_at));
|
|
1860
|
+
const remainingSeconds = Math.max(0, Math.floor((remainingEndMs - remainingStartMs) / 1000));
|
|
1861
|
+
if (remainingSeconds > 0 && !overlapsTimebox) {
|
|
1862
|
+
plannedDrains.push({
|
|
1863
|
+
entityType: "work_block",
|
|
1864
|
+
entityId: block.instance_id,
|
|
1865
|
+
eventKind: "work_block_plan",
|
|
1866
|
+
sourceKind: "work_block",
|
|
1867
|
+
totalAp: rateToTotalAp(profile.sustainRateApPerHour, remainingSeconds),
|
|
1868
|
+
rateApPerHour: profile.sustainRateApPerHour,
|
|
1869
|
+
title: block.title,
|
|
1870
|
+
why: "Work blocks act as planning containers and forecast background load when no richer task plan exists.",
|
|
1871
|
+
startsAt: block.start_at,
|
|
1872
|
+
endsAt: block.end_at,
|
|
1873
|
+
role: "background",
|
|
1874
|
+
metadata: {
|
|
1875
|
+
templateId: block.id,
|
|
1876
|
+
kind: block.kind
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
if (Date.parse(block.start_at) <= now.getTime() &&
|
|
1881
|
+
Date.parse(block.end_at) > now.getTime() &&
|
|
1882
|
+
!overlapsTimebox &&
|
|
1883
|
+
activeTaskRunTaskIds.size === 0 &&
|
|
1884
|
+
!actualSourceWindows.some((window) => overlapsWindow(block.start_at, block.end_at, window.startAt, window.endAt))) {
|
|
1885
|
+
activeDrains.push({
|
|
1886
|
+
entityType: "work_block",
|
|
1887
|
+
entityId: block.instance_id,
|
|
1888
|
+
eventKind: "work_block_context",
|
|
1889
|
+
sourceKind: "work_block",
|
|
1890
|
+
totalAp: 0,
|
|
1891
|
+
rateApPerHour: profile.sustainRateApPerHour,
|
|
1892
|
+
title: block.title,
|
|
1893
|
+
why: "The current work block still claims capacity as a container for focused effort.",
|
|
1894
|
+
startsAt: block.start_at,
|
|
1895
|
+
endsAt: block.end_at,
|
|
1896
|
+
role: "background",
|
|
1897
|
+
metadata: {
|
|
1898
|
+
templateId: block.id,
|
|
1899
|
+
kind: block.kind
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return {
|
|
1905
|
+
actualContributions,
|
|
1906
|
+
plannedDrains,
|
|
1907
|
+
activeDrains,
|
|
1908
|
+
timeboxWindows,
|
|
1909
|
+
workBlockWindows
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
function buildCalendarDrains(userId, now, range, lifeForceProfile, blockingWindows) {
|
|
1913
|
+
const actualContributions = [];
|
|
1914
|
+
const nowIsoValue = now.toISOString();
|
|
1915
|
+
const activeDrains = [];
|
|
1916
|
+
const plannedDrains = [];
|
|
1917
|
+
try {
|
|
1918
|
+
const rows = getDatabase()
|
|
1919
|
+
.prepare(`SELECT
|
|
1920
|
+
forge_events.id,
|
|
1921
|
+
forge_events.title,
|
|
1922
|
+
forge_events.start_at,
|
|
1923
|
+
forge_events.end_at,
|
|
1924
|
+
forge_events.availability,
|
|
1925
|
+
forge_events.event_type,
|
|
1926
|
+
COUNT(forge_event_links.id) AS link_count
|
|
1927
|
+
FROM forge_events
|
|
1928
|
+
LEFT JOIN forge_event_links
|
|
1929
|
+
ON forge_event_links.forge_event_id = forge_events.id
|
|
1930
|
+
WHERE forge_events.deleted_at IS NULL
|
|
1931
|
+
AND forge_events.end_at > ?
|
|
1932
|
+
AND forge_events.start_at < ?
|
|
1933
|
+
GROUP BY
|
|
1934
|
+
forge_events.id,
|
|
1935
|
+
forge_events.title,
|
|
1936
|
+
forge_events.start_at,
|
|
1937
|
+
forge_events.end_at,
|
|
1938
|
+
forge_events.availability,
|
|
1939
|
+
forge_events.event_type`)
|
|
1940
|
+
.all(range.from, range.to);
|
|
1941
|
+
for (const row of rows) {
|
|
1942
|
+
const calendarProfile = buildEffectiveProfile(readEntityActionProfile("calendar_event", row.id, {
|
|
1943
|
+
profileKey: `calendar_event_${row.id}`,
|
|
1944
|
+
title: row.title,
|
|
1945
|
+
entityType: "calendar_event"
|
|
1946
|
+
}) ??
|
|
1947
|
+
buildCalendarEventActionProfile({
|
|
1948
|
+
eventId: row.id,
|
|
1949
|
+
title: row.title,
|
|
1950
|
+
eventType: row.event_type,
|
|
1951
|
+
availability: row.availability,
|
|
1952
|
+
startAt: row.start_at,
|
|
1953
|
+
endAt: row.end_at
|
|
1954
|
+
}), lifeForceProfile);
|
|
1955
|
+
const overlapsBlockingWindow = blockingWindows.some((window) => overlapsWindow(row.start_at, row.end_at, window.startAt, window.endAt));
|
|
1956
|
+
const elapsedWindow = {
|
|
1957
|
+
startAt: row.start_at,
|
|
1958
|
+
endAt: new Date(Math.min(now.getTime(), Date.parse(row.end_at))).toISOString()
|
|
1959
|
+
};
|
|
1960
|
+
const elapsedSeconds = computeUncoveredSeconds(elapsedWindow, blockingWindows);
|
|
1961
|
+
if (elapsedSeconds > 0 && row.link_count === 0) {
|
|
1962
|
+
actualContributions.push({
|
|
1963
|
+
entityType: "calendar_event",
|
|
1964
|
+
entityId: row.id,
|
|
1965
|
+
eventKind: "calendar_event_actual",
|
|
1966
|
+
sourceKind: "calendar",
|
|
1967
|
+
totalAp: rateToTotalAp(calendarProfile.sustainRateApPerHour, elapsedSeconds),
|
|
1968
|
+
rateApPerHour: calendarProfile.sustainRateApPerHour,
|
|
1969
|
+
title: row.title,
|
|
1970
|
+
why: "Busy calendar events debit today's AP when they were real containers and nothing richer occupied the same window.",
|
|
1971
|
+
startsAt: row.start_at,
|
|
1972
|
+
endsAt: elapsedWindow.endAt,
|
|
1973
|
+
role: "background"
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
const remainingStartMs = Math.max(now.getTime(), Date.parse(row.start_at));
|
|
1977
|
+
const remainingEndMs = Math.min(range.endMs, Date.parse(row.end_at));
|
|
1978
|
+
const remainingSeconds = Math.max(0, Math.floor((remainingEndMs - remainingStartMs) / 1000));
|
|
1979
|
+
if (remainingSeconds > 0 && row.link_count === 0) {
|
|
1980
|
+
plannedDrains.push({
|
|
1981
|
+
entityType: "calendar_event",
|
|
1982
|
+
entityId: row.id,
|
|
1983
|
+
eventKind: "calendar_event_plan",
|
|
1984
|
+
sourceKind: "calendar",
|
|
1985
|
+
totalAp: rateToTotalAp(calendarProfile.sustainRateApPerHour, remainingSeconds),
|
|
1986
|
+
rateApPerHour: calendarProfile.sustainRateApPerHour,
|
|
1987
|
+
title: row.title,
|
|
1988
|
+
why: "Busy calendar events reserve attention and social bandwidth even before deeper work is linked to them.",
|
|
1989
|
+
startsAt: row.start_at,
|
|
1990
|
+
endsAt: row.end_at,
|
|
1991
|
+
role: "background"
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
if (row.start_at <= nowIsoValue &&
|
|
1995
|
+
row.end_at > nowIsoValue &&
|
|
1996
|
+
!overlapsBlockingWindow) {
|
|
1997
|
+
activeDrains.push({
|
|
1998
|
+
entityType: "calendar_event",
|
|
1999
|
+
entityId: row.id,
|
|
2000
|
+
eventKind: "calendar_context",
|
|
2001
|
+
sourceKind: "calendar",
|
|
2002
|
+
totalAp: 0,
|
|
2003
|
+
rateApPerHour: calendarProfile.sustainRateApPerHour,
|
|
2004
|
+
title: row.title,
|
|
2005
|
+
why: "Calendar context occupies mental and social capacity even before task work is logged.",
|
|
2006
|
+
startsAt: row.start_at,
|
|
2007
|
+
endsAt: row.end_at,
|
|
2008
|
+
role: "background"
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
catch {
|
|
2014
|
+
return { actualContributions, activeDrains, plannedDrains };
|
|
2015
|
+
}
|
|
2016
|
+
return { actualContributions, activeDrains, plannedDrains };
|
|
2017
|
+
}
|
|
873
2018
|
function syncApLedger(userId, range, contributions) {
|
|
874
2019
|
runInTransaction(() => {
|
|
875
2020
|
const database = getDatabase();
|
|
@@ -1010,17 +2155,41 @@ export function buildLifeForcePayload(now = new Date(), userIds) {
|
|
|
1010
2155
|
const profile = ensureLifeForceProfile(user.id);
|
|
1011
2156
|
const snapshot = getOrCreateDaySnapshot(user.id, now);
|
|
1012
2157
|
const range = buildDayRange(now);
|
|
1013
|
-
const taskRuns = buildTaskRunContributions(user.id, range, now);
|
|
1014
|
-
const notes = buildNoteContributions(user.id, range, now);
|
|
1015
|
-
const habits = buildHabitContributions(user.id, range);
|
|
1016
|
-
const workouts = buildWorkoutContributions(user.id, range, now);
|
|
1017
|
-
const
|
|
2158
|
+
const taskRuns = buildTaskRunContributions(user.id, range, now, profile);
|
|
2159
|
+
const notes = buildNoteContributions(user.id, range, now, profile);
|
|
2160
|
+
const habits = buildHabitContributions(user.id, range, profile);
|
|
2161
|
+
const workouts = buildWorkoutContributions(user.id, range, now, profile);
|
|
2162
|
+
const movement = buildMovementTripContributions(user.id, range, now, profile);
|
|
2163
|
+
const wakeImpulses = buildWakeImpulseContributions(user.id, range, profile);
|
|
2164
|
+
const adjustments = buildWorkAdjustmentContributions(user.id, range, profile);
|
|
2165
|
+
const actualSourceWindows = [
|
|
2166
|
+
...taskRuns.contributions,
|
|
2167
|
+
...workouts.contributions,
|
|
2168
|
+
...movement.contributions
|
|
2169
|
+
]
|
|
2170
|
+
.filter((entry) => Boolean(entry.startsAt && entry.endsAt))
|
|
2171
|
+
.map((entry) => ({
|
|
2172
|
+
startAt: entry.startsAt,
|
|
2173
|
+
endAt: entry.endsAt
|
|
2174
|
+
}));
|
|
2175
|
+
const activeTaskRunTaskIds = new Set(taskRuns.activeDrains.map((entry) => entry.entityId));
|
|
2176
|
+
const plannedContainers = buildTimeboxAndWorkBlockDrains(user.id, range, now, profile, activeTaskRunTaskIds, actualSourceWindows);
|
|
2177
|
+
const calendarBlockingWindows = [
|
|
2178
|
+
...actualSourceWindows,
|
|
2179
|
+
...plannedContainers.timeboxWindows,
|
|
2180
|
+
...plannedContainers.workBlockWindows
|
|
2181
|
+
];
|
|
2182
|
+
const calendarDrains = buildCalendarDrains(user.id, now, range, profile, calendarBlockingWindows);
|
|
1018
2183
|
const contributions = [
|
|
1019
2184
|
...taskRuns.contributions,
|
|
1020
2185
|
...adjustments,
|
|
1021
2186
|
...notes,
|
|
1022
2187
|
...habits,
|
|
1023
|
-
...workouts.contributions
|
|
2188
|
+
...workouts.contributions,
|
|
2189
|
+
...movement.contributions,
|
|
2190
|
+
...wakeImpulses,
|
|
2191
|
+
...plannedContainers.actualContributions,
|
|
2192
|
+
...calendarDrains.actualContributions
|
|
1024
2193
|
];
|
|
1025
2194
|
const seededProfilesByKey = new Map(seededActionProfiles().map((entry) => [entry.profileKey, entry]));
|
|
1026
2195
|
const taskDurationRows = getDatabase()
|
|
@@ -1034,22 +2203,60 @@ export function buildLifeForcePayload(now = new Date(), userIds) {
|
|
|
1034
2203
|
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, resolveTaskActionProfile({
|
|
1035
2204
|
id: contribution.entityId,
|
|
1036
2205
|
plannedDurationSeconds: taskDurationById.get(contribution.entityId) ?? null
|
|
1037
|
-
}));
|
|
2206
|
+
}, profile));
|
|
1038
2207
|
continue;
|
|
1039
2208
|
}
|
|
1040
2209
|
if (contribution.entityType === "note") {
|
|
1041
|
-
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, seededProfilesByKey.get("note_quick") ??
|
|
2210
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, buildEffectiveProfile(seededProfilesByKey.get("note_quick") ?? seededActionProfiles()[0], profile));
|
|
1042
2211
|
continue;
|
|
1043
2212
|
}
|
|
1044
2213
|
if (contribution.entityType === "habit") {
|
|
1045
|
-
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, seededProfilesByKey.get("habit_default") ??
|
|
2214
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, buildEffectiveProfile(seededProfilesByKey.get("habit_default") ?? seededActionProfiles()[0], profile));
|
|
1046
2215
|
continue;
|
|
1047
2216
|
}
|
|
1048
2217
|
if (contribution.entityType === "workout_session") {
|
|
1049
|
-
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, seededProfilesByKey.get("workout_default") ??
|
|
2218
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, buildEffectiveProfile(seededProfilesByKey.get("workout_default") ?? seededActionProfiles()[0], profile));
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
if (contribution.entityType === "movement_trip") {
|
|
2222
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, buildEffectiveProfile(seededProfilesByKey.get("movement_trip_default") ?? seededActionProfiles()[0], profile));
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
if (contribution.entityType === "task_timebox") {
|
|
2226
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, buildEffectiveProfile(readEntityActionProfile("task_timebox", contribution.entityId, {
|
|
2227
|
+
profileKey: `task_timebox_${contribution.entityId}`,
|
|
2228
|
+
title: contribution.title,
|
|
2229
|
+
entityType: "task_timebox"
|
|
2230
|
+
}) ??
|
|
2231
|
+
(seededProfilesByKey.get("task_timebox_default") ?? seededActionProfiles()[0]), profile));
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
if (contribution.entityType === "work_block") {
|
|
2235
|
+
const templateId = typeof contribution.metadata?.templateId === "string"
|
|
2236
|
+
? contribution.metadata.templateId
|
|
2237
|
+
: contribution.entityId;
|
|
2238
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, buildEffectiveProfile(readEntityActionProfile("work_block_template", templateId, {
|
|
2239
|
+
profileKey: `work_block_template_${templateId}`,
|
|
2240
|
+
title: contribution.title,
|
|
2241
|
+
entityType: "work_block_template"
|
|
2242
|
+
}) ??
|
|
2243
|
+
(seededProfilesByKey.get("work_block_main") ?? seededActionProfiles()[0]), profile));
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
if (contribution.entityType === "calendar_event") {
|
|
2247
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, buildEffectiveProfile(readEntityActionProfile("calendar_event", contribution.entityId, {
|
|
2248
|
+
profileKey: `calendar_event_${contribution.entityId}`,
|
|
2249
|
+
title: contribution.title,
|
|
2250
|
+
entityType: "calendar_event"
|
|
2251
|
+
}) ??
|
|
2252
|
+
(seededProfilesByKey.get("calendar_event_default") ?? seededActionProfiles()[0]), profile));
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
if (contribution.eventKind === "wake_start") {
|
|
2256
|
+
profileLookup.set(`${contribution.entityType}:${contribution.entityId}`, buildEffectiveProfile(seededProfilesByKey.get("wake_start") ?? seededActionProfiles()[0], profile));
|
|
1050
2257
|
}
|
|
1051
2258
|
}
|
|
1052
|
-
const adjustmentApByTaskId = readTodayAdjustmentApByTaskId(user.id, range);
|
|
2259
|
+
const adjustmentApByTaskId = readTodayAdjustmentApByTaskId(user.id, range, profile);
|
|
1053
2260
|
for (const [taskId, adjustmentAp] of adjustmentApByTaskId.entries()) {
|
|
1054
2261
|
const existing = taskRuns.totalsByTaskId.get(taskId) ?? { todayAp: 0, totalAp: 0 };
|
|
1055
2262
|
existing.todayAp += adjustmentAp;
|
|
@@ -1066,12 +2273,21 @@ export function buildLifeForcePayload(now = new Date(), userIds) {
|
|
|
1066
2273
|
const currentCurve = parseCurvePoints(snapshot.points_json);
|
|
1067
2274
|
const minuteOfDay = now.getUTCHours() * 60 + now.getUTCMinutes();
|
|
1068
2275
|
const instantCapacityApPerHour = interpolateCurveRate(currentCurve, minuteOfDay);
|
|
2276
|
+
const activeSourceWindows = [
|
|
2277
|
+
...taskRuns.activeDrains,
|
|
2278
|
+
...workouts.activeDrains,
|
|
2279
|
+
...movement.activeDrains,
|
|
2280
|
+
...plannedContainers.activeDrains
|
|
2281
|
+
].map((entry) => ({
|
|
2282
|
+
startAt: entry.startsAt ?? now.toISOString(),
|
|
2283
|
+
endAt: entry.endsAt ?? now.toISOString()
|
|
2284
|
+
}));
|
|
1069
2285
|
const activeDrains = [
|
|
1070
2286
|
...taskRuns.activeDrains,
|
|
1071
2287
|
...workouts.activeDrains,
|
|
1072
|
-
...
|
|
1073
|
-
|
|
1074
|
-
|
|
2288
|
+
...movement.activeDrains,
|
|
2289
|
+
...plannedContainers.activeDrains,
|
|
2290
|
+
...calendarDrains.activeDrains
|
|
1075
2291
|
]
|
|
1076
2292
|
.sort((left, right) => (right.rateApPerHour ?? 0) - (left.rateApPerHour ?? 0))
|
|
1077
2293
|
.map((entry, index) => ({
|
|
@@ -1086,6 +2302,23 @@ export function buildLifeForcePayload(now = new Date(), userIds) {
|
|
|
1086
2302
|
startedAt: entry.startsAt,
|
|
1087
2303
|
endsAt: entry.endsAt
|
|
1088
2304
|
}));
|
|
2305
|
+
const plannedDrains = [
|
|
2306
|
+
...plannedContainers.plannedDrains,
|
|
2307
|
+
...calendarDrains.plannedDrains
|
|
2308
|
+
]
|
|
2309
|
+
.sort((left, right) => Date.parse(left.startsAt ?? now.toISOString()) - Date.parse(right.startsAt ?? now.toISOString()))
|
|
2310
|
+
.map((entry) => ({
|
|
2311
|
+
id: `${entry.sourceKind}:${entry.entityId}`,
|
|
2312
|
+
sourceType: entry.entityType,
|
|
2313
|
+
sourceId: entry.entityId,
|
|
2314
|
+
title: entry.title,
|
|
2315
|
+
role: entry.role,
|
|
2316
|
+
apPerHour: Number((entry.rateApPerHour ?? 0).toFixed(2)),
|
|
2317
|
+
instantAp: Number((entry.totalAp ?? 0).toFixed(2)),
|
|
2318
|
+
why: entry.why,
|
|
2319
|
+
startedAt: entry.startsAt,
|
|
2320
|
+
endsAt: entry.endsAt
|
|
2321
|
+
}));
|
|
1089
2322
|
const sortedRates = activeDrains
|
|
1090
2323
|
.map((entry) => entry.apPerHour)
|
|
1091
2324
|
.sort((left, right) => right - left);
|
|
@@ -1099,7 +2332,8 @@ export function buildLifeForcePayload(now = new Date(), userIds) {
|
|
|
1099
2332
|
const instantFreeApPerHour = Math.max(0, rawInstantFreeApPerHour);
|
|
1100
2333
|
const overloadApPerHour = Math.max(0, Number((-rawInstantFreeApPerHour).toFixed(2)));
|
|
1101
2334
|
const remainingAp = Number((snapshot.daily_budget_ap - spentTodayAp).toFixed(2));
|
|
1102
|
-
const
|
|
2335
|
+
const plannedRemainingAp = Number(plannedDrains.reduce((sum, entry) => sum + entry.instantAp, 0).toFixed(2));
|
|
2336
|
+
const forecastAp = Number((spentTodayAp + plannedRemainingAp).toFixed(2));
|
|
1103
2337
|
const targetBandMinAp = Number((snapshot.daily_budget_ap * 0.85).toFixed(2));
|
|
1104
2338
|
const targetBandMaxAp = Number(snapshot.daily_budget_ap.toFixed(2));
|
|
1105
2339
|
const workTime = computeWorkTime(now);
|
|
@@ -1132,6 +2366,7 @@ export function buildLifeForcePayload(now = new Date(), userIds) {
|
|
|
1132
2366
|
spentTodayAp: Number(spentTodayAp.toFixed(2)),
|
|
1133
2367
|
remainingAp,
|
|
1134
2368
|
forecastAp,
|
|
2369
|
+
plannedRemainingAp,
|
|
1135
2370
|
targetBandMinAp,
|
|
1136
2371
|
targetBandMaxAp,
|
|
1137
2372
|
instantCapacityApPerHour: Number(instantCapacityApPerHour.toFixed(2)),
|
|
@@ -1148,6 +2383,7 @@ export function buildLifeForcePayload(now = new Date(), userIds) {
|
|
|
1148
2383
|
locked: point.minuteOfDay <= minuteOfDay
|
|
1149
2384
|
})),
|
|
1150
2385
|
activeDrains,
|
|
2386
|
+
plannedDrains,
|
|
1151
2387
|
warnings: buildWarnings({
|
|
1152
2388
|
spentTodayAp,
|
|
1153
2389
|
dailyBudgetAp: snapshot.daily_budget_ap,
|