@nightkatana/kronosys-app 1.0.0-beta.21 → 1.0.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/app/changelog/page.tsx +87 -19
  3. package/app/globals.css +10 -8
  4. package/app/guide/page.tsx +71 -34
  5. package/app/implementation/page.tsx +70 -60
  6. package/app/licenses/page.tsx +79 -47
  7. package/app/logs/page.tsx +103 -47
  8. package/app/page.tsx +104 -169
  9. package/app/reporting/page.tsx +1918 -1436
  10. package/app/settings/page.tsx +66 -44
  11. package/components/KronosysPayloadProvider.tsx +19 -5
  12. package/components/dashboard/AppShellHeaderKronoFocus.tsx +78 -0
  13. package/components/dashboard/AppShellHeaderToolbarLayout.tsx +36 -0
  14. package/components/dashboard/AppShellHeaderUtilityRibbon.tsx +19 -0
  15. package/components/dashboard/AppShellHeaderWallClock.tsx +23 -17
  16. package/components/dashboard/AppShellRouteNav.tsx +336 -209
  17. package/components/dashboard/AppShellToolbarCommandCenter.tsx +225 -0
  18. package/components/dashboard/AppShellToolbarRouteNav.tsx +204 -0
  19. package/components/dashboard/DashboardCommandCenter.tsx +119 -30
  20. package/components/dashboard/KronoFocusPanel.tsx +287 -260
  21. package/components/dashboard/LanguageMenu.tsx +23 -7
  22. package/components/dashboard/PageRefreshButton.tsx +42 -16
  23. package/components/dashboard/ReportingTour.tsx +20 -2
  24. package/components/dashboard/SessionListPanel.tsx +4 -4
  25. package/components/dashboard/ThemeToggle.tsx +4 -3
  26. package/components/dashboard/useAnchoredFloatingPortalStyle.ts +9 -2
  27. package/components/dashboard/useKronoFocusLiveSeconds.ts +4 -2
  28. package/lib/appShellHeaderClasses.ts +22 -3
  29. package/lib/appShellToolbarChrome.ts +112 -0
  30. package/lib/appShellToolbarDeferredIntents.ts +112 -0
  31. package/lib/appShellToolbarSessionSlices.ts +67 -0
  32. package/lib/dashboardCopy.ts +78 -29
  33. package/lib/dashboardQuickSearch.ts +37 -6
  34. package/lib/dashboardUrlSession.ts +36 -0
  35. package/lib/generatedUserChangelog.ts +14 -0
  36. package/lib/implementationNotes.ts +18 -14
  37. package/lib/reportingAggregate.ts +68 -9
  38. package/lib/reportingMetricHelp.ts +8 -8
  39. package/lib/reportingStrings.ts +118 -9
  40. package/lib/reportingTagWeekBreakdown.ts +55 -13
  41. package/lib/settingsCopy.ts +6 -7
  42. package/lib/userGuideCopy.ts +29 -26
  43. package/package.json +7 -5
  44. package/server/db.ts +6 -4
  45. package/server/dbSchema.ts +2 -2
  46. package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +0 -17
package/app/page.tsx CHANGED
@@ -18,12 +18,24 @@ import {
18
18
  type GitRepoStatisticsPayload,
19
19
  type KronosysUpdatePayload,
20
20
  } from "@/lib/kronosysApi";
21
+ import {
22
+ resolveUrlSession,
23
+ type UrlSessionResolution,
24
+ } from "@/lib/dashboardUrlSession";
25
+ import {
26
+ consumeAppShellDeferredNewSession,
27
+ consumeAppShellDeferredScroll,
28
+ consumeAppShellDeferredSessionListFocus,
29
+ consumeAppShellDeferredTaskFocus,
30
+ consumeAppShellDeferredTaskTemplateDraft,
31
+ consumeAppShellDeferredTodayGantt,
32
+ } from "@/lib/appShellToolbarDeferredIntents";
21
33
  import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
22
34
  import {
23
35
  appShellHeaderClassName,
24
36
  appShellHeaderTitleMetaRowClassName,
25
- appShellHeaderToolbarClassName,
26
37
  } from "@/lib/appShellHeaderClasses";
