@nordbyte/nordrelay 0.3.1 → 0.4.1
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 +221 -35
- package/dist/access-control.js +3 -0
- package/dist/agent-activity.js +300 -0
- package/dist/agent-adapter.js +17 -30
- package/dist/agent-factory.js +27 -0
- package/dist/agent-feature-matrix.js +42 -0
- package/dist/agent-updates.js +294 -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 +483 -354
- package/dist/channel-actions.js +372 -0
- 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 +12 -1
- package/dist/config.js +113 -9
- 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 +115 -9
- 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 +798 -72
- package/dist/session-format.js +98 -19
- package/dist/session-registry.js +40 -15
- package/dist/settings-service.js +35 -4
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-client.js +275 -0
- package/dist/web-dashboard-style.js +9 -0
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +296 -196
- 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 +187 -12
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
- package/CHANGELOG.md +0 -26
package/dist/web-dashboard.js
CHANGED
|
@@ -12,8 +12,11 @@ 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
|
+
import { dashboardJs } from "./web-dashboard-client.js";
|
|
16
|
+
import { dashboardCss } from "./web-dashboard-style.js";
|
|
17
|
+
import { renderDashboardNav } from "./web-dashboard-ui.js";
|
|
15
18
|
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
16
|
-
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
|
19
|
+
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
|
|
17
20
|
const options = parseOptions(process.argv.slice(2));
|
|
18
21
|
const auth = resolveDashboardAuth(options.host);
|
|
19
22
|
if (auth.publicBind && !auth.token && !(auth.user && auth.password)) {
|
|
@@ -64,6 +67,14 @@ async function handleRequest(req, res) {
|
|
|
64
67
|
sendText(res, 200, renderDashboardApp({ authRequired: auth.required }), "text/html; charset=utf-8");
|
|
65
68
|
return;
|
|
66
69
|
}
|
|
70
|
+
if (url.pathname === "/assets/dashboard.css") {
|
|
71
|
+
sendText(res, 200, dashboardCss(), "text/css; charset=utf-8");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (url.pathname === "/assets/dashboard.js") {
|
|
75
|
+
sendText(res, 200, dashboardJs(), "application/javascript; charset=utf-8");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
67
78
|
if (url.pathname === "/api/events" && req.method === "GET") {
|
|
68
79
|
handleEvents(req, res);
|
|
69
80
|
return;
|
|
@@ -82,20 +93,96 @@ async function handleApi(req, res, url) {
|
|
|
82
93
|
agentAdapters: listAgentAdapterDescriptors(),
|
|
83
94
|
enabledAgents: enabledAgents(config),
|
|
84
95
|
controls: await runtime.controlOptions(),
|
|
85
|
-
status: await runtime.
|
|
96
|
+
status: await runtime.bootstrapStatus(),
|
|
86
97
|
});
|
|
87
98
|
return;
|
|
88
99
|
}
|
|
89
100
|
if (req.method === "GET" && url.pathname === "/api/control-options") {
|
|
90
|
-
sendJson(res, 200, await runtime.controlOptions());
|
|
101
|
+
sendJson(res, 200, await runtime.controlOptions(parseAgentId(url.searchParams.get("agent") ?? undefined)));
|
|
91
102
|
return;
|
|
92
103
|
}
|
|
93
104
|
if (req.method === "GET" && url.pathname === "/api/health") {
|
|
94
105
|
sendJson(res, 200, await runtime.status());
|
|
95
106
|
return;
|
|
96
107
|
}
|
|
108
|
+
if (req.method === "GET" && url.pathname === "/api/version") {
|
|
109
|
+
sendJson(res, 200, await runtime.version());
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (req.method === "POST" && url.pathname === "/api/update") {
|
|
113
|
+
sendJson(res, 202, runtime.updateConnector());
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (req.method === "GET" && url.pathname === "/api/agent-updates") {
|
|
117
|
+
sendJson(res, 200, { jobs: runtime.agentUpdateJobs() });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (req.method === "POST" && url.pathname === "/api/agent-update") {
|
|
121
|
+
const body = await readJsonBody(req);
|
|
122
|
+
sendJson(res, 202, { job: runtime.startAgentUpdate(parseAgentIdRequired(stringField(body, "agentId"))) });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const agentUpdateLogMatch = url.pathname.match(/^\/api\/agent-update\/([^/]+)\/log$/);
|
|
126
|
+
if (req.method === "GET" && agentUpdateLogMatch?.[1]) {
|
|
127
|
+
sendJson(res, 200, runtime.agentUpdateLog(decodeURIComponent(agentUpdateLogMatch[1])));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const agentUpdateInputMatch = url.pathname.match(/^\/api\/agent-update\/([^/]+)\/input$/);
|
|
131
|
+
if (req.method === "POST" && agentUpdateInputMatch?.[1]) {
|
|
132
|
+
const body = await readJsonBody(req);
|
|
133
|
+
sendJson(res, 200, { job: runtime.sendAgentUpdateInput(decodeURIComponent(agentUpdateInputMatch[1]), stringField(body, "input")) });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const agentUpdateCancelMatch = url.pathname.match(/^\/api\/agent-update\/([^/]+)\/cancel$/);
|
|
137
|
+
if (req.method === "POST" && agentUpdateCancelMatch?.[1]) {
|
|
138
|
+
sendJson(res, 200, { job: runtime.cancelAgentUpdate(decodeURIComponent(agentUpdateCancelMatch[1])) });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (req.method === "GET" && (url.pathname === "/api/tasks" || url.pathname === "/api/progress")) {
|
|
142
|
+
sendJson(res, 200, runtime.tasks());
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (req.method === "GET" && url.pathname === "/api/adapters/health") {
|
|
146
|
+
sendJson(res, 200, { adapters: await runtime.adapterHealth() });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (req.method === "GET" && url.pathname === "/api/permissions") {
|
|
150
|
+
sendJson(res, 200, runtime.permissions());
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (req.method === "GET" && url.pathname === "/api/audit") {
|
|
154
|
+
sendJson(res, 200, { events: runtime.audit(numberParam(url, "limit", 50)) });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (req.method === "GET" && url.pathname === "/api/locks") {
|
|
158
|
+
sendJson(res, 200, { locks: runtime.locks() });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (req.method === "POST" && url.pathname === "/api/locks") {
|
|
162
|
+
const body = await readJsonBody(req);
|
|
163
|
+
sendJson(res, 200, { lock: runtime.lockWebSession(optionalStringField(body, "ownerName")), locks: runtime.locks() });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (req.method === "DELETE" && url.pathname === "/api/locks") {
|
|
167
|
+
sendJson(res, 200, runtime.unlockWebSession());
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (req.method === "GET" && url.pathname === "/api/auth/status") {
|
|
171
|
+
sendJson(res, 200, await runtime.authStatus(parseAgentId(url.searchParams.get("agent") ?? undefined)));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (req.method === "POST" && url.pathname === "/api/auth/login") {
|
|
175
|
+
const body = await readJsonBody(req);
|
|
176
|
+
sendJson(res, 200, await runtime.login(parseAgentId(optionalStringField(body, "agentId"))));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (req.method === "POST" && url.pathname === "/api/auth/logout") {
|
|
180
|
+
const body = await readJsonBody(req);
|
|
181
|
+
sendJson(res, 200, await runtime.logout(parseAgentId(optionalStringField(body, "agentId"))));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
97
184
|
if (req.method === "GET" && url.pathname === "/api/settings") {
|
|
98
|
-
sendJson(res, 200, await settings.snapshot());
|
|
185
|
+
sendJson(res, 200, await settings.snapshot(process.env, activeSettingsValues(config)));
|
|
99
186
|
return;
|
|
100
187
|
}
|
|
101
188
|
if (req.method === "PATCH" && url.pathname === "/api/settings") {
|
|
@@ -108,7 +195,7 @@ async function handleApi(req, res, url) {
|
|
|
108
195
|
return;
|
|
109
196
|
}
|
|
110
197
|
if (req.method === "GET" && url.pathname === "/api/sessions") {
|
|
111
|
-
sendJson(res, 200, await runtime.listSessionsPage(numberParam(url, "page", 1), numberParam(url, "limit", 50), url.searchParams.get("query") ?? ""));
|
|
198
|
+
sendJson(res, 200, await runtime.listSessionsPage(numberParam(url, "page", 1), numberParam(url, "limit", 50), url.searchParams.get("query") ?? "", parseAgentId(url.searchParams.get("agent") ?? undefined)));
|
|
112
199
|
return;
|
|
113
200
|
}
|
|
114
201
|
if (req.method === "POST" && url.pathname === "/api/agent") {
|
|
@@ -144,6 +231,10 @@ async function handleApi(req, res, url) {
|
|
|
144
231
|
sendJson(res, 200, { session: await runtime.attachSession(stringField(body, "threadId")) });
|
|
145
232
|
return;
|
|
146
233
|
}
|
|
234
|
+
if (req.method === "GET" && url.pathname === "/api/sessions/detail") {
|
|
235
|
+
sendJson(res, 200, await runtime.sessionDetail(requiredSearch(url, "threadId")));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
147
238
|
if (req.method === "GET" && url.pathname === "/api/models") {
|
|
148
239
|
sendJson(res, 200, { models: await runtime.listModels() });
|
|
149
240
|
return;
|
|
@@ -181,7 +272,7 @@ async function handleApi(req, res, url) {
|
|
|
181
272
|
}));
|
|
182
273
|
return;
|
|
183
274
|
}
|
|
184
|
-
if (req.method === "POST" && url.pathname === "/api/abort") {
|
|
275
|
+
if (req.method === "POST" && (url.pathname === "/api/abort" || url.pathname === "/api/stop")) {
|
|
185
276
|
await runtime.abort();
|
|
186
277
|
sendJson(res, 200, { ok: true });
|
|
187
278
|
return;
|
|
@@ -190,6 +281,14 @@ async function handleApi(req, res, url) {
|
|
|
190
281
|
sendJson(res, 200, await runtime.handback());
|
|
191
282
|
return;
|
|
192
283
|
}
|
|
284
|
+
if (req.method === "POST" && url.pathname === "/api/retry") {
|
|
285
|
+
sendJson(res, 202, await runtime.retry());
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (req.method === "POST" && url.pathname === "/api/sync") {
|
|
289
|
+
sendJson(res, 200, await runtime.sync());
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
193
292
|
if (req.method === "GET" && url.pathname === "/api/queue") {
|
|
194
293
|
sendJson(res, 200, { queue: runtime.queue(), paused: runtime.queuePaused() });
|
|
195
294
|
return;
|
|
@@ -225,6 +324,22 @@ async function handleApi(req, res, url) {
|
|
|
225
324
|
sendJson(res, 200, { removed: await runtime.deleteArtifact(requiredSearch(url, "turnId")) });
|
|
226
325
|
return;
|
|
227
326
|
}
|
|
327
|
+
if (req.method === "POST" && url.pathname === "/api/artifacts/bulk") {
|
|
328
|
+
const body = await readJsonBody(req);
|
|
329
|
+
const action = stringField(body, "action");
|
|
330
|
+
const turnIds = Array.isArray(body.turnIds) ? body.turnIds.filter((item) => typeof item === "string") : [];
|
|
331
|
+
if (action !== "delete") {
|
|
332
|
+
throw new Error("Unsupported artifact bulk action.");
|
|
333
|
+
}
|
|
334
|
+
const removed = [];
|
|
335
|
+
for (const turnId of turnIds) {
|
|
336
|
+
if (await runtime.deleteArtifact(turnId)) {
|
|
337
|
+
removed.push(turnId);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
sendJson(res, 200, { removed });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
228
343
|
if (req.method === "GET" && url.pathname === "/api/artifacts/zip") {
|
|
229
344
|
const bundle = await runtime.createArtifactZip(requiredSearch(url, "turnId"));
|
|
230
345
|
if (!bundle) {
|
|
@@ -256,7 +371,12 @@ async function handleApi(req, res, url) {
|
|
|
256
371
|
return;
|
|
257
372
|
}
|
|
258
373
|
if (req.method === "GET" && url.pathname === "/api/logs") {
|
|
259
|
-
sendJson(res, 200, await runtime.logs(url.searchParams.get("target")
|
|
374
|
+
sendJson(res, 200, await runtime.logs(parseLogTarget(url.searchParams.get("target") ?? undefined), numberParam(url, "lines", 120)));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (req.method === "POST" && url.pathname === "/api/logs/clear") {
|
|
378
|
+
const body = await readJsonBody(req);
|
|
379
|
+
sendJson(res, 200, runtime.clearLogs(parseLogTarget(optionalStringField(body, "target"))));
|
|
260
380
|
return;
|
|
261
381
|
}
|
|
262
382
|
if (req.method === "GET" && url.pathname === "/api/diagnostics") {
|
|
@@ -427,7 +547,7 @@ function sendJson(res, status, value) {
|
|
|
427
547
|
res.end(`${JSON.stringify(value)}\n`);
|
|
428
548
|
}
|
|
429
549
|
function sendText(res, status, text, contentType) {
|
|
430
|
-
res.writeHead(status, { "content-type": contentType });
|
|
550
|
+
res.writeHead(status, { "content-type": contentType, "cache-control": "no-store" });
|
|
431
551
|
res.end(text);
|
|
432
552
|
}
|
|
433
553
|
function sendFile(res, filePath, filename) {
|
|
@@ -456,11 +576,17 @@ function parseAgentId(value) {
|
|
|
456
576
|
if (!value) {
|
|
457
577
|
return undefined;
|
|
458
578
|
}
|
|
579
|
+
return parseAgentIdRequired(value);
|
|
580
|
+
}
|
|
581
|
+
function parseAgentIdRequired(value) {
|
|
459
582
|
if (!isAgentId(value)) {
|
|
460
583
|
throw new Error(`Invalid agent: ${value}`);
|
|
461
584
|
}
|
|
462
585
|
return value;
|
|
463
586
|
}
|
|
587
|
+
function parseLogTarget(value) {
|
|
588
|
+
return value === "update" || value === "agent-updates" ? value : "connector";
|
|
589
|
+
}
|
|
464
590
|
function objectRecord(value) {
|
|
465
591
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
466
592
|
return {};
|
|
@@ -504,6 +630,110 @@ function optionalEnv(key) {
|
|
|
504
630
|
const value = process.env[key]?.trim();
|
|
505
631
|
return value || undefined;
|
|
506
632
|
}
|
|
633
|
+
function activeSettingsValues(current) {
|
|
634
|
+
return {
|
|
635
|
+
TELEGRAM_ALLOW_ANY_CHAT: boolValue(current.telegramAllowAnyChat),
|
|
636
|
+
TELEGRAM_BOT_TOKEN: current.telegramBotToken,
|
|
637
|
+
TELEGRAM_ADMIN_USER_IDS: current.telegramAdminUserIds.join(","),
|
|
638
|
+
TELEGRAM_ALLOWED_USER_IDS: current.telegramAllowedUserIds.join(","),
|
|
639
|
+
TELEGRAM_READONLY_USER_IDS: current.telegramReadOnlyUserIds.join(","),
|
|
640
|
+
TELEGRAM_ALLOWED_CHAT_IDS: current.telegramAllowedChatIds.join(","),
|
|
641
|
+
TELEGRAM_ROLE_POLICIES_JSON: optionalEnv("TELEGRAM_ROLE_POLICIES_JSON"),
|
|
642
|
+
TELEGRAM_TRANSPORT: current.telegramTransport,
|
|
643
|
+
TELEGRAM_WEBHOOK_URL: current.telegramWebhookUrl,
|
|
644
|
+
TELEGRAM_WEBHOOK_HOST: current.telegramWebhookHost,
|
|
645
|
+
TELEGRAM_WEBHOOK_PORT: String(current.telegramWebhookPort),
|
|
646
|
+
TELEGRAM_WEBHOOK_PATH: current.telegramWebhookPath,
|
|
647
|
+
TELEGRAM_WEBHOOK_SECRET: current.telegramWebhookSecret,
|
|
648
|
+
NORDRELAY_CODEX_ENABLED: boolValue(current.codexEnabled),
|
|
649
|
+
NORDRELAY_PI_ENABLED: boolValue(current.piEnabled),
|
|
650
|
+
NORDRELAY_HERMES_ENABLED: boolValue(current.hermesEnabled),
|
|
651
|
+
NORDRELAY_OPENCLAW_ENABLED: boolValue(current.openClawEnabled),
|
|
652
|
+
NORDRELAY_CLAUDE_CODE_ENABLED: boolValue(current.claudeCodeEnabled),
|
|
653
|
+
NORDRELAY_DEFAULT_AGENT: current.defaultAgent,
|
|
654
|
+
CODEX_API_KEY: current.codexApiKey,
|
|
655
|
+
CODEX_CLI_PATH: optionalEnv("CODEX_CLI_PATH"),
|
|
656
|
+
CODEX_USE_BUNDLED_CLI: process.env.CODEX_USE_BUNDLED_CLI,
|
|
657
|
+
CODEX_MODEL: current.codexModel,
|
|
658
|
+
CODEX_SYNC_INTERVAL_MS: String(current.codexSyncIntervalMs),
|
|
659
|
+
CODEX_EXTERNAL_BUSY_CHECK_MS: String(current.codexExternalBusyCheckMs),
|
|
660
|
+
CODEX_EXTERNAL_BUSY_STALE_MS: String(current.codexExternalBusyStaleMs),
|
|
661
|
+
CODEX_SANDBOX_MODE: current.codexSandboxMode,
|
|
662
|
+
CODEX_APPROVAL_POLICY: current.codexApprovalPolicy,
|
|
663
|
+
CODEX_LAUNCH_PROFILES_JSON: optionalEnv("CODEX_LAUNCH_PROFILES_JSON"),
|
|
664
|
+
CODEX_DEFAULT_LAUNCH_PROFILE: current.defaultLaunchProfileId,
|
|
665
|
+
ENABLE_UNSAFE_LAUNCH_PROFILES: boolValue(current.enableUnsafeLaunchProfiles),
|
|
666
|
+
PI_CLI_PATH: current.piCliPath,
|
|
667
|
+
PI_SESSION_DIR: current.piSessionDir,
|
|
668
|
+
PI_DEFAULT_MODEL: current.piDefaultModel,
|
|
669
|
+
PI_DEFAULT_THINKING: current.piDefaultThinking,
|
|
670
|
+
PI_DEFAULT_PROFILE: current.piDefaultLaunchProfileId,
|
|
671
|
+
HERMES_CLI_PATH: current.hermesCliPath,
|
|
672
|
+
HERMES_HOME: current.hermesHome,
|
|
673
|
+
HERMES_STATE_DB_PATH: current.hermesStateDbPath,
|
|
674
|
+
HERMES_API_BASE_URL: current.hermesApiBaseUrl,
|
|
675
|
+
HERMES_API_KEY: current.hermesApiKey,
|
|
676
|
+
HERMES_DEFAULT_MODEL: current.hermesDefaultModel,
|
|
677
|
+
HERMES_DEFAULT_REASONING: current.hermesDefaultReasoning,
|
|
678
|
+
HERMES_DEFAULT_PROFILE: current.hermesDefaultLaunchProfileId,
|
|
679
|
+
OPENCLAW_GATEWAY_URL: current.openClawGatewayUrl,
|
|
680
|
+
OPENCLAW_CLI_PATH: current.openClawCliPath,
|
|
681
|
+
OPENCLAW_GATEWAY_TOKEN: current.openClawGatewayToken,
|
|
682
|
+
OPENCLAW_GATEWAY_PASSWORD: current.openClawGatewayPassword,
|
|
683
|
+
OPENCLAW_AGENT_ID: current.openClawAgentId,
|
|
684
|
+
OPENCLAW_HOME: current.openClawHome,
|
|
685
|
+
OPENCLAW_STATE_DIR: current.openClawStateDir,
|
|
686
|
+
OPENCLAW_DEFAULT_MODEL: current.openClawDefaultModel,
|
|
687
|
+
OPENCLAW_DEFAULT_THINKING: current.openClawDefaultThinking,
|
|
688
|
+
OPENCLAW_DEFAULT_PROFILE: current.openClawDefaultLaunchProfileId,
|
|
689
|
+
CLAUDE_CODE_CLI_PATH: current.claudeCodeCliPath,
|
|
690
|
+
CLAUDE_CONFIG_DIR: current.claudeCodeConfigDir,
|
|
691
|
+
CLAUDE_CODE_DEFAULT_MODEL: current.claudeCodeDefaultModel,
|
|
692
|
+
CLAUDE_CODE_DEFAULT_EFFORT: current.claudeCodeDefaultEffort,
|
|
693
|
+
CLAUDE_CODE_DEFAULT_PROFILE: current.claudeCodeDefaultLaunchProfileId,
|
|
694
|
+
CLAUDE_CODE_MAX_TURNS: String(current.claudeCodeMaxTurns),
|
|
695
|
+
CONNECTOR_LOG_FORMAT: current.logFormat,
|
|
696
|
+
TOOL_VERBOSITY: current.toolVerbosity,
|
|
697
|
+
SHOW_TURN_TOKEN_USAGE: boolValue(current.showTurnTokenUsage),
|
|
698
|
+
ENABLE_TELEGRAM_LOGIN: boolValue(current.enableTelegramLogin),
|
|
699
|
+
ENABLE_TELEGRAM_REACTIONS: boolValue(current.enableTelegramReactions),
|
|
700
|
+
TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS: String(current.telegramRateLimitMinIntervalMs),
|
|
701
|
+
TELEGRAM_EDIT_MIN_INTERVAL_MS: String(current.telegramEditMinIntervalMs),
|
|
702
|
+
TELEGRAM_CLI_MIRROR_MODE: current.telegramMirrorMode,
|
|
703
|
+
TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS: String(current.telegramMirrorMinUpdateMs),
|
|
704
|
+
TELEGRAM_NOTIFY_MODE: current.telegramNotifyMode,
|
|
705
|
+
TELEGRAM_QUIET_HOURS: current.telegramQuietHours ? `${current.telegramQuietHours.startHour}-${current.telegramQuietHours.endHour}` : "",
|
|
706
|
+
TELEGRAM_REDACT_PATTERNS: current.telegramRedactPatterns.join(","),
|
|
707
|
+
NORDRELAY_UPDATE_METHOD: process.env.NORDRELAY_UPDATE_METHOD || "auto",
|
|
708
|
+
MAX_FILE_SIZE: String(current.maxFileSize),
|
|
709
|
+
ARTIFACT_RETENTION_DAYS: String(current.artifactRetentionDays),
|
|
710
|
+
ARTIFACT_MAX_TURNS: String(current.artifactMaxTurnDirs),
|
|
711
|
+
ARTIFACT_MAX_INBOX_DIRS: String(current.artifactMaxInboxDirs),
|
|
712
|
+
ARTIFACT_IGNORE_DIRS: current.artifactIgnoreDirs.join(","),
|
|
713
|
+
ARTIFACT_IGNORE_GLOBS: current.artifactIgnoreGlobs.join(","),
|
|
714
|
+
TELEGRAM_AUTO_SEND_ARTIFACTS: boolValue(current.telegramAutoSendArtifacts),
|
|
715
|
+
WORKSPACE_ALLOWED_ROOTS: current.workspaceAllowedRoots.join(","),
|
|
716
|
+
WORKSPACE_WARN_ROOTS: current.workspaceWarnRoots.join(","),
|
|
717
|
+
NORDRELAY_STATE_BACKEND: current.stateBackend,
|
|
718
|
+
NORDRELAY_AUDIT_MAX_EVENTS: String(current.auditMaxEvents),
|
|
719
|
+
NORDRELAY_SESSION_LOCK_TTL_MS: String(current.sessionLockTtlMs),
|
|
720
|
+
NORDRELAY_VERSION_CACHE_TTL_MS: process.env.NORDRELAY_VERSION_CACHE_TTL_MS,
|
|
721
|
+
VOICE_PREFERRED_BACKEND: current.voicePreferredBackend,
|
|
722
|
+
VOICE_DEFAULT_LANGUAGE: current.voiceDefaultLanguage,
|
|
723
|
+
VOICE_TRANSCRIBE_ONLY: boolValue(current.voiceTranscribeOnly),
|
|
724
|
+
FASTER_WHISPER_PYTHON: process.env.FASTER_WHISPER_PYTHON,
|
|
725
|
+
FASTER_WHISPER_MODEL: process.env.FASTER_WHISPER_MODEL,
|
|
726
|
+
FASTER_WHISPER_DEVICE: process.env.FASTER_WHISPER_DEVICE,
|
|
727
|
+
FASTER_WHISPER_COMPUTE_TYPE: process.env.FASTER_WHISPER_COMPUTE_TYPE,
|
|
728
|
+
FASTER_WHISPER_LANGUAGE: process.env.FASTER_WHISPER_LANGUAGE,
|
|
729
|
+
FASTER_WHISPER_TIMEOUT_MS: process.env.FASTER_WHISPER_TIMEOUT_MS,
|
|
730
|
+
NORDRELAY_DASHBOARD_HOST: options.host,
|
|
731
|
+
NORDRELAY_DASHBOARD_PORT: String(options.port),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function boolValue(value) {
|
|
735
|
+
return value ? "true" : "false";
|
|
736
|
+
}
|
|
507
737
|
function requireArg(argv, index, flag) {
|
|
508
738
|
const value = argv[index];
|
|
509
739
|
if (!value || value.startsWith("--")) {
|
|
@@ -570,22 +800,14 @@ function renderDashboardApp(options) {
|
|
|
570
800
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
571
801
|
<title>NordRelay Dashboard</title>
|
|
572
802
|
<script>document.documentElement.dataset.theme = localStorage.getItem('nordrelayTheme') || 'light';</script>
|
|
573
|
-
<
|
|
803
|
+
<link rel="stylesheet" href="/assets/dashboard.css">
|
|
574
804
|
</head>
|
|
575
805
|
<body>
|
|
576
806
|
<div class="app">
|
|
577
807
|
<aside class="sidebar" id="sidebar">
|
|
578
808
|
<div class="brand"><span class="mark">NR</span><div><strong>NordRelay</strong><small>Remote control</small></div></div>
|
|
579
809
|
<nav>
|
|
580
|
-
|
|
581
|
-
<button data-page="chat">Chat</button>
|
|
582
|
-
<button data-page="sessions">Sessions</button>
|
|
583
|
-
<button data-page="queue">Queue</button>
|
|
584
|
-
<button data-page="activity">Activity</button>
|
|
585
|
-
<button data-page="artifacts">Artifacts</button>
|
|
586
|
-
<button data-page="settings">Settings</button>
|
|
587
|
-
<button data-page="logs">Logs</button>
|
|
588
|
-
<button data-page="diagnostics">Diagnostics</button>
|
|
810
|
+
${renderDashboardNav()}
|
|
589
811
|
</nav>
|
|
590
812
|
</aside>
|
|
591
813
|
<main>
|
|
@@ -596,6 +818,7 @@ function renderDashboardApp(options) {
|
|
|
596
818
|
<p id="sessionLine">Loading session...</p>
|
|
597
819
|
</div>
|
|
598
820
|
<div class="header-actions">
|
|
821
|
+
<span id="connectionStatus" class="badge">Connecting</span>
|
|
599
822
|
<select id="agentSelect"></select>
|
|
600
823
|
<button id="themeBtn" class="secondary" title="Toggle dark theme">Dark</button>
|
|
601
824
|
<button id="refreshBtn">Refresh</button>
|
|
@@ -606,7 +829,10 @@ function renderDashboardApp(options) {
|
|
|
606
829
|
<div class="metrics" id="metrics"></div>
|
|
607
830
|
<div class="stack">
|
|
608
831
|
<div class="panel"><h2>Current Session</h2><pre id="sessionText"></pre></div>
|
|
609
|
-
<div class="
|
|
832
|
+
<div class="overview-adapter-grid">
|
|
833
|
+
<div class="panel"><h2>Agent Adapters</h2><div id="agentAdapters"></div></div>
|
|
834
|
+
<div class="panel"><h2>Chat Adapters</h2><div id="chatAdapters"></div></div>
|
|
835
|
+
</div>
|
|
610
836
|
</div>
|
|
611
837
|
</section>
|
|
612
838
|
|
|
@@ -615,6 +841,10 @@ function renderDashboardApp(options) {
|
|
|
615
841
|
<div class="panel chat-panel">
|
|
616
842
|
<div class="chat-toolbar">
|
|
617
843
|
<button id="newSessionBtn">New session</button>
|
|
844
|
+
<button id="retryBtn" class="secondary">Retry</button>
|
|
845
|
+
<button id="editLastBtn" class="secondary">Edit last</button>
|
|
846
|
+
<button id="syncBtn" class="secondary">Sync</button>
|
|
847
|
+
<button id="notifyBtn" class="secondary">Notify</button>
|
|
618
848
|
<button id="clearChatBtn" class="secondary">Clear history</button>
|
|
619
849
|
<button id="abortBtn">Abort</button>
|
|
620
850
|
<button id="handbackBtn">Handback</button>
|
|
@@ -627,6 +857,7 @@ function renderDashboardApp(options) {
|
|
|
627
857
|
<div class="attachment-row">
|
|
628
858
|
<label class="file-button" for="fileInput">Attach files</label>
|
|
629
859
|
<input id="fileInput" type="file" multiple>
|
|
860
|
+
<button type="button" id="recordBtn" class="secondary">Record voice</button>
|
|
630
861
|
<span id="fileSummary">No files selected</span>
|
|
631
862
|
<button type="button" id="clearFilesBtn" class="secondary">Clear</button>
|
|
632
863
|
</div>
|
|
@@ -638,6 +869,13 @@ function renderDashboardApp(options) {
|
|
|
638
869
|
</div>
|
|
639
870
|
</section>
|
|
640
871
|
|
|
872
|
+
<section class="page" id="page-tasks">
|
|
873
|
+
<div class="panel">
|
|
874
|
+
<div class="row"><button id="reloadTasksBtn">Reload tasks</button></div>
|
|
875
|
+
<div id="tasksList" class="list"></div>
|
|
876
|
+
</div>
|
|
877
|
+
</section>
|
|
878
|
+
|
|
641
879
|
<section class="page" id="page-sessions">
|
|
642
880
|
<div class="panel">
|
|
643
881
|
<div class="sessions-toolbar">
|
|
@@ -658,16 +896,44 @@ function renderDashboardApp(options) {
|
|
|
658
896
|
|
|
659
897
|
<section class="page" id="page-activity">
|
|
660
898
|
<div class="panel">
|
|
661
|
-
<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="activityLimit" type="number" value="100" min="1" max="500"><button id="loadActivityBtn">Load activity</button></div>
|
|
899
|
+
<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>
|
|
662
900
|
<div id="activityList" class="list"></div>
|
|
663
901
|
</div>
|
|
664
902
|
</section>
|
|
665
903
|
|
|
666
904
|
<section class="page" id="page-artifacts">
|
|
667
905
|
<div class="panel">
|
|
668
|
-
<div class="row"><button id="reloadArtifactsBtn">Reload artifacts</button></div>
|
|
669
|
-
<div id="artifactList" class="list"></div>
|
|
906
|
+
<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>
|
|
670
907
|
<div id="artifactPreview" class="preview"></div>
|
|
908
|
+
<div id="artifactList" class="list"></div>
|
|
909
|
+
</div>
|
|
910
|
+
</section>
|
|
911
|
+
|
|
912
|
+
<section class="page" id="page-adapters">
|
|
913
|
+
<div class="panel">
|
|
914
|
+
<div class="row"><button id="reloadAdaptersBtn">Reload adapters</button></div>
|
|
915
|
+
<div id="adapterHealth" class="list"></div>
|
|
916
|
+
</div>
|
|
917
|
+
</section>
|
|
918
|
+
|
|
919
|
+
<section class="page" id="page-access">
|
|
920
|
+
<div class="panel">
|
|
921
|
+
<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>
|
|
922
|
+
<div id="accessPanel" class="settings-grid"></div>
|
|
923
|
+
<h2>Locks</h2>
|
|
924
|
+
<div id="locksList" class="list"></div>
|
|
925
|
+
<h2>Audit</h2>
|
|
926
|
+
<div class="row"><input id="auditLimit" type="number" value="50" min="1" max="200"><button id="loadAuditBtn">Load audit</button></div>
|
|
927
|
+
<div id="auditList" class="list"></div>
|
|
928
|
+
</div>
|
|
929
|
+
</section>
|
|
930
|
+
|
|
931
|
+
<section class="page" id="page-version">
|
|
932
|
+
<div class="panel">
|
|
933
|
+
<div class="row version-actions"><button id="loadVersionBtn">Check versions</button><button id="updateBtn" class="secondary">Update NordRelay</button></div>
|
|
934
|
+
<div id="versionPanel" class="list"></div>
|
|
935
|
+
<h2 class="version-update-title">Agent update jobs</h2>
|
|
936
|
+
<div id="agentUpdateJobs" class="list"></div>
|
|
671
937
|
</div>
|
|
672
938
|
</section>
|
|
673
939
|
|
|
@@ -681,8 +947,8 @@ function renderDashboardApp(options) {
|
|
|
681
947
|
|
|
682
948
|
<section class="page" id="page-logs">
|
|
683
949
|
<div class="panel">
|
|
684
|
-
<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="logLines" type="number" value="120" min="1" max="300"><label class="checkbox"><input id="logAutoRefresh" type="checkbox"> Auto</label><button id="loadLogsBtn">Load logs</button><button id="downloadLogsBtn" class="secondary">Download</button></div>
|
|
685
|
-
<pre id="logs"></pre>
|
|
950
|
+
<div class="row"><select id="logTarget"><option value="connector">Connector</option><option value="update">NordRelay Update</option><option value="agent-updates">Agent Updates</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><button id="clearLogsBtn" class="danger">Clear</button></div>
|
|
951
|
+
<pre id="logs" class="log-view"></pre>
|
|
686
952
|
</div>
|
|
687
953
|
</section>
|
|
688
954
|
|
|
@@ -712,179 +978,13 @@ function renderDashboardApp(options) {
|
|
|
712
978
|
<div class="row dialog-actions"><button type="button" id="cancelSessionBtn" class="secondary">Cancel</button><button id="createSessionBtn" value="default">Create session</button></div>
|
|
713
979
|
</form>
|
|
714
980
|
</dialog>
|
|
981
|
+
<dialog id="sessionDetailDialog">
|
|
982
|
+
<div id="sessionDetail"></div>
|
|
983
|
+
<div class="row dialog-actions"><button id="closeSessionDetailBtn" class="secondary">Close</button></div>
|
|
984
|
+
</dialog>
|
|
985
|
+
<div id="toolTooltip" class="tool-tooltip"></div>
|
|
715
986
|
<div id="toast"></div>
|
|
716
|
-
<script
|
|
987
|
+
<script src="/assets/dashboard.js"></script>
|
|
717
988
|
</body>
|
|
718
989
|
</html>`;
|
|
719
990
|
}
|
|
720
|
-
function dashboardCss() {
|
|
721
|
-
return `
|
|
722
|
-
: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}
|
|
723
|
-
: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}
|
|
724
|
-
*{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{min-height:calc(100vh - 170px);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:360px;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{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:999px;padding:2px 8px;color:var(--muted);font-size:12px}.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}.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}}
|
|
725
|
-
`;
|
|
726
|
-
}
|
|
727
|
-
function dashboardJs() {
|
|
728
|
-
return `
|
|
729
|
-
const token = localStorage.getItem('nordrelayDashboardToken') || '';
|
|
730
|
-
const state = { snapshot:null, controls:null, enabledAgents:[], settings:[], currentPage:'overview', settingsGroup:null, logsPlain:'', logTimer:null, toastTimer:null, cliStatusActive:false };
|
|
731
|
-
const authHeaders = () => token ? { authorization: 'Bearer ' + token } : {};
|
|
732
|
-
async function api(path, options={}) {
|
|
733
|
-
const headers = { ...(options.body ? {'content-type':'application/json'} : {}), ...authHeaders(), ...(options.headers||{}) };
|
|
734
|
-
const res = await fetch(path, { ...options, headers });
|
|
735
|
-
if (res.status === 401) { location.reload(); return; }
|
|
736
|
-
const text = await res.text();
|
|
737
|
-
const data = text ? JSON.parse(text) : {};
|
|
738
|
-
if (!res.ok) throw new Error(data.error || res.statusText);
|
|
739
|
-
return data;
|
|
740
|
-
}
|
|
741
|
-
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)}}
|
|
742
|
-
function esc(s){return String(s??'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]))}
|
|
743
|
-
function attr(s){return esc(s).replace(/"/g,'"')}
|
|
744
|
-
function cssEscape(s){return window.CSS&&CSS.escape?CSS.escape(s):String(s).replace(/[^a-zA-Z0-9_-]/g,'\\\\$&')}
|
|
745
|
-
function short(s,max=250){const text=String(s??'');return text.length>max?text.slice(0,max-1)+'...':text}
|
|
746
|
-
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('Thread ID copied')}
|
|
747
|
-
function fmtDate(s){return s?new Date(s).toLocaleString(): '-'}
|
|
748
|
-
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'}
|
|
749
|
-
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'}
|
|
750
|
-
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'}
|
|
751
|
-
function isCliRunningStatus(msg){return /^Codex CLI running\\b/.test(String(msg||''))}
|
|
752
|
-
function isCliDoneStatus(msg){return /^Codex CLI task\\b/.test(String(msg||''))}
|
|
753
|
-
function applyTheme(theme){document.documentElement.dataset.theme=theme;localStorage.setItem('nordrelayTheme',theme);document.getElementById('themeBtn').textContent=theme==='dark'?'Light':'Dark'}
|
|
754
|
-
function toggleTheme(){applyTheme(document.documentElement.dataset.theme==='dark'?'light':'dark')}
|
|
755
|
-
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();}
|
|
756
|
-
document.querySelectorAll('nav button').forEach(b=>b.onclick=()=>page(b.dataset.page));
|
|
757
|
-
document.getElementById('menuBtn').onclick=()=>document.getElementById('sidebar').classList.toggle('open');
|
|
758
|
-
document.getElementById('refreshBtn').onclick=()=>loadBootstrap();
|
|
759
|
-
document.getElementById('themeBtn').onclick=toggleTheme;
|
|
760
|
-
applyTheme(localStorage.getItem('nordrelayTheme') || 'light');
|
|
761
|
-
|
|
762
|
-
function createPaginator(containerId, onChange, pageSize=50){
|
|
763
|
-
const container=document.getElementById(containerId);
|
|
764
|
-
return {
|
|
765
|
-
page:1,
|
|
766
|
-
pageSize,
|
|
767
|
-
reset(){this.page=1},
|
|
768
|
-
render(meta={}){
|
|
769
|
-
const hasPrevious=Boolean(meta.hasPrevious);
|
|
770
|
-
const hasNext=Boolean(meta.hasNext);
|
|
771
|
-
container.innerHTML='<span>Page '+this.page+' / '+this.pageSize+' per page</span><div class="pager-actions"><button data-page-action="prev" '+(!hasPrevious?'disabled':'')+'>Previous</button><button data-page-action="next" '+(!hasNext?'disabled':'')+'>Next</button></div>';
|
|
772
|
-
const prev=container.querySelector('[data-page-action="prev"]');
|
|
773
|
-
const next=container.querySelector('[data-page-action="next"]');
|
|
774
|
-
prev.onclick=()=>{if(hasPrevious){this.page-=1;onChange()}};
|
|
775
|
-
next.onclick=()=>{if(hasNext){this.page+=1;onChange()}};
|
|
776
|
-
}
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
const sessionsPager=createPaginator('sessionsPager',()=>loadSessions(false),50);
|
|
780
|
-
|
|
781
|
-
async function loadBootstrap(){
|
|
782
|
-
const data = await api('/api/bootstrap');
|
|
783
|
-
state.snapshot = data.status.snapshot;
|
|
784
|
-
state.controls = data.controls;
|
|
785
|
-
state.enabledAgents = data.enabledAgents || [];
|
|
786
|
-
renderSnapshot(state.snapshot);
|
|
787
|
-
renderSessionControls();
|
|
788
|
-
populateNewSessionForm(data.enabledAgents);
|
|
789
|
-
renderAdapters(data.channels, data.agentAdapters);
|
|
790
|
-
document.getElementById('footerVersion').textContent='NordRelay '+(data.status.health?.version || '');
|
|
791
|
-
document.getElementById('footerHealth').textContent='Health: '+(data.status.health?.state?.status || 'unknown');
|
|
792
|
-
const agentSelect=document.getElementById('agentSelect');
|
|
793
|
-
agentSelect.innerHTML=data.enabledAgents.map(a=>'<option value="'+a+'">'+a+'</option>').join('');
|
|
794
|
-
agentSelect.value=state.snapshot.session.agentId;
|
|
795
|
-
agentSelect.onchange=()=>safe(async()=>{await api('/api/agent',{method:'POST',body:JSON.stringify({agentId:agentSelect.value})});toast('Agent switched');await loadBootstrap();await loadChatHistory()});
|
|
796
|
-
}
|
|
797
|
-
function renderSnapshot(s){
|
|
798
|
-
document.getElementById('sessionLine').textContent=(s.session.agentLabel||'Agent')+' / '+(s.session.model||'default')+' / '+(s.session.threadId||'not started');
|
|
799
|
-
document.getElementById('sessionText').textContent=s.sessionText||'';
|
|
800
|
-
document.getElementById('metrics').innerHTML=[
|
|
801
|
-
['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.fastMode?'on':'off']
|
|
802
|
-
].map(([k,v])=>'<div class="metric"><div class="label">'+esc(k)+'</div><div class="value">'+esc(v)+'</div></div>').join('');
|
|
803
|
-
renderQueue(s.queue,s.queuePaused);
|
|
804
|
-
}
|
|
805
|
-
function renderSessionControls(){
|
|
806
|
-
const c=state.controls||{};const s=state.snapshot?.session||{};const caps=c.capabilities||{};
|
|
807
|
-
const modelOptions=['<option value="">Default</option>'].concat((c.models||[]).map(m=>'<option value="'+attr(m.slug)+'" '+(m.slug===s.model?'selected':'')+'>'+esc(m.displayName||m.slug)+'</option>')).join('');
|
|
808
|
-
const reasoningOptions=(c.reasoningOptions||[]).map(v=>'<option value="'+attr(v)+'" '+(v===s.reasoningEffort?'selected':'')+'>'+esc(v)+'</option>').join('');
|
|
809
|
-
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('');
|
|
810
|
-
document.getElementById('sessionControls').innerHTML=[
|
|
811
|
-
caps.modelSelection?'<label>Model<select id="controlModel">'+modelOptions+'</select></label>':'',
|
|
812
|
-
caps.reasoningSelection?'<label>'+esc(c.reasoningLabel||'Reasoning')+'<select id="controlReasoning">'+reasoningOptions+'</select></label>':'',
|
|
813
|
-
caps.launchProfiles?'<label>Launch<select id="controlLaunch">'+launchOptions+'</select></label>':'',
|
|
814
|
-
caps.fastMode?'<label class="checkbox"><input id="controlFast" type="checkbox" '+(s.fastMode?'checked':'')+'> Fast mode</label>':''
|
|
815
|
-
].join('');
|
|
816
|
-
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()}});
|
|
817
|
-
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()});
|
|
818
|
-
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()});
|
|
819
|
-
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()});
|
|
820
|
-
}
|
|
821
|
-
function renderAdapters(channels, agents){
|
|
822
|
-
document.getElementById('adapters').innerHTML='<div class="list">'+[...channels.map(c=>'<div class="item"><strong>'+esc(c.label)+' - '+esc(c.status)+'</strong><small>'+esc(c.capabilities.join(', '))+'</small></div>'),...agents.map(a=>'<div class="item"><strong>'+esc(a.label)+' - '+esc(a.status)+'</strong><small>'+esc(a.notes||a.envFlag||'available')+'</small></div>')].join('')+'</div>';
|
|
823
|
-
}
|
|
824
|
-
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}
|
|
825
|
-
function renderChatMessages(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}
|
|
826
|
-
async function loadChatHistory(){const data=await api('/api/chat/history');renderChatMessages(data.messages||[])}
|
|
827
|
-
let currentAgentMessage=null;
|
|
828
|
-
function connectEvents(){
|
|
829
|
-
const qs = token ? '?token='+encodeURIComponent(token) : '';
|
|
830
|
-
const events = new EventSource('/api/events'+qs);
|
|
831
|
-
events.addEventListener('snapshot', e=>{const d=JSON.parse(e.data).data;state.snapshot=d;renderSnapshot(d);renderSessionControls()});
|
|
832
|
-
events.addEventListener('chat_history', e=>renderChatMessages(JSON.parse(e.data).messages||[]));
|
|
833
|
-
events.addEventListener('activity_update', e=>renderActivity(JSON.parse(e.data).events||[]));
|
|
834
|
-
events.addEventListener('session_update', e=>{loadBootstrap();loadChatHistory()});
|
|
835
|
-
events.addEventListener('queue_update', e=>{const d=JSON.parse(e.data);renderQueue(d.queue,d.paused)});
|
|
836
|
-
events.addEventListener('turn_start', e=>{const d=JSON.parse(e.data);appendMessage('user',d.prompt);currentAgentMessage=appendMessage('agent','')});
|
|
837
|
-
events.addEventListener('text_delta', e=>{const d=JSON.parse(e.data);if(!currentAgentMessage)currentAgentMessage=appendMessage('agent','');currentAgentMessage.textContent+=d.delta;currentAgentMessage.scrollIntoView({block:'end'})});
|
|
838
|
-
events.addEventListener('tool_start', e=>{const d=JSON.parse(e.data);tool('tool','Started '+d.toolName)});
|
|
839
|
-
events.addEventListener('tool_update', e=>{const d=JSON.parse(e.data);if(d.partialResult)tool('tool',d.partialResult.slice(-600))});
|
|
840
|
-
events.addEventListener('tool_end', e=>{const d=JSON.parse(e.data);tool(d.isError?'danger':'tool','Finished '+d.toolCallId+(d.isError?' with error':''))});
|
|
841
|
-
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'))});
|
|
842
|
-
events.addEventListener('turn_error', e=>{const d=JSON.parse(e.data);appendMessage('system','Error: '+d.error);currentAgentMessage=null});
|
|
843
|
-
events.addEventListener('turn_complete', ()=>{currentAgentMessage=null;loadBootstrap()});
|
|
844
|
-
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)});
|
|
845
|
-
events.onerror=()=>{};
|
|
846
|
-
}
|
|
847
|
-
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)})}
|
|
848
|
-
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()}
|
|
849
|
-
setInterval(updateToolAgeTitles,30000);
|
|
850
|
-
let selectedFiles=[];
|
|
851
|
-
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)+')'}
|
|
852
|
-
async function filePayload(file){return {name:file.name || 'upload',mimeType:file.type || 'application/octet-stream',dataBase64:await fileToBase64(file)}}
|
|
853
|
-
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)}
|
|
854
|
-
document.getElementById('fileInput').onchange=e=>{selectedFiles=Array.from(e.target.files||[]);renderSelectedFiles()};
|
|
855
|
-
document.getElementById('clearFilesBtn').onclick=()=>{selectedFiles=[];document.getElementById('fileInput').value='';renderSelectedFiles()};
|
|
856
|
-
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)appendMessage('system','Queued prompt '+r.queueId)},e);
|
|
857
|
-
document.getElementById('newSessionBtn').onclick=()=>openNewSessionDialog();
|
|
858
|
-
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')}});
|
|
859
|
-
document.getElementById('abortBtn').onclick=()=>safe(async()=>{await api('/api/abort',{method:'POST'});toast('Abort sent')});
|
|
860
|
-
document.getElementById('handbackBtn').onclick=()=>safe(async()=>{const r=await api('/api/handback',{method:'POST'});appendMessage('system','Handback command:\\n'+(r.command||'No command available'))});
|
|
861
|
-
function populateNewSessionForm(agents){const c=state.controls||{};const s=state.snapshot?.session||{};document.getElementById('newAgent').innerHTML=(agents||[]).map(a=>'<option value="'+attr(a)+'" '+(a===s.agentId?'selected':'')+'>'+esc(a)+'</option>').join('');document.getElementById('newWorkspace').value=s.workspace||'';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(m.displayName||m.slug)+'</option>').join(''));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);document.getElementById('newLaunchWrap').style.display=(c.capabilities&&c.capabilities.launchProfiles)?'grid':'none';document.getElementById('newFastWrap').style.display=(c.capabilities&&c.capabilities.fastMode)?'inline-flex':'none'}
|
|
862
|
-
function openNewSessionDialog(){populateNewSessionForm(state.enabledAgents);document.getElementById('newSessionDialog').showModal()}
|
|
863
|
-
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);
|
|
864
|
-
document.getElementById('cancelSessionBtn').onclick=()=>document.getElementById('newSessionDialog').close();
|
|
865
|
-
function val(id){return document.getElementById(id).value.trim()}
|
|
866
|
-
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></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||''));document.querySelectorAll('[data-switch]').forEach(b=>b.onclick=async()=>{await api('/api/sessions/switch',{method:'POST',body:JSON.stringify({threadId:b.dataset.switch})});toast('Session switched');loadBootstrap()})}
|
|
867
|
-
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()}};
|
|
868
|
-
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)}})})}
|
|
869
|
-
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)}));
|
|
870
|
-
async function loadArtifacts(){const data=await api('/api/artifacts');document.getElementById('artifactList').innerHTML=data.reports.map(r=>'<div class="item"><strong>'+esc(r.turnId)+' - '+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>'+r.artifacts.slice(0,12).map(a=>'<small><a href="/api/artifacts/file?turnId='+encodeURIComponent(r.turnId)+'&path='+encodeURIComponent(a.relativePath)+(token?'&token='+encodeURIComponent(token):'')+'">'+esc(a.name)+'</a> '+fmtBytes(a.sizeBytes)+' <button class="secondary" data-preview-turn="'+attr(r.turnId)+'" data-preview-path="'+attr(a.relativePath)+'">Preview</button></small>').join('')+'</div>').join('')||'<div class="item">No artifacts.</div>';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'});loadArtifacts()}}));document.querySelectorAll('[data-preview-turn]').forEach(b=>b.onclick=()=>previewArtifact(b.dataset.previewTurn,b.dataset.previewPath))}
|
|
871
|
-
document.getElementById('reloadArtifactsBtn').onclick=loadArtifacts;
|
|
872
|
-
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>'+esc(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>'}
|
|
873
|
-
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);renderActivity(data.events||[])}
|
|
874
|
-
function renderActivity(events){document.getElementById('activityList').innerHTML=(events||[]).map(e=>'<div class="item"><strong>'+esc(fmtDate(e.timestamp)+' / '+e.source+' / '+e.status+' / '+e.type)+'</strong><small>'+esc(short(e.prompt||e.detail||'',220))+'</small><small>'+esc((e.threadId||'-')+' / '+(e.workspace||'-')+' / '+fmtDuration(e.durationMs))+'</small></div>').join('')||'<div class="item">No activity.</div>'}
|
|
875
|
-
document.getElementById('loadActivityBtn').onclick=()=>loadActivity();
|
|
876
|
-
async function loadSettings(){const data=await api('/api/settings');state.settings=data.settings;renderSettings()}
|
|
877
|
-
function renderSettings(){const groups={};state.settings.forEach(s=>(groups[s.group]??=[]).push(s));const names=Object.keys(groups);if(!state.settingsGroup||!groups[state.settingsGroup])state.settingsGroup=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]||[];document.getElementById('settingsForm').innerHTML='<div class="settings-section"><h2>'+esc(state.settingsGroup||'Settings')+'</h2>'+items.map(s=>'<div class="setting" data-setting-box="'+attr(s.key)+'"><label>'+esc(s.label)+'</label>'+settingInput(s)+'<small>'+esc(s.key)+' - '+esc(s.description)+(s.restartRequired?' Restart required.':'')+(s.configured?' Configured.':' Inherited/default.')+'</small><div class="setting-error"></div></div>').join('')+'</div>'}
|
|
878
|
-
function settingInput(s){const value=esc(s.value||''); if(s.options)return '<select data-setting="'+s.key+'"><option value=""></option>'+s.options.map(o=>'<option value="'+attr(o)+'" '+(s.value===o?'selected':'')+'>'+esc(o)+'</option>').join('')+'</select>'; if(s.kind==='boolean')return '<select data-setting="'+s.key+'"><option value=""></option><option value="true" '+(s.value==='true'?'selected':'')+'>true</option><option value="false" '+(s.value==='false'?'selected':'')+'>false</option></select>'; if(s.kind==='json')return '<textarea rows="4" data-setting="'+s.key+'">'+value+'</textarea>'; return '<input data-setting="'+s.key+'" value="'+value+'" '+(s.kind==='secret'?'type="password"':'')+'>'}
|
|
879
|
-
document.getElementById('saveSettingsBtn').onclick=()=>safe(async()=>{document.querySelectorAll('.setting-error').forEach(e=>e.textContent='');const patch={};document.querySelectorAll('[data-setting]').forEach(el=>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')});
|
|
880
|
-
document.getElementById('restartBtn').onclick=()=>safe(async()=>{if(confirm('Restart NordRelay now?')){await api('/api/runtime/restart',{method:'POST'});toast('Restart requested')}});
|
|
881
|
-
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()}document.getElementById('loadLogsBtn').onclick=loadLogs;
|
|
882
|
-
function renderLogs(){const level=val('logLevel');const query=val('logSearch').toLowerCase();const lines=state.logsPlain.split(/\\n/).filter(line=>(level==='all'||line.includes(level))&&(!query||line.toLowerCase().includes(query)));document.getElementById('logs').textContent=lines.join('\\n')||'(empty)'}
|
|
883
|
-
document.getElementById('logLevel').onchange=renderLogs;document.getElementById('logSearch').oninput=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([document.getElementById('logs').textContent||''],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='nordrelay-log.txt';a.click();URL.revokeObjectURL(a.href)};
|
|
884
|
-
async function loadDiagnostics(){const data=await api('/api/diagnostics');document.getElementById('diagnostics').innerHTML=diagnosticsHtml(data)}
|
|
885
|
-
function diagnosticsHtml(d){const h=d.health||{};const s=d.snapshot?.session||{};const vc=d.versionChecks||{};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',s.fastMode?'on':'off']])+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>'}
|
|
886
|
-
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>'}
|
|
887
|
-
function safe(fn,event){if(event&&event.preventDefault)event.preventDefault();Promise.resolve().then(fn).catch(err=>toast(err.message||String(err)))}
|
|
888
|
-
loadBootstrap().then(()=>{connectEvents();loadChatHistory();loadSessions();loadArtifacts();loadSettings();loadLogs();loadDiagnostics();loadActivity()}).catch(err=>toast(err.message));
|
|
889
|
-
`;
|
|
890
|
-
}
|