@nordbyte/nordrelay 0.5.2 → 0.6.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.
Files changed (52) hide show
  1. package/.env.example +63 -11
  2. package/README.md +90 -19
  3. package/dist/access-control.js +1 -0
  4. package/dist/activity-events.js +44 -0
  5. package/dist/audit-log.js +40 -2
  6. package/dist/bot-rendering.js +10 -7
  7. package/dist/bot.js +458 -5
  8. package/dist/channel-actions.js +7 -2
  9. package/dist/channel-adapter.js +34 -7
  10. package/dist/channel-command-service.js +156 -0
  11. package/dist/channel-turn-service.js +237 -0
  12. package/dist/config-metadata.js +78 -13
  13. package/dist/config.js +77 -7
  14. package/dist/context-key.js +77 -5
  15. package/dist/discord-artifacts.js +165 -0
  16. package/dist/discord-bot.js +2014 -0
  17. package/dist/discord-channel-runtime.js +133 -0
  18. package/dist/discord-command-surface.js +119 -0
  19. package/dist/discord-rate-limit.js +141 -0
  20. package/dist/index.js +16 -5
  21. package/dist/job-store.js +127 -0
  22. package/dist/metrics.js +41 -0
  23. package/dist/relay-external-activity-monitor.js +47 -6
  24. package/dist/relay-runtime.js +986 -281
  25. package/dist/runtime-cache.js +57 -0
  26. package/dist/session-locks.js +10 -7
  27. package/dist/support-bundle.js +1 -0
  28. package/dist/telegram-access-commands.js +15 -2
  29. package/dist/telegram-access-middleware.js +16 -3
  30. package/dist/telegram-agent-commands.js +25 -0
  31. package/dist/telegram-artifact-commands.js +46 -0
  32. package/dist/telegram-diagnostics-command.js +5 -50
  33. package/dist/telegram-general-commands.js +2 -6
  34. package/dist/telegram-operational-commands.js +14 -6
  35. package/dist/telegram-queue-commands.js +74 -4
  36. package/dist/telegram-support-command.js +7 -0
  37. package/dist/telegram-update-commands.js +27 -0
  38. package/dist/user-management.js +208 -0
  39. package/dist/web-api-contract.js +9 -0
  40. package/dist/web-dashboard-access-routes.js +74 -1
  41. package/dist/web-dashboard-artifact-routes.js +3 -3
  42. package/dist/web-dashboard-assets.js +2 -0
  43. package/dist/web-dashboard-pages.js +97 -13
  44. package/dist/web-dashboard-runtime-routes.js +53 -8
  45. package/dist/web-dashboard-session-routes.js +27 -20
  46. package/dist/web-dashboard-ui.js +1 -0
  47. package/dist/web-dashboard.js +148 -6
  48. package/dist/web-state.js +33 -2
  49. package/dist/webui-assets/dashboard.css +75 -1
  50. package/dist/webui-assets/dashboard.js +358 -47
  51. package/package.json +3 -1
  52. package/plugins/nordrelay/scripts/nordrelay.mjs +210 -17
@@ -1,4 +1,5 @@
1
1
  import { createServer } from "node:http";
2
+ import { randomBytes } from "node:crypto";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { URL } from "node:url";
@@ -17,7 +18,7 @@ import { handleDashboardAccessRoute } from "./web-dashboard-access-routes.js";
17
18
  import { handleDashboardArtifactRoute } from "./web-dashboard-artifact-routes.js";
18
19
  import { dashboardCss, dashboardJs } from "./web-dashboard-assets.js";
19
20
  import { objectRecord, optionalStringField, parseCookies, readJsonBody, sendJson, sendText, } from "./web-dashboard-http.js";
20
- import { renderDashboardApp, renderLoginPage } from "./web-dashboard-pages.js";
21
+ import { renderDashboardApp, renderFirstRunSetupPage, renderLoginPage } from "./web-dashboard-pages.js";
21
22
  import { handleDashboardRuntimeRoute } from "./web-dashboard-runtime-routes.js";
