@nightkatana/kronosys-app 1.0.0-beta.15 → 1.0.0-beta.17
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/app/api/restore/route.ts +151 -0
- package/app/changelog/page.tsx +69 -4
- package/app/implementation/page.tsx +12 -1
- package/app/page.tsx +121 -50
- package/app/settings/page.tsx +150 -3
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +103 -28
- package/components/dashboard/SessionListPanel.tsx +32 -4
- package/components/dashboard/SettingsTagsProjectsSection.tsx +427 -34
- package/components/dashboard/TaskFocusPanel.tsx +343 -312
- package/components/dashboard/TaskNotesDisplay.test.tsx +110 -0
- package/components/dashboard/TaskSessionLiveCard.tsx +70 -26
- package/lib/dashboardCopy.ts +18 -6
- package/lib/generatedUserChangelog.ts +47 -0
- package/lib/implementationNotes.ts +64 -2
- package/lib/settingsCopy.ts +73 -4
- package/lib/userGuideCopy.ts +24 -16
- package/package.json +2 -1
- package/server/actionDispatch.test.ts +129 -0
- package/server/actionDispatch.ts +62 -6
- package/server/actionTaskSession.test.ts +10 -0
- package/server/actionTaskSession.ts +159 -1
- package/server/mainTimerHydrate.test.ts +29 -1
- package/server/mainTimerHydrate.ts +15 -0
- package/server/sessionWallHydrate.test.ts +18 -0
- package/server/sessionWallHydrate.ts +10 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
|
|
5
|
+
import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
|
|
6
|
+
import { ensureDataDirectory } from "@/lib/dataDir";
|
|
7
|
+
import { getSqlite, resetSqliteConnection } from "@/server/db";
|
|
8
|
+
import { writePayload } from "@/server/payloadStore";
|
|
9
|
+
|
|
10
|
+
export const runtime = "nodejs";
|
|
11
|
+
|
|
12
|
+
const DB_NAME = "kronosys.sqlite";
|
|
13
|
+
const MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
function nowStamp(): string {
|
|
16
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "-");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
20
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validatePayloadShape(v: unknown): v is KronosysUpdatePayload {
|
|
24
|
+
if (!isRecord(v)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return typeof v.viewType === "string" && v.viewType.trim().length > 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanSqliteSidecars(dbPath: string): void {
|
|
31
|
+
for (const ext of [".wal", ".shm"]) {
|
|
32
|
+
const p = `${dbPath}${ext}`;
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(p)) {
|
|
35
|
+
fs.unlinkSync(p);
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
/* ignore */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function restoreFromSqliteBuffer(buf: Buffer): { backupPath: string | null } {
|
|
44
|
+
const dir = ensureDataDirectory();
|
|
45
|
+
const dbPath = path.join(dir, DB_NAME);
|
|
46
|
+
const tmpPath = `${dbPath}.restore-tmp`;
|
|
47
|
+
const backupPath = `${dbPath}.pre-restore-${nowStamp()}.bak`;
|
|
48
|
+
let createdBackup: string | null = null;
|
|
49
|
+
|
|
50
|
+
resetSqliteConnection();
|
|
51
|
+
if (fs.existsSync(dbPath)) {
|
|
52
|
+
fs.copyFileSync(dbPath, backupPath);
|
|
53
|
+
createdBackup = backupPath;
|
|
54
|
+
}
|
|
55
|
+
cleanSqliteSidecars(dbPath);
|
|
56
|
+
fs.writeFileSync(tmpPath, buf);
|
|
57
|
+
fs.renameSync(tmpPath, dbPath);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const db = getSqlite();
|
|
61
|
+
db.pragma("integrity_check");
|
|
62
|
+
db.exec("SELECT 1;");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
resetSqliteConnection();
|
|
65
|
+
if (createdBackup && fs.existsSync(createdBackup)) {
|
|
66
|
+
fs.copyFileSync(createdBackup, dbPath);
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { backupPath: createdBackup };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function POST(req: Request) {
|
|
75
|
+
const ctype = req.headers.get("content-type") ?? "";
|
|
76
|
+
const isMultipart = ctype.toLowerCase().includes("multipart/form-data");
|
|
77
|
+
if (!isMultipart) {
|
|
78
|
+
return NextResponse.json(
|
|
79
|
+
{ ok: false, error: "unsupported_content_type" },
|
|
80
|
+
{ status: 415 },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const form = await req.formData();
|
|
85
|
+
const file = form.get("file");
|
|
86
|
+
const formatField = form.get("format");
|
|
87
|
+
const formatRaw =
|
|
88
|
+
typeof formatField === "string" ? formatField.trim().toLowerCase() : "";
|
|
89
|
+
if (!(file instanceof File)) {
|
|
90
|
+
return NextResponse.json({ ok: false, error: "file_required" }, { status: 400 });
|
|
91
|
+
}
|
|
92
|
+
if (file.size <= 0 || file.size > MAX_UPLOAD_BYTES) {
|
|
93
|
+
return NextResponse.json(
|
|
94
|
+
{ ok: false, error: "invalid_file_size", maxBytes: MAX_UPLOAD_BYTES },
|
|
95
|
+
{ status: 400 },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const lowerName = file.name.toLowerCase();
|
|
100
|
+
let format: "json" | "sqlite";
|
|
101
|
+
if (formatRaw === "json" || formatRaw === "sqlite") {
|
|
102
|
+
format = formatRaw;
|
|
103
|
+
} else {
|
|
104
|
+
format = lowerName.endsWith(".json") ? "json" : "sqlite";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (format === "json") {
|
|
108
|
+
const text = await file.text();
|
|
109
|
+
let parsed: unknown;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(text);
|
|
112
|
+
} catch {
|
|
113
|
+
return NextResponse.json({ ok: false, error: "invalid_json" }, { status: 400 });
|
|
114
|
+
}
|
|
115
|
+
if (!validatePayloadShape(parsed)) {
|
|
116
|
+
return NextResponse.json(
|
|
117
|
+
{ ok: false, error: "invalid_kronosys_payload" },
|
|
118
|
+
{ status: 400 },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
writePayload(parsed);
|
|
122
|
+
return NextResponse.json({ ok: true, restored: "json" });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const bytes = await file.arrayBuffer();
|
|
126
|
+
const buf = Buffer.from(bytes);
|
|
127
|
+
if (buf.length < 1024) {
|
|
128
|
+
return NextResponse.json(
|
|
129
|
+
{ ok: false, error: "sqlite_too_small" },
|
|
130
|
+
{ status: 400 },
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const header = buf.subarray(0, 16).toString("utf8");
|
|
134
|
+
if (!header.startsWith("SQLite format 3")) {
|
|
135
|
+
return NextResponse.json(
|
|
136
|
+
{ ok: false, error: "invalid_sqlite_file" },
|
|
137
|
+
{ status: 400 },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const { backupPath } = restoreFromSqliteBuffer(buf);
|
|
143
|
+
return NextResponse.json({ ok: true, restored: "sqlite", backupPath });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
146
|
+
return NextResponse.json(
|
|
147
|
+
{ ok: false, error: "restore_failed", detail },
|
|
148
|
+
{ status: 500 },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
package/app/changelog/page.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Suspense, useMemo } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { useSearchParams } from "next/navigation";
|
|
6
|
+
import ReactMarkdown, { type Components } from "react-markdown";
|
|
6
7
|
import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
|
|
7
8
|
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
8
9
|
import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
|
|
@@ -18,6 +19,55 @@ import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
|
|
|
18
19
|
|
|
19
20
|
type LiveShape = { language?: string };
|
|
20
21
|
|
|
22
|
+
type ChangelogItemGroup = {
|
|
23
|
+
heading: string;
|
|
24
|
+
items: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const CHANGELOG_MARKDOWN_COMPONENTS: Components = {
|
|
28
|
+
p: ({ children }) => <span>{children}</span>,
|
|
29
|
+
a: ({ children, href }) => (
|
|
30
|
+
<a
|
|
31
|
+
href={href}
|
|
32
|
+
className="text-violet-700 underline decoration-violet-400/80 underline-offset-2 hover:text-violet-900 dark:text-violet-300 dark:decoration-violet-500/60 dark:hover:text-violet-200"
|
|
33
|
+
target="_blank"
|
|
34
|
+
rel="noopener noreferrer"
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</a>
|
|
38
|
+
),
|
|
39
|
+
code: ({ children }) => (
|
|
40
|
+
<code className="rounded bg-zinc-200/70 px-1 py-0.5 font-mono text-[0.82em] dark:bg-zinc-700/80">
|
|
41
|
+
{children}
|
|
42
|
+
</code>
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function groupChangelogItems(items: string[]): ChangelogItemGroup[] {
|
|
47
|
+
const groups: ChangelogItemGroup[] = [];
|
|
48
|
+
const indexByHeading = new Map<string, number>();
|
|
49
|
+
const fallbackHeading = "Notes";
|
|
50
|
+
|
|
51
|
+
for (const raw of items) {
|
|
52
|
+
const item = raw.trim();
|
|
53
|
+
const m = /^([^—:]+)\s*[—:]\s*(.+)$/u.exec(item);
|
|
54
|
+
const heading = m?.[1]?.trim() || fallbackHeading;
|
|
55
|
+
const content = m?.[2]?.trim() || item;
|
|
56
|
+
if (!content) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const existingIndex = indexByHeading.get(heading);
|
|
60
|
+
if (existingIndex !== undefined) {
|
|
61
|
+
groups[existingIndex].items.push(content);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
indexByHeading.set(heading, groups.length);
|
|
65
|
+
groups.push({ heading, items: [content] });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return groups;
|
|
69
|
+
}
|
|
70
|
+
|
|
21
71
|
function ChangelogBody() {
|
|
22
72
|
const searchParams = useSearchParams();
|
|
23
73
|
const dashboardSessionNavId = searchParams.get("session");
|
|
@@ -90,11 +140,26 @@ function ChangelogBody() {
|
|
|
90
140
|
<h2 className="text-base font-semibold text-zinc-900 dark:text-zinc-100">
|
|
91
141
|
v{entry.version}
|
|
92
142
|
</h2>
|
|
93
|
-
<
|
|
94
|
-
{entry.items.map((
|
|
95
|
-
<
|
|
143
|
+
<div className="mt-3 space-y-3">
|
|
144
|
+
{groupChangelogItems(entry.items).map((group) => (
|
|
145
|
+
<div key={group.heading} className="space-y-1.5">
|
|
146
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
|
|
147
|
+
{group.heading}
|
|
148
|
+
</h3>
|
|
149
|
+
<ul className="list-inside list-disc space-y-1.5 text-sm leading-relaxed text-zinc-700 dark:text-zinc-300">
|
|
150
|
+
{group.items.map((item) => (
|
|
151
|
+
<li key={`${group.heading}:${item}`}>
|
|
152
|
+
<ReactMarkdown
|
|
153
|
+
components={CHANGELOG_MARKDOWN_COMPONENTS}
|
|
154
|
+
>
|
|
155
|
+
{item}
|
|
156
|
+
</ReactMarkdown>
|
|
157
|
+
</li>
|
|
158
|
+
))}
|
|
159
|
+
</ul>
|
|
160
|
+
</div>
|
|
96
161
|
))}
|
|
97
|
-
</
|
|
162
|
+
</div>
|
|
98
163
|
</section>
|
|
99
164
|
))
|
|
100
165
|
)}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { useSearchParams } from "next/navigation";
|
|
6
|
-
import { Check, FileCode2, X } from "lucide-react";
|
|
6
|
+
import { Check, FileCode2, ScrollText, X } from "lucide-react";
|
|
7
7
|
import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
|
|
8
8
|
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
9
9
|
import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
|
|
@@ -153,6 +153,17 @@ function ImplementationBody() {
|
|
|
153
153
|
<div className="flex w-full justify-end">
|
|
154
154
|
<div className={appShellHeaderToolbarClassName}>
|
|
155
155
|
<AppShellCommandCenterPlaceholder />
|
|
156
|
+
<Link
|
|
157
|
+
href={withDashboardSessionParam(
|
|
158
|
+
"/changelog",
|
|
159
|
+
dashboardSessionNavId,
|
|
160
|
+
)}
|
|
161
|
+
className="inline-flex size-10 shrink-0 items-center justify-center rounded-lg border border-zinc-300 bg-white text-zinc-700 shadow-sm transition hover:border-zinc-400 hover:bg-zinc-50 hover:text-violet-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 dark:border-zinc-600 dark:bg-zinc-800/90 dark:text-zinc-200 dark:hover:border-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-violet-300"
|
|
162
|
+
aria-label={notes.openChangelogAria}
|
|
163
|
+
title={notes.openChangelogTooltip}
|
|
164
|
+
>
|
|
165
|
+
<ScrollText size={18} />
|
|
166
|
+
</Link>
|
|
156
167
|
<AppShellRouteNav
|
|
157
168
|
current="implementation"
|
|
158
169
|
labels={nav}
|
package/app/page.tsx
CHANGED
|
@@ -232,6 +232,7 @@ function DashboardHome() {
|
|
|
232
232
|
>("keep");
|
|
233
233
|
const [tourOpen, setTourOpen] = useState(false);
|
|
234
234
|
const [newSessionModalOpen, setNewSessionModalOpen] = useState(false);
|
|
235
|
+
const pendingNewSessionFocusRef = useRef(false);
|
|
235
236
|
const [gitBannerDismissed, setGitBannerDismissed] = useState(false);
|
|
236
237
|
const [gitIdentitySetupModalOpen, setGitIdentitySetupModalOpen] =
|
|
237
238
|
useState(false);
|
|
@@ -894,6 +895,32 @@ function DashboardHome() {
|
|
|
894
895
|
});
|
|
895
896
|
}, []);
|
|
896
897
|
|
|
898
|
+
const focusTaskLauncherInput = useCallback(() => {
|
|
899
|
+
document.getElementById("dashboard-col-tasks")?.scrollIntoView({
|
|
900
|
+
behavior: "smooth",
|
|
901
|
+
block: "start",
|
|
902
|
+
});
|
|
903
|
+
const tryFocus = () => {
|
|
904
|
+
const input = document.getElementById(
|
|
905
|
+
"kronosys-task-launcher-input",
|
|
906
|
+
) as HTMLInputElement | null;
|
|
907
|
+
if (!input) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
input.focus();
|
|
911
|
+
input.select();
|
|
912
|
+
return true;
|
|
913
|
+
};
|
|
914
|
+
globalThis.requestAnimationFrame(() => {
|
|
915
|
+
if (tryFocus()) {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
globalThis.setTimeout(() => {
|
|
919
|
+
void tryFocus();
|
|
920
|
+
}, 140);
|
|
921
|
+
});
|
|
922
|
+
}, []);
|
|
923
|
+
|
|
897
924
|
const scrollToTaskInPanel = useCallback((taskId: string) => {
|
|
898
925
|
const el =
|
|
899
926
|
document.getElementById(`kronosys-active-task-${taskId}`) ??
|
|
@@ -1209,6 +1236,45 @@ function DashboardHome() {
|
|
|
1209
1236
|
await refresh();
|
|
1210
1237
|
}, [pathname, refresh, router, searchParams, sessionQueryMode]);
|
|
1211
1238
|
|
|
1239
|
+
useEffect(() => {
|
|
1240
|
+
if (!pendingNewSessionFocusRef.current) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
const liveSid =
|
|
1244
|
+
typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
|
|
1245
|
+
if (!liveSid) {
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
pendingNewSessionFocusRef.current = false;
|
|
1249
|
+
focusTaskLauncherInput();
|
|
1250
|
+
}, [focusTaskLauncherInput, live?.sessionId]);
|
|
1251
|
+
|
|
1252
|
+
const createNewSessionAndFocus = useCallback(
|
|
1253
|
+
async (sessionScope: unknown) => {
|
|
1254
|
+
setNewSessionModalOpen(false);
|
|
1255
|
+
pendingNewSessionFocusRef.current = true;
|
|
1256
|
+
await post({ type: "newSession", sessionScope });
|
|
1257
|
+
if (sessionQueryMode) {
|
|
1258
|
+
router.replace(
|
|
1259
|
+
pathnameWithUpdatedSessionQuery(
|
|
1260
|
+
pathname,
|
|
1261
|
+
searchParams.toString(),
|
|
1262
|
+
null,
|
|
1263
|
+
),
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
focusTaskLauncherInput();
|
|
1267
|
+
},
|
|
1268
|
+
[
|
|
1269
|
+
focusTaskLauncherInput,
|
|
1270
|
+
pathname,
|
|
1271
|
+
post,
|
|
1272
|
+
router,
|
|
1273
|
+
searchParams,
|
|
1274
|
+
sessionQueryMode,
|
|
1275
|
+
],
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1212
1278
|
const openSessionInNewTab = useCallback(
|
|
1213
1279
|
(sessionId: string) => {
|
|
1214
1280
|
const url = `${
|
|
@@ -1576,7 +1642,7 @@ function DashboardHome() {
|
|
|
1576
1642
|
<div className="grid w-full grid-cols-1 gap-8 xl:grid-cols-[minmax(0,1fr)_minmax(0,2.25fr)_minmax(0,1fr)] xl:items-start xl:gap-x-6 2xl:gap-x-10">
|
|
1577
1643
|
<div
|
|
1578
1644
|
id="dashboard-col-sessions"
|
|
1579
|
-
className="flex min-w-0 flex-col xl:
|
|
1645
|
+
className="flex min-w-0 flex-col xl:min-h-0 xl:overflow-visible xl:pb-6 xl:pr-3 2xl:pr-4"
|
|
1580
1646
|
>
|
|
1581
1647
|
<div className="w-full min-w-0 xl:flex xl:min-h-0 xl:flex-1 xl:flex-col">
|
|
1582
1648
|
<div className="mx-auto flex w-full max-w-md flex-col gap-8 sm:max-w-lg xl:mx-0 xl:max-w-none xl:min-h-0 xl:flex-1 xl:flex-col">
|
|
@@ -1611,53 +1677,59 @@ function DashboardHome() {
|
|
|
1611
1677
|
? renderSelectedSessionSidebarCard()
|
|
1612
1678
|
: null}
|
|
1613
1679
|
|
|
1614
|
-
<
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
sessionDurationAlertThresholdMinutes
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1680
|
+
<div className="min-h-[14rem] xl:h-full xl:min-h-0 xl:flex-1">
|
|
1681
|
+
<SessionListPanel
|
|
1682
|
+
sessions={sessionListPanelSessions}
|
|
1683
|
+
lang={lang}
|
|
1684
|
+
displayTimeZone={dashboardDisplayTimeZone}
|
|
1685
|
+
use24HourClock={dashboardUse24HourClock}
|
|
1686
|
+
liveSessionId={live?.sessionId}
|
|
1687
|
+
selectedSessionId={selectedSessionId}
|
|
1688
|
+
t={dt}
|
|
1689
|
+
onSelectSession={(id) => void handleSelectSession(id)}
|
|
1690
|
+
onOpenSessionInNewTab={openSessionInNewTab}
|
|
1691
|
+
onEndLiveSession={() =>
|
|
1692
|
+
void handleRequestEndLiveSession()
|
|
1693
|
+
}
|
|
1694
|
+
onArchiveSession={confirmArchiveSession}
|
|
1695
|
+
onDeleteSession={(id) => setDeleteSessionId(id)}
|
|
1696
|
+
onOpenArchives={() =>
|
|
1697
|
+
void router.push(
|
|
1698
|
+
withDashboardSessionParam(
|
|
1699
|
+
"/settings#settings-archived-sessions",
|
|
1700
|
+
selectedSessionId,
|
|
1701
|
+
),
|
|
1702
|
+
)
|
|
1703
|
+
}
|
|
1704
|
+
archivedCount={historyArchived.length}
|
|
1705
|
+
mongoPushEnabled={mongoPushEnabled}
|
|
1706
|
+
onPushSessionToMongo={(id) =>
|
|
1707
|
+
void handlePushSessionToMongo(id)
|
|
1708
|
+
}
|
|
1709
|
+
pushingSessionId={mongoPushBusyId}
|
|
1710
|
+
sessionDurationAlertThresholdMinutes={
|
|
1711
|
+
sessionDurationAlertThresholdMinutes
|
|
1712
|
+
}
|
|
1713
|
+
onOpenSessionGantt={openGanttForSessionId}
|
|
1714
|
+
sessionRowExitAnimateId={sessionRowExitAnimateId}
|
|
1715
|
+
onSessionRowExitAnimationDone={
|
|
1716
|
+
clearSessionRowExitStates
|
|
1717
|
+
}
|
|
1718
|
+
liveChromeExitSessionId={endLiveListExitAnimateId}
|
|
1719
|
+
sortPinSessionId={endLiveListExitAnimateId}
|
|
1720
|
+
sessionDetailInline={
|
|
1721
|
+
archiveSessionInMainHistoryList &&
|
|
1722
|
+
typeof columnArchiveId === "string" &&
|
|
1723
|
+
columnArchiveId.length > 0
|
|
1724
|
+
? {
|
|
1725
|
+
sessionId: columnArchiveId,
|
|
1726
|
+
content: renderSelectedSessionSidebarCard(),
|
|
1727
|
+
}
|
|
1728
|
+
: null
|
|
1729
|
+
}
|
|
1730
|
+
forcePageScroll
|
|
1731
|
+
/>
|
|
1732
|
+
</div>
|
|
1661
1733
|
</div>
|
|
1662
1734
|
</div>
|
|
1663
1735
|
</div>
|
|
@@ -1742,8 +1814,7 @@ function DashboardHome() {
|
|
|
1742
1814
|
t={dt}
|
|
1743
1815
|
onCancel={() => setNewSessionModalOpen(false)}
|
|
1744
1816
|
onConfirm={(sessionScope) => {
|
|
1745
|
-
|
|
1746
|
-
void post({ type: "newSession", sessionScope });
|
|
1817
|
+
void createNewSessionAndFocus(sessionScope);
|
|
1747
1818
|
}}
|
|
1748
1819
|
/>
|
|
1749
1820
|
<GlobalPauseConfirmModal
|