@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
@@ -1,23 +1,26 @@
1
1
  "use client";
2
2
 
3
- import { Suspense, useMemo } from "react";
3
+ import { Suspense, useCallback, useMemo } from "react";
4
4
  import Link from "next/link";
5
5
  import { useRouter, useSearchParams } from "next/navigation";
6
6
  import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
7
7
  import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
8
8
  import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
9
9
  import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
10
- import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
10
+ import { AppShellToolbarRouteNav } from "@/components/dashboard/AppShellToolbarRouteNav";
11
11
  import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
12
+ import { postKronosysAction } from "@/lib/kronosysApi";
12
13
  import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
13
14
  import {
14
15
  appShellHeaderClassName,
15
16
  appShellHeaderTitleMetaRowClassName,
16
- appShellHeaderToolbarClassName,
17
17
  } from "@/lib/appShellHeaderClasses";
18
- import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
18
+ import { AppShellHeaderToolbarLayout } from "@/components/dashboard/AppShellHeaderToolbarLayout";
19
+ import { AppShellToolbarCommandCenter } from "@/components/dashboard/AppShellToolbarCommandCenter";
19
20
  import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
20
21
  import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
22
+ import { AppShellHeaderKronoFocus } from "@/components/dashboard/AppShellHeaderKronoFocus";
23
+ import { AppShellHeaderUtilityRibbon } from "@/components/dashboard/AppShellHeaderUtilityRibbon";
21
24
  import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
22
25
  import { reportingNav } from "@/lib/reportingStrings";
