@scp-dev/dashboard 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/public/app.css +187 -0
- package/dist/public/app.js +80 -0
- package/dist/public/graph.css +85 -0
- package/dist/public/graph.js +742 -0
- package/dist/public/index.html +1 -0
- package/dist/public/selfhost.js +1315 -0
- package/dist/server.js +460 -0
- package/package.json +18 -3
|
@@ -0,0 +1,1315 @@
|
|
|
1
|
+
(()=>{var f=((e)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(typeof require<"u"?require:t)[n]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var U=86400000;var ot=(e)=>{let t=2166136261;for(let n=0;n<e.length;n++)t=Math.imul(t^e.charCodeAt(n),16777619);return t>>>0};function at(e){let t=e.split(/[^\p{L}\p{N}]+/u).filter(Boolean);return(t.length>1?t[0][0]+t[1][0]:(t[0]??e).slice(0,1)).toUpperCase()||"?"}function ee(e){let t=(l)=>l.split("@")[0]||l,n=new Map;for(let l of e)n.set(t(l.email),(n.get(t(l.email))??0)+1);let o=e.map((l)=>({id:l.user_id,name:n.get(t(l.email))>1?l.email:t(l.email)}));o.sort((l,d)=>l.name.localeCompare(d.name));let i=new Set,s=new Map;for(let{id:l,name:d}of o){let a=ot(d)%8;for(let c=0;c<8&&i.has(a);c++)a=(a+1)%8;i.add(a),s.set(l,{name:d,initials:at(d),tone:a})}return s}function B(e,t){return{...e,machines:e.machines.map(({actor:n,...o})=>({...o,identity:n!==null&&t.get(n)||null}))}}var lt=["inbox.wait","ask","scopes","groups"],un=[...lt,"peek","judge","holds"],te={bodyBytes:65536,recipients:16,attachments:16,ttlDefaultS:604800,ttlMaxS:2592000,purgeAfterS:604800,inboxMax:1000,inboxPage:100,waitMaxS:50,hopMax:64,threadMax:200,causalWindowS:120,pairBurst:20,pairBurstWindowS:10,pairPerMinute:30,pairPerHour:300,userPerDay:2000,pingPongAlternations:100,pingPongWindowS:600,pingPongBlockS:600,onlineWithinS:60,hookOnlineS:300,hookIdleS:1800,hookEndedGraceS:30,restartGraceS:60,injectMessages:5,injectBytes:7168,pushBytes:20480,pushes:20,pushGapS:30,pushQuietS:3,pushGraceS:6,stopBlocks:3,directiveHolds:5,injectHoldS:10,deferredShowS:600,deferredSettleMs:2000,listOfflineWithinS:86400,historicAfterS:3600,handleBoundS:2592000,endpointRetireS:86400,endpointRetireMax:1000,idempotencyS:86400,auditKeepS:2592000,noteChars:280,handbackBytes:16384,stderrBytes:1024,activityKeep:5000,activitySummaryChars:200,textField:512,lineageMax:16,depthMax:32,fanOutMax:32,fanOutTotal:64,treePerMinute:120,treePerHour:1200,treePauseS:600,groupPerMinute:60,grantTtlS:604800,groupIdleS:86400,policyLoadsKept:8,titleRefreshTurns:5,wakeDebounceS:60,wakeBackoffMaxS:900,wakeRefChars:512,wakeEtaMaxS:3600};var Re=["codex-queue"];var Se=new Set(["cli","hook","mcp","shell"]);var An=`harness IN (${[...Se].map((e)=>`'${e}'`).join(", ")})`;var ne="_gone_",Ln=[`CREATE TABLE IF NOT EXISTS endpoints (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
handle TEXT NOT NULL UNIQUE,
|
|
4
|
+
kind TEXT NOT NULL,
|
|
5
|
+
harness TEXT,
|
|
6
|
+
model TEXT,
|
|
7
|
+
cwd TEXT,
|
|
8
|
+
parent TEXT,
|
|
9
|
+
labels TEXT NOT NULL DEFAULT '{}',
|
|
10
|
+
status_state TEXT,
|
|
11
|
+
status_note TEXT,
|
|
12
|
+
token_hash TEXT,
|
|
13
|
+
connections INTEGER NOT NULL DEFAULT 0,
|
|
14
|
+
last_seen INTEGER,
|
|
15
|
+
last_received_at INTEGER,
|
|
16
|
+
last_received_hop INTEGER,
|
|
17
|
+
created_at INTEGER NOT NULL,
|
|
18
|
+
session TEXT,
|
|
19
|
+
hook_seen INTEGER,
|
|
20
|
+
hook_ended INTEGER,
|
|
21
|
+
process TEXT,
|
|
22
|
+
stop_blocks INTEGER
|
|
23
|
+
)`,"CREATE UNIQUE INDEX IF NOT EXISTS endpoints_token ON endpoints (token_hash)",`CREATE TABLE IF NOT EXISTS messages (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
thread TEXT NOT NULL,
|
|
26
|
+
reply_to TEXT,
|
|
27
|
+
kind TEXT NOT NULL,
|
|
28
|
+
from_ep TEXT NOT NULL,
|
|
29
|
+
to_addr TEXT NOT NULL,
|
|
30
|
+
body TEXT NOT NULL,
|
|
31
|
+
mime TEXT NOT NULL,
|
|
32
|
+
data_class TEXT NOT NULL,
|
|
33
|
+
attachments TEXT NOT NULL,
|
|
34
|
+
hop INTEGER NOT NULL,
|
|
35
|
+
created_at INTEGER NOT NULL,
|
|
36
|
+
expires_at INTEGER NOT NULL,
|
|
37
|
+
sender_tty INTEGER,
|
|
38
|
+
require_reply INTEGER
|
|
39
|
+
)`,"CREATE INDEX IF NOT EXISTS messages_thread ON messages (thread)","CREATE INDEX IF NOT EXISTS messages_reply_to ON messages (reply_to)","CREATE INDEX IF NOT EXISTS messages_created ON messages (created_at)",`CREATE TABLE IF NOT EXISTS deliveries (
|
|
40
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
41
|
+
message_id TEXT NOT NULL,
|
|
42
|
+
endpoint TEXT NOT NULL,
|
|
43
|
+
acked_at INTEGER,
|
|
44
|
+
expires_at INTEGER NOT NULL,
|
|
45
|
+
offered_until INTEGER,
|
|
46
|
+
offered_hold TEXT,
|
|
47
|
+
injected_at INTEGER,
|
|
48
|
+
nudged_at INTEGER,
|
|
49
|
+
deferred_at INTEGER,
|
|
50
|
+
deferred_ack INTEGER
|
|
51
|
+
)`,"CREATE INDEX IF NOT EXISTS deliveries_inbox ON deliveries (endpoint, acked_at, seq)","CREATE INDEX IF NOT EXISTS deliveries_message ON deliveries (message_id)","CREATE INDEX IF NOT EXISTS deliveries_expires ON deliveries (expires_at)",`CREATE TABLE IF NOT EXISTS idempotency (
|
|
52
|
+
from_ep TEXT NOT NULL,
|
|
53
|
+
client_id TEXT NOT NULL,
|
|
54
|
+
payload_hash TEXT NOT NULL,
|
|
55
|
+
result TEXT NOT NULL,
|
|
56
|
+
created_at INTEGER NOT NULL,
|
|
57
|
+
PRIMARY KEY (from_ep, client_id)
|
|
58
|
+
)`,"CREATE TABLE IF NOT EXISTS pair_events (from_ep TEXT NOT NULL, to_ep TEXT NOT NULL, at INTEGER NOT NULL)","CREATE INDEX IF NOT EXISTS pair_events_pair ON pair_events (from_ep, to_ep, at)","CREATE TABLE IF NOT EXISTS blocks (pair TEXT PRIMARY KEY, until INTEGER NOT NULL)",`CREATE TABLE IF NOT EXISTS audit (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
at INTEGER NOT NULL,
|
|
61
|
+
event TEXT NOT NULL,
|
|
62
|
+
endpoint TEXT,
|
|
63
|
+
ref TEXT,
|
|
64
|
+
detail TEXT NOT NULL DEFAULT '{}'
|
|
65
|
+
)`,"CREATE INDEX IF NOT EXISTS audit_at ON audit (at)",`CREATE TABLE IF NOT EXISTS grants (
|
|
66
|
+
token_hash TEXT PRIMARY KEY,
|
|
67
|
+
issuer TEXT NOT NULL,
|
|
68
|
+
under TEXT NOT NULL,
|
|
69
|
+
name TEXT,
|
|
70
|
+
labels TEXT,
|
|
71
|
+
caps TEXT,
|
|
72
|
+
groups TEXT,
|
|
73
|
+
used_by TEXT,
|
|
74
|
+
created_at INTEGER NOT NULL,
|
|
75
|
+
expires_at INTEGER NOT NULL,
|
|
76
|
+
revoked_at INTEGER,
|
|
77
|
+
spent_cwd TEXT,
|
|
78
|
+
title TEXT,
|
|
79
|
+
bound_process TEXT
|
|
80
|
+
)`,"CREATE TABLE IF NOT EXISTS groups (name TEXT PRIMARY KEY, owner TEXT NOT NULL, tree TEXT, rules TEXT, pinned INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, id TEXT)","CREATE TABLE IF NOT EXISTS group_members (name TEXT NOT NULL, endpoint TEXT NOT NULL, added_by TEXT NOT NULL, at INTEGER NOT NULL, PRIMARY KEY (name, endpoint))","CREATE TABLE IF NOT EXISTS policy_binds (policy TEXT NOT NULL, name TEXT NOT NULL, grp TEXT NOT NULL, PRIMARY KEY (policy, name))","CREATE TABLE IF NOT EXISTS policy_loads (policy TEXT PRIMARY KEY, at INTEGER NOT NULL)","CREATE INDEX IF NOT EXISTS group_members_endpoint ON group_members (endpoint)","CREATE TABLE IF NOT EXISTS admins (subject TEXT NOT NULL, scope_kind TEXT NOT NULL, scope TEXT NOT NULL, scope_id TEXT, granted_at INTEGER NOT NULL, revoked_at INTEGER)","CREATE TABLE IF NOT EXISTS tree_events (tree TEXT NOT NULL, at INTEGER NOT NULL)","CREATE INDEX IF NOT EXISTS tree_events_tree ON tree_events (tree, at)","CREATE TABLE IF NOT EXISTS relay_migrations (name TEXT PRIMARY KEY, at INTEGER NOT NULL)",`CREATE TABLE IF NOT EXISTS activity (
|
|
81
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
|
+
at INTEGER NOT NULL,
|
|
83
|
+
endpoint TEXT NOT NULL,
|
|
84
|
+
kind TEXT NOT NULL,
|
|
85
|
+
tool TEXT,
|
|
86
|
+
summary TEXT,
|
|
87
|
+
agent TEXT
|
|
88
|
+
)`,"CREATE TABLE IF NOT EXISTS peek_turns (id INTEGER PRIMARY KEY AUTOINCREMENT, endpoint TEXT NOT NULL, at INTEGER NOT NULL, role TEXT NOT NULL, text TEXT NOT NULL)","CREATE INDEX IF NOT EXISTS peek_turns_endpoint ON peek_turns (endpoint, id)","CREATE TRIGGER IF NOT EXISTS audit_no_update BEFORE UPDATE ON audit BEGIN SELECT RAISE(ABORT, 'audit is append-only'); END",`CREATE TRIGGER IF NOT EXISTS audit_no_delete BEFORE DELETE ON audit
|
|
89
|
+
WHEN OLD.at > CAST(strftime('%s', 'now') AS INTEGER) * 1000 - ${te.auditKeepS*1000}
|
|
90
|
+
BEGIN SELECT RAISE(ABORT, 'audit is append-only'); END`];var On=`stderr (last ${te.stderrBytes/1024} KB):`,dt=4,xn=new RegExp(`^[0-9a-z]{${dt},}$`);var Dn=`COALESCE(wake IN (${Re.map((e)=>`'${e}'`).join(", ")}), 0)`;class k{value;constructor(e){this.value=e}toString(){return this.value}}var ht={"&":"&","<":"<",">":">",'"':""","'":"'"};function Ne(e){if(e instanceof k)return e.value;if(Array.isArray(e))return e.map(Ne).join("");if(e===null||e===void 0||e===!1)return"";return String(e).replace(/[&<>"']/g,(t)=>ht[t])}function r(e,...t){let n=e[0];for(let o=0;o<t.length;o++)n+=Ne(t[o])+e[o+1];return new k(n)}function R(e){let t=2166136261;for(let n=0;n<e.length;n++)t=Math.imul(t^e.charCodeAt(n),16777619);return(t>>>0).toString(36)}function re(e,t){let n=e.email?r`<nav class="nav" aria-label="Main">
|
|
91
|
+
<a href="/orgs" ${e.nav==="orgs"?r`aria-current="page"`:""}>Orgs</a>
|
|
92
|
+
<a href="/device" ${e.nav==="device"?r`aria-current="page"`:""}>Device</a>
|
|
93
|
+
<form method="post" action="/sign-out"><input type="hidden" name="csrf" value="${e.csrf}"><button class="linkish" type="submit">Sign out</button></form>
|
|
94
|
+
</nav>`:r`<nav class="nav" aria-label="Main"><a href="/sign-in">Sign in</a></nav>`;return`<!doctype html>${r`<html lang="en">
|
|
95
|
+
<head>
|
|
96
|
+
<meta charset="utf-8">
|
|
97
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
98
|
+
<meta name="referrer" content="same-origin">
|
|
99
|
+
<title>${e.title} | scp relay</title>
|
|
100
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
101
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
102
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap">
|
|
103
|
+
<link rel="stylesheet" href="/app.css?v=${R(gt)}">
|
|
104
|
+
${e.head??""}
|
|
105
|
+
</head>
|
|
106
|
+
${e.bare?r`<body class="bare">${t}</body>
|
|
107
|
+
</html>`:r`<body${e.narrow?"":r` class="wide"`}>
|
|
108
|
+
<header class="head"><div class="wrap head-in">
|
|
109
|
+
<a class="brand" href="/"><svg class="mark" viewBox="0 0 32 32" fill="currentColor" aria-hidden="true"><path d="M3 3h10v4H7v6H3zM29 29H19v-4h6v-6h4z"/><rect x="11" y="11" width="10" height="10"/></svg>scp <span class="brand-dim">relay</span></a>
|
|
110
|
+
${n}
|
|
111
|
+
</div></header>
|
|
112
|
+
<main><div class="wrap page">${t}</div></main>
|
|
113
|
+
<footer class="foot"><div class="wrap rule foot-in">
|
|
114
|
+
<p class="mini">${e.email?r`Signed in as ${e.email}`:"Hosted demo relay for the Swarm Communication Protocol"}</p>
|
|
115
|
+
<a class="mini-link" href="https://scp.dev/spec">Spec</a>
|
|
116
|
+
</div></footer>
|
|
117
|
+
</body>
|
|
118
|
+
</html>`}`}`}var gt=`
|
|
119
|
+
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
|
120
|
+
:root{--bg:#fff;--surface:#f7f7f6;--fg:#0b0b0b;--ink:#555;--dim:#6b6b6b;--faint:#a8a8a8;--line:rgb(0 0 0/.12);--line-soft:rgb(0 0 0/.065);--acc:#4746d4;--acc-soft:rgb(71 70 212/.09);--warn:#b4321f;--head:rgb(255 255 255/.82);
|
|
121
|
+
--sans:"Geist",-apple-system,BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;--mono:"Geist Mono",ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;--gutter:clamp(1rem,4vw,3rem);--headh:4rem}
|
|
122
|
+
@media (prefers-color-scheme:dark){:root{--bg:#090909;--surface:#111;--fg:#f2f2f0;--ink:#a6a6a6;--dim:#8c8c8c;--faint:#575757;--line:rgb(255 255 255/.13);--line-soft:rgb(255 255 255/.07);--acc:#9d9cff;--acc-soft:rgb(157 156 255/.1);--warn:#ff8a78;--head:rgb(9 9 9/.78)}}
|
|
123
|
+
html{color-scheme:light dark;-webkit-text-size-adjust:100%}
|
|
124
|
+
body{display:flex;flex-direction:column;min-height:100dvh;background:var(--bg);color:var(--fg);font-family:var(--sans);font-size:.9375rem;line-height:1.6;-webkit-font-smoothing:antialiased}
|
|
125
|
+
main{flex:1}
|
|
126
|
+
a{color:inherit}
|
|
127
|
+
::selection{background:var(--fg);color:var(--bg)}
|
|
128
|
+
:focus-visible{outline:1px solid var(--fg);outline-offset:3px}
|
|
129
|
+
code,pre,kbd{font-family:var(--mono)}
|
|
130
|
+
.wrap{position:relative;width:100%;max-width:56rem;margin-inline:auto;padding-inline:var(--gutter);border-inline:1px solid var(--line-soft)}
|
|
131
|
+
.rule{border-top:1px solid var(--line-soft)}
|
|
132
|
+
@media (max-width:48rem){.wrap{border-inline:0}}
|
|
133
|
+
.wide .wrap{max-width:100rem}
|
|
134
|
+
.head{position:sticky;top:0;z-index:20;border-bottom:1px solid var(--line-soft);background:var(--head);-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}
|
|
135
|
+
.head-in{display:flex;align-items:center;justify-content:space-between;gap:1.5rem;height:var(--headh)}
|
|
136
|
+
.brand{display:inline-flex;align-items:center;gap:.625rem;font-weight:600;letter-spacing:.02em;text-decoration:none}
|
|
137
|
+
.brand-dim{color:var(--dim);font-weight:500}
|
|
138
|
+
.mark{width:16px;height:16px;flex:none;color:var(--fg)}
|
|
139
|
+
.nav{display:flex;align-items:center;gap:1.5rem}
|
|
140
|
+
.nav a,.linkish{font-family:var(--mono);font-size:.75rem;color:var(--ink);padding-block:.25rem;border:0;border-bottom:1px solid transparent;background:none;text-decoration:none;cursor:pointer}
|
|
141
|
+
.nav a:hover,.linkish:hover,.nav a[aria-current]{color:var(--fg)}
|
|
142
|
+
.nav a[aria-current]{border-bottom-color:var(--fg)}
|
|
143
|
+
.nav form{display:inline}
|
|
144
|
+
.page{padding-block:clamp(2.5rem,6vw,4.5rem) 5rem;min-height:calc(100dvh - var(--headh) - 7rem)}
|
|
145
|
+
.label{font-family:var(--mono);font-size:.6875rem;letter-spacing:.14em;text-transform:uppercase;color:var(--dim)}
|
|
146
|
+
h1{margin-top:1rem;font-size:clamp(2rem,4.6vw,3rem);line-height:1;font-weight:580;letter-spacing:-.045em;text-wrap:balance}
|
|
147
|
+
h2{font-size:1.125rem;font-weight:560;letter-spacing:-.015em}
|
|
148
|
+
.lead{margin-top:1rem;max-width:36rem;color:var(--ink);font-size:1rem}
|
|
149
|
+
.small{font-size:.875rem;color:var(--ink)}
|
|
150
|
+
.mini{font-family:var(--mono);font-size:.6875rem;color:var(--dim)}
|
|
151
|
+
.mini-link{font-family:var(--mono);font-size:.6875rem;color:var(--dim);text-decoration:none}
|
|
152
|
+
.mini-link:hover{color:var(--fg)}
|
|
153
|
+
.block{margin-top:3rem;padding-top:1.75rem;border-top:1px solid var(--line-soft)}
|
|
154
|
+
.block>h2+*{margin-top:1rem}
|
|
155
|
+
.form{display:grid;gap:1rem;max-width:26rem;margin-top:2rem}
|
|
156
|
+
.form.inline{display:flex;flex-wrap:wrap;align-items:end;max-width:none}
|
|
157
|
+
.field{display:grid;gap:.375rem}
|
|
158
|
+
.field span{font-family:var(--mono);font-size:.6875rem;letter-spacing:.08em;text-transform:uppercase;color:var(--dim)}
|
|
159
|
+
input[type=email],input[type=text],select{height:2.75rem;padding-inline:.875rem;border:1px solid var(--line);border-radius:0;background:var(--bg);color:var(--fg);font:inherit;font-size:.9375rem}
|
|
160
|
+
input:focus,select:focus{outline:none;border-color:var(--fg)}
|
|
161
|
+
input.code{font-family:var(--mono);font-size:1.25rem;letter-spacing:.2em;text-transform:uppercase}
|
|
162
|
+
.btn{display:inline-flex;align-items:center;justify-content:center;height:2.75rem;padding-inline:1.25rem;border:1px solid;font-family:var(--mono);font-size:.75rem;white-space:nowrap;cursor:pointer;text-decoration:none;transition:background-color .2s,color .2s,border-color .2s}
|
|
163
|
+
.btn-primary{border-color:var(--fg);background:var(--fg);color:var(--bg)}
|
|
164
|
+
.btn-primary:hover{background:transparent;color:var(--fg)}
|
|
165
|
+
.btn-ghost{border-color:var(--line);background:transparent;color:var(--fg)}
|
|
166
|
+
.btn-ghost:hover{border-color:var(--fg)}
|
|
167
|
+
.btn-small{height:2rem;padding-inline:.75rem;font-size:.6875rem}
|
|
168
|
+
.acts{display:flex;flex-wrap:wrap;gap:.75rem}
|
|
169
|
+
.table{margin-top:1rem;overflow-x:auto;border:1px solid var(--line-soft)}
|
|
170
|
+
table{width:100%;border-collapse:collapse;font-size:.875rem}
|
|
171
|
+
th,td{padding:.75rem 1rem;text-align:left;vertical-align:middle;border-bottom:1px solid var(--line-soft)}
|
|
172
|
+
tr:last-child td{border-bottom:0}
|
|
173
|
+
th{background:var(--surface);font-family:var(--mono);font-size:.6875rem;font-weight:400;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);white-space:nowrap}
|
|
174
|
+
td.mono,.mono{font-family:var(--mono);font-size:.75rem}
|
|
175
|
+
td.end,th.end{text-align:right}
|
|
176
|
+
td form{display:inline}
|
|
177
|
+
.empty{margin-top:1rem;padding:1.25rem;border:1px dashed var(--line);color:var(--dim);font-size:.875rem}
|
|
178
|
+
.notice{margin-top:1.5rem;padding:.875rem 1rem;border:1px solid var(--line-soft);border-left:1px solid var(--acc);background:var(--acc-soft);font-size:.875rem}
|
|
179
|
+
.notice.err{border-left-color:var(--warn);background:transparent;color:var(--warn)}
|
|
180
|
+
.secret{margin-top:1rem;padding:1rem 1.125rem;border:1px solid var(--line);background:var(--surface);font-family:var(--mono);font-size:.8125rem;overflow-wrap:anywhere;user-select:all}
|
|
181
|
+
.usercode{margin-top:1.5rem;font-family:var(--mono);font-size:clamp(2rem,6vw,3rem);letter-spacing:.12em}
|
|
182
|
+
.role{font-family:var(--mono);font-size:.6875rem;padding:.125rem .375rem;border:1px solid var(--line);color:var(--ink)}
|
|
183
|
+
h3{font-size:.9375rem;font-weight:560;letter-spacing:-.01em}
|
|
184
|
+
.nowrap{white-space:nowrap}
|
|
185
|
+
.push{margin-left:auto}
|
|
186
|
+
.block-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem 1rem}
|
|
187
|
+
.dot{display:inline-block;width:6px;height:6px;margin-right:.5rem;vertical-align:middle;border:1px solid var(--faint);background:transparent}
|
|
188
|
+
.dot.online{border-color:var(--acc);background:var(--acc)}
|
|
189
|
+
.dot.idle{border-color:var(--dim);background:var(--dim)}
|
|
190
|
+
.pulse{animation:pulse 2.4s ease-in-out infinite}
|
|
191
|
+
@keyframes pulse{50%{opacity:.25}}
|
|
192
|
+
.tag{display:inline-block;font-family:var(--mono);font-size:.6875rem;padding:.0625rem .375rem;background:var(--surface);border:1px solid var(--line-soft);color:var(--ink);white-space:nowrap}
|
|
193
|
+
.machine{margin-top:1.5rem}
|
|
194
|
+
table.agents,table.activity{table-layout:fixed;min-width:56rem}
|
|
195
|
+
table.agents th,table.agents td,table.activity th,table.activity td{height:2.125rem;padding:.25rem .75rem;line-height:1.25rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
196
|
+
table.agents .end,table.activity .end{text-align:right}
|
|
197
|
+
table.agents .tag+.tag{margin-left:.25rem}
|
|
198
|
+
.c-agent{width:30%}.c-title{width:18%}.c-presence{width:8.5rem}.c-status{width:20%}.c-labels{width:16%}.c-seen{width:6.5rem}
|
|
199
|
+
.c-when{width:7rem}.c-who{width:30%}
|
|
200
|
+
tr.sub td.agent{padding-left:1.75rem}
|
|
201
|
+
tr.sub.d2 td.agent{padding-left:2.75rem}
|
|
202
|
+
tr.sub.d3 td.agent{padding-left:3.75rem}
|
|
203
|
+
tr.sub.d4 td.agent{padding-left:4.75rem}
|
|
204
|
+
.caret{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;margin-right:.125rem;padding:0;border:0;background:none;color:var(--dim);vertical-align:-2px;cursor:pointer}
|
|
205
|
+
.caret svg{transition:transform .15s}
|
|
206
|
+
.caret[aria-expanded=true] svg{transform:rotate(90deg)}
|
|
207
|
+
.caret:hover{color:var(--fg)}
|
|
208
|
+
:root{--h-claude:#c15f3c;--h-codex:#0f8a6c;--h-opencode:#b56a00;--h-gemini:#1a73e8;--h-agy:#4285f4;--h-grok:#3a3a3a;--h-muse:#0866ff;--h-pi:#c03d86;--h-t3code:#4f46e5}
|
|
209
|
+
@media (prefers-color-scheme:dark){:root{--h-claude:#e08b6b;--h-codex:#3fc6a0;--h-opencode:#e6a43f;--h-gemini:#7aa9ff;--h-agy:#8ab4f8;--h-grok:#e6e6e6;--h-muse:#3d8bff;--h-pi:#ec7bb8;--h-t3code:#a5a1ff}}
|
|
210
|
+
.hicon{display:inline-block;overflow:visible;width:12px;height:12px;margin-right:.375rem;vertical-align:-1px;color:var(--dim)}
|
|
211
|
+
.h-claude{color:var(--h-claude)}.h-codex{color:var(--h-codex)}.h-opencode{color:var(--h-opencode)}.h-gemini{color:var(--h-gemini)}.h-agy{color:var(--h-agy)}.h-grok{color:var(--h-grok)}.h-muse{color:var(--h-muse)}.h-pi{color:var(--h-pi)}.h-t3code{color:var(--h-t3code)}
|
|
212
|
+
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
|
|
213
|
+
.wake-badge{display:inline-flex;vertical-align:middle;margin-left:.25rem;color:var(--acc)}
|
|
214
|
+
.bolt{width:11px;height:11px}
|
|
215
|
+
.dot.idle.wake,.dot.hibernated{border-color:var(--acc);background:var(--acc-soft)}
|
|
216
|
+
details.offline{margin-top:.5rem}
|
|
217
|
+
details.offline>summary{display:inline-block;padding:.125rem 0;cursor:pointer;list-style:none}
|
|
218
|
+
details.offline>summary::-webkit-details-marker{display:none}
|
|
219
|
+
details.offline>summary::before{content:"+ ";color:var(--faint)}
|
|
220
|
+
details.offline[open]>summary::before{content:"- "}
|
|
221
|
+
details.offline .when-open,details.offline[open] .when-shut{display:none}
|
|
222
|
+
details.offline[open] .when-open{display:inline}
|
|
223
|
+
details.offline>summary:hover{color:var(--fg)}
|
|
224
|
+
.machine .table{margin-top:.625rem}
|
|
225
|
+
.machine-head{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem .75rem}
|
|
226
|
+
.machine-head .dot{margin-right:0}
|
|
227
|
+
.machine-head h3{font-family:var(--mono);font-size:.875rem;font-weight:500}
|
|
228
|
+
.steps{list-style:none;display:grid;gap:1.25rem;margin-top:1.5rem;counter-reset:step}
|
|
229
|
+
.steps li{counter-increment:step}
|
|
230
|
+
.steps .label::before{content:counter(step,decimal-leading-zero) " ";color:var(--faint)}
|
|
231
|
+
.cmd{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-top:.5rem;padding:.5rem .5rem .5rem 1rem;border:1px solid var(--line);background:var(--surface)}
|
|
232
|
+
.cmd code{font-size:.8125rem;overflow-wrap:anywhere;user-select:all}
|
|
233
|
+
.cmd .prompt{color:var(--faint);user-select:none}
|
|
234
|
+
.sub-block{margin-top:2.25rem;padding-top:1.5rem;border-top:1px solid var(--line-soft)}
|
|
235
|
+
.sub-block>*+*{margin-top:.75rem}
|
|
236
|
+
.org-head h1{margin-top:.75rem;font-size:clamp(1.75rem,3.4vw,2.25rem)}
|
|
237
|
+
.org-title{display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem 1rem}
|
|
238
|
+
.tabs{display:flex;gap:1.75rem;margin-top:1.75rem;border-bottom:1px solid var(--line-soft);overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
|
|
239
|
+
.tabs::-webkit-scrollbar{display:none}
|
|
240
|
+
.tabs a{flex:none;margin-bottom:-1px;padding:.75rem 0 .6875rem;border-bottom:1px solid transparent;font-family:var(--mono);font-size:.75rem;color:var(--ink);text-decoration:none;white-space:nowrap;transition:color .15s,border-color .15s}
|
|
241
|
+
.tabs a:hover{color:var(--fg)}
|
|
242
|
+
.tabs a[aria-current]{color:var(--fg);border-bottom-color:var(--fg)}
|
|
243
|
+
@media (max-width:40rem){.tabs{gap:1.25rem;margin-inline:calc(-1 * var(--gutter));padding-inline:var(--gutter);-webkit-mask-image:linear-gradient(90deg,#000 80%,transparent);mask-image:linear-gradient(90deg,#000 80%,transparent)}
|
|
244
|
+
table.activity{min-width:0}table.activity colgroup,table.activity thead{display:none}
|
|
245
|
+
table.activity tr{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--line-soft)}
|
|
246
|
+
table.activity tr:last-child{border-bottom:0}
|
|
247
|
+
table.activity td{height:auto!important;padding:0!important;border:0}
|
|
248
|
+
table.activity td:last-child{grid-column:1/-1;white-space:normal!important}}
|
|
249
|
+
.tab-body>.block:first-child,.tab-body>.notice+.block,.tab-body>[role=alert]+.block{margin-top:2rem;padding-top:0;border-top:0}
|
|
250
|
+
.tab-body>.block~.block{margin-top:2.5rem}
|
|
251
|
+
.live-stamp .dot{animation:none;border-color:var(--faint);background:transparent}
|
|
252
|
+
[data-feed-state=open] .live-stamp .dot{border-color:var(--acc);background:var(--acc);animation:pulse 2.4s ease-in-out infinite}
|
|
253
|
+
.summary-head{margin-top:2rem}
|
|
254
|
+
.tiles{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:1px;margin-top:1rem;border:1px solid var(--line-soft);background:var(--line-soft)}
|
|
255
|
+
.tile{padding:1rem 1.25rem 1.125rem;background:var(--bg)}
|
|
256
|
+
.tile-label{font-family:var(--mono);font-size:.6875rem;letter-spacing:.08em;text-transform:uppercase;color:var(--dim)}
|
|
257
|
+
.tile-value{margin-top:.375rem;font-family:var(--mono);font-size:2rem;line-height:1.1;font-weight:500;letter-spacing:-.03em;color:var(--fg)}
|
|
258
|
+
.tile.on .tile-value{color:var(--acc)}
|
|
259
|
+
.tile .mini{margin-top:.25rem}
|
|
260
|
+
@media (max-width:56rem){.tiles{grid-template-columns:repeat(6,minmax(0,1fr))}.tile{grid-column:span 2}.tile:nth-child(n+4){grid-column:span 3}}
|
|
261
|
+
@media (max-width:30rem){.tiles{grid-template-columns:repeat(2,minmax(0,1fr))}.tile,.tile:nth-child(n+4){grid-column:auto}.tile:last-child{grid-column:span 2}.tile{padding:.875rem 1rem}.tile-value{font-size:1.625rem}}
|
|
262
|
+
.panels{display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);align-items:start;gap:1.25rem;margin-top:1.25rem}
|
|
263
|
+
@media (max-width:56rem){.panels{grid-template-columns:minmax(0,1fr)}}
|
|
264
|
+
.panel{display:flex;flex-direction:column;padding:1.125rem 1.25rem 1.25rem;border:1px solid var(--line-soft)}
|
|
265
|
+
.panel-head{display:flex;align-items:baseline;gap:1rem}
|
|
266
|
+
.panel h2{font-size:.9375rem}
|
|
267
|
+
.now-list{list-style:none;margin-top:.75rem}
|
|
268
|
+
.now-list li{padding:.625rem 0;border-top:1px solid var(--line-soft)}
|
|
269
|
+
.now-list li:first-child{border-top:0}
|
|
270
|
+
.now-who{display:flex;align-items:center;gap:.5rem;min-width:0}
|
|
271
|
+
.now-who .hicon{margin-right:0}
|
|
272
|
+
.now-who .mono{font-weight:500;color:var(--fg)}
|
|
273
|
+
.now-who .dot{margin-right:.375rem}
|
|
274
|
+
.now-what{margin-top:.125rem;padding-left:1.25rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
275
|
+
.now-what .role{margin-right:.125rem}
|
|
276
|
+
.more-line,.last-line{margin-top:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
277
|
+
.last-line{display:flex;gap:.375rem;padding-top:.75rem;border-top:1px solid var(--line-soft)}
|
|
278
|
+
.last-line .what{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
279
|
+
.last-line .mini-link{flex:none;margin-left:auto;padding-left:1rem}
|
|
280
|
+
.panel .empty{margin-top:.75rem}
|
|
281
|
+
.strip{display:grid;gap:.5rem;margin-top:1rem}
|
|
282
|
+
.strip-row{display:flex;align-items:center;gap:.625rem;min-width:0}
|
|
283
|
+
.strip-who{display:flex;flex:none;align-items:baseline;gap:.375rem;width:11rem;min-width:0;overflow:hidden;white-space:nowrap}
|
|
284
|
+
.strip-who .hicon{flex:none;margin-right:0}
|
|
285
|
+
.strip-who .mini{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
286
|
+
.strip-name{flex:0 0 auto;max-width:7.5rem;overflow:hidden;text-overflow:ellipsis}
|
|
287
|
+
.cells{display:flex;flex-wrap:wrap;gap:3px;min-width:0}
|
|
288
|
+
.cell{display:inline-block;width:10px;height:10px;border:1px solid var(--faint);background:transparent}
|
|
289
|
+
.cell.online{border-color:var(--acc);background:var(--acc)}
|
|
290
|
+
.cell.idle{border-color:var(--dim);background:var(--dim)}
|
|
291
|
+
.cell.idle.wake,.cell.hibernated{border-color:var(--acc);background:var(--acc-soft)}
|
|
292
|
+
.strip-key{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;margin:1rem 0 1.25rem}
|
|
293
|
+
.strip-key .cell{width:8px;height:8px;margin-left:.375rem}
|
|
294
|
+
.strip-key .cell:first-child{margin-left:0}
|
|
295
|
+
.fleet-line{margin-top:auto;padding-top:.875rem;border-top:1px solid var(--line-soft)}
|
|
296
|
+
.notice.held{margin-top:1rem}
|
|
297
|
+
.notice a{color:var(--fg)}
|
|
298
|
+
table.orgs td.end,table.orgs th.end{text-align:right}
|
|
299
|
+
.agents-cell{white-space:nowrap}
|
|
300
|
+
.harnesses{display:inline-flex;align-items:center;gap:.25rem;margin-right:.5rem;vertical-align:-1px}
|
|
301
|
+
.harnesses .hicon{margin-right:0}
|
|
302
|
+
.harnesses .more{line-height:1}
|
|
303
|
+
.foot-in{display:flex;flex-wrap:wrap;justify-content:space-between;gap:1rem;padding-block:2rem}
|
|
304
|
+
@media (prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}
|
|
305
|
+
`;var vs=30*U;var _s=["default-src 'none'","style-src 'self' https://fonts.googleapis.com","font-src https://fonts.gstatic.com","script-src 'self' https://challenges.cloudflare.com","frame-src https://challenges.cloudflare.com","connect-src 'self' https://challenges.cloudflare.com","img-src 'self' data:","form-action 'self'","frame-ancestors 'none'","base-uri 'none'"].join("; ");var L=8000;function O(e,t){if(!e)return"never";let n=Math.max(0,Math.round((t-Date.parse(e))/1000));if(n<5)return"just now";if(n<60)return`${n}s ago`;if(n<3600)return`${Math.floor(n/60)}m ago`;if(n<86400)return`${Math.floor(n/3600)}h ago`;return`${Math.floor(n/86400)}d ago`}function E(e){let[,t="",...n]=e.split("/");return{machine:t,handle:n.join("/")||t}}function X(e,t=!1){let n=new Map(e.machines.map((s)=>[s.name,{name:s.name,machine:s,endpoints:[],away:0}])),o=(s)=>{let l=n.get(s);if(!l)l={name:s,machine:null,endpoints:[],away:0},n.set(s,l);return l},i=new Map(e.endpoints.filter((s)=>s.kind==="agent").map((s)=>[s.address,s]));for(let s of i.values()){let l=s,d=new Set([s.address]);for(let p=t?i.get(l.parent??""):void 0;p&&!d.has(p.address);p=i.get(p.parent??""))d.add(p.address),l=p;let a=E(s.address).machine,c=t?E(l.address).machine:a;if(o(c).endpoints.push(s),c!==a)o(a).away++}return[...n.values()]}var x=(e)=>e.wake&&e.wake!=="none"?e.wake:null,bt=r`<svg class="bolt" viewBox="0 0 16 16" aria-hidden="true"><path d="M9.5 1 3 9h4.5L6.5 15 13 7H8.5z" fill="currentColor"/></svg>`;function G(e){if(e.startsWith("hook:"))return`a message runs its ${e.slice(5)} wake hook`;if(e==="spawn-resume")return"a message resumes it through its spawn";return`a message wakes it via ${e}`}function wt(e){let t=x(e),n=e.waking?"hibernated, its machine is waking":t?`${e.presence}, ${G(t)}`:e.presence==="idle"?"idle, reads messages at its next turn":e.presence;return{cell:r`<span class="dot ${e.presence}${t?" wake":""}" aria-hidden="true"></span><span class="mini">${e.waking?"waking":e.presence}</span>${t?r` <span class="wake-badge">${bt}<span class="sr">${G(t)}</span></span>`:""}`,tip:n}}var de={claude:'<path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z" fill="currentColor"/>',codex:'<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" fill="currentColor"/>',opencode:'<path d="M21.6 24H2.4V0h19.2zM16.8 4.8H7.2v14.4h9.6z" fill="currentColor"/><path d="M16.8 9.6H7.2v9.6h9.6z" fill="currentColor" opacity=".45"/>',gemini:'<path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81" fill="currentColor"/>',agy:'<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" fill="currentColor"/>',grok:'<g transform="translate(1.2 1.2) scale(.9)"><path d="M6.469 8.776 16.512 23h-4.464L2.005 8.776H6.47zm-.004 7.9 2.233 3.164L6.467 23H2l4.465-6.324zM22 2.582V23h-3.659V7.764L22 2.582zM22 1l-9.952 14.095-2.233-3.163L17.533 1H22z" fill="currentColor"/></g>',muse:'<g transform="translate(-1.2 -1.2) scale(1.1)"><path d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z" fill="currentColor"/></g>',pi:'<path d="M0 0h18v6H0zM0 6h6v18H0zM12 6h6v6h-6zM6 12h6v6H6zM18 12h6v12h-6z" fill="currentColor"/>',t3code:'<path d="M5.177,18.414V7.823H1V5.362H12.374V7.823H8.197V18.414H5.177ZM17.593,18.638C16.686,18.638 15.784,18.52 14.889,18.283C13.994,18.035 13.236,17.687 12.615,17.239L13.789,14.927C14.287,15.288 14.865,15.574 15.523,15.785C16.182,15.996 16.847,16.102 17.518,16.102C18.277,16.102 18.873,15.953 19.308,15.654C19.743,15.356 19.961,14.946 19.961,14.424C19.961,13.927 19.768,13.535 19.383,13.249C18.998,12.963 18.376,12.82 17.518,12.82H16.139V10.825L19.774,6.705L20.11,7.786H13.267V5.362H22.403V7.32L18.786,11.441L17.257,10.564H18.134C19.737,10.564 20.949,10.925 21.769,11.646C22.59,12.367 23,13.293 23,14.424C23,15.157 22.807,15.847 22.422,16.494C22.037,17.127 21.446,17.643 20.651,18.041C19.855,18.439 18.836,18.638 17.593,18.638Z" fill="currentColor"/>',generic:'<g transform="scale(1.5)"><rect x="1.75" y="2.75" width="12.5" height="10.5" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M4.5 6.25 6.5 8l-2 1.75M8 10h3" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></g>'},yt={"claude-code":"claude","claude-desktop":"claude","claude-cowork":"claude","claude-desktop-chat":"claude","codex-desktop":"codex",claude:"claude",codex:"codex",opencode:"opencode",gemini:"gemini",agy:"agy",antigravity:"agy",grok:"grok",muse:"muse",xai:"grok",pi:"pi",t3code:"t3code",t3:"t3code"},J=(e)=>yt[(e??"").toLowerCase()]??"generic",q=(e)=>{let t=J(e);return r`<svg class="hicon h-${t}" width="12" height="12" viewBox="0 0 24 24" role="img" aria-label="${e??"unknown harness"}">${new k(de[t])}</svg>`},vt=r`<svg width="8" height="8" viewBox="0 0 8 8" aria-hidden="true"><path d="M2 .75 6.5 4 2 7.25z" fill="currentColor"/></svg>`,V=(e)=>e.startsWith(ne)?"(ended)":e,S=(e)=>V(E(e).handle),_t=(e)=>Object.entries(e).filter(([t])=>t!=="machine"),Oe={online:0,idle:1,hibernated:1,offline:2},oe=(e,t)=>(Oe[e.presence]??3)-(Oe[t.presence]??3)||E(e.address).handle.localeCompare(E(t.address).handle);function xe(e){let t=new Set(e.map((a)=>a.address)),n=new Map,o=[];for(let a of e)if(a.parent&&a.parent!==a.address&&t.has(a.parent))n.set(a.parent,[...n.get(a.parent)??[],a]);else o.push(a);let i=[],s=new Set,l=(a,c)=>{for(let p of a.sort(oe)){if(s.has(p.address))continue;s.add(p.address),i.push({e:p,depth:c.length,anc:c,kids:0}),l(n.get(p.address)??[],[...c,p.address])}};l(o,[]),l(e.filter((a)=>!s.has(a.address)),[]);let d=new Map(i.map((a)=>[a.e.address,a]));for(let a of i)if(a.anc.length)d.get(a.anc.at(-1)).kids++;return i}var ae=(e)=>e.presence!=="offline";function kt(e){let t=e.filter((a)=>a.kind==="agent"),n=new Set(t.map((a)=>a.address)),o=new Map,i=[];for(let a of t)if(a.parent&&a.parent!==a.address&&n.has(a.parent))o.set(a.parent,[...o.get(a.parent)??[],a]);else i.push(a);let s=new Set,l=(a)=>{s.add(a.address);let c=[],p=[...o.get(a.address)??[]];while(p.length){let b=p.pop();if(s.has(b.address))continue;s.add(b.address),c.push(b),p.push(...o.get(b.address)??[])}return{root:a,kids:c.filter(ae).sort(oe)}},d=i.map(l);for(let a of t)if(!s.has(a.address))d.push(l(a));return d.filter((a)=>ae(a.root)||a.kids.length).sort((a,c)=>c.kids.length-a.kids.length||oe(a.root,c.root))}var Rt=3,le=(e)=>r`<span class="cell ${e.presence}${x(e)?" wake":""}" title="${S(e.address)}, ${e.presence}"></span>`;function De({root:e,kids:t}){let{machine:n}=E(e.address);return r`<div class="strip-row"><span class="strip-who" title="${e.address}">${q(e.harness)}<span class="mono strip-name">${S(e.address)}</span><span class="mini">${n}</span></span>
|
|
306
|
+
<span class="cells">${ae(e)?le(e):""}${t.map(le)}</span>${t.length?r`<span class="mini push">${t.length}</span>`:""}</div>`}function St(e){let t=e.filter((n)=>!n.kids.length);if(t.length<=Rt)return e.map(De);return[...e.filter((n)=>n.kids.length).map(De),r`<div class="strip-row"><span class="strip-who"><span class="mono strip-name">${t.length} solo agents</span></span>
|
|
307
|
+
<span class="cells">${t.map((n)=>le(n.root))}</span></div>`]}function Me(e,t){return r`<div class="strip" aria-label="Agents per orchestrator">${St(kt(e.endpoints))}</div><p class="mini strip-key"><span class="cell online"></span>online <span class="cell idle wake"></span>wakeable <span class="cell idle"></span>next turn <a class="mini-link push" href="/orgs/${t}/graph">Graph</a></p>`}function Tt({e,depth:t,anc:n,kids:o},i,s,l){let d=S(e.address),{machine:a}=E(e.address),c=e.parent?S(e.parent):null,p=_t(e.labels),b=e.status?e.status.note?`${e.status.state}: ${e.status.note}`:e.status.state:"none",st=e.presence==="online"?"now":O(e.last_seen,s),ve=wt(e),it=o?r`<button class="caret" type="button" data-tree="${e.address}" aria-expanded="${String(!i.has(e.address))}" aria-label="${o} under ${d}">${vt}</button>`:r`<span class="caret" aria-hidden="true"></span>`,rt=[d,e.harness??"unknown harness",c?`sub of ${c}`:""].filter(Boolean).join(", ");return r`<tr${t?r` class="sub d${Math.min(t,4)}" data-anc="${n.join(" ")}"${n.some((C)=>i.has(C))?" hidden":""}`:""}>
|
|
308
|
+
<td class="agent" title="${rt}">${it}${q(e.harness)}<span class="mono">${d}</span>${a!==l?r` <span class="tag" title="runs on ${a}">${a}</span>`:""}${c&&!t?r` <span class="mini">sub of ${c}</span>`:""}</td>
|
|
309
|
+
<td class="small" title="${e.title??""}">${e.title??""}</td>
|
|
310
|
+
<td title="${ve.tip}">${ve.cell}</td>
|
|
311
|
+
<td title="${b}">${e.status?r`<span class="role">${e.status.state}</span>${e.status.note?r` <span class="small">${e.status.note}</span>`:""}`:r`<span class="mini">none</span>`}</td>
|
|
312
|
+
<td title="${p.map(([C,Q])=>`${C}=${Q}`).join(", ")}">${p.map(([C,Q])=>r`<span class="tag">${C}=${Q}</span>`)}</td>
|
|
313
|
+
<td class="mono end" title="${e.last_seen??"never"}">${st}</td>
|
|
314
|
+
</tr>`}var Nt=r`<colgroup><col class="c-agent"><col class="c-title"><col class="c-presence"><col class="c-status"><col class="c-labels"><col class="c-seen"></colgroup>`,Ie=(e,t,n,o)=>{let i=new Set(e.filter((s)=>s.kids&&s.e.presence==="offline").map((s)=>s.e.address));return r`<div class="table"><table class="agents">${Nt}${n?r`<thead><tr><th>Agent</th><th>Title</th><th>Presence</th><th>Status</th><th>Labels</th><th class="end">Seen</th></tr></thead>`:""}<tbody>${e.map((s)=>Tt(s,i,t,o))}</tbody></table></div>`};function At(e,t){let n=e.machine,o=n?n.online?"online":"offline":e.endpoints.some((l)=>l.presence==="online")?"online":"offline",i=xe(e.endpoints.filter((l)=>l.presence!=="offline")),s=xe(e.endpoints.filter((l)=>l.presence==="offline"));return r`<div class="machine">
|
|
315
|
+
<div class="machine-head">
|
|
316
|
+
<span class="dot ${o}" aria-hidden="true"></span><h3 class="mono">${e.name}</h3>
|
|
317
|
+
${n?r`<span class="role">${n.kind}</span>`:""}
|
|
318
|
+
<span class="mini">${o==="online"?"online":`seen ${O(n?.last_seen??null,t)}`}</span>
|
|
319
|
+
<span class="mini push">${e.endpoints.length} ${e.endpoints.length===1?"agent":"agents"}</span>
|
|
320
|
+
</div>
|
|
321
|
+
${e.endpoints.length?r`${i.length?Ie(i,t,!0,e.name):""}${s.length?r`<details class="offline" data-key="${e.name}"><summary class="mini"><span class="when-shut">show</span><span class="when-open">hide</span> ${s.length} offline</summary>${Ie(s,t,!i.length,e.name)}</details>`:""}`:r`<p class="empty">${e.away?`Its ${e.away===1?"agent shows":"agents show"} under a parent on another machine.`:"No agents on this machine right now."}</p>`}
|
|
322
|
+
</div>`}function z(e){switch(e.kind){case"session_start":return"started a session";case"session_end":return"ended its session";case"stop":return e.summary?`finished a turn: ${e.summary}`:"finished a turn";case"tool":return[e.tool,e.summary].filter(Boolean).join(": ")||"used a tool";default:return e.summary??e.kind}}var ce=(e)=>[...e].sort((t,n)=>n.at.localeCompare(t.at));function pe(e,t){return r`<section class="block"><div class="block-head"><h2>Recent activity</h2>${Y(t)}</div>
|
|
323
|
+
${e.length?r`<div class="table"><table class="activity"><colgroup><col class="c-when"><col class="c-who"><col></colgroup><thead><tr><th>When</th><th>Agent</th><th>What</th></tr></thead>
|
|
324
|
+
<tbody>${ce(e).map((n)=>{let{machine:o,handle:i}=E(n.endpoint),s=`${o}/${V(i)}`,l=z(n);return r`<tr><td class="mono" title="${n.at}">${O(n.at,t)}</td><td class="mono" title="${n.agent?`${s} ${n.agent}`:s}">${s}${n.agent?r` <span class="mini">${n.agent}</span>`:""}</td><td class="small" title="${l}">${l}</td></tr>`})}</tbody></table></div>`:r`<p class="empty">No activity yet. Hooks report tool use and turns as agents work.</p>`}</section>`}var j=(e)=>r`<div class="cmd"><code><span class="prompt" aria-hidden="true">$ </span>${e}</code><button class="btn btn-ghost btn-small" type="button" data-copy="${e}">Copy</button></div>`,Y=(e)=>r`<span class="mini push live-stamp"><span class="dot online pulse" aria-hidden="true"></span><span data-feed>polling</span>, updated ${new Date(e).toISOString().slice(11,19)} UTC</span>`;function ue(e){return r`<section class="block onboard"><h2>Connect your first machine</h2>
|
|
325
|
+
<p class="small">No machines have joined this hub yet. Run these on the machine your agents work on. The page updates on its own once it connects.</p>
|
|
326
|
+
<ol class="steps">
|
|
327
|
+
<li><p class="label">Install the CLI</p>${j("npm i -g @scp-dev/cli")}</li>
|
|
328
|
+
<li><p class="label">Sign in, link this machine, install hooks and MCP</p>${j("swarm up")}</li>
|
|
329
|
+
<li><p class="label">Start a named orchestrator that wakes on peer messages</p>${j("swarm claude --name lead")}</li>
|
|
330
|
+
</ol>
|
|
331
|
+
<div class="sub-block">
|
|
332
|
+
<h3>Use an API token for ambient agents</h3>
|
|
333
|
+
<p class="small">An agent that runs unattended, on a server or in CI, signs in with an org API token instead of a device login. Create one under <a href="/orgs/${e}/tokens">Tokens and devices</a>, then:</p>
|
|
334
|
+
${j("swarm up --token <token> --machine <name>")}
|
|
335
|
+
<p class="small">Or set <code>SCP_TOKEN</code> in its environment. Its agents show the label <span class="tag">token=<name></span>.</p>
|
|
336
|
+
</div>
|
|
337
|
+
</section>`}var P=()=>v("The hub did not answer. This page retries on its own.",!0)||r``;function he(e,t,n){if(!e)return P();let o=X(e,!0);if(!o.length)return ue(t);let i=o.reduce((l,d)=>l+d.endpoints.length,0),s=o.filter((l)=>l.machine?.online||l.endpoints.some((d)=>d.presence==="online")).length;return r`<section class="block"><div class="block-head"><h2>Machines</h2>
|
|
338
|
+
<span class="mini">${o.length} ${o.length===1?"machine":"machines"}, ${s} online, ${i} ${i===1?"agent":"agents"}</span>
|
|
339
|
+
${Y(n)}</div>
|
|
340
|
+
${o.map((l)=>At(l,n))}
|
|
341
|
+
</section>`}var K=`
|
|
342
|
+
const chosen = new Map();
|
|
343
|
+
const fold = (root) => {
|
|
344
|
+
for (const b of root.querySelectorAll("[data-tree]")) {
|
|
345
|
+
const v = chosen.get(b.getAttribute("data-tree"));
|
|
346
|
+
if (v) b.setAttribute("aria-expanded", v);
|
|
347
|
+
}
|
|
348
|
+
const shut = new Set([...root.querySelectorAll('[data-tree][aria-expanded="false"]')].map((b) => b.getAttribute("data-tree")));
|
|
349
|
+
for (const r of root.querySelectorAll("tr[data-anc]")) r.hidden = r.getAttribute("data-anc").split(" ").some((a) => shut.has(a));
|
|
350
|
+
};
|
|
351
|
+
document.addEventListener("click", (e) => {
|
|
352
|
+
const t = e.target instanceof Element ? e.target.closest("[data-tree]") : null;
|
|
353
|
+
if (!t) return;
|
|
354
|
+
chosen.set(t.getAttribute("data-tree"), t.getAttribute("aria-expanded") === "true" ? "false" : "true");
|
|
355
|
+
fold(document);
|
|
356
|
+
});
|
|
357
|
+
document.addEventListener("click", async (e) => {
|
|
358
|
+
const b = e.target instanceof Element ? e.target.closest("[data-copy]") : null;
|
|
359
|
+
if (!b) return;
|
|
360
|
+
try {
|
|
361
|
+
await navigator.clipboard.writeText(b.getAttribute("data-copy"));
|
|
362
|
+
b.textContent = "Copied";
|
|
363
|
+
setTimeout(() => { b.textContent = "Copy"; }, 1500);
|
|
364
|
+
} catch {}
|
|
365
|
+
});
|
|
366
|
+
const live = document.querySelector("[data-live]");
|
|
367
|
+
if (live) {
|
|
368
|
+
const every = Number(live.getAttribute("data-live")) || 8000;
|
|
369
|
+
let timer = 0;
|
|
370
|
+
const later = (ms) => { clearTimeout(timer); timer = setTimeout(tick, ms); };
|
|
371
|
+
let soon = false;
|
|
372
|
+
const tick = async () => {
|
|
373
|
+
soon = false;
|
|
374
|
+
const selecting = String(window.getSelection() || "").length > 0;
|
|
375
|
+
if (!document.hidden && !selecting) {
|
|
376
|
+
try {
|
|
377
|
+
let next;
|
|
378
|
+
if (typeof window.scpRegion === "function") next = await window.scpRegion();
|
|
379
|
+
else {
|
|
380
|
+
const res = await fetch(location.href, { credentials: "same-origin", headers: { accept: "text/html" } });
|
|
381
|
+
next = res.ok ? new DOMParser().parseFromString(await res.text(), "text/html").querySelector("[data-live]") : null;
|
|
382
|
+
}
|
|
383
|
+
if (next) {
|
|
384
|
+
const open = new Set([...live.querySelectorAll("details[open][data-key]")].map((d) => d.getAttribute("data-key")));
|
|
385
|
+
for (const d of next.querySelectorAll("details[data-key]")) if (open.has(d.getAttribute("data-key"))) d.open = true;
|
|
386
|
+
fold(next);
|
|
387
|
+
live.replaceChildren(...next.childNodes);
|
|
388
|
+
}
|
|
389
|
+
} catch {}
|
|
390
|
+
}
|
|
391
|
+
later(document.documentElement.getAttribute("data-stream") === "open" ? every * 4 : every);
|
|
392
|
+
};
|
|
393
|
+
document.addEventListener("scp:refresh", () => { if (!soon) { soon = true; later(250); } });
|
|
394
|
+
later(every);
|
|
395
|
+
const path = live.getAttribute("data-events");
|
|
396
|
+
if (path && !document.querySelector("[data-graph]") && typeof WebSocket === "function") {
|
|
397
|
+
const state = (open) => {
|
|
398
|
+
document.documentElement.setAttribute("data-feed-state", open ? "open" : "poll");
|
|
399
|
+
for (const s of document.querySelectorAll("[data-feed]")) s.textContent = open ? "live" : "polling";
|
|
400
|
+
};
|
|
401
|
+
let backoff = 1000;
|
|
402
|
+
const connect = () => {
|
|
403
|
+
let ws, ping = 0;
|
|
404
|
+
try { ws = new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + location.host + path); } catch { return; }
|
|
405
|
+
ws.onopen = () => { backoff = 1000; state(true); ping = setInterval(() => { try { ws.send("ping"); } catch {} }, 30000); };
|
|
406
|
+
ws.onmessage = (m) => { if (typeof m.data === "string" && m.data !== "pong" && !m.data.includes('"hello"')) document.dispatchEvent(new CustomEvent("scp:refresh")); };
|
|
407
|
+
ws.onclose = (c) => {
|
|
408
|
+
clearInterval(ping);
|
|
409
|
+
state(false);
|
|
410
|
+
if (c.code === 4003) return;
|
|
411
|
+
setTimeout(connect, backoff);
|
|
412
|
+
backoff = Math.min(backoff * 2, 30000);
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
new MutationObserver(() => state(document.documentElement.getAttribute("data-feed-state") === "open")).observe(live, { childList: true });
|
|
416
|
+
connect();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const tab = document.querySelector('.tabs [aria-current="page"]');
|
|
420
|
+
if (tab) tab.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
421
|
+
`;var Ys=7*U;var $e=[["overview","Overview",""],["activity","Activity","/activity"],["sessions","Sessions","/sessions"],["graph","Graph","/graph"],["tokens","Tokens and devices","/tokens"],["members","Members","/members"]],Mt=(e,t,n)=>r`<nav class="tabs" aria-label="${e}">${t.map(([o,i])=>r`<a href="${o}"${o===n?r` aria-current="page"`:""}>${i}</a>`)}</nav>`;function ge(e,t){let n=`/orgs/${e.org.handle}`;return r`<div class="org-head">
|
|
422
|
+
<p class="label"><a class="mini-link" href="/orgs">Orgs</a> / ${e.org.handle}</p>
|
|
423
|
+
<div class="org-title"><h1>${e.org.name}</h1><span class="mini">you are <span class="role">${e.role}</span></span></div>
|
|
424
|
+
</div>
|
|
425
|
+
${Mt("Organization",$e.map(([,o,i])=>[n+i,o]),n+$e.find(([o])=>o===t)[2])}`}function Ue(e,t,n,o={}){let i=r`<script src="/app.js?v=${R(K)}" defer></script>`,s=o.live?r`${i}<noscript><meta http-equiv="refresh" content="${Math.round(L/1000)}"></noscript>`:i,l=o.live?r`<div class="tab-body" data-live="${L}" data-events="/orgs/${e.org.handle}/events">${n}</div>`:r`<div class="tab-body">${n}</div>`;return{head:s,nav:"orgs",body:r`${ge(e,t)}${l}`}}var v=(e,t=!1)=>e?r`<p class="notice${t?" err":""}" role="${t?"alert":"status"}">${e}</p>`:"";function Ut(e,t){let n=e.endpoints.filter((s)=>s.kind==="agent"),o=n.filter((s)=>s.presence==="idle"),i=X(e);return{online:n.filter((s)=>s.presence==="online").length,wakeable:o.filter((s)=>x(s)).length,nextTurn:o.filter((s)=>!x(s)).length,offline:n.filter((s)=>s.presence==="offline").length,machines:i.length,machinesOnline:i.filter((s)=>s.machine?.online||s.endpoints.some((l)=>l.presence==="online")).length,messages:t?t.edges.filter((s)=>s.kind==="message").reduce((s,l)=>s+(l.count??0),0):null,minutes:t?.minutes??15}}var W=(e,t,n,o="")=>r`<div class="tile${o?` ${o}`:""}"><p class="tile-label">${e}</p><p class="tile-value">${t}</p><p class="mini">${n}</p></div>`;function Ht(e){return r`<div class="tiles">
|
|
426
|
+
${W("Online",e.online,e.online===1?"agent working":"agents working","on")}
|
|
427
|
+
${W("Idle, wakeable",e.wakeable,"a message wakes them")}
|
|
428
|
+
${W("Idle, next turn",e.nextTurn,"read at their next turn")}
|
|
429
|
+
${W("Machines",e.machines,`${e.machinesOnline} online`)}
|
|
430
|
+
${W("Messages",e.messages??"-",`last ${e.minutes} min`)}
|
|
431
|
+
</div>`}function qt(e,t){if(e.status)return{lead:r`<span class="role">${e.status.state}</span>`,text:e.status.note??""};let n=t.get(e.address);if(n?.summary)return{lead:r`<span class="mini">tl;dr</span>`,text:n.summary};if(e.title)return{lead:"",text:e.title};return null}var me=6,qe={online:0,idle:1,hibernated:1,offline:2};function Pt(e,t,n){let o=new Map,i=ce(e.activity);for(let a of i)if(a.kind==="stop"&&a.summary&&!o.has(a.endpoint))o.set(a.endpoint,a);let s=e.endpoints.filter((a)=>a.kind==="agent"&&a.presence!=="offline").map((a)=>({e:a,g:qt(a,o)})).sort((a,c)=>(qe[a.e.presence]??3)-(qe[c.e.presence]??3)||Number(!a.g)-Number(!c.g)||(c.e.last_seen??"").localeCompare(a.e.last_seen??"")),l=s.slice(0,me),d=i[0];return r`<section class="panel now"><div class="panel-head"><h2>Happening now</h2><a class="mini-link push" href="/orgs/${n}/sessions">All sessions</a></div>
|
|
432
|
+
${l.length?r`<ul class="now-list">${l.map(({e:a,g:c})=>{let p=x(a);return r`<li><div class="now-who">${q(a.harness)}<span class="mono">${S(a.address)}</span><span class="mini">${E(a.address).machine}</span><span class="push mini"><span class="dot ${a.presence}${p?" wake":""}" aria-hidden="true"></span>${a.presence}</span></div>
|
|
433
|
+
<p class="now-what small" title="${c?.text??""}">${c?r`${c.lead}${c.lead&&c.text?" ":""}${c.text}`:r`<span class="mini">no status yet</span>`}</p></li>`})}</ul>${s.length>me?r`<p class="mini more-line">and ${s.length-me} more on the <a class="mini-link" href="/orgs/${n}/sessions">Sessions</a> tab</p>`:""}`:r`<p class="empty">No agent is online or idle right now.</p>`}
|
|
434
|
+
${d?r`<p class="mini last-line"><span class="what" title="${z(d)}">Last activity ${O(d.at,t)}: ${S(d.endpoint)}, ${z(d)}</span><a class="mini-link" href="/orgs/${n}/activity">All activity</a></p>`:""}
|
|
435
|
+
</section>`}var F=(e,t,n=`${t}s`)=>`${e} ${e===1?t:n}`;function Pe(e,t,n,o,i){if(!e)return P();if(!e.machines.length&&!e.endpoints.length)return ue(o);let s=Ut(e,t),l=n.holds?r`<p class="notice held" role="status">${F(n.holds,"message")} held by the relay judge, waiting for review. <a href="/orgs/${o}/members#judge">Review</a></p>`:"";return r`<div class="block-head summary-head"><p class="mini">${F(s.online+s.wakeable+s.nextTurn,"agent")} live, ${s.offline} offline</p>${Y(i)}</div>
|
|
436
|
+
${l}
|
|
437
|
+
${Ht(s)}
|
|
438
|
+
<div class="panels">
|
|
439
|
+
${Pt(e,i,o)}
|
|
440
|
+
<section class="panel"><div class="panel-head"><h2>Fleet</h2></div>
|
|
441
|
+
${Me(e,o)}
|
|
442
|
+
<p class="small fleet-line">${F(s.machines,"machine")}, ${s.machinesOnline} online. ${F(n.devices,"device")} and ${F(n.tokens,"API token")} signed in. <a class="mini-link" href="/orgs/${o}/tokens">Manage</a></p>
|
|
443
|
+
</section>
|
|
444
|
+
</div>`}var Wt=["claude","codex","opencode","gemini","agy","grok","muse","pi","t3code"],Ft='<g transform="scale(1.5)"><circle cx="8" cy="5.25" r="2.75" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M2.75 14c.5-3 2.6-4.75 5.25-4.75S12.75 11 13.25 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></g>',Bt='<path d="M3 3h10v4H7v6H3zM29 29H19v-4h6v-6h4z"/><rect x="11" y="11" width="10" height="10"/>';function We(e){let t=e.split("/");return t.length>=3?{machine:t[1],handle:t.slice(2).join("/")}:{machine:"",handle:""}}function jt(e,t){if(!t||e.length<=t.length+1||!e.startsWith(t))return e;return"-_./:".includes(e[t.length])?e.slice(t.length+1):e}function Fe(e){if(!e)return r``;let t=e.nodes.map((i)=>{let{machine:s,handle:l}=We(i.address),d=i.kind==="human"?"human":V(l),a=i.parent?We(i.parent):null;return{address:i.address,kind:i.kind,machine:s,name:d,label:d===l&&a&&a.machine===s?jt(l,a.handle):d,icon:i.kind==="human"?"human":J(i.harness),harness:i.harness,title:i.title,presence:i.presence,status:i.status,wake:i.wake&&i.wake!=="none"?i.wake:null,how:i.waking?"its machine is waking":i.wake&&i.wake!=="none"?G(i.wake):null,parent:i.parent,historic:i.historic===!0}}),n=e.machines.filter((i)=>i.kind!=="revoked").map((i)=>({name:i.name,online:i.online,identity:i.identity??null})),o=JSON.stringify({minutes:e.minutes,machines:n,nodes:t,edges:e.edges}).replace(/</g,"\\u003c");return r`<script type="application/json" data-graph-data>${new k(o)}</script>`}var Be=r`<div class="graph-canvas"><svg class="graph-svg" role="group" aria-label="Agents by machine"></svg><div class="graph-tip" role="tooltip" hidden></div></div>`,je=r`<span class="trails" role="group" aria-label="Message trail speed">trails <span class="seg">${["1","0.5","0.25"].map((e)=>r`<button type="button" data-trail="${e}" aria-pressed="${e==="1"?"true":"false"}">${e}x</button>`)}</span></span>`,Ge=(e)=>r`<label class="mini"><input type="checkbox" data-graph-nextturn> show next-turn idle</label><label class="mini"><input type="checkbox" data-graph-offline> show offline</label>${e?r`<label class="mini"><input type="checkbox" data-graph-historic> show historic</label>`:""}`,Xe=r`<p class="mini graph-legend"><span class="key spawn" aria-hidden="true"></span>spawned <span class="key msg" aria-hidden="true"></span>messages, last <span data-graph-minutes>15</span> min</p>`,fe=()=>r`<script src="/app.js?v=${R(K)}" defer></script><script src="/graph.js?v=${R(Kt())}" defer></script><link rel="stylesheet" href="/graph.css?v=${R(Gt)}">`;function Je(e,t,n={}){return r`<div class="graph gfull" data-graph data-full data-events="/orgs/${e.handle}/events" aria-label="Live graph of agents and their links">
|
|
445
|
+
${Be}
|
|
446
|
+
<header class="g-over g-left">
|
|
447
|
+
<a class="mini-link" href="/orgs/${e.handle}/graph">Back to the dashboard</a>
|
|
448
|
+
<h1 title="${e.name}">${e.name}</h1>
|
|
449
|
+
<p class="mini" data-graph-counts></p>
|
|
450
|
+
<p class="mini" data-graph-stream></p>
|
|
451
|
+
</header>
|
|
452
|
+
<div class="g-over g-right">
|
|
453
|
+
${Xe}
|
|
454
|
+
${Ge(n.historic===!0)}
|
|
455
|
+
<span class="mini">${je}</span>
|
|
456
|
+
</div>
|
|
457
|
+
<div hidden data-live="${L}">${Fe(t)}</div>
|
|
458
|
+
${t?"":r`<p class="g-empty mini">The hub did not answer. This page retries on its own.</p>`}
|
|
459
|
+
</div>`}function Ve(e,t,n={}){return r`${ge(e,"graph")}
|
|
460
|
+
<div class="graph gfull gtab" data-graph data-full data-events="/orgs/${e.org.handle}/events" aria-label="Live graph of agents and their links">
|
|
461
|
+
${Be}
|
|
462
|
+
<div class="g-over g-left">
|
|
463
|
+
<p class="mini" data-graph-counts></p>
|
|
464
|
+
<p class="mini" data-graph-stream></p>
|
|
465
|
+
</div>
|
|
466
|
+
<div class="g-over g-right">
|
|
467
|
+
${Xe}
|
|
468
|
+
<span class="g-acts">${Ge(n.historic===!0)}<a class="mini-link" href="/orgs/${e.org.handle}/graph/full">Full screen</a></span>
|
|
469
|
+
<span class="mini">${je}</span>
|
|
470
|
+
</div>
|
|
471
|
+
<div hidden data-live="${L}">${Fe(t)}</div>
|
|
472
|
+
${t?"":r`<p class="g-empty mini">The hub did not answer. This page retries on its own.</p>`}
|
|
473
|
+
</div>`}var Gt=`
|
|
474
|
+
:root{--id-0:#2f6fdb;--id-1:#c2410c;--id-2:#0f8a6c;--id-3:#a3369e;--id-4:#a16207;--id-5:#6d4fd6;--id-6:#be123c;--id-7:#3f7d20}
|
|
475
|
+
@media (prefers-color-scheme:dark){:root{--id-0:#7aa9ff;--id-1:#f59a63;--id-2:#3fc6a0;--id-3:#e27ad9;--id-4:#e3b341;--id-5:#a991ff;--id-6:#fb7185;--id-7:#8fd16a}}
|
|
476
|
+
.g-over label{display:inline-flex;align-items:center;gap:.375rem;cursor:pointer}
|
|
477
|
+
.trails{display:inline-flex;align-items:center;gap:.375rem}
|
|
478
|
+
.trails .seg{display:inline-flex;border:1px solid var(--line)}
|
|
479
|
+
.trails .seg button{height:1.375rem;padding-inline:.4375rem;border:0;background:transparent;color:var(--ink);font-family:var(--mono);font-size:.625rem;cursor:pointer}
|
|
480
|
+
.trails .seg button+button{border-left:1px solid var(--line)}
|
|
481
|
+
.trails .seg button[aria-pressed=true]{background:var(--fg);color:var(--bg)}
|
|
482
|
+
.graph{margin-top:1.25rem}
|
|
483
|
+
.graph-legend{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem}
|
|
484
|
+
.graph-legend .key{display:inline-block;width:1.25rem;height:0;margin-left:.5rem;border-top:1.5px solid var(--faint)}
|
|
485
|
+
.graph-legend .key:first-child{margin-left:0}
|
|
486
|
+
.graph-legend .key.msg{border-top-color:var(--acc)}
|
|
487
|
+
[data-graph-stream]:empty{display:none}
|
|
488
|
+
[data-graph-stream]::before{content:"";display:inline-block;width:6px;height:6px;margin-right:.375rem;vertical-align:1px;border:1px solid var(--faint)}
|
|
489
|
+
[data-stream=open] [data-graph-stream]::before{border-color:var(--acc);background:var(--acc);animation:pulse 2.4s ease-in-out infinite}
|
|
490
|
+
.graph-canvas{position:relative;margin-top:.75rem;overflow:hidden;border:1px solid var(--line-soft);background:var(--bg)}
|
|
491
|
+
.graph-svg{display:block;width:100%;height:420px;font-family:var(--mono);user-select:none;-webkit-user-select:none}
|
|
492
|
+
.graph-svg .hull{fill:var(--surface);stroke:var(--surface);stroke-linejoin:round;stroke-linecap:round}
|
|
493
|
+
.graph-svg .hull-rim{fill:none;stroke:var(--line-soft);stroke-linejoin:round;stroke-linecap:round}
|
|
494
|
+
.graph-svg .region.ided .hull-rim{stroke:color-mix(in srgb,var(--id) 30%,var(--line-soft))}
|
|
495
|
+
.graph-svg .region.offline,.graph-svg .region-head.offline{opacity:.55}
|
|
496
|
+
.graph-svg .mlabel{font-size:11px;font-weight:500;fill:var(--fg);paint-order:stroke;stroke:var(--surface);stroke-width:5px;stroke-linejoin:round}
|
|
497
|
+
.graph-svg .mlabel .n{fill:var(--dim);font-weight:400}
|
|
498
|
+
.graph-svg .mid{font-size:10px;fill:color-mix(in srgb,var(--id) 40%,var(--dim));paint-order:stroke;stroke:var(--surface);stroke-width:5px;stroke-linejoin:round}
|
|
499
|
+
.graph-svg .mdot{fill:var(--bg);stroke:var(--faint)}
|
|
500
|
+
.graph-svg .mdot.online{fill:var(--acc);stroke:var(--acc)}
|
|
501
|
+
.graph-svg .empty-note{font-size:10.5px;fill:var(--faint)}
|
|
502
|
+
.graph-svg .hub circle{fill:var(--bg);stroke:var(--line)}
|
|
503
|
+
.graph-svg .hub svg{color:var(--dim)}
|
|
504
|
+
.graph-svg .hub text{font-size:10.5px;fill:var(--dim);text-anchor:middle;paint-order:stroke;stroke:var(--bg);stroke-width:5px;stroke-linejoin:round}
|
|
505
|
+
.graph-svg .edge{fill:none;stroke-linecap:round;transition:opacity calc(.25s / var(--trail,1))}
|
|
506
|
+
.graph-svg .edge.spawn{stroke:var(--faint);stroke-width:1.25}
|
|
507
|
+
.graph-svg .edge.msg{stroke:var(--acc)}
|
|
508
|
+
.graph-svg .edge.msg.live{opacity:1!important}
|
|
509
|
+
.graph-svg .arrow{fill:var(--acc)}
|
|
510
|
+
.graph-svg .pulse{fill:var(--acc)}
|
|
511
|
+
.graph-svg .pulse-halo{fill:var(--acc);opacity:.25}
|
|
512
|
+
.graph-svg .pulse.reply,.graph-svg .pulse-halo.reply{fill:var(--fg)}
|
|
513
|
+
.graph-svg .node{cursor:pointer;outline:none;transition:opacity .3s}
|
|
514
|
+
.graph-svg .node .disc{fill:var(--bg);stroke:var(--line);stroke-width:1;transition:stroke .2s}
|
|
515
|
+
.graph-svg .node.online .disc{stroke:color-mix(in srgb,var(--fg) 34%,transparent)}
|
|
516
|
+
.graph-svg .node.human .disc{stroke-dasharray:2.5 2}
|
|
517
|
+
.graph-svg .node.offline{opacity:.5}
|
|
518
|
+
.graph-svg .node.historic{opacity:.3}
|
|
519
|
+
.graph-svg .node.offline .disc{stroke-dasharray:2 2}
|
|
520
|
+
.graph-svg .node.enter{opacity:0}
|
|
521
|
+
.graph-svg .node:hover .disc,.graph-svg .node:focus-visible .disc,.graph-svg .node.on .disc{stroke:var(--fg);stroke-width:1.5}
|
|
522
|
+
.graph-svg .ring{fill:none;stroke:var(--acc);stroke-width:2;opacity:0;transform-box:fill-box;transform-origin:center}
|
|
523
|
+
.graph-svg .node.hit .ring{animation:graph-hit .7s ease-out}
|
|
524
|
+
@keyframes graph-hit{0%{opacity:.9;transform:scale(1)}100%{opacity:0;transform:scale(1.55)}}
|
|
525
|
+
.graph-svg .glyph{color:var(--ink);overflow:visible}
|
|
526
|
+
.graph-svg .pdot{stroke:var(--bg);stroke-width:2;fill:var(--faint)}
|
|
527
|
+
.graph-svg .pdot.online{fill:var(--acc)}
|
|
528
|
+
.graph-svg .pdot.idle{fill:var(--dim)}
|
|
529
|
+
.graph-svg .bolt-g{color:var(--acc)}
|
|
530
|
+
.graph-svg .bolt-g circle{fill:var(--bg)}
|
|
531
|
+
.graph-svg .nlabel{font-size:11px;fill:var(--fg);text-anchor:middle;paint-order:stroke;stroke:var(--surface);stroke-width:5px;stroke-linejoin:round}
|
|
532
|
+
.graph-svg .node.offline .nlabel{fill:var(--dim)}
|
|
533
|
+
.graph-svg.focus .edge:not(.hl),.graph-svg.focus .node:not(.hl){opacity:.12!important}
|
|
534
|
+
.graph-svg.focus .edge.hl.spawn{stroke:var(--ink)}
|
|
535
|
+
.graph-tip{position:absolute;z-index:3;width:max-content;max-width:20rem;padding:.625rem .75rem;border:1px solid var(--line);background:var(--bg);font-size:.75rem;line-height:1.45;pointer-events:none;box-shadow:0 6px 24px rgb(0 0 0/.1)}
|
|
536
|
+
.graph-tip .t-head{display:flex;align-items:center;gap:.4rem;font-family:var(--mono);font-size:.8125rem;font-weight:500;color:var(--fg)}
|
|
537
|
+
.graph-tip .t-head svg{width:13px;height:13px;flex:none;overflow:visible;color:var(--ink)}
|
|
538
|
+
.graph-tip .t-addr{margin-top:.125rem;font-family:var(--mono);font-size:.6875rem;color:var(--dim);overflow-wrap:anywhere}
|
|
539
|
+
.graph-tip .t-row{margin-top:.375rem;color:var(--ink)}
|
|
540
|
+
.graph-tip .t-kv{display:grid;grid-template-columns:auto 1fr;gap:.125rem .75rem;margin-top:.5rem;font-family:var(--mono);font-size:.6875rem}
|
|
541
|
+
.graph-tip .t-kv dt{color:var(--dim)}
|
|
542
|
+
.graph-tip .t-kv dd{color:var(--fg);overflow-wrap:anywhere}
|
|
543
|
+
body.bare{overflow:hidden;height:100dvh;min-height:0}
|
|
544
|
+
.gfull{position:fixed;inset:0;margin:0}
|
|
545
|
+
.gfull.gtab{position:relative;inset:auto;height:max(26rem,calc(100dvh - 19rem));margin-top:1.5rem;border:1px solid var(--line-soft)}
|
|
546
|
+
.gtab .g-left{top:.75rem;left:.75rem}
|
|
547
|
+
.gtab .g-right{top:.75rem;right:.75rem}
|
|
548
|
+
.g-acts{display:inline-flex;align-items:center;gap:1rem}
|
|
549
|
+
@media (max-width:40rem){.gtab .g-right{top:auto;bottom:.75rem;left:.75rem;justify-items:start}.gtab{height:max(26rem,calc(100dvh - 16rem))}}
|
|
550
|
+
.gfull .graph-canvas{position:absolute;inset:0;margin:0;border:0}
|
|
551
|
+
.gfull .graph-svg{height:100%}
|
|
552
|
+
.g-over{position:absolute;z-index:2;display:grid;gap:.25rem;padding:.875rem 1rem;border:1px solid var(--line-soft);background:var(--head);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}
|
|
553
|
+
.g-left{top:1rem;left:1rem;max-width:min(24rem,calc(100vw - 2rem))}
|
|
554
|
+
.g-left h1{font-size:1.25rem;line-height:1.25;letter-spacing:-.02em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
555
|
+
.g-right{top:1rem;right:1rem;justify-items:end}
|
|
556
|
+
.g-empty{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%)}
|
|
557
|
+
@media (max-width:40rem){.g-right{top:auto;bottom:1rem}}
|
|
558
|
+
`,Xt=`function drawn(n, o, talks) {
|
|
559
|
+
if (n.historic) return !!o.historic;
|
|
560
|
+
if (n.presence === "offline") return o.offline || (n.kind === "human" && talks.has(n.address));
|
|
561
|
+
if (n.presence === "idle" && n.kind === "agent" && !n.wake) return o.nextTurn;
|
|
562
|
+
return true;
|
|
563
|
+
}`,Jt=`function slot(e, w, h, min) {
|
|
564
|
+
return { x0: e.x0, x1: Math.max(e.x1, e.x0 + w, e.x0 + min), y0: e.y0 - h, y1: e.y1 };
|
|
565
|
+
}`,Vt=`function bands(items, o) {
|
|
566
|
+
const byKey = new Map();
|
|
567
|
+
for (const it of items) { if (!byKey.has(it.key)) byKey.set(it.key, { key: it.key, items: [] }); byKey.get(it.key).items.push(it); }
|
|
568
|
+
const groups = [...byKey.values()];
|
|
569
|
+
for (const g of groups) {
|
|
570
|
+
g.inset = g.key ? o.pad : 0;
|
|
571
|
+
g.top = g.key ? o.badge : 0;
|
|
572
|
+
g.w = g.items.reduce((w, it) => w + it.w, 0) + o.gap * (g.items.length - 1) + 2 * g.inset;
|
|
573
|
+
g.h = Math.max(...g.items.map((it) => it.h)) + g.top + 2 * g.inset;
|
|
574
|
+
}
|
|
575
|
+
const area = groups.reduce((a, g) => a + (g.w + o.groupGap) * (g.h + o.groupGap), 0);
|
|
576
|
+
const target = Math.max(Math.sqrt(area * o.aspect), ...groups.map((g) => g.w));
|
|
577
|
+
const rows = [];
|
|
578
|
+
for (const g of groups) {
|
|
579
|
+
const row = rows[rows.length - 1];
|
|
580
|
+
if (row && row.w + o.groupGap + g.w <= target) { row.groups.push(g); row.w += o.groupGap + g.w; }
|
|
581
|
+
else rows.push({ groups: [g], w: g.w });
|
|
582
|
+
}
|
|
583
|
+
const width = Math.max(1, ...rows.map((r) => r.w));
|
|
584
|
+
const mid = Math.floor(rows.length / 2);
|
|
585
|
+
const places = new Map(), rects = [];
|
|
586
|
+
let y = 0, hub = null;
|
|
587
|
+
rows.forEach((row, r) => {
|
|
588
|
+
if (o.hub && r === mid) {
|
|
589
|
+
hub = { x: width / 2, y: rows.length === 1 ? o.hub / 2 - 10 : y + o.hub / 2 - o.groupGap / 2 };
|
|
590
|
+
y += o.hub;
|
|
591
|
+
}
|
|
592
|
+
let x = (width - row.w) / 2;
|
|
593
|
+
for (const g of row.groups) {
|
|
594
|
+
rects.push({ key: g.key, x, y, w: g.w, h: g.h, n: g.items.length });
|
|
595
|
+
let ix = x + g.inset;
|
|
596
|
+
for (const it of g.items) { places.set(it.name, { x: ix, y: y + g.inset + g.top }); ix += it.w + o.gap; }
|
|
597
|
+
x += g.w + o.groupGap;
|
|
598
|
+
}
|
|
599
|
+
y += Math.max(...row.groups.map((g) => g.h)) + o.groupGap;
|
|
600
|
+
});
|
|
601
|
+
return { places, bands: rects, hub, width, height: Math.max(y - o.groupGap, 1) };
|
|
602
|
+
}`,zt={gap:14,ring:84,reach:160,span:2.4,wide:1.4,sub:1.6,apart:64,aspect:1.8},Yt=`function cluster(ns, B, o) {
|
|
603
|
+
const at = new Map(ns.map((n, i) => [n.address, i]));
|
|
604
|
+
const by = (a, b) => (ns[a].address < ns[b].address ? -1 : ns[a].address > ns[b].address ? 1 : 0);
|
|
605
|
+
const kids = ns.map(() => []);
|
|
606
|
+
const up = ns.map((n, i) => { const p = n.parent ? at.get(n.parent) : undefined; return p === undefined || p === i ? -1 : p; });
|
|
607
|
+
up.forEach((p, i) => { if (p >= 0) kids[p].push(i); });
|
|
608
|
+
for (const k of kids) k.sort(by);
|
|
609
|
+
const P = ns.map(() => ({ x: 0, y: 0 }));
|
|
610
|
+
const seen = new Set();
|
|
611
|
+
const foot = (i) => Math.max(2 * B[i].hw, B[i].up + B[i].down) + o.gap;
|
|
612
|
+
const step = (a, b) => (foot(a) + foot(b)) / 2;
|
|
613
|
+
const fan = (i, dir, span, out) => {
|
|
614
|
+
const c = kids[i].filter((k) => !seen.has(k));
|
|
615
|
+
if (!c.length) return;
|
|
616
|
+
for (const k of c) { seen.add(k); out.push(k); }
|
|
617
|
+
const rise = Math.max(...c.map((k) => B[k].up + B[k].down)) + o.gap;
|
|
618
|
+
let all = 0;
|
|
619
|
+
for (let k = 1; k < c.length; k++) all += step(c[k - 1], c[k]);
|
|
620
|
+
const per = Math.ceil(c.length / Math.max(1, Math.ceil(all / (span * o.reach))));
|
|
621
|
+
let r = Math.max(o.ring, B[i].down + rise - B[i].up);
|
|
622
|
+
for (let j = 0; j < c.length; j += per) {
|
|
623
|
+
const ring = c.slice(j, j + per);
|
|
624
|
+
let arc = 0;
|
|
625
|
+
for (let k = 1; k < ring.length; k++) arc += step(ring[k - 1], ring[k]);
|
|
626
|
+
r = Math.max(r, arc / span / o.wide);
|
|
627
|
+
const rx = r * o.wide;
|
|
628
|
+
let t = dir + arc / rx / 2;
|
|
629
|
+
ring.forEach((k, n) => {
|
|
630
|
+
let dx = Math.cos(t) * rx, dy = Math.sin(t) * r;
|
|
631
|
+
if (Math.sin(dir) > 0.5) dy = Math.max(dy, B[i].down + B[k].up + o.gap);
|
|
632
|
+
P[k] = { x: P[i].x + dx, y: P[i].y + dy, t };
|
|
633
|
+
if (n + 1 < ring.length) t -= step(k, ring[n + 1]) / rx;
|
|
634
|
+
});
|
|
635
|
+
r += rise;
|
|
636
|
+
}
|
|
637
|
+
for (const k of c) fan(k, (P[k].t + Math.PI / 2) / 2, o.sub, out);
|
|
638
|
+
};
|
|
639
|
+
const grow = (i) => { seen.add(i); const out = [i]; P[i] = { x: 0, y: 0 }; fan(i, Math.PI / 2, o.span, out); return out; };
|
|
640
|
+
const roots = ns.map((_, i) => i).filter((i) => up[i] < 0).sort(by);
|
|
641
|
+
const groups = roots.filter((i) => kids[i].length).map(grow).sort((a, b) => b.length - a.length);
|
|
642
|
+
for (const i of ns.map((_, i) => i).sort(by)) if (!seen.has(i) && up[i] >= 0) groups.push(grow(i));
|
|
643
|
+
const solo = roots.filter((i) => !kids[i].length);
|
|
644
|
+
if (solo.length) {
|
|
645
|
+
const cols = Math.ceil(Math.sqrt(solo.length));
|
|
646
|
+
const cw = Math.max(...solo.map(foot)), ch = Math.max(...solo.map((i) => B[i].up + B[i].down)) + o.gap;
|
|
647
|
+
solo.forEach((i, k) => { P[i] = { x: (k % cols) * cw, y: Math.floor(k / cols) * ch }; });
|
|
648
|
+
groups.push(solo);
|
|
649
|
+
}
|
|
650
|
+
const ext = groups.map((m) => {
|
|
651
|
+
const Q = m.map((i) => P[i]), C = m.map((i) => B[i]), one = m.map(() => 1);
|
|
652
|
+
for (let k = 0; k < 120; k++) if (!collide(Q, C, one)) break;
|
|
653
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
654
|
+
for (const i of m) { x0 = Math.min(x0, P[i].x - B[i].hw); x1 = Math.max(x1, P[i].x + B[i].hw); y0 = Math.min(y0, P[i].y - B[i].up); y1 = Math.max(y1, P[i].y + B[i].down); }
|
|
655
|
+
return { m, x0, y0, w: x1 - x0, h: y1 - y0 };
|
|
656
|
+
});
|
|
657
|
+
const area = ext.reduce((a, e) => a + (e.w + o.apart) * (e.h + o.apart), 0);
|
|
658
|
+
const target = Math.max(Math.sqrt(area * o.aspect), ...ext.map((e) => e.w));
|
|
659
|
+
const rows = [];
|
|
660
|
+
for (const e of ext) {
|
|
661
|
+
const row = rows[rows.length - 1];
|
|
662
|
+
if (row && row.w + o.apart + e.w <= target) { row.items.push(e); row.w += o.apart + e.w; }
|
|
663
|
+
else rows.push({ items: [e], w: e.w });
|
|
664
|
+
}
|
|
665
|
+
const width = Math.max(0, ...rows.map((r) => r.w));
|
|
666
|
+
let y = 0;
|
|
667
|
+
for (const row of rows) {
|
|
668
|
+
let x = (width - row.w) / 2;
|
|
669
|
+
for (const e of row.items) {
|
|
670
|
+
for (const i of e.m) { P[i].x += x - e.x0; P[i].y += y - e.y0; }
|
|
671
|
+
x += e.w + o.apart;
|
|
672
|
+
}
|
|
673
|
+
y += Math.max(...row.items.map((e) => e.h)) + o.apart * 1.5;
|
|
674
|
+
}
|
|
675
|
+
return P.map((p) => ({ x: p.x, y: p.y }));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** pushes overlapping node and label boxes apart along their shallower axis; true while any overlapped */
|
|
679
|
+
function collide(P, B, mob) {
|
|
680
|
+
let hit = false;
|
|
681
|
+
for (let i = 0; i < P.length; i++) for (let j = i + 1; j < P.length; j++) {
|
|
682
|
+
const dx = P[j].x - P[i].x, dy = P[j].y - P[i].y;
|
|
683
|
+
const ox = B[i].hw + B[j].hw - Math.abs(dx);
|
|
684
|
+
const oy = (dy >= 0 ? B[i].down + B[j].up : B[i].up + B[j].down) - Math.abs(dy);
|
|
685
|
+
if (ox <= 0 || oy <= 0) continue;
|
|
686
|
+
hit = true;
|
|
687
|
+
const mi = mob[i], mj = mob[j], sum = mi + mj || 1;
|
|
688
|
+
if (ox < oy) { const s = (dx >= 0 ? 1 : -1) * (ox + 0.5); P[i].x -= s * mi / sum; P[j].x += s * mj / sum; }
|
|
689
|
+
else { const s = (dy >= 0 ? 1 : -1) * (oy + 0.5); P[i].y -= s * mi / sum; P[j].y += s * mj / sum; }
|
|
690
|
+
}
|
|
691
|
+
return hit;
|
|
692
|
+
}`,Kt=()=>`(() => {
|
|
693
|
+
const root = document.querySelector("[data-graph]");
|
|
694
|
+
const live = document.querySelector("[data-live]");
|
|
695
|
+
if (!root || !live) return;
|
|
696
|
+
const bar = document.querySelector("[data-view-bar]");
|
|
697
|
+
const full = root.hasAttribute("data-full");
|
|
698
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
699
|
+
const ICONS = ${JSON.stringify({...de,human:Ft})};
|
|
700
|
+
const MARK = ${JSON.stringify(Bt)};
|
|
701
|
+
const COLORED = new Set(${JSON.stringify(Wt)});
|
|
702
|
+
const BOLT = "M9.5 1 3 9h4.5L6.5 15 13 7H8.5z";
|
|
703
|
+
const R = 16, PAD = 20, HEAD = 26, ID_LINE = 14, GAP = 26, BAND_GAP = 44, LABEL_MAX = 176, NAME_MAX = 220, ID_MAX = 200, MIN_W = 96, CAP = 1.6;
|
|
704
|
+
const TRAILS = [1, 0.5, 0.25];
|
|
705
|
+
const LAYOUT = ${JSON.stringify(zt)};
|
|
706
|
+
${Jt}
|
|
707
|
+
${Yt}
|
|
708
|
+
${Vt}
|
|
709
|
+
${Xt}
|
|
710
|
+
const svg = root.querySelector(".graph-svg");
|
|
711
|
+
const canvas = root.querySelector(".graph-canvas");
|
|
712
|
+
const tip = root.querySelector(".graph-tip");
|
|
713
|
+
const offline = document.querySelector("[data-graph-offline]");
|
|
714
|
+
const nextTurn = document.querySelector("[data-graph-nextturn]");
|
|
715
|
+
const historic = document.querySelector("[data-graph-historic]");
|
|
716
|
+
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
717
|
+
const load = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
|
|
718
|
+
const save = (k, v) => { try { localStorage.setItem(k, v); } catch {} };
|
|
719
|
+
const el = (tag, attrs, parent) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); if (parent) parent.appendChild(e); return e; };
|
|
720
|
+
const ease = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
721
|
+
const lerp = (a, b, k) => a + (b - a) * k;
|
|
722
|
+
const r1 = (v) => Math.round(v * 10) / 10;
|
|
723
|
+
const machineOf = (a) => { const p = a.split("/"); return p.length >= 3 ? p[1] : ""; };
|
|
724
|
+
|
|
725
|
+
const defs = el("defs", {}, svg);
|
|
726
|
+
el("path", { d: "M0 0L7 3.5L0 7z", class: "arrow" }, el("marker", { id: "graph-arrow", viewBox: "0 0 7 7", refX: "1", refY: "3.5", markerWidth: "6", markerHeight: "6", markerUnits: "userSpaceOnUse", orient: "auto" }, defs));
|
|
727
|
+
const layers = {};
|
|
728
|
+
for (const k of ["regions", "edges", "hub", "labels", "pulses", "nodes"]) layers[k] = el("g", { class: "layer-" + k }, svg);
|
|
729
|
+
|
|
730
|
+
let data = null;
|
|
731
|
+
let focus = null;
|
|
732
|
+
let first = true;
|
|
733
|
+
let streamOpen = false;
|
|
734
|
+
const sim = new Map();
|
|
735
|
+
const shapes = new Map();
|
|
736
|
+
const shown = new Map();
|
|
737
|
+
const nodeEls = new Map();
|
|
738
|
+
const edgeEls = new Map();
|
|
739
|
+
const regionEls = new Map();
|
|
740
|
+
let place = new Map();
|
|
741
|
+
let hubAt = null;
|
|
742
|
+
let hubEl = null;
|
|
743
|
+
let viewTarget = null;
|
|
744
|
+
let viewShown = null;
|
|
745
|
+
let anim = 0;
|
|
746
|
+
let trail = TRAILS.includes(Number(load("scp.graph.trails"))) ? Number(load("scp.graph.trails")) : 1;
|
|
747
|
+
|
|
748
|
+
const measure = document.createElement("canvas").getContext("2d");
|
|
749
|
+
const widths = new Map();
|
|
750
|
+
function textWidth(text, weight, size = 11) {
|
|
751
|
+
const key = weight + "|" + size + "|" + text;
|
|
752
|
+
if (!widths.has(key)) {
|
|
753
|
+
measure.font = weight + " " + size + "px " + (getComputedStyle(document.documentElement).getPropertyValue("--mono") || "monospace");
|
|
754
|
+
widths.set(key, measure.measureText(text).width);
|
|
755
|
+
}
|
|
756
|
+
return widths.get(key);
|
|
757
|
+
}
|
|
758
|
+
function fit(text, weight = 400, max = LABEL_MAX, size = 11) {
|
|
759
|
+
if (textWidth(text, weight, size) <= max) return text;
|
|
760
|
+
let s = text;
|
|
761
|
+
while (s.length > 1 && textWidth(s + "…", weight, size) > max) s = s.slice(0, -1);
|
|
762
|
+
return s + "…";
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function read() {
|
|
766
|
+
const block = live.querySelector("script[data-graph-data]");
|
|
767
|
+
if (!block) return null;
|
|
768
|
+
try { return JSON.parse(block.textContent); } catch { return null; }
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function setView(v) {
|
|
772
|
+
document.documentElement.setAttribute("data-view", v);
|
|
773
|
+
if (bar) for (const b of bar.querySelectorAll("[data-view-set]")) b.setAttribute("aria-pressed", String(b.getAttribute("data-view-set") === v));
|
|
774
|
+
save("scp.overview.view", v);
|
|
775
|
+
if (v === "graph") apply();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function visible(g) {
|
|
779
|
+
const o = { offline: !!(offline && offline.checked), nextTurn: !!(nextTurn && nextTurn.checked), historic: !!(historic && historic.checked) };
|
|
780
|
+
const talks = new Set();
|
|
781
|
+
for (const e of g.edges) if (e.kind === "message") { talks.add(e.from); talks.add(e.to); }
|
|
782
|
+
const nodes = g.nodes.filter((n) => drawn(n, o, talks));
|
|
783
|
+
const on = new Set(nodes.map((n) => n.address));
|
|
784
|
+
const edges = g.edges.filter((e) => on.has(e.from) && on.has(e.to));
|
|
785
|
+
const byName = new Map();
|
|
786
|
+
for (const m of g.machines) byName.set(m.name, { name: m.name, online: m.online, identity: m.identity || null, nodes: [] });
|
|
787
|
+
for (const n of nodes) {
|
|
788
|
+
if (!byName.has(n.machine)) byName.set(n.machine, { name: n.machine, online: n.presence === "online", identity: null, nodes: [] });
|
|
789
|
+
byName.get(n.machine).nodes.push(n);
|
|
790
|
+
}
|
|
791
|
+
const groups = [...byName.values()].filter((gr) => gr.nodes.length || gr.online);
|
|
792
|
+
const who = (gr) => (gr.identity ? gr.identity.name : "");
|
|
793
|
+
groups.sort((a, b) => (a.name === "") - (b.name === "") || !who(a) - !who(b) || who(a).localeCompare(who(b)) || a.name.localeCompare(b.name));
|
|
794
|
+
return { nodes, edges, groups };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function box(n) {
|
|
798
|
+
return { hw: Math.max(R + 4, textWidth(fit(n.label), 400) / 2 + 6), up: R + 6, down: R + 22 };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** one machine's layout, recomputed only when its nodes, labels or spawn links change, so a refresh never moves anything */
|
|
802
|
+
function settle(g, edges) {
|
|
803
|
+
const ns = g.nodes;
|
|
804
|
+
const ids = new Set(ns.map((n) => n.address));
|
|
805
|
+
const spawn = edges.filter((e) => e.kind === "spawn" && ids.has(e.from) && ids.has(e.to));
|
|
806
|
+
const sig = ns.map((n) => n.address + ":" + fit(n.label)).sort().join(" ") + "|" + spawn.map((e) => e.from + ">" + e.to).sort().join(" ");
|
|
807
|
+
if (shapes.get(g.name) === sig) return;
|
|
808
|
+
shapes.set(g.name, sig);
|
|
809
|
+
const up = new Map(spawn.map((e) => [e.to, e.from]));
|
|
810
|
+
const P = cluster(ns.map((n) => ({ address: n.address, parent: up.get(n.address) || null })), ns.map(box), LAYOUT);
|
|
811
|
+
ns.forEach((n, i) => sim.set(n.address, P[i]));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** a region's header: the machine name and agent count, the identity on a second line, each cut to a sane width */
|
|
815
|
+
function headOf(g) {
|
|
816
|
+
const full = g.name || "org";
|
|
817
|
+
const name = fit(full, 500, NAME_MAX);
|
|
818
|
+
const id = g.identity ? fit("identity: " + g.identity.name, 400, ID_MAX, 10) : "";
|
|
819
|
+
const count = g.nodes.length + (g.nodes.length === 1 ? " agent" : " agents") + (g.online ? "" : ", offline");
|
|
820
|
+
const w = Math.max(textWidth(name, 500) + 8 + textWidth(count, 400), id ? textWidth(id, 400, 10) : 0) + 34;
|
|
821
|
+
return { name, id, count, title: full + (g.identity ? "\\nidentity: " + g.identity.name : ""), w, h: HEAD + (id ? ID_LINE : 0) };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** a region's extent around its own origin, header included; an empty machine is one short row for its note */
|
|
825
|
+
function extent(g) {
|
|
826
|
+
const h = headOf(g);
|
|
827
|
+
if (!g.nodes.length) return slot({ x0: 0, x1: 0, y0: 0, y1: 12 }, h.w, h.h, MIN_W);
|
|
828
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
829
|
+
for (const n of g.nodes) {
|
|
830
|
+
const p = sim.get(n.address), b = box(n);
|
|
831
|
+
x0 = Math.min(x0, p.x - b.hw); x1 = Math.max(x1, p.x + b.hw); y0 = Math.min(y0, p.y - b.up); y1 = Math.max(y1, p.y + b.down);
|
|
832
|
+
}
|
|
833
|
+
return slot({ x0, x1, y0, y1 }, h.w, h.h, MIN_W);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** regions with one identity's machines side by side, wrapped to the window's shape; the hub sits above a single row, else between the middle rows */
|
|
837
|
+
function pack(list) {
|
|
838
|
+
const cw = Math.max(280, canvas.clientWidth), ch = full ? Math.max(240, canvas.clientHeight) : 0;
|
|
839
|
+
const aspect = full ? cw / ch : Math.max(1.4, cw / 560);
|
|
840
|
+
const ext = list.map(extent);
|
|
841
|
+
const items = list.map((g, i) => ({ name: g.name, key: g.identity ? g.identity.name : "", w: ext[i].x1 - ext[i].x0 + 2 * PAD, h: ext[i].y1 - ext[i].y0 + 2 * PAD }));
|
|
842
|
+
const L = bands(items, { gap: GAP, groupGap: BAND_GAP, pad: 0, badge: 0, hub: list.length > 1 ? 96 : 0, aspect: aspect * 1.15 });
|
|
843
|
+
const out = new Map();
|
|
844
|
+
list.forEach((g, i) => {
|
|
845
|
+
const p = L.places.get(g.name), e = ext[i];
|
|
846
|
+
out.set(g.name, { ox: p.x + PAD - e.x0, oy: p.y + PAD - e.y0, x: p.x, y: p.y, w: items[i].w, h: items[i].h });
|
|
847
|
+
});
|
|
848
|
+
hubAt = L.hub;
|
|
849
|
+
const widest = L.width;
|
|
850
|
+
const height = L.height;
|
|
851
|
+
const margin = full ? Math.min(120, cw * 0.06) : 24;
|
|
852
|
+
let top = margin, bottom = margin;
|
|
853
|
+
const at = full ? canvas.getBoundingClientRect().top : 0;
|
|
854
|
+
if (full) for (const o of root.querySelectorAll(".g-over")) {
|
|
855
|
+
const b = o.getBoundingClientRect();
|
|
856
|
+
if (b.top - at < ch / 2) top = Math.max(top, b.bottom - at + 16);
|
|
857
|
+
else bottom = Math.max(bottom, ch - (b.top - at) + 16);
|
|
858
|
+
}
|
|
859
|
+
const scale = full
|
|
860
|
+
? Math.min(CAP, (cw - 2 * margin) / widest, (ch - top - bottom) / height)
|
|
861
|
+
: Math.min(CAP, (cw - 2 * margin) / widest, 760 / (height + 2 * margin));
|
|
862
|
+
const vw = cw / scale;
|
|
863
|
+
const vh = full ? ch / scale : Math.max(340 / scale, height + (2 * margin) / scale);
|
|
864
|
+
const cy = full ? height / 2 - (top - bottom) / 2 / scale : height / 2;
|
|
865
|
+
viewTarget = { x: widest / 2 - vw / 2, y: cy - vh / 2, w: vw, h: vh, px: full ? 0 : vh * scale };
|
|
866
|
+
return out;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function counts(v) {
|
|
870
|
+
const out = document.querySelector("[data-graph-counts]");
|
|
871
|
+
if (!out) return;
|
|
872
|
+
const agents = v.nodes.filter((n) => n.kind === "agent");
|
|
873
|
+
const on = agents.filter((n) => n.presence === "online").length;
|
|
874
|
+
const m = v.groups.filter((g) => g.name).length;
|
|
875
|
+
out.textContent = m + (m === 1 ? " machine, " : " machines, ") + agents.length + (agents.length === 1 ? " agent, " : " agents, ") + on + " online";
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function apply() {
|
|
879
|
+
if (!data) return;
|
|
880
|
+
const has = data.nodes.length > 0 || data.machines.length > 0;
|
|
881
|
+
if (bar) bar.hidden = !has;
|
|
882
|
+
if (!full) root.hidden = !has;
|
|
883
|
+
if (!has && !full) { document.documentElement.removeAttribute("data-view"); return; }
|
|
884
|
+
if (bar && !document.documentElement.hasAttribute("data-view")) return setView(load("scp.overview.view") === "table" ? "table" : "graph");
|
|
885
|
+
if (bar && document.documentElement.getAttribute("data-view") !== "graph") return;
|
|
886
|
+
for (const m of root.querySelectorAll("[data-graph-minutes]")) m.textContent = String(data.minutes);
|
|
887
|
+
const v = visible(data);
|
|
888
|
+
counts(v);
|
|
889
|
+
for (const g of v.groups) settle(g, v.edges);
|
|
890
|
+
place = pack(v.groups);
|
|
891
|
+
sync(v);
|
|
892
|
+
start();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** where a node rests, in view coordinates */
|
|
896
|
+
function target(a) {
|
|
897
|
+
const s = sim.get(a), o = place.get(machineOf(a));
|
|
898
|
+
return s && o ? { x: o.ox + s.x, y: o.oy + s.y } : null;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function sync(v) {
|
|
902
|
+
const keep = new Set();
|
|
903
|
+
for (const g of v.groups) {
|
|
904
|
+
keep.add(g.name);
|
|
905
|
+
let r = regionEls.get(g.name);
|
|
906
|
+
if (!r) {
|
|
907
|
+
const gr = el("g", { class: "region" }, layers.regions);
|
|
908
|
+
const head = el("g", { class: "region-head" }, layers.labels);
|
|
909
|
+
r = { g: gr, head, rim: el("path", { class: "hull-rim" }, gr), hull: el("path", { class: "hull" }, gr), dot: el("circle", { class: "mdot", r: "3" }, head), label: el("text", { class: "mlabel" }, head), idl: el("text", { class: "mid" }, head), note: el("text", { class: "empty-note" }, gr) };
|
|
910
|
+
r.title = el("title", {}, head);
|
|
911
|
+
r.name = el("tspan", {}, r.label);
|
|
912
|
+
r.n = el("tspan", { class: "n", dx: "8" }, r.label);
|
|
913
|
+
regionEls.set(g.name, r);
|
|
914
|
+
}
|
|
915
|
+
const head = headOf(g);
|
|
916
|
+
r.g.classList.toggle("offline", !g.online);
|
|
917
|
+
r.head.classList.toggle("offline", !g.online);
|
|
918
|
+
r.dot.setAttribute("class", "mdot" + (g.online ? " online" : ""));
|
|
919
|
+
r.name.textContent = head.name;
|
|
920
|
+
r.idl.textContent = head.id;
|
|
921
|
+
r.title.textContent = head.title;
|
|
922
|
+
r.n.textContent = head.count;
|
|
923
|
+
r.note.textContent = g.nodes.length ? "" : "no agents";
|
|
924
|
+
r.g.classList.toggle("ided", !!g.identity);
|
|
925
|
+
r.g.style.setProperty("--id", g.identity ? "var(--id-" + g.identity.tone + ")" : "");
|
|
926
|
+
r.head.style.setProperty("--id", g.identity ? "var(--id-" + g.identity.tone + ")" : "");
|
|
927
|
+
r.group = g;
|
|
928
|
+
}
|
|
929
|
+
for (const [k, r] of regionEls) if (!keep.has(k)) { r.g.remove(); r.head.remove(); regionEls.delete(k); }
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
const on = new Set();
|
|
933
|
+
for (const n of v.nodes) {
|
|
934
|
+
on.add(n.address);
|
|
935
|
+
const e = nodeEls.get(n.address) || makeNode(n);
|
|
936
|
+
e.n = n;
|
|
937
|
+
paintNode(e);
|
|
938
|
+
}
|
|
939
|
+
for (const [a, e] of nodeEls) if (!on.has(a)) {
|
|
940
|
+
nodeEls.delete(a);
|
|
941
|
+
shown.delete(a);
|
|
942
|
+
e.g.classList.add("enter");
|
|
943
|
+
setTimeout(() => e.g.remove(), 320);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const edgesOn = new Set();
|
|
947
|
+
for (const e of v.edges) {
|
|
948
|
+
const key = e.kind + ":" + e.from + ">" + e.to;
|
|
949
|
+
edgesOn.add(key);
|
|
950
|
+
let p = edgeEls.get(key);
|
|
951
|
+
if (!p) {
|
|
952
|
+
p = el("path", { class: "edge " + (e.kind === "message" ? "msg" : "spawn") }, layers.edges);
|
|
953
|
+
if (e.kind === "message") p.setAttribute("marker-end", "url(#graph-arrow)");
|
|
954
|
+
p.dataset.from = e.from;
|
|
955
|
+
p.dataset.to = e.to;
|
|
956
|
+
edgeEls.set(key, p);
|
|
957
|
+
}
|
|
958
|
+
p.edge = e;
|
|
959
|
+
paintEdge(p);
|
|
960
|
+
}
|
|
961
|
+
for (const [k, p] of edgeEls) if (!edgesOn.has(k)) { p.remove(); edgeEls.delete(k); }
|
|
962
|
+
|
|
963
|
+
if (hubAt && !hubEl) {
|
|
964
|
+
hubEl = el("g", { class: "hub", "aria-hidden": "true" }, layers.hub);
|
|
965
|
+
el("circle", { r: "15" }, hubEl);
|
|
966
|
+
el("svg", { x: "-7", y: "-7", width: "14", height: "14", viewBox: "0 0 32 32", fill: "currentColor" }, hubEl).innerHTML = MARK;
|
|
967
|
+
el("text", { y: "-22" }, hubEl).textContent = "hub";
|
|
968
|
+
}
|
|
969
|
+
if (!hubAt && hubEl) { hubEl.remove(); hubEl = null; }
|
|
970
|
+
applyFocus();
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function makeNode(n) {
|
|
974
|
+
const g = el("g", { class: "node enter", tabindex: "0", role: "button" }, layers.nodes);
|
|
975
|
+
g.dataset.address = n.address;
|
|
976
|
+
const e = { g, n };
|
|
977
|
+
e.ring = el("circle", { class: "ring", r: String(R) }, g);
|
|
978
|
+
e.disc = n.kind === "human" ? el("rect", { class: "disc", x: -R, y: -R, width: 2 * R, height: 2 * R, rx: 6 }, g) : el("circle", { class: "disc", r: String(R) }, g);
|
|
979
|
+
e.glyph = el("svg", { class: "glyph", x: "-7.5", y: "-7.5", width: "15", height: "15", viewBox: "0 0 24 24" }, g);
|
|
980
|
+
e.pdot = el("circle", { class: "pdot", cx: String(r1(R * 0.72)), cy: String(r1(-R * 0.72)), r: "4" }, g);
|
|
981
|
+
e.bolt = el("g", { class: "bolt-g", transform: "translate(" + r1(R * 0.72) + " " + r1(R * 0.72) + ")" }, g);
|
|
982
|
+
el("circle", { r: "6" }, e.bolt);
|
|
983
|
+
el("path", { d: BOLT, transform: "translate(-4.4 -4.4) scale(.55)", fill: "currentColor" }, e.bolt);
|
|
984
|
+
e.label = el("text", { class: "nlabel", y: String(R + 15) }, g);
|
|
985
|
+
const from = n.parent && shown.get(n.parent);
|
|
986
|
+
const t = target(n.address);
|
|
987
|
+
if (t) shown.set(n.address, from ? { ...from } : { ...t });
|
|
988
|
+
nodeEls.set(n.address, e);
|
|
989
|
+
requestAnimationFrame(() => requestAnimationFrame(() => g.classList.remove("enter")));
|
|
990
|
+
return e;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function paintNode(e) {
|
|
994
|
+
const n = e.n, c = e.g.classList;
|
|
995
|
+
for (const p of ["online", "idle", "offline"]) c.toggle(p, n.presence === p);
|
|
996
|
+
c.toggle("human", n.kind === "human");
|
|
997
|
+
c.toggle("historic", !!n.historic);
|
|
998
|
+
e.g.setAttribute("aria-label", n.name + ", " + n.presence + (n.historic ? ", historic" : "") + (n.how ? ", " + n.how : ""));
|
|
999
|
+
if (e.icon !== n.icon) {
|
|
1000
|
+
e.icon = n.icon;
|
|
1001
|
+
e.glyph.innerHTML = ICONS[n.icon] || ICONS.generic;
|
|
1002
|
+
e.glyph.style.color = COLORED.has(n.icon) ? "var(--h-" + n.icon + ")" : "";
|
|
1003
|
+
}
|
|
1004
|
+
e.pdot.setAttribute("class", "pdot " + n.presence);
|
|
1005
|
+
e.pdot.style.display = n.kind === "human" ? "none" : "";
|
|
1006
|
+
e.bolt.style.display = n.wake && n.kind !== "human" ? "" : "none";
|
|
1007
|
+
e.label.textContent = fit(n.label);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/** message edges fade over the graph's window and thicken with their count */
|
|
1011
|
+
function paintEdge(p) {
|
|
1012
|
+
const e = p.edge;
|
|
1013
|
+
if (e.kind !== "message") return;
|
|
1014
|
+
const age = Math.max(0, Date.now() - Date.parse(e.last_at)) / (data.minutes * 60000);
|
|
1015
|
+
p.style.opacity = String(r1(0.22 + 0.66 * Math.max(0, 1 - age)));
|
|
1016
|
+
p.style.strokeWidth = String(r1(1.1 + Math.min(2.6, Math.log2(e.count) * 0.55)));
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/** an edge's path: straight for a spawn within a machine and an S curve from the parent down to a child on another machine; a message bows gently within a machine and between machines rises to the hub and runs through it, each direction on its own lane */
|
|
1020
|
+
function curve(e, a, b) {
|
|
1021
|
+
const trim = (p, c, by) => { const ux = c.x - p.x, uy = c.y - p.y, l = Math.max(Math.hypot(ux, uy), 1); return { x: p.x + ux / l * by, y: p.y + uy / l * by }; };
|
|
1022
|
+
const pt = (p) => r1(p.x) + " " + r1(p.y);
|
|
1023
|
+
const end = R + (e.kind === "message" ? 8 : 3);
|
|
1024
|
+
if (e.kind === "message" && hubAt && machineOf(e.from) !== machineOf(e.to)) {
|
|
1025
|
+
const hy = hubAt.y + (b.x >= a.x ? -3 : 3), h = { x: hubAt.x, y: hy };
|
|
1026
|
+
const c1 = { x: a.x, y: lerp(a.y, hy, 0.6) }, c2 = { x: lerp(a.x, h.x, 0.55), y: hy };
|
|
1027
|
+
const c3 = { x: lerp(b.x, h.x, 0.55), y: hy }, c4 = { x: b.x, y: lerp(b.y, hy, 0.6) };
|
|
1028
|
+
return "M" + pt(trim(a, c1, R + 3)) + "C" + pt(c1) + " " + pt(c2) + " " + pt(h) + "C" + pt(c3) + " " + pt(c4) + " " + pt(trim(b, c4, end));
|
|
1029
|
+
}
|
|
1030
|
+
if (e.kind === "spawn" && machineOf(e.from) !== machineOf(e.to)) {
|
|
1031
|
+
const k = Math.max(Math.abs(b.y - a.y), Math.abs(b.x - a.x) * 0.35, 60) * 0.5;
|
|
1032
|
+
const c1 = { x: a.x, y: a.y + k }, c2 = { x: b.x, y: b.y - k };
|
|
1033
|
+
return "M" + pt(trim(a, c1, R + 3)) + "C" + pt(c1) + " " + pt(c2) + " " + pt(trim(b, c2, end));
|
|
1034
|
+
}
|
|
1035
|
+
const dx = b.x - a.x, dy = b.y - a.y, d = Math.max(Math.hypot(dx, dy), 1), nx = -dy / d, ny = dx / d;
|
|
1036
|
+
const bow = e.kind === "spawn" ? 0 : Math.min(46, d * 0.2);
|
|
1037
|
+
const c1 = { x: a.x + dx / 3 + nx * bow, y: a.y + dy / 3 + ny * bow }, c2 = { x: a.x + dx * 2 / 3 + nx * bow, y: a.y + dy * 2 / 3 + ny * bow };
|
|
1038
|
+
return "M" + pt(trim(a, c1, R + 3)) + "C" + pt(c1) + " " + pt(c2) + " " + pt(trim(b, c2, end));
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function hullOf(pts) {
|
|
1042
|
+
const p = [...pts].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
1043
|
+
if (p.length < 3) return p;
|
|
1044
|
+
const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
1045
|
+
const lo = [], hi = [];
|
|
1046
|
+
for (const q of p) { while (lo.length >= 2 && cross(lo[lo.length - 2], lo[lo.length - 1], q) <= 0) lo.pop(); lo.push(q); }
|
|
1047
|
+
for (const q of p.reverse()) { while (hi.length >= 2 && cross(hi[hi.length - 2], hi[hi.length - 1], q) <= 0) hi.pop(); hi.push(q); }
|
|
1048
|
+
return lo.slice(0, -1).concat(hi.slice(0, -1));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function draw() {
|
|
1052
|
+
for (const [a, e] of nodeEls) {
|
|
1053
|
+
const p = shown.get(a);
|
|
1054
|
+
if (p) e.g.setAttribute("transform", "translate(" + r1(p.x) + " " + r1(p.y) + ")");
|
|
1055
|
+
}
|
|
1056
|
+
for (const [, p] of edgeEls) {
|
|
1057
|
+
const a = shown.get(p.edge.from), b = shown.get(p.edge.to);
|
|
1058
|
+
if (a && b) p.setAttribute("d", curve(p.edge, a, b));
|
|
1059
|
+
}
|
|
1060
|
+
for (const [name, r] of regionEls) {
|
|
1061
|
+
const g = r.group, o = place.get(name);
|
|
1062
|
+
if (!o) continue;
|
|
1063
|
+
const pts = [];
|
|
1064
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
1065
|
+
for (const n of g.nodes) {
|
|
1066
|
+
const p = shown.get(n.address);
|
|
1067
|
+
if (!p) continue;
|
|
1068
|
+
const b = box(n);
|
|
1069
|
+
x0 = Math.min(x0, p.x - b.hw); x1 = Math.max(x1, p.x + b.hw); y0 = Math.min(y0, p.y - b.up); y1 = Math.max(y1, p.y + b.down);
|
|
1070
|
+
pts.push([p.x - b.hw + 6, p.y - b.up + 6], [p.x + b.hw - 6, p.y - b.up + 6], [p.x - b.hw + 6, p.y + b.down - 6], [p.x + b.hw - 6, p.y + b.down - 6]);
|
|
1071
|
+
}
|
|
1072
|
+
const head = headOf(g);
|
|
1073
|
+
if (!pts.length) { x0 = o.x + PAD; y0 = o.y + PAD + head.h; x1 = o.x + o.w - PAD; y1 = o.y + o.h - PAD; pts.push([x0, y0], [x1, y0], [x0, y1], [x1, y1]); }
|
|
1074
|
+
const hw = x0 + Math.max(head.w, MIN_W);
|
|
1075
|
+
if (hw > x1) { x1 = hw; pts.push([x0, y1], [x1, y1]); }
|
|
1076
|
+
pts.push([x0, y0 - head.h], [x1, y0 - head.h]);
|
|
1077
|
+
const d = "M" + hullOf(pts).map((q) => r1(q[0]) + " " + r1(q[1])).join("L") + "Z";
|
|
1078
|
+
r.hull.setAttribute("d", d);
|
|
1079
|
+
r.hull.setAttribute("stroke-width", String(2 * PAD));
|
|
1080
|
+
r.rim.setAttribute("d", d);
|
|
1081
|
+
r.rim.setAttribute("stroke-width", String(2 * PAD + 2));
|
|
1082
|
+
const lx = x0 - PAD + 16, ly = y0 - head.h - PAD + 22;
|
|
1083
|
+
r.dot.setAttribute("cx", r1(lx));
|
|
1084
|
+
r.dot.setAttribute("cy", r1(ly - 4));
|
|
1085
|
+
r.label.setAttribute("x", r1(lx + 9));
|
|
1086
|
+
r.label.setAttribute("y", r1(ly));
|
|
1087
|
+
r.idl.setAttribute("x", r1(lx + 9));
|
|
1088
|
+
r.idl.setAttribute("y", r1(ly + ID_LINE));
|
|
1089
|
+
r.note.setAttribute("x", r1(lx));
|
|
1090
|
+
r.note.setAttribute("y", r1(y0 + 9));
|
|
1091
|
+
r.box = { x0: x0 - PAD, y0: y0 - head.h - PAD, x1: x1 + PAD, y1: y1 + PAD };
|
|
1092
|
+
}
|
|
1093
|
+
if (hubEl && hubAt) hubEl.setAttribute("transform", "translate(" + r1(hubAt.x) + " " + r1(hubAt.y) + ")");
|
|
1094
|
+
if (viewShown) {
|
|
1095
|
+
svg.setAttribute("viewBox", [viewShown.x, viewShown.y, viewShown.w, viewShown.h].map(r1).join(" "));
|
|
1096
|
+
if (!full) svg.style.height = Math.round(viewShown.px) + "px";
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/** eases every node from where it shows to where it rests, and the view with it */
|
|
1101
|
+
function start() {
|
|
1102
|
+
cancelAnimationFrame(anim);
|
|
1103
|
+
const from = new Map();
|
|
1104
|
+
for (const [a] of nodeEls) { const t = target(a); if (t) from.set(a, shown.get(a) || { ...t }); }
|
|
1105
|
+
const v0 = viewShown || viewTarget;
|
|
1106
|
+
const dur = first || reduce ? 0 : 700;
|
|
1107
|
+
first = false;
|
|
1108
|
+
const t0 = performance.now();
|
|
1109
|
+
const step = (now) => {
|
|
1110
|
+
const k = dur ? ease(Math.min(1, (now - t0) / dur)) : 1;
|
|
1111
|
+
for (const [a, f] of from) { const t = target(a); if (t) shown.set(a, { x: lerp(f.x, t.x, k), y: lerp(f.y, t.y, k) }); }
|
|
1112
|
+
viewShown = { x: lerp(v0.x, viewTarget.x, k), y: lerp(v0.y, viewTarget.y, k), w: lerp(v0.w, viewTarget.w, k), h: lerp(v0.h, viewTarget.h, k), px: lerp(v0.px, viewTarget.px, k) };
|
|
1113
|
+
draw();
|
|
1114
|
+
if (k < 1) anim = requestAnimationFrame(step);
|
|
1115
|
+
};
|
|
1116
|
+
step(t0);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** a dot from sender to recipient along their edge, about a second end to end; \`bump\` counts the message on the edge */
|
|
1120
|
+
function pulse(from, to, kind, bump) {
|
|
1121
|
+
if (!data) return;
|
|
1122
|
+
let e = data.edges.find((x) => x.kind === "message" && x.from === from && x.to === to);
|
|
1123
|
+
if (e && bump) { e.count++; e.last_at = new Date().toISOString(); }
|
|
1124
|
+
else if (!e) {
|
|
1125
|
+
if (!data.nodes.some((n) => n.address === from) || !data.nodes.some((n) => n.address === to)) return refresh();
|
|
1126
|
+
data.edges.push({ kind: "message", from, to, count: 1, last_at: new Date().toISOString() });
|
|
1127
|
+
apply();
|
|
1128
|
+
}
|
|
1129
|
+
const p = edgeEls.get("message:" + from + ">" + to);
|
|
1130
|
+
if (!p) return;
|
|
1131
|
+
paintEdge(p);
|
|
1132
|
+
p.classList.add("live");
|
|
1133
|
+
setTimeout(() => p.classList.remove("live"), 1600 / trail);
|
|
1134
|
+
const dest = nodeEls.get(to);
|
|
1135
|
+
const hit = () => {
|
|
1136
|
+
if (!dest) return;
|
|
1137
|
+
dest.g.classList.remove("hit");
|
|
1138
|
+
void dest.g.getBBox();
|
|
1139
|
+
dest.g.classList.add("hit");
|
|
1140
|
+
setTimeout(() => dest.g.classList.remove("hit"), 750);
|
|
1141
|
+
};
|
|
1142
|
+
if (reduce || document.hidden) return hit();
|
|
1143
|
+
const cls = kind === "reply" ? " reply" : "";
|
|
1144
|
+
const halo = el("circle", { class: "pulse-halo" + cls, r: "11" }, layers.pulses);
|
|
1145
|
+
const dot = el("circle", { class: "pulse" + cls, r: "4.5" }, layers.pulses);
|
|
1146
|
+
const t0 = performance.now(), dur = 950 / trail;
|
|
1147
|
+
const step = (now) => {
|
|
1148
|
+
const k = Math.min(1, (now - t0) / dur);
|
|
1149
|
+
let q = null;
|
|
1150
|
+
try { q = p.getPointAtLength(ease(k) * p.getTotalLength()); } catch {}
|
|
1151
|
+
if (q) for (const c of [halo, dot]) { c.setAttribute("cx", r1(q.x)); c.setAttribute("cy", r1(q.y)); }
|
|
1152
|
+
if (k < 1 && p.isConnected) requestAnimationFrame(step);
|
|
1153
|
+
else { halo.remove(); dot.remove(); hit(); }
|
|
1154
|
+
};
|
|
1155
|
+
requestAnimationFrame(step);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function applyFocus() {
|
|
1159
|
+
if (focus && !nodeEls.has(focus)) focus = null;
|
|
1160
|
+
svg.classList.toggle("focus", !!focus);
|
|
1161
|
+
const near = new Set(focus ? [focus] : []);
|
|
1162
|
+
for (const [, p] of edgeEls) {
|
|
1163
|
+
const on = !!focus && (p.dataset.from === focus || p.dataset.to === focus);
|
|
1164
|
+
p.classList.toggle("hl", on);
|
|
1165
|
+
if (on) { near.add(p.dataset.from); near.add(p.dataset.to); }
|
|
1166
|
+
}
|
|
1167
|
+
for (const [a, e] of nodeEls) { e.g.classList.toggle("hl", near.has(a)); e.g.classList.toggle("on", a === focus); }
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function showTip(g) {
|
|
1171
|
+
const n = data && data.nodes.find((x) => x.address === g.dataset.address);
|
|
1172
|
+
if (!n) return;
|
|
1173
|
+
const div = (c, t) => { const d = document.createElement("div"); d.className = c; d.textContent = t; return d; };
|
|
1174
|
+
const head = document.createElement("div");
|
|
1175
|
+
head.className = "t-head";
|
|
1176
|
+
const icon = document.createElementNS(NS, "svg");
|
|
1177
|
+
icon.setAttribute("viewBox", "0 0 24 24");
|
|
1178
|
+
icon.innerHTML = ICONS[n.icon] || ICONS.generic;
|
|
1179
|
+
if (COLORED.has(n.icon)) icon.style.color = "var(--h-" + n.icon + ")";
|
|
1180
|
+
head.append(icon, document.createTextNode(n.name));
|
|
1181
|
+
const sum = (side) => data.edges.filter((e) => e.kind === "message" && e[side] === n.address).reduce((s, e) => s + e.count, 0);
|
|
1182
|
+
const out = sum("from"), inn = sum("to");
|
|
1183
|
+
const wake = n.how ? n.how : n.kind === "agent" && n.presence === "idle" ? "reads at its next turn" : "";
|
|
1184
|
+
const rows = [
|
|
1185
|
+
["presence", n.presence + (wake ? ", " + wake : "")],
|
|
1186
|
+
["status", n.status ? n.status.state + (n.status.note ? ": " + n.status.note : "") : ""],
|
|
1187
|
+
["harness", n.kind === "agent" ? n.harness || "unknown" : ""],
|
|
1188
|
+
["machine", n.machine],
|
|
1189
|
+
["member", (data.machines.find((m) => m.name === n.machine)?.identity || {}).name || ""],
|
|
1190
|
+
["parent", n.parent ? n.parent.split("/").slice(2).join("/") : ""],
|
|
1191
|
+
["messages", out || inn ? out + " sent, " + inn + " received in " + data.minutes + " min" : ""],
|
|
1192
|
+
].filter((r) => r[1]);
|
|
1193
|
+
const kv = document.createElement("dl");
|
|
1194
|
+
kv.className = "t-kv";
|
|
1195
|
+
for (const [k, v] of rows) { const dt = document.createElement("dt"); dt.textContent = k; const dd = document.createElement("dd"); dd.textContent = v; kv.append(dt, dd); }
|
|
1196
|
+
tip.replaceChildren(head, div("t-addr", n.address), ...(n.title ? [div("t-row", n.title)] : []), kv);
|
|
1197
|
+
tip.hidden = false;
|
|
1198
|
+
const c = canvas.getBoundingClientRect(), r = g.getBoundingClientRect();
|
|
1199
|
+
let left = r.right - c.left + 10;
|
|
1200
|
+
if (left + tip.offsetWidth > canvas.clientWidth - 8) left = r.left - c.left - tip.offsetWidth - 10;
|
|
1201
|
+
tip.style.left = Math.max(8, left) + "px";
|
|
1202
|
+
tip.style.top = Math.max(8, Math.min(r.top - c.top - 6, canvas.clientHeight - tip.offsetHeight - 8)) + "px";
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function hideTip() { tip.hidden = true; }
|
|
1206
|
+
|
|
1207
|
+
function refresh() { document.dispatchEvent(new CustomEvent("scp:refresh")); }
|
|
1208
|
+
|
|
1209
|
+
/** a refreshed live region; without the stream, a newer message on an edge pulses it */
|
|
1210
|
+
function update() {
|
|
1211
|
+
const next = read();
|
|
1212
|
+
if (!next) return;
|
|
1213
|
+
const before = data;
|
|
1214
|
+
data = next;
|
|
1215
|
+
apply();
|
|
1216
|
+
if (!before || streamOpen) return;
|
|
1217
|
+
for (const e of next.edges) {
|
|
1218
|
+
if (e.kind !== "message") continue;
|
|
1219
|
+
const o = before.edges.find((x) => x.kind === "message" && x.from === e.from && x.to === e.to);
|
|
1220
|
+
if (!o || e.last_at > o.last_at) pulse(e.from, e.to, "message", false);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function onEvent(ev) {
|
|
1225
|
+
if (!data || !ev || typeof ev !== "object") return;
|
|
1226
|
+
if (ev.type === "message" && Array.isArray(ev.to)) for (const to of ev.to) pulse(ev.from, to, ev.kind, true);
|
|
1227
|
+
else if (ev.type === "presence") {
|
|
1228
|
+
const n = data.nodes.find((x) => x.address === ev.endpoint);
|
|
1229
|
+
if (!n || typeof ev.waking === "boolean" || n.presence === "hibernated") return refresh();
|
|
1230
|
+
if (n.presence === ev.presence) return;
|
|
1231
|
+
n.presence = ev.presence;
|
|
1232
|
+
const m = data.machines.find((x) => x.name === n.machine);
|
|
1233
|
+
if (m && ev.presence !== "offline" && ev.presence !== "hibernated") m.online = true;
|
|
1234
|
+
apply();
|
|
1235
|
+
} else if (ev.type === "endpoints" || ev.type === "hello") refresh();
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
let backoff = 1000;
|
|
1239
|
+
function connect() {
|
|
1240
|
+
const path = root.getAttribute("data-events");
|
|
1241
|
+
if (!path || typeof WebSocket !== "function") return;
|
|
1242
|
+
let ws, ping = 0;
|
|
1243
|
+
try { ws = new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + location.host + path); } catch { return; }
|
|
1244
|
+
ws.onopen = () => { backoff = 1000; setStream(true); ping = setInterval(() => { try { ws.send("ping"); } catch {} }, 30000); };
|
|
1245
|
+
ws.onmessage = (m) => { if (typeof m.data !== "string" || m.data === "pong") return; try { onEvent(JSON.parse(m.data)); } catch {} };
|
|
1246
|
+
ws.onclose = (c) => {
|
|
1247
|
+
clearInterval(ping);
|
|
1248
|
+
setStream(false);
|
|
1249
|
+
if (c.code === 4003) return;
|
|
1250
|
+
setTimeout(connect, backoff);
|
|
1251
|
+
backoff = Math.min(backoff * 2, 30000);
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function setStream(open) {
|
|
1255
|
+
streamOpen = open;
|
|
1256
|
+
document.documentElement.setAttribute("data-stream", open ? "open" : "poll");
|
|
1257
|
+
for (const s of document.querySelectorAll("[data-graph-stream]")) s.textContent = open ? "live" : "polling";
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const nodeAt = (e) => (e.target instanceof Element ? e.target.closest(".node") : null);
|
|
1261
|
+
svg.addEventListener("click", (e) => { const g = nodeAt(e); focus = g && g.dataset.address !== focus ? g.dataset.address : null; applyFocus(); });
|
|
1262
|
+
svg.addEventListener("keydown", (e) => {
|
|
1263
|
+
const g = nodeAt(e);
|
|
1264
|
+
if (!g || (e.key !== "Enter" && e.key !== " ")) return;
|
|
1265
|
+
e.preventDefault();
|
|
1266
|
+
focus = g.dataset.address !== focus ? g.dataset.address : null;
|
|
1267
|
+
applyFocus();
|
|
1268
|
+
});
|
|
1269
|
+
svg.addEventListener("pointerover", (e) => { const g = nodeAt(e); if (g) showTip(g); });
|
|
1270
|
+
svg.addEventListener("pointerout", (e) => { const g = nodeAt(e); if (g && !g.contains(e.relatedTarget)) hideTip(); });
|
|
1271
|
+
svg.addEventListener("focusin", (e) => { const g = nodeAt(e); if (g) showTip(g); });
|
|
1272
|
+
svg.addEventListener("focusout", hideTip);
|
|
1273
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && focus) { focus = null; applyFocus(); } });
|
|
1274
|
+
const trailButtons = document.querySelectorAll("[data-trail]");
|
|
1275
|
+
function paintTrail() {
|
|
1276
|
+
svg.style.setProperty("--trail", String(trail));
|
|
1277
|
+
for (const b of trailButtons) b.setAttribute("aria-pressed", String(Number(b.getAttribute("data-trail")) === trail));
|
|
1278
|
+
}
|
|
1279
|
+
for (const b of trailButtons) b.addEventListener("click", () => { trail = Number(b.getAttribute("data-trail")); save("scp.graph.trails", String(trail)); paintTrail(); });
|
|
1280
|
+
paintTrail();
|
|
1281
|
+
if (bar) for (const b of bar.querySelectorAll("[data-view-set]")) b.addEventListener("click", () => setView(b.getAttribute("data-view-set")));
|
|
1282
|
+
for (const [input, key] of [[offline, "scp.overview.offline"], [nextTurn, "scp.graph.nextturn"]]) {
|
|
1283
|
+
if (!input) continue;
|
|
1284
|
+
input.checked = load(key) === "1";
|
|
1285
|
+
input.addEventListener("change", () => { save(key, input.checked ? "1" : "0"); apply(); });
|
|
1286
|
+
}
|
|
1287
|
+
if (historic) {
|
|
1288
|
+
historic.checked = load("scp.graph.historic") === "1";
|
|
1289
|
+
historic.addEventListener("change", () => { save("scp.graph.historic", historic.checked ? "1" : "0"); apply(); refresh(); });
|
|
1290
|
+
}
|
|
1291
|
+
let size = "";
|
|
1292
|
+
new ResizeObserver(() => { const s = canvas.clientWidth + "x" + (full ? canvas.clientHeight : 0); if (canvas.clientWidth && s !== size) { size = s; apply(); } }).observe(canvas);
|
|
1293
|
+
new MutationObserver(update).observe(live, { childList: true });
|
|
1294
|
+
setInterval(() => { for (const [, p] of edgeEls) paintEdge(p); }, 20000);
|
|
1295
|
+
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => { widths.clear(); shapes.clear(); apply(); });
|
|
1296
|
+
setStream(false);
|
|
1297
|
+
update();
|
|
1298
|
+
connect();
|
|
1299
|
+
})();
|
|
1300
|
+
`;var ze={overview:"",agents:"/sessions",devices:"/tokens",machines:"/tokens",settings:"/members",judge:"/members"};function Ye(e){if(!e?.startsWith("/")||/[\u0000-\u001f\u007f\\]/.test(e))return"/";let t="https://relay.invalid";try{let n=new URL(e,t);return n.origin===t&&n.pathname.replace(/\/+$/,"")!=="/sign-in"?n.pathname+n.search+n.hash:"/"}catch{return"/"}}function Ke(e,t){let n=e.replace(/\/+$/,"")||"/",o=`/orgs/${t.org.handle}`;if(n==="/"||n==="/orgs"||n==="/sign-in")return{redirect:o};if(n==="/device")return{page:"device"};if(n===o)return{tab:"overview"};if(!n.startsWith(`${o}/`))return{page:"not-found"};let i=n.slice(o.length+1);if(Object.hasOwn(ze,i))return{redirect:o+ze[i]};if(["activity","sessions","graph","graph/full","tokens","members"].includes(i))return{tab:i};return{page:"not-found"}}function T(e,t,n){return re({...t,email:e.signedIn?e.member.name:null,csrf:e.csrf},n)}function Zt(e,t,n,o){let i=n.graph?B(n.graph,ee(e.members.map((s)=>({user_id:s.id,email:s.name})))):null;switch(t){case"overview":{let s={devices:e.devices,tokens:e.tokens,holds:null};return Pe(n.overview,i,s,e.org.handle,o)}case"activity":return n.overview?pe(n.overview.activity,o):P();case"sessions":return he(n.overview,e.org.handle,o);case"tokens":return r`${v("Tokens and devices are managed with the scp-relay token CLI.")}`;case"members":return r`${v("Members are managed with the scp-relay account CLI.")}`;case"graph":return Ve({org:e.org,role:e.member.role},i,{historic:!0})}}function Ze(e,t,n,o){let i={org:e.org,role:e.member.role},s=t==="overview"?e.org.name:`${e.org.name} ${t==="graph/full"?"graph":t}`;if(t==="graph/full"){let p=n.graph?B(n.graph,ee(e.members.map((b)=>({user_id:b.id,email:b.name})))):null;return T(e,{title:s,bare:!0,head:fe()},Je(e.org,p,{historic:!0}))}let l=Zt(e,t,n,o);if(t==="graph")return T(e,{title:s,nav:"orgs",head:fe()},l);let d=t==="overview"||t==="activity"||t==="sessions",{body:a,...c}=Ue(i,t,l,{live:d});return T(e,{...c,title:s},a)}function N(e,t="",n){return T(e,{title:"Sign in",narrow:!0},r`<p class="label">Self-hosted relay</p><h1>Sign in</h1>
|
|
1301
|
+
<p class="lead">Use a relay token or sign in with a device code.</p>
|
|
1302
|
+
${v(t,!0)}
|
|
1303
|
+
<form class="form" data-sign-in method="post" action="/v1/dashboard/sign-in">
|
|
1304
|
+
<input type="hidden" name="csrf" value="${e.csrf}">
|
|
1305
|
+
<label class="field"><span>Token</span><input type="password" name="token" placeholder="scp_t_... or scp_d_..." required autofocus autocomplete="off"></label>
|
|
1306
|
+
<div class="acts"><button class="btn btn-primary" type="submit">Sign in</button></div>
|
|
1307
|
+
</form>
|
|
1308
|
+
<form class="form" data-device method="post" action="/v1/dashboard/device">
|
|
1309
|
+
<input type="hidden" name="csrf" value="${e.csrf}">
|
|
1310
|
+
<div class="acts"><button class="btn btn-ghost" type="submit"${n?r` disabled`:""}>Sign in with a device code</button></div>
|
|
1311
|
+
</form>
|
|
1312
|
+
${n?r`<section class="block"><h2>Approve this device</h2><p class="small">Run this command on the relay, replacing the account and member placeholders:</p><pre><code>scp-relay device approve ${n.user_code} --account <handle> --member <name></code></pre><p class="mini" role="status">Waiting for approval.</p></section>`:""}`)}var Ee=(e)=>`'${e.replace(/'/g,"'\\''")}'`;function Qe(e,t="<user_code>"){return T(e,{title:"Device",nav:"device",narrow:!0},r`<p class="label">Device sign-in</p><h1>Approve a device</h1>
|
|
1313
|
+
<p class="lead">Approve the code shown on the device by running this command on the relay:</p>
|
|
1314
|
+
<pre><code>scp-relay device approve ${Ee(t)} --account ${Ee(e.org.handle)} --member ${Ee(e.member.name)}</code></pre>
|
|
1315
|
+
<p class="small">Only approve a code for a device you recognize.</p>`)}function et(e){return T(e,{title:"Not found",narrow:!0},r`<h1>Not found</h1><p class="lead">This page does not exist.</p><a href="/">Return home</a>`)}function be(e,t){return T(e,{title:"Dashboard unavailable",narrow:!0},r`<h1>Dashboard unavailable</h1>${v(t,!0)}<a href="">Reload</a>`)}var m={signedIn:!1,csrf:""},D=0,we=!1;class M extends Error{}async function I(e,t){let n=await fetch(`/v1/dashboard/${e}`,{method:t?"POST":"GET",credentials:"same-origin",cache:"no-store",headers:t?{accept:"application/json","content-type":"application/json"}:{accept:"application/json"},...t?{body:JSON.stringify({...t,csrf:m.csrf})}:{}}),o=await n.json();if(!n.ok){let i=o&&typeof o==="object"?o:{};if(n.status===401&&i.error==="signed_out")throw new M;throw Error(typeof i.message==="string"?i.message:i.error==="invalid_token"?"The token is not valid.":"The relay could not complete the request.")}return o}function w(e){let t=new DOMParser().parseFromString(e,"text/html"),n=[...t.querySelectorAll("script[src]")];document.head.replaceWith(t.head),document.body.replaceWith(t.body);for(let o of n){let i=document.createElement("script");for(let s of o.attributes)i.setAttribute(s.name,s.value);i.async=!1,o.replaceWith(i)}}function ye(){location.replace(`/sign-in?next=${encodeURIComponent(location.pathname+location.search)}`)}async function tt(e){try{return await I(e)}catch(t){if(t instanceof M)throw t;return null}}function Qt(){try{return localStorage.getItem("scp.graph.historic")==="1"}catch{return!1}}async function en(e){let t=["overview","activity","sessions","graph","graph/full"].includes(e),n=["overview","graph","graph/full"].includes(e),[o,i]=await Promise.all([t?tt("overview"):null,n?tt(Qt()?"graph?historic=1":"graph"):null]);return{overview:o,graph:i}}var Z=(e)=>e instanceof Error?e.message:"The relay could not complete the request.",nt=()=>location.replace(Ye(new URLSearchParams(location.search).get("next")));async function tn(e,t){let n=Date.now()+e.expires_in*1000,o=Math.max(1,e.interval)*1000;while(t===D){if(await new Promise((s)=>setTimeout(s,o)),t!==D)return;if(Date.now()>=n)break;let i=await I("device/poll",{device_code:e.device_code});if(t!==D)return;if(i.status==="ok"){nt();return}if(i.status!=="pending"){w(N(m,i.status==="denied"?"Device sign-in was denied.":"The device code expired. Try again."));return}}if(t===D)w(N(m,"The device code expired. Try again."))}document.addEventListener("submit",(e)=>{let t=e.target;if(!(t instanceof HTMLFormElement))return;let n=t.getAttribute("action")==="/sign-out",o=t.hasAttribute("data-sign-in"),i=t.hasAttribute("data-device");if(!n&&!o&&!i)return;if(e.preventDefault(),we)return;we=!0;let s=++D;for(let l of t.querySelectorAll("button"))l.disabled=!0;(async()=>{try{if(n)await I("sign-out",{}),location.replace("/sign-in");else if(o){let l=new FormData(t).get("token");await I("sign-in",{token:typeof l==="string"?l:""}),nt()}else{let l=await I("device",{});w(N(m,"",l)),tn(l,s).catch((d)=>{if(s===D)w(N(m,Z(d)))})}}catch(l){if(l instanceof M)ye();else w(m.signedIn?be(m,Z(l)):N(m,Z(l)))}finally{we=!1}})()});async function nn(){if(m=await I("session"),!m.signedIn){w(N(m));return}let e=m,t=Ke(location.pathname,e);if("redirect"in t){location.replace(t.redirect);return}if("page"in t){w(t.page==="device"?Qe(e,new URLSearchParams(location.search).get("user_code")??void 0):et(e));return}let n=async()=>Ze(e,t.tab,await en(t.tab),Date.now());window.scpRegion=async()=>{try{return new DOMParser().parseFromString(await n(),"text/html").querySelector("[data-live]")}catch(o){if(o instanceof M)ye();return null}},w(await n())}nn().catch((e)=>{if(e instanceof M)ye();else w(be(m,Z(e)))});})();
|