@nightkatana/kronosys-app 1.0.0-beta.0
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 +81 -0
- package/app/api/action/route.ts +16 -0
- package/app/api/backup/route.ts +84 -0
- package/app/api/health/route.ts +22 -0
- package/app/api/state/route.ts +27 -0
- package/app/apple-icon.png +0 -0
- package/app/changelog/page.tsx +122 -0
- package/app/globals.css +210 -0
- package/app/guide/layout.tsx +11 -0
- package/app/guide/page.tsx +278 -0
- package/app/icon.png +0 -0
- package/app/layout.tsx +77 -0
- package/app/licenses/layout.tsx +11 -0
- package/app/licenses/page.tsx +246 -0
- package/app/manifest.ts +32 -0
- package/app/page.tsx +1610 -0
- package/app/reporting/page.tsx +2943 -0
- package/app/settings/layout.tsx +10 -0
- package/app/settings/page.tsx +3518 -0
- package/bin/kronosys.mjs +46 -0
- package/components/KronosysPackageVersionProvider.tsx +19 -0
- package/components/KronosysPayloadProvider.tsx +109 -0
- package/components/PwaRegister.tsx +25 -0
- package/components/SiteLegalFooter.tsx +21 -0
- package/components/ThemeProvider.tsx +78 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +394 -0
- package/components/dashboard/AppShellRouteNav.tsx +131 -0
- package/components/dashboard/AppVersionStamp.tsx +16 -0
- package/components/dashboard/DashboardCollapsibleSection.tsx +57 -0
- package/components/dashboard/DashboardColumnHintsBanner.tsx +159 -0
- package/components/dashboard/DashboardCommandCenter.tsx +470 -0
- package/components/dashboard/DashboardLangGateModal.tsx +118 -0
- package/components/dashboard/DashboardLoadingOverlay.tsx +42 -0
- package/components/dashboard/DashboardSimpleModal.tsx +337 -0
- package/components/dashboard/DashboardSuspenseFallback.tsx +52 -0
- package/components/dashboard/DashboardToastProvider.tsx +64 -0
- package/components/dashboard/DashboardTour.tsx +435 -0
- package/components/dashboard/DeferredDescriptionPopoverWrap.tsx +39 -0
- package/components/dashboard/DeleteSessionModal.tsx +130 -0
- package/components/dashboard/DescriptionTooltipPortaled.tsx +31 -0
- package/components/dashboard/GitIdentityQuickSetupModal.tsx +211 -0
- package/components/dashboard/HeaderIntegrationBadges.tsx +69 -0
- package/components/dashboard/InlineMetricHelpTrigger.tsx +102 -0
- package/components/dashboard/IssuePickerModal.tsx +168 -0
- package/components/dashboard/KronoFocusPanel.tsx +834 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +357 -0
- package/components/dashboard/KronosysTimePopoverField.tsx +233 -0
- package/components/dashboard/LanguageMenu.tsx +123 -0
- package/components/dashboard/MongoMirrorSyncLine.tsx +57 -0
- package/components/dashboard/NewSessionScopeModal.tsx +410 -0
- package/components/dashboard/PageRefreshButton.tsx +130 -0
- package/components/dashboard/PlainHelpPopover.tsx +97 -0
- package/components/dashboard/ReportingPageToc.tsx +68 -0
- package/components/dashboard/ReportingTour.tsx +342 -0
- package/components/dashboard/SavedProjectPicker.tsx +92 -0
- package/components/dashboard/SavedTagPicker.tsx +115 -0
- package/components/dashboard/ScrollToTopFab.tsx +41 -0
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +630 -0
- package/components/dashboard/SessionEndReasonEditor.tsx +114 -0
- package/components/dashboard/SessionListPanel.tsx +320 -0
- package/components/dashboard/SessionLocMetricsSection.tsx +128 -0
- package/components/dashboard/SettingsTagsProjectsSection.tsx +993 -0
- package/components/dashboard/SettingsTour.tsx +332 -0
- package/components/dashboard/TagPills.tsx +149 -0
- package/components/dashboard/TagsHelpTrigger.tsx +84 -0
- package/components/dashboard/TaskFocusPanel.tsx +1261 -0
- package/components/dashboard/TaskSessionLiveCard.tsx +832 -0
- package/components/dashboard/TaskSubtasksBlock.tsx +748 -0
- package/components/dashboard/ThemeToggle.test.tsx +26 -0
- package/components/dashboard/ThemeToggle.tsx +36 -0
- package/components/dashboard/UserGuideBodyText.tsx +62 -0
- package/components/dashboard/WorkspaceGitRepoCard.tsx +191 -0
- package/components/dashboard/taskFieldStyles.ts +139 -0
- package/components/dashboard/useAnchoredFloatingPortalStyle.ts +71 -0
- package/components/dashboard/useDescriptionPopoverAfterMs.ts +220 -0
- package/components/dashboard/useKronoFocusLiveSeconds.ts +36 -0
- package/components/dashboard/useSmoothStopwatchMs.ts +25 -0
- package/lib/appShellHeaderClasses.ts +12 -0
- package/lib/backupCsvExport.test.ts +149 -0
- package/lib/backupCsvExport.ts +392 -0
- package/lib/changelogCopy.ts +34 -0
- package/lib/concurrentTaskStartPreference.ts +29 -0
- package/lib/dashboardClockFormat.ts +13 -0
- package/lib/dashboardColumnChrome.ts +3 -0
- package/lib/dashboardColumnHintsStorage.ts +57 -0
- package/lib/dashboardCopy.ts +1831 -0
- package/lib/dashboardDetachedUrlHintStorage.ts +24 -0
- package/lib/dashboardGitIdentityBannerStorage.ts +36 -0
- package/lib/dashboardLangStorage.ts +72 -0
- package/lib/dashboardQuickSearch.ts +476 -0
- package/lib/dashboardQuickSearchQuery.test.ts +63 -0
- package/lib/dashboardQuickSearchQuery.ts +179 -0
- package/lib/dashboardSessionNav.ts +33 -0
- package/lib/dashboardShortcuts.ts +268 -0
- package/lib/dashboardTimeZone.ts +91 -0
- package/lib/dashboardTourStorage.ts +68 -0
- package/lib/dataDir.test.ts +87 -0
- package/lib/dataDir.ts +83 -0
- package/lib/devDataPreferenceFile.ts +55 -0
- package/lib/devDataRuntimeInfo.ts +34 -0
- package/lib/formatIsoShort.test.ts +46 -0
- package/lib/formatIsoShort.ts +29 -0
- package/lib/generatedUserChangelog.ts +34 -0
- package/lib/gitlabIssueSearch.ts +8 -0
- package/lib/kronoFocusDurationHistory.ts +71 -0
- package/lib/kronoFocusRhythm.test.ts +130 -0
- package/lib/kronoFocusRhythm.ts +46 -0
- package/lib/kronoFocusTimerUrgency.test.ts +74 -0
- package/lib/kronoFocusTimerUrgency.ts +24 -0
- package/lib/kronosysApi.ts +143 -0
- package/lib/legacyEditorPayloadKeys.ts +52 -0
- package/lib/legacyKronoFocusStorageKeys.test.ts +29 -0
- package/lib/legacyKronoFocusStorageKeys.ts +32 -0
- package/lib/licensesCopy.ts +128 -0
- package/lib/openPlainTextInNewTab.ts +49 -0
- package/lib/readKronosysPackageVersion.ts +10 -0
- package/lib/reportingAggregate.test.ts +325 -0
- package/lib/reportingAggregate.ts +819 -0
- package/lib/reportingDatePresets.ts +41 -0
- package/lib/reportingMetricHelp.ts +430 -0
- package/lib/reportingNonFinalIndicators.test.ts +157 -0
- package/lib/reportingNonFinalIndicators.ts +102 -0
- package/lib/reportingStrings.ts +491 -0
- package/lib/reportingTagWeekBreakdown.test.ts +141 -0
- package/lib/reportingTagWeekBreakdown.ts +181 -0
- package/lib/reportingWeekLayout.test.ts +239 -0
- package/lib/reportingWeekLayout.ts +313 -0
- package/lib/sessionAssiduity.test.ts +25 -0
- package/lib/sessionAssiduity.ts +33 -0
- package/lib/sessionEndReason.ts +55 -0
- package/lib/sessionEndWarnings.test.ts +200 -0
- package/lib/sessionEndWarnings.ts +125 -0
- package/lib/sessionListMerge.test.ts +101 -0
- package/lib/sessionListMerge.ts +70 -0
- package/lib/sessionTaskSidebarStats.test.ts +24 -0
- package/lib/sessionTaskSidebarStats.ts +54 -0
- package/lib/settingsCopy.ts +1276 -0
- package/lib/taskParsing.test.ts +153 -0
- package/lib/taskParsing.ts +737 -0
- package/lib/theme.ts +15 -0
- package/lib/translucentButtonClasses.ts +34 -0
- package/lib/usageProfile.test.ts +84 -0
- package/lib/usageProfile.ts +52 -0
- package/lib/userGuideCopy.ts +464 -0
- package/lib/workspaceLocDefaults.ts +21 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +15 -0
- package/package.json +87 -0
- package/postcss.config.mjs +12 -0
- package/public/apple-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.png +0 -0
- package/public/next.svg +1 -0
- package/public/sw.js +13 -0
- package/public/traceback.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/actionDispatch.test.ts +723 -0
- package/server/actionDispatch.ts +1476 -0
- package/server/actionTaskSession.test.ts +713 -0
- package/server/actionTaskSession.ts +717 -0
- package/server/db.ts +42 -0
- package/server/defaultCfg.ts +87 -0
- package/server/gitlabTokenStore.ts +34 -0
- package/server/kronoFocusHydrate.test.ts +142 -0
- package/server/kronoFocusHydrate.ts +69 -0
- package/server/kronoFocusMigrate.test.ts +53 -0
- package/server/kronoFocusMigrate.ts +78 -0
- package/server/mainTimerHydrate.test.ts +65 -0
- package/server/mainTimerHydrate.ts +53 -0
- package/server/payloadStore.test.ts +78 -0
- package/server/payloadStore.ts +83 -0
- package/server/sessionWallHydrate.test.ts +46 -0
- package/server/sessionWallHydrate.ts +88 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { strToU8, zipSync } from "fflate";
|
|
2
|
+
|
|
3
|
+
import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
|
|
4
|
+
import { collectTasksDeduped, type LooseSession } from "@/lib/reportingAggregate";
|
|
5
|
+
|
|
6
|
+
/** Parties CSV téléchargeables (avant effacement complet ou pour archivage). */
|
|
7
|
+
export type CsvBackupTable =
|
|
8
|
+
| "sessions"
|
|
9
|
+
| "tasks"
|
|
10
|
+
| "subtasks"
|
|
11
|
+
| "tag_project_registry"
|
|
12
|
+
| "tag_descriptions"
|
|
13
|
+
| "project_descriptions"
|
|
14
|
+
| "git_identity"
|
|
15
|
+
| "store_json_blobs";
|
|
16
|
+
|
|
17
|
+
export const CSV_BACKUP_TABLES: readonly CsvBackupTable[] = [
|
|
18
|
+
"sessions",
|
|
19
|
+
"tasks",
|
|
20
|
+
"subtasks",
|
|
21
|
+
"tag_project_registry",
|
|
22
|
+
"tag_descriptions",
|
|
23
|
+
"project_descriptions",
|
|
24
|
+
"git_identity",
|
|
25
|
+
"store_json_blobs",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export function isCsvBackupTable(s: string): s is CsvBackupTable {
|
|
29
|
+
return (CSV_BACKUP_TABLES as readonly string[]).includes(s);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function csvEscapeCell(cell: string): string {
|
|
33
|
+
if (/[",\n\r]/.test(cell)) {
|
|
34
|
+
return `"${cell.replace(/"/g, '""')}"`;
|
|
35
|
+
}
|
|
36
|
+
return cell;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function csvLine(cells: string[]): string {
|
|
40
|
+
return cells.map(csvEscapeCell).join(",");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function asSessionRecord(raw: unknown): Record<string, unknown> | null {
|
|
44
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const o = raw as Record<string, unknown>;
|
|
48
|
+
return typeof o.sessionId === "string" && o.sessionId.trim() !== "" ? o : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type SessionWalk = {
|
|
52
|
+
session: Record<string, unknown>;
|
|
53
|
+
/** Emplacement dans le payload (la session courante peut être `archived: true`). */
|
|
54
|
+
source: "history" | "historyArchived" | "current";
|
|
55
|
+
/** Indique si la session est dans la liste archives ou marquée archivée en direct. */
|
|
56
|
+
archivedList: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function historyList(payload: KronosysUpdatePayload): unknown[] {
|
|
60
|
+
return Array.isArray(payload.history) ? payload.history : [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function historyArchivedList(payload: KronosysUpdatePayload): unknown[] {
|
|
64
|
+
return Array.isArray(payload.historyArchived) ? payload.historyArchived : [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function* walkSessions(payload: KronosysUpdatePayload): Generator<SessionWalk> {
|
|
68
|
+
for (const h of historyList(payload)) {
|
|
69
|
+
const s = asSessionRecord(h);
|
|
70
|
+
if (s) {
|
|
71
|
+
yield { session: s, source: "history", archivedList: false };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const h of historyArchivedList(payload)) {
|
|
75
|
+
const s = asSessionRecord(h);
|
|
76
|
+
if (s) {
|
|
77
|
+
yield { session: s, source: "historyArchived", archivedList: true };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const cur = asSessionRecord(payload.current);
|
|
81
|
+
if (cur) {
|
|
82
|
+
yield {
|
|
83
|
+
session: cur,
|
|
84
|
+
source: "current",
|
|
85
|
+
archivedList: cur.archived === true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function taskCountDeduped(sess: LooseSession): number {
|
|
91
|
+
return collectTasksDeduped(sess).length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function jsonCell(v: unknown): string {
|
|
95
|
+
return csvEscapeCell(JSON.stringify(v ?? null));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildSessionsBackupCsv(payload: KronosysUpdatePayload): string {
|
|
99
|
+
const headers = [
|
|
100
|
+
"sessionId",
|
|
101
|
+
"sessionName",
|
|
102
|
+
"startAt",
|
|
103
|
+
"scheduledStartAt",
|
|
104
|
+
"sessionStartOffsetMinutes",
|
|
105
|
+
"endAt",
|
|
106
|
+
"savedAt",
|
|
107
|
+
"archived",
|
|
108
|
+
"sessionSource",
|
|
109
|
+
"sessionDurationMinutes",
|
|
110
|
+
"sessionEndReasonKind",
|
|
111
|
+
"sessionEndReasonNote",
|
|
112
|
+
"codingMinutesSession",
|
|
113
|
+
"activeMinutes",
|
|
114
|
+
"linesWrittenTotal",
|
|
115
|
+
"linesWrittenHuman",
|
|
116
|
+
"linesWrittenAi",
|
|
117
|
+
"mongoPushedAt",
|
|
118
|
+
"mongoLastPushedSavedAt",
|
|
119
|
+
"kronoFocusSessionsCompleted",
|
|
120
|
+
"taskCountDeduped",
|
|
121
|
+
"locByLanguageJson",
|
|
122
|
+
"sessionJson",
|
|
123
|
+
];
|
|
124
|
+
const lines = [csvLine(headers)];
|
|
125
|
+
for (const { session: h, source, archivedList } of walkSessions(payload)) {
|
|
126
|
+
const sess = h as LooseSession;
|
|
127
|
+
const kf = h.kronoFocus as Record<string, unknown> | undefined;
|
|
128
|
+
const kfDone = typeof kf?.sessionsCompleted === "number" ? kf.sessionsCompleted : "";
|
|
129
|
+
lines.push(
|
|
130
|
+
csvLine([
|
|
131
|
+
String(h.sessionId ?? ""),
|
|
132
|
+
String(h.sessionName ?? ""),
|
|
133
|
+
String(h.startAt ?? ""),
|
|
134
|
+
String(h.scheduledStartAt ?? ""),
|
|
135
|
+
typeof h.sessionStartOffsetMinutes === "number" && Number.isFinite(h.sessionStartOffsetMinutes)
|
|
136
|
+
? String(h.sessionStartOffsetMinutes)
|
|
137
|
+
: "",
|
|
138
|
+
String(h.endAt ?? ""),
|
|
139
|
+
String(h.savedAt ?? ""),
|
|
140
|
+
archivedList ? "yes" : "no",
|
|
141
|
+
source,
|
|
142
|
+
typeof h.sessionDurationMinutes === "number" && Number.isFinite(h.sessionDurationMinutes)
|
|
143
|
+
? String(h.sessionDurationMinutes)
|
|
144
|
+
: "",
|
|
145
|
+
String(h.sessionEndReasonKind ?? ""),
|
|
146
|
+
String(h.sessionEndReasonNote ?? ""),
|
|
147
|
+
typeof h.codingMinutesSession === "number" && Number.isFinite(h.codingMinutesSession)
|
|
148
|
+
? String(h.codingMinutesSession)
|
|
149
|
+
: "",
|
|
150
|
+
typeof h.activeMinutes === "number" && Number.isFinite(h.activeMinutes) ? String(h.activeMinutes) : "",
|
|
151
|
+
typeof h.linesWrittenTotal === "number" && Number.isFinite(h.linesWrittenTotal)
|
|
152
|
+
? String(h.linesWrittenTotal)
|
|
153
|
+
: "",
|
|
154
|
+
typeof h.linesWrittenHuman === "number" && Number.isFinite(h.linesWrittenHuman)
|
|
155
|
+
? String(h.linesWrittenHuman)
|
|
156
|
+
: "",
|
|
157
|
+
typeof h.linesWrittenAi === "number" && Number.isFinite(h.linesWrittenAi) ? String(h.linesWrittenAi) : "",
|
|
158
|
+
String(h.mongoPushedAt ?? ""),
|
|
159
|
+
String(h.mongoLastPushedSavedAt ?? ""),
|
|
160
|
+
String(kfDone),
|
|
161
|
+
String(taskCountDeduped(sess)),
|
|
162
|
+
JSON.stringify(h.locByLanguage ?? null),
|
|
163
|
+
JSON.stringify(h),
|
|
164
|
+
])
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function buildTasksBackupCsv(payload: KronosysUpdatePayload): string {
|
|
171
|
+
const headers = [
|
|
172
|
+
"sessionId",
|
|
173
|
+
"sessionSource",
|
|
174
|
+
"sessionArchived",
|
|
175
|
+
"taskId",
|
|
176
|
+
"name",
|
|
177
|
+
"project",
|
|
178
|
+
"tagsJson",
|
|
179
|
+
"isDone",
|
|
180
|
+
"durationMs",
|
|
181
|
+
"startTime",
|
|
182
|
+
"endTime",
|
|
183
|
+
"manualTaskTimerPaused",
|
|
184
|
+
"shouldCommit",
|
|
185
|
+
"usedKronoFocus",
|
|
186
|
+
"kronoFocusCycles",
|
|
187
|
+
"activeSubtaskTimerId",
|
|
188
|
+
"taskJson",
|
|
189
|
+
];
|
|
190
|
+
const lines = [csvLine(headers)];
|
|
191
|
+
for (const { session: h, source, archivedList } of walkSessions(payload)) {
|
|
192
|
+
const sess = h as LooseSession;
|
|
193
|
+
for (const t of collectTasksDeduped(sess)) {
|
|
194
|
+
const tr = t as Record<string, unknown>;
|
|
195
|
+
lines.push(
|
|
196
|
+
csvLine([
|
|
197
|
+
String(h.sessionId ?? ""),
|
|
198
|
+
source,
|
|
199
|
+
archivedList ? "yes" : "no",
|
|
200
|
+
String(tr.id ?? ""),
|
|
201
|
+
String(tr.name ?? ""),
|
|
202
|
+
tr.project === null || tr.project === undefined ? "" : String(tr.project),
|
|
203
|
+
JSON.stringify(tr.tags ?? []),
|
|
204
|
+
tr.isDone === true ? "yes" : tr.isDone === false ? "no" : "",
|
|
205
|
+
typeof tr.durationMs === "number" && Number.isFinite(tr.durationMs) ? String(tr.durationMs) : "",
|
|
206
|
+
String(tr.startTime ?? ""),
|
|
207
|
+
String(tr.endTime ?? ""),
|
|
208
|
+
tr.manualTaskTimerPaused === true ? "yes" : tr.manualTaskTimerPaused === false ? "no" : "",
|
|
209
|
+
tr.shouldCommit === true ? "yes" : tr.shouldCommit === false ? "no" : "",
|
|
210
|
+
tr.usedKronoFocus === true ? "yes" : tr.usedKronoFocus === false ? "no" : "",
|
|
211
|
+
typeof tr.kronoFocusCycles === "number" && Number.isFinite(tr.kronoFocusCycles)
|
|
212
|
+
? String(tr.kronoFocusCycles)
|
|
213
|
+
: "",
|
|
214
|
+
String(tr.activeSubtaskTimerId ?? ""),
|
|
215
|
+
JSON.stringify(tr),
|
|
216
|
+
])
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function buildSubtasksBackupCsv(payload: KronosysUpdatePayload): string {
|
|
224
|
+
const headers = [
|
|
225
|
+
"sessionId",
|
|
226
|
+
"sessionSource",
|
|
227
|
+
"sessionArchived",
|
|
228
|
+
"parentTaskId",
|
|
229
|
+
"subtaskId",
|
|
230
|
+
"title",
|
|
231
|
+
"done",
|
|
232
|
+
"durationMs",
|
|
233
|
+
"subtaskJson",
|
|
234
|
+
];
|
|
235
|
+
const lines = [csvLine(headers)];
|
|
236
|
+
for (const { session: h, source, archivedList } of walkSessions(payload)) {
|
|
237
|
+
const sess = h as LooseSession;
|
|
238
|
+
for (const t of collectTasksDeduped(sess)) {
|
|
239
|
+
const tr = t as Record<string, unknown>;
|
|
240
|
+
const tid = String(tr.id ?? "");
|
|
241
|
+
const subs = Array.isArray(tr.subtasks) ? tr.subtasks : [];
|
|
242
|
+
for (const raw of subs) {
|
|
243
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const st = raw as Record<string, unknown>;
|
|
247
|
+
lines.push(
|
|
248
|
+
csvLine([
|
|
249
|
+
String(h.sessionId ?? ""),
|
|
250
|
+
source,
|
|
251
|
+
archivedList ? "yes" : "no",
|
|
252
|
+
tid,
|
|
253
|
+
String(st.id ?? ""),
|
|
254
|
+
String(st.title ?? st.name ?? ""),
|
|
255
|
+
st.done === true ? "yes" : st.done === false ? "no" : "",
|
|
256
|
+
typeof st.durationMs === "number" && Number.isFinite(st.durationMs) ? String(st.durationMs) : "",
|
|
257
|
+
JSON.stringify(st),
|
|
258
|
+
])
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return lines.join("\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function buildTagProjectRegistryCsv(payload: KronosysUpdatePayload): string {
|
|
267
|
+
const headers = ["listKind", "value"];
|
|
268
|
+
const lines = [csvLine(headers)];
|
|
269
|
+
const push = (listKind: string, value: string) => {
|
|
270
|
+
const v = value.trim();
|
|
271
|
+
if (v) {
|
|
272
|
+
lines.push(csvLine([listKind, v]));
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
for (const t of payload.knownTags || []) {
|
|
276
|
+
push("known_tag", String(t));
|
|
277
|
+
}
|
|
278
|
+
for (const p of payload.knownProjects || []) {
|
|
279
|
+
push("known_project", String(p));
|
|
280
|
+
}
|
|
281
|
+
for (const t of payload.userKnownTags || []) {
|
|
282
|
+
push("user_known_tag", String(t));
|
|
283
|
+
}
|
|
284
|
+
for (const t of payload.excludedSuggestionTags || []) {
|
|
285
|
+
push("excluded_suggestion_tag", String(t));
|
|
286
|
+
}
|
|
287
|
+
return lines.join("\n");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function buildTagDescriptionsCsv(payload: KronosysUpdatePayload): string {
|
|
291
|
+
const headers = ["tagKey", "description"];
|
|
292
|
+
const lines = [csvLine(headers)];
|
|
293
|
+
const td = payload.tagDescriptions;
|
|
294
|
+
if (td && typeof td === "object") {
|
|
295
|
+
for (const [k, v] of Object.entries(td)) {
|
|
296
|
+
if (typeof v === "string") {
|
|
297
|
+
lines.push(csvLine([k, v]));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return lines.join("\n");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function buildProjectDescriptionsCsv(payload: KronosysUpdatePayload): string {
|
|
305
|
+
const headers = ["projectKey", "description"];
|
|
306
|
+
const lines = [csvLine(headers)];
|
|
307
|
+
const pd = payload.projectDescriptions;
|
|
308
|
+
if (pd && typeof pd === "object") {
|
|
309
|
+
for (const [k, v] of Object.entries(pd)) {
|
|
310
|
+
if (typeof v === "string") {
|
|
311
|
+
lines.push(csvLine([k, v]));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return lines.join("\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function buildGitIdentityCsv(payload: KronosysUpdatePayload): string {
|
|
319
|
+
const headers = ["gitUserName", "gitUserEmail", "gitAccountLogin"];
|
|
320
|
+
const g = payload.gitIdentity;
|
|
321
|
+
const row =
|
|
322
|
+
g && typeof g === "object"
|
|
323
|
+
? [
|
|
324
|
+
String(g.gitUserName ?? ""),
|
|
325
|
+
String(g.gitUserEmail ?? ""),
|
|
326
|
+
String(g.gitAccountLogin ?? ""),
|
|
327
|
+
]
|
|
328
|
+
: ["", "", ""];
|
|
329
|
+
return [csvLine(headers), csvLine(row)].join("\n");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function buildStoreJsonBlobsCsv(payload: KronosysUpdatePayload): string {
|
|
333
|
+
const headers = ["blobKind", "json"];
|
|
334
|
+
const lines = [csvLine(headers)];
|
|
335
|
+
const push = (kind: string, data: unknown) => {
|
|
336
|
+
lines.push(csvLine([kind, JSON.stringify(data ?? null)]));
|
|
337
|
+
};
|
|
338
|
+
push("cfg", payload.cfg ?? null);
|
|
339
|
+
push("gitStats", payload.gitStats ?? null);
|
|
340
|
+
push("workspaceCodeSnapshot", payload.workspaceCodeSnapshot ?? null);
|
|
341
|
+
push(
|
|
342
|
+
"dashboardTopLevel",
|
|
343
|
+
{
|
|
344
|
+
inspectingSessionId: payload.inspectingSessionId ?? null,
|
|
345
|
+
dismissArchiveSessionConfirm: payload.dismissArchiveSessionConfirm ?? null,
|
|
346
|
+
dashboardDataOrigin: payload.dashboardDataOrigin ?? null,
|
|
347
|
+
viewType: payload.viewType ?? null,
|
|
348
|
+
workspaceFolderPaths: payload.workspaceFolderPaths ?? null,
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
return lines.join("\n");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function buildCsvBackupPart(table: CsvBackupTable, payload: KronosysUpdatePayload): string {
|
|
355
|
+
switch (table) {
|
|
356
|
+
case "sessions":
|
|
357
|
+
return buildSessionsBackupCsv(payload);
|
|
358
|
+
case "tasks":
|
|
359
|
+
return buildTasksBackupCsv(payload);
|
|
360
|
+
case "subtasks":
|
|
361
|
+
return buildSubtasksBackupCsv(payload);
|
|
362
|
+
case "tag_project_registry":
|
|
363
|
+
return buildTagProjectRegistryCsv(payload);
|
|
364
|
+
case "tag_descriptions":
|
|
365
|
+
return buildTagDescriptionsCsv(payload);
|
|
366
|
+
case "project_descriptions":
|
|
367
|
+
return buildProjectDescriptionsCsv(payload);
|
|
368
|
+
case "git_identity":
|
|
369
|
+
return buildGitIdentityCsv(payload);
|
|
370
|
+
case "store_json_blobs":
|
|
371
|
+
return buildStoreJsonBlobsCsv(payload);
|
|
372
|
+
default:
|
|
373
|
+
throw new Error(`unknown CSV table: ${String(table)}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function csvBackupFilename(table: CsvBackupTable, stamp: string): string {
|
|
378
|
+
return `kronosys-${table}-${stamp}.csv`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Archive ZIP contenant les huit exportations CSV (UTF-8), pour un seul téléchargement. */
|
|
382
|
+
export function buildCsvBackupZipSync(payload: KronosysUpdatePayload, stamp: string): Uint8Array {
|
|
383
|
+
const files: Record<string, Uint8Array> = {};
|
|
384
|
+
for (const table of CSV_BACKUP_TABLES) {
|
|
385
|
+
files[csvBackupFilename(table, stamp)] = strToU8(buildCsvBackupPart(table, payload), false);
|
|
386
|
+
}
|
|
387
|
+
return zipSync(files, { level: 6 });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function csvBackupZipFilename(stamp: string): string {
|
|
391
|
+
return `kronosys-csv-backup-${stamp}.zip`;
|
|
392
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Lang } from "@/lib/dashboardCopy";
|
|
2
|
+
import { USER_CHANGELOG_ENTRIES } from "@/lib/generatedUserChangelog";
|
|
3
|
+
|
|
4
|
+
type ChangelogEntry = {
|
|
5
|
+
version: string;
|
|
6
|
+
items: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type ChangelogBundle = {
|
|
10
|
+
title: string;
|
|
11
|
+
subtitle: string;
|
|
12
|
+
empty: string;
|
|
13
|
+
entries: ChangelogEntry[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const enBundle: ChangelogBundle = {
|
|
17
|
+
title: "User CHANGELOG",
|
|
18
|
+
subtitle:
|
|
19
|
+
"This page lists user-visible changes after each software update.",
|
|
20
|
+
empty: "No changelog entry is available yet.",
|
|
21
|
+
entries: USER_CHANGELOG_ENTRIES,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const frBundle: ChangelogBundle = {
|
|
25
|
+
title: "CHANGELOG usager",
|
|
26
|
+
subtitle:
|
|
27
|
+
"Cette page liste les changements visibles côté utilisateur après chaque mise à jour du logiciel.",
|
|
28
|
+
empty: "Aucune entrée de changelog n’est encore disponible.",
|
|
29
|
+
entries: USER_CHANGELOG_ENTRIES,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function changelogBundle(lang: Lang): ChangelogBundle {
|
|
33
|
+
return lang === "fr" ? frBundle : enBundle;
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const STORAGE_KEY = "kronosys.concurrentTaskStartPreference";
|
|
2
|
+
|
|
3
|
+
export type ConcurrentTaskStartPreference = "pause" | "finish" | "parallel";
|
|
4
|
+
|
|
5
|
+
export function readConcurrentTaskStartPreference(): ConcurrentTaskStartPreference | null {
|
|
6
|
+
if (typeof window === "undefined") {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const v = window.localStorage.getItem(STORAGE_KEY);
|
|
11
|
+
if (v === "pause" || v === "finish" || v === "parallel") {
|
|
12
|
+
return v;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function writeConcurrentTaskStartPreference(mode: ConcurrentTaskStartPreference): void {
|
|
21
|
+
if (typeof window === "undefined") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
window.localStorage.setItem(STORAGE_KEY, mode);
|
|
26
|
+
} catch {
|
|
27
|
+
// quota / private mode
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const CFG_KEY = "dashboardUse24HourClock";
|
|
2
|
+
|
|
3
|
+
/** `true` : affichage sur 24 h ; `false` : horloge 12 h (AM/PM). Défaut : 24 h. */
|
|
4
|
+
export function readDashboardUse24HourClockFromCfg(cfg: Record<string, unknown> | undefined): boolean {
|
|
5
|
+
const v = cfg?.[CFG_KEY];
|
|
6
|
+
if (v === false) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
if (v === true) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** L’utilisateur a replié l’aide des colonnes du tableau de bord (ne plus l’afficher par défaut). */
|
|
2
|
+
export const DASHBOARD_COLUMN_HINTS_CLOSED_KEY = "kronosys.dashboard.columnHints.closed.v1";
|
|
3
|
+
|
|
4
|
+
/** L’utilisateur a masqué tout le bandeau d’aide (ne plus l’afficher du tout). */
|
|
5
|
+
export const DASHBOARD_COLUMN_HINTS_DISMISSED_KEY = "kronosys.dashboard.columnHints.dismissed.v1";
|
|
6
|
+
|
|
7
|
+
export function readDashboardColumnHintsClosed(): boolean {
|
|
8
|
+
if (typeof window === "undefined") {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return window.localStorage.getItem(DASHBOARD_COLUMN_HINTS_CLOSED_KEY) === "1";
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function writeDashboardColumnHintsClosed(closed: boolean): void {
|
|
19
|
+
if (typeof window === "undefined") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
if (closed) {
|
|
24
|
+
window.localStorage.setItem(DASHBOARD_COLUMN_HINTS_CLOSED_KEY, "1");
|
|
25
|
+
} else {
|
|
26
|
+
window.localStorage.removeItem(DASHBOARD_COLUMN_HINTS_CLOSED_KEY);
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
/* ignore */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readDashboardColumnHintsDismissed(): boolean {
|
|
34
|
+
if (typeof window === "undefined") {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return window.localStorage.getItem(DASHBOARD_COLUMN_HINTS_DISMISSED_KEY) === "1";
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function writeDashboardColumnHintsDismissed(dismissed: boolean): void {
|
|
45
|
+
if (typeof window === "undefined") {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
if (dismissed) {
|
|
50
|
+
window.localStorage.setItem(DASHBOARD_COLUMN_HINTS_DISMISSED_KEY, "1");
|
|
51
|
+
} else {
|
|
52
|
+
window.localStorage.removeItem(DASHBOARD_COLUMN_HINTS_DISMISSED_KEY);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
/* ignore */
|
|
56
|
+
}
|
|
57
|
+
}
|