ScuttleBot

feat: web UI — status dashboard and agent management

lmata 2026-03-31 12:59 trunk
Commit 21649aaf9e30737480d91302517788371d3baf4ce15e6ec8465b94c5ac5ca877
--- internal/api/server.go
+++ internal/api/server.go
@@ -31,18 +31,24 @@
3131
log: log,
3232
}
3333
}
3434
3535
// Handler returns the HTTP handler with all routes registered.
36
-// Auth middleware wraps every route — no endpoint is reachable without a valid token.
36
+// /v1/ routes require a valid Bearer token. /ui/ is served unauthenticated.
3737
func (s *Server) Handler() http.Handler {
38
- mux := http.NewServeMux()
39
-
40
- mux.HandleFunc("GET /v1/status", s.handleStatus)
41
- mux.HandleFunc("GET /v1/agents", s.handleListAgents)
42
- mux.HandleFunc("GET /v1/agents/{nick}", s.handleGetAgent)
43
- mux.HandleFunc("POST /v1/agents/register", s.handleRegister)
44
- mux.HandleFunc("POST /v1/agents/{nick}/rotate", s.handleRotate)
45
- mux.HandleFunc("POST /v1/agents/{nick}/revoke", s.handleRevoke)
46
-
47
- return s.authMiddleware(mux)
38
+ apiMux := http.NewServeMux()
39
+ apiMux.HandleFunc("GET /v1/status", s.handleStatus)
40
+ apiMux.HandleFunc("GET /v1/agents", s.handleListAgents)
41
+ apiMux.HandleFunc("GET /v1/agents/{nick}", s.handleGetAgent)
42
+ apiMux.HandleFunc("POST /v1/agents/register", s.handleRegister)
43
+ apiMux.HandleFunc("POST /v1/agents/{nick}/rotate", s.handleRotate)
44
+ apiMux.HandleFunc("POST /v1/agents/{nick}/revoke", s.handleRevoke)
45
+
46
+ outer := http.NewServeMux()
47
+ outer.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
48
+ http.Redirect(w, r, "/ui/", http.StatusFound)
49
+ })
50
+ outer.Handle("/ui/", s.uiFileServer())
51
+ outer.Handle("/v1/", s.authMiddleware(apiMux))
52
+
53
+ return outer
4854
}
4955
5056
ADDED internal/api/ui.go
5157
ADDED internal/api/ui/index.html
--- internal/api/server.go
+++ internal/api/server.go
@@ -31,18 +31,24 @@
31 log: log,
32 }
33 }
34
35 // Handler returns the HTTP handler with all routes registered.
36 // Auth middleware wraps every route — no endpoint is reachable without a valid token.
37 func (s *Server) Handler() http.Handler {
38 mux := http.NewServeMux()
39
40 mux.HandleFunc("GET /v1/status", s.handleStatus)
41 mux.HandleFunc("GET /v1/agents", s.handleListAgents)
42 mux.HandleFunc("GET /v1/agents/{nick}", s.handleGetAgent)
43 mux.HandleFunc("POST /v1/agents/register", s.handleRegister)
44 mux.HandleFunc("POST /v1/agents/{nick}/rotate", s.handleRotate)
45 mux.HandleFunc("POST /v1/agents/{nick}/revoke", s.handleRevoke)
46
47 return s.authMiddleware(mux)
 
 
 
 
 
 
48 }
49
50 DDED internal/api/ui.go
51 DDED internal/api/ui/index.html
--- internal/api/server.go
+++ internal/api/server.go
@@ -31,18 +31,24 @@
31 log: log,
32 }
33 }
34
35 // Handler returns the HTTP handler with all routes registered.
36 // /v1/ routes require a valid Bearer token. /ui/ is served unauthenticated.
37 func (s *Server) Handler() http.Handler {
38 apiMux := http.NewServeMux()
39 apiMux.HandleFunc("GET /v1/status", s.handleStatus)
40 apiMux.HandleFunc("GET /v1/agents", s.handleListAgents)
41 apiMux.HandleFunc("GET /v1/agents/{nick}", s.handleGetAgent)
42 apiMux.HandleFunc("POST /v1/agents/register", s.handleRegister)
43 apiMux.HandleFunc("POST /v1/agents/{nick}/rotate", s.handleRotate)
44 apiMux.HandleFunc("POST /v1/agents/{nick}/revoke", s.handleRevoke)
45
46 outer := http.NewServeMux()
47 outer.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
48 http.Redirect(w, r, "/ui/", http.StatusFound)
49 })
50 outer.Handle("/ui/", s.uiFileServer())
51 outer.Handle("/v1/", s.authMiddleware(apiMux))
52
53 return outer
54 }
55
56 DDED internal/api/ui.go
57 DDED internal/api/ui/index.html
--- a/internal/api/ui.go
+++ b/internal/api/ui.go
@@ -0,0 +1,13 @@
1
+package api
2
+
3
+import (
4
+ "embed"
5
+ "net/http"
6
+)
7
+
8
+//go:embed ui/*
9
+var uiFS embed.FS
10
+
11
+func (s *Server) uiFileServer() http.Handler {
12
+ return http.FileServerFS(uiFS)
13
+}
--- a/internal/api/ui.go
+++ b/internal/api/ui.go
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/internal/api/ui.go
+++ b/internal/api/ui.go
@@ -0,0 +1,13 @@
1 package api
2
3 import (
4 "embed"
5 "net/http"
6 )
7
8 //go:embed ui/*
9 var uiFS embed.FS
10
11 func (s *Server) uiFileServer() http.Handler {
12 return http.FileServerFS(uiFS)
13 }
--- a/internal/api/ui/index.html
+++ b/internal/api/ui/index.html
@@ -0,0 +1,143 @@
1
+<!DOCTYPE html>
2
+<html lang=-pane { display:none; flex:1; min-height:0; overflow-y:auto; }
3
+.tab-pane.active { display:flex; flex-direction:col umn; }
4
+.pane-scroll { flex margin:0 auto; padding:24px; dis 'Cascadia Code', direction:column; gap:20px; }
5
+
6
+/* cards #0d1117; color: #e6edf3; min-height: 100vh; }
7
+ header { background: ont-size:13px; cbottom: 0; box-shadow:0 0 6padding: 12px 24px; display: { background:#db61 center; gap: 16px; }
8
+ header h1 16px; color: O@BF,2R: 0.05em; }
9
+ header .tagline { font-size: 12px; color: #8b949e; }
10
+ header .spacer { flex: 1; }
11
+ .token-badge { font-size: 12px; color: #8b949e; display: I@3Iz,18: center; gap: 8px; }
12
+ .token-badge code { background: #21262d; border: X@36F,14: 4px; padding: 2px 8px; color: #a5d6ff; max-width: 200px; overflow: M@2e0,16: ellipsis; white-space: nowrap; }
13
+ button { cursor: pointer; border: X@36F,F: 6px; padding: K@1Ci,1e: 13px; font-family: inherit; background: #21262d; color: #e6edf3; transition: background 0.1s; }
14
+ buttonK@Vk,E: #30363d; }
15
+ S@1G0,1: M@1GS,e: #1f6feb; color: #fff; }
16
+ button.primaryK@Vk,1U: #388bfd; }
17
+ button.danger { background: #21262d; border-color: #f85149; color: #f85149; }
18
+ J@1Iy,2J: { background: #3d1f1e; }
19
+ button.small { padding: 3px 8px; font-size: 12px; }
20
+ main { max-width: 960px; margin: 0 auto; padding: 24px; display: L@2Wk,y: column; gap: 24px; }
21
+ .card { background: #161b22; border: X@36F,p: 8px; overflow: hidden; }
22
+ .card-header { padding: P@Td,1: J@36F,9:display: I@3Iz,N: center; gap: 8px; }
23
+ S@WE,E: 14px; color: L@3KV,~: 600; }
24
+ .card-header .badge { background: #1f6feb22; border: H@NsG,I:44; c; }
25
+ L@aD,Z: 16px; }
26
+ .status-grid { display: S@dE,1R: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
27
+ .stat { background: #0d1117; border: J@y~,1J:border-radius: 6px; padding: 12px 16px; }
28
+ .stat .label { font-size: 11px; color: O@3He,1: Q@20l,2: 0L@3IR,1q: 4px; }
29
+ .stat .value { font-size: 20px; color: #58a6ff; font-weight: 600; }
30
+ .stat .sub { font-size: 11px; color: K@1Tz,~: 2px; }
31
+ .dot { display: inline-block; width: 8px; height: 8pxG@36W,T: 50%; margin-right: 6px; }
32
+ O@sy,i: #3fformval { color: K@1fC: 600; }
33
+ .card-header .badgpx; padding:H@1Cl,f: 12px; }
34
+ .cred-box .cred-row { display: I@3Iz,J: baseline; gap: 8pxG@3IW,K: 6px; }
35
+ .cred-box a@1dZ,a: 0; }
36
+ .cred-box .cred-key { color: J@1eV,d: 90px; }
37
+ .cred-box .cred-val { color: K@1fC,1: G@1fW,2V: 1; }
38
+ .cred-box .copy-btn { flex-shrink: 0; }
39
+ .modal-overlay { display: none; position: fixed; inset: 0; background: #0d111788; z-index: 100; align-items: O@2lU,D: center; }
40
+ U@39V,m: flex; }
41
+ .modal { background: #161b22; border: X@36F,u: 10px; padding: 24px; width: 480px; max-width: 95vw; }
42
+ M@3By,5: 15pxG@3IW,B: 16px; }
43
+ Q@3Ck,7: flex; G@Bw0,1B: flex-end; gap: 8px; margin-top: 16px; }
44
+</style>
45
+</head>
46
+<body>
47
+
48
+<header>
49
+S@3sg,L:<span class="tagline"d@3tE,S@Bj0,1U:<div class="token-badge">
50
+ <span>token:</span>
51
+ <code id="token-display">not set</code>
52
+L@6Fl,Z:all" onclick="openTokenModal()">setJ@5JF,I:</header>
53
+
54
+<main>
55
+19@487,J@Jx0,B:ℹ</span>
56
+f@49n,8:. It wasJ@4AW,S:when scuttlebot started:<br>y@4A~,3:...H@4B~,Q:</div>
57
+
58
+ <!-- Status -->
59
+J@AdG,M@7cG,8:-header"L@5uz,Y@4F1,N:
60
+ <h2>status</h2>
61
+G@5A0,G@8yG,4:bodyL@6AG,M:status-grid" id="statuH@4fk,V@4Q0,3:abeT@4LW,2:ueg@4Ly,V@4Q0,3:abeU@4NA,2:ueg@4Nd,V@4Q0,3:abeU@4Oq,2:ueg@4PJ,V@4Q0,3:abeV@4QW,2:ueK@GkW,1Q@4RJ,I@Rhk,12@4T3,Z:</div>
62
+ </div>
63
+
64
+ <!-- Agents -->
65
+J@AdG,M@7cG,U:-header">
66
+ <h2>agents</h2L@5uz,N@5PG,8:>0</spanK@Fol,J@5Ol,J@FAG,3:allo@5G0,Q@3TG,W@8tZ,9:<div id="G@FCG,2:">I@AXl,U: </div>
67
+
68
+ <!-- Register -->
69
+J@AdG,M@7cG,W:-header"><h2>register agent</h2>O@5m0,H@4uW,11@B1R,X@B2Q,3: <H@A8W,U@BUS,H@7l0,r@B3t,J@ACG,S@AMl,U@B5f,a@B66,H@9N0,n:operator">operator — human, +o + full permissionP@9I~,M@B6g,U:>worker — gets +v in channelP@9I~,G@9Fl,U@B7z,I:gets +o in channelP@9I~,G@9Fl,R:bserver">observer — read-O@B9V,N@MGS,X@ARl,J@B2~,P@Brv,P@MGl,3:regP@Bsk,P:fleet, #ops, #project.fooP@6CW,L@Bt~,W@BDS,H@A3W,1:<G@ARW,Y@BEO,P@MGl,s@BFH,O@54W,L@Bt~,Q@BH6,H:is allowed to senG@7iG,G@4f0,w@BI3,M@91G,T@ATj,L@5aG,r@BLG,H@SMl,g:form>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Chat -->
74
+P@AdG,3:hatX@4rg,H@6ml,R:header">
75
+ <h2>chat</h2L@5uz,T:badge" id="chat-channel-badgeN@4rl,6:</spanK@Fol,J@5Ol,7: <spanW@5vW,M@QtW,L@45B,M@GlS,Q:display:flex;height:440px"H@Ba0,b:chat-channel-list" style="width:140px;V@1wh,G@2XW,S:flex-shrink:0;padding:8px 0;I@R8W,G:direction:columnG@BCl,J:style="padding:6px K@8fi,P@oW,L:letter-spacing:0.06emG@8HW,N@7Kg,S:<div style="padding:6px 8px;P@21W,7:21262d;H@M9F,3:4px_@5Y~,I@HPj,W@5Zo,6:flex:1G@Gm0,D:padding:3px 6M@3i0,T@AVj,3:allP@5lN,8: style="R@5sw,3:1pxH@5lk,Z@5bW,K@ATV,M@4YG,I:flex:1;min-width:0G@BCl,I:id="chat-messages"U@5Sh,1:;I@TW,I@R8W,H@1xV,7:gap:2pxP@6CW,z:empty" id="chat-placeholder">select a channel to view messagesZ@4f0,G@8uW,6:0px 14P@3cf,7:30363d;H@M9F,X@Ntl,P@MGl,9:chat-nickL@3h0,g:your nick" style="width:110px;flex-shrink:0K@9m0,Z@5Z0,1Q@5zf,T@600,3:allY@60T,G:ChatMessage()">sp@AXE,Y: </div>
76
+</main>
77
+
78
+<!-- Token modalH@AxV,U:modal-overlay" id="token-modalH@68l,X:modal">
79
+ <h3>set API token</h3R@Bkz,1:3G@O0G,X:;margin-bottom:14px">The token isJ@4AW,R:when scuttlebot starts:<br>y@4A~,l:&lt;value&gt;</code></p>
80
+ <label>token</labelT@5MG,4:tokeS@3g~,A:token hereM@9m0,G:pellcheck="falseJ@5D~,l:btn-row">
81
+ <button onclick="closeTokenModalQ@BKh,F:<button class="M@6zW,5:TokenM@6zz,P@67V,V:script>
82
+// --- token managementP@C9h,1:{V@Ig0,9:cuttlebotR@CAe,D:setToken(t) {Q@Cfk,1F:cuttlebot_token', t);
83
+ updateTokenDisplay();
84
+}
85
+function updateTokenDisplay() {X@CHG,U@Hul,V:token-display');
86
+ const bannerS@CeW,1Y:no-token-banner');
87
+ if (t) {
88
+ el.textContent = t.slice(0, 8) + '…' + t.slice(-4);
89
+ bannerR@FjW,F:} else {
90
+ elG@Rcl,K:not set';
91
+ bannerI@RQ0,_:flex';
92
+ }
93
+}
94
+function openTokenModalW@GGk,V:token-input').value = getToken(U@Ojl,B:token-modalS@FTW,f@Faj,O:token-input').focus(), 5K@Fbl,E:TokenModal() {Q@Sx0,B:token-modalS@Fe0,K: }
95
+function saveTokem@CeH,S@H3~,i:v) { setToken(v); closeTokenModal(); loadAll(W@H9x,B:token-modalL@HAh,16:click', function(e) {
96
+ if (e.target === this) closeTokenModal();
97
+});
98
+R@Jsk,O:keydown', function(e) {
99
+H@J5k,S:Escape') closeTokenModal();
100
+15@Cnz,5:tokenN@CHQ,h@Cp3,_: 'Bearer ' + token, 'Content-Type': 2G@CqK,3I@Cvm,1I@Cz2,H:
101
+ const orig =G@Q5l,1:;Q@Q6l,1:�h@Q7I,J:orig; }, 1200);
102
+ }18@DZ4,1:sP@HQT,7:status'd@DfG,o@Dbh,3: + 2Q@Dc6: <divJ@6oG,1:8P@8fk,W@FwB,m:now — it will not be shown again.</div>`
103
+ );
104
+}u@Jv_,w:const icons = { info: 'ℹ', error: '✕', success: '✓' }O@MTF,_@Jwm,5:icons1D@Jxw,4: || 3W@JzA,9: + ' ' + L@K1k,4:);
105
+}d@HBq,5:;
106
+let2M@HCU,7:nelListP@HEw,R@Sal,9:chat-cardK@ONW,1:'L@Q6W,q:// bridge disabled or error — keep chat card hiddenN@N9x,B:ChannelListt@HFz,1a:t-channel-list');
107
+ // Remove old channel items (keep header div and join input div)
108
+ Array.from(lisL@C3j,c:chan-item')).forEach(el => el.remove())1r@HHL,V: === chatChannel ? ' active' : 1f@HJ_,33@HLD,1: 23@HOG,J:await loadChannels(d@HRe,4:
109
+ M@H9S,M: + e.message);
110
+ }
111
+}
112
+
113
+P@TE0,L@HPj,Y@HAk,1:
114
+H@J5k,N:Enter') joinChannel();
115
+f@HUg,p@HWW,B:annel-badgeH@Dxl,b@HWj,B:annel-badgeK@ONW,U@OGG,1Z@HY9,4:{
116
+ N@MWB,_:active', el.textContent === ch);
117
+ }n@H_U,v:essages');
118
+ // clear previous messages (keep placeholder)k@HaM,1: 14@Hb5,4: '')_@HQD,p@Hcl,T: || []).forEach(appendMessage2F@He9,J: '');
119
+ const url =N@Hsk,k@HhG,V@HgR,3:urlR@Hh~,6:statusS@CeW,Y@Hiw,G:= () => { statusG@Rcl,H:● live'; statusQ@Rdx,1e:};
120
+ es.onmessage = (e) => {
121
+ try {
122
+ const msg = JSON.parse(e.data);
123
+ appendMessage(msg);
124
+ f@HeC,n: } catch(_) {}
125
+ };
126
+ es.onerror = () => { statusG@Rcl,v:○ reconnecting…'; status.style.color = '#8b949e'; };
127
+}I@I7~,A:essage(msgn@I8Y,x:essages');
128
+ const t = new Date(msg.at);
129
+ const timeStr = tN@K1l,j@IHk,X:isBridge = msg.nick === 'bridge';19@IIy,J:;
130
+ row.innerHTML =O@IK~,O@IM9,6:
131
+ +J@IK~,15:nick${isBridge ? ' bridge-nick' : ''}">${esc(msg.nick)}</span>`
132
+ +K@IK~,G:ext">${esc(msg.td@IOV,1:}J@HUj,D:ndChatMessage1f@Irx,W@HiW,4:nickL@H3z,O@IuU,7:= inputM@CfF,2L@IvK,5: '');N@NPl,V@Hcl,s@IyZ,4:
133
+ M@IzQ,X@N3w,5:inputL@OIl,R@Sal,O@I~v,_: = false;
134
+ input.focus();
135
+ }
136
+}
137
+
138
+U@JUl,4:textg@HTT,1:
139
+H@J5k,U:Enter') sendChatMessage();
140
+});17@TNA,1C:Channels(); }
141
+updateTokenDisplay();
142
+loadAll();
143
+setInterval(loadStatus, 15000T@TOx,2CRpaK;
--- a/internal/api/ui/index.html
+++ b/internal/api/ui/index.html
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/internal/api/ui/index.html
+++ b/internal/api/ui/index.html
@@ -0,0 +1,143 @@
1 <!DOCTYPE html>
2 <html lang=-pane { display:none; flex:1; min-height:0; overflow-y:auto; }
3 .tab-pane.active { display:flex; flex-direction:col umn; }
4 .pane-scroll { flex margin:0 auto; padding:24px; dis 'Cascadia Code', direction:column; gap:20px; }
5
6 /* cards #0d1117; color: #e6edf3; min-height: 100vh; }
7 header { background: ont-size:13px; cbottom: 0; box-shadow:0 0 6padding: 12px 24px; display: { background:#db61 center; gap: 16px; }
8 header h1 16px; color: O@BF,2R: 0.05em; }
9 header .tagline { font-size: 12px; color: #8b949e; }
10 header .spacer { flex: 1; }
11 .token-badge { font-size: 12px; color: #8b949e; display: I@3Iz,18: center; gap: 8px; }
12 .token-badge code { background: #21262d; border: X@36F,14: 4px; padding: 2px 8px; color: #a5d6ff; max-width: 200px; overflow: M@2e0,16: ellipsis; white-space: nowrap; }
13 button { cursor: pointer; border: X@36F,F: 6px; padding: K@1Ci,1e: 13px; font-family: inherit; background: #21262d; color: #e6edf3; transition: background 0.1s; }
14 buttonK@Vk,E: #30363d; }
15 S@1G0,1: M@1GS,e: #1f6feb; color: #fff; }
16 button.primaryK@Vk,1U: #388bfd; }
17 button.danger { background: #21262d; border-color: #f85149; color: #f85149; }
18 J@1Iy,2J: { background: #3d1f1e; }
19 button.small { padding: 3px 8px; font-size: 12px; }
20 main { max-width: 960px; margin: 0 auto; padding: 24px; display: L@2Wk,y: column; gap: 24px; }
21 .card { background: #161b22; border: X@36F,p: 8px; overflow: hidden; }
22 .card-header { padding: P@Td,1: J@36F,9:display: I@3Iz,N: center; gap: 8px; }
23 S@WE,E: 14px; color: L@3KV,~: 600; }
24 .card-header .badge { background: #1f6feb22; border: H@NsG,I:44; c; }
25 L@aD,Z: 16px; }
26 .status-grid { display: S@dE,1R: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
27 .stat { background: #0d1117; border: J@y~,1J:border-radius: 6px; padding: 12px 16px; }
28 .stat .label { font-size: 11px; color: O@3He,1: Q@20l,2: 0L@3IR,1q: 4px; }
29 .stat .value { font-size: 20px; color: #58a6ff; font-weight: 600; }
30 .stat .sub { font-size: 11px; color: K@1Tz,~: 2px; }
31 .dot { display: inline-block; width: 8px; height: 8pxG@36W,T: 50%; margin-right: 6px; }
32 O@sy,i: #3fformval { color: K@1fC: 600; }
33 .card-header .badgpx; padding:H@1Cl,f: 12px; }
34 .cred-box .cred-row { display: I@3Iz,J: baseline; gap: 8pxG@3IW,K: 6px; }
35 .cred-box a@1dZ,a: 0; }
36 .cred-box .cred-key { color: J@1eV,d: 90px; }
37 .cred-box .cred-val { color: K@1fC,1: G@1fW,2V: 1; }
38 .cred-box .copy-btn { flex-shrink: 0; }
39 .modal-overlay { display: none; position: fixed; inset: 0; background: #0d111788; z-index: 100; align-items: O@2lU,D: center; }
40 U@39V,m: flex; }
41 .modal { background: #161b22; border: X@36F,u: 10px; padding: 24px; width: 480px; max-width: 95vw; }
42 M@3By,5: 15pxG@3IW,B: 16px; }
43 Q@3Ck,7: flex; G@Bw0,1B: flex-end; gap: 8px; margin-top: 16px; }
44 </style>
45 </head>
46 <body>
47
48 <header>
49 S@3sg,L:<span class="tagline"d@3tE,S@Bj0,1U:<div class="token-badge">
50 <span>token:</span>
51 <code id="token-display">not set</code>
52 L@6Fl,Z:all" onclick="openTokenModal()">setJ@5JF,I:</header>
53
54 <main>
55 19@487,J@Jx0,B:ℹ</span>
56 f@49n,8:. It wasJ@4AW,S:when scuttlebot started:<br>y@4A~,3:...H@4B~,Q:</div>
57
58 <!-- Status -->
59 J@AdG,M@7cG,8:-header"L@5uz,Y@4F1,N:
60 <h2>status</h2>
61 G@5A0,G@8yG,4:bodyL@6AG,M:status-grid" id="statuH@4fk,V@4Q0,3:abeT@4LW,2:ueg@4Ly,V@4Q0,3:abeU@4NA,2:ueg@4Nd,V@4Q0,3:abeU@4Oq,2:ueg@4PJ,V@4Q0,3:abeV@4QW,2:ueK@GkW,1Q@4RJ,I@Rhk,12@4T3,Z:</div>
62 </div>
63
64 <!-- Agents -->
65 J@AdG,M@7cG,U:-header">
66 <h2>agents</h2L@5uz,N@5PG,8:>0</spanK@Fol,J@5Ol,J@FAG,3:allo@5G0,Q@3TG,W@8tZ,9:<div id="G@FCG,2:">I@AXl,U: </div>
67
68 <!-- Register -->
69 J@AdG,M@7cG,W:-header"><h2>register agent</h2>O@5m0,H@4uW,11@B1R,X@B2Q,3: <H@A8W,U@BUS,H@7l0,r@B3t,J@ACG,S@AMl,U@B5f,a@B66,H@9N0,n:operator">operator — human, +o + full permissionP@9I~,M@B6g,U:>worker — gets +v in channelP@9I~,G@9Fl,U@B7z,I:gets +o in channelP@9I~,G@9Fl,R:bserver">observer — read-O@B9V,N@MGS,X@ARl,J@B2~,P@Brv,P@MGl,3:regP@Bsk,P:fleet, #ops, #project.fooP@6CW,L@Bt~,W@BDS,H@A3W,1:<G@ARW,Y@BEO,P@MGl,s@BFH,O@54W,L@Bt~,Q@BH6,H:is allowed to senG@7iG,G@4f0,w@BI3,M@91G,T@ATj,L@5aG,r@BLG,H@SMl,g:form>
70 </div>
71 </div>
72
73 <!-- Chat -->
74 P@AdG,3:hatX@4rg,H@6ml,R:header">
75 <h2>chat</h2L@5uz,T:badge" id="chat-channel-badgeN@4rl,6:</spanK@Fol,J@5Ol,7: <spanW@5vW,M@QtW,L@45B,M@GlS,Q:display:flex;height:440px"H@Ba0,b:chat-channel-list" style="width:140px;V@1wh,G@2XW,S:flex-shrink:0;padding:8px 0;I@R8W,G:direction:columnG@BCl,J:style="padding:6px K@8fi,P@oW,L:letter-spacing:0.06emG@8HW,N@7Kg,S:<div style="padding:6px 8px;P@21W,7:21262d;H@M9F,3:4px_@5Y~,I@HPj,W@5Zo,6:flex:1G@Gm0,D:padding:3px 6M@3i0,T@AVj,3:allP@5lN,8: style="R@5sw,3:1pxH@5lk,Z@5bW,K@ATV,M@4YG,I:flex:1;min-width:0G@BCl,I:id="chat-messages"U@5Sh,1:;I@TW,I@R8W,H@1xV,7:gap:2pxP@6CW,z:empty" id="chat-placeholder">select a channel to view messagesZ@4f0,G@8uW,6:0px 14P@3cf,7:30363d;H@M9F,X@Ntl,P@MGl,9:chat-nickL@3h0,g:your nick" style="width:110px;flex-shrink:0K@9m0,Z@5Z0,1Q@5zf,T@600,3:allY@60T,G:ChatMessage()">sp@AXE,Y: </div>
76 </main>
77
78 <!-- Token modalH@AxV,U:modal-overlay" id="token-modalH@68l,X:modal">
79 <h3>set API token</h3R@Bkz,1:3G@O0G,X:;margin-bottom:14px">The token isJ@4AW,R:when scuttlebot starts:<br>y@4A~,l:&lt;value&gt;</code></p>
80 <label>token</labelT@5MG,4:tokeS@3g~,A:token hereM@9m0,G:pellcheck="falseJ@5D~,l:btn-row">
81 <button onclick="closeTokenModalQ@BKh,F:<button class="M@6zW,5:TokenM@6zz,P@67V,V:script>
82 // --- token managementP@C9h,1:{V@Ig0,9:cuttlebotR@CAe,D:setToken(t) {Q@Cfk,1F:cuttlebot_token', t);
83 updateTokenDisplay();
84 }
85 function updateTokenDisplay() {X@CHG,U@Hul,V:token-display');
86 const bannerS@CeW,1Y:no-token-banner');
87 if (t) {
88 el.textContent = t.slice(0, 8) + '…' + t.slice(-4);
89 bannerR@FjW,F:} else {
90 elG@Rcl,K:not set';
91 bannerI@RQ0,_:flex';
92 }
93 }
94 function openTokenModalW@GGk,V:token-input').value = getToken(U@Ojl,B:token-modalS@FTW,f@Faj,O:token-input').focus(), 5K@Fbl,E:TokenModal() {Q@Sx0,B:token-modalS@Fe0,K: }
95 function saveTokem@CeH,S@H3~,i:v) { setToken(v); closeTokenModal(); loadAll(W@H9x,B:token-modalL@HAh,16:click', function(e) {
96 if (e.target === this) closeTokenModal();
97 });
98 R@Jsk,O:keydown', function(e) {
99 H@J5k,S:Escape') closeTokenModal();
100 15@Cnz,5:tokenN@CHQ,h@Cp3,_: 'Bearer ' + token, 'Content-Type': 2G@CqK,3I@Cvm,1I@Cz2,H:
101 const orig =G@Q5l,1:;Q@Q6l,1:�h@Q7I,J:orig; }, 1200);
102 }18@DZ4,1:sP@HQT,7:status'd@DfG,o@Dbh,3: + 2Q@Dc6: <divJ@6oG,1:8P@8fk,W@FwB,m:now — it will not be shown again.</div>`
103 );
104 }u@Jv_,w:const icons = { info: 'ℹ', error: '✕', success: '✓' }O@MTF,_@Jwm,5:icons1D@Jxw,4: || 3W@JzA,9: + ' ' + L@K1k,4:);
105 }d@HBq,5:;
106 let2M@HCU,7:nelListP@HEw,R@Sal,9:chat-cardK@ONW,1:'L@Q6W,q:// bridge disabled or error — keep chat card hiddenN@N9x,B:ChannelListt@HFz,1a:t-channel-list');
107 // Remove old channel items (keep header div and join input div)
108 Array.from(lisL@C3j,c:chan-item')).forEach(el => el.remove())1r@HHL,V: === chatChannel ? ' active' : 1f@HJ_,33@HLD,1: 23@HOG,J:await loadChannels(d@HRe,4:
109 M@H9S,M: + e.message);
110 }
111 }
112
113 P@TE0,L@HPj,Y@HAk,1:
114 H@J5k,N:Enter') joinChannel();
115 f@HUg,p@HWW,B:annel-badgeH@Dxl,b@HWj,B:annel-badgeK@ONW,U@OGG,1Z@HY9,4:{
116 N@MWB,_:active', el.textContent === ch);
117 }n@H_U,v:essages');
118 // clear previous messages (keep placeholder)k@HaM,1: 14@Hb5,4: '')_@HQD,p@Hcl,T: || []).forEach(appendMessage2F@He9,J: '');
119 const url =N@Hsk,k@HhG,V@HgR,3:urlR@Hh~,6:statusS@CeW,Y@Hiw,G:= () => { statusG@Rcl,H:● live'; statusQ@Rdx,1e:};
120 es.onmessage = (e) => {
121 try {
122 const msg = JSON.parse(e.data);
123 appendMessage(msg);
124 f@HeC,n: } catch(_) {}
125 };
126 es.onerror = () => { statusG@Rcl,v:○ reconnecting…'; status.style.color = '#8b949e'; };
127 }I@I7~,A:essage(msgn@I8Y,x:essages');
128 const t = new Date(msg.at);
129 const timeStr = tN@K1l,j@IHk,X:isBridge = msg.nick === 'bridge';19@IIy,J:;
130 row.innerHTML =O@IK~,O@IM9,6:
131 +J@IK~,15:nick${isBridge ? ' bridge-nick' : ''}">${esc(msg.nick)}</span>`
132 +K@IK~,G:ext">${esc(msg.td@IOV,1:}J@HUj,D:ndChatMessage1f@Irx,W@HiW,4:nickL@H3z,O@IuU,7:= inputM@CfF,2L@IvK,5: '');N@NPl,V@Hcl,s@IyZ,4:
133 M@IzQ,X@N3w,5:inputL@OIl,R@Sal,O@I~v,_: = false;
134 input.focus();
135 }
136 }
137
138 U@JUl,4:textg@HTT,1:
139 H@J5k,U:Enter') sendChatMessage();
140 });17@TNA,1C:Channels(); }
141 updateTokenDisplay();
142 loadAll();
143 setInterval(loadStatus, 15000T@TOx,2CRpaK;

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button