@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.
Files changed (179) hide show
  1. package/README.md +81 -0
  2. package/app/api/action/route.ts +16 -0
  3. package/app/api/backup/route.ts +84 -0
  4. package/app/api/health/route.ts +22 -0
  5. package/app/api/state/route.ts +27 -0
  6. package/app/apple-icon.png +0 -0
  7. package/app/changelog/page.tsx +122 -0
  8. package/app/globals.css +210 -0
  9. package/app/guide/layout.tsx +11 -0
  10. package/app/guide/page.tsx +278 -0
  11. package/app/icon.png +0 -0
  12. package/app/layout.tsx +77 -0
  13. package/app/licenses/layout.tsx +11 -0
  14. package/app/licenses/page.tsx +246 -0
  15. package/app/manifest.ts +32 -0
  16. package/app/page.tsx +1610 -0
  17. package/app/reporting/page.tsx +2943 -0
  18. package/app/settings/layout.tsx +10 -0
  19. package/app/settings/page.tsx +3518 -0
  20. package/bin/kronosys.mjs +46 -0
  21. package/components/KronosysPackageVersionProvider.tsx +19 -0
  22. package/components/KronosysPayloadProvider.tsx +109 -0
  23. package/components/PwaRegister.tsx +25 -0
  24. package/components/SiteLegalFooter.tsx +21 -0
  25. package/components/ThemeProvider.tsx +78 -0
  26. package/components/dashboard/AppShellLiveSessionDrawer.tsx +394 -0
  27. package/components/dashboard/AppShellRouteNav.tsx +131 -0
  28. package/components/dashboard/AppVersionStamp.tsx +16 -0
  29. package/components/dashboard/DashboardCollapsibleSection.tsx +57 -0
  30. package/components/dashboard/DashboardColumnHintsBanner.tsx +159 -0
  31. package/components/dashboard/DashboardCommandCenter.tsx +470 -0
  32. package/components/dashboard/DashboardLangGateModal.tsx +118 -0
  33. package/components/dashboard/DashboardLoadingOverlay.tsx +42 -0
  34. package/components/dashboard/DashboardSimpleModal.tsx +337 -0
  35. package/components/dashboard/DashboardSuspenseFallback.tsx +52 -0
  36. package/components/dashboard/DashboardToastProvider.tsx +64 -0
  37. package/components/dashboard/DashboardTour.tsx +435 -0
  38. package/components/dashboard/DeferredDescriptionPopoverWrap.tsx +39 -0
  39. package/components/dashboard/DeleteSessionModal.tsx +130 -0
  40. package/components/dashboard/DescriptionTooltipPortaled.tsx +31 -0
  41. package/components/dashboard/GitIdentityQuickSetupModal.tsx +211 -0
  42. package/components/dashboard/HeaderIntegrationBadges.tsx +69 -0
  43. package/components/dashboard/InlineMetricHelpTrigger.tsx +102 -0
  44. package/components/dashboard/IssuePickerModal.tsx +168 -0
  45. package/components/dashboard/KronoFocusPanel.tsx +834 -0
  46. package/components/dashboard/KronosysDatetimePopoverField.tsx +357 -0
  47. package/components/dashboard/KronosysTimePopoverField.tsx +233 -0
  48. package/components/dashboard/LanguageMenu.tsx +123 -0
  49. package/components/dashboard/MongoMirrorSyncLine.tsx +57 -0
  50. package/components/dashboard/NewSessionScopeModal.tsx +410 -0
  51. package/components/dashboard/PageRefreshButton.tsx +130 -0
  52. package/components/dashboard/PlainHelpPopover.tsx +97 -0
  53. package/components/dashboard/ReportingPageToc.tsx +68 -0
  54. package/components/dashboard/ReportingTour.tsx +342 -0
  55. package/components/dashboard/SavedProjectPicker.tsx +92 -0
  56. package/components/dashboard/SavedTagPicker.tsx +115 -0
  57. package/components/dashboard/ScrollToTopFab.tsx +41 -0
  58. package/components/dashboard/SelectedSessionSidebarBlock.tsx +630 -0
  59. package/components/dashboard/SessionEndReasonEditor.tsx +114 -0
  60. package/components/dashboard/SessionListPanel.tsx +320 -0
  61. package/components/dashboard/SessionLocMetricsSection.tsx +128 -0
  62. package/components/dashboard/SettingsTagsProjectsSection.tsx +993 -0
  63. package/components/dashboard/SettingsTour.tsx +332 -0
  64. package/components/dashboard/TagPills.tsx +149 -0
  65. package/components/dashboard/TagsHelpTrigger.tsx +84 -0
  66. package/components/dashboard/TaskFocusPanel.tsx +1261 -0
  67. package/components/dashboard/TaskSessionLiveCard.tsx +832 -0
  68. package/components/dashboard/TaskSubtasksBlock.tsx +748 -0
  69. package/components/dashboard/ThemeToggle.test.tsx +26 -0
  70. package/components/dashboard/ThemeToggle.tsx +36 -0
  71. package/components/dashboard/UserGuideBodyText.tsx +62 -0
  72. package/components/dashboard/WorkspaceGitRepoCard.tsx +191 -0
  73. package/components/dashboard/taskFieldStyles.ts +139 -0
  74. package/components/dashboard/useAnchoredFloatingPortalStyle.ts +71 -0
  75. package/components/dashboard/useDescriptionPopoverAfterMs.ts +220 -0
  76. package/components/dashboard/useKronoFocusLiveSeconds.ts +36 -0
  77. package/components/dashboard/useSmoothStopwatchMs.ts +25 -0
  78. package/lib/appShellHeaderClasses.ts +12 -0
  79. package/lib/backupCsvExport.test.ts +149 -0
  80. package/lib/backupCsvExport.ts +392 -0
  81. package/lib/changelogCopy.ts +34 -0
  82. package/lib/concurrentTaskStartPreference.ts +29 -0
  83. package/lib/dashboardClockFormat.ts +13 -0
  84. package/lib/dashboardColumnChrome.ts +3 -0
  85. package/lib/dashboardColumnHintsStorage.ts +57 -0
  86. package/lib/dashboardCopy.ts +1831 -0
  87. package/lib/dashboardDetachedUrlHintStorage.ts +24 -0
  88. package/lib/dashboardGitIdentityBannerStorage.ts +36 -0
  89. package/lib/dashboardLangStorage.ts +72 -0
  90. package/lib/dashboardQuickSearch.ts +476 -0
  91. package/lib/dashboardQuickSearchQuery.test.ts +63 -0
  92. package/lib/dashboardQuickSearchQuery.ts +179 -0
  93. package/lib/dashboardSessionNav.ts +33 -0
  94. package/lib/dashboardShortcuts.ts +268 -0
  95. package/lib/dashboardTimeZone.ts +91 -0
  96. package/lib/dashboardTourStorage.ts +68 -0
  97. package/lib/dataDir.test.ts +87 -0
  98. package/lib/dataDir.ts +83 -0
  99. package/lib/devDataPreferenceFile.ts +55 -0
  100. package/lib/devDataRuntimeInfo.ts +34 -0
  101. package/lib/formatIsoShort.test.ts +46 -0
  102. package/lib/formatIsoShort.ts +29 -0
  103. package/lib/generatedUserChangelog.ts +34 -0
  104. package/lib/gitlabIssueSearch.ts +8 -0
  105. package/lib/kronoFocusDurationHistory.ts +71 -0
  106. package/lib/kronoFocusRhythm.test.ts +130 -0
  107. package/lib/kronoFocusRhythm.ts +46 -0
  108. package/lib/kronoFocusTimerUrgency.test.ts +74 -0
  109. package/lib/kronoFocusTimerUrgency.ts +24 -0
  110. package/lib/kronosysApi.ts +143 -0
  111. package/lib/legacyEditorPayloadKeys.ts +52 -0
  112. package/lib/legacyKronoFocusStorageKeys.test.ts +29 -0
  113. package/lib/legacyKronoFocusStorageKeys.ts +32 -0
  114. package/lib/licensesCopy.ts +128 -0
  115. package/lib/openPlainTextInNewTab.ts +49 -0
  116. package/lib/readKronosysPackageVersion.ts +10 -0
  117. package/lib/reportingAggregate.test.ts +325 -0
  118. package/lib/reportingAggregate.ts +819 -0
  119. package/lib/reportingDatePresets.ts +41 -0
  120. package/lib/reportingMetricHelp.ts +430 -0
  121. package/lib/reportingNonFinalIndicators.test.ts +157 -0
  122. package/lib/reportingNonFinalIndicators.ts +102 -0
  123. package/lib/reportingStrings.ts +491 -0
  124. package/lib/reportingTagWeekBreakdown.test.ts +141 -0
  125. package/lib/reportingTagWeekBreakdown.ts +181 -0
  126. package/lib/reportingWeekLayout.test.ts +239 -0
  127. package/lib/reportingWeekLayout.ts +313 -0
  128. package/lib/sessionAssiduity.test.ts +25 -0
  129. package/lib/sessionAssiduity.ts +33 -0
  130. package/lib/sessionEndReason.ts +55 -0
  131. package/lib/sessionEndWarnings.test.ts +200 -0
  132. package/lib/sessionEndWarnings.ts +125 -0
  133. package/lib/sessionListMerge.test.ts +101 -0
  134. package/lib/sessionListMerge.ts +70 -0
  135. package/lib/sessionTaskSidebarStats.test.ts +24 -0
  136. package/lib/sessionTaskSidebarStats.ts +54 -0
  137. package/lib/settingsCopy.ts +1276 -0
  138. package/lib/taskParsing.test.ts +153 -0
  139. package/lib/taskParsing.ts +737 -0
  140. package/lib/theme.ts +15 -0
  141. package/lib/translucentButtonClasses.ts +34 -0
  142. package/lib/usageProfile.test.ts +84 -0
  143. package/lib/usageProfile.ts +52 -0
  144. package/lib/userGuideCopy.ts +464 -0
  145. package/lib/workspaceLocDefaults.ts +21 -0
  146. package/next-env.d.ts +6 -0
  147. package/next.config.ts +15 -0
  148. package/package.json +87 -0
  149. package/postcss.config.mjs +12 -0
  150. package/public/apple-icon.png +0 -0
  151. package/public/favicon.ico +0 -0
  152. package/public/file.svg +1 -0
  153. package/public/globe.svg +1 -0
  154. package/public/icon-192.png +0 -0
  155. package/public/icon-512.png +0 -0
  156. package/public/icon.png +0 -0
  157. package/public/next.svg +1 -0
  158. package/public/sw.js +13 -0
  159. package/public/traceback.png +0 -0
  160. package/public/vercel.svg +1 -0
  161. package/public/window.svg +1 -0
  162. package/server/actionDispatch.test.ts +723 -0
  163. package/server/actionDispatch.ts +1476 -0
  164. package/server/actionTaskSession.test.ts +713 -0
  165. package/server/actionTaskSession.ts +717 -0
  166. package/server/db.ts +42 -0
  167. package/server/defaultCfg.ts +87 -0
  168. package/server/gitlabTokenStore.ts +34 -0
  169. package/server/kronoFocusHydrate.test.ts +142 -0
  170. package/server/kronoFocusHydrate.ts +69 -0
  171. package/server/kronoFocusMigrate.test.ts +53 -0
  172. package/server/kronoFocusMigrate.ts +78 -0
  173. package/server/mainTimerHydrate.test.ts +65 -0
  174. package/server/mainTimerHydrate.ts +53 -0
  175. package/server/payloadStore.test.ts +78 -0
  176. package/server/payloadStore.ts +83 -0
  177. package/server/sessionWallHydrate.test.ts +46 -0
  178. package/server/sessionWallHydrate.ts +88 -0
  179. package/tsconfig.json +41 -0
