@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
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Réserve la même largeur que {@link DashboardCommandCenter} lorsque la palette n’est pas
5
+ * disponible sur une route (navigation stable entre pages).
6
+ */
7
+ export function AppShellCommandCenterPlaceholder() {
8
+ return (
9
+ <div
10
+ className="invisible pointer-events-none flex shrink-0 items-center gap-1.5"
11
+ aria-hidden
12
+ >
13
+ <div className="inline-flex h-10 w-[9.5rem] max-w-[min(42vw,11rem)] shrink-0 items-center rounded-lg px-2 sm:w-44 sm:max-w-none sm:px-2.5" />
14
+ <div className="inline-flex size-10 shrink-0 items-center justify-center rounded-lg" />
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,210 @@
1
+ "use client";
2
+
3
+ import { Clock, FolderOpen, Globe, User } from "lucide-react";
4
+ import { useMemo } from "react";
5
+ import type {
6
+ KronosysUpdatePayload,
7
+ WorkspaceCodeSnapshotPayload,
8
+ } from "@/lib/kronosysApi";
9
+ import type { DashboardStrings } from "@/lib/dashboardCopy";
10
+ import { readDashboardUse24HourClockFromCfg } from "@/lib/dashboardClockFormat";
11
+ import { readDashboardTimeZoneFromCfg } from "@/lib/dashboardTimeZone";
12
+ import { workspaceFolderPathStrings } from "@/lib/legacyEditorPayloadKeys";
13
+ import { showWorkspaceFoldersEmptyMessage } from "@/lib/usageProfile";
14
+ import { HeaderIntegrationBadges } from "@/components/dashboard/HeaderIntegrationBadges";
15
+
16
+ export function AppShellHeaderSessionMeta({
17
+ payload,
18
+ dt,
19
+ domId,
20
+ }: Readonly<{
21
+ payload: KronosysUpdatePayload | null | undefined;
22
+ dt: DashboardStrings;
23
+ /** Ancre optionnelle (ex. visite guidée du tableau de bord). */
24
+ domId?: string;
25
+ }>) {
26
+ const cfg = payload?.cfg as Record<string, unknown> | undefined;
27
+
28
+ const dashboardDisplayTimeZone = useMemo(
29
+ () => readDashboardTimeZoneFromCfg(cfg),
30
+ [cfg],
31
+ );
32
+ const dashboardUse24HourClock = useMemo(
33
+ () => readDashboardUse24HourClockFromCfg(cfg),
34
+ [cfg],
35
+ );
36
+
37
+ const resolvedWorkspaceRoots = useMemo(() => {
38
+ const top = workspaceFolderPathStrings(payload);
39
+ if (top.length > 0) {
40
+ return top;
41
+ }
42
+ const fromCfg = workspaceFolderPathStrings(cfg);
43
+ if (fromCfg.length > 0) {
44
+ return fromCfg;
45
+ }
46
+ const snap = payload?.workspaceCodeSnapshot as
47
+ | WorkspaceCodeSnapshotPayload
48
+ | undefined;
49
+ if (snap?.ok === true) {
50
+ const w = snap.workspaceFolder?.trim();
51
+ if (w) {
52
+ return [w];
53
+ }
54
+ }
55
+ return [];
56
+ }, [payload, cfg]);
57
+
58
+ const gitIdentity = payload?.gitIdentity as
59
+ | {
60
+ gitUserName?: unknown;
61
+ gitUserEmail?: unknown;
62
+ gitAccountLogin?: unknown;
63
+ }
64
+ | undefined;
65
+ const gitUserName =
66
+ typeof gitIdentity?.gitUserName === "string"
67
+ ? gitIdentity.gitUserName.trim()
68
+ : "";
69
+ const gitUserEmail =
70
+ typeof gitIdentity?.gitUserEmail === "string"
71
+ ? gitIdentity.gitUserEmail.trim()
72
+ : "";
73
+ const gitAccountLogin =
74
+ typeof gitIdentity?.gitAccountLogin === "string"
75
+ ? gitIdentity.gitAccountLogin.trim()
76
+ : "";
77
+
78
+ const gitContextLine = [
79
+ gitUserName || null,
80
+ gitUserEmail || null,
81
+ gitAccountLogin || null,
82
+ ]
83
+ .filter(Boolean)
84
+ .join(" · ");
85
+
86
+ const showWorkspaceFoldersEmpty = showWorkspaceFoldersEmptyMessage(
87
+ payload,
88
+ resolvedWorkspaceRoots.length,
89
+ );
90
+
91
+ const mongoEnabled = cfg?.mongodbEnabled === true;
92
+ const mongoRemoteStatus = payload?.remoteStatus as
93
+ | "connected"
94
+ | "failed"
95
+ | "pending"
96
+ | undefined;
97
+ const mongoConnected = mongoEnabled && mongoRemoteStatus === "connected";
98
+ const localPersistenceDriver =
99
+ typeof cfg?.localPersistenceDriver === "string" &&
100
+ cfg.localPersistenceDriver.trim().toLowerCase() === "json"
101
+ ? "json"
102
+ : "sqlite";
103
+
104
+ const showHeaderUserRow =
105
+ Boolean(payload) &&
106
+ Boolean(
107
+ gitContextLine ||
108
+ showWorkspaceFoldersEmpty ||
109
+ resolvedWorkspaceRoots.length > 0 ||
110
+ cfg,
111
+ );
112
+
113
+ const headerClockShort = dashboardUse24HourClock
114
+ ? dt.headerClockFormat24Short
115
+ : dt.headerClockFormat12Short;
116
+ const headerDisplayPrefsTitle = dt.headerDisplayRegionTitle
117
+ .replace("{timeZone}", dashboardDisplayTimeZone)
118
+ .replace("{clock}", headerClockShort);
119
+
120
+ if (!showHeaderUserRow) {
121
+ return null;
122
+ }
123
+
124
+ return (
125
+ <div
126
+ id={domId}
127
+ className="min-w-0 shrink-0 sm:max-w-[min(100%,36rem)] sm:justify-self-end"
128
+ >
129
+ <div className="flex min-w-0 flex-col items-stretch gap-1.5 text-xs text-zinc-600 sm:items-end sm:text-right dark:text-zinc-400">
130
+ {gitContextLine ? (
131
+ <p
132
+ className="flex max-w-full items-center gap-x-2 sm:justify-end"
133
+ title={gitContextLine}
134
+ >
135
+ <User
136
+ className="shrink-0 text-zinc-500 dark:text-zinc-500"
137
+ size={14}
138
+ aria-hidden
139
+ />
140
+ <span className="min-w-0 max-w-[min(100%,48rem)] truncate font-medium text-zinc-800 dark:text-zinc-300">
141
+ {gitContextLine}
142
+ </span>
143
+ </p>
144
+ ) : null}
145
+ <p
146
+ className="flex max-w-full flex-wrap items-center gap-x-1.5 sm:justify-end"
147
+ title={headerDisplayPrefsTitle}
148
+ >
149
+ <Globe
150
+ className="shrink-0 text-zinc-500 dark:text-zinc-500"
151
+ size={14}
152
+ aria-hidden
153
+ />
154
+ <span className="min-w-0 max-w-[min(100%,48rem)] break-all font-mono text-[0.7rem] font-medium text-zinc-800 dark:text-zinc-300">
155
+ {dashboardDisplayTimeZone}
156
+ </span>
157
+ <span className="text-zinc-400/70 dark:text-zinc-600" aria-hidden>
158
+ ·
159
+ </span>
160
+ <Clock
161
+ className="shrink-0 text-zinc-500 dark:text-zinc-500"
162
+ size={14}
163
+ aria-hidden
164
+ />
165
+ <span className="shrink-0 font-medium text-zinc-800 dark:text-zinc-300">
166
+ {headerClockShort}
167
+ </span>
168
+ </p>
169
+ {showWorkspaceFoldersEmpty ? (
170
+ <p className="flex max-w-full items-start gap-x-2 sm:justify-end">
171
+ <FolderOpen
172
+ className="mt-0.5 shrink-0 text-zinc-500 dark:text-zinc-500"
173
+ size={14}
174
+ aria-hidden
175
+ />
176
+ <span className="min-w-0 max-w-[min(100%,48rem)] break-words font-medium text-zinc-800 sm:text-right dark:text-zinc-300">
177
+ {dt.workspaceFoldersEmpty ?? "—"}
178
+ </span>
179
+ </p>
180
+ ) : resolvedWorkspaceRoots.length > 0 ? (
181
+ resolvedWorkspaceRoots.map((p) => (
182
+ <p
183
+ key={p}
184
+ className="flex max-w-full items-start gap-x-2 sm:justify-end"
185
+ >
186
+ <FolderOpen
187
+ className="mt-0.5 shrink-0 text-zinc-500 dark:text-zinc-500"
188
+ size={14}
189
+ aria-hidden
190
+ />
191
+ <span className="min-w-0 max-w-[min(100%,48rem)] break-all font-medium text-zinc-800 sm:text-right dark:text-zinc-300">
192
+ {p}
193
+ </span>
194
+ </p>
195
+ ))
196
+ ) : null}
197
+ {cfg ? (
198
+ <div className="flex w-full min-w-0 sm:justify-end">
199
+ <HeaderIntegrationBadges
200
+ t={dt}
201
+ localPersistenceDriver={localPersistenceDriver}
202
+ mongoConnected={mongoConnected}
203
+ mongoEnabled={mongoEnabled}
204
+ />
205
+ </div>
206
+ ) : null}
207
+ </div>
208
+ </div>
209
+ );
210
+ }
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import { Clock } from "lucide-react";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
6
+ import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
7
+ import { formatAppShellWallClock } from "@/lib/formatAppShellWallClock";
8
+
9
+ export function AppShellHeaderWallClock({
10
+ lang,
11
+ dt,
12
+ }: Readonly<{
13
+ lang: Lang;
14
+ dt: DashboardStrings;
15
+ }>) {
16
+ const { payload } = useKronosysPayload();
17
+ const cfg = payload?.cfg as Record<string, unknown> | undefined;
18
+ const [now, setNow] = useState<Date | null>(null);
19
+
20
+ useEffect(() => {
21
+ setNow(new Date());
22
+ const id = globalThis.setInterval(() => setNow(new Date()), 1000);
23
+ return () => globalThis.clearInterval(id);
24
+ }, []);
25
+
26
+ const { display, ariaDatetime, timeZone } = useMemo(
27
+ () => formatAppShellWallClock(now ?? new Date(0), cfg, lang),
28
+ [now, cfg, lang],
29
+ );
30
+
31
+ const aria = dt.headerWallClockAriaLabel
32
+ .replace("{timeZone}", timeZone)
33
+ .replace("{datetime}", ariaDatetime);
34
+
35
+ return (
36
+ <time
37
+ suppressHydrationWarning
38
+ dateTime={now ? now.toISOString() : ""}
39
+ className="inline-flex h-10 shrink-0 items-center gap-1.5 rounded-lg border border-zinc-300 bg-white px-2.5 font-mono text-sm tabular-nums text-zinc-800 shadow-sm dark:border-zinc-600 dark:bg-zinc-800/90 dark:text-zinc-100"
40
+ title={aria}
41
+ aria-label={aria}
42
+ >
43
+ <Clock
44
+ size={16}
45
+ strokeWidth={2}
46
+ className="shrink-0 text-zinc-500 dark:text-zinc-400"
47
+ aria-hidden
48
+ />
49
+ <span className="whitespace-nowrap" suppressHydrationWarning>
50
+ {now ? display : "--:--:--"}
51
+ </span>
52
+ </time>
53
+ );
54
+ }
@@ -3,22 +3,44 @@
3
3
  import Link from "next/link";
4
4
  import { useLayoutEffect, useMemo, useState } from "react";
5
5
  import { usePathname } from "next/navigation";
6
- import { Check, ChevronLeft, ChevronRight, Circle, LayoutDashboard, Timer } from "lucide-react";
6
+ import {
7
+ Check,
8
+ ChevronLeft,
9
+ ChevronRight,
10
+ Circle,
11
+ LayoutDashboard,
12
+ Timer,
13
+ } from "lucide-react";
7
14
  import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
8
15
  import { useSmoothStopwatchDisplayMs } from "@/components/dashboard/useSmoothStopwatchMs";
9
16
  import { useKronoFocusLiveSeconds } from "@/components/dashboard/useKronoFocusLiveSeconds";
10
- import { dashboardStrings, type DashboardStrings, type Lang } from "@/lib/dashboardCopy";
11
- import { formatStopwatchMs, formatWallDurationMs, taskTitleForDisplay } from "@/lib/taskParsing";
17
+ import {
18
+ dashboardStrings,
19
+ type DashboardStrings,
20
+ type Lang,
21
+ } from "@/lib/dashboardCopy";
22
+ import {
23
+ formatStopwatchMs,
24
+ formatWallDurationMs,
25
+ taskTitleForDisplay,
26
+ } from "@/lib/taskParsing";
12
27
  import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
28
+ import { isTaskDisplayPlanned } from "@/lib/temporalDisplayPlanned";
13
29
 
14
30
  type LiveTask = {
15
31
  id?: string;
16
32
  name?: string;
17
33
  isDone?: boolean;
18
34
  manualTaskTimerPaused?: boolean;
35
+ startTime?: string;
19
36
  activeSubtaskTimerId?: string;
20
37
  durationMs?: number;
21
- subtasks?: Array<{ id?: string; title?: string; done?: boolean; durationMs?: number }>;
38
+ subtasks?: Array<{
39
+ id?: string;
40
+ title?: string;
41
+ done?: boolean;
42
+ durationMs?: number;
43
+ }>;
22
44
  };
23
45
 
24
46
  type LiveShape = {
@@ -41,7 +63,10 @@ type LiveShape = {
41
63
  };
42
64
  };
43
65
 
44
- function kfPhaseLabel(dt: DashboardStrings, mode: "work" | "break" | "longBreak" | undefined): string {
66
+ function kfPhaseLabel(
67
+ dt: DashboardStrings,
68
+ mode: "work" | "break" | "longBreak" | undefined,
69
+ ): string {
45
70
  if (mode === "break") {
46
71
  return dt.breakMode;
47
72
  }
@@ -57,7 +82,9 @@ function kfCountdownHMS(totalSec: number): string {
57
82
  const h = Math.floor(s / 3600);
58
83
  const m = Math.floor((s % 3600) / 60);
59
84
  const sec = s % 60;
60
- return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
85
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(
86
+ sec,
87
+ ).padStart(2, "0")}`;
61
88
  }
62
89
 
63
90
  function truncateDrawerLabel(s: string, max: number): string {
@@ -76,9 +103,15 @@ function runningTasksFromLive(live: LiveShape | undefined): LiveTask[] {
76
103
  Array.isArray(live.activeTasks) && live.activeTasks.length > 0
77
104
  ? live.activeTasks
78
105
  : live.activeTask
79
- ? [live.activeTask]
80
- : [];
81
- return raw.filter((t) => t && !t.isDone && !t.manualTaskTimerPaused);
106
+ ? [live.activeTask]
107
+ : [];
108
+ return raw.filter(
109
+ (t) =>
110
+ t &&
111
+ !t.isDone &&
112
+ !t.manualTaskTimerPaused &&
113
+ !isTaskDisplayPlanned(t, Date.now()),
114
+ );
82
115
  }
83
116
 
84
117
  function subtaskTitleFor(task: LiveTask, subId: string): string | undefined {
@@ -108,22 +141,38 @@ function DrawerSubtaskRow({
108
141
  <li className="flex min-w-0 items-baseline justify-between gap-1.5 py-0.5">
109
142
  <span className="flex min-w-0 items-center gap-1">
110
143
  {done ? (
111
- <Check className="size-3 shrink-0 text-emerald-600 dark:text-emerald-400" strokeWidth={2.5} aria-hidden />
144
+ <Check
145
+ className="size-3 shrink-0 text-emerald-600 dark:text-emerald-400"
146
+ strokeWidth={2.5}
147
+ aria-hidden
148
+ />
112
149
  ) : (
113
150
  <Circle
114
- className={`size-2.5 shrink-0 ${isTracking ? "text-emerald-600 dark:text-emerald-400" : "text-zinc-400 dark:text-zinc-500"}`}
151
+ className={`size-2.5 shrink-0 ${
152
+ isTracking
153
+ ? "text-emerald-600 dark:text-emerald-400"
154
+ : "text-zinc-400 dark:text-zinc-500"
155
+ }`}
115
156
  strokeWidth={2}
116
157
  aria-hidden
117
158
  />
118
159
  )}
119
160
  <span
120
- className={`min-w-0 truncate ${done ? "text-zinc-500 line-through dark:text-zinc-500" : "text-zinc-700 dark:text-zinc-300"}`}
161
+ className={`min-w-0 truncate ${
162
+ done
163
+ ? "text-zinc-500 line-through dark:text-zinc-500"
164
+ : "text-zinc-700 dark:text-zinc-300"
165
+ }`}
121
166
  >
122
167
  {label || "—"}
123
168
  </span>
124
169
  </span>
125
170
  <span
126
- className={`shrink-0 font-mono tabular-nums tracking-tight ${isTracking ? "font-semibold text-emerald-700 dark:text-emerald-400" : "text-zinc-500 dark:text-zinc-400"}`}
171
+ className={`shrink-0 font-mono tabular-nums tracking-tight ${
172
+ isTracking
173
+ ? "font-semibold text-emerald-700 dark:text-emerald-400"
174
+ : "text-zinc-500 dark:text-zinc-400"
175
+ }`}
127
176
  >
128
177
  {timeStr}
129
178
  </span>
@@ -141,7 +190,9 @@ function DrawerRunningTaskRow({
141
190
  subtaskTrackingLabel: string;
142
191
  dashboardT: DashboardStrings;
143
192
  }>) {
144
- const title = taskTitleForDisplay(typeof task.name === "string" ? task.name : "");
193
+ const title = taskTitleForDisplay(
194
+ typeof task.name === "string" ? task.name : "",
195
+ );
145
196
  const subId = String(task.activeSubtaskTimerId ?? "").trim();
146
197
  const subTitle = subId ? subtaskTitleFor(task, subId) : undefined;
147
198
  const baseMs = Math.max(0, Math.floor(Number(task.durationMs) || 0));
@@ -155,9 +206,15 @@ function DrawerRunningTaskRow({
155
206
  return (
156
207
  <li className="min-w-0 text-xs leading-snug">
157
208
  <div className="flex min-w-0 items-baseline justify-between gap-2">
158
- <span className="min-w-0 truncate font-medium text-zinc-800 dark:text-zinc-200">{title}</span>
209
+ <span className="min-w-0 truncate font-medium text-zinc-800 dark:text-zinc-200">
210
+ {title}
211
+ </span>
159
212
  <span
160
- className={`shrink-0 font-mono text-xs tabular-nums tracking-tight ${smoothMain ? "font-semibold text-emerald-700 dark:text-emerald-400" : "text-zinc-600 dark:text-zinc-400"}`}
213
+ className={`shrink-0 font-mono text-xs tabular-nums tracking-tight ${
214
+ smoothMain
215
+ ? "font-semibold text-emerald-700 dark:text-emerald-400"
216
+ : "text-zinc-600 dark:text-zinc-400"
217
+ }`}
161
218
  aria-live={smoothMain ? "polite" : "off"}
162
219
  >
163
220
  {timeStr}
@@ -171,7 +228,11 @@ function DrawerRunningTaskRow({
171
228
  {subs.map((st) => {
172
229
  const sid = String(st.id ?? "").trim();
173
230
  return (
174
- <DrawerSubtaskRow key={sid || String(st.title)} sub={st} isTracking={subId !== "" && subId === sid} />
231
+ <DrawerSubtaskRow
232
+ key={sid || String(st.title)}
233
+ sub={st}
234
+ isTracking={subId !== "" && subId === sid}
235
+ />
175
236
  );
176
237
  })}
177
238
  </ul>
@@ -194,33 +255,40 @@ export function AppShellLiveSessionDrawer() {
194
255
  const lang: Lang = live?.language === "fr" ? "fr" : "en";
195
256
  const dt = dashboardStrings(lang);
196
257
 
197
- const liveSid = typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
258
+ const liveSid =
259
+ typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
198
260
  const hasLiveSession = liveSid !== "" && live?.archived !== true;
199
261
  const show = !onDashboardHome && Boolean(payload) && hasLiveSession;
200
262
 
201
263
  const sessionWallMinutes = live?.sessionDurationMinutes ?? 0;
202
264
  const wallClockMsBase = useMemo(
203
265
  () => Math.max(0, Math.floor(sessionWallMinutes * 60_000)),
204
- [sessionWallMinutes]
266
+ [sessionWallMinutes],
205
267
  );
206
- const sessionEnded = typeof live?.endAt === "string" && live.endAt.trim() !== "";
268
+ const sessionEnded =
269
+ typeof live?.endAt === "string" && live.endAt.trim() !== "";
207
270
  const smoothSessionWall =
208
271
  liveSid !== "" &&
209
272
  live?.archived !== true &&
210
273
  !sessionEnded &&
211
274
  live?.isPaused !== true;
212
- const sessionWallDisplayMs = useSmoothStopwatchDisplayMs(wallClockMsBase, smoothSessionWall);
275
+ const sessionWallDisplayMs = useSmoothStopwatchDisplayMs(
276
+ wallClockMsBase,
277
+ smoothSessionWall,
278
+ );
213
279
 
214
280
  const runningTasks = useMemo(() => runningTasksFromLive(live), [live]);
215
281
  const kf = live?.kronoFocus;
216
282
  const kfSecs = useKronoFocusLiveSeconds(
217
283
  kf?.timeLeftSeconds ?? 0,
218
284
  kf?.status ?? "idle",
219
- kf?.kronoFocusDeadlineAtMs
285
+ kf?.kronoFocusDeadlineAtMs,
220
286
  );
221
287
  const kfActive = kf?.status === "running" || kf?.status === "paused";
222
288
  const kfLinkedName =
223
- typeof kf?.linkedTaskName === "string" && kf.linkedTaskName.trim() !== "" ? kf.linkedTaskName.trim() : "";
289
+ typeof kf?.linkedTaskName === "string" && kf.linkedTaskName.trim() !== ""
290
+ ? kf.linkedTaskName.trim()
291
+ : "";
224
292
  const hasActivity =
225
293
  runningTasks.length > 0 || live?.isPaused === true || kfActive;
226
294
 
@@ -268,14 +336,32 @@ export function AppShellLiveSessionDrawer() {
268
336
  type="button"
269
337
  className="inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-zinc-200 text-zinc-600 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-300 dark:hover:bg-zinc-800/80"
270
338
  onClick={() => setCollapsed((c) => !c)}
271
- title={collapsed ? dt.appShellLiveDrawerExpand : dt.appShellLiveDrawerCollapse}
339
+ title={
340
+ collapsed
341
+ ? dt.appShellLiveDrawerExpand
342
+ : dt.appShellLiveDrawerCollapse
343
+ }
272
344
  aria-expanded={collapsed === false}
273
- aria-label={collapsed ? dt.appShellLiveDrawerExpand : dt.appShellLiveDrawerCollapse}
345
+ aria-label={
346
+ collapsed
347
+ ? dt.appShellLiveDrawerExpand
348
+ : dt.appShellLiveDrawerCollapse
349
+ }
274
350
  >
275
351
  {collapsed ? (
276
- <ChevronLeft size={18} strokeWidth={2} className="shrink-0" aria-hidden />
352
+ <ChevronLeft
353
+ size={18}
354
+ strokeWidth={2}
355
+ className="shrink-0"
356
+ aria-hidden
357
+ />
277
358
  ) : (
278
- <ChevronRight size={18} strokeWidth={2} className="shrink-0" aria-hidden />
359
+ <ChevronRight
360
+ size={18}
361
+ strokeWidth={2}
362
+ className="shrink-0"
363
+ aria-hidden
364
+ />
279
365
  )}
280
366
  </button>
281
367
  {!collapsed ? (
@@ -296,7 +382,12 @@ export function AppShellLiveSessionDrawer() {
296
382
  className="flex min-h-0 flex-1 flex-col items-center gap-1 border-t border-zinc-200 px-0.5 py-2 dark:border-zinc-700"
297
383
  aria-label={dt.kronoFocusTitle}
298
384
  >
299
- <Timer className="shrink-0 text-violet-600 dark:text-violet-400" size={16} strokeWidth={2} aria-hidden />
385
+ <Timer
386
+ className="shrink-0 text-violet-600 dark:text-violet-400"
387
+ size={16}
388
+ strokeWidth={2}
389
+ aria-hidden
390
+ />
300
391
  <span
301
392
  className="w-full max-w-[2.75rem] break-all text-center font-mono text-[0.58rem] font-semibold leading-tight tracking-tight text-violet-800 tabular-nums dark:text-violet-200"
302
393
  aria-live="polite"
@@ -332,27 +423,41 @@ export function AppShellLiveSessionDrawer() {
332
423
  </p>
333
424
  {kfLinkedName ? (
334
425
  <p className="mt-1.5 min-w-0 text-[0.65rem] leading-snug text-zinc-600 dark:text-zinc-400">
335
- <span className="font-medium text-zinc-500 dark:text-zinc-500">{dt.kronoFocusLinkedTaskIntro}</span>{" "}
336
- <span className="break-words">{truncateDrawerLabel(kfLinkedName, 120)}</span>
426
+ <span className="font-medium text-zinc-500 dark:text-zinc-500">
427
+ {dt.kronoFocusLinkedTaskIntro}
428
+ </span>{" "}
429
+ <span className="break-words">
430
+ {truncateDrawerLabel(kfLinkedName, 120)}
431
+ </span>
337
432
  </p>
338
433
  ) : null}
339
434
  </section>
340
435
  ) : null}
341
436
 
342
437
  <div className="min-w-0">
343
- <p className="truncate font-medium text-zinc-900 dark:text-zinc-50">{sessionLabel}</p>
438
+ <p className="truncate font-medium text-zinc-900 dark:text-zinc-50">
439
+ {sessionLabel}
440
+ </p>
344
441
  <p className="mt-1 text-xs text-zinc-600 dark:text-zinc-400">
345
- <span className="font-medium text-zinc-500 dark:text-zinc-500">{dt.appShellLiveDrawerWallClock}</span>
442
+ <span className="font-medium text-zinc-500 dark:text-zinc-500">
443
+ {dt.appShellLiveDrawerWallClock}
444
+ </span>
346
445
  {" · "}
347
446
  <span
348
- className={`tabular-nums ${smoothSessionWall ? "font-mono font-semibold text-emerald-700 dark:text-emerald-400" : "text-zinc-600 dark:text-zinc-400"}`}
447
+ className={`tabular-nums ${
448
+ smoothSessionWall
449
+ ? "font-mono font-semibold text-emerald-700 dark:text-emerald-400"
450
+ : "text-zinc-600 dark:text-zinc-400"
451
+ }`}
349
452
  aria-live={smoothSessionWall ? "polite" : "off"}
350
453
  >
351
454
  {wallLabel}
352
455
  </span>
353
456
  </p>
354
457
  {live?.isPaused === true ? (
355
- <p className="mt-1 text-xs font-medium text-amber-700 dark:text-amber-300">{dt.appShellLiveDrawerSessionPaused}</p>
458
+ <p className="mt-1 text-xs font-medium text-amber-700 dark:text-amber-300">
459
+ {dt.appShellLiveDrawerSessionPaused}
460
+ </p>
356
461
  ) : null}
357
462
  </div>
358
463
 
@@ -361,17 +466,23 @@ export function AppShellLiveSessionDrawer() {
361
466
  {dt.appShellLiveDrawerTasksHeading}
362
467
  </p>
363
468
  {runningTasks.length === 0 ? (
364
- <p className="mt-1 text-xs text-zinc-500 dark:text-zinc-500">{dt.appShellLiveDrawerNoRunningTasks}</p>
469
+ <p className="mt-1 text-xs text-zinc-500 dark:text-zinc-500">
470
+ {dt.appShellLiveDrawerNoRunningTasks}
471
+ </p>
365
472
  ) : (
366
473
  <ul className="mt-1.5 space-y-2">
367
474
  {runningTasks.map((t) => {
368
475
  const id = typeof t.id === "string" ? t.id : "";
369
- const title = taskTitleForDisplay(typeof t.name === "string" ? t.name : "");
476
+ const title = taskTitleForDisplay(
477
+ typeof t.name === "string" ? t.name : "",
478
+ );
370
479
  return (
371
480
  <DrawerRunningTaskRow
372
481
  key={id || title}
373
482
  task={t}
374
- subtaskTrackingLabel={dt.appShellLiveDrawerSubtaskTracking}
483
+ subtaskTrackingLabel={
484
+ dt.appShellLiveDrawerSubtaskTracking
485
+ }
375
486
  dashboardT={dt}
376
487
  />
377
488
  );
@@ -384,7 +495,12 @@ export function AppShellLiveSessionDrawer() {
384
495
  href={dashHref}
385
496
  className="mt-auto inline-flex items-center justify-center gap-2 rounded-lg border border-violet-400/60 bg-violet-50 px-3 py-2 text-xs font-medium text-violet-950 hover:bg-violet-100/90 dark:border-violet-600/50 dark:bg-violet-950/40 dark:text-violet-100 dark:hover:bg-violet-900/50"
386
497
  >
387
- <LayoutDashboard size={16} strokeWidth={2} className="shrink-0" aria-hidden />
498
+ <LayoutDashboard
499
+ size={16}
500
+ strokeWidth={2}
501
+ className="shrink-0"
502
+ aria-hidden
503
+ />
388
504
  {dt.appShellLiveDrawerOpenDashboard}
389
505
  </Link>
390
506
  </div>