@nordbyte/nordrelay 0.3.1 → 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 +204 -30
- 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 +328 -159
- 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 +83 -2
- 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 +673 -51
- package/dist/session-format.js +28 -18
- package/dist/session-registry.js +40 -15
- package/dist/settings-service.js +35 -4
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +329 -47
- 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 +131 -3
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
- package/CHANGELOG.md +0 -26
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
|
+
import { renderDashboardNav } from "./web-dashboard-ui.js";
|
|
15
16
|
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
16
|
-
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
|
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)) {
|
|
@@ -87,15 +88,66 @@ async function handleApi(req, res, url) {
|
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
if (req.method === "GET" && url.pathname === "/api/control-options") {
|
|
90
|
-
sendJson(res, 200, await runtime.controlOptions());
|
|
91
|
+
sendJson(res, 200, await runtime.controlOptions(parseAgentId(url.searchParams.get("agent") ?? undefined)));
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
if (req.method === "GET" && url.pathname === "/api/health") {
|
|
94
95
|
sendJson(res, 200, await runtime.status());
|
|
95
96
|
return;
|
|
96
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
|
+
}
|
|
97
149
|
if (req.method === "GET" && url.pathname === "/api/settings") {
|
|
98
|
-
sendJson(res, 200, await settings.snapshot());
|
|
150
|
+
sendJson(res, 200, await settings.snapshot(process.env, activeSettingsValues(config)));
|
|
99
151
|
return;
|
|
100
152
|
}
|
|
101
153
|
if (req.method === "PATCH" && url.pathname === "/api/settings") {
|
|
@@ -144,6 +196,10 @@ async function handleApi(req, res, url) {
|
|
|
144
196
|
sendJson(res, 200, { session: await runtime.attachSession(stringField(body, "threadId")) });
|
|
145
197
|
return;
|
|
146
198
|
}
|
|
199
|
+
if (req.method === "GET" && url.pathname === "/api/sessions/detail") {
|
|
200
|
+
sendJson(res, 200, await runtime.sessionDetail(requiredSearch(url, "threadId")));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
147
203
|
if (req.method === "GET" && url.pathname === "/api/models") {
|
|
148
204
|
sendJson(res, 200, { models: await runtime.listModels() });
|
|
149
205
|
return;
|
|
@@ -181,7 +237,7 @@ async function handleApi(req, res, url) {
|
|
|
181
237
|
}));
|
|
182
238
|
return;
|
|
183
239
|
}
|
|
184
|
-
if (req.method === "POST" && url.pathname === "/api/abort") {
|
|
240
|
+
if (req.method === "POST" && (url.pathname === "/api/abort" || url.pathname === "/api/stop")) {
|
|
185
241
|
await runtime.abort();
|
|
186
242
|
sendJson(res, 200, { ok: true });
|
|
187
243
|
return;
|
|
@@ -190,6 +246,14 @@ async function handleApi(req, res, url) {
|
|
|
190
246
|
sendJson(res, 200, await runtime.handback());
|
|
191
247
|
return;
|
|
192
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
|
+
}
|
|
193
257
|
if (req.method === "GET" && url.pathname === "/api/queue") {
|
|
194
258
|
sendJson(res, 200, { queue: runtime.queue(), paused: runtime.queuePaused() });
|
|
195
259
|
return;
|
|
@@ -225,6 +289,22 @@ async function handleApi(req, res, url) {
|
|
|
225
289
|
sendJson(res, 200, { removed: await runtime.deleteArtifact(requiredSearch(url, "turnId")) });
|
|
226
290
|
return;
|
|
227
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
|
+
}
|
|
228
308
|
if (req.method === "GET" && url.pathname === "/api/artifacts/zip") {
|
|
229
309
|
const bundle = await runtime.createArtifactZip(requiredSearch(url, "turnId"));
|
|
230
310
|
if (!bundle) {
|
|
@@ -427,7 +507,7 @@ function sendJson(res, status, value) {
|
|
|
427
507
|
res.end(`${JSON.stringify(value)}\n`);
|
|
428
508
|
}
|
|
429
509
|
function sendText(res, status, text, contentType) {
|
|
430
|
-
res.writeHead(status, { "content-type": contentType });
|
|
510
|
+
res.writeHead(status, { "content-type": contentType, "cache-control": "no-store" });
|
|
431
511
|
res.end(text);
|
|
432
512
|
}
|
|
433
513
|
function sendFile(res, filePath, filename) {
|
|
@@ -504,6 +584,110 @@ function optionalEnv(key) {
|
|
|
504
584
|
const value = process.env[key]?.trim();
|
|
505
585
|
return value || undefined;
|
|
506
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
|
+
}
|
|
507
691
|
function requireArg(argv, index, flag) {
|
|
508
692
|
const value = argv[index];
|
|
509
693
|
if (!value || value.startsWith("--")) {
|
|
@@ -577,15 +761,7 @@ function renderDashboardApp(options) {
|
|
|
577
761
|
<aside class="sidebar" id="sidebar">
|
|
578
762
|
<div class="brand"><span class="mark">NR</span><div><strong>NordRelay</strong><small>Remote control</small></div></div>
|
|
579
763
|
<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>
|
|
764
|
+
${renderDashboardNav()}
|
|
589
765
|
</nav>
|
|
590
766
|
</aside>
|
|
591
767
|
<main>
|
|
@@ -596,6 +772,7 @@ function renderDashboardApp(options) {
|
|
|
596
772
|
<p id="sessionLine">Loading session...</p>
|
|
597
773
|
</div>
|
|
598
774
|
<div class="header-actions">
|
|
775
|
+
<span id="connectionStatus" class="badge">Connecting</span>
|
|
599
776
|
<select id="agentSelect"></select>
|
|
600
777
|
<button id="themeBtn" class="secondary" title="Toggle dark theme">Dark</button>
|
|
601
778
|
<button id="refreshBtn">Refresh</button>
|
|
@@ -615,6 +792,10 @@ function renderDashboardApp(options) {
|
|
|
615
792
|
<div class="panel chat-panel">
|
|
616
793
|
<div class="chat-toolbar">
|
|
617
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>
|
|
618
799
|
<button id="clearChatBtn" class="secondary">Clear history</button>
|
|
619
800
|
<button id="abortBtn">Abort</button>
|
|
620
801
|
<button id="handbackBtn">Handback</button>
|
|
@@ -627,6 +808,7 @@ function renderDashboardApp(options) {
|
|
|
627
808
|
<div class="attachment-row">
|
|
628
809
|
<label class="file-button" for="fileInput">Attach files</label>
|
|
629
810
|
<input id="fileInput" type="file" multiple>
|
|
811
|
+
<button type="button" id="recordBtn" class="secondary">Record voice</button>
|
|
630
812
|
<span id="fileSummary">No files selected</span>
|
|
631
813
|
<button type="button" id="clearFilesBtn" class="secondary">Clear</button>
|
|
632
814
|
</div>
|
|
@@ -638,6 +820,13 @@ function renderDashboardApp(options) {
|
|
|
638
820
|
</div>
|
|
639
821
|
</section>
|
|
640
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
|
+
|
|
641
830
|
<section class="page" id="page-sessions">
|
|
642
831
|
<div class="panel">
|
|
643
832
|
<div class="sessions-toolbar">
|
|
@@ -658,19 +847,45 @@ function renderDashboardApp(options) {
|
|
|
658
847
|
|
|
659
848
|
<section class="page" id="page-activity">
|
|
660
849
|
<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>
|
|
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>
|
|
662
851
|
<div id="activityList" class="list"></div>
|
|
663
852
|
</div>
|
|
664
853
|
</section>
|
|
665
854
|
|
|
666
855
|
<section class="page" id="page-artifacts">
|
|
667
856
|
<div class="panel">
|
|
668
|
-
<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>
|
|
669
858
|
<div id="artifactList" class="list"></div>
|
|
670
859
|
<div id="artifactPreview" class="preview"></div>
|
|
671
860
|
</div>
|
|
672
861
|
</section>
|
|
673
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>
|
|
886
|
+
</div>
|
|
887
|
+
</section>
|
|
888
|
+
|
|
674
889
|
<section class="page" id="page-settings">
|
|
675
890
|
<div class="panel">
|
|
676
891
|
<div class="row"><button id="saveSettingsBtn">Save settings</button><button id="restartBtn" class="secondary">Restart NordRelay</button><span id="settingsStatus"></span></div>
|
|
@@ -681,7 +896,7 @@ function renderDashboardApp(options) {
|
|
|
681
896
|
|
|
682
897
|
<section class="page" id="page-logs">
|
|
683
898
|
<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>
|
|
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>
|
|
685
900
|
<pre id="logs"></pre>
|
|
686
901
|
</div>
|
|
687
902
|
</section>
|
|
@@ -712,6 +927,10 @@ function renderDashboardApp(options) {
|
|
|
712
927
|
<div class="row dialog-actions"><button type="button" id="cancelSessionBtn" class="secondary">Cancel</button><button id="createSessionBtn" value="default">Create session</button></div>
|
|
713
928
|
</form>
|
|
714
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>
|
|
715
934
|
<div id="toast"></div>
|
|
716
935
|
<script>${dashboardJs()}</script>
|
|
717
936
|
</body>
|
|
@@ -721,13 +940,15 @@ function dashboardCss() {
|
|
|
721
940
|
return `
|
|
722
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}
|
|
723
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}
|
|
724
|
-
|
|
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}}
|
|
725
946
|
`;
|
|
726
947
|
}
|
|
727
948
|
function dashboardJs() {
|
|
728
949
|
return `
|
|
729
950
|
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 };
|
|
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 };
|
|
731
952
|
const authHeaders = () => token ? { authorization: 'Bearer ' + token } : {};
|
|
732
953
|
async function api(path, options={}) {
|
|
733
954
|
const headers = { ...(options.body ? {'content-type':'application/json'} : {}), ...authHeaders(), ...(options.headers||{}) };
|
|
@@ -743,16 +964,18 @@ function esc(s){return String(s??'').replace(/[&<>]/g,c=>({'&':'&','<':'<
|
|
|
743
964
|
function attr(s){return esc(s).replace(/"/g,'"')}
|
|
744
965
|
function cssEscape(s){return window.CSS&&CSS.escape?CSS.escape(s):String(s).replace(/[^a-zA-Z0-9_-]/g,'\\\\$&')}
|
|
745
966
|
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(
|
|
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)}
|
|
747
968
|
function fmtDate(s){return s?new Date(s).toLocaleString(): '-'}
|
|
748
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'}
|
|
749
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:'')}
|
|
750
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'}
|
|
751
|
-
function isCliRunningStatus(msg){return
|
|
752
|
-
function isCliDoneStatus(msg){return
|
|
974
|
+
function isCliRunningStatus(msg){return / CLI running\\b/.test(String(msg||''))}
|
|
975
|
+
function isCliDoneStatus(msg){return / CLI task\\b/.test(String(msg||''))}
|
|
753
976
|
function applyTheme(theme){document.documentElement.dataset.theme=theme;localStorage.setItem('nordrelayTheme',theme);document.getElementById('themeBtn').textContent=theme==='dark'?'Light':'Dark'}
|
|
754
977
|
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();}
|
|
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();}
|
|
756
979
|
document.querySelectorAll('nav button').forEach(b=>b.onclick=()=>page(b.dataset.page));
|
|
757
980
|
document.getElementById('menuBtn').onclick=()=>document.getElementById('sidebar').classList.toggle('open');
|
|
758
981
|
document.getElementById('refreshBtn').onclick=()=>loadBootstrap();
|
|
@@ -798,13 +1021,13 @@ function renderSnapshot(s){
|
|
|
798
1021
|
document.getElementById('sessionLine').textContent=(s.session.agentLabel||'Agent')+' / '+(s.session.model||'default')+' / '+(s.session.threadId||'not started');
|
|
799
1022
|
document.getElementById('sessionText').textContent=s.sessionText||'';
|
|
800
1023
|
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']
|
|
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']
|
|
802
1025
|
].map(([k,v])=>'<div class="metric"><div class="label">'+esc(k)+'</div><div class="value">'+esc(v)+'</div></div>').join('');
|
|
803
1026
|
renderQueue(s.queue,s.queuePaused);
|
|
804
1027
|
}
|
|
805
1028
|
function renderSessionControls(){
|
|
806
1029
|
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
|
|
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('');
|
|
808
1031
|
const reasoningOptions=(c.reasoningOptions||[]).map(v=>'<option value="'+attr(v)+'" '+(v===s.reasoningEffort?'selected':'')+'>'+esc(v)+'</option>').join('');
|
|
809
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('');
|
|
810
1033
|
document.getElementById('sessionControls').innerHTML=[
|
|
@@ -819,70 +1042,129 @@ function renderSessionControls(){
|
|
|
819
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()});
|
|
820
1043
|
}
|
|
821
1044
|
function renderAdapters(channels, agents){
|
|
822
|
-
|
|
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>';
|
|
823
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>'}
|
|
824
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}
|
|
825
|
-
function
|
|
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}
|
|
826
1053
|
async function loadChatHistory(){const data=await api('/api/chat/history');renderChatMessages(data.messages||[])}
|
|
827
1054
|
let currentAgentMessage=null;
|
|
828
1055
|
function connectEvents(){
|
|
1056
|
+
if(state.events) state.events.close();
|
|
829
1057
|
const qs = token ? '?token='+encodeURIComponent(token) : '';
|
|
830
1058
|
const events = new EventSource('/api/events'+qs);
|
|
1059
|
+
state.events=events;
|
|
1060
|
+
setConnection('Connecting','warn');
|
|
1061
|
+
events.onopen=()=>{if(state.reconnectTimer){clearTimeout(state.reconnectTimer);state.reconnectTimer=null}setConnection('Live','ok')};
|
|
831
1062
|
events.addEventListener('snapshot', e=>{const d=JSON.parse(e.data).data;state.snapshot=d;renderSnapshot(d);renderSessionControls()});
|
|
832
1063
|
events.addEventListener('chat_history', e=>renderChatMessages(JSON.parse(e.data).messages||[]));
|
|
833
1064
|
events.addEventListener('activity_update', e=>renderActivity(JSON.parse(e.data).events||[]));
|
|
834
1065
|
events.addEventListener('session_update', e=>{loadBootstrap();loadChatHistory()});
|
|
835
1066
|
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)});
|
|
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()});
|
|
839
1070
|
events.addEventListener('tool_update', e=>{const d=JSON.parse(e.data);if(d.partialResult)tool('tool',d.partialResult.slice(-600))});
|
|
840
1071
|
events.addEventListener('tool_end', e=>{const d=JSON.parse(e.data);tool(d.isError?'danger':'tool','Finished '+d.toolCallId+(d.isError?' with error':''))});
|
|
841
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'))});
|
|
842
1073
|
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()});
|
|
1074
|
+
events.addEventListener('turn_complete', ()=>{currentAgentMessage=null;notify('NordRelay turn finished','The active task completed.');loadBootstrap();if(state.currentPage==='tasks')loadTasks()});
|
|
844
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)});
|
|
845
|
-
events.onerror=()=>{};
|
|
1076
|
+
events.onerror=()=>{setConnection('Reconnecting','error');if(!state.reconnectTimer)state.reconnectTimer=setTimeout(()=>{state.reconnectTimer=null;connectEvents()},5000)};
|
|
846
1077
|
}
|
|
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})}
|
|
847
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)})}
|
|
848
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()}
|
|
849
1083
|
setInterval(updateToolAgeTitles,30000);
|
|
850
1084
|
let selectedFiles=[];
|
|
851
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()}
|
|
852
1087
|
async function filePayload(file){return {name:file.name || 'upload',mimeType:file.type || 'application/octet-stream',dataBase64:await fileToBase64(file)}}
|
|
853
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)}
|
|
854
|
-
document.getElementById('fileInput').onchange=e=>{
|
|
1089
|
+
document.getElementById('fileInput').onchange=e=>{addFiles(e.target.files)};
|
|
855
1090
|
document.getElementById('clearFilesBtn').onclick=()=>{selectedFiles=[];document.getElementById('fileInput').value='';renderSelectedFiles()};
|
|
856
|
-
document.
|
|
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);
|
|
857
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();
|
|
858
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')}});
|
|
859
1102
|
document.getElementById('abortBtn').onclick=()=>safe(async()=>{await api('/api/abort',{method:'POST'});toast('Abort sent')});
|
|
860
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'))});
|
|
861
|
-
|
|
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)})}
|
|
862
1107
|
function openNewSessionDialog(){populateNewSessionForm(state.enabledAgents);document.getElementById('newSessionDialog').showModal()}
|
|
863
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);
|
|
864
1109
|
document.getElementById('cancelSessionBtn').onclick=()=>document.getElementById('newSessionDialog').close();
|
|
865
1110
|
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()})}
|
|
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();
|
|
867
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()}};
|
|
868
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)}})})}
|
|
869
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)}));
|
|
870
|
-
async function
|
|
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))}
|
|
871
1124
|
document.getElementById('reloadArtifactsBtn').onclick=loadArtifacts;
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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'))}
|
|
875
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)};
|
|
876
1136
|
async function loadSettings(){const data=await api('/api/settings');state.settings=data.settings;renderSettings()}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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()});
|
|
880
1147
|
document.getElementById('restartBtn').onclick=()=>safe(async()=>{if(confirm('Restart NordRelay now?')){await api('/api/runtime/restart',{method:'POST'});toast('Restart requested')}});
|
|
881
|
-
async function
|
|
882
|
-
|
|
883
|
-
document.getElementById('
|
|
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()}});
|
|
884
1166
|
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>'}
|
|
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>'}
|
|
886
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>'}
|
|
887
1169
|
function safe(fn,event){if(event&&event.preventDefault)event.preventDefault();Promise.resolve().then(fn).catch(err=>toast(err.message||String(err)))}
|
|
888
1170
|
loadBootstrap().then(()=>{connectEvents();loadChatHistory();loadSessions();loadArtifacts();loadSettings();loadLogs();loadDiagnostics();loadActivity()}).catch(err=>toast(err.message));
|