@nordbyte/nordrelay 0.3.0 → 0.4.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/.env.example +45 -2
- package/README.md +227 -47
- package/dist/agent-activity.js +300 -0
- package/dist/agent-adapter.js +17 -30
- package/dist/agent-factory.js +27 -0
- package/dist/agent.js +123 -9
- package/dist/artifacts.js +1 -1
- package/dist/audit-log.js +1 -1
- package/dist/bot-ui.js +1 -1
- package/dist/bot.js +333 -161
- package/dist/claude-code-auth.js +121 -0
- package/dist/claude-code-cli.js +19 -0
- package/dist/claude-code-launch.js +73 -0
- package/dist/claude-code-session.js +660 -0
- package/dist/claude-code-state.js +590 -0
- package/dist/codex-session.js +15 -2
- package/dist/config.js +113 -9
- package/dist/context-key.js +23 -0
- package/dist/hermes-api.js +150 -0
- package/dist/hermes-auth.js +96 -0
- package/dist/hermes-cli.js +19 -0
- package/dist/hermes-launch.js +57 -0
- package/dist/hermes-session.js +477 -0
- package/dist/hermes-state.js +609 -0
- package/dist/index.js +51 -8
- package/dist/openclaw-auth.js +27 -0
- package/dist/openclaw-cli.js +19 -0
- package/dist/openclaw-gateway.js +285 -0
- package/dist/openclaw-launch.js +65 -0
- package/dist/openclaw-session.js +549 -0
- package/dist/openclaw-state.js +409 -0
- package/dist/operations.js +84 -3
- package/dist/pi-auth.js +59 -0
- package/dist/pi-launch.js +61 -0
- package/dist/pi-rpc.js +18 -0
- package/dist/pi-session.js +103 -15
- package/dist/pi-state.js +253 -0
- package/dist/relay-runtime.js +1073 -22
- package/dist/session-format.js +28 -18
- package/dist/session-registry.js +43 -18
- package/dist/settings-service.js +80 -26
- package/dist/state-backend.js +17 -8
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +463 -55
- package/dist/web-state.js +131 -0
- package/docker-compose.yml +1 -1
- package/package.json +8 -3
- package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
- package/plugins/nordrelay/commands/remote.md +2 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +173 -20
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
- package/CHANGELOG.md +0 -17
package/dist/web-dashboard.js
CHANGED
|
@@ -12,8 +12,9 @@ import { friendlyErrorText } from "./error-messages.js";
|
|
|
12
12
|
import { escapeHTML } from "./format.js";
|
|
13
13
|
import { RelayRuntime } from "./relay-runtime.js";
|
|
14
14
|
import { resolveDashboardEnvPath, SettingsService } from "./settings-service.js";
|
|
15
|
-
|
|
16
|
-
const
|
|
15
|
+
import { renderDashboardNav } from "./web-dashboard-ui.js";
|
|
16
|
+
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
17
|
+
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
|
|
17
18
|
const options = parseOptions(process.argv.slice(2));
|
|
18
19
|
const auth = resolveDashboardAuth(options.host);
|
|
19
20
|
if (auth.publicBind && !auth.token && !(auth.user && auth.password)) {
|
|
@@ -81,16 +82,72 @@ async function handleApi(req, res, url) {
|
|
|
81
82
|
channels: listChannelDescriptors(),
|
|
82
83
|
agentAdapters: listAgentAdapterDescriptors(),
|
|
83
84
|
enabledAgents: enabledAgents(config),
|
|
85
|
+
controls: await runtime.controlOptions(),
|
|
84
86
|
status: await runtime.status(),
|
|
85
87
|
});
|
|
86
88
|
return;
|
|
87
89
|
}
|
|
90
|
+
if (req.method === "GET" && url.pathname === "/api/control-options") {
|
|
91
|
+
sendJson(res, 200, await runtime.controlOptions(parseAgentId(url.searchParams.get("agent") ?? undefined)));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
88
94
|
if (req.method === "GET" && url.pathname === "/api/health") {
|
|
89
95
|
sendJson(res, 200, await runtime.status());
|
|
90
96
|
return;
|
|
91
97
|
}
|
|
98
|
+
if (req.method === "GET" && url.pathname === "/api/version") {
|
|
99
|
+
sendJson(res, 200, await runtime.version());
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (req.method === "POST" && url.pathname === "/api/update") {
|
|
103
|
+
sendJson(res, 202, runtime.updateConnector());
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (req.method === "GET" && (url.pathname === "/api/tasks" || url.pathname === "/api/progress")) {
|
|
107
|
+
sendJson(res, 200, runtime.tasks());
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (req.method === "GET" && url.pathname === "/api/adapters/health") {
|
|
111
|
+
sendJson(res, 200, { adapters: await runtime.adapterHealth() });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (req.method === "GET" && url.pathname === "/api/permissions") {
|
|
115
|
+
sendJson(res, 200, runtime.permissions());
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (req.method === "GET" && url.pathname === "/api/audit") {
|
|
119
|
+
sendJson(res, 200, { events: runtime.audit(numberParam(url, "limit", 50)) });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (req.method === "GET" && url.pathname === "/api/locks") {
|
|
123
|
+
sendJson(res, 200, { locks: runtime.locks() });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (req.method === "POST" && url.pathname === "/api/locks") {
|
|
127
|
+
const body = await readJsonBody(req);
|
|
128
|
+
sendJson(res, 200, { lock: runtime.lockWebSession(optionalStringField(body, "ownerName")), locks: runtime.locks() });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (req.method === "DELETE" && url.pathname === "/api/locks") {
|
|
132
|
+
sendJson(res, 200, runtime.unlockWebSession());
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (req.method === "GET" && url.pathname === "/api/auth/status") {
|
|
136
|
+
sendJson(res, 200, await runtime.authStatus(parseAgentId(url.searchParams.get("agent") ?? undefined)));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (req.method === "POST" && url.pathname === "/api/auth/login") {
|
|
140
|
+
const body = await readJsonBody(req);
|
|
141
|
+
sendJson(res, 200, await runtime.login(parseAgentId(optionalStringField(body, "agentId"))));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (req.method === "POST" && url.pathname === "/api/auth/logout") {
|
|
145
|
+
const body = await readJsonBody(req);
|
|
146
|
+
sendJson(res, 200, await runtime.logout(parseAgentId(optionalStringField(body, "agentId"))));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
92
149
|
if (req.method === "GET" && url.pathname === "/api/settings") {
|
|
93
|
-
sendJson(res, 200, await settings.snapshot());
|
|
150
|
+
sendJson(res, 200, await settings.snapshot(process.env, activeSettingsValues(config)));
|
|
94
151
|
return;
|
|
95
152
|
}
|
|
96
153
|
if (req.method === "PATCH" && url.pathname === "/api/settings") {
|
|
@@ -119,8 +176,12 @@ async function handleApi(req, res, url) {
|
|
|
119
176
|
const body = await readJsonBody(req);
|
|
120
177
|
sendJson(res, 200, {
|
|
121
178
|
session: await runtime.newSession({
|
|
179
|
+
agentId: parseAgentId(optionalStringField(body, "agentId")),
|
|
122
180
|
workspace: optionalStringField(body, "workspace"),
|
|
123
181
|
model: optionalStringField(body, "model"),
|
|
182
|
+
reasoningEffort: optionalStringField(body, "reasoningEffort"),
|
|
183
|
+
launchProfileId: optionalStringField(body, "launchProfileId"),
|
|
184
|
+
fastMode: optionalBooleanField(body, "fastMode"),
|
|
124
185
|
}),
|
|
125
186
|
});
|
|
126
187
|
return;
|
|
@@ -135,6 +196,10 @@ async function handleApi(req, res, url) {
|
|
|
135
196
|
sendJson(res, 200, { session: await runtime.attachSession(stringField(body, "threadId")) });
|
|
136
197
|
return;
|
|
137
198
|
}
|
|
199
|
+
if (req.method === "GET" && url.pathname === "/api/sessions/detail") {
|
|
200
|
+
sendJson(res, 200, await runtime.sessionDetail(requiredSearch(url, "threadId")));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
138
203
|
if (req.method === "GET" && url.pathname === "/api/models") {
|
|
139
204
|
sendJson(res, 200, { models: await runtime.listModels() });
|
|
140
205
|
return;
|
|
@@ -172,7 +237,7 @@ async function handleApi(req, res, url) {
|
|
|
172
237
|
}));
|
|
173
238
|
return;
|
|
174
239
|
}
|
|
175
|
-
if (req.method === "POST" && url.pathname === "/api/abort") {
|
|
240
|
+
if (req.method === "POST" && (url.pathname === "/api/abort" || url.pathname === "/api/stop")) {
|
|
176
241
|
await runtime.abort();
|
|
177
242
|
sendJson(res, 200, { ok: true });
|
|
178
243
|
return;
|
|
@@ -181,13 +246,39 @@ async function handleApi(req, res, url) {
|
|
|
181
246
|
sendJson(res, 200, await runtime.handback());
|
|
182
247
|
return;
|
|
183
248
|
}
|
|
249
|
+
if (req.method === "POST" && url.pathname === "/api/retry") {
|
|
250
|
+
sendJson(res, 202, await runtime.retry());
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (req.method === "POST" && url.pathname === "/api/sync") {
|
|
254
|
+
sendJson(res, 200, await runtime.sync());
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
184
257
|
if (req.method === "GET" && url.pathname === "/api/queue") {
|
|
185
|
-
sendJson(res, 200, { queue: runtime.queue() });
|
|
258
|
+
sendJson(res, 200, { queue: runtime.queue(), paused: runtime.queuePaused() });
|
|
186
259
|
return;
|
|
187
260
|
}
|
|
188
261
|
if (req.method === "POST" && url.pathname === "/api/queue") {
|
|
189
262
|
const body = await readJsonBody(req);
|
|
190
|
-
sendJson(res, 200, { queue: runtime.queueAction(stringField(body, "action"), optionalStringField(body, "id")) });
|
|
263
|
+
sendJson(res, 200, { queue: runtime.queueAction(stringField(body, "action"), optionalStringField(body, "id")), paused: runtime.queuePaused() });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (req.method === "GET" && url.pathname === "/api/chat/history") {
|
|
267
|
+
sendJson(res, 200, { messages: await runtime.chatHistory(numberParam(url, "limit", 200)) });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (req.method === "DELETE" && url.pathname === "/api/chat/history") {
|
|
271
|
+
sendJson(res, 200, await runtime.clearChatHistory());
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (req.method === "GET" && url.pathname === "/api/activity") {
|
|
275
|
+
sendJson(res, 200, {
|
|
276
|
+
events: runtime.activity({
|
|
277
|
+
limit: numberParam(url, "limit", 100),
|
|
278
|
+
source: (url.searchParams.get("source") || "all"),
|
|
279
|
+
status: (url.searchParams.get("status") || "all"),
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
191
282
|
return;
|
|
192
283
|
}
|
|
193
284
|
if (req.method === "GET" && url.pathname === "/api/artifacts") {
|
|
@@ -198,6 +289,22 @@ async function handleApi(req, res, url) {
|
|
|
198
289
|
sendJson(res, 200, { removed: await runtime.deleteArtifact(requiredSearch(url, "turnId")) });
|
|
199
290
|
return;
|
|
200
291
|
}
|
|
292
|
+
if (req.method === "POST" && url.pathname === "/api/artifacts/bulk") {
|
|
293
|
+
const body = await readJsonBody(req);
|
|
294
|
+
const action = stringField(body, "action");
|
|
295
|
+
const turnIds = Array.isArray(body.turnIds) ? body.turnIds.filter((item) => typeof item === "string") : [];
|
|
296
|
+
if (action !== "delete") {
|
|
297
|
+
throw new Error("Unsupported artifact bulk action.");
|
|
298
|
+
}
|
|
299
|
+
const removed = [];
|
|
300
|
+
for (const turnId of turnIds) {
|
|
301
|
+
if (await runtime.deleteArtifact(turnId)) {
|
|
302
|
+
removed.push(turnId);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
sendJson(res, 200, { removed });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
201
308
|
if (req.method === "GET" && url.pathname === "/api/artifacts/zip") {
|
|
202
309
|
const bundle = await runtime.createArtifactZip(requiredSearch(url, "turnId"));
|
|
203
310
|
if (!bundle) {
|
|
@@ -219,12 +326,25 @@ async function handleApi(req, res, url) {
|
|
|
219
326
|
sendFile(res, artifact.localPath, artifact.name);
|
|
220
327
|
return;
|
|
221
328
|
}
|
|
329
|
+
if (req.method === "GET" && url.pathname === "/api/artifacts/preview") {
|
|
330
|
+
const preview = await runtime.artifactPreview(requiredSearch(url, "turnId"), requiredSearch(url, "path"));
|
|
331
|
+
if (!preview) {
|
|
332
|
+
sendJson(res, 404, { error: "Artifact not found" });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
sendJson(res, 200, preview);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
222
338
|
if (req.method === "GET" && url.pathname === "/api/logs") {
|
|
223
339
|
sendJson(res, 200, await runtime.logs(url.searchParams.get("target") || "connector", numberParam(url, "lines", 120)));
|
|
224
340
|
return;
|
|
225
341
|
}
|
|
226
342
|
if (req.method === "GET" && url.pathname === "/api/diagnostics") {
|
|
227
|
-
sendJson(res, 200, await runtime.
|
|
343
|
+
sendJson(res, 200, await runtime.diagnostics());
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (req.method === "POST" && url.pathname === "/api/runtime/restart") {
|
|
347
|
+
sendJson(res, 202, runtime.restartConnector());
|
|
228
348
|
return;
|
|
229
349
|
}
|
|
230
350
|
sendJson(res, 404, { error: "Unknown endpoint" });
|
|
@@ -387,7 +507,7 @@ function sendJson(res, status, value) {
|
|
|
387
507
|
res.end(`${JSON.stringify(value)}\n`);
|
|
388
508
|
}
|
|
389
509
|
function sendText(res, status, text, contentType) {
|
|
390
|
-
res.writeHead(status, { "content-type": contentType });
|
|
510
|
+
res.writeHead(status, { "content-type": contentType, "cache-control": "no-store" });
|
|
391
511
|
res.end(text);
|
|
392
512
|
}
|
|
393
513
|
function sendFile(res, filePath, filename) {
|
|
@@ -408,6 +528,19 @@ function optionalStringField(value, key) {
|
|
|
408
528
|
const field = value[key];
|
|
409
529
|
return typeof field === "string" && field.trim() ? field.trim() : undefined;
|
|
410
530
|
}
|
|
531
|
+
function optionalBooleanField(value, key) {
|
|
532
|
+
const field = value[key];
|
|
533
|
+
return typeof field === "boolean" ? field : undefined;
|
|
534
|
+
}
|
|
535
|
+
function parseAgentId(value) {
|
|
536
|
+
if (!value) {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
if (!isAgentId(value)) {
|
|
540
|
+
throw new Error(`Invalid agent: ${value}`);
|
|
541
|
+
}
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
411
544
|
function objectRecord(value) {
|
|
412
545
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
413
546
|
return {};
|
|
@@ -451,6 +584,110 @@ function optionalEnv(key) {
|
|
|
451
584
|
const value = process.env[key]?.trim();
|
|
452
585
|
return value || undefined;
|
|
453
586
|
}
|
|
587
|
+
function activeSettingsValues(current) {
|
|
588
|
+
return {
|
|
589
|
+
TELEGRAM_ALLOW_ANY_CHAT: boolValue(current.telegramAllowAnyChat),
|
|
590
|
+
TELEGRAM_BOT_TOKEN: current.telegramBotToken,
|
|
591
|
+
TELEGRAM_ADMIN_USER_IDS: current.telegramAdminUserIds.join(","),
|
|
592
|
+
TELEGRAM_ALLOWED_USER_IDS: current.telegramAllowedUserIds.join(","),
|
|
593
|
+
TELEGRAM_READONLY_USER_IDS: current.telegramReadOnlyUserIds.join(","),
|
|
594
|
+
TELEGRAM_ALLOWED_CHAT_IDS: current.telegramAllowedChatIds.join(","),
|
|
595
|
+
TELEGRAM_ROLE_POLICIES_JSON: optionalEnv("TELEGRAM_ROLE_POLICIES_JSON"),
|
|
596
|
+
TELEGRAM_TRANSPORT: current.telegramTransport,
|
|
597
|
+
TELEGRAM_WEBHOOK_URL: current.telegramWebhookUrl,
|
|
598
|
+
TELEGRAM_WEBHOOK_HOST: current.telegramWebhookHost,
|
|
599
|
+
TELEGRAM_WEBHOOK_PORT: String(current.telegramWebhookPort),
|
|
600
|
+
TELEGRAM_WEBHOOK_PATH: current.telegramWebhookPath,
|
|
601
|
+
TELEGRAM_WEBHOOK_SECRET: current.telegramWebhookSecret,
|
|
602
|
+
NORDRELAY_CODEX_ENABLED: boolValue(current.codexEnabled),
|
|
603
|
+
NORDRELAY_PI_ENABLED: boolValue(current.piEnabled),
|
|
604
|
+
NORDRELAY_HERMES_ENABLED: boolValue(current.hermesEnabled),
|
|
605
|
+
NORDRELAY_OPENCLAW_ENABLED: boolValue(current.openClawEnabled),
|
|
606
|
+
NORDRELAY_CLAUDE_CODE_ENABLED: boolValue(current.claudeCodeEnabled),
|
|
607
|
+
NORDRELAY_DEFAULT_AGENT: current.defaultAgent,
|
|
608
|
+
CODEX_API_KEY: current.codexApiKey,
|
|
609
|
+
CODEX_CLI_PATH: optionalEnv("CODEX_CLI_PATH"),
|
|
610
|
+
CODEX_USE_BUNDLED_CLI: process.env.CODEX_USE_BUNDLED_CLI,
|
|
611
|
+
CODEX_MODEL: current.codexModel,
|
|
612
|
+
CODEX_SYNC_INTERVAL_MS: String(current.codexSyncIntervalMs),
|
|
613
|
+
CODEX_EXTERNAL_BUSY_CHECK_MS: String(current.codexExternalBusyCheckMs),
|
|
614
|
+
CODEX_EXTERNAL_BUSY_STALE_MS: String(current.codexExternalBusyStaleMs),
|
|
615
|
+
CODEX_SANDBOX_MODE: current.codexSandboxMode,
|
|
616
|
+
CODEX_APPROVAL_POLICY: current.codexApprovalPolicy,
|
|
617
|
+
CODEX_LAUNCH_PROFILES_JSON: optionalEnv("CODEX_LAUNCH_PROFILES_JSON"),
|
|
618
|
+
CODEX_DEFAULT_LAUNCH_PROFILE: current.defaultLaunchProfileId,
|
|
619
|
+
ENABLE_UNSAFE_LAUNCH_PROFILES: boolValue(current.enableUnsafeLaunchProfiles),
|
|
620
|
+
PI_CLI_PATH: current.piCliPath,
|
|
621
|
+
PI_SESSION_DIR: current.piSessionDir,
|
|
622
|
+
PI_DEFAULT_MODEL: current.piDefaultModel,
|
|
623
|
+
PI_DEFAULT_THINKING: current.piDefaultThinking,
|
|
624
|
+
PI_DEFAULT_PROFILE: current.piDefaultLaunchProfileId,
|
|
625
|
+
HERMES_CLI_PATH: current.hermesCliPath,
|
|
626
|
+
HERMES_HOME: current.hermesHome,
|
|
627
|
+
HERMES_STATE_DB_PATH: current.hermesStateDbPath,
|
|
628
|
+
HERMES_API_BASE_URL: current.hermesApiBaseUrl,
|
|
629
|
+
HERMES_API_KEY: current.hermesApiKey,
|
|
630
|
+
HERMES_DEFAULT_MODEL: current.hermesDefaultModel,
|
|
631
|
+
HERMES_DEFAULT_REASONING: current.hermesDefaultReasoning,
|
|
632
|
+
HERMES_DEFAULT_PROFILE: current.hermesDefaultLaunchProfileId,
|
|
633
|
+
OPENCLAW_GATEWAY_URL: current.openClawGatewayUrl,
|
|
634
|
+
OPENCLAW_CLI_PATH: current.openClawCliPath,
|
|
635
|
+
OPENCLAW_GATEWAY_TOKEN: current.openClawGatewayToken,
|
|
636
|
+
OPENCLAW_GATEWAY_PASSWORD: current.openClawGatewayPassword,
|
|
637
|
+
OPENCLAW_AGENT_ID: current.openClawAgentId,
|
|
638
|
+
OPENCLAW_HOME: current.openClawHome,
|
|
639
|
+
OPENCLAW_STATE_DIR: current.openClawStateDir,
|
|
640
|
+
OPENCLAW_DEFAULT_MODEL: current.openClawDefaultModel,
|
|
641
|
+
OPENCLAW_DEFAULT_THINKING: current.openClawDefaultThinking,
|
|
642
|
+
OPENCLAW_DEFAULT_PROFILE: current.openClawDefaultLaunchProfileId,
|
|
643
|
+
CLAUDE_CODE_CLI_PATH: current.claudeCodeCliPath,
|
|
644
|
+
CLAUDE_CONFIG_DIR: current.claudeCodeConfigDir,
|
|
645
|
+
CLAUDE_CODE_DEFAULT_MODEL: current.claudeCodeDefaultModel,
|
|
646
|
+
CLAUDE_CODE_DEFAULT_EFFORT: current.claudeCodeDefaultEffort,
|
|
647
|
+
CLAUDE_CODE_DEFAULT_PROFILE: current.claudeCodeDefaultLaunchProfileId,
|
|
648
|
+
CLAUDE_CODE_MAX_TURNS: String(current.claudeCodeMaxTurns),
|
|
649
|
+
CONNECTOR_LOG_FORMAT: current.logFormat,
|
|
650
|
+
TOOL_VERBOSITY: current.toolVerbosity,
|
|
651
|
+
SHOW_TURN_TOKEN_USAGE: boolValue(current.showTurnTokenUsage),
|
|
652
|
+
ENABLE_TELEGRAM_LOGIN: boolValue(current.enableTelegramLogin),
|
|
653
|
+
ENABLE_TELEGRAM_REACTIONS: boolValue(current.enableTelegramReactions),
|
|
654
|
+
TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS: String(current.telegramRateLimitMinIntervalMs),
|
|
655
|
+
TELEGRAM_EDIT_MIN_INTERVAL_MS: String(current.telegramEditMinIntervalMs),
|
|
656
|
+
TELEGRAM_CLI_MIRROR_MODE: current.telegramMirrorMode,
|
|
657
|
+
TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS: String(current.telegramMirrorMinUpdateMs),
|
|
658
|
+
TELEGRAM_NOTIFY_MODE: current.telegramNotifyMode,
|
|
659
|
+
TELEGRAM_QUIET_HOURS: current.telegramQuietHours ? `${current.telegramQuietHours.startHour}-${current.telegramQuietHours.endHour}` : "",
|
|
660
|
+
TELEGRAM_REDACT_PATTERNS: current.telegramRedactPatterns.join(","),
|
|
661
|
+
NORDRELAY_UPDATE_METHOD: process.env.NORDRELAY_UPDATE_METHOD || "auto",
|
|
662
|
+
MAX_FILE_SIZE: String(current.maxFileSize),
|
|
663
|
+
ARTIFACT_RETENTION_DAYS: String(current.artifactRetentionDays),
|
|
664
|
+
ARTIFACT_MAX_TURNS: String(current.artifactMaxTurnDirs),
|
|
665
|
+
ARTIFACT_MAX_INBOX_DIRS: String(current.artifactMaxInboxDirs),
|
|
666
|
+
ARTIFACT_IGNORE_DIRS: current.artifactIgnoreDirs.join(","),
|
|
667
|
+
ARTIFACT_IGNORE_GLOBS: current.artifactIgnoreGlobs.join(","),
|
|
668
|
+
TELEGRAM_AUTO_SEND_ARTIFACTS: boolValue(current.telegramAutoSendArtifacts),
|
|
669
|
+
WORKSPACE_ALLOWED_ROOTS: current.workspaceAllowedRoots.join(","),
|
|
670
|
+
WORKSPACE_WARN_ROOTS: current.workspaceWarnRoots.join(","),
|
|
671
|
+
NORDRELAY_STATE_BACKEND: current.stateBackend,
|
|
672
|
+
NORDRELAY_AUDIT_MAX_EVENTS: String(current.auditMaxEvents),
|
|
673
|
+
NORDRELAY_SESSION_LOCK_TTL_MS: String(current.sessionLockTtlMs),
|
|
674
|
+
NORDRELAY_VERSION_CACHE_TTL_MS: process.env.NORDRELAY_VERSION_CACHE_TTL_MS,
|
|
675
|
+
VOICE_PREFERRED_BACKEND: current.voicePreferredBackend,
|
|
676
|
+
VOICE_DEFAULT_LANGUAGE: current.voiceDefaultLanguage,
|
|
677
|
+
VOICE_TRANSCRIBE_ONLY: boolValue(current.voiceTranscribeOnly),
|
|
678
|
+
FASTER_WHISPER_PYTHON: process.env.FASTER_WHISPER_PYTHON,
|
|
679
|
+
FASTER_WHISPER_MODEL: process.env.FASTER_WHISPER_MODEL,
|
|
680
|
+
FASTER_WHISPER_DEVICE: process.env.FASTER_WHISPER_DEVICE,
|
|
681
|
+
FASTER_WHISPER_COMPUTE_TYPE: process.env.FASTER_WHISPER_COMPUTE_TYPE,
|
|
682
|
+
FASTER_WHISPER_LANGUAGE: process.env.FASTER_WHISPER_LANGUAGE,
|
|
683
|
+
FASTER_WHISPER_TIMEOUT_MS: process.env.FASTER_WHISPER_TIMEOUT_MS,
|
|
684
|
+
NORDRELAY_DASHBOARD_HOST: options.host,
|
|
685
|
+
NORDRELAY_DASHBOARD_PORT: String(options.port),
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function boolValue(value) {
|
|
689
|
+
return value ? "true" : "false";
|
|
690
|
+
}
|
|
454
691
|
function requireArg(argv, index, flag) {
|
|
455
692
|
const value = argv[index];
|
|
456
693
|
if (!value || value.startsWith("--")) {
|
|
@@ -524,14 +761,7 @@ function renderDashboardApp(options) {
|
|
|
524
761
|
<aside class="sidebar" id="sidebar">
|
|
525
762
|
<div class="brand"><span class="mark">NR</span><div><strong>NordRelay</strong><small>Remote control</small></div></div>
|
|
526
763
|
<nav>
|
|
527
|
-
|
|
528
|
-
<button data-page="chat">Chat</button>
|
|
529
|
-
<button data-page="sessions">Sessions</button>
|
|
530
|
-
<button data-page="queue">Queue</button>
|
|
531
|
-
<button data-page="artifacts">Artifacts</button>
|
|
532
|
-
<button data-page="settings">Settings</button>
|
|
533
|
-
<button data-page="logs">Logs</button>
|
|
534
|
-
<button data-page="diagnostics">Diagnostics</button>
|
|
764
|
+
${renderDashboardNav()}
|
|
535
765
|
</nav>
|
|
536
766
|
</aside>
|
|
537
767
|
<main>
|
|
@@ -542,6 +772,7 @@ function renderDashboardApp(options) {
|
|
|
542
772
|
<p id="sessionLine">Loading session...</p>
|
|
543
773
|
</div>
|
|
544
774
|
<div class="header-actions">
|
|
775
|
+
<span id="connectionStatus" class="badge">Connecting</span>
|
|
545
776
|
<select id="agentSelect"></select>
|
|
546
777
|
<button id="themeBtn" class="secondary" title="Toggle dark theme">Dark</button>
|
|
547
778
|
<button id="refreshBtn">Refresh</button>
|
|
@@ -561,9 +792,15 @@ function renderDashboardApp(options) {
|
|
|
561
792
|
<div class="panel chat-panel">
|
|
562
793
|
<div class="chat-toolbar">
|
|
563
794
|
<button id="newSessionBtn">New session</button>
|
|
795
|
+
<button id="retryBtn" class="secondary">Retry</button>
|
|
796
|
+
<button id="editLastBtn" class="secondary">Edit last</button>
|
|
797
|
+
<button id="syncBtn" class="secondary">Sync</button>
|
|
798
|
+
<button id="notifyBtn" class="secondary">Notify</button>
|
|
799
|
+
<button id="clearChatBtn" class="secondary">Clear history</button>
|
|
564
800
|
<button id="abortBtn">Abort</button>
|
|
565
801
|
<button id="handbackBtn">Handback</button>
|
|
566
802
|
</div>
|
|
803
|
+
<div class="control-grid" id="sessionControls"></div>
|
|
567
804
|
<div id="messages" class="messages"></div>
|
|
568
805
|
<form id="promptForm" class="composer">
|
|
569
806
|
<div class="composer-fields">
|
|
@@ -571,6 +808,7 @@ function renderDashboardApp(options) {
|
|
|
571
808
|
<div class="attachment-row">
|
|
572
809
|
<label class="file-button" for="fileInput">Attach files</label>
|
|
573
810
|
<input id="fileInput" type="file" multiple>
|
|
811
|
+
<button type="button" id="recordBtn" class="secondary">Record voice</button>
|
|
574
812
|
<span id="fileSummary">No files selected</span>
|
|
575
813
|
<button type="button" id="clearFilesBtn" class="secondary">Clear</button>
|
|
576
814
|
</div>
|
|
@@ -582,6 +820,13 @@ function renderDashboardApp(options) {
|
|
|
582
820
|
</div>
|
|
583
821
|
</section>
|
|
584
822
|
|
|
823
|
+
<section class="page" id="page-tasks">
|
|
824
|
+
<div class="panel">
|
|
825
|
+
<div class="row"><button id="reloadTasksBtn">Reload tasks</button></div>
|
|
826
|
+
<div id="tasksList" class="list"></div>
|
|
827
|
+
</div>
|
|
828
|
+
</section>
|
|
829
|
+
|
|
585
830
|
<section class="page" id="page-sessions">
|
|
586
831
|
<div class="panel">
|
|
587
832
|
<div class="sessions-toolbar">
|
|
@@ -595,21 +840,55 @@ function renderDashboardApp(options) {
|
|
|
595
840
|
|
|
596
841
|
<section class="page" id="page-queue">
|
|
597
842
|
<div class="panel">
|
|
598
|
-
<div class="row"><button data-queue="pause">Pause</button><button data-queue="resume">Resume</button><button data-queue="clear">Clear</button></div>
|
|
843
|
+
<div class="row"><button data-queue="pause">Pause</button><button data-queue="resume">Resume</button><button data-queue="clear" class="danger">Clear</button><span id="queueStatus"></span></div>
|
|
599
844
|
<div id="queueList" class="list"></div>
|
|
600
845
|
</div>
|
|
601
846
|
</section>
|
|
602
847
|
|
|
848
|
+
<section class="page" id="page-activity">
|
|
849
|
+
<div class="panel">
|
|
850
|
+
<div class="row"><select id="activitySource"><option value="all">All sources</option><option value="web">Web</option><option value="cli">CLI</option></select><select id="activityStatus"><option value="all">All statuses</option><option value="queued">Queued</option><option value="running">Running</option><option value="completed">Completed</option><option value="failed">Failed</option><option value="aborted">Aborted</option><option value="info">Info</option></select><input id="activitySince" type="datetime-local"><input id="activityLimit" type="number" value="100" min="1" max="500"><button id="loadActivityBtn">Load activity</button><button id="exportActivityBtn" class="secondary">Export</button></div>
|
|
851
|
+
<div id="activityList" class="list"></div>
|
|
852
|
+
</div>
|
|
853
|
+
</section>
|
|
854
|
+
|
|
603
855
|
<section class="page" id="page-artifacts">
|
|
604
856
|
<div class="panel">
|
|
605
|
-
<div class="row"><button id="reloadArtifactsBtn">Reload artifacts</button></div>
|
|
857
|
+
<div class="row"><button id="reloadArtifactsBtn">Reload artifacts</button><input id="artifactSearch" placeholder="Search artifacts"><select id="artifactKind"><option value="all">All files</option><option value="images">Images</option><option value="docs">Docs/code</option></select><button id="zipSelectedArtifactsBtn" class="secondary">ZIP selected</button><button id="deleteSelectedArtifactsBtn" class="danger">Delete selected</button></div>
|
|
606
858
|
<div id="artifactList" class="list"></div>
|
|
859
|
+
<div id="artifactPreview" class="preview"></div>
|
|
860
|
+
</div>
|
|
861
|
+
</section>
|
|
862
|
+
|
|
863
|
+
<section class="page" id="page-adapters">
|
|
864
|
+
<div class="panel">
|
|
865
|
+
<div class="row"><button id="reloadAdaptersBtn">Reload adapters</button></div>
|
|
866
|
+
<div id="adapterHealth" class="list"></div>
|
|
867
|
+
</div>
|
|
868
|
+
</section>
|
|
869
|
+
|
|
870
|
+
<section class="page" id="page-access">
|
|
871
|
+
<div class="panel">
|
|
872
|
+
<div class="row"><button id="loadAccessBtn">Reload access</button><button id="saveAccessBtn">Save access settings</button><button id="lockSessionBtn" class="secondary">Lock web session</button><button id="unlockSessionBtn" class="secondary">Unlock web session</button></div>
|
|
873
|
+
<div id="accessPanel" class="settings-grid"></div>
|
|
874
|
+
<h2>Locks</h2>
|
|
875
|
+
<div id="locksList" class="list"></div>
|
|
876
|
+
<h2>Audit</h2>
|
|
877
|
+
<div class="row"><input id="auditLimit" type="number" value="50" min="1" max="200"><button id="loadAuditBtn">Load audit</button></div>
|
|
878
|
+
<div id="auditList" class="list"></div>
|
|
879
|
+
</div>
|
|
880
|
+
</section>
|
|
881
|
+
|
|
882
|
+
<section class="page" id="page-version">
|
|
883
|
+
<div class="panel">
|
|
884
|
+
<div class="row"><button id="loadVersionBtn">Check versions</button><button id="updateBtn" class="secondary">Update NordRelay</button></div>
|
|
885
|
+
<div id="versionPanel" class="list"></div>
|
|
607
886
|
</div>
|
|
608
887
|
</section>
|
|
609
888
|
|
|
610
889
|
<section class="page" id="page-settings">
|
|
611
890
|
<div class="panel">
|
|
612
|
-
<div class="row"><button id="saveSettingsBtn">Save settings</button><span id="settingsStatus"></span></div>
|
|
891
|
+
<div class="row"><button id="saveSettingsBtn">Save settings</button><button id="restartBtn" class="secondary">Restart NordRelay</button><span id="settingsStatus"></span></div>
|
|
613
892
|
<div id="settingsTabs" class="tabs"></div>
|
|
614
893
|
<div id="settingsForm" class="settings-grid"></div>
|
|
615
894
|
</div>
|
|
@@ -617,13 +896,13 @@ function renderDashboardApp(options) {
|
|
|
617
896
|
|
|
618
897
|
<section class="page" id="page-logs">
|
|
619
898
|
<div class="panel">
|
|
620
|
-
<div class="row"><select id="logTarget"><option value="connector">Connector</option><option value="update">Update</option></select><input id="logLines" type="number" value="120" min="1" max="300"><button id="loadLogsBtn">Load logs</button></div>
|
|
899
|
+
<div class="row"><select id="logTarget"><option value="connector">Connector</option><option value="update">Update</option></select><select id="logLevel"><option value="all">All levels</option><option value="ERROR">Error</option><option value="WARN">Warn</option><option value="INFO">Info</option></select><input id="logSearch" placeholder="Search logs"><input id="logSince" type="datetime-local" title="Show entries after this time"><input id="logLines" type="number" value="120" min="1" max="300"><label class="checkbox"><input id="logAutoRefresh" type="checkbox"> Auto</label><label class="checkbox"><input id="logFollow" type="checkbox"> Follow</label><button id="loadLogsBtn">Load logs</button><button id="downloadLogsBtn" class="secondary">Download</button></div>
|
|
621
900
|
<pre id="logs"></pre>
|
|
622
901
|
</div>
|
|
623
902
|
</section>
|
|
624
903
|
|
|
625
904
|
<section class="page" id="page-diagnostics">
|
|
626
|
-
<div class="panel"><
|
|
905
|
+
<div class="panel"><div id="diagnostics" class="list"></div></div>
|
|
627
906
|
</section>
|
|
628
907
|
|
|
629
908
|
<footer>
|
|
@@ -633,6 +912,25 @@ function renderDashboardApp(options) {
|
|
|
633
912
|
</footer>
|
|
634
913
|
</main>
|
|
635
914
|
</div>
|
|
915
|
+
<dialog id="newSessionDialog">
|
|
916
|
+
<form method="dialog" id="newSessionForm">
|
|
917
|
+
<h2>New Session</h2>
|
|
918
|
+
<div class="form-grid">
|
|
919
|
+
<label>Agent<select id="newAgent"></select></label>
|
|
920
|
+
<label>Workspace<input id="newWorkspace" list="workspaceOptions" placeholder="Current workspace"></label>
|
|
921
|
+
<label>Model<select id="newModel"></select></label>
|
|
922
|
+
<label id="newReasoningWrap">Reasoning<select id="newReasoning"></select></label>
|
|
923
|
+
<label id="newLaunchWrap">Launch profile<select id="newLaunch"></select></label>
|
|
924
|
+
<label id="newFastWrap" class="checkbox"><input id="newFast" type="checkbox"> Fast mode</label>
|
|
925
|
+
</div>
|
|
926
|
+
<datalist id="workspaceOptions"></datalist>
|
|
927
|
+
<div class="row dialog-actions"><button type="button" id="cancelSessionBtn" class="secondary">Cancel</button><button id="createSessionBtn" value="default">Create session</button></div>
|
|
928
|
+
</form>
|
|
929
|
+
</dialog>
|
|
930
|
+
<dialog id="sessionDetailDialog">
|
|
931
|
+
<div id="sessionDetail"></div>
|
|
932
|
+
<div class="row dialog-actions"><button id="closeSessionDetailBtn" class="secondary">Close</button></div>
|
|
933
|
+
</dialog>
|
|
636
934
|
<div id="toast"></div>
|
|
637
935
|
<script>${dashboardJs()}</script>
|
|
638
936
|
</body>
|
|
@@ -642,13 +940,15 @@ function dashboardCss() {
|
|
|
642
940
|
return `
|
|
643
941
|
:root{color-scheme:light;--bg:#f4f6f2;--surface:#ffffff;--surface-soft:#fbfcf8;--text:#18201b;--muted:#5d675f;--border:#dce3d9;--border-soft:#e7ede4;--sidebar:#17251d;--sidebar-text:#f4f8f2;--sidebar-muted:#aebcaf;--accent:#235c42;--accent-strong:#17452f;--accent-soft:#dff5e8;--warn:#fff7da;--danger:#9b1c1c;--pre:#111812;--pre-text:#f3f7ef;--shadow:0 8px 24px rgba(24,32,27,.04);--link:#1d6a4c}
|
|
644
942
|
:root[data-theme="dark"]{color-scheme:dark;--bg:#101411;--surface:#171d19;--surface-soft:#1d251f;--text:#edf4ee;--muted:#a7b3aa;--border:#2d3830;--border-soft:#263128;--sidebar:#0c120f;--sidebar-text:#edf7ef;--sidebar-muted:#8da091;--accent:#4fa876;--accent-strong:#64bd89;--accent-soft:#173d2a;--warn:#3b3216;--danger:#cc4b4b;--pre:#070a08;--pre-text:#e8f1ea;--shadow:0 10px 28px rgba(0,0,0,.22);--link:#75c99a}
|
|
645
|
-
|
|
943
|
+
.agent-settings-nav{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 12px;padding:10px;border:1px solid var(--border-soft);border-radius:8px;background:var(--surface)}.agent-settings-nav strong{font-size:13px;color:var(--muted);margin-right:4px}.agent-settings-nav button{background:var(--surface);color:var(--text);border-color:var(--border);height:32px}.agent-settings-nav button.active{background:var(--accent);color:white;border-color:var(--accent)}@media(max-width:560px){.agent-settings-nav{align-items:stretch}.agent-settings-nav button{width:100%}}
|
|
944
|
+
.drop-active{outline:2px dashed var(--accent);outline-offset:-8px}.chip{display:inline-flex;align-items:center;border-radius:999px;border:1px solid var(--border);padding:2px 8px;font-size:12px;color:var(--muted);margin-right:6px}.chip.error{color:var(--danger);border-color:var(--danger)}.chip.warn{color:#8a6a12;border-color:#d9c27a;background:var(--warn)}.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;margin-top:12px}.artifact-card{border:1px solid var(--border-soft);border-radius:8px;padding:8px;background:var(--surface-soft);min-width:0}.artifact-card img{width:100%;aspect-ratio:1.4;object-fit:cover;border:1px solid var(--border);border-radius:6px;background:var(--surface)}.artifact-card small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.setting.dirty{border-color:var(--accent)}.setting-actions{display:flex;gap:8px;align-items:center;margin-top:8px}.setting-help{font-size:12px;color:var(--muted)}.restart-banner{border:1px solid #d9c27a;background:var(--warn);border-radius:8px;padding:10px;margin:0 0 12px}.task-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:10px}.log-line{display:block}.log-line.ERROR{color:var(--danger);font-weight:700}.log-line.WARN{color:#8a6a12;font-weight:700}.connection-ok{color:#1e754e;border-color:#8ed0aa}.connection-warn{color:#8a6a12;border-color:#d9c27a}.connection-error{color:var(--danger);border-color:var(--danger)}
|
|
945
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,system-ui,-apple-system,Segoe UI,sans-serif}.app{min-height:100vh;display:grid;grid-template-columns:260px 1fr}.sidebar{background:var(--sidebar);color:var(--sidebar-text);padding:18px;display:flex;flex-direction:column;gap:22px}.brand{display:flex;align-items:center;gap:12px}.mark{display:grid;place-items:center;width:38px;height:38px;border-radius:8px;background:#d7ffe5;color:#173d29;font-weight:800}.brand small{display:block;color:var(--sidebar-muted)}nav{display:flex;flex-direction:column;gap:6px}nav button,.menu{border:0;border-radius:6px;padding:10px 12px;background:transparent;color:inherit;text-align:left;font:inherit;cursor:pointer}nav button.active,nav button:hover{background:color-mix(in srgb,var(--accent) 35%,transparent)}main{min-width:0;display:flex;flex-direction:column}header{position:sticky;top:0;z-index:5;display:flex;justify-content:space-between;gap:16px;align-items:center;padding:16px 22px;background:color-mix(in srgb,var(--surface) 92%,transparent);backdrop-filter:blur(12px);border-bottom:1px solid var(--border)}h1{font-size:24px;margin:0}h2{font-size:16px;margin:0 0 12px}p{margin:4px 0 0;color:var(--muted)}a{color:var(--link)}.header-actions,.row,.chat-toolbar,.attachment-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.menu{display:none;background:var(--surface-soft);color:var(--text)}.page{display:none;padding:22px}.page.active{display:block}.stack{display:flex;flex-direction:column;gap:16px}.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-bottom:16px}.metric,.panel{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px;box-shadow:var(--shadow)}.metric .label{font-size:12px;text-transform:uppercase;color:var(--muted)}.metric .value{font-size:22px;font-weight:750;margin-top:4px;overflow:hidden;text-overflow:ellipsis}button,select,input,textarea{border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);font:inherit}button{height:36px;padding:0 12px;background:var(--accent);color:white;border-color:var(--accent);cursor:pointer}button:hover{background:var(--accent-strong)}button.secondary{background:var(--surface);color:var(--text)}input,select{height:36px;padding:0 10px}textarea{width:100%;padding:10px;resize:vertical}.chat-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:16px;align-items:start}.chat-panel{height:calc(100vh - 170px);min-height:520px;display:flex;flex-direction:column}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;margin:12px 0}.control-grid label,.form-grid label{display:grid;gap:5px;font-size:12px;color:var(--muted)}.messages{flex:1;min-height:0;overflow:auto;border:1px solid var(--border-soft);border-radius:8px;padding:12px;background:var(--surface-soft)}.message{margin:0 0 12px;padding:10px 12px;border-radius:8px;max-width:92%;white-space:pre-wrap;word-break:break-word}.message.user{margin-left:auto;background:var(--accent-soft)}.message.agent{background:color-mix(in srgb,var(--surface-soft) 80%,var(--border))}.message.system{background:var(--warn)}.composer{display:grid;grid-template-columns:1fr auto;gap:10px;margin-top:12px}.composer-fields{min-width:0}.composer button{height:auto;min-width:90px}.attachment-row{margin-top:8px;color:var(--muted);font-size:13px}.file-button{display:inline-flex;align-items:center;height:34px;padding:0 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);cursor:pointer}input[type=file]{display:none}.sessions-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}.sessions-toolbar .search-row{flex:1 1 320px}.sessions-toolbar .attach-row{flex:1 1 360px;justify-content:flex-end;margin-left:auto}.sessions-toolbar input{min-width:220px}.copy-id{height:auto;padding:0;border:0;background:transparent;color:var(--link);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}.copy-id:hover{background:transparent;text-decoration:underline}.side-panel{max-height:calc(100vh - 126px);display:flex;flex-direction:column}.tool-stream{display:flex;flex-direction:column;gap:8px;overflow:auto;max-height:calc(100vh - 190px);padding-right:4px}.tool{border:1px solid var(--border-soft);border-radius:6px;padding:8px;background:var(--surface-soft);white-space:pre-wrap;word-break:break-word}.list{display:flex;flex-direction:column;gap:8px;margin-top:12px}.item{border:1px solid var(--border-soft);border-radius:8px;padding:12px;background:var(--surface-soft)}.item strong{display:block;overflow-wrap:anywhere}.item small{display:block;color:var(--muted);overflow-wrap:anywhere}.queue-item{cursor:grab}.queue-item.dragging{opacity:.55}.badge,.adapter-status{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:999px;padding:2px 8px;color:var(--muted);font-size:12px}.adapter-status{margin-left:6px;text-transform:capitalize}.adapter-status.enabled,.adapter-status.available{color:#1e754e;border-color:#8ed0aa;background:color-mix(in srgb,var(--accent-soft) 55%,transparent)}.adapter-status.disabled{color:var(--muted)}.adapter-status.planned{color:#8a6a12;border-color:#d9c27a;background:var(--warn)}.preview{margin-top:12px}.preview img{max-width:100%;border:1px solid var(--border);border-radius:8px;background:var(--surface)}.settings-grid{display:block}.setting{border:1px solid var(--border-soft);border-radius:8px;padding:12px;margin-bottom:10px;background:var(--surface-soft)}.setting label{display:block;font-size:13px;font-weight:700;margin-bottom:6px}.setting small{display:block;color:var(--muted);margin-top:6px}.setting input,.setting textarea,.setting select{width:100%}.setting-error{color:var(--danger);font-size:12px;margin-top:6px}.checkbox{display:inline-flex!important;grid-template-columns:auto 1fr!important;align-items:center;gap:8px}.checkbox input{height:auto;width:auto}.tabs{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.tabs button{background:var(--surface);color:var(--text);border-color:var(--border);height:34px}.tabs button.active{background:var(--accent);color:white;border-color:var(--accent)}.pager{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-top:12px;color:var(--muted)}.pager-actions{display:flex;gap:8px}.pager button:disabled{opacity:.45;cursor:not-allowed}pre{white-space:pre-wrap;word-break:break-word;background:var(--pre);color:var(--pre-text);border-radius:8px;padding:14px;overflow:auto}footer{margin-top:auto;display:flex;gap:18px;flex-wrap:wrap;padding:14px 22px;border-top:1px solid var(--border);color:var(--muted);background:var(--surface)}dialog{border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text);width:min(720px,calc(100vw - 28px));padding:18px;box-shadow:0 18px 70px rgba(0,0,0,.22)}dialog::backdrop{background:rgba(0,0,0,.35)}.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}.dialog-actions{justify-content:flex-end;margin-top:16px}#toast{position:fixed;right:18px;bottom:18px;display:none;background:var(--accent);color:white;border-radius:8px;padding:12px 14px;max-width:360px}.danger{background:var(--danger);border-color:var(--danger);color:white}@media(max-width:860px){.app{display:block}.sidebar{position:fixed;inset:0 auto 0 0;width:270px;transform:translateX(-100%);transition:.18s transform;z-index:20}.sidebar.open{transform:translateX(0)}.menu{display:inline-block}.header-actions{justify-content:flex-end}.page{padding:14px}.chat-layout{grid-template-columns:1fr}.chat-panel{height:auto;min-height:0}.messages{max-height:55vh;min-height:300px}.composer{grid-template-columns:1fr}.composer button{height:40px}.side-panel{order:-1;max-height:360px}.tool-stream{max-height:300px}header{align-items:flex-start}.metrics{grid-template-columns:1fr 1fr}}@media(max-width:560px){.metrics{grid-template-columns:1fr}.row{align-items:stretch}.row>*{width:100%}header{display:grid;grid-template-columns:auto 1fr}.header-actions{grid-column:1/3}.message{max-width:100%}.pager{align-items:stretch}.pager-actions,.pager button{width:100%}.attachment-row>*,.sessions-toolbar,.sessions-toolbar .row,.sessions-toolbar input,.sessions-toolbar button{width:100%}.sessions-toolbar .attach-row{margin-left:0;justify-content:stretch}}
|
|
646
946
|
`;
|
|
647
947
|
}
|
|
648
948
|
function dashboardJs() {
|
|
649
949
|
return `
|
|
650
950
|
const token = localStorage.getItem('nordrelayDashboardToken') || '';
|
|
651
|
-
const state = { snapshot:null, settings:[], currentPage:'overview',
|
|
951
|
+
const state = { snapshot:null, controls:null, newSessionControls:null, enabledAgents:[], settings:[], currentPage:'overview', settingsGroup:null, logsPlain:'', logTimer:null, toastTimer:null, cliStatusActive:false, selectedArtifactTurns:new Set(), mediaRecorder:null, recordedChunks:[], events:null, reconnectTimer:null, notifications:false };
|
|
652
952
|
const authHeaders = () => token ? { authorization: 'Bearer ' + token } : {};
|
|
653
953
|
async function api(path, options={}) {
|
|
654
954
|
const headers = { ...(options.body ? {'content-type':'application/json'} : {}), ...authHeaders(), ...(options.headers||{}) };
|
|
@@ -659,16 +959,23 @@ async function api(path, options={}) {
|
|
|
659
959
|
if (!res.ok) throw new Error(data.error || res.statusText);
|
|
660
960
|
return data;
|
|
661
961
|
}
|
|
662
|
-
function toast(msg){const el=document.getElementById('toast');el.textContent=msg;el.style.display='block';setTimeout(()=>el.style.display='none',3500)}
|
|
962
|
+
function toast(msg,options={}){const el=document.getElementById('toast');el.textContent=msg;el.style.display='block';if(state.toastTimer)clearTimeout(state.toastTimer);state.toastTimer=null;if(!options.sticky){state.toastTimer=setTimeout(()=>{el.style.display='none';state.toastTimer=null},options.duration||3500)}}
|
|
663
963
|
function esc(s){return String(s??'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]))}
|
|
664
964
|
function attr(s){return esc(s).replace(/"/g,'"')}
|
|
965
|
+
function cssEscape(s){return window.CSS&&CSS.escape?CSS.escape(s):String(s).replace(/[^a-zA-Z0-9_-]/g,'\\\\$&')}
|
|
665
966
|
function short(s,max=250){const text=String(s??'');return text.length>max?text.slice(0,max-1)+'...':text}
|
|
666
|
-
async function copyText(text){if(!text)return;try{await navigator.clipboard.writeText(text)}catch{const area=document.createElement('textarea');area.value=text;area.style.position='fixed';area.style.opacity='0';document.body.appendChild(area);area.select();document.execCommand('copy');area.remove()}toast(
|
|
967
|
+
async function copyText(text,label='Copied'){if(!text)return;try{await navigator.clipboard.writeText(text)}catch{const area=document.createElement('textarea');area.value=text;area.style.position='fixed';area.style.opacity='0';document.body.appendChild(area);area.select();document.execCommand('copy');area.remove()}toast(label)}
|
|
667
968
|
function fmtDate(s){return s?new Date(s).toLocaleString(): '-'}
|
|
969
|
+
function fmtDuration(ms){if(!ms&&ms!==0)return '-';const sec=Math.round(ms/1000);if(sec<60)return sec+'s';return Math.floor(sec/60)+'m '+(sec%60)+'s'}
|
|
668
970
|
function fmtBytes(n){if(n<1024)return n+' B';if(n<1048576)return (n/1024).toFixed(1).replace(/\\.0$/,'')+' KB';return (n/1048576).toFixed(1).replace(/\\.0$/,'')+' MB'}
|
|
971
|
+
function compactNum(n){if(!n)return'';if(n>=1000000000)return Math.round(n/100000000)/10+'B';if(n>=1000000)return Math.round(n/100000)/10+'M';if(n>=1000)return Math.round(n/100)/10+'K';return String(n)}
|
|
972
|
+
function modelLabel(m){const meta=[m.contextWindow?compactNum(m.contextWindow):'',m.supportsImages===true?'img':m.supportsImages===false?'text':'',m.supportsThinking===true?'think':''].filter(Boolean).join(' ');return (m.displayName||m.slug)+(meta?' · '+meta:'')}
|
|
973
|
+
function fmtAge(ms){const sec=Math.max(0,Math.floor(ms/1000));if(sec<60)return sec+'s ago';const min=Math.floor(sec/60);if(min<60)return min+'m ago';return Math.floor(min/60)+'h ago'}
|
|
974
|
+
function isCliRunningStatus(msg){return / CLI running\\b/.test(String(msg||''))}
|
|
975
|
+
function isCliDoneStatus(msg){return / CLI task\\b/.test(String(msg||''))}
|
|
669
976
|
function applyTheme(theme){document.documentElement.dataset.theme=theme;localStorage.setItem('nordrelayTheme',theme);document.getElementById('themeBtn').textContent=theme==='dark'?'Light':'Dark'}
|
|
670
977
|
function toggleTheme(){applyTheme(document.documentElement.dataset.theme==='dark'?'light':'dark')}
|
|
671
|
-
function page(name){state.currentPage=name;document.querySelectorAll('nav button').forEach(b=>b.classList.toggle('active',b.dataset.page===name));document.querySelectorAll('.page').forEach(p=>p.classList.toggle('active',p.id==='page-'+name));document.getElementById('pageTitle').textContent=name[0].toUpperCase()+name.slice(1);document.getElementById('sidebar').classList.remove('open'); if(name==='sessions') loadSessions(); if(name==='settings') loadSettings(); if(name==='logs') loadLogs(); if(name==='diagnostics') loadDiagnostics(); if(name==='artifacts') loadArtifacts();}
|
|
978
|
+
function page(name){state.currentPage=name;document.querySelectorAll('nav button').forEach(b=>b.classList.toggle('active',b.dataset.page===name));document.querySelectorAll('.page').forEach(p=>p.classList.toggle('active',p.id==='page-'+name));document.getElementById('pageTitle').textContent=name[0].toUpperCase()+name.slice(1);document.getElementById('sidebar').classList.remove('open'); if(name==='sessions') loadSessions(); if(name==='settings') loadSettings(); if(name==='logs') loadLogs(); if(name==='diagnostics') loadDiagnostics(); if(name==='artifacts') loadArtifacts(); if(name==='activity') loadActivity(); if(name==='tasks') loadTasks(); if(name==='adapters') loadAdapterHealth(); if(name==='access') loadAccess(); if(name==='version') loadVersion();}
|
|
672
979
|
document.querySelectorAll('nav button').forEach(b=>b.onclick=()=>page(b.dataset.page));
|
|
673
980
|
document.getElementById('menuBtn').onclick=()=>document.getElementById('sidebar').classList.toggle('open');
|
|
674
981
|
document.getElementById('refreshBtn').onclick=()=>loadBootstrap();
|
|
@@ -697,68 +1004,169 @@ const sessionsPager=createPaginator('sessionsPager',()=>loadSessions(false),50);
|
|
|
697
1004
|
async function loadBootstrap(){
|
|
698
1005
|
const data = await api('/api/bootstrap');
|
|
699
1006
|
state.snapshot = data.status.snapshot;
|
|
1007
|
+
state.controls = data.controls;
|
|
1008
|
+
state.enabledAgents = data.enabledAgents || [];
|
|
700
1009
|
renderSnapshot(state.snapshot);
|
|
1010
|
+
renderSessionControls();
|
|
1011
|
+
populateNewSessionForm(data.enabledAgents);
|
|
701
1012
|
renderAdapters(data.channels, data.agentAdapters);
|
|
702
1013
|
document.getElementById('footerVersion').textContent='NordRelay '+(data.status.health?.version || '');
|
|
703
1014
|
document.getElementById('footerHealth').textContent='Health: '+(data.status.health?.state?.status || 'unknown');
|
|
704
1015
|
const agentSelect=document.getElementById('agentSelect');
|
|
705
1016
|
agentSelect.innerHTML=data.enabledAgents.map(a=>'<option value="'+a+'">'+a+'</option>').join('');
|
|
706
1017
|
agentSelect.value=state.snapshot.session.agentId;
|
|
707
|
-
agentSelect.onchange=async()=>{await api('/api/agent',{method:'POST',body:JSON.stringify({agentId:agentSelect.value})});toast('Agent switched');loadBootstrap()};
|
|
1018
|
+
agentSelect.onchange=()=>safe(async()=>{await api('/api/agent',{method:'POST',body:JSON.stringify({agentId:agentSelect.value})});toast('Agent switched');await loadBootstrap();await loadChatHistory()});
|
|
708
1019
|
}
|
|
709
1020
|
function renderSnapshot(s){
|
|
710
1021
|
document.getElementById('sessionLine').textContent=(s.session.agentLabel||'Agent')+' / '+(s.session.model||'default')+' / '+(s.session.threadId||'not started');
|
|
711
1022
|
document.getElementById('sessionText').textContent=s.sessionText||'';
|
|
712
1023
|
document.getElementById('metrics').innerHTML=[
|
|
713
|
-
['Status',s.processing?'working':'idle'],['Agent',s.session.agentLabel],['Queue',s.queue.length],['Workspace',s.session.workspace],['Thread',s.session.threadId||'not started'],['Reasoning',s.session.reasoningEffort||'default']
|
|
1024
|
+
['Status',s.processing?'working':'idle'],['Agent',s.session.agentLabel],['Queue',s.queue.length],['Workspace',s.session.workspace],['Thread',s.session.threadId||'not started'],['Reasoning',s.session.reasoningEffort||'default'],['Fast',s.session.capabilities&&s.session.capabilities.fastMode?(s.session.fastMode?'on':'off'):'n/a']
|
|
714
1025
|
].map(([k,v])=>'<div class="metric"><div class="label">'+esc(k)+'</div><div class="value">'+esc(v)+'</div></div>').join('');
|
|
715
|
-
renderQueue(s.queue);
|
|
1026
|
+
renderQueue(s.queue,s.queuePaused);
|
|
1027
|
+
}
|
|
1028
|
+
function renderSessionControls(){
|
|
1029
|
+
const c=state.controls||{};const s=state.snapshot?.session||{};const caps=c.capabilities||{};
|
|
1030
|
+
const modelOptions=['<option value="">Default</option>'].concat((c.models||[]).map(m=>'<option value="'+attr(m.slug)+'" '+(m.slug===s.model?'selected':'')+'>'+esc(modelLabel(m))+'</option>')).join('');
|
|
1031
|
+
const reasoningOptions=(c.reasoningOptions||[]).map(v=>'<option value="'+attr(v)+'" '+(v===s.reasoningEffort?'selected':'')+'>'+esc(v)+'</option>').join('');
|
|
1032
|
+
const launchOptions=(c.launchProfiles||[]).map(p=>'<option value="'+attr(p.id)+'" '+(p.id===(s.nextLaunchProfileId||s.launchProfileId)?'selected':'')+'>'+esc(p.label+' - '+p.behavior+(p.unsafe?' - unsafe':''))+'</option>').join('');
|
|
1033
|
+
document.getElementById('sessionControls').innerHTML=[
|
|
1034
|
+
caps.modelSelection?'<label>Model<select id="controlModel">'+modelOptions+'</select></label>':'',
|
|
1035
|
+
caps.reasoningSelection?'<label>'+esc(c.reasoningLabel||'Reasoning')+'<select id="controlReasoning">'+reasoningOptions+'</select></label>':'',
|
|
1036
|
+
caps.launchProfiles?'<label>Launch<select id="controlLaunch">'+launchOptions+'</select></label>':'',
|
|
1037
|
+
caps.fastMode?'<label class="checkbox"><input id="controlFast" type="checkbox" '+(s.fastMode?'checked':'')+'> Fast mode</label>':''
|
|
1038
|
+
].join('');
|
|
1039
|
+
const model=document.getElementById('controlModel'); if(model) model.onchange=()=>safe(async()=>{if(model.value){await api('/api/session/model',{method:'POST',body:JSON.stringify({model:model.value})});toast('Model updated');loadBootstrap()}});
|
|
1040
|
+
const reasoning=document.getElementById('controlReasoning'); if(reasoning) reasoning.onchange=()=>safe(async()=>{await api('/api/session/reasoning',{method:'POST',body:JSON.stringify({reasoning:reasoning.value})});toast((c.reasoningLabel||'Reasoning')+' updated');loadBootstrap()});
|
|
1041
|
+
const launch=document.getElementById('controlLaunch'); if(launch) launch.onchange=()=>safe(async()=>{await api('/api/session/launch',{method:'POST',body:JSON.stringify({profileId:launch.value})});toast('Launch profile updated');loadBootstrap()});
|
|
1042
|
+
const fast=document.getElementById('controlFast'); if(fast) fast.onchange=()=>safe(async()=>{await api('/api/session/fast',{method:'POST',body:JSON.stringify({enabled:fast.checked})});toast('Fast mode updated');loadBootstrap()});
|
|
716
1043
|
}
|
|
717
1044
|
function renderAdapters(channels, agents){
|
|
718
|
-
|
|
1045
|
+
const channelCards=(channels||[]).map(c=>adapterCard(c.label,c.status,c.capabilities.join(', ')));
|
|
1046
|
+
const agentCards=(agents||[]).map(a=>{const available=a.status==='available';const status=available?(state.enabledAgents.includes(a.id)?'enabled':'disabled'):(a.status||'planned');const detail=[available?'integrated':(a.status||'planned'),a.envFlag,a.notes].filter(Boolean).join(' / ');return adapterCard(a.label,status,detail)});
|
|
1047
|
+
document.getElementById('adapters').innerHTML='<div class="list">'+channelCards.concat(agentCards).join('')+'</div>';
|
|
719
1048
|
}
|
|
1049
|
+
function adapterCard(label,status,detail){return '<div class="item"><strong>'+esc(label)+' <span class="adapter-status '+esc(status)+'">'+esc(status)+'</span></strong><small>'+esc(detail||'')+'</small></div>'}
|
|
720
1050
|
function appendMessage(cls,text){const box=document.getElementById('messages');const div=document.createElement('div');div.className='message '+cls;div.textContent=text;box.appendChild(div);box.scrollTop=box.scrollHeight;return div}
|
|
1051
|
+
function appendQueuedMessage(id){const div=appendMessage('system','Queued prompt '+id);const btn=document.createElement('button');btn.textContent='Cancel queued message';btn.className='danger';btn.onclick=()=>safe(async()=>{const r=await api('/api/queue',{method:'POST',body:JSON.stringify({action:'cancel',id})});renderQueue(r.queue,r.paused);div.textContent='Cancelled queued prompt '+id});div.appendChild(document.createElement('br'));div.appendChild(btn)}
|
|
1052
|
+
function renderChatMessages(messages){state.chatMessages=messages||[];const box=document.getElementById('messages');box.innerHTML=(messages||[]).map(m=>'<div class="message '+esc(m.role)+'"><small>'+esc((m.source||'web')+' / '+fmtDate(m.timestamp))+'</small>\\n'+esc(m.text)+'</div>').join('');box.scrollTop=box.scrollHeight}
|
|
1053
|
+
async function loadChatHistory(){const data=await api('/api/chat/history');renderChatMessages(data.messages||[])}
|
|
721
1054
|
let currentAgentMessage=null;
|
|
722
1055
|
function connectEvents(){
|
|
1056
|
+
if(state.events) state.events.close();
|
|
723
1057
|
const qs = token ? '?token='+encodeURIComponent(token) : '';
|
|
724
1058
|
const events = new EventSource('/api/events'+qs);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
events.
|
|
728
|
-
events.addEventListener('
|
|
729
|
-
events.addEventListener('
|
|
730
|
-
events.addEventListener('
|
|
1059
|
+
state.events=events;
|
|
1060
|
+
setConnection('Connecting','warn');
|
|
1061
|
+
events.onopen=()=>{if(state.reconnectTimer){clearTimeout(state.reconnectTimer);state.reconnectTimer=null}setConnection('Live','ok')};
|
|
1062
|
+
events.addEventListener('snapshot', e=>{const d=JSON.parse(e.data).data;state.snapshot=d;renderSnapshot(d);renderSessionControls()});
|
|
1063
|
+
events.addEventListener('chat_history', e=>renderChatMessages(JSON.parse(e.data).messages||[]));
|
|
1064
|
+
events.addEventListener('activity_update', e=>renderActivity(JSON.parse(e.data).events||[]));
|
|
1065
|
+
events.addEventListener('session_update', e=>{loadBootstrap();loadChatHistory()});
|
|
1066
|
+
events.addEventListener('queue_update', e=>{const d=JSON.parse(e.data);renderQueue(d.queue,d.paused)});
|
|
1067
|
+
events.addEventListener('turn_start', e=>{const d=JSON.parse(e.data);appendMessage('user',d.prompt);currentAgentMessage=appendMessage('agent','');if(state.currentPage==='tasks')loadTasks()});
|
|
1068
|
+
events.addEventListener('text_delta', e=>{const d=JSON.parse(e.data);if(!currentAgentMessage)currentAgentMessage=appendMessage('agent','');currentAgentMessage.textContent+=d.delta;currentAgentMessage.scrollIntoView({block:'end'});if(state.currentPage==='tasks')loadTasks()});
|
|
1069
|
+
events.addEventListener('tool_start', e=>{const d=JSON.parse(e.data);tool('tool','Started '+d.toolName);if(state.currentPage==='tasks')loadTasks()});
|
|
731
1070
|
events.addEventListener('tool_update', e=>{const d=JSON.parse(e.data);if(d.partialResult)tool('tool',d.partialResult.slice(-600))});
|
|
732
1071
|
events.addEventListener('tool_end', e=>{const d=JSON.parse(e.data);tool(d.isError?'danger':'tool','Finished '+d.toolCallId+(d.isError?' with error':''))});
|
|
733
1072
|
events.addEventListener('todo_update', e=>{const d=JSON.parse(e.data);tool('tool','Plan:\\n'+d.items.map(i=>(i.completed?'[x] ':'[ ] ')+i.text).join('\\n'))});
|
|
734
1073
|
events.addEventListener('turn_error', e=>{const d=JSON.parse(e.data);appendMessage('system','Error: '+d.error);currentAgentMessage=null});
|
|
735
|
-
events.addEventListener('turn_complete', ()=>{currentAgentMessage=null;loadBootstrap()});
|
|
736
|
-
events.addEventListener('status', e=>{const d=JSON.parse(e.data);
|
|
737
|
-
events.onerror=()=>{};
|
|
1074
|
+
events.addEventListener('turn_complete', ()=>{currentAgentMessage=null;notify('NordRelay turn finished','The active task completed.');loadBootstrap();if(state.currentPage==='tasks')loadTasks()});
|
|
1075
|
+
events.addEventListener('status', e=>{const d=JSON.parse(e.data);const msg=d.message||'';if(isCliRunningStatus(msg)){state.cliStatusActive=true;toast(msg,{sticky:true});return}if(isCliDoneStatus(msg))state.cliStatusActive=false;toast(msg)});
|
|
1076
|
+
events.onerror=()=>{setConnection('Reconnecting','error');if(!state.reconnectTimer)state.reconnectTimer=setTimeout(()=>{state.reconnectTimer=null;connectEvents()},5000)};
|
|
738
1077
|
}
|
|
739
|
-
function
|
|
1078
|
+
function setConnection(text,kind){const el=document.getElementById('connectionStatus');el.textContent=text;el.className='badge connection-'+kind}
|
|
1079
|
+
async function enableNotifications(){if(!('Notification' in window)){toast('Browser notifications are not supported');return}const permission=Notification.permission==='granted'?'granted':await Notification.requestPermission();state.notifications=permission==='granted';toast(state.notifications?'Browser notifications enabled':'Browser notifications denied')}
|
|
1080
|
+
function notify(title,body){if(state.notifications&&'Notification' in window&&Notification.permission==='granted')new Notification(title,{body})}
|
|
1081
|
+
function updateToolAgeTitles(){document.querySelectorAll('.tool[data-created-at]').forEach(el=>{const created=Number(el.dataset.createdAt||Date.now());el.title='Updated '+fmtAge(Date.now()-created)})}
|
|
1082
|
+
function tool(cls,text){const div=document.createElement('div');div.className='tool '+(cls==='danger'?'danger':'');div.dataset.createdAt=String(Date.now());div.textContent=text;document.getElementById('toolStream').prepend(div);updateToolAgeTitles()}
|
|
1083
|
+
setInterval(updateToolAgeTitles,30000);
|
|
740
1084
|
let selectedFiles=[];
|
|
741
1085
|
function renderSelectedFiles(){const summary=document.getElementById('fileSummary');if(selectedFiles.length===0){summary.textContent='No files selected';return}const names=selectedFiles.slice(0,3).map(f=>f.name || 'file').join(', ');const more=selectedFiles.length>3?' +'+(selectedFiles.length-3)+' more':'';const bytes=selectedFiles.reduce((sum,file)=>sum+file.size,0);summary.textContent=names+more+' ('+fmtBytes(bytes)+')'}
|
|
1086
|
+
function addFiles(files){selectedFiles=selectedFiles.concat(Array.from(files||[]));renderSelectedFiles()}
|
|
742
1087
|
async function filePayload(file){return {name:file.name || 'upload',mimeType:file.type || 'application/octet-stream',dataBase64:await fileToBase64(file)}}
|
|
743
1088
|
async function fileToBase64(file){const buffer=await file.arrayBuffer();const bytes=new Uint8Array(buffer);let binary='';const chunk=0x8000;for(let i=0;i<bytes.length;i+=chunk){binary+=String.fromCharCode(...bytes.subarray(i,i+chunk))}return btoa(binary)}
|
|
744
|
-
document.getElementById('fileInput').onchange=e=>{
|
|
1089
|
+
document.getElementById('fileInput').onchange=e=>{addFiles(e.target.files)};
|
|
745
1090
|
document.getElementById('clearFilesBtn').onclick=()=>{selectedFiles=[];document.getElementById('fileInput').value='';renderSelectedFiles()};
|
|
746
|
-
document.
|
|
747
|
-
document.
|
|
748
|
-
document.
|
|
749
|
-
document.
|
|
750
|
-
async
|
|
1091
|
+
document.addEventListener('paste',e=>{const files=Array.from(e.clipboardData?.files||[]);if(files.length){addFiles(files);toast('Pasted '+files.length+' file(s)')}});
|
|
1092
|
+
document.addEventListener('dragover',e=>{e.preventDefault();document.body.classList.add('drop-active')});
|
|
1093
|
+
document.addEventListener('dragleave',()=>document.body.classList.remove('drop-active'));
|
|
1094
|
+
document.addEventListener('drop',e=>{e.preventDefault();document.body.classList.remove('drop-active');const files=Array.from(e.dataTransfer?.files||[]);if(files.length){addFiles(files);toast('Added '+files.length+' dropped file(s)')}});
|
|
1095
|
+
document.getElementById('promptForm').onsubmit=e=>safe(async()=>{e.preventDefault();const input=document.getElementById('promptInput');const text=input.value.trim();if(!text&&selectedFiles.length===0)return;const files=selectedFiles;input.value='';selectedFiles=[];document.getElementById('fileInput').value='';renderSelectedFiles();const payloadFiles=files.length?await Promise.all(files.map(filePayload)):[];const r=files.length?await api('/api/prompt/upload',{method:'POST',body:JSON.stringify({text,files:payloadFiles})}):await api('/api/prompt',{method:'POST',body:JSON.stringify({text})});if(r.transcribeOnly)appendMessage('system','Transcribed audio:\\n'+(r.transcript||'(empty)'));else if(r.queued)appendQueuedMessage(r.queueId)},e);
|
|
1096
|
+
document.getElementById('newSessionBtn').onclick=()=>openNewSessionDialog();
|
|
1097
|
+
document.getElementById('retryBtn').onclick=()=>safe(async()=>{const r=await api('/api/retry',{method:'POST'});toast(r.queued?'Retry queued '+r.queueId:'Retry started')});
|
|
1098
|
+
document.getElementById('editLastBtn').onclick=()=>{const last=[...(state.chatMessages||[])].reverse().find(m=>m.role==='user');if(last){document.getElementById('promptInput').value=last.text;document.getElementById('promptInput').focus()}else toast('No user prompt found')};
|
|
1099
|
+
document.getElementById('syncBtn').onclick=()=>safe(async()=>{const r=await api('/api/sync',{method:'POST'});toast(r.changed?'Synced: '+(r.changedFields||[]).join(', '):'Already in sync');loadBootstrap()});
|
|
1100
|
+
document.getElementById('notifyBtn').onclick=()=>enableNotifications();
|
|
1101
|
+
document.getElementById('clearChatBtn').onclick=()=>safe(async()=>{if(confirm('Clear chat history for the current thread?')){const r=await api('/api/chat/history',{method:'DELETE'});renderChatMessages(r.messages||[]);toast('Removed '+r.removed+' messages')}});
|
|
1102
|
+
document.getElementById('abortBtn').onclick=()=>safe(async()=>{await api('/api/abort',{method:'POST'});toast('Abort sent')});
|
|
1103
|
+
document.getElementById('handbackBtn').onclick=()=>safe(async()=>{const r=await api('/api/handback',{method:'POST'});appendMessage('system','Handback command:\\n'+(r.command||'No command available'))});
|
|
1104
|
+
document.getElementById('recordBtn').onclick=()=>safe(async()=>{const btn=document.getElementById('recordBtn');if(state.mediaRecorder&&state.mediaRecorder.state==='recording'){state.mediaRecorder.stop();btn.textContent='Record voice';return}const stream=await navigator.mediaDevices.getUserMedia({audio:true});state.recordedChunks=[];state.mediaRecorder=new MediaRecorder(stream);state.mediaRecorder.ondataavailable=e=>{if(e.data.size>0)state.recordedChunks.push(e.data)};state.mediaRecorder.onstop=()=>{stream.getTracks().forEach(t=>t.stop());const blob=new Blob(state.recordedChunks,{type:'audio/webm'});addFiles([new File([blob],'voice-note.webm',{type:'audio/webm'})]);toast('Voice note attached')};state.mediaRecorder.start();btn.textContent='Stop recording'});
|
|
1105
|
+
function renderNewSessionControls(c){const s=state.snapshot?.session||{};const caps=c.capabilities||{};document.getElementById('workspaceOptions').innerHTML=(c.workspaces||[]).map(w=>'<option value="'+attr(w)+'"></option>').join('');document.getElementById('newModel').innerHTML='<option value="">Default</option>'+((c.models||[]).map(m=>'<option value="'+attr(m.slug)+'">'+esc(modelLabel(m))+'</option>').join(''));document.getElementById('newModel').parentElement.style.display=caps.modelSelection?'grid':'none';const reasoningWrap=document.getElementById('newReasoningWrap');reasoningWrap.firstChild.nodeValue=(c.reasoningLabel||'Reasoning');reasoningWrap.style.display=caps.reasoningSelection?'grid':'none';document.getElementById('newReasoning').innerHTML='<option value="">Default</option>'+((c.reasoningOptions||[]).map(v=>'<option value="'+attr(v)+'">'+esc(v)+'</option>').join(''));document.getElementById('newLaunch').innerHTML='<option value="">Default</option>'+((c.launchProfiles||[]).map(p=>'<option value="'+attr(p.id)+'">'+esc(p.label+' - '+p.behavior)+'</option>').join(''));document.getElementById('newFast').checked=Boolean(s.fastMode&&caps.fastMode);document.getElementById('newLaunchWrap').style.display=caps.launchProfiles?'grid':'none';document.getElementById('newFastWrap').style.display=caps.fastMode?'inline-flex':'none'}
|
|
1106
|
+
function populateNewSessionForm(agents){const s=state.snapshot?.session||{};const agentSelect=document.getElementById('newAgent');agentSelect.innerHTML=(agents||[]).map(a=>'<option value="'+attr(a)+'" '+(a===s.agentId?'selected':'')+'>'+esc(a)+'</option>').join('');agentSelect.value=s.agentId||agentSelect.value;document.getElementById('newWorkspace').value=s.workspace||'';state.newSessionControls=state.controls||{};renderNewSessionControls(state.newSessionControls);agentSelect.onchange=()=>safe(async()=>{state.newSessionControls=await api('/api/control-options?agent='+encodeURIComponent(agentSelect.value));renderNewSessionControls(state.newSessionControls)})}
|
|
1107
|
+
function openNewSessionDialog(){populateNewSessionForm(state.enabledAgents);document.getElementById('newSessionDialog').showModal()}
|
|
1108
|
+
document.getElementById('newSessionForm').onsubmit=e=>safe(async()=>{e.preventDefault();const payload={agentId:val('newAgent'),workspace:val('newWorkspace')||undefined,model:val('newModel')||undefined,reasoningEffort:val('newReasoning')||undefined,launchProfileId:val('newLaunch')||undefined,fastMode:document.getElementById('newFast').checked};await api('/api/sessions/new',{method:'POST',body:JSON.stringify(payload)});document.getElementById('newSessionDialog').close();toast('New session started');await loadBootstrap();await loadChatHistory()},e);
|
|
1109
|
+
document.getElementById('cancelSessionBtn').onclick=()=>document.getElementById('newSessionDialog').close();
|
|
1110
|
+
function val(id){return document.getElementById(id).value.trim()}
|
|
1111
|
+
async function loadSessions(reset=true){if(reset)sessionsPager.reset();const q=document.getElementById('sessionSearch').value||'';const data=await api('/api/sessions?query='+encodeURIComponent(q)+'&page='+sessionsPager.page+'&limit='+sessionsPager.pageSize);document.getElementById('sessionsList').innerHTML=data.sessions.map(s=>'<div class="item"><strong title="'+attr(s.title||s.firstUserMessage||s.id)+'">'+esc(short(s.title||s.firstUserMessage||s.id))+'</strong><small><button type="button" class="copy-id" data-copy-id="'+attr(s.id)+'" title="Copy thread ID">'+esc(short(s.id,64))+'</button> / '+esc(short((s.cwd||'')+' / '+fmtDate(s.updatedAt)))+'</small><div class="row"><button data-switch="'+attr(s.id)+'">Switch</button><button class="secondary" data-session-detail="'+attr(s.id)+'">Details</button></div></div>').join('')||'<div class="item">No sessions found.</div>';sessionsPager.render(data.pagination||{});document.querySelectorAll('[data-copy-id]').forEach(b=>b.onclick=()=>copyText(b.dataset.copyId||'','Thread ID copied'));document.querySelectorAll('[data-switch]').forEach(b=>b.onclick=()=>safe(async()=>{await api('/api/sessions/switch',{method:'POST',body:JSON.stringify({threadId:b.dataset.switch})});toast('Session switched');loadBootstrap()}));document.querySelectorAll('[data-session-detail]').forEach(b=>b.onclick=()=>safe(()=>loadSessionDetail(b.dataset.sessionDetail)))}
|
|
1112
|
+
async function loadSessionDetail(threadId){const d=await api('/api/sessions/detail?threadId='+encodeURIComponent(threadId));const r=d.record||{};document.getElementById('sessionDetail').innerHTML='<h2>Session detail</h2>'+card('Metadata',[['Thread',threadId],['Agent',r.agentId],['Title',r.title],['Workspace',r.cwd],['Model',r.model],['Reasoning',r.reasoningEffort],['Updated',fmtDate(r.updatedAt)],['Path',r.sessionPath]])+'<h2>Recent messages</h2><div class="list">'+(d.messages||[]).slice(-20).map(m=>'<div class="item"><strong>'+esc(m.role+' / '+fmtDate(m.timestamp))+'</strong><small>'+esc(short(m.text,500))+'</small></div>').join('')+'</div><h2>Activity</h2><div class="list">'+(d.activity||[]).map(e=>'<div class="item"><strong>'+esc(e.status+' / '+e.type+' / '+fmtDate(e.timestamp))+'</strong><small>'+esc(short(e.prompt||e.detail||'',300))+'</small></div>').join('')+'</div>';document.getElementById('sessionDetailDialog').showModal()}
|
|
1113
|
+
document.getElementById('closeSessionDetailBtn').onclick=()=>document.getElementById('sessionDetailDialog').close();
|
|
751
1114
|
document.getElementById('sessionSearchBtn').onclick=()=>loadSessions(true);document.getElementById('sessionSearch').addEventListener('keydown',e=>{if(e.key==='Enter')loadSessions(true)});document.getElementById('attachBtn').onclick=async()=>{const threadId=document.getElementById('attachInput').value.trim();if(threadId){await api('/api/sessions/attach',{method:'POST',body:JSON.stringify({threadId})});toast('Session attached');loadBootstrap()}};
|
|
752
|
-
function renderQueue(queue){document.getElementById('queueList').innerHTML=(queue||[]).map(q=>'<div class="item"><strong>'+esc(q.id
|
|
753
|
-
document.querySelectorAll('[data-queue]').forEach(b=>b.onclick=
|
|
754
|
-
async function
|
|
1115
|
+
function renderQueue(queue,paused){document.getElementById('queueStatus').textContent=paused?'Paused':'Running';document.getElementById('queueList').innerHTML=(queue||[]).map((q,i)=>'<div class="item queue-item" draggable="true" data-queue-id="'+attr(q.id)+'"><strong>'+esc((i+1)+'. '+q.id+' - '+q.description)+'</strong><small>Created '+fmtDate(q.createdAt)+' / attempts '+q.attempts+(q.lastError?' / '+esc(q.lastError):'')+'</small><div class="row"><button data-q="run" data-id="'+q.id+'">Run</button><button data-q="top" data-id="'+q.id+'">Top</button><button data-q="up" data-id="'+q.id+'">Up</button><button data-q="down" data-id="'+q.id+'">Down</button><button data-q="cancel" data-id="'+q.id+'" class="danger">Cancel</button></div></div>').join('')||'<div class="item">Queue is empty.</div>';document.querySelectorAll('[data-q]').forEach(b=>b.onclick=()=>safe(async()=>{const r=await api('/api/queue',{method:'POST',body:JSON.stringify({action:b.dataset.q,id:b.dataset.id})});renderQueue(r.queue,r.paused)}));let dragged=null;document.querySelectorAll('.queue-item').forEach(item=>{item.ondragstart=()=>{dragged=item.dataset.queueId;item.classList.add('dragging')};item.ondragend=()=>item.classList.remove('dragging');item.ondragover=e=>e.preventDefault();item.ondrop=()=>safe(async()=>{if(dragged&&dragged!==item.dataset.queueId){const ids=Array.from(document.querySelectorAll('.queue-item')).map(el=>el.dataset.queueId);const targetIndex=Math.max(0,ids.indexOf(item.dataset.queueId));await api('/api/queue',{method:'POST',body:JSON.stringify({action:'top',id:dragged})});for(let i=0;i<targetIndex;i++)await api('/api/queue',{method:'POST',body:JSON.stringify({action:'down',id:dragged})});const r=await api('/api/queue');renderQueue(r.queue,r.paused)}})})}
|
|
1116
|
+
document.querySelectorAll('[data-queue]').forEach(b=>b.onclick=()=>safe(async()=>{const r=await api('/api/queue',{method:'POST',body:JSON.stringify({action:b.dataset.queue})});renderQueue(r.queue,r.paused)}));
|
|
1117
|
+
async function loadTasks(){const d=await api('/api/tasks');renderTasks(d)}
|
|
1118
|
+
function taskCard(t,title){if(!t)return '<div class="item"><strong>'+esc(title)+'</strong><small>Idle</small></div>';const tools=(t.tools||[]).map(x=>x.name+' x'+x.count).join(', ')||'-';return '<div class="item"><strong>'+esc(title+' · '+t.status)+'</strong><small>'+esc((t.agentLabel||t.agentId||t.source)+' / '+(t.threadId||'-'))+'</small><small>'+esc('Elapsed '+fmtDuration(t.durationMs)+' / current '+(t.currentTool||'-')+' / last '+(t.lastTool||'-'))+'</small><small>'+esc('Tools: '+tools+' / output chars '+(t.outputChars||0))+'</small><small>'+esc(t.prompt||t.detail||'')+'</small></div>'}
|
|
1119
|
+
function renderTasks(d){document.getElementById('tasksList').innerHTML='<div class="task-grid">'+taskCard(d.current,'Current web turn')+taskCard(d.external,'External CLI turn')+'</div><h2>Queue</h2><div class="list">'+((d.queue||[]).map(q=>'<div class="item"><strong>'+esc(q.id+' · '+q.description)+'</strong><small>'+esc(fmtDate(q.createdAt)+' / attempts '+q.attempts)+'</small><div class="row"><button data-q="run" data-id="'+attr(q.id)+'">Run</button><button data-q="cancel" data-id="'+attr(q.id)+'" class="danger">Cancel</button></div></div>').join('')||'<div class="item">Queue is empty.</div>')+'</div><h2>Recent turns</h2><div class="list">'+((d.recent||[]).map(e=>'<div class="item"><strong>'+esc(e.status+' / '+e.source+' / '+e.type)+'</strong><small>'+esc(fmtDate(e.timestamp)+' / '+(e.threadId||'-'))+'</small><small>'+esc(short(e.prompt||e.detail||'',300))+'</small></div>').join('')||'<div class="item">No recent tasks.</div>')+'</div>';document.querySelectorAll('#tasksList [data-q]').forEach(b=>b.onclick=()=>safe(async()=>{const r=await api('/api/queue',{method:'POST',body:JSON.stringify({action:b.dataset.q,id:b.dataset.id})});renderQueue(r.queue,r.paused);loadTasks()}))}
|
|
1120
|
+
document.getElementById('reloadTasksBtn').onclick=()=>loadTasks();
|
|
1121
|
+
async function loadArtifacts(){const data=await api('/api/artifacts');state.artifactReports=data.reports||[];renderArtifacts()}
|
|
1122
|
+
function artifactMatches(a,kind,query){const name=(a.name||a.relativePath||'').toLowerCase();if(query&&!name.includes(query))return false;if(kind==='images')return /\\.(png|jpe?g|gif|webp|svg)$/i.test(name);if(kind==='docs')return !/\\.(png|jpe?g|gif|webp|svg)$/i.test(name);return true}
|
|
1123
|
+
function renderArtifacts(){const query=(document.getElementById('artifactSearch').value||'').toLowerCase();const kind=document.getElementById('artifactKind').value;const reports=state.artifactReports||[];document.getElementById('artifactList').innerHTML=reports.map(r=>{const files=(r.artifacts||[]).filter(a=>artifactMatches(a,kind,query));if(files.length===0)return'';const gallery=files.map(a=>{const href='/api/artifacts/file?turnId='+encodeURIComponent(r.turnId)+'&path='+encodeURIComponent(a.relativePath)+(token?'&token='+encodeURIComponent(token):'');const img=/\\.(png|jpe?g|gif|webp|svg)$/i.test(a.name)?'<img src="'+href+'">':'<pre>'+esc(a.name.split('.').pop()||'file')+'</pre>';return '<div class="artifact-card"><label><input type="checkbox" data-artifact-select="'+attr(r.turnId)+'" '+(state.selectedArtifactTurns.has(r.turnId)?'checked':'')+'> '+esc(short(a.name,32))+'</label>'+img+'<small>'+esc(fmtBytes(a.sizeBytes))+'</small><div class="row"><a href="'+href+'">Open</a><button class="secondary" data-preview-turn="'+attr(r.turnId)+'" data-preview-path="'+attr(a.relativePath)+'">Preview</button></div></div>'}).join('');return '<div class="item"><strong>'+esc(r.turnId)+' - '+files.length+'/'+r.fileCount+' files - '+fmtBytes(r.totalSizeBytes)+'</strong><small>'+fmtDate(r.updatedAt)+' / '+esc(r.source||'turn')+'</small><div class="row"><a href="/api/artifacts/zip?turnId='+encodeURIComponent(r.turnId)+(token?'&token='+encodeURIComponent(token):'')+'">Download ZIP</a><button data-del-art="'+esc(r.turnId)+'" class="danger">Delete</button></div><div class="gallery">'+gallery+'</div></div>'}).join('')||'<div class="item">No artifacts.</div>';document.querySelectorAll('[data-artifact-select]').forEach(c=>c.onchange=()=>{if(c.checked)state.selectedArtifactTurns.add(c.dataset.artifactSelect);else state.selectedArtifactTurns.delete(c.dataset.artifactSelect)});document.querySelectorAll('[data-del-art]').forEach(b=>b.onclick=()=>safe(async()=>{if(confirm('Delete artifact turn '+b.dataset.delArt+'?')){await api('/api/artifacts?turnId='+encodeURIComponent(b.dataset.delArt),{method:'DELETE'});state.selectedArtifactTurns.delete(b.dataset.delArt);loadArtifacts()}}));document.querySelectorAll('[data-preview-turn]').forEach(b=>b.onclick=()=>previewArtifact(b.dataset.previewTurn,b.dataset.previewPath))}
|
|
755
1124
|
document.getElementById('reloadArtifactsBtn').onclick=loadArtifacts;
|
|
1125
|
+
document.getElementById('artifactSearch').oninput=renderArtifacts;
|
|
1126
|
+
document.getElementById('artifactKind').onchange=renderArtifacts;
|
|
1127
|
+
document.getElementById('zipSelectedArtifactsBtn').onclick=()=>{const turnIds=[...state.selectedArtifactTurns];if(turnIds.length===0){toast('No artifact turns selected');return}turnIds.forEach(turnId=>window.open('/api/artifacts/zip?turnId='+encodeURIComponent(turnId)+(token?'&token='+encodeURIComponent(token):''),'_blank'))};
|
|
1128
|
+
document.getElementById('deleteSelectedArtifactsBtn').onclick=()=>safe(async()=>{const turnIds=[...state.selectedArtifactTurns];if(turnIds.length===0){toast('No artifact turns selected');return}if(confirm('Delete '+turnIds.length+' selected artifact turn(s)?')){const r=await api('/api/artifacts/bulk',{method:'POST',body:JSON.stringify({action:'delete',turnIds})});state.selectedArtifactTurns.clear();toast('Deleted '+(r.removed||[]).length+' artifact turn(s)');loadArtifacts()}});
|
|
1129
|
+
function highlightCode(text){return esc(text).replace(/\\b(import|export|const|let|function|return|if|else|for|while|class|interface|type|async|await)\\b/g,'<span class="chip">$1</span>')}
|
|
1130
|
+
async function previewArtifact(turnId,path){const data=await api('/api/artifacts/preview?turnId='+encodeURIComponent(turnId)+'&path='+encodeURIComponent(path));const target=document.getElementById('artifactPreview');if(data.kind==='image'){target.innerHTML='<div class="panel"><h2>'+esc(data.name)+'</h2><img src="/api/artifacts/file?turnId='+encodeURIComponent(turnId)+'&path='+encodeURIComponent(path)+(token?'&token='+encodeURIComponent(token):'')+'"></div>';return}if(data.kind==='text'){target.innerHTML='<div class="panel"><h2>'+esc(data.name)+' '+fmtBytes(data.sizeBytes)+'</h2><pre>'+highlightCode(data.text||'')+'</pre>'+(data.truncated?'<small>Preview truncated.</small>':'')+'</div>';return}target.innerHTML='<div class="panel"><h2>'+esc(data.name)+'</h2><p>'+esc(data.detail||'Preview unavailable')+'</p></div>'}
|
|
1131
|
+
async function loadActivity(){const q='?source='+encodeURIComponent(val('activitySource'))+'&status='+encodeURIComponent(val('activityStatus'))+'&limit='+encodeURIComponent(val('activityLimit')||'100');const data=await api('/api/activity'+q);state.activityEvents=data.events||[];renderActivity(state.activityEvents)}
|
|
1132
|
+
function renderActivity(events){const since=val('activitySince')?new Date(val('activitySince')).getTime():0;const filtered=(events||[]).filter(e=>!since||new Date(e.timestamp).getTime()>=since);document.getElementById('activityList').innerHTML=filtered.map(e=>'<div class="item"><strong><span class="chip '+(e.status==='failed'?'error':e.status==='queued'?'warn':'')+'">'+esc(e.status)+'</span>'+esc(fmtDate(e.timestamp)+' / '+e.source+' / '+e.type)+'</strong><small>'+esc(short(e.prompt||e.detail||'',220))+'</small><small><button type="button" class="copy-id" data-copy-id="'+attr(e.threadId||'')+'">'+esc(e.threadId||'-')+'</button> / '+esc((e.workspace||'-')+' / '+fmtDuration(e.durationMs))+'</small></div>').join('')||'<div class="item">No activity.</div>';document.querySelectorAll('#activityList [data-copy-id]').forEach(b=>b.onclick=()=>copyText(b.dataset.copyId||'','Thread ID copied'))}
|
|
1133
|
+
document.getElementById('loadActivityBtn').onclick=()=>loadActivity();
|
|
1134
|
+
document.getElementById('activitySince').onchange=()=>renderActivity(state.activityEvents||[]);
|
|
1135
|
+
document.getElementById('exportActivityBtn').onclick=()=>{const rows=(state.activityEvents||[]).map(e=>[e.timestamp,e.source,e.status,e.type,e.threadId||'',e.prompt||e.detail||''].join('\\t')).join('\\n');const blob=new Blob([rows],{type:'text/tab-separated-values'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='nordrelay-activity.tsv';a.click();URL.revokeObjectURL(a.href)};
|
|
756
1136
|
async function loadSettings(){const data=await api('/api/settings');state.settings=data.settings;renderSettings()}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1137
|
+
const settingsGroupOrder=['Agents','Codex','Pi','Hermes','OpenClaw','Claude Code','Telegram','Operations','Artifacts','Workspace','Voice','Dashboard'];
|
|
1138
|
+
const agentSettingGroups=['Codex','Pi','Hermes','OpenClaw','Claude Code'];
|
|
1139
|
+
function orderedSettingsGroups(groups){const known=settingsGroupOrder.filter(name=>groups[name]);const extra=Object.keys(groups).filter(name=>!settingsGroupOrder.includes(name)).sort();return known.concat(extra)}
|
|
1140
|
+
function agentSettingsNav(current){return '<div class="agent-settings-nav"><strong>Agent settings</strong>'+agentSettingGroups.map(name=>'<button type="button" data-setting-tab="'+attr(name)+'" class="'+(name===current?'active':'')+'">'+esc(name)+'</button>').join('')+'</div>'}
|
|
1141
|
+
function renderSettings(){const groups={};state.settings.forEach(s=>(groups[s.group]??=[]).push(s));const names=orderedSettingsGroups(groups);if(!state.settingsGroup||!groups[state.settingsGroup])state.settingsGroup=groups.Agents?'Agents':names[0];document.getElementById('settingsTabs').innerHTML=names.map(name=>'<button data-setting-tab="'+attr(name)+'" class="'+(name===state.settingsGroup?'active':'')+'">'+esc(name)+' ('+groups[name].length+')</button>').join('');document.querySelectorAll('[data-setting-tab]').forEach(b=>b.onclick=()=>{state.settingsGroup=b.dataset.settingTab;renderSettings()});const items=groups[state.settingsGroup]||[];const nav=(state.settingsGroup==='Agents'||agentSettingGroups.includes(state.settingsGroup))?agentSettingsNav(state.settingsGroup):'';document.getElementById('settingsForm').innerHTML='<div class="settings-section"><h2>'+esc(state.settingsGroup||'Settings')+'</h2><div id="settingsRestartBanner"></div>'+nav+items.map(s=>'<div class="setting" data-setting-box="'+attr(s.key)+'" data-restart-required="'+(s.restartRequired?'true':'false')+'"><label>'+esc(s.label)+'</label>'+settingInput(s)+'<small>'+esc(s.key)+' - '+esc(s.description)+(s.effectiveValue?' Active: '+esc(s.effectiveValue)+'.':'')+(s.restartRequired?' Restart required.':'')+(s.configured?' Configured.':' Inherited/default.')+'</small><div class="setting-actions"><button type="button" class="secondary" data-reset-setting="'+attr(s.key)+'">Use default</button>'+(s.kind==='secret'?'<button type="button" class="secondary" data-reveal-setting="'+attr(s.key)+'">Reveal/replace</button>':'')+'</div><div class="setting-error"></div></div>').join('')+'</div>';document.querySelectorAll('[data-setting-tab]').forEach(b=>b.onclick=()=>{state.settingsGroup=b.dataset.settingTab;renderSettings()});bindSettingsUx()}
|
|
1142
|
+
function settingAttrs(s,original){return ' data-setting="'+attr(s.key)+'" data-original-value="'+attr(original)+'" data-configured="'+(s.configured?'true':'false')+'"'}
|
|
1143
|
+
function settingInput(s){const display=s.configured?(s.value||''):(s.effectiveValue||''); if(s.options){const blankLabel=s.effectiveValue?'Use active default ('+s.effectiveValue+')':'Use active default';return '<select'+settingAttrs(s,s.configured?s.value:'')+'><option value="" '+(!s.configured?'selected':'')+'>'+esc(blankLabel)+'</option>'+s.options.map(o=>'<option value="'+attr(o)+'" '+(s.configured&&s.value===o?'selected':'')+'>'+esc(o)+'</option>').join('')+'</select>'} if(s.kind==='boolean'){const blankLabel=s.effectiveValue?'Use active default ('+s.effectiveValue+')':'Use active default';return '<select'+settingAttrs(s,s.configured?s.value:'')+'><option value="" '+(!s.configured?'selected':'')+'>'+esc(blankLabel)+'</option><option value="true" '+(s.configured&&s.value==='true'?'selected':'')+'>true</option><option value="false" '+(s.configured&&s.value==='false'?'selected':'')+'>false</option></select>'} const value=esc(display); if(s.kind==='json')return '<textarea rows="4"'+settingAttrs(s,display)+'>'+value+'</textarea>'; return '<input'+settingAttrs(s,display)+' value="'+value+'" '+(s.kind==='secret'?'type="password"':'')+'>'}
|
|
1144
|
+
function bindSettingsUx(){document.querySelectorAll('[data-setting]').forEach(el=>{el.oninput=markSettingDirty;el.onchange=markSettingDirty});document.querySelectorAll('[data-reset-setting]').forEach(b=>b.onclick=()=>{const input=document.querySelector('[data-setting="'+cssEscape(b.dataset.resetSetting)+'"]');if(input){input.value='';markSettingDirty({target:input})}});document.querySelectorAll('[data-reveal-setting]').forEach(b=>b.onclick=()=>{const input=document.querySelector('[data-setting="'+cssEscape(b.dataset.revealSetting)+'"]');if(input){input.type=input.type==='password'?'text':'password';input.focus()}})}
|
|
1145
|
+
function markSettingDirty(e){const el=e.target;const box=el.closest('.setting');const dirty=el.value!==(el.dataset.originalValue??'');box.classList.toggle('dirty',dirty);const dirtyInputs=Array.from(document.querySelectorAll('[data-setting]')).filter(x=>x.value!==(x.dataset.originalValue??''));const restart=dirtyInputs.some(x=>x.closest('.setting')?.dataset.restartRequired==='true');document.getElementById('settingsStatus').textContent=dirtyInputs.length?dirtyInputs.length+' unsaved change(s)':'';const banner=document.getElementById('settingsRestartBanner');if(banner)banner.innerHTML=restart?'<div class="restart-banner">Some changed settings require a NordRelay restart.</div>':''}
|
|
1146
|
+
document.getElementById('saveSettingsBtn').onclick=()=>safe(async()=>{document.querySelectorAll('.setting-error').forEach(e=>e.textContent='');const patch={};document.querySelectorAll('[data-setting]').forEach(el=>{const original=el.dataset.originalValue??'';if(el.value!==original)patch[el.dataset.setting]=el.value});const r=await api('/api/settings',{method:'PATCH',body:JSON.stringify({settings:patch})});(r.errors||[]).forEach(err=>{const box=document.querySelector('[data-setting-box="'+cssEscape(err.key)+'"] .setting-error');if(box)box.textContent=err.message});document.getElementById('settingsStatus').textContent=(r.errors&&r.errors.length)?'Fix '+r.errors.length+' setting error(s)':(r.changedKeys.length?'Saved '+r.changedKeys.length+' setting(s)'+(r.restartRequired?' - restart required':''):'No changes');toast((r.errors&&r.errors.length)?'Settings need attention':'Settings saved');if(!(r.errors&&r.errors.length))await loadSettings()});
|
|
1147
|
+
document.getElementById('restartBtn').onclick=()=>safe(async()=>{if(confirm('Restart NordRelay now?')){await api('/api/runtime/restart',{method:'POST'});toast('Restart requested')}});
|
|
1148
|
+
async function loadAccess(){const d=await api('/api/permissions');document.getElementById('accessPanel').innerHTML=['TELEGRAM_ADMIN_USER_IDS','TELEGRAM_ALLOWED_USER_IDS','TELEGRAM_READONLY_USER_IDS','TELEGRAM_ALLOWED_CHAT_IDS','TELEGRAM_ALLOW_ANY_CHAT','TELEGRAM_ROLE_POLICIES_JSON'].map(key=>{const value=key==='TELEGRAM_ADMIN_USER_IDS'?d.telegramAdminUserIds.join(','):key==='TELEGRAM_ALLOWED_USER_IDS'?d.telegramAllowedUserIds.join(','):key==='TELEGRAM_READONLY_USER_IDS'?d.telegramReadOnlyUserIds.join(','):key==='TELEGRAM_ALLOWED_CHAT_IDS'?d.telegramAllowedChatIds.join(','):key==='TELEGRAM_ALLOW_ANY_CHAT'?String(d.telegramAllowAnyChat):JSON.stringify(d.telegramRolePolicies||{},null,2);return '<div class="setting"><label>'+esc(key)+'</label>'+(key.endsWith('_JSON')?'<textarea rows="5" data-access-setting="'+key+'">'+esc(value)+'</textarea>':'<input data-access-setting="'+key+'" value="'+esc(value)+'">')+'<small>Access control setting. Restart required after saving.</small></div>'}).join('');await loadLocks();await loadAudit()}
|
|
1149
|
+
document.getElementById('loadAccessBtn').onclick=()=>loadAccess();
|
|
1150
|
+
document.getElementById('saveAccessBtn').onclick=()=>safe(async()=>{const settings={};document.querySelectorAll('[data-access-setting]').forEach(el=>settings[el.dataset.accessSetting]=el.value);const r=await api('/api/settings',{method:'PATCH',body:JSON.stringify({settings})});toast((r.errors&&r.errors.length)?'Access settings need attention':'Access settings saved. Restart required.');if(r.errors&&r.errors.length)document.getElementById('accessPanel').insertAdjacentHTML('afterbegin','<div class="restart-banner">'+esc(r.errors.map(e=>e.key+': '+e.message).join(' / '))+'</div>')});
|
|
1151
|
+
async function loadLocks(){const d=await api('/api/locks');document.getElementById('locksList').innerHTML=(d.locks||[]).map(l=>'<div class="item"><strong>'+esc(l.contextKey)+'</strong><small>'+esc((l.ownerName||'owner')+' / '+l.ownerId+' / expires '+fmtDate(l.expiresAt))+'</small></div>').join('')||'<div class="item">No active locks.</div>'}
|
|
1152
|
+
document.getElementById('lockSessionBtn').onclick=()=>safe(async()=>{await api('/api/locks',{method:'POST',body:JSON.stringify({ownerName:'Web dashboard'})});toast('Web session locked');loadLocks()});
|
|
1153
|
+
document.getElementById('unlockSessionBtn').onclick=()=>safe(async()=>{await api('/api/locks',{method:'DELETE'});toast('Web session unlocked');loadLocks()});
|
|
1154
|
+
async function loadAudit(){const d=await api('/api/audit?limit='+encodeURIComponent(val('auditLimit')||'50'));document.getElementById('auditList').innerHTML=(d.events||[]).map(e=>'<div class="item"><strong>'+esc(fmtDate(e.timestamp)+' / '+(e.channelId||'-')+' / '+e.status+' / '+e.action)+'</strong><small>'+esc((e.contextKey||'-')+' / '+(e.agentId||'-')+' / '+(e.threadId||'-'))+'</small><small>'+esc(e.description||e.detail||'')+'</small></div>').join('')||'<div class="item">No audit events.</div>'}
|
|
1155
|
+
document.getElementById('loadAuditBtn').onclick=()=>loadAudit();
|
|
1156
|
+
async function loadLogs(){const target=document.getElementById('logTarget').value;const lines=document.getElementById('logLines').value;const data=await api('/api/logs?target='+target+'&lines='+lines);state.logsPlain=data.plain||'';renderLogs();if(document.getElementById('logFollow').checked)document.getElementById('logs').scrollTop=document.getElementById('logs').scrollHeight}document.getElementById('loadLogsBtn').onclick=loadLogs;
|
|
1157
|
+
function logLevelOf(line){if(line.includes(' ERROR '))return'ERROR';if(line.includes(' WARN '))return'WARN';if(line.includes(' INFO '))return'INFO';return''}
|
|
1158
|
+
function logTimeOf(line){const m=line.match(/^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})/);return m?new Date(m[1].replace(' ','T')).getTime():0}
|
|
1159
|
+
function renderLogs(){const level=val('logLevel');const query=val('logSearch').toLowerCase();const since=val('logSince')?new Date(val('logSince')).getTime():0;const lines=state.logsPlain.split(/\\n/).filter(line=>(level==='all'||line.includes(level))&&(!query||line.toLowerCase().includes(query))&&(!since||!logTimeOf(line)||logTimeOf(line)>=since));document.getElementById('logs').innerHTML=lines.map(line=>'<span class="log-line '+logLevelOf(line)+'">'+esc(line)+'</span>').join('\\n')||'(empty)'}
|
|
1160
|
+
document.getElementById('logLevel').onchange=renderLogs;document.getElementById('logSearch').oninput=renderLogs;document.getElementById('logSince').onchange=renderLogs;document.getElementById('logAutoRefresh').onchange=e=>{clearInterval(state.logTimer);state.logTimer=null;if(e.target.checked)state.logTimer=setInterval(loadLogs,5000)};document.getElementById('downloadLogsBtn').onclick=()=>{const blob=new Blob([state.logsPlain||''],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='nordrelay-log.txt';a.click();URL.revokeObjectURL(a.href)};
|
|
1161
|
+
async function loadAdapterHealth(){const d=await api('/api/adapters/health');document.getElementById('adapterHealth').innerHTML=(d.adapters||[]).map(a=>'<div class="item"><strong>'+esc(a.label)+' <span class="adapter-status '+esc(a.status)+'">'+esc(a.status)+'</span></strong><small>'+esc('CLI: '+(a.cli.label||'-')+' / path '+(a.cli.path||'-')+' / version '+(a.cli.version||'-'))+'</small><small>'+esc('Auth: '+(a.auth.supported?(a.auth.authenticated?'authenticated':'not authenticated'):'not managed')+' '+(a.auth.detail||''))+'</small><small>'+esc('Version: '+a.version.installed+' / latest '+(a.version.latest||'-')+' / '+a.version.status)+'</small><div class="row"><button data-auth-status="'+attr(a.id)+'">Auth status</button><button data-auth-login="'+attr(a.id)+'" class="secondary" '+(!a.capabilities.login?'disabled':'')+'>Login</button><button data-auth-logout="'+attr(a.id)+'" class="secondary" '+(!a.capabilities.logout?'disabled':'')+'>Logout</button></div></div>').join('')||'<div class="item">No adapters.</div>';document.querySelectorAll('[data-auth-status]').forEach(b=>b.onclick=()=>safe(async()=>{const r=await api('/api/auth/status?agent='+encodeURIComponent(b.dataset.authStatus));toast(r.agentLabel+': '+r.detail,{duration:6000})}));document.querySelectorAll('[data-auth-login]').forEach(b=>b.onclick=()=>safe(async()=>{const r=await api('/api/auth/login',{method:'POST',body:JSON.stringify({agentId:b.dataset.authLogin})});toast((r.result?.message||r.detail),{duration:8000});loadAdapterHealth()}));document.querySelectorAll('[data-auth-logout]').forEach(b=>b.onclick=()=>safe(async()=>{const r=await api('/api/auth/logout',{method:'POST',body:JSON.stringify({agentId:b.dataset.authLogout})});toast((r.result?.message||r.detail),{duration:8000});loadAdapterHealth()}))}
|
|
1162
|
+
document.getElementById('reloadAdaptersBtn').onclick=()=>loadAdapterHealth();
|
|
1163
|
+
async function loadVersion(){const d=await api('/api/version');const checks=d.versionChecks||{};document.getElementById('versionPanel').innerHTML=Object.values(checks).map(v=>'<div class="item"><strong>'+esc((v.status==='current'?'OK ':v.status==='outdated'?'WARN ':'')+v.label)+'</strong><small>'+esc('Installed: '+(v.installedLabel||'-'))+'</small><small>'+esc('Latest: '+(v.latestVersion||'-')+' / '+v.status)+'</small><small>'+esc(v.detail||'')+'</small></div>').join('')+card('Runtime',[['Status',d.state?.status],['Version',d.health?.version],['Codex CLI',d.health?.codexCli],['Pi CLI',d.health?.piCli],['Hermes CLI',d.health?.hermesCli],['OpenClaw CLI',d.health?.openClawCli],['Claude Code CLI',d.health?.claudeCodeCli]])}
|
|
1164
|
+
document.getElementById('loadVersionBtn').onclick=()=>loadVersion();
|
|
1165
|
+
document.getElementById('updateBtn').onclick=()=>safe(async()=>{if(confirm('Start NordRelay self-update now?')){const r=await api('/api/update',{method:'POST'});toast('Update started via '+r.method+'. Log: '+r.logPath,{duration:8000});page('logs');document.getElementById('logTarget').value='update';loadLogs()}});
|
|
1166
|
+
async function loadDiagnostics(){const data=await api('/api/diagnostics');document.getElementById('diagnostics').innerHTML=diagnosticsHtml(data)}
|
|
1167
|
+
function diagnosticsHtml(d){const h=d.health||{};const s=d.snapshot?.session||{};const vc=d.versionChecks||{};const caps=s.capabilities||{};const agentDiag=d.runtime?.agentDiagnostics;return '<div class="list">'+card('Runtime',[['Status',h.state?.status],['PID',h.state?.pid],['App PID',h.state?.appPid],['State',h.stateFile],['Log',h.logFile],['State backend',d.runtime?.stateBackend],['Uptime',h.uptimeSeconds+'s']])+card('Agent',[['Agent',s.agentLabel],['Thread',s.threadId],['Workspace',s.workspace],['Model',s.model],['Reasoning',s.reasoningEffort],['Fast',caps.fastMode?(s.fastMode?'on':'off'):'n/a']])+card('Agent State',(agentDiag?.lines||[]).map(x=>[x.label,x.value]))+card('CLI Versions',Object.values(vc).map(v=>[v.label,(v.status==='current'?'OK ':'WARN ')+(v.installedLabel||'-')+' latest '+(v.latestVersion||'-')]))+card('External Mirror',d.runtime?.externalMirror?Object.entries(d.runtime.externalMirror):[['Status','idle']])+'</div>'}
|
|
1168
|
+
function card(title,rows){return '<div class="item"><strong>'+esc(title)+'</strong>'+rows.map(r=>'<small>'+esc(r[0])+': '+esc(r[1]??'-')+'</small>').join('')+'</div>'}
|
|
1169
|
+
function safe(fn,event){if(event&&event.preventDefault)event.preventDefault();Promise.resolve().then(fn).catch(err=>toast(err.message||String(err)))}
|
|
1170
|
+
loadBootstrap().then(()=>{connectEvents();loadChatHistory();loadSessions();loadArtifacts();loadSettings();loadLogs();loadDiagnostics();loadActivity()}).catch(err=>toast(err.message));
|
|
763
1171
|
`;
|
|
764
1172
|
}
|