@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,74 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { getKronoFocusTimerUrgency } from "./kronoFocusTimerUrgency";
|
|
4
|
+
|
|
5
|
+
describe("getKronoFocusTimerUrgency", () => {
|
|
6
|
+
it("retourne false/false si status est idle", () => {
|
|
7
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 0, mode: "work", status: "idle" }))
|
|
8
|
+
.toEqual({ blink: false, urgentHighlight: false });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("retourne false/false si status est idle (longBreak)", () => {
|
|
12
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 10, mode: "longBreak", status: "idle" }))
|
|
13
|
+
.toEqual({ blink: false, urgentHighlight: false });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// --- mode work ---
|
|
17
|
+
it("work running — plus de 5min → pas d'urgence", () => {
|
|
18
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 301, mode: "work", status: "running" }))
|
|
19
|
+
.toEqual({ blink: false, urgentHighlight: false });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("work running — exactement 5min → blink=true, urgentHighlight=false", () => {
|
|
23
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 300, mode: "work", status: "running" }))
|
|
24
|
+
.toEqual({ blink: true, urgentHighlight: false });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("work running — moins de 5min → blink=true, urgentHighlight=false", () => {
|
|
28
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 120, mode: "work", status: "running" }))
|
|
29
|
+
.toEqual({ blink: true, urgentHighlight: false });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("work running — exactement 30s → blink=true, urgentHighlight=true", () => {
|
|
33
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 30, mode: "work", status: "running" }))
|
|
34
|
+
.toEqual({ blink: true, urgentHighlight: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("work running — 0s → blink=true, urgentHighlight=true", () => {
|
|
38
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 0, mode: "work", status: "running" }))
|
|
39
|
+
.toEqual({ blink: true, urgentHighlight: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// --- mode break (pause courte — pas de règle 5min) ---
|
|
43
|
+
it("break running — 300s → blink=false (règle 5min ne s'applique pas)", () => {
|
|
44
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 300, mode: "break", status: "running" }))
|
|
45
|
+
.toEqual({ blink: false, urgentHighlight: false });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("break running — 30s → blink=true, urgentHighlight=true", () => {
|
|
49
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 30, mode: "break", status: "running" }))
|
|
50
|
+
.toEqual({ blink: true, urgentHighlight: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("break running — 31s → blink=false, urgentHighlight=false", () => {
|
|
54
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 31, mode: "break", status: "running" }))
|
|
55
|
+
.toEqual({ blink: false, urgentHighlight: false });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// --- mode longBreak (règle 5min s'applique) ---
|
|
59
|
+
it("longBreak running — 300s → blink=true, urgentHighlight=false", () => {
|
|
60
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 300, mode: "longBreak", status: "running" }))
|
|
61
|
+
.toEqual({ blink: true, urgentHighlight: false });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("longBreak running — 301s → blink=false", () => {
|
|
65
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 301, mode: "longBreak", status: "running" }))
|
|
66
|
+
.toEqual({ blink: false, urgentHighlight: false });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// --- status paused (actif) ---
|
|
70
|
+
it("work paused — 300s → mêmes règles que running", () => {
|
|
71
|
+
expect(getKronoFocusTimerUrgency({ timeLeftSeconds: 300, mode: "work", status: "paused" }))
|
|
72
|
+
.toEqual({ blink: true, urgentHighlight: false });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Seuil « 5 minutes restantes » pour le focus (25 min) et la pause longue (15 min), pas pour la pause courte (5 min entière). */
|
|
2
|
+
const FIVE_MINUTES_SECONDS = 300;
|
|
3
|
+
const THIRTY_SECONDS = 30;
|
|
4
|
+
|
|
5
|
+
export type KronoFocusTimerMode = "work" | "break" | "longBreak";
|
|
6
|
+
export type KronoFocusTimerStatus = "idle" | "running" | "paused";
|
|
7
|
+
|
|
8
|
+
export function getKronoFocusTimerUrgency(params: {
|
|
9
|
+
timeLeftSeconds: number;
|
|
10
|
+
mode: KronoFocusTimerMode;
|
|
11
|
+
status: KronoFocusTimerStatus;
|
|
12
|
+
}): { blink: boolean; urgentHighlight: boolean } {
|
|
13
|
+
const { timeLeftSeconds, mode, status } = params;
|
|
14
|
+
const active = status === "running" || status === "paused";
|
|
15
|
+
if (!active) {
|
|
16
|
+
return { blink: false, urgentHighlight: false };
|
|
17
|
+
}
|
|
18
|
+
const useFiveMinuteRule = mode === "work" || mode === "longBreak";
|
|
19
|
+
const blink =
|
|
20
|
+
timeLeftSeconds <= THIRTY_SECONDS ||
|
|
21
|
+
(useFiveMinuteRule && timeLeftSeconds <= FIVE_MINUTES_SECONDS);
|
|
22
|
+
const urgentHighlight = timeLeftSeconds <= THIRTY_SECONDS;
|
|
23
|
+
return { blink, urgentHighlight };
|
|
24
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export type GitIdentityPayload = {
|
|
2
|
+
gitUserName?: string | null;
|
|
3
|
+
gitUserEmail?: string | null;
|
|
4
|
+
/** Identifiant sur la forge (GitHub, GitLab, etc.) — distinct de `git config user.name`. */
|
|
5
|
+
gitAccountLogin?: string | null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/** Aperçu Git du workspace (collecte locale / instantané). */
|
|
9
|
+
export type GitRepoStatisticsPayload = {
|
|
10
|
+
isGitRepo?: boolean;
|
|
11
|
+
currentBranch?: string | null;
|
|
12
|
+
localBranchCount?: number;
|
|
13
|
+
remoteBranchCount?: number;
|
|
14
|
+
commitsOnHead?: number;
|
|
15
|
+
commitsAllRefs?: number | null;
|
|
16
|
+
stashCount?: number;
|
|
17
|
+
logGraphLines?: string[];
|
|
18
|
+
trackedPathsSample?: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Aperçu lignes / langages du 1er dossier workspace (scan disque ou `git ls-files`), hors métriques de session. */
|
|
22
|
+
export type WorkspaceCodeSnapshotPayload =
|
|
23
|
+
| {
|
|
24
|
+
ok: true;
|
|
25
|
+
totalLines: number;
|
|
26
|
+
fileCount: number;
|
|
27
|
+
byLanguage: Array<{ languageId: string; lines: number; percent: number }>;
|
|
28
|
+
source: "git" | "walk";
|
|
29
|
+
computedAt: string;
|
|
30
|
+
workspaceFolder: string;
|
|
31
|
+
}
|
|
32
|
+
| { ok: false; reason: string; message?: string };
|
|
33
|
+
|
|
34
|
+
/** Isolation `v4-dev` vs partage `v4` en mode `next dev` (voir `lib/dataDir.ts`). */
|
|
35
|
+
export type DevDataRuntimeInfo = {
|
|
36
|
+
isNextDevelopment: boolean;
|
|
37
|
+
traceDataDirOverride: boolean;
|
|
38
|
+
envForcesProductionData: boolean;
|
|
39
|
+
useProductionDataInDevelopment: boolean;
|
|
40
|
+
activeDataDirectory: string;
|
|
41
|
+
preferredDataDirectory: string;
|
|
42
|
+
dataDirectoryResolutionMismatch: boolean;
|
|
43
|
+
preferencesFilePath: string;
|
|
44
|
+
productionDataDirectory: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type KronosysUpdatePayload = {
|
|
48
|
+
viewType: string;
|
|
49
|
+
current?: Record<string, unknown>;
|
|
50
|
+
history?: unknown[];
|
|
51
|
+
cfg?: Record<string, unknown>;
|
|
52
|
+
inspectingSessionId?: string | null;
|
|
53
|
+
knownTags?: string[];
|
|
54
|
+
knownProjects?: string[];
|
|
55
|
+
/** Étiquettes épinglées manuellement (raccourcis même sans tâche). */
|
|
56
|
+
userKnownTags?: string[];
|
|
57
|
+
/** Étiquettes masquées des raccourcis (données des tâches inchangées). */
|
|
58
|
+
excludedSuggestionTags?: string[];
|
|
59
|
+
/** Descriptions libres (clé = étiquette normalisée, sans #, insensible à la casse). */
|
|
60
|
+
tagDescriptions?: Record<string, string>;
|
|
61
|
+
/** Descriptions libres (clé = nom de projet en minuscules). */
|
|
62
|
+
projectDescriptions?: Record<string, string>;
|
|
63
|
+
gitIdentity?: GitIdentityPayload;
|
|
64
|
+
gitStats?: GitRepoStatisticsPayload;
|
|
65
|
+
/** Si vrai, archivage sans modale de confirmation (préférence locale). */
|
|
66
|
+
dismissArchiveSessionConfirm?: boolean;
|
|
67
|
+
workspaceCodeSnapshot?: WorkspaceCodeSnapshotPayload;
|
|
68
|
+
/** Chemins absolus des racines du workspace (projet ouvert pour la collecte). */
|
|
69
|
+
workspaceFolderPaths?: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Source du flux affiché. Le serveur autonome utilise `local_next`.
|
|
72
|
+
* `legacy_external` désigne d’anciennes charges migrées depuis un hôte graphique.
|
|
73
|
+
*/
|
|
74
|
+
dashboardDataOrigin?: "local_next" | "legacy_external";
|
|
75
|
+
devDataRuntime?: DevDataRuntimeInfo;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export async function fetchKronosysState(view: "dashboard" | "sidebar"): Promise<{
|
|
80
|
+
type: string;
|
|
81
|
+
payload: KronosysUpdatePayload;
|
|
82
|
+
}> {
|
|
83
|
+
/** Évite les réponses servies depuis le cache navigateur / proxy (rafraîchissement manuel « mort »). */
|
|
84
|
+
const bust = `_=${Date.now()}`;
|
|
85
|
+
const res = await fetch(`/api/state?view=${view}&${bust}`, {
|
|
86
|
+
cache: "no-store",
|
|
87
|
+
headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
throw new Error(`Kronosys API ${res.status}: ${await res.text()}`);
|
|
91
|
+
}
|
|
92
|
+
return res.json();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type KronosysActionResponse = {
|
|
96
|
+
ok: boolean;
|
|
97
|
+
result?: {
|
|
98
|
+
remoteIssues?: Array<{ title?: string; number: number | string; source?: string }>;
|
|
99
|
+
remoteIssuesError?: string;
|
|
100
|
+
sessionOp?: { ok: boolean; message?: string; revertedName?: string };
|
|
101
|
+
settingsError?: string;
|
|
102
|
+
mongoConnectionTest?: { outcome: "connected" | "failed" | "disabled" };
|
|
103
|
+
gitlabConnectionTest?: {
|
|
104
|
+
outcome: "connected" | "failed";
|
|
105
|
+
/** Aucun jeton saisi et aucun GITLAB_TOKEN côté serveur de test. */
|
|
106
|
+
reason?: "no_token";
|
|
107
|
+
message?: string;
|
|
108
|
+
};
|
|
109
|
+
glabRepoCreate?: { ok: boolean; error?: string };
|
|
110
|
+
mongoResync?:
|
|
111
|
+
| { ok: true; upserted: number; failed: number }
|
|
112
|
+
| { ok: false; reason: "disabled" | "unsupported" }
|
|
113
|
+
| { ok: false; reason: "failed"; message: string };
|
|
114
|
+
pushSessionToMongo?:
|
|
115
|
+
| { ok: true }
|
|
116
|
+
| { ok: false; error: "disabled" | "not_found" | "mongo_failed" | "uri_incomplete" };
|
|
117
|
+
} | null;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export async function postKronosysAction(
|
|
121
|
+
body: Record<string, unknown>,
|
|
122
|
+
options?: { signal?: AbortSignal },
|
|
123
|
+
): Promise<KronosysActionResponse> {
|
|
124
|
+
const res = await fetch("/api/action", {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify(body),
|
|
128
|
+
signal: options?.signal,
|
|
129
|
+
});
|
|
130
|
+
const text = await res.text();
|
|
131
|
+
let data: KronosysActionResponse & { error?: string };
|
|
132
|
+
try {
|
|
133
|
+
data = (text ? JSON.parse(text) : {}) as KronosysActionResponse & { error?: string };
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(text.trim() ? text : `Kronosys action ${res.status} (invalid JSON)`);
|
|
136
|
+
}
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
const detail =
|
|
139
|
+
typeof data?.error === "string" && data.error.length > 0 ? data.error : text || `HTTP ${res.status}`;
|
|
140
|
+
throw new Error(detail);
|
|
141
|
+
}
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const fromCodes = (codes: readonly number[]) => codes.map((n) => String.fromCharCode(n)).join("");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ancien préfixe d’outils graphiques (6 caractères) — construit à l’exécution pour migrer les instantanés JSON.
|
|
5
|
+
* Ne pas réintroduire ce nom littéral dans le dépôt.
|
|
6
|
+
*/
|
|
7
|
+
const LEGACY_TOOL_PREFIX = fromCodes([118, 115, 99, 111, 100, 101]);
|
|
8
|
+
|
|
9
|
+
/** Ancienne clé de chemins de racines dans les charges utiles persistées. */
|
|
10
|
+
export const LEGACY_WORKSPACE_FOLDER_PATHS_KEY = `${LEGACY_TOOL_PREFIX}WorkspaceFolderPaths`;
|
|
11
|
+
|
|
12
|
+
/** Ancienne valeur de `dashboardDataOrigin` pour les flux non autonomes. */
|
|
13
|
+
export const LEGACY_DASHBOARD_DATA_ORIGIN_VALUE = `${LEGACY_TOOL_PREFIX}_extension`;
|
|
14
|
+
|
|
15
|
+
export function migrateLegacyPayloadRecord(p: Record<string, unknown>): boolean {
|
|
16
|
+
let changed = false;
|
|
17
|
+
const modern = p.workspaceFolderPaths;
|
|
18
|
+
const hasModernPaths = Array.isArray(modern) && modern.length > 0;
|
|
19
|
+
if (!hasModernPaths && Array.isArray(p[LEGACY_WORKSPACE_FOLDER_PATHS_KEY])) {
|
|
20
|
+
p.workspaceFolderPaths = p[LEGACY_WORKSPACE_FOLDER_PATHS_KEY];
|
|
21
|
+
changed = true;
|
|
22
|
+
}
|
|
23
|
+
if (LEGACY_WORKSPACE_FOLDER_PATHS_KEY in p) {
|
|
24
|
+
delete p[LEGACY_WORKSPACE_FOLDER_PATHS_KEY];
|
|
25
|
+
changed = true;
|
|
26
|
+
}
|
|
27
|
+
if (p.dashboardDataOrigin === LEGACY_DASHBOARD_DATA_ORIGIN_VALUE) {
|
|
28
|
+
p.dashboardDataOrigin = "legacy_external";
|
|
29
|
+
changed = true;
|
|
30
|
+
}
|
|
31
|
+
return changed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readWorkspaceFolderPathsArray(p: Record<string, unknown> | undefined): unknown[] {
|
|
35
|
+
if (!p) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const next = p.workspaceFolderPaths;
|
|
39
|
+
if (Array.isArray(next) && next.length > 0) {
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
const leg = p[LEGACY_WORKSPACE_FOLDER_PATHS_KEY];
|
|
43
|
+
if (Array.isArray(leg)) {
|
|
44
|
+
return leg;
|
|
45
|
+
}
|
|
46
|
+
return Array.isArray(next) ? next : [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function workspaceFolderPathStrings(p: unknown): string[] {
|
|
50
|
+
const raw = readWorkspaceFolderPathsArray(p as Record<string, unknown> | undefined);
|
|
51
|
+
return raw.filter((x): x is string => typeof x === "string" && x.trim() !== "");
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
LEGACY_ACTION_PAUSE,
|
|
5
|
+
LEGACY_ACTION_RESET,
|
|
6
|
+
LEGACY_ACTION_SET_WORK_DURATION,
|
|
7
|
+
LEGACY_ACTION_START,
|
|
8
|
+
LEGACY_SESSION_TIMER_OBJECT_KEY,
|
|
9
|
+
LEGACY_TASK_CYCLES_KEY,
|
|
10
|
+
LEGACY_TASK_USED_FLAG_KEY,
|
|
11
|
+
LEGACY_TIMER_DEADLINE_MS_KEY,
|
|
12
|
+
} from "./legacyKronoFocusStorageKeys";
|
|
13
|
+
|
|
14
|
+
describe("legacyKronoFocusStorageKeys", () => {
|
|
15
|
+
it("reconstruit les mêmes identifiants que les anciennes versions", () => {
|
|
16
|
+
expect(LEGACY_SESSION_TIMER_OBJECT_KEY).toBe(_(["pom", "odoro"]));
|
|
17
|
+
expect(LEGACY_TIMER_DEADLINE_MS_KEY).toBe(_(["pom", "odoro", "DeadlineAtMs"]));
|
|
18
|
+
expect(LEGACY_TASK_CYCLES_KEY).toBe(_(["pom", "odoro", "Cycles"]));
|
|
19
|
+
expect(LEGACY_TASK_USED_FLAG_KEY).toBe(_(["used", "Pom", "odoro"]));
|
|
20
|
+
expect(LEGACY_ACTION_START).toBe(_(["start", "Pom", "odoro"]));
|
|
21
|
+
expect(LEGACY_ACTION_PAUSE).toBe(_(["pause", "Pom", "odoro"]));
|
|
22
|
+
expect(LEGACY_ACTION_RESET).toBe(_(["reset", "Pom", "odoro"]));
|
|
23
|
+
expect(LEGACY_ACTION_SET_WORK_DURATION).toBe(_(["set", "Pom", "odoro", "WorkDuration"]));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function _(parts: string[]): string {
|
|
28
|
+
return parts.join("");
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clés JSON et types d’actions des anciennes versions.
|
|
3
|
+
* Construites par fragments pour éviter le littéral complet recherchable d’un coup.
|
|
4
|
+
*/
|
|
5
|
+
const _ = (parts: string[]): string => parts.join("");
|
|
6
|
+
|
|
7
|
+
/** Objet minuteur sur la session. */
|
|
8
|
+
export const LEGACY_SESSION_TIMER_OBJECT_KEY = _(["pom", "odoro"]);
|
|
9
|
+
|
|
10
|
+
/** `…DeadlineAtMs` sur l’objet minuteur hérité. */
|
|
11
|
+
export const LEGACY_TIMER_DEADLINE_MS_KEY = _(["pom", "odoro", "DeadlineAtMs"]);
|
|
12
|
+
|
|
13
|
+
/** Cycles sur une tâche (hérité). */
|
|
14
|
+
export const LEGACY_TASK_CYCLES_KEY = _(["pom", "odoro", "Cycles"]);
|
|
15
|
+
|
|
16
|
+
/** Indicateur « minuteur utilisé » sur une tâche (hérité). */
|
|
17
|
+
export const LEGACY_TASK_USED_FLAG_KEY = _(["used", "Pom", "odoro"]);
|
|
18
|
+
|
|
19
|
+
/** Action API héritée : démarrer le minuteur. */
|
|
20
|
+
export const LEGACY_ACTION_START = _(["start", "Pom", "odoro"]);
|
|
21
|
+
|
|
22
|
+
/** Action API héritée : pause. */
|
|
23
|
+
export const LEGACY_ACTION_PAUSE = _(["pause", "Pom", "odoro"]);
|
|
24
|
+
|
|
25
|
+
/** Action API héritée : réinitialiser. */
|
|
26
|
+
export const LEGACY_ACTION_RESET = _(["reset", "Pom", "odoro"]);
|
|
27
|
+
|
|
28
|
+
/** Action API héritée : durée de travail. */
|
|
29
|
+
export const LEGACY_ACTION_SET_WORK_DURATION = _(["set", "Pom", "odoro", "WorkDuration"]);
|
|
30
|
+
|
|
31
|
+
/** Champ JSON sur `startTask` (hérité). */
|
|
32
|
+
export const LEGACY_START_TASK_WITH_TIMER_BODY_KEY = LEGACY_ACTION_START;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Textes de la page Licences (FR / EN). */
|
|
2
|
+
|
|
3
|
+
export type LicensesLang = "fr" | "en";
|
|
4
|
+
|
|
5
|
+
export type LicensesCopy = {
|
|
6
|
+
title: string;
|
|
7
|
+
navDashboard: string;
|
|
8
|
+
navReporting: string;
|
|
9
|
+
navSettings: string;
|
|
10
|
+
intro: string;
|
|
11
|
+
kronosysSectionTitle: string;
|
|
12
|
+
kronosysProduct: string;
|
|
13
|
+
mitHeading: string;
|
|
14
|
+
thirdPartySectionTitle: string;
|
|
15
|
+
devThirdPartySectionTitle: string;
|
|
16
|
+
thirdPartyColName: string;
|
|
17
|
+
thirdPartyColLicense: string;
|
|
18
|
+
thirdPartyColLink: string;
|
|
19
|
+
fontsSectionTitle: string;
|
|
20
|
+
fontsBody: string;
|
|
21
|
+
extensionSectionTitle: string;
|
|
22
|
+
extensionNote: string;
|
|
23
|
+
disclaimer: string;
|
|
24
|
+
copyrightLine: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const fr: LicensesCopy = {
|
|
28
|
+
title: "Licences et mentions",
|
|
29
|
+
navDashboard: "Tableau de bord",
|
|
30
|
+
navReporting: "Rapports",
|
|
31
|
+
navSettings: "Paramètres",
|
|
32
|
+
intro:
|
|
33
|
+
"Kronosys (tableau de bord web local) est distribué sous licence MIT. Ce tableau de bord s’appuie sur des logiciels libres et des polices sous licences ouvertes, listés ci-dessous.",
|
|
34
|
+
kronosysSectionTitle: "Logiciel Kronosys",
|
|
35
|
+
kronosysProduct:
|
|
36
|
+
"Application web Kronosys (`apps/kronosys`, paquet npm `@nightkatana/kronosys-app`) : licence MIT. L’avis de copyright ci-dessous s’applique au code fourni par les auteurs du projet.",
|
|
37
|
+
mitHeading: "Texte de la licence MIT",
|
|
38
|
+
thirdPartySectionTitle: "Logiciels tiers (tableau de bord web)",
|
|
39
|
+
devThirdPartySectionTitle: "Outils de construction (tableau de bord, hors bundle utilisateur)",
|
|
40
|
+
thirdPartyColName: "Composant",
|
|
41
|
+
thirdPartyColLicense: "Licence",
|
|
42
|
+
thirdPartyColLink: "Projet",
|
|
43
|
+
fontsSectionTitle: "Polices (chargées via Next.js / Google Fonts)",
|
|
44
|
+
fontsBody:
|
|
45
|
+
"Les polices Rubik et Geist Mono utilisées dans l’interface sont sous SIL Open Font License 1.1 (OFL). Les fichiers de polices sont fournis par Google Fonts et Vercel dans le cadre de leurs dépôts respectifs.",
|
|
46
|
+
extensionSectionTitle: "Dépendances npm du tableau de bord",
|
|
47
|
+
extensionNote:
|
|
48
|
+
"L’application embarque d’autres bibliothèques npm (par ex. minimatch, client MongoDB si activé). Chaque paquet est soumis à sa propre licence ; le fichier `package-lock.json` à la racine du dépôt permet d’identifier les versions exactes. Pour une liste détaillée des licences, vous pouvez utiliser un outil du type `npm-license-crawler` ou `license-checker` sur le dépôt.",
|
|
49
|
+
disclaimer:
|
|
50
|
+
"Les liens vers des sites tiers sont fournis à titre informatif. Les titulaires de droits restent les auteurs de chaque projet.",
|
|
51
|
+
copyrightLine: "Copyright (c) 2026 NightKatana",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const en: LicensesCopy = {
|
|
55
|
+
title: "Licenses and notices",
|
|
56
|
+
navDashboard: "Dashboard",
|
|
57
|
+
navReporting: "Reporting",
|
|
58
|
+
navSettings: "Settings",
|
|
59
|
+
intro:
|
|
60
|
+
"Kronosys (local web dashboard) is distributed under the MIT License. This dashboard relies on open-source software and fonts under open licenses, listed below.",
|
|
61
|
+
kronosysSectionTitle: "Kronosys software",
|
|
62
|
+
kronosysProduct:
|
|
63
|
+
"The Kronosys web app (`apps/kronosys`, npm package `@nightkatana/kronosys-app`) is under the MIT License. The copyright notice below applies to the code provided by the project authors.",
|
|
64
|
+
mitHeading: "MIT License text",
|
|
65
|
+
thirdPartySectionTitle: "Third-party software (web dashboard)",
|
|
66
|
+
devThirdPartySectionTitle: "Build tooling (dashboard, not shipped to end users)",
|
|
67
|
+
thirdPartyColName: "Component",
|
|
68
|
+
thirdPartyColLicense: "License",
|
|
69
|
+
thirdPartyColLink: "Project",
|
|
70
|
+
fontsSectionTitle: "Fonts (loaded via Next.js / Google Fonts)",
|
|
71
|
+
fontsBody:
|
|
72
|
+
"The Rubik and Geist Mono fonts used in the UI are under the SIL Open Font License 1.1 (OFL). Font files are provided by Google Fonts and Vercel in their respective repositories.",
|
|
73
|
+
extensionSectionTitle: "Dashboard npm dependencies",
|
|
74
|
+
extensionNote:
|
|
75
|
+
"The web app bundles additional npm packages (e.g. minimatch, MongoDB driver when enabled). Each package is under its own license; the root `package-lock.json` records exact versions. For a full license listing, use a tool such as `license-checker` on the repository.",
|
|
76
|
+
disclaimer:
|
|
77
|
+
"Links to third-party sites are for information only. Rights remain with each project’s authors.",
|
|
78
|
+
copyrightLine: "Copyright (c) 2026 NightKatana",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function licensesCopy(lang: LicensesLang): LicensesCopy {
|
|
82
|
+
return lang === "en" ? en : fr;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Dépendances directes du tableau de bord (runtime) — licences usuelles, à vérifier sur le site du projet si besoin. */
|
|
86
|
+
export const DASHBOARD_THIRD_PARTY: ReadonlyArray<{
|
|
87
|
+
name: string;
|
|
88
|
+
license: string;
|
|
89
|
+
url: string;
|
|
90
|
+
}> = [
|
|
91
|
+
{ name: "Next.js", license: "MIT", url: "https://github.com/vercel/next.js" },
|
|
92
|
+
{ name: "React", license: "MIT", url: "https://github.com/facebook/react" },
|
|
93
|
+
{ name: "React DOM", license: "MIT", url: "https://github.com/facebook/react" },
|
|
94
|
+
{ name: "lucide-react (icônes)", license: "ISC", url: "https://github.com/lucide-icons/lucide" },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export const DASHBOARD_DEV_THIRD_PARTY: ReadonlyArray<{
|
|
98
|
+
name: string;
|
|
99
|
+
license: string;
|
|
100
|
+
url: string;
|
|
101
|
+
}> = [
|
|
102
|
+
{ name: "Tailwind CSS", license: "MIT", url: "https://github.com/tailwindlabs/tailwindcss" },
|
|
103
|
+
{ name: "TypeScript", license: "Apache-2.0", url: "https://github.com/microsoft/TypeScript" },
|
|
104
|
+
{ name: "ESLint", license: "MIT", url: "https://github.com/eslint/eslint" },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/** Texte MIT du dépôt (LICENSE) — identique en toutes langues. */
|
|
108
|
+
export const KRONOSYS_MIT_LICENSE_BODY = `MIT License
|
|
109
|
+
|
|
110
|
+
Copyright (c) 2026 NightKatana
|
|
111
|
+
|
|
112
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
113
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
114
|
+
in the Software without restriction, including without limitation the rights
|
|
115
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
116
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
117
|
+
furnished to do so, subject to the following conditions:
|
|
118
|
+
|
|
119
|
+
The above copyright notice and this permission notice shall be included in all
|
|
120
|
+
copies or substantial portions of the Software.
|
|
121
|
+
|
|
122
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
123
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
124
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
125
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
126
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
127
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
128
|
+
SOFTWARE.`;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Lang } from "@/lib/dashboardCopy";
|
|
2
|
+
|
|
3
|
+
function escapeHtml(s: string): string {
|
|
4
|
+
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Ouvre un onglet avec du texte brut (échappé), thème sombre, pour lecture longue (graphe Git, listes). */
|
|
8
|
+
export function openPlainTextInNewTab(options: {
|
|
9
|
+
documentTitle: string;
|
|
10
|
+
heading: string;
|
|
11
|
+
body: string;
|
|
12
|
+
lang: Lang;
|
|
13
|
+
}): void {
|
|
14
|
+
const { documentTitle, heading, body, lang } = options;
|
|
15
|
+
const html = `<!DOCTYPE html>
|
|
16
|
+
<html lang="${lang}">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="utf-8">
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
20
|
+
<title>${escapeHtml(documentTitle)}</title>
|
|
21
|
+
<style>
|
|
22
|
+
body { margin: 0; background: #09090b; color: #d4d4d8; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 13px; line-height: 1.45; }
|
|
23
|
+
h1 { font-family: system-ui, -apple-system, sans-serif; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #a1a1aa; margin: 0 0 1rem; }
|
|
24
|
+
pre { margin: 0; white-space: pre; overflow-x: auto; tab-size: 2; }
|
|
25
|
+
main { padding: 1.25rem 1.25rem 2rem; max-width: min(120ch, 100vw); margin: 0 auto; box-sizing: border-box; }
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<main>
|
|
30
|
+
<h1>${escapeHtml(heading)}</h1>
|
|
31
|
+
<pre>${escapeHtml(body)}</pre>
|
|
32
|
+
</main>
|
|
33
|
+
</body>
|
|
34
|
+
</html>`;
|
|
35
|
+
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
|
|
36
|
+
const url = URL.createObjectURL(blob);
|
|
37
|
+
const w = window.open(url, "_blank", "noopener,noreferrer");
|
|
38
|
+
if (w) {
|
|
39
|
+
w.addEventListener(
|
|
40
|
+
"load",
|
|
41
|
+
() => {
|
|
42
|
+
URL.revokeObjectURL(url);
|
|
43
|
+
},
|
|
44
|
+
{ once: true }
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
URL.revokeObjectURL(url);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Lit `version` dans le `package.json` du répertoire de travail (racine de l’app Next). */
|
|
5
|
+
export function readKronosysPackageVersion(): string {
|
|
6
|
+
const pkgPath = path.join(process.cwd(), "package.json");
|
|
7
|
+
const raw = fs.readFileSync(pkgPath, "utf8");
|
|
8
|
+
const v = (JSON.parse(raw) as { version?: unknown }).version;
|
|
9
|
+
return typeof v === "string" && v.trim().length > 0 ? v.trim() : "0.0.0";
|
|
10
|
+
}
|