22
23
  import { handleDashboardSessionRoute } from "./web-dashboard-session-routes.js";
23
24
  const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
@@ -28,6 +29,11 @@ const settings = new SettingsService(resolveDashboardEnvPath(options.home));
28
29
  const users = new UserStore(options.home);
29
30
  const auditLog = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
30
31
  const loginAttempts = new Map();
32
+ const firstRunSetupToken = users.hasAdminUser() ? undefined : randomBytes(18).toString("base64url");
33
+ const firstRunSetupRequiresToken = !isLoopbackHost(options.host);
34
+ if (firstRunSetupToken) {
35
+ console.log(`NordRelay first-run setup token: ${firstRunSetupToken}`);
36
+ }
31
37
  class AccessDeniedError extends Error {
32
38
  }
33
39
  const server = createServer((req, res) => {
@@ -45,6 +51,10 @@ async function handleRequest(req, res) {
45
51
  await handleLogin(req, res);
46
52
  return;
47
53
  }
54
+ if (url.pathname === "/api/setup/admin" && req.method === "POST") {
55
+ await handleFirstRunSetup(req, res);
56
+ return;
57
+ }
48
58
  if (url.pathname === "/api/dashboard/logout" && req.method === "POST") {
49
59
  handleLogout(req, res);
50
60
  return;
@@ -60,6 +70,10 @@ async function handleRequest(req, res) {
60
70
  }
61
71
  if (!authenticated) {
62
72
  if (url.pathname === "/" || url.pathname === "/index.html") {
73
+ if (!users.hasAdminUser()) {
74
+ sendText(res, 200, renderFirstRunSetupPage({ tokenRequired: firstRunSetupRequiresToken || !isLoopbackRequest(req) }), "text/html; charset=utf-8");
75
+ return;
76
+ }
63
77
  sendText(res, 200, renderLoginPage({ adminConfigured: users.hasAdminUser() }), "text/html; charset=utf-8");
64
78
  return;
65
79
  }
@@ -104,6 +118,7 @@ async function handleApi(req, res, url, authUser) {
104
118
  status: "denied",
105
119
  channelId: "web",
106
120
  contextKey: "web",
121
+ actor: webActivityActor(authUser),
107
122
  actorId: authUser.user.id,
108
123
  actorRole: authUser.groups.map((group) => group.name).join(", "),
109
124
  description: `Denied unknown endpoint ${req.method ?? "GET"} ${url.pathname}`,
@@ -117,6 +132,7 @@ async function handleApi(req, res, url, authUser) {
117
132
  status: "denied",
118
133
  channelId: "web",
119
134
  contextKey: "web",
135
+ actor: webActivityActor(authUser),
120
136
  actorId: authUser.user.id,
121
137
  actorRole: authUser.groups.map((group) => group.name).join(", "),
122
138
  description: `${permission} required for ${req.method ?? "GET"} ${url.pathname}`,
@@ -133,6 +149,8 @@ async function handleApi(req, res, url, authUser) {
133
149
  assertAgentUpdateJobScope,
134
150
  assertCurrentSessionScope,
135
151
  scopedTasks,
152
+ scopedActiveSessions,
153
+ activityActor: webActivityActor(authUser),
136
154
  })) {
137
155
  return;
138
156
  }
@@ -182,6 +200,7 @@ async function handleApi(req, res, url, authUser) {
182
200
  assertSessionDetailScope,
183
201
  scopedSessionPage,
184
202
  filterActivityByScope,
203
+ activityActor: webActivityActor(authUser),
185
204
  })) {
186
205
  return;
187
206
  }
@@ -189,6 +208,7 @@ async function handleApi(req, res, url, authUser) {
189
208
  runtime,
190
209
  authUser,
191
210
  assertCurrentSessionScope,
211
+ activityActor: webActivityActor(authUser),
192
212
  })) {
193
213
  return;
194
214
  }
@@ -239,6 +259,58 @@ async function handleEvents(req, res) {
239
259
  unsubscribe();
240
260
  });
241
261
  }