38
+ import { AppShellHeaderToolbarLayout } from "@/components/dashboard/AppShellHeaderToolbarLayout";
27
39
  import { dashboardColumnTitleRowClassName } from "@/lib/dashboardColumnChrome";
28
40
  import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
29
41
  import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
@@ -52,7 +64,8 @@ import {
52
64
  formatEndLiveSessionModalIntro,
53
65
  getEndLiveSessionWarningFlags,
54
66
  } from "@/lib/sessionEndWarnings";
55
- import { KronoFocusPanel } from "@/components/dashboard/KronoFocusPanel";
67
+ import { AppShellHeaderKronoFocus } from "@/components/dashboard/AppShellHeaderKronoFocus";
68
+ import { AppShellHeaderUtilityRibbon } from "@/components/dashboard/AppShellHeaderUtilityRibbon";
56
69
  import { TaskFocusPanel } from "@/components/dashboard/TaskFocusPanel";
57
70
  import { DeleteSessionModal } from "@/components/dashboard/DeleteSessionModal";
58
71
  import {
@@ -70,7 +83,6 @@ import { SelectedSessionSidebarBlock } from "@/components/dashboard/SelectedSess
70
83
  import { DashboardPauseBackdrop } from "@/components/dashboard/DashboardPauseBackdrop";
71
84
  import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
72
85
  import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
73
- import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
74
86
  import { DashboardColumnHintsBanner } from "@/components/dashboard/DashboardColumnHintsBanner";
75
87
  import {
76
88
  DashboardCommandCenter,
@@ -100,13 +112,9 @@ import {
100
112
  readDashboardTimeZoneFromCfg,
101
113
  } from "@/lib/dashboardTimeZone";
102
114
  import { NewSessionScopeModal } from "@/components/dashboard/NewSessionScopeModal";
103
- import { GlobalPauseConfirmModal } from "@/components/dashboard/GlobalPauseConfirmModal";
115
+ import { AppShellToolbarRouteNav } from "@/components/dashboard/AppShellToolbarRouteNav";
104
116
  import { TaskTimelineGanttModal } from "@/components/dashboard/TaskTimelineGanttModal";
105
117
  import { useKronosysPackageVersion } from "@/components/KronosysPackageVersionProvider";
106
- import {
107
- buildGlobalPauseActivationPreview,
108
- isGlobalPauseActivationNoOp,
109
- } from "@/lib/globalPausePreview";
110
118
 
111
119
  type LiveTaskShape = {
112
120
  id: string;
@@ -172,37 +180,6 @@ type LiveShape = {
172
180
  codingSignalsByLanguage?: Array<[string, number]>;
173
181
  };
174
182
 
175
- type UrlSessionResolution =
176
- | { mode: "none" }
177
- | { mode: "loading" }
178
- | { mode: "ok"; id: string }
179
- | { mode: "invalid" };
180
-
181
- function resolveUrlSession(
182
- urlParam: string | null,
183
- payload: KronosysUpdatePayload | null,
184
- live: LiveShape | undefined,
185
- history: SessionListEntry[],
186
- historyArchived: SessionListEntry[],
187
- ): UrlSessionResolution {
188
- if (!urlParam) {
189
- return { mode: "none" };
190
- }
191
- if (!payload) {
192
- return { mode: "loading" };
193
- }
194
- const liveSid =
195
- typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
196
- if (
197
- (liveSid !== "" && urlParam === liveSid) ||
198
- history.some((s) => s.sessionId === urlParam) ||
199
- historyArchived.some((s) => s.sessionId === urlParam)
200
- ) {
201
- return { mode: "ok", id: urlParam };
202
- }
203
- return { mode: "invalid" };
204
- }
205
-
206
183
  function DashboardHome() {
207
184
  const searchParams = useSearchParams();
208
185
  const pathname = usePathname();
@@ -260,12 +237,14 @@ function DashboardHome() {
260
237
  useState(false);
261
238
  const [updateChangelogModalOpen, setUpdateChangelogModalOpen] =
262
239
  useState(false);
263
- const [globalPauseConfirmOpen, setGlobalPauseConfirmOpen] = useState(false);
264
240
  const [ganttForSessionId, setGanttForSessionId] = useState<string | null>(
265
241
  null,
266
242
  );
267
243
  const [ganttModalOpen, setGanttModalOpen] = useState(false);
268
244
  const [todayGanttModalOpen, setTodayGanttModalOpen] = useState(false);
245
+ /** Langue affichée : état local mis à jour par le sélecteur initial, le menu ou la synchro post-visite. */
246
+ const [uiLang, setUiLang] = useState<Lang>("en");
247
+ const [langGateOpen, setLangGateOpen] = useState(false);
269
248
  const packageVersion = useKronosysPackageVersion();
270
249
 
271
250
  useEffect(() => {
@@ -471,9 +450,6 @@ function DashboardHome() {
471
450
 
472
451
  const hasPayload = Boolean(payload);
473
452
  const serverLang: Lang = live?.language === "fr" ? "fr" : "en";
474
- /** Langue affichée : état local mis à jour par le sélecteur initial, le menu ou la synchro post-visite. */
475
- const [uiLang, setUiLang] = useState<Lang>("en");
476
- const [langGateOpen, setLangGateOpen] = useState(false);
477
453
 
478
454
  useLayoutEffect(() => {
479
455
  if (typeof window === "undefined") {
@@ -628,49 +604,6 @@ function DashboardHome() {
628
604
  "globalPauseContext" in live &&
629
605
  (live as { globalPauseContext?: unknown }).globalPauseContext,
630
606
  );
631
- const globalPauseActivationPreview = useMemo(
632
- () =>
633
- live && typeof live === "object"
634
- ? buildGlobalPauseActivationPreview(live as Record<string, unknown>)
635
- : null,
636
- [live],
637
- );
638
- const globalPauseNavDisabled = useMemo(
639
- () =>
640
- !globalPauseContextActive &&
641
- globalPauseActivationPreview !== null &&
642
- isGlobalPauseActivationNoOp(globalPauseActivationPreview),
643
- [globalPauseContextActive, globalPauseActivationPreview],
644
- );
645
- const onGlobalPauseNavPress = useCallback(() => {
646
- if (globalPauseContextActive) {
647
- void post({ type: "toggleGlobalPauseContext" });
648
- return;
649
- }
650
- if (globalPauseNavDisabled) {
651
- return;
652
- }
653
- setGlobalPauseConfirmOpen(true);
654
- }, [globalPauseContextActive, globalPauseNavDisabled, post]);
655
- const confirmGlobalPauseActivation = useCallback(() => {
656
- setGlobalPauseConfirmOpen(false);
657
- void post({ type: "toggleGlobalPauseContext" });
658
- }, [post]);
659
- const cancelGlobalPauseConfirm = useCallback(() => {
660
- setGlobalPauseConfirmOpen(false);
661
- }, []);
662
-
663
- useEffect(() => {
664
- if (globalPauseContextActive) {
665
- setGlobalPauseConfirmOpen(false);
666
- }
667
- }, [globalPauseContextActive]);
668
-
669
- useEffect(() => {
670
- if (!live) {
671
- setGlobalPauseConfirmOpen(false);
672
- }
673
- }, [live]);
674
607
 
675
608
  const completeLangGate = useCallback(
676
609
  (next: Lang) => {
@@ -987,12 +920,6 @@ function DashboardHome() {
987
920
  return Math.round(clamped * 60);
988
921
  }, [payload?.cfg]);
989
922
 
990
- const dashboardShowKronoFocusInHeader = useMemo(() => {
991
- const raw = (payload?.cfg as Record<string, unknown> | undefined)
992
- ?.dashboardShowKronoFocusInHeader;
993
- return raw !== false;
994
- }, [payload?.cfg]);
995
-
996
923
  const dashboardShowKronoFocusInTaskOps = useMemo(() => {
997
924
  const raw = (payload?.cfg as Record<string, unknown> | undefined)
998
925
  ?.dashboardShowKronoFocusInTaskOps;
@@ -1063,6 +990,59 @@ function DashboardHome() {
1063
990
  setTodayGanttModalOpen(false);
1064
991
  }, []);
1065
992
 
993
+ useLayoutEffect(() => {
994
+ if (pathname !== "/") {
995
+ return;
996
+ }
997
+ const scroll = consumeAppShellDeferredScroll();
998
+ if (scroll === "sessions") {
999
+ document.getElementById("dashboard-col-sessions")?.scrollIntoView({
1000
+ behavior: "smooth",
1001
+ block: "start",
1002
+ });
1003
+ } else if (scroll === "tasks") {
1004
+ document.getElementById("dashboard-col-tasks")?.scrollIntoView({
1005
+ behavior: "smooth",
1006
+ block: "start",
1007
+ });
1008
+ } else if (scroll === "tags") {
1009
+ document.getElementById("dashboard-col-tags")?.scrollIntoView({
1010
+ behavior: "smooth",
1011
+ block: "start",
1012
+ });
1013
+ }
1014
+ const sessionListFocus = consumeAppShellDeferredSessionListFocus();
1015
+ if (sessionListFocus) {
1016
+ scrollToSessionInList(sessionListFocus);
1017
+ }
1018
+ if (consumeAppShellDeferredTodayGantt()) {
1019
+ setTodayGanttModalOpen(true);
1020
+ }
1021
+ if (consumeAppShellDeferredNewSession()) {
1022
+ setNewSessionModalOpen(true);
1023
+ }
1024
+ const taskFocus = consumeAppShellDeferredTaskFocus();
1025
+ if (taskFocus) {
1026
+ focusTasksColumnForSearch();
1027
+ globalThis.requestAnimationFrame(() => {
1028
+ scrollToTaskInPanel(taskFocus);
1029
+ });
1030
+ }
1031
+ const templateDraft = consumeAppShellDeferredTaskTemplateDraft();
1032
+ if (templateDraft) {
1033
+ focusTaskLauncherInput();
1034
+ globalThis.requestAnimationFrame(() => {
1035
+ taskLauncherApplyDraftRef.current?.(templateDraft);
1036
+ });
1037
+ }
1038
+ }, [
1039
+ pathname,
1040
+ focusTaskLauncherInput,
1041
+ focusTasksColumnForSearch,
1042
+ scrollToSessionInList,
1043
+ scrollToTaskInPanel,
1044
+ ]);
1045
+
1066
1046
  const ganttResolvedSession = useMemo(():
1067
1047
  | SessionListEntry
1068
1048
  | LiveShape
@@ -1515,48 +1495,41 @@ function DashboardHome() {
1515
1495
  dt={dt}
1516
1496
  />
1517
1497
  </div>
1518
- <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">
1519
- <div className="flex flex-wrap items-center justify-between gap-4 xl:contents">
1520
- <div
1521
- className="hidden min-w-0 xl:col-start-1 xl:row-start-1 xl:block"
1522
- aria-hidden
1523
- />
1524
- <div
1525
- id="dashboard-tour-anchor-app-toolbar"
1526
- className={`${appShellHeaderToolbarClassName} xl:col-start-3 xl:row-start-1 xl:justify-self-end`}
1527
- >
1498
+ <AppShellHeaderToolbarLayout
1499
+ id="dashboard-tour-anchor-app-toolbar"
1500
+ leading={
1501
+ <>
1528
1502
  <AppShellHeaderWallClock lang={lang} dt={dt} />
1503
+ <AppShellHeaderKronoFocus
1504
+ payload={payload}
1505
+ dt={dt}
1506
+ post={postVoid}
1507
+ viewingArchive={isInspecting}
1508
+ tourDomId="dashboard-tour-anchor-kronoFocus-header"
1509
+ />
1529
1510
  <DashboardCommandCenter
1530
1511
  dt={dt}
1531
1512
  handlers={commandHandlers}
1532
1513
  searchItems={quickSearchItems}
1533
1514
  toolbarDomId="dashboard-tour-command-center"
1534
1515
  />
1535
- <AppShellRouteNav
1536
- current="dashboard"
1537
- labels={nav}
1538
- navAriaLabel={dt.appShellRouteNavAria}
1539
- dashboardSessionId={selectedSessionId}
1540
- ganttControl={{
1541
- label: dt.appShellRouteNavTodayGantt,
1542
- onPress: openTodayGanttModal,
1543
- }}
1544
- reserveGlobalPauseSlot={!(live && live.archived !== true)}
1545
- globalPauseControl={
1546
- live && live.archived !== true
1547
- ? {
1548
- active: globalPauseContextActive,
1549
- label: globalPauseContextActive
1550
- ? dt.appShellRouteNavGlobalResume
1551
- : dt.appShellRouteNavGlobalPause,
1552
- disabled: globalPauseNavDisabled,
1553
- disabledTooltip:
1554
- dt.appShellRouteNavGlobalPauseDisabledTooltip,
1555
- onPress: onGlobalPauseNavPress,
1556
- }
1557
- : undefined
1558
- }
1559
- />
1516
+ </>
1517
+ }
1518
+ nav={
1519
+ <AppShellToolbarRouteNav
1520
+ current="dashboard"
1521
+ labels={nav}
1522
+ navAriaLabel={dt.appShellRouteNavAria}
1523
+ dashboardSessionId={selectedSessionId}
1524
+ lang={lang}
1525
+ dt={dt}
1526
+ onOpenTodayGantt={openTodayGanttModal}
1527
+ />
1528
+ }
1529
+ trailing={
1530
+ <AppShellHeaderUtilityRibbon
1531
+ ariaLabel={dt.appShellUtilityToolbarGroupAria}
1532
+ >
1560
1533
  <ThemeToggle lang={lang} />
1561
1534
  <PageRefreshButton
1562
1535
  title={dt.pageRefreshTitle}
@@ -1583,35 +1556,9 @@ function DashboardHome() {
1583
1556
  void post({ type: "setLanguage", lang: next });
1584
1557
  }}
1585
1558
  />
1586
- </div>
1587
- </div>
1588
- {payload && live && dashboardShowKronoFocusInHeader ? (
1589
- <div
1590
- id="dashboard-tour-anchor-kronoFocus-header"
1591
- 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"
1592
- >
1593
- <KronoFocusPanel
1594
- variant="headerBar"
1595
- kronoFocus={live.kronoFocus}
1596
- liveActiveTaskIds={
1597
- Array.isArray(live.activeTasks) && live.activeTasks.length > 0
1598
- ? live.activeTasks
1599
- .map((t) => t.id)
1600
- .filter(
1601
- (id): id is string =>
1602
- typeof id === "string" && id.length > 0,
1603
- )
1604
- : live.activeTask?.id
1605
- ? [live.activeTask.id]
1606
- : undefined
1607
- }
1608
- t={dt}
1609
- post={postVoid}
1610
- viewingArchive={isInspecting}
1611
- />
1612
- </div>
1613
- ) : null}
1614
- </div>
1559
+ </AppShellHeaderUtilityRibbon>
1560
+ }
1561
+ />
1615
1562
  </header>
1616
1563
 
1617
1564
  {payload &&
@@ -1915,16 +1862,6 @@ function DashboardHome() {
1915
1862
  });
1916
1863
  }}
1917
1864
  />
1918
- <GlobalPauseConfirmModal
1919
- open={globalPauseConfirmOpen}
1920
- preview={globalPauseActivationPreview}
1921
- lang={lang}
1922
- displayTimeZone={dashboardDisplayTimeZone}
1923
- use24HourClock={dashboardUse24HourClock}
1924
- t={dt}
1925
- onCancel={cancelGlobalPauseConfirm}
1926
- onConfirm={confirmGlobalPauseActivation}
1927
- />
1928
1865
  <TaskTimelineGanttModal
1929
1866
  open={ganttModalOpen}
1930
1867
  onClose={closeGanttModal}
@@ -2146,9 +2083,7 @@ function DashboardHome() {
2146
2083
  open={tourOpen}
2147
2084
  onOpenChange={setTourOpen}
2148
2085
  dt={dt}
2149
- kronoFocusTourStep={Boolean(
2150
- payload && live && dashboardShowKronoFocusInHeader,
2151
- )}
2086
+ kronoFocusTourStep={Boolean(payload && live)}
2152
2087
  gitIdentityBannerTourStep={showGitIdentityBanner}
2153
2088
  />
2154
2089
  </div>