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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +28 -1
  2. package/app/api/action/route.ts +39 -3
  3. package/app/api/action-logs/route.ts +24 -0
  4. package/app/api/backup/route.ts +1 -1
  5. package/app/api/restore/route.ts +145 -0
  6. package/app/changelog/page.tsx +71 -4
  7. package/app/globals.css +127 -0
  8. package/app/guide/page.tsx +61 -15
  9. package/app/implementation/page.tsx +700 -0
  10. package/app/layout.tsx +14 -3
  11. package/app/licenses/page.tsx +99 -37
  12. package/app/logs/page.tsx +258 -0
  13. package/app/manifest.ts +5 -5
  14. package/app/page.tsx +784 -229
  15. package/app/reporting/page.tsx +1266 -474
  16. package/app/settings/page.tsx +252 -18
  17. package/bin/kronosys.mjs +140 -15
  18. package/components/KronosysPayloadProvider.tsx +2 -0
  19. package/components/RouteTransition.tsx +18 -0
  20. package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
  21. package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
  22. package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
  23. package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
  24. package/components/dashboard/AppShellRouteNav.tsx +323 -48
  25. package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
  26. package/components/dashboard/DashboardSimpleModal.tsx +168 -25
  27. package/components/dashboard/DashboardTour.tsx +115 -29
  28. package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
  29. package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
  30. package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
  31. package/components/dashboard/NewSessionScopeModal.tsx +211 -20
  32. package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
  33. package/components/dashboard/ReportingTour.tsx +87 -21
  34. package/components/dashboard/SavedProjectPicker.tsx +16 -3
  35. package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
  36. package/components/dashboard/SessionListPanel.tsx +327 -44
  37. package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
  38. package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
  39. package/components/dashboard/SettingsTour.tsx +86 -21
  40. package/components/dashboard/TagPills.tsx +14 -1
  41. package/components/dashboard/TaskFocusPanel.tsx +1081 -478
  42. package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
  43. package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
  44. package/components/dashboard/taskFieldStyles.ts +20 -4
  45. package/components/dashboard/useReportingInteractionState.ts +80 -0
  46. package/lib/appShellHeaderClasses.ts +13 -0
  47. package/lib/businessRulesMatrix.ts +210 -0
  48. package/lib/copyToClipboard.ts +43 -0
  49. package/lib/dashboardCopy.ts +494 -84
  50. package/lib/dashboardQuickSearch.ts +54 -2
  51. package/lib/dashboardTimeZone.ts +109 -0
  52. package/lib/formatAppShellWallClock.ts +66 -0
  53. package/lib/formatSessionNameTemplate.ts +141 -0
  54. package/lib/generatedUserChangelog.ts +177 -6
  55. package/lib/globalPausePreview.ts +292 -0
  56. package/lib/implementationNotes.ts +1188 -0
  57. package/lib/kronosysApi.ts +6 -0
  58. package/lib/kronosysDashboardModalGates.ts +24 -0
  59. package/lib/plannedBoundaryAttention.ts +9 -0
  60. package/lib/plannedBoundaryConflict.ts +23 -0
  61. package/lib/reportingAggregate.ts +517 -75
  62. package/lib/reportingMetricHelp.ts +8 -0
  63. package/lib/reportingStrings.ts +37 -3
  64. package/lib/sessionListMerge.ts +4 -0
  65. package/lib/sessionTaskSidebarStats.ts +182 -21
  66. package/lib/settingsCopy.ts +178 -4
  67. package/lib/taskParsing.ts +360 -103
  68. package/lib/taskTemplateDraft.ts +135 -0
  69. package/lib/taskTimelineGantt.ts +265 -0
  70. package/lib/temporalDisplayPlanned.ts +71 -0
  71. package/lib/userGuideCopy.ts +121 -47
  72. package/next.config.ts +7 -0
  73. package/package.json +12 -24
  74. package/server/actionDispatch.ts +1000 -77
  75. package/server/actionTaskSession.ts +337 -24
  76. package/server/db.ts +7 -15
  77. package/server/dbSchema.ts +24 -0
  78. package/server/defaultCfg.ts +5 -0
  79. package/server/gitlabTokenStore.ts +0 -12
  80. package/server/liveHistorySync.ts +53 -0
  81. package/server/mainTimerHydrate.ts +38 -2
  82. package/server/payloadStore.ts +33 -11
  83. package/server/sessionWallHydrate.ts +66 -3
  84. package/server/userActionLog.ts +126 -0
  85. package/sonar-project.properties +11 -0
  86. package/tsconfig.json +2 -1
  87. package/components/dashboard/IssuePickerModal.tsx +0 -168
  88. package/components/dashboard/ThemeToggle.test.tsx +0 -26
  89. package/lib/backupCsvExport.test.ts +0 -149
  90. package/lib/dashboardQuickSearchQuery.test.ts +0 -63
  91. package/lib/dataDir.test.ts +0 -87
  92. package/lib/formatIsoShort.test.ts +0 -46
  93. package/lib/kronoFocusRhythm.test.ts +0 -130
  94. package/lib/kronoFocusTimerUrgency.test.ts +0 -74
  95. package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
  96. package/lib/reportingAggregate.test.ts +0 -325
  97. package/lib/reportingNonFinalIndicators.test.ts +0 -157
  98. package/lib/reportingTagWeekBreakdown.test.ts +0 -141
  99. package/lib/reportingWeekLayout.test.ts +0 -239
  100. package/lib/sessionAssiduity.test.ts +0 -25
  101. package/lib/sessionEndWarnings.test.ts +0 -200
  102. package/lib/sessionListMerge.test.ts +0 -101
  103. package/lib/sessionTaskSidebarStats.test.ts +0 -24
  104. package/lib/taskParsing.test.ts +0 -153
  105. package/lib/usageProfile.test.ts +0 -84
  106. package/server/actionDispatch.test.ts +0 -723
  107. package/server/actionTaskSession.test.ts +0 -713
  108. package/server/kronoFocusHydrate.test.ts +0 -142
  109. package/server/kronoFocusMigrate.test.ts +0 -53
  110. package/server/mainTimerHydrate.test.ts +0 -65
  111. package/server/payloadStore.test.ts +0 -78
  112. package/server/sessionWallHydrate.test.ts +0 -46
