@nightkatana/kronosys-app 1.0.0-beta.0
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 +81 -0
- package/app/api/action/route.ts +16 -0
- package/app/api/backup/route.ts +84 -0
- package/app/api/health/route.ts +22 -0
- package/app/api/state/route.ts +27 -0
- package/app/apple-icon.png +0 -0
- package/app/changelog/page.tsx +122 -0
- package/app/globals.css +210 -0
- package/app/guide/layout.tsx +11 -0
- package/app/guide/page.tsx +278 -0
- package/app/icon.png +0 -0
- package/app/layout.tsx +77 -0
- package/app/licenses/layout.tsx +11 -0
- package/app/licenses/page.tsx +246 -0
- package/app/manifest.ts +32 -0
- package/app/page.tsx +1610 -0
- package/app/reporting/page.tsx +2943 -0
- package/app/settings/layout.tsx +10 -0
- package/app/settings/page.tsx +3518 -0
- package/bin/kronosys.mjs +46 -0
- package/components/KronosysPackageVersionProvider.tsx +19 -0
- package/components/KronosysPayloadProvider.tsx +109 -0
- package/components/PwaRegister.tsx +25 -0
- package/components/SiteLegalFooter.tsx +21 -0
- package/components/ThemeProvider.tsx +78 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +394 -0
- package/components/dashboard/AppShellRouteNav.tsx +131 -0
- package/components/dashboard/AppVersionStamp.tsx +16 -0
- package/components/dashboard/DashboardCollapsibleSection.tsx +57 -0
- package/components/dashboard/DashboardColumnHintsBanner.tsx +159 -0
- package/components/dashboard/DashboardCommandCenter.tsx +470 -0
- package/components/dashboard/DashboardLangGateModal.tsx +118 -0
- package/components/dashboard/DashboardLoadingOverlay.tsx +42 -0
- package/components/dashboard/DashboardSimpleModal.tsx +337 -0
- package/components/dashboard/DashboardSuspenseFallback.tsx +52 -0
- package/components/dashboard/DashboardToastProvider.tsx +64 -0
- package/components/dashboard/DashboardTour.tsx +435 -0
- package/components/dashboard/DeferredDescriptionPopoverWrap.tsx +39 -0
- package/components/dashboard/DeleteSessionModal.tsx +130 -0
- package/components/dashboard/DescriptionTooltipPortaled.tsx +31 -0
- package/components/dashboard/GitIdentityQuickSetupModal.tsx +211 -0
- package/components/dashboard/HeaderIntegrationBadges.tsx +69 -0
- package/components/dashboard/InlineMetricHelpTrigger.tsx +102 -0
- package/components/dashboard/IssuePickerModal.tsx +168 -0
- package/components/dashboard/KronoFocusPanel.tsx +834 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +357 -0
- package/components/dashboard/KronosysTimePopoverField.tsx +233 -0
- package/components/dashboard/LanguageMenu.tsx +123 -0
- package/components/dashboard/MongoMirrorSyncLine.tsx +57 -0
- package/components/dashboard/NewSessionScopeModal.tsx +410 -0
- package/components/dashboard/PageRefreshButton.tsx +130 -0
- package/components/dashboard/PlainHelpPopover.tsx +97 -0
- package/components/dashboard/ReportingPageToc.tsx +68 -0
- package/components/dashboard/ReportingTour.tsx +342 -0
- package/components/dashboard/SavedProjectPicker.tsx +92 -0
- package/components/dashboard/SavedTagPicker.tsx +115 -0
- package/components/dashboard/ScrollToTopFab.tsx +41 -0
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +630 -0
- package/components/dashboard/SessionEndReasonEditor.tsx +114 -0
- package/components/dashboard/SessionListPanel.tsx +320 -0
- package/components/dashboard/SessionLocMetricsSection.tsx +128 -0
- package/components/dashboard/SettingsTagsProjectsSection.tsx +993 -0
- package/components/dashboard/SettingsTour.tsx +332 -0
- package/components/dashboard/TagPills.tsx +149 -0
- package/components/dashboard/TagsHelpTrigger.tsx +84 -0
- package/components/dashboard/TaskFocusPanel.tsx +1261 -0
- package/components/dashboard/TaskSessionLiveCard.tsx +832 -0
- package/components/dashboard/TaskSubtasksBlock.tsx +748 -0
- package/components/dashboard/ThemeToggle.test.tsx +26 -0
- package/components/dashboard/ThemeToggle.tsx +36 -0
- package/components/dashboard/UserGuideBodyText.tsx +62 -0
- package/components/dashboard/WorkspaceGitRepoCard.tsx +191 -0
- package/components/dashboard/taskFieldStyles.ts +139 -0
- package/components/dashboard/useAnchoredFloatingPortalStyle.ts +71 -0
- package/components/dashboard/useDescriptionPopoverAfterMs.ts +220 -0
- package/components/dashboard/useKronoFocusLiveSeconds.ts +36 -0
- package/components/dashboard/useSmoothStopwatchMs.ts +25 -0
- package/lib/appShellHeaderClasses.ts +12 -0
- package/lib/backupCsvExport.test.ts +149 -0
- package/lib/backupCsvExport.ts +392 -0
- package/lib/changelogCopy.ts +34 -0
- package/lib/concurrentTaskStartPreference.ts +29 -0
- package/lib/dashboardClockFormat.ts +13 -0
- package/lib/dashboardColumnChrome.ts +3 -0
- package/lib/dashboardColumnHintsStorage.ts +57 -0
- package/lib/dashboardCopy.ts +1831 -0
- package/lib/dashboardDetachedUrlHintStorage.ts +24 -0
- package/lib/dashboardGitIdentityBannerStorage.ts +36 -0
- package/lib/dashboardLangStorage.ts +72 -0
- package/lib/dashboardQuickSearch.ts +476 -0
- package/lib/dashboardQuickSearchQuery.test.ts +63 -0
- package/lib/dashboardQuickSearchQuery.ts +179 -0
- package/lib/dashboardSessionNav.ts +33 -0
- package/lib/dashboardShortcuts.ts +268 -0
- package/lib/dashboardTimeZone.ts +91 -0
- package/lib/dashboardTourStorage.ts +68 -0
- package/lib/dataDir.test.ts +87 -0
- package/lib/dataDir.ts +83 -0
- package/lib/devDataPreferenceFile.ts +55 -0
- package/lib/devDataRuntimeInfo.ts +34 -0
- package/lib/formatIsoShort.test.ts +46 -0
- package/lib/formatIsoShort.ts +29 -0
- package/lib/generatedUserChangelog.ts +34 -0
- package/lib/gitlabIssueSearch.ts +8 -0
- package/lib/kronoFocusDurationHistory.ts +71 -0
- package/lib/kronoFocusRhythm.test.ts +130 -0
- package/lib/kronoFocusRhythm.ts +46 -0
- package/lib/kronoFocusTimerUrgency.test.ts +74 -0
- package/lib/kronoFocusTimerUrgency.ts +24 -0
- package/lib/kronosysApi.ts +143 -0
- package/lib/legacyEditorPayloadKeys.ts +52 -0
- package/lib/legacyKronoFocusStorageKeys.test.ts +29 -0
- package/lib/legacyKronoFocusStorageKeys.ts +32 -0
- package/lib/licensesCopy.ts +128 -0
- package/lib/openPlainTextInNewTab.ts +49 -0
- package/lib/readKronosysPackageVersion.ts +10 -0
- package/lib/reportingAggregate.test.ts +325 -0
- package/lib/reportingAggregate.ts +819 -0
- package/lib/reportingDatePresets.ts +41 -0
- package/lib/reportingMetricHelp.ts +430 -0
- package/lib/reportingNonFinalIndicators.test.ts +157 -0
- package/lib/reportingNonFinalIndicators.ts +102 -0
- package/lib/reportingStrings.ts +491 -0
- package/lib/reportingTagWeekBreakdown.test.ts +141 -0
- package/lib/reportingTagWeekBreakdown.ts +181 -0
- package/lib/reportingWeekLayout.test.ts +239 -0
- package/lib/reportingWeekLayout.ts +313 -0
- package/lib/sessionAssiduity.test.ts +25 -0
- package/lib/sessionAssiduity.ts +33 -0
- package/lib/sessionEndReason.ts +55 -0
- package/lib/sessionEndWarnings.test.ts +200 -0
- package/lib/sessionEndWarnings.ts +125 -0
- package/lib/sessionListMerge.test.ts +101 -0
- package/lib/sessionListMerge.ts +70 -0
- package/lib/sessionTaskSidebarStats.test.ts +24 -0
- package/lib/sessionTaskSidebarStats.ts +54 -0
- package/lib/settingsCopy.ts +1276 -0
- package/lib/taskParsing.test.ts +153 -0
- package/lib/taskParsing.ts +737 -0
- package/lib/theme.ts +15 -0
- package/lib/translucentButtonClasses.ts +34 -0
- package/lib/usageProfile.test.ts +84 -0
- package/lib/usageProfile.ts +52 -0
- package/lib/userGuideCopy.ts +464 -0
- package/lib/workspaceLocDefaults.ts +21 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +15 -0
- package/package.json +87 -0
- package/postcss.config.mjs +12 -0
- package/public/apple-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.png +0 -0
- package/public/next.svg +1 -0
- package/public/sw.js +13 -0
- package/public/traceback.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/actionDispatch.test.ts +723 -0
- package/server/actionDispatch.ts +1476 -0
- package/server/actionTaskSession.test.ts +713 -0
- package/server/actionTaskSession.ts +717 -0
- package/server/db.ts +42 -0
- package/server/defaultCfg.ts +87 -0
- package/server/gitlabTokenStore.ts +34 -0
- package/server/kronoFocusHydrate.test.ts +142 -0
- package/server/kronoFocusHydrate.ts +69 -0
- package/server/kronoFocusMigrate.test.ts +53 -0
- package/server/kronoFocusMigrate.ts +78 -0
- package/server/mainTimerHydrate.test.ts +65 -0
- package/server/mainTimerHydrate.ts +53 -0
- package/server/payloadStore.test.ts +78 -0
- package/server/payloadStore.ts +83 -0
- package/server/sessionWallHydrate.test.ts +46 -0
- package/server/sessionWallHydrate.ts +88 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
|
|
4
|
+
import {
|
|
5
|
+
normalizeProjectKey,
|
|
6
|
+
normalizeTagKey,
|
|
7
|
+
normalizeTaskTagsForStorage,
|
|
8
|
+
type TaskTagsStorageNormalizeOpts,
|
|
9
|
+
} from "@/lib/taskParsing";
|
|
10
|
+
|
|
11
|
+
export function asRecord(v: unknown): Record<string, unknown> | undefined {
|
|
12
|
+
return v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ResolvedTaskSession = { session: Record<string, unknown>; isLive: boolean };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Résout la session cible pour les mutations de tâches.
|
|
19
|
+
* Sans `sessionId` : session live (`current`) uniquement.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveTaskSession(p: KronosysUpdatePayload, sessionId: unknown): ResolvedTaskSession | null {
|
|
22
|
+
const sid = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
23
|
+
const cur = asRecord(p.current);
|
|
24
|
+
if (!sid) {
|
|
25
|
+
if (!cur) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return { session: cur, isLive: true };
|
|
29
|
+
}
|
|
30
|
+
if (cur && String(cur.sessionId ?? "") === sid) {
|
|
31
|
+
return { session: cur, isLive: true };
|
|
32
|
+
}
|
|
33
|
+
const hist = (p.history || []) as Record<string, unknown>[];
|
|
34
|
+
const row = hist.find((h) => String(h.sessionId ?? "") === sid);
|
|
35
|
+
if (row) {
|
|
36
|
+
return { session: row, isLive: false };
|
|
37
|
+
}
|
|
38
|
+
const arch = (p.historyArchived || []) as Record<string, unknown>[];
|
|
39
|
+
const ar = arch.find((h) => String(h.sessionId ?? "") === sid);
|
|
40
|
+
return ar ? { session: ar, isLive: false } : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getActiveTasksArray(sess: Record<string, unknown>): Record<string, unknown>[] {
|
|
44
|
+
return Array.isArray(sess.activeTasks) ? ([...sess.activeTasks] as Record<string, unknown>[]) : [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getTasksArray(sess: Record<string, unknown>): Record<string, unknown>[] {
|
|
48
|
+
return Array.isArray(sess.tasks) ? ([...sess.tasks] as Record<string, unknown>[]) : [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Repère une tâche : pile active (`activeTasks` / `activeTask`) puis liste `tasks`. */
|
|
52
|
+
export function findTaskRecord(sess: Record<string, unknown>, taskId: string): Record<string, unknown> | null {
|
|
53
|
+
const id = taskId;
|
|
54
|
+
for (const t of getActiveTasksArray(sess)) {
|
|
55
|
+
if (String(t.id) === id) {
|
|
56
|
+
return t;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const at = asRecord(sess.activeTask);
|
|
60
|
+
if (at && String(at.id) === id) {
|
|
61
|
+
return at;
|
|
62
|
+
}
|
|
63
|
+
return getTasksArray(sess).find((t) => String(t.id) === id) ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parcourt toutes les tâches d’une session sans doublon (même tâche dans `activeTasks` + `activeTask` + `tasks`).
|
|
68
|
+
*/
|
|
69
|
+
export function forEachTaskRecordInSession(
|
|
70
|
+
sess: Record<string, unknown>,
|
|
71
|
+
fn: (task: Record<string, unknown>, taskId: string) => void
|
|
72
|
+
): void {
|
|
73
|
+
const seen = new Set<string>();
|
|
74
|
+
for (const t of getActiveTasksArray(sess)) {
|
|
75
|
+
const id = String(t.id ?? "");
|
|
76
|
+
if (!id || seen.has(id)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
seen.add(id);
|
|
80
|
+
fn(t, id);
|
|
81
|
+
}
|
|
82
|
+
const at = asRecord(sess.activeTask);
|
|
83
|
+
if (at) {
|
|
84
|
+
const id = String(at.id ?? "");
|
|
85
|
+
if (id && !seen.has(id)) {
|
|
86
|
+
seen.add(id);
|
|
87
|
+
fn(at, id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const t of getTasksArray(sess)) {
|
|
91
|
+
const id = String(t.id ?? "");
|
|
92
|
+
if (!id || seen.has(id)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
seen.add(id);
|
|
96
|
+
fn(t, id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function flushAllSubtaskTimersInSession(sess: Record<string, unknown>): void {
|
|
101
|
+
forEachTaskRecordInSession(sess, (task) => {
|
|
102
|
+
flushSubtaskTimerOnTask(task);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Avant de laisser une tâche au minuteur principal (non en pause) : seule tâche « en cours »
|
|
108
|
+
* (minuteries parentes ailleurs en pause ; tous les minuteurs sous-tâches arrêtés en accumulant le temps).
|
|
109
|
+
*/
|
|
110
|
+
export function prepareSessionForExclusiveMainTimerUnpaused(
|
|
111
|
+
sess: Record<string, unknown>,
|
|
112
|
+
mainTaskIdToUnpause: string
|
|
113
|
+
): void {
|
|
114
|
+
flushAllSubtaskTimersInSession(sess);
|
|
115
|
+
const idKeep = String(mainTaskIdToUnpause);
|
|
116
|
+
forEachTaskRecordInSession(sess, (task, id) => {
|
|
117
|
+
if (task.isDone === true) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (id !== idKeep && task.manualTaskTimerPaused !== true) {
|
|
121
|
+
flushMainTimerSegmentOnTask(task);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
forEachTaskRecordInSession(sess, (task, id) => {
|
|
125
|
+
if (task.isDone === true) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
task.manualTaskTimerPaused = id !== idKeep;
|
|
129
|
+
});
|
|
130
|
+
forEachTaskRecordInSession(sess, (task, id) => {
|
|
131
|
+
if (id === idKeep && task.isDone !== true && task.manualTaskTimerPaused !== true) {
|
|
132
|
+
ensureMainTimerSegmentForRunningTask(task);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Avant de démarrer le suivi d’une sous-tâche : arrêt des suivi sous-tâches sur les autres tâches,
|
|
139
|
+
* puis toutes les **autres** tâches non terminées en pause (minuteur parent) — la tâche ciblée reste
|
|
140
|
+
* gérée par l’appelant (`manualTaskTimerPaused = false` sur le parent, en parallèle du suivi).
|
|
141
|
+
*/
|
|
142
|
+
export function prepareSessionForSubtaskTimer(sess: Record<string, unknown>, focusTaskId: string): void {
|
|
143
|
+
const idFocus = String(focusTaskId);
|
|
144
|
+
forEachTaskRecordInSession(sess, (task, id) => {
|
|
145
|
+
if (id === idFocus) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (task.isDone === true) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (task.manualTaskTimerPaused !== true) {
|
|
152
|
+
flushMainTimerSegmentOnTask(task);
|
|
153
|
+
}
|
|
154
|
+
flushSubtaskTimerOnTask(task);
|
|
155
|
+
});
|
|
156
|
+
forEachTaskRecordInSession(sess, (task, id) => {
|
|
157
|
+
if (task.isDone === true) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (id === idFocus) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
task.manualTaskTimerPaused = true;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function ensureSubtasks(task: Record<string, unknown>): Record<string, unknown>[] {
|
|
168
|
+
if (!Array.isArray(task.subtasks)) {
|
|
169
|
+
task.subtasks = [];
|
|
170
|
+
}
|
|
171
|
+
return task.subtasks as Record<string, unknown>[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Somme des `durationMs` enregistrées sur les sous-tâches (entiers non négatifs). */
|
|
175
|
+
export function sumSubtasksDurationMsStored(task: Record<string, unknown>): number {
|
|
176
|
+
let sum = 0;
|
|
177
|
+
for (const st of ensureSubtasks(task)) {
|
|
178
|
+
if (!st || typeof st !== "object" || Array.isArray(st)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const row = st as Record<string, unknown>;
|
|
182
|
+
const d = row.durationMs;
|
|
183
|
+
const v = typeof d === "number" && Number.isFinite(d) ? d : 0;
|
|
184
|
+
sum += Math.max(0, Math.floor(v));
|
|
185
|
+
}
|
|
186
|
+
return sum;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Règle métier : le total parent (`durationMs`) peut dépasser la somme des points (travail sans
|
|
191
|
+
* suivi par sous-tâche), jamais l’inverse. Si des données contredisent cela, on élève le parent.
|
|
192
|
+
*/
|
|
193
|
+
export function ensureTaskParentDurationCoversSubtasksMs(task: Record<string, unknown>): void {
|
|
194
|
+
const sumSub = sumSubtasksDurationMsStored(task);
|
|
195
|
+
const raw = task.durationMs;
|
|
196
|
+
const parent = typeof raw === "number" && Number.isFinite(raw) ? Math.max(0, Math.floor(raw)) : 0;
|
|
197
|
+
if (sumSub > parent) {
|
|
198
|
+
task.durationMs = sumSub;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Clé ISO : début du segment de minuteur courant sur une sous-tâche (tâche parente). */
|
|
203
|
+
const SUBTASK_TIMER_STARTED_AT = "subtaskTimerStartedAt";
|
|
204
|
+
|
|
205
|
+
/** Clé ISO : début du segment du minuteur principal (hors sous-tâche) — persisté pour ne pas perdre le temps hors tableau de bord. */
|
|
206
|
+
export const MAIN_TIMER_SEGMENT_STARTED_AT = "mainTimerSegmentStartedAt";
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Vide le segment de minuteur principal en cours : ajoute le temps écoulé à `durationMs`.
|
|
210
|
+
* Sans effet s’un suivi sous-tâche est actif (le parent ne cumule pas en parallèle).
|
|
211
|
+
*/
|
|
212
|
+
export function flushMainTimerSegmentOnTask(task: Record<string, unknown>): void {
|
|
213
|
+
if (String(task.activeSubtaskTimerId ?? "").trim() !== "") {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const raw = task[MAIN_TIMER_SEGMENT_STARTED_AT];
|
|
217
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const startedMs = Date.parse(raw);
|
|
221
|
+
if (!Number.isFinite(startedMs)) {
|
|
222
|
+
delete task[MAIN_TIMER_SEGMENT_STARTED_AT];
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const elapsed = Math.max(0, Math.floor(Date.now() - startedMs));
|
|
226
|
+
const parentPrev =
|
|
227
|
+
typeof task.durationMs === "number" && Number.isFinite(task.durationMs) ? Number(task.durationMs) : 0;
|
|
228
|
+
task.durationMs = Math.floor(parentPrev + elapsed);
|
|
229
|
+
delete task[MAIN_TIMER_SEGMENT_STARTED_AT];
|
|
230
|
+
ensureTaskParentDurationCoversSubtasksMs(task);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Démarre un segment de minuteur principal si la tâche est au suivi (hors sous-tâche). */
|
|
234
|
+
export function ensureMainTimerSegmentForRunningTask(task: Record<string, unknown>): void {
|
|
235
|
+
if (task.isDone === true) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (task.manualTaskTimerPaused === true) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (String(task.activeSubtaskTimerId ?? "").trim() !== "") {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const raw = task[MAIN_TIMER_SEGMENT_STARTED_AT];
|
|
245
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
task[MAIN_TIMER_SEGMENT_STARTED_AT] = new Date().toISOString();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Ajoute le segment de minuteur sous-tâche en cours à `durationMs` de la sous-tâche active
|
|
253
|
+
* et le même segment à la tâche parente (répartition par point + total tâche ; les rapports ne lisent que le parent).
|
|
254
|
+
* Le total parent peut rester supérieur à la somme des points (travail sans sous-tâches), jamais l’inverse
|
|
255
|
+
* — rattrapage via `ensureTaskParentDurationCoversSubtasksMs` en fin de flush.
|
|
256
|
+
* Efface ensuite `activeSubtaskTimerId` et l’horodatage de segment.
|
|
257
|
+
*/
|
|
258
|
+
export function flushSubtaskTimerOnTask(task: Record<string, unknown>): void {
|
|
259
|
+
const activeId = String(task.activeSubtaskTimerId ?? "").trim();
|
|
260
|
+
if (!activeId) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const raw = task[SUBTASK_TIMER_STARTED_AT];
|
|
264
|
+
const startedMs =
|
|
265
|
+
typeof raw === "string" && raw.trim() !== ""
|
|
266
|
+
? Date.parse(raw)
|
|
267
|
+
: typeof raw === "number" && Number.isFinite(raw)
|
|
268
|
+
? raw
|
|
269
|
+
: Number.NaN;
|
|
270
|
+
if (!Number.isFinite(startedMs)) {
|
|
271
|
+
task.activeSubtaskTimerId = null;
|
|
272
|
+
delete task[SUBTASK_TIMER_STARTED_AT];
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const elapsed = Math.max(0, Date.now() - startedMs);
|
|
276
|
+
const st = ensureSubtasks(task).find((s) => String(s.id) === activeId);
|
|
277
|
+
if (st && typeof st === "object" && !Array.isArray(st)) {
|
|
278
|
+
const row = st as Record<string, unknown>;
|
|
279
|
+
const prev = typeof row.durationMs === "number" && Number.isFinite(row.durationMs) ? row.durationMs : 0;
|
|
280
|
+
row.durationMs = Math.floor(Number(prev) + elapsed);
|
|
281
|
+
}
|
|
282
|
+
const parentPrev =
|
|
283
|
+
typeof task.durationMs === "number" && Number.isFinite(task.durationMs) ? Number(task.durationMs) : 0;
|
|
284
|
+
task.durationMs = Math.floor(parentPrev + elapsed);
|
|
285
|
+
task.activeSubtaskTimerId = null;
|
|
286
|
+
delete task[SUBTASK_TIMER_STARTED_AT];
|
|
287
|
+
ensureTaskParentDurationCoversSubtasksMs(task);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function finishTaskInSession(
|
|
291
|
+
sess: Record<string, unknown>,
|
|
292
|
+
taskId: string,
|
|
293
|
+
shouldCommit: boolean,
|
|
294
|
+
tagNormOpts?: TaskTagsStorageNormalizeOpts
|
|
295
|
+
): boolean {
|
|
296
|
+
const id = taskId;
|
|
297
|
+
let task: Record<string, unknown> | null = null;
|
|
298
|
+
let fromActiveStack = false;
|
|
299
|
+
|
|
300
|
+
const activeArr = getActiveTasksArray(sess);
|
|
301
|
+
const idx = activeArr.findIndex((t) => String(t.id) === id);
|
|
302
|
+
const at = asRecord(sess.activeTask);
|
|
303
|
+
|
|
304
|
+
if (idx >= 0) {
|
|
305
|
+
task = activeArr[idx] as Record<string, unknown>;
|
|
306
|
+
const sameAsActive = Boolean(at && String(at.id) === id);
|
|
307
|
+
activeArr.splice(idx, 1);
|
|
308
|
+
sess.activeTasks = activeArr;
|
|
309
|
+
if (sameAsActive) {
|
|
310
|
+
sess.activeTask = activeArr[0] ?? null;
|
|
311
|
+
}
|
|
312
|
+
fromActiveStack = true;
|
|
313
|
+
} else if (at && String(at.id) === id) {
|
|
314
|
+
task = at;
|
|
315
|
+
sess.activeTask = null;
|
|
316
|
+
sess.activeTasks = activeArr;
|
|
317
|
+
fromActiveStack = true;
|
|
318
|
+
} else {
|
|
319
|
+
task = getTasksArray(sess).find((t) => String(t.id) === id) ?? null;
|
|
320
|
+
if (!task || task.isDone === true) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
flushSubtaskTimerOnTask(task);
|
|
326
|
+
flushMainTimerSegmentOnTask(task);
|
|
327
|
+
task.isDone = true;
|
|
328
|
+
task.endTime = new Date().toISOString();
|
|
329
|
+
task.shouldCommit = shouldCommit;
|
|
330
|
+
task.manualTaskTimerPaused = false;
|
|
331
|
+
for (const st of ensureSubtasks(task)) {
|
|
332
|
+
if (st && typeof st === "object" && !Array.isArray(st)) {
|
|
333
|
+
(st as Record<string, unknown>).done = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
task.tags = normalizeTaskTagsForStorage((task.tags as string[] | undefined) ?? [], tagNormOpts);
|
|
338
|
+
ensureTaskParentDurationCoversSubtasksMs(task);
|
|
339
|
+
|
|
340
|
+
const tasks = getTasksArray(sess);
|
|
341
|
+
const existingIdx = tasks.findIndex((t) => String(t.id) === id);
|
|
342
|
+
if (existingIdx >= 0) {
|
|
343
|
+
tasks[existingIdx] = task;
|
|
344
|
+
} else {
|
|
345
|
+
tasks.push(task);
|
|
346
|
+
}
|
|
347
|
+
sess.tasks = tasks;
|
|
348
|
+
|
|
349
|
+
if (fromActiveStack) {
|
|
350
|
+
const after = getActiveTasksArray(sess);
|
|
351
|
+
const curAt = asRecord(sess.activeTask);
|
|
352
|
+
if (curAt && String(curAt.id) === id) {
|
|
353
|
+
sess.activeTask = after[0] ?? null;
|
|
354
|
+
}
|
|
355
|
+
if (!sess.activeTask && after.length > 0) {
|
|
356
|
+
sess.activeTask = after[0] ?? null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function deleteTaskInSession(sess: Record<string, unknown>, taskId: string): boolean {
|
|
363
|
+
const id = taskId;
|
|
364
|
+
const before = findTaskRecord(sess, id);
|
|
365
|
+
if (!before) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
flushSubtaskTimerOnTask(before);
|
|
369
|
+
flushMainTimerSegmentOnTask(before);
|
|
370
|
+
sess.activeTasks = getActiveTasksArray(sess).filter((t) => String(t.id) !== id);
|
|
371
|
+
sess.tasks = getTasksArray(sess).filter((t) => String(t.id) !== id);
|
|
372
|
+
const curAt = asRecord(sess.activeTask);
|
|
373
|
+
if (curAt && String(curAt.id) === id) {
|
|
374
|
+
const next = (sess.activeTasks as Record<string, unknown>[])[0] ?? null;
|
|
375
|
+
sess.activeTask = next;
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function updateTaskInSession(
|
|
381
|
+
sess: Record<string, unknown>,
|
|
382
|
+
taskId: string,
|
|
383
|
+
patch: { name?: string; tags?: string[]; project?: string | null },
|
|
384
|
+
tagNormOpts?: TaskTagsStorageNormalizeOpts
|
|
385
|
+
): boolean {
|
|
386
|
+
const task = findTaskRecord(sess, taskId);
|
|
387
|
+
if (!task) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
if (typeof patch.name === "string") {
|
|
391
|
+
task.name = patch.name;
|
|
392
|
+
}
|
|
393
|
+
if (Array.isArray(patch.tags)) {
|
|
394
|
+
task.tags = normalizeTaskTagsForStorage(patch.tags, tagNormOpts);
|
|
395
|
+
}
|
|
396
|
+
if (patch.project !== undefined) {
|
|
397
|
+
task.project = patch.project;
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function updateTaskStartTimeInSession(
|
|
403
|
+
sess: Record<string, unknown>,
|
|
404
|
+
taskId: string,
|
|
405
|
+
startTimeIso: string
|
|
406
|
+
): boolean {
|
|
407
|
+
const task = findTaskRecord(sess, taskId);
|
|
408
|
+
if (!task) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
const startMs = Date.parse(startTimeIso);
|
|
412
|
+
if (!Number.isFinite(startMs)) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
const nowMs = Date.now();
|
|
416
|
+
task.startTime = new Date(startMs).toISOString();
|
|
417
|
+
const endRaw = typeof task.endTime === "string" ? task.endTime : "";
|
|
418
|
+
const endMs = Date.parse(endRaw);
|
|
419
|
+
if (Number.isFinite(endMs) && endMs >= startMs) {
|
|
420
|
+
task.durationMs = Math.max(0, Math.floor(endMs - startMs));
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
task.durationMs = Math.max(0, Math.floor(nowMs - startMs));
|
|
424
|
+
const subtaskRunning = String(task.activeSubtaskTimerId ?? "").trim() !== "";
|
|
425
|
+
if (!subtaskRunning && task.manualTaskTimerPaused !== true && task.isDone !== true) {
|
|
426
|
+
task[MAIN_TIMER_SEGMENT_STARTED_AT] = new Date(nowMs).toISOString();
|
|
427
|
+
}
|
|
428
|
+
ensureTaskParentDurationCoversSubtasksMs(task);
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Corrige l’heure de fin d’une tâche **terminée** ; recalcule `durationMs`.
|
|
434
|
+
* Les tâches encore ouvertes utilisent le flux habituel (terminer / minuteur).
|
|
435
|
+
*/
|
|
436
|
+
export function updateTaskEndTimeInSession(
|
|
437
|
+
sess: Record<string, unknown>,
|
|
438
|
+
taskId: string,
|
|
439
|
+
endTimeIso: string
|
|
440
|
+
): boolean {
|
|
441
|
+
const task = findTaskRecord(sess, taskId);
|
|
442
|
+
if (!task || task.isDone !== true) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
const endMs = Date.parse(endTimeIso);
|
|
446
|
+
if (!Number.isFinite(endMs)) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
const startRaw = typeof task.startTime === "string" ? task.startTime : "";
|
|
450
|
+
const startMs = Date.parse(startRaw);
|
|
451
|
+
if (!Number.isFinite(startMs) || endMs < startMs) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
task.endTime = new Date(endMs).toISOString();
|
|
455
|
+
task.durationMs = Math.max(0, Math.floor(endMs - startMs));
|
|
456
|
+
ensureTaskParentDurationCoversSubtasksMs(task);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function setTaskPausedInSession(sess: Record<string, unknown>, taskId: string, paused: boolean): boolean {
|
|
461
|
+
const task = findTaskRecord(sess, taskId);
|
|
462
|
+
if (!task || task.isDone === true) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
if (paused) {
|
|
466
|
+
flushSubtaskTimerOnTask(task);
|
|
467
|
+
flushMainTimerSegmentOnTask(task);
|
|
468
|
+
task.manualTaskTimerPaused = true;
|
|
469
|
+
} else {
|
|
470
|
+
flushSubtaskTimerOnTask(task);
|
|
471
|
+
prepareSessionForExclusiveMainTimerUnpaused(sess, taskId);
|
|
472
|
+
}
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function toggleSubtaskInSession(
|
|
477
|
+
sess: Record<string, unknown>,
|
|
478
|
+
taskId: string,
|
|
479
|
+
subtaskId: string
|
|
480
|
+
): boolean {
|
|
481
|
+
const task = findTaskRecord(sess, taskId);
|
|
482
|
+
if (!task) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
const st = ensureSubtasks(task).find((s) => String(s.id) === subtaskId);
|
|
486
|
+
if (!st) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
const sid = String(subtaskId);
|
|
490
|
+
const wasTracking = String(task.activeSubtaskTimerId ?? "") === sid;
|
|
491
|
+
const nextDone = !Boolean(st.done);
|
|
492
|
+
if (wasTracking && nextDone) {
|
|
493
|
+
flushSubtaskTimerOnTask(task);
|
|
494
|
+
}
|
|
495
|
+
st.done = nextDone;
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function updateSubtaskTitleInSession(
|
|
500
|
+
sess: Record<string, unknown>,
|
|
501
|
+
taskId: string,
|
|
502
|
+
subtaskId: string,
|
|
503
|
+
title: string
|
|
504
|
+
): boolean {
|
|
505
|
+
const task = findTaskRecord(sess, taskId);
|
|
506
|
+
if (!task) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
const st = ensureSubtasks(task).find((s) => String(s.id) === subtaskId);
|
|
510
|
+
if (!st) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
st.title = title;
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function setActiveSubtaskTimerInSession(
|
|
518
|
+
sess: Record<string, unknown>,
|
|
519
|
+
taskId: string,
|
|
520
|
+
subtaskId: string | null
|
|
521
|
+
): boolean {
|
|
522
|
+
const task = findTaskRecord(sess, taskId);
|
|
523
|
+
if (!task) {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
if (subtaskId === null || subtaskId === undefined) {
|
|
527
|
+
flushSubtaskTimerOnTask(task);
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
const sid = String(subtaskId);
|
|
531
|
+
const st = ensureSubtasks(task).find((s) => String(s.id) === sid);
|
|
532
|
+
if (!st) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
flushMainTimerSegmentOnTask(task);
|
|
536
|
+
prepareSessionForSubtaskTimer(sess, String(taskId));
|
|
537
|
+
flushSubtaskTimerOnTask(task);
|
|
538
|
+
task.activeSubtaskTimerId = sid;
|
|
539
|
+
task[SUBTASK_TIMER_STARTED_AT] = new Date().toISOString();
|
|
540
|
+
task.manualTaskTimerPaused = false;
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function deleteSubtaskInSession(sess: Record<string, unknown>, taskId: string, subtaskId: string): boolean {
|
|
545
|
+
const task = findTaskRecord(sess, taskId);
|
|
546
|
+
if (!task) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
const sid = String(subtaskId);
|
|
550
|
+
if (String(task.activeSubtaskTimerId ?? "") === sid) {
|
|
551
|
+
flushSubtaskTimerOnTask(task);
|
|
552
|
+
}
|
|
553
|
+
const list = ensureSubtasks(task);
|
|
554
|
+
const next = list.filter((s) => String(s.id) !== sid);
|
|
555
|
+
task.subtasks = next;
|
|
556
|
+
ensureTaskParentDurationCoversSubtasksMs(task);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Réordonne les sous-tâches selon une liste complète d’identifiants (permutation exacte attendue).
|
|
562
|
+
*/
|
|
563
|
+
export function reorderSubtasksInSession(
|
|
564
|
+
sess: Record<string, unknown>,
|
|
565
|
+
taskId: string,
|
|
566
|
+
orderedSubtaskIds: string[]
|
|
567
|
+
): boolean {
|
|
568
|
+
const task = findTaskRecord(sess, taskId);
|
|
569
|
+
if (!task) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
const list = ensureSubtasks(task);
|
|
573
|
+
if (list.length <= 1) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
const currentIds = new Set(list.map((s) => String(s.id)));
|
|
577
|
+
if (orderedSubtaskIds.length !== currentIds.size) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
const seen = new Set<string>();
|
|
581
|
+
for (const raw of orderedSubtaskIds) {
|
|
582
|
+
const id = String(raw);
|
|
583
|
+
if (!currentIds.has(id) || seen.has(id)) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
seen.add(id);
|
|
587
|
+
}
|
|
588
|
+
const byId = new Map(list.map((s) => [String(s.id), s]));
|
|
589
|
+
task.subtasks = orderedSubtaskIds.map((raw) => byId.get(String(raw))!);
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function addSubtaskInSession(sess: Record<string, unknown>, taskId: string, title: string): boolean {
|
|
594
|
+
const task = findTaskRecord(sess, taskId);
|
|
595
|
+
if (!task || task.isDone === true) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
const list = ensureSubtasks(task);
|
|
599
|
+
list.push({
|
|
600
|
+
id: randomUUID(),
|
|
601
|
+
title,
|
|
602
|
+
done: false,
|
|
603
|
+
durationMs: 0,
|
|
604
|
+
});
|
|
605
|
+
task.subtasks = list;
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function addHistoricalTaskToSession(
|
|
610
|
+
sess: Record<string, unknown>,
|
|
611
|
+
input: {
|
|
612
|
+
name: string;
|
|
613
|
+
tags: string[];
|
|
614
|
+
project?: string | null;
|
|
615
|
+
durationMs: number;
|
|
616
|
+
startTime: string;
|
|
617
|
+
endTime: string;
|
|
618
|
+
},
|
|
619
|
+
newId: string,
|
|
620
|
+
tagNormOpts?: TaskTagsStorageNormalizeOpts
|
|
621
|
+
): void {
|
|
622
|
+
const task: Record<string, unknown> = {
|
|
623
|
+
id: newId,
|
|
624
|
+
name: input.name.trim(),
|
|
625
|
+
startTime: input.startTime,
|
|
626
|
+
endTime: input.endTime,
|
|
627
|
+
durationMs: input.durationMs,
|
|
628
|
+
isDone: true,
|
|
629
|
+
kronoFocusCycles: 0,
|
|
630
|
+
tags: normalizeTaskTagsForStorage(input.tags, tagNormOpts),
|
|
631
|
+
project: input.project ?? null,
|
|
632
|
+
subtasks: [],
|
|
633
|
+
};
|
|
634
|
+
const tasks = getTasksArray(sess);
|
|
635
|
+
tasks.push(task);
|
|
636
|
+
sess.tasks = tasks;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function eachSessionRecord(p: KronosysUpdatePayload, fn: (sess: Record<string, unknown>) => void): void {
|
|
640
|
+
const cur = asRecord(p.current);
|
|
641
|
+
if (cur) {
|
|
642
|
+
fn(cur);
|
|
643
|
+
}
|
|
644
|
+
for (const h of (p.history || []) as Record<string, unknown>[]) {
|
|
645
|
+
fn(h);
|
|
646
|
+
}
|
|
647
|
+
for (const h of (p.historyArchived || []) as Record<string, unknown>[]) {
|
|
648
|
+
fn(h);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function stripTagFromTaskTags(
|
|
653
|
+
task: Record<string, unknown>,
|
|
654
|
+
tagLower: string,
|
|
655
|
+
tagNormOpts?: TaskTagsStorageNormalizeOpts
|
|
656
|
+
): void {
|
|
657
|
+
if (!Array.isArray(task.tags)) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const tags = task.tags as string[];
|
|
661
|
+
task.tags = normalizeTaskTagsForStorage(
|
|
662
|
+
tags.filter((x) => normalizeTagKey(x).toLowerCase() !== tagLower),
|
|
663
|
+
tagNormOpts
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function purgeTagEverywhere(
|
|
668
|
+
p: KronosysUpdatePayload,
|
|
669
|
+
rawTag: string,
|
|
670
|
+
tagNormOpts?: TaskTagsStorageNormalizeOpts
|
|
671
|
+
): void {
|
|
672
|
+
const nk = normalizeTagKey(rawTag).toLowerCase();
|
|
673
|
+
p.knownTags = ((p.knownTags || []) as string[]).filter((t) => normalizeTagKey(t).toLowerCase() !== nk);
|
|
674
|
+
p.userKnownTags = ((p.userKnownTags || []) as string[]).filter((t) => normalizeTagKey(t).toLowerCase() !== nk);
|
|
675
|
+
p.excludedSuggestionTags = ((p.excludedSuggestionTags || []) as string[]).filter(
|
|
676
|
+
(t) => normalizeTagKey(t).toLowerCase() !== nk
|
|
677
|
+
);
|
|
678
|
+
const td = { ...(asRecord(p.tagDescriptions) ?? {}) } as Record<string, string>;
|
|
679
|
+
delete td[nk];
|
|
680
|
+
p.tagDescriptions = td;
|
|
681
|
+
|
|
682
|
+
eachSessionRecord(p, (sess) => {
|
|
683
|
+
for (const t of [...getActiveTasksArray(sess), ...getTasksArray(sess)]) {
|
|
684
|
+
stripTagFromTaskTags(t, nk, tagNormOpts);
|
|
685
|
+
}
|
|
686
|
+
const at = asRecord(sess.activeTask);
|
|
687
|
+
if (at) {
|
|
688
|
+
stripTagFromTaskTags(at, nk, tagNormOpts);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function purgeProjectEverywhere(p: KronosysUpdatePayload, rawName: string): void {
|
|
694
|
+
const pk = normalizeProjectKey(rawName).toLowerCase();
|
|
695
|
+
p.knownProjects = ((p.knownProjects || []) as string[]).filter(
|
|
696
|
+
(n) => normalizeProjectKey(n).toLowerCase() !== pk
|
|
697
|
+
);
|
|
698
|
+
const pd = { ...(asRecord(p.projectDescriptions) ?? {}) } as Record<string, string>;
|
|
699
|
+
delete pd[pk];
|
|
700
|
+
p.projectDescriptions = pd;
|
|
701
|
+
|
|
702
|
+
eachSessionRecord(p, (sess) => {
|
|
703
|
+
for (const t of [...getActiveTasksArray(sess), ...getTasksArray(sess)]) {
|
|
704
|
+
const proj = t.project;
|
|
705
|
+
if (typeof proj === "string" && normalizeProjectKey(proj).toLowerCase() === pk) {
|
|
706
|
+
t.project = null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const at = asRecord(sess.activeTask);
|
|
710
|
+
if (at) {
|
|
711
|
+
const proj = at.project;
|
|
712
|
+
if (typeof proj === "string" && normalizeProjectKey(proj).toLowerCase() === pk) {
|
|
713
|
+
at.project = null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|