infinicode 2.3.5 → 2.4.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/.opencode/plugins/home-logo-animation.tsx +5 -1
- package/.opencode/plugins/mesh-commands.tsx +4 -3
- package/.opencode/plugins/routing-mode-display.tsx +6 -2
- package/.opencode/tui.json +2 -1
- package/bin/robopark.js +2 -0
- package/dist/cli.js +46 -1
- package/dist/commands/maintain.d.ts +29 -0
- package/dist/commands/maintain.js +186 -0
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/mesh-install.d.ts +10 -0
- package/dist/commands/mesh-install.js +160 -0
- package/dist/commands/robopark.d.ts +6 -0
- package/dist/commands/robopark.js +450 -0
- package/dist/commands/run.js +82 -22
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +152 -11
- package/dist/kernel/agents/backends/cli-backend.d.ts +41 -0
- package/dist/kernel/agents/backends/cli-backend.js +140 -0
- package/dist/kernel/agents/backends/detect.d.ts +17 -0
- package/dist/kernel/agents/backends/detect.js +140 -0
- package/dist/kernel/agents/backends/index.d.ts +11 -0
- package/dist/kernel/agents/backends/index.js +3 -0
- package/dist/kernel/agents/backends/parse-utils.d.ts +20 -0
- package/dist/kernel/agents/backends/parse-utils.js +72 -0
- package/dist/kernel/agents/backends/profiles.d.ts +67 -0
- package/dist/kernel/agents/backends/profiles.js +48 -0
- package/dist/kernel/agents/backends/registry.d.ts +31 -0
- package/dist/kernel/agents/backends/registry.js +174 -0
- package/dist/kernel/agents/backends/rotation.d.ts +51 -0
- package/dist/kernel/agents/backends/rotation.js +107 -0
- package/dist/kernel/agents/backends/types.d.ts +67 -0
- package/dist/kernel/agents/backends/types.js +1 -0
- package/dist/kernel/agents/index.d.ts +8 -0
- package/dist/kernel/agents/index.js +8 -0
- package/dist/kernel/federation/dashboard-html.d.ts +13 -0
- package/dist/kernel/federation/dashboard-html.js +371 -0
- package/dist/kernel/federation/federation.d.ts +112 -2
- package/dist/kernel/federation/federation.js +270 -10
- package/dist/kernel/federation/moltfed-adapter.js +1 -0
- package/dist/kernel/federation/telemetry.d.ts +1 -1
- package/dist/kernel/federation/telemetry.js +2 -1
- package/dist/kernel/federation/transport-http.d.ts +17 -1
- package/dist/kernel/federation/transport-http.js +65 -2
- package/dist/kernel/federation/types.d.ts +46 -1
- package/dist/kernel/free-providers.js +83 -0
- package/dist/kernel/gateway/openai-gateway.d.ts +55 -3
- package/dist/kernel/gateway/openai-gateway.js +428 -49
- package/dist/kernel/index.d.ts +2 -1
- package/dist/kernel/index.js +3 -1
- package/dist/kernel/logger.d.ts +16 -3
- package/dist/kernel/logger.js +38 -0
- package/dist/kernel/mcp/mcp-server.js +65 -10
- package/dist/kernel/orchestrator.d.ts +4 -0
- package/dist/kernel/orchestrator.js +34 -0
- package/dist/kernel/provider-manager.d.ts +13 -0
- package/dist/kernel/provider-manager.js +58 -5
- package/dist/kernel/providers/openai-compatible-provider.d.ts +6 -0
- package/dist/kernel/providers/openai-compatible-provider.js +22 -3
- package/dist/kernel/recovery-manager.js +55 -15
- package/dist/kernel/router.js +15 -2
- package/dist/kernel/types.d.ts +12 -2
- package/dist/robopark-cli.d.ts +2 -0
- package/dist/robopark-cli.js +23 -0
- package/package.json +6 -3
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — RoboPark Control dashboard (served by a mesh node).
|
|
3
|
+
*
|
|
4
|
+
* A single self-contained page (no external assets — CSP-clean, ships in dist as
|
|
5
|
+
* a compiled string). It polls GET /fed/status, renders the fleet + this node's
|
|
6
|
+
* hardware + a live activity feed, and runs allow-listed actions via
|
|
7
|
+
* POST /fed/command and POST /fed/apply. The browser authenticates every request
|
|
8
|
+
* with the ?token= it was loaded with (browsers can't set Bearer on navigation).
|
|
9
|
+
*
|
|
10
|
+
* Kept free of backticks and ${...} so the whole page fits inside this TS
|
|
11
|
+
* template literal without escaping; the client JS uses string concatenation.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DASHBOARD_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>RoboPark Control</title>\n<style>\n:root{\n --bg:#FFF8DE; --panel:#FBF1CE; --panel-2:#F3E4B4; --border:#E0C982; --border-strong:#B97816;\n --ink:#231704; --muted:#6E4A12; --gold:#7A420A; --gold-bright:#B97816;\n --ok:#2F7A32; --ok-bg:#E4F0DD; --warn:#9A6A12; --warn-bg:#F6E9C4; --bad:#B5382E; --bad-bg:#F6DED9;\n --info:#1F6F9E; --info-bg:#DCEEF7;\n --mono:ui-monospace,\"Cascadia Code\",Consolas,monospace;\n --sans:system-ui,\"Segoe UI\",Roboto,sans-serif;\n}\n@media (prefers-color-scheme:dark){:root{\n --bg:#0C0904; --panel:#161009; --panel-2:#1F1710; --border:#3A2A0C; --border-strong:#7A420A;\n --ink:#F3E6C4; --muted:#A98A4E; --gold:#E6B23C; --gold-bright:#FFD86B;\n --ok:#7FCE7A; --ok-bg:#15230F; --warn:#E0A23C; --warn-bg:#241A0A; --bad:#FF7A6E; --bad-bg:#2A100C;\n --info:#6FB6FF; --info-bg:#0F2231;\n}}\n:root[data-theme=\"dark\"]{\n --bg:#0C0904; --panel:#161009; --panel-2:#1F1710; --border:#3A2A0C; --border-strong:#7A420A;\n --ink:#F3E6C4; --muted:#A98A4E; --gold:#E6B23C; --gold-bright:#FFD86B;\n --ok:#7FCE7A; --ok-bg:#15230F; --warn:#E0A23C; --warn-bg:#241A0A; --bad:#FF7A6E; --bad-bg:#2A100C;\n --info:#6FB6FF; --info-bg:#0F2231;\n}\n:root[data-theme=\"light\"]{\n --bg:#FFF8DE; --panel:#FBF1CE; --panel-2:#F3E4B4; --border:#E0C982; --border-strong:#B97816;\n --ink:#231704; --muted:#6E4A12; --gold:#7A420A; --gold-bright:#B97816;\n --ok:#2F7A32; --ok-bg:#E4F0DD; --warn:#9A6A12; --warn-bg:#F6E9C4; --bad:#B5382E; --bad-bg:#F6DED9;\n --info:#1F6F9E; --info-bg:#DCEEF7;\n}\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%}\nbody{background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.45;-webkit-font-smoothing:antialiased;}\n.mono{font-family:var(--mono);font-variant-numeric:tabular-nums;}\n\n/* top bar */\nheader.bar{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;\n padding:0.7rem 1.1rem;border-bottom:1px solid var(--border);background:var(--panel);position:sticky;top:0;z-index:5;}\n.brand{display:flex;align-items:center;gap:0.6rem;}\n.livedot{width:10px;height:10px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 70%,transparent);animation:pulse 2.4s infinite;flex:none;}\n.livedot.stale{background:var(--muted);animation:none;}\n@keyframes pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 55%,transparent)}70%{box-shadow:0 0 0 7px transparent}100%{box-shadow:0 0 0 0 transparent}}\n@media (prefers-reduced-motion:reduce){.livedot{animation:none}}\n.brand b{font-size:0.98rem;letter-spacing:-0.01em;}\n.brand .who{font-family:var(--mono);font-size:0.72rem;color:var(--muted);}\n.chips{display:flex;gap:0.45rem;flex-wrap:wrap;align-items:center;}\n.chip{font-family:var(--mono);font-size:0.68rem;padding:0.28em 0.62em;border-radius:100px;border:1px solid var(--border);\n background:var(--panel-2);color:var(--muted);display:inline-flex;gap:0.4em;align-items:center;white-space:nowrap;}\n.chip b{color:var(--ink);font-weight:600;}\n.chip.ok b{color:var(--ok)} .chip.bad b{color:var(--bad)} .chip.info b{color:var(--info)}\n.themebtn{font-family:var(--mono);font-size:0.68rem;background:transparent;border:1px solid var(--border);color:var(--muted);\n border-radius:100px;padding:0.34em 0.8em;cursor:pointer;}\n.themebtn:focus-visible{outline:2px solid var(--gold-bright);outline-offset:2px;}\n\n/* layout */\n.wrap{display:grid;grid-template-columns:1fr 340px;gap:1rem;padding:1rem 1.1rem 6.5rem;max-width:1400px;margin:0 auto;}\n@media (max-width:920px){.wrap{grid-template-columns:1fr}}\nsection h2,aside h2{font-family:var(--mono);font-size:0.7rem;letter-spacing:0.12em;text-transform:uppercase;color:var(--gold-bright);\n margin:0.2rem 0 0.7rem;display:flex;align-items:center;gap:0.5rem;}\nsection h2 .count{color:var(--muted);}\n\n/* fleet cards */\n.fleet{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:0.75rem;}\n.node{background:var(--panel);border:1px solid var(--border);border-left:4px solid var(--muted);border-radius:9px;\n padding:0.75rem 0.85rem;cursor:pointer;position:relative;transition:border-color .12s,transform .12s;}\n.node:hover{transform:translateY(-1px);}\n.node.up{border-left-color:var(--ok);} .node.down{border-left-color:var(--muted);opacity:0.72;}\n.node.sel{outline:2px solid var(--gold-bright);outline-offset:1px;}\n.node:focus-visible{outline:2px solid var(--gold-bright);outline-offset:1px;}\n.node .top{display:flex;align-items:baseline;justify-content:space-between;gap:0.5rem;margin-bottom:0.15rem;}\n.node .name{font-family:var(--mono);font-size:0.9rem;font-weight:600;word-break:break-word;}\n.node .role{font-family:var(--mono);font-size:0.62rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--gold);}\n.node .meta{font-size:0.72rem;color:var(--muted);margin-bottom:0.5rem;display:flex;gap:0.5rem;flex-wrap:wrap;}\n.node .selfbadge{font-family:var(--mono);font-size:0.58rem;color:var(--info);border:1px solid color-mix(in srgb,var(--info) 40%,transparent);\n border-radius:4px;padding:0 0.3em;}\n.bar-track{height:6px;border-radius:4px;background:var(--panel-2);overflow:hidden;margin-bottom:0.3rem;}\n.bar-fill{height:100%;background:var(--ok);width:0;transition:width .4s ease;}\n.bar-fill.warn{background:var(--warn)} .bar-fill.bad{background:var(--bad)}\n.node .load{font-family:var(--mono);font-size:0.64rem;color:var(--muted);display:flex;justify-content:space-between;}\n.caps{display:flex;gap:0.25rem;flex-wrap:wrap;margin-top:0.5rem;}\n.cap{font-family:var(--mono);font-size:0.58rem;color:var(--muted);background:var(--panel-2);border-radius:4px;padding:0.05em 0.35em;}\n.empty{color:var(--muted);font-size:0.82rem;padding:1.4rem;text-align:center;border:1px dashed var(--border);border-radius:9px;}\n\n/* hardware meters */\n.hw{background:var(--panel);border:1px solid var(--border);border-radius:9px;padding:0.85rem;margin-top:1rem;}\n.meter{margin-bottom:0.65rem;}\n.meter:last-child{margin-bottom:0;}\n.meter .lab{display:flex;justify-content:space-between;font-family:var(--mono);font-size:0.68rem;color:var(--muted);margin-bottom:0.25rem;}\n.meter .lab b{color:var(--ink);}\n\n/* activity feed */\naside{position:sticky;top:64px;align-self:start;max-height:calc(100vh - 84px);display:flex;flex-direction:column;}\n.feed{background:var(--panel);border:1px solid var(--border);border-radius:9px;overflow-y:auto;flex:1;min-height:240px;font-family:var(--mono);font-size:0.7rem;}\n.line{padding:0.28rem 0.6rem;border-bottom:1px solid color-mix(in srgb,var(--border) 55%,transparent);display:flex;gap:0.5rem;line-height:1.3;}\n.line:last-child{border-bottom:none;}\n.line .t{color:var(--muted);flex:none;}\n.line .n{color:var(--gold);flex:none;max-width:9ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.line .d{color:var(--ink);flex:1;word-break:break-word;}\n.line.k-ev .d{color:var(--info);} .line.k-hw .n{color:var(--muted);} .line.k-rt .d{color:var(--gold-bright);}\n\n/* command bar */\nfooter.cmd{position:fixed;left:0;right:0;bottom:0;background:var(--panel);border-top:1px solid var(--border-strong);\n padding:0.6rem 1.1rem;display:flex;gap:0.55rem;align-items:center;flex-wrap:wrap;z-index:6;}\nfooter .sel{font-family:var(--mono);font-size:0.72rem;color:var(--muted);white-space:nowrap;}\nfooter .sel b{color:var(--ink);}\nfooter input{flex:1;min-width:180px;font-family:var(--mono);font-size:0.8rem;background:var(--bg);color:var(--ink);\n border:1px solid var(--border);border-radius:7px;padding:0.5em 0.7em;}\nfooter input:focus-visible{outline:2px solid var(--gold-bright);outline-offset:1px;}\nbutton.act{font-family:var(--sans);font-size:0.76rem;font-weight:600;cursor:pointer;border-radius:7px;padding:0.5em 0.85em;\n border:1px solid var(--border-strong);background:var(--gold-bright);color:#231704;white-space:nowrap;}\nbutton.act.ghost{background:transparent;color:var(--gold);}\nbutton.act:focus-visible{outline:2px solid var(--ink);outline-offset:1px;}\nbutton.act:disabled{opacity:0.5;cursor:default;}\n#out{position:fixed;right:1.1rem;bottom:3.6rem;max-width:min(560px,90vw);max-height:44vh;overflow:auto;background:var(--panel-2);\n border:1px solid var(--border-strong);border-radius:9px;padding:0.7rem 0.85rem;font-family:var(--mono);font-size:0.72rem;\n white-space:pre-wrap;display:none;box-shadow:0 8px 30px rgba(0,0,0,0.25);z-index:7;}\n#out .x{float:right;cursor:pointer;color:var(--muted);border:none;background:none;font-family:var(--mono);}\n.offline{background:var(--bad-bg);color:var(--bad);text-align:center;font-family:var(--mono);font-size:0.72rem;padding:0.4rem;display:none;}\n</style>\n</head>\n<body>\n<div class=\"offline\" id=\"offline\">connection lost \u2014 retrying</div>\n<header class=\"bar\">\n <div class=\"brand\">\n <span class=\"livedot\" id=\"livedot\"></span>\n <div>\n <b>RoboPark Control</b>\n <div class=\"who mono\" id=\"who\">connecting\u2026</div>\n </div>\n </div>\n <div class=\"chips\" id=\"summary\"></div>\n <button class=\"themebtn\" id=\"theme\">Theme</button>\n</header>\n\n<div class=\"wrap\">\n <section>\n <h2>Fleet <span class=\"count\" id=\"fleetcount\"></span></h2>\n <div class=\"fleet\" id=\"fleet\"></div>\n <div class=\"hw\" id=\"hw\"></div>\n </section>\n <aside>\n <h2>Live activity</h2>\n <div class=\"feed\" id=\"feed\"></div>\n </aside>\n</div>\n\n<footer class=\"cmd\">\n <span class=\"sel\">on <b id=\"selName\">this node</b></span>\n <input id=\"cmdInput\" placeholder=\"/status /nodes /health (Enter to run)\" autocomplete=\"off\" spellcheck=\"false\">\n <button class=\"act\" id=\"runBtn\">Run</button>\n <button class=\"act ghost\" id=\"applyBtn\">Apply config to fleet</button>\n</footer>\n<div id=\"out\"><button class=\"x\" id=\"outClose\">close</button><span id=\"outText\"></span></div>\n\n<script>\n(function(){\n var TOKEN = new URLSearchParams(location.search).get('token') || '';\n var QS = TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '';\n var selfId = null, selected = null, lastSeq = -1, everOk = false;\n\n var $ = function(id){ return document.getElementById(id); };\n function esc(s){ var d=document.createElement('div'); d.textContent = (s==null?'':String(s)); return d.innerHTML; }\n function fmtBytes(b){ if(!b) return '0'; var u=['B','KB','MB','GB','TB']; var i=Math.floor(Math.log(b)/Math.log(1024)); return (b/Math.pow(1024,i)).toFixed(i?1:0)+u[i]; }\n function loadClass(v){ return v>=80?'bad':(v>=50?'warn':''); }\n function clock(ts){ var d=new Date(ts); function p(n){return (n<10?'0':'')+n;} return p(d.getHours())+':'+p(d.getMinutes())+':'+p(d.getSeconds()); }\n\n // theme\n (function(){ var k='rp-ctl-theme', s=null; try{s=localStorage.getItem(k)}catch(e){}\n if(s) document.documentElement.setAttribute('data-theme', s);\n $('theme').addEventListener('click', function(){\n var m=window.matchMedia('(prefers-color-scheme: dark)');\n var cur=document.documentElement.getAttribute('data-theme')||(m.matches?'dark':'light');\n var nx=cur==='dark'?'light':'dark';\n document.documentElement.setAttribute('data-theme', nx);\n try{localStorage.setItem(k,nx)}catch(e){}\n });\n })();\n\n // Pull a short human detail out of an event's data payload.\n function detailOf(data){\n var p = data || {};\n if(typeof p==='string') return p;\n if(p.providerId!=null) return p.providerId + (('healthy' in p)?(p.healthy?' \u2713':' \u2717'):'');\n if(p.model) return String(p.model);\n if(p.provider) return String(p.provider);\n if(p.reason) return String(p.reason).slice(0,60);\n if(p.description) return String(p.description).slice(0,50);\n if(p.name) return String(p.name);\n return '';\n }\n function niceKind(k){ return String(k||'').toLowerCase().replace(/_/g,' '); }\n\n function summarizeFrame(f){\n var d = f.d || {};\n if(f.t==='hw'){ var c=d.cpu?Math.round(d.cpu.percent):0; var m=d.memory?Math.round(d.memory.percent):0; return 'cpu '+c+'% \u00B7 mem '+m+'%'; }\n if(f.t==='hb'){ return 'heartbeat \u00B7 load '+(d.load!=null?d.load:'-'); }\n if(f.t==='nd'){ return 'fleet update ('+(Array.isArray(d)?d.length:0)+' nodes)'; }\n // routing decision: compact payload is {k, m, task, w, d}\n if(f.t==='rt'){ var rp=d.d||{}; return 'route \u2192 '+(rp.model||rp.provider||rp.decision||detailOf(rp)||'?'); }\n // immediate lifecycle event: {k:type, m, task, w, d:payload}\n if(f.t==='ev'){ var det=detailOf(d.d); return niceKind(d.k)+(det?(' '+det):''); }\n // coalesced chatter: {count, tail:[{k, d}]}\n if(f.t==='log'){\n if(d.tail && d.tail.length){ var last=d.tail[d.tail.length-1]; var ld=detailOf(last.d);\n return niceKind(last.k)+(ld?(' '+ld):'')+(d.count>1?(' \u00D7'+d.count):''); }\n return 'log';\n }\n return f.t;\n }\n\n function renderFeed(activity){\n var feed = $('feed');\n if(!activity || !activity.length){\n if(!feed.dataset.seeded){ feed.innerHTML = '<div class=\"line\"><span class=\"d\" style=\"color:var(--muted)\">waiting for fleet activity \u2014 task runs, routing, and peer events stream here.</span></div>'; }\n return;\n }\n feed.dataset.seeded = '1';\n var near = feed.scrollHeight - feed.scrollTop - feed.clientHeight < 40;\n var html = '';\n for(var i=0;i<activity.length;i++){\n var e = activity[i]; var f = e.f || e; if(f.s!=null && f.s<=lastSeq && f.t!=='hw') { /* keep */ }\n var name = (f.n||'').slice(0,10);\n html += '<div class=\"line k-'+esc(f.t)+'\"><span class=\"t\">'+clock(f.ts)+'</span>'\n + '<span class=\"n\" title=\"'+esc(f.n)+'\">'+esc(name)+'</span>'\n + '<span class=\"d\">'+esc(summarizeFrame(f))+'</span></div>';\n }\n feed.innerHTML = html;\n if(near) feed.scrollTop = feed.scrollHeight;\n }\n\n function meter(label, value, sub){\n var v = Math.max(0, Math.min(100, Math.round(value||0)));\n return '<div class=\"meter\"><div class=\"lab\"><span>'+esc(label)+'</span><span><b>'+v+'%</b>'\n + (sub?(' '+esc(sub)):'')+'</span></div>'\n + '<div class=\"bar-track\"><div class=\"bar-fill '+loadClass(v)+'\" style=\"width:'+v+'%\"></div></div></div>';\n }\n\n function renderHw(hw){\n var el = $('hw');\n if(!hw){ el.innerHTML = '<div class=\"meter\"><div class=\"lab\"><span>This node</span><span>no telemetry yet</span></div></div>'; return; }\n var html = '<h2 style=\"margin-top:0\">This node \u00B7 '+esc(hw.system?hw.system.hostname:'')+'</h2>';\n html += meter('CPU', hw.cpu?hw.cpu.percent:0, hw.cpu?(hw.cpu.cores+' cores'):'');\n html += meter('Memory', hw.memory?hw.memory.percent:0, hw.memory?(fmtBytes(hw.memory.usedBytes)+' / '+fmtBytes(hw.memory.totalBytes)):'');\n if(hw.gpu){ html += meter('GPU '+esc(hw.gpu.name||hw.gpu.vendor||''), hw.gpu.utilizationPercent, hw.gpu.vramTotalBytes?(fmtBytes(hw.gpu.vramUsedBytes)+' / '+fmtBytes(hw.gpu.vramTotalBytes)+' vram'):''); }\n el.innerHTML = html;\n }\n\n function selectNode(id, name){\n selected = (id===selfId)?null:id;\n $('selName').textContent = selected ? name : 'this node';\n var cards = document.querySelectorAll('.node');\n for(var i=0;i<cards.length;i++) cards[i].classList.toggle('sel', cards[i].getAttribute('data-id')===id);\n }\n\n function renderFleet(nodes){\n var el = $('fleet');\n if(!nodes || !nodes.length){ el.innerHTML = '<div class=\"empty\">No nodes reporting yet. Start peers with <span class=\"mono\">infinicode serve</span> or <span class=\"mono\">robopark setup</span>.</div>'; return; }\n nodes.sort(function(a,b){ return (b.connected?1:0)-(a.connected?1:0); });\n var html = '';\n for(var i=0;i<nodes.length;i++){\n var n = nodes[i];\n var isSelf = n.nodeId===selfId;\n var load = n.load!=null?n.load:0;\n var caps = (n.capabilities||[]).slice(0,6);\n var capHtml=''; for(var c=0;c<caps.length;c++) capHtml += '<span class=\"cap\">'+esc(caps[c])+'</span>';\n var site = n.site?('\u00B7 '+esc(n.site)):'';\n html += '<div class=\"node '+(n.connected?'up':'down')+(selected===n.nodeId?' sel':'')+'\" tabindex=\"0\" role=\"button\" data-id=\"'+esc(n.nodeId)+'\" data-name=\"'+esc(n.displayName)+'\">'\n + '<div class=\"top\"><span class=\"name\">'+esc(n.displayName)+(isSelf?' <span class=\"selfbadge\">this</span>':'')+'</span><span class=\"role\">'+esc(n.role)+'</span></div>'\n + '<div class=\"meta\">'+esc(n.platform||'')+' '+site+' <span class=\"mono\">v'+esc(n.version||'?')+'</span></div>'\n + '<div class=\"bar-track\"><div class=\"bar-fill '+loadClass(load)+'\" style=\"width:'+(n.connected?load:0)+'%\"></div></div>'\n + '<div class=\"load\"><span>'+(n.connected?'load '+Math.round(load)+'%':'offline')+'</span><span>'+((n.commands||[]).length)+' cmds</span></div>'\n + (capHtml?('<div class=\"caps\">'+capHtml+'</div>'):'')\n + '</div>';\n }\n el.innerHTML = html;\n var cards = el.querySelectorAll('.node');\n for(var j=0;j<cards.length;j++){\n (function(card){\n function pick(){ selectNode(card.getAttribute('data-id'), card.getAttribute('data-name')); }\n card.addEventListener('click', pick);\n card.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ ev.preventDefault(); pick(); } });\n })(cards[j]);\n }\n }\n\n function renderSummary(s){\n var nodes = s.nodes||[];\n var up=0; for(var i=0;i<nodes.length;i++) if(nodes[i].connected) up++;\n var down = nodes.length-up;\n var runs = (s.runs||[]).filter(function(r){return r.status==='running';}).length;\n var loads = nodes.filter(function(n){return n.connected&&n.load!=null;}).map(function(n){return n.load;});\n var avg = loads.length?Math.round(loads.reduce(function(a,b){return a+b;},0)/loads.length):0;\n $('summary').innerHTML =\n '<span class=\"chip ok\">nodes up <b>'+up+'</b></span>'\n + '<span class=\"chip'+(down?' bad':'')+'\">down <b>'+down+'</b></span>'\n + '<span class=\"chip info\">active runs <b>'+runs+'</b></span>'\n + '<span class=\"chip\">avg load <b>'+avg+'%</b></span>';\n $('fleetcount').textContent = '(' + nodes.length + ')';\n var self = s.self||{};\n var role = s.role||{};\n $('who').textContent = (self.displayName||'node') + ' \u00B7 ' + (self.role||'?') + (role.site?(' \u00B7 '+role.site):'') + ' \u00B7 v' + (self.version||'?');\n }\n\n function setLive(ok){\n $('livedot').classList.toggle('stale', !ok);\n $('offline').style.display = (!ok && everOk) ? 'block' : 'none';\n }\n\n function poll(){\n fetch('/fed/status'+QS, {cache:'no-store'}).then(function(r){\n if(!r.ok) throw new Error('status '+r.status);\n return r.json();\n }).then(function(s){\n everOk = true; setLive(true);\n if(!selfId && s.self) selfId = s.self.nodeId;\n renderSummary(s);\n renderFleet(s.nodes||[]);\n renderHw(s.hardware);\n renderFeed(s.activity||[]);\n }).catch(function(){ setLive(false); });\n }\n\n // command execution\n function showOut(text){ $('outText').textContent = text; $('out').style.display='block'; }\n $('outClose').addEventListener('click', function(){ $('out').style.display='none'; });\n\n function runCommand(input){\n input = (input||'').trim();\n if(!input) return;\n // Target a peer by prefixing the kernel's node-scoped command form.\n var payload = selected ? ('/node ' + selected + ' ' + input) : input;\n $('runBtn').disabled = true;\n fetch('/fed/command'+QS, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ input: payload }) })\n .then(function(r){ return r.json().catch(function(){ return { ok:false, text:'HTTP '+r.status }; }); })\n .then(function(res){ showOut((res && (res.text!=null?res.text:JSON.stringify(res,null,2))) || 'no output'); poll(); })\n .catch(function(e){ showOut('request failed: '+e.message); })\n .then(function(){ $('runBtn').disabled=false; });\n }\n\n $('runBtn').addEventListener('click', function(){ runCommand($('cmdInput').value); });\n $('cmdInput').addEventListener('keydown', function(ev){ if(ev.key==='Enter'){ runCommand($('cmdInput').value); } });\n $('applyBtn').addEventListener('click', function(){\n $('applyBtn').disabled = true;\n fetch('/fed/apply'+QS, { method:'POST', headers:{'content-type':'application/json'}, body:'{}' })\n .then(function(r){ return r.json().catch(function(){ return { ok:false, text:'HTTP '+r.status }; }); })\n .then(function(res){ showOut('Apply config to fleet:\\n' + ((res&&(res.text!=null?res.text:JSON.stringify(res,null,2)))||'done')); })\n .catch(function(e){ showOut('apply failed: '+e.message); })\n .then(function(){ $('applyBtn').disabled=false; });\n });\n\n poll();\n setInterval(poll, 2000);\n})();\n</script>\n</body>\n</html>";
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — RoboPark Control dashboard (served by a mesh node).
|
|
3
|
+
*
|
|
4
|
+
* A single self-contained page (no external assets — CSP-clean, ships in dist as
|
|
5
|
+
* a compiled string). It polls GET /fed/status, renders the fleet + this node's
|
|
6
|
+
* hardware + a live activity feed, and runs allow-listed actions via
|
|
7
|
+
* POST /fed/command and POST /fed/apply. The browser authenticates every request
|
|
8
|
+
* with the ?token= it was loaded with (browsers can't set Bearer on navigation).
|
|
9
|
+
*
|
|
10
|
+
* Kept free of backticks and ${...} so the whole page fits inside this TS
|
|
11
|
+
* template literal without escaping; the client JS uses string concatenation.
|
|
12
|
+
*/
|
|
13
|
+
export const DASHBOARD_HTML = `<!doctype html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18
|
+
<title>RoboPark Control</title>
|
|
19
|
+
<style>
|
|
20
|
+
:root{
|
|
21
|
+
--bg:#FFF8DE; --panel:#FBF1CE; --panel-2:#F3E4B4; --border:#E0C982; --border-strong:#B97816;
|
|
22
|
+
--ink:#231704; --muted:#6E4A12; --gold:#7A420A; --gold-bright:#B97816;
|
|
23
|
+
--ok:#2F7A32; --ok-bg:#E4F0DD; --warn:#9A6A12; --warn-bg:#F6E9C4; --bad:#B5382E; --bad-bg:#F6DED9;
|
|
24
|
+
--info:#1F6F9E; --info-bg:#DCEEF7;
|
|
25
|
+
--mono:ui-monospace,"Cascadia Code",Consolas,monospace;
|
|
26
|
+
--sans:system-ui,"Segoe UI",Roboto,sans-serif;
|
|
27
|
+
}
|
|
28
|
+
@media (prefers-color-scheme:dark){:root{
|
|
29
|
+
--bg:#0C0904; --panel:#161009; --panel-2:#1F1710; --border:#3A2A0C; --border-strong:#7A420A;
|
|
30
|
+
--ink:#F3E6C4; --muted:#A98A4E; --gold:#E6B23C; --gold-bright:#FFD86B;
|
|
31
|
+
--ok:#7FCE7A; --ok-bg:#15230F; --warn:#E0A23C; --warn-bg:#241A0A; --bad:#FF7A6E; --bad-bg:#2A100C;
|
|
32
|
+
--info:#6FB6FF; --info-bg:#0F2231;
|
|
33
|
+
}}
|
|
34
|
+
:root[data-theme="dark"]{
|
|
35
|
+
--bg:#0C0904; --panel:#161009; --panel-2:#1F1710; --border:#3A2A0C; --border-strong:#7A420A;
|
|
36
|
+
--ink:#F3E6C4; --muted:#A98A4E; --gold:#E6B23C; --gold-bright:#FFD86B;
|
|
37
|
+
--ok:#7FCE7A; --ok-bg:#15230F; --warn:#E0A23C; --warn-bg:#241A0A; --bad:#FF7A6E; --bad-bg:#2A100C;
|
|
38
|
+
--info:#6FB6FF; --info-bg:#0F2231;
|
|
39
|
+
}
|
|
40
|
+
:root[data-theme="light"]{
|
|
41
|
+
--bg:#FFF8DE; --panel:#FBF1CE; --panel-2:#F3E4B4; --border:#E0C982; --border-strong:#B97816;
|
|
42
|
+
--ink:#231704; --muted:#6E4A12; --gold:#7A420A; --gold-bright:#B97816;
|
|
43
|
+
--ok:#2F7A32; --ok-bg:#E4F0DD; --warn:#9A6A12; --warn-bg:#F6E9C4; --bad:#B5382E; --bad-bg:#F6DED9;
|
|
44
|
+
--info:#1F6F9E; --info-bg:#DCEEF7;
|
|
45
|
+
}
|
|
46
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
47
|
+
html,body{height:100%}
|
|
48
|
+
body{background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.45;-webkit-font-smoothing:antialiased;}
|
|
49
|
+
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums;}
|
|
50
|
+
|
|
51
|
+
/* top bar */
|
|
52
|
+
header.bar{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;
|
|
53
|
+
padding:0.7rem 1.1rem;border-bottom:1px solid var(--border);background:var(--panel);position:sticky;top:0;z-index:5;}
|
|
54
|
+
.brand{display:flex;align-items:center;gap:0.6rem;}
|
|
55
|
+
.livedot{width:10px;height:10px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 70%,transparent);animation:pulse 2.4s infinite;flex:none;}
|
|
56
|
+
.livedot.stale{background:var(--muted);animation:none;}
|
|
57
|
+
@keyframes pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ok) 55%,transparent)}70%{box-shadow:0 0 0 7px transparent}100%{box-shadow:0 0 0 0 transparent}}
|
|
58
|
+
@media (prefers-reduced-motion:reduce){.livedot{animation:none}}
|
|
59
|
+
.brand b{font-size:0.98rem;letter-spacing:-0.01em;}
|
|
60
|
+
.brand .who{font-family:var(--mono);font-size:0.72rem;color:var(--muted);}
|
|
61
|
+
.chips{display:flex;gap:0.45rem;flex-wrap:wrap;align-items:center;}
|
|
62
|
+
.chip{font-family:var(--mono);font-size:0.68rem;padding:0.28em 0.62em;border-radius:100px;border:1px solid var(--border);
|
|
63
|
+
background:var(--panel-2);color:var(--muted);display:inline-flex;gap:0.4em;align-items:center;white-space:nowrap;}
|
|
64
|
+
.chip b{color:var(--ink);font-weight:600;}
|
|
65
|
+
.chip.ok b{color:var(--ok)} .chip.bad b{color:var(--bad)} .chip.info b{color:var(--info)}
|
|
66
|
+
.themebtn{font-family:var(--mono);font-size:0.68rem;background:transparent;border:1px solid var(--border);color:var(--muted);
|
|
67
|
+
border-radius:100px;padding:0.34em 0.8em;cursor:pointer;}
|
|
68
|
+
.themebtn:focus-visible{outline:2px solid var(--gold-bright);outline-offset:2px;}
|
|
69
|
+
|
|
70
|
+
/* layout */
|
|
71
|
+
.wrap{display:grid;grid-template-columns:1fr 340px;gap:1rem;padding:1rem 1.1rem 6.5rem;max-width:1400px;margin:0 auto;}
|
|
72
|
+
@media (max-width:920px){.wrap{grid-template-columns:1fr}}
|
|
73
|
+
section h2,aside h2{font-family:var(--mono);font-size:0.7rem;letter-spacing:0.12em;text-transform:uppercase;color:var(--gold-bright);
|
|
74
|
+
margin:0.2rem 0 0.7rem;display:flex;align-items:center;gap:0.5rem;}
|
|
75
|
+
section h2 .count{color:var(--muted);}
|
|
76
|
+
|
|
77
|
+
/* fleet cards */
|
|
78
|
+
.fleet{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:0.75rem;}
|
|
79
|
+
.node{background:var(--panel);border:1px solid var(--border);border-left:4px solid var(--muted);border-radius:9px;
|
|
80
|
+
padding:0.75rem 0.85rem;cursor:pointer;position:relative;transition:border-color .12s,transform .12s;}
|
|
81
|
+
.node:hover{transform:translateY(-1px);}
|
|
82
|
+
.node.up{border-left-color:var(--ok);} .node.down{border-left-color:var(--muted);opacity:0.72;}
|
|
83
|
+
.node.sel{outline:2px solid var(--gold-bright);outline-offset:1px;}
|
|
84
|
+
.node:focus-visible{outline:2px solid var(--gold-bright);outline-offset:1px;}
|
|
85
|
+
.node .top{display:flex;align-items:baseline;justify-content:space-between;gap:0.5rem;margin-bottom:0.15rem;}
|
|
86
|
+
.node .name{font-family:var(--mono);font-size:0.9rem;font-weight:600;word-break:break-word;}
|
|
87
|
+
.node .role{font-family:var(--mono);font-size:0.62rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--gold);}
|
|
88
|
+
.node .meta{font-size:0.72rem;color:var(--muted);margin-bottom:0.5rem;display:flex;gap:0.5rem;flex-wrap:wrap;}
|
|
89
|
+
.node .selfbadge{font-family:var(--mono);font-size:0.58rem;color:var(--info);border:1px solid color-mix(in srgb,var(--info) 40%,transparent);
|
|
90
|
+
border-radius:4px;padding:0 0.3em;}
|
|
91
|
+
.bar-track{height:6px;border-radius:4px;background:var(--panel-2);overflow:hidden;margin-bottom:0.3rem;}
|
|
92
|
+
.bar-fill{height:100%;background:var(--ok);width:0;transition:width .4s ease;}
|
|
93
|
+
.bar-fill.warn{background:var(--warn)} .bar-fill.bad{background:var(--bad)}
|
|
94
|
+
.node .load{font-family:var(--mono);font-size:0.64rem;color:var(--muted);display:flex;justify-content:space-between;}
|
|
95
|
+
.caps{display:flex;gap:0.25rem;flex-wrap:wrap;margin-top:0.5rem;}
|
|
96
|
+
.cap{font-family:var(--mono);font-size:0.58rem;color:var(--muted);background:var(--panel-2);border-radius:4px;padding:0.05em 0.35em;}
|
|
97
|
+
.empty{color:var(--muted);font-size:0.82rem;padding:1.4rem;text-align:center;border:1px dashed var(--border);border-radius:9px;}
|
|
98
|
+
|
|
99
|
+
/* hardware meters */
|
|
100
|
+
.hw{background:var(--panel);border:1px solid var(--border);border-radius:9px;padding:0.85rem;margin-top:1rem;}
|
|
101
|
+
.meter{margin-bottom:0.65rem;}
|
|
102
|
+
.meter:last-child{margin-bottom:0;}
|
|
103
|
+
.meter .lab{display:flex;justify-content:space-between;font-family:var(--mono);font-size:0.68rem;color:var(--muted);margin-bottom:0.25rem;}
|
|
104
|
+
.meter .lab b{color:var(--ink);}
|
|
105
|
+
|
|
106
|
+
/* activity feed */
|
|
107
|
+
aside{position:sticky;top:64px;align-self:start;max-height:calc(100vh - 84px);display:flex;flex-direction:column;}
|
|
108
|
+
.feed{background:var(--panel);border:1px solid var(--border);border-radius:9px;overflow-y:auto;flex:1;min-height:240px;font-family:var(--mono);font-size:0.7rem;}
|
|
109
|
+
.line{padding:0.28rem 0.6rem;border-bottom:1px solid color-mix(in srgb,var(--border) 55%,transparent);display:flex;gap:0.5rem;line-height:1.3;}
|
|
110
|
+
.line:last-child{border-bottom:none;}
|
|
111
|
+
.line .t{color:var(--muted);flex:none;}
|
|
112
|
+
.line .n{color:var(--gold);flex:none;max-width:9ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
113
|
+
.line .d{color:var(--ink);flex:1;word-break:break-word;}
|
|
114
|
+
.line.k-ev .d{color:var(--info);} .line.k-hw .n{color:var(--muted);} .line.k-rt .d{color:var(--gold-bright);}
|
|
115
|
+
|
|
116
|
+
/* command bar */
|
|
117
|
+
footer.cmd{position:fixed;left:0;right:0;bottom:0;background:var(--panel);border-top:1px solid var(--border-strong);
|
|
118
|
+
padding:0.6rem 1.1rem;display:flex;gap:0.55rem;align-items:center;flex-wrap:wrap;z-index:6;}
|
|
119
|
+
footer .sel{font-family:var(--mono);font-size:0.72rem;color:var(--muted);white-space:nowrap;}
|
|
120
|
+
footer .sel b{color:var(--ink);}
|
|
121
|
+
footer input{flex:1;min-width:180px;font-family:var(--mono);font-size:0.8rem;background:var(--bg);color:var(--ink);
|
|
122
|
+
border:1px solid var(--border);border-radius:7px;padding:0.5em 0.7em;}
|
|
123
|
+
footer input:focus-visible{outline:2px solid var(--gold-bright);outline-offset:1px;}
|
|
124
|
+
button.act{font-family:var(--sans);font-size:0.76rem;font-weight:600;cursor:pointer;border-radius:7px;padding:0.5em 0.85em;
|
|
125
|
+
border:1px solid var(--border-strong);background:var(--gold-bright);color:#231704;white-space:nowrap;}
|
|
126
|
+
button.act.ghost{background:transparent;color:var(--gold);}
|
|
127
|
+
button.act:focus-visible{outline:2px solid var(--ink);outline-offset:1px;}
|
|
128
|
+
button.act:disabled{opacity:0.5;cursor:default;}
|
|
129
|
+
#out{position:fixed;right:1.1rem;bottom:3.6rem;max-width:min(560px,90vw);max-height:44vh;overflow:auto;background:var(--panel-2);
|
|
130
|
+
border:1px solid var(--border-strong);border-radius:9px;padding:0.7rem 0.85rem;font-family:var(--mono);font-size:0.72rem;
|
|
131
|
+
white-space:pre-wrap;display:none;box-shadow:0 8px 30px rgba(0,0,0,0.25);z-index:7;}
|
|
132
|
+
#out .x{float:right;cursor:pointer;color:var(--muted);border:none;background:none;font-family:var(--mono);}
|
|
133
|
+
.offline{background:var(--bad-bg);color:var(--bad);text-align:center;font-family:var(--mono);font-size:0.72rem;padding:0.4rem;display:none;}
|
|
134
|
+
</style>
|
|
135
|
+
</head>
|
|
136
|
+
<body>
|
|
137
|
+
<div class="offline" id="offline">connection lost — retrying</div>
|
|
138
|
+
<header class="bar">
|
|
139
|
+
<div class="brand">
|
|
140
|
+
<span class="livedot" id="livedot"></span>
|
|
141
|
+
<div>
|
|
142
|
+
<b>RoboPark Control</b>
|
|
143
|
+
<div class="who mono" id="who">connecting…</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="chips" id="summary"></div>
|
|
147
|
+
<button class="themebtn" id="theme">Theme</button>
|
|
148
|
+
</header>
|
|
149
|
+
|
|
150
|
+
<div class="wrap">
|
|
151
|
+
<section>
|
|
152
|
+
<h2>Fleet <span class="count" id="fleetcount"></span></h2>
|
|
153
|
+
<div class="fleet" id="fleet"></div>
|
|
154
|
+
<div class="hw" id="hw"></div>
|
|
155
|
+
</section>
|
|
156
|
+
<aside>
|
|
157
|
+
<h2>Live activity</h2>
|
|
158
|
+
<div class="feed" id="feed"></div>
|
|
159
|
+
</aside>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<footer class="cmd">
|
|
163
|
+
<span class="sel">on <b id="selName">this node</b></span>
|
|
164
|
+
<input id="cmdInput" placeholder="/status /nodes /health (Enter to run)" autocomplete="off" spellcheck="false">
|
|
165
|
+
<button class="act" id="runBtn">Run</button>
|
|
166
|
+
<button class="act ghost" id="applyBtn">Apply config to fleet</button>
|
|
167
|
+
</footer>
|
|
168
|
+
<div id="out"><button class="x" id="outClose">close</button><span id="outText"></span></div>
|
|
169
|
+
|
|
170
|
+
<script>
|
|
171
|
+
(function(){
|
|
172
|
+
var TOKEN = new URLSearchParams(location.search).get('token') || '';
|
|
173
|
+
var QS = TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '';
|
|
174
|
+
var selfId = null, selected = null, lastSeq = -1, everOk = false;
|
|
175
|
+
|
|
176
|
+
var $ = function(id){ return document.getElementById(id); };
|
|
177
|
+
function esc(s){ var d=document.createElement('div'); d.textContent = (s==null?'':String(s)); return d.innerHTML; }
|
|
178
|
+
function fmtBytes(b){ if(!b) return '0'; var u=['B','KB','MB','GB','TB']; var i=Math.floor(Math.log(b)/Math.log(1024)); return (b/Math.pow(1024,i)).toFixed(i?1:0)+u[i]; }
|
|
179
|
+
function loadClass(v){ return v>=80?'bad':(v>=50?'warn':''); }
|
|
180
|
+
function clock(ts){ var d=new Date(ts); function p(n){return (n<10?'0':'')+n;} return p(d.getHours())+':'+p(d.getMinutes())+':'+p(d.getSeconds()); }
|
|
181
|
+
|
|
182
|
+
// theme
|
|
183
|
+
(function(){ var k='rp-ctl-theme', s=null; try{s=localStorage.getItem(k)}catch(e){}
|
|
184
|
+
if(s) document.documentElement.setAttribute('data-theme', s);
|
|
185
|
+
$('theme').addEventListener('click', function(){
|
|
186
|
+
var m=window.matchMedia('(prefers-color-scheme: dark)');
|
|
187
|
+
var cur=document.documentElement.getAttribute('data-theme')||(m.matches?'dark':'light');
|
|
188
|
+
var nx=cur==='dark'?'light':'dark';
|
|
189
|
+
document.documentElement.setAttribute('data-theme', nx);
|
|
190
|
+
try{localStorage.setItem(k,nx)}catch(e){}
|
|
191
|
+
});
|
|
192
|
+
})();
|
|
193
|
+
|
|
194
|
+
// Pull a short human detail out of an event's data payload.
|
|
195
|
+
function detailOf(data){
|
|
196
|
+
var p = data || {};
|
|
197
|
+
if(typeof p==='string') return p;
|
|
198
|
+
if(p.providerId!=null) return p.providerId + (('healthy' in p)?(p.healthy?' ✓':' ✗'):'');
|
|
199
|
+
if(p.model) return String(p.model);
|
|
200
|
+
if(p.provider) return String(p.provider);
|
|
201
|
+
if(p.reason) return String(p.reason).slice(0,60);
|
|
202
|
+
if(p.description) return String(p.description).slice(0,50);
|
|
203
|
+
if(p.name) return String(p.name);
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
function niceKind(k){ return String(k||'').toLowerCase().replace(/_/g,' '); }
|
|
207
|
+
|
|
208
|
+
function summarizeFrame(f){
|
|
209
|
+
var d = f.d || {};
|
|
210
|
+
if(f.t==='hw'){ var c=d.cpu?Math.round(d.cpu.percent):0; var m=d.memory?Math.round(d.memory.percent):0; return 'cpu '+c+'% · mem '+m+'%'; }
|
|
211
|
+
if(f.t==='hb'){ return 'heartbeat · load '+(d.load!=null?d.load:'-'); }
|
|
212
|
+
if(f.t==='nd'){ return 'fleet update ('+(Array.isArray(d)?d.length:0)+' nodes)'; }
|
|
213
|
+
// routing decision: compact payload is {k, m, task, w, d}
|
|
214
|
+
if(f.t==='rt'){ var rp=d.d||{}; return 'route → '+(rp.model||rp.provider||rp.decision||detailOf(rp)||'?'); }
|
|
215
|
+
// immediate lifecycle event: {k:type, m, task, w, d:payload}
|
|
216
|
+
if(f.t==='ev'){ var det=detailOf(d.d); return niceKind(d.k)+(det?(' '+det):''); }
|
|
217
|
+
// coalesced chatter: {count, tail:[{k, d}]}
|
|
218
|
+
if(f.t==='log'){
|
|
219
|
+
if(d.tail && d.tail.length){ var last=d.tail[d.tail.length-1]; var ld=detailOf(last.d);
|
|
220
|
+
return niceKind(last.k)+(ld?(' '+ld):'')+(d.count>1?(' ×'+d.count):''); }
|
|
221
|
+
return 'log';
|
|
222
|
+
}
|
|
223
|
+
return f.t;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderFeed(activity){
|
|
227
|
+
var feed = $('feed');
|
|
228
|
+
if(!activity || !activity.length){
|
|
229
|
+
if(!feed.dataset.seeded){ feed.innerHTML = '<div class="line"><span class="d" style="color:var(--muted)">waiting for fleet activity — task runs, routing, and peer events stream here.</span></div>'; }
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
feed.dataset.seeded = '1';
|
|
233
|
+
var near = feed.scrollHeight - feed.scrollTop - feed.clientHeight < 40;
|
|
234
|
+
var html = '';
|
|
235
|
+
for(var i=0;i<activity.length;i++){
|
|
236
|
+
var e = activity[i]; var f = e.f || e; if(f.s!=null && f.s<=lastSeq && f.t!=='hw') { /* keep */ }
|
|
237
|
+
var name = (f.n||'').slice(0,10);
|
|
238
|
+
html += '<div class="line k-'+esc(f.t)+'"><span class="t">'+clock(f.ts)+'</span>'
|
|
239
|
+
+ '<span class="n" title="'+esc(f.n)+'">'+esc(name)+'</span>'
|
|
240
|
+
+ '<span class="d">'+esc(summarizeFrame(f))+'</span></div>';
|
|
241
|
+
}
|
|
242
|
+
feed.innerHTML = html;
|
|
243
|
+
if(near) feed.scrollTop = feed.scrollHeight;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function meter(label, value, sub){
|
|
247
|
+
var v = Math.max(0, Math.min(100, Math.round(value||0)));
|
|
248
|
+
return '<div class="meter"><div class="lab"><span>'+esc(label)+'</span><span><b>'+v+'%</b>'
|
|
249
|
+
+ (sub?(' '+esc(sub)):'')+'</span></div>'
|
|
250
|
+
+ '<div class="bar-track"><div class="bar-fill '+loadClass(v)+'" style="width:'+v+'%"></div></div></div>';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderHw(hw){
|
|
254
|
+
var el = $('hw');
|
|
255
|
+
if(!hw){ el.innerHTML = '<div class="meter"><div class="lab"><span>This node</span><span>no telemetry yet</span></div></div>'; return; }
|
|
256
|
+
var html = '<h2 style="margin-top:0">This node · '+esc(hw.system?hw.system.hostname:'')+'</h2>';
|
|
257
|
+
html += meter('CPU', hw.cpu?hw.cpu.percent:0, hw.cpu?(hw.cpu.cores+' cores'):'');
|
|
258
|
+
html += meter('Memory', hw.memory?hw.memory.percent:0, hw.memory?(fmtBytes(hw.memory.usedBytes)+' / '+fmtBytes(hw.memory.totalBytes)):'');
|
|
259
|
+
if(hw.gpu){ html += meter('GPU '+esc(hw.gpu.name||hw.gpu.vendor||''), hw.gpu.utilizationPercent, hw.gpu.vramTotalBytes?(fmtBytes(hw.gpu.vramUsedBytes)+' / '+fmtBytes(hw.gpu.vramTotalBytes)+' vram'):''); }
|
|
260
|
+
el.innerHTML = html;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function selectNode(id, name){
|
|
264
|
+
selected = (id===selfId)?null:id;
|
|
265
|
+
$('selName').textContent = selected ? name : 'this node';
|
|
266
|
+
var cards = document.querySelectorAll('.node');
|
|
267
|
+
for(var i=0;i<cards.length;i++) cards[i].classList.toggle('sel', cards[i].getAttribute('data-id')===id);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function renderFleet(nodes){
|
|
271
|
+
var el = $('fleet');
|
|
272
|
+
if(!nodes || !nodes.length){ el.innerHTML = '<div class="empty">No nodes reporting yet. Start peers with <span class="mono">infinicode serve</span> or <span class="mono">robopark setup</span>.</div>'; return; }
|
|
273
|
+
nodes.sort(function(a,b){ return (b.connected?1:0)-(a.connected?1:0); });
|
|
274
|
+
var html = '';
|
|
275
|
+
for(var i=0;i<nodes.length;i++){
|
|
276
|
+
var n = nodes[i];
|
|
277
|
+
var isSelf = n.nodeId===selfId;
|
|
278
|
+
var load = n.load!=null?n.load:0;
|
|
279
|
+
var caps = (n.capabilities||[]).slice(0,6);
|
|
280
|
+
var capHtml=''; for(var c=0;c<caps.length;c++) capHtml += '<span class="cap">'+esc(caps[c])+'</span>';
|
|
281
|
+
var site = n.site?('· '+esc(n.site)):'';
|
|
282
|
+
html += '<div class="node '+(n.connected?'up':'down')+(selected===n.nodeId?' sel':'')+'" tabindex="0" role="button" data-id="'+esc(n.nodeId)+'" data-name="'+esc(n.displayName)+'">'
|
|
283
|
+
+ '<div class="top"><span class="name">'+esc(n.displayName)+(isSelf?' <span class="selfbadge">this</span>':'')+'</span><span class="role">'+esc(n.role)+'</span></div>'
|
|
284
|
+
+ '<div class="meta">'+esc(n.platform||'')+' '+site+' <span class="mono">v'+esc(n.version||'?')+'</span></div>'
|
|
285
|
+
+ '<div class="bar-track"><div class="bar-fill '+loadClass(load)+'" style="width:'+(n.connected?load:0)+'%"></div></div>'
|
|
286
|
+
+ '<div class="load"><span>'+(n.connected?'load '+Math.round(load)+'%':'offline')+'</span><span>'+((n.commands||[]).length)+' cmds</span></div>'
|
|
287
|
+
+ (capHtml?('<div class="caps">'+capHtml+'</div>'):'')
|
|
288
|
+
+ '</div>';
|
|
289
|
+
}
|
|
290
|
+
el.innerHTML = html;
|
|
291
|
+
var cards = el.querySelectorAll('.node');
|
|
292
|
+
for(var j=0;j<cards.length;j++){
|
|
293
|
+
(function(card){
|
|
294
|
+
function pick(){ selectNode(card.getAttribute('data-id'), card.getAttribute('data-name')); }
|
|
295
|
+
card.addEventListener('click', pick);
|
|
296
|
+
card.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ ev.preventDefault(); pick(); } });
|
|
297
|
+
})(cards[j]);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function renderSummary(s){
|
|
302
|
+
var nodes = s.nodes||[];
|
|
303
|
+
var up=0; for(var i=0;i<nodes.length;i++) if(nodes[i].connected) up++;
|
|
304
|
+
var down = nodes.length-up;
|
|
305
|
+
var runs = (s.runs||[]).filter(function(r){return r.status==='running';}).length;
|
|
306
|
+
var loads = nodes.filter(function(n){return n.connected&&n.load!=null;}).map(function(n){return n.load;});
|
|
307
|
+
var avg = loads.length?Math.round(loads.reduce(function(a,b){return a+b;},0)/loads.length):0;
|
|
308
|
+
$('summary').innerHTML =
|
|
309
|
+
'<span class="chip ok">nodes up <b>'+up+'</b></span>'
|
|
310
|
+
+ '<span class="chip'+(down?' bad':'')+'">down <b>'+down+'</b></span>'
|
|
311
|
+
+ '<span class="chip info">active runs <b>'+runs+'</b></span>'
|
|
312
|
+
+ '<span class="chip">avg load <b>'+avg+'%</b></span>';
|
|
313
|
+
$('fleetcount').textContent = '(' + nodes.length + ')';
|
|
314
|
+
var self = s.self||{};
|
|
315
|
+
var role = s.role||{};
|
|
316
|
+
$('who').textContent = (self.displayName||'node') + ' · ' + (self.role||'?') + (role.site?(' · '+role.site):'') + ' · v' + (self.version||'?');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function setLive(ok){
|
|
320
|
+
$('livedot').classList.toggle('stale', !ok);
|
|
321
|
+
$('offline').style.display = (!ok && everOk) ? 'block' : 'none';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function poll(){
|
|
325
|
+
fetch('/fed/status'+QS, {cache:'no-store'}).then(function(r){
|
|
326
|
+
if(!r.ok) throw new Error('status '+r.status);
|
|
327
|
+
return r.json();
|
|
328
|
+
}).then(function(s){
|
|
329
|
+
everOk = true; setLive(true);
|
|
330
|
+
if(!selfId && s.self) selfId = s.self.nodeId;
|
|
331
|
+
renderSummary(s);
|
|
332
|
+
renderFleet(s.nodes||[]);
|
|
333
|
+
renderHw(s.hardware);
|
|
334
|
+
renderFeed(s.activity||[]);
|
|
335
|
+
}).catch(function(){ setLive(false); });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// command execution
|
|
339
|
+
function showOut(text){ $('outText').textContent = text; $('out').style.display='block'; }
|
|
340
|
+
$('outClose').addEventListener('click', function(){ $('out').style.display='none'; });
|
|
341
|
+
|
|
342
|
+
function runCommand(input){
|
|
343
|
+
input = (input||'').trim();
|
|
344
|
+
if(!input) return;
|
|
345
|
+
// Target a peer by prefixing the kernel's node-scoped command form.
|
|
346
|
+
var payload = selected ? ('/node ' + selected + ' ' + input) : input;
|
|
347
|
+
$('runBtn').disabled = true;
|
|
348
|
+
fetch('/fed/command'+QS, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ input: payload }) })
|
|
349
|
+
.then(function(r){ return r.json().catch(function(){ return { ok:false, text:'HTTP '+r.status }; }); })
|
|
350
|
+
.then(function(res){ showOut((res && (res.text!=null?res.text:JSON.stringify(res,null,2))) || 'no output'); poll(); })
|
|
351
|
+
.catch(function(e){ showOut('request failed: '+e.message); })
|
|
352
|
+
.then(function(){ $('runBtn').disabled=false; });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
$('runBtn').addEventListener('click', function(){ runCommand($('cmdInput').value); });
|
|
356
|
+
$('cmdInput').addEventListener('keydown', function(ev){ if(ev.key==='Enter'){ runCommand($('cmdInput').value); } });
|
|
357
|
+
$('applyBtn').addEventListener('click', function(){
|
|
358
|
+
$('applyBtn').disabled = true;
|
|
359
|
+
fetch('/fed/apply'+QS, { method:'POST', headers:{'content-type':'application/json'}, body:'{}' })
|
|
360
|
+
.then(function(r){ return r.json().catch(function(){ return { ok:false, text:'HTTP '+r.status }; }); })
|
|
361
|
+
.then(function(res){ showOut('Apply config to fleet:\\n' + ((res&&(res.text!=null?res.text:JSON.stringify(res,null,2)))||'done')); })
|
|
362
|
+
.catch(function(e){ showOut('apply failed: '+e.message); })
|
|
363
|
+
.then(function(){ $('applyBtn').disabled=false; });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
poll();
|
|
367
|
+
setInterval(poll, 2000);
|
|
368
|
+
})();
|
|
369
|
+
</script>
|
|
370
|
+
</body>
|
|
371
|
+
</html>`;
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* (heartbeat / task.dispatch / command / config / update).
|
|
12
12
|
*/
|
|
13
13
|
import type { Capability, KernelLike, Logger } from '../types.js';
|
|
14
|
-
import type { HardwareSnapshot, NodeIdentity, NodeStatus, PeerInfo, SharedConfig, StreamFrame } from './types.js';
|
|
14
|
+
import type { HardwareSnapshot, NodeIdentity, NodeManifest, NodeStatus, PeerInfo, SharedConfig, StreamFrame } from './types.js';
|
|
15
15
|
import { ConfigSync } from './config-sync.js';
|
|
16
16
|
import { AutoUpdate } from './auto-update.js';
|
|
17
17
|
import { ComputeRouter, type ComputeNeed } from './compute-router.js';
|
|
18
18
|
import { type ManifestInputs } from './node-identity.js';
|
|
19
|
-
import { type RoleContext, type RunRecord } from './types.js';
|
|
19
|
+
import { type AgentHandle, type RoleContext, type RunRecord } from './types.js';
|
|
20
|
+
import { AgentBackendRegistry, type BackendId } from '../agents/index.js';
|
|
21
|
+
import { loadCoderConfig, type CoderConfig } from '../agents/backends/profiles.js';
|
|
20
22
|
export interface DispatchTask {
|
|
21
23
|
description: string;
|
|
22
24
|
capabilities: Capability[];
|
|
@@ -26,6 +28,21 @@ export interface DispatchTask {
|
|
|
26
28
|
/** When true, the receiving node auto-plans the goal into phases (a phased
|
|
27
29
|
* build) instead of running it as one atomic task. */
|
|
28
30
|
autoPlan?: boolean;
|
|
31
|
+
/** Which executor should run this on the receiving node: 'kernel' (native,
|
|
32
|
+
* default) or an installed CLI backend ('claude' | 'codex' | 'opencode'). */
|
|
33
|
+
agent?: BackendId;
|
|
34
|
+
/** Resume a specific CLI session on the receiving node instead of a fresh run. */
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
/** Model hint passed to a CLI backend (provider/model or a CLI-native id). */
|
|
37
|
+
model?: string;
|
|
38
|
+
/** Named coder profile to run (e.g. "deep", "cheap"). */
|
|
39
|
+
profile?: string;
|
|
40
|
+
/** Rotation strategy: 'auto' | 'tasktype' | 'policy' | 'roundrobin' | 'fallback'. */
|
|
41
|
+
strategy?: string;
|
|
42
|
+
/** Policy tier: 'free-first' | 'cheapest' | 'fastest' | 'highest-quality' | 'balanced'. */
|
|
43
|
+
policy?: string;
|
|
44
|
+
/** Task-type key for routing (e.g. 'refactor', 'docs', 'bulk'). */
|
|
45
|
+
taskType?: string;
|
|
29
46
|
}
|
|
30
47
|
export interface FederationOptions {
|
|
31
48
|
version: string;
|
|
@@ -43,6 +60,34 @@ export interface FederationOptions {
|
|
|
43
60
|
autoPullConfig?: boolean;
|
|
44
61
|
/** Satellite: how often to re-pull shared config (ms, default 30000; 0 disables). */
|
|
45
62
|
configPullMs?: number;
|
|
63
|
+
/** Coder profiles + rotation config (from node config `coder`). Merged over
|
|
64
|
+
* built-in defaults, so the mesh rotates across backends out of the box. */
|
|
65
|
+
coder?: Parameters<typeof loadCoderConfig>[0];
|
|
66
|
+
/** Serve the web control dashboard at GET / and /ui (off unless enabled). */
|
|
67
|
+
dashboard?: boolean;
|
|
68
|
+
/** Publish this node's shared config to the fleet (backs POST /fed/apply +
|
|
69
|
+
* the dashboard's "Apply config to fleet" button). */
|
|
70
|
+
onApply?: () => Promise<unknown> | unknown;
|
|
71
|
+
/** Gate remote/dashboard slash-commands. Returns {ok:false,reason} to reject —
|
|
72
|
+
* used to keep an unauthenticated, network-exposed node read-only. */
|
|
73
|
+
commandGate?: (input: string) => {
|
|
74
|
+
ok: boolean;
|
|
75
|
+
reason?: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Consolidated fleet view served at GET /fed/status. */
|
|
79
|
+
export interface FleetStatus {
|
|
80
|
+
self: NodeManifest;
|
|
81
|
+
role: RoleContext | null;
|
|
82
|
+
nodes: NodeStatus[];
|
|
83
|
+
hardware?: HardwareSnapshot;
|
|
84
|
+
runs: RunRecord[];
|
|
85
|
+
agents: AgentHandle[];
|
|
86
|
+
activity: Array<{
|
|
87
|
+
peerId: string;
|
|
88
|
+
f: StreamFrame;
|
|
89
|
+
}>;
|
|
90
|
+
ts: number;
|
|
46
91
|
}
|
|
47
92
|
export interface FederationDeps {
|
|
48
93
|
kernel: KernelLike;
|
|
@@ -50,6 +95,9 @@ export interface FederationDeps {
|
|
|
50
95
|
describe: () => ManifestInputs;
|
|
51
96
|
logger: Logger;
|
|
52
97
|
options: FederationOptions;
|
|
98
|
+
/** Executors this node can run dispatched tasks on. Defaults to the native
|
|
99
|
+
* kernel plus any installed coding CLIs (Claude Code / Codex / OpenCode). */
|
|
100
|
+
backends?: AgentBackendRegistry;
|
|
53
101
|
}
|
|
54
102
|
export declare class Federation {
|
|
55
103
|
private deps;
|
|
@@ -70,6 +118,12 @@ export declare class Federation {
|
|
|
70
118
|
readonly configSync: ConfigSync;
|
|
71
119
|
readonly autoUpdate: AutoUpdate;
|
|
72
120
|
readonly compute: ComputeRouter;
|
|
121
|
+
/** Executors available on this node (native kernel + installed CLIs). */
|
|
122
|
+
readonly backends: AgentBackendRegistry;
|
|
123
|
+
/** Resolved coder profiles + rotation policy for this node. */
|
|
124
|
+
readonly coderConfig: CoderConfig;
|
|
125
|
+
/** Per-node round-robin cursor for backend rotation. */
|
|
126
|
+
private readonly rotationState;
|
|
73
127
|
/** Local subscribers to the merged mesh frame stream (for MCP stream_activity). */
|
|
74
128
|
private frameSubs;
|
|
75
129
|
/** Recent frames for catch-up. */
|
|
@@ -90,6 +144,27 @@ export declare class Federation {
|
|
|
90
144
|
private runTask;
|
|
91
145
|
/** Run a dispatched task in the background, updating its RunRecord + streaming. */
|
|
92
146
|
private runTaskAsync;
|
|
147
|
+
/**
|
|
148
|
+
* Execute a dispatched task with a third-party CLI backend (Claude Code /
|
|
149
|
+
* Codex / OpenCode) instead of the native kernel. The backend's activity
|
|
150
|
+
* streams up the same mesh SSE feed (log/ev frames), and its native session id
|
|
151
|
+
* is captured on the RunRecord so the run can be followed and later messaged.
|
|
152
|
+
*/
|
|
153
|
+
private runViaBackend;
|
|
154
|
+
/** Stream one backend's frames up the mesh, tagged with the backend id. */
|
|
155
|
+
private backendFrameSink;
|
|
156
|
+
/** Run a single backend attempt. Never throws — a missing/unavailable backend
|
|
157
|
+
* returns a failed result so a rotation chain can fall through to the next. */
|
|
158
|
+
private runBackendOnce;
|
|
159
|
+
/** Write a backend result onto the run record. */
|
|
160
|
+
private writeResult;
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a rotation chain for the task and run it with fallback: try each
|
|
163
|
+
* profile's backend in order, streaming its activity; the first `completed`
|
|
164
|
+
* wins, otherwise fall through to the next. Composes task-type routing, policy
|
|
165
|
+
* tiers, round-robin and fallback (see agents/backends/rotation.ts).
|
|
166
|
+
*/
|
|
167
|
+
private runViaBackendChain;
|
|
93
168
|
/**
|
|
94
169
|
* Hand a task to the best-fit connected peer (compute-routed) or a named one.
|
|
95
170
|
* Non-blocking by default — returns a runId to follow. Pass wait to block
|
|
@@ -122,6 +197,30 @@ export declare class Federation {
|
|
|
122
197
|
awaitRun(runId: string, timeoutMs?: number): Promise<RunRecord>;
|
|
123
198
|
/** Run a slash-command on a specific peer. */
|
|
124
199
|
runCommandOn(nodeId: string, input: string): Promise<unknown>;
|
|
200
|
+
/**
|
|
201
|
+
* CLI agent sessions on THIS node — any run that used a CLI backend and
|
|
202
|
+
* captured a session id. Live while running, dormant (still resumable) once
|
|
203
|
+
* complete. Deduped by session, newest kept, so a session that's been messaged
|
|
204
|
+
* several times shows once.
|
|
205
|
+
*/
|
|
206
|
+
localAgents(): AgentHandle[];
|
|
207
|
+
/** All messageable CLI agent sessions across the mesh (local + peers). */
|
|
208
|
+
agents(): Promise<AgentHandle[]>;
|
|
209
|
+
/** Resolve an address (`nodeId/sessionId`, a sessionId, or a runId) to a handle. */
|
|
210
|
+
private resolveAgent;
|
|
211
|
+
/**
|
|
212
|
+
* Message a CLI agent session — wherever it lives on the mesh. Resumes that
|
|
213
|
+
* exact session with the text (so the agent keeps its context) and returns a
|
|
214
|
+
* runId to follow the reply. This is how one CLI agent talks to another.
|
|
215
|
+
*/
|
|
216
|
+
sendTo(address: string, text: string, opts?: {
|
|
217
|
+
wait?: boolean;
|
|
218
|
+
timeoutMs?: number;
|
|
219
|
+
}): Promise<{
|
|
220
|
+
runId: string;
|
|
221
|
+
nodeId: string;
|
|
222
|
+
record?: RunRecord;
|
|
223
|
+
} | null>;
|
|
125
224
|
/** Hub: publish shared config (providers/API keys/policy) to the fleet. */
|
|
126
225
|
publishConfig(cfg: Omit<SharedConfig, 'revision' | 'updatedAt'>): SharedConfig;
|
|
127
226
|
/** Satellite: pull shared config from a peer (usually the hub). */
|
|
@@ -147,9 +246,20 @@ export declare class Federation {
|
|
|
147
246
|
* new nodes that come online later are picked up too. Idempotent.
|
|
148
247
|
*/
|
|
149
248
|
startLanDiscovery(tag?: string, discoveryPort?: number): void;
|
|
249
|
+
/** Backend ids this node can execute tasks with ('kernel' + installed CLIs). */
|
|
250
|
+
availableBackends(): BackendId[];
|
|
251
|
+
/**
|
|
252
|
+
* The manifest inputs augmented with `backend:<id>` capability tags for each
|
|
253
|
+
* installed executor. This lets the compute-router match a task that asks for
|
|
254
|
+
* a specific agent (e.g. capabilities:['backend:claude']) to a node that
|
|
255
|
+
* actually has that CLI — without changing the caller's describe().
|
|
256
|
+
*/
|
|
257
|
+
private describeWithBackends;
|
|
150
258
|
/** Neutral node-status list (self + peers) — feeds the mesh map. */
|
|
151
259
|
nodeStatuses(): NodeStatus[];
|
|
152
260
|
peers(): PeerInfo[];
|
|
261
|
+
/** Consolidated fleet view for the dashboard + `robopark fleet` (GET /fed/status). */
|
|
262
|
+
statusSnapshot(): FleetStatus;
|
|
153
263
|
/** Latest local hardware snapshot (for the MOLTFED connector / panels). */
|
|
154
264
|
latestHardware(): HardwareSnapshot | undefined;
|
|
155
265
|
/** Subscribe to the merged mesh frame stream (local + peer frames). */
|