@nightkatana/kronosys-app 1.0.0-beta.16 → 1.0.0-beta.17

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.
@@ -68,7 +68,7 @@ import {
68
68
  writeDashboardColumnHintsDismissed,
69
69
  } from "@/lib/dashboardColumnHintsStorage";
70
70
  import { KronosysTimePopoverField } from "@/components/dashboard/KronosysTimePopoverField";
71
- import { Search } from "lucide-react";
71
+ import { FileText, Search } from "lucide-react";
72
72
  import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
73
73
 
74
74
  type LiveShape = { language?: string };
@@ -3457,6 +3457,12 @@ function SettingsPageContent() {
3457
3457
  archivedPageRows.map((sess) => {
3458
3458
  const label =
3459
3459
  sess.sessionName?.trim() || sess.sessionId.slice(0, 8);
3460
+ const noteRaw =
3461
+ typeof sess.sessionNote === "string"
3462
+ ? sess.sessionNote.trim()
3463
+ : "";
3464
+ const notePreview = noteRaw.replaceAll(/\s+/g, " ");
3465
+ const hasSessionNote = notePreview.length > 0;
3460
3466
  const n = archivedTaskCount(sess);
3461
3467
  const busy = archivedRestoreBusyId === sess.sessionId;
3462
3468
  return (
@@ -3471,6 +3477,19 @@ function SettingsPageContent() {
3471
3477
  <div className="text-[0.7rem] text-zinc-500">
3472
3478
  {n} {sessionTaskCountNoun(n, dt, lang)}
3473
3479
  </div>
3480
+ {hasSessionNote ? (
3481
+ <div
3482
+ className="mt-1 inline-flex min-w-0 max-w-full items-center gap-1 text-[0.7rem] text-zinc-500"
3483
+ title={notePreview}
3484
+ >
3485
+ <FileText
3486
+ size={12}
3487
+ className="shrink-0"
3488
+ aria-hidden
3489
+ />
3490
+ <span className="truncate">{notePreview}</span>
3491
+ </div>
3492
+ ) : null}
3474
3493
  </div>
3475
3494
  <button
3476
3495
  type="button"
@@ -35,8 +35,10 @@ type SessionMetricsShape = {
35
35
  /** Début officiel de la session (ISO 8601), aligné sur l’historique / la liste. */
36
36
  startAt?: string | null;
37
37
  endAt?: string | null;
38
+ scheduledEndAt?: string | null;
38
39
  sessionEndReasonKind?: string;
39
40
  sessionEndReasonNote?: string;
41
+ sessionNote?: string;
40
42
  sessionDurationMinutes?: number;
41
43
  codingMinutesSession?: number;
42
44
  activeMinutes?: number;
@@ -182,6 +184,7 @@ export function SelectedSessionSidebarBlock({
182
184
  number | null
183
185
  >(null);
184
186
  const [sessionEndDraft, setSessionEndDraft] = useState("");
187
+ const [sessionNoteDraft, setSessionNoteDraft] = useState("");
185
188
  const [sessionClosureExpanded, setSessionClosureExpanded] = useState(false);
186
189
 
187
190
  const sessionWallMinutes =
@@ -253,11 +256,8 @@ export function SelectedSessionSidebarBlock({
253
256
  const canEditSessionEndTime =
254
257
  allowSessionEndTimeEdit &&
255
258
  hasSessionContext &&
256
- sessionEnded &&
257
259
  typeof sessionCurrent?.startAt === "string" &&
258
- sessionCurrent.startAt.trim() !== "" &&
259
- typeof sessionCurrent?.endAt === "string" &&
260
- sessionCurrent.endAt.trim() !== "";
260
+ sessionCurrent.startAt.trim() !== "";
261
261
  const [sessionStartDraft, setSessionStartDraft] = useState("");
262
262
 
263
263
  useEffect(() => {
@@ -277,6 +277,8 @@ export function SelectedSessionSidebarBlock({
277
277
  const raw =
278
278
  typeof sessionCurrent?.endAt === "string"
279
279
  ? sessionCurrent.endAt.trim()
280
+ : typeof sessionCurrent?.scheduledEndAt === "string"
281
+ ? sessionCurrent.scheduledEndAt.trim()
280
282
  : "";
281
283
  const parsed = raw ? new Date(raw) : null;
282
284
  if (!parsed || Number.isNaN(parsed.getTime())) {
@@ -284,7 +286,19 @@ export function SelectedSessionSidebarBlock({
284
286
  return;
285
287
  }
286
288
  setSessionEndDraft(formatDatetimeLocalValue(parsed));
287
- }, [sessionCurrent?.sessionId, sessionCurrent?.endAt]);
289
+ }, [
290
+ sessionCurrent?.sessionId,
291
+ sessionCurrent?.endAt,
292
+ sessionCurrent?.scheduledEndAt,
293
+ ]);
294
+
295
+ useEffect(() => {
296
+ setSessionNoteDraft(
297
+ typeof sessionCurrent?.sessionNote === "string"
298
+ ? sessionCurrent.sessionNote
299
+ : "",
300
+ );
301
+ }, [sessionCurrent?.sessionId, sessionCurrent?.sessionNote]);
288
302
 
289
303
  useEffect(() => {
290
304
  setOptimisticSessionWallMinutes(null);
@@ -367,7 +381,7 @@ export function SelectedSessionSidebarBlock({
367
381
  return;
368
382
  }
369
383
  const endAt = new Date(endMs).toISOString();
370
- if (endAt === sessionCurrent.endAt) {
384
+ if (endAt === (sessionCurrent.endAt ?? sessionCurrent.scheduledEndAt)) {
371
385
  return;
372
386
  }
373
387
  setOptimisticSessionWallMinutes(Math.max(0, (endMs - startMs) / 60000));
@@ -392,12 +406,20 @@ export function SelectedSessionSidebarBlock({
392
406
  const raw =
393
407
  typeof sessionCurrent?.endAt === "string"
394
408
  ? sessionCurrent.endAt.trim()
409
+ : typeof sessionCurrent?.scheduledEndAt === "string"
410
+ ? sessionCurrent.scheduledEndAt.trim()
395
411
  : "";
396
412
  if (!raw) {
397
413
  return null;
398
414
  }
399
415
  return formatIsoInstantShort(raw, lang, displayTimeZone, use24HourClock);
400
- }, [sessionCurrent?.endAt, displayTimeZone, lang, use24HourClock]);
416
+ }, [
417
+ sessionCurrent?.endAt,
418
+ sessionCurrent?.scheduledEndAt,
419
+ displayTimeZone,
420
+ lang,
421
+ use24HourClock,
422
+ ]);
401
423
  const sessionCreatedFormatted = useMemo(() => {
402
424
  const raw =
403
425
  typeof sessionCurrent?.createdAt === "string" &&
@@ -660,6 +682,34 @@ export function SelectedSessionSidebarBlock({
660
682
  </div>
661
683
  </div>
662
684
  </div>
685
+ <div className="space-y-1 text-sm">
686
+ <label
687
+ htmlFor="kronosys-session-note"
688
+ className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500"
689
+ >
690
+ {t.sessionNoteLabel}
691
+ </label>
692
+ <textarea
693
+ id="kronosys-session-note"
694
+ className="w-full rounded-lg border border-zinc-300 bg-white/90 px-3 py-2 text-sm text-zinc-800 outline-none transition focus:border-violet-500 dark:border-zinc-700 dark:bg-zinc-900/70 dark:text-zinc-100"
695
+ placeholder={t.sessionNotePlaceholder}
696
+ value={sessionNoteDraft}
697
+ onChange={(e) => setSessionNoteDraft(e.target.value)}
698
+ onBlur={() => {
699
+ if (
700
+ sessionNoteDraft === (sessionCurrent?.sessionNote ?? "")
701
+ ) {
702
+ return;
703
+ }
704
+ void post({
705
+ type: "setSessionNote",
706
+ sessionId: targetSessionEndReasonId,
707
+ note: sessionNoteDraft,
708
+ });
709
+ }}
710
+ rows={3}
711
+ />
712
+ </div>
663
713
  </section>
664
714
  ) : null}
665
715
  </div>
@@ -776,7 +826,7 @@ export function SelectedSessionSidebarBlock({
776
826
  <button
777
827
  type="button"
778
828
  className="flex w-full items-center justify-between gap-2 rounded-md text-left hover:text-zinc-900 dark:hover:text-zinc-100"
779
- aria-expanded={sessionClosureExpanded ? "true" : "false"}
829
+ aria-expanded={sessionClosureExpanded}
780
830
  onClick={() => setSessionClosureExpanded((v) => !v)}
781
831
  >
782
832
  <div className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
@@ -6,6 +6,7 @@ import {
6
6
  Archive,
7
7
  Circle,
8
8
  ExternalLink,
9
+ FileText,
9
10
  LayoutGrid,
10
11
  Loader2,
11
12
  Square,
@@ -31,6 +32,7 @@ import { formatDuration } from "@/lib/taskParsing";
31
32
  export type SessionListEntry = {
32
33
  sessionId: string;
33
34
  sessionName?: string;
35
+ sessionNote?: string;
34
36
  savedAt?: string;
35
37
  /** Horodatage immuable de création de la session ; repli : `startAt` pour les anciennes données. */
36
38
  createdAt?: string | null;
@@ -327,6 +329,12 @@ export function SessionListPanel({
327
329
  : null;
328
330
 
329
331
  const taskNoun = sessionTaskCountNoun(n, t, lang);
332
+ const noteRaw =
333
+ typeof sess.sessionNote === "string"
334
+ ? sess.sessionNote.trim()
335
+ : "";
336
+ const notePreview = noteRaw.replaceAll(/\s+/g, " ");
337
+ const hasSessionNote = notePreview.length > 0;
330
338
  const wallMins = sessionWallClockMinutes(sess as LooseSession);
331
339
  const durationLabel = wallMins > 0 ? formatDuration(wallMins) : "—";
332
340
  const thresholdMin =
@@ -440,6 +448,19 @@ export function SessionListPanel({
440
448
  {durationLabel}
441
449
  </span>
442
450
  </span>
451
+ {hasSessionNote ? (
452
+ <span
453
+ className="mt-0.5 inline-flex min-w-0 items-center gap-1 text-zinc-500 dark:text-zinc-400"
454
+ title={notePreview}
455
+ >
456
+ <FileText
457
+ size={12}
458
+ className="shrink-0"
459
+ aria-hidden
460
+ />
461
+ <span className="truncate">{notePreview}</span>
462
+ </span>
463
+ ) : null}
443
464
  </span>
444
465
  </button>
445
466
  <div className="col-start-2 row-start-2 flex shrink-0 items-center gap-0.5 self-center">