@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,15 +1,39 @@
1
1
  "use client";
2
2
 
3
- import { Archive, Circle, ExternalLink, Loader2, Square, Trash2, UploadCloud } from "lucide-react";
4
- import { sessionTaskCountNoun, type DashboardStrings, type Lang } from "@/lib/dashboardCopy";
5
- import { DEFAULT_DASHBOARD_TIME_ZONE, isValidIanaTimeZone } from "@/lib/dashboardTimeZone";
3
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import type { ReactNode } from "react";
5
+ import {
6
+ Archive,
7
+ Circle,
8
+ ExternalLink,
9
+ FileText,
10
+ LayoutGrid,
11
+ Loader2,
12
+ Square,
13
+ Trash2,
14
+ UploadCloud,
15
+ } from "lucide-react";
16
+ import {
17
+ sessionTaskCountNoun,
18
+ type DashboardStrings,
19
+ type Lang,
20
+ } from "@/lib/dashboardCopy";
21
+ import {
22
+ DEFAULT_DASHBOARD_TIME_ZONE,
23
+ isValidIanaTimeZone,
24
+ } from "@/lib/dashboardTimeZone";
6
25
  import { formatIsoInstantShort } from "@/lib/formatIsoShort";
7
- import { sessionWallClockMinutes, type LooseSession } from "@/lib/reportingAggregate";
26
+ import {
27
+ sessionWallClockMinutes,
28
+ type LooseSession,
29
+ } from "@/lib/reportingAggregate";
8
30
  import { formatDuration } from "@/lib/taskParsing";
31
+ import { isOpenSessionDisplayPlanned } from "@/lib/temporalDisplayPlanned";
9
32
 