23
26
  import {
@@ -32,13 +35,21 @@ import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
32
35
  function LicensesBody() {
33
36
  const router = useRouter();
34
37
  const searchParams = useSearchParams();
35
- const { payload } = useKronosysPayload();
38
+ const { payload, refresh } = useKronosysPayload();
36
39
  const dashboardSessionNavId = searchParams.get("session");
37
40
  const lang: LicensesLang = searchParams.get("lang") === "en" ? "en" : "fr";
38
41
  const c = useMemo(() => licensesCopy(lang), [lang]);
39
42
  const nav = reportingNav(lang as Lang);
40
43
  const dt = dashboardStrings(lang as Lang);
41
44
 
45
+ const postHeaderAction = useCallback(
46
+ async (body: Record<string, unknown>) => {
47
+ await postKronosysAction(body);
48
+ await refresh({ routerInvalidate: true });
49
+ },
50
+ [refresh],
51
+ );
52
+
42
53
  return (
43
54
  <div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
44
55
  <header className={appShellHeaderClassName}>
@@ -66,54 +77,75 @@ function LicensesBody() {
66
77
  </div>
67
78
  <AppShellHeaderSessionMeta payload={payload} dt={dt} />
68
79
  </div>
69
- <div className="flex w-full justify-end">
70
- <div className={appShellHeaderToolbarClassName}>
71
- <AppShellHeaderWallClock lang={lang as Lang} dt={dt} />
72
- <AppShellCommandCenterPlaceholder />
73
- <AppShellRouteNav
80
+ <AppShellHeaderToolbarLayout
81
+ leading={
82
+ <>
83
+ <AppShellHeaderWallClock lang={lang as Lang} dt={dt} />
84
+ <AppShellHeaderKronoFocus
85
+ payload={payload}
86
+ dt={dt}
87
+ post={postHeaderAction}
88
+ />
89
+ <AppShellToolbarCommandCenter
90
+ dt={dt}
91
+ lang={lang as Lang}
92
+ dashboardSessionNavId={dashboardSessionNavId}
93
+ onManualRefresh={() => void refresh({ routerInvalidate: true })}
94
+ />
95
+ </>
96
+ }
97
+ nav={
98
+ <AppShellToolbarRouteNav
74
99
  current="licenses"
75
100
  labels={{ ...nav, licenses: c.title }}
76
101
  navAriaLabel={dt.appShellRouteNavAria}
77
102
  dashboardSessionId={dashboardSessionNavId}
78
- reserveGlobalPauseSlot
79
- />
80
- <ThemeToggle lang={lang as Lang} />
81
- <PageRefreshButton
82
- title={dt.pageRefreshTitle}
83
- ariaLabel={dt.pageRefreshAriaLabel}
84
- inlineMessages={{
85
- loading: dt.pageRefreshProgressLabel,
86
- success: dt.pageRefreshDoneToast,
87
- error: dt.pageRefreshFailedToast,
88
- }}
89
- onRefresh={() => {
90
- router.refresh();
91
- }}
92
- />
93
- <LanguageMenu
94
103
  lang={lang as Lang}
95
- labelEn="English"
96
- labelFr="Français"
97
- menuHeading={lang === "fr" ? "Langue" : "Language"}
98
- triggerAriaLabel={
99
- lang === "fr" ? "Langue de l’interface" : "Interface language"
100
- }
101
- onSelect={(next) => {
102
- const path =
103
- next === "en"
104
- ? withDashboardSessionParam(
105
- "/licenses?lang=en",
106
- dashboardSessionNavId,
107
- )
108
- : withDashboardSessionParam(
109
- "/licenses",
110
- dashboardSessionNavId,
111
- );
112
- router.push(path);
113
- }}
104
+ dt={dt}
114
105
  />
115
- </div>
116
- </div>
106
+ }
107
+ trailing={
108
+ <AppShellHeaderUtilityRibbon
109
+ ariaLabel={dt.appShellUtilityToolbarGroupAria}
110
+ >
111
+ <ThemeToggle lang={lang as Lang} />
112
+ <PageRefreshButton
113
+ title={dt.pageRefreshTitle}
114
+ ariaLabel={dt.pageRefreshAriaLabel}
115
+ inlineMessages={{
116
+ loading: dt.pageRefreshProgressLabel,
117
+ success: dt.pageRefreshDoneToast,
118
+ error: dt.pageRefreshFailedToast,
119
+ }}
120
+ onRefresh={() => {
121
+ router.refresh();
122
+ }}
123
+ />
124
+ <LanguageMenu
125
+ lang={lang as Lang}
126
+ labelEn="English"
127
+ labelFr="Français"
128
+ menuHeading={lang === "fr" ? "Langue" : "Language"}
129
+ triggerAriaLabel={
130
+ lang === "fr" ? "Langue de l’interface" : "Interface language"
131
+ }
132
+ onSelect={(next) => {
133
+ const path =
134
+ next === "en"
135
+ ? withDashboardSessionParam(
136
+ "/licenses?lang=en",
137
+ dashboardSessionNavId,
138
+ )
139
+ : withDashboardSessionParam(
140
+ "/licenses",
141
+ dashboardSessionNavId,
142
+ );
143
+ router.push(path);
144
+ }}
145
+ />
146
+ </AppShellHeaderUtilityRibbon>
147
+ }
148
+ />
117
149
  </header>
118
150
 
119
151
  <main className="mx-auto max-w-3xl px-5 py-8 pb-16 sm:px-8 lg:px-10">
package/app/logs/page.tsx CHANGED
@@ -10,12 +10,14 @@ import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
10
10
  import {
11
11
  appShellHeaderClassName,
12
12
  appShellHeaderTitleMetaRowClassName,
13
- appShellHeaderToolbarClassName,
14
13
  } from "@/lib/appShellHeaderClasses";
15
- import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
14
+ import { AppShellHeaderToolbarLayout } from "@/components/dashboard/AppShellHeaderToolbarLayout";
15
+ import { AppShellToolbarCommandCenter } from "@/components/dashboard/AppShellToolbarCommandCenter";
16
16
  import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
17
17
  import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
18
- import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
18
+ import { AppShellHeaderKronoFocus } from "@/components/dashboard/AppShellHeaderKronoFocus";
19
+ import { AppShellHeaderUtilityRibbon } from "@/components/dashboard/AppShellHeaderUtilityRibbon";
20
+ import { AppShellToolbarRouteNav } from "@/components/dashboard/AppShellToolbarRouteNav";
19
21
  import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
20
22
  import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
21
23
  import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
@@ -55,7 +57,8 @@ function LogsContent() {
55
57
  lang === "fr"
56
58
  ? {
57
59
  title: "Journal des actions",
58
- subtitle: "Historique des actions utilisateur enregistrées côté serveur.",
60
+ subtitle:
61
+ "Historique des actions utilisateur enregistrées côté serveur.",
59
62
  refresh: "Rafraîchir",
60
63
  empty: "Aucune action enregistrée.",
61
64
  action: "Action",
@@ -81,20 +84,37 @@ function LogsContent() {
81
84
  failed: "Failed",
82
85
  unknown: "—",
83
86
  },
84
- [lang]
87
+ [lang],
88
+ );
89
+
90
+ const postHeaderAction = useCallback(
91
+ async (body: Record<string, unknown>) => {
92
+ await postKronosysAction(body);
93
+ await refresh({ routerInvalidate: true });
94
+ },
95
+ [refresh],
85
96
  );
86
97
 
87
98
  const loadLogs = useCallback(async () => {
88
99
  setLoading(true);
89
100
  setError(null);
90
101
  try {
91
- const res = await fetch(withDashboardSessionParam("/api/action-logs?limit=200", dashboardSessionNavId), {
92
- cache: "no-store",
93
- });
102
+ const res = await fetch(
103
+ withDashboardSessionParam(
104
+ "/api/action-logs?limit=200",
105
+ dashboardSessionNavId,
106
+ ),
107
+ {
108
+ cache: "no-store",
109
+ },
110
+ );
94
111
  if (!res.ok) {
95
112
  throw new Error(`HTTP ${res.status}`);
96
113
  }
97
- const body = (await res.json()) as { ok?: boolean; logs?: ActionLogRow[] };
114
+ const body = (await res.json()) as {
115
+ ok?: boolean;
116
+ logs?: ActionLogRow[];
117
+ };
98
118
  setLogs(Array.isArray(body.logs) ? body.logs : []);
99
119
  } catch (e) {
100
120
  setError(e instanceof Error ? e.message : String(e));
@@ -147,47 +167,70 @@ function LogsContent() {
147
167
  </div>
148
168
  <AppShellHeaderSessionMeta payload={payload} dt={dt} />
149
169
  </div>
150
- <div className="flex w-full justify-end">
151
- <div className={appShellHeaderToolbarClassName}>
152
- <AppShellHeaderWallClock lang={lang} dt={dt} />
153
- <AppShellCommandCenterPlaceholder />
154
- <AppShellRouteNav
170
+ <AppShellHeaderToolbarLayout
171
+ leading={
172
+ <>
173
+ <AppShellHeaderWallClock lang={lang} dt={dt} />
174
+ <AppShellHeaderKronoFocus
175
+ payload={payload}
176
+ dt={dt}
177
+ post={postHeaderAction}
178
+ />
179
+ <AppShellToolbarCommandCenter
180
+ dt={dt}
181
+ lang={lang}
182
+ dashboardSessionNavId={dashboardSessionNavId}
183
+ onManualRefresh={() => refresh({ routerInvalidate: true })}
184
+ />
185
+ </>
186
+ }
187
+ nav={
188
+ <AppShellToolbarRouteNav
155
189
  current="logs"
156
190
  labels={nav}
157
191
  navAriaLabel={dt.appShellRouteNavAria}
158
192
  dashboardSessionId={dashboardSessionNavId}
159
- reserveGlobalPauseSlot
160
- />
161
- <ThemeToggle lang={lang} />
162
- <PageRefreshButton
163
- title={dt.pageRefreshTitle}
164
- ariaLabel={dt.pageRefreshAriaLabel}
165
- inlineMessages={{
166
- loading: dt.pageRefreshProgressLabel,
167
- success: dt.pageRefreshDoneToast,
168
- error: dt.pageRefreshFailedToast,
169
- }}
170
- onRefresh={async () => {
171
- await loadLogs();
172
- return await refresh({ routerInvalidate: true });
173
- }}
174
- />
175
- <LanguageMenu
176
193
  lang={lang}
177
- labelEn="English"
178
- labelFr="Français"
179
- menuHeading={lang === "fr" ? "Langue" : "Language"}
180
- triggerAriaLabel={
181
- lang === "fr" ? "Langue de l’interface" : "Interface language"
182
- }
183
- onSelect={(next) => void postLang(next)}
194
+ dt={dt}
184
195
  />
185
- </div>
186
- </div>
196
+ }
197
+ trailing={
198
+ <AppShellHeaderUtilityRibbon
199
+ ariaLabel={dt.appShellUtilityToolbarGroupAria}
200
+ >
201
+ <ThemeToggle lang={lang} />
202
+ <PageRefreshButton
203
+ title={dt.pageRefreshTitle}
204
+ ariaLabel={dt.pageRefreshAriaLabel}
205
+ inlineMessages={{
206
+ loading: dt.pageRefreshProgressLabel,
207
+ success: dt.pageRefreshDoneToast,
208
+ error: dt.pageRefreshFailedToast,
209
+ }}
210
+ onRefresh={async () => {
211
+ await loadLogs();
212
+ return await refresh({ routerInvalidate: true });
213
+ }}
214
+ />
215
+ <LanguageMenu
216
+ lang={lang}
217
+ labelEn="English"
218
+ labelFr="Français"
219
+ menuHeading={lang === "fr" ? "Langue" : "Language"}
220
+ triggerAriaLabel={
221
+ lang === "fr" ? "Langue de l’interface" : "Interface language"
222
+ }
223
+ onSelect={(next) => void postLang(next)}
224
+ />
225
+ </AppShellHeaderUtilityRibbon>
226
+ }
227
+ />
187
228
  </header>
188
229
 
189
230
  <main className="mx-auto w-full max-w-[1200px] px-5 pb-16 pt-6 sm:px-8 lg:px-10">
190
- <p className="text-sm text-zinc-600 dark:text-zinc-400">{labels.subtitle}</p>
231
+ <p className="text-sm text-zinc-600 dark:text-zinc-400">
232
+ {labels.subtitle}
233
+ </p>
191
234
  <div className="mt-4">
192
235
  <button
193
236
  type="button"
@@ -217,7 +260,10 @@ function LogsContent() {
217
260
  <tbody>
218
261
  {logs.length === 0 ? (
219
262
  <tr>
220
- <td colSpan={5} className="px-3 py-4 text-zinc-500 dark:text-zinc-400">
263
+ <td
264
+ colSpan={5}
265
+ className="px-3 py-4 text-zinc-500 dark:text-zinc-400"
266
+ >
221
267
  {loading ? t.loading : labels.empty}
222
268
  </td>
223
269
  </tr>
@@ -227,11 +273,21 @@ function LogsContent() {
227
273
  key={row.id}
228
274
  className="border-t border-zinc-200 text-zinc-700 dark:border-zinc-700 dark:text-zinc-200"
229
275
  >
230
- <td className="px-3 py-2">{new Date(row.createdAt).toLocaleString()}</td>
231
- <td className="px-3 py-2 font-mono text-xs">{row.actionType}</td>
232
- <td className="px-3 py-2">{row.ok ? labels.ok : labels.failed}</td>
233
- <td className="px-3 py-2">{row.sourceIp ?? labels.unknown}</td>
234
- <td className="px-3 py-2 font-mono text-xs">{row.sessionId ?? labels.unknown}</td>
276
+ <td className="px-3 py-2">
277
+ {new Date(row.createdAt).toLocaleString()}
278
+ </td>
279
+ <td className="px-3 py-2 font-mono text-xs">
280
+ {row.actionType}
281
+ </td>
282
+ <td className="px-3 py-2">
283
+ {row.ok ? labels.ok : labels.failed}
284
+ </td>
285
+ <td className="px-3 py-2">
286
+ {row.sourceIp ?? labels.unknown}
287
+ </td>
288
+ <td className="px-3 py-2 font-mono text-xs">
289
+ {row.sessionId ?? labels.unknown}
290
+ </td>
235
291
  </tr>
236
292
  ))
237
293
  )}