1
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!DOCTYPE html>
2
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<html lang="en">
3
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<head>
4
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<meta charset="UTF-8">
5
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<title>scuttlebot</title>
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; height:100vh; height:100dvh; 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 .card-desc { font-size:11px; color:#6e7681; font-weight:400; }
35
{ 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; }
36
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.card.collapsed .card-header { border-bottom:none; }
37
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.card.collapsed .card-body { display:none; }
38
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.card.collapsed .collapse-icon { transform:rotate(-90deg); }
39
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.card-body { padding:16px; }
40
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* behavior config panel */
41
{ 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; }
42
{ 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; }
43
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.beh-field label { display:block; font-size:11px; color:#8b949e; margin-bottom:3px; }
44
{ 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%; }
45
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.beh-field .hint { font-size:10px; color:#6e7681; margin-top:2px; }
46
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.spacer { flex:1; }
47
{ 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; white-space:nowrap; }
48
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
49
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* status */
50
{ 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; }
51
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.stat { background:#0d1117; border:1px solid #21262d; border-radius:6px; padding:12px 16px; }
52
{ 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; }
53
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.stat .val { font-size:20px; color:#58a6ff; font-weight:600; }
54
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.stat .sub { font-size:11px; color:#8b949e; margin-top:2px; }
55
{ 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; }
56
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.dot.green { background:#3fb950; box-shadow:0 0 6px #3fb950aa; }
57
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.dot.red { background:#f85149; }
58
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
59
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* table */
60
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
table { width:100%; border-collapse:collapse; font-size:13px; }
61
{ 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; }
62
{ 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; }
63
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
tr:last-child td { border-bottom:none; }
64
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
tr:hover td { background:#1c2128; }
65
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
66
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* tags */
67
{ 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; }
68
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.tag.ch { background:#1f6feb22; border-color:#1f6feb44; color:#79c0ff; }
69
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.tag.perm{ background:#21262d; border-color:#30363d; color:#8b949e; }
70
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.tag.type-operator { background:#db613622; border-color:#db613644; color:#ffa657; }
71
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.tag.type-orchestrator { background:#8957e522; border-color:#8957e544; color:#d2a8ff; }
72
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.tag.type-worker { background:#1f6feb22; border-color:#1f6feb44; color:#79c0ff; }
73
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.tag.type-observer { background:#21262d; border-color:#30363d; color:#8b949e; }
74
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.tag.revoked { background:#f8514922; border-color:#f8514944; color:#ff7b72; }
75
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
76
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* buttons */
77
{ 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; }
78
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
button:hover:not(:disabled) { background:#30363d; }
79
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
button:disabled { opacity:.5; cursor:default; }
80
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
button.primary { background:#1f6feb; border-color:#1f6feb; color:#fff; }
81
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
button.primary:hover:not(:disabled) { background:#388bfd; }
82
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
button.danger { border-color:#f85149; color:#f85149; }
83
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
button.danger:hover:not(:disabled) { background:#3d1f1e; }
84
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
button.sm { padding:3px 8px; font-size:12px; }
85
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.actions { display:flex; gap:6px; }
86
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
87
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* forms */
88
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
form { display:flex; flex-direction:column; gap:14px; }
89
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.form-row { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
90
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
label { display:block; font-size:12px; color:#8b949e; margin-bottom:4px; }
91
{ 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; }
92
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
input:focus,select:focus,textarea:focus { border-color:#58a6ff; }
93
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
select option { background:#161b22; }
94
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.hint { font-size:11px; color:#8b949e; margin-top:3px; }
95
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
96
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* alerts */
97
{ 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; }
98
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.alert.info { background:#1f6feb1a; border:1px solid #1f6feb44; color:#79c0ff; }
99
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.alert.error { background:#f851491a; border:1px solid #f8514944; color:#ff7b72; }
100
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.alert.success { background:#3fb9501a; border:1px solid #3fb95044; color:#7ee787; }
101
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.alert .icon { flex-shrink:0; font-size:15px; line-height:1.4; }
102
{ 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; }
103
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.cred-row { display:flex; align-items:baseline; gap:8px; margin-bottom:6px; }
104
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.cred-row:last-child { margin-bottom:0; }
105
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.cred-key { color:#8b949e; min-width:90px; }
106
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.cred-val { color:#a5d6ff; word-break:break-all; flex:1; }
107
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
108
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* search/filter bar */
109
{ 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; }
110
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.filter-bar input { max-width:280px; padding:5px 10px; }
111
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
112
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* empty */
113
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.empty { color:#8b949e; font-size:13px; text-align:center; padding:28px; }
114
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
115
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* drawer */
116
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.drawer-overlay { display:none; position:fixed; inset:0; background:#0d111760; z-index:50; }
117
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.drawer-overlay.open { display:block; }
118
{ 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; }
119
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.drawer.open { transform:translateX(0); }
120
{ 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; }
121
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.drawer-header h3 { font-size:14px; font-weight:600; flex:1; }
122
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.drawer-body { flex:1; overflow-y:auto; padding:20px; }
123
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
124
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* chat */
125
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
#pane-chat { flex-direction:row; overflow:hidden; }
126
{ 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; }
127
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-sidebar.collapsed { width:28px; }
128
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-sidebar.collapsed .chan-join,.chat-sidebar.collapsed .chan-list { display:none; }
129
{ 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; }
130
{ 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; }
131
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.sidebar-toggle:hover { color:#e6edf3; }
132
{ 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; }
133
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.sidebar-resize:hover,.sidebar-resize.dragging { background:#58a6ff55; }
134
{ 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; }
135
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chan-join input { flex:1; padding:4px 7px; font-size:12px; }
136
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chan-list { flex:1; overflow-y:auto; }
137
{ 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; }
138
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chan-item:hover { background:#1c2128; color:#e6edf3; }
139
{ 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; }
140
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-main { flex:1; display:flex; flex-direction:column; min-width:0; }
141
{ 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; }
142
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-ch-name { font-weight:600; color:#58a6ff; }
143
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.stream-badge { font-size:11px; color:#8b949e; margin-left:auto; }
144
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs { flex:1; overflow-y:auto; padding:4px 8px; display:flex; flex-direction:column; gap:0; }
145
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-row { font-size:13px; line-height:1.4; padding:1px 0; }
146
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-time { color:#8b949e; font-size:11px; margin-right:6px; }
147
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs.hide-timestamps .msg-time { visibility:hidden; width:0; min-width:0; margin:0; overflow:hidden; }
148
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs.columnar.hide-timestamps .msg-time { visibility:hidden; min-width:36px; margin:0; }
149
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-nick { font-weight:600; margin-right:6px; }
150
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-grouped .msg-nick { display:none; }
151
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-grouped .msg-time { display:none; }
152
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* columnar layout mode */
153
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs.columnar .msg-row { display:flex; gap:6px; }
154
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs.columnar .msg-time { flex-shrink:0; min-width:36px; padding-top:2px; margin-right:0; }
155
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs.columnar .msg-nick { flex-shrink:0; min-width:80px; text-align:right; margin-right:0; }
156
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs.columnar .msg-text { word-break:break-word; }
157
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs.columnar .msg-grouped .msg-nick { display:inline; visibility:hidden; }
158
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs.columnar .msg-grouped .msg-time { display:inline; color:transparent; }
159
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs.columnar .msg-grouped:hover .msg-time { color:#8b949e; }
160
{ 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; }
161
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-nicklist.collapsed { width:28px; overflow:hidden; }
162
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-nicklist.collapsed #nicklist-users { display:none; }
163
{ 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; }
164
{ 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; }
165
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.nicklist-nick.is-bot { color:#58a6ff; }
166
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.nicklist-nick.is-op { color:#3fb950; font-weight:600; }
167
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.nicklist-nick::before { content:""; }
168
{ 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; }
169
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* login screen */
170
{ 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; }
171
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.login-box { width:340px; }
172
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.login-brand { text-align:center; margin-bottom:24px; }
173
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.login-brand h1 { font-size:22px; color:#58a6ff; letter-spacing:.05em; }
174
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.login-brand p { color:#8b949e; font-size:13px; margin-top:6px; }
175
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* unread badge on chat tab */
176
{ 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; }
177
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-text { color:#e6edf3; word-break:break-word; }
178
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-row.hl-mention { background:#1f6feb18; border-left:2px solid #58a6ff; padding-left:6px; }
179
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-row.hl-danger { background:#f8514918; border-left:2px solid #f85149; padding-left:6px; }
180
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-row.hl-system { opacity:0.6; font-style:italic; }
181
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-text .hl-word { background:#f0883e33; border-radius:2px; padding:0 2px; }
182
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* meta blocks */
183
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta { display:none; margin:2px 0 4px 0; padding:8px 10px; background:#0d1117; border:1px solid #21262d; border-radius:6px; font-size:12px; line-height:1.5; cursor:default; }
184
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta.open { display:block; }
185
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta-toggle { display:inline-block; margin-left:6px; font-size:10px; color:#8b949e; cursor:pointer; padding:0 4px; border:1px solid #30363d; border-radius:3px; vertical-align:middle; }
186
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta-toggle:hover { color:#e6edf3; border-color:#58a6ff; }
187
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-type { font-size:10px; text-transform:uppercase; letter-spacing:.06em; color:#8b949e; margin-bottom:4px; }
188
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-tool { color:#d2a8ff; font-weight:600; }
189
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-file { color:#79c0ff; }
190
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-cmd { color:#a5d6ff; font-family:inherit; }
191
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-error { color:#ff7b72; }
192
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-status { display:inline-block; padding:1px 6px; border-radius:3px; font-size:11px; }
193
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-status.ok { background:#3fb95022; color:#3fb950; border:1px solid #3fb95044; }
194
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-status.error { background:#f8514922; color:#f85149; border:1px solid #f8514944; }
195
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-status.running { background:#1f6feb22; color:#58a6ff; border:1px solid #1f6feb44; }
196
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-kv { display:grid; grid-template-columns:auto 1fr; gap:2px 10px; }
197
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-kv dt { color:#8b949e; }
198
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta .meta-kv dd { color:#e6edf3; word-break:break-all; }
199
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta pre { margin:4px 0 0; padding:6px 8px; background:#161b22; border:1px solid #21262d; border-radius:4px; overflow-x:auto; white-space:pre-wrap; word-break:break-all; color:#e6edf3; font-size:12px; }
200
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-meta img { max-width:100%; max-height:300px; border-radius:4px; margin-top:4px; }
201
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* rich rendering v2 */
202
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-card { border-radius:6px; overflow:hidden; margin-top:4px; font-size:12px; }
203
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-card-header { display:flex; align-items:center; gap:8px; padding:6px 10px; font-size:11px; }
204
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-card-body { padding:0; }
205
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-card-body pre { margin:0; border:0; border-radius:0; padding:8px 10px; font-size:12px; line-height:1.5; }
206
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* terminal block */
207
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-terminal { border:1px solid #30363d; background:#0d1117; }
208
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-terminal .rich-card-header { background:#161b22; color:#8b949e; border-bottom:1px solid #21262d; }
209
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-terminal .rich-card-header .cmd { color:#a5d6ff; font-family:monospace; font-weight:600; }
210
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-terminal .exit-ok { color:#3fb950; } .rich-terminal .exit-fail { color:#f85149; }
211
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* file card */
212
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-file { border:1px solid #30363d; background:#0d1117; }
213
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-file .rich-card-header { background:#161b22; border-bottom:1px solid #21262d; }
214
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-file .rich-card-header .path { color:#79c0ff; font-family:monospace; font-weight:600; }
215
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-file .rich-card-header .lang { background:#1f6feb33; color:#58a6ff; padding:1px 6px; border-radius:3px; font-size:10px; }
216
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* diff rendering */
217
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-diff .line-add { background:#3fb95015; color:#3fb950; }
218
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-diff .line-del { background:#f8514915; color:#f85149; }
219
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-diff .line-ctx { color:#8b949e; }
220
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-diff .line-hdr { color:#d2a8ff; font-weight:600; }
221
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* error card */
222
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-error { border:1px solid #f8514944; background:#f8514910; }
223
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-error .rich-card-header { background:#f8514918; color:#f85149; border-bottom:1px solid #f8514944; }
224
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* thinking block */
225
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-thinking { border:1px solid #30363d; background:#0d1117; font-style:italic; color:#8b949e; padding:6px 10px; border-radius:6px; margin-top:4px; font-size:12px; }
226
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* search results */
227
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-search { border:1px solid #30363d; background:#0d1117; }
228
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-search .rich-card-header { background:#161b22; border-bottom:1px solid #21262d; }
229
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.rich-search .url { color:#58a6ff; word-break:break-all; }
230
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-input { padding:9px 13px; padding-bottom:calc(9px + env(safe-area-inset-bottom, 0px)); border-top:1px solid #30363d; display:flex; gap:7px; flex-shrink:0; background:#161b22; }
231
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
232
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* channels tab */
233
{ 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; }
234
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chan-card:last-child { border-bottom:none; }
235
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chan-card:hover { background:#1c2128; }
236
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chan-name { font-size:14px; font-weight:600; color:#58a6ff; }
237
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chan-meta { font-size:12px; color:#8b949e; }
238
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
239
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* settings */
240
{ 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; }
241
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.setting-row:last-child { border-bottom:none; }
242
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.setting-label { min-width:160px; font-size:13px; color:#c9d1d9; }
243
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.setting-desc { font-size:12px; color:#8b949e; flex:1; }
244
{ 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; }
245
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
246
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* modal */
247
{ 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; }
248
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.modal-overlay.open { display:flex; }
249
{ 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; }
250
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.modal h3 { font-size:15px; margin-bottom:16px; }
251
{ 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; }
252
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* charts */
253
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.charts-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:16px; }
254
{ 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; }
255
{ 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; }
256
{ 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; }
257
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
canvas { display:block; width:100% !important; }
258
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.bridge-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }
259
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
260
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* mobile */
261
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
@media (max-width: 600px) {
262
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* header: shorter, compact brand, scrollable nav */
263
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
header { padding:0 8px; height:44px; }
264
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.brand { padding-right:8px; margin-right:0; border-right:none; }
265
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.brand span { display:none; }
266
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
nav { overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none; position:relative; }
267
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
nav::-webkit-scrollbar { display:none; }
268
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.nav-tab { padding:0 10px; font-size:12px; }
269
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.header-right { display:none; }
270
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
271
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
272
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* content: reduce padding, stack grids */
273
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.pane-inner { padding:12px; gap:12px; }
274
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.stat-grid { grid-template-columns:1fr !important; }
275
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.card-body { padding:12px !important; }
276
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
277
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* charts and bridge grids: single column */
278
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.charts-grid { grid-template-columns:1fr !important; }
279
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.bridge-grid { grid-template-columns:1fr !important; }
280
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
281
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* forms: stack 2-column rows */
282
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.form-row { grid-template-columns:1fr !important; }
283
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
284
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* inline 2-column grids inside drawers/forms */
285
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
[style*="grid-template-columns:1fr 1fr"] { grid-template-columns:1fr !important; }
286
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
287
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* chat: hide both panels by default, show as overlay when toggled open */
288
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-sidebar { width:0 !important; min-width:0 !important; border-right:none !important; overflow:hidden !important; }
289
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-sidebar.mobile-open { position:fixed; top:44px; left:0; bottom:0; width:220px !important; z-index:50; border-right:1px solid #30363d !important; overflow:visible !important; }
290
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-nicklist { width:0 !important; min-width:0 !important; border-left:none !important; overflow:hidden !important; }
291
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-nicklist.mobile-open { position:fixed; top:44px; right:0; bottom:0; width:160px !important; z-index:50; border-left:1px solid #30363d !important; overflow-y:auto !important; }
292
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.sidebar-resize { display:none; }
293
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
294
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* settings: stack label above input */
295
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.setting-row { flex-direction:column !important; align-items:flex-start !important; gap:6px !important; }
296
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.setting-label { min-width:unset !important; }
297
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
298
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* drawer: full-width on mobile */
299
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.drawer { width:100vw; max-width:100vw; top:44px; }
300
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.drawer-header { padding:12px 14px; }
301
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.drawer-body { padding:14px; }
302
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
303
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* modal */
304
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.modal { width:95vw; padding:16px; }
305
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
306
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* filter bar */
307
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.filter-bar { flex-wrap:wrap; }
308
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.filter-bar input { max-width:100% !important; flex:1; min-width:0; }
309
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
310
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* login */
311
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.login-box { width:90vw; max-width:340px; }
312
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
313
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
/* chat: tighter on mobile */
314
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-msgs { padding:2px 4px; }
315
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-row { font-size:12px; line-height:1.3; }
316
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.msg-time { font-size:10px; }
317
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-input { padding:6px 8px; }
318
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.chat-topbar { padding:6px 10px; gap:6px; font-size:12px; }
319
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
320
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</style>
321
{ 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>
322
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</head>
323
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<body>
324
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
325
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- login screen — shown when unauthenticated -->
326
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="login-screen" id="login-screen" style="display:none">
327
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="login-box">
328
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="login-brand">
329
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h1>⬡ scuttlebot</h1>
330
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<p>agent coordination backplane</p>
331
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
332
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card">
333
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
334
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<form id="login-form" onsubmit="handleLogin(event)" style="gap:12px">
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!
<label>username</label>
337
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="login-username" autocomplete="username">
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!
<div>
340
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>password</label>
341
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="password" id="login-password" autocomplete="current-password">
342
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
343
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="login-error" style="display:none"></div>
344
{ 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>
345
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</form>
346
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<details style="margin-top:16px;border-top:1px solid #21262d;padding-top:14px">
347
{ 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>
348
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:8px;margin-top:10px">
349
{ 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">
350
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="saveTokenLogin()">apply</button>
351
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
352
{ 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>
353
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</details>
354
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
355
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
356
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<p style="text-align:center;font-size:11px;color:#6e7681;margin-top:14px">
357
{ 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>
358
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</p>
359
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
360
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
361
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
362
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<header>
363
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="brand">
364
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h1>⬡ scuttlebot</h1>
365
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span>agent coordination backplane</span>
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!
<nav>
368
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="nav-tab active" id="tab-status" onclick="switchTab('status')">◈ status</div>
369
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="nav-tab" id="tab-users" onclick="switchTab('users')">◉ users</div>
370
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="nav-tab" id="tab-agents" onclick="switchTab('agents')">◎ agents</div>
371
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="nav-tab" id="tab-channels" onclick="switchTab('channels')">◎ channels</div>
372
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="nav-tab" id="tab-chat" onclick="switchTab('chat')">◌ chat</div>
373
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="nav-tab" id="tab-ai" onclick="switchTab('ai')">✦ ai</div>
374
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="nav-tab" id="tab-settings" onclick="switchTab('settings')">⚙ settings</div>
375
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</nav>
376
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="header-right">
377
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span id="header-user-display" style="font-size:12px;color:#8b949e"></span>
378
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="logout()">sign out</button>
379
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
380
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</header>
381
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
382
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- STATUS -->
383
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="tab-pane active" id="pane-status">
384
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="pane-inner">
385
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="no-token-banner" class="alert info" style="display:none">
386
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span class="icon">ℹ</span>
387
{ 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>
388
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
389
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
390
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- server status card -->
391
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-status">
392
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" onclick="toggleCard('card-status',event)">
393
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span class="dot green" id="status-dot"></span><h2>server status</h2>
394
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span class="collapse-icon">▾</span>
395
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
396
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:11px;color:#8b949e" id="metrics-updated"></span>
397
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="loadStatus()" title="refresh">↻</button>
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 class="card-body">
400
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="stat-grid">
401
{ 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>
402
{ 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>
403
{ 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>
404
{ 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>
405
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
406
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="status-error" style="margin-top:12px;display:none"></div>
407
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
408
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
409
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
410
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- runtime -->
411
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-runtime">
412
{ 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>
413
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body" style="display:flex;flex-direction:column;gap:16px">
414
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="stat-grid">
415
{ 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>
416
{ 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>
417
{ 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>
418
{ 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>
419
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
420
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="charts-grid">
421
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chart-card">
422
{ 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>
423
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<canvas id="chart-heap" height="80"></canvas>
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 class="chart-card">
426
{ 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>
427
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<canvas id="chart-goroutines" height="80"></canvas>
428
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
429
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chart-card">
430
{ 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>
431
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<canvas id="chart-messages" height="80"></canvas>
432
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
433
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
434
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
435
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
436
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
437
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- bridge -->
438
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="bridge-card" style="display:none">
439
{ 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>
440
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
441
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="bridge-grid">
442
{ 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>
443
{ 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>
444
{ 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>
445
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
446
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
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!
449
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- registry -->
450
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-registry">
451
{ 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>
452
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
453
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="stat-grid">
454
{ 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>
455
{ 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>
456
{ 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>
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!
</div>
462
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
463
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
464
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- USERS -->
465
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="tab-pane" id="pane-users">
466
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="filter-bar">
467
{ 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">
468
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
469
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span class="badge" id="user-count" style="margin-right:4px">0</span>
470
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="loadAgents()">↻ refresh</button>
471
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="openAdoptDrawer()">adopt existing user</button>
472
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="openRegisterUserDrawer()">+ register user</button>
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 style="flex:1;overflow-y:auto">
475
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="users-container"></div>
476
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
477
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
478
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
479
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- AGENTS -->
480
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="tab-pane" id="pane-agents">
481
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="filter-bar">
482
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="agent-search" placeholder="search by nick, type, channel…" oninput="agentPage=0;renderAgentTable()" style="max-width:280px">
483
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<select id="agent-status-filter" onchange="agentPage=0;renderAgentTable()" style="padding:5px 8px;font-size:12px;width:90px">
484
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="all">all</option>
485
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="online">online</option>
486
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="offline">offline</option>
487
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="revoked">revoked</option>
488
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</select>
489
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
490
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span class="badge" id="agent-count" style="margin-right:4px">0</span>
491
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="loadAgents()">↻ refresh</button>
492
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm danger" id="bulk-delete-btn" style="display:none" onclick="bulkDeleteAgents()">delete selected</button>
493
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="openDrawer()">+ register agent</button>
494
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
495
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="agent-pagination" style="display:none;padding:4px 16px;font-size:12px;color:#8b949e;display:flex;align-items:center;gap:8px">
496
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" id="agent-prev" onclick="agentPage--;renderAgentTable()">← prev</button>
497
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span id="agent-page-info"></span>
498
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" id="agent-next" onclick="agentPage++;renderAgentTable()">next →</button>
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!
<div style="flex:1;overflow-y:auto">
501
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="agents-container"></div>
502
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
503
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
504
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
505
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- CHANNELS -->
506
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="tab-pane" id="pane-channels">
507
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="pane-inner">
508
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card">
509
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header">
510
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2>channels</h2>
511
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span class="badge" id="chan-count">0</span>
512
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
513
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="chan-search" placeholder="filter…" oninput="renderChanList()" style="width:120px;padding:5px 8px;font-size:12px">
514
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:6px;align-items:center">
515
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="quick-join-input" placeholder="#channel" style="width:140px;padding:5px 8px;font-size:12px" autocomplete="off">
516
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="quickJoin()">join</button>
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>
519
{ 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>
520
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
521
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
522
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- topology panel -->
523
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-topology">
524
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" onclick="toggleCard('card-topology',event)">
525
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2>topology</h2><span class="card-desc">channel types, provisioning rules, active task channels</span><span class="collapse-icon">▾</span>
526
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
527
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:6px;align-items:center">
528
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="provision-channel-input" placeholder="#project.name" style="width:160px;padding:5px 8px;font-size:12px" autocomplete="off">
529
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="provisionChannel()">provision</button>
530
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
531
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
532
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body" style="padding:0">
533
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="topology-types"></div>
534
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="topology-active" style="padding:12px 16px"><div class="empty">loading topology…</div></div>
535
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
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!
538
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- ROE templates -->
539
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-roe">
540
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" onclick="toggleCard('card-roe',event)">
541
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2>ROE templates</h2><span class="card-desc">rules-of-engagement presets for agent registration</span><span class="collapse-icon">▾</span>
542
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
543
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="event.stopPropagation();savePolicies()">save</button>
544
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
545
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
546
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<p style="font-size:12px;color:#8b949e;margin-bottom:12px">Define ROE templates applied to agents at registration. Includes channels, permissions, and rate limits.</p>
547
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="roe-list"></div>
548
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="addROETemplate()" style="margin-top:10px">+ add template</button>
549
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
550
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
551
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
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!
554
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- CHAT -->
555
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="tab-pane" id="pane-chat">
556
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chat-sidebar" id="chat-sidebar-left">
557
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="sidebar-head">
558
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span id="sidebar-left-label">channels</span>
559
{ 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>
560
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
561
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chan-join">
562
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="join-channel-input" placeholder="#general" autocomplete="off">
563
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="joinChannel()">+</button>
564
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
565
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chan-list" id="chan-list"></div>
566
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
567
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="sidebar-resize" id="resize-left" title="drag to resize"></div>
568
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chat-main">
569
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chat-topbar">
570
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span class="chat-ch-name" id="chat-ch-name">select a channel</span><span id="chat-channel-modes" style="color:#8b949e;font-size:11px;margin-left:6px"></span>
571
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
572
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:11px;color:#8b949e;margin-right:6px">chatting as</span>
573
{ 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()">
574
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="">— pick a user —</option>
575
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</select>
576
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" id="chat-layout-toggle" onclick="toggleChatLayout()" title="toggle compact/columnar" style="font-size:11px;padding:2px 6px">☰</button>
577
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" id="chat-ts-toggle" onclick="toggleTimestamps()" title="toggle timestamps" style="font-size:11px;padding:2px 6px">🕐</button>
578
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" id="chat-rich-toggle" onclick="toggleRichMode()" title="toggle rich/text mode" style="font-size:11px;padding:2px 6px">✨</button>
579
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="promptHighlightWords()" title="configure highlight keywords" style="font-size:11px;padding:2px 6px">✦</button>
580
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span class="stream-badge" id="chat-stream-status" style="margin-left:8px"></span>
581
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
582
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chat-msgs" id="chat-msgs">
583
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="empty" id="chat-placeholder">join a channel to start chatting</div>
584
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
585
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chat-input">
586
{ 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">
587
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="primary sm" id="chat-send-btn" onclick="sendMsg()">send</button>
588
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
589
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
590
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="sidebar-resize" id="resize-right" title="drag to resize"></div>
591
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chat-nicklist" id="chat-nicklist">
592
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="nicklist-head">
593
{ 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>
594
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span id="sidebar-right-label">users</span>
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 id="nicklist-users"></div>
597
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
598
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
599
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
600
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- SETTINGS -->
601
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="tab-pane" id="pane-settings">
602
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="pane-inner">
603
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
604
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- connection -->
605
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-connection">
606
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" style="cursor:default"><h2>connection</h2><span class="card-desc">current session and server endpoints</span></div>
607
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
608
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
609
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">signed in as</div>
610
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Current admin session.</div>
611
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<code class="setting-val" id="settings-username-display">—</code>
612
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm danger" onclick="logout()">sign out</button>
613
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
614
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
615
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">API endpoint</div>
616
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">REST API base URL.</div>
617
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<code class="setting-val" id="settings-api-url"></code>
618
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
619
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
620
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">IRC network</div>
621
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Ergo IRC server address.</div>
622
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<code class="setting-val">localhost:6667</code>
623
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
624
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
625
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">MCP server</div>
626
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Model Context Protocol endpoint.</div>
627
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<code class="setting-val">localhost:8081</code>
628
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
629
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
630
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
631
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
632
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- admin accounts -->
633
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-admins">
634
{ 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="card-desc">who can sign in to this UI</span><span class="collapse-icon">▾</span></div>
635
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="admins-list-container"></div>
636
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body" style="border-top:1px solid #21262d">
637
{ 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>
638
{ 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">
639
{ 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>
640
{ 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>
641
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button type="submit" class="primary sm" style="margin-bottom:1px">add admin</button>
642
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</form>
643
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="add-admin-result" style="margin-top:10px"></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!
</div>
646
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
647
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- api keys -->
648
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-apikeys">
649
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" onclick="toggleCard('card-apikeys',event)"><h2>API keys</h2><span class="card-desc">per-consumer tokens with scoped permissions</span><span class="collapse-icon">▾</span></div>
650
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="apikeys-list-container"></div>
651
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body" style="border-top:1px solid #21262d">
652
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<p style="font-size:12px;color:#8b949e;margin-bottom:12px">Create an API key with a name and scopes. The token is shown only once.</p>
653
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<form id="add-apikey-form" onsubmit="createAPIKey(event)" style="display:flex;flex-direction:column;gap:10px">
654
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
655
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="flex:1;min-width:160px"><label>name</label><input type="text" id="new-apikey-name" placeholder="e.g. kohakku-controller" autocomplete="off"></div>
656
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="flex:1;min-width:160px"><label>expires in</label><input type="text" id="new-apikey-expires" placeholder="e.g. 720h (empty=never)" autocomplete="off"></div>
657
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
658
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
659
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label style="margin-bottom:6px;display:block">scopes</label>
660
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:12px;flex-wrap:wrap;font-size:12px">
661
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label><input type="checkbox" value="admin" class="apikey-scope"> admin</label>
662
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label><input type="checkbox" value="agents" class="apikey-scope"> agents</label>
663
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label><input type="checkbox" value="channels" class="apikey-scope"> channels</label>
664
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label><input type="checkbox" value="chat" class="apikey-scope"> chat</label>
665
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label><input type="checkbox" value="topology" class="apikey-scope"> topology</label>
666
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label><input type="checkbox" value="bots" class="apikey-scope"> bots</label>
667
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label><input type="checkbox" value="config" class="apikey-scope"> config</label>
668
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label><input type="checkbox" value="read" class="apikey-scope"> read</label>
669
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
670
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
671
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button type="submit" class="primary sm" style="align-self:flex-start">create key</button>
672
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</form>
673
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="add-apikey-result" style="margin-top:10px"></div>
674
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
675
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
676
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
677
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- tls -->
678
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-tls">
679
{ 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="card-desc">certificate status</span><span class="collapse-icon">▾</span><div class="spacer"></div><span id="tls-badge" class="badge">loading…</span></div>
680
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
681
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="tls-status-rows"></div>
682
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="alert info" style="margin-top:12px;font-size:12px">
683
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span class="icon">ℹ</span>
684
{ 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>.
685
{ 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>
686
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
687
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
688
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
689
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
690
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- system behaviors -->
691
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-behaviors">
692
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" onclick="toggleCard('card-behaviors',event)">
693
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2>system behaviors</h2><span class="card-desc">bot toggles, rate limits, and default channel</span><span class="collapse-icon">▾</span>
694
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
695
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="savePolicies()">save</button>
696
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
697
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body" style="padding:0">
698
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="behaviors-list"></div>
699
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
700
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
701
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
702
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- on-join instructions -->
703
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-onjoin">
704
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" onclick="toggleCard('card-onjoin',event)">
705
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2>on-join instructions</h2><span class="card-desc">messages sent to agents when they join a channel</span><span class="collapse-icon">▾</span>
706
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
707
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="event.stopPropagation();savePolicies()">save</button>
708
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
709
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
710
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<p style="font-size:12px;color:#8b949e;margin-bottom:12px">Per-channel instructions delivered to agents on join. Supports <code>{nick}</code> and <code>{channel}</code> template variables.</p>
711
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="onjoin-list"></div>
712
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:8px;margin-top:12px;align-items:flex-end">
713
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="flex:0 0 160px"><label>channel</label><input type="text" id="onjoin-new-channel" placeholder="#channel" style="width:100%"></div>
714
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="flex:1"><label>message</label><input type="text" id="onjoin-new-message" placeholder="Welcome to {channel}, {nick}!" style="width:100%"></div>
715
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="addOnJoinMessage()">add</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>
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!
<!-- agent policy -->
721
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-agentpolicy">
722
{ 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="card-desc">autojoin and check-in rules for all agents</span><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="event.stopPropagation();saveAgentPolicy()">save</button></div>
723
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
724
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
725
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">require check-in</div>
726
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Agents must join a coordination channel before others.</div>
727
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
728
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="checkbox" id="policy-checkin-enabled" onchange="toggleCheckinChannel()">
729
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px">enabled</span>
730
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</label>
731
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
732
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row" id="policy-checkin-row" style="display:none">
733
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">check-in channel</div>
734
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Channel all agents must join first.</div>
735
{ 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">
736
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
737
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
738
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">required channels</div>
739
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Channels every agent is added to automatically.</div>
740
{ 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">
741
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
742
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
743
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">online timeout</div>
744
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Seconds since last heartbeat before an agent is considered offline. Default: 120.</div>
745
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="number" id="policy-online-timeout" placeholder="120" min="10" max="3600" style="width:100px;padding:4px 8px;font-size:12px">
746
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
747
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
748
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">reap after days</div>
749
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Remove stale agents not seen in this many days. 0 = never reap.</div>
750
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="number" id="policy-reap-days" placeholder="0" min="0" max="365" style="width:100px;padding:4px 8px;font-size:12px">
751
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
752
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
753
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="agentpolicy-save-result" style="display:none;margin:0 16px 12px"></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!
756
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- bridge -->
757
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-bridgepolicy">
758
{ 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="card-desc">IRC bot that powers the web chat UI</span><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="event.stopPropagation();saveBridgeConfig()">save</button></div>
759
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
760
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
761
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">enabled</div>
762
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Start the bridge bot that powers the web chat UI.</div>
763
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
764
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="checkbox" id="bridge-enabled">
765
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px">enabled</span>
766
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</label>
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 class="setting-row">
769
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">nick</div>
770
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">IRC nick for the bridge bot. Requires restart.</div>
771
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="bridge-nick" placeholder="bridge" style="width:160px;padding:4px 8px;font-size:12px">
772
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
773
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
774
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">channels</div>
775
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Channels the bridge joins at startup.</div>
776
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="bridge-channels" placeholder="#general, #fleet" style="width:280px;padding:4px 8px;font-size:12px">
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!
<div class="setting-row">
779
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">message buffer</div>
780
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Messages to keep per channel in memory.</div>
781
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;align-items:center;gap:6px">
782
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="number" id="bridge-buffer-size" placeholder="200" min="1" style="width:80px;padding:4px 8px;font-size:12px">
783
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px;color:#8b949e">messages</span>
784
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
785
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
786
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
787
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">web user TTL</div>
788
{ 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>
789
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;align-items:center;gap:6px">
790
{ 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">
791
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px;color:#8b949e">minutes</span>
792
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
793
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
794
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
795
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="bridge-save-result" style="display:none;margin:0 16px 12px"></div>
796
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
797
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
798
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- logging -->
799
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-logging">
800
{ 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="card-desc">write channel traffic to disk</span><span class="collapse-icon">▾</span><div class="spacer"></div><button class="sm primary" onclick="event.stopPropagation();saveLogging()">save</button></div>
801
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
802
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
803
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">enabled</div>
804
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Write every channel message to disk.</div>
805
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
806
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="checkbox" id="policy-logging-enabled" onchange="toggleLogOptions()">
807
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px">enabled</span>
808
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</label>
809
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
810
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="policy-log-options" style="display:none">
811
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
812
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">log directory</div>
813
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Directory to write log files into.</div>
814
{ 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">
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 class="setting-row">
817
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">format</div>
818
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Output format for log lines.</div>
819
{ 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">
820
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="jsonl">JSON Lines (.jsonl)</option>
821
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="csv">CSV (.csv)</option>
822
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="text">Plain text (.log)</option>
823
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</select>
824
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
825
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
826
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">rotation</div>
827
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">When to start a new log file.</div>
828
{ 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()">
829
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="none">None</option>
830
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="daily">Daily</option>
831
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="weekly">Weekly</option>
832
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="monthly">Monthly</option>
833
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="yearly">Yearly</option>
834
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="size">By size</option>
835
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</select>
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!
<div class="setting-row" id="policy-log-size-row" style="display:none">
838
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">max file size</div>
839
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Rotate when file reaches this size.</div>
840
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;align-items:center;gap:6px">
841
{ 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">
842
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px;color:#8b949e">MiB</span>
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 class="setting-row">
846
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">per-channel files</div>
847
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Write a separate file for each channel.</div>
848
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
849
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="checkbox" id="policy-log-per-channel">
850
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px">enabled</span>
851
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</label>
852
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
853
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
854
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">max age</div>
855
{ 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>
856
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;align-items:center;gap:6px">
857
{ 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">
858
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px;color:#8b949e">days</span>
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!
</div>
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!
<div id="logging-save-result" style="display:none;margin:0 16px 12px"></div>
864
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
865
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
866
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="policies-save-result" style="display:none"></div>
867
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
868
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- general -->
869
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-general">
870
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" onclick="toggleCard('card-general',event)">
871
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2>general</h2><span class="card-desc">API and MCP server addresses</span><span class="collapse-icon">▾</span>
872
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
873
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="event.stopPropagation();saveGeneralConfig()">save</button>
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!
<div class="card-body">
876
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
877
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">API address</div>
878
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Address scuttlebot listens on for HTTP API requests. Requires restart.</div>
879
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="general-api-addr" placeholder=":8080" style="width:160px;padding:4px 8px;font-size:12px">
880
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
881
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
882
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">MCP address</div>
883
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Address for the Model Context Protocol server. Requires restart.</div>
884
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="general-mcp-addr" placeholder=":8081" style="width:160px;padding:4px 8px;font-size:12px">
885
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
886
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
887
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="general-save-result" style="display:none;margin:0 16px 12px"></div>
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!
890
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- ergo -->
891
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-ergo">
892
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" onclick="toggleCard('card-ergo',event)">
893
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2>IRC server (ergo)</h2><span class="card-desc">embedded IRC server settings</span><span class="collapse-icon">▾</span>
894
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
895
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="event.stopPropagation();saveErgoConfig()">save</button>
896
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
897
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
898
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
899
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">network name</div>
900
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Human-readable IRC network name. Requires restart.</div>
901
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="ergo-network-name" placeholder="scuttlebot" style="width:220px;padding:4px 8px;font-size:12px">
902
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
903
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
904
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">server name</div>
905
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">IRC server hostname (e.g. irc.example.com). Requires restart.</div>
906
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="ergo-server-name" placeholder="irc.scuttlebot.local" style="width:220px;padding:4px 8px;font-size:12px">
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 class="setting-row">
909
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">IRC address</div>
910
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Address Ergo listens on for IRC connections. Requires restart.</div>
911
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="ergo-irc-addr" placeholder="127.0.0.1:6667" style="width:180px;padding:4px 8px;font-size:12px">
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 class="setting-row">
914
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">require SASL</div>
915
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Enforce SASL authentication for all IRC connections. Only registered accounts can connect. Hot-reloads.</div>
916
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
917
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="checkbox" id="ergo-require-sasl">
918
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px">enforce SASL</span>
919
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</label>
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!
<div class="setting-row">
922
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">default channel modes</div>
923
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Modes applied to new channels (e.g. "+n", "+Rn"). Hot-reloads.</div>
924
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="ergo-default-modes" placeholder="+n" style="width:120px;padding:4px 8px;font-size:12px">
925
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
926
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
927
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">message history</div>
928
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Enable persistent message history (CHATHISTORY). Hot-reloads.</div>
929
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
930
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="checkbox" id="ergo-history-enabled">
931
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px">enabled</span>
932
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</label>
933
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
934
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
935
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">external mode</div>
936
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Disable subprocess management — scuttlebot expects Ergo to already be running. Requires restart.</div>
937
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
938
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="checkbox" id="ergo-external">
939
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px">external</span>
940
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</label>
941
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
942
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
943
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="ergo-save-result" style="display:none;margin:0 16px 12px"></div>
944
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
945
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
946
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- TLS -->
947
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-tls-config">
948
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" onclick="toggleCard('card-tls-config',event)">
949
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2>TLS / HTTPS</h2><span class="card-desc">HTTPS and Let's Encrypt configuration</span><span class="collapse-icon">▾</span>
950
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
951
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="event.stopPropagation();saveTLSConfig()">save</button>
952
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
953
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
954
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
955
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">domain</div>
956
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Domain for Let's Encrypt certificate. Leave blank for HTTP only. Requires restart.</div>
957
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="tls-domain" placeholder="scuttlebot.example.com" style="width:240px;padding:4px 8px;font-size:12px">
958
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
959
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
960
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">email</div>
961
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Sent to Let's Encrypt for expiry notifications.</div>
962
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="email" id="tls-email" placeholder="[email protected] " style="width:240px;padding:4px 8px;font-size:12px">
963
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
964
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
965
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">allow insecure</div>
966
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Keep HTTP running on :80 alongside HTTPS.</div>
967
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
968
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="checkbox" id="tls-allow-insecure">
969
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px">enabled</span>
970
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</label>
971
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
972
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
973
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="tls-config-save-result" style="display:none;margin:0 16px 12px"></div>
974
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
975
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
976
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- topology -->
977
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-topology">
978
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" onclick="toggleCard('card-topology',event)">
979
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2>topology</h2><span class="card-desc">static channels and prefix-based channel rules</span><span class="collapse-icon">▾</span>
980
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
981
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="event.stopPropagation();loadConfigCards()">↺ refresh</button>
982
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="event.stopPropagation();saveTopologyConfig()">save</button>
983
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
984
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
985
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
986
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">manager nick</div>
987
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">IRC nick used by the topology manager to register channels via ChanServ.</div>
988
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="topo-nick" placeholder="topology" style="width:160px;padding:4px 8px;font-size:12px">
989
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
990
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
991
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">config history</div>
992
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Number of scuttlebot.yaml snapshots to keep before pruning.</div>
993
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;align-items:center;gap:6px">
994
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="number" id="topo-history-keep" placeholder="20" min="0" style="width:80px;padding:4px 8px;font-size:12px">
995
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px;color:#8b949e">snapshots</span>
996
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
997
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
998
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
999
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- static channels -->
1000
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="margin-top:20px;margin-bottom:8px;display:flex;align-items:center;gap:10px">
1001
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<strong style="font-size:13px">static channels</strong>
1002
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:11px;color:#8b949e;flex:1">Provisioned at startup. ChanServ registers these channels and invites the listed bots.</span>
1003
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="topoAddStaticChannel()">+ add</button>
1004
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1005
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="topo-static-channels">
1006
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="empty-state" style="padding:12px;font-size:12px">no static channels configured</div>
1007
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1008
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1009
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- channel types -->
1010
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="margin-top:20px;margin-bottom:8px;display:flex;align-items:center;gap:10px">
1011
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<strong style="font-size:13px">channel types</strong>
1012
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:11px;color:#8b949e;flex:1">Prefix-based rules applied when agents create channels.</span>
1013
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="topoAddChannelType()">+ add</button>
1014
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1015
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="topo-channel-types">
1016
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="empty-state" style="padding:12px;font-size:12px">no channel types configured</div>
1017
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1018
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1019
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="topo-save-result" style="display:none;margin-top:12px"></div>
1020
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1021
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1022
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1023
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- about -->
1024
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card">
1025
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" style="cursor:default"><h2>about</h2></div>
1026
{ 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">
1027
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<p><strong style="color:#e6edf3">ScuttleBot</strong> — agent coordination backplane over IRC.</p>
1028
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<p>Agents register, receive SASL credentials, and coordinate in IRC channels.</p>
1029
{ 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>
1030
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<p style="margin-top:12px;font-size:11px;color:#6e7681">
1031
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Copyright © 2026 CONFLICT LLC. All rights reserved.<br>
1032
{ 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>
1033
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</p>
1034
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1035
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1036
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1037
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1038
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1039
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1040
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- AI -->
1041
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="tab-pane" id="pane-ai">
1042
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="pane-inner">
1043
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1044
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- LLM backends -->
1045
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-ai-backends">
1046
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" style="cursor:default">
1047
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2>LLM backends</h2><span class="card-desc">configured providers for oracle and other LLM bots</span>
1048
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
1049
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="loadAI()">↺ refresh</button>
1050
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" onclick="openAddBackend()">+ add backend</button>
1051
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1052
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body" style="padding:0">
1053
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="ai-backends-list" style="padding:16px">
1054
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="empty-state">loading…</div>
1055
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1056
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1057
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1058
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1059
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- add/edit backend form (hidden until opened) -->
1060
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-ai-form" style="display:none">
1061
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-header" style="cursor:default">
1062
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h2 id="ai-form-title">add backend</h2>
1063
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
1064
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="closeBackendForm()">✕ cancel</button>
1065
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1066
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
1067
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px 16px">
1068
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1069
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>name *</label>
1070
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="bf-name" placeholder="openai-main" autocomplete="off">
1071
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="hint">unique identifier — used in oracle's backend field</div>
1072
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1073
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1074
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>backend type *</label>
1075
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<select id="bf-backend" onchange="onBackendTypeChange()">
1076
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="">— select type —</option>
1077
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<optgroup label="Native APIs">
1078
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="anthropic">anthropic</option>
1079
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="gemini">gemini</option>
1080
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="bedrock">bedrock</option>
1081
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="ollama">ollama</option>
1082
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</optgroup>
1083
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<optgroup label="OpenAI-compatible">
1084
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="openai">openai</option>
1085
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="openrouter">openrouter</option>
1086
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="together">together</option>
1087
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="groq">groq</option>
1088
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="fireworks">fireworks</option>
1089
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="mistral">mistral</option>
1090
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="ai21">ai21</option>
1091
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="huggingface">huggingface</option>
1092
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="deepseek">deepseek</option>
1093
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="cerebras">cerebras</option>
1094
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="xai">xai</option>
1095
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="litellm">litellm (local)</option>
1096
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="lmstudio">lm studio (local)</option>
1097
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="jan">jan (local)</option>
1098
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="localai">localai (local)</option>
1099
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="vllm">vllm (local)</option>
1100
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="anythingllm">anythingllm (local)</option>
1101
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</optgroup>
1102
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</select>
1103
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1104
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1105
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- shown for non-bedrock backends -->
1106
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="bf-apikey-row">
1107
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>API key</label>
1108
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="password" id="bf-apikey" placeholder="sk-…" autocomplete="new-password">
1109
{ 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>
1110
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1111
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1112
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- shown for ollama and OpenAI-compat local backends -->
1113
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="bf-baseurl-row">
1114
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>base URL</label>
1115
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="bf-baseurl" placeholder="http://localhost:11434" autocomplete="off">
1116
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="hint">Override default endpoint for self-hosted backends</div>
1117
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1118
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1119
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1120
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>model</label>
1121
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:6px;align-items:flex-start">
1122
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="flex:1">
1123
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<select id="bf-model-select" onchange="onModelSelectChange()" style="width:100%">
1124
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="">— select or load models —</option>
1125
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</select>
1126
{ 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">
1127
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1128
{ 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>
1129
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1130
{ 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>
1131
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1132
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1133
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;align-items:flex-end;gap:8px">
1134
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label style="margin:0;cursor:pointer;display:flex;align-items:center;gap:6px">
1135
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="checkbox" id="bf-default"> mark as default backend
1136
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</label>
1137
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1138
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1139
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- Bedrock-specific fields -->
1140
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="bf-bedrock-group" style="display:none;grid-column:1/-1">
1141
{ 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>
1142
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px 16px">
1143
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1144
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>region *</label>
1145
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="bf-region" placeholder="us-east-1" autocomplete="off">
1146
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1147
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1148
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>AWS key ID</label>
1149
{ 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">
1150
{ 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>
1151
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1152
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1153
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>AWS secret key</label>
1154
{ 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">
1155
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1156
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1157
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1158
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1159
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- allow/block filters -->
1160
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="grid-column:1/-1">
1161
{ 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>
1162
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px 16px">
1163
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1164
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>allow list</label>
1165
{ 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>
1166
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="hint">Only these models shown. Empty = all.</div>
1167
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1168
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1169
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>block list</label>
1170
{ 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>
1171
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="hint">Always hidden.</div>
1172
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1173
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1174
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1175
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1176
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="ai-form-result" style="display:none;margin-top:12px"></div>
1177
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">
1178
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="closeBackendForm()">cancel</button>
1179
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm primary" id="bf-submit-btn" onclick="submitBackendForm()">add backend</button>
1180
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1181
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1182
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1183
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1184
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- supported backends reference -->
1185
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-ai-supported">
1186
{ 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="card-desc">all available provider types</span><span class="collapse-icon">▾</span></div>
1187
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body" id="ai-supported-list">
1188
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="empty-state">loading…</div>
1189
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1190
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1191
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1192
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- config example -->
1193
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card" id="card-ai-example" style="display:none">
1194
{ 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="card-desc">copy-paste starter config</span><span class="collapse-icon">▾</span></div>
1195
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="card-body">
1196
{ 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:
1197
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
backends:
1198
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- name: openai-main
1199
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
backend: openai
1200
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
api_key: sk-...
1201
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
model: gpt-4o-mini
1202
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
block: [".*-instruct$"] # optional regex filter
1203
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1204
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- name: local-ollama
1205
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
backend: ollama
1206
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
base_url: http://localhost:11434
1207
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
model: llama3.2
1208
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
default: true
1209
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1210
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- name: anthropic-claude
1211
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
backend: anthropic
1212
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
api_key: sk-ant-...
1213
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
model: claude-3-5-sonnet-20241022
1214
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1215
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- name: bedrock-us
1216
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
backend: bedrock
1217
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
region: us-east-1
1218
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
aws_key_id: AKIA...
1219
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
aws_secret_key: ...
1220
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
allow: ["^anthropic\\."] # only Anthropic models
1221
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1222
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
- name: groq-fast
1223
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
backend: groq
1224
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
api_key: gsk_...</pre>
1225
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<p style="font-size:12px;color:#8b949e;margin-top:8px">
1226
{ 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.
1227
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</p>
1228
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1229
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1230
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1231
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1232
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1233
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1234
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- Register drawer -->
1235
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer-overlay" id="drawer-overlay" onclick="closeDrawer()"></div>
1236
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer" id="register-drawer">
1237
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer-header">
1238
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h3>register agent</h3>
1239
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
1240
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="closeDrawer()">✕</button>
1241
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1242
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer-body">
1243
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<form id="register-form" onsubmit="handleRegister(event)">
1244
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="form-row">
1245
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1246
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>nick *</label>
1247
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="reg-nick" placeholder="my-agent-01" required autocomplete="off">
1248
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1249
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1250
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>type</label>
1251
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<select id="reg-type">
1252
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="worker" selected>worker — +v</option>
1253
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="orchestrator">orchestrator — +o</option>
1254
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="observer">observer — read only</option>
1255
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</select>
1256
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1257
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1258
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1259
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>channels</label>
1260
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="reg-channels" placeholder="#fleet, #ops, #project.foo" autocomplete="off">
1261
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="hint">comma-separated; must start with #</div>
1262
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1263
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1264
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>permissions</label>
1265
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="reg-permissions" placeholder="task.create, task.update" autocomplete="off">
1266
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="hint">comma-separated message types this agent may send</div>
1267
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1268
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="register-result" style="display:none"></div>
1269
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;justify-content:flex-end;gap:8px">
1270
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button type="button" onclick="closeDrawer()">cancel</button>
1271
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button type="submit" class="primary">register</button>
1272
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1273
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</form>
1274
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1275
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1276
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1277
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- Register user drawer (operator with fresh credentials) -->
1278
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer-overlay" id="register-user-overlay" onclick="closeRegisterUserDrawer()"></div>
1279
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer" id="register-user-drawer">
1280
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer-header">
1281
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h3>register user</h3>
1282
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
1283
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="closeRegisterUserDrawer()">✕</button>
1284
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1285
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer-body">
1286
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<form id="register-user-form" onsubmit="handleRegisterUser(event)">
1287
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1288
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>nick *</label>
1289
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="regu-nick" placeholder="alice" required autocomplete="off">
1290
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="hint">new NickServ account will be created; credentials returned once</div>
1291
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1292
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1293
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>channels</label>
1294
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="regu-channels" placeholder="#ops, #general" autocomplete="off">
1295
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="hint">comma-separated</div>
1296
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1297
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="register-user-result" style="display:none"></div>
1298
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;justify-content:flex-end;gap:8px">
1299
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button type="button" onclick="closeRegisterUserDrawer()">cancel</button>
1300
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button type="submit" class="primary">register</button>
1301
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1302
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</form>
1303
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</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!
1306
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<!-- Adopt user drawer (claim pre-existing NickServ account) -->
1307
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer-overlay" id="adopt-overlay" onclick="closeAdoptDrawer()"></div>
1308
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer" id="adopt-drawer">
1309
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer-header">
1310
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<h3>adopt existing user</h3>
1311
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
1312
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="closeAdoptDrawer()">✕</button>
1313
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1314
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="drawer-body">
1315
{ 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>
1316
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<form id="adopt-form" onsubmit="handleAdopt(event)">
1317
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1318
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>nick *</label>
1319
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="adopt-nick" placeholder="existing-irc-nick" required autocomplete="off">
1320
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1321
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1322
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<label>channels</label>
1323
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="adopt-channels" placeholder="#ops, #general" autocomplete="off">
1324
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="hint">comma-separated</div>
1325
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1326
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="adopt-result" style="display:none"></div>
1327
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;justify-content:flex-end;gap:8px">
1328
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button type="button" onclick="closeAdoptDrawer()">cancel</button>
1329
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button type="submit" class="primary">adopt</button>
1330
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1331
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</form>
1332
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1333
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1334
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1335
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1336
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<script>
1337
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- tabs ---
1338
{ 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 };
1339
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function switchTab(name) {
1340
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
1341
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
1342
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('tab-' + name).classList.add('active');
1343
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('pane-' + name).classList.add('active');
1344
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (name === 'chat') {
1345
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_chatUnread = 0;
1346
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
delete document.getElementById('tab-chat').dataset.unread;
1347
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1348
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (TAB_LOADERS[name]) TAB_LOADERS[name]();
1349
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1350
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1351
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- auth ---
1352
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function getToken() { return localStorage.getItem('sb_token') || ''; }
1353
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function getUsername() { return localStorage.getItem('sb_username') || ''; }
1354
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1355
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function showLoginScreen() {
1356
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('login-screen').style.display = 'flex';
1357
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
setTimeout(() => document.getElementById('login-username')?.focus(), 80);
1358
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1359
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function hideLoginScreen() {
1360
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('login-screen').style.display = 'none';
1361
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1362
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1363
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function updateHeaderUser() {
1364
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const u = getUsername();
1365
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const t = getToken();
1366
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const label = u ? '@' + u : (t ? t.slice(0,8)+'…' : '');
1367
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('header-user-display').textContent = label;
1368
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const su = document.getElementById('settings-username-display');
1369
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (su) su.textContent = u || 'token auth';
1370
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('settings-api-url').textContent = location.origin;
1371
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('no-token-banner').style.display = t ? 'none' : 'flex';
1372
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1373
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1374
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function handleLogin(e) {
1375
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
e.preventDefault();
1376
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const username = document.getElementById('login-username').value.trim();
1377
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const password = document.getElementById('login-password').value;
1378
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const btn = document.getElementById('login-btn');
1379
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const errEl = document.getElementById('login-error');
1380
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!username || !password) return;
1381
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = true; btn.textContent = 'signing in…';
1382
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
errEl.style.display = 'none';
1383
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1384
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const resp = await fetch('/login', {
1385
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
method: 'POST',
1386
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
headers: {'Content-Type':'application/json'},
1387
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
body: JSON.stringify({username, password}),
1388
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
});
1389
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const data = await resp.json().catch(() => ({}));
1390
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!resp.ok) throw new Error(data.error || 'Login failed');
1391
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.setItem('sb_token', data.token);
1392
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.setItem('sb_username', data.username || username);
1393
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
hideLoginScreen();
1394
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
updateHeaderUser();
1395
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadAll();
1396
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(err) {
1397
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
errEl.style.display = 'block';
1398
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
errEl.innerHTML = renderAlert('error', err.message);
1399
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = false; btn.textContent = 'sign in';
1400
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1401
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1402
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1403
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function saveTokenLogin() {
1404
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const v = document.getElementById('token-login-input').value.trim();
1405
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!v) return;
1406
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.setItem('sb_token', v);
1407
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.removeItem('sb_username');
1408
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
hideLoginScreen();
1409
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
updateHeaderUser();
1410
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadAll();
1411
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1412
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1413
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function logout() {
1414
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.removeItem('sb_token');
1415
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.removeItem('sb_username');
1416
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
location.reload();
1417
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
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 initAuth() {
1420
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!getToken()) { showLoginScreen(); return; }
1421
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
hideLoginScreen();
1422
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
updateHeaderUser();
1423
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadAll();
1424
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1425
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1426
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.addEventListener('keydown', e => { if(e.key==='Escape'){ closeDrawer(); closeRegisterUserDrawer(); closeAdoptDrawer(); } });
1427
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1428
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- API ---
1429
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function api(method, path, body) {
1430
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const opts = { method, cache: 'no-store', headers: { 'Authorization':'Bearer '+getToken(), 'Content-Type':'application/json' } };
1431
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (body !== undefined) opts.body = JSON.stringify(body);
1432
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const res = await fetch(path, opts);
1433
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (res.status === 401) {
1434
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.removeItem('sb_token');
1435
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.removeItem('sb_username');
1436
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
showLoginScreen();
1437
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
throw new Error('Session expired — please sign in again');
1438
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1439
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (res.status === 204) return null;
1440
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const data = await res.json().catch(() => ({ error: res.statusText }));
1441
{ 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 });
1442
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return data;
1443
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1444
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function copyText(text, btn) {
1445
{ 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); });
1446
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1447
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1448
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- charts ---
1449
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const CHART_POINTS = 60; // 5 min at 5s intervals
1450
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const chartData = {
1451
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
labels: Array(CHART_POINTS).fill(''),
1452
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
heap: Array(CHART_POINTS).fill(null),
1453
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
goroutines: Array(CHART_POINTS).fill(null),
1454
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
messages: Array(CHART_POINTS).fill(null),
1455
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
};
1456
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let charts = {};
1457
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1458
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function mkChart(id, label, color) {
1459
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const ctx = document.getElementById(id).getContext('2d');
1460
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return new Chart(ctx, {
1461
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
type: 'line',
1462
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
data: {
1463
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
labels: chartData.labels,
1464
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
datasets: [{
1465
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
label,
1466
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
data: chartData[id.replace('chart-','')],
1467
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
borderColor: color,
1468
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
backgroundColor: color+'22',
1469
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
borderWidth: 1.5,
1470
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
pointRadius: 0,
1471
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
tension: 0.3,
1472
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
fill: true,
1473
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}],
1474
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
},
1475
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
options: {
1476
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
responsive: true,
1477
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
animation: false,
1478
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
plugins: { legend: { display: false } },
1479
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
scales: {
1480
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
x: { display: false },
1481
{ 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 } },
1482
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
},
1483
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
},
1484
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
});
1485
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1486
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1487
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function initCharts() {
1488
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (charts.heap) return;
1489
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
charts.heap = mkChart('chart-heap', 'heap', '#58a6ff');
1490
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
charts.goroutines = mkChart('chart-goroutines', 'goroutines', '#3fb950');
1491
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
charts.messages = mkChart('chart-messages', 'messages', '#d2a8ff');
1492
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1493
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1494
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function fmtBytes(b) {
1495
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (b == null) return '—';
1496
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (b < 1024) return b+'B';
1497
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (b < 1048576) return (b/1024).toFixed(1)+'KB';
1498
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return (b/1048576).toFixed(1)+'MB';
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!
function pushMetrics(m) {
1502
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
chartData.heap.push(m.runtime.heap_alloc_bytes/1048576);
1503
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
chartData.heap.shift();
1504
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
chartData.goroutines.push(m.runtime.goroutines);
1505
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
chartData.goroutines.shift();
1506
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const msgs = m.bridge ? m.bridge.messages_total : null;
1507
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
chartData.messages.push(msgs);
1508
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
chartData.messages.shift();
1509
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1510
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Reassign dataset data arrays (shared reference, Chart.js reads them directly).
1511
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
charts.heap.data.datasets[0].data = chartData.heap;
1512
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
charts.goroutines.data.datasets[0].data = chartData.goroutines;
1513
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
charts.messages.data.datasets[0].data = chartData.messages;
1514
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
charts.heap.update('none');
1515
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
charts.goroutines.update('none');
1516
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
charts.messages.update('none');
1517
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1518
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1519
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- status ---
1520
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadStatus() {
1521
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1522
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const [s, m] = await Promise.all([
1523
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
api('GET', '/v1/status'),
1524
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
api('GET', '/v1/metrics'),
1525
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
]);
1526
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1527
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Status card.
1528
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-status').innerHTML = '<span class="dot green"></span>'+s.status;
1529
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-uptime').textContent = s.uptime;
1530
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const onlineAgents = allAgents.filter(a => a.online).length;
1531
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-agents').textContent = onlineAgents + '/' + s.agents;
1532
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const d = new Date(s.started);
1533
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-started').textContent = d.toLocaleTimeString();
1534
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-started-rel').textContent = d.toLocaleDateString();
1535
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('status-error').style.display = 'none';
1536
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('metrics-updated').textContent = 'updated '+new Date().toLocaleTimeString();
1537
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1538
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Runtime card.
1539
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-goroutines').textContent = m.runtime.goroutines;
1540
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-heap').textContent = fmtBytes(m.runtime.heap_alloc_bytes);
1541
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-heapsys').textContent = fmtBytes(m.runtime.heap_sys_bytes);
1542
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-gc').textContent = m.runtime.gc_runs;
1543
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('chart-heap-val').textContent = fmtBytes(m.runtime.heap_alloc_bytes);
1544
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('chart-goroutines-val').textContent = m.runtime.goroutines;
1545
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('chart-messages-val').textContent = m.bridge ? m.bridge.messages_total : '—';
1546
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1547
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Bridge card.
1548
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (m.bridge) {
1549
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bridge-card').style.display = '';
1550
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-bridge-channels').textContent = m.bridge.channels;
1551
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-bridge-msgs').textContent = m.bridge.messages_total;
1552
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-bridge-subs').textContent = m.bridge.active_subscribers;
1553
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1554
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1555
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Registry card.
1556
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-reg-total').textContent = m.registry.total;
1557
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-reg-active').textContent = m.registry.active;
1558
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-reg-revoked').textContent = m.registry.revoked;
1559
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1560
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Push to charts.
1561
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
initCharts();
1562
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
pushMetrics(m);
1563
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
1564
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('stat-status').innerHTML = '<span class="dot red"></span>error';
1565
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('status-error');
1566
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.style.display = 'block';
1567
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = renderAlert('error', e.message);
1568
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1569
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1570
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1571
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let metricsTimer = null;
1572
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function startMetricsPoll() {
1573
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (metricsTimer) return;
1574
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
metricsTimer = setInterval(() => {
1575
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (document.getElementById('pane-status').classList.contains('active')) loadStatus();
1576
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}, 5000);
1577
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1578
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1579
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- agents + users (shared data) ---
1580
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let allAgents = [];
1581
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadAgents() {
1582
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1583
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const data = await api('GET', '/v1/agents');
1584
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
allAgents = data.agents || [];
1585
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderUsersTable();
1586
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderAgentTable();
1587
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
populateChatIdentity();
1588
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
1589
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const msg = '<div style="padding:16px">'+renderAlert('error', e.message)+'</div>';
1590
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('agents-container').innerHTML = msg;
1591
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('users-container').innerHTML = msg;
1592
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1593
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1594
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1595
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderTable(container, countEl, rows, emptyMsg, cols) {
1596
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (rows.length === 0) {
1597
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById(container).innerHTML = '<div class="empty">'+emptyMsg+'</div>';
1598
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
1599
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const ths = cols.map(c=>`<th>${c}</th>`).join('');
1600
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById(container).innerHTML =
1601
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`<table><thead><tr>${ths}</tr></thead><tbody>${rows.join('')}</tbody></table>`;
1602
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1603
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (countEl) document.getElementById(countEl).textContent = rows.length;
1604
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1605
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1606
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderUsersTable() {
1607
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const q = (document.getElementById('user-search').value||'').toLowerCase();
1608
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const users = allAgents.filter(a => a.type === 'operator' && (!q ||
1609
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
a.nick.toLowerCase().includes(q) ||
1610
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
(a.config?.channels||[]).some(c => c.toLowerCase().includes(q))));
1611
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rows = users.map(a => {
1612
{ 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('');
1613
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rev = a.revoked ? '<span class="tag revoked">revoked</span>' : '';
1614
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<tr>
1615
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><strong>${esc(a.nick)}</strong></td>
1616
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><span class="tag type-operator">operator</span>${rev}</td>
1617
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td>${chs||'<span style="color:#8b949e">—</span>'}</td>
1618
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="white-space:nowrap">${fmtTime(a.created_at)}</td>
1619
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><div class="actions">${!a.revoked?`
1620
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="rotateAgent('${esc(a.nick)}')">rotate</button>
1621
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm danger" onclick="revokeAgent('${esc(a.nick)}')">revoke</button>`:``}
1622
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm danger" onclick="deleteAgent('${esc(a.nick)}')">delete</button></div></td>
1623
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tr>`;
1624
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
});
1625
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const all = allAgents.filter(a => a.type === 'operator');
1626
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const countTxt = all.length + (rows.length !== all.length ? ' / '+rows.length+' shown' : '');
1627
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('user-count').textContent = countTxt;
1628
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTable('users-container', null, rows,
1629
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
all.length ? 'no users match the filter' : 'no users registered yet',
1630
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
['nick','type','channels','registered','']);
1631
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1632
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1633
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function relTime(ts) {
1634
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!ts) return 'never';
1635
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const ms = Date.now() - new Date(ts).getTime();
1636
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (ms < 0) return 'just now';
1637
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const s = Math.floor(ms/1000), m = Math.floor(s/60), h = Math.floor(m/60), d = Math.floor(h/24);
1638
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (d > 0) return d + 'd ago';
1639
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (h > 0) return h + 'h ago';
1640
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (m > 0) return m + 'm ago';
1641
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return s + 's ago';
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!
function presenceDot(a) {
1645
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (a.revoked) return '<span style="color:#f85149" title="revoked">◼</span>';
1646
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (a.online) return '<span style="color:#3fb950" title="online">●</span>';
1647
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (a.last_seen) {
1648
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const mins = (Date.now() - new Date(a.last_seen).getTime()) / 60000;
1649
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (mins < 10) return '<span style="color:#d29922" title="idle">●</span>';
1650
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1651
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return '<span style="color:#484f58" title="offline">●</span>';
1652
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1653
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1654
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let agentPage = 0;
1655
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const AGENTS_PER_PAGE = 25;
1656
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1657
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderAgentTable() {
1658
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const q = (document.getElementById('agent-search').value||'').toLowerCase();
1659
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const statusFilter = document.getElementById('agent-status-filter').value;
1660
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const bots = allAgents.filter(a => a.type !== 'operator');
1661
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1662
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Status filter.
1663
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let filtered = bots;
1664
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (statusFilter === 'online') filtered = bots.filter(a => a.online);
1665
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
else if (statusFilter === 'offline') filtered = bots.filter(a => !a.online && !a.revoked);
1666
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
else if (statusFilter === 'revoked') filtered = bots.filter(a => a.revoked);
1667
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1668
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Text search.
1669
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const agents = filtered.filter(a => !q ||
1670
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
a.nick.toLowerCase().includes(q) ||
1671
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
a.type.toLowerCase().includes(q) ||
1672
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
(a.config?.channels||[]).some(c => c.toLowerCase().includes(q)) ||
1673
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
(a.config?.permissions||[]).some(p => p.toLowerCase().includes(q)));
1674
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1675
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Sort: online first, then by last_seen descending, then by nick.
1676
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
agents.sort((a, b) => {
1677
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (a.online !== b.online) return a.online ? -1 : 1;
1678
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const aT = a.last_seen ? new Date(a.last_seen).getTime() : 0;
1679
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const bT = b.last_seen ? new Date(b.last_seen).getTime() : 0;
1680
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (aT !== bT) return bT - aT;
1681
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return a.nick.localeCompare(b.nick);
1682
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
});
1683
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1684
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Pagination.
1685
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const totalPages = Math.max(1, Math.ceil(agents.length / AGENTS_PER_PAGE));
1686
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (agentPage >= totalPages) agentPage = totalPages - 1;
1687
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (agentPage < 0) agentPage = 0;
1688
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const start = agentPage * AGENTS_PER_PAGE;
1689
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const pageAgents = agents.slice(start, start + AGENTS_PER_PAGE);
1690
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1691
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const onlineCount = bots.filter(a => a.online).length;
1692
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let countText = onlineCount + '/' + bots.length;
1693
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (agents.length !== bots.length) countText = agents.length + ' of ' + bots.length;
1694
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('agent-count').textContent = countText;
1695
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1696
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Pagination controls.
1697
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const pagEl = document.getElementById('agent-pagination');
1698
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (agents.length > AGENTS_PER_PAGE) {
1699
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
pagEl.style.display = 'flex';
1700
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('agent-prev').disabled = agentPage === 0;
1701
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('agent-next').disabled = agentPage >= totalPages - 1;
1702
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('agent-page-info').textContent = `page ${agentPage+1}/${totalPages}`;
1703
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
1704
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
pagEl.style.display = 'none';
1705
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1706
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1707
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rows = pageAgents.map(a => {
1708
{ 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('');
1709
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rev = a.revoked ? '<span class="tag revoked">revoked</span>' : '';
1710
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const seen = a.last_seen ? relTime(a.last_seen) : 'never';
1711
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const seenStyle = a.online ? 'color:#3fb950' : 'color:#8b949e';
1712
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<tr${a.revoked?' style="opacity:0.5"':''}>
1713
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><input type="checkbox" class="agent-select" value="${esc(a.nick)}" onchange="updateBulkBtn()" style="margin-right:6px">${presenceDot(a)} <strong>${esc(a.nick)}</strong></td>
1714
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><span class="tag type-${a.type}">${esc(a.type)}</span>${rev}</td>
1715
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td>${chs||'<span style="color:#8b949e">—</span>'}</td>
1716
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="white-space:nowrap;${seenStyle}">${seen}</td>
1717
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><div class="actions">${!a.revoked?`
1718
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="rotateAgent('${esc(a.nick)}')">rotate</button>
1719
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm danger" onclick="revokeAgent('${esc(a.nick)}')">revoke</button>`:``}
1720
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm danger" onclick="deleteAgent('${esc(a.nick)}')">delete</button></div></td>
1721
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tr>`;
1722
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
});
1723
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTable('agents-container', null, rows,
1724
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
bots.length ? 'no agents match the filter' : 'no agents registered yet',
1725
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
['<input type="checkbox" id="agent-select-all" onchange="toggleSelectAllAgents(this.checked)" style="margin-right:6px">nick','type','channels','last seen','']);
1726
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1727
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1728
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function revokeAgent(nick) {
1729
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!confirm(`Revoke "${nick}"? This cannot be undone.`)) return;
1730
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try { await api('POST', `/v1/agents/${nick}/revoke`); await loadAgents(); await loadStatus(); }
1731
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
catch(e) { alert('Revoke failed: '+e.message); }
1732
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1733
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function deleteAgent(nick) {
1734
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!confirm(`Delete "${nick}"? This permanently removes the agent from the registry.`)) return;
1735
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try { await api('DELETE', `/v1/agents/${nick}`); await loadAgents(); await loadStatus(); }
1736
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
catch(e) { alert('Delete failed: '+e.message); }
1737
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1738
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function toggleSelectAllAgents(checked) {
1739
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.querySelectorAll('.agent-select').forEach(cb => cb.checked = checked);
1740
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
updateBulkBtn();
1741
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1742
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function updateBulkBtn() {
1743
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const checked = document.querySelectorAll('.agent-select:checked');
1744
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const btn = document.getElementById('bulk-delete-btn');
1745
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.style.display = checked.length > 0 ? '' : 'none';
1746
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = `delete selected (${checked.length})`;
1747
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1748
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function bulkDeleteAgents() {
1749
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const nicks = [...document.querySelectorAll('.agent-select:checked')].map(cb => cb.value);
1750
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!nicks.length) return;
1751
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!confirm(`Delete ${nicks.length} agent(s)? This permanently removes them from the registry.\n\n${nicks.join(', ')}`)) return;
1752
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1753
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const result = await api('POST', '/v1/agents/bulk-delete', {nicks});
1754
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await loadAgents();
1755
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await loadStatus();
1756
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (result.failed > 0) alert(`Deleted ${result.deleted}, failed ${result.failed}`);
1757
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { alert('Bulk delete failed: ' + e.message); }
1758
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1759
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function rotateAgent(nick) {
1760
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1761
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const creds = await api('POST', `/v1/agents/${nick}/rotate`);
1762
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Show result in whichever drawer is relevant.
1763
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
showCredentials(nick, creds, null, 'rotate');
1764
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
openDrawer();
1765
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1766
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
catch(e) { alert('Rotate failed: '+e.message); }
1767
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1768
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1769
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- users drawers ---
1770
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function openRegisterUserDrawer() {
1771
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('register-user-overlay').classList.add('open');
1772
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('register-user-drawer').classList.add('open');
1773
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
setTimeout(() => document.getElementById('regu-nick').focus(), 100);
1774
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1775
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function closeRegisterUserDrawer() {
1776
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('register-user-overlay').classList.remove('open');
1777
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('register-user-drawer').classList.remove('open');
1778
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1779
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function openAdoptDrawer() {
1780
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('adopt-overlay').classList.add('open');
1781
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('adopt-drawer').classList.add('open');
1782
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
setTimeout(() => document.getElementById('adopt-nick').focus(), 100);
1783
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1784
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function closeAdoptDrawer() {
1785
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('adopt-overlay').classList.remove('open');
1786
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('adopt-drawer').classList.remove('open');
1787
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1788
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1789
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function handleRegisterUser(e) {
1790
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
e.preventDefault();
1791
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const nick = document.getElementById('regu-nick').value.trim();
1792
{ 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);
1793
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const resultEl = document.getElementById('register-user-result');
1794
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'none';
1795
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1796
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const res = await api('POST', '/v1/agents/register', { nick, type: 'operator', channels, permissions: [] });
1797
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
1798
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const pass = res.credentials?.passphrase || '';
1799
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('success',
1800
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`<div><strong>Registered: ${esc(nick)}</strong>
1801
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="cred-box">
1802
{ 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>
1803
{ 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>
1804
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1805
{ 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>`);
1806
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('register-user-form').reset();
1807
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await loadAgents(); await loadStatus();
1808
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { resultEl.style.display='block'; resultEl.innerHTML=renderAlert('error', e.message); }
1809
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1810
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1811
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function handleAdopt(e) {
1812
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
e.preventDefault();
1813
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const nick = document.getElementById('adopt-nick').value.trim();
1814
{ 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);
1815
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const resultEl = document.getElementById('adopt-result');
1816
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'none';
1817
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1818
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('POST', `/v1/agents/${nick}/adopt`, { type: 'operator', channels, permissions: [] });
1819
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
1820
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('success',
1821
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`<strong>${esc(nick)}</strong> adopted as operator — existing IRC session and passphrase unchanged.`);
1822
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('adopt-form').reset();
1823
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await loadAgents(); await loadStatus();
1824
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { resultEl.style.display='block'; resultEl.innerHTML=renderAlert('error', e.message); }
1825
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1826
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1827
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- drawer ---
1828
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function openDrawer() {
1829
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('drawer-overlay').classList.add('open');
1830
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('register-drawer').classList.add('open');
1831
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
setTimeout(() => document.getElementById('reg-nick').focus(), 100);
1832
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1833
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function closeDrawer() {
1834
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('drawer-overlay').classList.remove('open');
1835
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('register-drawer').classList.remove('open');
1836
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1837
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1838
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- register ---
1839
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function handleRegister(e) {
1840
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
e.preventDefault();
1841
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const nick = document.getElementById('reg-nick').value.trim();
1842
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const type = document.getElementById('reg-type').value;
1843
{ 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);
1844
{ 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);
1845
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const resultEl = document.getElementById('register-result');
1846
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'none';
1847
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1848
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const res = await api('POST', '/v1/agents/register', { nick, type, channels, permissions });
1849
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
showCredentials(nick, res.credentials, res.payload, 'register');
1850
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('register-form').reset();
1851
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await loadAgents(); await loadStatus();
1852
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { resultEl.style.display='block'; resultEl.innerHTML=renderAlert('error', e.message); }
1853
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1854
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function showCredentials(nick, creds, payload, mode) {
1855
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const resultEl = document.getElementById('register-result');
1856
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
1857
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const pass = creds?.passphrase || creds?.Passphrase || '';
1858
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sig = payload?.signature || '';
1859
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('success',
1860
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`<div><strong>${mode==='register'?'Registered':'Rotated'}: ${esc(nick)}</strong>
1861
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="cred-box">
1862
{ 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>
1863
{ 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>
1864
{ 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>`:''}
1865
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1866
{ 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>`
1867
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
);
1868
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1869
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1870
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- channels tab ---
1871
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let allChannels = [];
1872
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1873
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadChanTab() {
1874
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!getToken()) return;
1875
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1876
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const data = await api('GET', '/v1/channels');
1877
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
allChannels = (data.channels || []).sort();
1878
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderChanList();
1879
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
1880
{ 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>';
1881
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1882
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadTopology();
1883
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Load ROE templates from policies for the ROE card.
1884
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1885
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const s = await api('GET', '/v1/settings');
1886
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (s && s.policies) {
1887
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
currentPolicies = s.policies;
1888
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderROETemplates(s.policies.roe_templates || []);
1889
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1890
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {}
1891
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1892
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1893
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderChanList() {
1894
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const q = (document.getElementById('chan-search').value||'').toLowerCase();
1895
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const filtered = allChannels.filter(ch => !q || ch.toLowerCase().includes(q));
1896
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('chan-count').textContent = filtered.length + (filtered.length !== allChannels.length ? '/' + allChannels.length : '');
1897
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (allChannels.length === 0) {
1898
{ 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>';
1899
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return;
1900
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1901
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (filtered.length === 0) {
1902
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('channels-list').innerHTML = '<div class="empty">no channels match the filter</div>';
1903
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return;
1904
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1905
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('channels-list').innerHTML = filtered.map(ch =>
1906
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`<div class="chan-card">
1907
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
1908
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chan-name">${esc(ch)}</div>
1909
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="chan-meta">joined</div>
1910
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
1911
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="spacer"></div>
1912
{ 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>
1913
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`
1914
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
).join('');
1915
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1916
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function quickJoin() {
1917
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let ch = document.getElementById('quick-join-input').value.trim();
1918
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!ch) return;
1919
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!ch.startsWith('#')) ch = '#' + ch;
1920
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const slug = ch.replace(/^#/,'');
1921
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1922
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('POST', `/v1/channels/${slug}/join`);
1923
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('quick-join-input').value = '';
1924
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await loadChanTab();
1925
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderChanSidebar((await api('GET','/v1/channels')).channels||[]);
1926
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { alert('Join failed: '+e.message); }
1927
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1928
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('quick-join-input').addEventListener('keydown', e => { if(e.key==='Enter')quickJoin(); });
1929
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1930
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- topology panel (#115) + task channels (#114) ---
1931
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadTopology() {
1932
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1933
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const data = await api('GET', '/v1/topology');
1934
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTopologyTypes(data.types || []);
1935
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTopologyActive(data.active_channels || [], data.types || []);
1936
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
1937
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('topology-types').innerHTML = '';
1938
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('topology-active').innerHTML = '<div style="color:#8b949e;font-size:12px">topology not configured</div>';
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!
1942
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderTopologyTypes(types) {
1943
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!types.length) { document.getElementById('topology-types').innerHTML = ''; return; }
1944
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rows = types.map(t => {
1945
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const ttl = t.ttl_seconds > 0 ? `${Math.round(t.ttl_seconds/3600)}h` : '—';
1946
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const tags = [];
1947
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (t.ephemeral) tags.push('<span style="background:#f8514922;color:#f85149;padding:1px 5px;border-radius:3px;font-size:10px">ephemeral</span>');
1948
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (t.supervision) tags.push(`<span style="font-size:11px;color:#8b949e">→ ${esc(t.supervision)}</span>`);
1949
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<tr>
1950
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><strong>${esc(t.name)}</strong></td>
1951
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><code style="font-size:11px">#${esc(t.prefix)}*</code></td>
1952
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="font-size:12px">${(t.autojoin||[]).map(n => `<code style="font-size:11px;background:#21262d;padding:1px 4px;border-radius:3px">${esc(n)}</code>`).join(' ')}</td>
1953
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="font-size:12px">${ttl}</td>
1954
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td>${tags.join(' ')}</td>
1955
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tr>`;
1956
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}).join('');
1957
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('topology-types').innerHTML = `<table><thead><tr><th>type</th><th>prefix</th><th>autojoin</th><th>TTL</th><th></th></tr></thead><tbody>${rows}</tbody></table>`;
1958
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1959
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1960
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderTopologyActive(channels, types) {
1961
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('topology-active');
1962
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const tasks = channels.filter(c => c.ephemeral || c.type === 'task');
1963
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!tasks.length) {
1964
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = '<div style="color:#8b949e;font-size:12px;padding:4px 0">no active task channels</div>';
1965
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return;
1966
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1967
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rows = tasks.map(c => {
1968
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const age = c.provisioned_at ? timeSince(new Date(c.provisioned_at)) : '—';
1969
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const ttl = c.ttl_seconds > 0 ? `${Math.round(c.ttl_seconds/3600)}h` : '—';
1970
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<tr>
1971
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><strong>${esc(c.name)}</strong></td>
1972
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="font-size:12px;color:#8b949e">${esc(c.type || '—')}</td>
1973
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="font-size:12px">${age}</td>
1974
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="font-size:12px">${ttl}</td>
1975
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><button class="sm danger" onclick="dropChannel('${esc(c.name)}')">drop</button></td>
1976
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tr>`;
1977
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}).join('');
1978
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = `<table><thead><tr><th>channel</th><th>type</th><th>age</th><th>TTL</th><th></th></tr></thead><tbody>${rows}</tbody></table>`;
1979
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1980
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1981
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function timeSince(date) {
1982
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const s = Math.floor((new Date() - date) / 1000);
1983
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (s < 60) return s + 's';
1984
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (s < 3600) return Math.floor(s/60) + 'm';
1985
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (s < 86400) return Math.floor(s/3600) + 'h';
1986
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return Math.floor(s/86400) + 'd';
1987
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
1988
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
1989
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function provisionChannel() {
1990
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let ch = document.getElementById('provision-channel-input').value.trim();
1991
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!ch) return;
1992
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!ch.startsWith('#')) ch = '#' + ch;
1993
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
1994
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('POST', '/v1/channels', {name: ch});
1995
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('provision-channel-input').value = '';
1996
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadTopology();
1997
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadChanTab();
1998
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { alert('Provision failed: ' + e.message); }
1999
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2000
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2001
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function dropChannel(ch) {
2002
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!confirm('Drop channel ' + ch + '? This unregisters it from ChanServ.')) return;
2003
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const slug = ch.replace(/^#/,'');
2004
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2005
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('DELETE', `/v1/topology/channels/${slug}`);
2006
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadTopology();
2007
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadChanTab();
2008
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { alert('Drop failed: ' + e.message); }
2009
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2010
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2011
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- ROE template editor (#118) ---
2012
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderROETemplates(templates) {
2013
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('roe-list');
2014
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!templates || !templates.length) {
2015
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = '<div style="color:#8b949e;font-size:12px">No ROE templates defined. Click + add template to create one.</div>';
2016
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return;
2017
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2018
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = templates.map((t, i) => `
2019
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="border:1px solid #21262d;border-radius:6px;padding:12px;margin-bottom:10px;background:#0d1117">
2020
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:10px;align-items:center;margin-bottom:8px">
2021
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" value="${esc(t.name)}" placeholder="template name" style="flex:1;font-weight:600" onchange="updateROE(${i},'name',this.value)">
2022
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm danger" onclick="removeROE(${i})">remove</button>
2023
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
2024
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:6px">
2025
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="flex:1;min-width:200px"><label style="font-size:11px">channels (comma-separated)</label><input type="text" value="${esc((t.channels||[]).join(', '))}" onchange="updateROE(${i},'channels',this.value)" style="width:100%"></div>
2026
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="flex:1;min-width:200px"><label style="font-size:11px">permissions</label><input type="text" value="${esc((t.permissions||[]).join(', '))}" onchange="updateROE(${i},'permissions',this.value)" style="width:100%"></div>
2027
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
2028
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:10px">
2029
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div><label style="font-size:11px">msg/sec</label><input type="number" value="${t.rate_limit?.messages_per_second||''}" placeholder="10" style="width:70px" onchange="updateROERateLimit(${i},'messages_per_second',this.value)"></div>
2030
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div><label style="font-size:11px">burst</label><input type="number" value="${t.rate_limit?.burst||''}" placeholder="50" style="width:70px" onchange="updateROERateLimit(${i},'burst',this.value)"></div>
2031
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="flex:1"><label style="font-size:11px">description</label><input type="text" value="${esc(t.description||'')}" onchange="updateROE(${i},'description',this.value)" style="width:100%"></div>
2032
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
2033
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
2034
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`).join('');
2035
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2036
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2037
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function addROETemplate() {
2038
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!currentPolicies.roe_templates) currentPolicies.roe_templates = [];
2039
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
currentPolicies.roe_templates.push({name: 'new-template', channels: [], permissions: []});
2040
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderROETemplates(currentPolicies.roe_templates);
2041
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2042
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function removeROE(i) {
2043
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
currentPolicies.roe_templates.splice(i, 1);
2044
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderROETemplates(currentPolicies.roe_templates);
2045
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2046
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function updateROE(i, field, val) {
2047
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (field === 'channels' || field === 'permissions') {
2048
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
currentPolicies.roe_templates[i][field] = val.split(',').map(s => s.trim()).filter(Boolean);
2049
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
2050
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
currentPolicies.roe_templates[i][field] = val;
2051
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2052
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2053
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function updateROERateLimit(i, field, val) {
2054
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!currentPolicies.roe_templates[i].rate_limit) currentPolicies.roe_templates[i].rate_limit = {};
2055
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
currentPolicies.roe_templates[i].rate_limit[field] = Number(val) || 0;
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!
// --- chat ---
2059
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let chatChannel = null, chatSSE = null;
2060
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2061
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadChannels() {
2062
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!getToken()) return;
2063
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2064
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const data = await api('GET', '/v1/channels');
2065
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderChanSidebar(data.channels || []);
2066
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {}
2067
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2068
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderChanSidebar(channels) {
2069
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const list = document.getElementById('chan-list');
2070
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
list.innerHTML = '';
2071
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
channels.sort().forEach(ch => {
2072
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.createElement('div');
2073
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.className = 'chan-item' + (ch===chatChannel?' active':'');
2074
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.textContent = ch;
2075
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.onclick = () => selectChannel(ch);
2076
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
list.appendChild(el);
2077
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
});
2078
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2079
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function joinChannel() {
2080
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let ch = document.getElementById('join-channel-input').value.trim();
2081
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!ch) return;
2082
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!ch.startsWith('#')) ch = '#' + ch;
2083
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const slug = ch.replace(/^#/,'');
2084
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2085
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('POST', `/v1/channels/${slug}/join`);
2086
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('join-channel-input').value = '';
2087
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const data = await api('GET', '/v1/channels');
2088
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderChanSidebar(data.channels||[]);
2089
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
selectChannel(ch);
2090
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { alert('Join failed: '+e.message); }
2091
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2092
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('join-channel-input').addEventListener('keydown', e => { if(e.key==='Enter')joinChannel(); });
2093
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2094
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function selectChannel(ch) {
2095
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_lastMsgNick = null; _lastMsgAt = 0; _hideChatBanner(); _chatUnread = 0;
2096
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
chatChannel = ch;
2097
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('chat-ch-name').textContent = ch;
2098
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('chat-placeholder').style.display = 'none';
2099
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.querySelectorAll('.chan-item').forEach(el => el.classList.toggle('active', el.textContent===ch));
2100
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2101
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const area = document.getElementById('chat-msgs');
2102
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
Array.from(area.children).forEach(el => { if(!el.id) el.remove(); });
2103
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2104
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2105
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const slug = ch.replace(/^#/,'');
2106
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const data = await api('GET', `/v1/channels/${slug}/messages`);
2107
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
(data.messages||[]).forEach(m => appendMsg(m, true));
2108
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
area.scrollTop = area.scrollHeight;
2109
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {}
2110
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2111
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (chatSSE) { chatSSE.close(); chatSSE = null; }
2112
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const slug = ch.replace(/^#/,'');
2113
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const es = new EventSource(`/v1/channels/${slug}/stream?token=${encodeURIComponent(getToken())}`);
2114
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
chatSSE = es;
2115
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const badge = document.getElementById('chat-stream-status');
2116
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
es.onopen = () => { badge.textContent='● live'; badge.style.color='#3fb950'; };
2117
{ 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(_){} };
2118
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
es.onerror = () => { badge.textContent='○ reconnecting…'; badge.style.color='#8b949e'; };
2119
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2120
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadNicklist(ch);
2121
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (_nicklistTimer) clearInterval(_nicklistTimer);
2122
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_nicklistTimer = setInterval(() => loadNicklist(chatChannel), 10000);
2123
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2124
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2125
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _nicklistTimer = null;
2126
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadNicklist(ch) {
2127
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!ch) return;
2128
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2129
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const slug = ch.replace(/^#/,'');
2130
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const data = await api('GET', `/v1/channels/${slug}/users`);
2131
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderNicklist(data.users || [], data.channel_modes || '');
2132
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {}
2133
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2134
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const SYSTEM_BOTS = new Set(['bridge','oracle','sentinel','steward','scribe','warden','snitch','herald','scroll','systembot','auditbot']);
2135
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const AGENT_PREFIXES = ['claude-','codex-','gemini-','openclaw-'];
2136
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2137
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function nickTier(nick) {
2138
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const lower = nick.toLowerCase();
2139
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Check if operator (registered as operator type).
2140
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const agent = allAgents.find(a => a.nick === nick);
2141
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (agent && agent.type === 'operator') return 0; // ops
2142
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (SYSTEM_BOTS.has(lower)) return 1; // system bots
2143
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (AGENT_PREFIXES.some(p => lower.startsWith(p))) return 2; // agents
2144
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return 3; // regular users
2145
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2146
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2147
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function nickPrefix(nick) {
2148
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const tier = nickTier(nick);
2149
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (tier === 0) return '@';
2150
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (tier === 1) return '+';
2151
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return '';
2152
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2153
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2154
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderNicklist(users, channelModes) {
2155
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('nicklist-users');
2156
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// users may be [{nick, modes}] or ["nick"] for backwards compat.
2157
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const normalized = users.map(u => typeof u === 'string' ? {nick: u, modes: []} : u);
2158
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Sort: ops > system bots > agents > users, alpha within each tier.
2159
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sorted = normalized.slice().sort((a, b) => {
2160
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const ta = nickTier(a.nick), tb = nickTier(b.nick);
2161
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (ta !== tb) return ta - tb;
2162
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return a.nick.localeCompare(b.nick);
2163
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
});
2164
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = sorted.map(u => {
2165
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const modes = u.modes || [];
2166
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// IRC mode prefix: @ for op, + for voice
2167
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let prefix = '';
2168
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (modes.includes('o') || modes.includes('a') || modes.includes('q')) prefix = '@';
2169
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
else if (modes.includes('v')) prefix = '+';
2170
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
else prefix = nickPrefix(u.nick);
2171
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const tier = nickTier(u.nick);
2172
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const cls = (modes.includes('o') || tier === 0) ? ' is-op' : tier === 1 ? ' is-bot' : '';
2173
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const modeStr = modes.length ? ` [+${modes.join('')}]` : '';
2174
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div class="nicklist-nick${cls}" title="${esc(u.nick)}${modeStr}">${prefix}${esc(u.nick)}</div>`;
2175
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}).join('');
2176
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Show channel modes in header if available.
2177
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const modesEl = document.getElementById('chat-channel-modes');
2178
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (modesEl) modesEl.textContent = channelModes ? ` ${channelModes}` : '';
2179
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2180
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Nick colors — deterministic hash over a palette
2181
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const NICK_PALETTE = ['#58a6ff','#3fb950','#ffa657','#d2a8ff','#56d364','#79c0ff','#ff7b72','#a5d6ff','#f0883e','#39d353'];
2182
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function nickColor(nick) {
2183
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let h = 0;
2184
{ 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;
2185
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return NICK_PALETTE[h % NICK_PALETTE.length];
2186
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2187
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2188
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _lastMsgNick = null, _lastMsgAt = 0;
2189
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const GROUP_MS = 5 * 60 * 1000;
2190
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _chatNewBanner = null;
2191
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _chatUnread = 0;
2192
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2193
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function appendMsg(msg, isHistory) {
2194
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const area = document.getElementById('chat-msgs');
2195
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2196
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Attribution: RELAYMSG delivers nicks as "user/bridge"; legacy uses "[nick] text".
2197
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let displayNick = msg.nick;
2198
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let displayText = msg.text;
2199
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (msg.nick && msg.nick.endsWith('/bridge')) {
2200
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
displayNick = msg.nick.slice(0, -'/bridge'.length);
2201
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else if (msg.nick === 'bridge') {
2202
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const m = msg.text.match(/^\[([^\]]+)\] ([\s\S]*)$/);
2203
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (m) { displayNick = m[1]; displayText = m[2]; }
2204
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2205
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2206
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const atMs = new Date(msg.at).getTime();
2207
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const grouped = !isHistory && displayNick === _lastMsgNick && (atMs - _lastMsgAt) < GROUP_MS;
2208
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_lastMsgNick = displayNick;
2209
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_lastMsgAt = atMs;
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 timeStr = new Date(msg.at).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
2212
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const color = nickColor(displayNick);
2213
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2214
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const row = document.createElement('div');
2215
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
row.className = 'msg-row' + (grouped ? ' msg-grouped' : '');
2216
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Build meta toggle if metadata present and rich mode is on.
2217
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let metaToggle = '';
2218
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let metaBlock = '';
2219
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (msg.meta && msg.meta.type) {
2220
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const html = renderMeta(msg.meta);
2221
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (html) {
2222
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const show = isRichMode();
2223
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
metaToggle = `<span class="msg-meta-toggle" style="${show ? '' : 'display:none'}" onclick="this.parentElement.nextElementSibling.classList.toggle('open');event.stopPropagation()">✨</span>`;
2224
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
metaBlock = `<div class="msg-meta">${html}</div>`;
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!
row.innerHTML =
2229
{ 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>` +
2230
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`<span class="msg-nick" style="color:${color}">[${esc(displayNick)}]:</span>` +
2231
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`<span class="msg-text">${highlightText(esc(displayText))}${metaToggle}</span>`;
2232
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2233
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Apply row-level highlights.
2234
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const myNick = localStorage.getItem('sb_username') || '';
2235
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const lower = displayText.toLowerCase();
2236
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (myNick && lower.includes(myNick.toLowerCase())) {
2237
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
row.classList.add('hl-mention');
2238
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else if (getDangerWords().some(w => lower.includes(w))) {
2239
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
row.classList.add('hl-danger');
2240
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2241
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// System messages (joins, parts, reconnects).
2242
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (/\b(online|offline|reconnected|joined|parted)\b/i.test(displayText) && !displayText.includes(': ')) {
2243
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
row.classList.add('hl-system');
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!
area.appendChild(row);
2247
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Append meta block after the row so toggle can find it via nextElementSibling.
2248
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (metaBlock) {
2249
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const metaEl = document.createElement('div');
2250
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
metaEl.innerHTML = metaBlock;
2251
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
area.appendChild(metaEl.firstChild);
2252
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2253
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2254
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Unread badge when chat tab not active
2255
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!isHistory && !document.getElementById('tab-chat').classList.contains('active')) {
2256
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_chatUnread++;
2257
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('tab-chat').dataset.unread = _chatUnread > 9 ? '9+' : _chatUnread;
2258
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2259
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2260
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (isHistory) {
2261
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
area.scrollTop = area.scrollHeight;
2262
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
2263
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const nearBottom = area.scrollHeight - area.clientHeight - area.scrollTop < 100;
2264
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (nearBottom) {
2265
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
area.scrollTop = area.scrollHeight;
2266
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_hideChatBanner();
2267
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
2268
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_showChatBanner(area);
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!
2273
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function _showChatBanner(area) {
2274
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (_chatNewBanner) return;
2275
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_chatNewBanner = document.createElement('div');
2276
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_chatNewBanner.className = 'chat-new-banner';
2277
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_chatNewBanner.textContent = '↓ new messages';
2278
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_chatNewBanner.onclick = () => { area.scrollTop = area.scrollHeight; _hideChatBanner(); };
2279
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
area.appendChild(_chatNewBanner);
2280
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2281
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function _hideChatBanner() {
2282
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (_chatNewBanner) { _chatNewBanner.remove(); _chatNewBanner = null; }
2283
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2284
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function saveChatIdentity() {
2285
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.setItem('sb_chat_nick', document.getElementById('chat-identity').value);
2286
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2287
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function getChatNick() {
2288
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return localStorage.getItem('sb_chat_nick') || '';
2289
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2290
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function populateChatIdentity() {
2291
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sel = document.getElementById('chat-identity');
2292
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const current = getChatNick();
2293
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Only operators (human users) can chat — agents are not selectable.
2294
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const operators = allAgents.filter(a => a.type === 'operator' && !a.revoked);
2295
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
sel.innerHTML = '<option value="">— pick a user —</option>' +
2296
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
operators.map(a => `<option value="${esc(a.nick)}"${a.nick===current?' selected':''}>${esc(a.nick)}</option>`).join('');
2297
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Restore saved selection.
2298
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (current) sel.value = current;
2299
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2300
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2301
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function toggleChatLayout() {
2302
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('chat-msgs');
2303
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const columnar = el.classList.toggle('columnar');
2304
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.setItem('sb_chat_columnar', columnar ? '1' : '0');
2305
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2306
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Restore layout preference on load.
2307
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (localStorage.getItem('sb_chat_columnar') === '1') {
2308
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('chat-msgs').classList.add('columnar');
2309
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2310
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2311
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- timestamp toggle ---
2312
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function toggleTimestamps() {
2313
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('chat-msgs');
2314
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const hidden = el.classList.toggle('hide-timestamps');
2315
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.setItem('sb_hide_timestamps', hidden ? '1' : '0');
2316
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const btn = document.getElementById('chat-ts-toggle');
2317
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.style.opacity = hidden ? '0.3' : '1';
2318
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.title = hidden ? 'timestamps hidden — click to show' : 'timestamps shown — click to hide';
2319
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2320
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
(function() {
2321
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const hidden = localStorage.getItem('sb_hide_timestamps') === '1';
2322
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (hidden) document.getElementById('chat-msgs').classList.add('hide-timestamps');
2323
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const btn = document.getElementById('chat-ts-toggle');
2324
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (hidden) { btn.style.opacity = '0.3'; btn.title = 'timestamps hidden — click to show'; }
2325
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
else { btn.title = 'timestamps shown — click to hide'; }
2326
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
})();
2327
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2328
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- rich mode toggle ---
2329
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function isRichMode() { return localStorage.getItem('sb_rich_mode') === '1'; }
2330
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function applyRichToggleStyle(btn, on) {
2331
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (on) {
2332
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.style.background = '#1f6feb';
2333
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.style.borderColor = '#1f6feb';
2334
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.style.color = '#fff';
2335
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.title = 'rich mode ON — click for text only';
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!
btn.style.background = '';
2338
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.style.borderColor = '';
2339
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.style.color = '#8b949e';
2340
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.title = 'text only — click for rich mode';
2341
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2342
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2343
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function toggleRichMode() {
2344
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const on = !isRichMode();
2345
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.setItem('sb_rich_mode', on ? '1' : '0');
2346
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const btn = document.getElementById('chat-rich-toggle');
2347
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
applyRichToggleStyle(btn, on);
2348
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Toggle all existing meta blocks visibility.
2349
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.querySelectorAll('.msg-meta-toggle').forEach(el => { el.style.display = on ? '' : 'none'; });
2350
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!on) document.querySelectorAll('.msg-meta.open').forEach(el => el.classList.remove('open'));
2351
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2352
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Initialize toggle button state on load.
2353
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
(function() {
2354
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
applyRichToggleStyle(document.getElementById('chat-rich-toggle'), isRichMode());
2355
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
})();
2356
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2357
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- meta renderers ---
2358
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderMeta(meta) {
2359
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!meta || !meta.type || !meta.data) return null;
2360
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
switch (meta.type) {
2361
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case 'tool_result': return renderToolResult(meta.data);
2362
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case 'diff': return renderDiff(meta.data);
2363
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case 'error': return renderError(meta.data);
2364
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case 'status': return renderStatus(meta.data);
2365
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case 'artifact': return renderArtifact(meta.data);
2366
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
case 'image': return renderImage(meta.data);
2367
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
default: return renderGeneric(meta);
2368
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2369
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2370
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderToolResult(d) {
2371
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const tool = (d.tool || '').toLowerCase();
2372
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Bash/exec: terminal card
2373
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (tool === 'bash' || tool === 'exec' || tool === 'execute' || d.command) {
2374
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return renderTerminal(d);
2375
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2376
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Edit/Write/apply_patch: file card with diff
2377
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (tool === 'edit' || tool === 'write' || tool === 'apply_patch' || tool === 'notebookedit') {
2378
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return renderFileCard(d);
2379
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2380
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Read/Glob/Grep: file browser
2381
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (tool === 'read' || tool === 'glob' || tool === 'grep') {
2382
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return renderFileSearch(d);
2383
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2384
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// WebSearch/WebFetch
2385
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (tool === 'websearch' || tool === 'webfetch' || tool === 'web_search' || d.url) {
2386
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return renderSearchCard(d);
2387
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2388
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Thinking
2389
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (tool === 'thinking' || d.thinking) {
2390
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return '<div class="rich-thinking">' + esc(d.result || d.thinking || '') + '</div>';
2391
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2392
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Fallback: generic tool card
2393
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return renderGenericTool(d);
2394
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2395
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderTerminal(d) {
2396
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const cmd = d.command || d.tool || 'command';
2397
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const exitOk = d.exit_code === undefined || d.exit_code === 0;
2398
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const exitCls = exitOk ? 'exit-ok' : 'exit-fail';
2399
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const exitText = d.exit_code !== undefined ? ` [exit ${d.exit_code}]` : '';
2400
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let output = d.result || d.output || '';
2401
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (output.length > 2000) output = output.slice(0, 1997) + '...';
2402
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div class="rich-card rich-terminal">
2403
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-header"><span class="cmd">$ ${esc(cmd)}</span><span class="${exitCls}">${exitText}</span></div>
2404
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-body"><pre>${esc(output)}</pre></div>
2405
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
2406
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2407
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderFileCard(d) {
2408
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const path = d.file || d.file_path || d.path || '?';
2409
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const ext = path.split('.').pop().toLowerCase();
2410
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const langMap = {go:'Go',js:'JS',ts:'TS',py:'Python',rb:'Ruby',rs:'Rust',java:'Java',md:'Markdown',yaml:'YAML',json:'JSON',html:'HTML',css:'CSS',sh:'Shell',sql:'SQL'};
2411
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const lang = langMap[ext] || ext;
2412
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const tool = d.tool || 'file';
2413
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let body = '';
2414
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (d.diff || d.hunks) {
2415
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
body = renderDiffBlock(d.diff || d.hunks);
2416
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else if (d.result) {
2417
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
body = `<pre>${esc(d.result.length > 2000 ? d.result.slice(0, 1997) + '...' : d.result)}</pre>`;
2418
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2419
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div class="rich-card rich-file">
2420
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-header"><span class="path">${esc(path)}</span><span class="lang">${esc(lang)}</span><span style="color:#8b949e;margin-left:auto">${esc(tool)}</span></div>
2421
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-body">${body}</div>
2422
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
2423
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2424
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderDiffBlock(raw) {
2425
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const text = typeof raw === 'string' ? raw : JSON.stringify(raw, null, 2);
2426
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const lines = text.split('\n').map(line => {
2427
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (line.startsWith('@@')) return `<span class="line-hdr">${esc(line)}</span>`;
2428
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
else if (line.startsWith('+')) return `<span class="line-add">${esc(line)}</span>`;
2429
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
else if (line.startsWith('-')) return `<span class="line-del">${esc(line)}</span>`;
2430
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
else return `<span class="line-ctx">${esc(line)}</span>`;
2431
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
});
2432
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<pre class="rich-diff">${lines.join('\n')}</pre>`;
2433
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2434
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderFileSearch(d) {
2435
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const path = d.file || d.pattern || d.path || '';
2436
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const tool = d.tool || 'search';
2437
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let body = d.result || '';
2438
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (body.length > 2000) body = body.slice(0, 1997) + '...';
2439
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div class="rich-card rich-file">
2440
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-header"><span class="path">${esc(path)}</span><span style="color:#8b949e;margin-left:auto">${esc(tool)}</span></div>
2441
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-body"><pre>${esc(body)}</pre></div>
2442
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
2443
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2444
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderSearchCard(d) {
2445
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const url = d.url || '';
2446
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const result = d.result || d.summary || '';
2447
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div class="rich-card rich-search">
2448
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-header"><span class="url">${esc(url)}</span></div>
2449
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-body"><pre>${esc(result.length > 2000 ? result.slice(0, 1997) + '...' : result)}</pre></div>
2450
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
2451
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2452
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderGenericTool(d) {
2453
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const tool = d.tool || '?';
2454
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let body = '';
2455
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (d.result) body = d.result;
2456
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
else body = JSON.stringify(d, null, 2);
2457
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (body.length > 2000) body = body.slice(0, 1997) + '...';
2458
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div class="rich-card rich-file">
2459
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-header"><span style="color:#d2a8ff;font-weight:600">${esc(tool)}</span></div>
2460
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-body"><pre>${esc(body)}</pre></div>
2461
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
2462
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2463
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderDiff(d) {
2464
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const file = d.file || '';
2465
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div class="rich-card rich-file">
2466
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-header"><span class="path">${esc(file)}</span><span style="color:#8b949e;margin-left:auto">diff</span></div>
2467
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-body">${renderDiffBlock(d.hunks || d.diff || '')}</div>
2468
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
2469
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2470
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderError(d) {
2471
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const msg = d.message || d.error || '';
2472
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const stack = d.stack || '';
2473
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div class="rich-card rich-error">
2474
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-header">error</div>
2475
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-body"><pre>${esc(msg)}${stack ? '\n\n' + esc(stack) : ''}</pre></div>
2476
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
2477
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2478
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderStatus(d) {
2479
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const state = (d.state || 'running').toLowerCase();
2480
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const cls = state === 'ok' || state === 'success' || state === 'done' ? 'ok' : state === 'error' || state === 'failed' ? 'error' : 'running';
2481
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let html = '<span class="meta-status ' + cls + '">' + esc(d.state || '') + '</span>';
2482
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (d.message) html += ' <span>' + esc(d.message) + '</span>';
2483
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return html;
2484
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2485
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderArtifact(d) {
2486
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const path = d.name || d.path || '?';
2487
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const ext = path.split('.').pop().toLowerCase();
2488
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const langMap = {go:'Go',js:'JS',ts:'TS',py:'Python',rb:'Ruby',rs:'Rust'};
2489
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const lang = d.language || langMap[ext] || ext;
2490
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div class="rich-card rich-file">
2491
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="rich-card-header"><span class="path">${esc(path)}</span><span class="lang">${esc(lang)}</span><span style="color:#8b949e;margin-left:auto">artifact</span></div>
2492
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
2493
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2494
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderImage(d) {
2495
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!d.url) return '';
2496
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div style="margin-top:4px"><img src="${esc(d.url)}" alt="${esc(d.alt || '')}" loading="lazy" style="max-width:100%;max-height:400px;border-radius:6px;cursor:pointer" onclick="window.open(this.src)"></div>`;
2497
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2498
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderGeneric(meta) {
2499
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return '<div class="meta-type">' + esc(meta.type) + '</div><pre>' + esc(JSON.stringify(meta.data, null, 2)) + '</pre>';
2500
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2501
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2502
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function sendMsg() {
2503
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!chatChannel) return;
2504
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const input = document.getElementById('chat-text-input');
2505
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const nick = document.getElementById('chat-identity').value.trim() || 'web';
2506
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const text = input.value.trim();
2507
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!text) return;
2508
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
input.disabled = true;
2509
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('chat-send-btn').disabled = true;
2510
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2511
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const slug = chatChannel.replace(/^#/,'');
2512
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('POST', `/v1/channels/${slug}/messages`, { text, nick });
2513
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
input.value = '';
2514
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { alert('Send failed: '+e.message); }
2515
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
finally { input.disabled=false; document.getElementById('chat-send-btn').disabled=false; input.focus(); }
2516
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2517
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- chat input: Enter to send, Tab for nick completion ---
2518
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
(function() {
2519
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const input = document.getElementById('chat-text-input');
2520
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _tabCandidates = [];
2521
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _tabIdx = -1;
2522
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _tabPrefix = '';
2523
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2524
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
input.addEventListener('keydown', e => {
2525
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (e.key === 'Enter') { e.preventDefault(); sendMsg(); return; }
2526
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (e.key === 'Tab') {
2527
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
e.preventDefault();
2528
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const val = input.value;
2529
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const cursor = input.selectionStart;
2530
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Find the word being typed up to cursor
2531
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const before = val.slice(0, cursor);
2532
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const wordStart = before.search(/\S+$/);
2533
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const word = wordStart === -1 ? '' : before.slice(wordStart);
2534
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!word) return;
2535
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2536
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// On first Tab press with this prefix, build candidate list
2537
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (_tabIdx === -1 || word.toLowerCase() !== _tabPrefix.toLowerCase()) {
2538
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_tabPrefix = word;
2539
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const nicks = Array.from(document.querySelectorAll('#nicklist-users .nicklist-nick'))
2540
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.map(el => el.textContent.replace(/^[●@+$]\s*/, '').trim())
2541
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
.filter(n => n.toLowerCase().startsWith(word.replace(/^@/, '').toLowerCase()));
2542
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!nicks.length) return;
2543
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_tabCandidates = nicks;
2544
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_tabIdx = 0;
2545
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
2546
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_tabIdx = (_tabIdx + 1) % _tabCandidates.length;
2547
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2548
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2549
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const chosen = _tabCandidates[_tabIdx];
2550
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// If at start of message, append ': '
2551
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const suffix = (wordStart === 0 && before.slice(0, wordStart) === '') ? ': ' : ' ';
2552
{ 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);
2553
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
input.value = newVal;
2554
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const newPos = (wordStart === -1 ? 0 : wordStart) + chosen.length + suffix.length;
2555
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
input.setSelectionRange(newPos, newPos);
2556
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return;
2557
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2558
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Any other key resets tab state
2559
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_tabIdx = -1;
2560
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_tabPrefix = '';
2561
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
});
2562
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
})();
2563
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2564
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- sidebar collapse toggles ---
2565
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function isMobile() { return window.matchMedia('(max-width: 600px)').matches; }
2566
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2567
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- chat highlights ---
2568
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const DEFAULT_DANGER_WORDS = ['rm -rf','git reset','git push --force','force push','drop table','delete from','git checkout .','git restore .','--no-verify'];
2569
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2570
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function promptHighlightWords() {
2571
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const current = getDangerWords().join(', ');
2572
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const input = prompt('Highlight keywords (comma-separated).\nMessages containing these words get a red border.\n\nCurrent:', current);
2573
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (input === null) return; // cancelled
2574
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const words = input.split(',').map(s => s.trim()).filter(Boolean);
2575
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
localStorage.setItem('sb_highlight_words', JSON.stringify(words));
2576
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2577
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2578
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function getDangerWords() {
2579
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2580
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const custom = JSON.parse(localStorage.getItem('sb_highlight_words') || 'null');
2581
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (Array.isArray(custom)) return custom.map(w => w.toLowerCase());
2582
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {}
2583
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return DEFAULT_DANGER_WORDS;
2584
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2585
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2586
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function getHighlightWords() {
2587
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const words = [];
2588
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const myNick = localStorage.getItem('sb_username') || '';
2589
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (myNick) words.push(myNick);
2590
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
words.push(...getDangerWords());
2591
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return words;
2592
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2593
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2594
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function highlightText(escaped) {
2595
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const words = getHighlightWords();
2596
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (words.length === 0) return escaped;
2597
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Build regex from all highlight words, longest first to avoid partial matches.
2598
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sorted = words.slice().sort((a, b) => b.length - a.length);
2599
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const pattern = sorted.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
2600
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!pattern) return escaped;
2601
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const re = new RegExp('(' + pattern + ')', 'gi');
2602
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return escaped.replace(re, '<span class="hl-word">$1</span>');
2603
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2604
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2605
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function toggleSidebar(side) {
2606
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (side === 'left') {
2607
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('chat-sidebar-left');
2608
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const btn = document.getElementById('sidebar-left-toggle');
2609
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (isMobile()) {
2610
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const open = el.classList.toggle('mobile-open');
2611
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = open ? '‹' : '›';
2612
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.title = open ? 'close' : 'channels';
2613
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// close other panel
2614
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('chat-nicklist').classList.remove('mobile-open');
2615
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
2616
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const collapsed = el.classList.toggle('collapsed');
2617
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = collapsed ? '›' : '‹';
2618
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.title = collapsed ? 'expand' : 'collapse';
2619
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2620
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
2621
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('chat-nicklist');
2622
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const btn = document.getElementById('sidebar-right-toggle');
2623
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (isMobile()) {
2624
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const open = el.classList.toggle('mobile-open');
2625
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = open ? '›' : '‹';
2626
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.title = open ? 'close' : 'users';
2627
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// close other panel
2628
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('chat-sidebar-left').classList.remove('mobile-open');
2629
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
2630
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const collapsed = el.classList.toggle('collapsed');
2631
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = collapsed ? '‹' : '›';
2632
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.title = collapsed ? 'expand' : 'collapse';
2633
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2634
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2635
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2636
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2637
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- sidebar drag-to-resize ---
2638
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
(function() {
2639
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function makeResizable(handleId, sidebarId, side) {
2640
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const handle = document.getElementById(handleId);
2641
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sidebar = document.getElementById(sidebarId);
2642
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!handle || !sidebar) return;
2643
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let startX, startW;
2644
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
handle.addEventListener('mousedown', e => {
2645
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
e.preventDefault();
2646
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
startX = e.clientX;
2647
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
startW = sidebar.offsetWidth;
2648
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
handle.classList.add('dragging');
2649
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const onMove = mv => {
2650
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const delta = side === 'left' ? mv.clientX - startX : startX - mv.clientX;
2651
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const w = Math.max(28, Math.min(400, startW + delta));
2652
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
sidebar.style.width = w + 'px';
2653
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (w <= 30) sidebar.classList.add('collapsed');
2654
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
else sidebar.classList.remove('collapsed');
2655
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
};
2656
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const onUp = () => {
2657
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
handle.classList.remove('dragging');
2658
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.removeEventListener('mousemove', onMove);
2659
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.removeEventListener('mouseup', onUp);
2660
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
};
2661
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.addEventListener('mousemove', onMove);
2662
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.addEventListener('mouseup', onUp);
2663
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
});
2664
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2665
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
makeResizable('resize-left', 'chat-sidebar-left', 'left');
2666
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
makeResizable('resize-right', 'chat-nicklist', 'right');
2667
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
})();
2668
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2669
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- helpers ---
2670
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderAlert(type, msg) {
2671
{ 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>`;
2672
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2673
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function esc(s) {
2674
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
2675
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2676
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function fmtTime(iso) {
2677
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!iso) return '—';
2678
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const d = new Date(iso);
2679
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return d.toLocaleDateString()+' '+d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
2680
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2681
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2682
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- collapsible cards ---
2683
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function toggleCard(id, e) {
2684
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Don't collapse when clicking buttons inside the header.
2685
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (e && e.target.tagName === 'BUTTON') return;
2686
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById(id).classList.toggle('collapsed');
2687
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2688
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2689
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- per-bot config schemas ---
2690
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const BEHAVIOR_SCHEMAS = {
2691
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
snitch: [
2692
{ 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' },
2693
{ 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)' },
2694
{ 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' },
2695
{ 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' },
2696
{ 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' },
2697
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'joinpart_window_s', label:'Join/part window (s)',type:'number', placeholder:'30' },
2698
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2699
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
warden: [
2700
{ 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' },
2701
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'window_s', label:'Window (s)', type:'number', placeholder:'5' },
2702
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'warn_before_mute', label:'Warn before mute', type:'checkbox' },
2703
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'mute_duration_s', label:'Mute duration (s)', type:'number', placeholder:'300' },
2704
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'kick_after_mutes', label:'Kick after N mutes',type:'number', placeholder:'3' },
2705
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2706
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
oracle: [
2707
{ 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' },
2708
{ 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)' },
2709
{ 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' },
2710
{ 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' },
2711
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2712
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
scribe: [
2713
{ 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' },
2714
{ 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' },
2715
{ 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' },
2716
{ 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' },
2717
{ 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' },
2718
{ 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' },
2719
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2720
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
herald: [
2721
{ 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' },
2722
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'rate_limit', label:'Rate limit (msg/min)', type:'number', placeholder:'60' },
2723
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2724
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
scroll: [
2725
{ 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' },
2726
{ 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' },
2727
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2728
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
systembot: [
2729
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'log_joins', label:'Log joins', type:'checkbox' },
2730
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'log_parts', label:'Log parts/quits', type:'checkbox' },
2731
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'log_modes', label:'Log mode changes', type:'checkbox' },
2732
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'log_kicks', label:'Log kicks', type:'checkbox' },
2733
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2734
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
auditbot: [
2735
{ 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' },
2736
{ 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' },
2737
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2738
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
sentinel: [
2739
{ 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' },
2740
{ 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)' },
2741
{ 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' },
2742
{ 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' },
2743
{ 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)' },
2744
{ 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' },
2745
{ 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' },
2746
{ 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' },
2747
{ 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' },
2748
{ 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' },
2749
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2750
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
steward: [
2751
{ 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' },
2752
{ 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' },
2753
{ 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' },
2754
{ 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' },
2755
{ 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' },
2756
{ 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' },
2757
{ 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' },
2758
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2759
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
shepherd: [
2760
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'backend', label:'LLM backend', type:'llm-backend', hint:'Backend for reasoning and plan generation' },
2761
{ 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' },
2762
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'report_channel', label:'Report channel', type:'text', placeholder:'#ops', hint:'Channel for status reports' },
2763
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'checkin_interval_sec', label:'Check-in interval (s)', type:'number', placeholder:'0', hint:'Seconds between automatic check-ins (0 = disabled)' },
2764
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
{ key:'goal_source', label:'Goal source URL', type:'text', placeholder:'https://github.com/org/repo/milestone/1', hint:'GitHub milestone URL or other goal source' },
2765
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
2766
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
};
2767
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2768
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderBehConfig(b) {
2769
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const schema = BEHAVIOR_SCHEMAS[b.id];
2770
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!schema) return '';
2771
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const cfg = b.config || {};
2772
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const fields = schema.map(f => {
2773
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const val = cfg[f.key];
2774
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let input = '';
2775
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (f.type === 'checkbox') {
2776
{ 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)">`;
2777
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else if (f.type === 'select') {
2778
{ 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>`;
2779
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else if (f.type === 'llm-backend') {
2780
{ 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('');
2781
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const noMatch = val && !_llmBackendNames.includes(val);
2782
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
input = `<select onchange="onBehCfg('${esc(b.id)}','${f.key}',this.value)">
2783
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="">— select backend —</option>
2784
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${opts}
2785
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${noMatch ? `<option value="${esc(val)}" selected>${esc(val)}</option>` : ''}
2786
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</select>`;
2787
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else if (f.type === 'model-override') {
2788
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const selId = `beh-msel-${esc(b.id)}`;
2789
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const customId = `beh-mcustom-${esc(b.id)}`;
2790
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const backendKey = f.backendKey || 'backend';
2791
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const currentVal = val || '';
2792
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
input = `<div style="display:flex;gap:6px;align-items:flex-start">
2793
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="flex:1">
2794
{ 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}')">
2795
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="">— none / auto-select —</option>
2796
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${currentVal ? `<option value="${esc(currentVal)}" selected>${esc(currentVal)}</option>` : ''}
2797
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<option value="__other__">— other (type below) —</option>
2798
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</select>
2799
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" id="${customId}" placeholder="model-id" autocomplete="off"
2800
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
style="display:none;margin-top:6px"
2801
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onchange="onBehCfg('${esc(b.id)}','${f.key}',this.value)">
2802
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
2803
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button type="button" class="sm" style="white-space:nowrap;margin-top:1px"
2804
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onclick="loadBehModels(this,'${esc(b.id)}','${backendKey}','${f.key}','${selId}','${customId}')">↺</button>
2805
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
2806
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
2807
{ 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':''})">`;
2808
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2809
{ 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>`;
2810
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}).join('');
2811
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<div class="beh-config" id="beh-cfg-${esc(b.id)}">${fields}</div>`;
2812
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2813
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2814
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function toggleBehConfig(id) {
2815
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('beh-cfg-' + id);
2816
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!el) return;
2817
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.classList.toggle('open');
2818
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const btn = document.getElementById('beh-cfg-btn-' + id);
2819
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (btn) btn.textContent = el.classList.contains('open') ? 'configure ▴' : 'configure ▾';
2820
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2821
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2822
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function onBehCfg(id, key, val) {
2823
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const b = currentPolicies.behaviors.find(x => x.id === id);
2824
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!b) return;
2825
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!b.config) b.config = {};
2826
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
b.config[key] = val;
2827
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2828
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2829
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function onBehModelSelect(botId, key, selId, customId) {
2830
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sel = document.getElementById(selId);
2831
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const custom = document.getElementById(customId);
2832
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!sel) return;
2833
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
custom.style.display = sel.value === '__other__' ? '' : 'none';
2834
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (sel.value !== '__other__') onBehCfg(botId, key, sel.value);
2835
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2836
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2837
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadBehModels(btn, botId, backendKey, modelKey, selId, customId) {
2838
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const b = currentPolicies && currentPolicies.behaviors.find(x => x.id === botId);
2839
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const backendName = b && b.config && b.config[backendKey];
2840
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!backendName) {
2841
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
alert('Select a backend first, then click ↺ to load its models.');
2842
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return;
2843
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2844
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const origText = btn.textContent;
2845
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = '…';
2846
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = true;
2847
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2848
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const models = await api('GET', `/v1/llm/backends/${encodeURIComponent(backendName)}/models`);
2849
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sel = document.getElementById(selId);
2850
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const custom = document.getElementById(customId);
2851
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!sel) return;
2852
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const current = (b.config && b.config[modelKey]) || '';
2853
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
sel.innerHTML = '<option value="">— none / auto-select —</option>';
2854
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
for (const m of (models || [])) {
2855
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const id = typeof m === 'string' ? m : m.id;
2856
{ 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;
2857
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const opt = document.createElement('option');
2858
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
opt.value = id;
2859
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
opt.textContent = label;
2860
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (id === current) opt.selected = true;
2861
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
sel.appendChild(opt);
2862
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2863
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const other = document.createElement('option');
2864
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
other.value = '__other__';
2865
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
other.textContent = '— other (type below) —';
2866
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const matched = (models || []).some(m => (typeof m === 'string' ? m : m.id) === current);
2867
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (current && !matched) {
2868
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
other.selected = true;
2869
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (custom) { custom.value = current; custom.style.display = ''; }
2870
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2871
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
sel.appendChild(other);
2872
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
2873
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
alert('Model discovery failed: ' + e.message);
2874
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} finally {
2875
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = origText;
2876
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = false;
2877
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2878
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2879
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2880
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- admin accounts ---
2881
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadAdmins() {
2882
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2883
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const data = await api('GET', '/v1/admins');
2884
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderAdmins(data.admins || []);
2885
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
2886
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// admins endpoint may not exist on token-only setups
2887
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('admins-list-container').innerHTML = '';
2888
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2889
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2890
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2891
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderAdmins(admins) {
2892
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('admins-list-container');
2893
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!admins.length) { el.innerHTML = ''; return; }
2894
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rows = admins.map(a => `<tr>
2895
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><strong>${esc(a.username)}</strong></td>
2896
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="color:#8b949e;font-size:12px">${fmtTime(a.created)}</td>
2897
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><div class="actions">
2898
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="promptAdminPassword('${esc(a.username)}')">change password</button>
2899
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm danger" onclick="removeAdmin('${esc(a.username)}')">remove</button>
2900
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div></td>
2901
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tr>`).join('');
2902
{ 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>`;
2903
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2904
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2905
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function addAdmin(e) {
2906
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
e.preventDefault();
2907
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const username = document.getElementById('new-admin-username').value.trim();
2908
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const password = document.getElementById('new-admin-password').value;
2909
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const resultEl = document.getElementById('add-admin-result');
2910
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!username || !password) return;
2911
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2912
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('POST', '/v1/admins', { username, password });
2913
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('success', `Admin <strong>${esc(username)}</strong> added.`);
2914
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
2915
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('add-admin-form').reset();
2916
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await loadAdmins();
2917
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
setTimeout(() => { resultEl.style.display = 'none'; }, 3000);
2918
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
2919
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('error', e.message);
2920
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
2921
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2922
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2923
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2924
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function removeAdmin(username) {
2925
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!confirm(`Remove admin "${username}"?`)) return;
2926
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2927
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('DELETE', `/v1/admins/${encodeURIComponent(username)}`);
2928
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await loadAdmins();
2929
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { alert('Remove failed: ' + e.message); }
2930
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2931
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2932
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function promptAdminPassword(username) {
2933
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const pw = prompt(`New password for ${username}:`);
2934
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!pw) return;
2935
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2936
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('PUT', `/v1/admins/${encodeURIComponent(username)}/password`, { password: pw });
2937
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
alert('Password updated.');
2938
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { alert('Failed: ' + e.message); }
2939
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2940
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2941
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- API keys ---
2942
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadAPIKeys() {
2943
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2944
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const keys = await api('GET', '/v1/api-keys');
2945
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderAPIKeys(keys || []);
2946
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
2947
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('apikeys-list-container').innerHTML = '';
2948
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2949
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2950
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2951
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderAPIKeys(keys) {
2952
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('apikeys-list-container');
2953
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!keys.length) { el.innerHTML = ''; return; }
2954
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rows = keys.map(k => {
2955
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const status = k.active ? '<span style="color:#3fb950">active</span>' : '<span style="color:#f85149">revoked</span>';
2956
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const scopes = (k.scopes || []).map(s => `<code style="font-size:11px;background:#21262d;padding:1px 5px;border-radius:3px">${esc(s)}</code>`).join(' ');
2957
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const lastUsed = k.last_used ? fmtTime(k.last_used) : '—';
2958
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const revokeBtn = k.active ? `<button class="sm danger" onclick="revokeAPIKey('${esc(k.id)}')">revoke</button>` : '';
2959
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return `<tr>
2960
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><strong>${esc(k.name)}</strong><br><span style="color:#8b949e;font-size:11px">${esc(k.id)}</span></td>
2961
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td>${scopes}</td>
2962
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="font-size:12px">${status}</td>
2963
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="color:#8b949e;font-size:12px">${lastUsed}</td>
2964
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td><div class="actions">${revokeBtn}</div></td>
2965
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tr>`;
2966
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}).join('');
2967
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = `<table><thead><tr><th>name</th><th>scopes</th><th>status</th><th>last used</th><th></th></tr></thead><tbody>${rows}</tbody></table>`;
2968
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2969
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2970
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function createAPIKey(e) {
2971
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
e.preventDefault();
2972
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const name = document.getElementById('new-apikey-name').value.trim();
2973
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const expires = document.getElementById('new-apikey-expires').value.trim();
2974
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const scopes = [...document.querySelectorAll('.apikey-scope:checked')].map(cb => cb.value);
2975
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const resultEl = document.getElementById('add-apikey-result');
2976
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!name) { resultEl.innerHTML = '<span style="color:#f85149">name is required</span>'; return; }
2977
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!scopes.length) { resultEl.innerHTML = '<span style="color:#f85149">select at least one scope</span>'; return; }
2978
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2979
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const body = { name, scopes };
2980
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (expires) body.expires_in = expires;
2981
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const result = await api('POST', '/v1/api-keys', body);
2982
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = `<div style="background:#0d1117;border:1px solid #3fb95044;border-radius:6px;padding:12px;margin-top:8px">
2983
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="color:#3fb950;font-weight:600;margin-bottom:6px">Key created: ${esc(result.name)}</div>
2984
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="margin-bottom:4px;font-size:12px;color:#8b949e">Copy this token now — it will not be shown again:</div>
2985
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<code style="display:block;padding:8px;background:#161b22;border-radius:4px;word-break:break-all;user-select:all">${esc(result.token)}</code>
2986
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
2987
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('new-apikey-name').value = '';
2988
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('new-apikey-expires').value = '';
2989
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.querySelectorAll('.apikey-scope:checked').forEach(cb => cb.checked = false);
2990
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadAPIKeys();
2991
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
2992
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = `<span style="color:#f85149">${esc(e.message)}</span>`;
2993
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2994
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
2995
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
2996
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function revokeAPIKey(id) {
2997
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!confirm('Revoke this API key? This cannot be undone.')) return;
2998
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
2999
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('DELETE', `/v1/api-keys/${encodeURIComponent(id)}`);
3000
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadAPIKeys();
3001
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { alert('Failed: ' + e.message); }
3002
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3003
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3004
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- AI / LLM tab ---
3005
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadAI() {
3006
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await Promise.all([loadAIBackends(), loadAIKnown()]);
3007
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3008
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3009
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadAIBackends() {
3010
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('ai-backends-list');
3011
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3012
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const backends = await api('GET', '/v1/llm/backends');
3013
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!backends || backends.length === 0) {
3014
{ 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>';
3015
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return;
3016
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3017
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = backends.map(b => {
3018
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sid = CSS.escape(b.name);
3019
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const editable = b.source === 'policy';
3020
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const srcBadge = b.source === 'config'
3021
{ 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>'
3022
{ 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>';
3023
{ 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">
3024
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="flex:1;min-width:140px">
3025
{ 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>
3026
{ 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>
3027
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
3028
{ 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>
3029
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div id="ai-models-${sid}" style="width:100%;display:none"></div>
3030
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="discoverModels('${esc(b.name)}', this)">discover models</button>
3031
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${editable ? `<button class="sm" onclick="openEditBackend('${esc(b.name)}')">edit</button>
3032
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm danger" onclick="deleteBackend('${esc(b.name)}', this)">delete</button>` : ''}
3033
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
3034
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}).join('<div style="height:1px;background:#21262d;margin:4px 0"></div>');
3035
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3036
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = `<div class="empty-state" style="color:#f85149">${esc(String(e))}</div>`;
3037
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3038
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3039
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3040
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- backend form ---
3041
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3042
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _editingBackend = null; // null = adding, string = name being edited
3043
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _backendList = []; // cached for edit lookups
3044
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3045
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function openAddBackend() {
3046
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_editingBackend = null;
3047
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ai-form-title').textContent = 'add backend';
3048
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-submit-btn').textContent = 'add backend';
3049
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-name').value = '';
3050
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-name').disabled = false;
3051
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-backend').value = '';
3052
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-apikey').value = '';
3053
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-baseurl').value = '';
3054
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
populateModelSelect([], '');
3055
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-model-custom').value = '';
3056
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-model-custom').style.display = 'none';
3057
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-load-models-btn').textContent = '↺ load models';
3058
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-default').checked = false;
3059
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-region').value = '';
3060
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-aws-key-id').value = '';
3061
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-aws-secret').value = '';
3062
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-allow').value = '';
3063
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-block').value = '';
3064
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ai-form-result').style.display = 'none';
3065
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onBackendTypeChange();
3066
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const card = document.getElementById('card-ai-form');
3067
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
card.style.display = '';
3068
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
3069
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3070
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3071
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function openEditBackend(name) {
3072
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let b;
3073
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3074
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const backends = await api('GET', '/v1/llm/backends');
3075
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
b = backends.find(x => x.name === name);
3076
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) { return; }
3077
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!b) return;
3078
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3079
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_editingBackend = name;
3080
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ai-form-title').textContent = 'edit backend — ' + esc(name);
3081
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-submit-btn').textContent = 'save changes';
3082
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-name').value = name;
3083
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-name').disabled = false; // allow rename
3084
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-backend').value = b.backend || '';
3085
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-apikey').value = ''; // never pre-fill secrets
3086
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-baseurl').value = b.base_url || '';
3087
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const curated = KNOWN_MODELS[b.backend] || [];
3088
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
populateModelSelect(curated, b.model || '');
3089
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-model-custom').style.display = 'none';
3090
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-load-models-btn').textContent = '↺ load models';
3091
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-default').checked = !!b.default;
3092
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-region').value = b.region || '';
3093
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-aws-key-id').value = ''; // never pre-fill
3094
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-aws-secret').value = '';
3095
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-allow').value = (b.allow || []).join('\n');
3096
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-block').value = (b.block || []).join('\n');
3097
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ai-form-result').style.display = 'none';
3098
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onBackendTypeChange();
3099
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const card = document.getElementById('card-ai-form');
3100
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
card.style.display = '';
3101
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
3102
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3103
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3104
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function closeBackendForm() {
3105
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('card-ai-form').style.display = 'none';
3106
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_editingBackend = null;
3107
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3108
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3109
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Curated model lists per backend — shown before live discovery.
3110
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const KNOWN_MODELS = {
3111
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
anthropic: [
3112
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001',
3113
{ 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',
3114
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
3115
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
gemini: [
3116
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-flash',
3117
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'gemini-1.5-flash-8b', 'gemini-1.5-pro',
3118
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
3119
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
openai: [
3120
{ 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',
3121
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
3122
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
bedrock: [
3123
{ 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',
3124
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'anthropic.claude-3-opus-20240229-v1:0',
3125
{ 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',
3126
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'meta.llama3-70b-instruct-v1:0', 'meta.llama3-8b-instruct-v1:0',
3127
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'mistral.mistral-large-2402-v1:0',
3128
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
3129
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
ollama: ['llama3.2', 'llama3.1', 'mistral', 'codellama', 'gemma2', 'qwen2.5', 'phi3'],
3130
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
groq: [
3131
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'llama-3.3-70b-versatile', 'llama-3.1-8b-instant',
3132
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'mixtral-8x7b-32768', 'gemma2-9b-it',
3133
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
3134
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
mistral: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest', 'open-mistral-nemo'],
3135
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
deepseek: ['deepseek-chat', 'deepseek-reasoner'],
3136
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
xai: ['grok-2', 'grok-2-mini', 'grok-beta'],
3137
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
cerebras: ['llama3.1-8b', 'llama3.1-70b', 'llama3.3-70b'],
3138
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
together: [
3139
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'meta-llama/Llama-3.3-70B-Instruct-Turbo',
3140
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'meta-llama/Llama-3.1-8B-Instruct-Turbo',
3141
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'mistralai/Mixtral-8x7B-Instruct-v0.1',
3142
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'Qwen/Qwen2.5-72B-Instruct-Turbo',
3143
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
3144
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
fireworks: [
3145
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'accounts/fireworks/models/llama-v3p3-70b-instruct',
3146
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'accounts/fireworks/models/mixtral-8x7b-instruct',
3147
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
3148
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
openrouter: [], // too varied — always load live
3149
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
huggingface: [
3150
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'meta-llama/Llama-3.3-70B-Instruct',
3151
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'mistralai/Mistral-7B-Instruct-v0.3',
3152
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
'Qwen/Qwen2.5-72B-Instruct',
3153
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
],
3154
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
};
3155
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3156
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function populateModelSelect(models, currentVal) {
3157
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sel = document.getElementById('bf-model-select');
3158
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
sel.innerHTML = '<option value="">— none / auto-select —</option>';
3159
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
for (const m of models) {
3160
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const id = typeof m === 'string' ? m : m.id;
3161
{ 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;
3162
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const opt = document.createElement('option');
3163
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
opt.value = id;
3164
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
opt.textContent = label;
3165
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (id === currentVal) opt.selected = true;
3166
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
sel.appendChild(opt);
3167
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3168
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const other = document.createElement('option');
3169
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
other.value = '__other__';
3170
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
other.textContent = '— other (type below) —';
3171
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (currentVal && !models.find(m => (typeof m === 'string' ? m : m.id) === currentVal)) {
3172
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
other.selected = true;
3173
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-model-custom').value = currentVal;
3174
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-model-custom').style.display = '';
3175
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3176
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
sel.appendChild(other);
3177
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3178
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3179
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function onModelSelectChange() {
3180
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sel = document.getElementById('bf-model-select');
3181
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const custom = document.getElementById('bf-model-custom');
3182
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
custom.style.display = sel.value === '__other__' ? '' : 'none';
3183
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3184
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3185
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function getModelValue() {
3186
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const sel = document.getElementById('bf-model-select');
3187
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (sel.value === '__other__') return document.getElementById('bf-model-custom').value.trim();
3188
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return sel.value || '';
3189
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3190
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3191
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function onBackendTypeChange() {
3192
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const t = document.getElementById('bf-backend').value;
3193
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const isBedrock = t === 'bedrock';
3194
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const isLocal = ['ollama','litellm','lmstudio','jan','localai','vllm','anythingllm'].includes(t);
3195
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const hasKey = !isBedrock;
3196
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3197
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-bedrock-group').style.display = isBedrock ? '' : 'none';
3198
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-apikey-row').style.display = hasKey ? '' : 'none';
3199
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bf-baseurl-row').style.display = (isLocal || isBedrock) ? 'none' : '';
3200
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3201
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const curated = KNOWN_MODELS[t] || [];
3202
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
populateModelSelect(curated, '');
3203
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3204
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3205
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadLiveModels(btn) {
3206
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const t = document.getElementById('bf-backend').value;
3207
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!t) { alert('Select a backend type first.'); return; }
3208
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3209
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = true;
3210
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = 'loading…';
3211
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3212
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const payload = {
3213
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
backend: t,
3214
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
api_key: document.getElementById('bf-apikey')?.value || '',
3215
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
base_url: document.getElementById('bf-baseurl')?.value.trim() || '',
3216
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
region: document.getElementById('bf-region')?.value.trim() || '',
3217
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
aws_key_id: document.getElementById('bf-aws-key-id')?.value.trim() || '',
3218
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
aws_secret_key: document.getElementById('bf-aws-secret')?.value || '',
3219
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
};
3220
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const models = await api('POST', '/v1/llm/discover', payload);
3221
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const current = getModelValue();
3222
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
populateModelSelect(models, current);
3223
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = `↺ ${models.length} loaded`;
3224
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3225
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = '✕ failed';
3226
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
setTimeout(() => { btn.textContent = '↺ load models'; }, 2000);
3227
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
alert('Discovery failed: ' + String(e));
3228
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} finally {
3229
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = false;
3230
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3231
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3232
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3233
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function submitBackendForm() {
3234
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const name = document.getElementById('bf-name').value.trim();
3235
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const backend = document.getElementById('bf-backend').value;
3236
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!name || !backend) {
3237
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
showFormResult('name and backend type are required', 'error');
3238
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return;
3239
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3240
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3241
{ 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);
3242
{ 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);
3243
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3244
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const payload = {
3245
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
name, backend,
3246
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
api_key: document.getElementById('bf-apikey').value || undefined,
3247
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
base_url: document.getElementById('bf-baseurl').value.trim() || undefined,
3248
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
model: getModelValue() || undefined,
3249
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
default: document.getElementById('bf-default').checked || undefined,
3250
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
region: document.getElementById('bf-region').value.trim() || undefined,
3251
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
aws_key_id: document.getElementById('bf-aws-key-id').value.trim() || undefined,
3252
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
aws_secret_key: document.getElementById('bf-aws-secret').value || undefined,
3253
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
allow: allow.length ? allow : undefined,
3254
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
block: block.length ? block : undefined,
3255
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
};
3256
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3257
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const btn = document.getElementById('bf-submit-btn');
3258
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = true;
3259
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3260
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (_editingBackend && name !== _editingBackend) {
3261
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Rename: delete old, create new.
3262
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('DELETE', `/v1/llm/backends/${encodeURIComponent(_editingBackend)}`);
3263
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('POST', '/v1/llm/backends', payload);
3264
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else if (_editingBackend) {
3265
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('PUT', `/v1/llm/backends/${encodeURIComponent(_editingBackend)}`, payload);
3266
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
3267
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('POST', '/v1/llm/backends', payload);
3268
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3269
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
closeBackendForm();
3270
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await loadAIBackends();
3271
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3272
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
showFormResult(String(e), 'error');
3273
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} finally {
3274
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = false;
3275
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3276
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3277
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3278
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function deleteBackend(name, btn) {
3279
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = true;
3280
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3281
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await api('DELETE', `/v1/llm/backends/${encodeURIComponent(name)}`);
3282
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
await loadAIBackends();
3283
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3284
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = false;
3285
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
alert('Delete failed: ' + String(e));
3286
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3287
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3288
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3289
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function showFormResult(msg, type) {
3290
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('ai-form-result');
3291
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.style.display = '';
3292
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.className = 'alert ' + (type === 'error' ? 'danger' : 'info');
3293
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = `<span class="icon">${type === 'error' ? '✕' : 'ℹ'}</span><span>${esc(msg)}</span>`;
3294
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3295
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3296
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function discoverModels(name, btn) {
3297
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('ai-models-' + name);
3298
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!el) return;
3299
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = true;
3300
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = 'discovering…';
3301
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3302
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const models = await api('GET', `/v1/llm/backends/${encodeURIComponent(name)}/models`);
3303
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.style.display = 'block';
3304
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!models || models.length === 0) {
3305
{ 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>';
3306
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
3307
{ 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>
3308
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;flex-wrap:wrap;gap:4px">${models.map(m =>
3309
{ 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>`
3310
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
).join('')}</div>`;
3311
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3312
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = '↺ refresh';
3313
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3314
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.style.display = 'block';
3315
{ 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>`;
3316
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.textContent = 'retry';
3317
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} finally {
3318
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
btn.disabled = false;
3319
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3320
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3321
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3322
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadAIKnown() {
3323
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('ai-supported-list');
3324
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3325
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const known = await api('GET', '/v1/llm/known');
3326
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const native = known.filter(b => b.native);
3327
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const compat = known.filter(b => !b.native);
3328
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
native.sort((a,b) => a.name.localeCompare(b.name));
3329
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
compat.sort((a,b) => a.name.localeCompare(b.name));
3330
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = `
3331
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="margin-bottom:12px">
3332
{ 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>
3333
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;flex-wrap:wrap;gap:4px">${native.map(b =>
3334
{ 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>`
3335
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
).join('')}</div>
3336
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
3337
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
3338
{ 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>
3339
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;flex-wrap:wrap;gap:4px">${compat.map(b =>
3340
{ 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>`
3341
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
).join('')}</div>
3342
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>`;
3343
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3344
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = `<div class="empty-state" style="color:#f85149">${esc(String(e))}</div>`;
3345
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3346
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3347
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3348
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function showAIExample(e) {
3349
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
e.preventDefault();
3350
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const card = document.getElementById('card-ai-example');
3351
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
card.style.display = '';
3352
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
3353
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Expand it if collapsed.
3354
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const body = card.querySelector('.card-body');
3355
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (body) body.style.display = '';
3356
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3357
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3358
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- settings / policies ---
3359
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let currentPolicies = null;
3360
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _botCommands = {};
3361
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3362
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderOnJoinMessages(msgs) {
3363
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('onjoin-list');
3364
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!msgs || !Object.keys(msgs).length) { el.innerHTML = '<div style="color:#8b949e;font-size:12px">No on-join instructions configured.</div>'; return; }
3365
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = Object.entries(msgs).sort().map(([ch, msg]) => `
3366
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;gap:8px;align-items:center;padding:6px 0;border-bottom:1px solid #21262d">
3367
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<code style="font-size:12px;min-width:120px">${esc(ch)}</code>
3368
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" value="${esc(msg)}" style="flex:1;font-size:12px" onchange="updateOnJoinMessage('${esc(ch)}',this.value)">
3369
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm danger" onclick="removeOnJoinMessage('${esc(ch)}')">remove</button>
3370
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
3371
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`).join('');
3372
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3373
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function addOnJoinMessage() {
3374
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const ch = document.getElementById('onjoin-new-channel').value.trim();
3375
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const msg = document.getElementById('onjoin-new-message').value.trim();
3376
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!ch || !msg) return;
3377
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!currentPolicies.on_join_messages) currentPolicies.on_join_messages = {};
3378
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
currentPolicies.on_join_messages[ch] = msg;
3379
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('onjoin-new-channel').value = '';
3380
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('onjoin-new-message').value = '';
3381
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderOnJoinMessages(currentPolicies.on_join_messages);
3382
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3383
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function updateOnJoinMessage(ch, msg) {
3384
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!currentPolicies.on_join_messages) currentPolicies.on_join_messages = {};
3385
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
currentPolicies.on_join_messages[ch] = msg;
3386
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3387
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function removeOnJoinMessage(ch) {
3388
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (currentPolicies.on_join_messages) delete currentPolicies.on_join_messages[ch];
3389
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderOnJoinMessages(currentPolicies.on_join_messages);
3390
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3391
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _llmBackendNames = []; // cached backend names for oracle dropdown
3392
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3393
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadSettings() {
3394
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3395
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const [s, backends] = await Promise.all([
3396
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
api('GET', '/v1/settings'),
3397
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
api('GET', '/v1/llm/backends').catch(() => []),
3398
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
]);
3399
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_llmBackendNames = (backends || []).map(b => b.name);
3400
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTLSStatus(s.tls);
3401
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
currentPolicies = s.policies;
3402
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_botCommands = s.bot_commands || {};
3403
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderBehaviors(s.policies.behaviors || []);
3404
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderOnJoinMessages(s.policies.on_join_messages || {});
3405
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderAgentPolicy(s.policies.agent_policy || {});
3406
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderBridgePolicy(s.policies.bridge || {});
3407
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderLoggingPolicy(s.policies.logging || {});
3408
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadAdmins();
3409
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadAPIKeys();
3410
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
loadConfigCards();
3411
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3412
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('tls-badge').textContent = 'error';
3413
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3414
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3415
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3416
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderTLSStatus(tls) {
3417
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const badge = document.getElementById('tls-badge');
3418
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (tls.enabled) {
3419
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
badge.textContent = 'TLS active';
3420
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
badge.style.background = '#3fb95022'; badge.style.color = '#3fb950'; badge.style.borderColor = '#3fb95044';
3421
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} else {
3422
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
badge.textContent = 'HTTP only';
3423
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3424
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('tls-status-rows').innerHTML = `
3425
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
3426
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">mode</div>
3427
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc"></div>
3428
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<code class="setting-val">${tls.enabled ? 'HTTPS (Let\'s Encrypt)' : 'HTTP'}</code>
3429
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
3430
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${tls.enabled ? `
3431
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
3432
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">domain</div>
3433
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc"></div>
3434
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<code class="setting-val">${esc(tls.domain)}</code>
3435
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
3436
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-row">
3437
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-label">allow insecure</div>
3438
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div class="setting-desc">Plain HTTP also accepted.</div>
3439
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<code class="setting-val">${tls.allow_insecure ? 'yes' : 'no'}</code>
3440
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>` : ''}
3441
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`;
3442
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3443
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3444
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderBehaviors(behaviors) {
3445
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const hasSchema = id => !!BEHAVIOR_SCHEMAS[id];
3446
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('behaviors-list').innerHTML = behaviors.map(b => `
3447
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div>
3448
{ 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">
3449
{ 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">
3450
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<strong style="font-size:13px;white-space:nowrap">${esc(b.name)}</strong>
3451
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:12px;color:#8b949e">${esc(b.description)}</span>
3452
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<div style="display:flex;align-items:center;gap:8px;justify-content:flex-end">
3453
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${b.enabled ? `
3454
{ 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">
3455
{ 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">
3456
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
all channels
3457
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</label>
3458
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${b.join_all_channels
3459
{ 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)">`
3460
{ 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)">`
3461
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3462
{ 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>` : ''}
3463
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
` : ''}
3464
{ 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>
3465
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
3466
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
3467
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${b.enabled && hasSchema(b.id) ? renderBehConfig(b) : ''}
3468
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${_botCommands[b.id] ? `<div style="padding:6px 16px 8px 42px;border-bottom:1px solid #21262d;background:#0d1117">
3469
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<span style="font-size:11px;color:#8b949e;font-weight:600">commands:</span>
3470
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${_botCommands[b.id].map(c => `<code style="font-size:11px;margin-left:8px;background:#161b22;padding:1px 5px;border-radius:3px" title="${esc(c.description)} ${esc(c.usage)}">${esc(c.command)}</code>`).join('')}
3471
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>` : ''}
3472
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</div>
3473
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`).join('');
3474
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3475
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3476
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function onBehaviorToggle(id, enabled) {
3477
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const b = currentPolicies.behaviors.find(x => x.id === id);
3478
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (b) b.enabled = enabled;
3479
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderBehaviors(currentPolicies.behaviors);
3480
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3481
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function onBehaviorJoinAll(id, val) {
3482
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const b = currentPolicies.behaviors.find(x => x.id === id);
3483
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (b) b.join_all_channels = val;
3484
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderBehaviors(currentPolicies.behaviors);
3485
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3486
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function onBehaviorExclude(id, val) {
3487
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const b = currentPolicies.behaviors.find(x => x.id === id);
3488
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (b) b.exclude_channels = val.split(',').map(s=>s.trim()).filter(Boolean);
3489
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3490
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function onBehaviorChannels(id, val) {
3491
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const b = currentPolicies.behaviors.find(x => x.id === id);
3492
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (b) b.required_channels = val.split(',').map(s=>s.trim()).filter(Boolean);
3493
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3494
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3495
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderAgentPolicy(p) {
3496
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-checkin-enabled').checked = !!p.require_checkin;
3497
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-checkin-channel').value = p.checkin_channel || '';
3498
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-required-channels').value = (p.required_channels||[]).join(', ');
3499
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-online-timeout').value = p.online_timeout_secs || '';
3500
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-reap-days').value = p.reap_after_days || '';
3501
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
toggleCheckinChannel();
3502
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3503
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function toggleCheckinChannel() {
3504
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const on = document.getElementById('policy-checkin-enabled').checked;
3505
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-checkin-row').style.display = on ? '' : 'none';
3506
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3507
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3508
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderBridgePolicy(p) {
3509
{ 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;
3510
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3511
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3512
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderLoggingPolicy(l) {
3513
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-logging-enabled').checked = !!l.enabled;
3514
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-log-dir').value = l.dir || '';
3515
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-log-format').value = l.format || 'jsonl';
3516
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-log-rotation').value = l.rotation || 'none';
3517
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-log-max-size').value = l.max_size_mb || '';
3518
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-log-per-channel').checked = !!l.per_channel;
3519
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-log-max-age').value = l.max_age_days || '';
3520
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
toggleLogOptions();
3521
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
toggleRotationOptions();
3522
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3523
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function toggleLogOptions() {
3524
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const on = document.getElementById('policy-logging-enabled').checked;
3525
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-log-options').style.display = on ? '' : 'none';
3526
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3527
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function toggleRotationOptions() {
3528
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rot = document.getElementById('policy-log-rotation').value;
3529
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-log-size-row').style.display = rot === 'size' ? '' : 'none';
3530
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3531
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3532
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function savePolicies() {
3533
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// Saves behaviors only — agent_policy, logging, and bridge are now
3534
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// persisted to scuttlebot.yaml via PUT /v1/config.
3535
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!currentPolicies) return;
3536
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const p = JSON.parse(JSON.stringify(currentPolicies)); // deep copy
3537
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const resultEl = document.getElementById('policies-save-result');
3538
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3539
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
currentPolicies = await api('PUT', '/v1/settings/policies', p);
3540
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
3541
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('success', 'Behaviors saved.');
3542
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
setTimeout(() => { resultEl.style.display = 'none'; }, 3000);
3543
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3544
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
3545
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('error', e.message);
3546
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3547
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3548
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3549
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- topology config ---
3550
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _topoChannels = [];
3551
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let _topoTypes = [];
3552
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3553
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderTopoStaticChannels() {
3554
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('topo-static-channels');
3555
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!_topoChannels.length) {
3556
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = '<div class="empty-state" style="padding:12px;font-size:12px">no static channels configured</div>';
3557
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return;
3558
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3559
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = `
3560
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<table style="width:100%;font-size:12px;border-collapse:collapse">
3561
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<thead>
3562
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<tr style="color:#8b949e;border-bottom:1px solid #30363d">
3563
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<th style="text-align:left;padding:4px 8px;font-weight:500">name</th>
3564
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<th style="text-align:left;padding:4px 8px;font-weight:500">topic</th>
3565
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<th style="text-align:left;padding:4px 8px;font-weight:500">autojoin</th>
3566
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<th style="padding:4px 8px"></th>
3567
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tr>
3568
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</thead>
3569
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<tbody>
3570
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${_topoChannels.map((c, i) => `
3571
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<tr style="border-bottom:1px solid #21262d">
3572
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="padding:4px 8px">
3573
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" value="${esc(c.name||'')}" style="width:120px;padding:2px 6px;font-size:11px"
3574
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onchange="_topoChannels[${i}].name=this.value">
3575
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</td>
3576
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="padding:4px 8px">
3577
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" value="${esc(c.topic||'')}" style="width:160px;padding:2px 6px;font-size:11px"
3578
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onchange="_topoChannels[${i}].topic=this.value">
3579
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</td>
3580
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="padding:4px 8px">
3581
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" value="${esc((c.autojoin||[]).join(', '))}" placeholder="bridge, sentinel"
3582
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
style="width:160px;padding:2px 6px;font-size:11px"
3583
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onchange="_topoChannels[${i}].autojoin=this.value.split(',').map(s=>s.trim()).filter(Boolean)">
3584
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</td>
3585
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="padding:4px 8px;text-align:right">
3586
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="topoDeleteStaticChannel(${i})">✕</button>
3587
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</td>
3588
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tr>
3589
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`).join('')}
3590
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tbody>
3591
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</table>`;
3592
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3593
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3594
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function renderTopoChannelTypes() {
3595
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const el = document.getElementById('topo-channel-types');
3596
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (!_topoTypes.length) {
3597
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = '<div class="empty-state" style="padding:12px;font-size:12px">no channel types configured</div>';
3598
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
return;
3599
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3600
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
el.innerHTML = `
3601
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<table style="width:100%;font-size:12px;border-collapse:collapse">
3602
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<thead>
3603
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<tr style="color:#8b949e;border-bottom:1px solid #30363d">
3604
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<th style="text-align:left;padding:4px 8px;font-weight:500">name</th>
3605
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<th style="text-align:left;padding:4px 8px;font-weight:500">prefix</th>
3606
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<th style="text-align:left;padding:4px 8px;font-weight:500">ttl</th>
3607
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<th style="text-align:left;padding:4px 8px;font-weight:500">ephemeral</th>
3608
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<th style="padding:4px 8px"></th>
3609
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tr>
3610
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</thead>
3611
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<tbody>
3612
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
${_topoTypes.map((x, i) => `
3613
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<tr style="border-bottom:1px solid #21262d">
3614
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="padding:4px 8px">
3615
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" value="${esc(x.name||'')}" style="width:100px;padding:2px 6px;font-size:11px"
3616
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onchange="_topoTypes[${i}].name=this.value">
3617
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</td>
3618
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="padding:4px 8px">
3619
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" value="${esc(x.prefix||'')}" placeholder="task." style="width:100px;padding:2px 6px;font-size:11px"
3620
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onchange="_topoTypes[${i}].prefix=this.value">
3621
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</td>
3622
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="padding:4px 8px">
3623
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="text" value="${esc(x.ttl||'')}" placeholder="24h" style="width:80px;padding:2px 6px;font-size:11px"
3624
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
title="Duration string e.g. 1h, 24h, 72h"
3625
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onchange="_topoTypes[${i}].ttl=this.value">
3626
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</td>
3627
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="padding:4px 8px;text-align:center">
3628
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<input type="checkbox" ${x.ephemeral ? 'checked' : ''} style="width:auto"
3629
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
onchange="_topoTypes[${i}].ephemeral=this.checked">
3630
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</td>
3631
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<td style="padding:4px 8px;text-align:right">
3632
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
<button class="sm" onclick="topoDeleteChannelType(${i})">✕</button>
3633
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</td>
3634
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tr>
3635
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
`).join('')}
3636
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</tbody>
3637
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</table>`;
3638
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3639
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3640
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function topoAddStaticChannel() {
3641
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_topoChannels.push({name: '', topic: '', autojoin: []});
3642
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTopoStaticChannels();
3643
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// focus the last name input
3644
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rows = document.querySelectorAll('#topo-static-channels tbody tr');
3645
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (rows.length) rows[rows.length-1].querySelector('input').focus();
3646
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3647
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3648
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function topoAddChannelType() {
3649
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_topoTypes.push({name: '', prefix: '', ephemeral: false, ttl: ''});
3650
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTopoChannelTypes();
3651
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const rows = document.querySelectorAll('#topo-channel-types tbody tr');
3652
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (rows.length) rows[rows.length-1].querySelector('input').focus();
3653
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3654
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3655
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function topoDeleteStaticChannel(idx) {
3656
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_topoChannels.splice(idx, 1);
3657
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTopoStaticChannels();
3658
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3659
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3660
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function topoDeleteChannelType(idx) {
3661
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_topoTypes.splice(idx, 1);
3662
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTopoChannelTypes();
3663
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3664
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3665
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function saveTopologyConfig() {
3666
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const resultEl = document.getElementById('topo-save-result');
3667
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const channels = _topoChannels.filter(c => c.name.trim());
3668
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const types = _topoTypes.filter(x => x.name.trim() && x.prefix.trim());
3669
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const payload = {
3670
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
topology: {
3671
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
nick: document.getElementById('topo-nick').value.trim() || 'topology',
3672
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
channels: channels,
3673
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
types: types,
3674
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
},
3675
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
config_history: {
3676
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
keep: parseInt(document.getElementById('topo-history-keep').value, 10) || 20,
3677
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
},
3678
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
};
3679
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3680
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const res = await api('PUT', '/v1/config', payload);
3681
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
3682
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let msg = 'Topology config saved.';
3683
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (res.restart_required && res.restart_required.length) {
3684
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
msg += ' Restart required for: ' + res.restart_required.join(', ') + '.';
3685
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3686
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('success', msg);
3687
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
setTimeout(() => { resultEl.style.display = 'none'; }, 4000);
3688
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3689
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
3690
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('error', e.message);
3691
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3692
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3693
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3694
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- shared config save helper ---
3695
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function saveConfigPatch(patch, resultElId) {
3696
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const resultEl = document.getElementById(resultElId);
3697
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3698
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const res = await api('PUT', '/v1/config', patch);
3699
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
let msg = 'Saved.';
3700
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (res.restart_required && res.restart_required.length) {
3701
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
msg += ' Restart required for: ' + res.restart_required.join(', ') + '.';
3702
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3703
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
3704
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('success', msg);
3705
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
setTimeout(() => { resultEl.style.display = 'none'; }, 4000);
3706
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3707
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.style.display = 'block';
3708
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
resultEl.innerHTML = renderAlert('error', e.message);
3709
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3710
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3711
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3712
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- config-backed cards ---
3713
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
async function loadConfigCards() {
3714
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
try {
3715
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const cfg = await api('GET', '/v1/config');
3716
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// general
3717
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('general-api-addr').value = cfg.api_addr || '';
3718
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('general-mcp-addr').value = cfg.mcp_addr || '';
3719
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// ergo
3720
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const e = cfg.ergo || {};
3721
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ergo-network-name').value = e.network_name || '';
3722
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ergo-server-name').value = e.server_name || '';
3723
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ergo-irc-addr').value = e.irc_addr || '';
3724
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ergo-require-sasl').checked = !!e.require_sasl;
3725
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ergo-default-modes').value = e.default_channel_modes || '';
3726
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ergo-history-enabled').checked = !!(e.history && e.history.enabled);
3727
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('ergo-external').checked = !!e.external;
3728
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// tls
3729
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const t = cfg.tls || {};
3730
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('tls-domain').value = t.domain || '';
3731
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('tls-email').value = t.email || '';
3732
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('tls-allow-insecure').checked = !!t.allow_insecure;
3733
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// bridge (full)
3734
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const b = cfg.bridge || {};
3735
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bridge-enabled').checked = b.enabled !== false;
3736
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bridge-nick').value = b.nick || '';
3737
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bridge-channels').value = (b.channels || []).join(', ');
3738
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('bridge-buffer-size').value = b.buffer_size || '';
3739
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('policy-bridge-web-user-ttl').value = b.web_user_ttl_minutes || 5;
3740
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// topology + history
3741
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const topo = cfg.topology || {};
3742
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const h = cfg.config_history || {};
3743
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_topoChannels = (topo.channels || []).map(c => Object.assign({}, c));
3744
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
_topoTypes = (topo.types || []).map(x => Object.assign({}, x));
3745
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('topo-nick').value = topo.nick || 'topology';
3746
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
document.getElementById('topo-history-keep').value = h.keep != null ? h.keep : 20;
3747
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTopoStaticChannels();
3748
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
renderTopoChannelTypes();
3749
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
} catch(e) {
3750
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
console.error('loadConfigCards:', e);
3751
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3752
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3753
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3754
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function saveAgentPolicy() {
3755
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
saveConfigPatch({
3756
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
agent_policy: {
3757
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
require_checkin: document.getElementById('policy-checkin-enabled').checked,
3758
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
checkin_channel: document.getElementById('policy-checkin-channel').value.trim(),
3759
{ 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),
3760
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
online_timeout_secs: parseInt(document.getElementById('policy-online-timeout').value) || 0,
3761
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
reap_after_days: parseInt(document.getElementById('policy-reap-days').value) || 0,
3762
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3763
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}, 'agentpolicy-save-result');
3764
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3765
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3766
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function saveBridgeConfig() {
3767
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
saveConfigPatch({
3768
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
bridge: {
3769
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
enabled: document.getElementById('bridge-enabled').checked,
3770
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
nick: document.getElementById('bridge-nick').value.trim() || undefined,
3771
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
channels: document.getElementById('bridge-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
3772
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
buffer_size: parseInt(document.getElementById('bridge-buffer-size').value, 10) || undefined,
3773
{ 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,
3774
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3775
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}, 'bridge-save-result');
3776
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3777
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3778
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function saveLogging() {
3779
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
saveConfigPatch({
3780
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
logging: {
3781
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
enabled: document.getElementById('policy-logging-enabled').checked,
3782
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
dir: document.getElementById('policy-log-dir').value.trim(),
3783
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
format: document.getElementById('policy-log-format').value,
3784
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
rotation: document.getElementById('policy-log-rotation').value,
3785
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
max_size_mb: parseInt(document.getElementById('policy-log-max-size').value, 10) || 0,
3786
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
per_channel: document.getElementById('policy-log-per-channel').checked,
3787
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
max_age_days: parseInt(document.getElementById('policy-log-max-age').value, 10) || 0,
3788
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3789
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}, 'logging-save-result');
3790
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3791
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3792
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function saveGeneralConfig() {
3793
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const patch = {};
3794
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const addr = document.getElementById('general-api-addr').value.trim();
3795
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
const mcp = document.getElementById('general-mcp-addr').value.trim();
3796
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (addr) patch.api_addr = addr;
3797
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
if (mcp) patch.mcp_addr = mcp;
3798
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
saveConfigPatch(patch, 'general-save-result');
3799
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3800
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3801
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function saveErgoConfig() {
3802
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
saveConfigPatch({
3803
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
ergo: {
3804
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
network_name: document.getElementById('ergo-network-name').value.trim() || undefined,
3805
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
server_name: document.getElementById('ergo-server-name').value.trim() || undefined,
3806
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
irc_addr: document.getElementById('ergo-irc-addr').value.trim() || undefined,
3807
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
require_sasl: document.getElementById('ergo-require-sasl').checked,
3808
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
default_channel_modes: document.getElementById('ergo-default-modes').value.trim() || undefined,
3809
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
history: { enabled: document.getElementById('ergo-history-enabled').checked },
3810
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
external: document.getElementById('ergo-external').checked,
3811
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3812
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}, 'ergo-save-result');
3813
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3814
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3815
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function saveTLSConfig() {
3816
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
saveConfigPatch({
3817
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
tls: {
3818
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
domain: document.getElementById('tls-domain').value.trim() || undefined,
3819
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
email: document.getElementById('tls-email').value.trim() || undefined,
3820
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
allow_insecure: document.getElementById('tls-allow-insecure').checked,
3821
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3822
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}, 'tls-config-save-result');
3823
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
}
3824
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
3825
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
// --- init ---
3826
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
function loadAll() { loadStatus(); loadAgents(); loadSettings(); startMetricsPoll(); }
3827
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
initAuth();
3828
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</script>
3829
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</body>
3830
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!
</html>
3831
{ copied = false; pop = false }, 1000)" :class="copied && 'copied'">
Copy link Copied!