@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
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
|
|
4
|
-
|
|
5
|
-
import { hydrateKronoFocusInPayload } from "./kronoFocusHydrate";
|
|
6
|
-
|
|
7
|
-
function basePayload(pm: Record<string, unknown>): KronosysUpdatePayload {
|
|
8
|
-
return {
|
|
9
|
-
viewType: "dashboard",
|
|
10
|
-
current: {
|
|
11
|
-
sessionId: "s1",
|
|
12
|
-
kronoFocus: pm,
|
|
13
|
-
},
|
|
14
|
-
cfg: {},
|
|
15
|
-
} as KronosysUpdatePayload;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("hydrateKronoFocusInPayload", () => {
|
|
19
|
-
it("ancre une échéance pour un KronoFocus running sans deadline (données héritées)", () => {
|
|
20
|
-
const t0 = 1_000_000_000;
|
|
21
|
-
const p = basePayload({
|
|
22
|
-
mode: "work",
|
|
23
|
-
status: "running",
|
|
24
|
-
timeLeftSeconds: 100,
|
|
25
|
-
});
|
|
26
|
-
const dirty = hydrateKronoFocusInPayload(p, t0);
|
|
27
|
-
expect(dirty).toBe(true);
|
|
28
|
-
const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
|
|
29
|
-
expect(pm.kronoFocusDeadlineAtMs).toBe(t0 + 100_000);
|
|
30
|
-
expect(pm.timeLeftSeconds).toBe(100);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("décompte les secondes restantes avant l’échéance", () => {
|
|
34
|
-
const t0 = 1_000_000_000;
|
|
35
|
-
const p = basePayload({
|
|
36
|
-
mode: "work",
|
|
37
|
-
status: "running",
|
|
38
|
-
timeLeftSeconds: 9999,
|
|
39
|
-
kronoFocusDeadlineAtMs: t0 + 60_000,
|
|
40
|
-
});
|
|
41
|
-
const dirty = hydrateKronoFocusInPayload(p, t0 + 30_000);
|
|
42
|
-
expect(dirty).toBe(false);
|
|
43
|
-
const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
|
|
44
|
-
expect(pm.timeLeftSeconds).toBe(30);
|
|
45
|
-
expect(pm.status).toBe("running");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("à l’échéance d’un bloc travail : utilise shortBreakDurationSeconds du payload", () => {
|
|
49
|
-
const t0 = 1_000_000_000;
|
|
50
|
-
const p = basePayload({
|
|
51
|
-
mode: "work",
|
|
52
|
-
status: "running",
|
|
53
|
-
timeLeftSeconds: 10,
|
|
54
|
-
sessionsCompleted: 0,
|
|
55
|
-
shortBreakDurationSeconds: 10 * 60,
|
|
56
|
-
kronoFocusDeadlineAtMs: t0 + 10_000,
|
|
57
|
-
});
|
|
58
|
-
const dirty = hydrateKronoFocusInPayload(p, t0 + 10_000);
|
|
59
|
-
expect(dirty).toBe(true);
|
|
60
|
-
const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
|
|
61
|
-
expect(pm.status).toBe("paused");
|
|
62
|
-
expect(pm.mode).toBe("break");
|
|
63
|
-
expect(pm.timeLeftSeconds).toBe(10 * 60);
|
|
64
|
-
expect(pm.sessionsCompleted).toBe(1);
|
|
65
|
-
expect(pm.kronoFocusDeadlineAtMs).toBeUndefined();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("à l’échéance d’un bloc travail : pause, pause courte, incrémente sessionsCompleted", () => {
|
|
69
|
-
const t0 = 1_000_000_000;
|
|
70
|
-
const p = basePayload({
|
|
71
|
-
mode: "work",
|
|
72
|
-
status: "running",
|
|
73
|
-
timeLeftSeconds: 10,
|
|
74
|
-
sessionsCompleted: 0,
|
|
75
|
-
kronoFocusDeadlineAtMs: t0 + 10_000,
|
|
76
|
-
});
|
|
77
|
-
const dirty = hydrateKronoFocusInPayload(p, t0 + 10_000);
|
|
78
|
-
expect(dirty).toBe(true);
|
|
79
|
-
const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
|
|
80
|
-
expect(pm.status).toBe("paused");
|
|
81
|
-
expect(pm.mode).toBe("break");
|
|
82
|
-
expect(pm.timeLeftSeconds).toBe(5 * 60);
|
|
83
|
-
expect(pm.sessionsCompleted).toBe(1);
|
|
84
|
-
expect(pm.kronoFocusDeadlineAtMs).toBeUndefined();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("après 4 cycles travail : pause longue 15 min", () => {
|
|
88
|
-
const t0 = 1_000_000_000;
|
|
89
|
-
const p = basePayload({
|
|
90
|
-
mode: "work",
|
|
91
|
-
status: "running",
|
|
92
|
-
timeLeftSeconds: 1,
|
|
93
|
-
sessionsCompleted: 3,
|
|
94
|
-
kronoFocusDeadlineAtMs: t0 + 1000,
|
|
95
|
-
});
|
|
96
|
-
hydrateKronoFocusInPayload(p, t0 + 1000);
|
|
97
|
-
const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
|
|
98
|
-
expect(pm.sessionsCompleted).toBe(4);
|
|
99
|
-
expect(pm.mode).toBe("longBreak");
|
|
100
|
-
expect(pm.timeLeftSeconds).toBe(15 * 60);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("après 4 cycles travail : utilise longBreakDurationSeconds du payload", () => {
|
|
104
|
-
const t0 = 1_000_000_000;
|
|
105
|
-
const p = basePayload({
|
|
106
|
-
mode: "work",
|
|
107
|
-
status: "running",
|
|
108
|
-
timeLeftSeconds: 1,
|
|
109
|
-
sessionsCompleted: 3,
|
|
110
|
-
longBreakDurationSeconds: 22 * 60,
|
|
111
|
-
kronoFocusDeadlineAtMs: t0 + 1000,
|
|
112
|
-
});
|
|
113
|
-
hydrateKronoFocusInPayload(p, t0 + 1000);
|
|
114
|
-
const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
|
|
115
|
-
expect(pm.sessionsCompleted).toBe(4);
|
|
116
|
-
expect(pm.mode).toBe("longBreak");
|
|
117
|
-
expect(pm.timeLeftSeconds).toBe(22 * 60);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("fin d’une pause : retour au travail avec durée par défaut", () => {
|
|
121
|
-
const t0 = 1_000_000_000;
|
|
122
|
-
const p = basePayload({
|
|
123
|
-
mode: "break",
|
|
124
|
-
status: "running",
|
|
125
|
-
timeLeftSeconds: 1,
|
|
126
|
-
sessionsCompleted: 1,
|
|
127
|
-
kronoFocusDeadlineAtMs: t0 + 1000,
|
|
128
|
-
});
|
|
129
|
-
hydrateKronoFocusInPayload(p, t0 + 1000);
|
|
130
|
-
const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
|
|
131
|
-
expect(pm.mode).toBe("work");
|
|
132
|
-
expect(pm.status).toBe("paused");
|
|
133
|
-
expect(pm.timeLeftSeconds).toBe(25 * 60);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("n’agit pas si pas de session courante ou KronoFocus absent", () => {
|
|
137
|
-
const p: KronosysUpdatePayload = { viewType: "dashboard", cfg: {} } as KronosysUpdatePayload;
|
|
138
|
-
expect(hydrateKronoFocusInPayload(p, 0)).toBe(false);
|
|
139
|
-
const p2 = basePayload({ mode: "work", status: "idle", timeLeftSeconds: 100 });
|
|
140
|
-
expect(hydrateKronoFocusInPayload(p2, 0)).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
|
|
4
|
-
import {
|
|
5
|
-
LEGACY_SESSION_TIMER_OBJECT_KEY,
|
|
6
|
-
LEGACY_TASK_CYCLES_KEY,
|
|
7
|
-
LEGACY_TIMER_DEADLINE_MS_KEY,
|
|
8
|
-
} from "@/lib/legacyKronoFocusStorageKeys";
|
|
9
|
-
|
|
10
|
-
import { migrateLegacyKronoFocusPayload } from "./kronoFocusMigrate";
|
|
11
|
-
|
|
12
|
-
describe("migrateLegacyKronoFocusPayload", () => {
|
|
13
|
-
it("fusionne l’objet minuteur hérité sur la session vers kronoFocus et renomme l’échéance", () => {
|
|
14
|
-
const legacyBlock: Record<string, unknown> = {
|
|
15
|
-
mode: "work",
|
|
16
|
-
status: "paused",
|
|
17
|
-
timeLeftSeconds: 120,
|
|
18
|
-
sessionsCompleted: 1,
|
|
19
|
-
};
|
|
20
|
-
legacyBlock[LEGACY_TIMER_DEADLINE_MS_KEY] = 999;
|
|
21
|
-
|
|
22
|
-
const p = {
|
|
23
|
-
viewType: "dashboard",
|
|
24
|
-
current: {
|
|
25
|
-
sessionId: "s1",
|
|
26
|
-
[LEGACY_SESSION_TIMER_OBJECT_KEY]: legacyBlock,
|
|
27
|
-
},
|
|
28
|
-
} as unknown as KronosysUpdatePayload;
|
|
29
|
-
|
|
30
|
-
expect(migrateLegacyKronoFocusPayload(p)).toBe(true);
|
|
31
|
-
const cur = p.current as Record<string, unknown>;
|
|
32
|
-
expect(cur[LEGACY_SESSION_TIMER_OBJECT_KEY]).toBeUndefined();
|
|
33
|
-
const kf = cur.kronoFocus as Record<string, unknown>;
|
|
34
|
-
expect(kf.status).toBe("paused");
|
|
35
|
-
expect(kf.kronoFocusDeadlineAtMs).toBe(999);
|
|
36
|
-
expect(kf[LEGACY_TIMER_DEADLINE_MS_KEY]).toBeUndefined();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("renomme les cycles hérités sur les tâches en kronoFocusCycles", () => {
|
|
40
|
-
const p = {
|
|
41
|
-
viewType: "dashboard",
|
|
42
|
-
current: {
|
|
43
|
-
sessionId: "s1",
|
|
44
|
-
tasks: [{ id: "t1", name: "a", [LEGACY_TASK_CYCLES_KEY]: 3 }],
|
|
45
|
-
},
|
|
46
|
-
} as unknown as KronosysUpdatePayload;
|
|
47
|
-
|
|
48
|
-
expect(migrateLegacyKronoFocusPayload(p)).toBe(true);
|
|
49
|
-
const tasks = (p.current as Record<string, unknown>).tasks as Record<string, unknown>[];
|
|
50
|
-
expect(tasks[0].kronoFocusCycles).toBe(3);
|
|
51
|
-
expect(tasks[0][LEGACY_TASK_CYCLES_KEY]).toBeUndefined();
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
|
|
3
|
-
import { MAIN_TIMER_SEGMENT_STARTED_AT } from "./actionTaskSession";
|
|
4
|
-
import { materializeRunningMainTimersInPayload } from "./mainTimerHydrate";
|
|
5
|
-
|
|
6
|
-
describe("materializeRunningMainTimersInPayload", () => {
|
|
7
|
-
it("avance durationMs pour une tâche au minuteur principal entre deux appels", () => {
|
|
8
|
-
const t0 = Date.UTC(2026, 0, 10, 12, 0, 0);
|
|
9
|
-
const task = {
|
|
10
|
-
id: "t1",
|
|
11
|
-
name: "Tâche",
|
|
12
|
-
durationMs: 1000,
|
|
13
|
-
isDone: false,
|
|
14
|
-
manualTaskTimerPaused: false,
|
|
15
|
-
subtasks: [],
|
|
16
|
-
[MAIN_TIMER_SEGMENT_STARTED_AT]: new Date(t0).toISOString(),
|
|
17
|
-
};
|
|
18
|
-
const p = {
|
|
19
|
-
viewType: "dashboard",
|
|
20
|
-
cfg: {},
|
|
21
|
-
current: {
|
|
22
|
-
sessionId: "sid",
|
|
23
|
-
tasks: [],
|
|
24
|
-
activeTasks: [task],
|
|
25
|
-
activeTask: task,
|
|
26
|
-
},
|
|
27
|
-
} as unknown as KronosysUpdatePayload;
|
|
28
|
-
|
|
29
|
-
const t1 = t0 + 5000;
|
|
30
|
-
const w1 = materializeRunningMainTimersInPayload(p, t1);
|
|
31
|
-
expect(w1).toBe(true);
|
|
32
|
-
expect(task.durationMs).toBe(6000);
|
|
33
|
-
expect(typeof task[MAIN_TIMER_SEGMENT_STARTED_AT]).toBe("string");
|
|
34
|
-
|
|
35
|
-
const t2 = t1 + 3000;
|
|
36
|
-
const w2 = materializeRunningMainTimersInPayload(p, t2);
|
|
37
|
-
expect(w2).toBe(true);
|
|
38
|
-
expect(task.durationMs).toBe(9000);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("ignore les tâches en pause manuelle", () => {
|
|
42
|
-
const task = {
|
|
43
|
-
id: "t1",
|
|
44
|
-
name: "Tâche",
|
|
45
|
-
durationMs: 1000,
|
|
46
|
-
isDone: false,
|
|
47
|
-
manualTaskTimerPaused: true,
|
|
48
|
-
subtasks: [],
|
|
49
|
-
};
|
|
50
|
-
const p = {
|
|
51
|
-
viewType: "dashboard",
|
|
52
|
-
cfg: {},
|
|
53
|
-
current: {
|
|
54
|
-
sessionId: "sid",
|
|
55
|
-
tasks: [],
|
|
56
|
-
activeTasks: [task],
|
|
57
|
-
activeTask: task,
|
|
58
|
-
},
|
|
59
|
-
} as unknown as KronosysUpdatePayload;
|
|
60
|
-
|
|
61
|
-
const w = materializeRunningMainTimersInPayload(p, Date.UTC(2026, 0, 10, 12, 0, 10));
|
|
62
|
-
expect(w).toBe(false);
|
|
63
|
-
expect(task.durationMs).toBe(1000);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
-
|
|
7
|
-
import { resetSqliteConnection } from "@/server/db";
|
|
8
|
-
import { readPayload, writePayload } from "@/server/payloadStore";
|
|
9
|
-
|
|
10
|
-
describe("payloadStore (intégration SQLite)", () => {
|
|
11
|
-
let tmpDir: string;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kronosys-payload-"));
|
|
15
|
-
process.env.TRACE_DATA_DIR = tmpDir;
|
|
16
|
-
resetSqliteConnection();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
resetSqliteConnection();
|
|
21
|
-
delete process.env.TRACE_DATA_DIR;
|
|
22
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("readPayload retourne un payload initial valide sur base vierge", () => {
|
|
26
|
-
const p = readPayload();
|
|
27
|
-
expect(p.viewType).toBe("dashboard");
|
|
28
|
-
expect(Array.isArray(p.history)).toBe(true);
|
|
29
|
-
expect(p.cfg).toBeDefined();
|
|
30
|
-
expect(p.dashboardDataOrigin).toBe("local_next");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("writePayload puis readPayload retourne les mêmes données", () => {
|
|
34
|
-
const initial = readPayload();
|
|
35
|
-
const modified = { ...initial, knownTags: ["dev", "design"] };
|
|
36
|
-
writePayload(modified);
|
|
37
|
-
const back = readPayload();
|
|
38
|
-
expect(back.knownTags).toEqual(["dev", "design"]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("writePayload est idempotent (INSERT OR REPLACE)", () => {
|
|
42
|
-
const p = readPayload();
|
|
43
|
-
writePayload(p);
|
|
44
|
-
writePayload(p);
|
|
45
|
-
const back = readPayload();
|
|
46
|
-
expect(back.viewType).toBe("dashboard");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("readPayload retourne un payload initial si la valeur stockée est du JSON invalide", async () => {
|
|
50
|
-
// Écrire manuellement du JSON corrompu dans la DB
|
|
51
|
-
const { getSqlite } = await import("@/server/db");
|
|
52
|
-
const db = getSqlite();
|
|
53
|
-
db.exec(`CREATE TABLE IF NOT EXISTS kv_store (k TEXT PRIMARY KEY NOT NULL, v TEXT NOT NULL);`);
|
|
54
|
-
db.prepare("INSERT OR REPLACE INTO kv_store (k, v) VALUES (?, ?)").run(
|
|
55
|
-
"dashboard_payload_v1",
|
|
56
|
-
"{ invalide json [[["
|
|
57
|
-
);
|
|
58
|
-
const recovered = readPayload();
|
|
59
|
-
expect(recovered.viewType).toBe("dashboard");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("readPayload fusionne les défauts cfg si une clé est manquante dans le payload stocké", () => {
|
|
63
|
-
const p = readPayload();
|
|
64
|
-
// Supprimer une clé du cfg et réécrire
|
|
65
|
-
const cfg = { ...p.cfg } as Record<string, unknown>;
|
|
66
|
-
delete cfg["taskDefaultTagBucketEnabled"];
|
|
67
|
-
writePayload({ ...p, cfg });
|
|
68
|
-
const back = readPayload();
|
|
69
|
-
// Le defaultCfg doit remettre la clé manquante
|
|
70
|
-
expect(back.cfg).toHaveProperty("taskDefaultTagBucketEnabled");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("knownProjects et knownTags sont des tableaux après lecture initiale", () => {
|
|
74
|
-
const p = readPayload();
|
|
75
|
-
expect(Array.isArray(p.knownProjects)).toBe(true);
|
|
76
|
-
expect(Array.isArray(p.knownTags)).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
|
|
3
|
-
import { materializeSessionWallClockInPayload, SESSION_WALL_SEGMENT_STARTED_AT } from "./sessionWallHydrate";
|
|
4
|
-
|
|
5
|
-
describe("materializeSessionWallClockInPayload", () => {
|
|
6
|
-
it("accumule la durée murale entre deux appels", () => {
|
|
7
|
-
const t0 = Date.UTC(2026, 2, 1, 8, 0, 0);
|
|
8
|
-
const cur = {
|
|
9
|
-
sessionId: "sid-1",
|
|
10
|
-
archived: false,
|
|
11
|
-
isPaused: false,
|
|
12
|
-
endAt: null,
|
|
13
|
-
sessionDurationMinutes: 0,
|
|
14
|
-
[SESSION_WALL_SEGMENT_STARTED_AT]: new Date(t0).toISOString(),
|
|
15
|
-
};
|
|
16
|
-
const p = {
|
|
17
|
-
viewType: "dashboard",
|
|
18
|
-
cfg: {},
|
|
19
|
-
current: cur,
|
|
20
|
-
} as unknown as KronosysUpdatePayload;
|
|
21
|
-
|
|
22
|
-
const t1 = t0 + 120_000;
|
|
23
|
-
const w1 = materializeSessionWallClockInPayload(p, t1);
|
|
24
|
-
expect(w1).toBe(true);
|
|
25
|
-
expect(cur.sessionDurationMinutes).toBeCloseTo(2, 5);
|
|
26
|
-
|
|
27
|
-
const t2 = t1 + 60_000;
|
|
28
|
-
const w2 = materializeSessionWallClockInPayload(p, t2);
|
|
29
|
-
expect(w2).toBe(true);
|
|
30
|
-
expect(cur.sessionDurationMinutes).toBeCloseTo(3, 5);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("ne matérialise pas si la session est en pause", () => {
|
|
34
|
-
const cur = {
|
|
35
|
-
sessionId: "sid",
|
|
36
|
-
archived: false,
|
|
37
|
-
isPaused: true,
|
|
38
|
-
endAt: null,
|
|
39
|
-
sessionDurationMinutes: 1,
|
|
40
|
-
[SESSION_WALL_SEGMENT_STARTED_AT]: new Date().toISOString(),
|
|
41
|
-
};
|
|
42
|
-
const p = { viewType: "dashboard", cfg: {}, current: cur } as unknown as KronosysUpdatePayload;
|
|
43
|
-
expect(materializeSessionWallClockInPayload(p, Date.UTC(2026, 2, 1, 9, 0, 0))).toBe(false);
|
|
44
|
-
expect(cur.sessionDurationMinutes).toBe(1);
|
|
45
|
-
});
|
|
46
|
-
});
|