package/app/layout.tsx CHANGED
@@ -7,6 +7,8 @@ import { KronosysPackageVersionProvider } from "@/components/KronosysPackageVers
7
7
  import { DashboardToastProvider } from "@/components/dashboard/DashboardToastProvider";
8
8
  import { KronosysPayloadProvider } from "@/components/KronosysPayloadProvider";
9
9
  import { AppShellLiveSessionDrawer } from "@/components/dashboard/AppShellLiveSessionDrawer";
10
+ import { PlannedTaskBoundaryConflictWatcher } from "@/components/dashboard/PlannedTaskBoundaryConflictWatcher";
11
+ import { RouteTransition } from "@/components/RouteTransition";
10
12
  import { readKronosysPackageVersion } from "@/lib/readKronosysPackageVersion";
11
13
  import { getThemeBootstrapScript } from "@/lib/theme";
12
14
  import "./globals.css";
@@ -27,7 +29,11 @@ export const metadata: Metadata = {
27
29
  "Kronosys : sessions, tâches, KronoFocus — application web locale (Next.js + SQLite).",
28
30
  applicationName: "Kronosys Dashboard",
29
31
  icons: {
30
- icon: [{ url: "/icon-192.png", sizes: "192x192", type: "image/png" }, { url: "/icon-512.png", sizes: "512x512", type: "image/png" }],
32
+ icon: [
33
+ { url: "/icon-512.png", sizes: "512x512", type: "image/png" },
34
+ { url: "/icon-192.png", sizes: "192x192", type: "image/png" },
35
+ ],
36
+ shortcut: [{ url: "/icon-512.png", sizes: "512x512", type: "image/png" }],
31
37
  apple: [{ url: "/apple-icon.png", sizes: "180x180", type: "image/png" }],
32
38
  },
33
39
  appleWebApp: {
@@ -58,13 +64,18 @@ export default function RootLayout({
58
64
  suppressHydrationWarning
59
65
  >
60
66
  <body className="flex min-h-full flex-col font-sans">
61
- <script dangerouslySetInnerHTML={{ __html: getThemeBootstrapScript() }} />
67
+ <script
68
+ dangerouslySetInnerHTML={{ __html: getThemeBootstrapScript() }}
69
+ />
62
70
  <ThemeProvider>
63
71
  <KronosysPackageVersionProvider version={kronosysPackageVersion}>
64
72
  <DashboardToastProvider>
65
73
  <KronosysPayloadProvider>
74
+ <PlannedTaskBoundaryConflictWatcher />
66
75
  <PwaRegister />
67
- <div className="flex min-h-full flex-1 flex-col">{children}</div>
76
+ <div className="flex min-h-full flex-1 flex-col">
77
+ <RouteTransition>{children}</RouteTransition>
78
+ </div>
68
79
  <AppShellLiveSessionDrawer />
69
80
  <SiteLegalFooter />
70
81
  </KronosysPayloadProvider>
@@ -8,8 +8,17 @@ import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
8
8
  import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
9
9
  import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
10
10
  import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
11
+ import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
11
12
  import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
12
- import { appShellHeaderClassName, appShellHeaderToolRowClassName } from "@/lib/appShellHeaderClasses";
13
+ import {
14
+ appShellHeaderClassName,
15
+ appShellHeaderTitleMetaRowClassName,
16
+ appShellHeaderToolbarClassName,
17
+ } from "@/lib/appShellHeaderClasses";
18
+ import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
19
+ import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
20
+ import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
21
+ import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
13
22
  import { reportingNav } from "@/lib/reportingStrings";
14
23
  import {
15
24
  DASHBOARD_DEV_THIRD_PARTY,
@@ -23,6 +32,7 @@ import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
23
32
  function LicensesBody() {
24
33
  const router = useRouter();
25
34
  const searchParams = useSearchParams();
35
+ const { payload } = useKronosysPayload();
26
36
  const dashboardSessionNavId = searchParams.get("session");
27
37
  const lang: LicensesLang = searchParams.get("lang") === "en" ? "en" : "fr";
28
38
  const c = useMemo(() => licensesCopy(lang), [lang]);
@@ -32,7 +42,7 @@ function LicensesBody() {
32
42
  return (
33
43
  <div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
34
44
  <header className={appShellHeaderClassName}>
35
- <div className={appShellHeaderToolRowClassName}>
45
+ <div className={appShellHeaderTitleMetaRowClassName}>
36
46
  <div className="flex min-w-0 flex-col gap-1">
37
47
  <div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
38
48
  <Link
@@ -42,7 +52,9 @@ function LicensesBody() {
42
52
  Kronosys
43
53
  </Link>
44
54
  <span className="text-zinc-400 dark:text-zinc-600">/</span>
45
- <span className="text-lg font-medium text-zinc-700 dark:text-zinc-300">{c.title}</span>
55
+ <span className="text-lg font-medium text-zinc-700 dark:text-zinc-300">
56
+ {c.title}
57
+ </span>
46
58
  </div>
47
59
  <p className="flex flex-wrap items-center gap-x-2 text-xs font-medium leading-snug text-zinc-500 dark:text-zinc-400">
48
60
  <span>{dt.brandTagline}</span>
@@ -52,14 +64,20 @@ function LicensesBody() {
52
64
  <AppVersionStamp ariaLabelTemplate={dt.appVersionAriaLabel} />
53
65
  </p>
54
66
  </div>
55
- <div className="flex flex-wrap items-center gap-1.5 text-sm">
67
+ <AppShellHeaderSessionMeta payload={payload} dt={dt} />
68
+ </div>
69
+ <div className="flex w-full justify-end">
70
+ <div className={appShellHeaderToolbarClassName}>
71
+ <AppShellHeaderWallClock lang={lang as Lang} dt={dt} />
72
+ <AppShellCommandCenterPlaceholder />
56
73
  <AppShellRouteNav
57
74
  current="licenses"
58
75
  labels={{ ...nav, licenses: c.title }}
59
76
  navAriaLabel={dt.appShellRouteNavAria}
60
77
  dashboardSessionId={dashboardSessionNavId}
78
+ reserveGlobalPauseSlot
61
79
  />
62
- <ThemeToggle lang={lang} />
80
+ <ThemeToggle lang={lang as Lang} />
63
81
  <PageRefreshButton
64
82
  title={dt.pageRefreshTitle}
65
83
  ariaLabel={dt.pageRefreshAriaLabel}
@@ -72,21 +90,28 @@ function LicensesBody() {
72
90
  router.refresh();
73
91
  }}
74
92
  />
75
- <span className="hidden h-4 w-px bg-zinc-300 dark:bg-zinc-700 sm:inline-block" aria-hidden />
76
- <Link
77
- href={withDashboardSessionParam("/licenses", dashboardSessionNavId)}
78
- className={`rounded-md px-3 py-1.5 ${lang === "fr" ? "border border-violet-600/60 bg-violet-950/40 text-violet-100" : "border border-zinc-700 bg-zinc-900 text-zinc-200 hover:border-zinc-500"}`}
79
- hrefLang="fr"
80
- >
81
- Français
82
- </Link>
83
- <Link
84
- href={withDashboardSessionParam("/licenses?lang=en", dashboardSessionNavId)}
85
- className={`rounded-md px-3 py-1.5 ${lang === "en" ? "border border-violet-600/60 bg-violet-950/40 text-violet-100" : "border border-zinc-700 bg-zinc-900 text-zinc-200 hover:border-zinc-500"}`}
86
- hrefLang="en"
87
- >
88
- English
89
- </Link>
93
+ <LanguageMenu
94
+ 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
+ }}
114
+ />
90
115
  </div>
91
116
  </div>
92
117
  </header>
@@ -95,13 +120,22 @@ function LicensesBody() {
95
120
  <p className="text-sm leading-relaxed text-zinc-400">{c.intro}</p>
96
121
 
97
122
  <section className="mt-10" aria-labelledby="lic-kronosys-product">
98
- <h2 id="lic-kronosys-product" className="text-base font-semibold text-zinc-200">
123
+ <h2
124
+ id="lic-kronosys-product"
125
+ className="text-base font-semibold text-zinc-200"
126
+ >
99
127
  {c.kronosysSectionTitle}
100
128
  </h2>
101
- <p className="mt-2 text-sm leading-relaxed text-zinc-400">{c.kronosysProduct}</p>
102
- <p className="mt-2 text-sm font-medium text-zinc-300">{c.copyrightLine}</p>
129
+ <p className="mt-2 text-sm leading-relaxed text-zinc-400">
130
+ {c.kronosysProduct}
131
+ </p>
132
+ <p className="mt-2 text-sm font-medium text-zinc-300">
133
+ {c.copyrightLine}
134
+ </p>
103
135
 
104
- <h3 className="mt-6 text-sm font-semibold uppercase tracking-wide text-zinc-500">{c.mitHeading}</h3>
136
+ <h3 className="mt-6 text-sm font-semibold uppercase tracking-wide text-zinc-500">
137
+ {c.mitHeading}
138
+ </h3>
105
139
  <pre className="mt-3 overflow-x-auto rounded-lg border border-zinc-800 bg-zinc-900/60 p-4 text-xs leading-relaxed whitespace-pre-wrap text-zinc-300">
106
140
  {KRONOSYS_MIT_LICENSE_BODY}
107
141
  </pre>
@@ -115,16 +149,27 @@ function LicensesBody() {
115
149
  <table className="w-full min-w-[20rem] text-left text-sm">
116
150
  <thead className="border-b border-zinc-800 bg-zinc-900/50 text-xs uppercase text-zinc-500">
117
151
  <tr>
118
- <th className="px-3 py-2 font-medium">{c.thirdPartyColName}</th>
119
- <th className="px-3 py-2 font-medium">{c.thirdPartyColLicense}</th>
120
- <th className="px-3 py-2 font-medium">{c.thirdPartyColLink}</th>
152
+ <th className="px-3 py-2 font-medium">
153
+ {c.thirdPartyColName}
154
+ </th>
155
+ <th className="px-3 py-2 font-medium">
156
+ {c.thirdPartyColLicense}
157
+ </th>
158
+ <th className="px-3 py-2 font-medium">
159
+ {c.thirdPartyColLink}
160
+ </th>
121
161
  </tr>
122
162
  </thead>
123
163
  <tbody>
124
164
  {DASHBOARD_THIRD_PARTY.map((row) => (
125
- <tr key={row.name} className="border-b border-zinc-800/80 last:border-0">
165
+ <tr
166
+ key={row.name}
167
+ className="border-b border-zinc-800/80 last:border-0"
168
+ >
126
169
  <td className="px-3 py-2.5 text-zinc-200">{row.name}</td>
127
- <td className="px-3 py-2.5 font-mono text-xs text-zinc-400">{row.license}</td>
170
+ <td className="px-3 py-2.5 font-mono text-xs text-zinc-400">
171
+ {row.license}
172
+ </td>
128
173
  <td className="px-3 py-2.5">
129
174
  <a
130
175
  href={row.url}
@@ -150,16 +195,27 @@ function LicensesBody() {
150
195
  <table className="w-full min-w-[20rem] text-left text-sm">
151
196
  <thead className="border-b border-zinc-800 bg-zinc-900/50 text-xs uppercase text-zinc-500">
152
197
  <tr>
153
- <th className="px-3 py-2 font-medium">{c.thirdPartyColName}</th>
154
- <th className="px-3 py-2 font-medium">{c.thirdPartyColLicense}</th>
155
- <th className="px-3 py-2 font-medium">{c.thirdPartyColLink}</th>
198
+ <th className="px-3 py-2 font-medium">
199
+ {c.thirdPartyColName}
200
+ </th>
201
+ <th className="px-3 py-2 font-medium">
202
+ {c.thirdPartyColLicense}
203
+ </th>
204
+ <th className="px-3 py-2 font-medium">
205
+ {c.thirdPartyColLink}
206
+ </th>
156
207
  </tr>
157
208
  </thead>
158
209
  <tbody>
159
210
  {DASHBOARD_DEV_THIRD_PARTY.map((row) => (
160
- <tr key={row.name} className="border-b border-zinc-800/80 last:border-0">
211
+ <tr
212
+ key={row.name}
213
+ className="border-b border-zinc-800/80 last:border-0"
214
+ >
161
215
  <td className="px-3 py-2.5 text-zinc-200">{row.name}</td>
162
- <td className="px-3 py-2.5 font-mono text-xs text-zinc-400">{row.license}</td>
216
+ <td className="px-3 py-2.5 font-mono text-xs text-zinc-400">
217
+ {row.license}
218
+ </td>
163
219
  <td className="px-3 py-2.5">
164
220
  <a
165
221
  href={row.url}
@@ -181,7 +237,9 @@ function LicensesBody() {
181
237
  <h2 id="lic-fonts" className="text-base font-semibold text-zinc-200">
182
238
  {c.fontsSectionTitle}
183
239
  </h2>
184
- <p className="mt-2 text-sm leading-relaxed text-zinc-400">{c.fontsBody}</p>
240
+ <p className="mt-2 text-sm leading-relaxed text-zinc-400">
241
+ {c.fontsBody}
242
+ </p>
185
243
  <ul className="mt-3 list-inside list-disc text-sm text-zinc-400">
186
244
  <li>
187
245
  <a
@@ -220,13 +278,17 @@ function LicensesBody() {
220
278
  <h2 id="lic-ext" className="text-base font-semibold text-zinc-200">
221
279
  {c.extensionSectionTitle}
222
280
  </h2>
223
- <p className="mt-2 text-sm leading-relaxed text-zinc-400">{c.extensionNote}</p>
281
+ <p className="mt-2 text-sm leading-relaxed text-zinc-400">
282
+ {c.extensionNote}
283
+ </p>
224
284
  </section>
225
285
 
226
286
  <p className="mt-8 text-xs text-zinc-600">{c.disclaimer}</p>
227
287
  </main>
228
288
 
229
- <ScrollToTopFab ariaLabel={lang === "fr" ? "Retour en haut de la page" : "Back to top"} />
289
+ <ScrollToTopFab
290
+ ariaLabel={lang === "fr" ? "Retour en haut de la page" : "Back to top"}
291
+ />
230
292
  </div>
231
293
  );
232
294
  }
@@ -0,0 +1,258 @@
1
+ "use client";
2
+
3
+ import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
4
+ import Link from "next/link";
5
+ import { useSearchParams } from "next/navigation";
6
+ import { Logs } from "lucide-react";
7
+ import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
8
+ import { postKronosysAction } from "@/lib/kronosysApi";
9
+ import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
10
+ import {
11
+ appShellHeaderClassName,
12
+ appShellHeaderTitleMetaRowClassName,
13
+ appShellHeaderToolbarClassName,
14
+ } from "@/lib/appShellHeaderClasses";
15
+ import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
16
+ import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
17
+ import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
18
+ import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
19
+ import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
20
+ import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
21
+ import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
22
+ import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
23
+ import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
24
+ import { reportingNav, reportingStrings } from "@/lib/reportingStrings";
25
+
26
+ type ActionLogRow = {
27
+ id: number;
28
+ createdAt: string;
29
+ actionType: string;
30
+ ok: boolean;
31
+ sourceIp: string | null;
32
+ userAgent: string | null;
33
+ sessionId: string | null;
34
+ payloadJson: string | null;
35
+ };
36
+
37
+ type LiveShape = { language?: string };
38
+
39
+ function LogsContent() {
40
+ const searchParams = useSearchParams();
41
+ const dashboardSessionNavId = searchParams.get("session");
42
+ const { payload, refresh } = useKronosysPayload();
43
+ const live = payload?.current as LiveShape | undefined;
44
+ const lang: Lang = live?.language === "fr" ? "fr" : "en";
45
+ const dt = dashboardStrings(lang);
46
+ const nav = useMemo(() => reportingNav(lang), [lang]);
47
+ const t = reportingStrings(lang);
48
+
49
+ const [logs, setLogs] = useState<ActionLogRow[]>([]);
50
+ const [loading, setLoading] = useState(false);
51
+ const [error, setError] = useState<string | null>(null);
52
+
53
+ const labels = useMemo(
54
+ () =>
55
+ lang === "fr"
56
+ ? {
57
+ title: "Journal des actions",
58
+ subtitle: "Historique des actions utilisateur enregistrées côté serveur.",
59
+ refresh: "Rafraîchir",
60
+ empty: "Aucune action enregistrée.",
61
+ action: "Action",
62
+ status: "Statut",
63
+ date: "Date",
64
+ sourceIp: "IP source",
65
+ session: "Session",
66
+ ok: "OK",
67
+ failed: "Échec",
68
+ unknown: "—",
69
+ }
70
+ : {
71
+ title: "Action logs",
72
+ subtitle: "History of user actions recorded on the server.",
73
+ refresh: "Refresh",
74
+ empty: "No action logs available.",
75
+ action: "Action",
76
+ status: "Status",
77
+ date: "Date",
78
+ sourceIp: "Source IP",
79
+ session: "Session",
80
+ ok: "OK",
81
+ failed: "Failed",
82
+ unknown: "—",
83
+ },
84
+ [lang]
85
+ );
86
+
87
+ const loadLogs = useCallback(async () => {
88
+ setLoading(true);
89
+ setError(null);
90
+ try {
91
+ const res = await fetch(withDashboardSessionParam("/api/action-logs?limit=200", dashboardSessionNavId), {
92
+ cache: "no-store",
93
+ });
94
+ if (!res.ok) {
95
+ throw new Error(`HTTP ${res.status}`);
96
+ }
97
+ const body = (await res.json()) as { ok?: boolean; logs?: ActionLogRow[] };
98
+ setLogs(Array.isArray(body.logs) ? body.logs : []);
99
+ } catch (e) {
100
+ setError(e instanceof Error ? e.message : String(e));
101
+ } finally {
102
+ setLoading(false);
103
+ }
104
+ }, [dashboardSessionNavId]);
105
+
106
+ useEffect(() => {
107
+ void loadLogs();
108
+ }, [loadLogs]);
109
+
110
+ const postLang = async (next: Lang) => {
111
+ await postKronosysAction({ type: "setLanguage", lang: next });
112
+ await refresh();
113
+ };
114
+
115
+ return (
116
+ <div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
117
+ <header className={appShellHeaderClassName}>
118
+ <div className={appShellHeaderTitleMetaRowClassName}>
119
+ <div className="flex min-w-0 flex-col gap-1">
120
+ <div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
121
+ <Link
122
+ href={withDashboardSessionParam("/", dashboardSessionNavId)}
123
+ className="text-xl font-semibold tracking-tight text-zinc-900 hover:text-violet-700 dark:text-zinc-100 dark:hover:text-violet-300"
124
+ >
125
+ Kronosys
126
+ </Link>
127
+ <span className="text-zinc-400 dark:text-zinc-600" aria-hidden>
128
+ /
129
+ </span>
130
+ <span className="text-lg font-medium text-zinc-700 dark:text-zinc-300">
131
+ {labels.title}
132
+ </span>
133
+ <span
134
+ className="inline-flex items-center text-violet-500 dark:text-violet-400"
135
+ aria-hidden
136
+ >
137
+ <Logs className="size-5" strokeWidth={2} />
138
+ </span>
139
+ </div>
140
+ <p className="flex flex-wrap items-center gap-x-2 text-xs font-medium leading-snug text-zinc-500 dark:text-zinc-400">
141
+ <span>{dt.brandTagline}</span>
142
+ <span className="text-zinc-400/70 dark:text-zinc-600" aria-hidden>
143
+ ·
144
+ </span>
145
+ <AppVersionStamp ariaLabelTemplate={dt.appVersionAriaLabel} />
146
+ </p>
147
+ </div>
148
+ <AppShellHeaderSessionMeta payload={payload} dt={dt} />
149
+ </div>
150
+ <div className="flex w-full justify-end">
151
+ <div className={appShellHeaderToolbarClassName}>
152
+ <AppShellHeaderWallClock lang={lang} dt={dt} />
153
+ <AppShellCommandCenterPlaceholder />
154
+ <AppShellRouteNav
155
+ current="logs"
156
+ labels={nav}
157
+ navAriaLabel={dt.appShellRouteNavAria}
158
+ 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
+ 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)}
184
+ />
185
+ </div>
186
+ </div>
187
+ </header>
188
+
189
+ <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>
191
+ <div className="mt-4">
192
+ <button
193
+ type="button"
194
+ onClick={() => void loadLogs()}
195
+ disabled={loading}
196
+ className="rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-50 disabled:cursor-wait disabled:opacity-60 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200"
197
+ >
198
+ {loading ? t.loading : labels.refresh}
199
+ </button>
200
+ </div>
201
+
202
+ {error ? (
203
+ <p className="mt-4 text-sm text-red-600 dark:text-red-400">{error}</p>
204
+ ) : null}
205
+
206
+ <div className="mt-4 overflow-x-auto rounded-xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-800/70">
207
+ <table className="w-full min-w-[900px] text-left text-sm">
208
+ <thead className="bg-zinc-50 dark:bg-zinc-800/90">
209
+ <tr className="text-zinc-600 dark:text-zinc-300">
210
+ <th className="px-3 py-2">{labels.date}</th>
211
+ <th className="px-3 py-2">{labels.action}</th>
212
+ <th className="px-3 py-2">{labels.status}</th>
213
+ <th className="px-3 py-2">{labels.sourceIp}</th>
214
+ <th className="px-3 py-2">{labels.session}</th>
215
+ </tr>
216
+ </thead>
217
+ <tbody>
218
+ {logs.length === 0 ? (
219
+ <tr>
220
+ <td colSpan={5} className="px-3 py-4 text-zinc-500 dark:text-zinc-400">
221
+ {loading ? t.loading : labels.empty}
222
+ </td>
223
+ </tr>
224
+ ) : (
225
+ logs.map((row) => (
226
+ <tr
227
+ key={row.id}
228
+ className="border-t border-zinc-200 text-zinc-700 dark:border-zinc-700 dark:text-zinc-200"
229
+ >
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>
235
+ </tr>
236
+ ))
237
+ )}
238
+ </tbody>
239
+ </table>
240
+ </div>
241
+ </main>
242
+ </div>
243
+ );
244
+ }
245
+
246
+ export default function LogsPage() {
247
+ return (
248
+ <Suspense
249
+ fallback={
250
+ <div className="min-h-screen bg-zinc-100 px-6 py-10 text-sm text-zinc-500 dark:bg-zinc-900">
251
+ Kronosys…
252
+ </div>
253
+ }
254
+ >
255
+ <LogsContent />
256
+ </Suspense>
257
+ );
258
+ }
package/app/manifest.ts CHANGED
@@ -16,14 +16,14 @@ export default function manifest(): MetadataRoute.Manifest {
16
16
  categories: ["productivity", "developer"],
17
17
  icons: [
18
18
  {
19
- src: "/icon-192.png",
20
- sizes: "192x192",
19
+ src: "/icon-512.png",
20
+ sizes: "512x512",
21
21
  type: "image/png",
22
- purpose: "any",
22
+ purpose: "maskable",
23
23
  },
24
24
  {
25
- src: "/icon-512.png",
26
- sizes: "512x512",
25
+ src: "/icon-192.png",
26
+ sizes: "192x192",
27
27
  type: "image/png",
28
28
  purpose: "any",
29
29
  },