@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.
- package/app/settings/page.tsx +20 -1
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +58 -8
- package/components/dashboard/SessionListPanel.tsx +21 -0
- package/components/dashboard/SettingsTagsProjectsSection.tsx +427 -34
- package/components/dashboard/TaskFocusPanel.tsx +29 -0
- package/components/dashboard/TaskNotesDisplay.test.tsx +110 -0
- package/components/dashboard/TaskSessionLiveCard.tsx +70 -26
- package/lib/dashboardCopy.ts +12 -0
- package/lib/generatedUserChangelog.ts +18 -0
- package/lib/implementationNotes.ts +58 -2
- package/lib/settingsCopy.ts +29 -4
- package/lib/userGuideCopy.ts +11 -5
- package/package.json +1 -1
- package/server/actionDispatch.test.ts +129 -0
- package/server/actionDispatch.ts +60 -6
- package/server/actionTaskSession.test.ts +10 -0
- package/server/actionTaskSession.ts +159 -1
- package/server/mainTimerHydrate.test.ts +29 -1
- package/server/mainTimerHydrate.ts +15 -0
- package/server/sessionWallHydrate.test.ts +18 -0
- package/server/sessionWallHydrate.ts +10 -0
package/app/settings/page.tsx
CHANGED
|
@@ -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
|
-
}, [
|
|
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
|
-
}, [
|
|
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
|
|
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">
|