10
33
  export type SessionListEntry = {
11
34
  sessionId: string;
12
35
  sessionName?: string;
36
+ sessionNote?: string;
13
37
  savedAt?: string;
14
38
  /** Horodatage immuable de création de la session ; repli : `startAt` pour les anciennes données. */
15
39
  createdAt?: string | null;
@@ -33,12 +57,14 @@ function taskCount(s: SessionListEntry): number {
33
57
  Array.isArray(s.activeTasks) && s.activeTasks.length > 0
34
58
  ? s.activeTasks.length
35
59
  : s.activeTask
36
- ? 1
37
- : 0;
60
+ ? 1
61
+ : 0;
38
62
  return listed + nActive;
39
63
  }
40
64
 
41
- function sessionMongoPushState(sess: SessionListEntry): "never" | "synced" | "dirty" {
65
+ function sessionMongoPushState(
66
+ sess: SessionListEntry,
67
+ ): "never" | "synced" | "dirty" {
42
68
  const saved = sess.savedAt ?? "";
43
69
  const lastPush = sess.mongoLastPushedSavedAt;
44
70
  if (!lastPush) {
@@ -80,6 +106,16 @@ export function SessionListPanel({
80
106
  onPushSessionToMongo,
81
107
  pushingSessionId = null,
82
108
  sessionDurationAlertThresholdMinutes,
109
+ onOpenSessionGantt,
110
+ sessionRowExitAnimateId = null,
111
+ onSessionRowExitAnimationDone,
112
+ /** Pendant la sortie « fin de session live », conserve le badge live sur l’instantané. */
113
+ liveChromeExitSessionId = null,
114
+ /** Pendant la sortie « fin de session », garde la ligne en tête (le tri global ne la ramène pas plus bas). */
115
+ sortPinSessionId = null,
116
+ /** Détails session (carte latérale) rendus sous la ligne plutôt qu’au-dessus de la liste. */
117
+ sessionDetailInline,
118
+ forcePageScroll = false,
83
119
  }: {
84
120
  sessions: SessionListEntry[];
85
121
  lang: Lang;
@@ -106,13 +142,113 @@ export function SessionListPanel({
106
142
  pushingSessionId?: string | null;
107
143
  /** Minutes murales : durée affichée en alerte si ≥ seuil (même réglage que le panneau session). */
108
144
  sessionDurationAlertThresholdMinutes?: number;
145
+ /** Ouvre la vue Gantt pour la session de cette ligne (historique, archives ou live). */
146
+ onOpenSessionGantt?: (sessionId: string) => void;
147
+ /** Ligne en cours d’animation de disparition (archivage ou fin de session live). */
148
+ sessionRowExitAnimateId?: string | null;
149
+ /** Appelé une fois l’animation terminée (libère l’instantané côté parent). */
150
+ onSessionRowExitAnimationDone?: () => void;
151
+ liveChromeExitSessionId?: string | null;
152
+ sortPinSessionId?: string | null;
153
+ sessionDetailInline?: {
154
+ sessionId: string;
155
+ content: ReactNode;
156
+ } | null;
157
+ /** Quand vrai, laisse la page scroller (pas de scroll interne forcé). */
158
+ forcePageScroll?: boolean;
109
159
  }) {
110
- const sorted = sortSessions(sessions);
160
+ const sessionRowExitDoneRef = useRef<(() => void) | undefined>(undefined);
161
+ useLayoutEffect(() => {
162
+ sessionRowExitDoneRef.current = onSessionRowExitAnimationDone;
163
+ }, [onSessionRowExitAnimationDone]);
164
+
165
+ /** Double frame pour garantir une transition depuis l’état initial. */
166
+ const [exitStyleSessionId, setExitStyleSessionId] = useState<string | null>(
167
+ null,
168
+ );
169
+ const [detailVisibleSessionId, setDetailVisibleSessionId] = useState<
170
+ string | null
171
+ >(null);
172
+ const [detailEnterSessionId, setDetailEnterSessionId] = useState<
173
+ string | null
174
+ >(null);
175
+ const [listNowMs, setListNowMs] = useState(() => Date.now());
176
+
177
+ useEffect(() => {
178
+ if (!sessionRowExitAnimateId) {
179
+ setExitStyleSessionId(null);
180
+ return;
181
+ }
182
+
183
+ let raf1 = 0;
184
+ let raf2 = 0;
185
+ raf1 = requestAnimationFrame(() => {
186
+ raf2 = requestAnimationFrame(() => {
187
+ setExitStyleSessionId(sessionRowExitAnimateId);
188
+ });
189
+ });
190
+
191
+ const exitMs = 320;
192
+ const doneTimer = globalThis.setTimeout(() => {
193
+ sessionRowExitDoneRef.current?.();
194
+ setExitStyleSessionId(null);
195
+ }, exitMs);
196
+
197
+ return () => {
198
+ cancelAnimationFrame(raf1);
199
+ cancelAnimationFrame(raf2);
200
+ clearTimeout(doneTimer);
201
+ };
202
+ }, [sessionRowExitAnimateId]);
203
+
204
+ useEffect(() => {
205
+ const id = globalThis.setInterval(() => setListNowMs(Date.now()), 1000);
206
+ return () => globalThis.clearInterval(id);
207
+ }, []);
208
+
209
+ useEffect(() => {
210
+ const targetId =
211
+ typeof sessionDetailInline?.sessionId === "string" &&
212
+ sessionDetailInline.sessionId.trim() !== ""
213
+ ? sessionDetailInline.sessionId
214
+ : null;
215
+ if (!targetId) {
216
+ setDetailVisibleSessionId(null);
217
+ setDetailEnterSessionId(null);
218
+ return;
219
+ }
220
+ setDetailVisibleSessionId(targetId);
221
+ setDetailEnterSessionId(null);
222
+ let raf1 = 0;
223
+ let raf2 = 0;
224
+ raf1 = requestAnimationFrame(() => {
225
+ raf2 = requestAnimationFrame(() => setDetailEnterSessionId(targetId));
226
+ });
227
+ return () => {
228
+ cancelAnimationFrame(raf1);
229
+ cancelAnimationFrame(raf2);
230
+ };
231
+ }, [sessionDetailInline?.sessionId]);
232
+
233
+ const sorted = useMemo(() => {
234
+ const pin = sortPinSessionId?.trim();
235
+ if (!pin) {
236
+ return sortSessions(sessions);
237
+ }
238
+ const pinned = sessions.find((s) => s.sessionId === pin);
239
+ const rest = sessions.filter((s) => s.sessionId !== pin);
240
+ if (!pinned) {
241
+ return sortSessions(sessions);
242
+ }
243
+ return [pinned, ...sortSessions(rest)];
244
+ }, [sessions, sortPinSessionId]);
111
245
  const archivesLabel =
112
- archivedCount > 0 ? `${t.archivesModalTitle} (${archivedCount})` : t.archivesModalTitle;
246
+ archivedCount > 0
247
+ ? `${t.archivesModalTitle} (${archivedCount})`
248
+ : t.archivesModalTitle;
113
249
 
114
250
  return (
115
- <aside className="flex h-min min-w-0 max-w-full max-h-[calc(100vh-8rem)] flex-col rounded-xl border border-zinc-200 bg-white/90 shadow-sm dark:border-zinc-800 dark:bg-zinc-800/50 dark:shadow-none xl:z-0">
251
+ <aside className="flex h-full min-h-0 min-w-0 max-w-full flex-1 flex-col rounded-xl border border-zinc-200 bg-white/90 shadow-sm dark:border-zinc-800 dark:bg-zinc-800/50 dark:shadow-none xl:z-0">
116
252
  <div className="flex flex-wrap items-center justify-end gap-3 border-b border-zinc-200 px-5 py-3 dark:border-zinc-800">
117
253
  {onOpenArchives ? (
118
254
  <button
@@ -122,7 +258,12 @@ export function SessionListPanel({
122
258
  aria-label={archivesLabel}
123
259
  title={archivesLabel}
124
260
  >
125
- <Archive size={18} strokeWidth={1.75} className="text-zinc-600 dark:text-zinc-300" aria-hidden />
261
+ <Archive
262
+ size={18}
263
+ strokeWidth={1.75}
264
+ className="text-zinc-600 dark:text-zinc-300"
265
+ aria-hidden
266
+ />
126
267
  {archivedCount > 0 ? (
127
268
  <span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-violet-600 px-0.5 text-[0.6rem] font-semibold leading-none text-white">
128
269
  {archivedCount > 99 ? "99+" : archivedCount}
@@ -134,29 +275,73 @@ export function SessionListPanel({
134
275
  )}
135
276
  </div>
136
277
 
137
- <nav className="min-h-0 flex-1 overflow-y-auto px-3 py-3 pr-4" aria-label={t.sessionsListAriaLabel}>
278
+ <nav
279
+ className={`min-h-0 flex-1 px-3 py-3 pr-4 ${
280
+ forcePageScroll
281
+ ? "overflow-visible"
282
+ : "overflow-y-auto overscroll-contain"
283
+ }`}
284
+ aria-label={t.sessionsListAriaLabel}
285
+ >
138
286
  <ul className="space-y-1.5">
139
287
  {sorted.map((sess) => {
140
288
  const id = sess.sessionId;
141
- const isLive = id === liveSessionId;
289
+ const isLiveRow =
290
+ typeof liveSessionId === "string" && id === liveSessionId;
291
+ const showLiveChrome =
292
+ isLiveRow ||
293
+ (typeof liveChromeExitSessionId === "string" &&
294
+ liveChromeExitSessionId === id);
142
295
  const isSelected = id === selectedSessionId;
143
296
  const n = taskCount(sess);
297
+ const hasAssociatedTasks = n > 0;
144
298
  const tz =
145
- displayTimeZone.trim() && isValidIanaTimeZone(displayTimeZone.trim())
299
+ displayTimeZone.trim() &&
300
+ isValidIanaTimeZone(displayTimeZone.trim())
146
301
  ? displayTimeZone.trim()
147
302
  : DEFAULT_DASHBOARD_TIME_ZONE;
148
- const createdIso = (sess.createdAt?.trim() || sess.startAt?.trim() || sess.savedAt?.trim() || "").trim();
149
- const createdLabel = formatIsoInstantShort(createdIso, lang, tz, use24HourClock);
150
- const label = sess.sessionName?.trim() || (createdLabel ? `${id.slice(0, 8)} · ${createdLabel}` : id.slice(0, 8));
151
- const startIso = (sess.startAt?.trim() || sess.savedAt?.trim() || "").trim();
152
- const startLabel = formatIsoInstantShort(startIso, lang, tz, use24HourClock) ?? "";
303
+ const createdIso = (
304
+ sess.createdAt?.trim() ||
305
+ sess.startAt?.trim() ||
306
+ sess.savedAt?.trim() ||
307
+ ""
308
+ ).trim();
309
+ const createdLabel = formatIsoInstantShort(
310
+ createdIso,
311
+ lang,
312
+ tz,
313
+ use24HourClock,
314
+ );
315
+ const label =
316
+ sess.sessionName?.trim() ||
317
+ (createdLabel
318
+ ? `${id.slice(0, 8)} · ${createdLabel}`
319
+ : id.slice(0, 8));
320
+ const startIso = (
321
+ sess.startAt?.trim() ||
322
+ sess.savedAt?.trim() ||
323
+ ""
324
+ ).trim();
325
+ const startLabel =
326
+ formatIsoInstantShort(startIso, lang, tz, use24HourClock) ?? "—";
153
327
  const hasEnd =
154
328
  typeof sess.endAt === "string" && sess.endAt.trim() !== "";
155
329
  const endLabel = hasEnd
156
- ? formatIsoInstantShort(sess.endAt!.trim(), lang, tz, use24HourClock) ?? "—"
330
+ ? formatIsoInstantShort(
331
+ sess.endAt!.trim(),
332
+ lang,
333
+ tz,
334
+ use24HourClock,
335
+ ) ?? "—"
157
336
  : null;
158
337
 
159
338
  const taskNoun = sessionTaskCountNoun(n, t, lang);
339
+ const noteRaw =
340
+ typeof sess.sessionNote === "string"
341
+ ? sess.sessionNote.trim()
342
+ : "";
343
+ const notePreview = noteRaw.replaceAll(/\s+/g, " ");
344
+ const hasSessionNote = notePreview.length > 0;
160
345
  const wallMins = sessionWallClockMinutes(sess as LooseSession);
161
346
  const durationLabel = wallMins > 0 ? formatDuration(wallMins) : "—";
162
347
  const thresholdMin =
@@ -166,31 +351,62 @@ export function SessionListPanel({
166
351
  : null;
167
352
  const durationAlert =
168
353
  thresholdMin !== null && wallMins > 0 && wallMins >= thresholdMin;
169
- const thresholdHours = thresholdMin !== null ? Math.round(thresholdMin / 60) : 0;
354
+ const thresholdHours =
355
+ thresholdMin !== null ? Math.round(thresholdMin / 60) : 0;
170
356
  const durationTitle = durationAlert
171
- ? t.sessionListWallDurationAlertTooltip.replace("{hours}", String(thresholdHours))
357
+ ? t.sessionListWallDurationAlertTooltip.replace(
358
+ "{hours}",
359
+ String(thresholdHours),
360
+ )
172
361
  : t.sessionListWallDurationTitle;
173
362
  const mongoState = sessionMongoPushState(sess);
174
363
  const mongoTitle =
175
364
  mongoState === "synced"
176
365
  ? t.sessionMongoPushSyncedTitle
177
366
  : mongoState === "dirty"
178
- ? t.sessionMongoPushDirtyTitle
179
- : t.sessionMongoPushNeverTitle;
367
+ ? t.sessionMongoPushDirtyTitle
368
+ : t.sessionMongoPushNeverTitle;
180
369
  const mongoIconClass =
181
370
  mongoState === "synced"
182
371
  ? "text-emerald-400/95"
183
372
  : mongoState === "dirty"
184
- ? "text-amber-400/95"
185
- : "text-zinc-500";
186
-
373
+ ? "text-amber-400/95"
374
+ : "text-zinc-500";
375
+ const livePlanned =
376
+ showLiveChrome &&
377
+ isOpenSessionDisplayPlanned(
378
+ { startAt: sess.startAt, endAt: sess.endAt },
379
+ listNowMs,
380
+ );
381
+ const rowHasExpandedDetail =
382
+ detailVisibleSessionId === id &&
383
+ sessionDetailInline?.sessionId === id;
384
+ const isExitAnimatingRow = exitStyleSessionId === id;
187
385
  return (
188
- <li key={id} id={`kronosys-session-${id}`} className="scroll-mt-20">
386
+ <li
387
+ key={id}
388
+ id={`kronosys-session-${id}`}
389
+ className={`scroll-mt-20 overflow-hidden transition-[max-height,margin-top] duration-300 ease-out motion-reduce:transition-none ${
390
+ isExitAnimatingRow
391
+ ? "pointer-events-none max-h-0 -mt-1.5"
392
+ : rowHasExpandedDetail
393
+ ? "max-h-[160rem]"
394
+ : "max-h-[28rem]"
395
+ }`}
396
+ >
189
397
  <div
190
- className={`grid grid-cols-[minmax(0,1fr)_auto] grid-rows-[auto_auto] gap-x-1.5 gap-y-1 rounded-lg px-3.5 py-3 transition-colors ${
398
+ className={`grid grid-cols-[minmax(0,1fr)_auto] grid-rows-[auto_auto] gap-x-1.5 gap-y-1 rounded-lg px-3.5 py-3 transition-[opacity,transform,box-shadow,background-color] duration-300 ease-out motion-reduce:transition-none ${
399
+ isExitAnimatingRow
400
+ ? "pointer-events-none opacity-0 translate-x-2 scale-[0.98]"
401
+ : ""
402
+ } ${
191
403
  isSelected
192
404
  ? "bg-violet-500/15 ring-1 ring-violet-500/45 dark:bg-violet-600/20 dark:ring-violet-500/50"
193
- : "hover:bg-zinc-100/90 dark:hover:bg-zinc-800/80"
405
+ : livePlanned
406
+ ? "bg-sky-50/80 hover:bg-sky-100/85 dark:bg-sky-950/25 dark:hover:bg-sky-950/40"
407
+ : hasAssociatedTasks
408
+ ? "bg-emerald-50/75 hover:bg-emerald-100/80 dark:bg-emerald-950/20 dark:hover:bg-emerald-950/35"
409
+ : "bg-zinc-100/65 hover:bg-zinc-100/90 dark:bg-zinc-900/30 dark:hover:bg-zinc-800/80"
194
410
  }`}
195
411
  >
196
412
  <button
@@ -199,28 +415,36 @@ export function SessionListPanel({
199
415
  className="group col-start-1 row-start-1 row-span-2 flex min-w-0 flex-col gap-0.5 text-left outline-none focus-visible:ring-2 focus-visible:ring-violet-500/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900 rounded-md -mx-1 px-1 -my-0.5 py-0.5"
200
416
  >
201
417
  <span className="flex min-w-0 items-center gap-2 text-sm font-medium text-zinc-800 group-hover:text-zinc-950 dark:text-zinc-100 dark:group-hover:text-white">
202
- {isLive && (
418
+ {showLiveChrome ? (
203
419
  <Circle
204
- className="shrink-0 fill-emerald-400 text-emerald-400"
420
+ className={
421
+ livePlanned
422
+ ? "shrink-0 fill-sky-500 text-sky-500"
423
+ : "shrink-0 fill-emerald-400 text-emerald-400"
424
+ }
205
425
  size={10}
206
426
  aria-hidden
207
427
  />
208
- )}
428
+ ) : null}
209
429
  <span className="truncate">{label}</span>
210
- {isLive && (
430
+ {showLiveChrome && !livePlanned ? (
211
431
  <span className="shrink-0 rounded bg-emerald-100 px-1.5 py-0 text-[0.6rem] font-bold uppercase tracking-wide text-emerald-900 ring-1 ring-emerald-600/20 dark:bg-emerald-900/50 dark:text-emerald-300 dark:ring-0">
212
432
  {t.sessionLiveBadge}
213
433
  </span>
214
- )}
434
+ ) : null}
215
435
  </span>
216
436
  <span className="flex min-w-0 flex-col gap-0.5 text-[0.7rem] leading-snug text-zinc-500 group-hover:text-zinc-600 dark:group-hover:text-zinc-400">
217
437
  <span className="min-w-0 break-words">
218
- <span className="text-zinc-600 dark:text-zinc-500">{t.sessionListStartedPrefix}</span>{" "}
438
+ <span className="text-zinc-600 dark:text-zinc-500">
439
+ {t.sessionListStartedPrefix}
440
+ </span>{" "}
219
441
  {startLabel}
220
442
  </span>
221
443
  {endLabel !== null ? (
222
444
  <span className="min-w-0 break-words">
223
- <span className="text-zinc-600 dark:text-zinc-500">{t.sessionListEndedPrefix}</span>{" "}
445
+ <span className="text-zinc-600 dark:text-zinc-500">
446
+ {t.sessionListEndedPrefix}
447
+ </span>{" "}
224
448
  {endLabel}
225
449
  </span>
226
450
  ) : null}
@@ -228,16 +452,34 @@ export function SessionListPanel({
228
452
  <span>
229
453
  {n} {taskNoun}
230
454
  </span>
231
- <span className="text-zinc-400 dark:text-zinc-600"> · </span>
455
+ <span className="text-zinc-400 dark:text-zinc-600">
456
+ {" "}
457
+ ·{" "}
458
+ </span>
232
459
  <span
233
460
  className={`inline tabular-nums ${
234
- durationAlert ? "kronosys-session-duration-alert font-semibold" : ""
461
+ durationAlert
462
+ ? "kronosys-session-duration-alert font-semibold"
463
+ : ""
235
464
  }`}
236
465
  title={durationTitle}
237
466
  >
238
467
  {durationLabel}
239
468
  </span>
240
469
  </span>
470
+ {hasSessionNote ? (
471
+ <span
472
+ className="mt-0.5 inline-flex min-w-0 items-center gap-1 text-zinc-500 dark:text-zinc-400"
473
+ title={notePreview}
474
+ >
475
+ <FileText
476
+ size={12}
477
+ className="shrink-0"
478
+ aria-hidden
479
+ />
480
+ <span className="truncate">{notePreview}</span>
481
+ </span>
482
+ ) : null}
241
483
  </span>
242
484
  </button>
243
485
  <div className="col-start-2 row-start-2 flex shrink-0 items-center gap-0.5 self-center">
@@ -260,11 +502,14 @@ export function SessionListPanel({
260
502
  aria-label={t.sessionMongoPushBusy}
261
503
  />
262
504
  ) : (
263
- <UploadCloud size={sessionRowActionIconSize} aria-hidden />
505
+ <UploadCloud
506
+ size={sessionRowActionIconSize}
507
+ aria-hidden
508
+ />
264
509
  )}
265
510
  </button>
266
511
  ) : null}
267
- {isLive && onEndLiveSession ? (
512
+ {isLiveRow && onEndLiveSession ? (
268
513
  <button
269
514
  type="button"
270
515
  className={`${sessionRowActionBase} text-zinc-500 hover:text-rose-600 dark:hover:text-rose-300`}
@@ -275,6 +520,23 @@ export function SessionListPanel({
275
520
  <Square size={sessionRowActionIconSize} aria-hidden />
276
521
  </button>
277
522
  ) : null}
523
+ {onOpenSessionGantt ? (
524
+ <button
525
+ type="button"
526
+ className={`${sessionRowActionBase} text-zinc-500 hover:text-violet-700 dark:hover:text-violet-300`}
527
+ title={t.tasksTimelineGanttOpenBtn}
528
+ aria-label={t.sessionListOpenGanttAria}
529
+ onClick={(e) => {
530
+ e.stopPropagation();
531
+ onOpenSessionGantt(id);
532
+ }}
533
+ >
534
+ <LayoutGrid
535
+ size={sessionRowActionIconSize}
536
+ aria-hidden
537
+ />
538
+ </button>
539
+ ) : null}
278
540
  {onOpenSessionInNewTab ? (
279
541
  <button
280
542
  type="button"
@@ -283,10 +545,15 @@ export function SessionListPanel({
283
545
  aria-label={t.openSessionInNewTab}
284
546
  onClick={() => onOpenSessionInNewTab(id)}
285
547
  >
286
- <ExternalLink size={sessionRowActionIconSize} aria-hidden />
548
+ <ExternalLink
549
+ size={sessionRowActionIconSize}
550
+ aria-hidden
551
+ />
287
552
  </button>
288
553
  ) : null}
289
- {onArchiveSession && !isLive ? (
554
+ {onArchiveSession &&
555
+ !isLiveRow &&
556
+ liveChromeExitSessionId !== id ? (
290
557
  <button
291
558
  type="button"
292
559
  className={`${sessionRowActionBase} text-zinc-500 hover:text-amber-700 dark:hover:text-amber-200/90`}
@@ -297,7 +564,9 @@ export function SessionListPanel({
297
564
  <Archive size={sessionRowActionIconSize} aria-hidden />
298
565
  </button>
299
566
  ) : null}
300
- {onDeleteSession && !isLive ? (
567
+ {onDeleteSession &&
568
+ !isLiveRow &&
569
+ liveChromeExitSessionId !== id ? (
301
570
  <button
302
571
  type="button"
303
572
  className={`${sessionRowActionBase} text-zinc-500 hover:text-red-600 dark:hover:text-red-300`}
@@ -310,6 +579,20 @@ export function SessionListPanel({
310
579
  ) : null}
311
580
  </div>
312
581
  </div>
582
+ {detailVisibleSessionId === id &&
583
+ sessionDetailInline?.sessionId === id ? (
584
+ <section
585
+ id={`kronosys-session-${id}-metrics`}
586
+ className={`overflow-hidden border-t transition-[max-height,opacity,margin-top,padding-top,border-color] duration-300 ease-out motion-reduce:transition-none ${
587
+ detailEnterSessionId === id
588
+ ? "mt-2 max-h-[80rem] border-zinc-200/90 pt-3 pr-1 opacity-100 dark:border-zinc-700/70"
589
+ : "mt-0 max-h-0 border-transparent pt-0 opacity-0"
590
+ }`}
591
+ aria-label={t.selectedSessionSidebarTitle}
592
+ >
593
+ {sessionDetailInline.content}
594
+ </section>
595
+ ) : null}
313
596
  </li>
314
597
  );
315
598
  })}