@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.
Files changed (112) hide show
  1. package/README.md +28 -1
  2. package/app/api/action/route.ts +39 -3
  3. package/app/api/action-logs/route.ts +24 -0
  4. package/app/api/backup/route.ts +1 -1
  5. package/app/api/restore/route.ts +145 -0
  6. package/app/changelog/page.tsx +71 -4
  7. package/app/globals.css +127 -0
  8. package/app/guide/page.tsx +61 -15
  9. package/app/implementation/page.tsx +700 -0
  10. package/app/layout.tsx +14 -3
  11. package/app/licenses/page.tsx +99 -37
  12. package/app/logs/page.tsx +258 -0
  13. package/app/manifest.ts +5 -5
  14. package/app/page.tsx +784 -229
  15. package/app/reporting/page.tsx +1266 -474
  16. package/app/settings/page.tsx +252 -18
  17. package/bin/kronosys.mjs +140 -15
  18. package/components/KronosysPayloadProvider.tsx +2 -0
  19. package/components/RouteTransition.tsx +18 -0
  20. package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
  21. package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
  22. package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
  23. package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
  24. package/components/dashboard/AppShellRouteNav.tsx +323 -48
  25. package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
  26. package/components/dashboard/DashboardSimpleModal.tsx +168 -25
  27. package/components/dashboard/DashboardTour.tsx +115 -29
  28. package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
  29. package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
  30. package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
  31. package/components/dashboard/NewSessionScopeModal.tsx +211 -20
  32. package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
  33. package/components/dashboard/ReportingTour.tsx +87 -21
  34. package/components/dashboard/SavedProjectPicker.tsx +16 -3
  35. package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
  36. package/components/dashboard/SessionListPanel.tsx +327 -44
  37. package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
  38. package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
  39. package/components/dashboard/SettingsTour.tsx +86 -21
  40. package/components/dashboard/TagPills.tsx +14 -1
  41. package/components/dashboard/TaskFocusPanel.tsx +1081 -478
  42. package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
  43. package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
  44. package/components/dashboard/taskFieldStyles.ts +20 -4
  45. package/components/dashboard/useReportingInteractionState.ts +80 -0
  46. package/lib/appShellHeaderClasses.ts +13 -0
  47. package/lib/businessRulesMatrix.ts +210 -0
  48. package/lib/copyToClipboard.ts +43 -0
  49. package/lib/dashboardCopy.ts +494 -84
  50. package/lib/dashboardQuickSearch.ts +54 -2
  51. package/lib/dashboardTimeZone.ts +109 -0
  52. package/lib/formatAppShellWallClock.ts +66 -0
  53. package/lib/formatSessionNameTemplate.ts +141 -0
  54. package/lib/generatedUserChangelog.ts +177 -6
  55. package/lib/globalPausePreview.ts +292 -0
  56. package/lib/implementationNotes.ts +1188 -0
  57. package/lib/kronosysApi.ts +6 -0
  58. package/lib/kronosysDashboardModalGates.ts +24 -0
  59. package/lib/plannedBoundaryAttention.ts +9 -0
  60. package/lib/plannedBoundaryConflict.ts +23 -0
  61. package/lib/reportingAggregate.ts +517 -75
  62. package/lib/reportingMetricHelp.ts +8 -0
  63. package/lib/reportingStrings.ts +37 -3
  64. package/lib/sessionListMerge.ts +4 -0
  65. package/lib/sessionTaskSidebarStats.ts +182 -21
  66. package/lib/settingsCopy.ts +178 -4
  67. package/lib/taskParsing.ts +360 -103
  68. package/lib/taskTemplateDraft.ts +135 -0
  69. package/lib/taskTimelineGantt.ts +265 -0
  70. package/lib/temporalDisplayPlanned.ts +71 -0
  71. package/lib/userGuideCopy.ts +121 -47
  72. package/next.config.ts +7 -0
  73. package/package.json +12 -24
  74. package/server/actionDispatch.ts +1000 -77
  75. package/server/actionTaskSession.ts +337 -24
  76. package/server/db.ts +7 -15
  77. package/server/dbSchema.ts +24 -0
  78. package/server/defaultCfg.ts +5 -0
  79. package/server/gitlabTokenStore.ts +0 -12
  80. package/server/liveHistorySync.ts +53 -0
  81. package/server/mainTimerHydrate.ts +38 -2
  82. package/server/payloadStore.ts +33 -11
  83. package/server/sessionWallHydrate.ts +66 -3
  84. package/server/userActionLog.ts +126 -0
  85. package/sonar-project.properties +11 -0
  86. package/tsconfig.json +2 -1
  87. package/components/dashboard/IssuePickerModal.tsx +0 -168
  88. package/components/dashboard/ThemeToggle.test.tsx +0 -26
  89. package/lib/backupCsvExport.test.ts +0 -149
  90. package/lib/dashboardQuickSearchQuery.test.ts +0 -63
  91. package/lib/dataDir.test.ts +0 -87
  92. package/lib/formatIsoShort.test.ts +0 -46
  93. package/lib/kronoFocusRhythm.test.ts +0 -130
  94. package/lib/kronoFocusTimerUrgency.test.ts +0 -74
  95. package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
  96. package/lib/reportingAggregate.test.ts +0 -325
  97. package/lib/reportingNonFinalIndicators.test.ts +0 -157
  98. package/lib/reportingTagWeekBreakdown.test.ts +0 -141
  99. package/lib/reportingWeekLayout.test.ts +0 -239
  100. package/lib/sessionAssiduity.test.ts +0 -25
  101. package/lib/sessionEndWarnings.test.ts +0 -200
  102. package/lib/sessionListMerge.test.ts +0 -101
  103. package/lib/sessionTaskSidebarStats.test.ts +0 -24
  104. package/lib/taskParsing.test.ts +0 -153
  105. package/lib/usageProfile.test.ts +0 -84
  106. package/server/actionDispatch.test.ts +0 -723
  107. package/server/actionTaskSession.test.ts +0 -713
  108. package/server/kronoFocusHydrate.test.ts +0 -142
  109. package/server/kronoFocusMigrate.test.ts +0 -53
  110. package/server/mainTimerHydrate.test.ts +0 -65
  111. package/server/payloadStore.test.ts +0 -78
  112. package/server/sessionWallHydrate.test.ts +0 -46
