@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
package/app/settings/page.tsx
CHANGED
|
@@ -35,16 +35,20 @@ import { reportingNav } from "@/lib/reportingStrings";
|
|
|
35
35
|
import { settingsCopy, type SettingsCopy } from "@/lib/settingsCopy";
|
|
36
36
|
import {
|
|
37
37
|
appShellHeaderClassName,
|
|
38
|
-
|
|
38
|
+
appShellHeaderTitleMetaRowClassName,
|
|
39
|
+
appShellHeaderToolbarClassName,
|
|
39
40
|
} from "@/lib/appShellHeaderClasses";
|
|
41
|
+
import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
|
|
42
|
+
import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
|
|
43
|
+
import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
|
|
40
44
|
import { workspaceFolderPathStrings } from "@/lib/legacyEditorPayloadKeys";
|
|
41
45
|
import { showWorkspaceFoldersEmptyMessage } from "@/lib/usageProfile";
|
|
42
46
|
import { readDashboardUse24HourClockFromCfg } from "@/lib/dashboardClockFormat";
|
|
43
47
|
import {
|
|
44
48
|
DASHBOARD_TIME_ZONE_SELECT_OPTIONS,
|
|
45
|
-
isValidIanaTimeZone,
|
|
46
49
|
readDashboardTimeZoneFromCfg,
|
|
47
50
|
} from "@/lib/dashboardTimeZone";
|
|
51
|
+
import { SESSION_NAME_TEMPLATE_CFG_MAX_LEN } from "@/lib/formatSessionNameTemplate";
|
|
48
52
|
import { DEFAULT_WORKSPACE_LOC_EXCLUDED_DIRECTORY_NAMES } from "@/lib/workspaceLocDefaults";
|
|
49
53
|
import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
|
|
50
54
|
import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
|
|
@@ -65,7 +69,7 @@ import {
|
|
|
65
69
|
writeDashboardColumnHintsDismissed,
|
|
66
70
|
} from "@/lib/dashboardColumnHintsStorage";
|
|
67
71
|
import { KronosysTimePopoverField } from "@/components/dashboard/KronosysTimePopoverField";
|
|
68
|
-
import { Search } from "lucide-react";
|
|
72
|
+
import { FileText, Search } from "lucide-react";
|
|
69
73
|
import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
|
|
70
74
|
|
|
71
75
|
type LiveShape = { language?: string };
|
|
@@ -102,6 +106,8 @@ type SettingsForm = {
|
|
|
102
106
|
dashboardDisplayTimeZone: string;
|
|
103
107
|
/** `true` : affichage 24 h ; `false` : 12 h (AM/PM). */
|
|
104
108
|
dashboardUse24HourClock: boolean;
|
|
109
|
+
/** Gabarit du nom des nouvelles sessions (`%UUID`, strftime POSIX). */
|
|
110
|
+
dashboardDefaultSessionNameTemplate: string;
|
|
105
111
|
/** Seuil d’alerte (heures) pour la durée murale affichée dans le tableau de bord. */
|
|
106
112
|
dashboardSessionDurationAlertHours: number;
|
|
107
113
|
dashboardShowKronoFocusInHeader: boolean;
|
|
@@ -233,6 +239,10 @@ function cfgToForm(cfg: Record<string, unknown> | undefined): SettingsForm {
|
|
|
233
239
|
dashboardWebUrl: str(cfg?.dashboardWebUrl, "http://kronosys:5555"),
|
|
234
240
|
dashboardDisplayTimeZone: readDashboardTimeZoneFromCfg(cfg),
|
|
235
241
|
dashboardUse24HourClock: readDashboardUse24HourClockFromCfg(cfg),
|
|
242
|
+
dashboardDefaultSessionNameTemplate: str(
|
|
243
|
+
cfg?.dashboardDefaultSessionNameTemplate,
|
|
244
|
+
"",
|
|
245
|
+
).slice(0, SESSION_NAME_TEMPLATE_CFG_MAX_LEN),
|
|
236
246
|
dashboardSessionDurationAlertHours: Math.min(
|
|
237
247
|
Math.max(num(cfg?.dashboardSessionDurationAlertHours, 24), 1),
|
|
238
248
|
8760,
|
|
@@ -425,8 +435,8 @@ function archivedTaskCount(s: SessionListEntry): number {
|
|
|
425
435
|
Array.isArray(s.activeTasks) && s.activeTasks.length > 0
|
|
426
436
|
? s.activeTasks.length
|
|
427
437
|
: s.activeTask
|
|
428
|
-
|
|
429
|
-
|
|
438
|
+
? 1
|
|
439
|
+
: 0;
|
|
430
440
|
return listed + nActive;
|
|
431
441
|
}
|
|
432
442
|
|
|
@@ -514,6 +524,14 @@ function SettingsToc({
|
|
|
514
524
|
m(s.sectionTagsProjects, "settings-tags-projects") ||
|
|
515
525
|
m(s.tocSubTagsByProject, "settings-tags-by-project");
|
|
516
526
|
const showTagsBlock = showTagsGlobal || showTagsProjects || showTagsByProject;
|
|
527
|
+
const showTaskTemplates = m(
|
|
528
|
+
s.sectionTaskTemplates,
|
|
529
|
+
"settings-task-templates",
|
|
530
|
+
"template",
|
|
531
|
+
"modèle",
|
|
532
|
+
"task template",
|
|
533
|
+
"gabarit",
|
|
534
|
+
);
|
|
517
535
|
const showDanger = m(s.sectionDangerZone, "settings-danger-zone");
|
|
518
536
|
const showArchived = m(
|
|
519
537
|
s.sectionArchivedSessions,
|
|
@@ -578,7 +596,21 @@ function SettingsToc({
|
|
|
578
596
|
"settings-session-duration-alert",
|
|
579
597
|
"duration",
|
|
580
598
|
);
|
|
581
|
-
const
|
|
599
|
+
const showWebDefaultSessionName =
|
|
600
|
+
m(s.sectionWeb, "settings-web", "dashboard", "api") ||
|
|
601
|
+
m(
|
|
602
|
+
s.tocSubDefaultSessionNameTemplate,
|
|
603
|
+
"settings-default-session-name",
|
|
604
|
+
"default session",
|
|
605
|
+
"session name",
|
|
606
|
+
"%UUID",
|
|
607
|
+
"strftime",
|
|
608
|
+
"gabarit",
|
|
609
|
+
"nom de session",
|
|
610
|
+
) ||
|
|
611
|
+
m(s.dashboardDefaultSessionNameTemplate, "template", "uuid");
|
|
612
|
+
const showWebBlock =
|
|
613
|
+
showWebTour || showWebDuration || showWebDefaultSessionName;
|
|
582
614
|
const showLicenses = m(
|
|
583
615
|
s.licensesPageLink,
|
|
584
616
|
"/licenses",
|
|
@@ -600,6 +632,7 @@ function SettingsToc({
|
|
|
600
632
|
showCollection ||
|
|
601
633
|
showHistory ||
|
|
602
634
|
showTagsBlock ||
|
|
635
|
+
showTaskTemplates ||
|
|
603
636
|
showDanger ||
|
|
604
637
|
showArchived ||
|
|
605
638
|
showExport ||
|
|
@@ -615,7 +648,7 @@ function SettingsToc({
|
|
|
615
648
|
return (
|
|
616
649
|
<nav
|
|
617
650
|
aria-label={s.tocNavAriaLabel}
|
|
618
|
-
className="rounded-xl border border-zinc-200 bg-zinc-50/90 p-4 dark:border-zinc-800 dark:bg-zinc-900/40"
|
|
651
|
+
className="rounded-xl border border-zinc-200 bg-zinc-50/90 p-4 lg:max-h-[calc(100dvh-14rem)] lg:overflow-y-scroll lg:overscroll-contain dark:border-zinc-800 dark:bg-zinc-900/40"
|
|
619
652
|
>
|
|
620
653
|
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
|
|
621
654
|
{s.tocHeading}
|
|
@@ -740,6 +773,13 @@ function SettingsToc({
|
|
|
740
773
|
) : null}
|
|
741
774
|
</li>
|
|
742
775
|
) : null}
|
|
776
|
+
{showTaskTemplates ? (
|
|
777
|
+
<li>
|
|
778
|
+
<a href="#settings-task-templates" className={linkClass}>
|
|
779
|
+
{s.sectionTaskTemplates}
|
|
780
|
+
</a>
|
|
781
|
+
</li>
|
|
782
|
+
) : null}
|
|
743
783
|
{showGit ? (
|
|
744
784
|
<li>
|
|
745
785
|
<a href="#settings-git" className={linkClass}>
|
|
@@ -782,7 +822,7 @@ function SettingsToc({
|
|
|
782
822
|
<a href="#settings-web" className={linkClass}>
|
|
783
823
|
{s.sectionWeb}
|
|
784
824
|
</a>
|
|
785
|
-
{showWebTour || showWebDuration ? (
|
|
825
|
+
{showWebTour || showWebDuration || showWebDefaultSessionName ? (
|
|
786
826
|
<ul className={subListClass}>
|
|
787
827
|
{showWebTour ? (
|
|
788
828
|
<>
|
|
@@ -812,6 +852,16 @@ function SettingsToc({
|
|
|
812
852
|
</li>
|
|
813
853
|
</>
|
|
814
854
|
) : null}
|
|
855
|
+
{showWebDefaultSessionName ? (
|
|
856
|
+
<li>
|
|
857
|
+
<a
|
|
858
|
+
href="#settings-default-session-name"
|
|
859
|
+
className={linkClass}
|
|
860
|
+
>
|
|
861
|
+
{s.tocSubDefaultSessionNameTemplate}
|
|
862
|
+
</a>
|
|
863
|
+
</li>
|
|
864
|
+
) : null}
|
|
815
865
|
{showWebDuration ? (
|
|
816
866
|
<li>
|
|
817
867
|
<a
|
|
@@ -902,6 +952,10 @@ function SettingsPageContent() {
|
|
|
902
952
|
>(null);
|
|
903
953
|
const [clearHistoryConfirmOpen, setClearHistoryConfirmOpen] = useState(false);
|
|
904
954
|
const [clearHistoryBusy, setClearHistoryBusy] = useState(false);
|
|
955
|
+
const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false);
|
|
956
|
+
const [restoreBusy, setRestoreBusy] = useState(false);
|
|
957
|
+
const [restoreFile, setRestoreFile] = useState<File | null>(null);
|
|
958
|
+
const restoreFileInputRef = useRef<HTMLInputElement | null>(null);
|
|
905
959
|
const [settingsTocFilter, setSettingsTocFilter] = useState("");
|
|
906
960
|
const [settingsTourOpen, setSettingsTourOpen] = useState(false);
|
|
907
961
|
|
|
@@ -1137,8 +1191,8 @@ function SettingsPageContent() {
|
|
|
1137
1191
|
const gitlabTokenStatusText = gitlabTokenStoredFlag
|
|
1138
1192
|
? s.gitlabTokenStoredYes
|
|
1139
1193
|
: gitlabTokenFromEnvFlag
|
|
1140
|
-
|
|
1141
|
-
|
|
1194
|
+
? s.gitlabTokenFromEnvYes
|
|
1195
|
+
: s.gitlabTokenNone;
|
|
1142
1196
|
|
|
1143
1197
|
const headerApiError =
|
|
1144
1198
|
lang === "fr"
|
|
@@ -1507,6 +1561,56 @@ function SettingsPageContent() {
|
|
|
1507
1561
|
}
|
|
1508
1562
|
}, [refresh, router]);
|
|
1509
1563
|
|
|
1564
|
+
const restoreBackup = useCallback(async () => {
|
|
1565
|
+
if (!restoreFile) {
|
|
1566
|
+
setSettingsDialogAlert(s.dangerRestoreNoFile);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
setRestoreBusy(true);
|
|
1570
|
+
try {
|
|
1571
|
+
const fd = new FormData();
|
|
1572
|
+
fd.set("file", restoreFile);
|
|
1573
|
+
const format = restoreFile.name.toLowerCase().endsWith(".json")
|
|
1574
|
+
? "json"
|
|
1575
|
+
: "sqlite";
|
|
1576
|
+
fd.set("format", format);
|
|
1577
|
+
const res = await fetch("/api/restore", {
|
|
1578
|
+
method: "POST",
|
|
1579
|
+
body: fd,
|
|
1580
|
+
});
|
|
1581
|
+
const text = await res.text();
|
|
1582
|
+
let data: Record<string, unknown> = {};
|
|
1583
|
+
try {
|
|
1584
|
+
data = text ? (JSON.parse(text) as Record<string, unknown>) : {};
|
|
1585
|
+
} catch {
|
|
1586
|
+
data = {};
|
|
1587
|
+
}
|
|
1588
|
+
if (!res.ok || data.ok !== true) {
|
|
1589
|
+
const detail =
|
|
1590
|
+
typeof data.detail === "string"
|
|
1591
|
+
? data.detail
|
|
1592
|
+
: typeof data.error === "string"
|
|
1593
|
+
? data.error
|
|
1594
|
+
: text || `HTTP ${res.status}`;
|
|
1595
|
+
setSettingsDialogAlert(
|
|
1596
|
+
`${s.dangerRestoreFailed.replace("{detail}", detail)}`,
|
|
1597
|
+
);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
setRestoreConfirmOpen(false);
|
|
1601
|
+
setRestoreFile(null);
|
|
1602
|
+
if (restoreFileInputRef.current) {
|
|
1603
|
+
restoreFileInputRef.current.value = "";
|
|
1604
|
+
}
|
|
1605
|
+
await refresh({ preserveForm: false, routerInvalidate: true });
|
|
1606
|
+
setSettingsDialogAlert(
|
|
1607
|
+
format === "json" ? s.dangerRestoreDoneJson : s.dangerRestoreDoneSqlite,
|
|
1608
|
+
);
|
|
1609
|
+
} finally {
|
|
1610
|
+
setRestoreBusy(false);
|
|
1611
|
+
}
|
|
1612
|
+
}, [refresh, restoreFile, s]);
|
|
1613
|
+
|
|
1510
1614
|
const applyResetToDefaults = useCallback(async () => {
|
|
1511
1615
|
setResetDefaultsBusy(true);
|
|
1512
1616
|
setSettingsAck(null);
|
|
@@ -1529,7 +1633,7 @@ function SettingsPageContent() {
|
|
|
1529
1633
|
return (
|
|
1530
1634
|
<div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
|
|
1531
1635
|
<header className={appShellHeaderClassName}>
|
|
1532
|
-
<div className={
|
|
1636
|
+
<div className={appShellHeaderTitleMetaRowClassName}>
|
|
1533
1637
|
<div className="flex min-w-0 flex-col gap-1">
|
|
1534
1638
|
<Link
|
|
1535
1639
|
href={withDashboardSessionParam("/", dashboardSessionNavId)}
|
|
@@ -1545,12 +1649,18 @@ function SettingsPageContent() {
|
|
|
1545
1649
|
<AppVersionStamp ariaLabelTemplate={dt.appVersionAriaLabel} />
|
|
1546
1650
|
</p>
|
|
1547
1651
|
</div>
|
|
1548
|
-
<
|
|
1652
|
+
<AppShellHeaderSessionMeta payload={payload} dt={dt} />
|
|
1653
|
+
</div>
|
|
1654
|
+
<div className="flex w-full justify-end">
|
|
1655
|
+
<div className={appShellHeaderToolbarClassName}>
|
|
1656
|
+
<AppShellHeaderWallClock lang={lang} dt={dt} />
|
|
1657
|
+
<AppShellCommandCenterPlaceholder />
|
|
1549
1658
|
<AppShellRouteNav
|
|
1550
1659
|
current="settings"
|
|
1551
1660
|
labels={nav}
|
|
1552
1661
|
navAriaLabel={dt.appShellRouteNavAria}
|
|
1553
1662
|
dashboardSessionId={dashboardSessionNavId}
|
|
1663
|
+
reserveGlobalPauseSlot
|
|
1554
1664
|
/>
|
|
1555
1665
|
<ThemeToggle lang={lang} />
|
|
1556
1666
|
<PageRefreshButton
|
|
@@ -2656,8 +2766,8 @@ function SettingsPageContent() {
|
|
|
2656
2766
|
gitlabTestFeedback.tone === "ok"
|
|
2657
2767
|
? "mt-2 text-sm text-emerald-600 dark:text-emerald-400"
|
|
2658
2768
|
: gitlabTestFeedback.tone === "warn"
|
|
2659
|
-
|
|
2660
|
-
|
|
2769
|
+
? "mt-2 text-sm text-amber-600 dark:text-amber-400"
|
|
2770
|
+
: "mt-2 text-sm text-red-600 dark:text-red-400"
|
|
2661
2771
|
}
|
|
2662
2772
|
>
|
|
2663
2773
|
{gitlabTestFeedback.text}
|
|
@@ -2773,8 +2883,8 @@ function SettingsPageContent() {
|
|
|
2773
2883
|
{mongoRemoteStatus === "connected"
|
|
2774
2884
|
? s.mongoStatusConnected
|
|
2775
2885
|
: mongoRemoteStatus === "failed"
|
|
2776
|
-
|
|
2777
|
-
|
|
2886
|
+
? s.mongoStatusFailed
|
|
2887
|
+
: s.mongoStatusPending}
|
|
2778
2888
|
{" · "}
|
|
2779
2889
|
{mongoUriConfigured ? (
|
|
2780
2890
|
<span className="text-emerald-400/90">
|
|
@@ -2810,8 +2920,8 @@ function SettingsPageContent() {
|
|
|
2810
2920
|
mongoTestFeedback.tone === "ok"
|
|
2811
2921
|
? "text-emerald-400"
|
|
2812
2922
|
: mongoTestFeedback.tone === "warn"
|
|
2813
|
-
|
|
2814
|
-
|
|
2923
|
+
? "text-amber-400"
|
|
2924
|
+
: "text-red-300"
|
|
2815
2925
|
}`}
|
|
2816
2926
|
>
|
|
2817
2927
|
{mongoTestFeedback.text}
|
|
@@ -3168,6 +3278,36 @@ function SettingsPageContent() {
|
|
|
3168
3278
|
</label>
|
|
3169
3279
|
</div>
|
|
3170
3280
|
</Field>
|
|
3281
|
+
|
|
3282
|
+
<div
|
|
3283
|
+
id="settings-default-session-name"
|
|
3284
|
+
className="scroll-mt-24"
|
|
3285
|
+
>
|
|
3286
|
+
<Field
|
|
3287
|
+
label={s.dashboardDefaultSessionNameTemplate}
|
|
3288
|
+
description={s.dashboardDefaultSessionNameTemplateDesc}
|
|
3289
|
+
>
|
|
3290
|
+
<input
|
|
3291
|
+
type="text"
|
|
3292
|
+
className={inputClass(formLocked)}
|
|
3293
|
+
value={form.dashboardDefaultSessionNameTemplate}
|
|
3294
|
+
onChange={(e) =>
|
|
3295
|
+
update(
|
|
3296
|
+
"dashboardDefaultSessionNameTemplate",
|
|
3297
|
+
e.target.value.slice(
|
|
3298
|
+
0,
|
|
3299
|
+
SESSION_NAME_TEMPLATE_CFG_MAX_LEN,
|
|
3300
|
+
),
|
|
3301
|
+
)
|
|
3302
|
+
}
|
|
3303
|
+
disabled={formLocked}
|
|
3304
|
+
spellCheck={false}
|
|
3305
|
+
autoComplete="off"
|
|
3306
|
+
placeholder="Session %F %UUID"
|
|
3307
|
+
aria-label={s.dashboardDefaultSessionNameTemplate}
|
|
3308
|
+
/>
|
|
3309
|
+
</Field>
|
|
3310
|
+
</div>
|
|
3171
3311
|
</div>
|
|
3172
3312
|
<div
|
|
3173
3313
|
id="settings-dashboard-tour"
|
|
@@ -3319,6 +3459,12 @@ function SettingsPageContent() {
|
|
|
3319
3459
|
archivedPageRows.map((sess) => {
|
|
3320
3460
|
const label =
|
|
3321
3461
|
sess.sessionName?.trim() || sess.sessionId.slice(0, 8);
|
|
3462
|
+
const noteRaw =
|
|
3463
|
+
typeof sess.sessionNote === "string"
|
|
3464
|
+
? sess.sessionNote.trim()
|
|
3465
|
+
: "";
|
|
3466
|
+
const notePreview = noteRaw.replaceAll(/\s+/g, " ");
|
|
3467
|
+
const hasSessionNote = notePreview.length > 0;
|
|
3322
3468
|
const n = archivedTaskCount(sess);
|
|
3323
3469
|
const busy = archivedRestoreBusyId === sess.sessionId;
|
|
3324
3470
|
return (
|
|
@@ -3333,6 +3479,19 @@ function SettingsPageContent() {
|
|
|
3333
3479
|
<div className="text-[0.7rem] text-zinc-500">
|
|
3334
3480
|
{n} {sessionTaskCountNoun(n, dt, lang)}
|
|
3335
3481
|
</div>
|
|
3482
|
+
{hasSessionNote ? (
|
|
3483
|
+
<div
|
|
3484
|
+
className="mt-1 inline-flex min-w-0 max-w-full items-center gap-1 text-[0.7rem] text-zinc-500"
|
|
3485
|
+
title={notePreview}
|
|
3486
|
+
>
|
|
3487
|
+
<FileText
|
|
3488
|
+
size={12}
|
|
3489
|
+
className="shrink-0"
|
|
3490
|
+
aria-hidden
|
|
3491
|
+
/>
|
|
3492
|
+
<span className="truncate">{notePreview}</span>
|
|
3493
|
+
</div>
|
|
3494
|
+
) : null}
|
|
3336
3495
|
</div>
|
|
3337
3496
|
<button
|
|
3338
3497
|
type="button"
|
|
@@ -3406,6 +3565,64 @@ function SettingsPageContent() {
|
|
|
3406
3565
|
{s.sectionDangerZone}
|
|
3407
3566
|
</h2>
|
|
3408
3567
|
<div className="max-w-xl rounded-xl border border-red-300/90 bg-red-50/80 p-4 dark:border-red-900/55 dark:bg-red-950/30">
|
|
3568
|
+
<h3 className="text-sm font-semibold text-red-900 dark:text-red-200">
|
|
3569
|
+
{s.dangerBackupRestoreTitle}
|
|
3570
|
+
</h3>
|
|
3571
|
+
<p className="mt-2 text-xs leading-relaxed text-red-900/85 dark:text-red-200/85">
|
|
3572
|
+
{s.dangerBackupRestoreIntro}
|
|
3573
|
+
</p>
|
|
3574
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
3575
|
+
<a
|
|
3576
|
+
href="/api/backup?format=json"
|
|
3577
|
+
download
|
|
3578
|
+
className="rounded-lg border border-red-500/70 px-3 py-1.5 text-xs font-medium text-red-900 hover:bg-red-100 dark:text-red-200 dark:hover:bg-red-900/40"
|
|
3579
|
+
>
|
|
3580
|
+
{s.dangerClearHistoryBackupJson}
|
|
3581
|
+
</a>
|
|
3582
|
+
<a
|
|
3583
|
+
href="/api/backup?format=csv-zip"
|
|
3584
|
+
download
|
|
3585
|
+
className="rounded-lg border border-red-500/70 px-3 py-1.5 text-xs font-medium text-red-900 hover:bg-red-100 dark:text-red-200 dark:hover:bg-red-900/40"
|
|
3586
|
+
>
|
|
3587
|
+
{s.dangerClearHistoryBackupCsvZip}
|
|
3588
|
+
</a>
|
|
3589
|
+
<a
|
|
3590
|
+
href="/api/backup?format=sqlite"
|
|
3591
|
+
download
|
|
3592
|
+
className="rounded-lg border border-red-500/70 px-3 py-1.5 text-xs font-medium text-red-900 hover:bg-red-100 dark:text-red-200 dark:hover:bg-red-900/40"
|
|
3593
|
+
>
|
|
3594
|
+
{s.dangerClearHistoryBackupSqlite}
|
|
3595
|
+
</a>
|
|
3596
|
+
</div>
|
|
3597
|
+
<div className="mt-4 space-y-2">
|
|
3598
|
+
<label className="block text-xs font-medium text-red-900/90 dark:text-red-200/90">
|
|
3599
|
+
{s.dangerRestorePickFile}
|
|
3600
|
+
</label>
|
|
3601
|
+
<input
|
|
3602
|
+
ref={restoreFileInputRef}
|
|
3603
|
+
type="file"
|
|
3604
|
+
accept=".json,.sqlite,.db,application/json,application/vnd.sqlite3"
|
|
3605
|
+
disabled={restoreBusy || clearHistoryBusy}
|
|
3606
|
+
className="block w-full rounded-lg border border-red-300 bg-white px-3 py-2 text-xs text-zinc-800 file:mr-3 file:rounded file:border-0 file:bg-red-100 file:px-2.5 file:py-1 file:text-xs file:font-medium file:text-red-900 dark:border-red-900/60 dark:bg-zinc-950 dark:text-zinc-200 dark:file:bg-red-900/35 dark:file:text-red-200"
|
|
3607
|
+
onChange={(e) => {
|
|
3608
|
+
const f = e.target.files?.[0] ?? null;
|
|
3609
|
+
setRestoreFile(f);
|
|
3610
|
+
}}
|
|
3611
|
+
/>
|
|
3612
|
+
<p className="text-[11px] leading-relaxed text-red-900/80 dark:text-red-300/80">
|
|
3613
|
+
{s.dangerRestoreHint}
|
|
3614
|
+
</p>
|
|
3615
|
+
<button
|
|
3616
|
+
type="button"
|
|
3617
|
+
disabled={!restoreFile || restoreBusy || clearHistoryBusy}
|
|
3618
|
+
className="rounded-lg border border-red-600/80 bg-red-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-red-500 dark:bg-red-800/90 dark:hover:bg-red-700"
|
|
3619
|
+
onClick={() => setRestoreConfirmOpen(true)}
|
|
3620
|
+
>
|
|
3621
|
+
{restoreBusy
|
|
3622
|
+
? s.dangerRestoreBusy
|
|
3623
|
+
: s.dangerRestoreButton}
|
|
3624
|
+
</button>
|
|
3625
|
+
</div>
|
|
3409
3626
|
<h3 className="text-sm font-semibold text-red-900 dark:text-red-200">
|
|
3410
3627
|
{s.dangerClearHistoryTitle}
|
|
3411
3628
|
</h3>
|
|
@@ -3450,6 +3667,23 @@ function SettingsPageContent() {
|
|
|
3450
3667
|
onCancel={() => setMongoResyncConfirmOpen(false)}
|
|
3451
3668
|
onConfirm={() => void executeMongoResync()}
|
|
3452
3669
|
/>
|
|
3670
|
+
<DashboardConfirmModal
|
|
3671
|
+
open={restoreConfirmOpen}
|
|
3672
|
+
title={s.dangerRestoreButton}
|
|
3673
|
+
message={s.dangerRestoreConfirm}
|
|
3674
|
+
cancelLabel={s.dialogCancelBtn}
|
|
3675
|
+
confirmLabel={s.dialogConfirmBtn}
|
|
3676
|
+
confirmVariant="danger"
|
|
3677
|
+
pending={restoreBusy}
|
|
3678
|
+
onCancel={() => {
|
|
3679
|
+
if (!restoreBusy) {
|
|
3680
|
+
setRestoreConfirmOpen(false);
|
|
3681
|
+
}
|
|
3682
|
+
}}
|
|
3683
|
+
onConfirm={() => {
|
|
3684
|
+
void restoreBackup();
|
|
3685
|
+
}}
|
|
3686
|
+
/>
|
|
3453
3687
|
<DashboardConfirmModal
|
|
3454
3688
|
open={clearHistoryConfirmOpen}
|
|
3455
3689
|
title={s.dangerClearHistoryTitle}
|
package/bin/kronosys.mjs
CHANGED
|
@@ -5,21 +5,49 @@
|
|
|
5
5
|
* Supported commands:
|
|
6
6
|
* start -> next start -p 5555 (production, default; auto-build if needed)
|
|
7
7
|
* dev -> next dev --webpack -H kronosys -p 5555
|
|
8
|
+
* --version, -v, version -> print installed Kronosys version
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { spawn } from "node:child_process";
|
|
11
|
-
import { existsSync } from "node:fs";
|
|
12
|
+
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
12
14
|
import { dirname, resolve } from "node:path";
|
|
13
15
|
import { fileURLToPath } from "node:url";
|
|
14
16
|
|
|
15
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
18
|
const projectRoot = resolve(__dirname, "..");
|
|
19
|
+
const packageJson = JSON.parse(
|
|
20
|
+
readFileSync(resolve(projectRoot, "package.json"), "utf8"),
|
|
21
|
+
);
|
|
22
|
+
const packageVersion = packageJson.version;
|
|
23
|
+
const inNodeModules =
|
|
24
|
+
projectRoot.includes("/node_modules/") ||
|
|
25
|
+
projectRoot.includes("\\node_modules\\");
|
|
26
|
+
const isWindows = process.platform === "win32";
|
|
27
|
+
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
28
|
+
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
29
|
+
const psBin = process.platform === "win32" ? "powershell.exe" : null;
|
|
17
30
|
|
|
18
31
|
const [, , command = "start", ...rest] = process.argv;
|
|
19
32
|
|
|
33
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
34
|
+
console.log(packageVersion);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
const commands = {
|
|
21
|
-
start: [
|
|
22
|
-
dev: [
|
|
39
|
+
start: [npxBin, "next", "start", "-p", "5555", ...rest],
|
|
40
|
+
dev: [
|
|
41
|
+
npxBin,
|
|
42
|
+
"next",
|
|
43
|
+
"dev",
|
|
44
|
+
"--webpack",
|
|
45
|
+
"-H",
|
|
46
|
+
"kronosys",
|
|
47
|
+
"-p",
|
|
48
|
+
"5555",
|
|
49
|
+
...rest,
|
|
50
|
+
],
|
|
23
51
|
};
|
|
24
52
|
|
|
25
53
|
if (!commands[command]) {
|
|
@@ -28,14 +56,34 @@ if (!commands[command]) {
|
|
|
28
56
|
process.exit(1);
|
|
29
57
|
}
|
|
30
58
|
|
|
31
|
-
function runCommand(bin, args, env) {
|
|
59
|
+
function runCommand(bin, args, env, cwd) {
|
|
32
60
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
const windowsArg = (arg) => `'${String(arg).replaceAll("'", "''")}'`;
|
|
62
|
+
|
|
63
|
+
const child = isWindows
|
|
64
|
+
? spawn(
|
|
65
|
+
psBin,
|
|
66
|
+
[
|
|
67
|
+
"-NoLogo",
|
|
68
|
+
"-NoProfile",
|
|
69
|
+
"-ExecutionPolicy",
|
|
70
|
+
"Bypass",
|
|
71
|
+
"-Command",
|
|
72
|
+
"& " + [bin, ...args].map(windowsArg).join(" "),
|
|
73
|
+
],
|
|
74
|
+
{
|
|
75
|
+
cwd,
|
|
76
|
+
stdio: "inherit",
|
|
77
|
+
shell: false,
|
|
78
|
+
env,
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
: spawn(bin, args, {
|
|
82
|
+
cwd,
|
|
83
|
+
stdio: "inherit",
|
|
84
|
+
shell: false,
|
|
85
|
+
env,
|
|
86
|
+
});
|
|
39
87
|
|
|
40
88
|
child.on("error", (err) => {
|
|
41
89
|
rejectPromise(err);
|
|
@@ -47,23 +95,100 @@ function runCommand(bin, args, env) {
|
|
|
47
95
|
});
|
|
48
96
|
}
|
|
49
97
|
|
|
98
|
+
function prepareRuntimeRoot() {
|
|
99
|
+
const runtimeRoot = resolve(
|
|
100
|
+
homedir(),
|
|
101
|
+
".kronosys",
|
|
102
|
+
"runtime",
|
|
103
|
+
String(packageVersion),
|
|
104
|
+
);
|
|
105
|
+
const runtimePackageJson = resolve(runtimeRoot, "package.json");
|
|
106
|
+
|
|
107
|
+
if (!existsSync(runtimePackageJson)) {
|
|
108
|
+
mkdirSync(runtimeRoot, { recursive: true });
|
|
109
|
+
cpSync(projectRoot, runtimeRoot, {
|
|
110
|
+
recursive: true,
|
|
111
|
+
force: true,
|
|
112
|
+
filter: (src) => {
|
|
113
|
+
const normalized = src.replaceAll("\\", "/");
|
|
114
|
+
const rel = normalized
|
|
115
|
+
.replace(projectRoot.replaceAll("\\", "/"), "")
|
|
116
|
+
.replace(/^\/+/, "");
|
|
117
|
+
if (!rel) return true;
|
|
118
|
+
if (rel === ".next" || rel.startsWith(".next/")) return false;
|
|
119
|
+
if (rel === "node_modules" || rel.startsWith("node_modules/"))
|
|
120
|
+
return false;
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return runtimeRoot;
|
|
127
|
+
}
|
|
128
|
+
|
|
50
129
|
async function main() {
|
|
130
|
+
const runRoot = inNodeModules ? prepareRuntimeRoot() : projectRoot;
|
|
51
131
|
const env = {
|
|
52
132
|
...process.env,
|
|
53
133
|
NODE_ENV: command === "dev" ? "development" : "production",
|
|
54
134
|
};
|
|
55
135
|
|
|
56
|
-
//
|
|
57
|
-
if (
|
|
58
|
-
console.log("[kronosys]
|
|
59
|
-
const
|
|
136
|
+
// Global installs live under node_modules; install runtime deps in copied runtime dir.
|
|
137
|
+
if (inNodeModules && !existsSync(resolve(runRoot, "node_modules"))) {
|
|
138
|
+
console.log("[kronosys] Installing runtime dependencies...");
|
|
139
|
+
const installCode = await runCommand(
|
|
140
|
+
npmBin,
|
|
141
|
+
["install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
142
|
+
{
|
|
143
|
+
...env,
|
|
144
|
+
HUSKY: "0",
|
|
145
|
+
npm_config_cache:
|
|
146
|
+
process.env.npm_config_cache ?? resolve(homedir(), ".npm"),
|
|
147
|
+
},
|
|
148
|
+
runRoot,
|
|
149
|
+
);
|
|
150
|
+
if (installCode !== 0) {
|
|
151
|
+
process.exit(installCode);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Ensure native bindings are present for the current OS/ABI.
|
|
155
|
+
const rebuildCode = await runCommand(
|
|
156
|
+
npmBin,
|
|
157
|
+
["rebuild", "better-sqlite3", "--no-audit", "--no-fund"],
|
|
158
|
+
{
|
|
159
|
+
...env,
|
|
160
|
+
HUSKY: "0",
|
|
161
|
+
npm_config_cache:
|
|
162
|
+
process.env.npm_config_cache ?? resolve(homedir(), ".npm"),
|
|
163
|
+
},
|
|
164
|
+
runRoot,
|
|
165
|
+
);
|
|
166
|
+
if (rebuildCode !== 0) {
|
|
167
|
+
process.exit(rebuildCode);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Production mode requires a Next build. Build once on first run.
|
|
172
|
+
if (
|
|
173
|
+
command === "start" &&
|
|
174
|
+
!existsSync(resolve(runRoot, ".next", "BUILD_ID"))
|
|
175
|
+
) {
|
|
176
|
+
console.log(
|
|
177
|
+
"[kronosys] No production build found. Running `next build`...",
|
|
178
|
+
);
|
|
179
|
+
const buildCode = await runCommand(
|
|
180
|
+
npxBin,
|
|
181
|
+
["next", "build", "--webpack"],
|
|
182
|
+
env,
|
|
183
|
+
runRoot,
|
|
184
|
+
);
|
|
60
185
|
if (buildCode !== 0) {
|
|
61
186
|
process.exit(buildCode);
|
|
62
187
|
}
|
|
63
188
|
}
|
|
64
189
|
|
|
65
190
|
const [bin, ...args] = commands[command];
|
|
66
|
-
const code = await runCommand(bin, args, env);
|
|
191
|
+
const code = await runCommand(bin, args, env, runRoot);
|
|
67
192
|
process.exit(code);
|
|
68
193
|
}
|
|
69
194
|
|
|
@@ -53,6 +53,8 @@ export function KronosysPayloadProvider({ children }: { children: ReactNode }) {
|
|
|
53
53
|
return true;
|
|
54
54
|
} catch (e: unknown) {
|
|
55
55
|
lastRefreshOkRef.current = false;
|
|
56
|
+
latestPayloadRef.current = null;
|
|
57
|
+
setPayload(null);
|
|
56
58
|
setError(e instanceof Error ? e.message : String(e));
|
|
57
59
|
return false;
|
|
58
60
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
type Props = Readonly<{
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export function RouteTransition({ children }: Props) {
|
|
11
|
+
const pathname = usePathname();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div key={pathname} className="kronosys-route-transition-in">
|
|
15
|
+
{children}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|