infinicode 2.3.6 → 2.5.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/bin/robopark.js +2 -0
- package/dist/cli.js +26 -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/robopark.d.ts +6 -0
- package/dist/commands/robopark.js +450 -0
- package/dist/commands/serve.d.ts +5 -0
- package/dist/commands/serve.js +170 -11
- package/dist/kernel/agents/backends/cli-backend.d.ts +4 -0
- package/dist/kernel/agents/backends/cli-backend.js +13 -5
- package/dist/kernel/agents/backends/detect.d.ts +12 -0
- package/dist/kernel/agents/backends/detect.js +102 -2
- 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 +2 -1
- package/dist/kernel/agents/backends/registry.js +39 -5
- 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 +3 -1
- package/dist/kernel/federation/dashboard-html.d.ts +13 -0
- package/dist/kernel/federation/dashboard-html.js +561 -0
- package/dist/kernel/federation/federation.d.ts +75 -1
- package/dist/kernel/federation/federation.js +119 -23
- 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 +34 -1
- package/dist/kernel/federation/transport-http.js +117 -4
- package/dist/kernel/federation/types.d.ts +20 -0
- package/dist/kernel/mcp/mcp-server.js +23 -7
- package/dist/robopark-cli.d.ts +2 -0
- package/dist/robopark-cli.js +23 -0
- package/package.json +7 -3
|
@@ -0,0 +1,561 @@
|
|
|
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
|
+
|
|
135
|
+
/* tabs + product panes */
|
|
136
|
+
.tabs{display:flex;gap:0.2rem;padding:0 1.1rem;border-bottom:1px solid var(--border);background:var(--panel);position:sticky;top:53px;z-index:4;}
|
|
137
|
+
.tab{font-family:var(--mono);font-size:0.74rem;padding:0.6rem 0.95rem;cursor:pointer;color:var(--muted);background:none;border:none;border-bottom:2px solid transparent;}
|
|
138
|
+
.tab.active{color:var(--gold-bright);border-bottom-color:var(--gold-bright);}
|
|
139
|
+
.tab:focus-visible{outline:2px solid var(--gold-bright);outline-offset:-2px;}
|
|
140
|
+
.pane{display:none;} .pane.active{display:block;}
|
|
141
|
+
.rp-head{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:0.6rem;padding:1rem 1.1rem 0.2rem;}
|
|
142
|
+
.rp-totals{display:flex;gap:0.5rem;flex-wrap:wrap;}
|
|
143
|
+
.rp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:0.75rem;padding:0.8rem 1.1rem;}
|
|
144
|
+
.rp-card{background:var(--panel);border:1px solid var(--border);border-left:4px solid var(--gold-bright);border-radius:9px;padding:0.75rem 0.85rem;}
|
|
145
|
+
.rp-card h4{font-family:var(--mono);font-size:0.86rem;margin-bottom:0.5rem;word-break:break-word;}
|
|
146
|
+
.rp-metrics{display:grid;grid-template-columns:1fr 1fr;gap:0.4rem 0.6rem;}
|
|
147
|
+
.rp-m{display:flex;flex-direction:column;}
|
|
148
|
+
.rp-m .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:1.1rem;color:var(--ink);}
|
|
149
|
+
.rp-m .l{font-size:0.62rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--muted);}
|
|
150
|
+
.rp-m .v.warn{color:var(--warn);} .rp-m .v.bad{color:var(--bad);} .rp-m .v.ok{color:var(--ok);}
|
|
151
|
+
.rp-controls{display:flex;align-items:center;gap:0.6rem;flex-wrap:wrap;}
|
|
152
|
+
.pm{display:flex;align-items:center;gap:0.5rem;font-family:var(--mono);font-size:0.72rem;color:var(--muted);}
|
|
153
|
+
.pm .state{font-weight:700;letter-spacing:0.03em;}
|
|
154
|
+
.pm .state.on{color:var(--ok);} .pm .state.off{color:var(--muted);}
|
|
155
|
+
.toggle{position:relative;width:42px;height:22px;border-radius:22px;border:1px solid var(--border-strong);background:var(--panel-2);cursor:pointer;padding:0;transition:background .15s;}
|
|
156
|
+
.toggle::after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--muted);transition:transform .15s,background .15s;}
|
|
157
|
+
.toggle[aria-checked="true"]{background:var(--ok-bg);border-color:var(--ok);}
|
|
158
|
+
.toggle[aria-checked="true"]::after{transform:translateX(20px);background:var(--ok);}
|
|
159
|
+
.toggle:disabled{opacity:0.45;cursor:default;}
|
|
160
|
+
.toggle:focus-visible{outline:2px solid var(--gold-bright);outline-offset:2px;}
|
|
161
|
+
.rp-card .rp-actions{display:flex;gap:0.4rem;margin-top:0.6rem;}
|
|
162
|
+
.rp-btn{font-family:var(--mono);font-size:0.66rem;padding:0.3rem 0.55rem;border-radius:6px;border:1px solid var(--border-strong);background:var(--panel-2);color:var(--ink);cursor:pointer;}
|
|
163
|
+
.rp-btn:hover{border-color:var(--gold-bright);color:var(--gold-bright);}
|
|
164
|
+
.rp-btn.danger:hover{border-color:var(--bad);color:var(--bad);}
|
|
165
|
+
.rp-btn:disabled{opacity:0.4;cursor:default;}
|
|
166
|
+
.rp-btn:focus-visible{outline:2px solid var(--gold-bright);outline-offset:1px;}
|
|
167
|
+
.rp-ro{font-family:var(--mono);font-size:0.66rem;color:var(--warn);}
|
|
168
|
+
</style>
|
|
169
|
+
</head>
|
|
170
|
+
<body>
|
|
171
|
+
<div class="offline" id="offline">connection lost — retrying</div>
|
|
172
|
+
<header class="bar">
|
|
173
|
+
<div class="brand">
|
|
174
|
+
<span class="livedot" id="livedot"></span>
|
|
175
|
+
<div>
|
|
176
|
+
<b>RoboPark Control</b>
|
|
177
|
+
<div class="who mono" id="who">connecting…</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="chips" id="summary"></div>
|
|
181
|
+
<button class="themebtn" id="theme">Theme</button>
|
|
182
|
+
</header>
|
|
183
|
+
|
|
184
|
+
<div class="tabs">
|
|
185
|
+
<button class="tab active" data-tab="fleet" id="tabbtn-fleet">Fleet</button>
|
|
186
|
+
<button class="tab" data-tab="robopark" id="tabbtn-robopark">Sessions & Telemetry</button>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div class="pane active" id="pane-fleet">
|
|
190
|
+
<div class="wrap">
|
|
191
|
+
<section>
|
|
192
|
+
<h2>Fleet <span class="count" id="fleetcount"></span></h2>
|
|
193
|
+
<div class="fleet" id="fleet"></div>
|
|
194
|
+
<div class="hw" id="hw"></div>
|
|
195
|
+
</section>
|
|
196
|
+
<aside>
|
|
197
|
+
<h2>Live activity</h2>
|
|
198
|
+
<div class="feed" id="feed"></div>
|
|
199
|
+
</aside>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div class="pane" id="pane-robopark">
|
|
204
|
+
<div class="rp-head">
|
|
205
|
+
<h2 style="margin:0">RoboPark · sessions & telemetry</h2>
|
|
206
|
+
<div class="rp-controls">
|
|
207
|
+
<span class="pm" id="pm-wrap" hidden>
|
|
208
|
+
<span>production mode</span>
|
|
209
|
+
<button class="toggle" id="pm-toggle" role="switch" aria-checked="false" title="master enable — robots only join sessions when ON"></button>
|
|
210
|
+
<span class="state off" id="pm-state">off</span>
|
|
211
|
+
</span>
|
|
212
|
+
<div class="rp-totals" id="rp-totals"></div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="rp-grid" id="rp-grid"></div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<footer class="cmd">
|
|
219
|
+
<span class="sel">on <b id="selName">this node</b></span>
|
|
220
|
+
<input id="cmdInput" placeholder="/status /nodes /health (Enter to run)" autocomplete="off" spellcheck="false">
|
|
221
|
+
<button class="act" id="runBtn">Run</button>
|
|
222
|
+
<button class="act ghost" id="applyBtn">Apply config to fleet</button>
|
|
223
|
+
</footer>
|
|
224
|
+
<div id="out"><button class="x" id="outClose">close</button><span id="outText"></span></div>
|
|
225
|
+
|
|
226
|
+
<script>
|
|
227
|
+
(function(){
|
|
228
|
+
var TOKEN = new URLSearchParams(location.search).get('token') || '';
|
|
229
|
+
var QS = TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '';
|
|
230
|
+
var selfId = null, selected = null, lastSeq = -1, everOk = false;
|
|
231
|
+
var rpReadOnly = false, pmBusy = false;
|
|
232
|
+
|
|
233
|
+
var $ = function(id){ return document.getElementById(id); };
|
|
234
|
+
function esc(s){ var d=document.createElement('div'); d.textContent = (s==null?'':String(s)); return d.innerHTML; }
|
|
235
|
+
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]; }
|
|
236
|
+
function loadClass(v){ return v>=80?'bad':(v>=50?'warn':''); }
|
|
237
|
+
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()); }
|
|
238
|
+
|
|
239
|
+
// theme
|
|
240
|
+
(function(){ var k='rp-ctl-theme', s=null; try{s=localStorage.getItem(k)}catch(e){}
|
|
241
|
+
if(s) document.documentElement.setAttribute('data-theme', s);
|
|
242
|
+
$('theme').addEventListener('click', function(){
|
|
243
|
+
var m=window.matchMedia('(prefers-color-scheme: dark)');
|
|
244
|
+
var cur=document.documentElement.getAttribute('data-theme')||(m.matches?'dark':'light');
|
|
245
|
+
var nx=cur==='dark'?'light':'dark';
|
|
246
|
+
document.documentElement.setAttribute('data-theme', nx);
|
|
247
|
+
try{localStorage.setItem(k,nx)}catch(e){}
|
|
248
|
+
});
|
|
249
|
+
})();
|
|
250
|
+
|
|
251
|
+
// Pull a short human detail out of an event's data payload.
|
|
252
|
+
function detailOf(data){
|
|
253
|
+
var p = data || {};
|
|
254
|
+
if(typeof p==='string') return p;
|
|
255
|
+
if(p.providerId!=null) return p.providerId + (('healthy' in p)?(p.healthy?' ✓':' ✗'):'');
|
|
256
|
+
if(p.model) return String(p.model);
|
|
257
|
+
if(p.provider) return String(p.provider);
|
|
258
|
+
if(p.reason) return String(p.reason).slice(0,60);
|
|
259
|
+
if(p.description) return String(p.description).slice(0,50);
|
|
260
|
+
if(p.name) return String(p.name);
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
function niceKind(k){ return String(k||'').toLowerCase().replace(/_/g,' '); }
|
|
264
|
+
|
|
265
|
+
function summarizeFrame(f){
|
|
266
|
+
var d = f.d || {};
|
|
267
|
+
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+'%'; }
|
|
268
|
+
if(f.t==='hb'){ return 'heartbeat · load '+(d.load!=null?d.load:'-'); }
|
|
269
|
+
if(f.t==='nd'){ return 'fleet update ('+(Array.isArray(d)?d.length:0)+' nodes)'; }
|
|
270
|
+
// routing decision: compact payload is {k, m, task, w, d}
|
|
271
|
+
if(f.t==='rt'){ var rp=d.d||{}; return 'route → '+(rp.model||rp.provider||rp.decision||detailOf(rp)||'?'); }
|
|
272
|
+
// immediate lifecycle event: {k:type, m, task, w, d:payload}
|
|
273
|
+
if(f.t==='ev'){ var det=detailOf(d.d); return niceKind(d.k)+(det?(' '+det):''); }
|
|
274
|
+
// coalesced chatter: {count, tail:[{k, d}]}
|
|
275
|
+
if(f.t==='log'){
|
|
276
|
+
if(d.tail && d.tail.length){ var last=d.tail[d.tail.length-1]; var ld=detailOf(last.d);
|
|
277
|
+
return niceKind(last.k)+(ld?(' '+ld):'')+(d.count>1?(' ×'+d.count):''); }
|
|
278
|
+
return 'log';
|
|
279
|
+
}
|
|
280
|
+
return f.t;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderFeed(activity){
|
|
284
|
+
var feed = $('feed');
|
|
285
|
+
if(!activity || !activity.length){
|
|
286
|
+
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>'; }
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
feed.dataset.seeded = '1';
|
|
290
|
+
var near = feed.scrollHeight - feed.scrollTop - feed.clientHeight < 40;
|
|
291
|
+
var html = '';
|
|
292
|
+
for(var i=0;i<activity.length;i++){
|
|
293
|
+
var e = activity[i]; var f = e.f || e; if(f.s!=null && f.s<=lastSeq && f.t!=='hw') { /* keep */ }
|
|
294
|
+
var name = (f.n||'').slice(0,10);
|
|
295
|
+
html += '<div class="line k-'+esc(f.t)+'"><span class="t">'+clock(f.ts)+'</span>'
|
|
296
|
+
+ '<span class="n" title="'+esc(f.n)+'">'+esc(name)+'</span>'
|
|
297
|
+
+ '<span class="d">'+esc(summarizeFrame(f))+'</span></div>';
|
|
298
|
+
}
|
|
299
|
+
feed.innerHTML = html;
|
|
300
|
+
if(near) feed.scrollTop = feed.scrollHeight;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function meter(label, value, sub){
|
|
304
|
+
var v = Math.max(0, Math.min(100, Math.round(value||0)));
|
|
305
|
+
return '<div class="meter"><div class="lab"><span>'+esc(label)+'</span><span><b>'+v+'%</b>'
|
|
306
|
+
+ (sub?(' '+esc(sub)):'')+'</span></div>'
|
|
307
|
+
+ '<div class="bar-track"><div class="bar-fill '+loadClass(v)+'" style="width:'+v+'%"></div></div></div>';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function renderHw(hw){
|
|
311
|
+
var el = $('hw');
|
|
312
|
+
if(!hw){ el.innerHTML = '<div class="meter"><div class="lab"><span>This node</span><span>no telemetry yet</span></div></div>'; return; }
|
|
313
|
+
var html = '<h2 style="margin-top:0">This node · '+esc(hw.system?hw.system.hostname:'')+'</h2>';
|
|
314
|
+
html += meter('CPU', hw.cpu?hw.cpu.percent:0, hw.cpu?(hw.cpu.cores+' cores'):'');
|
|
315
|
+
html += meter('Memory', hw.memory?hw.memory.percent:0, hw.memory?(fmtBytes(hw.memory.usedBytes)+' / '+fmtBytes(hw.memory.totalBytes)):'');
|
|
316
|
+
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'):''); }
|
|
317
|
+
el.innerHTML = html;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function selectNode(id, name){
|
|
321
|
+
selected = (id===selfId)?null:id;
|
|
322
|
+
$('selName').textContent = selected ? name : 'this node';
|
|
323
|
+
var cards = document.querySelectorAll('.node');
|
|
324
|
+
for(var i=0;i<cards.length;i++) cards[i].classList.toggle('sel', cards[i].getAttribute('data-id')===id);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderFleet(nodes){
|
|
328
|
+
var el = $('fleet');
|
|
329
|
+
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; }
|
|
330
|
+
nodes.sort(function(a,b){ return (b.connected?1:0)-(a.connected?1:0); });
|
|
331
|
+
var html = '';
|
|
332
|
+
for(var i=0;i<nodes.length;i++){
|
|
333
|
+
var n = nodes[i];
|
|
334
|
+
var isSelf = n.nodeId===selfId;
|
|
335
|
+
var load = n.load!=null?n.load:0;
|
|
336
|
+
var caps = (n.capabilities||[]).slice(0,6);
|
|
337
|
+
var capHtml=''; for(var c=0;c<caps.length;c++) capHtml += '<span class="cap">'+esc(caps[c])+'</span>';
|
|
338
|
+
var site = n.site?('· '+esc(n.site)):'';
|
|
339
|
+
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)+'">'
|
|
340
|
+
+ '<div class="top"><span class="name">'+esc(n.displayName)+(isSelf?' <span class="selfbadge">this</span>':'')+'</span><span class="role">'+esc(n.role)+'</span></div>'
|
|
341
|
+
+ '<div class="meta">'+esc(n.platform||'')+' '+site+' <span class="mono">v'+esc(n.version||'?')+'</span></div>'
|
|
342
|
+
+ '<div class="bar-track"><div class="bar-fill '+loadClass(load)+'" style="width:'+(n.connected?load:0)+'%"></div></div>'
|
|
343
|
+
+ '<div class="load"><span>'+(n.connected?'load '+Math.round(load)+'%':'offline')+'</span><span>'+((n.commands||[]).length)+' cmds</span></div>'
|
|
344
|
+
+ (capHtml?('<div class="caps">'+capHtml+'</div>'):'')
|
|
345
|
+
+ '</div>';
|
|
346
|
+
}
|
|
347
|
+
el.innerHTML = html;
|
|
348
|
+
var cards = el.querySelectorAll('.node');
|
|
349
|
+
for(var j=0;j<cards.length;j++){
|
|
350
|
+
(function(card){
|
|
351
|
+
function pick(){ selectNode(card.getAttribute('data-id'), card.getAttribute('data-name')); }
|
|
352
|
+
card.addEventListener('click', pick);
|
|
353
|
+
card.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ ev.preventDefault(); pick(); } });
|
|
354
|
+
})(cards[j]);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function renderSummary(s){
|
|
359
|
+
var nodes = s.nodes||[];
|
|
360
|
+
var up=0; for(var i=0;i<nodes.length;i++) if(nodes[i].connected) up++;
|
|
361
|
+
var down = nodes.length-up;
|
|
362
|
+
var runs = (s.runs||[]).filter(function(r){return r.status==='running';}).length;
|
|
363
|
+
var loads = nodes.filter(function(n){return n.connected&&n.load!=null;}).map(function(n){return n.load;});
|
|
364
|
+
var avg = loads.length?Math.round(loads.reduce(function(a,b){return a+b;},0)/loads.length):0;
|
|
365
|
+
$('summary').innerHTML =
|
|
366
|
+
'<span class="chip ok">nodes up <b>'+up+'</b></span>'
|
|
367
|
+
+ '<span class="chip'+(down?' bad':'')+'">down <b>'+down+'</b></span>'
|
|
368
|
+
+ '<span class="chip info">active runs <b>'+runs+'</b></span>'
|
|
369
|
+
+ '<span class="chip">avg load <b>'+avg+'%</b></span>';
|
|
370
|
+
$('fleetcount').textContent = '(' + nodes.length + ')';
|
|
371
|
+
var self = s.self||{};
|
|
372
|
+
var role = s.role||{};
|
|
373
|
+
$('who').textContent = (self.displayName||'node') + ' · ' + (self.role||'?') + (role.site?(' · '+role.site):'') + ' · v' + (self.version||'?');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function setLive(ok){
|
|
377
|
+
$('livedot').classList.toggle('stale', !ok);
|
|
378
|
+
$('offline').style.display = (!ok && everOk) ? 'block' : 'none';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function poll(){
|
|
382
|
+
fetch('/fed/status'+QS, {cache:'no-store'}).then(function(r){
|
|
383
|
+
if(!r.ok) throw new Error('status '+r.status);
|
|
384
|
+
return r.json();
|
|
385
|
+
}).then(function(s){
|
|
386
|
+
everOk = true; setLive(true);
|
|
387
|
+
if(!selfId && s.self) selfId = s.self.nodeId;
|
|
388
|
+
if(s.control){ rpReadOnly = !!s.control.readonly; applyReadOnly(); }
|
|
389
|
+
renderSummary(s);
|
|
390
|
+
renderFleet(s.nodes||[]);
|
|
391
|
+
renderHw(s.hardware);
|
|
392
|
+
renderFeed(s.activity||[]);
|
|
393
|
+
}).catch(function(){ setLive(false); });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// command execution
|
|
397
|
+
function showOut(text){ $('outText').textContent = text; $('out').style.display='block'; }
|
|
398
|
+
$('outClose').addEventListener('click', function(){ $('out').style.display='none'; });
|
|
399
|
+
|
|
400
|
+
function runCommand(input){
|
|
401
|
+
input = (input||'').trim();
|
|
402
|
+
if(!input) return;
|
|
403
|
+
// Target a peer by prefixing the kernel's node-scoped command form.
|
|
404
|
+
var payload = selected ? ('/node ' + selected + ' ' + input) : input;
|
|
405
|
+
$('runBtn').disabled = true;
|
|
406
|
+
fetch('/fed/command'+QS, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ input: payload }) })
|
|
407
|
+
.then(function(r){ return r.json().catch(function(){ return { ok:false, text:'HTTP '+r.status }; }); })
|
|
408
|
+
.then(function(res){ showOut((res && (res.text!=null?res.text:JSON.stringify(res,null,2))) || 'no output'); poll(); })
|
|
409
|
+
.catch(function(e){ showOut('request failed: '+e.message); })
|
|
410
|
+
.then(function(){ $('runBtn').disabled=false; });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
$('runBtn').addEventListener('click', function(){ runCommand($('cmdInput').value); });
|
|
414
|
+
$('cmdInput').addEventListener('keydown', function(ev){ if(ev.key==='Enter'){ runCommand($('cmdInput').value); } });
|
|
415
|
+
$('applyBtn').addEventListener('click', function(){
|
|
416
|
+
$('applyBtn').disabled = true;
|
|
417
|
+
fetch('/fed/apply'+QS, { method:'POST', headers:{'content-type':'application/json'}, body:'{}' })
|
|
418
|
+
.then(function(r){ return r.json().catch(function(){ return { ok:false, text:'HTTP '+r.status }; }); })
|
|
419
|
+
.then(function(res){ showOut('Apply config to fleet:\\n' + ((res&&(res.text!=null?res.text:JSON.stringify(res,null,2)))||'done')); })
|
|
420
|
+
.catch(function(e){ showOut('apply failed: '+e.message); })
|
|
421
|
+
.then(function(){ $('applyBtn').disabled=false; });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ── tabs: Fleet + RoboPark sessions/telemetry ──
|
|
425
|
+
var tabs = document.querySelectorAll('.tab');
|
|
426
|
+
var activeTab = 'fleet';
|
|
427
|
+
for (var ti=0; ti<tabs.length; ti++){
|
|
428
|
+
(function(btn){
|
|
429
|
+
btn.addEventListener('click', function(){ selectTab(btn.getAttribute('data-tab')); });
|
|
430
|
+
})(tabs[ti]);
|
|
431
|
+
}
|
|
432
|
+
function selectTab(name){
|
|
433
|
+
activeTab = name;
|
|
434
|
+
for (var i=0;i<tabs.length;i++) tabs[i].classList.toggle('active', tabs[i].getAttribute('data-tab')===name);
|
|
435
|
+
$('pane-fleet').classList.toggle('active', name==='fleet');
|
|
436
|
+
$('pane-robopark').classList.toggle('active', name==='robopark');
|
|
437
|
+
if (location.hash.replace('#','') !== name) { try{ history.replaceState(null,'','#'+name); }catch(e){} }
|
|
438
|
+
if (name==='robopark') pollRobopark();
|
|
439
|
+
}
|
|
440
|
+
// deep-link: open the tab named in the URL hash on load
|
|
441
|
+
if (location.hash.replace('#','') === 'robopark') selectTab('robopark');
|
|
442
|
+
|
|
443
|
+
function robotsFrom(d){
|
|
444
|
+
if (Array.isArray(d)) return d;
|
|
445
|
+
if (d && Array.isArray(d.robots)) return d.robots;
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
// Reflect this node's read-only state on the RoboPark controls.
|
|
449
|
+
function applyReadOnly(){
|
|
450
|
+
var t=$('pm-toggle'); if(t) t.disabled = rpReadOnly || pmBusy;
|
|
451
|
+
var btns=document.querySelectorAll('.rp-btn'); for(var i=0;i<btns.length;i++) btns[i].disabled = rpReadOnly;
|
|
452
|
+
var note=$('rp-ro-note');
|
|
453
|
+
if(rpReadOnly && !note){
|
|
454
|
+
note=document.createElement('span'); note.id='rp-ro-note'; note.className='rp-ro';
|
|
455
|
+
note.textContent='read-only (no token) — start the hub with --token to enable control';
|
|
456
|
+
var wrap=$('pm-wrap'); if(wrap && wrap.parentNode) wrap.parentNode.insertBefore(note, wrap);
|
|
457
|
+
} else if(!rpReadOnly && note){ note.parentNode.removeChild(note); }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// A write through the scheduler proxy. Handles the read-only 403 gracefully.
|
|
461
|
+
function rpAction(method, path, body){
|
|
462
|
+
if(rpReadOnly){ showOut('This node is read-only — restart the hub with --token to enable control actions.'); return Promise.resolve(null); }
|
|
463
|
+
return fetch(path+QS, {
|
|
464
|
+
method: method, cache:'no-store',
|
|
465
|
+
headers: body?{'content-type':'application/json'}:undefined,
|
|
466
|
+
body: body?JSON.stringify(body):undefined
|
|
467
|
+
}).then(function(r){
|
|
468
|
+
if(r.status===403){ return r.json().then(function(j){ if(j&&j.readonly){ rpReadOnly=true; applyReadOnly(); } throw new Error(j&&j.error||'forbidden'); }); }
|
|
469
|
+
return r.json().catch(function(){ return {}; }).then(function(j){ if(!r.ok) throw new Error((j&&(j.error||j.detail))||('status '+r.status)); return j; });
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// production_mode master toggle
|
|
474
|
+
function setPm(on){
|
|
475
|
+
var wrap=$('pm-wrap'), st=$('pm-state'), tg=$('pm-toggle');
|
|
476
|
+
if(!wrap) return;
|
|
477
|
+
wrap.hidden=false;
|
|
478
|
+
tg.setAttribute('aria-checked', on?'true':'false');
|
|
479
|
+
st.textContent = on?'on':'off'; st.className='state '+(on?'on':'off');
|
|
480
|
+
}
|
|
481
|
+
(function(){
|
|
482
|
+
var tg=$('pm-toggle'); if(!tg) return;
|
|
483
|
+
tg.addEventListener('click', function(){
|
|
484
|
+
if(rpReadOnly||pmBusy) return;
|
|
485
|
+
var next = tg.getAttribute('aria-checked')!=='true';
|
|
486
|
+
pmBusy=true; tg.disabled=true;
|
|
487
|
+
rpAction('PUT','/robopark/api/settings',{production_mode:next}).then(function(res){
|
|
488
|
+
setPm(res&&typeof res.production_mode!=='undefined'?res.production_mode:next);
|
|
489
|
+
}).catch(function(e){ showOut('production_mode: '+e.message); })
|
|
490
|
+
.then(function(){ pmBusy=false; applyReadOnly(); });
|
|
491
|
+
});
|
|
492
|
+
})();
|
|
493
|
+
function pollSettings(){
|
|
494
|
+
fetch('/robopark/api/settings'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(s){
|
|
495
|
+
if(s && typeof s.production_mode!=='undefined') setPm(!!s.production_mode);
|
|
496
|
+
}).catch(function(){});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function robotName(x){ return x.robot_id||x.id||x.name||'robot'; }
|
|
500
|
+
function endSession(id){
|
|
501
|
+
if(!confirm('End the active session for '+id+'?')) return;
|
|
502
|
+
rpAction('POST','/robopark/api/robots/'+encodeURIComponent(id)+'/end-session?reason=operator').then(function(){
|
|
503
|
+
showOut('ended session for '+id); pollRobopark();
|
|
504
|
+
}).catch(function(e){ showOut('end-session ('+id+'): '+e.message); });
|
|
505
|
+
}
|
|
506
|
+
// event-delegated action buttons (cards are re-rendered each poll)
|
|
507
|
+
$('rp-grid').addEventListener('click', function(e){
|
|
508
|
+
var b=e.target.closest?e.target.closest('.rp-btn'):null; if(!b) return;
|
|
509
|
+
var id=b.getAttribute('data-robot'), act=b.getAttribute('data-act');
|
|
510
|
+
if(act==='end') endSession(id);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
function pollRobopark(){
|
|
514
|
+
pollSettings();
|
|
515
|
+
fetch('/robopark/api/telemetry'+QS, {cache:'no-store'}).then(function(r){ return r.json(); }).then(function(d){
|
|
516
|
+
var grid = $('rp-grid'), totals = $('rp-totals');
|
|
517
|
+
if (d && (d.disabled || d.ok===false)){
|
|
518
|
+
totals.innerHTML=''; $('pm-wrap').hidden=true;
|
|
519
|
+
grid.innerHTML = '<div class="empty" style="grid-column:1/-1">'+esc(d.error||'no scheduler linked')+' — start the hub with <span class="mono">--scheduler-url http://<scheduler>:8080</span></div>';
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
var robots = robotsFrom(d);
|
|
523
|
+
var tot = (d && d.totals) || {};
|
|
524
|
+
var totalSessions = tot.sessions_total!=null?tot.sessions_total:robots.reduce(function(a,x){return a+(x.sessions_total||0);},0);
|
|
525
|
+
var totalTrig = tot.trigger_count!=null?tot.trigger_count:robots.reduce(function(a,x){return a+(x.trigger_count||0);},0);
|
|
526
|
+
totals.innerHTML =
|
|
527
|
+
'<span class="chip">robots <b>'+robots.length+'</b></span>'
|
|
528
|
+
+ '<span class="chip">sessions <b>'+totalSessions+'</b></span>'
|
|
529
|
+
+ '<span class="chip">triggers <b>'+totalTrig+'</b></span>';
|
|
530
|
+
if (!robots.length){ grid.innerHTML='<div class="empty" style="grid-column:1/-1">No robot telemetry yet — it populates as robots trigger sessions.</div>'; return; }
|
|
531
|
+
var html='';
|
|
532
|
+
for (var i=0;i<robots.length;i++){
|
|
533
|
+
var x = robots[i];
|
|
534
|
+
var name = robotName(x);
|
|
535
|
+
var lat = x.avg_latency_ms!=null?Math.round(x.avg_latency_ms)+'ms':'—';
|
|
536
|
+
var dcls = x.drop_rate!=null?(x.drop_rate>=0.2?'bad':(x.drop_rate>=0.05?'warn':'ok')):'';
|
|
537
|
+
var drop = x.drop_rate!=null?Math.round(x.drop_rate*100)+'%':'—';
|
|
538
|
+
var active = x.active_session||x.in_session||x.session_active;
|
|
539
|
+
var idAttr = esc(name);
|
|
540
|
+
html += '<div class="rp-card"><h4>'+esc(name)+(active?' <span class="chip" style="color:var(--ok)">live</span>':'')+'</h4><div class="rp-metrics">'
|
|
541
|
+
+ '<div class="rp-m"><span class="v">'+(x.trigger_count!=null?x.trigger_count:0)+'</span><span class="l">triggers</span></div>'
|
|
542
|
+
+ '<div class="rp-m"><span class="v">'+(x.sessions_total!=null?x.sessions_total:0)+'</span><span class="l">sessions</span></div>'
|
|
543
|
+
+ '<div class="rp-m"><span class="v">'+lat+'</span><span class="l">avg latency</span></div>'
|
|
544
|
+
+ '<div class="rp-m"><span class="v '+dcls+'">'+drop+'</span><span class="l">drop rate</span></div>'
|
|
545
|
+
+ '</div><div class="rp-actions">'
|
|
546
|
+
+ '<button class="rp-btn danger" data-act="end" data-robot="'+idAttr+'"'+((rpReadOnly||!active)?' disabled':'')+'>End session</button>'
|
|
547
|
+
+ '</div></div>';
|
|
548
|
+
}
|
|
549
|
+
grid.innerHTML = html;
|
|
550
|
+
}).catch(function(){
|
|
551
|
+
$('rp-grid').innerHTML='<div class="empty" style="grid-column:1/-1">telemetry unreachable</div>';
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
poll();
|
|
556
|
+
setInterval(poll, 2000);
|
|
557
|
+
setInterval(function(){ if (activeTab==='robopark') pollRobopark(); }, 3000);
|
|
558
|
+
})();
|
|
559
|
+
</script>
|
|
560
|
+
</body>
|
|
561
|
+
</html>`;
|