@@ -0,0 +1,125 @@
1
+ import type { DashboardStrings } from "./dashboardCopy";
2
+
3
+ /**
4
+ * Détection des cas où terminer la session live mérite une confirmation
5
+ * (tâche suivie, autres tâches non terminées, sous-tâches ouvertes).
6
+ * Aligné sur la logique du panneau tâches (`tasks` + `activeTasks` / `activeTask`, pause manuelle).
7
+ */
8
+
9
+ export type LiveTaskEndSnapshot = {
10
+ id: string;
11
+ isDone?: boolean;
12
+ manualTaskTimerPaused?: boolean;
13
+ subtasks?: Array<{ done?: boolean }>;
14
+ };
15
+
16
+ export type LiveSessionEndSnapshot = {
17
+ activeTasks?: LiveTaskEndSnapshot[];
18
+ activeTask?: LiveTaskEndSnapshot | null;
19
+ tasks?: LiveTaskEndSnapshot[];
20
+ };
21
+
22
+ export type EndLiveSessionWarningFlags = {
23
+ hasActiveTracking: boolean;
24
+ hasPausedOrPendingTasks: boolean;
25
+ hasIncompleteSubtasks: boolean;
26
+ };
27
+
28
+ export function getEndLiveSessionWarningFlags(
29
+ live: LiveSessionEndSnapshot | null | undefined
30
+ ): EndLiveSessionWarningFlags {
31
+ if (!live) {
32
+ return {
33
+ hasActiveTracking: false,
34
+ hasPausedOrPendingTasks: false,
35
+ hasIncompleteSubtasks: false,
36
+ };
37
+ }
38
+
39
+ const listedTasks = Array.isArray(live.tasks) ? live.tasks : [];
40
+ const activeStack = Array.isArray(live.activeTasks) ? live.activeTasks : [];
41
+ const activeSingle = live.activeTask ? [live.activeTask] : [];
42
+ const activeTasks = [...activeStack, ...activeSingle];
43
+
44
+ const hasActiveTracking = activeTasks.some(
45
+ (t) => t && t.isDone !== true && !t.manualTaskTimerPaused
46
+ );
47
+
48
+ const mergedById = new Map<string, LiveTaskEndSnapshot>();
49
+ const add = (task: LiveTaskEndSnapshot | null | undefined) => {
50
+ if (!task) {
51
+ return;
52
+ }
53
+ const id = typeof task.id === "string" ? task.id.trim() : "";
54
+ if (!id) {
55
+ return;
56
+ }
57
+ const prev = mergedById.get(id);
58
+ mergedById.set(id, prev ? { ...prev, ...task } : task);
59
+ };
60
+ for (const task of listedTasks) {
61
+ add(task);
62
+ }
63
+ for (const task of activeTasks) {
64
+ add(task);
65
+ }
66
+
67
+ const mergedTasks = [...mergedById.values()];
68
+ const hasPausedOrPendingTasks = mergedTasks.some((t) => t.isDone !== true);
69
+
70
+ let hasIncompleteSubtasks = false;
71
+ for (const t of mergedTasks) {
72
+ const subs = t.subtasks ?? [];
73
+ if (subs.some((s) => s.done !== true)) {
74
+ hasIncompleteSubtasks = true;
75
+ break;
76
+ }
77
+ }
78
+
79
+ return {
80
+ hasActiveTracking,
81
+ hasPausedOrPendingTasks,
82
+ hasIncompleteSubtasks,
83
+ };
84
+ }
85
+
86
+ export function shouldConfirmEndLiveSession(flags: EndLiveSessionWarningFlags): boolean {
87
+ return flags.hasActiveTracking || flags.hasPausedOrPendingTasks || flags.hasIncompleteSubtasks;
88
+ }
89
+
90
+ type EndLiveConfirmStrings = Pick<
91
+ DashboardStrings,
92
+ | "sessionEndLiveConfirmIntro"
93
+ | "sessionEndLiveConfirmIntroCalm"
94
+ | "sessionEndLiveWarnActiveTask"
95
+ | "sessionEndLiveWarnPausedTasks"
96
+ | "sessionEndLiveWarnIncompleteSubtasks"
97
+ >;
98
+
99
+ export function formatEndLiveSessionConfirmMessage(
100
+ flags: EndLiveSessionWarningFlags,
101
+ t: EndLiveConfirmStrings
102
+ ): string {
103
+ const bullets: string[] = [];
104
+ if (flags.hasActiveTracking) {
105
+ bullets.push(`• ${t.sessionEndLiveWarnActiveTask}`);
106
+ }
107
+ if (flags.hasPausedOrPendingTasks) {
108
+ bullets.push(`• ${t.sessionEndLiveWarnPausedTasks}`);
109
+ }
110
+ if (flags.hasIncompleteSubtasks) {
111
+ bullets.push(`• ${t.sessionEndLiveWarnIncompleteSubtasks}`);
112
+ }
113
+ return [t.sessionEndLiveConfirmIntro, "", ...bullets].join("\n");
114
+ }
115
+
116
+ /** Texte du corps du modal : avertissements détaillés ou rappel court si rien d’ouvert. */
117
+ export function formatEndLiveSessionModalIntro(
118
+ flags: EndLiveSessionWarningFlags,
119
+ t: EndLiveConfirmStrings
120
+ ): string {
121
+ if (shouldConfirmEndLiveSession(flags)) {
122
+ return formatEndLiveSessionConfirmMessage(flags, t);
123
+ }
124
+ return t.sessionEndLiveConfirmIntroCalm;
125
+ }
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { mergeLiveSessionIntoHistory } from "./sessionListMerge";
4
+
5
+ const BASE_ENTRY = {
6
+ sessionId: "ses-1",
7
+ sessionName: "Session A",
8
+ savedAt: "2026-04-01T10:00:00Z",
9
+ };
10
+
11
+ describe("mergeLiveSessionIntoHistory", () => {
12
+ it("retourne l'historique tel quel si live est undefined", () => {
13
+ const history = [BASE_ENTRY];
14
+ expect(mergeLiveSessionIntoHistory(history, undefined)).toEqual(history);
15
+ });
16
+
17
+ it("retourne l'historique tel quel si live n'a pas de sessionId", () => {
18
+ const history = [BASE_ENTRY];
19
+ expect(mergeLiveSessionIntoHistory(history, {})).toEqual(history);
20
+ });
21
+
22
+ it("retourne l'historique tel quel si sessionId est une chaîne vide", () => {
23
+ const history = [BASE_ENTRY];
24
+ expect(mergeLiveSessionIntoHistory(history, { sessionId: " " })).toEqual(history);
25
+ });
26
+
27
+ it("prépose la session live en tête si absente de l'historique", () => {
28
+ const history = [BASE_ENTRY];
29
+ const live = { sessionId: "ses-live", sessionName: "Live" };
30
+ const result = mergeLiveSessionIntoHistory(history, live);
31
+ expect(result[0].sessionId).toBe("ses-live");
32
+ expect(result[1]).toEqual(BASE_ENTRY);
33
+ });
34
+
35
+ it("remplace l'entrée existante par la version live (déduplique)", () => {
36
+ const history = [
37
+ { ...BASE_ENTRY, sessionName: "Ancienne" },
38
+ { sessionId: "ses-2", sessionName: "Autre" },
39
+ ];
40
+ const live = { sessionId: "ses-1", sessionName: "Mise à jour" };
41
+ const result = mergeLiveSessionIntoHistory(history, live);
42
+ expect(result).toHaveLength(2);
43
+ expect(result[0].sessionId).toBe("ses-1");
44
+ expect(result[0].sessionName).toBe("Mise à jour");
45
+ });
46
+
47
+ it("copie sessionDurationMinutes si numérique fini", () => {
48
+ const result = mergeLiveSessionIntoHistory([], {
49
+ sessionId: "ses-x",
50
+ sessionDurationMinutes: 42,
51
+ });
52
+ expect(result[0].sessionDurationMinutes).toBe(42);
53
+ });
54
+
55
+ it("ignore sessionDurationMinutes si Infinity", () => {
56
+ const result = mergeLiveSessionIntoHistory([], {
57
+ sessionId: "ses-x",
58
+ sessionDurationMinutes: Infinity,
59
+ });
60
+ expect(result[0].sessionDurationMinutes).toBeUndefined();
61
+ });
62
+
63
+ it("copie mongoPushedAt et mongoLastPushedSavedAt si définis", () => {
64
+ const result = mergeLiveSessionIntoHistory([], {
65
+ sessionId: "ses-x",
66
+ mongoPushedAt: "2026-01-01T00:00:00Z",
67
+ mongoLastPushedSavedAt: "2026-01-02T00:00:00Z",
68
+ });
69
+ expect(result[0].mongoPushedAt).toBe("2026-01-01T00:00:00Z");
70
+ expect(result[0].mongoLastPushedSavedAt).toBe("2026-01-02T00:00:00Z");
71
+ });
72
+
73
+ it("copie sessionEndReasonKind si non vide", () => {
74
+ const result = mergeLiveSessionIntoHistory([], {
75
+ sessionId: "ses-x",
76
+ sessionEndReasonKind: "done",
77
+ });
78
+ expect(result[0].sessionEndReasonKind).toBe("done");
79
+ });
80
+
81
+ it("ignore sessionEndReasonKind si chaîne vide ou espaces", () => {
82
+ const result = mergeLiveSessionIntoHistory([], {
83
+ sessionId: "ses-x",
84
+ sessionEndReasonKind: " ",
85
+ });
86
+ expect(result[0].sessionEndReasonKind).toBeUndefined();
87
+ });
88
+
89
+ it("normalise sessionId numérique en chaîne", () => {
90
+ const result = mergeLiveSessionIntoHistory([], { sessionId: 99 });
91
+ expect(result[0].sessionId).toBe("99");
92
+ });
93
+
94
+ it("endAt null est préservé", () => {
95
+ const result = mergeLiveSessionIntoHistory([], {
96
+ sessionId: "ses-x",
97
+ endAt: null,
98
+ });
99
+ expect(result[0].endAt).toBeNull();
100
+ });
101
+ });
@@ -0,0 +1,70 @@
1
+ import type { SessionListEntry } from "@/components/dashboard/SessionListPanel";
2
+
3
+ type LiveLike = {
4
+ sessionId?: unknown;
5
+ sessionName?: string;
6
+ savedAt?: string;
7
+ createdAt?: string | null;
8
+ startAt?: string | null;
9
+ endAt?: string | null;
10
+ sessionEndReasonKind?: unknown;
11
+ sessionEndReasonNote?: unknown;
12
+ sessionDurationMinutes?: number;
13
+ tasks?: unknown[];
14
+ activeTasks?: unknown[];
15
+ activeTask?: unknown | null;
16
+ mongoPushedAt?: string;
17
+ mongoLastPushedSavedAt?: string;
18
+ };
19
+
20
+ function normalizeLiveId(raw: unknown): string {
21
+ if (typeof raw === "string") {
22
+ return raw.trim();
23
+ }
24
+ if (raw !== undefined && raw !== null && String(raw).trim() !== "") {
25
+ return String(raw).trim();
26
+ }
27
+ return "";
28
+ }
29
+
30
+ /**
31
+ * Garantit que la session en cours (`payload.current`) figure dans la colonne Sessions,
32
+ * même si l’API a renvoyé une liste d’historique incomplète ou désynchronisée.
33
+ */
34
+ export function mergeLiveSessionIntoHistory(
35
+ history: SessionListEntry[],
36
+ live: LiveLike | undefined
37
+ ): SessionListEntry[] {
38
+ const id = normalizeLiveId(live?.sessionId);
39
+ if (!id) {
40
+ return history;
41
+ }
42
+ const without = history.filter((s) => s.sessionId !== id);
43
+ const entry: SessionListEntry = {
44
+ sessionId: id,
45
+ sessionName: typeof live?.sessionName === "string" ? live.sessionName : undefined,
46
+ savedAt: typeof live?.savedAt === "string" ? live.savedAt : undefined,
47
+ createdAt: live?.createdAt ?? live?.startAt ?? undefined,
48
+ startAt: live?.startAt ?? undefined,
49
+ endAt: typeof live?.endAt === "string" || live?.endAt === null ? live.endAt : undefined,
50
+ tasks: Array.isArray(live?.tasks) ? live.tasks : undefined,
51
+ activeTasks: Array.isArray(live?.activeTasks) ? live.activeTasks : undefined,
52
+ activeTask: live?.activeTask ?? undefined,
53
+ };
54
+ if (typeof live?.sessionDurationMinutes === "number" && Number.isFinite(live.sessionDurationMinutes)) {
55
+ entry.sessionDurationMinutes = live.sessionDurationMinutes;
56
+ }
57
+ if (typeof live?.mongoPushedAt === "string") {
58
+ entry.mongoPushedAt = live.mongoPushedAt;
59
+ }
60
+ if (typeof live?.mongoLastPushedSavedAt === "string") {
61
+ entry.mongoLastPushedSavedAt = live.mongoLastPushedSavedAt;
62
+ }
63
+ if (typeof live?.sessionEndReasonKind === "string" && live.sessionEndReasonKind.trim() !== "") {
64
+ entry.sessionEndReasonKind = live.sessionEndReasonKind.trim();
65
+ }
66
+ if (typeof live?.sessionEndReasonNote === "string" && live.sessionEndReasonNote.trim() !== "") {
67
+ entry.sessionEndReasonNote = live.sessionEndReasonNote.trim();
68
+ }
69
+ return [entry, ...without];
70
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { countSessionTasksForSidebar } from "./sessionTaskSidebarStats";
3
+
4
+ describe("countSessionTasksForSidebar", () => {
5
+ it("session ouverte : compte les non terminées en liste en pause", () => {
6
+ expect(
7
+ countSessionTasksForSidebar({
8
+ endAt: null,
9
+ tasks: [{ isDone: true }, { isDone: false }, { isDone: false }],
10
+ activeTasks: [],
11
+ })
12
+ ).toEqual({ running: 0, pausedList: 2, completed: 1 });
13
+ });
14
+
15
+ it("session terminée (endAt) : pas d’en cours ni de liste en pause, seulement les terminées", () => {
16
+ expect(
17
+ countSessionTasksForSidebar({
18
+ endAt: "2026-04-16T12:00:00.000Z",
19
+ tasks: [{ isDone: true }, { isDone: false }, { isDone: false }],
20
+ activeTasks: [{ isDone: false, manualTaskTimerPaused: false }],
21
+ })
22
+ ).toEqual({ running: 0, pausedList: 0, completed: 1 });
23
+ });
24
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Compteurs de tâches pour la carte « session sélectionnée », alignés sur {@link TaskFocusPanel}.
3
+ */
4
+
5
+ type TaskLike = { id?: string; isDone?: boolean; manualTaskTimerPaused?: boolean };
6
+
7
+ type SessionLike = {
8
+ tasks?: TaskLike[];
9
+ activeTasks?: TaskLike[];
10
+ activeTask?: TaskLike | null;
11
+ /** Session clôturée : pas de compteurs « en cours » ou « en liste » pour l’affichage carte session. */
12
+ endAt?: string | null;
13
+ };
14
+
15
+ function sessionClosedForDisplay(session: SessionLike | null | undefined): boolean {
16
+ return typeof session?.endAt === "string" && session.endAt.trim() !== "";
17
+ }
18
+
19
+ function activeStackFromSession(session: SessionLike | null | undefined): TaskLike[] {
20
+ if (!session) {
21
+ return [];
22
+ }
23
+ const raw =
24
+ Array.isArray(session.activeTasks) && session.activeTasks.length > 0
25
+ ? session.activeTasks
26
+ : session.activeTask
27
+ ? [session.activeTask]
28
+ : [];
29
+ return raw.filter((t): t is TaskLike => Boolean(t));
30
+ }
31
+
32
+ /** Tâches avec minuteur actif (!terminé, pas en pause manuelle). */
33
+ export function countRunningTasksOnTimer(session: SessionLike | null | undefined): number {
34
+ return activeStackFromSession(session).filter((t) => !t.isDone && !t.manualTaskTimerPaused).length;
35
+ }
36
+
37
+ /**
38
+ * Même regroupement que le panneau tâches : minuteur actif, liste des non terminées, terminées.
39
+ */
40
+ export function countSessionTasksForSidebar(session: SessionLike | null | undefined): {
41
+ running: number;
42
+ pausedList: number;
43
+ completed: number;
44
+ } {
45
+ const taskList = session?.tasks ?? [];
46
+ const completed = taskList.filter((t) => t.isDone === true).length;
47
+ const closed = sessionClosedForDisplay(session);
48
+ const pausedList = closed ? 0 : taskList.filter((t) => t.isDone !== true).length;
49
+ return {
50
+ running: closed ? 0 : countRunningTasksOnTimer(session),
51
+ pausedList,
52
+ completed,
53
+ };
54
+ }