@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.
Files changed (179) hide show
  1. package/README.md +81 -0
  2. package/app/api/action/route.ts +16 -0
  3. package/app/api/backup/route.ts +84 -0
  4. package/app/api/health/route.ts +22 -0
  5. package/app/api/state/route.ts +27 -0
  6. package/app/apple-icon.png +0 -0
  7. package/app/changelog/page.tsx +122 -0
  8. package/app/globals.css +210 -0
  9. package/app/guide/layout.tsx +11 -0
  10. package/app/guide/page.tsx +278 -0
  11. package/app/icon.png +0 -0
  12. package/app/layout.tsx +77 -0
  13. package/app/licenses/layout.tsx +11 -0
  14. package/app/licenses/page.tsx +246 -0
  15. package/app/manifest.ts +32 -0
  16. package/app/page.tsx +1610 -0
  17. package/app/reporting/page.tsx +2943 -0
  18. package/app/settings/layout.tsx +10 -0
  19. package/app/settings/page.tsx +3518 -0
  20. package/bin/kronosys.mjs +46 -0
  21. package/components/KronosysPackageVersionProvider.tsx +19 -0
  22. package/components/KronosysPayloadProvider.tsx +109 -0
  23. package/components/PwaRegister.tsx +25 -0
  24. package/components/SiteLegalFooter.tsx +21 -0
  25. package/components/ThemeProvider.tsx +78 -0
  26. package/components/dashboard/AppShellLiveSessionDrawer.tsx +394 -0
  27. package/components/dashboard/AppShellRouteNav.tsx +131 -0
  28. package/components/dashboard/AppVersionStamp.tsx +16 -0
  29. package/components/dashboard/DashboardCollapsibleSection.tsx +57 -0
  30. package/components/dashboard/DashboardColumnHintsBanner.tsx +159 -0
  31. package/components/dashboard/DashboardCommandCenter.tsx +470 -0
  32. package/components/dashboard/DashboardLangGateModal.tsx +118 -0
  33. package/components/dashboard/DashboardLoadingOverlay.tsx +42 -0
  34. package/components/dashboard/DashboardSimpleModal.tsx +337 -0
  35. package/components/dashboard/DashboardSuspenseFallback.tsx +52 -0
  36. package/components/dashboard/DashboardToastProvider.tsx +64 -0
  37. package/components/dashboard/DashboardTour.tsx +435 -0
  38. package/components/dashboard/DeferredDescriptionPopoverWrap.tsx +39 -0
  39. package/components/dashboard/DeleteSessionModal.tsx +130 -0
  40. package/components/dashboard/DescriptionTooltipPortaled.tsx +31 -0
  41. package/components/dashboard/GitIdentityQuickSetupModal.tsx +211 -0
  42. package/components/dashboard/HeaderIntegrationBadges.tsx +69 -0
  43. package/components/dashboard/InlineMetricHelpTrigger.tsx +102 -0
  44. package/components/dashboard/IssuePickerModal.tsx +168 -0
  45. package/components/dashboard/KronoFocusPanel.tsx +834 -0
  46. package/components/dashboard/KronosysDatetimePopoverField.tsx +357 -0
  47. package/components/dashboard/KronosysTimePopoverField.tsx +233 -0
  48. package/components/dashboard/LanguageMenu.tsx +123 -0
  49. package/components/dashboard/MongoMirrorSyncLine.tsx +57 -0
  50. package/components/dashboard/NewSessionScopeModal.tsx +410 -0
  51. package/components/dashboard/PageRefreshButton.tsx +130 -0
  52. package/components/dashboard/PlainHelpPopover.tsx +97 -0
  53. package/components/dashboard/ReportingPageToc.tsx +68 -0
  54. package/components/dashboard/ReportingTour.tsx +342 -0
  55. package/components/dashboard/SavedProjectPicker.tsx +92 -0
  56. package/components/dashboard/SavedTagPicker.tsx +115 -0
  57. package/components/dashboard/ScrollToTopFab.tsx +41 -0
  58. package/components/dashboard/SelectedSessionSidebarBlock.tsx +630 -0
  59. package/components/dashboard/SessionEndReasonEditor.tsx +114 -0
  60. package/components/dashboard/SessionListPanel.tsx +320 -0
  61. package/components/dashboard/SessionLocMetricsSection.tsx +128 -0
  62. package/components/dashboard/SettingsTagsProjectsSection.tsx +993 -0
  63. package/components/dashboard/SettingsTour.tsx +332 -0
  64. package/components/dashboard/TagPills.tsx +149 -0
  65. package/components/dashboard/TagsHelpTrigger.tsx +84 -0
  66. package/components/dashboard/TaskFocusPanel.tsx +1261 -0
  67. package/components/dashboard/TaskSessionLiveCard.tsx +832 -0
  68. package/components/dashboard/TaskSubtasksBlock.tsx +748 -0
  69. package/components/dashboard/ThemeToggle.test.tsx +26 -0
  70. package/components/dashboard/ThemeToggle.tsx +36 -0
  71. package/components/dashboard/UserGuideBodyText.tsx +62 -0
  72. package/components/dashboard/WorkspaceGitRepoCard.tsx +191 -0
  73. package/components/dashboard/taskFieldStyles.ts +139 -0
  74. package/components/dashboard/useAnchoredFloatingPortalStyle.ts +71 -0
  75. package/components/dashboard/useDescriptionPopoverAfterMs.ts +220 -0
  76. package/components/dashboard/useKronoFocusLiveSeconds.ts +36 -0
  77. package/components/dashboard/useSmoothStopwatchMs.ts +25 -0
  78. package/lib/appShellHeaderClasses.ts +12 -0
  79. package/lib/backupCsvExport.test.ts +149 -0
  80. package/lib/backupCsvExport.ts +392 -0
  81. package/lib/changelogCopy.ts +34 -0
  82. package/lib/concurrentTaskStartPreference.ts +29 -0
  83. package/lib/dashboardClockFormat.ts +13 -0
  84. package/lib/dashboardColumnChrome.ts +3 -0
  85. package/lib/dashboardColumnHintsStorage.ts +57 -0
  86. package/lib/dashboardCopy.ts +1831 -0
  87. package/lib/dashboardDetachedUrlHintStorage.ts +24 -0
  88. package/lib/dashboardGitIdentityBannerStorage.ts +36 -0
  89. package/lib/dashboardLangStorage.ts +72 -0
  90. package/lib/dashboardQuickSearch.ts +476 -0
  91. package/lib/dashboardQuickSearchQuery.test.ts +63 -0
  92. package/lib/dashboardQuickSearchQuery.ts +179 -0
  93. package/lib/dashboardSessionNav.ts +33 -0
  94. package/lib/dashboardShortcuts.ts +268 -0
  95. package/lib/dashboardTimeZone.ts +91 -0
  96. package/lib/dashboardTourStorage.ts +68 -0
  97. package/lib/dataDir.test.ts +87 -0
  98. package/lib/dataDir.ts +83 -0
  99. package/lib/devDataPreferenceFile.ts +55 -0
  100. package/lib/devDataRuntimeInfo.ts +34 -0
  101. package/lib/formatIsoShort.test.ts +46 -0
  102. package/lib/formatIsoShort.ts +29 -0
  103. package/lib/generatedUserChangelog.ts +34 -0
  104. package/lib/gitlabIssueSearch.ts +8 -0
  105. package/lib/kronoFocusDurationHistory.ts +71 -0
  106. package/lib/kronoFocusRhythm.test.ts +130 -0
  107. package/lib/kronoFocusRhythm.ts +46 -0
  108. package/lib/kronoFocusTimerUrgency.test.ts +74 -0
  109. package/lib/kronoFocusTimerUrgency.ts +24 -0
  110. package/lib/kronosysApi.ts +143 -0
  111. package/lib/legacyEditorPayloadKeys.ts +52 -0
  112. package/lib/legacyKronoFocusStorageKeys.test.ts +29 -0
  113. package/lib/legacyKronoFocusStorageKeys.ts +32 -0
  114. package/lib/licensesCopy.ts +128 -0
  115. package/lib/openPlainTextInNewTab.ts +49 -0
  116. package/lib/readKronosysPackageVersion.ts +10 -0
  117. package/lib/reportingAggregate.test.ts +325 -0
  118. package/lib/reportingAggregate.ts +819 -0
  119. package/lib/reportingDatePresets.ts +41 -0
  120. package/lib/reportingMetricHelp.ts +430 -0
  121. package/lib/reportingNonFinalIndicators.test.ts +157 -0
  122. package/lib/reportingNonFinalIndicators.ts +102 -0
  123. package/lib/reportingStrings.ts +491 -0
  124. package/lib/reportingTagWeekBreakdown.test.ts +141 -0
  125. package/lib/reportingTagWeekBreakdown.ts +181 -0
  126. package/lib/reportingWeekLayout.test.ts +239 -0
  127. package/lib/reportingWeekLayout.ts +313 -0
  128. package/lib/sessionAssiduity.test.ts +25 -0
  129. package/lib/sessionAssiduity.ts +33 -0
  130. package/lib/sessionEndReason.ts +55 -0
  131. package/lib/sessionEndWarnings.test.ts +200 -0
  132. package/lib/sessionEndWarnings.ts +125 -0
  133. package/lib/sessionListMerge.test.ts +101 -0
  134. package/lib/sessionListMerge.ts +70 -0
  135. package/lib/sessionTaskSidebarStats.test.ts +24 -0
  136. package/lib/sessionTaskSidebarStats.ts +54 -0
  137. package/lib/settingsCopy.ts +1276 -0
  138. package/lib/taskParsing.test.ts +153 -0
  139. package/lib/taskParsing.ts +737 -0
  140. package/lib/theme.ts +15 -0
  141. package/lib/translucentButtonClasses.ts +34 -0
  142. package/lib/usageProfile.test.ts +84 -0
  143. package/lib/usageProfile.ts +52 -0
  144. package/lib/userGuideCopy.ts +464 -0
  145. package/lib/workspaceLocDefaults.ts +21 -0
  146. package/next-env.d.ts +6 -0
  147. package/next.config.ts +15 -0
  148. package/package.json +87 -0
  149. package/postcss.config.mjs +12 -0
  150. package/public/apple-icon.png +0 -0
  151. package/public/favicon.ico +0 -0
  152. package/public/file.svg +1 -0
  153. package/public/globe.svg +1 -0
  154. package/public/icon-192.png +0 -0
  155. package/public/icon-512.png +0 -0
  156. package/public/icon.png +0 -0
  157. package/public/next.svg +1 -0
  158. package/public/sw.js +13 -0
  159. package/public/traceback.png +0 -0
  160. package/public/vercel.svg +1 -0
  161. package/public/window.svg +1 -0
  162. package/server/actionDispatch.test.ts +723 -0
  163. package/server/actionDispatch.ts +1476 -0
  164. package/server/actionTaskSession.test.ts +713 -0
  165. package/server/actionTaskSession.ts +717 -0
  166. package/server/db.ts +42 -0
  167. package/server/defaultCfg.ts +87 -0
  168. package/server/gitlabTokenStore.ts +34 -0
  169. package/server/kronoFocusHydrate.test.ts +142 -0
  170. package/server/kronoFocusHydrate.ts +69 -0
  171. package/server/kronoFocusMigrate.test.ts +53 -0
  172. package/server/kronoFocusMigrate.ts +78 -0
  173. package/server/mainTimerHydrate.test.ts +65 -0
  174. package/server/mainTimerHydrate.ts +53 -0
  175. package/server/payloadStore.test.ts +78 -0
  176. package/server/payloadStore.ts +83 -0
  177. package/server/sessionWallHydrate.test.ts +46 -0
  178. package/server/sessionWallHydrate.ts +88 -0
  179. package/tsconfig.json +41 -0
