@nordbyte/nordrelay 0.8.2 → 0.8.3
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/README.md +4 -0
- package/dist/access/audit-log.js +30 -13
- package/dist/channels/discord/discord-bot.js +12 -27
- package/dist/channels/shared/channel-bridge-controller.js +1 -1
- package/dist/channels/shared/channel-prompt-queue.js +37 -0
- package/dist/channels/shared/channel-turn-service.js +23 -9
- package/dist/channels/slack/slack-bot.js +12 -15
- package/dist/channels/telegram/bot.js +18 -4
- package/dist/core/pagination.js +22 -0
- package/dist/peers/peer-store.js +16 -0
- package/dist/peers/peer-types.js +19 -0
- package/dist/peers/peer-web-proxy-contract.js +2 -0
- package/dist/runtime/relay-external-activity-monitor.js +15 -0
- package/dist/runtime/relay-queue-service.js +1 -0
- package/dist/runtime/relay-runtime-dashboard.js +3 -0
- package/dist/runtime/relay-runtime-helpers.js +3 -0
- package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +14 -10
- package/dist/runtime/relay-runtime-sessions.js +8 -0
- package/dist/runtime/relay-runtime-trace.js +92 -0
- package/dist/runtime/relay-runtime-updates-jobs.js +11 -5
- package/dist/runtime/relay-runtime.js +16 -6
- package/dist/state/prompt-store.js +13 -1
- package/dist/web/web-api-contract.js +2 -0
- package/dist/web/web-dashboard-access-routes.js +15 -12
- package/dist/web/web-dashboard-artifact-routes.js +6 -2
- package/dist/web/web-dashboard-assets.js +1 -0
- package/dist/web/web-dashboard-pages.js +58 -20
- package/dist/web/web-dashboard-peer-routes.js +19 -0
- package/dist/web/web-dashboard-runtime-routes.js +8 -1
- package/dist/web/web-dashboard-session-routes.js +17 -12
- package/dist/web/web-dashboard-ui.js +46 -10
- package/dist/web/web-performance.js +2 -0
- package/dist/web/web-state.js +33 -4
- package/dist/webui-assets/dashboard.css +227 -39
- package/dist/webui-assets/dashboard.js +728 -58
- package/package.json +4 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +333 -8
- package/plugins/nordrelay/scripts/service-installer.mjs +183 -0
- package/scripts/postinstall.mjs +122 -0
|
@@ -97,6 +97,7 @@ export function taskToUnifiedJob(id, kind, title, task, options) {
|
|
|
97
97
|
startedAt: task.startedAt,
|
|
98
98
|
updatedAt: task.updatedAt,
|
|
99
99
|
durationMs: task.durationMs,
|
|
100
|
+
correlationId: task.correlationId,
|
|
100
101
|
summary: task.prompt || task.detail,
|
|
101
102
|
logTail: task.currentTool || task.lastTool ? `Current tool: ${task.currentTool ?? "-"}\nLast tool: ${task.lastTool ?? "-"}` : undefined,
|
|
102
103
|
...options,
|
|
@@ -117,6 +118,7 @@ export function activityToUnifiedJob(event, kind, title, options) {
|
|
|
117
118
|
updatedAt: event.timestamp,
|
|
118
119
|
finishedAt: event.timestamp,
|
|
119
120
|
durationMs: event.durationMs,
|
|
121
|
+
correlationId: event.correlationId,
|
|
120
122
|
summary: event.prompt || event.detail,
|
|
121
123
|
logPath: event.detail,
|
|
122
124
|
logTail: event.detail,
|
|
@@ -145,6 +147,7 @@ export function promptActivityToUnifiedJob(event) {
|
|
|
145
147
|
threadId: event.threadId,
|
|
146
148
|
workspace: event.workspace,
|
|
147
149
|
owner: event.actor,
|
|
150
|
+
correlationId: event.correlationId,
|
|
148
151
|
startedAt: event.timestamp,
|
|
149
152
|
updatedAt: event.timestamp,
|
|
150
153
|
finishedAt: status === "running" || status === "queued" ? undefined : event.timestamp,
|
|
@@ -14,7 +14,7 @@ import { activeSessionSourceForContextKey, ChannelMirrorRegistry } from "../chan
|
|
|
14
14
|
import { listThreads as listCodexThreads } from "../agents/codex/codex-state.js";
|
|
15
15
|
import { friendlyErrorText } from "../core/error-messages.js";
|
|
16
16
|
import { clearLogFile, getAgentUpdateLogPath, getConnectorHealth, getConnectorLogPath, getPackageVersion, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "../support/operations.js";
|
|
17
|
-
import { PromptStore, toPromptEnvelope } from "../state/prompt-store.js";
|
|
17
|
+
import { ensurePromptCorrelationId, PromptStore, toPromptEnvelope } from "../state/prompt-store.js";
|
|
18
18
|
import { UnifiedJobStore } from "../state/job-store.js";
|
|
19
19
|
import { buildRuntimeMetrics } from "./metrics.js";
|
|
20
20
|
import { RelayArtifactService } from "./relay-artifact-service.js";
|
|
@@ -134,10 +134,11 @@ export async function relayRuntimeSendUploadPrompt(runtime, options, actor) {
|
|
|
134
134
|
}
|
|
135
135
|
export async function relayRuntimeSendEnvelope(runtime, envelope, actor) {
|
|
136
136
|
const activityActor = envelope.activityActor ?? actor;
|
|
137
|
+
const correlated = ensurePromptCorrelationId({ ...envelope, activityActor });
|
|
137
138
|
const session = await runtime.getSession(false);
|
|
138
139
|
const external = getExternalSnapshotForSession(session, runtime.config, { maxEvents: 0 });
|
|
139
140
|
if (session.isProcessing() || external?.activity.active) {
|
|
140
|
-
const queued = runtime.queueService.enqueue(
|
|
141
|
+
const queued = runtime.queueService.enqueue(correlated);
|
|
141
142
|
const info = runtime.publicInfo(session);
|
|
142
143
|
runtime.appendActivity({
|
|
143
144
|
source: "web",
|
|
@@ -147,7 +148,8 @@ export async function relayRuntimeSendEnvelope(runtime, envelope, actor) {
|
|
|
147
148
|
workspace: info.workspace,
|
|
148
149
|
agentId: info.agentId,
|
|
149
150
|
actor: activityActor,
|
|
150
|
-
|
|
151
|
+
correlationId: correlated.correlationId,
|
|
152
|
+
prompt: correlated.description,
|
|
151
153
|
detail: external?.activity.active
|
|
152
154
|
? `Queued because ${external.agentLabel} CLI is still processing another task.`
|
|
153
155
|
: `Queued at position ${runtime.queueService.length()}.`,
|
|
@@ -161,18 +163,19 @@ export async function relayRuntimeSendEnvelope(runtime, envelope, actor) {
|
|
|
161
163
|
workspace: info.workspace,
|
|
162
164
|
promptId: queued.id,
|
|
163
165
|
actor: activityActor,
|
|
164
|
-
|
|
166
|
+
correlationId: correlated.correlationId,
|
|
167
|
+
description: correlated.description,
|
|
165
168
|
});
|
|
166
169
|
if (external?.activity.active) {
|
|
167
170
|
runtime.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${runtime.queueService.length()} queued.`, "info");
|
|
168
171
|
}
|
|
169
172
|
runtime.broadcastQueue();
|
|
170
|
-
return { queued: true, queueId: queued.id };
|
|
173
|
+
return { queued: true, queueId: queued.id, correlationId: correlated.correlationId };
|
|
171
174
|
}
|
|
172
|
-
void runtime.runPrompt(session,
|
|
173
|
-
runtime.broadcast({ type: "turn_error", id: runtime.currentTurnId ?? "turn", error: friendlyErrorText(error), at: new Date().toISOString() });
|
|
175
|
+
void runtime.runPrompt(session, correlated).catch((error) => {
|
|
176
|
+
runtime.broadcast({ type: "turn_error", id: runtime.currentTurnId ?? "turn", error: friendlyErrorText(error), at: new Date().toISOString(), correlationId: correlated.correlationId });
|
|
174
177
|
});
|
|
175
|
-
return { queued: false };
|
|
178
|
+
return { queued: false, correlationId: correlated.correlationId };
|
|
176
179
|
}
|
|
177
180
|
export function relayRuntimeQueue(runtime) {
|
|
178
181
|
return runtime.queueService.list();
|
|
@@ -194,6 +197,7 @@ export function relayRuntimeQueueAction(runtime, action, id, actor) {
|
|
|
194
197
|
threadId: null,
|
|
195
198
|
workspace: runtime.config.workspace,
|
|
196
199
|
actor,
|
|
200
|
+
correlationId: affected?.correlationId,
|
|
197
201
|
prompt: affected?.description,
|
|
198
202
|
detail: id ? `${action}: ${id}` : `${action}: ${before.length} queued`,
|
|
199
203
|
});
|
|
@@ -207,9 +211,9 @@ export function relayRuntimeQueueAction(runtime, action, id, actor) {
|
|
|
207
211
|
runtime.broadcastQueue();
|
|
208
212
|
return runtime.queue();
|
|
209
213
|
}
|
|
210
|
-
export async function relayRuntimeArtifacts(runtime) {
|
|
214
|
+
export async function relayRuntimeArtifacts(runtime, limit = 20) {
|
|
211
215
|
const session = await runtime.getSession(true);
|
|
212
|
-
return runtime.artifactService.list(session.getInfo().workspace,
|
|
216
|
+
return runtime.artifactService.list(session.getInfo().workspace, limit);
|
|
213
217
|
}
|
|
214
218
|
export async function relayRuntimeArtifact(runtime, turnId) {
|
|
215
219
|
const session = await runtime.getSession(true);
|
|
@@ -348,6 +348,14 @@ export function relayRuntimeActivity(runtime, options = {}) {
|
|
|
348
348
|
const currentInfo = runtime.registry.get(runtime.contextKey)?.getInfo();
|
|
349
349
|
return runtime.activityStore.list(options).map((event) => runtime.enrichActivityEvent(event, currentInfo));
|
|
350
350
|
}
|
|
351
|
+
export function relayRuntimeActivityPage(runtime, options = {}) {
|
|
352
|
+
const currentInfo = runtime.registry.get(runtime.contextKey)?.getInfo();
|
|
353
|
+
const page = runtime.activityStore.listPage(options);
|
|
354
|
+
return {
|
|
355
|
+
...page,
|
|
356
|
+
items: page.items.map((event) => runtime.enrichActivityEvent(event, currentInfo)),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
351
359
|
export async function relayRuntimeRetry(runtime, actor) {
|
|
352
360
|
const cached = runtime.queueService.getLastPrompt();
|
|
353
361
|
if (!cached) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export async function relayRuntimeTrace(runtime, correlationId) {
|
|
2
|
+
const id = correlationId.trim();
|
|
3
|
+
if (!id) {
|
|
4
|
+
throw new Error("correlationId is required.");
|
|
5
|
+
}
|
|
6
|
+
const activity = runtime.activityStore.findByCorrelationId(id, 200).map((event) => runtime.enrichActivityEvent(event));
|
|
7
|
+
const audit = runtime.auditStore.findByCorrelationId(id, 200);
|
|
8
|
+
const chat = runtime.chatStore.findByCorrelationId(id, 200);
|
|
9
|
+
const queue = runtime.queue().filter((item) => item.correlationId === id);
|
|
10
|
+
const jobs = (await runtime.jobs({ limit: 500 })).jobs.filter((job) => job.correlationId === id);
|
|
11
|
+
const timeline = traceTimeline({ activity, audit, chat, queue, jobs });
|
|
12
|
+
const timestamps = timeline.map((item) => Date.parse(item.at)).filter(Number.isFinite);
|
|
13
|
+
const lastStatus = [...activity].reverse().find((event) => event.status)?.status ??
|
|
14
|
+
jobs.find((job) => job.status)?.status ??
|
|
15
|
+
(queue.length ? "queued" : "unknown");
|
|
16
|
+
return {
|
|
17
|
+
correlationId: id,
|
|
18
|
+
summary: {
|
|
19
|
+
startedAt: timestamps.length ? new Date(Math.min(...timestamps)).toISOString() : null,
|
|
20
|
+
updatedAt: timestamps.length ? new Date(Math.max(...timestamps)).toISOString() : null,
|
|
21
|
+
status: lastStatus,
|
|
22
|
+
sources: [...new Set(timeline.map((item) => item.source))],
|
|
23
|
+
threadId: activity.find((event) => event.threadId)?.threadId ?? chat.find((message) => message.threadId)?.threadId ?? jobs.find((job) => job.threadId)?.threadId,
|
|
24
|
+
workspace: activity.find((event) => event.workspace)?.workspace ?? jobs.find((job) => job.workspace)?.workspace,
|
|
25
|
+
agentId: activity.find((event) => event.agentId)?.agentId ?? jobs.find((job) => job.agentId)?.agentId,
|
|
26
|
+
},
|
|
27
|
+
activity,
|
|
28
|
+
audit,
|
|
29
|
+
chat,
|
|
30
|
+
queue,
|
|
31
|
+
jobs,
|
|
32
|
+
timeline,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function traceTimeline(input) {
|
|
36
|
+
return [
|
|
37
|
+
...input.activity.map((event) => ({
|
|
38
|
+
id: event.id,
|
|
39
|
+
at: event.timestamp,
|
|
40
|
+
source: "activity",
|
|
41
|
+
status: event.status,
|
|
42
|
+
type: event.type,
|
|
43
|
+
title: `${event.source} ${event.type}`,
|
|
44
|
+
detail: event.prompt || event.detail,
|
|
45
|
+
threadId: event.threadId,
|
|
46
|
+
workspace: event.workspace,
|
|
47
|
+
agentId: event.agentId,
|
|
48
|
+
})),
|
|
49
|
+
...input.audit.map((event) => ({
|
|
50
|
+
id: event.id,
|
|
51
|
+
at: event.timestamp,
|
|
52
|
+
source: "audit",
|
|
53
|
+
status: event.status,
|
|
54
|
+
type: event.action,
|
|
55
|
+
title: `${event.channelId} ${event.action}`,
|
|
56
|
+
detail: event.description || event.detail,
|
|
57
|
+
threadId: event.threadId,
|
|
58
|
+
workspace: event.workspace,
|
|
59
|
+
agentId: event.agentId,
|
|
60
|
+
})),
|
|
61
|
+
...input.chat.map((message) => ({
|
|
62
|
+
id: message.id,
|
|
63
|
+
at: message.timestamp,
|
|
64
|
+
source: "chat",
|
|
65
|
+
type: message.role,
|
|
66
|
+
title: `${message.source} ${message.role}`,
|
|
67
|
+
detail: message.text,
|
|
68
|
+
threadId: message.threadId,
|
|
69
|
+
})),
|
|
70
|
+
...input.queue.map((item) => ({
|
|
71
|
+
id: item.id,
|
|
72
|
+
at: item.createdAt,
|
|
73
|
+
source: "queue",
|
|
74
|
+
status: item.lastError ? "failed" : "queued",
|
|
75
|
+
type: "queued_prompt",
|
|
76
|
+
title: `Queued prompt ${item.id}`,
|
|
77
|
+
detail: item.lastError || item.description,
|
|
78
|
+
})),
|
|
79
|
+
...input.jobs.map((job) => ({
|
|
80
|
+
id: job.id,
|
|
81
|
+
at: job.updatedAt,
|
|
82
|
+
source: "job",
|
|
83
|
+
status: job.status,
|
|
84
|
+
type: job.kind,
|
|
85
|
+
title: job.title,
|
|
86
|
+
detail: job.summary || job.logTail,
|
|
87
|
+
threadId: job.threadId,
|
|
88
|
+
workspace: job.workspace,
|
|
89
|
+
agentId: job.agentId,
|
|
90
|
+
})),
|
|
91
|
+
].sort((left, right) => Date.parse(left.at) - Date.parse(right.at));
|
|
92
|
+
}
|
|
@@ -13,6 +13,7 @@ import { ChannelTurnService } from "../channels/shared/channel-turn-service.js";
|
|
|
13
13
|
import { activeSessionSourceForContextKey, ChannelMirrorRegistry } from "../channels/shared/channel-mirror-registry.js";
|
|
14
14
|
import { listThreads as listCodexThreads } from "../agents/codex/codex-state.js";
|
|
15
15
|
import { friendlyErrorText } from "../core/error-messages.js";
|
|
16
|
+
import { cursorPage, normalizeCursorLimit } from "../core/pagination.js";
|
|
16
17
|
import { clearLogFile, getAgentUpdateLogPath, getConnectorHealth, getConnectorLogPath, getPackageVersion, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "../support/operations.js";
|
|
17
18
|
import { PromptStore, toPromptEnvelope } from "../state/prompt-store.js";
|
|
18
19
|
import { UnifiedJobStore } from "../state/job-store.js";
|
|
@@ -161,7 +162,7 @@ export function relayRuntimeTasks(runtime) {
|
|
|
161
162
|
recent: runtime.activity({ limit: 20 }),
|
|
162
163
|
};
|
|
163
164
|
}
|
|
164
|
-
export async function relayRuntimeJobs(runtime) {
|
|
165
|
+
export async function relayRuntimeJobs(runtime, options = {}) {
|
|
165
166
|
const jobs = [];
|
|
166
167
|
const current = runtime.currentProgress;
|
|
167
168
|
if (current) {
|
|
@@ -190,6 +191,7 @@ export async function relayRuntimeJobs(runtime) {
|
|
|
190
191
|
threadId: null,
|
|
191
192
|
workspace: runtime.config.workspace,
|
|
192
193
|
owner: item.activityActor,
|
|
194
|
+
correlationId: item.correlationId,
|
|
193
195
|
startedAt: createdAt,
|
|
194
196
|
updatedAt: createdAt,
|
|
195
197
|
summary: item.description,
|
|
@@ -245,8 +247,12 @@ export async function relayRuntimeJobs(runtime) {
|
|
|
245
247
|
}
|
|
246
248
|
const liveJobs = dedupeJobs(jobs);
|
|
247
249
|
const storedJobs = runtime.jobStore.upsertMany(liveJobs);
|
|
250
|
+
const sortedJobs = dedupeJobs([...liveJobs, ...storedJobs]).sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
251
|
+
const limit = normalizeCursorLimit(options.limit, 100, 500);
|
|
252
|
+
const page = cursorPage(sortedJobs, options.cursor, limit, (job) => job.id);
|
|
248
253
|
return {
|
|
249
|
-
jobs:
|
|
254
|
+
jobs: page.items,
|
|
255
|
+
pagination: page.pagination,
|
|
250
256
|
updatedAt: new Date().toISOString(),
|
|
251
257
|
};
|
|
252
258
|
}
|
|
@@ -254,13 +260,13 @@ export async function relayRuntimeJobLog(runtime, id) {
|
|
|
254
260
|
if (id.startsWith("agent-update:")) {
|
|
255
261
|
const updateId = id.slice("agent-update:".length);
|
|
256
262
|
const log = runtime.agentUpdates.readLog(updateId);
|
|
257
|
-
return { job: (await runtime.jobs()).jobs.find((job) => job.id === id) ?? null, plain: log.plain };
|
|
263
|
+
return { job: (await runtime.jobs({ limit: 500 })).jobs.find((job) => job.id === id) ?? null, plain: log.plain };
|
|
258
264
|
}
|
|
259
265
|
if (id.startsWith("queue:")) {
|
|
260
266
|
const queueId = id.slice("queue:".length);
|
|
261
267
|
const item = runtime.queueService.rawList().find((candidate) => candidate.id === queueId);
|
|
262
268
|
return {
|
|
263
|
-
job: (await runtime.jobs()).jobs.find((job) => job.id === id) ?? null,
|
|
269
|
+
job: (await runtime.jobs({ limit: 500 })).jobs.find((job) => job.id === id) ?? null,
|
|
264
270
|
plain: item ? [
|
|
265
271
|
`Queued prompt: ${item.id}`,
|
|
266
272
|
`Created: ${new Date(item.createdAt).toISOString()}`,
|
|
@@ -270,7 +276,7 @@ export async function relayRuntimeJobLog(runtime, id) {
|
|
|
270
276
|
].filter(Boolean).join("\n") : "Queued prompt not found.",
|
|
271
277
|
};
|
|
272
278
|
}
|
|
273
|
-
const job = (await runtime.jobs()).jobs.find((candidate) => candidate.id === id) ?? null;
|
|
279
|
+
const job = (await runtime.jobs({ limit: 500 })).jobs.find((candidate) => candidate.id === id) ?? null;
|
|
274
280
|
return { job, plain: job?.logTail || job?.logPath || job?.summary || runtime.jobStore.get(id)?.summary || "No log available for this job." };
|
|
275
281
|
}
|
|
276
282
|
export async function relayRuntimeJobAction(runtime, id, action, actor) {
|
|
@@ -32,10 +32,11 @@ import { createSupportBundle } from "../support/support-bundle.js";
|
|
|
32
32
|
import { transcribeAudio } from "../artifacts/voice.js";
|
|
33
33
|
import { WebActivityStore, WebChatStore, } from "../web/web-state.js";
|
|
34
34
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "../core/workspace-policy.js";
|
|
35
|
-
import { relayRuntimeSubscribe, relayRuntimeSnapshot, relayRuntimeStatus, relayRuntimeBootstrapStatus, relayRuntimeVersion, relayRuntimeDiagnostics, relayRuntimeAdapterHealth, relayRuntimePermissions, relayRuntimeMetrics, relayRuntimeAudit, relayRuntimeSupportBundle, relayRuntimeLogs, relayRuntimeClearLogs, relayRuntimeRestartConnector, relayRuntimeDispose } from "./relay-runtime-dashboard.js";
|
|
35
|
+
import { relayRuntimeSubscribe, relayRuntimeSnapshot, relayRuntimeStatus, relayRuntimeBootstrapStatus, relayRuntimeVersion, relayRuntimeDiagnostics, relayRuntimeAdapterHealth, relayRuntimePermissions, relayRuntimeMetrics, relayRuntimeAudit, relayRuntimeAuditPage, relayRuntimeSupportBundle, relayRuntimeLogs, relayRuntimeClearLogs, relayRuntimeRestartConnector, relayRuntimeDispose } from "./relay-runtime-dashboard.js";
|
|
36
|
+
import { relayRuntimeTrace } from "./relay-runtime-trace.js";
|
|
36
37
|
import { relayRuntimeUpdateConnector, relayRuntimeAgentUpdateJobs, relayRuntimeStartAgentUpdate, relayRuntimeAgentUpdateLog, relayRuntimeDeleteAgentUpdateLog, relayRuntimeSendAgentUpdateInput, relayRuntimeCancelAgentUpdate, relayRuntimeTasks, relayRuntimeJobs, relayRuntimeJobLog, relayRuntimeJobAction, relayRuntimeRecordAgentUpdateLifecycle } from "./relay-runtime-updates-jobs.js";
|
|
37
38
|
import { relayRuntimeActiveSessions, relayRuntimeGetSession, relayRuntimeListKnownContextMetadata, relayRuntimeDiscoverRunningConnectorSessions, relayRuntimeDiscoverActiveCodexSessions, relayRuntimeExternalActiveSession, relayRuntimeSessionStubForMetadata, relayRuntimeCapabilitiesForAgent, relayRuntimeActiveSessionKey, relayRuntimePreferredActiveSession, relayRuntimeRecordActivity, relayRuntimeAppendActivity, relayRuntimeEnrichActivityInput, relayRuntimeEnrichActivityEvent, relayRuntimeEnrichActivityFields, relayRuntimeAppendAudit, relayRuntimeUpdateCurrentProgress, relayRuntimeAddCurrentTool, relayRuntimeBroadcastQueue, relayRuntimeBroadcastStatus, relayRuntimeBroadcast, relayRuntimeScheduleActiveSessionsBroadcast, relayRuntimePublicInfo } from "./relay-runtime-active-sessions.js";
|
|
38
|
-
import { relayRuntimeLocks, relayRuntimeLockWebSession, relayRuntimeUnlockWebSession, relayRuntimeControlOptions, relayRuntimeAuthStatus, relayRuntimeLogin, relayRuntimeLogout, relayRuntimeChatHistory, relayRuntimeWebMirrorPreference, relayRuntimeSessionDetail, relayRuntimeClearChatHistory, relayRuntimeActivity, relayRuntimeRetry, relayRuntimeSync, relayRuntimeListSessions, relayRuntimeListSessionsPage, relayRuntimeFilteredSessions, relayRuntimeListModels, relayRuntimeSetAgent, relayRuntimeNewSession, relayRuntimeSwitchSession, relayRuntimeAttachSession, relayRuntimeSetModel, relayRuntimeSetReasoningEffort, relayRuntimeSetFastMode, relayRuntimeSetLaunchProfile, relayRuntimeHandback, relayRuntimeAbort, relayRuntimeGetControlSession, relayRuntimeCliPathOptions } from "./relay-runtime-sessions.js";
|
|
39
|
+
import { relayRuntimeLocks, relayRuntimeLockWebSession, relayRuntimeUnlockWebSession, relayRuntimeControlOptions, relayRuntimeAuthStatus, relayRuntimeLogin, relayRuntimeLogout, relayRuntimeChatHistory, relayRuntimeWebMirrorPreference, relayRuntimeSessionDetail, relayRuntimeClearChatHistory, relayRuntimeActivity, relayRuntimeActivityPage, relayRuntimeRetry, relayRuntimeSync, relayRuntimeListSessions, relayRuntimeListSessionsPage, relayRuntimeFilteredSessions, relayRuntimeListModels, relayRuntimeSetAgent, relayRuntimeNewSession, relayRuntimeSwitchSession, relayRuntimeAttachSession, relayRuntimeSetModel, relayRuntimeSetReasoningEffort, relayRuntimeSetFastMode, relayRuntimeSetLaunchProfile, relayRuntimeHandback, relayRuntimeAbort, relayRuntimeGetControlSession, relayRuntimeCliPathOptions } from "./relay-runtime-sessions.js";
|
|
39
40
|
import { relayRuntimeSendPrompt, relayRuntimeSendUploadPrompt, relayRuntimeSendEnvelope, relayRuntimeQueue, relayRuntimeQueuePaused, relayRuntimeQueueAction, relayRuntimeArtifacts, relayRuntimeArtifact, relayRuntimeDeleteArtifact, relayRuntimeCreateArtifactZip, relayRuntimeArtifactPreview, relayRuntimeEnsureActiveThread, relayRuntimeEnsureIdle, relayRuntimeRunPrompt, relayRuntimeDrainQueue, relayRuntimeUpdateSession } from "./relay-runtime-prompt-queue-artifacts.js";
|
|
40
41
|
export const WEB_CONTEXT_KEY = "web:dashboard";
|
|
41
42
|
const ACTIVE_CODEX_DISCOVERY_LIMIT = 200;
|
|
@@ -208,8 +209,8 @@ export class RelayRuntime {
|
|
|
208
209
|
tasks() {
|
|
209
210
|
return relayRuntimeTasks(this);
|
|
210
211
|
}
|
|
211
|
-
async jobs() {
|
|
212
|
-
return relayRuntimeJobs(this);
|
|
212
|
+
async jobs(options = {}) {
|
|
213
|
+
return relayRuntimeJobs(this, options);
|
|
213
214
|
}
|
|
214
215
|
async jobLog(id) {
|
|
215
216
|
return relayRuntimeJobLog(this, id);
|
|
@@ -226,6 +227,12 @@ export class RelayRuntime {
|
|
|
226
227
|
audit(options = 50) {
|
|
227
228
|
return relayRuntimeAudit(this, options);
|
|
228
229
|
}
|
|
230
|
+
auditPage(options = {}) {
|
|
231
|
+
return relayRuntimeAuditPage(this, options);
|
|
232
|
+
}
|
|
233
|
+
async trace(correlationId) {
|
|
234
|
+
return relayRuntimeTrace(this, correlationId);
|
|
235
|
+
}
|
|
229
236
|
async supportBundle(actor) {
|
|
230
237
|
return relayRuntimeSupportBundle(this, actor);
|
|
231
238
|
}
|
|
@@ -265,6 +272,9 @@ export class RelayRuntime {
|
|
|
265
272
|
activity(options = {}) {
|
|
266
273
|
return relayRuntimeActivity(this, options);
|
|
267
274
|
}
|
|
275
|
+
activityPage(options = {}) {
|
|
276
|
+
return relayRuntimeActivityPage(this, options);
|
|
277
|
+
}
|
|
268
278
|
async retry(actor) {
|
|
269
279
|
return relayRuntimeRetry(this, actor);
|
|
270
280
|
}
|
|
@@ -331,8 +341,8 @@ export class RelayRuntime {
|
|
|
331
341
|
queueAction(action, id, actor) {
|
|
332
342
|
return relayRuntimeQueueAction(this, action, id, actor);
|
|
333
343
|
}
|
|
334
|
-
async artifacts() {
|
|
335
|
-
return relayRuntimeArtifacts(this);
|
|
344
|
+
async artifacts(limit = 20) {
|
|
345
|
+
return relayRuntimeArtifacts(this, limit);
|
|
336
346
|
}
|
|
337
347
|
async artifact(turnId) {
|
|
338
348
|
return relayRuntimeArtifact(this, turnId);
|
|
@@ -224,6 +224,16 @@ export function toPromptEnvelope(input, artifactOutDir) {
|
|
|
224
224
|
description: describePromptInput(input),
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
|
+
export function createCorrelationId() {
|
|
228
|
+
return randomUUID().replace(/-/g, "").slice(0, 12);
|
|
229
|
+
}
|
|
230
|
+
export function ensurePromptCorrelationId(prompt) {
|
|
231
|
+
const existing = prompt.correlationId?.trim();
|
|
232
|
+
if (existing) {
|
|
233
|
+
return { ...prompt, correlationId: existing };
|
|
234
|
+
}
|
|
235
|
+
return { ...prompt, correlationId: createCorrelationId() };
|
|
236
|
+
}
|
|
227
237
|
function createQueueId() {
|
|
228
238
|
return randomUUID().replace(/-/g, "").slice(0, 8);
|
|
229
239
|
}
|
|
@@ -232,7 +242,9 @@ function isPromptEnvelope(value) {
|
|
|
232
242
|
return false;
|
|
233
243
|
}
|
|
234
244
|
const candidate = value;
|
|
235
|
-
return isCodexPromptInput(candidate.input) &&
|
|
245
|
+
return isCodexPromptInput(candidate.input) &&
|
|
246
|
+
typeof candidate.description === "string" &&
|
|
247
|
+
(candidate.correlationId === undefined || typeof candidate.correlationId === "string");
|
|
236
248
|
}
|
|
237
249
|
function isQueuedPrompt(value) {
|
|
238
250
|
return isPromptEnvelope(value) &&
|
|
@@ -9,6 +9,7 @@ export const WEB_API_ROUTE_DEFINITIONS = [
|
|
|
9
9
|
exact("/api/progress", ["GET"], "inspect"),
|
|
10
10
|
exact("/api/metrics", ["GET"], "inspect"),
|
|
11
11
|
exact("/api/jobs", ["GET"], "inspect"),
|
|
12
|
+
exact("/api/trace", ["GET"], "sessions.read"),
|
|
12
13
|
dynamic("/api/jobs/:id/log", "^/api/jobs/[^/]+/log$", ["GET"], "inspect", `/api/jobs/${stringToken}/log`),
|
|
13
14
|
dynamic("/api/jobs/:id/action", "^/api/jobs/[^/]+/action$", ["POST"], "inspect", `/api/jobs/${stringToken}/action`),
|
|
14
15
|
exact("/api/active-sessions", ["GET"], "sessions.read"),
|
|
@@ -35,6 +36,7 @@ export const WEB_API_ROUTE_DEFINITIONS = [
|
|
|
35
36
|
exact("/api/peers/global-sessions", ["GET"], "sessions.read"),
|
|
36
37
|
dynamic("/api/peers/invitations/:id", "^/api/peers/invitations/[^/]+$", ["DELETE"], "peers.write", `/api/peers/invitations/${stringToken}`),
|
|
37
38
|
dynamic("/api/peers/:id/repin", "^/api/peers/[^/]+/repin$", ["POST"], "peers.write", `/api/peers/${stringToken}/repin`),
|
|
39
|
+
dynamic("/api/peers/:id/rotate", "^/api/peers/[^/]+/rotate$", ["POST"], "peers.write", `/api/peers/${stringToken}/rotate`),
|
|
38
40
|
dynamic("/api/peers/:id/health", "^/api/peers/[^/]+/health$", ["GET"], "peers.connect", `/api/peers/${stringToken}/health`),
|
|
39
41
|
dynamic("/api/peers/:id", "^/api/peers/[^/]+$", ["PATCH", "DELETE"], "peers.write", `/api/peers/${stringToken}`),
|
|
40
42
|
dynamic("/api/peers/:id/proxy", "^/api/peers/[^/]+/proxy$", ["POST"], "peers.connect", `/api/peers/${stringToken}/proxy`),
|
|
@@ -278,19 +278,22 @@ export async function handleDashboardAccessRoute(req, res, url, options) {
|
|
|
278
278
|
return true;
|
|
279
279
|
}
|
|
280
280
|
if (req.method === "GET" && url.pathname === "/api/audit") {
|
|
281
|
+
const page = runtime.auditPage({
|
|
282
|
+
limit: numberParam(url, "limit", 50),
|
|
283
|
+
cursor: url.searchParams.get("cursor") || undefined,
|
|
284
|
+
channelId: (url.searchParams.get("channel") || "all"),
|
|
285
|
+
category: (url.searchParams.get("category") || "all"),
|
|
286
|
+
status: (url.searchParams.get("status") || "all"),
|
|
287
|
+
action: url.searchParams.get("action") || "all",
|
|
288
|
+
actor: url.searchParams.get("actor") || undefined,
|
|
289
|
+
agentId: url.searchParams.get("agent") || "all",
|
|
290
|
+
threadId: url.searchParams.get("thread") || undefined,
|
|
291
|
+
workspace: url.searchParams.get("workspace") || undefined,
|
|
292
|
+
since: url.searchParams.get("since") || undefined,
|
|
293
|
+
});
|
|
281
294
|
sendJson(res, 200, {
|
|
282
|
-
events:
|
|
283
|
-
|
|
284
|
-
channelId: (url.searchParams.get("channel") || "all"),
|
|
285
|
-
category: (url.searchParams.get("category") || "all"),
|
|
286
|
-
status: (url.searchParams.get("status") || "all"),
|
|
287
|
-
action: url.searchParams.get("action") || "all",
|
|
288
|
-
actor: url.searchParams.get("actor") || undefined,
|
|
289
|
-
agentId: url.searchParams.get("agent") || "all",
|
|
290
|
-
threadId: url.searchParams.get("thread") || undefined,
|
|
291
|
-
workspace: url.searchParams.get("workspace") || undefined,
|
|
292
|
-
since: url.searchParams.get("since") || undefined,
|
|
293
|
-
}),
|
|
295
|
+
events: page.items,
|
|
296
|
+
pagination: page.pagination,
|
|
294
297
|
});
|
|
295
298
|
return true;
|
|
296
299
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cursorPage, normalizeCursorLimit } from "../core/pagination.js";
|
|
2
|
+
import { numberParam, readJsonBody, requiredSearch, sendFile, sendJson, stringField, } from "./web-dashboard-http.js";
|
|
2
3
|
export async function handleDashboardArtifactRoute(req, res, url, options) {
|
|
3
4
|
const { runtime, authUser } = options;
|
|
4
5
|
if (req.method === "GET" && url.pathname === "/api/artifacts") {
|
|
5
6
|
await options.assertCurrentSessionScope(authUser);
|
|
6
|
-
|
|
7
|
+
const limit = normalizeCursorLimit(numberParam(url, "limit", 50), 50, 200);
|
|
8
|
+
const reports = await runtime.artifacts(500);
|
|
9
|
+
const page = cursorPage(reports, url.searchParams.get("cursor") || undefined, limit, (report) => report.turnId);
|
|
10
|
+
sendJson(res, 200, { reports: page.items, pagination: page.pagination });
|
|
7
11
|
return true;
|
|
8
12
|
}
|
|
9
13
|
if (req.method === "DELETE" && url.pathname === "/api/artifacts") {
|
|
@@ -137,7 +137,6 @@ ${faviconLinks}
|
|
|
137
137
|
<select id="peerSelect" title="NordRelay target"></select>
|
|
138
138
|
<select id="agentSelect"></select>
|
|
139
139
|
<button id="themeBtn" class="secondary" title="Toggle dark theme">Dark</button>
|
|
140
|
-
<button id="refreshBtn">Refresh</button>
|
|
141
140
|
<button id="logoutBtn" class="secondary">Logout</button>
|
|
142
141
|
</div>
|
|
143
142
|
</header>
|
|
@@ -200,6 +199,7 @@ ${faviconLinks}
|
|
|
200
199
|
<div class="panel">
|
|
201
200
|
<div class="row"><button id="reloadTasksBtn">Reload tasks</button></div>
|
|
202
201
|
<div id="tasksList" class="list"></div>
|
|
202
|
+
<div id="jobsPager" class="pager"></div>
|
|
203
203
|
</div>
|
|
204
204
|
</section>
|
|
205
205
|
|
|
@@ -232,6 +232,14 @@ ${faviconLinks}
|
|
|
232
232
|
<div class="panel">
|
|
233
233
|
<div class="row"><select id="activitySource"><option value="all">All sources</option><option value="web">Web</option><option value="telegram">Telegram</option><option value="discord">Discord</option><option value="slack">Slack</option><option value="cli">CLI</option></select><select id="activityCategory"><option value="all">All categories</option><option value="prompt">Prompt</option><option value="session">Session</option><option value="queue">Queue</option><option value="agent-update">Agent update</option><option value="artifact">Artifact</option><option value="system">System</option><option value="auth">Auth</option><option value="security">Security</option><option value="tool">Tool</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="activityActor" placeholder="Actor"><input id="activityAgent" placeholder="Agent"><input id="activityThread" placeholder="Thread ID"><input id="activityWorkspace" placeholder="Workspace"><input id="activityType" placeholder="Type"><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>
|
|
234
234
|
<div id="activityList" class="list"></div>
|
|
235
|
+
<div id="activityPager" class="pager"></div>
|
|
236
|
+
</div>
|
|
237
|
+
</section>
|
|
238
|
+
|
|
239
|
+
<section class="page" id="page-trace">
|
|
240
|
+
<div class="panel">
|
|
241
|
+
<div class="row"><input id="traceCorrelationId" placeholder="Correlation ID"><button id="loadTraceBtn">Load trace</button></div>
|
|
242
|
+
<div id="traceDetail" class="list"></div>
|
|
235
243
|
</div>
|
|
236
244
|
</section>
|
|
237
245
|
|
|
@@ -240,6 +248,7 @@ ${faviconLinks}
|
|
|
240
248
|
<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>
|
|
241
249
|
<div id="artifactPreview" class="preview"></div>
|
|
242
250
|
<div id="artifactList" class="list"></div>
|
|
251
|
+
<div id="artifactPager" class="pager"></div>
|
|
243
252
|
</div>
|
|
244
253
|
</section>
|
|
245
254
|
|
|
@@ -268,49 +277,71 @@ ${faviconLinks}
|
|
|
268
277
|
|
|
269
278
|
<section class="page" id="page-access">
|
|
270
279
|
<div class="panel">
|
|
271
|
-
<div class="
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
+
<div class="section-header access-section-header">
|
|
281
|
+
<div id="accessTabs" class="section-tabs access-tabs" role="tablist" aria-label="Users sections">
|
|
282
|
+
<button type="button" role="tab" aria-selected="true" tabindex="0" data-access-tab="users" class="active">Users</button>
|
|
283
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="groups">Groups</button>
|
|
284
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="telegram">Telegram</button>
|
|
285
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="discord">Discord</button>
|
|
286
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="slack">Slack</button>
|
|
287
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="locks">Locks</button>
|
|
288
|
+
<button type="button" role="tab" aria-selected="false" tabindex="-1" data-access-tab="audit">Audit</button>
|
|
289
|
+
</div>
|
|
280
290
|
</div>
|
|
281
291
|
<div class="access-tab active" data-access-tab-panel="users">
|
|
282
|
-
<div
|
|
292
|
+
<div class="access-tab-heading">
|
|
293
|
+
<div class="row access-heading-actions"><button id="loadAccessBtn" class="secondary">Reload</button><button id="createUserBtn">Create user</button></div>
|
|
294
|
+
<div class="access-filter-row">
|
|
295
|
+
<input id="userSearch" placeholder="Search users">
|
|
296
|
+
<select id="userStatusFilter"><option value="all">All statuses</option><option value="active">Active</option><option value="disabled">Disabled</option></select>
|
|
297
|
+
<select id="userGroupFilter"><option value="all">All groups</option></select>
|
|
298
|
+
<select id="userIdentityFilter"><option value="all">All identities</option><option value="telegram">Telegram linked</option><option value="discord">Discord linked</option><option value="slack">Slack linked</option><option value="web">Web sessions</option><option value="unlinked">No chat identity</option></select>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div id="accessPanel" class="list user-list"></div>
|
|
302
|
+
<div id="usersPager" class="pager"></div>
|
|
283
303
|
</div>
|
|
284
304
|
<div class="access-tab" data-access-tab-panel="groups">
|
|
285
|
-
<
|
|
305
|
+
<div class="access-tab-heading">
|
|
306
|
+
<div class="row access-heading-actions"><button id="createGroupBtn" class="secondary">Create group</button></div>
|
|
307
|
+
<div class="access-filter-row"><input id="groupSearch" placeholder="Search groups"></div>
|
|
308
|
+
</div>
|
|
286
309
|
<div id="groupsList" class="list"></div>
|
|
287
310
|
</div>
|
|
288
311
|
<div class="access-tab" data-access-tab-panel="telegram">
|
|
289
|
-
<
|
|
312
|
+
<div class="access-tab-heading">
|
|
313
|
+
<div class="row access-heading-actions"><button id="createChatBtn" class="secondary">Add Telegram chat</button></div>
|
|
314
|
+
<input id="telegramChatSearch" placeholder="Search Telegram chats">
|
|
315
|
+
</div>
|
|
290
316
|
<div id="telegramChatsList" class="list"></div>
|
|
291
317
|
</div>
|
|
292
318
|
<div class="access-tab" data-access-tab-panel="discord">
|
|
293
319
|
<div class="access-tab-heading">
|
|
294
|
-
<
|
|
320
|
+
<div class="row access-heading-actions"><button id="createDiscordChannelBtn" class="secondary">Add Discord channel</button></div>
|
|
295
321
|
<input id="discordChannelSearch" placeholder="Search Discord channels">
|
|
296
322
|
</div>
|
|
297
323
|
<div id="discordChannelsList" class="list"></div>
|
|
298
324
|
</div>
|
|
299
325
|
<div class="access-tab" data-access-tab-panel="slack">
|
|
300
326
|
<div class="access-tab-heading">
|
|
301
|
-
<
|
|
327
|
+
<div class="row access-heading-actions"><button id="createSlackChannelBtn" class="secondary">Add Slack channel</button></div>
|
|
302
328
|
<input id="slackChannelSearch" placeholder="Search Slack channels">
|
|
303
329
|
</div>
|
|
304
330
|
<div id="slackChannelsList" class="list"></div>
|
|
305
331
|
</div>
|
|
306
332
|
<div class="access-tab" data-access-tab-panel="locks">
|
|
307
|
-
<
|
|
333
|
+
<div class="access-tab-heading">
|
|
334
|
+
<div class="row access-heading-actions"><button id="lockSessionBtn" class="secondary">Lock web session</button><button id="unlockSessionBtn" class="secondary">Unlock web session</button></div>
|
|
335
|
+
</div>
|
|
308
336
|
<div id="locksList" class="list"></div>
|
|
309
337
|
</div>
|
|
310
338
|
<div class="access-tab" data-access-tab-panel="audit">
|
|
311
|
-
<
|
|
312
|
-
|
|
339
|
+
<div class="access-tab-heading">
|
|
340
|
+
<div class="row access-heading-actions"><button id="loadAuditBtn">Load audit</button><button id="exportAuditBtn" class="secondary">Export</button></div>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="row audit-filter-row"><select id="auditChannel"><option value="all">All channels</option><option value="web">Web</option><option value="telegram">Telegram</option><option value="discord">Discord</option><option value="slack">Slack</option></select><select id="auditCategory"><option value="all">All categories</option><option value="prompt">Prompt</option><option value="session">Session</option><option value="queue">Queue</option><option value="agent-update">Agent update</option><option value="artifact">Artifact</option><option value="system">System</option><option value="auth">Auth</option><option value="security">Security</option><option value="tool">Tool</option></select><select id="auditStatus"><option value="all">All statuses</option><option value="ok">OK</option><option value="failed">Failed</option><option value="denied">Denied</option></select><input id="auditActor" placeholder="Actor"><input id="auditAgent" placeholder="Agent"><input id="auditThread" placeholder="Thread ID"><input id="auditWorkspace" placeholder="Workspace"><input id="auditSince" type="datetime-local"><input id="auditLimit" type="number" value="50" min="1" max="500"></div>
|
|
313
343
|
<div id="auditList" class="list"></div>
|
|
344
|
+
<div id="auditPager" class="pager"></div>
|
|
314
345
|
</div>
|
|
315
346
|
</div>
|
|
316
347
|
</section>
|
|
@@ -326,8 +357,11 @@ ${faviconLinks}
|
|
|
326
357
|
|
|
327
358
|
<section class="page" id="page-settings">
|
|
328
359
|
<div class="panel">
|
|
329
|
-
<div
|
|
330
|
-
|
|
360
|
+
<div id="settingsTabHeader" class="section-header settings-section-header">
|
|
361
|
+
<div id="settingsTabs" class="section-tabs settings-tabs" role="tablist" aria-label="Settings sections"></div>
|
|
362
|
+
</div>
|
|
363
|
+
<div id="settingsSubnav" class="settings-subnav" hidden></div>
|
|
364
|
+
<div id="settingsActions" class="row settings-actions"><button id="saveSettingsBtn">Save settings</button><button id="settingsWizardBtn" class="secondary">Setup wizard</button><button id="restartBtn" class="secondary">Restart NordRelay</button><span id="settingsStatus"></span></div>
|
|
331
365
|
<div id="settingsForm" class="settings-grid"></div>
|
|
332
366
|
</div>
|
|
333
367
|
</section>
|
|
@@ -372,6 +406,10 @@ ${faviconLinks}
|
|
|
372
406
|
<div id="sessionDetail"></div>
|
|
373
407
|
<div class="row dialog-actions"><button id="closeSessionDetailBtn" class="secondary">Close</button></div>
|
|
374
408
|
</dialog>
|
|
409
|
+
<dialog id="userDetailDialog">
|
|
410
|
+
<div id="userDetail"></div>
|
|
411
|
+
<div class="row dialog-actions"><button id="closeUserDetailBtn" class="secondary">Close</button></div>
|
|
412
|
+
</dialog>
|
|
375
413
|
<dialog id="adminDialog">
|
|
376
414
|
<form method="dialog" id="adminDialogForm">
|
|
377
415
|
<h2 id="adminDialogTitle">Edit</h2>
|