@nightkatana/kronosys-app 1.0.0-beta.2 → 1.0.0-beta.21
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 +28 -1
- package/app/api/action/route.ts +39 -3
- package/app/api/action-logs/route.ts +24 -0
- package/app/api/backup/route.ts +1 -1
- package/app/api/restore/route.ts +145 -0
- package/app/changelog/page.tsx +71 -4
- package/app/globals.css +127 -0
- package/app/guide/page.tsx +61 -15
- package/app/implementation/page.tsx +700 -0
- package/app/layout.tsx +14 -3
- package/app/licenses/page.tsx +99 -37
- package/app/logs/page.tsx +258 -0
- package/app/manifest.ts +5 -5
- package/app/page.tsx +784 -229
- package/app/reporting/page.tsx +1266 -474
- package/app/settings/page.tsx +252 -18
- package/bin/kronosys.mjs +140 -15
- package/components/KronosysPayloadProvider.tsx +2 -0
- package/components/RouteTransition.tsx +18 -0
- package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
- package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
- package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
- package/components/dashboard/AppShellRouteNav.tsx +323 -48
- package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
- package/components/dashboard/DashboardSimpleModal.tsx +168 -25
- package/components/dashboard/DashboardTour.tsx +115 -29
- package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
- package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
- package/components/dashboard/NewSessionScopeModal.tsx +211 -20
- package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
- package/components/dashboard/ReportingTour.tsx +87 -21
- package/components/dashboard/SavedProjectPicker.tsx +16 -3
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
- package/components/dashboard/SessionListPanel.tsx +327 -44
- package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
- package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
- package/components/dashboard/SettingsTour.tsx +86 -21
- package/components/dashboard/TagPills.tsx +14 -1
- package/components/dashboard/TaskFocusPanel.tsx +1081 -478
- package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
- package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
- package/components/dashboard/taskFieldStyles.ts +20 -4
- package/components/dashboard/useReportingInteractionState.ts +80 -0
- package/lib/appShellHeaderClasses.ts +13 -0
- package/lib/businessRulesMatrix.ts +210 -0
- package/lib/copyToClipboard.ts +43 -0
- package/lib/dashboardCopy.ts +494 -84
- package/lib/dashboardQuickSearch.ts +54 -2
- package/lib/dashboardTimeZone.ts +109 -0
- package/lib/formatAppShellWallClock.ts +66 -0
- package/lib/formatSessionNameTemplate.ts +141 -0
- package/lib/generatedUserChangelog.ts +177 -6
- package/lib/globalPausePreview.ts +292 -0
- package/lib/implementationNotes.ts +1188 -0
- package/lib/kronosysApi.ts +6 -0
- package/lib/kronosysDashboardModalGates.ts +24 -0
- package/lib/plannedBoundaryAttention.ts +9 -0
- package/lib/plannedBoundaryConflict.ts +23 -0
- package/lib/reportingAggregate.ts +517 -75
- package/lib/reportingMetricHelp.ts +8 -0
- package/lib/reportingStrings.ts +37 -3
- package/lib/sessionListMerge.ts +4 -0
- package/lib/sessionTaskSidebarStats.ts +182 -21
- package/lib/settingsCopy.ts +178 -4
- package/lib/taskParsing.ts +360 -103
- package/lib/taskTemplateDraft.ts +135 -0
- package/lib/taskTimelineGantt.ts +265 -0
- package/lib/temporalDisplayPlanned.ts +71 -0
- package/lib/userGuideCopy.ts +121 -47
- package/next.config.ts +7 -0
- package/package.json +12 -24
- package/server/actionDispatch.ts +1000 -77
- package/server/actionTaskSession.ts +337 -24
- package/server/db.ts +7 -15
- package/server/dbSchema.ts +24 -0
- package/server/defaultCfg.ts +5 -0
- package/server/gitlabTokenStore.ts +0 -12
- package/server/liveHistorySync.ts +53 -0
- package/server/mainTimerHydrate.ts +38 -2
- package/server/payloadStore.ts +33 -11
- package/server/sessionWallHydrate.ts +66 -3
- package/server/userActionLog.ts +126 -0
- package/sonar-project.properties +11 -0
- package/tsconfig.json +2 -1
- package/components/dashboard/IssuePickerModal.tsx +0 -168
- package/components/dashboard/ThemeToggle.test.tsx +0 -26
- package/lib/backupCsvExport.test.ts +0 -149
- package/lib/dashboardQuickSearchQuery.test.ts +0 -63
- package/lib/dataDir.test.ts +0 -87
- package/lib/formatIsoShort.test.ts +0 -46
- package/lib/kronoFocusRhythm.test.ts +0 -130
- package/lib/kronoFocusTimerUrgency.test.ts +0 -74
- package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
- package/lib/reportingAggregate.test.ts +0 -325
- package/lib/reportingNonFinalIndicators.test.ts +0 -157
- package/lib/reportingTagWeekBreakdown.test.ts +0 -141
- package/lib/reportingWeekLayout.test.ts +0 -239
- package/lib/sessionAssiduity.test.ts +0 -25
- package/lib/sessionEndWarnings.test.ts +0 -200
- package/lib/sessionListMerge.test.ts +0 -101
- package/lib/sessionTaskSidebarStats.test.ts +0 -24
- package/lib/taskParsing.test.ts +0 -153
- package/lib/usageProfile.test.ts +0 -84
- package/server/actionDispatch.test.ts +0 -723
- package/server/actionTaskSession.test.ts +0 -713
- package/server/kronoFocusHydrate.test.ts +0 -142
- package/server/kronoFocusMigrate.test.ts +0 -53
- package/server/mainTimerHydrate.test.ts +0 -65
- package/server/payloadStore.test.ts +0 -78
- package/server/sessionWallHydrate.test.ts +0 -46
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { sessionWallClockMinutes, type LooseSession } from "@/lib/reportingAggregate";
|
|
2
|
+
import { taskTitleForDisplay } from "@/lib/taskParsing";
|
|
3
|
+
|
|
4
|
+
function asRecord(v: unknown): Record<string, unknown> | undefined {
|
|
5
|
+
return v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getActiveTasksArray(sess: Record<string, unknown>): Record<string, unknown>[] {
|
|
9
|
+
return Array.isArray(sess.activeTasks) ? ([...sess.activeTasks] as Record<string, unknown>[]) : [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getTasksArray(sess: Record<string, unknown>): Record<string, unknown>[] {
|
|
13
|
+
return Array.isArray(sess.tasks) ? ([...sess.tasks] as Record<string, unknown>[]) : [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Aligné sur `SUBTASK_TIMER_STARTED_AT` / `MAIN_TIMER_SEGMENT_STARTED_AT` côté serveur. */
|
|
17
|
+
const SUBTASK_TIMER_STARTED_AT = "subtaskTimerStartedAt";
|
|
18
|
+
const MAIN_TIMER_SEGMENT_STARTED_AT = "mainTimerSegmentStartedAt";
|
|
19
|
+
|
|
20
|
+
/** Même ordre que `forEachTaskRecordInSession` côté serveur (`server/actionTaskSession.ts`). */
|
|
21
|
+
export function forEachTaskRecordInSessionShape(
|
|
22
|
+
sess: Record<string, unknown>,
|
|
23
|
+
fn: (task: Record<string, unknown>, taskId: string) => void,
|
|
24
|
+
): void {
|
|
25
|
+
const seen = new Set<string>();
|
|
26
|
+
for (const t of getActiveTasksArray(sess)) {
|
|
27
|
+
const id = String(t.id ?? "");
|
|
28
|
+
if (!id || seen.has(id)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
seen.add(id);
|
|
32
|
+
fn(t, id);
|
|
33
|
+
}
|
|
34
|
+
const at = asRecord(sess.activeTask);
|
|
35
|
+
if (at) {
|
|
36
|
+
const id = String(at.id ?? "");
|
|
37
|
+
if (id && !seen.has(id)) {
|
|
38
|
+
seen.add(id);
|
|
39
|
+
fn(at, id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const t of getTasksArray(sess)) {
|
|
43
|
+
const id = String(t.id ?? "");
|
|
44
|
+
if (!id || seen.has(id)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
seen.add(id);
|
|
48
|
+
fn(t, id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureSubtasksList(task: Record<string, unknown>): unknown[] {
|
|
53
|
+
return Array.isArray(task.subtasks) ? task.subtasks : [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Somme des `durationMs` persistées sur les sous-tâches (entiers non négatifs). */
|
|
57
|
+
function sumSubtasksDurationMsStored(task: Record<string, unknown>): number {
|
|
58
|
+
let sum = 0;
|
|
59
|
+
for (const st of ensureSubtasksList(task)) {
|
|
60
|
+
const row = asRecord(st);
|
|
61
|
+
if (!row) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const d = row.durationMs;
|
|
65
|
+
const v = typeof d === "number" && Number.isFinite(d) ? d : 0;
|
|
66
|
+
sum += Math.max(0, Math.floor(v));
|
|
67
|
+
}
|
|
68
|
+
return sum;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function subtasksInflightMs(task: Record<string, unknown>, nowMs: number): number {
|
|
72
|
+
const activeSub = String(task.activeSubtaskTimerId ?? "").trim();
|
|
73
|
+
if (!activeSub) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
const raw = task[SUBTASK_TIMER_STARTED_AT];
|
|
77
|
+
const startedMs =
|
|
78
|
+
typeof raw === "string" && raw.trim() !== ""
|
|
79
|
+
? Date.parse(raw.trim())
|
|
80
|
+
: typeof raw === "number" && Number.isFinite(raw)
|
|
81
|
+
? Number(raw)
|
|
82
|
+
: Number.NaN;
|
|
83
|
+
if (!Number.isFinite(startedMs)) {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
return Math.max(0, Math.floor(nowMs - startedMs));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Durée minuteur « totale » côté tâche (persistée + segment courant parent ou sous-tâche active),
|
|
91
|
+
* alignée sur les règles serveur : pas de double segment parent + sous-tâche.
|
|
92
|
+
*/
|
|
93
|
+
function taskInclusiveTimerMs(task: Record<string, unknown>, nowMs: number): number {
|
|
94
|
+
const stored =
|
|
95
|
+
typeof task.durationMs === "number" && Number.isFinite(task.durationMs)
|
|
96
|
+
? Math.max(0, Math.floor(Number(task.durationMs)))
|
|
97
|
+
: 0;
|
|
98
|
+
if (String(task.activeSubtaskTimerId ?? "").trim() !== "") {
|
|
99
|
+
return stored + subtasksInflightMs(task, nowMs);
|
|
100
|
+
}
|
|
101
|
+
const rawMain = task[MAIN_TIMER_SEGMENT_STARTED_AT];
|
|
102
|
+
if (typeof rawMain === "string" && rawMain.trim() !== "") {
|
|
103
|
+
const startedMs = Date.parse(rawMain.trim());
|
|
104
|
+
if (Number.isFinite(startedMs)) {
|
|
105
|
+
return stored + Math.max(0, Math.floor(nowMs - startedMs));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return stored;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Total temps sous-tâches (persisté + segment actif éventuel). */
|
|
112
|
+
function subtasksTimerMsDisplay(task: Record<string, unknown>, nowMs: number): number {
|
|
113
|
+
return sumSubtasksDurationMsStored(task) + subtasksInflightMs(task, nowMs);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function subtaskTitleFor(task: Record<string, unknown>, subId: string): string | undefined {
|
|
117
|
+
const list = task.subtasks;
|
|
118
|
+
if (!Array.isArray(list)) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const row = list.find((s) => asRecord(s) && String(asRecord(s)!.id ?? "") === subId);
|
|
122
|
+
const t = row && asRecord(row)?.title;
|
|
123
|
+
return typeof t === "string" && t.trim() !== "" ? t.trim() : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function optionalNonNegativeMinutesFromLive(raw: unknown): number | null {
|
|
127
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return Math.max(0, raw);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function looseSessionWallStub(sess: Record<string, unknown>): LooseSession {
|
|
134
|
+
return {
|
|
135
|
+
sessionId: typeof sess.sessionId === "string" ? sess.sessionId.trim() || "?" : "?",
|
|
136
|
+
savedAt:
|
|
137
|
+
typeof sess.savedAt === "string" && sess.savedAt.trim() !== "" ? sess.savedAt.trim() : undefined,
|
|
138
|
+
startAt:
|
|
139
|
+
typeof sess.startAt === "string" && sess.startAt.trim() !== ""
|
|
140
|
+
? sess.startAt.trim()
|
|
141
|
+
: typeof sess.createdAt === "string" && sess.createdAt.trim() !== ""
|
|
142
|
+
? sess.createdAt.trim()
|
|
143
|
+
: undefined,
|
|
144
|
+
endAt:
|
|
145
|
+
typeof sess.endAt === "string" && sess.endAt.trim() !== "" ? sess.endAt.trim() : undefined,
|
|
146
|
+
sessionDurationMinutes:
|
|
147
|
+
typeof sess.sessionDurationMinutes === "number" && Number.isFinite(sess.sessionDurationMinutes)
|
|
148
|
+
? sess.sessionDurationMinutes
|
|
149
|
+
: undefined,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type GlobalPausePreviewRow = {
|
|
154
|
+
taskId: string;
|
|
155
|
+
taskTitle: string;
|
|
156
|
+
pauseMainTimer: boolean;
|
|
157
|
+
/** Minuteur de sous-tâche actif : sera arrêté et consolidé. */
|
|
158
|
+
activeSubtaskLabel?: string;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export type GlobalPauseActivationPreview = {
|
|
162
|
+
sessionName: string;
|
|
163
|
+
sessionWallWillPause: boolean;
|
|
164
|
+
sessionWallAlreadyPaused: boolean;
|
|
165
|
+
rows: GlobalPausePreviewRow[];
|
|
166
|
+
/** ISO début session (priorité `startAt`, sinon `savedAt`, `createdAt`). */
|
|
167
|
+
sessionStartIso: string;
|
|
168
|
+
/** ISO fin ; vide si session encore ouverte côté client. */
|
|
169
|
+
sessionEndIso: string;
|
|
170
|
+
/** Durée murale (minutes), voir {@link sessionWallClockMinutes}. */
|
|
171
|
+
sessionWallMinutes: number;
|
|
172
|
+
taskCount: number;
|
|
173
|
+
subtaskCount: number;
|
|
174
|
+
/**
|
|
175
|
+
* Temps minuteur sur la partie « tâche » hors attribution sous-tâche (persisté + segments en cours),
|
|
176
|
+
* en millisecondes.
|
|
177
|
+
*/
|
|
178
|
+
taskMainTimerMsExclusive: number;
|
|
179
|
+
/** Somme des temps minuteur sous-tâches (persisté + segment actif éventuel), en millisecondes. */
|
|
180
|
+
subtasksTimerMsTotal: number;
|
|
181
|
+
/** Total minuteurs tâches (somme inclusive sur les tâches), identique à exclusif + sous-tâches. */
|
|
182
|
+
taskTimersTotalMs: number;
|
|
183
|
+
/** Minutes de codage IDE sur la session (`codingMinutesSession`), ou null si non suivies. */
|
|
184
|
+
sessionCodingMinutes: number | null;
|
|
185
|
+
/** Minutes actives IDE sur la session (`activeMinutes`), ou null si non suivies. */
|
|
186
|
+
sessionActiveMinutes: number | null;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export type GlobalPauseActivationPreviewOpts = {
|
|
190
|
+
/** Point de référence pour les segments en cours (tests). */
|
|
191
|
+
nowMs?: number;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Indique qu’activer la pause globale n’aurait aucun effet : horloge murale déjà figée
|
|
196
|
+
* et aucun minuteur de tâche ou de sous-tâche à traiter (même critères que la liste d’effets).
|
|
197
|
+
*/
|
|
198
|
+
export function isGlobalPauseActivationNoOp(preview: GlobalPauseActivationPreview): boolean {
|
|
199
|
+
return !preview.sessionWallWillPause && preview.rows.length === 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Aperçu de ce que fera `toggleGlobalPauseContext` lorsqu’il active la pause globale
|
|
204
|
+
* (pas lors de la reprise).
|
|
205
|
+
*/
|
|
206
|
+
export function buildGlobalPauseActivationPreview(
|
|
207
|
+
live: Record<string, unknown> | null | undefined,
|
|
208
|
+
opts?: GlobalPauseActivationPreviewOpts,
|
|
209
|
+
): GlobalPauseActivationPreview | null {
|
|
210
|
+
if (!live) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const nowMs = typeof opts?.nowMs === "number" && Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now();
|
|
214
|
+
|
|
215
|
+
const rawName = typeof live.sessionName === "string" ? live.sessionName.trim() : "";
|
|
216
|
+
const sid = typeof live.sessionId === "string" ? live.sessionId.trim() : "";
|
|
217
|
+
const sessionName = rawName || (sid ? sid.slice(0, 8) : "—");
|
|
218
|
+
|
|
219
|
+
const sessionWallAlreadyPaused = live.isPaused === true;
|
|
220
|
+
const sessionWallWillPause = !sessionWallAlreadyPaused;
|
|
221
|
+
|
|
222
|
+
const rows: GlobalPausePreviewRow[] = [];
|
|
223
|
+
let taskCount = 0;
|
|
224
|
+
let subtaskCount = 0;
|
|
225
|
+
let taskMainTimerMsExclusive = 0;
|
|
226
|
+
let subtasksTimerMsTotal = 0;
|
|
227
|
+
|
|
228
|
+
forEachTaskRecordInSessionShape(live, (task) => {
|
|
229
|
+
taskCount += 1;
|
|
230
|
+
for (const st of ensureSubtasksList(task)) {
|
|
231
|
+
if (asRecord(st)) {
|
|
232
|
+
subtaskCount += 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const subTotal = subtasksTimerMsDisplay(task, nowMs);
|
|
236
|
+
const inclusive = taskInclusiveTimerMs(task, nowMs);
|
|
237
|
+
subtasksTimerMsTotal += subTotal;
|
|
238
|
+
taskMainTimerMsExclusive += Math.max(0, inclusive - subTotal);
|
|
239
|
+
|
|
240
|
+
if (task.isDone === true) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const nameRaw = typeof task.name === "string" ? task.name : "";
|
|
244
|
+
const taskTitle = taskTitleForDisplay(nameRaw).trim() || String(task.id ?? "").slice(0, 8);
|
|
245
|
+
const activeSubtaskId =
|
|
246
|
+
typeof task.activeSubtaskTimerId === "string" ? task.activeSubtaskTimerId.trim() : "";
|
|
247
|
+
const hasSub = activeSubtaskId.length > 0;
|
|
248
|
+
const pauseMainTimer = task.manualTaskTimerPaused !== true;
|
|
249
|
+
const activeSubtaskLabel = hasSub
|
|
250
|
+
? subtaskTitleFor(task, activeSubtaskId) ?? activeSubtaskId.slice(0, 8)
|
|
251
|
+
: undefined;
|
|
252
|
+
|
|
253
|
+
if (!pauseMainTimer && !hasSub) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
rows.push({
|
|
257
|
+
taskId: String(task.id ?? ""),
|
|
258
|
+
taskTitle,
|
|
259
|
+
pauseMainTimer,
|
|
260
|
+
...(hasSub ? { activeSubtaskLabel } : {}),
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const startCand =
|
|
265
|
+
(typeof live.startAt === "string" && live.startAt.trim()) ||
|
|
266
|
+
(typeof live.savedAt === "string" && live.savedAt.trim()) ||
|
|
267
|
+
(typeof live.createdAt === "string" && live.createdAt.trim()) ||
|
|
268
|
+
"";
|
|
269
|
+
const endCand = typeof live.endAt === "string" && live.endAt.trim() !== "" ? live.endAt.trim() : "";
|
|
270
|
+
|
|
271
|
+
const wallLoose = looseSessionWallStub(live);
|
|
272
|
+
const sessionCodingMinutes = optionalNonNegativeMinutesFromLive(live.codingMinutesSession);
|
|
273
|
+
const sessionActiveMinutes = optionalNonNegativeMinutesFromLive(live.activeMinutes);
|
|
274
|
+
const taskTimersTotalMs = taskMainTimerMsExclusive + subtasksTimerMsTotal;
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
sessionName,
|
|
278
|
+
sessionWallWillPause,
|
|
279
|
+
sessionWallAlreadyPaused,
|
|
280
|
+
rows,
|
|
281
|
+
sessionStartIso: startCand.trim(),
|
|
282
|
+
sessionEndIso: endCand.trim(),
|
|
283
|
+
sessionWallMinutes: sessionWallClockMinutes(wallLoose),
|
|
284
|
+
taskCount,
|
|
285
|
+
subtaskCount,
|
|
286
|
+
taskMainTimerMsExclusive,
|
|
287
|
+
subtasksTimerMsTotal,
|
|
288
|
+
taskTimersTotalMs,
|
|
289
|
+
sessionCodingMinutes,
|
|
290
|
+
sessionActiveMinutes,
|
|
291
|
+
};
|
|
292
|
+
}
|