@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.
- package/README.md +28 -1
- package/app/api/action/route.ts +39 -3
- package/app/api/action-logs/route.ts +24 -0
- package/app/api/backup/route.ts +1 -1
- package/app/api/restore/route.ts +145 -0
- package/app/changelog/page.tsx +71 -4
- package/app/globals.css +127 -0
- package/app/guide/page.tsx +61 -15
- package/app/implementation/page.tsx +700 -0
- package/app/layout.tsx +14 -3
- package/app/licenses/page.tsx +99 -37
- package/app/logs/page.tsx +258 -0
- package/app/manifest.ts +5 -5
- package/app/page.tsx +784 -229
- package/app/reporting/page.tsx +1266 -474
- package/app/settings/page.tsx +252 -18
- package/bin/kronosys.mjs +140 -15
- package/components/KronosysPayloadProvider.tsx +2 -0
- package/components/RouteTransition.tsx +18 -0
- package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
- package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
- package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
- package/components/dashboard/AppShellRouteNav.tsx +323 -48
- package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
- package/components/dashboard/DashboardSimpleModal.tsx +168 -25
- package/components/dashboard/DashboardTour.tsx +115 -29
- package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
- package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
- package/components/dashboard/NewSessionScopeModal.tsx +211 -20
- package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
- package/components/dashboard/ReportingTour.tsx +87 -21
- package/components/dashboard/SavedProjectPicker.tsx +16 -3
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
- package/components/dashboard/SessionListPanel.tsx +327 -44
- package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
- package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
- package/components/dashboard/SettingsTour.tsx +86 -21
- package/components/dashboard/TagPills.tsx +14 -1
- package/components/dashboard/TaskFocusPanel.tsx +1081 -478
- package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
- package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
- package/components/dashboard/taskFieldStyles.ts +20 -4
- package/components/dashboard/useReportingInteractionState.ts +80 -0
- package/lib/appShellHeaderClasses.ts +13 -0
- package/lib/businessRulesMatrix.ts +210 -0
- package/lib/copyToClipboard.ts +43 -0
- package/lib/dashboardCopy.ts +494 -84
- package/lib/dashboardQuickSearch.ts +54 -2
- package/lib/dashboardTimeZone.ts +109 -0
- package/lib/formatAppShellWallClock.ts +66 -0
- package/lib/formatSessionNameTemplate.ts +141 -0
- package/lib/generatedUserChangelog.ts +177 -6
- package/lib/globalPausePreview.ts +292 -0
- package/lib/implementationNotes.ts +1188 -0
- package/lib/kronosysApi.ts +6 -0
- package/lib/kronosysDashboardModalGates.ts +24 -0
- package/lib/plannedBoundaryAttention.ts +9 -0
- package/lib/plannedBoundaryConflict.ts +23 -0
- package/lib/reportingAggregate.ts +517 -75
- package/lib/reportingMetricHelp.ts +8 -0
- package/lib/reportingStrings.ts +37 -3
- package/lib/sessionListMerge.ts +4 -0
- package/lib/sessionTaskSidebarStats.ts +182 -21
- package/lib/settingsCopy.ts +178 -4
- package/lib/taskParsing.ts +360 -103
- package/lib/taskTemplateDraft.ts +135 -0
- package/lib/taskTimelineGantt.ts +265 -0
- package/lib/temporalDisplayPlanned.ts +71 -0
- package/lib/userGuideCopy.ts +121 -47
- package/next.config.ts +7 -0
- package/package.json +12 -24
- package/server/actionDispatch.ts +1000 -77
- package/server/actionTaskSession.ts +337 -24
- package/server/db.ts +7 -15
- package/server/dbSchema.ts +24 -0
- package/server/defaultCfg.ts +5 -0
- package/server/gitlabTokenStore.ts +0 -12
- package/server/liveHistorySync.ts +53 -0
- package/server/mainTimerHydrate.ts +38 -2
- package/server/payloadStore.ts +33 -11
- package/server/sessionWallHydrate.ts +66 -3
- package/server/userActionLog.ts +126 -0
- package/sonar-project.properties +11 -0
- package/tsconfig.json +2 -1
- package/components/dashboard/IssuePickerModal.tsx +0 -168
- package/components/dashboard/ThemeToggle.test.tsx +0 -26
- package/lib/backupCsvExport.test.ts +0 -149
- package/lib/dashboardQuickSearchQuery.test.ts +0 -63
- package/lib/dataDir.test.ts +0 -87
- package/lib/formatIsoShort.test.ts +0 -46
- package/lib/kronoFocusRhythm.test.ts +0 -130
- package/lib/kronoFocusTimerUrgency.test.ts +0 -74
- package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
- package/lib/reportingAggregate.test.ts +0 -325
- package/lib/reportingNonFinalIndicators.test.ts +0 -157
- package/lib/reportingTagWeekBreakdown.test.ts +0 -141
- package/lib/reportingWeekLayout.test.ts +0 -239
- package/lib/sessionAssiduity.test.ts +0 -25
- package/lib/sessionEndWarnings.test.ts +0 -200
- package/lib/sessionListMerge.test.ts +0 -101
- package/lib/sessionTaskSidebarStats.test.ts +0 -24
- package/lib/taskParsing.test.ts +0 -153
- package/lib/usageProfile.test.ts +0 -84
- package/server/actionDispatch.test.ts +0 -723
- package/server/actionTaskSession.test.ts +0 -713
- package/server/kronoFocusHydrate.test.ts +0 -142
- package/server/kronoFocusMigrate.test.ts +0 -53
- package/server/mainTimerHydrate.test.ts +0 -65
- package/server/payloadStore.test.ts +0 -78
- 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 {
|
|
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 {
|
|
11
|
-
|
|
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<{
|
|
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(
|
|
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(
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
return raw.filter(
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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(
|
|
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">
|
|
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 ${
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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() !== ""
|
|
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={
|
|
339
|
+
title={
|
|
340
|
+
collapsed
|
|
341
|
+
? dt.appShellLiveDrawerExpand
|
|
342
|
+
: dt.appShellLiveDrawerCollapse
|
|
343
|
+
}
|
|
272
344
|
aria-expanded={collapsed === false}
|
|
273
|
-
aria-label={
|
|
345
|
+
aria-label={
|
|
346
|
+
collapsed
|
|
347
|
+
? dt.appShellLiveDrawerExpand
|
|
348
|
+
: dt.appShellLiveDrawerCollapse
|
|
349
|
+
}
|
|
274
350
|
>
|
|
275
351
|
{collapsed ? (
|
|
276
|
-
<ChevronLeft
|
|
352
|
+
<ChevronLeft
|
|
353
|
+
size={18}
|
|
354
|
+
strokeWidth={2}
|
|
355
|
+
className="shrink-0"
|
|
356
|
+
aria-hidden
|
|
357
|
+
/>
|
|
277
358
|
) : (
|
|
278
|
-
<ChevronRight
|
|
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
|
|
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">
|
|
336
|
-
|
|
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">
|
|
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">
|
|
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 ${
|
|
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">
|
|
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">
|
|
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(
|
|
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={
|
|
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
|
|
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>
|