262
+ async function handleFirstRunSetup(req, res) {
263
+ if (users.hasAdminUser()) {
264
+ sendJson(res, 409, { error: "Admin user already exists." });
265
+ return;
266
+ }
267
+ const body = await readJsonBody(req);
268
+ const email = optionalStringField(body, "email") ?? "";
269
+ const displayName = optionalStringField(body, "displayName") ?? email;
270
+ const password = optionalStringField(body, "password") ?? "";
271
+ const setupToken = optionalStringField(body, "setupToken") ?? "";
272
+ if ((firstRunSetupRequiresToken || !isLoopbackRequest(req)) && setupToken !== firstRunSetupToken) {
273
+ audit({
274
+ action: "auth_login_failed",
275
+ status: "denied",
276
+ channelId: "web",
277
+ contextKey: "web",
278
+ description: `Rejected remote first-run setup for ${email || "unknown"}`,
279
+ });
280
+ sendJson(res, 403, { error: "Setup token required." });
281
+ return;
282
+ }
283
+ if (setupToken && setupToken !== firstRunSetupToken) {
284
+ sendJson(res, 403, { error: "Invalid setup token." });
285
+ return;
286
+ }
287
+ if (!email || !password || password.length < 12) {
288
+ sendJson(res, 400, { error: "Email and a password with at least 12 characters are required." });
289
+ return;
290
+ }
291
+ const authUser = users.createAdmin({ email, displayName, password });
292
+ const session = users.createWebSession(authUser.user.id);
293
+ audit({
294
+ action: "user_created",
295
+ status: "ok",
296
+ channelId: "web",
297
+ contextKey: "web",
298
+ actor: webActivityActor(authUser),
299
+ actorId: authUser.user.id,
300
+ actorRole: authUser.groups.map((group) => group.name).join(", "),
301
+ description: `First admin created: ${authUser.user.email}`,
302
+ });
303
+ runtime.recordActivity({
304
+ source: "web",
305
+ status: "info",
306
+ type: "first_run_admin_created",
307
+ threadId: null,
308
+ actor: webActivityActor(authUser),
309
+ detail: authUser.user.email,
310
+ });
311
+ setSessionCookie(res, session.token);
312
+ sendJson(res, 201, currentUserDto(authUser));
313
+ }
242
314
  async function handleLogin(req, res) {
243
315
  const body = await readJsonBody(req);
244
316
  const email = optionalStringField(body, "email");
@@ -280,13 +352,32 @@ async function handleLogin(req, res) {
280
352
  status: "ok",
281
353
  channelId: "web",
282
354
  contextKey: "web",
355
+ actor: webActivityActor(authUser),
283
356
  actorId: authUser.user.id,
284
357
  actorRole: authUser.groups.map((group) => group.name).join(", "),
285
358
  description: `Login ${authUser.user.email}`,
286
359
  });
360
+ runtime.recordActivity({
361
+ source: "web",
362
+ status: "info",
363
+ type: "auth_login",
364
+ threadId: null,
365
+ actor: webActivityActor(authUser),
366
+ detail: authUser.user.email,
367
+ });
287
368
  setSessionCookie(res, session.token);
288
369
  sendJson(res, 200, currentUserDto(authUser));
289
370
  }
371
+ function isLoopbackRequest(req) {
372
+ const address = req.socket.remoteAddress ?? "";
373
+ return address === "127.0.0.1" ||
374
+ address === "::1" ||
375
+ address === "::ffff:127.0.0.1" ||
376
+ address === "localhost";
377
+ }
378
+ function isLoopbackHost(host) {
379
+ return host === "127.0.0.1" || host === "::1" || host === "localhost";
380
+ }
290
381
  function handleLogout(req, res) {
291
382
  const authUser = authenticateRequest(req);
292
383
  users.destroyWebSession(parseCookies(req.headers.cookie ?? "").nr_session);
@@ -345,10 +436,27 @@ function auditUserAction(authUser, action, description) {
345
436
  status: "ok",
346
437
  channelId: "web",
347
438
  contextKey: "web",
439
+ actor: webActivityActor(authUser),
348
440
  actorId: authUser.user.id,
349
441
  actorRole: authUser.groups.map((group) => group.name).join(", "),
350
442
  description,
351
443
  });
