@nightkatana/kronosys-app 1.0.0-beta.0
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 +81 -0
- package/app/api/action/route.ts +16 -0
- package/app/api/backup/route.ts +84 -0
- package/app/api/health/route.ts +22 -0
- package/app/api/state/route.ts +27 -0
- package/app/apple-icon.png +0 -0
- package/app/changelog/page.tsx +122 -0
- package/app/globals.css +210 -0
- package/app/guide/layout.tsx +11 -0
- package/app/guide/page.tsx +278 -0
- package/app/icon.png +0 -0
- package/app/layout.tsx +77 -0
- package/app/licenses/layout.tsx +11 -0
- package/app/licenses/page.tsx +246 -0
- package/app/manifest.ts +32 -0
- package/app/page.tsx +1610 -0
- package/app/reporting/page.tsx +2943 -0
- package/app/settings/layout.tsx +10 -0
- package/app/settings/page.tsx +3518 -0
- package/bin/kronosys.mjs +46 -0
- package/components/KronosysPackageVersionProvider.tsx +19 -0
- package/components/KronosysPayloadProvider.tsx +109 -0
- package/components/PwaRegister.tsx +25 -0
- package/components/SiteLegalFooter.tsx +21 -0
- package/components/ThemeProvider.tsx +78 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +394 -0
- package/components/dashboard/AppShellRouteNav.tsx +131 -0
- package/components/dashboard/AppVersionStamp.tsx +16 -0
- package/components/dashboard/DashboardCollapsibleSection.tsx +57 -0
- package/components/dashboard/DashboardColumnHintsBanner.tsx +159 -0
- package/components/dashboard/DashboardCommandCenter.tsx +470 -0
- package/components/dashboard/DashboardLangGateModal.tsx +118 -0
- package/components/dashboard/DashboardLoadingOverlay.tsx +42 -0
- package/components/dashboard/DashboardSimpleModal.tsx +337 -0
- package/components/dashboard/DashboardSuspenseFallback.tsx +52 -0
- package/components/dashboard/DashboardToastProvider.tsx +64 -0
- package/components/dashboard/DashboardTour.tsx +435 -0
- package/components/dashboard/DeferredDescriptionPopoverWrap.tsx +39 -0
- package/components/dashboard/DeleteSessionModal.tsx +130 -0
- package/components/dashboard/DescriptionTooltipPortaled.tsx +31 -0
- package/components/dashboard/GitIdentityQuickSetupModal.tsx +211 -0
- package/components/dashboard/HeaderIntegrationBadges.tsx +69 -0
- package/components/dashboard/InlineMetricHelpTrigger.tsx +102 -0
- package/components/dashboard/IssuePickerModal.tsx +168 -0
- package/components/dashboard/KronoFocusPanel.tsx +834 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +357 -0
- package/components/dashboard/KronosysTimePopoverField.tsx +233 -0
- package/components/dashboard/LanguageMenu.tsx +123 -0
- package/components/dashboard/MongoMirrorSyncLine.tsx +57 -0
- package/components/dashboard/NewSessionScopeModal.tsx +410 -0
- package/components/dashboard/PageRefreshButton.tsx +130 -0
- package/components/dashboard/PlainHelpPopover.tsx +97 -0
- package/components/dashboard/ReportingPageToc.tsx +68 -0
- package/components/dashboard/ReportingTour.tsx +342 -0
- package/components/dashboard/SavedProjectPicker.tsx +92 -0
- package/components/dashboard/SavedTagPicker.tsx +115 -0
- package/components/dashboard/ScrollToTopFab.tsx +41 -0
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +630 -0
- package/components/dashboard/SessionEndReasonEditor.tsx +114 -0
- package/components/dashboard/SessionListPanel.tsx +320 -0
- package/components/dashboard/SessionLocMetricsSection.tsx +128 -0
- package/components/dashboard/SettingsTagsProjectsSection.tsx +993 -0
- package/components/dashboard/SettingsTour.tsx +332 -0
- package/components/dashboard/TagPills.tsx +149 -0
- package/components/dashboard/TagsHelpTrigger.tsx +84 -0
- package/components/dashboard/TaskFocusPanel.tsx +1261 -0
- package/components/dashboard/TaskSessionLiveCard.tsx +832 -0
- package/components/dashboard/TaskSubtasksBlock.tsx +748 -0
- package/components/dashboard/ThemeToggle.test.tsx +26 -0
- package/components/dashboard/ThemeToggle.tsx +36 -0
- package/components/dashboard/UserGuideBodyText.tsx +62 -0
- package/components/dashboard/WorkspaceGitRepoCard.tsx +191 -0
- package/components/dashboard/taskFieldStyles.ts +139 -0
- package/components/dashboard/useAnchoredFloatingPortalStyle.ts +71 -0
- package/components/dashboard/useDescriptionPopoverAfterMs.ts +220 -0
- package/components/dashboard/useKronoFocusLiveSeconds.ts +36 -0
- package/components/dashboard/useSmoothStopwatchMs.ts +25 -0
- package/lib/appShellHeaderClasses.ts +12 -0
- package/lib/backupCsvExport.test.ts +149 -0
- package/lib/backupCsvExport.ts +392 -0
- package/lib/changelogCopy.ts +34 -0
- package/lib/concurrentTaskStartPreference.ts +29 -0
- package/lib/dashboardClockFormat.ts +13 -0
- package/lib/dashboardColumnChrome.ts +3 -0
- package/lib/dashboardColumnHintsStorage.ts +57 -0
- package/lib/dashboardCopy.ts +1831 -0
- package/lib/dashboardDetachedUrlHintStorage.ts +24 -0
- package/lib/dashboardGitIdentityBannerStorage.ts +36 -0
- package/lib/dashboardLangStorage.ts +72 -0
- package/lib/dashboardQuickSearch.ts +476 -0
- package/lib/dashboardQuickSearchQuery.test.ts +63 -0
- package/lib/dashboardQuickSearchQuery.ts +179 -0
- package/lib/dashboardSessionNav.ts +33 -0
- package/lib/dashboardShortcuts.ts +268 -0
- package/lib/dashboardTimeZone.ts +91 -0
- package/lib/dashboardTourStorage.ts +68 -0
- package/lib/dataDir.test.ts +87 -0
- package/lib/dataDir.ts +83 -0
- package/lib/devDataPreferenceFile.ts +55 -0
- package/lib/devDataRuntimeInfo.ts +34 -0
- package/lib/formatIsoShort.test.ts +46 -0
- package/lib/formatIsoShort.ts +29 -0
- package/lib/generatedUserChangelog.ts +34 -0
- package/lib/gitlabIssueSearch.ts +8 -0
- package/lib/kronoFocusDurationHistory.ts +71 -0
- package/lib/kronoFocusRhythm.test.ts +130 -0
- package/lib/kronoFocusRhythm.ts +46 -0
- package/lib/kronoFocusTimerUrgency.test.ts +74 -0
- package/lib/kronoFocusTimerUrgency.ts +24 -0
- package/lib/kronosysApi.ts +143 -0
- package/lib/legacyEditorPayloadKeys.ts +52 -0
- package/lib/legacyKronoFocusStorageKeys.test.ts +29 -0
- package/lib/legacyKronoFocusStorageKeys.ts +32 -0
- package/lib/licensesCopy.ts +128 -0
- package/lib/openPlainTextInNewTab.ts +49 -0
- package/lib/readKronosysPackageVersion.ts +10 -0
- package/lib/reportingAggregate.test.ts +325 -0
- package/lib/reportingAggregate.ts +819 -0
- package/lib/reportingDatePresets.ts +41 -0
- package/lib/reportingMetricHelp.ts +430 -0
- package/lib/reportingNonFinalIndicators.test.ts +157 -0
- package/lib/reportingNonFinalIndicators.ts +102 -0
- package/lib/reportingStrings.ts +491 -0
- package/lib/reportingTagWeekBreakdown.test.ts +141 -0
- package/lib/reportingTagWeekBreakdown.ts +181 -0
- package/lib/reportingWeekLayout.test.ts +239 -0
- package/lib/reportingWeekLayout.ts +313 -0
- package/lib/sessionAssiduity.test.ts +25 -0
- package/lib/sessionAssiduity.ts +33 -0
- package/lib/sessionEndReason.ts +55 -0
- package/lib/sessionEndWarnings.test.ts +200 -0
- package/lib/sessionEndWarnings.ts +125 -0
- package/lib/sessionListMerge.test.ts +101 -0
- package/lib/sessionListMerge.ts +70 -0
- package/lib/sessionTaskSidebarStats.test.ts +24 -0
- package/lib/sessionTaskSidebarStats.ts +54 -0
- package/lib/settingsCopy.ts +1276 -0
- package/lib/taskParsing.test.ts +153 -0
- package/lib/taskParsing.ts +737 -0
- package/lib/theme.ts +15 -0
- package/lib/translucentButtonClasses.ts +34 -0
- package/lib/usageProfile.test.ts +84 -0
- package/lib/usageProfile.ts +52 -0
- package/lib/userGuideCopy.ts +464 -0
- package/lib/workspaceLocDefaults.ts +21 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +15 -0
- package/package.json +87 -0
- package/postcss.config.mjs +12 -0
- package/public/apple-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.png +0 -0
- package/public/next.svg +1 -0
- package/public/sw.js +13 -0
- package/public/traceback.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/actionDispatch.test.ts +723 -0
- package/server/actionDispatch.ts +1476 -0
- package/server/actionTaskSession.test.ts +713 -0
- package/server/actionTaskSession.ts +717 -0
- package/server/db.ts +42 -0
- package/server/defaultCfg.ts +87 -0
- package/server/gitlabTokenStore.ts +34 -0
- package/server/kronoFocusHydrate.test.ts +142 -0
- package/server/kronoFocusHydrate.ts +69 -0
- package/server/kronoFocusMigrate.test.ts +53 -0
- package/server/kronoFocusMigrate.ts +78 -0
- package/server/mainTimerHydrate.test.ts +65 -0
- package/server/mainTimerHydrate.ts +53 -0
- package/server/payloadStore.test.ts +78 -0
- package/server/payloadStore.ts +83 -0
- package/server/sessionWallHydrate.test.ts +46 -0
- package/server/sessionWallHydrate.ts +88 -0
- package/tsconfig.json +41 -0
package/app/page.tsx
ADDED
|
@@ -0,0 +1,1610 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Suspense,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
13
|
+
import { Clock, FolderOpen, Globe, Plus, User, X } from "lucide-react";
|
|
14
|
+
import { HeaderIntegrationBadges } from "@/components/dashboard/HeaderIntegrationBadges";
|
|
15
|
+
import {
|
|
16
|
+
postKronosysAction,
|
|
17
|
+
type GitRepoStatisticsPayload,
|
|
18
|
+
type KronosysUpdatePayload,
|
|
19
|
+
type WorkspaceCodeSnapshotPayload,
|
|
20
|
+
} from "@/lib/kronosysApi";
|
|
21
|
+
import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
|
|
22
|
+
import { appShellHeaderClassName } from "@/lib/appShellHeaderClasses";
|
|
23
|
+
import { dashboardColumnTitleRowClassName } from "@/lib/dashboardColumnChrome";
|
|
24
|
+
import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
|
|
25
|
+
import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
|
|
26
|
+
import {
|
|
27
|
+
readDashboardLangUserChosen,
|
|
28
|
+
readStoredDashboardLang,
|
|
29
|
+
writeDashboardLangUserChosen,
|
|
30
|
+
writeStoredDashboardLang,
|
|
31
|
+
} from "@/lib/dashboardLangStorage";
|
|
32
|
+
import {
|
|
33
|
+
readGitIdentityBannerDismissed,
|
|
34
|
+
writeGitIdentityBannerDismissed,
|
|
35
|
+
} from "@/lib/dashboardGitIdentityBannerStorage";
|
|
36
|
+
import {
|
|
37
|
+
readDashboardDetachedUrlHintDismissed,
|
|
38
|
+
writeDashboardDetachedUrlHintDismissed,
|
|
39
|
+
} from "@/lib/dashboardDetachedUrlHintStorage";
|
|
40
|
+
import { settingsCopy } from "@/lib/settingsCopy";
|
|
41
|
+
import { reportingNav } from "@/lib/reportingStrings";
|
|
42
|
+
import {
|
|
43
|
+
pathnameWithUpdatedSessionQuery,
|
|
44
|
+
withDashboardSessionParam,
|
|
45
|
+
} from "@/lib/dashboardSessionNav";
|
|
46
|
+
import type { EndLiveSessionWarningFlags } from "@/lib/sessionEndWarnings";
|
|
47
|
+
import {
|
|
48
|
+
formatEndLiveSessionModalIntro,
|
|
49
|
+
getEndLiveSessionWarningFlags,
|
|
50
|
+
} from "@/lib/sessionEndWarnings";
|
|
51
|
+
import { KronoFocusPanel } from "@/components/dashboard/KronoFocusPanel";
|
|
52
|
+
import { TaskFocusPanel } from "@/components/dashboard/TaskFocusPanel";
|
|
53
|
+
import { DeleteSessionModal } from "@/components/dashboard/DeleteSessionModal";
|
|
54
|
+
import {
|
|
55
|
+
DashboardAlertModal,
|
|
56
|
+
DashboardConfirmModal,
|
|
57
|
+
} from "@/components/dashboard/DashboardSimpleModal";
|
|
58
|
+
import {
|
|
59
|
+
SessionListPanel,
|
|
60
|
+
type SessionListEntry,
|
|
61
|
+
} from "@/components/dashboard/SessionListPanel";
|
|
62
|
+
import { SettingsTagsProjectsSection } from "@/components/dashboard/SettingsTagsProjectsSection";
|
|
63
|
+
import { InlineMetricHelpTrigger } from "@/components/dashboard/InlineMetricHelpTrigger";
|
|
64
|
+
import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
|
|
65
|
+
import { SelectedSessionSidebarBlock } from "@/components/dashboard/SelectedSessionSidebarBlock";
|
|
66
|
+
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
67
|
+
import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
|
|
68
|
+
import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
|
|
69
|
+
import { DashboardColumnHintsBanner } from "@/components/dashboard/DashboardColumnHintsBanner";
|
|
70
|
+
import {
|
|
71
|
+
DashboardCommandCenter,
|
|
72
|
+
type DashboardCommandHandlers,
|
|
73
|
+
} from "@/components/dashboard/DashboardCommandCenter";
|
|
74
|
+
import { DashboardLoadingOverlay } from "@/components/dashboard/DashboardLoadingOverlay";
|
|
75
|
+
import { DashboardSuspenseFallback } from "@/components/dashboard/DashboardSuspenseFallback";
|
|
76
|
+
import { DashboardLangGateModal } from "@/components/dashboard/DashboardLangGateModal";
|
|
77
|
+
import { GitIdentityQuickSetupModal } from "@/components/dashboard/GitIdentityQuickSetupModal";
|
|
78
|
+
import { DashboardTour } from "@/components/dashboard/DashboardTour";
|
|
79
|
+
import { isDashboardTourCompleted } from "@/lib/dashboardTourStorage";
|
|
80
|
+
import { workspaceFolderPathStrings } from "@/lib/legacyEditorPayloadKeys";
|
|
81
|
+
import { mergeLiveSessionIntoHistory } from "@/lib/sessionListMerge";
|
|
82
|
+
import {
|
|
83
|
+
showIdeLinkedCodeTimingMetrics,
|
|
84
|
+
showWorkspaceFoldersEmptyMessage,
|
|
85
|
+
trackCodeMetricsFromCfg,
|
|
86
|
+
} from "@/lib/usageProfile";
|
|
87
|
+
import { buildDashboardQuickSearchItems } from "@/lib/dashboardQuickSearch";
|
|
88
|
+
import { readDashboardUse24HourClockFromCfg } from "@/lib/dashboardClockFormat";
|
|
89
|
+
import { readDashboardTimeZoneFromCfg } from "@/lib/dashboardTimeZone";
|
|
90
|
+
import { NewSessionScopeModal } from "@/components/dashboard/NewSessionScopeModal";
|
|
91
|
+
import { useKronosysPackageVersion } from "@/components/KronosysPackageVersionProvider";
|
|
92
|
+
|
|
93
|
+
type LiveTaskShape = {
|
|
94
|
+
id: string;
|
|
95
|
+
name?: string;
|
|
96
|
+
isDone?: boolean;
|
|
97
|
+
manualTaskTimerPaused?: boolean;
|
|
98
|
+
subtasks?: Array<{ done?: boolean }>;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type LiveShape = {
|
|
102
|
+
sessionId?: string;
|
|
103
|
+
sessionName?: string;
|
|
104
|
+
archived?: boolean;
|
|
105
|
+
endAt?: string | null;
|
|
106
|
+
sessionEndReasonKind?: string;
|
|
107
|
+
sessionEndReasonNote?: string;
|
|
108
|
+
/** Avertissement lié au périmètre de session (max horloge, calendrier, etc.). */
|
|
109
|
+
sessionScopeNotice?: { kind: "alert" | "info"; message: string } | null;
|
|
110
|
+
/** Session en pause (collecte / minuteurs). */
|
|
111
|
+
isPaused?: boolean;
|
|
112
|
+
/** Horodatage immuable de création de la session (ISO 8601). */
|
|
113
|
+
createdAt?: string | null;
|
|
114
|
+
/** Instant de création / début officiel de la session (ISO 8601). */
|
|
115
|
+
startAt?: string | null;
|
|
116
|
+
/** Temps écoulé (horloge murale) depuis le premier événement tracé dans la session. */
|
|
117
|
+
sessionDurationMinutes?: number;
|
|
118
|
+
codingMinutesSession?: number;
|
|
119
|
+
activeMinutes?: number;
|
|
120
|
+
totalEvents?: number;
|
|
121
|
+
language?: string;
|
|
122
|
+
activeTasks?: LiveTaskShape[];
|
|
123
|
+
activeTask?: LiveTaskShape | null;
|
|
124
|
+
tasks?: LiveTaskShape[];
|
|
125
|
+
kronoFocus?: {
|
|
126
|
+
mode: "work" | "break" | "longBreak";
|
|
127
|
+
status: "idle" | "running" | "paused";
|
|
128
|
+
timeLeftSeconds: number;
|
|
129
|
+
/** Horodatage fin de phase (ms) — décompte côté API serveur. */
|
|
130
|
+
kronoFocusDeadlineAtMs?: number;
|
|
131
|
+
workDurationSeconds?: number;
|
|
132
|
+
shortBreakDurationSeconds?: number;
|
|
133
|
+
longBreakDurationSeconds?: number;
|
|
134
|
+
linkedTaskId?: string;
|
|
135
|
+
linkedTaskName?: string;
|
|
136
|
+
};
|
|
137
|
+
linesWrittenTotal?: number;
|
|
138
|
+
linesWrittenHuman?: number;
|
|
139
|
+
linesWrittenAi?: number;
|
|
140
|
+
locByLanguage?: Array<[string, number]>;
|
|
141
|
+
codingSignalsByLanguage?: Array<[string, number]>;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
type UrlSessionResolution =
|
|
145
|
+
| { mode: "none" }
|
|
146
|
+
| { mode: "loading" }
|
|
147
|
+
| { mode: "ok"; id: string }
|
|
148
|
+
| { mode: "invalid" };
|
|
149
|
+
|
|
150
|
+
function resolveUrlSession(
|
|
151
|
+
urlParam: string | null,
|
|
152
|
+
payload: KronosysUpdatePayload | null,
|
|
153
|
+
live: LiveShape | undefined,
|
|
154
|
+
history: SessionListEntry[],
|
|
155
|
+
historyArchived: SessionListEntry[],
|
|
156
|
+
): UrlSessionResolution {
|
|
157
|
+
if (!urlParam) {
|
|
158
|
+
return { mode: "none" };
|
|
159
|
+
}
|
|
160
|
+
if (!payload) {
|
|
161
|
+
return { mode: "loading" };
|
|
162
|
+
}
|
|
163
|
+
const liveSid =
|
|
164
|
+
typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
|
|
165
|
+
if (
|
|
166
|
+
(liveSid !== "" && urlParam === liveSid) ||
|
|
167
|
+
history.some((s) => s.sessionId === urlParam) ||
|
|
168
|
+
historyArchived.some((s) => s.sessionId === urlParam)
|
|
169
|
+
) {
|
|
170
|
+
return { mode: "ok", id: urlParam };
|
|
171
|
+
}
|
|
172
|
+
return { mode: "invalid" };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function DashboardHome() {
|
|
176
|
+
const searchParams = useSearchParams();
|
|
177
|
+
const pathname = usePathname();
|
|
178
|
+
const router = useRouter();
|
|
179
|
+
const urlParam = searchParams.get("session");
|
|
180
|
+
const { payload, error, refresh } = useKronosysPayload();
|
|
181
|
+
const [sessionName, setSessionName] = useState("");
|
|
182
|
+
/** Évite d’écraser la saisie à chaque poll API (refresh toutes les 2 s). */
|
|
183
|
+
const sessionNameFieldActiveRef = useRef(false);
|
|
184
|
+
const sessionNameInputRef = useRef<HTMLInputElement>(null);
|
|
185
|
+
/** Session à répercuter sur les `router.push` (command center) ; lu au moment du clic. */
|
|
186
|
+
const dashboardNavSessionRef = useRef<string | undefined>(undefined);
|
|
187
|
+
const [sessionNameRowFocused, setSessionNameRowFocused] = useState(false);
|
|
188
|
+
const liveSessionIdSyncedRef = useRef<string | undefined>(undefined);
|
|
189
|
+
const viewedSessionForNameRef = useRef<string | undefined>(undefined);
|
|
190
|
+
const [deleteSessionId, setDeleteSessionId] = useState<string | null>(null);
|
|
191
|
+
const [mongoPushBusyId, setMongoPushBusyId] = useState<string | null>(null);
|
|
192
|
+
const [homeDialogAlert, setHomeDialogAlert] = useState<string | null>(null);
|
|
193
|
+
const [archiveConfirmSessionId, setArchiveConfirmSessionId] = useState<
|
|
194
|
+
string | null
|
|
195
|
+
>(null);
|
|
196
|
+
const [archiveDismissChecked, setArchiveDismissChecked] = useState(false);
|
|
197
|
+
const [endLiveSessionConfirmFlags, setEndLiveSessionConfirmFlags] =
|
|
198
|
+
useState<EndLiveSessionWarningFlags | null>(null);
|
|
199
|
+
const [endSessionReasonKind, setEndSessionReasonKind] = useState<string>("");
|
|
200
|
+
const [endSessionReasonNote, setEndSessionReasonNote] = useState("");
|
|
201
|
+
const [tourOpen, setTourOpen] = useState(false);
|
|
202
|
+
const [newSessionModalOpen, setNewSessionModalOpen] = useState(false);
|
|
203
|
+
const [gitBannerDismissed, setGitBannerDismissed] = useState(false);
|
|
204
|
+
const [gitIdentitySetupModalOpen, setGitIdentitySetupModalOpen] =
|
|
205
|
+
useState(false);
|
|
206
|
+
const [detachedUrlHintDismissed, setDetachedUrlHintDismissed] =
|
|
207
|
+
useState(false);
|
|
208
|
+
const [updateChangelogModalOpen, setUpdateChangelogModalOpen] =
|
|
209
|
+
useState(false);
|
|
210
|
+
const packageVersion = useKronosysPackageVersion();
|
|
211
|
+
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
setGitBannerDismissed(readGitIdentityBannerDismissed());
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
setDetachedUrlHintDismissed(readDashboardDetachedUrlHintDismissed());
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (typeof globalThis.window === "undefined") {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const key = "kronosys_last_seen_package_version_v1";
|
|
225
|
+
try {
|
|
226
|
+
const previous = globalThis.localStorage.getItem(key);
|
|
227
|
+
if (!previous) {
|
|
228
|
+
globalThis.localStorage.setItem(key, packageVersion);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (previous !== packageVersion) {
|
|
232
|
+
setUpdateChangelogModalOpen(true);
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// ignore localStorage unavailability
|
|
236
|
+
}
|
|
237
|
+
}, [packageVersion]);
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (archiveConfirmSessionId !== null) {
|
|
241
|
+
setArchiveDismissChecked(false);
|
|
242
|
+
}
|
|
243
|
+
}, [archiveConfirmSessionId]);
|
|
244
|
+
|
|
245
|
+
const live = payload?.current as LiveShape | undefined;
|
|
246
|
+
const historyArchived = (payload?.historyArchived ||
|
|
247
|
+
[]) as SessionListEntry[];
|
|
248
|
+
const history = useMemo(() => {
|
|
249
|
+
if (!payload) {
|
|
250
|
+
return [] as SessionListEntry[];
|
|
251
|
+
}
|
|
252
|
+
const raw = (payload.history || []) as SessionListEntry[];
|
|
253
|
+
return mergeLiveSessionIntoHistory(raw, live);
|
|
254
|
+
}, [payload, live]);
|
|
255
|
+
const urlResolution = resolveUrlSession(
|
|
256
|
+
urlParam,
|
|
257
|
+
payload,
|
|
258
|
+
live,
|
|
259
|
+
history,
|
|
260
|
+
historyArchived,
|
|
261
|
+
);
|
|
262
|
+
const sessionQueryMode = Boolean(urlParam);
|
|
263
|
+
const isDetachedUrlTab = urlResolution.mode === "ok";
|
|
264
|
+
const liveSidForUrlHint =
|
|
265
|
+
typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
|
|
266
|
+
const urlSessionFocusId =
|
|
267
|
+
urlResolution.mode === "ok" ? urlResolution.id.trim() : "";
|
|
268
|
+
const detachedUrlHintExplainsArchive =
|
|
269
|
+
isDetachedUrlTab &&
|
|
270
|
+
urlSessionFocusId !== "" &&
|
|
271
|
+
urlSessionFocusId !== liveSidForUrlHint;
|
|
272
|
+
const showDetachedUrlHintBanner =
|
|
273
|
+
detachedUrlHintExplainsArchive && !detachedUrlHintDismissed;
|
|
274
|
+
|
|
275
|
+
const inspectingId = payload?.inspectingSessionId as
|
|
276
|
+
| string
|
|
277
|
+
| null
|
|
278
|
+
| undefined;
|
|
279
|
+
const columnArchiveId =
|
|
280
|
+
isDetachedUrlTab && urlResolution.id !== live?.sessionId
|
|
281
|
+
? urlResolution.id
|
|
282
|
+
: (inspectingId ?? null);
|
|
283
|
+
const viewingSession = columnArchiveId
|
|
284
|
+
? ((history.find((s) => s.sessionId === columnArchiveId) ||
|
|
285
|
+
historyArchived.find((s) => s.sessionId === columnArchiveId)) as
|
|
286
|
+
| LiveShape
|
|
287
|
+
| undefined)
|
|
288
|
+
: undefined;
|
|
289
|
+
const sessionCurrent = (viewingSession ?? live) as LiveShape | undefined;
|
|
290
|
+
const isInspecting = Boolean(columnArchiveId);
|
|
291
|
+
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!payload) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (sessionNameFieldActiveRef.current) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const liveCur = payload.current as LiveShape | undefined;
|
|
301
|
+
const mergedHistory = mergeLiveSessionIntoHistory(
|
|
302
|
+
(payload.history || []) as SessionListEntry[],
|
|
303
|
+
liveCur,
|
|
304
|
+
);
|
|
305
|
+
const histArch = (payload.historyArchived || []) as SessionListEntry[];
|
|
306
|
+
|
|
307
|
+
if (columnArchiveId) {
|
|
308
|
+
const vs =
|
|
309
|
+
mergedHistory.find((s) => s.sessionId === columnArchiveId) ||
|
|
310
|
+
histArch.find((s) => s.sessionId === columnArchiveId);
|
|
311
|
+
const serverName = vs?.sessionName;
|
|
312
|
+
if (columnArchiveId !== viewedSessionForNameRef.current) {
|
|
313
|
+
viewedSessionForNameRef.current = columnArchiveId;
|
|
314
|
+
sessionNameFieldActiveRef.current = false;
|
|
315
|
+
setSessionName(typeof serverName === "string" ? serverName : "");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (typeof serverName === "string") {
|
|
319
|
+
setSessionName(serverName);
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
viewedSessionForNameRef.current = undefined;
|
|
325
|
+
const sid = liveCur?.sessionId;
|
|
326
|
+
const serverName = liveCur?.sessionName;
|
|
327
|
+
|
|
328
|
+
if (sid !== liveSessionIdSyncedRef.current) {
|
|
329
|
+
liveSessionIdSyncedRef.current = sid;
|
|
330
|
+
sessionNameFieldActiveRef.current = false;
|
|
331
|
+
setSessionName(typeof serverName === "string" ? serverName : "");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (typeof serverName === "string") {
|
|
336
|
+
setSessionName(serverName);
|
|
337
|
+
}
|
|
338
|
+
}, [payload, columnArchiveId]);
|
|
339
|
+
|
|
340
|
+
const hasPayload = Boolean(payload);
|
|
341
|
+
const serverLang: Lang = live?.language === "fr" ? "fr" : "en";
|
|
342
|
+
/** Langue affichée : état local mis à jour par le sélecteur initial, le menu ou la synchro post-visite. */
|
|
343
|
+
const [uiLang, setUiLang] = useState<Lang>("en");
|
|
344
|
+
const [langGateOpen, setLangGateOpen] = useState(false);
|
|
345
|
+
|
|
346
|
+
useLayoutEffect(() => {
|
|
347
|
+
if (typeof window === "undefined") {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const userChosen = readDashboardLangUserChosen();
|
|
351
|
+
const tourDone = isDashboardTourCompleted();
|
|
352
|
+
if (!userChosen && !tourDone) {
|
|
353
|
+
setLangGateOpen(true);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const stored = readStoredDashboardLang();
|
|
357
|
+
if (stored) {
|
|
358
|
+
setUiLang(stored);
|
|
359
|
+
}
|
|
360
|
+
}, []);
|
|
361
|
+
|
|
362
|
+
const lang: Lang = uiLang;
|
|
363
|
+
const dt = dashboardStrings(lang);
|
|
364
|
+
const tagProjSettingsCopy = settingsCopy(lang);
|
|
365
|
+
const nav = reportingNav(lang);
|
|
366
|
+
const handleManualRefresh = useCallback(async () => {
|
|
367
|
+
return await refresh({ routerInvalidate: true });
|
|
368
|
+
}, [refresh]);
|
|
369
|
+
|
|
370
|
+
/** Pendant la barrière de langue, aligner l’arrière-plan sur la langue du navigateur sans écrire le stockage. */
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
if (!hasPayload || !langGateOpen) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
setUiLang(serverLang);
|
|
376
|
+
}, [hasPayload, serverLang, langGateOpen]);
|
|
377
|
+
|
|
378
|
+
/** Après la visite guidée, conserver la langue choisie pour le tableau de bord. */
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
if (!hasPayload) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!isDashboardTourCompleted()) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
writeStoredDashboardLang(serverLang);
|
|
387
|
+
setUiLang(serverLang);
|
|
388
|
+
}, [hasPayload, serverLang]);
|
|
389
|
+
|
|
390
|
+
const stripTourReplayParam = useCallback(() => {
|
|
391
|
+
if (searchParams.get("tour") !== "replay") {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const next = new URLSearchParams(searchParams.toString());
|
|
395
|
+
next.delete("tour");
|
|
396
|
+
const q = next.toString();
|
|
397
|
+
router.replace(q ? `${pathname}?${q}` : pathname);
|
|
398
|
+
}, [pathname, router, searchParams]);
|
|
399
|
+
|
|
400
|
+
useEffect(() => {
|
|
401
|
+
if (!hasPayload) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (langGateOpen) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (searchParams.get("tour") === "replay") {
|
|
408
|
+
setTourOpen(true);
|
|
409
|
+
stripTourReplayParam();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (!isDashboardTourCompleted()) {
|
|
413
|
+
setTourOpen(true);
|
|
414
|
+
}
|
|
415
|
+
}, [hasPayload, langGateOpen, searchParams, stripTourReplayParam]);
|
|
416
|
+
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
const sid =
|
|
419
|
+
typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
|
|
420
|
+
if (!sid || live?.archived === true) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const t = dashboardStrings(lang);
|
|
424
|
+
const onBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
425
|
+
e.preventDefault();
|
|
426
|
+
e.returnValue = t.beforeUnloadLiveSessionMessage;
|
|
427
|
+
};
|
|
428
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
429
|
+
return () => window.removeEventListener("beforeunload", onBeforeUnload);
|
|
430
|
+
}, [lang, live?.sessionId, live?.archived]);
|
|
431
|
+
|
|
432
|
+
const header =
|
|
433
|
+
lang === "fr"
|
|
434
|
+
? {
|
|
435
|
+
title: "Kronosys",
|
|
436
|
+
apiError:
|
|
437
|
+
"Impossible de charger l’état Kronosys (API /api/state). Vérifiez que le serveur Next tourne et les permissions du fichier SQLite.",
|
|
438
|
+
session: "Nom de la session",
|
|
439
|
+
sessionDuration: "Durée de session",
|
|
440
|
+
sessionStart: "Début de session",
|
|
441
|
+
coding: "Temps de codage",
|
|
442
|
+
active: "Temps actif",
|
|
443
|
+
tasks: "Tâches",
|
|
444
|
+
newSession: "Nouvelle session",
|
|
445
|
+
en: "English",
|
|
446
|
+
fr: "Français",
|
|
447
|
+
}
|
|
448
|
+
: {
|
|
449
|
+
title: "Kronosys",
|
|
450
|
+
apiError:
|
|
451
|
+
"Cannot load Kronosys state (/api/state). Ensure the Next server is running and the SQLite data directory is writable.",
|
|
452
|
+
session: "Session name",
|
|
453
|
+
sessionDuration: "Session duration",
|
|
454
|
+
sessionStart: "Session start",
|
|
455
|
+
coding: "Coding time",
|
|
456
|
+
active: "Active time",
|
|
457
|
+
tasks: "Tasks",
|
|
458
|
+
newSession: "New session",
|
|
459
|
+
en: "English",
|
|
460
|
+
fr: "Français",
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const post = useCallback(
|
|
464
|
+
async (body: Record<string, unknown>) => {
|
|
465
|
+
const res = await postKronosysAction(body);
|
|
466
|
+
await refresh();
|
|
467
|
+
const op = res.result?.sessionOp;
|
|
468
|
+
if (op && !op.ok) {
|
|
469
|
+
if (op.message) {
|
|
470
|
+
setHomeDialogAlert(op.message);
|
|
471
|
+
}
|
|
472
|
+
if (typeof op.revertedName === "string") {
|
|
473
|
+
setSessionName(op.revertedName);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return res;
|
|
477
|
+
},
|
|
478
|
+
[refresh],
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const completeLangGate = useCallback(
|
|
482
|
+
(next: Lang) => {
|
|
483
|
+
writeStoredDashboardLang(next);
|
|
484
|
+
writeDashboardLangUserChosen();
|
|
485
|
+
setUiLang(next);
|
|
486
|
+
setLangGateOpen(false);
|
|
487
|
+
void post({ type: "setLanguage", lang: next });
|
|
488
|
+
},
|
|
489
|
+
[post],
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const handlePushSessionToMongo = useCallback(
|
|
493
|
+
async (sessionId: string) => {
|
|
494
|
+
setMongoPushBusyId(sessionId);
|
|
495
|
+
try {
|
|
496
|
+
const res = await postKronosysAction({
|
|
497
|
+
type: "pushSessionToMongo",
|
|
498
|
+
sessionId,
|
|
499
|
+
});
|
|
500
|
+
await refresh();
|
|
501
|
+
const p = res.result?.pushSessionToMongo;
|
|
502
|
+
if (p && !p.ok) {
|
|
503
|
+
const msg =
|
|
504
|
+
p.error === "disabled"
|
|
505
|
+
? dt.sessionMongoPushFailedDisabled
|
|
506
|
+
: p.error === "not_found"
|
|
507
|
+
? dt.sessionMongoPushFailedNotFound
|
|
508
|
+
: p.error === "uri_incomplete"
|
|
509
|
+
? dt.sessionMongoPushFailedUri
|
|
510
|
+
: dt.sessionMongoPushFailedMongo;
|
|
511
|
+
setHomeDialogAlert(msg);
|
|
512
|
+
}
|
|
513
|
+
} catch {
|
|
514
|
+
setHomeDialogAlert(dt.sessionMongoPushFailedMongo);
|
|
515
|
+
} finally {
|
|
516
|
+
setMongoPushBusyId(null);
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
[dt, refresh],
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const deleteMoveTargets =
|
|
523
|
+
deleteSessionId !== null
|
|
524
|
+
? history
|
|
525
|
+
.filter((s) => s.sessionId !== deleteSessionId)
|
|
526
|
+
.map((s) => ({
|
|
527
|
+
id: s.sessionId,
|
|
528
|
+
label: s.sessionName?.trim() || s.sessionId.slice(0, 8),
|
|
529
|
+
}))
|
|
530
|
+
: [];
|
|
531
|
+
|
|
532
|
+
const confirmArchiveSession = useCallback(
|
|
533
|
+
(sessionId: string) => {
|
|
534
|
+
if (payload?.dismissArchiveSessionConfirm === true) {
|
|
535
|
+
void post({ type: "archiveSession", sessionId, archived: true });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
setArchiveConfirmSessionId(sessionId);
|
|
539
|
+
},
|
|
540
|
+
[payload?.dismissArchiveSessionConfirm, post],
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const handleDeleteConfirm = useCallback(
|
|
544
|
+
async (opts: { moveTasksToSessionId?: string }) => {
|
|
545
|
+
if (!deleteSessionId) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
await post({
|
|
549
|
+
type: "deleteHistorySession",
|
|
550
|
+
sessionId: deleteSessionId,
|
|
551
|
+
moveTasksToSessionId: opts.moveTasksToSessionId,
|
|
552
|
+
});
|
|
553
|
+
setDeleteSessionId(null);
|
|
554
|
+
},
|
|
555
|
+
[deleteSessionId, post],
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
const postVoid = useCallback(
|
|
559
|
+
async (body: Record<string, unknown>) => {
|
|
560
|
+
await post(body);
|
|
561
|
+
},
|
|
562
|
+
[post],
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const handleRequestEndLiveSession = useCallback(() => {
|
|
566
|
+
const flags = getEndLiveSessionWarningFlags(live);
|
|
567
|
+
setEndSessionReasonKind("");
|
|
568
|
+
setEndSessionReasonNote("");
|
|
569
|
+
setEndLiveSessionConfirmFlags(flags);
|
|
570
|
+
}, [live]);
|
|
571
|
+
|
|
572
|
+
const canEndLiveSessionShortcut =
|
|
573
|
+
typeof live?.sessionId === "string" &&
|
|
574
|
+
live.sessionId.trim() !== "" &&
|
|
575
|
+
live.archived !== true;
|
|
576
|
+
|
|
577
|
+
const commandHandlers = useMemo(
|
|
578
|
+
(): DashboardCommandHandlers => ({
|
|
579
|
+
newSession: () => setNewSessionModalOpen(true),
|
|
580
|
+
refresh: () => void handleManualRefresh(),
|
|
581
|
+
openReporting: () =>
|
|
582
|
+
void router.push(
|
|
583
|
+
withDashboardSessionParam(
|
|
584
|
+
"/reporting",
|
|
585
|
+
dashboardNavSessionRef.current,
|
|
586
|
+
),
|
|
587
|
+
),
|
|
588
|
+
openSettings: () =>
|
|
589
|
+
void router.push(
|
|
590
|
+
withDashboardSessionParam(
|
|
591
|
+
"/settings",
|
|
592
|
+
dashboardNavSessionRef.current,
|
|
593
|
+
),
|
|
594
|
+
),
|
|
595
|
+
openUserGuide: () =>
|
|
596
|
+
void router.push(
|
|
597
|
+
withDashboardSessionParam("/guide", dashboardNavSessionRef.current),
|
|
598
|
+
),
|
|
599
|
+
focusSessions: () =>
|
|
600
|
+
document.getElementById("dashboard-col-sessions")?.scrollIntoView({
|
|
601
|
+
behavior: "smooth",
|
|
602
|
+
block: "start",
|
|
603
|
+
}),
|
|
604
|
+
focusTasks: () =>
|
|
605
|
+
document.getElementById("dashboard-col-tasks")?.scrollIntoView({
|
|
606
|
+
behavior: "smooth",
|
|
607
|
+
block: "start",
|
|
608
|
+
}),
|
|
609
|
+
focusTags: () =>
|
|
610
|
+
document.getElementById("dashboard-col-tags")?.scrollIntoView({
|
|
611
|
+
behavior: "smooth",
|
|
612
|
+
block: "start",
|
|
613
|
+
}),
|
|
614
|
+
toggleLang: () =>
|
|
615
|
+
void post({ type: "setLanguage", lang: lang === "fr" ? "en" : "fr" }),
|
|
616
|
+
endLiveSession: canEndLiveSessionShortcut
|
|
617
|
+
? () => void handleRequestEndLiveSession()
|
|
618
|
+
: undefined,
|
|
619
|
+
}),
|
|
620
|
+
[
|
|
621
|
+
canEndLiveSessionShortcut,
|
|
622
|
+
handleManualRefresh,
|
|
623
|
+
handleRequestEndLiveSession,
|
|
624
|
+
lang,
|
|
625
|
+
post,
|
|
626
|
+
router,
|
|
627
|
+
],
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
const cancelEndLiveSessionConfirm = useCallback(() => {
|
|
631
|
+
setEndLiveSessionConfirmFlags(null);
|
|
632
|
+
setEndSessionReasonKind("");
|
|
633
|
+
setEndSessionReasonNote("");
|
|
634
|
+
}, []);
|
|
635
|
+
|
|
636
|
+
const confirmEndLiveSession = useCallback(() => {
|
|
637
|
+
const kind = endSessionReasonKind;
|
|
638
|
+
const note = endSessionReasonNote.trim();
|
|
639
|
+
setEndLiveSessionConfirmFlags(null);
|
|
640
|
+
setEndSessionReasonKind("");
|
|
641
|
+
setEndSessionReasonNote("");
|
|
642
|
+
void post({
|
|
643
|
+
type: "endLiveSession",
|
|
644
|
+
...(kind === "planned" ||
|
|
645
|
+
kind === "early" ||
|
|
646
|
+
kind === "overrun" ||
|
|
647
|
+
kind === "other"
|
|
648
|
+
? { sessionEndReasonKind: kind }
|
|
649
|
+
: {}),
|
|
650
|
+
...(note.length > 0 ? { sessionEndReasonNote: note } : {}),
|
|
651
|
+
});
|
|
652
|
+
}, [post, endSessionReasonKind, endSessionReasonNote]);
|
|
653
|
+
|
|
654
|
+
const handleSelectSession = useCallback(
|
|
655
|
+
async (sessionId: string) => {
|
|
656
|
+
if (sessionQueryMode) {
|
|
657
|
+
const liveSid =
|
|
658
|
+
typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
|
|
659
|
+
const pickLive = sessionId === liveSid;
|
|
660
|
+
router.replace(
|
|
661
|
+
pathnameWithUpdatedSessionQuery(
|
|
662
|
+
pathname,
|
|
663
|
+
searchParams.toString(),
|
|
664
|
+
pickLive ? null : sessionId,
|
|
665
|
+
),
|
|
666
|
+
);
|
|
667
|
+
if (pickLive) {
|
|
668
|
+
await postKronosysAction({ type: "inspectSession", sessionId: null });
|
|
669
|
+
await postKronosysAction({ type: "setPaused", paused: false });
|
|
670
|
+
}
|
|
671
|
+
await refresh();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (sessionId === live?.sessionId) {
|
|
675
|
+
await postKronosysAction({ type: "inspectSession", sessionId: null });
|
|
676
|
+
await postKronosysAction({ type: "setPaused", paused: false });
|
|
677
|
+
} else {
|
|
678
|
+
await postKronosysAction({ type: "inspectSession", sessionId });
|
|
679
|
+
}
|
|
680
|
+
await refresh();
|
|
681
|
+
},
|
|
682
|
+
[
|
|
683
|
+
live?.sessionId,
|
|
684
|
+
pathname,
|
|
685
|
+
refresh,
|
|
686
|
+
router,
|
|
687
|
+
searchParams,
|
|
688
|
+
sessionQueryMode,
|
|
689
|
+
],
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
const scrollToSessionInList = useCallback((sessionId: string) => {
|
|
693
|
+
document.getElementById("dashboard-col-sessions")?.scrollIntoView({
|
|
694
|
+
behavior: "smooth",
|
|
695
|
+
block: "start",
|
|
696
|
+
});
|
|
697
|
+
globalThis.requestAnimationFrame(() => {
|
|
698
|
+
document.getElementById(`kronosys-session-${sessionId}`)?.scrollIntoView({
|
|
699
|
+
behavior: "smooth",
|
|
700
|
+
block: "nearest",
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
}, []);
|
|
704
|
+
|
|
705
|
+
const focusTasksColumnForSearch = useCallback(() => {
|
|
706
|
+
document.getElementById("dashboard-col-tasks")?.scrollIntoView({
|
|
707
|
+
behavior: "smooth",
|
|
708
|
+
block: "start",
|
|
709
|
+
});
|
|
710
|
+
}, []);
|
|
711
|
+
|
|
712
|
+
const scrollToTaskInPanel = useCallback((taskId: string) => {
|
|
713
|
+
const el =
|
|
714
|
+
document.getElementById(`kronosys-active-task-${taskId}`) ??
|
|
715
|
+
document.getElementById(`kronosys-task-${taskId}`);
|
|
716
|
+
el?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
717
|
+
}, []);
|
|
718
|
+
|
|
719
|
+
const sessionDurationAlertThresholdMinutes = useMemo(() => {
|
|
720
|
+
const raw = (payload?.cfg as Record<string, unknown> | undefined)
|
|
721
|
+
?.dashboardSessionDurationAlertHours;
|
|
722
|
+
const h = typeof raw === "number" && Number.isFinite(raw) ? raw : 24;
|
|
723
|
+
const clamped = Math.min(Math.max(h, 1), 8760);
|
|
724
|
+
return Math.round(clamped * 60);
|
|
725
|
+
}, [payload?.cfg]);
|
|
726
|
+
|
|
727
|
+
const dashboardShowKronoFocusInHeader = useMemo(() => {
|
|
728
|
+
const raw = (payload?.cfg as Record<string, unknown> | undefined)
|
|
729
|
+
?.dashboardShowKronoFocusInHeader;
|
|
730
|
+
return raw !== false;
|
|
731
|
+
}, [payload?.cfg]);
|
|
732
|
+
|
|
733
|
+
const dashboardShowKronoFocusInTaskOps = useMemo(() => {
|
|
734
|
+
const raw = (payload?.cfg as Record<string, unknown> | undefined)
|
|
735
|
+
?.dashboardShowKronoFocusInTaskOps;
|
|
736
|
+
return raw !== false;
|
|
737
|
+
}, [payload?.cfg]);
|
|
738
|
+
|
|
739
|
+
const dashboardAllowTaskStartTimeEdit = useMemo(() => {
|
|
740
|
+
const raw = (payload?.cfg as Record<string, unknown> | undefined)
|
|
741
|
+
?.dashboardAllowTaskStartTimeEdit;
|
|
742
|
+
return raw !== false;
|
|
743
|
+
}, [payload?.cfg]);
|
|
744
|
+
|
|
745
|
+
const dashboardAllowSessionStartTimeEdit = useMemo(() => {
|
|
746
|
+
const raw = (payload?.cfg as Record<string, unknown> | undefined)
|
|
747
|
+
?.dashboardAllowSessionStartTimeEdit;
|
|
748
|
+
return raw !== false;
|
|
749
|
+
}, [payload?.cfg]);
|
|
750
|
+
|
|
751
|
+
const dashboardAllowTaskEndTimeEdit = useMemo(() => {
|
|
752
|
+
const raw = (payload?.cfg as Record<string, unknown> | undefined)
|
|
753
|
+
?.dashboardAllowTaskEndTimeEdit;
|
|
754
|
+
return raw !== false;
|
|
755
|
+
}, [payload?.cfg]);
|
|
756
|
+
|
|
757
|
+
const dashboardAllowSessionEndTimeEdit = useMemo(() => {
|
|
758
|
+
const raw = (payload?.cfg as Record<string, unknown> | undefined)
|
|
759
|
+
?.dashboardAllowSessionEndTimeEdit;
|
|
760
|
+
return raw !== false;
|
|
761
|
+
}, [payload?.cfg]);
|
|
762
|
+
|
|
763
|
+
const dashboardDisplayTimeZone = useMemo(
|
|
764
|
+
() => readDashboardTimeZoneFromCfg(payload?.cfg as Record<string, unknown> | undefined),
|
|
765
|
+
[payload?.cfg]
|
|
766
|
+
);
|
|
767
|
+
const dashboardUse24HourClock = useMemo(
|
|
768
|
+
() => readDashboardUse24HourClockFromCfg(payload?.cfg as Record<string, unknown> | undefined),
|
|
769
|
+
[payload?.cfg]
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
const quickSearchItems = useMemo(
|
|
773
|
+
() =>
|
|
774
|
+
buildDashboardQuickSearchItems({
|
|
775
|
+
history,
|
|
776
|
+
historyArchived,
|
|
777
|
+
sessionCurrent: sessionCurrent as
|
|
778
|
+
| Record<string, unknown>
|
|
779
|
+
| null
|
|
780
|
+
| undefined,
|
|
781
|
+
dt,
|
|
782
|
+
liveSearchContext:
|
|
783
|
+
live &&
|
|
784
|
+
typeof live.sessionId === "string" &&
|
|
785
|
+
live.sessionId.trim() !== ""
|
|
786
|
+
? {
|
|
787
|
+
sessionId: live.sessionId.trim(),
|
|
788
|
+
sessionPaused: live.isPaused === true,
|
|
789
|
+
}
|
|
790
|
+
: null,
|
|
791
|
+
onSelectSession: (id) => {
|
|
792
|
+
void handleSelectSession(id);
|
|
793
|
+
},
|
|
794
|
+
scrollToSession: scrollToSessionInList,
|
|
795
|
+
focusTasksColumn: focusTasksColumnForSearch,
|
|
796
|
+
scrollToTask: scrollToTaskInPanel,
|
|
797
|
+
}),
|
|
798
|
+
[
|
|
799
|
+
dt,
|
|
800
|
+
focusTasksColumnForSearch,
|
|
801
|
+
handleSelectSession,
|
|
802
|
+
history,
|
|
803
|
+
historyArchived,
|
|
804
|
+
live,
|
|
805
|
+
scrollToSessionInList,
|
|
806
|
+
scrollToTaskInPanel,
|
|
807
|
+
sessionCurrent,
|
|
808
|
+
],
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
const handleResumeActiveSession = useCallback(async () => {
|
|
812
|
+
if (sessionQueryMode) {
|
|
813
|
+
router.replace(
|
|
814
|
+
pathnameWithUpdatedSessionQuery(
|
|
815
|
+
pathname,
|
|
816
|
+
searchParams.toString(),
|
|
817
|
+
null,
|
|
818
|
+
),
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
await postKronosysAction({ type: "inspectSession", sessionId: null });
|
|
822
|
+
await postKronosysAction({ type: "setPaused", paused: false });
|
|
823
|
+
await refresh();
|
|
824
|
+
}, [pathname, refresh, router, searchParams, sessionQueryMode]);
|
|
825
|
+
|
|
826
|
+
const openSessionInNewTab = useCallback(
|
|
827
|
+
(sessionId: string) => {
|
|
828
|
+
const url = `${globalThis.location.origin}${pathname}?session=${encodeURIComponent(sessionId)}`;
|
|
829
|
+
globalThis.open(url, "_blank", "noopener,noreferrer");
|
|
830
|
+
},
|
|
831
|
+
[pathname],
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
const selectedSessionId =
|
|
835
|
+
urlResolution.mode === "ok"
|
|
836
|
+
? urlResolution.id
|
|
837
|
+
: urlResolution.mode === "loading" && urlParam
|
|
838
|
+
? urlParam
|
|
839
|
+
: urlResolution.mode === "invalid"
|
|
840
|
+
? live?.sessionId
|
|
841
|
+
: (inspectingId ?? live?.sessionId);
|
|
842
|
+
|
|
843
|
+
const trimmedSelectedSessionId =
|
|
844
|
+
typeof selectedSessionId === "string" ? selectedSessionId.trim() : "";
|
|
845
|
+
dashboardNavSessionRef.current =
|
|
846
|
+
trimmedSelectedSessionId !== "" ? trimmedSelectedSessionId : undefined;
|
|
847
|
+
|
|
848
|
+
const gitIdentity = payload?.gitIdentity;
|
|
849
|
+
const gitUserName =
|
|
850
|
+
typeof gitIdentity?.gitUserName === "string"
|
|
851
|
+
? gitIdentity.gitUserName.trim()
|
|
852
|
+
: "";
|
|
853
|
+
const gitUserEmail =
|
|
854
|
+
typeof gitIdentity?.gitUserEmail === "string"
|
|
855
|
+
? gitIdentity.gitUserEmail.trim()
|
|
856
|
+
: "";
|
|
857
|
+
const gitAccountLogin =
|
|
858
|
+
typeof gitIdentity?.gitAccountLogin === "string"
|
|
859
|
+
? gitIdentity.gitAccountLogin.trim()
|
|
860
|
+
: "";
|
|
861
|
+
const gitContextLine = [
|
|
862
|
+
gitUserName || null,
|
|
863
|
+
gitUserEmail || null,
|
|
864
|
+
gitAccountLogin || null,
|
|
865
|
+
]
|
|
866
|
+
.filter(Boolean)
|
|
867
|
+
.join(" · ");
|
|
868
|
+
const hasGitIdentityConfigured = Boolean(
|
|
869
|
+
gitUserName || gitUserEmail || gitAccountLogin,
|
|
870
|
+
);
|
|
871
|
+
const showGitIdentityBanner =
|
|
872
|
+
Boolean(payload) && !hasGitIdentityConfigured && !gitBannerDismissed;
|
|
873
|
+
|
|
874
|
+
const dismissGitIdentityBanner = useCallback(() => {
|
|
875
|
+
writeGitIdentityBannerDismissed();
|
|
876
|
+
setGitBannerDismissed(true);
|
|
877
|
+
}, []);
|
|
878
|
+
|
|
879
|
+
const dismissDetachedUrlHint = useCallback(() => {
|
|
880
|
+
writeDashboardDetachedUrlHintDismissed();
|
|
881
|
+
setDetachedUrlHintDismissed(true);
|
|
882
|
+
}, []);
|
|
883
|
+
|
|
884
|
+
const acknowledgePackageVersion = useCallback(() => {
|
|
885
|
+
if (typeof globalThis.window === "undefined") {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
globalThis.localStorage.setItem(
|
|
890
|
+
"kronosys_last_seen_package_version_v1",
|
|
891
|
+
packageVersion,
|
|
892
|
+
);
|
|
893
|
+
} catch {
|
|
894
|
+
// ignore localStorage unavailability
|
|
895
|
+
}
|
|
896
|
+
}, [packageVersion]);
|
|
897
|
+
|
|
898
|
+
/** Après effacement des données ou retrait de la clé localStorage, resynchroniser si l’identité Git est encore vide. */
|
|
899
|
+
useEffect(() => {
|
|
900
|
+
if (!payload || hasGitIdentityConfigured) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
setGitBannerDismissed(readGitIdentityBannerDismissed());
|
|
904
|
+
}, [
|
|
905
|
+
payload,
|
|
906
|
+
hasGitIdentityConfigured,
|
|
907
|
+
gitUserName,
|
|
908
|
+
gitUserEmail,
|
|
909
|
+
gitAccountLogin,
|
|
910
|
+
]);
|
|
911
|
+
|
|
912
|
+
const cfg = payload?.cfg as Record<string, unknown> | undefined;
|
|
913
|
+
const trackCodeMetrics = trackCodeMetricsFromCfg(cfg);
|
|
914
|
+
const mongoEnabled = cfg?.mongodbEnabled === true;
|
|
915
|
+
const mongoRemoteStatus = payload?.remoteStatus as
|
|
916
|
+
| "connected"
|
|
917
|
+
| "failed"
|
|
918
|
+
| "pending"
|
|
919
|
+
| undefined;
|
|
920
|
+
const mongoConnected = mongoEnabled && mongoRemoteStatus === "connected";
|
|
921
|
+
const localPersistenceDriver =
|
|
922
|
+
typeof cfg?.localPersistenceDriver === "string" &&
|
|
923
|
+
cfg.localPersistenceDriver.trim().toLowerCase() === "json"
|
|
924
|
+
? "json"
|
|
925
|
+
: "sqlite";
|
|
926
|
+
const mongoPushEnabled = mongoConnected;
|
|
927
|
+
|
|
928
|
+
/** Chemins workspace : payload, puis cfg, puis instantané LOC. */
|
|
929
|
+
const resolvedWorkspaceRoots = useMemo(() => {
|
|
930
|
+
const top = workspaceFolderPathStrings(payload);
|
|
931
|
+
if (top.length > 0) {
|
|
932
|
+
return top;
|
|
933
|
+
}
|
|
934
|
+
const cfgObj = payload?.cfg as Record<string, unknown> | undefined;
|
|
935
|
+
const fromCfg = workspaceFolderPathStrings(cfgObj);
|
|
936
|
+
if (fromCfg.length > 0) {
|
|
937
|
+
return fromCfg;
|
|
938
|
+
}
|
|
939
|
+
const snap = payload?.workspaceCodeSnapshot as
|
|
940
|
+
| WorkspaceCodeSnapshotPayload
|
|
941
|
+
| undefined;
|
|
942
|
+
if (snap?.ok === true) {
|
|
943
|
+
const w = snap.workspaceFolder?.trim();
|
|
944
|
+
if (w) {
|
|
945
|
+
return [w];
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return [];
|
|
949
|
+
}, [payload]);
|
|
950
|
+
|
|
951
|
+
const gitStats = payload?.gitStats as GitRepoStatisticsPayload | undefined;
|
|
952
|
+
|
|
953
|
+
const showWorkspaceFoldersEmpty = showWorkspaceFoldersEmptyMessage(
|
|
954
|
+
payload,
|
|
955
|
+
resolvedWorkspaceRoots.length,
|
|
956
|
+
);
|
|
957
|
+
const showIdeCodeTimingMetrics = showIdeLinkedCodeTimingMetrics(payload, cfg);
|
|
958
|
+
|
|
959
|
+
const showHeaderUserRow =
|
|
960
|
+
Boolean(payload) &&
|
|
961
|
+
Boolean(
|
|
962
|
+
gitContextLine ||
|
|
963
|
+
showWorkspaceFoldersEmpty ||
|
|
964
|
+
resolvedWorkspaceRoots.length > 0 ||
|
|
965
|
+
cfg,
|
|
966
|
+
);
|
|
967
|
+
const headerClockShort = dashboardUse24HourClock
|
|
968
|
+
? dt.headerClockFormat24Short
|
|
969
|
+
: dt.headerClockFormat12Short;
|
|
970
|
+
const headerDisplayPrefsTitle = dt.headerDisplayRegionTitle
|
|
971
|
+
.replace("{timeZone}", dashboardDisplayTimeZone)
|
|
972
|
+
.replace("{clock}", headerClockShort);
|
|
973
|
+
|
|
974
|
+
return (
|
|
975
|
+
<div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
|
|
976
|
+
<header className={appShellHeaderClassName}>
|
|
977
|
+
<div className="mb-3 flex w-full flex-col gap-3 sm:mb-4 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
|
|
978
|
+
<div id="dashboard-tour-anchor-intro" className="min-w-0 shrink-0">
|
|
979
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
980
|
+
<h1 className="text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
|
|
981
|
+
{header.title}
|
|
982
|
+
</h1>
|
|
983
|
+
<p className="flex flex-wrap items-center gap-x-2 text-xs font-medium leading-snug text-zinc-500 dark:text-zinc-400">
|
|
984
|
+
<span>{dt.brandTagline}</span>
|
|
985
|
+
<span
|
|
986
|
+
className="text-zinc-400/70 dark:text-zinc-600"
|
|
987
|
+
aria-hidden
|
|
988
|
+
>
|
|
989
|
+
·
|
|
990
|
+
</span>
|
|
991
|
+
<AppVersionStamp ariaLabelTemplate={dt.appVersionAriaLabel} />
|
|
992
|
+
</p>
|
|
993
|
+
</div>
|
|
994
|
+
</div>
|
|
995
|
+
<div
|
|
996
|
+
id="dashboard-tour-anchor-user-storage"
|
|
997
|
+
className="min-w-0 shrink-0 sm:max-w-[min(100%,36rem)] sm:justify-self-end"
|
|
998
|
+
>
|
|
999
|
+
{showHeaderUserRow ? (
|
|
1000
|
+
<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">
|
|
1001
|
+
{gitContextLine ? (
|
|
1002
|
+
<p
|
|
1003
|
+
className="flex max-w-full items-center gap-x-2 sm:justify-end"
|
|
1004
|
+
title={gitContextLine}
|
|
1005
|
+
>
|
|
1006
|
+
<User
|
|
1007
|
+
className="shrink-0 text-zinc-500 dark:text-zinc-500"
|
|
1008
|
+
size={14}
|
|
1009
|
+
aria-hidden
|
|
1010
|
+
/>
|
|
1011
|
+
<span className="min-w-0 max-w-[min(100%,48rem)] truncate font-medium text-zinc-800 dark:text-zinc-300">
|
|
1012
|
+
{gitContextLine}
|
|
1013
|
+
</span>
|
|
1014
|
+
</p>
|
|
1015
|
+
) : null}
|
|
1016
|
+
<p className="flex max-w-full flex-wrap items-center gap-x-1.5 sm:justify-end" title={headerDisplayPrefsTitle}>
|
|
1017
|
+
<Globe className="shrink-0 text-zinc-500 dark:text-zinc-500" size={14} aria-hidden />
|
|
1018
|
+
<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">
|
|
1019
|
+
{dashboardDisplayTimeZone}
|
|
1020
|
+
</span>
|
|
1021
|
+
<span className="text-zinc-400/70 dark:text-zinc-600" aria-hidden>
|
|
1022
|
+
·
|
|
1023
|
+
</span>
|
|
1024
|
+
<Clock className="shrink-0 text-zinc-500 dark:text-zinc-500" size={14} aria-hidden />
|
|
1025
|
+
<span className="shrink-0 font-medium text-zinc-800 dark:text-zinc-300">{headerClockShort}</span>
|
|
1026
|
+
</p>
|
|
1027
|
+
{showWorkspaceFoldersEmpty ? (
|
|
1028
|
+
<p className="flex max-w-full items-start gap-x-2 sm:justify-end">
|
|
1029
|
+
<FolderOpen
|
|
1030
|
+
className="mt-0.5 shrink-0 text-zinc-500 dark:text-zinc-500"
|
|
1031
|
+
size={14}
|
|
1032
|
+
aria-hidden
|
|
1033
|
+
/>
|
|
1034
|
+
<span className="min-w-0 max-w-[min(100%,48rem)] break-words font-medium text-zinc-800 sm:text-right dark:text-zinc-300">
|
|
1035
|
+
{dt.workspaceFoldersEmpty ?? "—"}
|
|
1036
|
+
</span>
|
|
1037
|
+
</p>
|
|
1038
|
+
) : resolvedWorkspaceRoots.length > 0 ? (
|
|
1039
|
+
resolvedWorkspaceRoots.map((p) => (
|
|
1040
|
+
<p
|
|
1041
|
+
key={p}
|
|
1042
|
+
className="flex max-w-full items-start gap-x-2 sm:justify-end"
|
|
1043
|
+
>
|
|
1044
|
+
<FolderOpen
|
|
1045
|
+
className="mt-0.5 shrink-0 text-zinc-500 dark:text-zinc-500"
|
|
1046
|
+
size={14}
|
|
1047
|
+
aria-hidden
|
|
1048
|
+
/>
|
|
1049
|
+
<span className="min-w-0 max-w-[min(100%,48rem)] break-all font-medium text-zinc-800 sm:text-right dark:text-zinc-300">
|
|
1050
|
+
{p}
|
|
1051
|
+
</span>
|
|
1052
|
+
</p>
|
|
1053
|
+
))
|
|
1054
|
+
) : null}
|
|
1055
|
+
{cfg ? (
|
|
1056
|
+
<div className="flex w-full min-w-0 sm:justify-end">
|
|
1057
|
+
<HeaderIntegrationBadges
|
|
1058
|
+
t={dt}
|
|
1059
|
+
localPersistenceDriver={localPersistenceDriver}
|
|
1060
|
+
mongoConnected={mongoConnected}
|
|
1061
|
+
mongoEnabled={mongoEnabled}
|
|
1062
|
+
/>
|
|
1063
|
+
</div>
|
|
1064
|
+
) : null}
|
|
1065
|
+
</div>
|
|
1066
|
+
) : null}
|
|
1067
|
+
</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] xl:items-center xl:gap-x-6 xl:gap-y-0">
|
|
1070
|
+
<div className="flex flex-wrap items-center justify-between gap-4 xl:contents">
|
|
1071
|
+
<div
|
|
1072
|
+
className="hidden min-w-0 xl:col-start-1 xl:row-start-1 xl:block"
|
|
1073
|
+
aria-hidden
|
|
1074
|
+
/>
|
|
1075
|
+
<div
|
|
1076
|
+
id="dashboard-tour-anchor-app-toolbar"
|
|
1077
|
+
className="flex h-10 shrink-0 flex-wrap items-center justify-end gap-1.5 xl:col-start-3 xl:row-start-1 xl:justify-self-end"
|
|
1078
|
+
>
|
|
1079
|
+
<DashboardCommandCenter
|
|
1080
|
+
dt={dt}
|
|
1081
|
+
handlers={commandHandlers}
|
|
1082
|
+
searchItems={quickSearchItems}
|
|
1083
|
+
toolbarDomId="dashboard-tour-command-center"
|
|
1084
|
+
/>
|
|
1085
|
+
<AppShellRouteNav
|
|
1086
|
+
current="dashboard"
|
|
1087
|
+
labels={nav}
|
|
1088
|
+
navAriaLabel={dt.appShellRouteNavAria}
|
|
1089
|
+
dashboardSessionId={selectedSessionId}
|
|
1090
|
+
/>
|
|
1091
|
+
<ThemeToggle lang={lang} />
|
|
1092
|
+
<PageRefreshButton
|
|
1093
|
+
title={dt.pageRefreshTitle}
|
|
1094
|
+
ariaLabel={dt.pageRefreshAriaLabel}
|
|
1095
|
+
inlineMessages={{
|
|
1096
|
+
loading: dt.pageRefreshProgressLabel,
|
|
1097
|
+
success: dt.pageRefreshDoneToast,
|
|
1098
|
+
error: dt.pageRefreshFailedToast,
|
|
1099
|
+
}}
|
|
1100
|
+
onRefresh={handleManualRefresh}
|
|
1101
|
+
/>
|
|
1102
|
+
<LanguageMenu
|
|
1103
|
+
lang={lang}
|
|
1104
|
+
labelEn={header.en}
|
|
1105
|
+
labelFr={header.fr}
|
|
1106
|
+
menuHeading={lang === "fr" ? "Langue" : "Language"}
|
|
1107
|
+
triggerAriaLabel={
|
|
1108
|
+
lang === "fr" ? "Langue de l’interface" : "Interface language"
|
|
1109
|
+
}
|
|
1110
|
+
onSelect={(next) => {
|
|
1111
|
+
writeStoredDashboardLang(next);
|
|
1112
|
+
writeDashboardLangUserChosen();
|
|
1113
|
+
setUiLang(next);
|
|
1114
|
+
void post({ type: "setLanguage", lang: next });
|
|
1115
|
+
}}
|
|
1116
|
+
/>
|
|
1117
|
+
</div>
|
|
1118
|
+
</div>
|
|
1119
|
+
{payload && live && dashboardShowKronoFocusInHeader ? (
|
|
1120
|
+
<div
|
|
1121
|
+
id="dashboard-tour-anchor-kronoFocus-header"
|
|
1122
|
+
className="min-w-0 w-full xl:col-start-2 xl:row-start-1 xl:flex xl:h-10 xl:w-max xl:max-w-[min(100vw-10rem,52rem)] xl:items-center xl:justify-self-center"
|
|
1123
|
+
>
|
|
1124
|
+
<KronoFocusPanel
|
|
1125
|
+
variant="headerBar"
|
|
1126
|
+
kronoFocus={live.kronoFocus}
|
|
1127
|
+
liveActiveTaskIds={
|
|
1128
|
+
Array.isArray(live.activeTasks) && live.activeTasks.length > 0
|
|
1129
|
+
? live.activeTasks
|
|
1130
|
+
.map((t) => t.id)
|
|
1131
|
+
.filter(
|
|
1132
|
+
(id): id is string =>
|
|
1133
|
+
typeof id === "string" && id.length > 0,
|
|
1134
|
+
)
|
|
1135
|
+
: live.activeTask?.id
|
|
1136
|
+
? [live.activeTask.id]
|
|
1137
|
+
: undefined
|
|
1138
|
+
}
|
|
1139
|
+
t={dt}
|
|
1140
|
+
post={postVoid}
|
|
1141
|
+
viewingArchive={isInspecting}
|
|
1142
|
+
/>
|
|
1143
|
+
</div>
|
|
1144
|
+
) : null}
|
|
1145
|
+
</div>
|
|
1146
|
+
</header>
|
|
1147
|
+
|
|
1148
|
+
<div className="mx-auto w-full max-w-[1920px] px-5 pt-5 pb-8 sm:px-8 sm:pt-6 sm:pb-10 lg:px-12 lg:pt-7 lg:pb-11 xl:px-14 xl:pt-7 xl:pb-12">
|
|
1149
|
+
{error && (
|
|
1150
|
+
<div
|
|
1151
|
+
className="mb-8 rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-900 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-100"
|
|
1152
|
+
role="alert"
|
|
1153
|
+
>
|
|
1154
|
+
<strong className="block text-red-800 dark:text-red-200">
|
|
1155
|
+
API
|
|
1156
|
+
</strong>
|
|
1157
|
+
{header.apiError}
|
|
1158
|
+
<pre className="mt-2 overflow-x-auto text-xs text-red-800/90 dark:text-red-200/80">
|
|
1159
|
+
{error}
|
|
1160
|
+
</pre>
|
|
1161
|
+
</div>
|
|
1162
|
+
)}
|
|
1163
|
+
|
|
1164
|
+
{urlResolution.mode === "invalid" && payload && (
|
|
1165
|
+
<div
|
|
1166
|
+
className="mb-6 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-950 dark:border-amber-800/60 dark:bg-amber-950/35 dark:text-amber-100"
|
|
1167
|
+
role="status"
|
|
1168
|
+
>
|
|
1169
|
+
{dt.detachedSessionInvalidUrl}
|
|
1170
|
+
</div>
|
|
1171
|
+
)}
|
|
1172
|
+
|
|
1173
|
+
{showDetachedUrlHintBanner ? (
|
|
1174
|
+
<div
|
|
1175
|
+
className="mb-6 flex gap-3 rounded-lg border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900/60 dark:text-zinc-300"
|
|
1176
|
+
role="note"
|
|
1177
|
+
>
|
|
1178
|
+
<p className="min-w-0 flex-1 leading-snug">
|
|
1179
|
+
{dt.detachedSessionUrlHint}
|
|
1180
|
+
</p>
|
|
1181
|
+
<button
|
|
1182
|
+
type="button"
|
|
1183
|
+
className="inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-zinc-300 text-zinc-600 hover:bg-zinc-200/90 dark:border-zinc-600 dark:text-zinc-300 dark:hover:bg-zinc-800/80"
|
|
1184
|
+
onClick={dismissDetachedUrlHint}
|
|
1185
|
+
title={dt.detachedSessionUrlHintDismiss}
|
|
1186
|
+
aria-label={dt.detachedSessionUrlHintDismiss}
|
|
1187
|
+
>
|
|
1188
|
+
<X size={16} strokeWidth={2} aria-hidden />
|
|
1189
|
+
</button>
|
|
1190
|
+
</div>
|
|
1191
|
+
) : null}
|
|
1192
|
+
|
|
1193
|
+
{live?.sessionScopeNotice &&
|
|
1194
|
+
live.archived !== true &&
|
|
1195
|
+
typeof live.sessionId === "string" &&
|
|
1196
|
+
live.sessionId.trim() !== "" ? (
|
|
1197
|
+
<div
|
|
1198
|
+
className={`mb-6 rounded-lg border px-4 py-3 text-sm ${
|
|
1199
|
+
live.sessionScopeNotice.kind === "alert"
|
|
1200
|
+
? "border-red-400/70 bg-red-950/40 text-red-100"
|
|
1201
|
+
: "border-amber-400/70 bg-amber-950/35 text-amber-100"
|
|
1202
|
+
}`}
|
|
1203
|
+
role="status"
|
|
1204
|
+
aria-label={dt.sessionScopeNoticeAria}
|
|
1205
|
+
>
|
|
1206
|
+
{live.sessionScopeNotice.message}
|
|
1207
|
+
</div>
|
|
1208
|
+
) : null}
|
|
1209
|
+
|
|
1210
|
+
{payload && (
|
|
1211
|
+
<>
|
|
1212
|
+
<div id="dashboard-tour-anchor-column-hints">
|
|
1213
|
+
<DashboardColumnHintsBanner dt={dt} />
|
|
1214
|
+
</div>
|
|
1215
|
+
{showGitIdentityBanner ? (
|
|
1216
|
+
<div id="dashboard-tour-anchor-git-identity-banner">
|
|
1217
|
+
<div
|
|
1218
|
+
className="mb-6 flex flex-col gap-3 rounded-lg border border-violet-300/80 bg-violet-50/90 px-4 py-3 text-sm text-violet-950 dark:border-violet-700/50 dark:bg-violet-950/35 dark:text-violet-100"
|
|
1219
|
+
role="region"
|
|
1220
|
+
aria-label={dt.gitIdentityBannerAria}
|
|
1221
|
+
>
|
|
1222
|
+
<p className="leading-snug">{dt.gitIdentityBannerBody}</p>
|
|
1223
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1224
|
+
<button
|
|
1225
|
+
type="button"
|
|
1226
|
+
className="inline-flex rounded-lg bg-violet-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-violet-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-400 focus-visible:ring-offset-2 focus-visible:ring-offset-violet-50 dark:focus-visible:ring-offset-violet-950"
|
|
1227
|
+
onClick={() => setGitIdentitySetupModalOpen(true)}
|
|
1228
|
+
>
|
|
1229
|
+
{dt.gitIdentityBannerConfigure}
|
|
1230
|
+
</button>
|
|
1231
|
+
<button
|
|
1232
|
+
type="button"
|
|
1233
|
+
className="rounded-lg border border-violet-400/70 px-3 py-1.5 text-sm text-violet-900 hover:bg-violet-100/80 dark:border-violet-600/60 dark:text-violet-100 dark:hover:bg-violet-900/40"
|
|
1234
|
+
onClick={dismissGitIdentityBanner}
|
|
1235
|
+
>
|
|
1236
|
+
{dt.gitIdentityBannerDismiss}
|
|
1237
|
+
</button>
|
|
1238
|
+
</div>
|
|
1239
|
+
</div>
|
|
1240
|
+
</div>
|
|
1241
|
+
) : null}
|
|
1242
|
+
<div className="grid w-full grid-cols-1 gap-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,2.25fr)_minmax(0,1fr)] xl:items-start xl:gap-x-6 2xl:gap-x-10">
|
|
1243
|
+
<div
|
|
1244
|
+
id="dashboard-col-sessions"
|
|
1245
|
+
className="flex min-w-0 flex-col xl:sticky xl:top-8 xl:max-h-[calc(100vh-6rem)] xl:overflow-y-auto xl:pb-6 xl:pr-3 [scrollbar-gutter:stable] 2xl:pr-4"
|
|
1246
|
+
>
|
|
1247
|
+
<div className="w-full min-w-0">
|
|
1248
|
+
<div className="mx-auto flex w-full max-w-md flex-col gap-8 sm:max-w-lg xl:mx-0 xl:max-w-none">
|
|
1249
|
+
<div
|
|
1250
|
+
className={`min-w-0 ${dashboardColumnTitleRowClassName}`}
|
|
1251
|
+
>
|
|
1252
|
+
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
|
|
1253
|
+
{dt.sessionsColumnTitle}
|
|
1254
|
+
</h2>
|
|
1255
|
+
<InlineMetricHelpTrigger
|
|
1256
|
+
ariaLabel={dt.sessionsColumnHelpAria}
|
|
1257
|
+
body={dt.sessionsColumnHelpBody}
|
|
1258
|
+
preserveLineBreaks
|
|
1259
|
+
/>
|
|
1260
|
+
<button
|
|
1261
|
+
type="button"
|
|
1262
|
+
className="inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-lg border border-zinc-300 bg-white text-zinc-700 shadow-sm transition hover:border-violet-400/70 hover:bg-violet-50 hover:text-violet-800 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-100 dark:border-zinc-600 dark:bg-zinc-800/80 dark:text-zinc-200 dark:hover:border-violet-500/50 dark:hover:bg-violet-950/40 dark:hover:text-violet-200 dark:focus-visible:ring-violet-400/45 dark:focus-visible:ring-offset-zinc-900"
|
|
1263
|
+
title={header.newSession}
|
|
1264
|
+
aria-label={header.newSession}
|
|
1265
|
+
onClick={() => setNewSessionModalOpen(true)}
|
|
1266
|
+
>
|
|
1267
|
+
<Plus
|
|
1268
|
+
size={20}
|
|
1269
|
+
strokeWidth={2.25}
|
|
1270
|
+
className="shrink-0"
|
|
1271
|
+
aria-hidden
|
|
1272
|
+
/>
|
|
1273
|
+
</button>
|
|
1274
|
+
</div>
|
|
1275
|
+
|
|
1276
|
+
<SelectedSessionSidebarBlock
|
|
1277
|
+
lang={lang}
|
|
1278
|
+
t={dt}
|
|
1279
|
+
sessionCurrent={sessionCurrent}
|
|
1280
|
+
columnArchiveId={columnArchiveId}
|
|
1281
|
+
sessionName={sessionName}
|
|
1282
|
+
setSessionName={setSessionName}
|
|
1283
|
+
sessionNameInputRef={sessionNameInputRef}
|
|
1284
|
+
sessionNameFieldActiveRef={sessionNameFieldActiveRef}
|
|
1285
|
+
sessionNameRowFocused={sessionNameRowFocused}
|
|
1286
|
+
setSessionNameRowFocused={setSessionNameRowFocused}
|
|
1287
|
+
post={post}
|
|
1288
|
+
headerSessionLabel={header.session}
|
|
1289
|
+
headerSessionDuration={header.sessionDuration}
|
|
1290
|
+
headerSessionStart={header.sessionStart}
|
|
1291
|
+
displayTimeZone={dashboardDisplayTimeZone}
|
|
1292
|
+
use24HourClock={dashboardUse24HourClock}
|
|
1293
|
+
headerCoding={header.coding}
|
|
1294
|
+
headerActive={header.active}
|
|
1295
|
+
headerTasks={header.tasks}
|
|
1296
|
+
archivedBadge={dt.archivedBadge}
|
|
1297
|
+
trackCodeMetrics={trackCodeMetrics}
|
|
1298
|
+
showIdeCodeTimingMetrics={showIdeCodeTimingMetrics}
|
|
1299
|
+
gitStats={gitStats}
|
|
1300
|
+
liveSessionId={live?.sessionId}
|
|
1301
|
+
onEndLiveSession={() =>
|
|
1302
|
+
void handleRequestEndLiveSession()
|
|
1303
|
+
}
|
|
1304
|
+
sessionDurationAlertThresholdMinutes={
|
|
1305
|
+
sessionDurationAlertThresholdMinutes
|
|
1306
|
+
}
|
|
1307
|
+
allowSessionStartTimeEdit={
|
|
1308
|
+
dashboardAllowSessionStartTimeEdit
|
|
1309
|
+
}
|
|
1310
|
+
allowSessionEndTimeEdit={dashboardAllowSessionEndTimeEdit}
|
|
1311
|
+
/>
|
|
1312
|
+
|
|
1313
|
+
<SessionListPanel
|
|
1314
|
+
sessions={history}
|
|
1315
|
+
lang={lang}
|
|
1316
|
+
displayTimeZone={dashboardDisplayTimeZone}
|
|
1317
|
+
use24HourClock={dashboardUse24HourClock}
|
|
1318
|
+
liveSessionId={live?.sessionId}
|
|
1319
|
+
selectedSessionId={selectedSessionId}
|
|
1320
|
+
t={dt}
|
|
1321
|
+
onSelectSession={(id) => void handleSelectSession(id)}
|
|
1322
|
+
onOpenSessionInNewTab={openSessionInNewTab}
|
|
1323
|
+
onEndLiveSession={() =>
|
|
1324
|
+
void handleRequestEndLiveSession()
|
|
1325
|
+
}
|
|
1326
|
+
onArchiveSession={confirmArchiveSession}
|
|
1327
|
+
onDeleteSession={(id) => setDeleteSessionId(id)}
|
|
1328
|
+
onOpenArchives={() =>
|
|
1329
|
+
void router.push(
|
|
1330
|
+
withDashboardSessionParam(
|
|
1331
|
+
"/settings#settings-archived-sessions",
|
|
1332
|
+
selectedSessionId,
|
|
1333
|
+
),
|
|
1334
|
+
)
|
|
1335
|
+
}
|
|
1336
|
+
archivedCount={historyArchived.length}
|
|
1337
|
+
mongoPushEnabled={mongoPushEnabled}
|
|
1338
|
+
onPushSessionToMongo={(id) =>
|
|
1339
|
+
void handlePushSessionToMongo(id)
|
|
1340
|
+
}
|
|
1341
|
+
pushingSessionId={mongoPushBusyId}
|
|
1342
|
+
sessionDurationAlertThresholdMinutes={
|
|
1343
|
+
sessionDurationAlertThresholdMinutes
|
|
1344
|
+
}
|
|
1345
|
+
/>
|
|
1346
|
+
</div>
|
|
1347
|
+
</div>
|
|
1348
|
+
</div>
|
|
1349
|
+
|
|
1350
|
+
<div
|
|
1351
|
+
id="dashboard-col-tasks"
|
|
1352
|
+
className="min-w-0 space-y-8 border-zinc-200 xl:border-x xl:border-zinc-200/90 xl:px-5 2xl:px-8 dark:border-zinc-700/80"
|
|
1353
|
+
>
|
|
1354
|
+
<div className={`min-w-0 ${dashboardColumnTitleRowClassName}`}>
|
|
1355
|
+
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
|
|
1356
|
+
{dt.rightColumnTitle}
|
|
1357
|
+
</h2>
|
|
1358
|
+
<InlineMetricHelpTrigger
|
|
1359
|
+
ariaLabel={dt.tasksColumnSubtitleHelpAria}
|
|
1360
|
+
body={dt.tasksColumnSubtitle}
|
|
1361
|
+
/>
|
|
1362
|
+
</div>
|
|
1363
|
+
|
|
1364
|
+
<TaskFocusPanel
|
|
1365
|
+
payload={payload}
|
|
1366
|
+
lang={lang}
|
|
1367
|
+
t={dt}
|
|
1368
|
+
refresh={refresh}
|
|
1369
|
+
urlFocusedSessionId={
|
|
1370
|
+
isDetachedUrlTab ? urlResolution.id : undefined
|
|
1371
|
+
}
|
|
1372
|
+
liveSessionId={live?.sessionId}
|
|
1373
|
+
displayTimeZone={dashboardDisplayTimeZone}
|
|
1374
|
+
use24HourClock={dashboardUse24HourClock}
|
|
1375
|
+
onResumeActiveSession={handleResumeActiveSession}
|
|
1376
|
+
showKronoFocusInTaskOps={dashboardShowKronoFocusInTaskOps}
|
|
1377
|
+
allowTaskStartTimeEdit={dashboardAllowTaskStartTimeEdit}
|
|
1378
|
+
allowTaskEndTimeEdit={dashboardAllowTaskEndTimeEdit}
|
|
1379
|
+
/>
|
|
1380
|
+
</div>
|
|
1381
|
+
|
|
1382
|
+
<aside
|
|
1383
|
+
id="dashboard-col-tags"
|
|
1384
|
+
className="flex min-w-0 flex-col xl:sticky xl:top-8 xl:max-h-[calc(100vh-6rem)] xl:overflow-y-auto xl:pb-6 xl:pl-3 [scrollbar-gutter:stable] 2xl:pl-4"
|
|
1385
|
+
aria-labelledby="dashboard-tags-projects-heading"
|
|
1386
|
+
>
|
|
1387
|
+
<div className={`min-w-0 ${dashboardColumnTitleRowClassName}`}>
|
|
1388
|
+
<h2
|
|
1389
|
+
id="dashboard-tags-projects-heading"
|
|
1390
|
+
className="text-lg font-semibold text-zinc-900 dark:text-zinc-100"
|
|
1391
|
+
>
|
|
1392
|
+
{dt.tagsProjectsColumnTitle}
|
|
1393
|
+
</h2>
|
|
1394
|
+
<InlineMetricHelpTrigger
|
|
1395
|
+
ariaLabel={dt.tagsProjectsColumnHelpAria}
|
|
1396
|
+
body={`${dt.tagsProjectsColumnIntro}\n\n${dt.tagsProjectsColumnHelpBody}`}
|
|
1397
|
+
preserveLineBreaks
|
|
1398
|
+
panelClassName="w-[min(calc(100vw-2rem),24rem)]"
|
|
1399
|
+
/>
|
|
1400
|
+
</div>
|
|
1401
|
+
<div className="mt-5 min-w-0">
|
|
1402
|
+
<SettingsTagsProjectsSection
|
|
1403
|
+
variant="dashboard"
|
|
1404
|
+
s={tagProjSettingsCopy}
|
|
1405
|
+
lang={lang}
|
|
1406
|
+
payload={payload}
|
|
1407
|
+
saving={false}
|
|
1408
|
+
refresh={refresh}
|
|
1409
|
+
/>
|
|
1410
|
+
</div>
|
|
1411
|
+
</aside>
|
|
1412
|
+
</div>
|
|
1413
|
+
</>
|
|
1414
|
+
)}
|
|
1415
|
+
|
|
1416
|
+
{!payload && !error ? (
|
|
1417
|
+
<DashboardLoadingOverlay
|
|
1418
|
+
ariaLabel={dt.dashboardLoadingAriaLabel}
|
|
1419
|
+
message={dt.dashboardLoadingMessage}
|
|
1420
|
+
/>
|
|
1421
|
+
) : null}
|
|
1422
|
+
|
|
1423
|
+
<NewSessionScopeModal
|
|
1424
|
+
open={newSessionModalOpen}
|
|
1425
|
+
lang={lang}
|
|
1426
|
+
t={dt}
|
|
1427
|
+
onCancel={() => setNewSessionModalOpen(false)}
|
|
1428
|
+
onConfirm={(sessionScope) => {
|
|
1429
|
+
setNewSessionModalOpen(false);
|
|
1430
|
+
void post({ type: "newSession", sessionScope });
|
|
1431
|
+
}}
|
|
1432
|
+
/>
|
|
1433
|
+
<DashboardAlertModal
|
|
1434
|
+
open={homeDialogAlert !== null}
|
|
1435
|
+
message={homeDialogAlert ?? ""}
|
|
1436
|
+
okLabel={dt.dialogOkBtn}
|
|
1437
|
+
onClose={() => setHomeDialogAlert(null)}
|
|
1438
|
+
/>
|
|
1439
|
+
<DashboardConfirmModal
|
|
1440
|
+
open={updateChangelogModalOpen}
|
|
1441
|
+
title={lang === "fr" ? "Mise à jour détectée" : "Update detected"}
|
|
1442
|
+
message={
|
|
1443
|
+
lang === "fr"
|
|
1444
|
+
? `Kronosys a été mis à jour vers la version v${packageVersion}. Voulez-vous ouvrir le CHANGELOG usager maintenant?`
|
|
1445
|
+
: `Kronosys was updated to version v${packageVersion}. Do you want to open the user-facing CHANGELOG now?`
|
|
1446
|
+
}
|
|
1447
|
+
cancelLabel={lang === "fr" ? "Plus tard" : "Later"}
|
|
1448
|
+
confirmLabel={
|
|
1449
|
+
lang === "fr" ? "Ouvrir le CHANGELOG" : "Open CHANGELOG"
|
|
1450
|
+
}
|
|
1451
|
+
onCancel={() => {
|
|
1452
|
+
acknowledgePackageVersion();
|
|
1453
|
+
setUpdateChangelogModalOpen(false);
|
|
1454
|
+
}}
|
|
1455
|
+
onConfirm={() => {
|
|
1456
|
+
acknowledgePackageVersion();
|
|
1457
|
+
setUpdateChangelogModalOpen(false);
|
|
1458
|
+
void router.push(
|
|
1459
|
+
withDashboardSessionParam(
|
|
1460
|
+
"/changelog",
|
|
1461
|
+
dashboardNavSessionRef.current,
|
|
1462
|
+
),
|
|
1463
|
+
);
|
|
1464
|
+
}}
|
|
1465
|
+
/>
|
|
1466
|
+
<DashboardConfirmModal
|
|
1467
|
+
open={endLiveSessionConfirmFlags !== null}
|
|
1468
|
+
title={dt.sessionEndLiveConfirmTitle}
|
|
1469
|
+
message={
|
|
1470
|
+
endLiveSessionConfirmFlags
|
|
1471
|
+
? formatEndLiveSessionModalIntro(endLiveSessionConfirmFlags, dt)
|
|
1472
|
+
: ""
|
|
1473
|
+
}
|
|
1474
|
+
cancelLabel={dt.dialogCancelBtn}
|
|
1475
|
+
confirmLabel={dt.sessionEndLiveConfirmBtn}
|
|
1476
|
+
onCancel={cancelEndLiveSessionConfirm}
|
|
1477
|
+
onConfirm={confirmEndLiveSession}
|
|
1478
|
+
extra={
|
|
1479
|
+
endLiveSessionConfirmFlags ? (
|
|
1480
|
+
<div className="space-y-3 text-left text-sm text-zinc-700 dark:text-zinc-300">
|
|
1481
|
+
<p className="text-[0.8rem] leading-snug text-zinc-600 dark:text-zinc-400">
|
|
1482
|
+
{dt.sessionEndReasonModalHint}
|
|
1483
|
+
</p>
|
|
1484
|
+
<fieldset className="space-y-2 border-0 p-0">
|
|
1485
|
+
<legend className="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
|
|
1486
|
+
{dt.sessionEndReasonFieldsetLegend}
|
|
1487
|
+
</legend>
|
|
1488
|
+
{(
|
|
1489
|
+
[
|
|
1490
|
+
["", dt.sessionEndReasonSkip],
|
|
1491
|
+
["planned", dt.sessionEndReasonPlanned],
|
|
1492
|
+
["early", dt.sessionEndReasonEarly],
|
|
1493
|
+
["overrun", dt.sessionEndReasonOverrun],
|
|
1494
|
+
["other", dt.sessionEndReasonOther],
|
|
1495
|
+
] as const
|
|
1496
|
+
).map(([value, label]) => (
|
|
1497
|
+
<label
|
|
1498
|
+
key={value || "skip"}
|
|
1499
|
+
className="flex cursor-pointer items-start gap-2 rounded-md py-0.5 pr-1 hover:bg-zinc-100/80 dark:hover:bg-zinc-800/50"
|
|
1500
|
+
>
|
|
1501
|
+
<input
|
|
1502
|
+
type="radio"
|
|
1503
|
+
name="kronosys-session-end-reason"
|
|
1504
|
+
className="mt-1 size-4 shrink-0 border-zinc-300 text-violet-600 focus:ring-violet-500/50 dark:border-zinc-600"
|
|
1505
|
+
checked={endSessionReasonKind === value}
|
|
1506
|
+
onChange={() => setEndSessionReasonKind(value)}
|
|
1507
|
+
/>
|
|
1508
|
+
<span className="leading-snug">{label}</span>
|
|
1509
|
+
</label>
|
|
1510
|
+
))}
|
|
1511
|
+
</fieldset>
|
|
1512
|
+
<label className="block">
|
|
1513
|
+
<span className="sr-only">{dt.sessionEndReasonNoteAria}</span>
|
|
1514
|
+
<textarea
|
|
1515
|
+
className="mt-1 w-full min-h-[4rem] resize-y rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 focus:border-violet-500/70 focus:outline-none focus:ring-2 focus:ring-violet-500/25 dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-100 dark:placeholder:text-zinc-500"
|
|
1516
|
+
rows={3}
|
|
1517
|
+
maxLength={500}
|
|
1518
|
+
value={endSessionReasonNote}
|
|
1519
|
+
onChange={(e) => setEndSessionReasonNote(e.target.value)}
|
|
1520
|
+
placeholder={dt.sessionEndReasonNotePlaceholder}
|
|
1521
|
+
aria-label={dt.sessionEndReasonNoteAria}
|
|
1522
|
+
/>
|
|
1523
|
+
</label>
|
|
1524
|
+
</div>
|
|
1525
|
+
) : null
|
|
1526
|
+
}
|
|
1527
|
+
/>
|
|
1528
|
+
<DashboardConfirmModal
|
|
1529
|
+
open={archiveConfirmSessionId !== null}
|
|
1530
|
+
message={dt.sessionArchiveConfirm}
|
|
1531
|
+
cancelLabel={dt.dialogCancelBtn}
|
|
1532
|
+
confirmLabel={dt.dialogConfirmBtn}
|
|
1533
|
+
dismissCheckbox={{
|
|
1534
|
+
label: dt.sessionArchiveDontShowAgain,
|
|
1535
|
+
checked: archiveDismissChecked,
|
|
1536
|
+
onChange: setArchiveDismissChecked,
|
|
1537
|
+
}}
|
|
1538
|
+
onCancel={() => {
|
|
1539
|
+
setArchiveConfirmSessionId(null);
|
|
1540
|
+
setArchiveDismissChecked(false);
|
|
1541
|
+
}}
|
|
1542
|
+
onConfirm={async () => {
|
|
1543
|
+
const id = archiveConfirmSessionId;
|
|
1544
|
+
const saveDismiss = archiveDismissChecked;
|
|
1545
|
+
setArchiveConfirmSessionId(null);
|
|
1546
|
+
setArchiveDismissChecked(false);
|
|
1547
|
+
if (id) {
|
|
1548
|
+
await post({
|
|
1549
|
+
type: "archiveSession",
|
|
1550
|
+
sessionId: id,
|
|
1551
|
+
archived: true,
|
|
1552
|
+
...(saveDismiss ? { dismissArchiveConfirm: true } : {}),
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
}}
|
|
1556
|
+
/>
|
|
1557
|
+
<DeleteSessionModal
|
|
1558
|
+
open={deleteSessionId !== null}
|
|
1559
|
+
sessionLabel={
|
|
1560
|
+
deleteSessionId
|
|
1561
|
+
? history
|
|
1562
|
+
.find((s) => s.sessionId === deleteSessionId)
|
|
1563
|
+
?.sessionName?.trim() || deleteSessionId.slice(0, 8)
|
|
1564
|
+
: ""
|
|
1565
|
+
}
|
|
1566
|
+
moveTargets={deleteMoveTargets}
|
|
1567
|
+
t={dt}
|
|
1568
|
+
onClose={() => setDeleteSessionId(null)}
|
|
1569
|
+
onConfirm={handleDeleteConfirm}
|
|
1570
|
+
/>
|
|
1571
|
+
<DashboardLangGateModal
|
|
1572
|
+
open={langGateOpen}
|
|
1573
|
+
onSelect={completeLangGate}
|
|
1574
|
+
/>
|
|
1575
|
+
<GitIdentityQuickSetupModal
|
|
1576
|
+
open={gitIdentitySetupModalOpen}
|
|
1577
|
+
onClose={() => setGitIdentitySetupModalOpen(false)}
|
|
1578
|
+
dt={dt}
|
|
1579
|
+
s={tagProjSettingsCopy}
|
|
1580
|
+
initialGitIdentity={payload?.gitIdentity}
|
|
1581
|
+
onSave={async (fields) => {
|
|
1582
|
+
await post({
|
|
1583
|
+
type: "setGitIdentity",
|
|
1584
|
+
gitUserName: fields.gitUserName,
|
|
1585
|
+
gitUserEmail: fields.gitUserEmail,
|
|
1586
|
+
gitAccountLogin: fields.gitAccountLogin,
|
|
1587
|
+
});
|
|
1588
|
+
}}
|
|
1589
|
+
/>
|
|
1590
|
+
<DashboardTour
|
|
1591
|
+
open={tourOpen}
|
|
1592
|
+
onOpenChange={setTourOpen}
|
|
1593
|
+
dt={dt}
|
|
1594
|
+
kronoFocusTourStep={Boolean(
|
|
1595
|
+
payload && live && dashboardShowKronoFocusInHeader,
|
|
1596
|
+
)}
|
|
1597
|
+
gitIdentityBannerTourStep={showGitIdentityBanner}
|
|
1598
|
+
/>
|
|
1599
|
+
</div>
|
|
1600
|
+
</div>
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
export default function Page() {
|
|
1605
|
+
return (
|
|
1606
|
+
<Suspense fallback={<DashboardSuspenseFallback />}>
|
|
1607
|
+
<DashboardHome />
|
|
1608
|
+
</Suspense>
|
|
1609
|
+
);
|
|
1610
|
+
}
|