@@ -0,0 +1,78 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+
7
+ import { resetSqliteConnection } from "@/server/db";
8
+ import { readPayload, writePayload } from "@/server/payloadStore";
9
+
10
+ describe("payloadStore (intégration SQLite)", () => {
11
+ let tmpDir: string;
12
+
13
+ beforeEach(() => {
14
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kronosys-payload-"));
15
+ process.env.TRACE_DATA_DIR = tmpDir;
16
+ resetSqliteConnection();
17
+ });
18
+
19
+ afterEach(() => {
20
+ resetSqliteConnection();
21
+ delete process.env.TRACE_DATA_DIR;
22
+ fs.rmSync(tmpDir, { recursive: true, force: true });
23
+ });
24
+
25
+ it("readPayload retourne un payload initial valide sur base vierge", () => {
26
+ const p = readPayload();
27
+ expect(p.viewType).toBe("dashboard");
28
+ expect(Array.isArray(p.history)).toBe(true);
29
+ expect(p.cfg).toBeDefined();
30
+ expect(p.dashboardDataOrigin).toBe("local_next");
31
+ });
32
+
33
+ it("writePayload puis readPayload retourne les mêmes données", () => {
34
+ const initial = readPayload();
35
+ const modified = { ...initial, knownTags: ["dev", "design"] };
36
+ writePayload(modified);
37
+ const back = readPayload();
38
+ expect(back.knownTags).toEqual(["dev", "design"]);
39
+ });
40
+
41
+ it("writePayload est idempotent (INSERT OR REPLACE)", () => {
42
+ const p = readPayload();
43
+ writePayload(p);
44
+ writePayload(p);
45
+ const back = readPayload();
46
+ expect(back.viewType).toBe("dashboard");
47
+ });
48
+
49
+ it("readPayload retourne un payload initial si la valeur stockée est du JSON invalide", async () => {
50
+ // Écrire manuellement du JSON corrompu dans la DB
51
+ const { getSqlite } = await import("@/server/db");
52
+ const db = getSqlite();
53
+ db.exec(`CREATE TABLE IF NOT EXISTS kv_store (k TEXT PRIMARY KEY NOT NULL, v TEXT NOT NULL);`);
54
+ db.prepare("INSERT OR REPLACE INTO kv_store (k, v) VALUES (?, ?)").run(
55
+ "dashboard_payload_v1",
56
+ "{ invalide json [[["
57
+ );
58
+ const recovered = readPayload();
59
+ expect(recovered.viewType).toBe("dashboard");
60
+ });
61
+
62
+ it("readPayload fusionne les défauts cfg si une clé est manquante dans le payload stocké", () => {
63
+ const p = readPayload();
64
+ // Supprimer une clé du cfg et réécrire
65
+ const cfg = { ...p.cfg } as Record<string, unknown>;
66
+ delete cfg["taskDefaultTagBucketEnabled"];
67
+ writePayload({ ...p, cfg });
68
+ const back = readPayload();
69
+ // Le defaultCfg doit remettre la clé manquante
70
+ expect(back.cfg).toHaveProperty("taskDefaultTagBucketEnabled");
71
+ });
72
+
73
+ it("knownProjects et knownTags sont des tableaux après lecture initiale", () => {
74
+ const p = readPayload();
75
+ expect(Array.isArray(p.knownProjects)).toBe(true);
76
+ expect(Array.isArray(p.knownTags)).toBe(true);
77
+ });
78
+ });
@@ -0,0 +1,83 @@
1
+ import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
2
+ import { readUseProductionDataInDevelopmentFromFile } from "@/lib/devDataPreferenceFile";
3
+ import { migrateLegacyPayloadRecord } from "@/lib/legacyEditorPayloadKeys";
4
+
5
+ import { defaultKronosysCfg } from "./defaultCfg";
6
+ import { getSqlite } from "./db";
7
+ import { hydrateKronoFocusInPayload } from "./kronoFocusHydrate";
8
+ import { migrateLegacyKronoFocusPayload } from "./kronoFocusMigrate";
9
+ import { materializeRunningMainTimersInPayload } from "./mainTimerHydrate";
10
+ import { materializeSessionWallClockInPayload } from "./sessionWallHydrate";
11
+
12
+ const PAYLOAD_KEY = "dashboard_payload_v1";
13
+
14
+ function ensureTables(): void {
15
+ getSqlite().exec(`
16
+ CREATE TABLE IF NOT EXISTS kv_store (
17
+ k TEXT PRIMARY KEY NOT NULL,
18
+ v TEXT NOT NULL
19
+ );
20
+ `);
21
+ }
22
+
23
+ export function readPayload(): KronosysUpdatePayload {
24
+ ensureTables();
25
+ const row = getSqlite().prepare("SELECT v FROM kv_store WHERE k = ?").get(PAYLOAD_KEY) as { v: string } | undefined;
26
+ if (!row?.v) {
27
+ const initial = createInitialPayload();
28
+ writePayload(initial);
29
+ return initial;
30
+ }
31
+ try {
32
+ const parsed = JSON.parse(row.v) as KronosysUpdatePayload;
33
+ const cfg = { ...defaultKronosysCfg(), ...(typeof parsed.cfg === "object" && parsed.cfg ? parsed.cfg : {}) };
34
+ if (process.env.NODE_ENV === "development" && !process.env.TRACE_DATA_DIR?.trim()) {
35
+ (cfg as Record<string, unknown>).developmentUseProductionData =
36
+ readUseProductionDataInDevelopmentFromFile();
37
+ }
38
+ const merged = { ...parsed, cfg };
39
+ let legacyMigrated = migrateLegacyPayloadRecord(merged as Record<string, unknown>);
40
+ if (merged.cfg && typeof merged.cfg === "object") {
41
+ legacyMigrated = migrateLegacyPayloadRecord(merged.cfg as Record<string, unknown>) || legacyMigrated;
42
+ }
43
+ const migrated = migrateLegacyKronoFocusPayload(merged);
44
+ const hydrated = hydrateKronoFocusInPayload(merged);
45
+ const mainTimers = materializeRunningMainTimersInPayload(merged);
46
+ const sessionWall = materializeSessionWallClockInPayload(merged);
47
+ if (migrated || hydrated || mainTimers || sessionWall || legacyMigrated) {
48
+ writePayload(merged);
49
+ }
50
+ return merged;
51
+ } catch {
52
+ const initial = createInitialPayload();
53
+ writePayload(initial);
54
+ return initial;
55
+ }
56
+ }
57
+
58
+ export function writePayload(p: KronosysUpdatePayload): void {
59
+ ensureTables();
60
+ getSqlite().prepare("INSERT OR REPLACE INTO kv_store (k, v) VALUES (?, ?)").run(PAYLOAD_KEY, JSON.stringify(p));
61
+ }
62
+
63
+ function createInitialPayload(): KronosysUpdatePayload {
64
+ const cfg = { ...defaultKronosysCfg() } as Record<string, unknown>;
65
+ if (process.env.NODE_ENV === "development" && !process.env.TRACE_DATA_DIR?.trim()) {
66
+ cfg.developmentUseProductionData = readUseProductionDataInDevelopmentFromFile();
67
+ }
68
+ return {
69
+ viewType: "dashboard",
70
+ cfg,
71
+ history: [],
72
+ historyArchived: [] as unknown[],
73
+ inspectingSessionId: null,
74
+ knownTags: [],
75
+ knownProjects: [],
76
+ userKnownTags: [],
77
+ excludedSuggestionTags: [],
78
+ tagDescriptions: {},
79
+ projectDescriptions: {},
80
+ gitIdentity: {},
81
+ dashboardDataOrigin: "local_next",
82
+ };
83
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
3
+ import { materializeSessionWallClockInPayload, SESSION_WALL_SEGMENT_STARTED_AT } from "./sessionWallHydrate";
4
+
5
+ describe("materializeSessionWallClockInPayload", () => {
6
+ it("accumule la durée murale entre deux appels", () => {
7
+ const t0 = Date.UTC(2026, 2, 1, 8, 0, 0);
8
+ const cur = {
9
+ sessionId: "sid-1",
10
+ archived: false,
11
+ isPaused: false,
12
+ endAt: null,
13
+ sessionDurationMinutes: 0,
14
+ [SESSION_WALL_SEGMENT_STARTED_AT]: new Date(t0).toISOString(),
15
+ };
16
+ const p = {
17
+ viewType: "dashboard",
18
+ cfg: {},
19
+ current: cur,
20
+ } as unknown as KronosysUpdatePayload;
21
+
22
+ const t1 = t0 + 120_000;
23
+ const w1 = materializeSessionWallClockInPayload(p, t1);
24
+ expect(w1).toBe(true);
25
+ expect(cur.sessionDurationMinutes).toBeCloseTo(2, 5);
26
+
27
+ const t2 = t1 + 60_000;
28
+ const w2 = materializeSessionWallClockInPayload(p, t2);
29
+ expect(w2).toBe(true);
30
+ expect(cur.sessionDurationMinutes).toBeCloseTo(3, 5);
31
+ });
32
+
33
+ it("ne matérialise pas si la session est en pause", () => {
34
+ const cur = {
35
+ sessionId: "sid",
36
+ archived: false,
37
+ isPaused: true,
38
+ endAt: null,
39
+ sessionDurationMinutes: 1,
40
+ [SESSION_WALL_SEGMENT_STARTED_AT]: new Date().toISOString(),
41
+ };
42
+ const p = { viewType: "dashboard", cfg: {}, current: cur } as unknown as KronosysUpdatePayload;
43
+ expect(materializeSessionWallClockInPayload(p, Date.UTC(2026, 2, 1, 9, 0, 0))).toBe(false);
44
+ expect(cur.sessionDurationMinutes).toBe(1);
45
+ });
46
+ });
@@ -0,0 +1,88 @@
1
+ import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
2
+ import { asRecord } from "./actionTaskSession";
3
+
4
+ /** Horodatage ISO du segment de durée murale en cours (session live). */
5
+ export const SESSION_WALL_SEGMENT_STARTED_AT = "sessionWallSegmentStartedAt";
6
+
7
+ function liveSessionWallClockActive(cur: Record<string, unknown>): boolean {
8
+ const sid = typeof cur.sessionId === "string" ? cur.sessionId.trim() : "";
9
+ if (!sid) {
10
+ return false;
11
+ }
12
+ if (cur.archived === true) {
13
+ return false;
14
+ }
15
+ if (cur.isPaused === true) {
16
+ return false;
17
+ }
18
+ if (typeof cur.endAt === "string" && cur.endAt.trim() !== "") {
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+
24
+ /**
25
+ * Consolide le segment de durée murale dans `sessionDurationMinutes` (minutes, valeur réelle)
26
+ * et retire l’horodatage de segment.
27
+ */
28
+ export function flushSessionWallSegmentOnLive(cur: Record<string, unknown>): void {
29
+ const raw = cur[SESSION_WALL_SEGMENT_STARTED_AT];
30
+ if (typeof raw !== "string" || raw.trim() === "") {
31
+ return;
32
+ }
33
+ const startedMs = Date.parse(raw);
34
+ if (!Number.isFinite(startedMs)) {
35
+ delete cur[SESSION_WALL_SEGMENT_STARTED_AT];
36
+ return;
37
+ }
38
+ const elapsed = Math.max(0, Date.now() - startedMs);
39
+ const prevMin =
40
+ typeof cur.sessionDurationMinutes === "number" && Number.isFinite(cur.sessionDurationMinutes)
41
+ ? Number(cur.sessionDurationMinutes)
42
+ : 0;
43
+ cur.sessionDurationMinutes = prevMin + elapsed / 60000;
44
+ delete cur[SESSION_WALL_SEGMENT_STARTED_AT];
45
+ }
46
+
47
+ /** Démarre un segment de durée murale si la session est ouverte et non en pause. */
48
+ export function ensureSessionWallSegmentOnLive(cur: Record<string, unknown>): void {
49
+ if (!liveSessionWallClockActive(cur)) {
50
+ return;
51
+ }
52
+ const raw = cur[SESSION_WALL_SEGMENT_STARTED_AT];
53
+ if (typeof raw === "string" && raw.trim() !== "") {
54
+ return;
55
+ }
56
+ cur[SESSION_WALL_SEGMENT_STARTED_AT] = new Date().toISOString();
57
+ }
58
+
59
+ /**
60
+ * Fait progresser `sessionDurationMinutes` à chaque lecture d’état (poll), même hors tableau de bord.
61
+ */
62
+ export function materializeSessionWallClockInPayload(p: KronosysUpdatePayload, nowMs = Date.now()): boolean {
63
+ const cur = asRecord(p.current);
64
+ if (!cur || !liveSessionWallClockActive(cur)) {
65
+ return false;
66
+ }
67
+ const raw = cur[SESSION_WALL_SEGMENT_STARTED_AT];
68
+ if (typeof raw !== "string" || raw.trim() === "") {
69
+ cur[SESSION_WALL_SEGMENT_STARTED_AT] = new Date(nowMs).toISOString();
70
+ return true;
71
+ }
72
+ const startedMs = Date.parse(raw);
73
+ if (!Number.isFinite(startedMs)) {
74
+ cur[SESSION_WALL_SEGMENT_STARTED_AT] = new Date(nowMs).toISOString();
75
+ return true;
76
+ }
77
+ const elapsed = Math.max(0, nowMs - startedMs);
78
+ if (elapsed <= 0) {
79
+ return false;
80
+ }
81
+ const prevMin =
82
+ typeof cur.sessionDurationMinutes === "number" && Number.isFinite(cur.sessionDurationMinutes)
83
+ ? Number(cur.sessionDurationMinutes)
84
+ : 0;
85
+ cur.sessionDurationMinutes = prevMin + elapsed / 60000;
86
+ cur[SESSION_WALL_SEGMENT_STARTED_AT] = new Date(nowMs).toISOString();
87
+ return true;
88
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "ES2022"
8
+ ],
9
+ "allowJs": false,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "ESNext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "react-jsx",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ],
25
+ "paths": {
26
+ "@/*": [
27
+ "./*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules"
40
+ ]
41
+ }