444
+ runtime.recordActivity({
445
+ source: "web",
446
+ status: "info",
447
+ type: action,
448
+ threadId: null,
449
+ actor: webActivityActor(authUser),
450
+ detail: description,
451
+ });
452
+ }
453
+ function webActivityActor(authUser) {
454
+ return {
455
+ channel: "web",
456
+ id: authUser.user.id,
457
+ label: authUser.user.displayName || authUser.user.email,
458
+ username: authUser.user.email,
459
+ };
352
460
  }
353
461
  function scopedControlOptions(authUser, options) {
354
462
  return {
@@ -372,6 +480,12 @@ async function scopedTasks(authUser, tasks) {
372
480
  recent: filterActivityByScope(authUser, tasks.recent),
373
481
  };
374
482
  }
483
+ function scopedActiveSessions(authUser, active) {
484
+ return {
485
+ ...active,
486
+ sessions: active.sessions.filter((session) => canUseSession(authUser, session)),
487
+ };
488
+ }
375
489
  async function scopeRelayEvent(authUser, event, canUseCurrentSession = () => canUseCurrentSessionScope(authUser)) {
376
490
  switch (event.type) {
377
491
  case "snapshot":
@@ -504,6 +618,7 @@ function optionalEnv(key) {
504
618
  }
505
619
  function activeSettingsValues(current) {
506
620
  return {
621
+ TELEGRAM_ENABLED: boolValue(current.telegramEnabled),
507
622
  TELEGRAM_BOT_TOKEN: current.telegramBotToken,
508
623
  TELEGRAM_TRANSPORT: current.telegramTransport,
509
624
  TELEGRAM_WEBHOOK_URL: current.telegramWebhookUrl,
@@ -511,6 +626,20 @@ function activeSettingsValues(current) {
511
626
  TELEGRAM_WEBHOOK_PORT: String(current.telegramWebhookPort),
512
627
  TELEGRAM_WEBHOOK_PATH: current.telegramWebhookPath,
513
628
  TELEGRAM_WEBHOOK_SECRET: current.telegramWebhookSecret,
629
+ DISCORD_ENABLED: boolValue(current.discordEnabled),
630
+ DISCORD_BOT_TOKEN: current.discordBotToken,
631
+ DISCORD_CLIENT_ID: current.discordClientId,
632
+ DISCORD_GUILD_IDS: current.discordGuildIds.join(","),
633
+ DISCORD_ALLOWED_GUILD_IDS: current.discordAllowedGuildIds.join(","),
634
+ DISCORD_ALLOWED_CHANNEL_IDS: current.discordAllowedChannelIds.join(","),
635
+ DISCORD_MESSAGE_CONTENT_ENABLED: boolValue(current.discordMessageContentEnabled),
636
+ DISCORD_COMMAND_MODE: current.discordCommandMode,
637
+ DISCORD_AUTO_REGISTER_COMMANDS: boolValue(current.discordAutoRegisterCommands),
638
+ DISCORD_CLI_MIRROR_MODE: current.discordMirrorMode === current.mirrorMode ? "" : current.discordMirrorMode,
639
+ DISCORD_CLI_MIRROR_MIN_UPDATE_MS: current.discordMirrorMinUpdateMs === current.mirrorMinUpdateMs ? "" : String(current.discordMirrorMinUpdateMs),
640
+ DISCORD_NOTIFY_MODE: current.discordNotifyMode === current.notifyMode ? "" : current.discordNotifyMode,
641
+ DISCORD_QUIET_HOURS: quietOverrideValue(current.discordQuietHours, current.quietHours),
642
+ DISCORD_AUTO_SEND_ARTIFACTS: current.discordAutoSendArtifacts === current.autoSendArtifacts ? "" : boolValue(current.discordAutoSendArtifacts),
514
643
  NORDRELAY_CODEX_ENABLED: boolValue(current.codexEnabled),
515
644
  NORDRELAY_PI_ENABLED: boolValue(current.piEnabled),
516
645
  NORDRELAY_HERMES_ENABLED: boolValue(current.hermesEnabled),
@@ -565,10 +694,15 @@ function activeSettingsValues(current) {
565
694
  ENABLE_TELEGRAM_REACTIONS: boolValue(current.enableTelegramReactions),
566
695
  TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS: String(current.telegramRateLimitMinIntervalMs),
567
696
  TELEGRAM_EDIT_MIN_INTERVAL_MS: String(current.telegramEditMinIntervalMs),
568
- TELEGRAM_CLI_MIRROR_MODE: current.telegramMirrorMode,
569
- TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS: String(current.telegramMirrorMinUpdateMs),
570
- TELEGRAM_NOTIFY_MODE: current.telegramNotifyMode,
571
- TELEGRAM_QUIET_HOURS: current.telegramQuietHours ? `${current.telegramQuietHours.startHour}-${current.telegramQuietHours.endHour}` : "",
697
+ NORDRELAY_CLI_MIRROR_MODE: current.mirrorMode,
698
+ NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS: String(current.mirrorMinUpdateMs),
699
+ NORDRELAY_NOTIFY_MODE: current.notifyMode,
700
+ NORDRELAY_QUIET_HOURS: quietValue(current.quietHours),
701
+ NORDRELAY_AUTO_SEND_ARTIFACTS: boolValue(current.autoSendArtifacts),
702
+ TELEGRAM_CLI_MIRROR_MODE: current.telegramMirrorMode === current.mirrorMode ? "" : current.telegramMirrorMode,
703
+ TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS: current.telegramMirrorMinUpdateMs === current.mirrorMinUpdateMs ? "" : String(current.telegramMirrorMinUpdateMs),
704
+ TELEGRAM_NOTIFY_MODE: current.telegramNotifyMode === current.notifyMode ? "" : current.telegramNotifyMode,
705
+ TELEGRAM_QUIET_HOURS: quietOverrideValue(current.telegramQuietHours, current.quietHours),
572
706
  TELEGRAM_REDACT_PATTERNS: current.telegramRedactPatterns.join(","),
573
707
  NORDRELAY_UPDATE_METHOD: process.env.NORDRELAY_UPDATE_METHOD || "auto",
574
708
  MAX_FILE_SIZE: String(current.maxFileSize),
@@ -577,12 +711,14 @@ function activeSettingsValues(current) {
577
711
  ARTIFACT_MAX_INBOX_DIRS: String(current.artifactMaxInboxDirs),
578
712
  ARTIFACT_IGNORE_DIRS: current.artifactIgnoreDirs.join(","),
579
713
  ARTIFACT_IGNORE_GLOBS: current.artifactIgnoreGlobs.join(","),
580
- TELEGRAM_AUTO_SEND_ARTIFACTS: boolValue(current.telegramAutoSendArtifacts),
714
+ TELEGRAM_AUTO_SEND_ARTIFACTS: current.telegramAutoSendArtifacts === current.autoSendArtifacts ? "" : boolValue(current.telegramAutoSendArtifacts),
581
715
  WORKSPACE_ALLOWED_ROOTS: current.workspaceAllowedRoots.join(","),
582
716
  WORKSPACE_WARN_ROOTS: current.workspaceWarnRoots.join(","),
583
717
  NORDRELAY_STATE_BACKEND: current.stateBackend,
584
718
  NORDRELAY_AUDIT_MAX_EVENTS: String(current.auditMaxEvents),
585
719
  NORDRELAY_SESSION_LOCK_TTL_MS: String(current.sessionLockTtlMs),
720
+ NORDRELAY_DASHBOARD_CACHE_TTL_MS: String(current.dashboardCacheTtlMs),
721
+ NORDRELAY_UNIFIED_JOB_MAX_ITEMS: String(current.unifiedJobMaxItems),
586
722
  NORDRELAY_VERSION_CACHE_TTL_MS: process.env.NORDRELAY_VERSION_CACHE_TTL_MS,
587
723
  NORDRELAY_CLI_VERSION_CACHE_TTL_MS: process.env.NORDRELAY_CLI_VERSION_CACHE_TTL_MS,
588
724
  VOICE_PREFERRED_BACKEND: current.voicePreferredBackend,
@@ -601,6 +737,12 @@ function activeSettingsValues(current) {
601
737
  function boolValue(value) {
602
738
  return value ? "true" : "false";
603
739
  }
740
+ function quietValue(value) {
741
+ return value ? `${value.startHour}-${value.endHour}` : "";
742
+ }
743
+ function quietOverrideValue(channelValue, defaultValue) {
744
+ return quietValue(channelValue) === quietValue(defaultValue) ? "" : quietValue(channelValue);
745
+ }
604
746
  function requireArg(argv, index, flag) {
605
747
  const value = argv[index];
606
748
  if (!value || value.startsWith("--")) {
package/dist/web-state.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { activityActorLabel, activityCategoryForType, } from "./activity-events.js";
2
3
  import { createDocumentStore } from "./state-backend.js";
3
4
  const DEFAULT_CHAT_LIMIT = 300;
4
5
  const DEFAULT_ACTIVITY_LIMIT = 1000;
@@ -76,6 +77,7 @@ export class WebActivityStore {
76
77
  id: randomId(),
77
78
  timestamp: input.timestamp ?? new Date().toISOString(),
78
79
  ...input,
80
+ category: input.category ?? activityCategoryForType(input.type),
79
81
  };
80
82
  payload.events.push(event);
81
83
  if (payload.events.length > this.maxEvents) {
@@ -86,9 +88,17 @@ export class WebActivityStore {
86
88
  }
87
89
  list(options = {}) {
88
90
  const limit = Math.max(1, Math.min(500, options.limit ?? 100));
91
+ const since = normalizeSince(options.since);
89
92
  return this.readPayload().events
90
93
  .filter((event) => !options.source || options.source === "all" || event.source === options.source)
91
94
  .filter((event) => !options.status || options.status === "all" || event.status === options.status)
95
+ .filter((event) => !options.category || options.category === "all" || (event.category ?? activityCategoryForType(event.type)) === options.category)
96
+ .filter((event) => !options.agentId || options.agentId === "all" || event.agentId === options.agentId)
97
+ .filter((event) => !options.threadId || event.threadId === options.threadId)
98
+ .filter((event) => !options.workspace || event.workspace === options.workspace)
99
+ .filter((event) => !options.type || event.type.toLowerCase().includes(options.type.toLowerCase()))
100
+ .filter((event) => !options.actor || activityActorMatches(event.actor, options.actor))
101
+ .filter((event) => !since || Date.parse(event.timestamp) >= since)
92
102
  .slice(-limit)
93
103
  .reverse();
94
104
  }
@@ -113,7 +123,7 @@ function isWebChatMessage(value) {
113
123
  typeof candidate.text === "string" &&
114
124
  typeof candidate.timestamp === "string" &&
115
125
  ["user", "agent", "system", "tool"].includes(candidate.role) &&
116
- ["web", "cli"].includes(candidate.source);
126
+ ["web", "telegram", "discord", "cli"].includes(candidate.source);
117
127
  }
118
128
  function isWebActivityEvent(value) {
119
129
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -123,9 +133,30 @@ function isWebActivityEvent(value) {
123
133
  return typeof candidate.id === "string" &&
124
134
  typeof candidate.timestamp === "string" &&
125
135
  typeof candidate.type === "string" &&
126
- ["web", "cli"].includes(candidate.source) &&
136
+ ["web", "telegram", "discord", "cli"].includes(candidate.source) &&
127
137
  ["queued", "running", "completed", "failed", "aborted", "info"].includes(candidate.status);
128
138
  }
139
+ function normalizeSince(value) {
140
+ if (value === undefined || value === null || value === "") {
141
+ return null;
142
+ }
143
+ const time = typeof value === "number" ? value : Date.parse(value);
144
+ return Number.isFinite(time) ? time : null;
145
+ }
146
+ function activityActorMatches(actor, query) {
147
+ const needle = query.trim().toLowerCase();
148
+ if (!needle) {
149
+ return true;
150
+ }
151
+ return [
152
+ activityActorLabel(actor),
153
+ actor?.id,
154
+ actor?.username,
155
+ actor?.channelUserId,
156
+ actor?.channel,
157
+ ].some((value) => String(value ?? "").toLowerCase().includes(needle));
158
+ }
129
159
  function randomId() {
130
160
  return randomUUID().replace(/-/g, "").slice(0, 12);
131
161
  }
162
+ export { activityActorLabel, activityCategoryForType, auditCategoryForAction, } from "./activity-events.js";
@@ -79,6 +79,41 @@
79
79
  width: 100%;
80
80
  }
81
81
  }
82
+ .access-tab {
83
+ display: none;
84
+ }
85
+ .access-tab.active {
86
+ display: block;
87
+ }
88
+ .access-tab-heading {
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: space-between;
92
+ gap: 12px;
93
+ margin: 0 0 10px;
94
+ }
95
+ .access-tab-heading h2 {
96
+ margin: 0;
97
+ }
98
+ .access-tab-heading input {
99
+ max-width: 320px;
100
+ }
101
+ .access-id-row {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 8px;
105
+ flex-wrap: wrap;
106
+ }
107
+ @media (max-width: 560px) {
108
+ .access-tab-heading {
109
+ align-items: stretch;
110
+ flex-direction: column;
111
+ }
112
+ .access-tab-heading input {
113
+ max-width: none;
114
+ width: 100%;
115
+ }
116
+ }
82
117
  .drop-active {
83
118
  outline: 2px dashed var(--accent);
84
119
  outline-offset: -8px;
@@ -132,6 +167,16 @@
132
167
  text-overflow: ellipsis;
133
168
  white-space: nowrap;
134
169
  }
170
+ #activeSessions {
171
+ display: grid;
172
+ grid-template-columns: repeat(2, minmax(0, 1fr));
173
+ gap: 8px;
174
+ }
175
+ @media (min-width: 1320px) {
176
+ #activeSessions {
177
+ grid-template-columns: repeat(3, minmax(0, 1fr));
178
+ }
179
+ }
135
180
  .overview-adapter-grid {
136
181
  display: grid;
137
182
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -152,6 +197,31 @@
152
197
  .setting.dirty {
153
198
  border-color: var(--accent);
154
199
  }
200
+ .setting-label {
201
+ display: flex !important;
202
+ align-items: center;
203
+ gap: 6px;
204
+ }
205
+ .setting-info {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ width: 18px;
210
+ height: 18px;
211
+ border: 1px solid var(--border);
212
+ border-radius: 50%;
213
+ color: var(--muted);
214
+ background: var(--surface);
215
+ font-size: 12px;
216
+ line-height: 1;
217
+ cursor: help;
218
+ font-weight: 700;
219
+ flex: 0 0 auto;
220
+ }
221
+ .setting-info:focus {
222
+ outline: 2px solid var(--accent);
223
+ outline-offset: 2px;
224
+ }
155
225
  .setting-actions {
156
226
  display: flex;
157
227
  gap: 8px;
@@ -169,7 +239,8 @@
169
239
  padding: 10px;
170
240
  margin: 0 0 12px;
171
241
  }
172
- .task-grid {
242
+ .task-grid,
243
+ .metrics-grid {
173
244
  display: grid;
174
245
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
175
246
  gap: 10px;
@@ -914,6 +985,9 @@ dialog::backdrop {
914
985
  .page {
915
986
  padding: 14px;
916
987
  }
988
+ #activeSessions {
989
+ grid-template-columns: 1fr;
990
+ }
917
991
  .overview-adapter-grid {
918
992
  grid-template-columns: 1fr;
919
993
  }