forge-openclaw-plugin 0.2.15 → 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 +39 -4
- package/dist/assets/{board-C_m78kvK.js → board-8L3uX7_O.js} +2 -2
- package/dist/assets/{board-C_m78kvK.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-CpZvZumD.js → motion-1GAqqi8M.js} +2 -2
- package/dist/assets/{motion-CpZvZumD.js.map → motion-1GAqqi8M.js.map} +1 -1
- package/dist/assets/{table-DtyXTw03.js → table-DBGlgRjk.js} +2 -2
- package/dist/assets/{table-DtyXTw03.js.map → table-DBGlgRjk.js.map} +1 -1
- package/dist/assets/{ui-BXbpiKyS.js → ui-iTluWjC4.js} +2 -2
- package/dist/assets/{ui-BXbpiKyS.js.map → ui-iTluWjC4.js.map} +1 -1
- package/dist/assets/{vendor-QBH6qVEe.js → vendor-BvM2F9Dp.js} +151 -81
- package/dist/assets/vendor-BvM2F9Dp.js.map +1 -0
- package/dist/assets/{viz-w-IMeueL.js → viz-CNeunkfu.js} +2 -2
- package/dist/assets/{viz-w-IMeueL.js.map → viz-CNeunkfu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/openclaw/local-runtime.js +142 -9
- package/dist/openclaw/parity.js +1 -0
- package/dist/openclaw/plugin-entry-shared.js +7 -1
- package/dist/openclaw/routes.js +7 -0
- package/dist/openclaw/tools.js +198 -16
- package/dist/server/app.js +2615 -251
- package/dist/server/managers/platform/secrets-manager.js +44 -1
- package/dist/server/managers/runtime.js +3 -1
- package/dist/server/openapi.js +2212 -170
- package/dist/server/repositories/calendar.js +1101 -0
- package/dist/server/repositories/deleted-entities.js +10 -2
- package/dist/server/repositories/habits.js +358 -0
- package/dist/server/repositories/notes.js +161 -28
- package/dist/server/repositories/projects.js +45 -13
- package/dist/server/repositories/rewards.js +176 -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/context.js +16 -6
- package/dist/server/services/dashboard.js +6 -3
- package/dist/server/services/entity-crud.js +116 -3
- package/dist/server/services/gamification.js +66 -18
- package/dist/server/services/insights.js +2 -1
- package/dist/server/services/projects.js +32 -8
- package/dist/server/services/reviews.js +17 -2
- package/dist/server/services/work-time.js +27 -0
- package/dist/server/types.js +1069 -45
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/003_habits.sql +30 -0
- package/server/migrations/004_habit_links.sql +8 -0
- package/server/migrations/005_habit_psyche_links.sql +24 -0
- 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 +130 -10
- package/skills/forge-openclaw/cron_jobs.md +395 -0
- package/dist/assets/index-BWtLtXwb.js +0 -36
- package/dist/assets/index-BWtLtXwb.js.map +0 -1
- package/dist/assets/index-Dp5GXY_z.css +0 -1
- package/dist/assets/vendor-QBH6qVEe.js.map +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { listActivityEvents } from "../repositories/activity-events.js";
|
|
2
2
|
import { listGoals } from "../repositories/goals.js";
|
|
3
|
+
import { listHabits } from "../repositories/habits.js";
|
|
4
|
+
import { listRewardLedger } from "../repositories/rewards.js";
|
|
3
5
|
import { listTags, listTagsByIds } from "../repositories/tags.js";
|
|
4
6
|
import { listTasks } from "../repositories/tasks.js";
|
|
5
7
|
import { getDashboard } from "./dashboard.js";
|
|
@@ -109,6 +111,7 @@ export function getOverviewContext(now = new Date()) {
|
|
|
109
111
|
const dashboard = getDashboard();
|
|
110
112
|
const focusTasks = dashboard.tasks.filter((task) => task.status === "focus" || task.status === "in_progress").length;
|
|
111
113
|
const overdueTasks = dashboard.tasks.filter((task) => task.status !== "done" && task.dueDate !== null && task.dueDate < now.toISOString().slice(0, 10)).length;
|
|
114
|
+
const dueHabits = dashboard.habits.filter((habit) => habit.dueToday).slice(0, 6);
|
|
112
115
|
return overviewContextSchema.parse({
|
|
113
116
|
generatedAt: now.toISOString(),
|
|
114
117
|
strategicHeader: {
|
|
@@ -124,8 +127,9 @@ export function getOverviewContext(now = new Date()) {
|
|
|
124
127
|
projects: dashboard.projects.slice(0, 5),
|
|
125
128
|
activeGoals: dashboard.goals.filter((goal) => goal.status === "active").slice(0, 6),
|
|
126
129
|
topTasks: sortStrategicTasks(dashboard.tasks.filter((task) => task.status !== "done")).slice(0, 6),
|
|
130
|
+
dueHabits,
|
|
127
131
|
recentEvidence: listActivityEvents({ limit: 12 }),
|
|
128
|
-
achievements: buildAchievementSignals(listGoals(), listTasks(), now),
|
|
132
|
+
achievements: buildAchievementSignals(listGoals(), listTasks(), listHabits(), now),
|
|
129
133
|
domainBalance: buildDomainBalance(listGoals(), listTasks()),
|
|
130
134
|
neglectedGoals: buildNeglectedGoals(listGoals(), listTasks(), now)
|
|
131
135
|
});
|
|
@@ -133,10 +137,12 @@ export function getOverviewContext(now = new Date()) {
|
|
|
133
137
|
export function getTodayContext(now = new Date()) {
|
|
134
138
|
const goals = listGoals();
|
|
135
139
|
const tasks = listTasks();
|
|
136
|
-
const
|
|
140
|
+
const habits = listHabits();
|
|
141
|
+
const gamification = buildGamificationProfile(goals, tasks, habits, now);
|
|
137
142
|
const inProgressTasks = sortStrategicTasks(tasks.filter((task) => task.status === "in_progress")).slice(0, 4);
|
|
138
143
|
const readyTasks = sortStrategicTasks(tasks.filter((task) => task.status === "focus" || task.status === "backlog")).slice(0, 4);
|
|
139
144
|
const deferredTasks = sortStrategicTasks(tasks.filter((task) => task.status === "blocked")).slice(0, 4);
|
|
145
|
+
const dueHabits = habits.filter((habit) => habit.dueToday).slice(0, 6);
|
|
140
146
|
const completedTasks = [...tasks]
|
|
141
147
|
.filter((task) => task.status === "done" && task.completedAt !== null)
|
|
142
148
|
.sort((left, right) => Date.parse(right.completedAt ?? "") - Date.parse(left.completedAt ?? ""))
|
|
@@ -185,13 +191,17 @@ export function getTodayContext(now = new Date()) {
|
|
|
185
191
|
completed: false
|
|
186
192
|
}
|
|
187
193
|
],
|
|
188
|
-
|
|
194
|
+
dueHabits,
|
|
195
|
+
milestoneRewards: buildMilestoneRewards(goals, tasks, habits, now),
|
|
196
|
+
recentHabitRewards: listRewardLedger({ entityType: "habit", limit: 8 }),
|
|
189
197
|
momentum: {
|
|
190
198
|
streakDays: gamification.streakDays,
|
|
191
199
|
momentumScore: gamification.momentumScore,
|
|
192
|
-
recoveryHint:
|
|
193
|
-
?
|
|
194
|
-
:
|
|
200
|
+
recoveryHint: dueHabits.length > 0
|
|
201
|
+
? `${dueHabits.length} habit${dueHabits.length === 1 ? "" : "s"} still need a check-in today. Closing one will keep momentum honest.`
|
|
202
|
+
: overdueCount > 0
|
|
203
|
+
? `Clear ${overdueCount} overdue task${overdueCount === 1 ? "" : "s"} to keep momentum from decaying.`
|
|
204
|
+
: "No overdue drag right now. Preserve the rhythm with one decisive completion."
|
|
195
205
|
}
|
|
196
206
|
});
|
|
197
207
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listGoals } from "../repositories/goals.js";
|
|
2
2
|
import { listActivityEvents } from "../repositories/activity-events.js";
|
|
3
|
+
import { listHabits } from "../repositories/habits.js";
|
|
3
4
|
import { buildNotesSummaryByEntity } from "../repositories/notes.js";
|
|
4
5
|
import { listTagsByIds, listTags } from "../repositories/tags.js";
|
|
5
6
|
import { listTasks } from "../repositories/tasks.js";
|
|
@@ -116,6 +117,7 @@ function buildGoalSummary(tasks, goalId) {
|
|
|
116
117
|
export function getDashboard() {
|
|
117
118
|
const goals = listGoals();
|
|
118
119
|
const tasks = listTasks();
|
|
120
|
+
const habits = listHabits();
|
|
119
121
|
const tags = listTags();
|
|
120
122
|
const now = new Date();
|
|
121
123
|
const weekStart = startOfWeek(now).toISOString();
|
|
@@ -150,9 +152,9 @@ export function getDashboard() {
|
|
|
150
152
|
const suggestedTags = tags.filter((tag) => ["value", "execution"].includes(tag.kind)).slice(0, 6);
|
|
151
153
|
const owners = [...new Set(tasks.map((task) => task.owner).filter(Boolean))].sort((left, right) => left.localeCompare(right));
|
|
152
154
|
const executionBuckets = buildExecutionBuckets(tasks, todayIso, weekEndIso);
|
|
153
|
-
const gamification = buildGamificationProfile(goals, tasks, now);
|
|
154
|
-
const achievements = buildAchievementSignals(goals, tasks, now);
|
|
155
|
-
const milestoneRewards = buildMilestoneRewards(goals, tasks, now);
|
|
155
|
+
const gamification = buildGamificationProfile(goals, tasks, habits, now);
|
|
156
|
+
const achievements = buildAchievementSignals(goals, tasks, habits, now);
|
|
157
|
+
const milestoneRewards = buildMilestoneRewards(goals, tasks, habits, now);
|
|
156
158
|
const recentActivity = listActivityEvents({ limit: 12 });
|
|
157
159
|
const notesSummaryByEntity = buildNotesSummaryByEntity();
|
|
158
160
|
return dashboardPayloadSchema.parse({
|
|
@@ -160,6 +162,7 @@ export function getDashboard() {
|
|
|
160
162
|
goals: goalCards,
|
|
161
163
|
projects,
|
|
162
164
|
tasks,
|
|
165
|
+
habits,
|
|
163
166
|
tags,
|
|
164
167
|
suggestedTags,
|
|
165
168
|
owners,
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { getDatabase, runInTransaction } from "../db.js";
|
|
2
2
|
import { createInsight, deleteInsight, getInsightById, listInsights, updateInsight } from "../repositories/collaboration.js";
|
|
3
|
+
import { createCalendarEvent, createTaskTimebox, createWorkBlockTemplate, deleteCalendarEvent, deleteTaskTimebox, deleteWorkBlockTemplate, getCalendarEventById, getTaskTimeboxById, getWorkBlockTemplateById, listCalendarEvents, listTaskTimeboxes, listWorkBlockTemplates, updateCalendarEvent, updateTaskTimebox, updateWorkBlockTemplate } from "../repositories/calendar.js";
|
|
3
4
|
import { createNote, deleteNote, getNoteById, listNotes, unlinkNotesForEntity, updateNote } from "../repositories/notes.js";
|
|
4
5
|
import { createBehaviorPatternSchema, createBehaviorSchema, createBeliefEntrySchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorPatternSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "../psyche-types.js";
|
|
5
6
|
import { buildSettingsBinPayload, cascadeSoftDeleteAnchoredCollaboration, clearDeletedEntityRecord, getDeletedEntityRecord, listDeletedEntities, restoreAnchoredCollaboration, restoreDeletedEntityRecord, upsertDeletedEntityRecord } from "../repositories/deleted-entities.js";
|
|
6
7
|
import { createGoal, deleteGoal, getGoalById, listGoals, updateGoal } from "../repositories/goals.js";
|
|
8
|
+
import { createHabit, deleteHabit, getHabitById, listHabits, updateHabit } from "../repositories/habits.js";
|
|
7
9
|
import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, deleteBehavior, deleteBehaviorPattern, deleteBeliefEntry, deleteEmotionDefinition, deleteEventType, deleteModeGuideSession, deleteModeProfile, deletePsycheValue, deleteTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "../repositories/psyche.js";
|
|
8
10
|
import { createProject, deleteProject, getProjectById, listProjects, updateProject } from "../repositories/projects.js";
|
|
9
11
|
import { createTag, deleteTag, getTagById, listTags, updateTag } from "../repositories/tags.js";
|
|
10
12
|
import { createTask, deleteTask, getTaskById, listTasks, updateTask } from "../repositories/tasks.js";
|
|
11
|
-
import { createGoalSchema, createInsightSchema, createNoteSchema, createProjectSchema, createTagSchema, createTaskSchema, updateGoalSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateTagSchema, updateTaskSchema } from "../types.js";
|
|
13
|
+
import { createCalendarEventSchema, createGoalSchema, createHabitSchema, createInsightSchema, createNoteSchema, createProjectSchema, createTaskTimeboxSchema, createTagSchema, createTaskSchema, createWorkBlockTemplateSchema, updateCalendarEventSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateTaskTimeboxSchema, updateTagSchema, updateTaskSchema, updateWorkBlockTemplateSchema } from "../types.js";
|
|
14
|
+
const ENTITY_CALENDAR_LIST_RANGE = {
|
|
15
|
+
from: "1970-01-01T00:00:00.000Z",
|
|
16
|
+
to: "2100-01-01T00:00:00.000Z"
|
|
17
|
+
};
|
|
12
18
|
class AtomicBatchRollback extends Error {
|
|
13
19
|
index;
|
|
14
20
|
code;
|
|
@@ -25,6 +31,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
25
31
|
goal: {
|
|
26
32
|
entityType: "goal",
|
|
27
33
|
routeBase: "/api/v1/goals",
|
|
34
|
+
deleteMode: "soft_default",
|
|
35
|
+
inBin: true,
|
|
28
36
|
list: () => listGoals(),
|
|
29
37
|
get: (id) => getGoalById(id),
|
|
30
38
|
create: (data, context) => createGoal(data, context),
|
|
@@ -34,6 +42,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
34
42
|
project: {
|
|
35
43
|
entityType: "project",
|
|
36
44
|
routeBase: "/api/v1/projects",
|
|
45
|
+
deleteMode: "soft_default",
|
|
46
|
+
inBin: true,
|
|
37
47
|
list: () => listProjects(),
|
|
38
48
|
get: (id) => getProjectById(id),
|
|
39
49
|
create: (data, context) => createProject(data, context),
|
|
@@ -43,15 +53,30 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
43
53
|
task: {
|
|
44
54
|
entityType: "task",
|
|
45
55
|
routeBase: "/api/v1/tasks",
|
|
56
|
+
deleteMode: "soft_default",
|
|
57
|
+
inBin: true,
|
|
46
58
|
list: () => listTasks(),
|
|
47
59
|
get: (id) => getTaskById(id),
|
|
48
60
|
create: (data, context) => createTask(data, context),
|
|
49
61
|
update: (id, patch, context) => updateTask(id, patch, context),
|
|
50
62
|
hardDelete: (id, context) => deleteTask(id, context)
|
|
51
63
|
},
|
|
64
|
+
habit: {
|
|
65
|
+
entityType: "habit",
|
|
66
|
+
routeBase: "/api/v1/habits",
|
|
67
|
+
deleteMode: "soft_default",
|
|
68
|
+
inBin: true,
|
|
69
|
+
list: () => listHabits(),
|
|
70
|
+
get: (id) => getHabitById(id),
|
|
71
|
+
create: (data, context) => createHabit(data, context),
|
|
72
|
+
update: (id, patch, context) => updateHabit(id, patch, context),
|
|
73
|
+
hardDelete: (id, context) => deleteHabit(id, context)
|
|
74
|
+
},
|
|
52
75
|
tag: {
|
|
53
76
|
entityType: "tag",
|
|
54
77
|
routeBase: "/api/v1/tags",
|
|
78
|
+
deleteMode: "soft_default",
|
|
79
|
+
inBin: true,
|
|
55
80
|
list: () => listTags(),
|
|
56
81
|
get: (id) => getTagById(id),
|
|
57
82
|
create: (data, context) => createTag(data, context),
|
|
@@ -61,6 +86,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
61
86
|
note: {
|
|
62
87
|
entityType: "note",
|
|
63
88
|
routeBase: "/api/v1/notes",
|
|
89
|
+
deleteMode: "soft_default",
|
|
90
|
+
inBin: true,
|
|
64
91
|
list: () => listNotes(),
|
|
65
92
|
get: (id) => getNoteById(id),
|
|
66
93
|
create: (data, context) => createNote(data, context),
|
|
@@ -70,15 +97,52 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
70
97
|
insight: {
|
|
71
98
|
entityType: "insight",
|
|
72
99
|
routeBase: "/api/v1/insights",
|
|
100
|
+
deleteMode: "soft_default",
|
|
101
|
+
inBin: true,
|
|
73
102
|
list: () => listInsights(),
|
|
74
103
|
get: (id) => getInsightById(id),
|
|
75
104
|
create: (data, context) => createInsight(data, context),
|
|
76
105
|
update: (id, patch, context) => updateInsight(id, patch, context),
|
|
77
106
|
hardDelete: (id, context) => deleteInsight(id, context)
|
|
78
107
|
},
|
|
108
|
+
calendar_event: {
|
|
109
|
+
entityType: "calendar_event",
|
|
110
|
+
routeBase: "/api/v1/calendar/events",
|
|
111
|
+
deleteMode: "immediate",
|
|
112
|
+
inBin: false,
|
|
113
|
+
list: () => listCalendarEvents(ENTITY_CALENDAR_LIST_RANGE),
|
|
114
|
+
get: (id) => getCalendarEventById(id),
|
|
115
|
+
create: (data) => createCalendarEvent(data),
|
|
116
|
+
update: (id, patch) => updateCalendarEvent(id, patch),
|
|
117
|
+
hardDelete: (id) => deleteCalendarEvent(id)
|
|
118
|
+
},
|
|
119
|
+
work_block_template: {
|
|
120
|
+
entityType: "work_block_template",
|
|
121
|
+
routeBase: "/api/v1/calendar/work-block-templates",
|
|
122
|
+
deleteMode: "immediate",
|
|
123
|
+
inBin: false,
|
|
124
|
+
list: () => listWorkBlockTemplates(),
|
|
125
|
+
get: (id) => getWorkBlockTemplateById(id),
|
|
126
|
+
create: (data) => createWorkBlockTemplate(data),
|
|
127
|
+
update: (id, patch) => updateWorkBlockTemplate(id, patch),
|
|
128
|
+
hardDelete: (id) => deleteWorkBlockTemplate(id)
|
|
129
|
+
},
|
|
130
|
+
task_timebox: {
|
|
131
|
+
entityType: "task_timebox",
|
|
132
|
+
routeBase: "/api/v1/calendar/timeboxes",
|
|
133
|
+
deleteMode: "immediate",
|
|
134
|
+
inBin: false,
|
|
135
|
+
list: () => listTaskTimeboxes(ENTITY_CALENDAR_LIST_RANGE),
|
|
136
|
+
get: (id) => getTaskTimeboxById(id),
|
|
137
|
+
create: (data) => createTaskTimebox(data),
|
|
138
|
+
update: (id, patch) => updateTaskTimebox(id, patch),
|
|
139
|
+
hardDelete: (id) => deleteTaskTimebox(id)
|
|
140
|
+
},
|
|
79
141
|
psyche_value: {
|
|
80
142
|
entityType: "psyche_value",
|
|
81
143
|
routeBase: "/api/v1/psyche/values",
|
|
144
|
+
deleteMode: "soft_default",
|
|
145
|
+
inBin: true,
|
|
82
146
|
list: () => listPsycheValues(),
|
|
83
147
|
get: (id) => getPsycheValueById(id),
|
|
84
148
|
create: (data, context) => createPsycheValue(data, context),
|
|
@@ -88,6 +152,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
88
152
|
behavior_pattern: {
|
|
89
153
|
entityType: "behavior_pattern",
|
|
90
154
|
routeBase: "/api/v1/psyche/patterns",
|
|
155
|
+
deleteMode: "soft_default",
|
|
156
|
+
inBin: true,
|
|
91
157
|
list: () => listBehaviorPatterns(),
|
|
92
158
|
get: (id) => getBehaviorPatternById(id),
|
|
93
159
|
create: (data, context) => createBehaviorPattern(data, context),
|
|
@@ -97,6 +163,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
97
163
|
behavior: {
|
|
98
164
|
entityType: "behavior",
|
|
99
165
|
routeBase: "/api/v1/psyche/behaviors",
|
|
166
|
+
deleteMode: "soft_default",
|
|
167
|
+
inBin: true,
|
|
100
168
|
list: () => listBehaviors(),
|
|
101
169
|
get: (id) => getBehaviorById(id),
|
|
102
170
|
create: (data, context) => createBehavior(data, context),
|
|
@@ -106,6 +174,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
106
174
|
belief_entry: {
|
|
107
175
|
entityType: "belief_entry",
|
|
108
176
|
routeBase: "/api/v1/psyche/beliefs",
|
|
177
|
+
deleteMode: "soft_default",
|
|
178
|
+
inBin: true,
|
|
109
179
|
list: () => listBeliefEntries(),
|
|
110
180
|
get: (id) => getBeliefEntryById(id),
|
|
111
181
|
create: (data, context) => createBeliefEntry(data, context),
|
|
@@ -115,6 +185,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
115
185
|
mode_profile: {
|
|
116
186
|
entityType: "mode_profile",
|
|
117
187
|
routeBase: "/api/v1/psyche/modes",
|
|
188
|
+
deleteMode: "soft_default",
|
|
189
|
+
inBin: true,
|
|
118
190
|
list: () => listModeProfiles(),
|
|
119
191
|
get: (id) => getModeProfileById(id),
|
|
120
192
|
create: (data, context) => createModeProfile(data, context),
|
|
@@ -124,6 +196,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
124
196
|
mode_guide_session: {
|
|
125
197
|
entityType: "mode_guide_session",
|
|
126
198
|
routeBase: "/api/v1/psyche/mode-guides",
|
|
199
|
+
deleteMode: "soft_default",
|
|
200
|
+
inBin: true,
|
|
127
201
|
list: () => listModeGuideSessions(200),
|
|
128
202
|
get: (id) => getModeGuideSessionById(id),
|
|
129
203
|
create: (data, context) => createModeGuideSession(data, context),
|
|
@@ -133,6 +207,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
133
207
|
event_type: {
|
|
134
208
|
entityType: "event_type",
|
|
135
209
|
routeBase: "/api/v1/psyche/event-types",
|
|
210
|
+
deleteMode: "soft_default",
|
|
211
|
+
inBin: true,
|
|
136
212
|
list: () => listEventTypes(),
|
|
137
213
|
get: (id) => getEventTypeById(id),
|
|
138
214
|
create: (data, context) => createEventType(data, context),
|
|
@@ -142,6 +218,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
142
218
|
emotion_definition: {
|
|
143
219
|
entityType: "emotion_definition",
|
|
144
220
|
routeBase: "/api/v1/psyche/emotions",
|
|
221
|
+
deleteMode: "soft_default",
|
|
222
|
+
inBin: true,
|
|
145
223
|
list: () => listEmotionDefinitions(),
|
|
146
224
|
get: (id) => getEmotionDefinitionById(id),
|
|
147
225
|
create: (data, context) => createEmotionDefinition(data, context),
|
|
@@ -151,6 +229,8 @@ const CRUD_ENTITY_CAPABILITIES = {
|
|
|
151
229
|
trigger_report: {
|
|
152
230
|
entityType: "trigger_report",
|
|
153
231
|
routeBase: "/api/v1/psyche/reports",
|
|
232
|
+
deleteMode: "soft_default",
|
|
233
|
+
inBin: true,
|
|
154
234
|
list: () => listTriggerReports(200),
|
|
155
235
|
get: (id) => getTriggerReportById(id),
|
|
156
236
|
create: (data, context) => createTriggerReport(data, context),
|
|
@@ -163,8 +243,8 @@ export function getCrudEntityCapabilityMatrix() {
|
|
|
163
243
|
entityType: capability.entityType,
|
|
164
244
|
routeBase: capability.routeBase,
|
|
165
245
|
pluginExposed: true,
|
|
166
|
-
deleteMode:
|
|
167
|
-
inBin:
|
|
246
|
+
deleteMode: capability.deleteMode,
|
|
247
|
+
inBin: capability.inBin
|
|
168
248
|
}));
|
|
169
249
|
}
|
|
170
250
|
function getCapability(entityType) {
|
|
@@ -174,9 +254,13 @@ const CREATE_ENTITY_SCHEMAS = {
|
|
|
174
254
|
goal: createGoalSchema,
|
|
175
255
|
project: createProjectSchema,
|
|
176
256
|
task: createTaskSchema,
|
|
257
|
+
habit: createHabitSchema,
|
|
177
258
|
tag: createTagSchema,
|
|
178
259
|
note: createNoteSchema,
|
|
179
260
|
insight: createInsightSchema,
|
|
261
|
+
calendar_event: createCalendarEventSchema,
|
|
262
|
+
work_block_template: createWorkBlockTemplateSchema,
|
|
263
|
+
task_timebox: createTaskTimeboxSchema,
|
|
180
264
|
psyche_value: createPsycheValueSchema,
|
|
181
265
|
behavior_pattern: createBehaviorPatternSchema,
|
|
182
266
|
behavior: createBehaviorSchema,
|
|
@@ -191,9 +275,13 @@ const UPDATE_ENTITY_SCHEMAS = {
|
|
|
191
275
|
goal: updateGoalSchema,
|
|
192
276
|
project: updateProjectSchema,
|
|
193
277
|
task: updateTaskSchema,
|
|
278
|
+
habit: updateHabitSchema,
|
|
194
279
|
tag: updateTagSchema,
|
|
195
280
|
note: updateNoteSchema,
|
|
196
281
|
insight: updateInsightSchema,
|
|
282
|
+
calendar_event: updateCalendarEventSchema,
|
|
283
|
+
work_block_template: updateWorkBlockTemplateSchema,
|
|
284
|
+
task_timebox: updateTaskTimeboxSchema,
|
|
197
285
|
psyche_value: updatePsycheValueSchema,
|
|
198
286
|
behavior_pattern: updateBehaviorPatternSchema,
|
|
199
287
|
behavior: updateBehaviorSchema,
|
|
@@ -312,6 +400,16 @@ function matchesLinkedTo(entityType, entity, linkedTo) {
|
|
|
312
400
|
return linkedTo.entityType === "goal" && entity.goalId === linkedTo.id;
|
|
313
401
|
case "task":
|
|
314
402
|
return (linkedTo.entityType === "goal" && entity.goalId === linkedTo.id) || (linkedTo.entityType === "project" && entity.projectId === linkedTo.id);
|
|
403
|
+
case "habit":
|
|
404
|
+
return ((linkedTo.entityType === "goal" && Array.isArray(entity.linkedGoalIds) && entity.linkedGoalIds.includes(linkedTo.id)) ||
|
|
405
|
+
(linkedTo.entityType === "project" && Array.isArray(entity.linkedProjectIds) && entity.linkedProjectIds.includes(linkedTo.id)) ||
|
|
406
|
+
(linkedTo.entityType === "task" && Array.isArray(entity.linkedTaskIds) && entity.linkedTaskIds.includes(linkedTo.id)) ||
|
|
407
|
+
(linkedTo.entityType === "psyche_value" && Array.isArray(entity.linkedValueIds) && entity.linkedValueIds.includes(linkedTo.id)) ||
|
|
408
|
+
(linkedTo.entityType === "behavior_pattern" && Array.isArray(entity.linkedPatternIds) && entity.linkedPatternIds.includes(linkedTo.id)) ||
|
|
409
|
+
(linkedTo.entityType === "behavior" && Array.isArray(entity.linkedBehaviorIds) && entity.linkedBehaviorIds.includes(linkedTo.id)) ||
|
|
410
|
+
(linkedTo.entityType === "belief_entry" && Array.isArray(entity.linkedBeliefIds) && entity.linkedBeliefIds.includes(linkedTo.id)) ||
|
|
411
|
+
(linkedTo.entityType === "mode_profile" && Array.isArray(entity.linkedModeIds) && entity.linkedModeIds.includes(linkedTo.id)) ||
|
|
412
|
+
(linkedTo.entityType === "trigger_report" && Array.isArray(entity.linkedReportIds) && entity.linkedReportIds.includes(linkedTo.id)));
|
|
315
413
|
case "note":
|
|
316
414
|
return (Array.isArray(entity.links) &&
|
|
317
415
|
entity.links.some((link) => typeof link === "object" &&
|
|
@@ -322,6 +420,17 @@ function matchesLinkedTo(entityType, entity, linkedTo) {
|
|
|
322
420
|
link.entityId === linkedTo.id));
|
|
323
421
|
case "insight":
|
|
324
422
|
return entity.entityType === linkedTo.entityType && entity.entityId === linkedTo.id;
|
|
423
|
+
case "calendar_event":
|
|
424
|
+
return (Array.isArray(entity.links) &&
|
|
425
|
+
entity.links.some((link) => typeof link === "object" &&
|
|
426
|
+
link !== null &&
|
|
427
|
+
"entityType" in link &&
|
|
428
|
+
"entityId" in link &&
|
|
429
|
+
link.entityType === linkedTo.entityType &&
|
|
430
|
+
link.entityId === linkedTo.id));
|
|
431
|
+
case "task_timebox":
|
|
432
|
+
return ((linkedTo.entityType === "task" && entity.taskId === linkedTo.id) ||
|
|
433
|
+
(linkedTo.entityType === "project" && entity.projectId === linkedTo.id));
|
|
325
434
|
case "psyche_value":
|
|
326
435
|
return ((linkedTo.entityType === "goal" && Array.isArray(entity.linkedGoalIds) && entity.linkedGoalIds.includes(linkedTo.id)) ||
|
|
327
436
|
(linkedTo.entityType === "project" && Array.isArray(entity.linkedProjectIds) && entity.linkedProjectIds.includes(linkedTo.id)) ||
|
|
@@ -390,6 +499,10 @@ function purgeAnchoredCollaboration(entityType, entityId) {
|
|
|
390
499
|
export function deleteEntity(entityType, id, options, context) {
|
|
391
500
|
const capability = getCapability(entityType);
|
|
392
501
|
const mode = options.mode ?? "soft";
|
|
502
|
+
if (capability.deleteMode === "immediate") {
|
|
503
|
+
clearDeletedEntityRecord(entityType, id);
|
|
504
|
+
return capability.hardDelete(id, context);
|
|
505
|
+
}
|
|
393
506
|
const existing = capability.get(id);
|
|
394
507
|
if (!existing) {
|
|
395
508
|
const deleted = getDeletedEntityRecord(entityType, id);
|
|
@@ -12,9 +12,15 @@ function startOfWeek(date) {
|
|
|
12
12
|
function dayKey(isoDate) {
|
|
13
13
|
return isoDate.slice(0, 10);
|
|
14
14
|
}
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
function isAlignedHabitCheckIn(habit, checkIn) {
|
|
16
|
+
return (habit.polarity === "positive" && checkIn.status === "done") || (habit.polarity === "negative" && checkIn.status === "missed");
|
|
17
|
+
}
|
|
18
|
+
function calculateStreak(tasks, habits, now) {
|
|
19
|
+
const completedDays = new Set([
|
|
20
|
+
...tasks
|
|
21
|
+
.flatMap((task) => (task.status === "done" && task.completedAt !== null ? [dayKey(task.completedAt)] : [])),
|
|
22
|
+
...habits.flatMap((habit) => habit.checkIns.filter((checkIn) => isAlignedHabitCheckIn(habit, checkIn)).map((checkIn) => checkIn.dateKey))
|
|
23
|
+
]);
|
|
18
24
|
if (completedDays.size === 0) {
|
|
19
25
|
return 0;
|
|
20
26
|
}
|
|
@@ -41,17 +47,27 @@ function latestCompletionForTasks(tasks) {
|
|
|
41
47
|
.flatMap((task) => (task.completedAt ? [task.completedAt] : []))
|
|
42
48
|
.sort((left, right) => Date.parse(right) - Date.parse(left))[0] ?? null;
|
|
43
49
|
}
|
|
44
|
-
|
|
50
|
+
function latestAlignedHabitAt(habits) {
|
|
51
|
+
return habits
|
|
52
|
+
.flatMap((habit) => habit.checkIns
|
|
53
|
+
.filter((checkIn) => isAlignedHabitCheckIn(habit, checkIn))
|
|
54
|
+
.map((checkIn) => checkIn.createdAt))
|
|
55
|
+
.sort((left, right) => Date.parse(right) - Date.parse(left))[0] ?? null;
|
|
56
|
+
}
|
|
57
|
+
export function buildGamificationProfile(goals, tasks, habits, now = new Date()) {
|
|
45
58
|
const weekStart = startOfWeek(now).toISOString();
|
|
46
59
|
const doneTasks = tasks.filter((task) => task.status === "done");
|
|
47
60
|
const totalXp = getTotalXp();
|
|
48
61
|
const weeklyXp = getWeeklyXp(weekStart);
|
|
49
62
|
const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress").length;
|
|
50
63
|
const overdueTasks = tasks.filter((task) => task.status !== "done" && task.dueDate !== null && task.dueDate < now.toISOString().slice(0, 10)).length;
|
|
64
|
+
const dueHabits = habits.filter((habit) => habit.dueToday).length;
|
|
65
|
+
const alignedHabitCheckIns = habits.flatMap((habit) => habit.checkIns.filter((checkIn) => isAlignedHabitCheckIn(habit, checkIn)));
|
|
66
|
+
const habitMomentum = habits.reduce((sum, habit) => sum + habit.streakCount * 3 + (habit.dueToday ? -4 : 2), 0);
|
|
51
67
|
const alignedDonePoints = doneTasks
|
|
52
68
|
.filter((task) => task.goalId !== null && task.tagIds.length > 0)
|
|
53
69
|
.reduce((sum, task) => sum + task.points, 0);
|
|
54
|
-
const streakDays = calculateStreak(tasks, now);
|
|
70
|
+
const streakDays = calculateStreak(tasks, habits, now);
|
|
55
71
|
const levelState = calculateLevel(totalXp);
|
|
56
72
|
const goalScores = goals
|
|
57
73
|
.map((goal) => ({
|
|
@@ -69,17 +85,20 @@ export function buildGamificationProfile(goals, tasks, now = new Date()) {
|
|
|
69
85
|
weeklyXp,
|
|
70
86
|
streakDays,
|
|
71
87
|
comboMultiplier: Number((1 + Math.min(0.75, streakDays * 0.05)).toFixed(2)),
|
|
72
|
-
momentumScore: Math.max(0, Math.min(100, Math.round(weeklyXp / 6 + alignedDonePoints / 20 + focusTasks * 5 - overdueTasks * 9))),
|
|
88
|
+
momentumScore: Math.max(0, Math.min(100, Math.round(weeklyXp / 6 + alignedDonePoints / 20 + focusTasks * 5 + alignedHabitCheckIns.length * 4 + habitMomentum - overdueTasks * 9 - dueHabits * 3))),
|
|
73
89
|
topGoalId: topGoal?.goalId ?? null,
|
|
74
90
|
topGoalTitle: topGoal?.goalTitle ?? null
|
|
75
91
|
});
|
|
76
92
|
}
|
|
77
|
-
export function buildAchievementSignals(goals, tasks, now = new Date()) {
|
|
78
|
-
const profile = buildGamificationProfile(goals, tasks, now);
|
|
93
|
+
export function buildAchievementSignals(goals, tasks, habits, now = new Date()) {
|
|
94
|
+
const profile = buildGamificationProfile(goals, tasks, habits, now);
|
|
79
95
|
const doneTasks = tasks.filter((task) => task.status === "done");
|
|
80
96
|
const alignedDoneTasks = doneTasks.filter((task) => task.goalId !== null && task.tagIds.length > 0);
|
|
81
97
|
const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
|
|
82
98
|
const highValueGoals = goals.filter((goal) => doneTasks.some((task) => task.goalId === goal.id));
|
|
99
|
+
const alignedHabitCount = habits.reduce((sum, habit) => sum + habit.checkIns.filter((checkIn) => isAlignedHabitCheckIn(habit, checkIn)).length, 0);
|
|
100
|
+
const topHabitStreak = Math.max(0, ...habits.map((habit) => habit.streakCount));
|
|
101
|
+
const latestHabitWin = latestAlignedHabitAt(habits);
|
|
83
102
|
return [
|
|
84
103
|
{
|
|
85
104
|
id: "streak-operator",
|
|
@@ -125,15 +144,34 @@ export function buildAchievementSignals(goals, tasks, now = new Date()) {
|
|
|
125
144
|
progressLabel: `${Math.min(focusTasks.length, 1)}/1 live directives`,
|
|
126
145
|
unlocked: focusTasks.length > 0,
|
|
127
146
|
unlockedAt: focusTasks.length > 0 ? now.toISOString() : null
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "habit-keeper",
|
|
150
|
+
title: "Habit Keeper",
|
|
151
|
+
summary: "Turn recurring behavior into visible operating evidence.",
|
|
152
|
+
tier: alignedHabitCount >= 12 ? "gold" : alignedHabitCount >= 6 ? "silver" : "bronze",
|
|
153
|
+
progressLabel: `${Math.min(alignedHabitCount, 12)}/12 aligned habit wins`,
|
|
154
|
+
unlocked: alignedHabitCount >= 12,
|
|
155
|
+
unlockedAt: alignedHabitCount >= 12 ? latestHabitWin : null
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "ritual-pressure",
|
|
159
|
+
title: "Ritual Pressure",
|
|
160
|
+
summary: "Keep one habit alive long enough that it changes the texture of the week.",
|
|
161
|
+
tier: topHabitStreak >= 10 ? "gold" : "silver",
|
|
162
|
+
progressLabel: `${Math.min(topHabitStreak, 10)}/10 habit streak`,
|
|
163
|
+
unlocked: topHabitStreak >= 10,
|
|
164
|
+
unlockedAt: topHabitStreak >= 10 ? latestHabitWin : null
|
|
128
165
|
}
|
|
129
166
|
].map((achievement) => achievementSignalSchema.parse(achievement));
|
|
130
167
|
}
|
|
131
|
-
export function buildMilestoneRewards(goals, tasks, now = new Date()) {
|
|
132
|
-
const profile = buildGamificationProfile(goals, tasks, now);
|
|
168
|
+
export function buildMilestoneRewards(goals, tasks, habits, now = new Date()) {
|
|
169
|
+
const profile = buildGamificationProfile(goals, tasks, habits, now);
|
|
133
170
|
const doneTasks = tasks.filter((task) => task.status === "done");
|
|
134
171
|
const topGoal = profile.topGoalId ? goals.find((goal) => goal.id === profile.topGoalId) ?? null : null;
|
|
135
172
|
const topGoalXp = topGoal ? doneTasks.filter((task) => task.goalId === topGoal.id).reduce((sum, task) => sum + task.points, 0) : 0;
|
|
136
173
|
const completedToday = doneTasks.filter((task) => task.completedAt?.slice(0, 10) === now.toISOString().slice(0, 10)).length;
|
|
174
|
+
const alignedHabitCount = habits.reduce((sum, habit) => sum + habit.checkIns.filter((checkIn) => isAlignedHabitCheckIn(habit, checkIn)).length, 0);
|
|
137
175
|
return [
|
|
138
176
|
{
|
|
139
177
|
id: "next-level",
|
|
@@ -174,13 +212,23 @@ export function buildMilestoneRewards(goals, tasks, now = new Date()) {
|
|
|
174
212
|
current: topGoal ? topGoalXp : 0,
|
|
175
213
|
target: topGoal ? topGoal.targetPoints : 1,
|
|
176
214
|
completed: topGoal ? topGoalXp >= topGoal.targetPoints : false
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: "habit-mass",
|
|
218
|
+
title: "Habit mass threshold",
|
|
219
|
+
summary: "Make recurring behavior part of the same reward engine as tasks and projects.",
|
|
220
|
+
rewardLabel: "Consistency cache +75 xp",
|
|
221
|
+
progressLabel: `${Math.min(alignedHabitCount, 14)}/14 aligned habit check-ins`,
|
|
222
|
+
current: alignedHabitCount,
|
|
223
|
+
target: 14,
|
|
224
|
+
completed: alignedHabitCount >= 14
|
|
177
225
|
}
|
|
178
226
|
].map((reward) => milestoneRewardSchema.parse(reward));
|
|
179
227
|
}
|
|
180
|
-
export function buildXpMomentumPulse(goals, tasks, now = new Date()) {
|
|
181
|
-
const profile = buildGamificationProfile(goals, tasks, now);
|
|
182
|
-
const achievements = buildAchievementSignals(goals, tasks, now);
|
|
183
|
-
const milestoneRewards = buildMilestoneRewards(goals, tasks, now);
|
|
228
|
+
export function buildXpMomentumPulse(goals, tasks, habits, now = new Date()) {
|
|
229
|
+
const profile = buildGamificationProfile(goals, tasks, habits, now);
|
|
230
|
+
const achievements = buildAchievementSignals(goals, tasks, habits, now);
|
|
231
|
+
const milestoneRewards = buildMilestoneRewards(goals, tasks, habits, now);
|
|
184
232
|
const nextMilestone = milestoneRewards.find((reward) => !reward.completed) ?? milestoneRewards[0] ?? null;
|
|
185
233
|
const unlockedAchievements = achievements.filter((achievement) => achievement.unlocked).length;
|
|
186
234
|
const status = profile.momentumScore >= 80 ? "surging" : profile.momentumScore >= 60 ? "steady" : "recovering";
|
|
@@ -206,10 +254,10 @@ export function buildXpMomentumPulse(goals, tasks, now = new Date()) {
|
|
|
206
254
|
nextMilestoneLabel: nextMilestone?.rewardLabel ?? "Keep building visible momentum"
|
|
207
255
|
};
|
|
208
256
|
}
|
|
209
|
-
export function buildGamificationOverview(goals, tasks, now = new Date()) {
|
|
257
|
+
export function buildGamificationOverview(goals, tasks, habits, now = new Date()) {
|
|
210
258
|
return gamificationOverviewSchema.parse({
|
|
211
|
-
profile: buildGamificationProfile(goals, tasks, now),
|
|
212
|
-
achievements: buildAchievementSignals(goals, tasks, now),
|
|
213
|
-
milestoneRewards: buildMilestoneRewards(goals, tasks, now)
|
|
259
|
+
profile: buildGamificationProfile(goals, tasks, habits, now),
|
|
260
|
+
achievements: buildAchievementSignals(goals, tasks, habits, now),
|
|
261
|
+
milestoneRewards: buildMilestoneRewards(goals, tasks, habits, now)
|
|
214
262
|
});
|
|
215
263
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { listActivityEvents } from "../repositories/activity-events.js";
|
|
2
2
|
import { listInsights } from "../repositories/collaboration.js";
|
|
3
3
|
import { listGoals } from "../repositories/goals.js";
|
|
4
|
+
import { listHabits } from "../repositories/habits.js";
|
|
4
5
|
import { listTasks } from "../repositories/tasks.js";
|
|
5
6
|
import { getOverviewContext } from "./context.js";
|
|
6
7
|
import { buildGamificationProfile } from "./gamification.js";
|
|
@@ -33,7 +34,7 @@ function buildHeatmap(tasks, now) {
|
|
|
33
34
|
export function getInsightsPayload(now = new Date()) {
|
|
34
35
|
const goals = listGoals();
|
|
35
36
|
const tasks = listTasks();
|
|
36
|
-
const gamification = buildGamificationProfile(goals, tasks, now);
|
|
37
|
+
const gamification = buildGamificationProfile(goals, tasks, listHabits(), now);
|
|
37
38
|
const overview = getOverviewContext(now);
|
|
38
39
|
const activity = listActivityEvents({ limit: 60 });
|
|
39
40
|
const trends = Array.from({ length: 6 }, (_, offset) => {
|
|
@@ -3,6 +3,7 @@ import { getGoalById, listGoals } from "../repositories/goals.js";
|
|
|
3
3
|
import { buildNotesSummaryByEntity } from "../repositories/notes.js";
|
|
4
4
|
import { listProjects } from "../repositories/projects.js";
|
|
5
5
|
import { listTasks } from "../repositories/tasks.js";
|
|
6
|
+
import { listProjectWorkAdjustmentSecondsMap } from "../repositories/work-adjustments.js";
|
|
6
7
|
import { emptyTaskTimeSummary } from "./work-time.js";
|
|
7
8
|
import { projectBoardPayloadSchema, projectSummarySchema } from "../types.js";
|
|
8
9
|
function projectTaskSummary(tasks) {
|
|
@@ -33,25 +34,48 @@ function projectTaskSummary(tasks) {
|
|
|
33
34
|
nextTaskId: nextTask?.id ?? null,
|
|
34
35
|
nextTaskTitle: nextTask?.title ?? null,
|
|
35
36
|
momentumLabel,
|
|
36
|
-
time: tasks.reduce((summary, task) =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
time: tasks.reduce((summary, task) => {
|
|
38
|
+
const liveTrackedSeconds = task.time.liveTrackedSeconds ?? 0;
|
|
39
|
+
const liveCreditedSeconds = task.time.liveCreditedSeconds ?? 0;
|
|
40
|
+
const manualAdjustedSeconds = task.time.manualAdjustedSeconds ?? 0;
|
|
41
|
+
return {
|
|
42
|
+
totalTrackedSeconds: summary.totalTrackedSeconds + task.time.totalTrackedSeconds,
|
|
43
|
+
totalCreditedSeconds: Math.round((summary.totalCreditedSeconds + task.time.totalCreditedSeconds) * 100) / 100,
|
|
44
|
+
liveTrackedSeconds: summary.liveTrackedSeconds + liveTrackedSeconds,
|
|
45
|
+
liveCreditedSeconds: Math.round((summary.liveCreditedSeconds + liveCreditedSeconds) * 100) / 100,
|
|
46
|
+
manualAdjustedSeconds: summary.manualAdjustedSeconds + manualAdjustedSeconds,
|
|
47
|
+
activeRunCount: summary.activeRunCount + task.time.activeRunCount,
|
|
48
|
+
hasCurrentRun: summary.hasCurrentRun || task.time.hasCurrentRun,
|
|
49
|
+
currentRunId: summary.currentRunId ?? task.time.currentRunId
|
|
50
|
+
};
|
|
51
|
+
}, emptyTaskTimeSummary())
|
|
43
52
|
};
|
|
44
53
|
}
|
|
45
54
|
export function listProjectSummaries(filters = {}) {
|
|
46
55
|
const goals = new Map(listGoals().map((goal) => [goal.id, goal]));
|
|
47
56
|
const tasks = listTasks();
|
|
57
|
+
const projectAdjustmentSeconds = listProjectWorkAdjustmentSecondsMap();
|
|
48
58
|
return listProjects(filters).map((project) => {
|
|
49
59
|
const goal = goals.get(project.goalId);
|
|
50
60
|
const projectTasks = tasks.filter((task) => task.projectId === project.id);
|
|
61
|
+
const taskSummary = projectTaskSummary(projectTasks);
|
|
62
|
+
const projectAdjustmentSecondsTotal = projectAdjustmentSeconds.get(project.id) ?? 0;
|
|
63
|
+
const manualAdjustedSeconds = (taskSummary.time.manualAdjustedSeconds ?? 0) + projectAdjustmentSecondsTotal;
|
|
64
|
+
const time = {
|
|
65
|
+
totalTrackedSeconds: Math.max(0, taskSummary.time.totalTrackedSeconds + projectAdjustmentSecondsTotal),
|
|
66
|
+
totalCreditedSeconds: Math.round(Math.max(0, taskSummary.time.totalCreditedSeconds + projectAdjustmentSecondsTotal) * 100) / 100,
|
|
67
|
+
liveTrackedSeconds: taskSummary.time.liveTrackedSeconds ?? 0,
|
|
68
|
+
liveCreditedSeconds: taskSummary.time.liveCreditedSeconds ?? 0,
|
|
69
|
+
manualAdjustedSeconds,
|
|
70
|
+
activeRunCount: taskSummary.time.activeRunCount,
|
|
71
|
+
hasCurrentRun: taskSummary.time.hasCurrentRun,
|
|
72
|
+
currentRunId: taskSummary.time.currentRunId
|
|
73
|
+
};
|
|
51
74
|
return projectSummarySchema.parse({
|
|
52
75
|
...project,
|
|
53
76
|
goalTitle: goal?.title ?? "Unknown life goal",
|
|
54
|
-
...
|
|
77
|
+
...taskSummary,
|
|
78
|
+
time
|
|
55
79
|
});
|
|
56
80
|
});
|
|
57
81
|
}
|