forge-openclaw-plugin 0.2.18 → 0.2.19
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 +36 -4
- package/dist/assets/{board-2KevHCI0.js → board-8L3uX7_O.js} +2 -2
- package/dist/assets/{board-2KevHCI0.js.map → board-8L3uX7_O.js.map} +1 -1
- package/dist/assets/index-Cj1IBH_w.js +36 -0
- package/dist/assets/index-Cj1IBH_w.js.map +1 -0
- package/dist/assets/index-DQT6EbuS.css +1 -0
- package/dist/assets/{motion-q19HPmWs.js → motion-1GAqqi8M.js} +2 -2
- package/dist/assets/{motion-q19HPmWs.js.map → motion-1GAqqi8M.js.map} +1 -1
- package/dist/assets/{table-BDMHBY4a.js → table-DBGlgRjk.js} +2 -2
- package/dist/assets/{table-BDMHBY4a.js.map → table-DBGlgRjk.js.map} +1 -1
- package/dist/assets/{ui-CQ_AsFs8.js → ui-iTluWjC4.js} +2 -2
- package/dist/assets/{ui-CQ_AsFs8.js.map → ui-iTluWjC4.js.map} +1 -1
- package/dist/assets/{vendor-5HifrnRK.js → vendor-BvM2F9Dp.js} +139 -84
- package/dist/assets/vendor-BvM2F9Dp.js.map +1 -0
- package/dist/assets/{viz-CQzkRnTu.js → viz-CNeunkfu.js} +2 -2
- package/dist/assets/{viz-CQzkRnTu.js.map → viz-CNeunkfu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/openclaw/parity.js +1 -0
- package/dist/openclaw/routes.js +7 -0
- package/dist/openclaw/tools.js +183 -16
- package/dist/server/app.js +2509 -263
- package/dist/server/managers/platform/secrets-manager.js +44 -1
- package/dist/server/managers/runtime.js +3 -1
- package/dist/server/openapi.js +2037 -172
- package/dist/server/repositories/calendar.js +1101 -0
- package/dist/server/repositories/deleted-entities.js +10 -2
- package/dist/server/repositories/notes.js +161 -28
- package/dist/server/repositories/projects.js +45 -13
- package/dist/server/repositories/rewards.js +114 -6
- package/dist/server/repositories/settings.js +47 -5
- package/dist/server/repositories/task-runs.js +46 -10
- package/dist/server/repositories/tasks.js +25 -9
- package/dist/server/repositories/weekly-reviews.js +109 -0
- package/dist/server/repositories/work-adjustments.js +105 -0
- package/dist/server/services/calendar-runtime.js +1301 -0
- package/dist/server/services/entity-crud.js +94 -3
- package/dist/server/services/projects.js +32 -8
- package/dist/server/services/reviews.js +15 -1
- package/dist/server/services/work-time.js +27 -0
- package/dist/server/types.js +934 -49
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/006_work_adjustments.sql +14 -0
- package/server/migrations/007_weekly_review_closures.sql +17 -0
- package/server/migrations/008_calendar_execution.sql +147 -0
- package/server/migrations/009_true_calendar_events.sql +195 -0
- package/server/migrations/010_calendar_selection_state.sql +6 -0
- package/server/migrations/011_calendar_timezone_backfill.sql +11 -0
- package/server/migrations/012_work_block_ranges.sql +7 -0
- package/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
- package/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
- package/skills/forge-openclaw/SKILL.md +117 -11
- package/dist/assets/index-CDYW4WDH.js +0 -36
- package/dist/assets/index-CDYW4WDH.js.map +0 -1
- package/dist/assets/index-yroQr6YZ.css +0 -1
- package/dist/assets/vendor-5HifrnRK.js.map +0 -1
|
@@ -12,6 +12,18 @@ function toInt(value) {
|
|
|
12
12
|
function hashToken(token) {
|
|
13
13
|
return createHash("sha256").update(token).digest("hex");
|
|
14
14
|
}
|
|
15
|
+
function defaultMicrosoftRedirectUri() {
|
|
16
|
+
const port = process.env.PORT?.trim() || "4317";
|
|
17
|
+
return `http://127.0.0.1:${port}/api/v1/calendar/oauth/microsoft/callback`;
|
|
18
|
+
}
|
|
19
|
+
function normalizeMicrosoftTenantId(value) {
|
|
20
|
+
const trimmed = value?.trim();
|
|
21
|
+
return trimmed && trimmed.length > 0 ? trimmed : "common";
|
|
22
|
+
}
|
|
23
|
+
function normalizeMicrosoftRedirectUri(value) {
|
|
24
|
+
const trimmed = value?.trim();
|
|
25
|
+
return trimmed && trimmed.length > 0 ? trimmed : defaultMicrosoftRedirectUri();
|
|
26
|
+
}
|
|
15
27
|
function buildTokenSecret() {
|
|
16
28
|
return `fg_live_${randomBytes(18).toString("hex")}`;
|
|
17
29
|
}
|
|
@@ -108,7 +120,7 @@ function readSettingsRow() {
|
|
|
108
120
|
.prepare(`SELECT
|
|
109
121
|
operator_name, operator_email, operator_title, theme_preference, locale_preference,
|
|
110
122
|
goal_drift_alerts, daily_quest_reminders, achievement_celebrations, max_active_tasks, time_accounting_mode,
|
|
111
|
-
integrity_score, last_audit_at, psyche_auth_required, created_at, updated_at
|
|
123
|
+
integrity_score, last_audit_at, psyche_auth_required, microsoft_client_id, microsoft_tenant_id, microsoft_redirect_uri, created_at, updated_at
|
|
112
124
|
FROM app_settings
|
|
113
125
|
WHERE id = 1`)
|
|
114
126
|
.get();
|
|
@@ -166,6 +178,9 @@ export function isPsycheAuthRequired() {
|
|
|
166
178
|
}
|
|
167
179
|
export function getSettings() {
|
|
168
180
|
const row = readSettingsRow();
|
|
181
|
+
const microsoftClientId = row.microsoft_client_id?.trim() ?? "";
|
|
182
|
+
const microsoftTenantId = normalizeMicrosoftTenantId(row.microsoft_tenant_id);
|
|
183
|
+
const microsoftRedirectUri = normalizeMicrosoftRedirectUri(row.microsoft_redirect_uri);
|
|
169
184
|
return settingsPayloadSchema.parse({
|
|
170
185
|
profile: {
|
|
171
186
|
operatorName: row.operator_name,
|
|
@@ -191,6 +206,21 @@ export function getSettings() {
|
|
|
191
206
|
tokenCount: listAgentTokens().filter((token) => token.status === "active").length,
|
|
192
207
|
psycheAuthRequired: boolFromInt(row.psyche_auth_required)
|
|
193
208
|
},
|
|
209
|
+
calendarProviders: {
|
|
210
|
+
microsoft: {
|
|
211
|
+
clientId: microsoftClientId,
|
|
212
|
+
tenantId: microsoftTenantId,
|
|
213
|
+
redirectUri: microsoftRedirectUri,
|
|
214
|
+
usesClientSecret: false,
|
|
215
|
+
readOnly: true,
|
|
216
|
+
authMode: "public_client_pkce",
|
|
217
|
+
isConfigured: microsoftClientId.length > 0,
|
|
218
|
+
isReadyForSignIn: microsoftClientId.length > 0,
|
|
219
|
+
setupMessage: microsoftClientId.length > 0
|
|
220
|
+
? "Microsoft local sign-in is configured. Test it if you want, then continue to the guided sign-in flow."
|
|
221
|
+
: "Save the Microsoft client ID and the Forge callback redirect URI here before you try to sign in."
|
|
222
|
+
}
|
|
223
|
+
},
|
|
194
224
|
agents: listAgentIdentities(),
|
|
195
225
|
agentTokens: listAgentTokens()
|
|
196
226
|
});
|
|
@@ -217,15 +247,25 @@ export function updateSettings(input, activity) {
|
|
|
217
247
|
},
|
|
218
248
|
themePreference: parsed.themePreference ?? current.themePreference,
|
|
219
249
|
localePreference: parsed.localePreference ?? current.localePreference,
|
|
220
|
-
psycheAuthRequired: parsed.security?.psycheAuthRequired ?? current.security.psycheAuthRequired
|
|
250
|
+
psycheAuthRequired: parsed.security?.psycheAuthRequired ?? current.security.psycheAuthRequired,
|
|
251
|
+
calendarProviders: {
|
|
252
|
+
microsoft: {
|
|
253
|
+
clientId: parsed.calendarProviders?.microsoft?.clientId?.trim() ??
|
|
254
|
+
current.calendarProviders.microsoft.clientId,
|
|
255
|
+
tenantId: normalizeMicrosoftTenantId(parsed.calendarProviders?.microsoft?.tenantId ??
|
|
256
|
+
current.calendarProviders.microsoft.tenantId),
|
|
257
|
+
redirectUri: normalizeMicrosoftRedirectUri(parsed.calendarProviders?.microsoft?.redirectUri ??
|
|
258
|
+
current.calendarProviders.microsoft.redirectUri)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
221
261
|
};
|
|
222
262
|
getDatabase()
|
|
223
263
|
.prepare(`UPDATE app_settings
|
|
224
264
|
SET operator_name = ?, operator_email = ?, operator_title = ?, theme_preference = ?, locale_preference = ?,
|
|
225
265
|
goal_drift_alerts = ?, daily_quest_reminders = ?, achievement_celebrations = ?, max_active_tasks = ?, time_accounting_mode = ?,
|
|
226
|
-
psyche_auth_required = ?, updated_at = ?
|
|
266
|
+
psyche_auth_required = ?, microsoft_client_id = ?, microsoft_tenant_id = ?, microsoft_redirect_uri = ?, updated_at = ?
|
|
227
267
|
WHERE id = 1`)
|
|
228
|
-
.run(next.profile.operatorName, next.profile.operatorEmail, next.profile.operatorTitle, next.themePreference, next.localePreference, toInt(next.notifications.goalDriftAlerts), toInt(next.notifications.dailyQuestReminders), toInt(next.notifications.achievementCelebrations), next.execution.maxActiveTasks, next.execution.timeAccountingMode, toInt(next.psycheAuthRequired), now);
|
|
268
|
+
.run(next.profile.operatorName, next.profile.operatorEmail, next.profile.operatorTitle, next.themePreference, next.localePreference, toInt(next.notifications.goalDriftAlerts), toInt(next.notifications.dailyQuestReminders), toInt(next.notifications.achievementCelebrations), next.execution.maxActiveTasks, next.execution.timeAccountingMode, toInt(next.psycheAuthRequired), next.calendarProviders.microsoft.clientId, next.calendarProviders.microsoft.tenantId, next.calendarProviders.microsoft.redirectUri, now);
|
|
229
269
|
if (activity) {
|
|
230
270
|
recordActivityEvent({
|
|
231
271
|
entityType: "system",
|
|
@@ -241,7 +281,9 @@ export function updateSettings(input, activity) {
|
|
|
241
281
|
goalDriftAlerts: next.notifications.goalDriftAlerts,
|
|
242
282
|
dailyQuestReminders: next.notifications.dailyQuestReminders,
|
|
243
283
|
maxActiveTasks: next.execution.maxActiveTasks,
|
|
244
|
-
timeAccountingMode: next.execution.timeAccountingMode
|
|
284
|
+
timeAccountingMode: next.execution.timeAccountingMode,
|
|
285
|
+
microsoftConfigured: next.calendarProviders.microsoft.clientId.trim().length > 0,
|
|
286
|
+
microsoftTenantId: next.calendarProviders.microsoft.tenantId
|
|
245
287
|
}
|
|
246
288
|
});
|
|
247
289
|
}
|
|
@@ -3,6 +3,7 @@ import { getDatabase, runInTransaction } from "../db.js";
|
|
|
3
3
|
import { HttpError } from "../errors.js";
|
|
4
4
|
import { computeWorkTime } from "../services/work-time.js";
|
|
5
5
|
import { recordActivityEvent } from "./activity-events.js";
|
|
6
|
+
import { bindTaskRunToTimebox, evaluateSchedulingForTask, finalizeTaskRunTimebox, heartbeatTaskRunTimebox } from "./calendar.js";
|
|
6
7
|
import { createLinkedNotes } from "./notes.js";
|
|
7
8
|
import { recordTaskRunCompletionReward, recordTaskRunProgressRewards, recordTaskRunStartReward } from "./rewards.js";
|
|
8
9
|
import { getTaskById, updateTaskInTransaction } from "./tasks.js";
|
|
@@ -27,6 +28,7 @@ function selectClause() {
|
|
|
27
28
|
task_runs.completed_at,
|
|
28
29
|
task_runs.released_at,
|
|
29
30
|
task_runs.timed_out_at,
|
|
31
|
+
task_runs.override_reason,
|
|
30
32
|
task_runs.updated_at,
|
|
31
33
|
tasks.title AS task_title
|
|
32
34
|
FROM task_runs
|
|
@@ -74,6 +76,7 @@ function mapTaskRun(row, now = new Date(), cached = computeWorkTime(now)) {
|
|
|
74
76
|
completedAt: row.completed_at,
|
|
75
77
|
releasedAt: row.released_at,
|
|
76
78
|
timedOutAt: row.timed_out_at,
|
|
79
|
+
overrideReason: row.override_reason ?? null,
|
|
77
80
|
updatedAt: row.updated_at
|
|
78
81
|
});
|
|
79
82
|
}
|
|
@@ -289,6 +292,15 @@ export function claimTaskRun(taskId, input, now = new Date(), activity = { sourc
|
|
|
289
292
|
const parsedInput = taskRunClaimSchema.parse(input);
|
|
290
293
|
markExpiredRunsTimedOutInTransaction(now);
|
|
291
294
|
requireKnownTask(taskId);
|
|
295
|
+
const task = getTaskById(taskId);
|
|
296
|
+
const scheduling = evaluateSchedulingForTask(task, now);
|
|
297
|
+
if (scheduling.blocked && (!parsedInput.overrideReason || parsedInput.overrideReason.trim().length === 0)) {
|
|
298
|
+
throw new HttpError(409, "task_run_calendar_blocked", `Calendar rules block starting ${task.title} right now. Add an override reason to proceed.`, {
|
|
299
|
+
taskId,
|
|
300
|
+
conflicts: scheduling.conflicts,
|
|
301
|
+
effectiveRules: scheduling.effectiveRules
|
|
302
|
+
});
|
|
303
|
+
}
|
|
292
304
|
const existing = getActiveTaskRunRow(taskId, now);
|
|
293
305
|
const nowIso = now.toISOString();
|
|
294
306
|
if (existing) {
|
|
@@ -298,9 +310,9 @@ export function claimTaskRun(taskId, input, now = new Date(), activity = { sourc
|
|
|
298
310
|
const nextExpiry = leaseExpiry(now, parsedInput.leaseTtlSeconds);
|
|
299
311
|
getDatabase()
|
|
300
312
|
.prepare(`UPDATE task_runs
|
|
301
|
-
SET timer_mode = ?, planned_duration_seconds = ?, is_current = ?, heartbeat_at = ?, lease_expires_at = ?, lease_ttl_seconds = ?, note = ?, updated_at = ?
|
|
313
|
+
SET timer_mode = ?, planned_duration_seconds = ?, is_current = ?, heartbeat_at = ?, lease_expires_at = ?, lease_ttl_seconds = ?, note = ?, override_reason = ?, updated_at = ?
|
|
302
314
|
WHERE id = ?`)
|
|
303
|
-
.run(parsedInput.timerMode, parsedInput.plannedDurationSeconds, existing.is_current, nowIso, nextExpiry, parsedInput.leaseTtlSeconds, parsedInput.note, nowIso, existing.id);
|
|
315
|
+
.run(parsedInput.timerMode, parsedInput.plannedDurationSeconds, existing.is_current, nowIso, nextExpiry, parsedInput.leaseTtlSeconds, parsedInput.note, parsedInput.overrideReason ?? null, nowIso, existing.id);
|
|
304
316
|
maybeSetCurrentRun(parsedInput.actor, existing.id, parsedInput.isCurrent, now);
|
|
305
317
|
touchTaskInProgress(taskId, parsedInput.actor, activity.source);
|
|
306
318
|
recordActivityEvent({
|
|
@@ -315,9 +327,15 @@ export function claimTaskRun(taskId, input, now = new Date(), activity = { sourc
|
|
|
315
327
|
taskId,
|
|
316
328
|
leaseTtlSeconds: parsedInput.leaseTtlSeconds,
|
|
317
329
|
timerMode: parsedInput.timerMode,
|
|
318
|
-
plannedDurationSeconds: parsedInput.plannedDurationSeconds
|
|
330
|
+
plannedDurationSeconds: parsedInput.plannedDurationSeconds,
|
|
331
|
+
overrideReason: parsedInput.overrideReason ?? null
|
|
319
332
|
}
|
|
320
333
|
});
|
|
334
|
+
heartbeatTaskRunTimebox(existing.id, {
|
|
335
|
+
title: task.title,
|
|
336
|
+
endsAt: nextExpiry,
|
|
337
|
+
overrideReason: parsedInput.overrideReason ?? null
|
|
338
|
+
});
|
|
321
339
|
const cached = computeWorkTime(now);
|
|
322
340
|
return {
|
|
323
341
|
run: mapTaskRun(requireRun(existing.id), now, cached),
|
|
@@ -329,13 +347,22 @@ export function claimTaskRun(taskId, input, now = new Date(), activity = { sourc
|
|
|
329
347
|
const expiry = leaseExpiry(now, parsedInput.leaseTtlSeconds);
|
|
330
348
|
getDatabase()
|
|
331
349
|
.prepare(`INSERT INTO task_runs (
|
|
332
|
-
id, task_id, actor, status, timer_mode, planned_duration_seconds, is_current, note, lease_ttl_seconds, claimed_at, heartbeat_at, lease_expires_at, updated_at
|
|
350
|
+
id, task_id, actor, status, timer_mode, planned_duration_seconds, is_current, note, lease_ttl_seconds, claimed_at, heartbeat_at, lease_expires_at, override_reason, updated_at
|
|
333
351
|
)
|
|
334
|
-
VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
335
|
-
.run(runId, taskId, parsedInput.actor, parsedInput.timerMode, parsedInput.plannedDurationSeconds, 0, parsedInput.note, parsedInput.leaseTtlSeconds, nowIso, nowIso, expiry, nowIso);
|
|
352
|
+
VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
353
|
+
.run(runId, taskId, parsedInput.actor, parsedInput.timerMode, parsedInput.plannedDurationSeconds, 0, parsedInput.note, parsedInput.leaseTtlSeconds, nowIso, nowIso, expiry, parsedInput.overrideReason ?? null, nowIso);
|
|
336
354
|
maybeSetCurrentRun(parsedInput.actor, runId, parsedInput.isCurrent, now);
|
|
337
355
|
touchTaskInProgress(taskId, parsedInput.actor, activity.source);
|
|
338
356
|
const run = mapTaskRun(requireRun(runId), now);
|
|
357
|
+
bindTaskRunToTimebox({
|
|
358
|
+
taskId,
|
|
359
|
+
taskRunId: run.id,
|
|
360
|
+
startedAt: now,
|
|
361
|
+
title: task.title,
|
|
362
|
+
projectId: task.projectId,
|
|
363
|
+
plannedDurationSeconds: parsedInput.plannedDurationSeconds,
|
|
364
|
+
overrideReason: parsedInput.overrideReason ?? null
|
|
365
|
+
});
|
|
339
366
|
recordActivityEvent({
|
|
340
367
|
entityType: "task_run",
|
|
341
368
|
entityId: run.id,
|
|
@@ -350,7 +377,8 @@ export function claimTaskRun(taskId, input, now = new Date(), activity = { sourc
|
|
|
350
377
|
taskId: run.taskId,
|
|
351
378
|
leaseTtlSeconds: run.leaseTtlSeconds,
|
|
352
379
|
timerMode: run.timerMode,
|
|
353
|
-
plannedDurationSeconds: run.plannedDurationSeconds
|
|
380
|
+
plannedDurationSeconds: run.plannedDurationSeconds,
|
|
381
|
+
overrideReason: parsedInput.overrideReason ?? null
|
|
354
382
|
}
|
|
355
383
|
});
|
|
356
384
|
recordTaskRunStartReward(run.id, run.taskId, run.actor, activity.source);
|
|
@@ -370,15 +398,16 @@ export function heartbeatTaskRun(taskRunId, input, now = new Date(), activity =
|
|
|
370
398
|
const note = input.note ?? current.note;
|
|
371
399
|
getDatabase()
|
|
372
400
|
.prepare(`UPDATE task_runs
|
|
373
|
-
SET heartbeat_at = ?, lease_expires_at = ?, lease_ttl_seconds = ?, note = ?, updated_at = ?
|
|
401
|
+
SET heartbeat_at = ?, lease_expires_at = ?, lease_ttl_seconds = ?, note = ?, override_reason = ?, updated_at = ?
|
|
374
402
|
WHERE id = ?`)
|
|
375
|
-
.run(nowIso, nextExpiry, input.leaseTtlSeconds, note, nowIso, taskRunId);
|
|
403
|
+
.run(nowIso, nextExpiry, input.leaseTtlSeconds, note, input.overrideReason ?? current.override_reason, nowIso, taskRunId);
|
|
376
404
|
const run = mapTaskRun({
|
|
377
405
|
...current,
|
|
378
406
|
heartbeat_at: nowIso,
|
|
379
407
|
lease_expires_at: nextExpiry,
|
|
380
408
|
lease_ttl_seconds: input.leaseTtlSeconds,
|
|
381
409
|
note,
|
|
410
|
+
override_reason: input.overrideReason ?? current.override_reason,
|
|
382
411
|
updated_at: nowIso
|
|
383
412
|
}, now);
|
|
384
413
|
recordActivityEvent({
|
|
@@ -391,9 +420,15 @@ export function heartbeatTaskRun(taskRunId, input, now = new Date(), activity =
|
|
|
391
420
|
source: activity.source,
|
|
392
421
|
metadata: {
|
|
393
422
|
taskId: run.taskId,
|
|
394
|
-
leaseTtlSeconds: run.leaseTtlSeconds
|
|
423
|
+
leaseTtlSeconds: run.leaseTtlSeconds,
|
|
424
|
+
overrideReason: input.overrideReason ?? null
|
|
395
425
|
}
|
|
396
426
|
});
|
|
427
|
+
heartbeatTaskRunTimebox(taskRunId, {
|
|
428
|
+
title: run.taskTitle,
|
|
429
|
+
endsAt: nextExpiry,
|
|
430
|
+
overrideReason: input.overrideReason ?? null
|
|
431
|
+
});
|
|
397
432
|
recordTaskRunProgressRewards(run.id, run.taskId, input.actor ?? run.actor, activity.source, run.creditedSeconds);
|
|
398
433
|
return run;
|
|
399
434
|
});
|
|
@@ -451,6 +486,7 @@ function finishTaskRun(taskRunId, nextStatus, timestampColumn, input, now, activ
|
|
|
451
486
|
[timestampColumn]: nowIso,
|
|
452
487
|
updated_at: nowIso
|
|
453
488
|
}, now);
|
|
489
|
+
finalizeTaskRunTimebox(taskRunId, nextStatus === "completed" ? "completed" : "cancelled", nowIso);
|
|
454
490
|
recordActivityEvent({
|
|
455
491
|
entityType: "task_run",
|
|
456
492
|
entityId: run.id,
|
|
@@ -11,7 +11,7 @@ import { pruneLinkedEntityReferences } from "./psyche.js";
|
|
|
11
11
|
import { awardTaskCompletionReward, reverseLatestTaskCompletionReward } from "./rewards.js";
|
|
12
12
|
import { assertTaskRelations } from "../services/relations.js";
|
|
13
13
|
import { computeWorkTime, emptyTaskTimeSummary } from "../services/work-time.js";
|
|
14
|
-
import { taskSchema } from "../types.js";
|
|
14
|
+
import { calendarSchedulingRulesSchema, taskSchema } from "../types.js";
|
|
15
15
|
function readTaskTagIds(taskId) {
|
|
16
16
|
const rows = getDatabase()
|
|
17
17
|
.prepare(`SELECT tag_id FROM task_tags WHERE task_id = ? ORDER BY tag_id`)
|
|
@@ -32,6 +32,10 @@ function mapTask(row, time = emptyTaskTimeSummary()) {
|
|
|
32
32
|
effort: row.effort,
|
|
33
33
|
energy: row.energy,
|
|
34
34
|
points: row.points,
|
|
35
|
+
plannedDurationSeconds: row.planned_duration_seconds,
|
|
36
|
+
schedulingRules: row.scheduling_rules_json === null
|
|
37
|
+
? null
|
|
38
|
+
: calendarSchedulingRulesSchema.parse(JSON.parse(row.scheduling_rules_json)),
|
|
35
39
|
sortOrder: row.sort_order,
|
|
36
40
|
completedAt: row.completed_at,
|
|
37
41
|
createdAt: row.created_at,
|
|
@@ -129,9 +133,17 @@ function updateTaskRecord(current, input, activity) {
|
|
|
129
133
|
getDatabase()
|
|
130
134
|
.prepare(`UPDATE tasks
|
|
131
135
|
SET title = ?, description = ?, status = ?, priority = ?, owner = ?, goal_id = ?, due_date = ?, effort = ?,
|
|
132
|
-
energy = ?, points = ?, sort_order = ?, completed_at = ?, updated_at = ?, project_id = ?
|
|
136
|
+
energy = ?, points = ?, planned_duration_seconds = ?, scheduling_rules_json = ?, sort_order = ?, completed_at = ?, updated_at = ?, project_id = ?
|
|
133
137
|
WHERE id = ?`)
|
|
134
|
-
.run(input.title ?? current.title, input.description ?? current.description, nextStatus, input.priority ?? current.priority, input.owner ?? current.owner, nextGoalId, input.dueDate === undefined ? current.dueDate : input.dueDate, input.effort ?? current.effort, input.energy ?? current.energy, input.points ?? current.points,
|
|
138
|
+
.run(input.title ?? current.title, input.description ?? current.description, nextStatus, input.priority ?? current.priority, input.owner ?? current.owner, nextGoalId, input.dueDate === undefined ? current.dueDate : input.dueDate, input.effort ?? current.effort, input.energy ?? current.energy, input.points ?? current.points, input.plannedDurationSeconds === undefined
|
|
139
|
+
? current.plannedDurationSeconds
|
|
140
|
+
: input.plannedDurationSeconds, input.schedulingRules === undefined
|
|
141
|
+
? current.schedulingRules === null
|
|
142
|
+
? null
|
|
143
|
+
: JSON.stringify(current.schedulingRules)
|
|
144
|
+
: input.schedulingRules === null
|
|
145
|
+
? null
|
|
146
|
+
: JSON.stringify(input.schedulingRules), nextSort, completedAt, updatedAt, nextProjectId, current.id);
|
|
135
147
|
replaceTaskTags(current.id, nextTagIds);
|
|
136
148
|
const updated = getTaskById(current.id);
|
|
137
149
|
if (updated && activity) {
|
|
@@ -210,6 +222,8 @@ function fingerprintTaskCreate(input) {
|
|
|
210
222
|
effort: input.effort,
|
|
211
223
|
energy: input.energy,
|
|
212
224
|
points: input.points,
|
|
225
|
+
plannedDurationSeconds: input.plannedDurationSeconds,
|
|
226
|
+
schedulingRules: input.schedulingRules,
|
|
213
227
|
sortOrder: input.sortOrder ?? null,
|
|
214
228
|
tagIds: input.tagIds,
|
|
215
229
|
notes: input.notes.map((note) => ({
|
|
@@ -232,11 +246,11 @@ function insertTaskRecord(input, activity) {
|
|
|
232
246
|
const completedAt = normalizeCompletedAt(input.status, null);
|
|
233
247
|
getDatabase()
|
|
234
248
|
.prepare(`INSERT INTO tasks (
|
|
235
|
-
id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points,
|
|
236
|
-
completed_at, created_at, updated_at
|
|
249
|
+
id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points,
|
|
250
|
+
planned_duration_seconds, scheduling_rules_json, sort_order, completed_at, created_at, updated_at
|
|
237
251
|
)
|
|
238
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
239
|
-
.run(id, input.title, input.description, input.status, input.priority, input.owner, relationState.goalId, relationState.projectId, input.dueDate, input.effort, input.energy, input.points, sortOrder, completedAt, now, now);
|
|
252
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
253
|
+
.run(id, input.title, input.description, input.status, input.priority, input.owner, relationState.goalId, relationState.projectId, input.dueDate, input.effort, input.energy, input.points, input.plannedDurationSeconds, input.schedulingRules === null ? null : JSON.stringify(input.schedulingRules), sortOrder, completedAt, now, now);
|
|
240
254
|
replaceTaskTags(id, input.tagIds);
|
|
241
255
|
const task = getTaskById(id);
|
|
242
256
|
if (activity) {
|
|
@@ -306,7 +320,8 @@ export function listTasks(filters = {}) {
|
|
|
306
320
|
params.push(filters.limit);
|
|
307
321
|
}
|
|
308
322
|
const rows = getDatabase()
|
|
309
|
-
.prepare(`SELECT id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points,
|
|
323
|
+
.prepare(`SELECT id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points,
|
|
324
|
+
planned_duration_seconds, scheduling_rules_json, sort_order,
|
|
310
325
|
completed_at, created_at, updated_at
|
|
311
326
|
FROM tasks
|
|
312
327
|
${whereSql}
|
|
@@ -330,7 +345,8 @@ export function getTaskById(taskId) {
|
|
|
330
345
|
return undefined;
|
|
331
346
|
}
|
|
332
347
|
const row = getDatabase()
|
|
333
|
-
.prepare(`SELECT id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points,
|
|
348
|
+
.prepare(`SELECT id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points,
|
|
349
|
+
planned_duration_seconds, scheduling_rules_json, sort_order,
|
|
334
350
|
completed_at, created_at, updated_at
|
|
335
351
|
FROM tasks
|
|
336
352
|
WHERE id = ?`)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getDatabase, runInTransaction } from "../db.js";
|
|
3
|
+
import { recordActivityEvent } from "./activity-events.js";
|
|
4
|
+
import { getRewardLedgerEventById, recordWeeklyReviewCompletionReward } from "./rewards.js";
|
|
5
|
+
import { weeklyReviewClosureSchema } from "../types.js";
|
|
6
|
+
function mapWeeklyReviewClosure(row) {
|
|
7
|
+
return weeklyReviewClosureSchema.parse({
|
|
8
|
+
id: row.id,
|
|
9
|
+
weekKey: row.week_key,
|
|
10
|
+
weekStartDate: row.week_start_date,
|
|
11
|
+
weekEndDate: row.week_end_date,
|
|
12
|
+
windowLabel: row.window_label,
|
|
13
|
+
actor: row.actor,
|
|
14
|
+
source: row.source,
|
|
15
|
+
rewardId: row.reward_id,
|
|
16
|
+
activityEventId: row.activity_event_id,
|
|
17
|
+
createdAt: row.created_at
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function getWeeklyReviewClosure(weekKey) {
|
|
21
|
+
const row = getDatabase()
|
|
22
|
+
.prepare(`SELECT
|
|
23
|
+
id,
|
|
24
|
+
week_key,
|
|
25
|
+
week_start_date,
|
|
26
|
+
week_end_date,
|
|
27
|
+
window_label,
|
|
28
|
+
actor,
|
|
29
|
+
source,
|
|
30
|
+
reward_id,
|
|
31
|
+
activity_event_id,
|
|
32
|
+
created_at
|
|
33
|
+
FROM weekly_review_closures
|
|
34
|
+
WHERE week_key = ?`)
|
|
35
|
+
.get(weekKey);
|
|
36
|
+
return row ? mapWeeklyReviewClosure(row) : null;
|
|
37
|
+
}
|
|
38
|
+
export function finalizeWeeklyReviewClosure(input) {
|
|
39
|
+
return runInTransaction(() => {
|
|
40
|
+
const existing = getWeeklyReviewClosure(input.weekKey);
|
|
41
|
+
if (existing) {
|
|
42
|
+
const existingReward = getRewardLedgerEventById(existing.rewardId);
|
|
43
|
+
if (!existingReward) {
|
|
44
|
+
throw new Error(`Weekly review closure ${existing.id} is missing reward ${existing.rewardId}.`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
closure: existing,
|
|
48
|
+
reward: existingReward,
|
|
49
|
+
created: false
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const reward = recordWeeklyReviewCompletionReward({
|
|
53
|
+
weekKey: input.weekKey,
|
|
54
|
+
windowLabel: input.windowLabel,
|
|
55
|
+
rewardXp: input.rewardXp
|
|
56
|
+
}, {
|
|
57
|
+
actor: input.actor ?? null,
|
|
58
|
+
source: input.source
|
|
59
|
+
});
|
|
60
|
+
const activity = recordActivityEvent({
|
|
61
|
+
entityType: "system",
|
|
62
|
+
entityId: input.weekKey,
|
|
63
|
+
eventType: "weekly_review_finalized",
|
|
64
|
+
title: `Weekly review finalized: ${input.windowLabel}`,
|
|
65
|
+
description: `Review completion locked this cycle and awarded ${reward.deltaXp} XP.`,
|
|
66
|
+
actor: input.actor ?? null,
|
|
67
|
+
source: input.source,
|
|
68
|
+
metadata: {
|
|
69
|
+
weekKey: input.weekKey,
|
|
70
|
+
weekStartDate: input.weekStartDate,
|
|
71
|
+
weekEndDate: input.weekEndDate,
|
|
72
|
+
rewardId: reward.id,
|
|
73
|
+
rewardXp: reward.deltaXp
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const createdAt = new Date().toISOString();
|
|
77
|
+
const closure = weeklyReviewClosureSchema.parse({
|
|
78
|
+
id: `wrc_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
|
|
79
|
+
weekKey: input.weekKey,
|
|
80
|
+
weekStartDate: input.weekStartDate,
|
|
81
|
+
weekEndDate: input.weekEndDate,
|
|
82
|
+
windowLabel: input.windowLabel,
|
|
83
|
+
actor: input.actor ?? null,
|
|
84
|
+
source: input.source,
|
|
85
|
+
rewardId: reward.id,
|
|
86
|
+
activityEventId: activity.id,
|
|
87
|
+
createdAt
|
|
88
|
+
});
|
|
89
|
+
getDatabase()
|
|
90
|
+
.prepare(`INSERT INTO weekly_review_closures (
|
|
91
|
+
id,
|
|
92
|
+
week_key,
|
|
93
|
+
week_start_date,
|
|
94
|
+
week_end_date,
|
|
95
|
+
window_label,
|
|
96
|
+
actor,
|
|
97
|
+
source,
|
|
98
|
+
reward_id,
|
|
99
|
+
activity_event_id,
|
|
100
|
+
created_at
|
|
101
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
102
|
+
.run(closure.id, closure.weekKey, closure.weekStartDate, closure.weekEndDate, closure.windowLabel, closure.actor, closure.source, closure.rewardId, closure.activityEventId, closure.createdAt);
|
|
103
|
+
return {
|
|
104
|
+
closure,
|
|
105
|
+
reward,
|
|
106
|
+
created: true
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getDatabase } from "../db.js";
|
|
3
|
+
import { createWorkAdjustmentSchema, workAdjustmentSchema } from "../types.js";
|
|
4
|
+
function mapWorkAdjustment(row) {
|
|
5
|
+
return workAdjustmentSchema.parse({
|
|
6
|
+
id: row.id,
|
|
7
|
+
entityType: row.entity_type,
|
|
8
|
+
entityId: row.entity_id,
|
|
9
|
+
requestedDeltaMinutes: row.requested_delta_minutes,
|
|
10
|
+
appliedDeltaMinutes: row.applied_delta_minutes,
|
|
11
|
+
note: row.note,
|
|
12
|
+
actor: row.actor,
|
|
13
|
+
source: row.source,
|
|
14
|
+
createdAt: row.created_at
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function createWorkAdjustment(input, activity, now = new Date()) {
|
|
18
|
+
const parsed = createWorkAdjustmentSchema.parse(input);
|
|
19
|
+
const adjustment = workAdjustmentSchema.parse({
|
|
20
|
+
id: `wadj_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
|
|
21
|
+
entityType: parsed.entityType,
|
|
22
|
+
entityId: parsed.entityId,
|
|
23
|
+
requestedDeltaMinutes: parsed.deltaMinutes,
|
|
24
|
+
appliedDeltaMinutes: input.appliedDeltaMinutes,
|
|
25
|
+
note: parsed.note,
|
|
26
|
+
actor: activity.actor ?? null,
|
|
27
|
+
source: activity.source,
|
|
28
|
+
createdAt: now.toISOString()
|
|
29
|
+
});
|
|
30
|
+
getDatabase()
|
|
31
|
+
.prepare(`INSERT INTO work_adjustments (
|
|
32
|
+
id,
|
|
33
|
+
entity_type,
|
|
34
|
+
entity_id,
|
|
35
|
+
requested_delta_minutes,
|
|
36
|
+
applied_delta_minutes,
|
|
37
|
+
note,
|
|
38
|
+
actor,
|
|
39
|
+
source,
|
|
40
|
+
created_at
|
|
41
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
42
|
+
.run(adjustment.id, adjustment.entityType, adjustment.entityId, adjustment.requestedDeltaMinutes, adjustment.appliedDeltaMinutes, adjustment.note, adjustment.actor, adjustment.source, adjustment.createdAt);
|
|
43
|
+
return adjustment;
|
|
44
|
+
}
|
|
45
|
+
export function listWorkAdjustmentsForEntity(entityType, entityId, limit = 50) {
|
|
46
|
+
const rows = getDatabase()
|
|
47
|
+
.prepare(`SELECT
|
|
48
|
+
id,
|
|
49
|
+
entity_type,
|
|
50
|
+
entity_id,
|
|
51
|
+
requested_delta_minutes,
|
|
52
|
+
applied_delta_minutes,
|
|
53
|
+
note,
|
|
54
|
+
actor,
|
|
55
|
+
source,
|
|
56
|
+
created_at
|
|
57
|
+
FROM work_adjustments
|
|
58
|
+
WHERE entity_type = ? AND entity_id = ?
|
|
59
|
+
ORDER BY created_at DESC
|
|
60
|
+
LIMIT ?`)
|
|
61
|
+
.all(entityType, entityId, limit);
|
|
62
|
+
return rows.map(mapWorkAdjustment);
|
|
63
|
+
}
|
|
64
|
+
export function getWorkAdjustmentSecondsByEntity(entityType, entityId) {
|
|
65
|
+
const row = getDatabase()
|
|
66
|
+
.prepare(`SELECT COALESCE(SUM(applied_delta_minutes), 0) AS total_minutes
|
|
67
|
+
FROM work_adjustments
|
|
68
|
+
WHERE entity_type = ? AND entity_id = ?`)
|
|
69
|
+
.get(entityType, entityId);
|
|
70
|
+
return Math.trunc((row?.total_minutes ?? 0) * 60);
|
|
71
|
+
}
|
|
72
|
+
function getWorkAdjustmentSecondsMap(entityType, entityIds) {
|
|
73
|
+
if (entityIds.length === 0) {
|
|
74
|
+
return new Map();
|
|
75
|
+
}
|
|
76
|
+
const placeholders = entityIds.map(() => "?").join(", ");
|
|
77
|
+
const rows = getDatabase()
|
|
78
|
+
.prepare(`SELECT entity_id, COALESCE(SUM(applied_delta_minutes), 0) AS total_minutes
|
|
79
|
+
FROM work_adjustments
|
|
80
|
+
WHERE entity_type = ? AND entity_id IN (${placeholders})
|
|
81
|
+
GROUP BY entity_id`)
|
|
82
|
+
.all(entityType, ...entityIds);
|
|
83
|
+
return new Map(rows.map((row) => [row.entity_id, Math.trunc(row.total_minutes * 60)]));
|
|
84
|
+
}
|
|
85
|
+
function listWorkAdjustmentSecondsMap(entityType) {
|
|
86
|
+
const rows = getDatabase()
|
|
87
|
+
.prepare(`SELECT entity_id, COALESCE(SUM(applied_delta_minutes), 0) AS total_minutes
|
|
88
|
+
FROM work_adjustments
|
|
89
|
+
WHERE entity_type = ?
|
|
90
|
+
GROUP BY entity_id`)
|
|
91
|
+
.all(entityType);
|
|
92
|
+
return new Map(rows.map((row) => [row.entity_id, Math.trunc(row.total_minutes * 60)]));
|
|
93
|
+
}
|
|
94
|
+
export function getTaskWorkAdjustmentSecondsMap(taskIds) {
|
|
95
|
+
return getWorkAdjustmentSecondsMap("task", taskIds);
|
|
96
|
+
}
|
|
97
|
+
export function getProjectWorkAdjustmentSecondsMap(projectIds) {
|
|
98
|
+
return getWorkAdjustmentSecondsMap("project", projectIds);
|
|
99
|
+
}
|
|
100
|
+
export function listTaskWorkAdjustmentSecondsMap() {
|
|
101
|
+
return listWorkAdjustmentSecondsMap("task");
|
|
102
|
+
}
|
|
103
|
+
export function listProjectWorkAdjustmentSecondsMap() {
|
|
104
|
+
return listWorkAdjustmentSecondsMap("project");
|
|
105
|
+
}
|