@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,149 +0,0 @@
1
- import { unzipSync } from "fflate";
2
- import { describe, expect, it } from "vitest";
3
- import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
4
- import {
5
- buildCsvBackupZipSync,
6
- buildGitIdentityCsv,
7
- buildSessionsBackupCsv,
8
- buildStoreJsonBlobsCsv,
9
- buildSubtasksBackupCsv,
10
- buildTagDescriptionsCsv,
11
- buildTagProjectRegistryCsv,
12
- buildTasksBackupCsv,
13
- isCsvBackupTable,
14
- } from "./backupCsvExport";
15
-
16
- function samplePayload(): KronosysUpdatePayload {
17
- return {
18
- viewType: "dashboard",
19
- cfg: { autoStart: false, plannedSessions: [{ id: "p1" }] },
20
- history: [
21
- {
22
- sessionId: "s-hist",
23
- sessionName: "Hist",
24
- startAt: "2026-04-01T10:00:00.000Z",
25
- endAt: "2026-04-01T11:00:00.000Z",
26
- savedAt: "2026-04-01T11:05:00.000Z",
27
- sessionDurationMinutes: 60,
28
- tasks: [
29
- {
30
- id: "t1",
31
- name: "Do, thing",
32
- tags: ["a", "b"],
33
- project: "acme",
34
- isDone: true,
35
- durationMs: 3_600_000,
36
- subtasks: [{ id: "st1", title: "Step, one", done: true, durationMs: 1000 }],
37
- },
38
- ],
39
- },
40
- ],
41
- historyArchived: [
42
- {
43
- sessionId: "s-arch",
44
- sessionName: "Arch",
45
- savedAt: "2026-04-02T12:00:00.000Z",
46
- tasks: [],
47
- },
48
- ],
49
- current: {
50
- sessionId: "s-live",
51
- sessionName: "Live",
52
- savedAt: "2026-04-03T08:00:00.000Z",
53
- activeTasks: [{ id: "t2", name: "Running", isDone: false, durationMs: 0 }],
54
- tasks: [],
55
- },
56
- knownTags: ["a"],
57
- knownProjects: ["acme"],
58
- userKnownTags: ["pinned"],
59
- excludedSuggestionTags: ["hide-me"],
60
- tagDescriptions: { alpha: "Desc, with comma" },
61
- projectDescriptions: { acme: "Client" },
62
- gitIdentity: { gitUserName: "n", gitUserEmail: "e@x.ca", gitAccountLogin: "gh" },
63
- inspectingSessionId: "s-hist",
64
- dismissArchiveSessionConfirm: true,
65
- } as KronosysUpdatePayload;
66
- }
67
-
68
- describe("backupCsvExport", () => {
69
- it("isCsvBackupTable reconnaît les noms de tables", () => {
70
- expect(isCsvBackupTable("sessions")).toBe(true);
71
- expect(isCsvBackupTable("nope")).toBe(false);
72
- });
73
-
74
- it("buildSessionsBackupCsv inclut la source et le JSON de session", () => {
75
- const csv = buildSessionsBackupCsv(samplePayload());
76
- expect(csv).toContain("scheduledStartAt");
77
- expect(csv).toContain("sessionStartOffsetMinutes");
78
- expect(csv).toContain("sessionSource");
79
- expect(csv).toContain("s-hist");
80
- expect(csv).toContain("history");
81
- expect(csv).toContain("historyArchived");
82
- expect(csv).toContain("current");
83
- expect(csv).toContain("sessionJson");
84
- expect(csv).toContain("Hist");
85
- });
86
-
87
- it("buildTasksBackupCsv échappe les virgules dans le nom et inclut taskJson", () => {
88
- const csv = buildTasksBackupCsv(samplePayload());
89
- expect(csv).toContain("Do, thing");
90
- expect(csv).toContain("t1");
91
- expect(csv).toContain("taskJson");
92
- expect(csv).toContain('""id"":""t1""');
93
- });
94
-
95
- it("buildSubtasksBackupCsv liste les sous-tâches", () => {
96
- const csv = buildSubtasksBackupCsv(samplePayload());
97
- expect(csv).toContain("st1");
98
- expect(csv).toContain("Step, one");
99
- });
100
-
101
- it("buildTagProjectRegistryCsv couvre les quatre listes", () => {
102
- const csv = buildTagProjectRegistryCsv(samplePayload());
103
- expect(csv).toContain("known_tag,a");
104
- expect(csv).toContain("known_project,acme");
105
- expect(csv).toContain("user_known_tag,pinned");
106
- expect(csv).toContain("excluded_suggestion_tag,hide-me");
107
- });
108
-
109
- it("buildTagDescriptionsCsv échappe les descriptions", () => {
110
- const csv = buildTagDescriptionsCsv(samplePayload());
111
- expect(csv).toContain("alpha");
112
- expect(csv).toContain("Desc, with comma");
113
- });
114
-
115
- it("buildGitIdentityCsv exporte les trois champs", () => {
116
- const csv = buildGitIdentityCsv(samplePayload());
117
- expect(csv.split("\n")[1]).toContain("n");
118
- expect(csv.split("\n")[1]).toContain("e@x.ca");
119
- expect(csv.split("\n")[1]).toContain("gh");
120
- });
121
-
122
- it("buildCsvBackupZipSync regroupe les huit CSV", () => {
123
- const stamp = "TEST-STAMP";
124
- const zip = buildCsvBackupZipSync(samplePayload(), stamp);
125
- const entries = unzipSync(zip);
126
- const names = Object.keys(entries).sort();
127
- expect(names).toEqual([
128
- `kronosys-git_identity-${stamp}.csv`,
129
- `kronosys-project_descriptions-${stamp}.csv`,
130
- `kronosys-sessions-${stamp}.csv`,
131
- `kronosys-store_json_blobs-${stamp}.csv`,
132
- `kronosys-subtasks-${stamp}.csv`,
133
- `kronosys-tag_descriptions-${stamp}.csv`,
134
- `kronosys-tag_project_registry-${stamp}.csv`,
135
- `kronosys-tasks-${stamp}.csv`,
136
- ]);
137
- const sessionsUtf8 = new TextDecoder().decode(entries[`kronosys-sessions-${stamp}.csv`]);
138
- expect(sessionsUtf8).toContain("sessionId");
139
- expect(sessionsUtf8).toContain("s-hist");
140
- });
141
-
142
- it("buildStoreJsonBlobsCsv inclut cfg et métadonnées de tableau de bord", () => {
143
- const csv = buildStoreJsonBlobsCsv(samplePayload());
144
- expect(csv.startsWith("blobKind,json\n")).toBe(true);
145
- expect(csv).toContain("autoStart");
146
- expect(csv).toContain("inspectingSessionId");
147
- expect(csv).toContain("s-hist");
148
- });
149
- });
@@ -1,63 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- dataSearchItemMatches,
4
- parseDataSearchQuery,
5
- } from "@/lib/dashboardQuickSearchQuery";
6
- import type { DashboardSearchItem } from "@/lib/dashboardQuickSearch";
7
-
8
- function item(hay: string, dur?: number): DashboardSearchItem {
9
- return {
10
- id: "x",
11
- title: "t",
12
- haystack: hay,
13
- durationForSearchSec: dur,
14
- onSelect: () => {},
15
- };
16
- }
17
-
18
- describe("parseDataSearchQuery", () => {
19
- it("extrait mm:ss et opérateur (après le temps)", () => {
20
- const r = parseDataSearchQuery(" 44:33 > ");
21
- expect(r.textPart).toBe("");
22
- expect(r.durationPredicates).toEqual([{ op: "gt", seconds: 44 * 60 + 33 }]);
23
- });
24
-
25
- it("extrait et laisse du texte", () => {
26
- const r = parseDataSearchQuery("bug 12:55 <");
27
- expect(r.textPart).toBe("bug");
28
- expect(r.durationPredicates).toEqual([{ op: "lt", seconds: 12 * 60 + 55 }]);
29
- });
30
-
31
- it("heure pleine h:m:s", () => {
32
- const r = parseDataSearchQuery("1:0:0 >");
33
- expect(r.durationPredicates).toEqual([{ op: "gt", seconds: 3600 }]);
34
- });
35
-
36
- it("opérateur avant le temps", () => {
37
- const r = parseDataSearchQuery("> 0:10");
38
- expect(r.textPart).toBe("");
39
- expect(r.durationPredicates).toEqual([{ op: "gt", seconds: 10 }]);
40
- });
41
- });
42
-
43
- describe("dataSearchItemMatches", () => {
44
- it("filtre par durée seule", () => {
45
- const under = item("x", 40);
46
- const over = item("x", 90);
47
- expect(dataSearchItemMatches(under, "1:0 <")).toBe(true);
48
- expect(dataSearchItemMatches(over, "1:0 <")).toBe(false);
49
- });
50
-
51
- it("sans durée enregistrée : échec si prédicat temporel", () => {
52
- expect(dataSearchItemMatches(item("hello", undefined), "0:1 >")).toBe(false);
53
- });
54
-
55
- it("texte + durée", () => {
56
- const good = item("alpha #tag", 4000);
57
- const badText = item("beta", 4000);
58
- const badDur = item("alpha", 10);
59
- expect(dataSearchItemMatches(good, "alpha 1:0:0 >")).toBe(true);
60
- expect(dataSearchItemMatches(badText, "alpha 1:0:0 >")).toBe(false);
61
- expect(dataSearchItemMatches(badDur, "alpha 1:0:0 >")).toBe(false);
62
- });
63
- });
@@ -1,87 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const readUseProd = vi.hoisted(() => vi.fn(() => false));
4
-
5
- vi.mock("@/lib/devDataPreferenceFile", async (importOriginal) => {
6
- const mod = await importOriginal<typeof import("@/lib/devDataPreferenceFile")>();
7
- return {
8
- ...mod,
9
- readUseProductionDataInDevelopmentFromFile: () => readUseProd(),
10
- };
11
- });
12
-
13
- import {
14
- computeDataDirectoryForCurrentProcessEnv,
15
- resetDataDirectoryCache,
16
- resolveProductionDefaultDataDirectory,
17
- } from "./dataDir";
18
- import { resetSqliteConnection } from "@/server/db";
19
-
20
- describe("dataDir (dev / prod isolation)", () => {
21
- const origNodeEnv = process.env.NODE_ENV;
22
- const origTrace = process.env.TRACE_DATA_DIR;
23
- const origKrono = process.env.KRONOSYS_DEV_USE_PROD_DATA;
24
- const env = process.env as Record<string, string | undefined>;
25
-
26
- beforeEach(() => {
27
- readUseProd.mockReturnValue(false);
28
- delete process.env.TRACE_DATA_DIR;
29
- delete process.env.KRONOSYS_DEV_USE_PROD_DATA;
30
- resetDataDirectoryCache();
31
- resetSqliteConnection();
32
- });
33
-
34
- afterEach(() => {
35
- if (origNodeEnv === undefined) {
36
- delete env.NODE_ENV;
37
- } else {
38
- env.NODE_ENV = origNodeEnv;
39
- }
40
- if (origTrace === undefined) {
41
- delete process.env.TRACE_DATA_DIR;
42
- } else {
43
- process.env.TRACE_DATA_DIR = origTrace;
44
- }
45
- if (origKrono === undefined) {
46
- delete process.env.KRONOSYS_DEV_USE_PROD_DATA;
47
- } else {
48
- process.env.KRONOSYS_DEV_USE_PROD_DATA = origKrono;
49
- }
50
- readUseProd.mockReset();
51
- readUseProd.mockReturnValue(false);
52
- resetDataDirectoryCache();
53
- resetSqliteConnection();
54
- });
55
-
56
- it("uses TRACE_DATA_DIR when set", () => {
57
- env.NODE_ENV = "development";
58
- process.env.TRACE_DATA_DIR = "/tmp/kronosys-trace-xyz";
59
- expect(computeDataDirectoryForCurrentProcessEnv()).toBe("/tmp/kronosys-trace-xyz");
60
- });
61
-
62
- it("uses v4 for non-development NODE_ENV", () => {
63
- env.NODE_ENV = "production";
64
- const prod = resolveProductionDefaultDataDirectory();
65
- expect(computeDataDirectoryForCurrentProcessEnv()).toBe(prod);
66
- });
67
-
68
- it("uses v4-dev in development by default (isolated file preference off)", () => {
69
- env.NODE_ENV = "development";
70
- const prod = resolveProductionDefaultDataDirectory();
71
- const got = computeDataDirectoryForCurrentProcessEnv();
72
- expect(got).not.toBe(prod);
73
- expect(got).toMatch(/[\\/]v4-dev$/);
74
- });
75
-
76
- it("uses production v4 in development when KRONOSYS_DEV_USE_PROD_DATA is set", () => {
77
- env.NODE_ENV = "development";
78
- process.env.KRONOSYS_DEV_USE_PROD_DATA = "1";
79
- expect(computeDataDirectoryForCurrentProcessEnv()).toBe(resolveProductionDefaultDataDirectory());
80
- });
81
-
82
- it("uses production v4 in development when file preference is on", () => {
83
- env.NODE_ENV = "development";
84
- readUseProd.mockReturnValue(true);
85
- expect(computeDataDirectoryForCurrentProcessEnv()).toBe(resolveProductionDefaultDataDirectory());
86
- });
87
- });
@@ -1,46 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { formatIsoInstantShort } from "./formatIsoShort";
4
-
5
- describe("formatIsoInstantShort", () => {
6
- it("retourne null pour une chaîne vide", () => {
7
- expect(formatIsoInstantShort("", "fr")).toBeNull();
8
- expect(formatIsoInstantShort(" ", "fr")).toBeNull();
9
- });
10
-
11
- it("retourne null pour une date invalide", () => {
12
- expect(formatIsoInstantShort("pas-une-date", "fr")).toBeNull();
13
- expect(formatIsoInstantShort("9999-99-99", "fr")).toBeNull();
14
- });
15
-
16
- it("retourne une chaîne non vide pour une date ISO valide en fr", () => {
17
- const result = formatIsoInstantShort("2026-04-29T12:00:00Z", "fr");
18
- expect(result).not.toBeNull();
19
- expect(typeof result).toBe("string");
20
- expect((result as string).length).toBeGreaterThan(0);
21
- });
22
-
23
- it("retourne une chaîne non vide pour une date ISO valide en en", () => {
24
- const result = formatIsoInstantShort("2026-04-29T12:00:00Z", "en");
25
- expect(result).not.toBeNull();
26
- expect(typeof result).toBe("string");
27
- });
28
-
29
- it("fr utilise le locale fr-CA (pas en-CA)", () => {
30
- // fr-CA formate typiquement aa-mm-jj hh:mm alors qu'en-CA est différent
31
- const fr = formatIsoInstantShort("2026-01-15T00:00:00Z", "fr");
32
- const en = formatIsoInstantShort("2026-01-15T00:00:00Z", "en");
33
- expect(fr).not.toEqual(en);
34
- });
35
-
36
- it("supporte les dates sans timezone explicite (heure locale)", () => {
37
- const result = formatIsoInstantShort("2026-06-01T08:30:00", "fr");
38
- expect(result).not.toBeNull();
39
- });
40
-
41
- it("supporte les timestamps millis (new Date via ISO)", () => {
42
- const iso = new Date(2026, 3, 1, 9, 0, 0).toISOString();
43
- const result = formatIsoInstantShort(iso, "en");
44
- expect(result).not.toBeNull();
45
- });
46
- });
@@ -1,130 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import {
4
- KRONO_FOCUS_MAX_BREAK_SEC,
5
- KRONO_FOCUS_MAX_WORK_SEC,
6
- KRONO_FOCUS_MIN_SEGMENT_SEC,
7
- KRONO_FOCUS_RHYTHM_PRESETS,
8
- clampBreakDurationSeconds,
9
- clampWorkDurationSeconds,
10
- readLongBreakDurationSeconds,
11
- readShortBreakDurationSeconds,
12
- readWorkDurationSeconds,
13
- } from "./kronoFocusRhythm";
14
-
15
- describe("constantes", () => {
16
- it("KRONO_FOCUS_MIN_SEGMENT_SEC = 60s", () => {
17
- expect(KRONO_FOCUS_MIN_SEGMENT_SEC).toBe(60);
18
- });
19
-
20
- it("KRONO_FOCUS_MAX_WORK_SEC = 8h", () => {
21
- expect(KRONO_FOCUS_MAX_WORK_SEC).toBe(8 * 3600);
22
- });
23
-
24
- it("KRONO_FOCUS_MAX_BREAK_SEC = 8h", () => {
25
- expect(KRONO_FOCUS_MAX_BREAK_SEC).toBe(8 * 3600);
26
- });
27
- });
28
-
29
- describe("clampWorkDurationSeconds", () => {
30
- it("valeur dans la plage → retournée telle quelle (floor)", () => {
31
- expect(clampWorkDurationSeconds(25 * 60)).toBe(25 * 60);
32
- });
33
-
34
- it("valeur inférieure au minimum → ramenée à 60s", () => {
35
- expect(clampWorkDurationSeconds(0)).toBe(60);
36
- expect(clampWorkDurationSeconds(30)).toBe(60);
37
- });
38
-
39
- it("valeur supérieure au maximum → ramenée à 8h", () => {
40
- expect(clampWorkDurationSeconds(9 * 3600)).toBe(8 * 3600);
41
- });
42
-
43
- it("applique Math.floor sur les décimales", () => {
44
- expect(clampWorkDurationSeconds(90.9)).toBe(90);
45
- });
46
- });
47
-
48
- describe("clampBreakDurationSeconds", () => {
49
- it("valeur dans la plage → retournée telle quelle", () => {
50
- expect(clampBreakDurationSeconds(5 * 60)).toBe(5 * 60);
51
- });
52
-
53
- it("valeur inférieure au minimum → 60s", () => {
54
- expect(clampBreakDurationSeconds(10)).toBe(60);
55
- });
56
-
57
- it("valeur supérieure au maximum → 8h", () => {
58
- expect(clampBreakDurationSeconds(9 * 3600)).toBe(8 * 3600);
59
- });
60
- });
61
-
62
- describe("readWorkDurationSeconds", () => {
63
- it("retourne le défaut 25min si pm est undefined", () => {
64
- expect(readWorkDurationSeconds(undefined)).toBe(25 * 60);
65
- });
66
-
67
- it("retourne la valeur du payload si valide", () => {
68
- expect(readWorkDurationSeconds({ workDurationSeconds: 50 * 60 })).toBe(50 * 60);
69
- });
70
-
71
- it("retourne le défaut si la valeur est inférieure au minimum", () => {
72
- expect(readWorkDurationSeconds({ workDurationSeconds: 30 })).toBe(25 * 60);
73
- });
74
-
75
- it("retourne le défaut si la valeur est non numérique", () => {
76
- expect(readWorkDurationSeconds({ workDurationSeconds: "abc" })).toBe(25 * 60);
77
- });
78
-
79
- it("retourne le défaut si la valeur est Infinity", () => {
80
- expect(readWorkDurationSeconds({ workDurationSeconds: Infinity })).toBe(25 * 60);
81
- });
82
-
83
- it("clamp la valeur si elle dépasse le maximum", () => {
84
- expect(readWorkDurationSeconds({ workDurationSeconds: 9 * 3600 })).toBe(8 * 3600);
85
- });
86
- });
87
-
88
- describe("readShortBreakDurationSeconds", () => {
89
- it("retourne le défaut 5min si pm est undefined", () => {
90
- expect(readShortBreakDurationSeconds(undefined)).toBe(5 * 60);
91
- });
92
-
93
- it("retourne la valeur du payload si valide", () => {
94
- expect(readShortBreakDurationSeconds({ shortBreakDurationSeconds: 10 * 60 })).toBe(10 * 60);
95
- });
96
-
97
- it("retourne le défaut si la valeur est trop petite", () => {
98
- expect(readShortBreakDurationSeconds({ shortBreakDurationSeconds: 10 })).toBe(5 * 60);
99
- });
100
- });
101
-
102
- describe("readLongBreakDurationSeconds", () => {
103
- it("retourne le défaut 15min si pm est undefined", () => {
104
- expect(readLongBreakDurationSeconds(undefined)).toBe(15 * 60);
105
- });
106
-
107
- it("retourne la valeur du payload si valide", () => {
108
- expect(readLongBreakDurationSeconds({ longBreakDurationSeconds: 20 * 60 })).toBe(20 * 60);
109
- });
110
- });
111
-
112
- describe("KRONO_FOCUS_RHYTHM_PRESETS", () => {
113
- it("contient 4 presets", () => {
114
- expect(KRONO_FOCUS_RHYTHM_PRESETS).toHaveLength(4);
115
- });
116
-
117
- it("le premier preset est 25-5-15", () => {
118
- const p = KRONO_FOCUS_RHYTHM_PRESETS[0];
119
- expect(p.id).toBe("25-5-15");
120
- expect(p.workSeconds).toBe(25 * 60);
121
- expect(p.shortBreakSeconds).toBe(5 * 60);
122
- expect(p.longBreakSeconds).toBe(15 * 60);
123
- });
124
-
125
- it("tous les presets ont workSeconds >= 60", () => {
126
- for (const p of KRONO_FOCUS_RHYTHM_PRESETS) {
127
- expect(p.workSeconds).toBeGreaterThanOrEqual(60);
128
- }
129
- });
130
- });
@@ -1,74 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { getKronoFocusTimerUrgency } from "./kronoFocusTimerUrgency";
4
-
5
- describe("getKronoFocusTimerUrgency", () => {
6
- it("retourne false/false si status est idle", () => {
7
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 0, mode: "work", status: "idle" }))
8
- .toEqual({ blink: false, urgentHighlight: false });
9
- });
10
-
11
- it("retourne false/false si status est idle (longBreak)", () => {
12
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 10, mode: "longBreak", status: "idle" }))
13
- .toEqual({ blink: false, urgentHighlight: false });
14
- });
15
-
16
- // --- mode work ---
17
- it("work running — plus de 5min → pas d'urgence", () => {
18
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 301, mode: "work", status: "running" }))
19
- .toEqual({ blink: false, urgentHighlight: false });
20
- });
21
-
22
- it("work running — exactement 5min → blink=true, urgentHighlight=false", () => {
23
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 300, mode: "work", status: "running" }))
24
- .toEqual({ blink: true, urgentHighlight: false });
25
- });
26
-
27
- it("work running — moins de 5min → blink=true, urgentHighlight=false", () => {
28
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 120, mode: "work", status: "running" }))
29
- .toEqual({ blink: true, urgentHighlight: false });
30
- });
31
-
32
- it("work running — exactement 30s → blink=true, urgentHighlight=true", () => {
33
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 30, mode: "work", status: "running" }))
34
- .toEqual({ blink: true, urgentHighlight: true });
35
- });
36
-
37
- it("work running — 0s → blink=true, urgentHighlight=true", () => {
38
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 0, mode: "work", status: "running" }))
39
- .toEqual({ blink: true, urgentHighlight: true });
40
- });
41
-
42
- // --- mode break (pause courte — pas de règle 5min) ---
43
- it("break running — 300s → blink=false (règle 5min ne s'applique pas)", () => {
44
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 300, mode: "break", status: "running" }))
45
- .toEqual({ blink: false, urgentHighlight: false });
46
- });
47
-
48
- it("break running — 30s → blink=true, urgentHighlight=true", () => {
49
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 30, mode: "break", status: "running" }))
50
- .toEqual({ blink: true, urgentHighlight: true });
51
- });
52
-
53
- it("break running — 31s → blink=false, urgentHighlight=false", () => {
54
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 31, mode: "break", status: "running" }))
55
- .toEqual({ blink: false, urgentHighlight: false });
56
- });
57
-
58
- // --- mode longBreak (règle 5min s'applique) ---
59
- it("longBreak running — 300s → blink=true, urgentHighlight=false", () => {
60
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 300, mode: "longBreak", status: "running" }))
61
- .toEqual({ blink: true, urgentHighlight: false });
62
- });
63
-
64
- it("longBreak running — 301s → blink=false", () => {
65
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 301, mode: "longBreak", status: "running" }))
66
- .toEqual({ blink: false, urgentHighlight: false });
67
- });
68
-
69
- // --- status paused (actif) ---
70
- it("work paused — 300s → mêmes règles que running", () => {
71
- expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 300, mode: "work", status: "paused" }))
72
- .toEqual({ blink: true, urgentHighlight: false });
73
- });
74
- });
@@ -1,29 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import {
4
- LEGACY_ACTION_PAUSE,
5
- LEGACY_ACTION_RESET,
6
- LEGACY_ACTION_SET_WORK_DURATION,
7
- LEGACY_ACTION_START,
8
- LEGACY_SESSION_TIMER_OBJECT_KEY,
9
- LEGACY_TASK_CYCLES_KEY,
10
- LEGACY_TASK_USED_FLAG_KEY,
11
- LEGACY_TIMER_DEADLINE_MS_KEY,
12
- } from "./legacyKronoFocusStorageKeys";
13
-
14
- describe("legacyKronoFocusStorageKeys", () => {
15
- it("reconstruit les mêmes identifiants que les anciennes versions", () => {
16
- expect(LEGACY_SESSION_TIMER_OBJECT_KEY).toBe(_(["pom", "odoro"]));
17
- expect(LEGACY_TIMER_DEADLINE_MS_KEY).toBe(_(["pom", "odoro", "DeadlineAtMs"]));
18
- expect(LEGACY_TASK_CYCLES_KEY).toBe(_(["pom", "odoro", "Cycles"]));
19
- expect(LEGACY_TASK_USED_FLAG_KEY).toBe(_(["used", "Pom", "odoro"]));
20
- expect(LEGACY_ACTION_START).toBe(_(["start", "Pom", "odoro"]));
21
- expect(LEGACY_ACTION_PAUSE).toBe(_(["pause", "Pom", "odoro"]));
22
- expect(LEGACY_ACTION_RESET).toBe(_(["reset", "Pom", "odoro"]));
23
- expect(LEGACY_ACTION_SET_WORK_DURATION).toBe(_(["set", "Pom", "odoro", "WorkDuration"]));
24
- });
25
- });
26
-
27
- function _(parts: string[]): string {
28
- return parts.join("");
29
- }