@@ -3,545 +3,2476 @@
3 3 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<head>
4 4 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<meta charset="UTF-8">
5 5 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6 6 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<title>scuttlebot</title>
7 7 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<style>
8 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- * { box-sizing: border-box; margin: 0; padding: 0; }
9 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- body { font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace; background: #0d1117; color: #e6edf3; min-height: 100vh; }
10 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- header { background: #161b22; border-bottom: 1px solid #30363d; padding: 12px 24px; display: flex; align-items: center; gap: 16px; }
11 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- header h1 { font-size: 16px; color: #58a6ff; letter-spacing: 0.05em; }
12 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- header .tagline { font-size: 12px; color: #8b949e; }
13 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- header .spacer { flex: 1; }
14 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .token-badge { font-size: 12px; color: #8b949e; display: flex; align-items: center; gap: 8px; }
15 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .token-badge code { background: #21262d; border: 1px solid #30363d; border-radius: 4px; padding: 2px 8px; color: #a5d6ff; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
16 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- button { cursor: pointer; border: 1px solid #30363d; border-radius: 6px; padding: 6px 12px; font-size: 13px; font-family: inherit; background: #21262d; color: #e6edf3; transition: background 0.1s; }
17 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- button:hover { background: #30363d; }
18 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- button.primary { background: #1f6feb; border-color: #1f6feb; color: #fff; }
19 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- button.primary:hover { background: #388bfd; }
20 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- button.danger { background: #21262d; border-color: #f85149; color: #f85149; }
21 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- button.danger:hover { background: #3d1f1e; }
22 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- button.small { padding: 3px 8px; font-size: 12px; }
23 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- main { max-width: 960px; margin: 0 auto; padding: 24px; display: flex; flex-direction: column; gap: 24px; }
24 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
25 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .card-header { padding: 12px 16px; border-bottom: 1px solid #30363d; display: flex; align-items: center; gap: 8px; }
26 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .card-header h2 { font-size: 14px; color: #e6edf3; font-weight: 600; }
27 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .card-header .badge { background: #1f6feb22; border: 1px solid #1f6feb44; color: #58a6ff; border-radius: 999px; padding: 1px 8px; font-size: 12px; }
28 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .card-body { padding: 16px; }
29 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
30 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .stat { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; padding: 12px 16px; }
31 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .stat .label { font-size: 11px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px; }
32 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .stat .value { font-size: 20px; color: #58a6ff; font-weight: 600; }
33 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .stat .sub { font-size: 11px; color: #8b949e; margin-top: 2px; }
34 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
35 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .dot.green { background: #3fb950; box-shadow: 0 0 6px #3fb950aa; }
36 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .dot.red { background: #f85149; }
37 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
38 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- th { text-align: left; padding: 8px 12px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: #8b949e; border-bottom: 1px solid #21262d; }
39 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- td { padding: 10px 12px; border-bottom: 1px solid #21262d; color: #e6edf3; vertical-align: top; }
40 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- tr:last-child td { border-bottom: none; }
41 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- tr:hover td { background: #1c2128; }
42 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .tag { display: inline-block; background: #1f6feb22; border: 1px solid #1f6feb44; color: #79c0ff; border-radius: 4px; padding: 1px 6px; font-size: 11px; margin: 1px; }
43 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .tag.type-operator { background: #db613622; border-color: #db613644; color: #ffa657; }
44 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .tag.type-orchestrator { background: #8957e522; border-color: #8957e544; color: #d2a8ff; }
45 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .tag.type-worker { background: #1f6feb22; border-color: #1f6feb44; color: #79c0ff; }
46 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .tag.type-observer { background: #21262d; border-color: #30363d; color: #8b949e; }
47 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .tag.revoked { background: #f8514922; border-color: #f8514944; color: #ff7b72; }
48 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .actions { display: flex; gap: 6px; flex-wrap: wrap; }
49 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .empty { color: #8b949e; font-size: 13px; text-align: center; padding: 24px; }
50 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .chan-item { padding: 8px 12px; font-size: 13px; cursor: pointer; color: #8b949e; border-bottom: 1px solid #21262d; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
51 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .chan-item:hover { background: #1c2128; color: #e6edf3; }
52 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .chan-item.active { background: #1f6feb22; color: #58a6ff; border-left: 2px solid #58a6ff; }
53 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .msg-row { display: flex; gap: 8px; font-size: 13px; line-height: 1.6; padding: 1px 0; }
54 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .msg-time { color: #8b949e; font-size: 11px; flex-shrink: 0; padding-top: 3px; min-width: 44px; }
55 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .msg-nick { color: #58a6ff; font-weight: 600; flex-shrink: 0; min-width: 80px; text-align: right; }
56 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .msg-nick.bridge-nick { color: #3fb950; }
57 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .msg-text { color: #e6edf3; word-break: break-word; }
58 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- form { display: flex; flex-direction: column; gap: 14px; }
59 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
60 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- label { display: block; font-size: 12px; color: #8b949e; margin-bottom: 4px; }
61 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- input, select, textarea { width: 100%; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 8px 10px; font-size: 13px; font-family: inherit; color: #e6edf3; outline: none; transition: border-color 0.1s; }
62 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- input:focus, select:focus, textarea:focus { border-color: #58a6ff; }
63 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- select option { background: #161b22; }
64 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- textarea { resize: vertical; min-height: 60px; }
65 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .hint { font-size: 11px; color: #8b949e; margin-top: 3px; }
66 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .alert { border-radius: 6px; padding: 12px 14px; font-size: 13px; display: flex; gap: 10px; align-items: flex-start; }
67 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .alert.info { background: #1f6feb1a; border: 1px solid #1f6feb44; color: #79c0ff; }
68 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .alert.error { background: #f851491a; border: 1px solid #f8514944; color: #ff7b72; }
69 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .alert.success { background: #3fb9501a; border: 1px solid #3fb95044; color: #7ee787; }
70 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .alert .icon { flex-shrink: 0; font-size: 16px; line-height: 1.3; }
71 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .cred-box { background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 12px; font-size: 12px; }
72 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .cred-box .cred-row { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
73 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .cred-box .cred-row:last-child { margin-bottom: 0; }
74 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .cred-box .cred-key { color: #8b949e; min-width: 90px; }
75 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .cred-box .cred-val { color: #a5d6ff; word-break: break-all; flex: 1; }
76 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .cred-box .copy-btn { flex-shrink: 0; }
77 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .modal-overlay { display: none; position: fixed; inset: 0; background: #0d111788; z-index: 100; align-items: center; justify-content: center; }
78 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .modal-overlay.open { display: flex; }
79 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .modal { background: #161b22; border: 1px solid #30363d; border-radius: 10px; padding: 24px; width: 480px; max-width: 95vw; }
80 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .modal h3 { font-size: 15px; margin-bottom: 16px; }
81 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- .modal .btn-row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
8 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ body { font-family: ui-monospace,'Cascadia Code','Source Code Pro',monospace; background:#0d1117; color:#e6edf3; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
10 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
11 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* header */
12 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ header { background:#161b22; border-bottom:1px solid #30363d; padding:0 20px; display:flex; align-items:stretch; flex-shrink:0; height:48px; }
13 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .brand { display:flex; align-items:center; gap:8px; padding-right:20px; border-right:1px solid #30363d; margin-right:4px; }
14 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .brand h1 { font-size:14px; color:#58a6ff; letter-spacing:.05em; }
15 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .brand span { font-size:11px; color:#8b949e; }
16 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ nav { display:flex; align-items:stretch; flex:1; }
17 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .nav-tab { display:flex; align-items:center; gap:6px; padding:0 14px; font-size:13px; color:#8b949e; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-1px; transition:color .1s; white-space:nowrap; }
18 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .nav-tab:hover { color:#c9d1d9; }
19 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .nav-tab.active { color:#e6edf3; border-bottom-color:#58a6ff; }
20 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .header-right { display:flex; align-items:center; gap:8px; margin-left:auto; font-size:12px; color:#8b949e; }
21 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .header-right code { background:#21262d; border:1px solid #30363d; border-radius:4px; padding:2px 7px; color:#a5d6ff; max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
22 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
23 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* tab panes */
24 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .tab-pane { display:none; flex:1; min-height:0; overflow-y:auto; }
25 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .tab-pane.active { display:flex; flex-direction:column; }
26 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .pane-scroll { flex:1; overflow-y:auto; }
27 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .pane-inner { max-width:1000px; margin:0 auto; padding:24px; display:flex; flex-direction:column; gap:20px; }
28 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
29 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* cards */
30 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .card { background:#161b22; border:1px solid #30363d; border-radius:8px; overflow:hidden; }
31 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .card-header { padding:12px 16px; border-bottom:1px solid #30363d; display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none; }
32 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .card-header:hover { background:#1c2128; }
33 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .card-header h2 { font-size:14px; font-weight:600; }
34 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .card-header .collapse-icon { font-size:11px; color:#8b949e; margin-left:2px; transition:transform .15s; }
35 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .card.collapsed .card-header { border-bottom:none; }
36 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .card.collapsed .card-body { display:none; }
37 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .card.collapsed .collapse-icon { transform:rotate(-90deg); }
38 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .card-body { padding:16px; }
39 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* behavior config panel */
40 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .beh-config { background:#0d1117; border-top:1px solid #21262d; padding:14px 16px 14px 42px; display:none; }
41 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .beh-config.open { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:12px; }
42 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .beh-field label { display:block; font-size:11px; color:#8b949e; margin-bottom:3px; }
43 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .beh-field input[type=text],.beh-field input[type=number],.beh-field select { width:100%; }
44 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .beh-field .hint { font-size:10px; color:#6e7681; margin-top:2px; }
45 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .spacer { flex:1; }
46 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .badge { background:#1f6feb22; border:1px solid #1f6feb44; color:#58a6ff; border-radius:999px; padding:1px 8px; font-size:12px; }
47 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
48 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* status */
49 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .stat-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; }
50 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .stat { background:#0d1117; border:1px solid #21262d; border-radius:6px; padding:12px 16px; }
51 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .stat .lbl { font-size:11px; color:#8b949e; text-transform:uppercase; letter-spacing:.06em; margin-bottom:4px; }
52 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .stat .val { font-size:20px; color:#58a6ff; font-weight:600; }
53 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .stat .sub { font-size:11px; color:#8b949e; margin-top:2px; }
54 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:5px; }
55 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .dot.green { background:#3fb950; box-shadow:0 0 6px #3fb950aa; }
56 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .dot.red { background:#f85149; }
57 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
58 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* table */
59 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ table { width:100%; border-collapse:collapse; font-size:13px; }
60 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ th { text-align:left; padding:8px 12px; font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:#8b949e; border-bottom:1px solid #21262d; white-space:nowrap; }
61 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ td { padding:9px 12px; border-bottom:1px solid #21262d; color:#e6edf3; vertical-align:middle; }
62 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tr:last-child td { border-bottom:none; }
63 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tr:hover td { background:#1c2128; }
64 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
65 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* tags */
66 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .tag { display:inline-block; border-radius:4px; padding:1px 6px; font-size:11px; margin:1px; border:1px solid; }
67 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .tag.ch { background:#1f6feb22; border-color:#1f6feb44; color:#79c0ff; }
68 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .tag.perm{ background:#21262d; border-color:#30363d; color:#8b949e; }
69 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .tag.type-operator { background:#db613622; border-color:#db613644; color:#ffa657; }
70 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .tag.type-orchestrator { background:#8957e522; border-color:#8957e544; color:#d2a8ff; }
71 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .tag.type-worker { background:#1f6feb22; border-color:#1f6feb44; color:#79c0ff; }
72 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .tag.type-observer { background:#21262d; border-color:#30363d; color:#8b949e; }
73 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .tag.revoked { background:#f8514922; border-color:#f8514944; color:#ff7b72; }
74 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
75 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* buttons */
76 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ button { cursor:pointer; border:1px solid #30363d; border-radius:6px; padding:6px 12px; font-size:13px; font-family:inherit; background:#21262d; color:#e6edf3; transition:background .1s; }
77 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ button:hover:not(:disabled) { background:#30363d; }
78 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ button:disabled { opacity:.5; cursor:default; }
79 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ button.primary { background:#1f6feb; border-color:#1f6feb; color:#fff; }
80 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ button.primary:hover:not(:disabled) { background:#388bfd; }
81 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ button.danger { border-color:#f85149; color:#f85149; }
82 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ button.danger:hover:not(:disabled) { background:#3d1f1e; }
83 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ button.sm { padding:3px 8px; font-size:12px; }
84 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .actions { display:flex; gap:6px; }
85 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
86 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* forms */
87 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ form { display:flex; flex-direction:column; gap:14px; }
88 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .form-row { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
89 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ label { display:block; font-size:12px; color:#8b949e; margin-bottom:4px; }
90 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input,select,textarea { width:100%; background:#0d1117; border:1px solid #30363d; border-radius:6px; padding:8px 10px; font-size:13px; font-family:inherit; color:#e6edf3; outline:none; transition:border-color .1s; }
91 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input:focus,select:focus,textarea:focus { border-color:#58a6ff; }
92 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ select option { background:#161b22; }
93 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .hint { font-size:11px; color:#8b949e; margin-top:3px; }
94 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
95 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* alerts */
96 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .alert { border-radius:6px; padding:12px 14px; font-size:13px; display:flex; gap:10px; align-items:flex-start; }
97 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .alert.info { background:#1f6feb1a; border:1px solid #1f6feb44; color:#79c0ff; }
98 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .alert.error { background:#f851491a; border:1px solid #f8514944; color:#ff7b72; }
99 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .alert.success { background:#3fb9501a; border:1px solid #3fb95044; color:#7ee787; }
100 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .alert .icon { flex-shrink:0; font-size:15px; line-height:1.4; }
101 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .cred-box { background:#0d1117; border:1px solid #30363d; border-radius:6px; padding:12px; font-size:12px; margin-top:10px; }
102 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .cred-row { display:flex; align-items:baseline; gap:8px; margin-bottom:6px; }
103 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .cred-row:last-child { margin-bottom:0; }
104 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .cred-key { color:#8b949e; min-width:90px; }
105 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .cred-val { color:#a5d6ff; word-break:break-all; flex:1; }
106 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
107 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* search/filter bar */
108 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .filter-bar { display:flex; gap:8px; align-items:center; padding:10px 16px; border-bottom:1px solid #30363d; }
109 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .filter-bar input { max-width:280px; padding:5px 10px; }
110 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
111 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* empty */
112 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .empty { color:#8b949e; font-size:13px; text-align:center; padding:28px; }
113 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
114 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* drawer */
115 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .drawer-overlay { display:none; position:fixed; inset:0; background:#0d111760; z-index:50; }
116 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .drawer-overlay.open { display:block; }
117 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .drawer { position:fixed; top:48px; right:0; bottom:0; width:480px; max-width:95vw; background:#161b22; border-left:1px solid #30363d; transform:translateX(100%); transition:transform .2s; z-index:51; display:flex; flex-direction:column; }
118 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .drawer.open { transform:translateX(0); }
119 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .drawer-header { padding:16px 20px; border-bottom:1px solid #30363d; display:flex; align-items:center; gap:8px; flex-shrink:0; }
120 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .drawer-header h3 { font-size:14px; font-weight:600; flex:1; }
121 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .drawer-body { flex:1; overflow-y:auto; padding:20px; }
122 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
123 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* chat */
124 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ #pane-chat { flex-direction:row; overflow:hidden; }
125 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-sidebar { width:180px; min-width:0; flex-shrink:0; border-right:1px solid #30363d; display:flex; flex-direction:column; background:#161b22; overflow:hidden; transition:width .15s; }
126 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-sidebar.collapsed { width:28px; }
127 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-sidebar.collapsed .chan-join,.chat-sidebar.collapsed .chan-list { display:none; }
128 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .sidebar-head { padding:8px 12px; font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:#8b949e; border-bottom:1px solid #30363d; flex-shrink:0; display:flex; align-items:center; gap:4px; }
129 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .sidebar-toggle { margin-left:auto; background:none; border:none; color:#8b949e; cursor:pointer; font-size:14px; padding:0 2px; line-height:1; }
130 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .sidebar-toggle:hover { color:#e6edf3; }
131 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .sidebar-resize { width:4px; flex-shrink:0; cursor:col-resize; background:transparent; transition:background .1s; z-index:10; }
132 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .sidebar-resize:hover,.sidebar-resize.dragging { background:#58a6ff55; }
133 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-join { display:flex; gap:5px; padding:7px 9px; border-bottom:1px solid #21262d; flex-shrink:0; }
134 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-join input { flex:1; padding:4px 7px; font-size:12px; }
135 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-list { flex:1; overflow-y:auto; }
136 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-item { padding:7px 14px; font-size:13px; cursor:pointer; color:#8b949e; border-bottom:1px solid #21262d; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
137 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-item:hover { background:#1c2128; color:#e6edf3; }
138 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-item.active { background:#1f6feb22; color:#58a6ff; border-left:2px solid #58a6ff; padding-left:12px; }
139 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-main { flex:1; display:flex; flex-direction:column; min-width:0; }
140 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-topbar { padding:9px 16px; border-bottom:1px solid #30363d; display:flex; align-items:center; gap:10px; flex-shrink:0; background:#161b22; font-size:13px; }
141 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-ch-name { font-weight:600; color:#58a6ff; }
142 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .stream-badge { font-size:11px; color:#8b949e; margin-left:auto; }
143 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-msgs { flex:1; overflow-y:auto; padding:10px 16px; display:flex; flex-direction:column; gap:1px; }
144 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .msg-row { display:flex; gap:10px; font-size:13px; line-height:1.6; padding:1px 0; }
145 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .msg-time { color:#8b949e; font-size:11px; flex-shrink:0; padding-top:3px; min-width:40px; }
146 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .msg-nick { font-weight:600; flex-shrink:0; min-width:90px; text-align:right; }
147 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .msg-grouped .msg-nick { visibility:hidden; }
148 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .msg-grouped .msg-time { color:transparent; }
149 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .msg-grouped:hover .msg-time { color:#8b949e; transition:color .1s; }
150 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-nicklist { width:148px; min-width:0; flex-shrink:0; border-left:1px solid #30363d; display:flex; flex-direction:column; background:#161b22; overflow-y:auto; overflow-x:hidden; transition:width .15s; }
151 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-nicklist.collapsed { width:28px; overflow:hidden; }
152 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-nicklist.collapsed #nicklist-users { display:none; }
153 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .nicklist-head { padding:8px 12px; font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:#8b949e; border-bottom:1px solid #30363d; flex-shrink:0; display:flex; align-items:center; gap:4px; }
154 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .nicklist-nick { padding:5px 12px; font-size:12px; color:#8b949e; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
155 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .nicklist-nick.is-bot { color:#58a6ff; }
156 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .nicklist-nick::before { content:"● "; font-size:8px; vertical-align:middle; }
157 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-new-banner { align-self:center; margin:4px auto 0; background:#1f6feb; color:#fff; border-radius:20px; padding:3px 14px; font-size:12px; cursor:pointer; display:inline-block; white-space:nowrap; }
158 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* login screen */
159 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .login-screen { position:fixed; inset:0; background:#0d1117; z-index:200; display:flex; align-items:center; justify-content:center; flex-direction:column; }
160 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .login-box { width:340px; }
161 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .login-brand { text-align:center; margin-bottom:24px; }
162 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .login-brand h1 { font-size:22px; color:#58a6ff; letter-spacing:.05em; }
163 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .login-brand p { color:#8b949e; font-size:13px; margin-top:6px; }
164 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* unread badge on chat tab */
165 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .nav-tab[data-unread]::after { content:attr(data-unread); background:#f85149; color:#fff; border-radius:999px; padding:1px 5px; font-size:10px; margin-left:5px; vertical-align:middle; }
166 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .msg-text { color:#e6edf3; word-break:break-word; }
167 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chat-input { padding:9px 13px; border-top:1px solid #30363d; display:flex; gap:7px; flex-shrink:0; background:#161b22; }
168 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
169 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* channels tab */
170 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-card { display:flex; align-items:center; gap:12px; padding:12px 16px; border-bottom:1px solid #21262d; }
171 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-card:last-child { border-bottom:none; }
172 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-card:hover { background:#1c2128; }
173 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-name { font-size:14px; font-weight:600; color:#58a6ff; }
174 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chan-meta { font-size:12px; color:#8b949e; }
175 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
176 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* settings */
177 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .setting-row { display:flex; align-items:center; gap:12px; padding:14px 0; border-bottom:1px solid #21262d; }
178 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .setting-row:last-child { border-bottom:none; }
179 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .setting-label { min-width:160px; font-size:13px; color:#c9d1d9; }
180 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .setting-desc { font-size:12px; color:#8b949e; flex:1; }
181 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .setting-val { font-size:12px; font-family:inherit; color:#a5d6ff; background:#0d1117; border:1px solid #30363d; border-radius:4px; padding:4px 10px; }
182 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
183 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* modal */
184 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .modal-overlay { display:none; position:fixed; inset:0; background:#0d111788; z-index:100; align-items:center; justify-content:center; }
185 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .modal-overlay.open { display:flex; }
186 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .modal { background:#161b22; border:1px solid #30363d; border-radius:10px; padding:24px; width:480px; max-width:95vw; }
187 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .modal h3 { font-size:15px; margin-bottom:16px; }
188 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .modal .btn-row { display:flex; justify-content:flex-end; gap:8px; margin-top:16px; }
189 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ /* charts */
190 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .charts-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:16px; }
191 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chart-card { background:#161b22; border:1px solid #30363d; border-radius:8px; padding:14px 16px; }
192 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chart-label { font-size:11px; color:#8b949e; text-transform:uppercase; letter-spacing:.06em; margin-bottom:10px; display:flex; align-items:center; gap:6px; }
193 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .chart-label .val { margin-left:auto; font-size:13px; color:#e6edf3; font-weight:600; letter-spacing:0; text-transform:none; }
194 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ canvas { display:block; width:100% !important; }
195 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .bridge-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }
82 196 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</style>
197 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <script src="https://cdn.jsdelivr.net/npm/[email protected] /dist/chart.umd.min.js"></script>
83 198 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</head>
84 199 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<body>
200 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
201 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- login screen — shown when unauthenticated -->
202 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="login-screen" id="login-screen" style="display:none">
203 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="login-box">
204 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="login-brand">
205 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h1>⬡ scuttlebot</h1>
206 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <p>agent coordination backplane</p>
207 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
208 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card">
209 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
210 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <form id="login-form" onsubmit="handleLogin(event)" style="gap:12px">
211 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
212 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>username</label>
213 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="login-username" autocomplete="username">
214 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
215 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
216 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>password</label>
217 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="password" id="login-password" autocomplete="current-password">
218 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
219 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="login-error" style="display:none"></div>
220 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button type="submit" class="primary" style="width:100%;margin-top:4px" id="login-btn">sign in</button>
221 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </form>
222 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <details style="margin-top:16px;border-top:1px solid #21262d;padding-top:14px">
223 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <summary style="font-size:12px;color:#8b949e;cursor:pointer;user-select:none">use API token instead</summary>
224 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;gap:8px;margin-top:10px">
225 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="token-login-input" placeholder="paste API token" style="flex:1;font-size:12px" autocomplete="off" spellcheck="false">
226 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm primary" onclick="saveTokenLogin()">apply</button>
227 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
228 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint" style="margin-top:4px">Token is printed to stderr at startup.</div>
229 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </details>
230 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
231 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
232 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <p style="text-align:center;font-size:11px;color:#6e7681;margin-top:14px">
233 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <a href="https://scuttlebot.dev" target="_blank" rel="noopener" style="color:#58a6ff;text-decoration:none">ScuttleBot</a> · Powered by <a href="https://weareconflict.com" target="_blank" rel="noopener" style="color:#58a6ff;text-decoration:none">CONFLICT</a>
234 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </p>
235 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
236 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
85 237 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
86 238 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
<header>
87 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <h1>⬡ scuttlebot</h1>
88 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="tagline">agent coordination backplane</span>
89 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="spacer"></div>
90 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="token-badge">
91 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span>token:</span>
92 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <code id="token-display">not set</code>
93 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <button class="small" onclick="openTokenModal()">set</button>
239 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="brand">
240 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h1>⬡ scuttlebot</h1>
241 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span>agent coordination backplane</span>
242 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
243 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <nav>
244 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="nav-tab active" id="tab-status" onclick="switchTab('status')">◈ status</div>
245 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="nav-tab" id="tab-users" onclick="switchTab('users')">◉ users</div>
246 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="nav-tab" id="tab-agents" onclick="switchTab('agents')">◎ agents</div>
247 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="nav-tab" id="tab-channels" onclick="switchTab('channels')">◎ channels</div>
248 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="nav-tab" id="tab-chat" onclick="switchTab('chat')">◌ chat</div>
249 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="nav-tab" id="tab-ai" onclick="switchTab('ai')">✦ ai</div>
250 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="nav-tab" id="tab-settings" onclick="switchTab('settings')">⚙ settings</div>
251 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </nav>
252 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="header-right">
253 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span id="header-user-display" style="font-size:12px;color:#8b949e"></span>
254 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="logout()">sign out</button>
94 255 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</div>
95 256 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</header>
96 257 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
97 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <main>
98 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div id="no-token-banner" class="alert info" style="display:none">
99 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="icon">ℹ</span>
100 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span>Paste your API token to continue. It was printed to stderr when scuttlebot started:<br><code style="color:#a5d6ff">level=INFO msg="api token" token=...</code></span>
101 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
102 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
103 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <!-- Status -->
104 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card">
105 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card-header">
106 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="dot green" id="status-dot"></span>
107 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <h2>status</h2>
108 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
109 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card-body">
110 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="status-grid" id="status-grid">
111 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="stat"><div class="label">state</div><div class="value" id="stat-status">—</div></div>
112 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="stat"><div class="label">uptime</div><div class="value" id="stat-uptime">—</div></div>
113 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="stat"><div class="label">agents</div><div class="value" id="stat-agents">—</div></div>
114 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="stat"><div class="label">started</div><div class="value" style="font-size:13px" id="stat-started">—</div><div class="sub" id="stat-started-rel"></div></div>
115 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
116 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div id="status-error" style="margin-top:12px;display:none"></div>
117 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
118 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
119 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
120 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <!-- Agents -->
121 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card">
122 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card-header">
123 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <h2>agents</h2>
124 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="badge" id="agent-count">0</span>
125 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="spacer"></div>
126 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <button class="small" onclick="loadAgents()">↻ refresh</button>
127 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
128 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card-body" style="padding:0">
129 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div id="agents-container"></div>
130 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
131 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
132 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
133 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <!-- Register -->
134 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card">
135 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card-header"><h2>register agent</h2></div>
136 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card-body">
137 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <form id="register-form" onsubmit="handleRegister(event)">
138 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="form-row">
139 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div>
140 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <label>nick *</label>
141 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <input type="text" id="reg-nick" placeholder="my-agent-01" required>
142 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
143 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div>
144 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <label>type</label>
145 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <select id="reg-type">
146 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <option value="operator">operator — human, +o + full permissions</option>
147 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <option value="worker">worker — gets +v in channels</option>
148 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <option value="orchestrator">orchestrator — gets +o in channels</option>
149 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <option value="observer">observer — read-only</option>
150 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </select>
151 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
152 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
153 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div>
154 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <label>channels</label>
155 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <input type="text" id="reg-channels" placeholder="#fleet, #ops, #project.foo">
156 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="hint">comma-separated; must start with #</div>
157 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
158 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div>
159 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <label>permissions</label>
160 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <input type="text" id="reg-permissions" placeholder="task.create, task.update">
161 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="hint">comma-separated message types this agent is allowed to send</div>
162 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
163 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div id="register-result" style="display:none"></div>
164 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div style="display:flex;justify-content:flex-end">
165 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <button type="submit" class="primary">register</button>
166 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
167 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </form>
168 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
169 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
170 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
171 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <!-- Chat -->
172 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card" id="chat-card" style="display:none">
173 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="card-header">
174 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <h2>chat</h2>
175 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="badge" id="chat-channel-badge" style="display:none"></span>
176 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="spacer"></div>
177 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span id="chat-stream-status" style="font-size:11px;color:#8b949e"></span>
178 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
179 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div style="display:flex;height:440px">
180 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div id="chat-channel-list" style="width:140px;border-right:1px solid #30363d;overflow-y:auto;flex-shrink:0;padding:8px 0;display:flex;flex-direction:column">
181 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div style="padding:6px 12px;font-size:11px;text-transform:uppercase;letter-spacing:0.06em;color:#8b949e">channels</div>
182 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div style="padding:6px 8px;border-bottom:1px solid #21262d;display:flex;gap:4px">
183 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <input type="text" id="join-channel-input" placeholder="#channel" style="flex:1;font-size:11px;padding:3px 6px" autocomplete="off">
184 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <button class="small" onclick="joinChannel()" style="padding:3px 6px;font-size:11px">+</button>
185 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
186 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
187 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div style="display:flex;flex-direction:column;flex:1;min-width:0">
188 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div id="chat-messages" style="flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:2px">
189 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="empty" id="chat-placeholder">select a channel to view messages</div>
190 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
191 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div style="padding:10px 14px;border-top:1px solid #30363d;display:flex;gap:8px;align-items:center">
192 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <input type="text" id="chat-nick-input" placeholder="your nick" style="width:110px;flex-shrink:0" autocomplete="off">
193 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <input type="text" id="chat-text-input" placeholder="type a message…" style="flex:1" autocomplete="off">
194 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <button class="primary small" id="chat-send-btn" onclick="sendChatMessage()">send</button>
195 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
196 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
197 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
198 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
199 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </main>
200 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
201 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <!-- Token modal -->
202 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="modal-overlay" id="token-modal">
203 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="modal">
204 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <h3>set API token</h3>
205 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <p style="font-size:13px;color:#8b949e;margin-bottom:14px">The token is printed to stderr when scuttlebot starts:<br><code style="color:#a5d6ff">level=INFO msg="api token" token=<value></code></p>
206 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <label>token</label>
207 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <input type="text" id="token-input" placeholder="paste token here" autocomplete="off" spellcheck="false">
208 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="btn-row">
209 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <button onclick="closeTokenModal()">cancel</button>
210 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <button class="primary" onclick="saveToken()">save</button>
211 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
212 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
213 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
214 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
215 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <script>
216 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- // --- token management ---
217 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function getToken() { return localStorage.getItem('scuttlebot_token') || ''; }
218 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function setToken(t) {
219 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- localStorage.setItem('scuttlebot_token', t);
220 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- updateTokenDisplay();
221 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
222 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function updateTokenDisplay() {
223 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const t = getToken();
224 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const el = document.getElementById('token-display');
225 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const banner = document.getElementById('no-token-banner');
226 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (t) {
227 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- el.textContent = t.slice(0, 8) + '…' + t.slice(-4);
228 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- banner.style.display = 'none';
229 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } else {
230 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- el.textContent = 'not set';
231 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- banner.style.display = 'flex';
232 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
233 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
234 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function openTokenModal() {
235 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('token-input').value = getToken();
236 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('token-modal').classList.add('open');
237 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- setTimeout(() => document.getElementById('token-input').focus(), 50);
238 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
239 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function closeTokenModal() { document.getElementById('token-modal').classList.remove('open'); }
240 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function saveToken() {
241 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const v = document.getElementById('token-input').value.trim();
242 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (v) { setToken(v); closeTokenModal(); loadAll(); }
243 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
244 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('token-modal').addEventListener('click', function(e) {
245 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (e.target === this) closeTokenModal();
246 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- });
247 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.addEventListener('keydown', function(e) {
248 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (e.key === 'Escape') closeTokenModal();
249 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- });
250 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
251 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- // --- API ---
252 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- async function api(method, path, body) {
253 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const token = getToken();
254 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const opts = { method, headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' } };
255 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (body !== undefined) opts.body = JSON.stringify(body);
256 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const res = await fetch(path, opts);
258 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- STATUS -->
259 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="tab-pane active" id="pane-status">
260 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="pane-inner">
261 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="no-token-banner" class="alert info" style="display:none">
262 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span class="icon">ℹ</span>
263 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span>Paste your API token to continue — printed to stderr at startup: <code style="color:#a5d6ff">level=INFO msg="api token" token=…</code></span>
264 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
265 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
266 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- server status card -->
267 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-status">
268 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-status',event)">
269 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span class="dot green" id="status-dot"></span><h2>server status</h2>
270 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span class="collapse-icon">▾</span>
271 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
272 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span style="font-size:11px;color:#8b949e" id="metrics-updated"></span>
273 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="loadStatus()" title="refresh">↻</button>
274 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
275 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
276 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat-grid">
277 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">state</div><div class="val" id="stat-status">—</div></div>
278 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">uptime</div><div class="val" id="stat-uptime">—</div></div>
279 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">agents</div><div class="val" id="stat-agents">—</div></div>
280 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">started</div><div class="val" style="font-size:13px" id="stat-started">—</div><div class="sub" id="stat-started-rel"></div></div>
281 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
282 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="status-error" style="margin-top:12px;display:none"></div>
283 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
284 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
285 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
286 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- runtime -->
287 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-runtime">
288 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-runtime',event)"><h2>runtime</h2><span class="collapse-icon">▾</span></div>
289 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body" style="display:flex;flex-direction:column;gap:16px">
290 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat-grid">
291 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">goroutines</div><div class="val" id="stat-goroutines">—</div></div>
292 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">heap alloc</div><div class="val" id="stat-heap">—</div></div>
293 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">heap sys</div><div class="val" id="stat-heapsys">—</div></div>
294 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">GC runs</div><div class="val" id="stat-gc">—</div></div>
295 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
296 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="charts-grid">
297 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chart-card">
298 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chart-label">heap alloc <span class="val" id="chart-heap-val">—</span></div>
299 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <canvas id="chart-heap" height="80"></canvas>
300 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
301 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chart-card">
302 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chart-label">goroutines <span class="val" id="chart-goroutines-val">—</span></div>
303 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <canvas id="chart-goroutines" height="80"></canvas>
304 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
305 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chart-card">
306 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chart-label">messages total <span class="val" id="chart-messages-val">—</span></div>
307 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <canvas id="chart-messages" height="80"></canvas>
308 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
309 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
310 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
311 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
312 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
313 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- bridge -->
314 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="bridge-card" style="display:none">
315 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('bridge-card',event)"><h2>bridge</h2><span class="collapse-icon">▾</span></div>
316 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
317 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="bridge-grid">
318 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">channels</div><div class="val" id="stat-bridge-channels">—</div></div>
319 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">messages total</div><div class="val" id="stat-bridge-msgs">—</div></div>
320 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">active streams</div><div class="val" id="stat-bridge-subs">—</div></div>
321 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
322 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
323 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
324 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
325 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- registry -->
326 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-registry">
327 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-registry',event)"><h2>registry</h2><span class="collapse-icon">▾</span></div>
328 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
329 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat-grid">
330 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">total</div><div class="val" id="stat-reg-total">—</div></div>
331 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">active</div><div class="val" id="stat-reg-active">—</div></div>
332 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="stat"><div class="lbl">revoked</div><div class="val" id="stat-reg-revoked">—</div></div>
333 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
334 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
335 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
336 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
337 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
338 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
339 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
340 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- USERS -->
341 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="tab-pane" id="pane-users">
342 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="filter-bar">
343 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="user-search" placeholder="search by nick or channel…" oninput="renderUsersTable()" style="max-width:320px">
344 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
345 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span class="badge" id="user-count" style="margin-right:4px">0</span>
346 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="loadAgents()">↻ refresh</button>
347 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="openAdoptDrawer()">adopt existing user</button>
348 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm primary" onclick="openRegisterUserDrawer()">+ register user</button>
349 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
350 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="flex:1;overflow-y:auto">
351 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="users-container"></div>
352 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
353 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
354 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
355 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- AGENTS -->
356 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="tab-pane" id="pane-agents">
357 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="filter-bar">
358 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="agent-search" placeholder="search by nick, type, channel…" oninput="renderAgentTable()" style="max-width:320px">
359 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
360 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span class="badge" id="agent-count" style="margin-right:4px">0</span>
361 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="loadAgents()">↻ refresh</button>
362 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm primary" onclick="openDrawer()">+ register agent</button>
363 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
364 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="flex:1;overflow-y:auto">
365 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="agents-container"></div>
366 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
367 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
368 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
369 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- CHANNELS -->
370 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="tab-pane" id="pane-channels">
371 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="pane-inner">
372 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card">
373 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header">
374 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h2>channels</h2>
375 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span class="badge" id="chan-count">0</span>
376 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
377 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;gap:6px;align-items:center">
378 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="quick-join-input" placeholder="#channel" style="width:160px;padding:5px 8px;font-size:12px" autocomplete="off">
379 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm primary" onclick="quickJoin()">join</button>
380 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
381 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
382 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="channels-list"><div class="empty">no channels joined yet — type a channel name above</div></div>
383 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
384 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
385 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
386 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
387 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- CHAT -->
388 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="tab-pane" id="pane-chat">
389 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chat-sidebar" id="chat-sidebar-left">
390 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="sidebar-head">
391 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span id="sidebar-left-label">channels</span>
392 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sidebar-toggle" id="sidebar-left-toggle" title="collapse" onclick="toggleSidebar('left')">‹</button>
393 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
394 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chan-join">
395 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="join-channel-input" placeholder="#general" autocomplete="off">
396 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="joinChannel()">+</button>
397 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
398 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chan-list" id="chan-list"></div>
399 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
400 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="sidebar-resize" id="resize-left" title="drag to resize"></div>
401 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chat-main">
402 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chat-topbar">
403 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span class="chat-ch-name" id="chat-ch-name">select a channel</span>
404 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
405 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span style="font-size:11px;color:#8b949e;margin-right:6px">chatting as</span>
406 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <select id="chat-identity" style="width:140px;padding:3px 6px;font-size:12px" onchange="saveChatIdentity()">
407 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="">— pick a user —</option>
408 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </select>
409 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span class="stream-badge" id="chat-stream-status" style="margin-left:8px"></span>
410 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
411 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chat-msgs" id="chat-msgs">
412 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="empty" id="chat-placeholder">join a channel to start chatting</div>
413 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
414 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chat-input">
415 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="chat-text-input" placeholder="type a message…" style="flex:1" autocomplete="off">
416 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="primary sm" id="chat-send-btn" onclick="sendMsg()">send</button>
417 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
418 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
419 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="sidebar-resize" id="resize-right" title="drag to resize"></div>
420 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chat-nicklist" id="chat-nicklist">
421 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="nicklist-head">
422 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sidebar-toggle" id="sidebar-right-toggle" title="collapse" onclick="toggleSidebar('right')">›</button>
423 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span id="sidebar-right-label">users</span>
424 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
425 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="nicklist-users"></div>
426 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
427 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
428 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
429 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- SETTINGS -->
430 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="tab-pane" id="pane-settings">
431 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="pane-inner">
432 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
433 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- connection -->
434 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-connection">
435 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" style="cursor:default"><h2>connection</h2></div>
436 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
437 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
438 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">signed in as</div>
439 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Current admin session.</div>
440 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <code class="setting-val" id="settings-username-display">—</code>
441 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm danger" onclick="logout()">sign out</button>
442 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
443 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
444 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">API endpoint</div>
445 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">REST API base URL.</div>
446 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <code class="setting-val" id="settings-api-url"></code>
447 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
448 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
449 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">IRC network</div>
450 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Ergo IRC server address.</div>
451 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <code class="setting-val">localhost:6667</code>
452 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
453 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
454 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">MCP server</div>
455 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Model Context Protocol endpoint.</div>
456 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <code class="setting-val">localhost:8081</code>
457 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
458 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
459 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
460 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
461 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- admin accounts -->
462 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-admins">
463 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-admins',event)"><h2>admin accounts</h2><span class="collapse-icon">▾</span></div>
464 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="admins-list-container"></div>
465 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body" style="border-top:1px solid #21262d">
466 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <p style="font-size:12px;color:#8b949e;margin-bottom:12px">Add an admin account. Admins sign in at the login screen with username + password.</p>
467 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <form id="add-admin-form" onsubmit="addAdmin(event)" style="flex-direction:row;align-items:flex-end;gap:10px;flex-wrap:wrap">
468 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="flex:1;min-width:130px"><label>username</label><input type="text" id="new-admin-username" autocomplete="off"></div>
469 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="flex:1;min-width:130px"><label>password</label><input type="password" id="new-admin-password" autocomplete="new-password"></div>
470 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button type="submit" class="primary sm" style="margin-bottom:1px">add admin</button>
471 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </form>
472 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="add-admin-result" style="margin-top:10px"></div>
473 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
474 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
475 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
476 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- tls -->
477 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-tls">
478 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-tls',event)"><h2>TLS / SSL</h2><span class="collapse-icon">▾</span><div class="spacer"></div><span id="tls-badge" class="badge">loading…</span></div>
479 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
480 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="tls-status-rows"></div>
481 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="alert info" style="margin-top:12px;font-size:12px">
482 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span class="icon">ℹ</span>
483 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span>TLS is configured in <code style="color:#a5d6ff">scuttlebot.yaml</code> under <code style="color:#a5d6ff">tls:</code>.
484 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Set <code style="color:#a5d6ff">domain:</code> to enable Let's Encrypt. <code style="color:#a5d6ff">allow_insecure: true</code> keeps HTTP running alongside HTTPS.</span>
485 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
486 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
487 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
488 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
489 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- system behaviors -->
490 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-behaviors">
491 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-behaviors',event)">
492 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h2>system behaviors</h2><span class="collapse-icon">▾</span>
493 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
494 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm primary" onclick="savePolicies()">save</button>
495 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
496 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body" style="padding:0">
497 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="behaviors-list"></div>
498 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
499 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
500 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
501 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- agent policy -->
502 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-agentpolicy">
503 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-agentpolicy',event)"><h2>agent policy</h2><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="savePolicies()">save</button></div>
504 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
505 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
506 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">require check-in</div>
507 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Agents must join a coordination channel before others.</div>
508 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
509 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="checkbox" id="policy-checkin-enabled" onchange="toggleCheckinChannel()">
510 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span style="font-size:12px">enabled</span>
511 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </label>
512 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
513 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row" id="policy-checkin-row" style="display:none">
514 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">check-in channel</div>
515 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Channel all agents must join first.</div>
516 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="policy-checkin-channel" placeholder="#coordination" style="width:180px;padding:4px 8px;font-size:12px">
517 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
518 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
519 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">required channels</div>
520 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Channels every agent is added to automatically.</div>
521 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="policy-required-channels" placeholder="#fleet, #alerts" style="width:220px;padding:4px 8px;font-size:12px">
522 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
523 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
524 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
525 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
526 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- bridge -->
527 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-bridgepolicy">
528 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-bridgepolicy',event)"><h2>web bridge</h2><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="savePolicies()">save</button></div>
529 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
530 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
531 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">web user TTL</div>
532 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">How long HTTP-posted nicks stay visible in the channel user list after their last message.</div>
533 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;align-items:center;gap:6px">
534 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="number" id="policy-bridge-web-user-ttl" placeholder="5" min="1" style="width:80px;padding:4px 8px;font-size:12px">
535 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span style="font-size:12px;color:#8b949e">minutes</span>
536 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
537 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
538 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
539 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
540 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
541 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- logging -->
542 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-logging">
543 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-logging',event)"><h2>message logging</h2><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="savePolicies()">save</button></div>
544 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
545 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
546 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">enabled</div>
547 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Write every channel message to disk.</div>
548 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
549 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="checkbox" id="policy-logging-enabled" onchange="toggleLogOptions()">
550 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span style="font-size:12px">enabled</span>
551 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </label>
552 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
553 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="policy-log-options" style="display:none">
554 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
555 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">log directory</div>
556 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Directory to write log files into.</div>
557 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="policy-log-dir" placeholder="./data/logs" style="width:280px;padding:4px 8px;font-size:12px">
558 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
559 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
560 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">format</div>
561 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Output format for log lines.</div>
562 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <select id="policy-log-format" style="width:160px;padding:4px 8px;font-size:12px">
563 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="jsonl">JSON Lines (.jsonl)</option>
564 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="csv">CSV (.csv)</option>
565 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="text">Plain text (.log)</option>
566 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </select>
567 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
568 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
569 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">rotation</div>
570 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">When to start a new log file.</div>
571 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <select id="policy-log-rotation" style="width:160px;padding:4px 8px;font-size:12px" onchange="toggleRotationOptions()">
572 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="none">None</option>
573 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="daily">Daily</option>
574 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="weekly">Weekly</option>
575 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="monthly">Monthly</option>
576 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="yearly">Yearly</option>
577 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="size">By size</option>
578 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </select>
579 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
580 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row" id="policy-log-size-row" style="display:none">
581 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">max file size</div>
582 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Rotate when file reaches this size.</div>
583 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;align-items:center;gap:6px">
584 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="number" id="policy-log-max-size" placeholder="100" min="1" style="width:80px;padding:4px 8px;font-size:12px">
585 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span style="font-size:12px;color:#8b949e">MiB</span>
586 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
587 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
588 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
589 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">per-channel files</div>
590 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Write a separate file for each channel.</div>
591 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer">
592 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="checkbox" id="policy-log-per-channel">
593 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span style="font-size:12px">enabled</span>
594 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </label>
595 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
596 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
597 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">max age</div>
598 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Delete rotated files older than N days. 0 = keep forever.</div>
599 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;align-items:center;gap:6px">
600 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="number" id="policy-log-max-age" placeholder="0" min="0" style="width:80px;padding:4px 8px;font-size:12px">
601 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span style="font-size:12px;color:#8b949e">days</span>
602 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
603 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
604 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
605 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
606 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
607 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
608 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="policies-save-result" style="display:none"></div>
609 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
610 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- about -->
611 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card">
612 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" style="cursor:default"><h2>about</h2></div>
613 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body" style="font-size:13px;color:#8b949e;line-height:1.8">
614 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <p><strong style="color:#e6edf3">ScuttleBot</strong> — agent coordination backplane over IRC.</p>
615 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <p>Agents register, receive SASL credentials, and coordinate in IRC channels.</p>
616 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <p>Everything is human observable: all activity is visible in the IRC channel log.</p>
617 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <p style="margin-top:12px;font-size:11px;color:#6e7681">
618 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Copyright © 2026 CONFLICT LLC. All rights reserved.<br>
619 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <a href="https://scuttlebot.dev" target="_blank" rel="noopener" style="color:#58a6ff;text-decoration:none">ScuttleBot</a> — Powered by <a href="https://weareconflict.com" target="_blank" rel="noopener" style="color:#58a6ff;text-decoration:none">CONFLICT</a>
620 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </p>
621 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
622 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
623 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
624 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
625 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
626 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
627 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- AI -->
628 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="tab-pane" id="pane-ai">
629 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="pane-inner">
630 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
631 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- LLM backends -->
632 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-ai-backends">
633 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" style="cursor:default">
634 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h2>LLM backends</h2>
635 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
636 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="loadAI()">↺ refresh</button>
637 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm primary" onclick="openAddBackend()">+ add backend</button>
638 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
639 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body" style="padding:0">
640 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="ai-backends-list" style="padding:16px">
641 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="empty-state">loading…</div>
642 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
643 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
644 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
645 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
646 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- add/edit backend form (hidden until opened) -->
647 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-ai-form" style="display:none">
648 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" style="cursor:default">
649 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h2 id="ai-form-title">add backend</h2>
650 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
651 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="closeBackendForm()">✕ cancel</button>
652 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
653 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
654 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px 16px">
655 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
656 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>name *</label>
657 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="bf-name" placeholder="openai-main" autocomplete="off">
658 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">unique identifier — used in oracle's backend field</div>
659 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
660 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
661 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>backend type *</label>
662 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <select id="bf-backend" onchange="onBackendTypeChange()">
663 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="">— select type —</option>
664 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <optgroup label="Native APIs">
665 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="anthropic">anthropic</option>
666 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="gemini">gemini</option>
667 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="bedrock">bedrock</option>
668 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="ollama">ollama</option>
669 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </optgroup>
670 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <optgroup label="OpenAI-compatible">
671 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="openai">openai</option>
672 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="openrouter">openrouter</option>
673 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="together">together</option>
674 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="groq">groq</option>
675 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="fireworks">fireworks</option>
676 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="mistral">mistral</option>
677 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="ai21">ai21</option>
678 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="huggingface">huggingface</option>
679 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="deepseek">deepseek</option>
680 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="cerebras">cerebras</option>
681 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="xai">xai</option>
682 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="litellm">litellm (local)</option>
683 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="lmstudio">lm studio (local)</option>
684 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="jan">jan (local)</option>
685 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="localai">localai (local)</option>
686 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="vllm">vllm (local)</option>
687 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="anythingllm">anythingllm (local)</option>
688 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </optgroup>
689 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </select>
690 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
691 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
692 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- shown for non-bedrock backends -->
693 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="bf-apikey-row">
694 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>API key</label>
695 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="password" id="bf-apikey" placeholder="sk-…" autocomplete="new-password">
696 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint" id="bf-apikey-hint">Leave blank to use env var or instance role</div>
697 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
698 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
699 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- shown for ollama and OpenAI-compat local backends -->
700 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="bf-baseurl-row">
701 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>base URL</label>
702 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="bf-baseurl" placeholder="http://localhost:11434" autocomplete="off">
703 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">Override default endpoint for self-hosted backends</div>
704 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
705 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
706 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
707 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>model</label>
708 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;gap:6px;align-items:flex-start">
709 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="flex:1">
710 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <select id="bf-model-select" onchange="onModelSelectChange()" style="width:100%">
711 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="">— select or load models —</option>
712 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </select>
713 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="bf-model-custom" placeholder="model-id" autocomplete="off" style="display:none;margin-top:6px">
714 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
715 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button type="button" class="sm" id="bf-load-models-btn" onclick="loadLiveModels(this)" style="white-space:nowrap;margin-top:1px">↺ load models</button>
716 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
717 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">Pick from list or load live from API. Leave blank to auto-select.</div>
718 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
719 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
720 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;align-items:flex-end;gap:8px">
721 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label style="margin:0;cursor:pointer;display:flex;align-items:center;gap:6px">
722 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="checkbox" id="bf-default"> mark as default backend
723 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </label>
724 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
725 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
726 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- Bedrock-specific fields -->
727 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="bf-bedrock-group" style="display:none;grid-column:1/-1">
728 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="font-size:12px;color:#8b949e;font-weight:500;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px">AWS / Bedrock</div>
729 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px 16px">
730 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
731 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>region *</label>
732 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="bf-region" placeholder="us-east-1" autocomplete="off">
733 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
734 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
735 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>AWS key ID</label>
736 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="bf-aws-key-id" placeholder="AKIA… (or leave blank for IAM role)" autocomplete="off">
737 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">Leave blank — scuttlebot will auto-detect IAM role (ECS/EC2/EKS)</div>
738 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
739 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
740 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>AWS secret key</label>
741 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="password" id="bf-aws-secret" placeholder="(or leave blank for IAM role)" autocomplete="new-password">
742 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
743 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
744 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
745 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
746 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- allow/block filters -->
747 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="grid-column:1/-1">
748 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="font-size:12px;color:#8b949e;font-weight:500;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px">Model filters (regex, one per line)</div>
749 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px 16px">
750 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
751 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>allow list</label>
752 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <textarea id="bf-allow" rows="3" placeholder="^gpt-4 ^claude-3" style="font-family:var(--font-mono);font-size:12px"></textarea>
753 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">Only these models shown. Empty = all.</div>
754 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
755 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
756 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>block list</label>
757 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <textarea id="bf-block" rows="3" placeholder=".*-instruct$ .*-preview$" style="font-family:var(--font-mono);font-size:12px"></textarea>
758 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">Always hidden.</div>
759 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
760 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
761 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
762 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
763 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="ai-form-result" style="display:none;margin-top:12px"></div>
764 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
765 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="closeBackendForm()">cancel</button>
766 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm primary" id="bf-submit-btn" onclick="submitBackendForm()">add backend</button>
767 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
768 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
769 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
770 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
771 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- supported backends reference -->
772 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-ai-supported">
773 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-ai-supported',event)"><h2>supported backends</h2><span class="collapse-icon">▾</span></div>
774 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body" id="ai-supported-list">
775 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="empty-state">loading…</div>
776 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
777 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
778 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
779 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- config example -->
780 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card" id="card-ai-example" style="display:none">
781 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-header" onclick="toggleCard('card-ai-example',event)"><h2>YAML example</h2><span class="collapse-icon">▾</span></div>
782 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="card-body">
783 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <pre style="font-size:12px;color:#a5d6ff;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:12px;overflow-x:auto;white-space:pre">llm:
784 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backends:
785 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - name: openai-main
786 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backend: openai
787 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ api_key: sk-...
788 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ model: gpt-4o-mini
789 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ block: [".*-instruct$"] # optional regex filter
790 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
791 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - name: local-ollama
792 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backend: ollama
793 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ base_url: http://localhost:11434
794 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ model: llama3.2
795 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ default: true
796 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
797 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - name: anthropic-claude
798 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backend: anthropic
799 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ api_key: sk-ant-...
800 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ model: claude-3-5-sonnet-20241022
801 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
802 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - name: bedrock-us
803 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backend: bedrock
804 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ region: us-east-1
805 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ aws_key_id: AKIA...
806 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ aws_secret_key: ...
807 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ allow: ["^anthropic\\."] # only Anthropic models
808 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
809 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - name: groq-fast
810 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backend: groq
811 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ api_key: gsk_...</pre>
812 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <p style="font-size:12px;color:#8b949e;margin-top:8px">
813 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Reference a backend from oracle's behavior config using the <code>backend</code> key.
814 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </p>
815 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
816 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
817 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
818 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
819 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
820 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
821 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- Register drawer -->
822 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer-overlay" id="drawer-overlay" onclick="closeDrawer()"></div>
823 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer" id="register-drawer">
824 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer-header">
825 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h3>register agent</h3>
826 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
827 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="closeDrawer()">✕</button>
828 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
829 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer-body">
830 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <form id="register-form" onsubmit="handleRegister(event)">
831 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="form-row">
832 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
833 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>nick *</label>
834 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="reg-nick" placeholder="my-agent-01" required autocomplete="off">
835 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
836 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
837 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>type</label>
838 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <select id="reg-type">
839 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="worker" selected>worker — +v</option>
840 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="orchestrator">orchestrator — +o</option>
841 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="observer">observer — read only</option>
842 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </select>
843 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
844 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
845 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
846 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>channels</label>
847 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="reg-channels" placeholder="#fleet, #ops, #project.foo" autocomplete="off">
848 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">comma-separated; must start with #</div>
849 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
850 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
851 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>permissions</label>
852 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="reg-permissions" placeholder="task.create, task.update" autocomplete="off">
853 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">comma-separated message types this agent may send</div>
854 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
855 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="register-result" style="display:none"></div>
856 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;justify-content:flex-end;gap:8px">
857 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button type="button" onclick="closeDrawer()">cancel</button>
858 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button type="submit" class="primary">register</button>
859 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
860 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </form>
861 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
862 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
863 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
864 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- Register user drawer (operator with fresh credentials) -->
865 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer-overlay" id="register-user-overlay" onclick="closeRegisterUserDrawer()"></div>
866 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer" id="register-user-drawer">
867 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer-header">
868 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h3>register user</h3>
869 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
870 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="closeRegisterUserDrawer()">✕</button>
871 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
872 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer-body">
873 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <form id="register-user-form" onsubmit="handleRegisterUser(event)">
874 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
875 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>nick *</label>
876 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="regu-nick" placeholder="alice" required autocomplete="off">
877 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">new NickServ account will be created; credentials returned once</div>
878 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
879 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
880 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>channels</label>
881 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="regu-channels" placeholder="#ops, #general" autocomplete="off">
882 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">comma-separated</div>
883 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
884 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="register-user-result" style="display:none"></div>
885 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;justify-content:flex-end;gap:8px">
886 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button type="button" onclick="closeRegisterUserDrawer()">cancel</button>
887 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button type="submit" class="primary">register</button>
888 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
889 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </form>
890 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
891 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
892 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
893 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <!-- Adopt user drawer (claim pre-existing NickServ account) -->
894 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer-overlay" id="adopt-overlay" onclick="closeAdoptDrawer()"></div>
895 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer" id="adopt-drawer">
896 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer-header">
897 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <h3>adopt existing user</h3>
898 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
899 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="closeAdoptDrawer()">✕</button>
900 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
901 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="drawer-body">
902 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <p style="font-size:12px;color:#8b949e;margin-bottom:16px">Adds a pre-existing NickServ account to the registry without changing its password. Use this for accounts already connected to IRC.</p>
903 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <form id="adopt-form" onsubmit="handleAdopt(event)">
904 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
905 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>nick *</label>
906 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="adopt-nick" placeholder="existing-irc-nick" required autocomplete="off">
907 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
908 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
909 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label>channels</label>
910 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="adopt-channels" placeholder="#ops, #general" autocomplete="off">
911 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="hint">comma-separated</div>
912 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
913 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="adopt-result" style="display:none"></div>
914 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;justify-content:flex-end;gap:8px">
915 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button type="button" onclick="closeAdoptDrawer()">cancel</button>
916 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button type="submit" class="primary">adopt</button>
917 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
918 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </form>
919 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
920 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
921 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
922 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
923 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <script>
924 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- tabs ---
925 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const TAB_LOADERS = { status: loadStatus, users: loadAgents, agents: loadAgents, channels: loadChanTab, chat: () => { populateChatIdentity(); loadChannels(); }, ai: loadAI, settings: loadSettings };
926 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function switchTab(name) {
927 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
928 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
929 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('tab-' + name).classList.add('active');
930 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('pane-' + name).classList.add('active');
931 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (name === 'chat') {
932 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _chatUnread = 0;
933 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ delete document.getElementById('tab-chat').dataset.unread;
934 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
935 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (TAB_LOADERS[name]) TAB_LOADERS[name]();
936 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
937 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
938 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- auth ---
939 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function getToken() { return localStorage.getItem('sb_token') || ''; }
940 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function getUsername() { return localStorage.getItem('sb_username') || ''; }
941 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
942 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function showLoginScreen() {
943 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('login-screen').style.display = 'flex';
944 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ setTimeout(() => document.getElementById('login-username')?.focus(), 80);
945 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
946 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function hideLoginScreen() {
947 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('login-screen').style.display = 'none';
948 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
949 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
950 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function updateHeaderUser() {
951 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const u = getUsername();
952 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const t = getToken();
953 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const label = u ? '@' + u : (t ? t.slice(0,8)+'…' : '');
954 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('header-user-display').textContent = label;
955 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const su = document.getElementById('settings-username-display');
956 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (su) su.textContent = u || 'token auth';
957 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('settings-api-url').textContent = location.origin;
958 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('no-token-banner').style.display = t ? 'none' : 'flex';
959 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
960 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
961 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function handleLogin(e) {
962 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ e.preventDefault();
963 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const username = document.getElementById('login-username').value.trim();
964 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const password = document.getElementById('login-password').value;
965 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const btn = document.getElementById('login-btn');
966 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const errEl = document.getElementById('login-error');
967 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!username || !password) return;
968 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = true; btn.textContent = 'signing in…';
969 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ errEl.style.display = 'none';
970 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
971 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const resp = await fetch('/login', {
972 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ method: 'POST',
973 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ headers: {'Content-Type':'application/json'},
974 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ body: JSON.stringify({username, password}),
975 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ });
976 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const data = await resp.json().catch(() => ({}));
977 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!resp.ok) throw new Error(data.error || 'Login failed');
978 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ localStorage.setItem('sb_token', data.token);
979 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ localStorage.setItem('sb_username', data.username || username);
980 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ hideLoginScreen();
981 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ updateHeaderUser();
982 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadAll();
983 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(err) {
984 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ errEl.style.display = 'block';
985 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ errEl.innerHTML = renderAlert('error', err.message);
986 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = false; btn.textContent = 'sign in';
987 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
988 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
989 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
990 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function saveTokenLogin() {
991 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const v = document.getElementById('token-login-input').value.trim();
992 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!v) return;
993 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ localStorage.setItem('sb_token', v);
994 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ localStorage.removeItem('sb_username');
995 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ hideLoginScreen();
996 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ updateHeaderUser();
997 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadAll();
998 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
999 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1000 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function logout() {
1001 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ localStorage.removeItem('sb_token');
1002 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ localStorage.removeItem('sb_username');
1003 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ location.reload();
1004 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1005 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1006 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function initAuth() {
1007 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!getToken()) { showLoginScreen(); return; }
1008 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ hideLoginScreen();
1009 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ updateHeaderUser();
1010 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadAll();
1011 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1012 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1013 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.addEventListener('keydown', e => { if(e.key==='Escape'){ closeDrawer(); closeRegisterUserDrawer(); closeAdoptDrawer(); } });
1014 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1015 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- API ---
1016 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function api(method, path, body) {
1017 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const opts = { method, headers: { 'Authorization':'Bearer '+getToken(), 'Content-Type':'application/json' } };
1018 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (body !== undefined) opts.body = JSON.stringify(body);
1019 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const res = await fetch(path, opts);
1020 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (res.status === 401) {
1021 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ localStorage.removeItem('sb_token');
1022 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ localStorage.removeItem('sb_username');
1023 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ showLoginScreen();
1024 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ throw new Error('Session expired — please sign in again');
1025 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
257 1026 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
if (res.status === 204) return null;
258 1027 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
const data = await res.json().catch(() => ({ error: res.statusText }));
259 1028 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
if (!res.ok) throw Object.assign(new Error(data.error || res.statusText), { status: res.status });
260 1029 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
return data;
261 1030 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
262 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
263 1031 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
function copyText(text, btn) {
264 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- navigator.clipboard.writeText(text).then(() => {
265 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const orig = btn.textContent;
266 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- btn.textContent = '✓';
267 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- setTimeout(() => { btn.textContent = orig; }, 1200);
1032 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ navigator.clipboard.writeText(text).then(() => { const o=btn.textContent; btn.textContent='✓'; setTimeout(()=>{btn.textContent=o;},1200); });
1033 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1034 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1035 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- charts ---
1036 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const CHART_POINTS = 60; // 5 min at 5s intervals
1037 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const chartData = {
1038 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ labels: Array(CHART_POINTS).fill(''),
1039 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ heap: Array(CHART_POINTS).fill(null),
1040 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ goroutines: Array(CHART_POINTS).fill(null),
1041 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ messages: Array(CHART_POINTS).fill(null),
1042 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ };
1043 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let charts = {};
1044 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1045 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function mkChart(id, label, color) {
1046 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const ctx = document.getElementById(id).getContext('2d');
1047 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return new Chart(ctx, {
1048 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ type: 'line',
1049 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ data: {
1050 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ labels: chartData.labels,
1051 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ datasets: [{
1052 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ label,
1053 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ data: chartData[id.replace('chart-','')],
1054 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ borderColor: color,
1055 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backgroundColor: color+'22',
1056 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ borderWidth: 1.5,
1057 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ pointRadius: 0,
1058 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tension: 0.3,
1059 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ fill: true,
1060 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }],
1061 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
1062 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ options: {
1063 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ responsive: true,
1064 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ animation: false,
1065 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ plugins: { legend: { display: false } },
1066 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ scales: {
1067 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ x: { display: false },
1068 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ y: { display: true, grid: { color: '#21262d' }, ticks: { color: '#8b949e', font: { size: 10 }, maxTicksLimit: 4 } },
1069 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
1070 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ },
268 1071 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
});
269 1072 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
1073 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1074 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function initCharts() {
1075 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (charts.heap) return;
1076 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ charts.heap = mkChart('chart-heap', 'heap', '#58a6ff');
1077 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ charts.goroutines = mkChart('chart-goroutines', 'goroutines', '#3fb950');
1078 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ charts.messages = mkChart('chart-messages', 'messages', '#d2a8ff');
1079 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1080 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1081 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function fmtBytes(b) {
1082 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (b == null) return '—';
1083 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (b < 1024) return b+'B';
1084 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (b < 1048576) return (b/1024).toFixed(1)+'KB';
1085 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return (b/1048576).toFixed(1)+'MB';
1086 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1087 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1088 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function pushMetrics(m) {
1089 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ chartData.heap.push(m.runtime.heap_alloc_bytes/1048576);
1090 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ chartData.heap.shift();
1091 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ chartData.goroutines.push(m.runtime.goroutines);
1092 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ chartData.goroutines.shift();
1093 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const msgs = m.bridge ? m.bridge.messages_total : null;
1094 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ chartData.messages.push(msgs);
1095 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ chartData.messages.shift();
1096 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1097 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Reassign dataset data arrays (shared reference, Chart.js reads them directly).
1098 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ charts.heap.data.datasets[0].data = chartData.heap;
1099 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ charts.goroutines.data.datasets[0].data = chartData.goroutines;
1100 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ charts.messages.data.datasets[0].data = chartData.messages;
1101 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ charts.heap.update('none');
1102 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ charts.goroutines.update('none');
1103 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ charts.messages.update('none');
1104 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
270 1105 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
271 1106 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
// --- status ---
272 1107 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
async function loadStatus() {
273 1108 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
try {
274 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const s = await api('GET', '/v1/status');
275 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('stat-status').innerHTML = '<span class="dot green"></span>' + s.status;
1109 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const [s, m] = await Promise.all([
1110 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ api('GET', '/v1/status'),
1111 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ api('GET', '/v1/metrics'),
1112 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ]);
1113 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1114 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Status card.
1115 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-status').innerHTML = '<span class="dot green"></span>'+s.status;
276 1116 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
document.getElementById('stat-uptime').textContent = s.uptime;
277 1117 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
document.getElementById('stat-agents').textContent = s.agents;
278 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const started = new Date(s.started);
279 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('stat-started').textContent = started.toLocaleTimeString();
280 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('stat-started-rel').textContent = started.toLocaleDateString();
1118 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const d = new Date(s.started);
1119 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-started').textContent = d.toLocaleTimeString();
1120 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-started-rel').textContent = d.toLocaleDateString();
281 1121 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
document.getElementById('status-error').style.display = 'none';
282 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } catch (e) {
1122 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('metrics-updated').textContent = 'updated '+new Date().toLocaleTimeString();
1123 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1124 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Runtime card.
1125 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-goroutines').textContent = m.runtime.goroutines;
1126 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-heap').textContent = fmtBytes(m.runtime.heap_alloc_bytes);
1127 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-heapsys').textContent = fmtBytes(m.runtime.heap_sys_bytes);
1128 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-gc').textContent = m.runtime.gc_runs;
1129 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('chart-heap-val').textContent = fmtBytes(m.runtime.heap_alloc_bytes);
1130 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('chart-goroutines-val').textContent = m.runtime.goroutines;
1131 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('chart-messages-val').textContent = m.bridge ? m.bridge.messages_total : '—';
1132 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1133 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Bridge card.
1134 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (m.bridge) {
1135 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bridge-card').style.display = '';
1136 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-bridge-channels').textContent = m.bridge.channels;
1137 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-bridge-msgs').textContent = m.bridge.messages_total;
1138 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-bridge-subs').textContent = m.bridge.active_subscribers;
1139 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1140 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1141 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Registry card.
1142 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-reg-total').textContent = m.registry.total;
1143 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-reg-active').textContent = m.registry.active;
1144 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('stat-reg-revoked').textContent = m.registry.revoked;
1145 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1146 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Push to charts.
1147 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ initCharts();
1148 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ pushMetrics(m);
1149 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
283 1150 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
document.getElementById('stat-status').innerHTML = '<span class="dot red"></span>error';
284 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const err = document.getElementById('status-error');
285 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- err.style.display = 'block';
286 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- err.innerHTML = renderAlert('error', e.message);
1151 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('status-error');
1152 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.style.display = 'block';
1153 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = renderAlert('error', e.message);
287 1154 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
288 1155 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
289 1156 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
290 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- // --- agents ---
1157 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let metricsTimer = null;
1158 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function startMetricsPoll() {
1159 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (metricsTimer) return;
1160 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ metricsTimer = setInterval(() => {
1161 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (document.getElementById('pane-status').classList.contains('active')) loadStatus();
1162 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }, 5000);
1163 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1164 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1165 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- agents + users (shared data) ---
1166 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let allAgents = [];
291 1167 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
async function loadAgents() {
292 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const container = document.getElementById('agents-container');
293 1168 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
try {
294 1169 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
const data = await api('GET', '/v1/agents');
295 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const agents = data.agents || [];
296 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('agent-count').textContent = agents.length;
297 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (agents.length === 0) {
298 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- container.innerHTML = '<div class="empty">no agents registered yet</div>';
299 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- return;
300 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
301 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- let rows = agents.map(a => {
302 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const channels = (a.config?.channels || []).map(c => `<span class="tag">${esc(c)}</span>`).join('');
303 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const perms = (a.config?.permissions || []).map(p => `<span class="tag">${esc(p)}</span>`).join('');
304 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const revoked = a.revoked_at ? '<span class="tag revoked">revoked</span>' : '';
305 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- return `<tr>
306 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <td><strong>${esc(a.nick)}</strong></td>
307 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <td><span class="tag type-${a.type}">${esc(a.type)}</span>${revoked}</td>
308 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <td>${channels || '<span style="color:#8b949e">—</span>'}</td>
309 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <td>${perms || '<span style="color:#8b949e">—</span>'}</td>
310 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <td>${fmtTime(a.created_at)}</td>
311 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <td>
312 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="actions">
313 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- ${!a.revoked_at ? `<button class="small" onclick="rotateAgent('${esc(a.nick)}')">rotate</button>
314 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <button class="small danger" onclick="revokeAgent('${esc(a.nick)}')">revoke</button>` : ''}
315 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
316 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </td>
317 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </tr>`;
318 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }).join('');
319 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- container.innerHTML = `<table>
320 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <thead><tr><th>nick</th><th>type</th><th>channels</th><th>permissions</th><th>registered</th><th>actions</th></tr></thead>
321 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <tbody>${rows}</tbody>
322 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </table>`;
323 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } catch (e) {
324 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- container.innerHTML = renderAlert('error', e.message);
325 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
1170 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ allAgents = data.agents || [];
1171 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderUsersTable();
1172 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderAgentTable();
1173 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ populateChatIdentity();
1174 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
1175 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const msg = '<div style="padding:16px">'+renderAlert('error', e.message)+'</div>';
1176 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('agents-container').innerHTML = msg;
1177 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('users-container').innerHTML = msg;
1178 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1179 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1180 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1181 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderTable(container, countEl, rows, emptyMsg, cols) {
1182 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (rows.length === 0) {
1183 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById(container).innerHTML = '<div class="empty">'+emptyMsg+'</div>';
1184 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
1185 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const ths = cols.map(c=>`<th>${c}</th>`).join('');
1186 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById(container).innerHTML =
1187 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<table><thead><tr>${ths}</tr></thead><tbody>${rows.join('')}</tbody></table>`;
1188 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1189 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (countEl) document.getElementById(countEl).textContent = rows.length;
1190 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1191 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1192 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderUsersTable() {
1193 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const q = (document.getElementById('user-search').value||'').toLowerCase();
1194 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const users = allAgents.filter(a => a.type === 'operator' && (!q ||
1195 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ a.nick.toLowerCase().includes(q) ||
1196 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ (a.config?.channels||[]).some(c => c.toLowerCase().includes(q))));
1197 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const rows = users.map(a => {
1198 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const chs = (a.config?.channels||[]).map(c=>`<span class="tag ch">${esc(c)}</span>`).join('');
1199 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const rev = a.revoked ? '<span class="tag revoked">revoked</span>' : '';
1200 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return `<tr>
1201 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><strong>${esc(a.nick)}</strong></td>
1202 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><span class="tag type-operator">operator</span>${rev}</td>
1203 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td>${chs||'<span style="color:#8b949e">—</span>'}</td>
1204 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td style="white-space:nowrap">${fmtTime(a.created_at)}</td>
1205 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><div class="actions">${!a.revoked?`
1206 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="rotateAgent('${esc(a.nick)}')">rotate</button>
1207 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm danger" onclick="revokeAgent('${esc(a.nick)}')">revoke</button>`:``}
1208 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm danger" onclick="deleteAgent('${esc(a.nick)}')">delete</button></div></td>
1209 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </tr>`;
1210 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ });
1211 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const all = allAgents.filter(a => a.type === 'operator');
1212 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const countTxt = all.length + (rows.length !== all.length ? ' / '+rows.length+' shown' : '');
1213 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('user-count').textContent = countTxt;
1214 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderTable('users-container', null, rows,
1215 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ all.length ? 'no users match the filter' : 'no users registered yet',
1216 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ['nick','type','channels','registered','']);
1217 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1218 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1219 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderAgentTable() {
1220 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const q = (document.getElementById('agent-search').value||'').toLowerCase();
1221 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const bots = allAgents.filter(a => a.type !== 'operator');
1222 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const agents = bots.filter(a => !q ||
1223 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ a.nick.toLowerCase().includes(q) ||
1224 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ a.type.toLowerCase().includes(q) ||
1225 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ (a.config?.channels||[]).some(c => c.toLowerCase().includes(q)) ||
1226 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ (a.config?.permissions||[]).some(p => p.toLowerCase().includes(q)));
1227 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('agent-count').textContent = bots.length + (agents.length !== bots.length ? ' / '+agents.length+' shown' : '');
1228 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const rows = agents.map(a => {
1229 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const chs = (a.config?.channels||[]).map(c=>`<span class="tag ch">${esc(c)}</span>`).join('');
1230 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const perms = (a.config?.permissions||[]).map(p=>`<span class="tag perm">${esc(p)}</span>`).join('');
1231 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const rev = a.revoked ? '<span class="tag revoked">revoked</span>' : '';
1232 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return `<tr>
1233 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><strong>${esc(a.nick)}</strong></td>
1234 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><span class="tag type-${a.type}">${esc(a.type)}</span>${rev}</td>
1235 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td>${chs||'<span style="color:#8b949e">—</span>'}</td>
1236 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td>${perms||'<span style="color:#8b949e">—</span>'}</td>
1237 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td style="white-space:nowrap">${fmtTime(a.created_at)}</td>
1238 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><div class="actions">${!a.revoked?`
1239 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="rotateAgent('${esc(a.nick)}')">rotate</button>
1240 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm danger" onclick="revokeAgent('${esc(a.nick)}')">revoke</button>`:``}
1241 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm danger" onclick="deleteAgent('${esc(a.nick)}')">delete</button></div></td>
1242 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </tr>`;
1243 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ });
1244 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderTable('agents-container', null, rows,
1245 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ bots.length ? 'no agents match the filter' : 'no agents registered yet',
1246 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ['nick','type','channels','permissions','registered','']);
326 1247 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
327 1248 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
328 1249 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
async function revokeAgent(nick) {
329 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (!confirm(`Revoke agent "${nick}"? This cannot be undone.`)) return;
330 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- try {
331 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- await api('POST', `/v1/agents/${nick}/revoke`);
332 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- await loadAgents();
333 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- await loadStatus();
334 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } catch(e) {
335 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- alert('Revoke failed: ' + e.message);
336 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
337 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
338 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
1250 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!confirm(`Revoke "${nick}"? This cannot be undone.`)) return;
1251 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try { await api('POST', `/v1/agents/${nick}/revoke`); await loadAgents(); await loadStatus(); }
1252 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ catch(e) { alert('Revoke failed: '+e.message); }
1253 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1254 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function deleteAgent(nick) {
1255 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!confirm(`Delete "${nick}"? This permanently removes the agent from the registry.`)) return;
1256 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try { await api('DELETE', `/v1/agents/${nick}`); await loadAgents(); await loadStatus(); }
1257 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ catch(e) { alert('Delete failed: '+e.message); }
1258 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
339 1259 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
async function rotateAgent(nick) {
340 1260 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
try {
341 1261 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
const creds = await api('POST', `/v1/agents/${nick}/rotate`);
1262 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Show result in whichever drawer is relevant.
342 1263 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
showCredentials(nick, creds, null, 'rotate');
343 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } catch(e) {
344 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- alert('Rotate failed: ' + e.message);
1264 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ openDrawer();
345 1265 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
1266 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ catch(e) { alert('Rotate failed: '+e.message); }
1267 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1268 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1269 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- users drawers ---
1270 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function openRegisterUserDrawer() {
1271 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('register-user-overlay').classList.add('open');
1272 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('register-user-drawer').classList.add('open');
1273 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ setTimeout(() => document.getElementById('regu-nick').focus(), 100);
1274 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1275 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function closeRegisterUserDrawer() {
1276 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('register-user-overlay').classList.remove('open');
1277 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('register-user-drawer').classList.remove('open');
1278 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1279 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function openAdoptDrawer() {
1280 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('adopt-overlay').classList.add('open');
1281 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('adopt-drawer').classList.add('open');
1282 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ setTimeout(() => document.getElementById('adopt-nick').focus(), 100);
1283 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1284 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function closeAdoptDrawer() {
1285 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('adopt-overlay').classList.remove('open');
1286 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('adopt-drawer').classList.remove('open');
1287 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1288 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1289 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function handleRegisterUser(e) {
1290 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ e.preventDefault();
1291 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const nick = document.getElementById('regu-nick').value.trim();
1292 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const channels = document.getElementById('regu-channels').value.split(',').map(s=>s.trim()).filter(Boolean);
1293 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const resultEl = document.getElementById('register-user-result');
1294 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.style.display = 'none';
1295 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1296 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const res = await api('POST', '/v1/agents/register', { nick, type: 'operator', channels, permissions: [] });
1297 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.style.display = 'block';
1298 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const pass = res.credentials?.passphrase || '';
1299 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.innerHTML = renderAlert('success',
1300 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<div><strong>Registered: ${esc(nick)}</strong>
1301 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="cred-box">
1302 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="cred-row"><span class="cred-key">nick</span><span class="cred-val">${esc(nick)}</span><button class="sm" onclick="copyText('${esc(nick)}',this)">copy</button></div>
1303 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="cred-row"><span class="cred-key">passphrase</span><span class="cred-val">${esc(pass)}</span><button class="sm" onclick="copyText('${esc(pass)}',this)">copy</button></div>
1304 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
1305 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="margin-top:8px;font-size:11px;color:#7ee787">⚠ Save the passphrase — shown once only.</div></div>`);
1306 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('register-user-form').reset();
1307 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await loadAgents(); await loadStatus();
1308 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { resultEl.style.display='block'; resultEl.innerHTML=renderAlert('error', e.message); }
1309 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1310 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1311 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function handleAdopt(e) {
1312 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ e.preventDefault();
1313 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const nick = document.getElementById('adopt-nick').value.trim();
1314 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const channels = document.getElementById('adopt-channels').value.split(',').map(s=>s.trim()).filter(Boolean);
1315 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const resultEl = document.getElementById('adopt-result');
1316 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.style.display = 'none';
1317 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1318 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('POST', `/v1/agents/${nick}/adopt`, { type: 'operator', channels, permissions: [] });
1319 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.style.display = 'block';
1320 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.innerHTML = renderAlert('success',
1321 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<strong>${esc(nick)}</strong> adopted as operator — existing IRC session and passphrase unchanged.`);
1322 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('adopt-form').reset();
1323 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await loadAgents(); await loadStatus();
1324 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { resultEl.style.display='block'; resultEl.innerHTML=renderAlert('error', e.message); }
1325 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1326 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1327 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- drawer ---
1328 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function openDrawer() {
1329 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('drawer-overlay').classList.add('open');
1330 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('register-drawer').classList.add('open');
1331 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ setTimeout(() => document.getElementById('reg-nick').focus(), 100);
1332 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1333 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function closeDrawer() {
1334 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('drawer-overlay').classList.remove('open');
1335 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('register-drawer').classList.remove('open');
346 1336 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
347 1337 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
348 1338 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
// --- register ---
349 1339 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
async function handleRegister(e) {
350 1340 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
e.preventDefault();
351 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const nick = document.getElementById('reg-nick').value.trim();
352 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const type = document.getElementById('reg-type').value;
353 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const channelsRaw = document.getElementById('reg-channels').value;
354 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const permsRaw = document.getElementById('reg-permissions').value;
355 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
356 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const channels = channelsRaw.split(',').map(s => s.trim()).filter(Boolean);
357 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const permissions = permsRaw.split(',').map(s => s.trim()).filter(Boolean);
358 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
359 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const resultEl = document.getElementById('register-result');
1341 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const nick = document.getElementById('reg-nick').value.trim();
1342 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const type = document.getElementById('reg-type').value;
1343 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const channels = document.getElementById('reg-channels').value.split(',').map(s=>s.trim()).filter(Boolean);
1344 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const permissions = document.getElementById('reg-permissions').value.split(',').map(s=>s.trim()).filter(Boolean);
1345 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const resultEl = document.getElementById('register-result');
360 1346 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
resultEl.style.display = 'none';
361 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
362 1347 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
try {
363 1348 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
const res = await api('POST', '/v1/agents/register', { nick, type, channels, permissions });
364 1349 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
showCredentials(nick, res.credentials, res.payload, 'register');
365 1350 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
document.getElementById('register-form').reset();
366 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- await loadAgents();
367 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- await loadStatus();
1351 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await loadAgents(); await loadStatus();
1352 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { resultEl.style.display='block'; resultEl.innerHTML=renderAlert('error', e.message); }
1353 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1354 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function showCredentials(nick, creds, payload, mode) {
1355 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const resultEl = document.getElementById('register-result');
1356 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.style.display = 'block';
1357 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const pass = creds?.passphrase || creds?.Passphrase || '';
1358 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const sig = payload?.signature || '';
1359 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.innerHTML = renderAlert('success',
1360 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<div><strong>${mode==='register'?'Registered':'Rotated'}: ${esc(nick)}</strong>
1361 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="cred-box">
1362 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="cred-row"><span class="cred-key">nick</span><span class="cred-val">${esc(creds?.nick||nick)}</span><button class="sm" onclick="copyText('${esc(creds?.nick||nick)}',this)">copy</button></div>
1363 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="cred-row"><span class="cred-key">passphrase</span><span class="cred-val">${esc(pass)}</span><button class="sm" onclick="copyText('${esc(pass)}',this)">copy</button></div>
1364 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ${sig?`<div class="cred-row"><span class="cred-key">sig</span><span class="cred-val" style="font-size:11px">${esc(sig)}</span></div>`:''}
1365 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
1366 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="margin-top:8px;font-size:11px;color:#7ee787">⚠ Save the passphrase — shown once only.</div></div>`
1367 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ );
1368 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1369 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1370 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- channels tab ---
1371 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadChanTab() {
1372 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!getToken()) return;
1373 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1374 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const data = await api('GET', '/v1/channels');
1375 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const channels = data.channels || [];
1376 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('chan-count').textContent = channels.length;
1377 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (channels.length === 0) {
1378 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('channels-list').innerHTML = '<div class="empty">no channels joined yet — type a channel name above and click join</div>';
1379 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return;
1380 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1381 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('channels-list').innerHTML = channels.sort().map(ch =>
1382 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<div class="chan-card">
1383 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
1384 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chan-name">${esc(ch)}</div>
1385 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="chan-meta">joined</div>
1386 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
1387 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="spacer"></div>
1388 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="switchTab('chat');setTimeout(()=>selectChannel('${esc(ch)}'),50)">open chat →</button>
1389 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>`
1390 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ).join('');
1391 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
1392 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('channels-list').innerHTML = '<div style="padding:16px">'+renderAlert('error', e.message)+'</div>';
1393 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1394 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1395 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function quickJoin() {
1396 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let ch = document.getElementById('quick-join-input').value.trim();
1397 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!ch) return;
1398 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!ch.startsWith('#')) ch = '#' + ch;
1399 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const slug = ch.replace(/^#/,'');
1400 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1401 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('POST', `/v1/channels/${slug}/join`);
1402 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('quick-join-input').value = '';
1403 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await loadChanTab();
1404 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderChanSidebar((await api('GET','/v1/channels')).channels||[]);
1405 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { alert('Join failed: '+e.message); }
1406 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1407 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('quick-join-input').addEventListener('keydown', e => { if(e.key==='Enter')quickJoin(); });
1408 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1409 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- chat ---
1410 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let chatChannel = null, chatSSE = null;
1411 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1412 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadChannels() {
1413 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!getToken()) return;
1414 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1415 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const data = await api('GET', '/v1/channels');
1416 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderChanSidebar(data.channels || []);
1417 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {}
1418 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1419 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderChanSidebar(channels) {
1420 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const list = document.getElementById('chan-list');
1421 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ list.innerHTML = '';
1422 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ channels.sort().forEach(ch => {
1423 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.createElement('div');
1424 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.className = 'chan-item' + (ch===chatChannel?' active':'');
1425 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.textContent = ch;
1426 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.onclick = () => selectChannel(ch);
1427 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ list.appendChild(el);
1428 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ });
1429 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1430 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function joinChannel() {
1431 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let ch = document.getElementById('join-channel-input').value.trim();
1432 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!ch) return;
1433 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!ch.startsWith('#')) ch = '#' + ch;
1434 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const slug = ch.replace(/^#/,'');
1435 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1436 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('POST', `/v1/channels/${slug}/join`);
1437 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('join-channel-input').value = '';
1438 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const data = await api('GET', '/v1/channels');
1439 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderChanSidebar(data.channels||[]);
1440 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ selectChannel(ch);
1441 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { alert('Join failed: '+e.message); }
1442 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1443 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('join-channel-input').addEventListener('keydown', e => { if(e.key==='Enter')joinChannel(); });
1444 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1445 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function selectChannel(ch) {
1446 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _lastMsgNick = null; _lastMsgAt = 0; _hideChatBanner(); _chatUnread = 0;
1447 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ chatChannel = ch;
1448 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('chat-ch-name').textContent = ch;
1449 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('chat-placeholder').style.display = 'none';
1450 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.querySelectorAll('.chan-item').forEach(el => el.classList.toggle('active', el.textContent===ch));
1451 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1452 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const area = document.getElementById('chat-msgs');
1453 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Array.from(area.children).forEach(el => { if(!el.id) el.remove(); });
1454 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1455 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1456 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const slug = ch.replace(/^#/,'');
1457 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const data = await api('GET', `/v1/channels/${slug}/messages`);
1458 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ (data.messages||[]).forEach(m => appendMsg(m, true));
1459 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ area.scrollTop = area.scrollHeight;
1460 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {}
1461 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1462 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (chatSSE) { chatSSE.close(); chatSSE = null; }
1463 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const slug = ch.replace(/^#/,'');
1464 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const es = new EventSource(`/v1/channels/${slug}/stream?token=${encodeURIComponent(getToken())}`);
1465 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ chatSSE = es;
1466 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const badge = document.getElementById('chat-stream-status');
1467 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ es.onopen = () => { badge.textContent='● live'; badge.style.color='#3fb950'; };
1468 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ es.onmessage = ev => { try { appendMsg(JSON.parse(ev.data)); area.scrollTop=area.scrollHeight; } catch(_){} };
1469 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ es.onerror = () => { badge.textContent='○ reconnecting…'; badge.style.color='#8b949e'; };
1470 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1471 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadNicklist(ch);
1472 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (_nicklistTimer) clearInterval(_nicklistTimer);
1473 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _nicklistTimer = setInterval(() => loadNicklist(chatChannel), 10000);
1474 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1475 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1476 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let _nicklistTimer = null;
1477 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadNicklist(ch) {
1478 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!ch) return;
1479 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1480 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const slug = ch.replace(/^#/,'');
1481 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const data = await api('GET', `/v1/channels/${slug}/users`);
1482 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderNicklist(data.users || []);
1483 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {}
1484 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1485 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderNicklist(users) {
1486 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('nicklist-users');
1487 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const knownBots = new Set(['bridge','oracle','sentinel','steward','scribe','warden','snitch','herald','scroll','systembot','auditbot','claude']);
1488 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = users.sort((a,b) => a.localeCompare(b)).map(nick => {
1489 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const isBot = knownBots.has(nick.toLowerCase());
1490 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return `<div class="nicklist-nick${isBot?' is-bot':''}" title="${esc(nick)}">${esc(nick)}</div>`;
1491 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }).join('');
1492 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1493 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Nick colors — deterministic hash over a palette
1494 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const NICK_PALETTE = ['#58a6ff','#3fb950','#ffa657','#d2a8ff','#56d364','#79c0ff','#ff7b72','#a5d6ff','#f0883e','#39d353'];
1495 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function nickColor(nick) {
1496 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let h = 0;
1497 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (let i = 0; i < nick.length; i++) h = (h * 31 + nick.charCodeAt(i)) & 0xffff;
1498 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return NICK_PALETTE[h % NICK_PALETTE.length];
1499 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1500 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1501 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let _lastMsgNick = null, _lastMsgAt = 0;
1502 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const GROUP_MS = 5 * 60 * 1000;
1503 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let _chatNewBanner = null;
1504 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let _chatUnread = 0;
1505 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1506 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function appendMsg(msg, isHistory) {
1507 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const area = document.getElementById('chat-msgs');
1508 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1509 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Parse "[nick] text" sent by the bridge bot on behalf of a web user
1510 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let displayNick = msg.nick;
1511 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let displayText = msg.text;
1512 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (msg.nick === 'bridge') {
1513 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const m = msg.text.match(/^\[([^\]]+)\] ([\s\S]*)$/);
1514 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (m) { displayNick = m[1]; displayText = m[2]; }
1515 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1516 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1517 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const atMs = new Date(msg.at).getTime();
1518 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const grouped = !isHistory && displayNick === _lastMsgNick && (atMs - _lastMsgAt) < GROUP_MS;
1519 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _lastMsgNick = displayNick;
1520 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _lastMsgAt = atMs;
1521 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1522 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const timeStr = new Date(msg.at).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
1523 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const color = nickColor(displayNick);
1524 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1525 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const row = document.createElement('div');
1526 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ row.className = 'msg-row' + (grouped ? ' msg-grouped' : '');
1527 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ row.innerHTML =
1528 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<span class="msg-time" title="${esc(new Date(msg.at).toLocaleString())}">${esc(timeStr)}</span>` +
1529 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<span class="msg-nick" style="color:${color}">${esc(displayNick)}</span>` +
1530 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<span class="msg-text">${esc(displayText)}</span>`;
1531 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ area.appendChild(row);
1532 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1533 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Unread badge when chat tab not active
1534 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!isHistory && !document.getElementById('tab-chat').classList.contains('active')) {
1535 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _chatUnread++;
1536 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('tab-chat').dataset.unread = _chatUnread > 9 ? '9+' : _chatUnread;
1537 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1538 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1539 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (isHistory) {
1540 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ area.scrollTop = area.scrollHeight;
1541 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
1542 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const nearBottom = area.scrollHeight - area.clientHeight - area.scrollTop < 100;
1543 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (nearBottom) {
1544 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ area.scrollTop = area.scrollHeight;
1545 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _hideChatBanner();
1546 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
1547 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _showChatBanner(area);
1548 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1549 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1550 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1551 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1552 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function _showChatBanner(area) {
1553 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (_chatNewBanner) return;
1554 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _chatNewBanner = document.createElement('div');
1555 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _chatNewBanner.className = 'chat-new-banner';
1556 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _chatNewBanner.textContent = '↓ new messages';
1557 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _chatNewBanner.onclick = () => { area.scrollTop = area.scrollHeight; _hideChatBanner(); };
1558 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ area.appendChild(_chatNewBanner);
1559 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1560 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function _hideChatBanner() {
1561 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (_chatNewBanner) { _chatNewBanner.remove(); _chatNewBanner = null; }
1562 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1563 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function saveChatIdentity() {
1564 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ localStorage.setItem('sb_chat_nick', document.getElementById('chat-identity').value);
1565 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1566 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function getChatNick() {
1567 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return localStorage.getItem('sb_chat_nick') || '';
1568 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1569 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function populateChatIdentity() {
1570 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const sel = document.getElementById('chat-identity');
1571 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const current = getChatNick();
1572 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Operators + any registered nick can send (all types visible, operators first)
1573 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const operators = allAgents.filter(a => a.type === 'operator' && !a.revoked);
1574 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const bots = allAgents.filter(a => a.type !== 'operator' && !a.revoked);
1575 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const options = [...operators, ...bots];
1576 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sel.innerHTML = '<option value="">— pick a user —</option>' +
1577 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ options.map(a => `<option value="${esc(a.nick)}"${a.nick===current?' selected':''}>${esc(a.nick)} (${esc(a.type)})</option>`).join('');
1578 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Restore saved selection.
1579 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (current) sel.value = current;
1580 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1581 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1582 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function sendMsg() {
1583 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!chatChannel) return;
1584 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const input = document.getElementById('chat-text-input');
1585 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const nick = document.getElementById('chat-identity').value.trim() || 'web';
1586 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const text = input.value.trim();
1587 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!text) return;
1588 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input.disabled = true;
1589 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('chat-send-btn').disabled = true;
1590 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1591 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const slug = chatChannel.replace(/^#/,'');
1592 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('POST', `/v1/channels/${slug}/messages`, { text, nick });
1593 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input.value = '';
1594 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { alert('Send failed: '+e.message); }
1595 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ finally { input.disabled=false; document.getElementById('chat-send-btn').disabled=false; input.focus(); }
1596 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1597 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- chat input: Enter to send, Tab for nick completion ---
1598 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ (function() {
1599 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const input = document.getElementById('chat-text-input');
1600 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let _tabCandidates = [];
1601 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let _tabIdx = -1;
1602 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let _tabPrefix = '';
1603 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1604 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input.addEventListener('keydown', e => {
1605 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (e.key === 'Enter') { e.preventDefault(); sendMsg(); return; }
1606 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (e.key === 'Tab') {
1607 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ e.preventDefault();
1608 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const val = input.value;
1609 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const cursor = input.selectionStart;
1610 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Find the word being typed up to cursor
1611 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const before = val.slice(0, cursor);
1612 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const wordStart = before.search(/\S+$/);
1613 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const word = wordStart === -1 ? '' : before.slice(wordStart);
1614 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!word) return;
1615 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1616 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // On first Tab press with this prefix, build candidate list
1617 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (_tabIdx === -1 || word.toLowerCase() !== _tabPrefix.toLowerCase()) {
1618 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _tabPrefix = word;
1619 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const nicks = Array.from(document.querySelectorAll('#nicklist-users .nicklist-nick'))
1620 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .map(el => el.textContent.replace(/^●\s*/, '').trim())
1621 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ .filter(n => n.toLowerCase().startsWith(word.toLowerCase()));
1622 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!nicks.length) return;
1623 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _tabCandidates = nicks;
1624 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _tabIdx = 0;
1625 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
1626 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _tabIdx = (_tabIdx + 1) % _tabCandidates.length;
1627 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1628 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1629 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const chosen = _tabCandidates[_tabIdx];
1630 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // If at start of message, append ': '
1631 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const suffix = (wordStart === 0 && before.slice(0, wordStart) === '') ? ': ' : ' ';
1632 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const newVal = val.slice(0, wordStart === -1 ? 0 : wordStart) + chosen + suffix + val.slice(cursor);
1633 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input.value = newVal;
1634 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const newPos = (wordStart === -1 ? 0 : wordStart) + chosen.length + suffix.length;
1635 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input.setSelectionRange(newPos, newPos);
1636 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return;
1637 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1638 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Any other key resets tab state
1639 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _tabIdx = -1;
1640 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _tabPrefix = '';
1641 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ });
1642 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })();
1643 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1644 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- sidebar collapse toggles ---
1645 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function toggleSidebar(side) {
1646 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (side === 'left') {
1647 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('chat-sidebar-left');
1648 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const btn = document.getElementById('sidebar-left-toggle');
1649 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const collapsed = el.classList.toggle('collapsed');
1650 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.textContent = collapsed ? '›' : '‹';
1651 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.title = collapsed ? 'expand' : 'collapse';
1652 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
1653 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('chat-nicklist');
1654 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const btn = document.getElementById('sidebar-right-toggle');
1655 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const collapsed = el.classList.toggle('collapsed');
1656 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.textContent = collapsed ? '‹' : '›';
1657 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.title = collapsed ? 'expand' : 'collapse';
1658 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1659 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1660 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1661 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- sidebar drag-to-resize ---
1662 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ (function() {
1663 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function makeResizable(handleId, sidebarId, side) {
1664 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const handle = document.getElementById(handleId);
1665 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const sidebar = document.getElementById(sidebarId);
1666 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!handle || !sidebar) return;
1667 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let startX, startW;
1668 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ handle.addEventListener('mousedown', e => {
1669 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ e.preventDefault();
1670 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ startX = e.clientX;
1671 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ startW = sidebar.offsetWidth;
1672 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ handle.classList.add('dragging');
1673 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const onMove = mv => {
1674 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const delta = side === 'left' ? mv.clientX - startX : startX - mv.clientX;
1675 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const w = Math.max(28, Math.min(400, startW + delta));
1676 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sidebar.style.width = w + 'px';
1677 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (w <= 30) sidebar.classList.add('collapsed');
1678 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ else sidebar.classList.remove('collapsed');
1679 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ };
1680 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const onUp = () => {
1681 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ handle.classList.remove('dragging');
1682 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.removeEventListener('mousemove', onMove);
1683 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.removeEventListener('mouseup', onUp);
1684 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ };
1685 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.addEventListener('mousemove', onMove);
1686 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.addEventListener('mouseup', onUp);
1687 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ });
1688 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1689 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ makeResizable('resize-left', 'chat-sidebar-left', 'left');
1690 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ makeResizable('resize-right', 'chat-nicklist', 'right');
1691 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ })();
1692 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1693 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- helpers ---
1694 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderAlert(type, msg) {
1695 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return `<div class="alert ${type}"><span class="icon">${{info:'ℹ',error:'✕',success:'✓'}[type]}</span><div>${msg}</div></div>`;
1696 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1697 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function esc(s) {
1698 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
1699 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1700 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function fmtTime(iso) {
1701 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!iso) return '—';
1702 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const d = new Date(iso);
1703 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return d.toLocaleDateString()+' '+d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
1704 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1705 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1706 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- collapsible cards ---
1707 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function toggleCard(id, e) {
1708 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Don't collapse when clicking buttons inside the header.
1709 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (e && e.target.tagName === 'BUTTON') return;
1710 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById(id).classList.toggle('collapsed');
1711 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1712 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1713 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- per-bot config schemas ---
1714 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const BEHAVIOR_SCHEMAS = {
1715 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ snitch: [
1716 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'alert_channel', label:'Alert channel', type:'text', placeholder:'#ops', hint:'Channel to post alerts in' },
1717 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'alert_nicks', label:'Alert nicks', type:'text', placeholder:'alice, bob', hint:'Operators to DM (comma-separated)' },
1718 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'flood_messages', label:'Flood threshold', type:'number', placeholder:'10', hint:'Messages in window that triggers alert' },
1719 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'flood_window_s', label:'Flood window (s)', type:'number', placeholder:'5', hint:'Rolling window duration in seconds' },
1720 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'joinpart_threshold',label:'Join/part threshold', type:'number', placeholder:'5', hint:'Join+part events before cycling alert' },
1721 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'joinpart_window_s', label:'Join/part window (s)',type:'number', placeholder:'30' },
1722 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
1723 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ warden: [
1724 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'flood_threshold', label:'Flood threshold', type:'number', placeholder:'10', hint:'Messages/window before action' },
1725 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'window_s', label:'Window (s)', type:'number', placeholder:'5' },
1726 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'warn_before_mute', label:'Warn before mute', type:'checkbox' },
1727 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'mute_duration_s', label:'Mute duration (s)', type:'number', placeholder:'300' },
1728 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'kick_after_mutes', label:'Kick after N mutes',type:'number', placeholder:'3' },
1729 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
1730 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ oracle: [
1731 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'backend', label:'LLM backend', type:'llm-backend', hint:'Backend configured in the AI tab' },
1732 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'model', label:'Model override', type:'model-override', backendKey:'backend', hint:'Override the backend default model (optional)' },
1733 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'scribe_dir', label:'Scribe log dir', type:'text', placeholder:'./data/logs/scribe', hint:'Directory scribe writes to — oracle reads history from here' },
1734 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'max_messages', label:'Max messages', type:'number', placeholder:'50', hint:'Default message count for summaries' },
1735 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
1736 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ scribe: [
1737 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'dir', label:'Log directory', type:'text', placeholder:'./data/logs', hint:'Directory to write log files into' },
1738 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'format', label:'Format', type:'select', options:['jsonl','csv','text'], hint:'jsonl=structured, csv=spreadsheet, text=human-readable' },
1739 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'rotation', label:'Rotation', type:'select', options:['none','daily','weekly','monthly','yearly','size'], hint:'When to start a new log file' },
1740 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'max_size_mb', label:'Max size (MiB)', type:'number', placeholder:'100', hint:'Only applies when rotation = size' },
1741 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'per_channel', label:'Per-channel files',type:'checkbox', hint:'Separate file per channel' },
1742 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'max_age_days', label:'Max age (days)', type:'number', placeholder:'0', hint:'Prune old rotated files; 0 = keep all' },
1743 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
1744 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ herald: [
1745 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'webhook_path', label:'Webhook path', type:'text', placeholder:'/webhooks/herald', hint:'HTTP path that receives inbound events' },
1746 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'rate_limit', label:'Rate limit (msg/min)', type:'number', placeholder:'60' },
1747 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
1748 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ scroll: [
1749 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'max_replay', label:'Max replay', type:'number', placeholder:'100', hint:'Max messages per request' },
1750 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'require_auth', label:'Require auth', type:'checkbox', hint:'Only registered agents can query history' },
1751 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
1752 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ systembot: [
1753 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'log_joins', label:'Log joins', type:'checkbox' },
1754 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'log_parts', label:'Log parts/quits', type:'checkbox' },
1755 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'log_modes', label:'Log mode changes', type:'checkbox' },
1756 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'log_kicks', label:'Log kicks', type:'checkbox' },
1757 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
1758 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ auditbot: [
1759 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'retention_days', label:'Retention (days)', type:'number', placeholder:'90', hint:'0 = keep forever' },
1760 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'log_path', label:'Log path', type:'text', placeholder:'/var/log/scuttlebot/audit.log' },
1761 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
1762 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sentinel: [
1763 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'backend', label:'LLM backend', type:'llm-backend', hint:'Backend configured in the AI tab' },
1764 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'model', label:'Model override', type:'model-override', backendKey:'backend', hint:'Override the backend default model (optional)' },
1765 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'mod_channel', label:'Mod channel', type:'text', placeholder:'#moderation', hint:'Channel where incident reports are posted' },
1766 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'dm_operators', label:'DM operators', type:'checkbox', hint:'Also send incident reports as DMs to operator nicks' },
1767 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'alert_nicks', label:'Operator nicks', type:'text', placeholder:'alice,bob', hint:'Comma-separated nicks to DM on incidents (requires DM operators)' },
1768 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'policy', label:'Policy', type:'text', placeholder:'Flag harassment, hate speech, spam and threats.', hint:'Plain-English description of what to flag' },
1769 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'min_severity', label:'Min severity', type:'select', options:['low','medium','high'], hint:'Minimum severity level to report' },
1770 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'window_size', label:'Window size', type:'number', placeholder:'20', hint:'Messages to buffer per channel before analysis' },
1771 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'window_age_sec', label:'Window age (s)', type:'number', placeholder:'300', hint:'Max seconds before a stale buffer is force-scanned' },
1772 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'cooldown_sec', label:'Cooldown (s)', type:'number', placeholder:'600', hint:'Min seconds between reports about the same nick' },
1773 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
1774 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ steward: [
1775 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'mod_channel', label:'Mod channel', type:'text', placeholder:'#moderation', hint:'Channel steward watches for sentinel reports and posts action logs' },
1776 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'operator_nicks', label:'Operator nicks', type:'text', placeholder:'alice,bob', hint:'Comma-separated nicks allowed to issue direct commands via DM' },
1777 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'dm_on_action', label:'DM operators', type:'checkbox', hint:'Send a DM to operator nicks when steward takes action' },
1778 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'auto_act', label:'Auto-act', type:'checkbox', hint:'Automatically act on sentinel incident reports' },
1779 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'warn_on_low', label:'Warn on low', type:'checkbox', hint:'Send a warning notice for low-severity incidents' },
1780 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'mute_duration_sec', label:'Mute duration (s)',type:'number', placeholder:'600', hint:'How long medium-severity mutes last' },
1781 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ { key:'cooldown_sec', label:'Cooldown (s)', type:'number', placeholder:'300', hint:'Min seconds between automated actions on the same nick' },
1782 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
1783 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ };
1784 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1785 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderBehConfig(b) {
1786 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const schema = BEHAVIOR_SCHEMAS[b.id];
1787 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!schema) return '';
1788 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const cfg = b.config || {};
1789 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const fields = schema.map(f => {
1790 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const val = cfg[f.key];
1791 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let input = '';
1792 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (f.type === 'checkbox') {
1793 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input = `<input type="checkbox" ${val?'checked':''} style="accent-color:#58a6ff" onchange="onBehCfg('${esc(b.id)}','${f.key}',this.checked)">`;
1794 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else if (f.type === 'select') {
1795 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input = `<select onchange="onBehCfg('${esc(b.id)}','${f.key}',this.value)">${(f.options||[]).map(o=>`<option ${val===o?'selected':''}>${esc(o)}</option>`).join('')}</select>`;
1796 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else if (f.type === 'llm-backend') {
1797 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const opts = _llmBackendNames.map(n => `<option value="${esc(n)}" ${val===n?'selected':''}>${esc(n)}</option>`).join('');
1798 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const noMatch = val && !_llmBackendNames.includes(val);
1799 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input = `<select onchange="onBehCfg('${esc(b.id)}','${f.key}',this.value)">
1800 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="">— select backend —</option>
1801 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ${opts}
1802 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ${noMatch ? `<option value="${esc(val)}" selected>${esc(val)}</option>` : ''}
1803 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </select>`;
1804 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else if (f.type === 'model-override') {
1805 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const selId = `beh-msel-${esc(b.id)}`;
1806 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const customId = `beh-mcustom-${esc(b.id)}`;
1807 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const backendKey = f.backendKey || 'backend';
1808 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const currentVal = val || '';
1809 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input = `<div style="display:flex;gap:6px;align-items:flex-start">
1810 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="flex:1">
1811 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <select id="${selId}" style="width:100%" onchange="onBehModelSelect('${esc(b.id)}','${f.key}','${selId}','${customId}')">
1812 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="">— none / auto-select —</option>
1813 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ${currentVal ? `<option value="${esc(currentVal)}" selected>${esc(currentVal)}</option>` : ''}
1814 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <option value="__other__">— other (type below) —</option>
1815 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </select>
1816 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="text" id="${customId}" placeholder="model-id" autocomplete="off"
1817 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ style="display:none;margin-top:6px"
1818 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ onchange="onBehCfg('${esc(b.id)}','${f.key}',this.value)">
1819 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
1820 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button type="button" class="sm" style="white-space:nowrap;margin-top:1px"
1821 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ onclick="loadBehModels(this,'${esc(b.id)}','${backendKey}','${f.key}','${selId}','${customId}')">↺</button>
1822 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>`;
1823 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
1824 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ input = `<input type="${f.type}" placeholder="${esc(f.placeholder||'')}" value="${esc(String(val??''))}" onchange="onBehCfg('${esc(b.id)}','${f.key}',this.value${f.type==='number'?'*1':''})">`;
1825 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1826 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return `<div class="beh-field"><label>${esc(f.label)}</label>${input}${f.hint?`<div class="hint">${esc(f.hint)}</div>`:''}</div>`;
1827 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }).join('');
1828 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return `<div class="beh-config" id="beh-cfg-${esc(b.id)}">${fields}</div>`;
1829 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1830 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1831 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function toggleBehConfig(id) {
1832 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('beh-cfg-' + id);
1833 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!el) return;
1834 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.classList.toggle('open');
1835 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const btn = document.getElementById('beh-cfg-btn-' + id);
1836 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (btn) btn.textContent = el.classList.contains('open') ? 'configure ▴' : 'configure ▾';
1837 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1838 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1839 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function onBehCfg(id, key, val) {
1840 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const b = currentPolicies.behaviors.find(x => x.id === id);
1841 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!b) return;
1842 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!b.config) b.config = {};
1843 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b.config[key] = val;
1844 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1845 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1846 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function onBehModelSelect(botId, key, selId, customId) {
1847 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const sel = document.getElementById(selId);
1848 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const custom = document.getElementById(customId);
1849 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!sel) return;
1850 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ custom.style.display = sel.value === '__other__' ? '' : 'none';
1851 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (sel.value !== '__other__') onBehCfg(botId, key, sel.value);
1852 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1853 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1854 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadBehModels(btn, botId, backendKey, modelKey, selId, customId) {
1855 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const b = currentPolicies && currentPolicies.behaviors.find(x => x.id === botId);
1856 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const backendName = b && b.config && b.config[backendKey];
1857 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!backendName) {
1858 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ alert('Select a backend first, then click ↺ to load its models.');
1859 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return;
1860 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1861 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const origText = btn.textContent;
1862 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.textContent = '…';
1863 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = true;
1864 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1865 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const models = await api('GET', `/v1/llm/backends/${encodeURIComponent(backendName)}/models`);
1866 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const sel = document.getElementById(selId);
1867 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const custom = document.getElementById(customId);
1868 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!sel) return;
1869 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const current = (b.config && b.config[modelKey]) || '';
1870 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sel.innerHTML = '<option value="">— none / auto-select —</option>';
1871 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (const m of (models || [])) {
1872 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const id = typeof m === 'string' ? m : m.id;
1873 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const label = (typeof m === 'object' && m.name && m.name !== id) ? `${id} — ${m.name}` : id;
1874 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const opt = document.createElement('option');
1875 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ opt.value = id;
1876 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ opt.textContent = label;
1877 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (id === current) opt.selected = true;
1878 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sel.appendChild(opt);
1879 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1880 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const other = document.createElement('option');
1881 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ other.value = '__other__';
1882 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ other.textContent = '— other (type below) —';
1883 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const matched = (models || []).some(m => (typeof m === 'string' ? m : m.id) === current);
1884 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (current && !matched) {
1885 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ other.selected = true;
1886 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (custom) { custom.value = current; custom.style.display = ''; }
1887 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1888 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sel.appendChild(other);
1889 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
1890 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ alert('Model discovery failed: ' + e.message);
1891 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } finally {
1892 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.textContent = origText;
1893 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = false;
1894 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1895 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1896 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1897 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- admin accounts ---
1898 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadAdmins() {
1899 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1900 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const data = await api('GET', '/v1/admins');
1901 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderAdmins(data.admins || []);
1902 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
1903 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // admins endpoint may not exist on token-only setups
1904 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('admins-list-container').innerHTML = '';
1905 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1906 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1907 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1908 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderAdmins(admins) {
1909 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('admins-list-container');
1910 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!admins.length) { el.innerHTML = ''; return; }
1911 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const rows = admins.map(a => `<tr>
1912 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><strong>${esc(a.username)}</strong></td>
1913 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td style="color:#8b949e;font-size:12px">${fmtTime(a.created)}</td>
1914 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <td><div class="actions">
1915 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="promptAdminPassword('${esc(a.username)}')">change password</button>
1916 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm danger" onclick="removeAdmin('${esc(a.username)}')">remove</button>
1917 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div></td>
1918 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </tr>`).join('');
1919 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = `<table><thead><tr><th>username</th><th>created</th><th></th></tr></thead><tbody>${rows}</tbody></table>`;
1920 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1921 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1922 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function addAdmin(e) {
1923 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ e.preventDefault();
1924 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const username = document.getElementById('new-admin-username').value.trim();
1925 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const password = document.getElementById('new-admin-password').value;
1926 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const resultEl = document.getElementById('add-admin-result');
1927 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!username || !password) return;
1928 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1929 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('POST', '/v1/admins', { username, password });
1930 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.innerHTML = renderAlert('success', `Admin <strong>${esc(username)}</strong> added.`);
1931 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.style.display = 'block';
1932 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('add-admin-form').reset();
1933 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await loadAdmins();
1934 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ setTimeout(() => { resultEl.style.display = 'none'; }, 3000);
1935 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
1936 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.innerHTML = renderAlert('error', e.message);
1937 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.style.display = 'block';
1938 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1939 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1940 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1941 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function removeAdmin(username) {
1942 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!confirm(`Remove admin "${username}"?`)) return;
1943 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1944 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('DELETE', `/v1/admins/${encodeURIComponent(username)}`);
1945 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await loadAdmins();
1946 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { alert('Remove failed: ' + e.message); }
1947 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1948 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1949 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function promptAdminPassword(username) {
1950 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const pw = prompt(`New password for ${username}:`);
1951 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!pw) return;
1952 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1953 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('PUT', `/v1/admins/${encodeURIComponent(username)}/password`, { password: pw });
1954 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ alert('Password updated.');
1955 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { alert('Failed: ' + e.message); }
1956 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1957 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1958 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- AI / LLM tab ---
1959 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadAI() {
1960 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await Promise.all([loadAIBackends(), loadAIKnown()]);
1961 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1962 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1963 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadAIBackends() {
1964 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('ai-backends-list');
1965 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
1966 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const backends = await api('GET', '/v1/llm/backends');
1967 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!backends || backends.length === 0) {
1968 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = '<div class="empty-state">No LLM backends configured yet. Click <strong>+ add backend</strong> above or add them to <code>scuttlebot.yaml</code>.</div>';
1969 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return;
1970 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1971 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = backends.map(b => {
1972 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const sid = CSS.escape(b.name);
1973 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const editable = b.source === 'policy';
1974 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const srcBadge = b.source === 'config'
1975 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ? '<span class="badge" style="background:#21262d;color:#8b949e;border:1px solid #30363d">yaml</span>'
1976 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ : '<span class="badge" style="background:#21262d;color:#58a6ff;border:1px solid #1f6feb">ui</span>';
1977 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return `<div class="setting-row" style="flex-wrap:wrap;gap:8px;align-items:center">
1978 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="flex:1;min-width:140px">
1979 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="font-weight:500;color:#e6edf3">${esc(b.name)} ${srcBadge}${b.default ? ' <span class="badge" style="background:#1f6feb">default</span>' : ''}</div>
1980 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="font-size:11px;color:#8b949e;margin-top:2px">${esc(b.backend)}${b.region ? ' · ' + esc(b.region) : ''}${b.base_url ? ' · ' + esc(b.base_url) : ''}</div>
1981 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
1982 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="min-width:100px;font-size:12px;color:#8b949e">${b.model ? 'model: <code style="color:#a5d6ff">' + esc(b.model) + '</code>' : '<span style="color:#6e7681">model: auto</span>'}</div>
1983 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div id="ai-models-${sid}" style="width:100%;display:none"></div>
1984 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm" onclick="discoverModels('${esc(b.name)}', this)">discover models</button>
1985 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ${editable ? `<button class="sm" onclick="openEditBackend('${esc(b.name)}')">edit</button>
1986 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <button class="sm danger" onclick="deleteBackend('${esc(b.name)}', this)">delete</button>` : ''}
1987 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>`;
1988 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }).join('<div style="height:1px;background:#21262d;margin:4px 0"></div>');
1989 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
1990 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = `<div class="empty-state" style="color:#f85149">${esc(String(e))}</div>`;
1991 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1992 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
1993 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1994 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- backend form ---
1995 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1996 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let _editingBackend = null; // null = adding, string = name being edited
1997 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let _backendList = []; // cached for edit lookups
1998 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
1999 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function openAddBackend() {
2000 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _editingBackend = null;
2001 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('ai-form-title').textContent = 'add backend';
2002 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-submit-btn').textContent = 'add backend';
2003 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-name').value = '';
2004 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-name').disabled = false;
2005 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-backend').value = '';
2006 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-apikey').value = '';
2007 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-baseurl').value = '';
2008 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ populateModelSelect([], '');
2009 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-model-custom').value = '';
2010 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-model-custom').style.display = 'none';
2011 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-load-models-btn').textContent = '↺ load models';
2012 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-default').checked = false;
2013 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-region').value = '';
2014 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-aws-key-id').value = '';
2015 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-aws-secret').value = '';
2016 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-allow').value = '';
2017 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-block').value = '';
2018 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('ai-form-result').style.display = 'none';
2019 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ onBackendTypeChange();
2020 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const card = document.getElementById('card-ai-form');
2021 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ card.style.display = '';
2022 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ card.scrollIntoView({ behavior: 'smooth', block: 'start' });
2023 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2024 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2025 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function openEditBackend(name) {
2026 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let b;
2027 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
2028 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const backends = await api('GET', '/v1/llm/backends');
2029 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ b = backends.find(x => x.name === name);
2030 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) { return; }
2031 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!b) return;
2032 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2033 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _editingBackend = name;
2034 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('ai-form-title').textContent = 'edit backend — ' + esc(name);
2035 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-submit-btn').textContent = 'save changes';
2036 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-name').value = name;
2037 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-name').disabled = true; // name is immutable
2038 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-backend').value = b.backend || '';
2039 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-apikey').value = ''; // never pre-fill secrets
2040 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-baseurl').value = b.base_url || '';
2041 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const curated = KNOWN_MODELS[b.backend] || [];
2042 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ populateModelSelect(curated, b.model || '');
2043 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-model-custom').style.display = 'none';
2044 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-load-models-btn').textContent = '↺ load models';
2045 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-default').checked = !!b.default;
2046 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-region').value = b.region || '';
2047 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-aws-key-id').value = ''; // never pre-fill
2048 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-aws-secret').value = '';
2049 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-allow').value = (b.allow || []).join('\n');
2050 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-block').value = (b.block || []).join('\n');
2051 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('ai-form-result').style.display = 'none';
2052 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ onBackendTypeChange();
2053 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const card = document.getElementById('card-ai-form');
2054 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ card.style.display = '';
2055 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ card.scrollIntoView({ behavior: 'smooth', block: 'start' });
2056 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2057 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2058 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function closeBackendForm() {
2059 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('card-ai-form').style.display = 'none';
2060 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _editingBackend = null;
2061 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2062 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2063 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Curated model lists per backend — shown before live discovery.
2064 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const KNOWN_MODELS = {
2065 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ anthropic: [
2066 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001',
2067 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229',
2068 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
2069 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ gemini: [
2070 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-flash',
2071 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'gemini-1.5-flash-8b', 'gemini-1.5-pro',
2072 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
2073 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ openai: [
2074 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1', 'o1-mini', 'o3-mini', 'gpt-3.5-turbo',
2075 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
2076 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ bedrock: [
2077 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'anthropic.claude-3-5-sonnet-20241022-v2:0', 'anthropic.claude-3-5-haiku-20241022-v1:0',
2078 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'anthropic.claude-3-opus-20240229-v1:0',
2079 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'amazon.nova-pro-v1:0', 'amazon.nova-lite-v1:0', 'amazon.nova-micro-v1:0',
2080 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'meta.llama3-70b-instruct-v1:0', 'meta.llama3-8b-instruct-v1:0',
2081 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'mistral.mistral-large-2402-v1:0',
2082 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
2083 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ollama: ['llama3.2', 'llama3.1', 'mistral', 'codellama', 'gemma2', 'qwen2.5', 'phi3'],
2084 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ groq: [
2085 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'llama-3.3-70b-versatile', 'llama-3.1-8b-instant',
2086 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'mixtral-8x7b-32768', 'gemma2-9b-it',
2087 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
2088 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ mistral: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest', 'open-mistral-nemo'],
2089 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ deepseek: ['deepseek-chat', 'deepseek-reasoner'],
2090 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ xai: ['grok-2', 'grok-2-mini', 'grok-beta'],
2091 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ cerebras: ['llama3.1-8b', 'llama3.1-70b', 'llama3.3-70b'],
2092 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ together: [
2093 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
2094 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'meta-llama/Llama-3.1-8B-Instruct-Turbo',
2095 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'mistralai/Mixtral-8x7B-Instruct-v0.1',
2096 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'Qwen/Qwen2.5-72B-Instruct-Turbo',
2097 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
2098 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ fireworks: [
2099 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'accounts/fireworks/models/llama-v3p3-70b-instruct',
2100 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'accounts/fireworks/models/mixtral-8x7b-instruct',
2101 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
2102 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ openrouter: [], // too varied — always load live
2103 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ huggingface: [
2104 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'meta-llama/Llama-3.3-70B-Instruct',
2105 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'mistralai/Mistral-7B-Instruct-v0.3',
2106 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ 'Qwen/Qwen2.5-72B-Instruct',
2107 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ],
2108 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ };
2109 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2110 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function populateModelSelect(models, currentVal) {
2111 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const sel = document.getElementById('bf-model-select');
2112 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sel.innerHTML = '<option value="">— none / auto-select —</option>';
2113 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for (const m of models) {
2114 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const id = typeof m === 'string' ? m : m.id;
2115 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const label = (typeof m === 'object' && m.name && m.name !== id) ? `${id} — ${m.name}` : id;
2116 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const opt = document.createElement('option');
2117 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ opt.value = id;
2118 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ opt.textContent = label;
2119 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (id === currentVal) opt.selected = true;
2120 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sel.appendChild(opt);
2121 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2122 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const other = document.createElement('option');
2123 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ other.value = '__other__';
2124 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ other.textContent = '— other (type below) —';
2125 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (currentVal && !models.find(m => (typeof m === 'string' ? m : m.id) === currentVal)) {
2126 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ other.selected = true;
2127 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-model-custom').value = currentVal;
2128 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-model-custom').style.display = '';
2129 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2130 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sel.appendChild(other);
2131 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2132 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2133 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function onModelSelectChange() {
2134 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const sel = document.getElementById('bf-model-select');
2135 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const custom = document.getElementById('bf-model-custom');
2136 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ custom.style.display = sel.value === '__other__' ? '' : 'none';
2137 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2138 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2139 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function getModelValue() {
2140 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const sel = document.getElementById('bf-model-select');
2141 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (sel.value === '__other__') return document.getElementById('bf-model-custom').value.trim();
2142 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return sel.value || '';
2143 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2144 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2145 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function onBackendTypeChange() {
2146 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const t = document.getElementById('bf-backend').value;
2147 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const isBedrock = t === 'bedrock';
2148 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const isLocal = ['ollama','litellm','lmstudio','jan','localai','vllm','anythingllm'].includes(t);
2149 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const hasKey = !isBedrock;
2150 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2151 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-bedrock-group').style.display = isBedrock ? '' : 'none';
2152 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-apikey-row').style.display = hasKey ? '' : 'none';
2153 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('bf-baseurl-row').style.display = (isLocal || isBedrock) ? 'none' : '';
2154 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2155 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const curated = KNOWN_MODELS[t] || [];
2156 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ populateModelSelect(curated, '');
2157 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2158 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2159 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadLiveModels(btn) {
2160 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const t = document.getElementById('bf-backend').value;
2161 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!t) { alert('Select a backend type first.'); return; }
2162 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2163 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = true;
2164 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.textContent = 'loading…';
2165 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
2166 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const payload = {
2167 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backend: t,
2168 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ api_key: document.getElementById('bf-apikey')?.value || '',
2169 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ base_url: document.getElementById('bf-baseurl')?.value.trim() || '',
2170 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ region: document.getElementById('bf-region')?.value.trim() || '',
2171 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ aws_key_id: document.getElementById('bf-aws-key-id')?.value.trim() || '',
2172 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ aws_secret_key: document.getElementById('bf-aws-secret')?.value || '',
2173 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ };
2174 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const models = await api('POST', '/v1/llm/discover', payload);
2175 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const current = getModelValue();
2176 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ populateModelSelect(models, current);
2177 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.textContent = `↺ ${models.length} loaded`;
2178 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
2179 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.textContent = '✕ failed';
2180 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ setTimeout(() => { btn.textContent = '↺ load models'; }, 2000);
2181 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ alert('Discovery failed: ' + String(e));
2182 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } finally {
2183 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = false;
2184 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2185 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2186 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2187 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function submitBackendForm() {
2188 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const name = document.getElementById('bf-name').value.trim();
2189 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const backend = document.getElementById('bf-backend').value;
2190 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!name || !backend) {
2191 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ showFormResult('name and backend type are required', 'error');
2192 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return;
2193 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2194 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2195 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const allow = document.getElementById('bf-allow').value.trim().split('\n').map(s=>s.trim()).filter(Boolean);
2196 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const block = document.getElementById('bf-block').value.trim().split('\n').map(s=>s.trim()).filter(Boolean);
2197 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2198 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const payload = {
2199 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ name, backend,
2200 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ api_key: document.getElementById('bf-apikey').value || undefined,
2201 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ base_url: document.getElementById('bf-baseurl').value.trim() || undefined,
2202 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ model: getModelValue() || undefined,
2203 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ default: document.getElementById('bf-default').checked || undefined,
2204 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ region: document.getElementById('bf-region').value.trim() || undefined,
2205 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ aws_key_id: document.getElementById('bf-aws-key-id').value.trim() || undefined,
2206 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ aws_secret_key: document.getElementById('bf-aws-secret').value || undefined,
2207 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ allow: allow.length ? allow : undefined,
2208 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ block: block.length ? block : undefined,
2209 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ };
2210 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2211 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const btn = document.getElementById('bf-submit-btn');
2212 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = true;
2213 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
2214 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (_editingBackend) {
2215 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('PUT', `/v1/llm/backends/${encodeURIComponent(_editingBackend)}`, payload);
2216 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
2217 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('POST', '/v1/llm/backends', payload);
2218 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2219 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ closeBackendForm();
2220 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await loadAIBackends();
2221 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
2222 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ showFormResult(String(e), 'error');
2223 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } finally {
2224 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = false;
2225 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2226 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2227 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2228 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function deleteBackend(name, btn) {
2229 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = true;
2230 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
2231 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await api('DELETE', `/v1/llm/backends/${encodeURIComponent(name)}`);
2232 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ await loadAIBackends();
2233 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
2234 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = false;
2235 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ alert('Delete failed: ' + String(e));
2236 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2237 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2238 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2239 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function showFormResult(msg, type) {
2240 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('ai-form-result');
2241 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.style.display = '';
2242 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.className = 'alert ' + (type === 'error' ? 'danger' : 'info');
2243 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = `<span class="icon">${type === 'error' ? '✕' : 'ℹ'}</span><span>${esc(msg)}</span>`;
2244 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2245 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2246 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function discoverModels(name, btn) {
2247 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('ai-models-' + name);
2248 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!el) return;
2249 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = true;
2250 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.textContent = 'discovering…';
2251 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
2252 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const models = await api('GET', `/v1/llm/backends/${encodeURIComponent(name)}/models`);
2253 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.style.display = 'block';
2254 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!models || models.length === 0) {
2255 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = '<div style="font-size:12px;color:#8b949e;padding:6px 0">No models found (check filters).</div>';
2256 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
2257 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = `<div style="font-size:12px;color:#8b949e;margin-bottom:6px">${models.length} model${models.length !== 1 ? 's' : ''} available:</div>
2258 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;flex-wrap:wrap;gap:4px">${models.map(m =>
2259 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<code style="background:#161b22;border:1px solid #30363d;border-radius:4px;padding:2px 6px;font-size:11px;color:#a5d6ff">${esc(m.id)}${m.name && m.name !== m.id ? ' <span style="color:#6e7681">(' + esc(m.name) + ')</span>' : ''}</code>`
2260 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ).join('')}</div>`;
2261 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2262 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.textContent = '↺ refresh';
2263 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
2264 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.style.display = 'block';
2265 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = `<div style="font-size:12px;color:#f85149">Error: ${esc(String(e))}</div>`;
2266 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.textContent = 'retry';
2267 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } finally {
2268 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ btn.disabled = false;
2269 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2270 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2271 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2272 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadAIKnown() {
2273 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const el = document.getElementById('ai-supported-list');
2274 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
2275 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const known = await api('GET', '/v1/llm/known');
2276 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const native = known.filter(b => b.native);
2277 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const compat = known.filter(b => !b.native);
2278 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ native.sort((a,b) => a.name.localeCompare(b.name));
2279 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ compat.sort((a,b) => a.name.localeCompare(b.name));
2280 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = `
2281 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="margin-bottom:12px">
2282 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="font-size:12px;color:#8b949e;font-weight:500;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px">native APIs</div>
2283 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;flex-wrap:wrap;gap:4px">${native.map(b =>
2284 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<span style="background:#161b22;border:1px solid #30363d;border-radius:4px;padding:3px 8px;font-size:12px;color:#e6edf3">${esc(b.name)}</span>`
2285 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ).join('')}</div>
2286 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
2287 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
2288 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="font-size:12px;color:#8b949e;font-weight:500;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px">OpenAI-compatible</div>
2289 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;flex-wrap:wrap;gap:4px">${compat.map(b =>
2290 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `<span style="background:#161b22;border:1px solid #30363d;border-radius:4px;padding:3px 8px;font-size:12px;color:#e6edf3" title="${esc(b.base_url)}">${esc(b.name)}</span>`
2291 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ).join('')}</div>
2292 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>`;
2293 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
2294 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ el.innerHTML = `<div class="empty-state" style="color:#f85149">${esc(String(e))}</div>`;
2295 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2296 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2297 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2298 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function showAIExample(e) {
2299 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ e.preventDefault();
2300 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const card = document.getElementById('card-ai-example');
2301 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ card.style.display = '';
2302 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ card.scrollIntoView({ behavior: 'smooth', block: 'start' });
2303 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Expand it if collapsed.
2304 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const body = card.querySelector('.card-body');
2305 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (body) body.style.display = '';
2306 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2307 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2308 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // --- settings / policies ---
2309 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let currentPolicies = null;
2310 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ let _llmBackendNames = []; // cached backend names for oracle dropdown
2311 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2312 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function loadSettings() {
2313 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
2314 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const [s, backends] = await Promise.all([
2315 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ api('GET', '/v1/settings'),
2316 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ api('GET', '/v1/llm/backends').catch(() => []),
2317 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ]);
2318 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ _llmBackendNames = (backends || []).map(b => b.name);
2319 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderTLSStatus(s.tls);
2320 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ currentPolicies = s.policies;
2321 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderBehaviors(s.policies.behaviors || []);
2322 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderAgentPolicy(s.policies.agent_policy || {});
2323 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderBridgePolicy(s.policies.bridge || {});
2324 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderLoggingPolicy(s.policies.logging || {});
2325 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ loadAdmins();
2326 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } catch(e) {
2327 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('tls-badge').textContent = 'error';
2328 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2329 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2330 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2331 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderTLSStatus(tls) {
2332 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const badge = document.getElementById('tls-badge');
2333 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (tls.enabled) {
2334 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ badge.textContent = 'TLS active';
2335 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ badge.style.background = '#3fb95022'; badge.style.color = '#3fb950'; badge.style.borderColor = '#3fb95044';
2336 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ } else {
2337 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ badge.textContent = 'HTTP only';
2338 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2339 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('tls-status-rows').innerHTML = `
2340 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
2341 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">mode</div>
2342 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc"></div>
2343 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <code class="setting-val">${tls.enabled ? 'HTTPS (Let\'s Encrypt)' : 'HTTP'}</code>
2344 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
2345 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ${tls.enabled ? `
2346 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
2347 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">domain</div>
2348 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc"></div>
2349 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <code class="setting-val">${esc(tls.domain)}</code>
2350 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
2351 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-row">
2352 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-label">allow insecure</div>
2353 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div class="setting-desc">Plain HTTP also accepted.</div>
2354 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <code class="setting-val">${tls.allow_insecure ? 'yes' : 'no'}</code>
2355 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>` : ''}
2356 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `;
2357 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2358 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2359 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderBehaviors(behaviors) {
2360 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const hasSchema = id => !!BEHAVIOR_SCHEMAS[id];
2361 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('behaviors-list').innerHTML = behaviors.map(b => `
2362 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div>
2363 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:grid;grid-template-columns:20px 90px 1fr auto;align-items:center;gap:12px;padding:11px 16px;border-bottom:1px solid #21262d">
2364 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="checkbox" ${b.enabled?'checked':''} onchange="onBehaviorToggle('${esc(b.id)}',this.checked)" style="width:14px;height:14px;cursor:pointer;accent-color:#58a6ff">
2365 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <strong style="font-size:13px;white-space:nowrap">${esc(b.name)}</strong>
2366 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span style="font-size:12px;color:#8b949e">${esc(b.description)}</span>
2367 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <div style="display:flex;align-items:center;gap:8px;justify-content:flex-end">
2368 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ${b.enabled ? `
2369 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <label style="display:flex;align-items:center;gap:4px;font-size:11px;color:#8b949e;cursor:pointer;white-space:nowrap">
2370 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <input type="checkbox" ${b.join_all_channels?'checked':''} onchange="onBehaviorJoinAll('${esc(b.id)}',this.checked)" style="accent-color:#58a6ff">
2371 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ all channels
2372 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </label>
2373 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ${b.join_all_channels
2374 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ? `<input type="text" placeholder="exclude #private, /regex/" style="width:160px;padding:3px 7px;font-size:11px" title="Exclude: comma-separated names or /regex/" value="${esc((b.exclude_channels||[]).join(', '))}" onchange="onBehaviorExclude('${esc(b.id)}',this.value)">`
2375 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ : `<input type="text" placeholder="#ops, /^#proj-.*/" style="width:160px;padding:3px 7px;font-size:11px" title="Join: comma-separated names or /regex/ patterns" value="${esc((b.required_channels||[]).join(', '))}" onchange="onBehaviorChannels('${esc(b.id)}',this.value)">`
2376 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2377 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ${hasSchema(b.id) ? `<button class="sm" id="beh-cfg-btn-${esc(b.id)}" onclick="toggleBehConfig('${esc(b.id)}')" style="font-size:11px;white-space:nowrap">configure ▾</button>` : ''}
2378 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ` : ''}
2379 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ <span class="tag type-observer" style="font-size:11px;min-width:64px;text-align:center">${esc(b.nick)}</span>
2380 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
2381 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
2382 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ${b.enabled && hasSchema(b.id) ? renderBehConfig(b) : ''}
2383 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ </div>
2384 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ `).join('');
2385 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2386 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2387 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function onBehaviorToggle(id, enabled) {
2388 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const b = currentPolicies.behaviors.find(x => x.id === id);
2389 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (b) b.enabled = enabled;
2390 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderBehaviors(currentPolicies.behaviors);
2391 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2392 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function onBehaviorJoinAll(id, val) {
2393 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const b = currentPolicies.behaviors.find(x => x.id === id);
2394 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (b) b.join_all_channels = val;
2395 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ renderBehaviors(currentPolicies.behaviors);
2396 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2397 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function onBehaviorExclude(id, val) {
2398 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const b = currentPolicies.behaviors.find(x => x.id === id);
2399 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (b) b.exclude_channels = val.split(',').map(s=>s.trim()).filter(Boolean);
2400 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2401 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function onBehaviorChannels(id, val) {
2402 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const b = currentPolicies.behaviors.find(x => x.id === id);
2403 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (b) b.required_channels = val.split(',').map(s=>s.trim()).filter(Boolean);
2404 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2405 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2406 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderAgentPolicy(p) {
2407 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-checkin-enabled').checked = !!p.require_checkin;
2408 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-checkin-channel').value = p.checkin_channel || '';
2409 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-required-channels').value = (p.required_channels||[]).join(', ');
2410 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ toggleCheckinChannel();
2411 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2412 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function toggleCheckinChannel() {
2413 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const on = document.getElementById('policy-checkin-enabled').checked;
2414 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-checkin-row').style.display = on ? '' : 'none';
2415 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2416 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2417 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderBridgePolicy(p) {
2418 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-bridge-web-user-ttl').value = p.web_user_ttl_minutes || 5;
2419 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2420 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2421 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function renderLoggingPolicy(l) {
2422 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-logging-enabled').checked = !!l.enabled;
2423 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-log-dir').value = l.dir || '';
2424 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-log-format').value = l.format || 'jsonl';
2425 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-log-rotation').value = l.rotation || 'none';
2426 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-log-max-size').value = l.max_size_mb || '';
2427 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-log-per-channel').checked = !!l.per_channel;
2428 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-log-max-age').value = l.max_age_days || '';
2429 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ toggleLogOptions();
2430 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ toggleRotationOptions();
2431 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2432 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function toggleLogOptions() {
2433 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const on = document.getElementById('policy-logging-enabled').checked;
2434 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-log-options').style.display = on ? '' : 'none';
2435 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2436 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function toggleRotationOptions() {
2437 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const rot = document.getElementById('policy-log-rotation').value;
2438 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ document.getElementById('policy-log-size-row').style.display = rot === 'size' ? '' : 'none';
2439 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
2440 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
2441 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ async function savePolicies() {
2442 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if (!currentPolicies) return;
2443 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const p = JSON.parse(JSON.stringify(currentPolicies)); // deep copy
2444 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ p.agent_policy = {
2445 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ require_checkin: document.getElementById('policy-checkin-enabled').checked,
2446 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ checkin_channel: document.getElementById('policy-checkin-channel').value.trim(),
2447 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ required_channels: document.getElementById('policy-required-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
2448 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ };
2449 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ p.bridge = {
2450 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ web_user_ttl_minutes: parseInt(document.getElementById('policy-bridge-web-user-ttl').value, 10) || 5,
2451 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ };
2452 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ p.logging = {
2453 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ enabled: document.getElementById('policy-logging-enabled').checked,
2454 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dir: document.getElementById('policy-log-dir').value.trim(),
2455 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ format: document.getElementById('policy-log-format').value,
2456 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ rotation: document.getElementById('policy-log-rotation').value,
2457 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ max_size_mb: parseInt(document.getElementById('policy-log-max-size').value, 10) || 0,
2458 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ per_channel: document.getElementById('policy-log-per-channel').checked,
2459 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ max_age_days: parseInt(document.getElementById('policy-log-max-age').value, 10) || 0,
2460 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ };
2461 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ const resultEl = document.getElementById('policies-save-result');
2462 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try {
2463 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ currentPolicies = await api('PUT', '/v1/settings/policies', p);
2464 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.style.display = 'block';
2465 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ resultEl.innerHTML = renderAlert('success', 'Settings saved.');
2466 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ setTimeout(() => { resultEl.style.display = 'none'; }, 3000);
368 2467 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
} catch(e) {
369 2468 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
resultEl.style.display = 'block';
370 2469 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
resultEl.innerHTML = renderAlert('error', e.message);
371 2470 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
372 2471 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
373 2472 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
374 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function showCredentials(nick, creds, payload, mode) {
375 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const resultEl = document.getElementById('register-result');
376 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- resultEl.style.display = 'block';
377 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
378 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const passphrase = creds?.passphrase || creds?.Passphrase || '';
379 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const payloadSig = payload?.signature || '';
380 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const payloadEnc = payload ? JSON.stringify(payload.payload || payload, null, 2) : '';
381 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
382 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- resultEl.innerHTML = renderAlert('success',
383 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- `<strong>${mode === 'register' ? 'Agent registered' : 'Credentials rotated'}: ${esc(nick)}</strong>
384 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div style="margin-top:10px" class="cred-box">
385 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="cred-row">
386 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="cred-key">nick</span>
387 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="cred-val">${esc(creds?.nick || nick)}</span>
388 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <button class="small copy-btn" onclick="copyText('${esc(creds?.nick || nick)}', this)">copy</button>
389 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
390 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div class="cred-row">
391 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="cred-key">passphrase</span>
392 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="cred-val">${esc(passphrase)}</span>
393 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <button class="small copy-btn" onclick="copyText('${esc(passphrase)}', this)">copy</button>
394 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
395 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- ${payloadSig ? `<div class="cred-row">
396 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="cred-key">sig</span>
397 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <span class="cred-val" style="font-size:11px">${esc(payloadSig)}</span>
398 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>` : ''}
399 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- </div>
400 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- <div style="margin-top:8px;font-size:11px;color:#7ee787">⚠ Save the passphrase now — it will not be shown again.</div>`
401 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- );
402 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
403 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
404 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- // --- helpers ---
405 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function renderAlert(type, msg) {
406 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const icons = { info: 'ℹ', error: '✕', success: '✓' };
407 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- return `<div class="alert ${type}"><span class="icon">${icons[type]}</span><div>${msg}</div></div>`;
408 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
409 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function esc(s) {
410 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
411 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
412 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function fmtTime(iso) {
413 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (!iso) return '—';
414 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const d = new Date(iso);
415 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- return d.toLocaleDateString() + ' ' + d.toLocaleTimeString();
416 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
417 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
418 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- // --- chat ---
419 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- let chatChannel = null;
420 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- let chatSSE = null;
421 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
422 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- async function loadChannels() {
423 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (!getToken()) return;
424 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- try {
425 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const data = await api('GET', '/v1/channels');
426 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- renderChannelList(data.channels || []);
427 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('chat-card').style.display = '';
428 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } catch(e) {
429 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- // bridge disabled or error — keep chat card hidden
430 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
431 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
432 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
433 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function renderChannelList(channels) {
434 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const list = document.getElementById('chat-channel-list');
435 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- // Remove old channel items (keep header div and join input div)
436 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- Array.from(list.querySelectorAll('.chan-item')).forEach(el => el.remove());
437 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- channels.sort().forEach(ch => {
438 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const el = document.createElement('div');
439 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- el.className = 'chan-item' + (ch === chatChannel ? ' active' : '');
440 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- el.textContent = ch;
441 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- el.onclick = () => selectChannel(ch);
442 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- list.appendChild(el);
443 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- });
444 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
445 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
446 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- async function joinChannel() {
447 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- let ch = document.getElementById('join-channel-input').value.trim();
448 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (!ch) return;
449 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (!ch.startsWith('#')) ch = '#' + ch;
450 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const slug = ch.replace(/^#/, '');
451 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- try {
452 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- await api('POST', `/v1/channels/${slug}/join`);
453 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('join-channel-input').value = '';
454 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- await loadChannels();
455 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- selectChannel(ch);
456 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } catch(e) {
457 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- alert('Join failed: ' + e.message);
458 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
459 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
460 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
461 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('join-channel-input').addEventListener('keydown', e => {
462 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (e.key === 'Enter') joinChannel();
463 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- });
464 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
465 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- async function selectChannel(ch) {
466 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- chatChannel = ch;
467 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('chat-channel-badge').textContent = ch;
468 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('chat-channel-badge').style.display = '';
469 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('chat-placeholder').style.display = 'none';
470 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.querySelectorAll('.chan-item').forEach(el => {
471 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- el.classList.toggle('active', el.textContent === ch);
472 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- });
473 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
474 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const area = document.getElementById('chat-messages');
475 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- // clear previous messages (keep placeholder)
476 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- Array.from(area.children).forEach(el => { if (!el.id) el.remove(); });
477 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
478 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- try {
479 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const slug = ch.replace(/^#/, '');
480 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const data = await api('GET', `/v1/channels/${slug}/messages`);
481 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- (data.messages || []).forEach(appendMessage);
482 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- area.scrollTop = area.scrollHeight;
483 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } catch(e) {}
484 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
485 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (chatSSE) { chatSSE.close(); chatSSE = null; }
486 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const slug = ch.replace(/^#/, '');
487 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const url = `/v1/channels/${slug}/stream?token=${encodeURIComponent(getToken())}`;
488 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const es = new EventSource(url);
489 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- chatSSE = es;
490 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const status = document.getElementById('chat-stream-status');
491 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- es.onopen = () => { status.textContent = '● live'; status.style.color = '#3fb950'; };
492 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- es.onmessage = (e) => {
493 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- try {
494 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const msg = JSON.parse(e.data);
495 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- appendMessage(msg);
496 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- area.scrollTop = area.scrollHeight;
497 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } catch(_) {}
498 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- };
499 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- es.onerror = () => { status.textContent = '○ reconnecting…'; status.style.color = '#8b949e'; };
500 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
501 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
502 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function appendMessage(msg) {
503 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const area = document.getElementById('chat-messages');
504 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const t = new Date(msg.at);
505 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const timeStr = t.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
506 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const isBridge = msg.nick === 'bridge';
507 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const row = document.createElement('div');
508 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- row.className = 'msg-row';
509 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- row.innerHTML = `<span class="msg-time">${esc(timeStr)}</span>`
510 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- + `<span class="msg-nick${isBridge ? ' bridge-nick' : ''}">${esc(msg.nick)}</span>`
511 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- + `<span class="msg-text">${esc(msg.text)}</span>`;
512 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- area.appendChild(row);
513 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
514 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
515 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- async function sendChatMessage() {
516 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (!chatChannel) return;
517 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const input = document.getElementById('chat-text-input');
518 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const nick = document.getElementById('chat-nick-input').value.trim() || 'web';
519 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const text = input.value.trim();
520 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (!text) return;
521 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- input.disabled = true;
522 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('chat-send-btn').disabled = true;
523 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- try {
524 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- const slug = chatChannel.replace(/^#/, '');
525 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- await api('POST', `/v1/channels/${slug}/messages`, { text, nick });
526 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- input.value = '';
527 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } catch(e) {
528 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- alert('Send failed: ' + e.message);
529 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- } finally {
530 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- input.disabled = false;
531 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('chat-send-btn').disabled = false;
532 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- input.focus();
533 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
534 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- }
535 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
536 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- document.getElementById('chat-text-input').addEventListener('keydown', e => {
537 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- if (e.key === 'Enter') sendChatMessage();
538 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- });
539 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
-
540 2473 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
// --- init ---
541 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- function loadAll() { loadStatus(); loadAgents(); loadChannels(); }
542 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- updateTokenDisplay();
543 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- loadAll();
544 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- setInterval(loadStatus, 15000);
2474 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ function loadAll() { loadStatus(); loadAgents(); loadSettings(); startMetricsPoll(); }
2475 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ initAuth();
545 2476 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</script>
546 2477 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</body>
547 2478 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
</html>
548 2479 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
549 2480 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
A DDED internal/auth/admin.go
550 2481 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
A DDED internal/auth/admin_test.go