@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
@@ -36,11 +36,13 @@ import { settingsCopy, type SettingsCopy } from "@/lib/settingsCopy";
36
36
  import {
37
37
  appShellHeaderClassName,
38
38
  appShellHeaderTitleMetaRowClassName,
39
- appShellHeaderToolbarClassName,
40
39
  } from "@/lib/appShellHeaderClasses";
41
- import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
40
+ import { AppShellHeaderToolbarLayout } from "@/components/dashboard/AppShellHeaderToolbarLayout";
41
+ import { AppShellToolbarCommandCenter } from "@/components/dashboard/AppShellToolbarCommandCenter";
42
42
  import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
43
43
  import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
44
+ import { AppShellHeaderKronoFocus } from "@/components/dashboard/AppShellHeaderKronoFocus";
45
+ import { AppShellHeaderUtilityRibbon } from "@/components/dashboard/AppShellHeaderUtilityRibbon";
44
46
  import { workspaceFolderPathStrings } from "@/lib/legacyEditorPayloadKeys";
45
47
  import { showWorkspaceFoldersEmptyMessage } from "@/lib/usageProfile";
46
48
  import { readDashboardUse24HourClockFromCfg } from "@/lib/dashboardClockFormat";
@@ -54,7 +56,7 @@ import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
54
56
  import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
55
57
  import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
56
58
  import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
57
- import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
59
+ import { AppShellToolbarRouteNav } from "@/components/dashboard/AppShellToolbarRouteNav";
58
60
  import { SettingsTagsProjectsSection } from "@/components/dashboard/SettingsTagsProjectsSection";
59
61
  import { SettingsTour } from "@/components/dashboard/SettingsTour";
60
62
  import { resetGitIdentityBannerDismissed } from "@/lib/dashboardGitIdentityBannerStorage";
@@ -1040,6 +1042,14 @@ function SettingsPageContent() {
1040
1042
  return await refresh({ routerInvalidate: true, preserveForm: true });
1041
1043
  }, [refresh]);
1042
1044
 
