@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.
- package/README.md +28 -1
- package/app/api/action/route.ts +39 -3
- package/app/api/action-logs/route.ts +24 -0
- package/app/api/backup/route.ts +1 -1
- package/app/api/restore/route.ts +145 -0
- package/app/changelog/page.tsx +71 -4
- package/app/globals.css +127 -0
- package/app/guide/page.tsx +61 -15
- package/app/implementation/page.tsx +700 -0
- package/app/layout.tsx +14 -3
- package/app/licenses/page.tsx +99 -37
- package/app/logs/page.tsx +258 -0
- package/app/manifest.ts +5 -5
- package/app/page.tsx +784 -229
- package/app/reporting/page.tsx +1266 -474
- package/app/settings/page.tsx +252 -18
- package/bin/kronosys.mjs +140 -15
- package/components/KronosysPayloadProvider.tsx +2 -0
- package/components/RouteTransition.tsx +18 -0
- package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
- package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
- package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
- package/components/dashboard/AppShellRouteNav.tsx +323 -48
- package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
- package/components/dashboard/DashboardSimpleModal.tsx +168 -25
- package/components/dashboard/DashboardTour.tsx +115 -29
- package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
- package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
- package/components/dashboard/NewSessionScopeModal.tsx +211 -20
- package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
- package/components/dashboard/ReportingTour.tsx +87 -21
- package/components/dashboard/SavedProjectPicker.tsx +16 -3
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
- package/components/dashboard/SessionListPanel.tsx +327 -44
- package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
- package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
- package/components/dashboard/SettingsTour.tsx +86 -21
- package/components/dashboard/TagPills.tsx +14 -1
- package/components/dashboard/TaskFocusPanel.tsx +1081 -478
- package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
- package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
- package/components/dashboard/taskFieldStyles.ts +20 -4
- package/components/dashboard/useReportingInteractionState.ts +80 -0
- package/lib/appShellHeaderClasses.ts +13 -0
- package/lib/businessRulesMatrix.ts +210 -0
- package/lib/copyToClipboard.ts +43 -0
- package/lib/dashboardCopy.ts +494 -84
- package/lib/dashboardQuickSearch.ts +54 -2
- package/lib/dashboardTimeZone.ts +109 -0
- package/lib/formatAppShellWallClock.ts +66 -0
- package/lib/formatSessionNameTemplate.ts +141 -0
- package/lib/generatedUserChangelog.ts +177 -6
- package/lib/globalPausePreview.ts +292 -0
- package/lib/implementationNotes.ts +1188 -0
- package/lib/kronosysApi.ts +6 -0
- package/lib/kronosysDashboardModalGates.ts +24 -0
- package/lib/plannedBoundaryAttention.ts +9 -0
- package/lib/plannedBoundaryConflict.ts +23 -0
- package/lib/reportingAggregate.ts +517 -75
- package/lib/reportingMetricHelp.ts +8 -0
- package/lib/reportingStrings.ts +37 -3
- package/lib/sessionListMerge.ts +4 -0
- package/lib/sessionTaskSidebarStats.ts +182 -21
- package/lib/settingsCopy.ts +178 -4
- package/lib/taskParsing.ts +360 -103
- package/lib/taskTemplateDraft.ts +135 -0
- package/lib/taskTimelineGantt.ts +265 -0
- package/lib/temporalDisplayPlanned.ts +71 -0
- package/lib/userGuideCopy.ts +121 -47
- package/next.config.ts +7 -0
- package/package.json +12 -24
- package/server/actionDispatch.ts +1000 -77
- package/server/actionTaskSession.ts +337 -24
- package/server/db.ts +7 -15
- package/server/dbSchema.ts +24 -0
- package/server/defaultCfg.ts +5 -0
- package/server/gitlabTokenStore.ts +0 -12
- package/server/liveHistorySync.ts +53 -0
- package/server/mainTimerHydrate.ts +38 -2
- package/server/payloadStore.ts +33 -11
- package/server/sessionWallHydrate.ts +66 -3
- package/server/userActionLog.ts +126 -0
- package/sonar-project.properties +11 -0
- package/tsconfig.json +2 -1
- package/components/dashboard/IssuePickerModal.tsx +0 -168
- package/components/dashboard/ThemeToggle.test.tsx +0 -26
- package/lib/backupCsvExport.test.ts +0 -149
- package/lib/dashboardQuickSearchQuery.test.ts +0 -63
- package/lib/dataDir.test.ts +0 -87
- package/lib/formatIsoShort.test.ts +0 -46
- package/lib/kronoFocusRhythm.test.ts +0 -130
- package/lib/kronoFocusTimerUrgency.test.ts +0 -74
- package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
- package/lib/reportingAggregate.test.ts +0 -325
- package/lib/reportingNonFinalIndicators.test.ts +0 -157
- package/lib/reportingTagWeekBreakdown.test.ts +0 -141
- package/lib/reportingWeekLayout.test.ts +0 -239
- package/lib/sessionAssiduity.test.ts +0 -25
- package/lib/sessionEndWarnings.test.ts +0 -200
- package/lib/sessionListMerge.test.ts +0 -101
- package/lib/sessionTaskSidebarStats.test.ts +0 -24
- package/lib/taskParsing.test.ts +0 -153
- package/lib/usageProfile.test.ts +0 -84
- package/server/actionDispatch.test.ts +0 -723
- package/server/actionTaskSession.test.ts +0 -713
- package/server/kronoFocusHydrate.test.ts +0 -142
- package/server/kronoFocusMigrate.test.ts +0 -53
- package/server/mainTimerHydrate.test.ts +0 -65
- package/server/payloadStore.test.ts +0 -78
- package/server/sessionWallHydrate.test.ts +0 -46
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Suspense,
|
|
5
|
+
useCallback,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
type Dispatch,
|
|
9
|
+
type SetStateAction,
|
|
10
|
+
} from "react";
|
|
11
|
+
import Link from "next/link";
|
|
12
|
+
import { useSearchParams } from "next/navigation";
|
|
13
|
+
import {
|
|
14
|
+
Ban,
|
|
15
|
+
Check,
|
|
16
|
+
ChevronDown,
|
|
17
|
+
Copy,
|
|
18
|
+
FileCode2,
|
|
19
|
+
ScrollText,
|
|
20
|
+
X,
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
|
|
23
|
+
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
24
|
+
import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
|
|
25
|
+
import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
|
|
26
|
+
import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
|
|
27
|
+
import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
|
|
28
|
+
import {
|
|
29
|
+
appShellHeaderClassName,
|
|
30
|
+
appShellHeaderTitleMetaRowClassName,
|
|
31
|
+
appShellHeaderToolbarClassName,
|
|
32
|
+
} from "@/lib/appShellHeaderClasses";
|
|
33
|
+
import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
|
|
34
|
+
import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
|
|
35
|
+
import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
|
|
36
|
+
import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
|
|
37
|
+
import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
|
|
38
|
+
import { implementationNotesBundle } from "@/lib/implementationNotes";
|
|
39
|
+
import type {
|
|
40
|
+
ImplementationNotesBundle,
|
|
41
|
+
ImplementationUserStory,
|
|
42
|
+
} from "@/lib/implementationNotes";
|
|
43
|
+
import { copyTextToClipboard } from "@/lib/copyToClipboard";
|
|
44
|
+
import { businessRulesMatrixBundle } from "@/lib/businessRulesMatrix";
|
|
45
|
+
import { reportingNav } from "@/lib/reportingStrings";
|
|
46
|
+
import { postKronosysAction } from "@/lib/kronosysApi";
|
|
47
|
+
import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
|
|
48
|
+
|
|
49
|
+
type LiveShape = { language?: string };
|
|
50
|
+
|
|
51
|
+
function userStoryScreenReaderLabel(
|
|
52
|
+
lang: Lang,
|
|
53
|
+
implemented: boolean,
|
|
54
|
+
discarded?: boolean,
|
|
55
|
+
): string {
|
|
56
|
+
if (discarded) {
|
|
57
|
+
return lang === "fr"
|
|
58
|
+
? "Intention écartée volontairement ; documentée pour éviter les rediscussions."
|
|
59
|
+
: "Intent voluntarily discarded; kept on record to avoid reopening the same debate.";
|
|
60
|
+
}
|
|
61
|
+
if (lang === "fr") {
|
|
62
|
+
return implemented ? "Capacité livrée." : "Absent ou hors périmètre.";
|
|
63
|
+
}
|
|
64
|
+
return implemented ? "Capability shipped." : "Missing or out of scope.";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function coverageScreenReaderLabel(
|
|
68
|
+
lang: Lang,
|
|
69
|
+
columnLabel: string,
|
|
70
|
+
checked: boolean | undefined,
|
|
71
|
+
discarded?: boolean,
|
|
72
|
+
): string {
|
|
73
|
+
if (discarded) {
|
|
74
|
+
return lang === "fr"
|
|
75
|
+
? `${columnLabel} : sans objet pour une ligne écartée.`
|
|
76
|
+
: `${columnLabel}: not applicable for a discarded intent.`;
|
|
77
|
+
}
|
|
78
|
+
const yn =
|
|
79
|
+
lang === "fr"
|
|
80
|
+
? checked ? "oui" : "non"
|
|
81
|
+
: checked
|
|
82
|
+
? "yes"
|
|
83
|
+
: "no";
|
|
84
|
+
return `${columnLabel}: ${yn}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function implementationCopyFeedbackMessage(
|
|
88
|
+
lang: Lang,
|
|
89
|
+
status: "copied" | "failed",
|
|
90
|
+
): string {
|
|
91
|
+
if (status === "copied") {
|
|
92
|
+
return lang === "fr" ? "Copié" : "Copied";
|
|
93
|
+
}
|
|
94
|
+
return lang === "fr"
|
|
95
|
+
? "Copie impossible (navigateur ou contexte HTTP)."
|
|
96
|
+
: "Could not copy (browser or non-HTTPS context).";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type StoryCopyFeedback = {
|
|
100
|
+
storyId: string;
|
|
101
|
+
status: "copied" | "failed";
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function applyStoryCopyResult(
|
|
105
|
+
storyId: string,
|
|
106
|
+
ok: boolean,
|
|
107
|
+
setCopyFeedback: Dispatch<SetStateAction<StoryCopyFeedback | null>>,
|
|
108
|
+
): void {
|
|
109
|
+
setCopyFeedback({ storyId, status: ok ? "copied" : "failed" });
|
|
110
|
+
globalThis.setTimeout(() => {
|
|
111
|
+
setCopyFeedback((prev) => (prev?.storyId === storyId ? null : prev));
|
|
112
|
+
}, 2500);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function stableStoryUuid(groupId: string, index: number): string {
|
|
116
|
+
const seed = `${groupId}:${index}`;
|
|
117
|
+
let hash = 2166136261;
|
|
118
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
119
|
+
hash ^= seed.codePointAt(i) ?? 0;
|
|
120
|
+
hash = Math.imul(hash, 16777619);
|
|
121
|
+
}
|
|
122
|
+
const toHex = (value: number) => (value >>> 0).toString(16).padStart(8, "0");
|
|
123
|
+
const part1 = toHex(hash);
|
|
124
|
+
const part2 = toHex(hash ^ 0x9e3779b9);
|
|
125
|
+
const part3Raw = toHex(hash ^ 0x85ebca6b).slice(0, 4);
|
|
126
|
+
const part4Raw = toHex(hash ^ 0xc2b2ae35).slice(0, 4);
|
|
127
|
+
const part5 = `${toHex(hash ^ 0x27d4eb2f)}${toHex(hash ^ 0x165667b1).slice(
|
|
128
|
+
0,
|
|
129
|
+
4,
|
|
130
|
+
)}`;
|
|
131
|
+
const part3 = `${(
|
|
132
|
+
(Number.parseInt(part3Raw[0] ?? "0", 16) & 0x0f) |
|
|
133
|
+
0x40
|
|
134
|
+
).toString(16)}${part3Raw.slice(1)}`;
|
|
135
|
+
const part4 = `${(
|
|
136
|
+
(Number.parseInt(part4Raw[0] ?? "0", 16) & 0x3) |
|
|
137
|
+
0x8
|
|
138
|
+
).toString(16)}${part4Raw.slice(1)}`;
|
|
139
|
+
return `${part1}-${part2.slice(0, 4)}-${part3}-${part4}-${part5}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ImplementationGlyph(
|
|
143
|
+
props: Readonly<{ implemented: boolean; discarded?: boolean }>,
|
|
144
|
+
) {
|
|
145
|
+
const { implemented, discarded } = props;
|
|
146
|
+
if (discarded) {
|
|
147
|
+
return (
|
|
148
|
+
<span className="inline-flex text-zinc-400 dark:text-zinc-500" aria-hidden>
|
|
149
|
+
<Ban className="size-4 shrink-0" strokeWidth={2.5} />
|
|
150
|
+
</span>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (implemented) {
|
|
154
|
+
return (
|
|
155
|
+
<span
|
|
156
|
+
className="inline-flex text-emerald-600 dark:text-emerald-500"
|
|
157
|
+
aria-hidden
|
|
158
|
+
>
|
|
159
|
+
<Check className="size-4 shrink-0" strokeWidth={2.5} />
|
|
160
|
+
</span>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return (
|
|
164
|
+
<span className="inline-flex text-red-600 dark:text-red-500" aria-hidden>
|
|
165
|
+
<X className="size-4 shrink-0" strokeWidth={2.5} />
|
|
166
|
+
</span>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function CoverageGlyph(props: Readonly<{ checked: boolean }>) {
|
|
171
|
+
const { checked } = props;
|
|
172
|
+
if (checked) {
|
|
173
|
+
return (
|
|
174
|
+
<span
|
|
175
|
+
className="inline-flex text-emerald-600 dark:text-emerald-500"
|
|
176
|
+
aria-hidden
|
|
177
|
+
>
|
|
178
|
+
<Check className="size-4 shrink-0" strokeWidth={2.5} />
|
|
179
|
+
</span>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return (
|
|
183
|
+
<span className="inline-flex text-red-600 dark:text-red-500" aria-hidden>
|
|
184
|
+
<X className="size-4 shrink-0" strokeWidth={2.5} />
|
|
185
|
+
</span>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function ImplementationStoryExpandedBlock(
|
|
190
|
+
props: Readonly<{
|
|
191
|
+
storyKey: string;
|
|
192
|
+
story: ImplementationUserStory;
|
|
193
|
+
notes: ImplementationNotesBundle;
|
|
194
|
+
}>,
|
|
195
|
+
) {
|
|
196
|
+
const { storyKey, story, notes } = props;
|
|
197
|
+
return (
|
|
198
|
+
<section
|
|
199
|
+
id={`implementation-story-extra-${storyKey}`}
|
|
200
|
+
aria-labelledby={`implementation-story-toggle-${storyKey}`}
|
|
201
|
+
className="border-l-2 border-violet-200 pl-3 text-[11px] leading-snug text-zinc-600 dark:border-violet-600/60 dark:text-zinc-400"
|
|
202
|
+
>
|
|
203
|
+
{story.detail ? <p>{story.detail}</p> : null}
|
|
204
|
+
{story.example ? (
|
|
205
|
+
<p className={story.detail ? "mt-2" : ""}>
|
|
206
|
+
<span className="font-medium text-zinc-700 dark:text-zinc-300">
|
|
207
|
+
{notes.storyDetailExampleLabel}
|
|
208
|
+
</span>{" "}
|
|
209
|
+
{story.example}
|
|
210
|
+
</p>
|
|
211
|
+
) : null}
|
|
212
|
+
</section>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function ImplementationStoryPrimaryCell(
|
|
217
|
+
props: Readonly<{
|
|
218
|
+
story: ImplementationUserStory;
|
|
219
|
+
storyKey: string;
|
|
220
|
+
expanded: boolean;
|
|
221
|
+
onToggle: () => void;
|
|
222
|
+
notes: ImplementationNotesBundle;
|
|
223
|
+
}>,
|
|
224
|
+
) {
|
|
225
|
+
const { story, storyKey, expanded, onToggle, notes } = props;
|
|
226
|
+
const hasExtra = Boolean(story.detail || story.example);
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<td className="py-2.5 pr-2 align-top leading-snug">
|
|
230
|
+
<div className="flex gap-1.5">
|
|
231
|
+
<span className="inline-flex size-7 shrink-0 items-start justify-center pt-0.5">
|
|
232
|
+
{hasExtra ? (
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
className="rounded-md p-0.5 text-zinc-500 transition hover:bg-zinc-200/90 hover:text-zinc-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:hover:text-zinc-100"
|
|
236
|
+
aria-expanded={expanded}
|
|
237
|
+
aria-controls={`implementation-story-extra-${storyKey}`}
|
|
238
|
+
id={`implementation-story-toggle-${storyKey}`}
|
|
239
|
+
onClick={onToggle}
|
|
240
|
+
aria-label={
|
|
241
|
+
expanded
|
|
242
|
+
? notes.storyDetailsToggleCollapse
|
|
243
|
+
: notes.storyDetailsToggleExpand
|
|
244
|
+
}
|
|
245
|
+
>
|
|
246
|
+
<ChevronDown
|
|
247
|
+
className={`size-4 shrink-0 transition-transform duration-150 ease-out motion-reduce:transition-none ${
|
|
248
|
+
expanded ? "rotate-180" : ""
|
|
249
|
+
}`}
|
|
250
|
+
aria-hidden
|
|
251
|
+
/>
|
|
252
|
+
</button>
|
|
253
|
+
) : null}
|
|
254
|
+
</span>
|
|
255
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
256
|
+
<div className="flex flex-wrap items-start gap-x-2 gap-y-1.5">
|
|
257
|
+
{story.discarded ? (
|
|
258
|
+
<span className="inline-flex shrink-0 rounded-full border border-zinc-300 bg-zinc-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-400">
|
|
259
|
+
{notes.storyDiscardedBadge}
|
|
260
|
+
</span>
|
|
261
|
+
) : null}
|
|
262
|
+
<p className="min-w-0 flex-1 text-sm text-zinc-800 dark:text-zinc-200">
|
|
263
|
+
{story.text}
|
|
264
|
+
</p>
|
|
265
|
+
</div>
|
|
266
|
+
{expanded && hasExtra ? (
|
|
267
|
+
<ImplementationStoryExpandedBlock
|
|
268
|
+
storyKey={storyKey}
|
|
269
|
+
story={story}
|
|
270
|
+
notes={notes}
|
|
271
|
+
/>
|
|
272
|
+
) : null}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
</td>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function ImplementationBody() {
|
|
280
|
+
const searchParams = useSearchParams();
|
|
281
|
+
const dashboardSessionNavId = searchParams.get("session");
|
|
282
|
+
const { payload, refresh } = useKronosysPayload();
|
|
283
|
+
const live = payload?.current as LiveShape | undefined;
|
|
284
|
+
const lang: Lang = live?.language === "fr" ? "fr" : "en";
|
|
285
|
+
const dt = dashboardStrings(lang);
|
|
286
|
+
const nav = useMemo(() => reportingNav(lang), [lang]);
|
|
287
|
+
const notes = useMemo(() => implementationNotesBundle(lang), [lang]);
|
|
288
|
+
const businessRules = useMemo(() => businessRulesMatrixBundle(lang), [lang]);
|
|
289
|
+
const [copyFeedback, setCopyFeedback] = useState<StoryCopyFeedback | null>(
|
|
290
|
+
null,
|
|
291
|
+
);
|
|
292
|
+
const [expandedStoryIds, setExpandedStoryIds] = useState<Set<string>>(
|
|
293
|
+
() => new Set(),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const toggleStoryExpanded = useCallback((storyKey: string) => {
|
|
297
|
+
setExpandedStoryIds((prev) => {
|
|
298
|
+
const next = new Set(prev);
|
|
299
|
+
if (next.has(storyKey)) {
|
|
300
|
+
next.delete(storyKey);
|
|
301
|
+
} else {
|
|
302
|
+
next.add(storyKey);
|
|
303
|
+
}
|
|
304
|
+
return next;
|
|
305
|
+
});
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
const postLang = async (next: Lang) => {
|
|
309
|
+
await postKronosysAction({ type: "setLanguage", lang: next });
|
|
310
|
+
await refresh();
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const copyStoryId = useCallback((storyId: string) => {
|
|
314
|
+
copyTextToClipboard(storyId).then((ok) =>
|
|
315
|
+
applyStoryCopyResult(storyId, ok, setCopyFeedback),
|
|
316
|
+
);
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
|
|
321
|
+
<header className={appShellHeaderClassName}>
|
|
322
|
+
<div className={appShellHeaderTitleMetaRowClassName}>
|
|
323
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
324
|
+
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
325
|
+
<Link
|
|
326
|
+
href={withDashboardSessionParam("/", dashboardSessionNavId)}
|
|
327
|
+
className="text-xl font-semibold tracking-tight text-zinc-900 hover:text-violet-700 dark:text-zinc-100 dark:hover:text-violet-300"
|
|
328
|
+
>
|
|
329
|
+
Kronosys
|
|
330
|
+
</Link>
|
|
331
|
+
<span className="text-zinc-400 dark:text-zinc-600" aria-hidden>
|
|
332
|
+
/
|
|
333
|
+
</span>
|
|
334
|
+
<span className="text-lg font-medium text-zinc-700 dark:text-zinc-300">
|
|
335
|
+
{notes.pageTitle}
|
|
336
|
+
</span>
|
|
337
|
+
<span
|
|
338
|
+
className="inline-flex items-center text-violet-500 dark:text-violet-400"
|
|
339
|
+
aria-hidden
|
|
340
|
+
>
|
|
341
|
+
<FileCode2 className="size-5" strokeWidth={2} />
|
|
342
|
+
</span>
|
|
343
|
+
</div>
|
|
344
|
+
<p className="flex flex-wrap items-center gap-x-2 text-xs font-medium leading-snug text-zinc-500 dark:text-zinc-400">
|
|
345
|
+
<span>{dt.brandTagline}</span>
|
|
346
|
+
<span className="text-zinc-400/70 dark:text-zinc-600" aria-hidden>
|
|
347
|
+
·
|
|
348
|
+
</span>
|
|
349
|
+
<AppVersionStamp ariaLabelTemplate={dt.appVersionAriaLabel} />
|
|
350
|
+
</p>
|
|
351
|
+
</div>
|
|
352
|
+
<AppShellHeaderSessionMeta payload={payload} dt={dt} />
|
|
353
|
+
</div>
|
|
354
|
+
<div className="flex w-full justify-end">
|
|
355
|
+
<div className={appShellHeaderToolbarClassName}>
|
|
356
|
+
<AppShellHeaderWallClock lang={lang} dt={dt} />
|
|
357
|
+
<AppShellCommandCenterPlaceholder />
|
|
358
|
+
<Link
|
|
359
|
+
href={withDashboardSessionParam(
|
|
360
|
+
"/changelog",
|
|
361
|
+
dashboardSessionNavId,
|
|
362
|
+
)}
|
|
363
|
+
className="inline-flex size-10 shrink-0 items-center justify-center rounded-lg border border-zinc-300 bg-white text-zinc-700 shadow-sm transition hover:border-zinc-400 hover:bg-zinc-50 hover:text-violet-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 dark:border-zinc-600 dark:bg-zinc-800/90 dark:text-zinc-200 dark:hover:border-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-violet-300"
|
|
364
|
+
aria-label={notes.openChangelogAria}
|
|
365
|
+
title={notes.openChangelogTooltip}
|
|
366
|
+
>
|
|
367
|
+
<ScrollText size={18} />
|
|
368
|
+
</Link>
|
|
369
|
+
<AppShellRouteNav
|
|
370
|
+
current="implementation"
|
|
371
|
+
labels={nav}
|
|
372
|
+
navAriaLabel={dt.appShellRouteNavAria}
|
|
373
|
+
dashboardSessionId={dashboardSessionNavId}
|
|
374
|
+
reserveGlobalPauseSlot
|
|
375
|
+
/>
|
|
376
|
+
<ThemeToggle lang={lang} />
|
|
377
|
+
<PageRefreshButton
|
|
378
|
+
title={dt.pageRefreshTitle}
|
|
379
|
+
ariaLabel={dt.pageRefreshAriaLabel}
|
|
380
|
+
inlineMessages={{
|
|
381
|
+
loading: dt.pageRefreshProgressLabel,
|
|
382
|
+
success: dt.pageRefreshDoneToast,
|
|
383
|
+
error: dt.pageRefreshFailedToast,
|
|
384
|
+
}}
|
|
385
|
+
onRefresh={async () => {
|
|
386
|
+
return await refresh({ routerInvalidate: true });
|
|
387
|
+
}}
|
|
388
|
+
/>
|
|
389
|
+
<LanguageMenu
|
|
390
|
+
lang={lang}
|
|
391
|
+
labelEn="English"
|
|
392
|
+
labelFr="Français"
|
|
393
|
+
menuHeading={lang === "fr" ? "Langue" : "Language"}
|
|
394
|
+
triggerAriaLabel={
|
|
395
|
+
lang === "fr" ? "Langue de l’interface" : "Interface language"
|
|
396
|
+
}
|
|
397
|
+
onSelect={(next) => void postLang(next)}
|
|
398
|
+
/>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
</header>
|
|
402
|
+
|
|
403
|
+
<main className="mx-auto w-full max-w-4xl px-5 pb-16 pt-6 sm:px-8 lg:px-10">
|
|
404
|
+
<p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
|
|
405
|
+
{notes.pageSubtitle}
|
|
406
|
+
</p>
|
|
407
|
+
<p className="mt-2 text-xs font-medium text-zinc-700 dark:text-zinc-300">
|
|
408
|
+
{notes.statusKeyLine}
|
|
409
|
+
</p>
|
|
410
|
+
<p className="mt-1 text-xs leading-snug text-zinc-500 dark:text-zinc-500">
|
|
411
|
+
{notes.storyDiscardedLegendLine}
|
|
412
|
+
</p>
|
|
413
|
+
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-500">
|
|
414
|
+
{notes.lastUpdated}
|
|
415
|
+
</p>
|
|
416
|
+
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-500">
|
|
417
|
+
{notes.repositoryDocLabel}
|
|
418
|
+
</p>
|
|
419
|
+
|
|
420
|
+
<section
|
|
421
|
+
className="mt-8 rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-800/60"
|
|
422
|
+
aria-labelledby="implementation-user-stories-heading"
|
|
423
|
+
>
|
|
424
|
+
<h2
|
|
425
|
+
id="implementation-user-stories-heading"
|
|
426
|
+
className="text-base font-semibold text-zinc-900 dark:text-zinc-100"
|
|
427
|
+
>
|
|
428
|
+
{notes.storyGroupsHeading}
|
|
429
|
+
</h2>
|
|
430
|
+
<p className="mt-2 text-xs leading-snug text-zinc-500 dark:text-zinc-500">
|
|
431
|
+
{notes.storyExpandHintLine}
|
|
432
|
+
</p>
|
|
433
|
+
<div className="mt-6 space-y-8">
|
|
434
|
+
{notes.storyGroups.map((group) => (
|
|
435
|
+
<section
|
|
436
|
+
key={group.id}
|
|
437
|
+
id={group.id}
|
|
438
|
+
className="rounded-lg border border-zinc-100 bg-zinc-50/80 p-3 dark:border-zinc-700/80 dark:bg-zinc-900/40"
|
|
439
|
+
aria-labelledby={`${group.id}-heading`}
|
|
440
|
+
>
|
|
441
|
+
<h3
|
|
442
|
+
id={`${group.id}-heading`}
|
|
443
|
+
className="text-sm font-semibold text-zinc-900 dark:text-zinc-100"
|
|
444
|
+
>
|
|
445
|
+
{group.title}
|
|
446
|
+
</h3>
|
|
447
|
+
{group.lead ? (
|
|
448
|
+
<p className="mt-1.5 text-xs leading-snug text-zinc-600 dark:text-zinc-400">
|
|
449
|
+
{group.lead}
|
|
450
|
+
</p>
|
|
451
|
+
) : null}
|
|
452
|
+
<div className="mt-3 overflow-x-auto">
|
|
453
|
+
<table className="w-full border-collapse text-left text-sm text-zinc-700 dark:text-zinc-300">
|
|
454
|
+
<caption className="sr-only">{group.title}</caption>
|
|
455
|
+
<thead>
|
|
456
|
+
<tr className="border-b border-zinc-200 text-xs uppercase tracking-wide text-zinc-500 dark:border-zinc-700/80 dark:text-zinc-400">
|
|
457
|
+
<th className="px-2 py-2 text-left font-semibold">
|
|
458
|
+
{notes.storyColumnLabel}
|
|
459
|
+
</th>
|
|
460
|
+
<th className="w-28 px-1 py-2 text-left font-semibold">
|
|
461
|
+
UUID
|
|
462
|
+
</th>
|
|
463
|
+
<th className="w-10 px-2 py-2 text-left font-semibold">
|
|
464
|
+
{notes.implementationColumnLabel}
|
|
465
|
+
</th>
|
|
466
|
+
<th className="w-16 px-1 py-2 text-center font-semibold">
|
|
467
|
+
{notes.integrationColumnLabel}
|
|
468
|
+
</th>
|
|
469
|
+
<th className="w-14 px-1 py-2 text-center font-semibold">
|
|
470
|
+
{notes.e2eColumnLabel}
|
|
471
|
+
</th>
|
|
472
|
+
</tr>
|
|
473
|
+
</thead>
|
|
474
|
+
<tbody>
|
|
475
|
+
{group.stories.map((story, idx) => {
|
|
476
|
+
const storyUuid = stableStoryUuid(group.id, idx);
|
|
477
|
+
const storyKey = `${group.id}-${idx}`;
|
|
478
|
+
const srMark = userStoryScreenReaderLabel(
|
|
479
|
+
lang,
|
|
480
|
+
story.implemented,
|
|
481
|
+
story.discarded,
|
|
482
|
+
);
|
|
483
|
+
return (
|
|
484
|
+
<tr
|
|
485
|
+
key={`${group.id}-${idx}`}
|
|
486
|
+
className={
|
|
487
|
+
idx === 0
|
|
488
|
+
? ""
|
|
489
|
+
: "border-t border-zinc-200 dark:border-zinc-700/80"
|
|
490
|
+
}
|
|
491
|
+
>
|
|
492
|
+
<ImplementationStoryPrimaryCell
|
|
493
|
+
story={story}
|
|
494
|
+
storyKey={storyKey}
|
|
495
|
+
expanded={expandedStoryIds.has(storyKey)}
|
|
496
|
+
onToggle={() => toggleStoryExpanded(storyKey)}
|
|
497
|
+
notes={notes}
|
|
498
|
+
/>
|
|
499
|
+
<td className="w-28 align-top px-1 py-2.5">
|
|
500
|
+
<button
|
|
501
|
+
type="button"
|
|
502
|
+
onClick={() => {
|
|
503
|
+
copyStoryId(storyUuid);
|
|
504
|
+
}}
|
|
505
|
+
className="inline-flex shrink-0 items-center gap-1 rounded-md border border-zinc-300 bg-white px-1.5 py-1 text-[10px] font-medium text-zinc-700 shadow-sm hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700/80"
|
|
506
|
+
aria-label={
|
|
507
|
+
lang === "fr"
|
|
508
|
+
? `Copier l’identifiant ${storyUuid}`
|
|
509
|
+
: `Copy identifier ${storyUuid}`
|
|
510
|
+
}
|
|
511
|
+
title={
|
|
512
|
+
lang === "fr"
|
|
513
|
+
? "Copier l’identifiant du point"
|
|
514
|
+
: "Copy story identifier"
|
|
515
|
+
}
|
|
516
|
+
>
|
|
517
|
+
<Copy className="size-3.5" />
|
|
518
|
+
<span className="font-mono leading-none">
|
|
519
|
+
{storyUuid.slice(0, 8)}
|
|
520
|
+
</span>
|
|
521
|
+
</button>
|
|
522
|
+
{copyFeedback?.storyId === storyUuid ? (
|
|
523
|
+
<p
|
|
524
|
+
className={
|
|
525
|
+
copyFeedback.status === "copied"
|
|
526
|
+
? "mt-1 text-[10px] text-emerald-600 dark:text-emerald-500"
|
|
527
|
+
: "mt-1 text-[10px] text-red-600 dark:text-red-500"
|
|
528
|
+
}
|
|
529
|
+
>
|
|
530
|
+
{implementationCopyFeedbackMessage(
|
|
531
|
+
lang,
|
|
532
|
+
copyFeedback.status,
|
|
533
|
+
)}
|
|
534
|
+
</p>
|
|
535
|
+
) : null}
|
|
536
|
+
</td>
|
|
537
|
+
<td className="w-10 align-top px-2 py-2.5">
|
|
538
|
+
<span className="sr-only">{srMark} </span>
|
|
539
|
+
<ImplementationGlyph
|
|
540
|
+
implemented={story.implemented}
|
|
541
|
+
discarded={story.discarded}
|
|
542
|
+
/>
|
|
543
|
+
</td>
|
|
544
|
+
<td className="w-16 shrink-0 align-top px-1 py-2.5 text-center">
|
|
545
|
+
<span className="sr-only">
|
|
546
|
+
{coverageScreenReaderLabel(
|
|
547
|
+
lang,
|
|
548
|
+
notes.integrationColumnLabel,
|
|
549
|
+
story.integrationChecked,
|
|
550
|
+
story.discarded,
|
|
551
|
+
)}
|
|
552
|
+
</span>
|
|
553
|
+
{story.discarded ? (
|
|
554
|
+
<span
|
|
555
|
+
className="text-zinc-400 dark:text-zinc-500"
|
|
556
|
+
aria-hidden
|
|
557
|
+
>
|
|
558
|
+
—
|
|
559
|
+
</span>
|
|
560
|
+
) : (
|
|
561
|
+
<CoverageGlyph
|
|
562
|
+
checked={story.integrationChecked === true}
|
|
563
|
+
/>
|
|
564
|
+
)}
|
|
565
|
+
</td>
|
|
566
|
+
<td className="w-14 shrink-0 align-top px-1 py-2.5 text-center">
|
|
567
|
+
<span className="sr-only">
|
|
568
|
+
{coverageScreenReaderLabel(
|
|
569
|
+
lang,
|
|
570
|
+
notes.e2eColumnLabel,
|
|
571
|
+
story.e2eChecked,
|
|
572
|
+
story.discarded,
|
|
573
|
+
)}
|
|
574
|
+
</span>
|
|
575
|
+
{story.discarded ? (
|
|
576
|
+
<span
|
|
577
|
+
className="text-zinc-400 dark:text-zinc-500"
|
|
578
|
+
aria-hidden
|
|
579
|
+
>
|
|
580
|
+
—
|
|
581
|
+
</span>
|
|
582
|
+
) : (
|
|
583
|
+
<CoverageGlyph
|
|
584
|
+
checked={story.e2eChecked === true}
|
|
585
|
+
/>
|
|
586
|
+
)}
|
|
587
|
+
</td>
|
|
588
|
+
</tr>
|
|
589
|
+
);
|
|
590
|
+
})}
|
|
591
|
+
</tbody>
|
|
592
|
+
</table>
|
|
593
|
+
</div>
|
|
594
|
+
</section>
|
|
595
|
+
))}
|
|
596
|
+
</div>
|
|
597
|
+
</section>
|
|
598
|
+
|
|
599
|
+
<section
|
|
600
|
+
className="mt-8 rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-800/60"
|
|
601
|
+
aria-labelledby="implementation-business-rules-heading"
|
|
602
|
+
>
|
|
603
|
+
<h2
|
|
604
|
+
id="implementation-business-rules-heading"
|
|
605
|
+
className="text-base font-semibold text-zinc-900 dark:text-zinc-100"
|
|
606
|
+
>
|
|
607
|
+
{businessRules.heading}
|
|
608
|
+
</h2>
|
|
609
|
+
<p className="mt-2 text-xs leading-snug text-zinc-600 dark:text-zinc-400">
|
|
610
|
+
{businessRules.subtitle}
|
|
611
|
+
</p>
|
|
612
|
+
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-500">
|
|
613
|
+
{businessRules.statusLegend}
|
|
614
|
+
</p>
|
|
615
|
+
<div className="mt-4 overflow-x-auto">
|
|
616
|
+
<table className="w-full border-collapse text-left text-xs text-zinc-700 dark:text-zinc-300">
|
|
617
|
+
<caption className="sr-only">{businessRules.heading}</caption>
|
|
618
|
+
<thead>
|
|
619
|
+
<tr className="border-b border-zinc-200 text-[11px] uppercase tracking-wide text-zinc-500 dark:border-zinc-700/80 dark:text-zinc-400">
|
|
620
|
+
<th className="px-2 py-2 text-left font-semibold">
|
|
621
|
+
{businessRules.columns.id}
|
|
622
|
+
</th>
|
|
623
|
+
<th className="px-2 py-2 text-left font-semibold">
|
|
624
|
+
{businessRules.columns.domain}
|
|
625
|
+
</th>
|
|
626
|
+
<th className="px-2 py-2 text-left font-semibold">
|
|
627
|
+
{businessRules.columns.rule}
|
|
628
|
+
</th>
|
|
629
|
+
<th className="px-2 py-2 text-left font-semibold">
|
|
630
|
+
{businessRules.columns.status}
|
|
631
|
+
</th>
|
|
632
|
+
<th className="px-2 py-2 text-left font-semibold">
|
|
633
|
+
{businessRules.columns.code}
|
|
634
|
+
</th>
|
|
635
|
+
<th className="px-2 py-2 text-left font-semibold">
|
|
636
|
+
{businessRules.columns.tests}
|
|
637
|
+
</th>
|
|
638
|
+
<th className="px-2 py-2 text-left font-semibold">
|
|
639
|
+
{businessRules.columns.note}
|
|
640
|
+
</th>
|
|
641
|
+
</tr>
|
|
642
|
+
</thead>
|
|
643
|
+
<tbody>
|
|
644
|
+
{businessRules.rows.map((row, idx) => (
|
|
645
|
+
<tr
|
|
646
|
+
key={row.id}
|
|
647
|
+
className={
|
|
648
|
+
idx === 0
|
|
649
|
+
? ""
|
|
650
|
+
: "border-t border-zinc-200 dark:border-zinc-700/80"
|
|
651
|
+
}
|
|
652
|
+
>
|
|
653
|
+
<td className="px-2 py-2 align-top font-mono text-[11px]">
|
|
654
|
+
{row.id}
|
|
655
|
+
</td>
|
|
656
|
+
<td className="px-2 py-2 align-top">{row.domain}</td>
|
|
657
|
+
<td className="px-2 py-2 align-top leading-snug">
|
|
658
|
+
{row.title}
|
|
659
|
+
</td>
|
|
660
|
+
<td className="px-2 py-2 align-top">
|
|
661
|
+
<span className="inline-flex rounded-full border border-zinc-300 bg-zinc-100 px-2 py-0.5 text-[11px] dark:border-zinc-600 dark:bg-zinc-700/60">
|
|
662
|
+
{businessRules.statuses[row.status]}
|
|
663
|
+
</span>
|
|
664
|
+
</td>
|
|
665
|
+
<td className="px-2 py-2 align-top leading-snug">
|
|
666
|
+
{row.codeRefs.join(", ")}
|
|
667
|
+
</td>
|
|
668
|
+
<td className="px-2 py-2 align-top leading-snug">
|
|
669
|
+
{row.testRefs.join(", ")}
|
|
670
|
+
</td>
|
|
671
|
+
<td className="px-2 py-2 align-top leading-snug">
|
|
672
|
+
{row.note ?? "—"}
|
|
673
|
+
</td>
|
|
674
|
+
</tr>
|
|
675
|
+
))}
|
|
676
|
+
</tbody>
|
|
677
|
+
</table>
|
|
678
|
+
</div>
|
|
679
|
+
</section>
|
|
680
|
+
</main>
|
|
681
|
+
<ScrollToTopFab
|
|
682
|
+
ariaLabel={lang === "fr" ? "Retour en haut de la page" : "Back to top"}
|
|
683
|
+
/>
|
|
684
|
+
</div>
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export default function ImplementationPage() {
|
|
689
|
+
return (
|
|
690
|
+
<Suspense
|
|
691
|
+
fallback={
|
|
692
|
+
<div className="min-h-screen bg-zinc-100 px-6 py-10 text-sm text-zinc-500 dark:bg-zinc-900">
|
|
693
|
+
Kronosys…
|
|
694
|
+
</div>
|
|
695
|
+
}
|
|
696
|
+
>
|
|
697
|
+
<ImplementationBody />
|
|
698
|
+
</Suspense>
|
|
699
|
+
);
|
|
700
|
+
}
|