@@ -1,239 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import {
4
- addDaysYmd,
5
- buildKeyedWeekCalendarRows,
6
- buildProjectWeekCalendarRows,
7
- buildTagWeekCalendarRows,
8
- formatWeekRangeLabel,
9
- localCalendarDaysBetween,
10
- localWeekStartKeyFromDayKey,
11
- weekdayColumnLabels,
12
- weekdayDateColumnHeaders,
13
- } from "./reportingWeekLayout";
14
-
15
- // ---------------------------------------------------------------------------
16
- // addDaysYmd
17
- // ---------------------------------------------------------------------------
18
- describe("addDaysYmd", () => {
19
- it("ajoute des jours positifs", () => {
20
- expect(addDaysYmd("2026-04-29", 1)).toBe("2026-04-30");
21
- expect(addDaysYmd("2026-04-29", 7)).toBe("2026-05-06");
22
- });
23
-
24
- it("soustrait des jours (delta négatif)", () => {
25
- expect(addDaysYmd("2026-05-01", -1)).toBe("2026-04-30");
26
- });
27
-
28
- it("traverse les fins de mois et d'année", () => {
29
- expect(addDaysYmd("2026-12-31", 1)).toBe("2027-01-01");
30
- expect(addDaysYmd("2026-02-28", 1)).toBe("2026-03-01");
31
- });
32
-
33
- it("delta = 0 retourne la même date", () => {
34
- expect(addDaysYmd("2026-04-15", 0)).toBe("2026-04-15");
35
- });
36
- });
37
-
38
- // ---------------------------------------------------------------------------
39
- // localCalendarDaysBetween
40
- // ---------------------------------------------------------------------------
41
- describe("localCalendarDaysBetween", () => {
42
- it("même jour → 0", () => {
43
- expect(localCalendarDaysBetween("2026-04-29", "2026-04-29")).toBe(0);
44
- });
45
-
46
- it("différence positive", () => {
47
- expect(localCalendarDaysBetween("2026-04-20", "2026-04-27")).toBe(7);
48
- });
49
-
50
- it("différence négative (fin < début)", () => {
51
- expect(localCalendarDaysBetween("2026-04-27", "2026-04-20")).toBe(-7);
52
- });
53
- });
54
-
55
- // ---------------------------------------------------------------------------
56
- // localWeekStartKeyFromDayKey
57
- // ---------------------------------------------------------------------------
58
- describe("localWeekStartKeyFromDayKey", () => {
59
- it("retourne null pour chaîne vide", () => {
60
- expect(localWeekStartKeyFromDayKey("", "monday")).toBeNull();
61
- });
62
-
63
- it("retourne null pour 'undated'", () => {
64
- expect(localWeekStartKeyFromDayKey("undated", "monday")).toBeNull();
65
- });
66
-
67
- it("retourne null pour format invalide", () => {
68
- expect(localWeekStartKeyFromDayKey("2026-04", "monday")).toBeNull();
69
- });
70
-
71
- it("lundi de la semaine contenant mercredi 2026-04-29 (weekStartsOn=monday)", () => {
72
- // 2026-04-29 est un mercredi → lundi = 2026-04-27
73
- expect(localWeekStartKeyFromDayKey("2026-04-29", "monday")).toBe("2026-04-27");
74
- });
75
-
76
- it("dimanche de la semaine contenant mercredi 2026-04-29 (weekStartsOn=sunday)", () => {
77
- // semaine commence dimanche → 2026-04-26
78
- expect(localWeekStartKeyFromDayKey("2026-04-29", "sunday")).toBe("2026-04-26");
79
- });
80
-
81
- it("samedi de la semaine (weekStartsOn=saturday)", () => {
82
- // semaine commence samedi → 2026-04-25
83
- expect(localWeekStartKeyFromDayKey("2026-04-29", "saturday")).toBe("2026-04-25");
84
- });
85
-
86
- it("le premier jour de la semaine pointe sur lui-même", () => {
87
- // 2026-04-27 est un lundi
88
- expect(localWeekStartKeyFromDayKey("2026-04-27", "monday")).toBe("2026-04-27");
89
- });
90
- });
91
-
92
- // ---------------------------------------------------------------------------
93
- // weekdayColumnLabels
94
- // ---------------------------------------------------------------------------
95
- describe("weekdayColumnLabels", () => {
96
- it("retourne 7 libellés", () => {
97
- expect(weekdayColumnLabels("monday", "en-CA")).toHaveLength(7);
98
- expect(weekdayColumnLabels("sunday", "fr-CA")).toHaveLength(7);
99
- });
100
-
101
- it("les libellés sont non vides", () => {
102
- const labels = weekdayColumnLabels("monday", "en-CA");
103
- for (const l of labels) {
104
- expect(l.trim().length).toBeGreaterThan(0);
105
- }
106
- });
107
- });
108
-
109
- // ---------------------------------------------------------------------------
110
- // weekdayDateColumnHeaders
111
- // ---------------------------------------------------------------------------
112
- describe("weekdayDateColumnHeaders", () => {
113
- it("retourne 7 en-têtes depuis le weekStartKey", () => {
114
- const headers = weekdayDateColumnHeaders("2026-04-27", "en-CA");
115
- expect(headers).toHaveLength(7);
116
- });
117
-
118
- it("le premier dateKey correspond au weekStartKey", () => {
119
- const headers = weekdayDateColumnHeaders("2026-04-27", "en-CA");
120
- expect(headers[0].dateKey).toBe("2026-04-27");
121
- });
122
-
123
- it("le dernier dateKey est weekStart + 6 jours", () => {
124
- const headers = weekdayDateColumnHeaders("2026-04-27", "en-CA");
125
- expect(headers[6].dateKey).toBe("2026-05-03");
126
- });
127
-
128
- it("chaque en-tête a weekdayShort et calendarDateShort non vides", () => {
129
- const headers = weekdayDateColumnHeaders("2026-04-27", "fr-CA");
130
- for (const h of headers) {
131
- expect(h.weekdayShort.trim().length).toBeGreaterThan(0);
132
- expect(h.calendarDateShort.trim().length).toBeGreaterThan(0);
133
- }
134
- });
135
- });
136
-
137
- // ---------------------------------------------------------------------------
138
- // formatWeekRangeLabel
139
- // ---------------------------------------------------------------------------
140
- describe("formatWeekRangeLabel", () => {
141
- it("retourne une chaîne non vide", () => {
142
- const label = formatWeekRangeLabel("2026-04-27", "fr-CA");
143
- expect(label.trim().length).toBeGreaterThan(0);
144
- });
145
-
146
- it("inclut l'année dans le label", () => {
147
- const label = formatWeekRangeLabel("2026-04-27", "en-CA");
148
- expect(label).toContain("2026");
149
- });
150
- });
151
-
152
- // ---------------------------------------------------------------------------
153
- // buildKeyedWeekCalendarRows
154
- // ---------------------------------------------------------------------------
155
- describe("buildKeyedWeekCalendarRows", () => {
156
- it("retourne un tableau vide pour une entrée vide", () => {
157
- expect(buildKeyedWeekCalendarRows([], "monday")).toEqual([]);
158
- });
159
-
160
- it("ignore les lignes 'undated'", () => {
161
- const rows = [{ day: "undated", rowKey: "dev", displayLabel: "Dev", minutes: 60 }];
162
- expect(buildKeyedWeekCalendarRows(rows, "monday")).toEqual([]);
163
- });
164
-
165
- it("ignore les lignes avec minutes <= 0", () => {
166
- const rows = [{ day: "2026-04-27", rowKey: "dev", displayLabel: "Dev", minutes: 0 }];
167
- expect(buildKeyedWeekCalendarRows(rows, "monday")).toEqual([]);
168
- });
169
-
170
- it("agrège correctement deux entrées dans la même semaine pour la même clé", () => {
171
- const rows = [
172
- { day: "2026-04-27", rowKey: "dev", displayLabel: "Dev", minutes: 30 },
173
- { day: "2026-04-28", rowKey: "dev", displayLabel: "Dev", minutes: 45 },
174
- ];
175
- const result = buildKeyedWeekCalendarRows(rows, "monday");
176
- expect(result).toHaveLength(1);
177
- expect(result[0].total).toBe(75);
178
- expect(result[0].slots[0]).toBe(30); // lundi (idx 0)
179
- expect(result[0].slots[1]).toBe(45); // mardi (idx 1)
180
- });
181
-
182
- it("deux clés différentes → deux lignes", () => {
183
- const rows = [
184
- { day: "2026-04-27", rowKey: "dev", displayLabel: "Dev", minutes: 30 },
185
- { day: "2026-04-27", rowKey: "design", displayLabel: "Design", minutes: 20 },
186
- ];
187
- const result = buildKeyedWeekCalendarRows(rows, "monday");
188
- expect(result).toHaveLength(2);
189
- });
190
-
191
- it("deux semaines différentes → deux lignes pour la même clé", () => {
192
- const rows = [
193
- { day: "2026-04-27", rowKey: "dev", displayLabel: "Dev", minutes: 30 },
194
- { day: "2026-05-04", rowKey: "dev", displayLabel: "Dev", minutes: 60 },
195
- ];
196
- const result = buildKeyedWeekCalendarRows(rows, "monday");
197
- expect(result).toHaveLength(2);
198
- });
199
-
200
- it("la clé vide est triée en dernier (empty key last)", () => {
201
- const rows = [
202
- { day: "2026-04-27", rowKey: "", displayLabel: "", minutes: 10 },
203
- { day: "2026-04-27", rowKey: "zzz", displayLabel: "Zzz", minutes: 10 },
204
- { day: "2026-04-27", rowKey: "aaa", displayLabel: "Aaa", minutes: 10 },
205
- ];
206
- const result = buildKeyedWeekCalendarRows(rows, "monday");
207
- expect(result[result.length - 1].rowKey).toBe("");
208
- });
209
- });
210
-
211
- // ---------------------------------------------------------------------------
212
- // buildTagWeekCalendarRows (wrapper)
213
- // ---------------------------------------------------------------------------
214
- describe("buildTagWeekCalendarRows", () => {
215
- it("mappe correctement tagKey et displayTag", () => {
216
- const rows = [
217
- { day: "2026-04-27", tagKey: "dev", displayTag: "Développement", minutes: 60 },
218
- ];
219
- const result = buildTagWeekCalendarRows(rows, "monday");
220
- expect(result[0].tagKey).toBe("dev");
221
- expect(result[0].displayTag).toBe("Développement");
222
- expect(result[0].total).toBe(60);
223
- });
224
- });
225
-
226
- // ---------------------------------------------------------------------------
227
- // buildProjectWeekCalendarRows (wrapper)
228
- // ---------------------------------------------------------------------------
229
- describe("buildProjectWeekCalendarRows", () => {
230
- it("mappe correctement projectKey et displayProject", () => {
231
- const rows = [
232
- { day: "2026-04-27", projectKey: "acme", displayProject: "Acme Corp", minutes: 90 },
233
- ];
234
- const result = buildProjectWeekCalendarRows(rows, "monday");
235
- expect(result[0].projectKey).toBe("acme");
236
- expect(result[0].displayProject).toBe("Acme Corp");
237
- expect(result[0].total).toBe(90);
238
- });
239
- });
@@ -1,25 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { assiduityFromScheduledStart } from "./sessionAssiduity";
3
-
4
- describe("sessionAssiduity", () => {
5
- it("retourne un écart positif si l’ouverture est après l’heure prévue", () => {
6
- const a = assiduityFromScheduledStart(
7
- Date.parse("2026-04-24T09:15:00.000Z"),
8
- "2026-04-24T09:00:00.000Z"
9
- );
10
- expect(a?.sessionStartOffsetMinutes).toBe(15);
11
- expect(a?.scheduledStartAt).toBe("2026-04-24T09:00:00.000Z");
12
- });
13
-
14
- it("retourne un écart négatif si l’ouverture est en avance", () => {
15
- const a = assiduityFromScheduledStart(
16
- Date.parse("2026-04-24T08:50:00.000Z"),
17
- "2026-04-24T09:00:00.000Z"
18
- );
19
- expect(a?.sessionStartOffsetMinutes).toBe(-10);
20
- });
21
-
22
- it("refuse une chaîne d’horodatage invalide", () => {
23
- expect(assiduityFromScheduledStart(Date.now(), "pas-une-date")).toBeNull();
24
- });
25
- });
@@ -1,200 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import {
4
- formatEndLiveSessionConfirmMessage,
5
- formatEndLiveSessionModalIntro,
6
- getEndLiveSessionWarningFlags,
7
- shouldConfirmEndLiveSession,
8
- } from "./sessionEndWarnings";
9
-
10
- const STRINGS = {
11
- sessionEndLiveConfirmIntro: "Attention :",
12
- sessionEndLiveConfirmIntroCalm: "Tout est tranquille.",
13
- sessionEndLiveWarnActiveTask: "Une tâche est en cours.",
14
- sessionEndLiveWarnPausedTasks: "Des tâches sont en attente.",
15
- sessionEndLiveWarnIncompleteSubtasks: "Des sous-tâches sont ouvertes.",
16
- };
17
-
18
- describe("getEndLiveSessionWarningFlags", () => {
19
- it("retourne tout false si live est null", () => {
20
- const flags = getEndLiveSessionWarningFlags(null);
21
- expect(flags).toEqual({
22
- hasActiveTracking: false,
23
- hasPausedOrPendingTasks: false,
24
- hasIncompleteSubtasks: false,
25
- });
26
- });
27
-
28
- it("retourne tout false si live est undefined", () => {
29
- expect(getEndLiveSessionWarningFlags(undefined)).toEqual({
30
- hasActiveTracking: false,
31
- hasPausedOrPendingTasks: false,
32
- hasIncompleteSubtasks: false,
33
- });
34
- });
35
-
36
- it("hasActiveTracking=true si activeTasks contient une tâche non terminée non en pause", () => {
37
- const flags = getEndLiveSessionWarningFlags({
38
- activeTasks: [{ id: "t1", isDone: false, manualTaskTimerPaused: false }],
39
- });
40
- expect(flags.hasActiveTracking).toBe(true);
41
- });
42
-
43
- it("hasActiveTracking=false si la tâche active est en pause manuelle", () => {
44
- const flags = getEndLiveSessionWarningFlags({
45
- activeTasks: [{ id: "t1", isDone: false, manualTaskTimerPaused: true }],
46
- });
47
- expect(flags.hasActiveTracking).toBe(false);
48
- });
49
-
50
- it("hasActiveTracking=false si la tâche active est terminée", () => {
51
- const flags = getEndLiveSessionWarningFlags({
52
- activeTasks: [{ id: "t1", isDone: true }],
53
- });
54
- expect(flags.hasActiveTracking).toBe(false);
55
- });
56
-
57
- it("utilise activeTask (forme simple) si activeTasks est absent", () => {
58
- const flags = getEndLiveSessionWarningFlags({
59
- activeTask: { id: "t1", isDone: false, manualTaskTimerPaused: false },
60
- });
61
- expect(flags.hasActiveTracking).toBe(true);
62
- });
63
-
64
- it("hasPausedOrPendingTasks=true si tasks contient une tâche non terminée", () => {
65
- const flags = getEndLiveSessionWarningFlags({
66
- tasks: [{ id: "t1", isDone: false }],
67
- });
68
- expect(flags.hasPausedOrPendingTasks).toBe(true);
69
- });
70
-
71
- it("hasPausedOrPendingTasks=false si toutes les tâches sont terminées", () => {
72
- const flags = getEndLiveSessionWarningFlags({
73
- tasks: [{ id: "t1", isDone: true }, { id: "t2", isDone: true }],
74
- });
75
- expect(flags.hasPausedOrPendingTasks).toBe(false);
76
- });
77
-
78
- it("hasIncompleteSubtasks=true si une tâche a une sous-tâche non terminée", () => {
79
- const flags = getEndLiveSessionWarningFlags({
80
- tasks: [{ id: "t1", isDone: false, subtasks: [{ done: false }] }],
81
- });
82
- expect(flags.hasIncompleteSubtasks).toBe(true);
83
- });
84
-
85
- it("hasIncompleteSubtasks=false si toutes les sous-tâches sont done", () => {
86
- const flags = getEndLiveSessionWarningFlags({
87
- tasks: [{ id: "t1", isDone: false, subtasks: [{ done: true }] }],
88
- });
89
- expect(flags.hasIncompleteSubtasks).toBe(false);
90
- });
91
-
92
- it("tâche active avec sous-tâche incomplète est détectée même si absente de tasks", () => {
93
- const flags = getEndLiveSessionWarningFlags({
94
- activeTasks: [{ id: "t-unique", isDone: false, subtasks: [{ done: false }] }],
95
- tasks: [],
96
- });
97
- expect(flags.hasIncompleteSubtasks).toBe(true);
98
- });
99
-
100
- it("avertit pour une tâche active non terminée absente de la liste tasks", () => {
101
- expect(
102
- getEndLiveSessionWarningFlags({
103
- tasks: [],
104
- activeTasks: [{ id: "a", isDone: false }],
105
- })
106
- ).toEqual({
107
- hasActiveTracking: true,
108
- hasPausedOrPendingTasks: true,
109
- hasIncompleteSubtasks: false,
110
- });
111
- });
112
-
113
- it("avertit pour une tâche active en pause absente de la liste tasks", () => {
114
- expect(
115
- getEndLiveSessionWarningFlags({
116
- tasks: [],
117
- activeTasks: [{ id: "a", isDone: false, manualTaskTimerPaused: true }],
118
- }).hasPausedOrPendingTasks
119
- ).toBe(true);
120
- });
121
-
122
- it("avertit pour une sous-tâche ouverte sur activeTask quand activeTasks existe aussi", () => {
123
- expect(
124
- getEndLiveSessionWarningFlags({
125
- tasks: [],
126
- activeTasks: [{ id: "a", isDone: true }],
127
- activeTask: {
128
- id: "b",
129
- isDone: true,
130
- subtasks: [{ done: true }, { done: false }],
131
- },
132
- }).hasIncompleteSubtasks
133
- ).toBe(true);
134
- });
135
- });
136
-
137
- describe("shouldConfirmEndLiveSession", () => {
138
- it("retourne false si aucun flag", () => {
139
- expect(shouldConfirmEndLiveSession({
140
- hasActiveTracking: false,
141
- hasPausedOrPendingTasks: false,
142
- hasIncompleteSubtasks: false,
143
- })).toBe(false);
144
- });
145
-
146
- it("retourne true si hasActiveTracking", () => {
147
- expect(shouldConfirmEndLiveSession({
148
- hasActiveTracking: true,
149
- hasPausedOrPendingTasks: false,
150
- hasIncompleteSubtasks: false,
151
- })).toBe(true);
152
- });
153
-
154
- it("retourne true si hasPausedOrPendingTasks seulement", () => {
155
- expect(shouldConfirmEndLiveSession({
156
- hasActiveTracking: false,
157
- hasPausedOrPendingTasks: true,
158
- hasIncompleteSubtasks: false,
159
- })).toBe(true);
160
- });
161
- });
162
-
163
- describe("formatEndLiveSessionConfirmMessage", () => {
164
- it("inclut uniquement les bullets correspondant aux flags actifs", () => {
165
- const msg = formatEndLiveSessionConfirmMessage(
166
- { hasActiveTracking: true, hasPausedOrPendingTasks: false, hasIncompleteSubtasks: true },
167
- STRINGS,
168
- );
169
- expect(msg).toContain("Attention :");
170
- expect(msg).toContain("• Une tâche est en cours.");
171
- expect(msg).toContain("• Des sous-tâches sont ouvertes.");
172
- expect(msg).not.toContain("attente");
173
- });
174
-
175
- it("message vide de bullets si aucun flag (cas théorique)", () => {
176
- const msg = formatEndLiveSessionConfirmMessage(
177
- { hasActiveTracking: false, hasPausedOrPendingTasks: false, hasIncompleteSubtasks: false },
178
- STRINGS,
179
- );
180
- expect(msg).toBe("Attention :\n");
181
- });
182
- });
183
-
184
- describe("formatEndLiveSessionModalIntro", () => {
185
- it("retourne le message calme si aucun flag", () => {
186
- const intro = formatEndLiveSessionModalIntro(
187
- { hasActiveTracking: false, hasPausedOrPendingTasks: false, hasIncompleteSubtasks: false },
188
- STRINGS,
189
- );
190
- expect(intro).toBe("Tout est tranquille.");
191
- });
192
-
193
- it("retourne le message d'avertissement si au moins un flag", () => {
194
- const intro = formatEndLiveSessionModalIntro(
195
- { hasActiveTracking: false, hasPausedOrPendingTasks: true, hasIncompleteSubtasks: false },
196
- STRINGS,
197
- );
198
- expect(intro).toContain("Des tâches sont en attente.");
199
- });
200
- });
@@ -1,101 +0,0 @@
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
- });
@@ -1,24 +0,0 @@
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
- });