1045
+ const postHeaderAction = useCallback(
1046
+ async (body: Record<string, unknown>) => {
1047
+ await postKronosysAction(body);
1048
+ await refresh({ routerInvalidate: true, preserveForm: true });
1049
+ },
1050
+ [refresh],
1051
+ );
1052
+
1043
1053
  const restoreDashboardColumnHintsPreference = useCallback(() => {
1044
1054
  writeDashboardColumnHintsDismissed(false);
1045
1055
  writeDashboardColumnHintsClosed(false);
@@ -1263,6 +1273,7 @@ function SettingsPageContent() {
1263
1273
  type: "updateKronosysSettings",
1264
1274
  settings: {
1265
1275
  ...formRest,
1276
+ dashboardShowKronoFocusInHeader: true,
1266
1277
  workspaceLocExcludedDirectoryNames: splitLinesToWorkspaceLocDirs(
1267
1278
  workspaceLocExcludedDirsText,
1268
1279
  ),
@@ -1651,40 +1662,61 @@ function SettingsPageContent() {
1651
1662
  </div>
1652
1663
  <AppShellHeaderSessionMeta payload={payload} dt={dt} />
1653
1664
  </div>
1654
- <div className="flex w-full justify-end">
1655
- <div className={appShellHeaderToolbarClassName}>
1656
- <AppShellHeaderWallClock lang={lang} dt={dt} />
1657
- <AppShellCommandCenterPlaceholder />
1658
- <AppShellRouteNav
1665
+ <AppShellHeaderToolbarLayout
1666
+ leading={
1667
+ <>
1668
+ <AppShellHeaderWallClock lang={lang} dt={dt} />
1669
+ <AppShellHeaderKronoFocus
1670
+ payload={payload}
1671
+ dt={dt}
1672
+ post={postHeaderAction}
1673
+ />
1674
+ <AppShellToolbarCommandCenter
1675
+ dt={dt}
1676
+ lang={lang}
1677
+ dashboardSessionNavId={dashboardSessionNavId}
1678
+ onManualRefresh={handleManualRefresh}
1679
+ />
1680
+ </>
1681
+ }
1682
+ nav={
1683
+ <AppShellToolbarRouteNav
1659
1684
  current="settings"
1660
1685
  labels={nav}
1661
1686
  navAriaLabel={dt.appShellRouteNavAria}
1662
1687
  dashboardSessionId={dashboardSessionNavId}
1663
- reserveGlobalPauseSlot
1664
- />
1665
- <ThemeToggle lang={lang} />
1666
- <PageRefreshButton
1667
- title={dt.pageRefreshTitle}
1668
- ariaLabel={dt.pageRefreshAriaLabel}
1669
- inlineMessages={{
1670
- loading: dt.pageRefreshProgressLabel,
1671
- success: dt.pageRefreshDoneToast,
1672
- error: dt.pageRefreshFailedToast,
1673
- }}
1674
- onRefresh={handleManualRefresh}
1675
- />
1676
- <LanguageMenu
1677
1688
  lang={lang}
1678
- labelEn="English"
1679
- labelFr="Français"
1680
- menuHeading={lang === "fr" ? "Langue" : "Language"}
1681
- triggerAriaLabel={
1682
- lang === "fr" ? "Langue de l’interface" : "Interface language"
1683
- }
1684
- onSelect={(next) => void postLang(next)}
1689
+ dt={dt}
1685
1690
  />
1686
- </div>
1687
- </div>
1691
+ }
1692
+ trailing={
1693
+ <AppShellHeaderUtilityRibbon
1694
+ ariaLabel={dt.appShellUtilityToolbarGroupAria}
1695
+ >
1696
+ <ThemeToggle lang={lang} />
1697
+ <PageRefreshButton
1698
+ title={dt.pageRefreshTitle}
1699
+ ariaLabel={dt.pageRefreshAriaLabel}
1700
+ inlineMessages={{
1701
+ loading: dt.pageRefreshProgressLabel,
1702
+ success: dt.pageRefreshDoneToast,
1703
+ error: dt.pageRefreshFailedToast,
1704
+ }}
1705
+ onRefresh={handleManualRefresh}
1706
+ />
1707
+ <LanguageMenu
1708
+ lang={lang}
1709
+ labelEn="English"
1710
+ labelFr="Français"
1711
+ menuHeading={lang === "fr" ? "Langue" : "Language"}
1712
+ triggerAriaLabel={
1713
+ lang === "fr" ? "Langue de l’interface" : "Interface language"
1714
+ }
1715
+ onSelect={(next) => void postLang(next)}
1716
+ />
1717
+ </AppShellHeaderUtilityRibbon>
1718
+ }
1719
+ />
1688
1720
  </header>
1689
1721
 
1690
1722
  <main className="mx-auto w-full max-w-[1920px] px-5 py-8 sm:px-8 lg:px-10 xl:px-12 2xl:px-14">
@@ -2305,19 +2337,9 @@ function SettingsPageContent() {
2305
2337
  <h2 className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
2306
2338
  {s.sectionKronoFocus}
2307
2339
  </h2>
2308
- <Field
2309
- label={s.kronoFocusShowInHeader}
2310
- description={s.kronoFocusShowInHeaderDesc}
2311
- >
2312
- <SettingsCheckbox
2313
- checked={form.dashboardShowKronoFocusInHeader}
2314
- onChange={(v) =>
2315
- update("dashboardShowKronoFocusInHeader", v)
2316
- }
2317
- disabled={formLocked}
2318
- ariaLabel={s.kronoFocusShowInHeader}
2319
- />
2320
- </Field>
2340
+ <p className="mb-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
2341
+ {s.kronoFocusHeaderPinnedNote}
2342
+ </p>
2321
2343
  <Field
2322
2344
  label={s.kronoFocusShowInTaskOps}
2323
2345
  description={s.kronoFocusShowInTaskOpsDesc}
@@ -12,7 +12,10 @@ import {
12
12
  } from "react";
13
13
  import { useRouter } from "next/navigation";
14
14
 
15
- import { fetchKronosysState, type KronosysUpdatePayload } from "@/lib/kronosysApi";
15
+ import {
16
+ fetchKronosysState,
17
+ type KronosysUpdatePayload,
18
+ } from "@/lib/kronosysApi";
16
19
 
17
20
  export type KronosysPayloadContextValue = {
18
21
  payload: KronosysUpdatePayload | null;
@@ -27,7 +30,8 @@ export type KronosysPayloadContextValue = {
27
30
  getLatestPayload: () => KronosysUpdatePayload | null;
28
31
  };
29
32
 
30
- const KronosysPayloadContext = createContext<KronosysPayloadContextValue | null>(null);
33
+ const KronosysPayloadContext =
34
+ createContext<KronosysPayloadContextValue | null>(null);
31
35
 
32
36
  export function KronosysPayloadProvider({ children }: { children: ReactNode }) {
33
37
  const router = useRouter();
@@ -80,9 +84,13 @@ export function KronosysPayloadProvider({ children }: { children: ReactNode }) {
80
84
  timer = setTimeout(() => void pollState(), delay);
81
85
  };
82
86
 
83
- void pollState();
87
+ /** Différer le premier fetch après le commit pour éviter les courses avec Suspense / enfants (React 19). */
88
+ const kickoff = globalThis.setTimeout(() => {
89
+ void pollState();
90
+ }, 0);
84
91
  return () => {
85
92
  cancelled = true;
93
+ globalThis.clearTimeout(kickoff);
86
94
  if (timer) {
87
95
  clearTimeout(timer);
88
96
  }
@@ -99,13 +107,19 @@ export function KronosysPayloadProvider({ children }: { children: ReactNode }) {
99
107
  [payload, error, refresh, getLatestPayload],
100
108
  );
101
109
 
102
- return <KronosysPayloadContext.Provider value={value}>{children}</KronosysPayloadContext.Provider>;
110
+ return (
111
+ <KronosysPayloadContext.Provider value={value}>
112
+ {children}
113
+ </KronosysPayloadContext.Provider>
114
+ );
103
115
  }
104
116
 
105
117
  export function useKronosysPayload(): KronosysPayloadContextValue {
106
118
  const v = useContext(KronosysPayloadContext);
107
119
  if (!v) {
108
- throw new Error("useKronosysPayload doit être utilisé sous KronosysPayloadProvider.");
120
+ throw new Error(
121
+ "useKronosysPayload doit être utilisé sous KronosysPayloadProvider.",
122
+ );
109
123
  }
110
124
  return v;
111
125
  }
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import { useCallback, type ComponentProps } from "react";
4
+ import { KronoFocusPanel } from "@/components/dashboard/KronoFocusPanel";
5
+ import type { DashboardStrings } from "@/lib/dashboardCopy";
6
+
7
+ type LiveForHeader = {
8
+ archived?: boolean;
9
+ kronoFocus?: ComponentProps<typeof KronoFocusPanel>["kronoFocus"];
10
+ activeTasks?: Array<{ id?: string }>;
11
+ activeTask?: { id?: string };
12
+ };
13
+
14
+ export function AppShellHeaderKronoFocus({
15
+ payload,
16
+ dt,
17
+ post,
18
+ viewingArchive = false,
19
+ tourDomId,
20
+ className = "flex min-w-0 max-w-[min(100vw-12rem,40rem)] shrink-0 items-center",
21
+ }: Readonly<{
22
+ payload: unknown;
23
+ dt: DashboardStrings;
24
+ post: (body: Record<string, unknown>) => Promise<void>;
25
+ /** Tableau de bord : consultation d’une session archivée tout en pilotant la session live. */
26
+ viewingArchive?: boolean;
27
+ tourDomId?: string;
28
+ className?: string;
29
+ }>) {
30
+ const noopPost = useCallback(async (_body: Record<string, unknown>) => {},
31
+ []);
32
+
33
+ const live = (payload as { current?: LiveForHeader } | null | undefined)
34
+ ?.current;
35
+
36
+ if (!payload || !live) {
37
+ return (
38
+ <div
39
+ id={tourDomId}
40
+ className={`${className} opacity-85`}
41
+ aria-busy="true"
42
+ aria-label={dt.appShellHeaderKronoFocusLoadingAria}
43
+ >
44
+ <KronoFocusPanel
45
+ variant="headerBar"
46
+ kronoFocus={undefined}
47
+ liveActiveTaskIds={undefined}
48
+ t={dt}
49
+ post={noopPost}
50
+ viewingArchive={false}
51
+ />
52
+ </div>
53
+ );
54
+ }
55
+
56
+ let liveActiveTaskIds: string[] | undefined;
57
+ if (Array.isArray(live.activeTasks) && live.activeTasks.length > 0) {
58
+ const ids = live.activeTasks
59
+ .map((t) => t.id)
60
+ .filter((id): id is string => typeof id === "string" && id.length > 0);
61
+ liveActiveTaskIds = ids.length > 0 ? ids : undefined;
62
+ } else if (live.activeTask?.id) {
63
+ liveActiveTaskIds = [live.activeTask.id];
64
+ }
65
+
66
+ return (
67
+ <div id={tourDomId} className={className}>
68
+ <KronoFocusPanel
69
+ variant="headerBar"
70
+ kronoFocus={live.kronoFocus}
71
+ liveActiveTaskIds={liveActiveTaskIds}
72
+ t={dt}
73
+ post={post}
74
+ viewingArchive={viewingArchive}
75
+ />
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,36 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ appShellHeaderToolbarClassName,
4
+ appShellHeaderToolbarClusterClassName,
5
+ appShellHeaderToolbarLeadingClassName,
6
+ appShellHeaderToolbarNavClassName,
7
+ appShellHeaderToolbarTrailingClassName,
8
+ } from "@/lib/appShellHeaderClasses";
9
+
10
+ type AppShellHeaderToolbarLayoutProps = Readonly<{
11
+ id?: string;
12
+ leading: ReactNode;
13
+ nav: ReactNode;
14
+ trailing: ReactNode;
15
+ }>;
16
+
17
+ /**
18
+ * Toute la barre d’outils (leading + nav + trailing) est rendue comme **un seul bloc** centré
19
+ * horizontalement dans la rangée ; défilement horizontal si la fenêtre est plus étroite que le bloc.
20
+ */
21
+ export function AppShellHeaderToolbarLayout({
22
+ id,
23
+ leading,
24
+ nav,
25
+ trailing,
26
+ }: AppShellHeaderToolbarLayoutProps) {
27
+ return (
28
+ <div id={id} className={appShellHeaderToolbarClassName}>
29
+ <div className={appShellHeaderToolbarClusterClassName}>
30
+ <div className={appShellHeaderToolbarLeadingClassName}>{leading}</div>
31
+ <div className={appShellHeaderToolbarNavClassName}>{nav}</div>
32
+ <div className={appShellHeaderToolbarTrailingClassName}>{trailing}</div>
33
+ </div>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { appShellToolbarRibbonGroupClass } from "@/lib/appShellToolbarChrome";
5
+
6
+ export function AppShellHeaderUtilityRibbon({
7
+ ariaLabel,
8
+ children,
9
+ }: Readonly<{ ariaLabel: string; children: ReactNode }>) {
10
+ return (
11
+ <div
12
+ role="group"
13
+ aria-label={ariaLabel}
14
+ className={appShellToolbarRibbonGroupClass}
15
+ >
16
+ {children}
17
+ </div>
18
+ );
19
+ }
@@ -5,6 +5,10 @@ import { useEffect, useMemo, useState } from "react";
5
5
  import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
6
6
  import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
7
7
  import { formatAppShellWallClock } from "@/lib/formatAppShellWallClock";
8
+ import {
9
+ appShellToolbarRaisedWideTriggerClass,
10
+ appShellToolbarRibbonGroupClass,
11
+ } from "@/lib/appShellToolbarChrome";
8
12
 
9
13
  export function AppShellHeaderWallClock({
10
14
  lang,
@@ -33,22 +37,24 @@ export function AppShellHeaderWallClock({
33
37
  .replace("{datetime}", ariaDatetime);
34
38
 
35
39
  return (
36
- <time
37
- suppressHydrationWarning
38
- dateTime={now ? now.toISOString() : ""}
39
- className="inline-flex h-10 shrink-0 items-center gap-1.5 rounded-lg border border-zinc-300 bg-white px-2.5 font-mono text-sm tabular-nums text-zinc-800 shadow-sm dark:border-zinc-600 dark:bg-zinc-800/90 dark:text-zinc-100"
40
- title={aria}
41
- aria-label={aria}
42
- >
43
- <Clock
44
- size={16}
45
- strokeWidth={2}
46
- className="shrink-0 text-zinc-500 dark:text-zinc-400"
47
- aria-hidden
48
- />
49
- <span className="whitespace-nowrap" suppressHydrationWarning>
50
- {now ? display : "--:--:--"}
51
- </span>
52
- </time>
40
+ <div className={appShellToolbarRibbonGroupClass}>
41
+ <time
42
+ suppressHydrationWarning
43
+ dateTime={now ? now.toISOString() : ""}
44
+ className={`${appShellToolbarRaisedWideTriggerClass} gap-1.5 px-2.5 font-mono text-sm tabular-nums text-zinc-800 dark:text-zinc-100`}
45
+ title={aria}
46
+ aria-label={aria}
47
+ >
48
+ <Clock
49
+ size={16}
50
+ strokeWidth={2}
51
+ className="shrink-0 text-zinc-500 dark:text-zinc-400"
52
+ aria-hidden
53
+ />
54
+ <span className="whitespace-nowrap" suppressHydrationWarning>
55
+ {now ? display : "--:--:--"}
56
+ </span>
57
+ </time>
58
+ </div>
53
59
  );
54
60
  }