@timmeck/brain-core 2.36.16 → 2.36.17
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/command-center.html +266 -529
- package/dist/dashboard/__tests__/command-center-server.test.js +57 -0
- package/dist/dashboard/__tests__/command-center-server.test.js.map +1 -1
- package/dist/dashboard/command-center-server.d.ts +7 -0
- package/dist/dashboard/command-center-server.js +53 -10
- package/dist/dashboard/command-center-server.js.map +1 -1
- package/package.json +1 -1
package/command-center.html
CHANGED
|
@@ -73,7 +73,6 @@ a{color:var(--cyan);text-decoration:none}
|
|
|
73
73
|
/* ── Gauge ─────────────────────────────────────────────── */
|
|
74
74
|
.gauge-wrap{text-align:center;padding:12px 0}
|
|
75
75
|
.gauge-svg{width:140px;height:80px}
|
|
76
|
-
.gauge-label{font-size:11px;color:var(--text-dim);margin-top:6px}
|
|
77
76
|
|
|
78
77
|
/* ── Pipeline ──────────────────────────────────────────── */
|
|
79
78
|
.pipeline{display:flex;align-items:center;gap:0;padding:20px 0;overflow-x:auto}
|
|
@@ -86,6 +85,35 @@ a{color:var(--cyan);text-decoration:none}
|
|
|
86
85
|
.pipe-arrow{color:var(--cyan);font-size:20px;opacity:.5;flex-shrink:0;animation:pulse-arrow 2s ease-in-out infinite}
|
|
87
86
|
@keyframes pulse-arrow{0%,100%{opacity:.3;transform:translateX(0)}50%{opacity:1;transform:translateX(4px)}}
|
|
88
87
|
|
|
88
|
+
/* ── Thought Stream ────────────────────────────────────── */
|
|
89
|
+
.thought-stream{max-height:400px;overflow-y:auto;font-size:12px}
|
|
90
|
+
.thought-item{padding:8px 10px;border-bottom:1px solid rgba(100,140,255,0.06);display:flex;gap:10px;align-items:flex-start;animation:fadeIn .3s ease}
|
|
91
|
+
@keyframes fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
|
|
92
|
+
.thought-time{color:var(--text-dim);white-space:nowrap;font-size:10px;min-width:55px;padding-top:1px}
|
|
93
|
+
.thought-engine{font-weight:600;font-size:11px;min-width:90px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
94
|
+
.thought-content{color:var(--text);flex:1;line-height:1.4}
|
|
95
|
+
.thought-sig{font-size:9px;padding:1px 6px;border-radius:8px;font-weight:600;white-space:nowrap}
|
|
96
|
+
.sig-breakthrough{color:var(--yellow);background:rgba(255,204,0,0.12)}
|
|
97
|
+
.sig-notable{color:var(--cyan);background:rgba(0,229,255,0.1)}
|
|
98
|
+
.sig-routine{color:var(--text-dim);background:rgba(100,140,255,0.06)}
|
|
99
|
+
|
|
100
|
+
/* ── Activity Pulse ────────────────────────────────────── */
|
|
101
|
+
.activity-pulse{display:flex;align-items:center;gap:6px;margin-right:12px}
|
|
102
|
+
.pulse-dot{width:10px;height:10px;border-radius:50%;background:var(--green);animation:pulse-glow 2s ease-in-out infinite}
|
|
103
|
+
.pulse-dot.idle{background:var(--yellow);animation:pulse-glow-yellow 3s ease-in-out infinite}
|
|
104
|
+
.pulse-dot.off{background:var(--text-dim);animation:none}
|
|
105
|
+
@keyframes pulse-glow{0%,100%{box-shadow:0 0 4px var(--green)}50%{box-shadow:0 0 14px var(--green)}}
|
|
106
|
+
@keyframes pulse-glow-yellow{0%,100%{box-shadow:0 0 4px var(--yellow)}50%{box-shadow:0 0 10px var(--yellow)}}
|
|
107
|
+
.pulse-label{font-size:11px;color:var(--text-dim)}
|
|
108
|
+
|
|
109
|
+
/* ── LLM Stats ─────────────────────────────────────────── */
|
|
110
|
+
.llm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:10px}
|
|
111
|
+
.llm-stat{text-align:center;padding:10px}
|
|
112
|
+
.llm-stat-value{font-size:22px;font-weight:700;color:var(--cyan)}
|
|
113
|
+
.llm-stat-label{font-size:10px;color:var(--text-dim);margin-top:4px;text-transform:uppercase;letter-spacing:1px}
|
|
114
|
+
.llm-bar{height:6px;border-radius:3px;background:rgba(100,140,255,0.1);margin-top:8px;overflow:hidden}
|
|
115
|
+
.llm-bar-fill{height:100%;border-radius:3px;transition:width .5s ease}
|
|
116
|
+
|
|
89
117
|
/* ── Event Feed ────────────────────────────────────────── */
|
|
90
118
|
.feed{max-height:300px;overflow-y:auto;font-size:12px}
|
|
91
119
|
.feed-item{padding:6px 0;border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:flex-start}
|
|
@@ -94,7 +122,6 @@ a{color:var(--cyan);text-decoration:none}
|
|
|
94
122
|
.feed-source.src-brain{color:var(--brain-color)}
|
|
95
123
|
.feed-source.src-trading{color:var(--trading-color)}
|
|
96
124
|
.feed-source.src-marketing{color:var(--marketing-color)}
|
|
97
|
-
.feed-msg{color:var(--text)}
|
|
98
125
|
|
|
99
126
|
/* ── Engine Grid ───────────────────────────────────────── */
|
|
100
127
|
.engine-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px}
|
|
@@ -168,6 +195,10 @@ canvas{display:block;width:100%;height:100%}
|
|
|
168
195
|
<div class="header">
|
|
169
196
|
<div class="header-title" id="pageTitle">Ecosystem Overview</div>
|
|
170
197
|
<div class="header-badges">
|
|
198
|
+
<div class="activity-pulse" id="activityPulse">
|
|
199
|
+
<div class="pulse-dot off" id="pulseDot"></div>
|
|
200
|
+
<span class="pulse-label" id="pulseLabel">Waiting...</span>
|
|
201
|
+
</div>
|
|
171
202
|
<span class="badge badge-off" id="healthBadge">--</span>
|
|
172
203
|
<span class="badge badge-off" id="connectionBadge">Connecting...</span>
|
|
173
204
|
</div>
|
|
@@ -178,39 +209,41 @@ canvas{display:block;width:100%;height:100%}
|
|
|
178
209
|
<div class="page active" id="page-overview">
|
|
179
210
|
<div class="section">
|
|
180
211
|
<div class="section-title"><span class="icon">🤖</span> Brain Status</div>
|
|
181
|
-
<div class="grid grid-3" id="brainCards">
|
|
182
|
-
<div class="card brain-card brain-brain"><div class="brain-status"><span class="dot dot-off"></span><span class="brain-name">Brain</span></div><div class="brain-meta"><span>Offline</span></div></div>
|
|
183
|
-
<div class="card brain-card brain-trading"><div class="brain-status"><span class="dot dot-off"></span><span class="brain-name">Trading-Brain</span></div><div class="brain-meta"><span>Offline</span></div></div>
|
|
184
|
-
<div class="card brain-card brain-marketing"><div class="brain-status"><span class="dot dot-off"></span><span class="brain-name">Marketing-Brain</span></div><div class="brain-meta"><span>Offline</span></div></div>
|
|
185
|
-
</div>
|
|
212
|
+
<div class="grid grid-3" id="brainCards"></div>
|
|
186
213
|
</div>
|
|
187
214
|
|
|
188
215
|
<div class="grid grid-2">
|
|
189
|
-
<div
|
|
190
|
-
<div class="section
|
|
191
|
-
|
|
192
|
-
<div class="
|
|
193
|
-
<
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
216
|
+
<div>
|
|
217
|
+
<div class="section">
|
|
218
|
+
<div class="section-title"><span class="icon">💡</span> Health</div>
|
|
219
|
+
<div class="card">
|
|
220
|
+
<div class="gauge-wrap">
|
|
221
|
+
<svg class="gauge-svg" viewBox="0 0 140 80">
|
|
222
|
+
<path d="M 15 75 A 55 55 0 0 1 125 75" fill="none" stroke="rgba(100,140,255,0.15)" stroke-width="8" stroke-linecap="round"/>
|
|
223
|
+
<path id="gaugeArc" d="M 15 75 A 55 55 0 0 1 125 75" fill="none" stroke="var(--cyan)" stroke-width="8" stroke-linecap="round" stroke-dasharray="0 200"/>
|
|
224
|
+
<text x="70" y="65" text-anchor="middle" fill="var(--text-bright)" font-size="22" font-weight="700" id="gaugeValue">--</text>
|
|
225
|
+
<text x="70" y="78" text-anchor="middle" fill="var(--text-dim)" font-size="9" id="gaugeLabel">Health Score</text>
|
|
226
|
+
</svg>
|
|
227
|
+
</div>
|
|
199
228
|
</div>
|
|
200
229
|
</div>
|
|
230
|
+
<div class="section">
|
|
231
|
+
<div class="section-title"><span class="icon">📊</span> LLM Nutzung</div>
|
|
232
|
+
<div class="card" id="llmCard"><div class="empty">Waiting for LLM data...</div></div>
|
|
233
|
+
</div>
|
|
201
234
|
</div>
|
|
202
235
|
|
|
203
236
|
<div class="section">
|
|
204
|
-
<div class="section-title"><span class="icon">&#
|
|
205
|
-
<div class="card">
|
|
206
|
-
<div class="
|
|
237
|
+
<div class="section-title"><span class="icon">💭</span> Brain denkt gerade... <span id="thoughtCount" style="color:var(--text-dim);font-weight:400;font-size:12px"></span></div>
|
|
238
|
+
<div class="card" style="padding:0">
|
|
239
|
+
<div class="thought-stream" id="thoughtStream"><div class="empty">Waiting for thoughts...</div></div>
|
|
207
240
|
</div>
|
|
208
241
|
</div>
|
|
209
242
|
</div>
|
|
210
243
|
|
|
211
244
|
<div class="section">
|
|
212
|
-
<div class="section-title"><span class="icon">🌐</span> Peer
|
|
213
|
-
<div class="card canvas-wrap" style="height:
|
|
245
|
+
<div class="section-title"><span class="icon">🌐</span> Peer Verbindungen</div>
|
|
246
|
+
<div class="card canvas-wrap" style="height:180px">
|
|
214
247
|
<canvas id="peerCanvas"></canvas>
|
|
215
248
|
</div>
|
|
216
249
|
</div>
|
|
@@ -234,10 +267,9 @@ canvas{display:block;width:100%;height:100%}
|
|
|
234
267
|
<div class="pipe-stage"><div class="pipe-box"><div class="pipe-icon">⚡</div><div class="pipe-label">Aktionen</div><div class="pipe-value" id="lp-actions">0</div></div></div>
|
|
235
268
|
</div>
|
|
236
269
|
</div>
|
|
237
|
-
|
|
238
270
|
<div class="section">
|
|
239
271
|
<div class="section-title"><span class="icon">🏭</span> Engine Stationen</div>
|
|
240
|
-
<div class="engine-grid" id="brainEngineGrid"><div class="empty">Loading
|
|
272
|
+
<div class="engine-grid" id="brainEngineGrid"><div class="empty">Loading...</div></div>
|
|
241
273
|
</div>
|
|
242
274
|
</div>
|
|
243
275
|
|
|
@@ -245,7 +277,7 @@ canvas{display:block;width:100%;height:100%}
|
|
|
245
277
|
<div class="page" id="page-trading">
|
|
246
278
|
<div class="section">
|
|
247
279
|
<div class="section-title"><span class="icon">📈</span> Trading Pipeline</div>
|
|
248
|
-
<div class="pipeline"
|
|
280
|
+
<div class="pipeline">
|
|
249
281
|
<div class="pipe-stage"><div class="pipe-box" style="border-color:rgba(0,255,136,0.2)"><div class="pipe-icon">📡</div><div class="pipe-label">Signale</div><div class="pipe-value" id="tp-signals" style="color:var(--green)">0</div></div></div>
|
|
250
282
|
<div class="pipe-arrow" style="color:var(--green)">➡</div>
|
|
251
283
|
<div class="pipe-stage"><div class="pipe-box" style="border-color:rgba(0,255,136,0.2)"><div class="pipe-icon">🔎</div><div class="pipe-label">Analyse</div><div class="pipe-value" id="tp-analysis" style="color:var(--green)">0</div></div></div>
|
|
@@ -257,16 +289,14 @@ canvas{display:block;width:100%;height:100%}
|
|
|
257
289
|
<div class="pipe-stage"><div class="pipe-box" style="border-color:rgba(0,255,136,0.2)"><div class="pipe-icon">🎓</div><div class="pipe-label">Lernen</div><div class="pipe-value" id="tp-learned" style="color:var(--green)">0</div></div></div>
|
|
258
290
|
</div>
|
|
259
291
|
</div>
|
|
260
|
-
|
|
261
292
|
<div class="grid grid-3">
|
|
262
293
|
<div class="card"><div class="card-title">Equity</div><div class="card-value" id="tradingEquity" style="color:var(--green)">--</div><div class="card-sub">Paper Trading Balance</div></div>
|
|
263
294
|
<div class="card"><div class="card-title">Win Rate</div><div class="card-value" id="tradingWinRate" style="color:var(--green)">--</div><div class="card-sub">Gewinnrate</div></div>
|
|
264
295
|
<div class="card"><div class="card-title">Offene Positionen</div><div class="card-value" id="tradingPositions" style="color:var(--green)">--</div><div class="card-sub">Aktive Trades</div></div>
|
|
265
296
|
</div>
|
|
266
|
-
|
|
267
297
|
<div class="section" style="margin-top:16px">
|
|
268
298
|
<div class="section-title"><span class="icon">🏭</span> Trading Engines</div>
|
|
269
|
-
<div class="engine-grid" id="tradingEngineGrid"><div class="empty">Loading
|
|
299
|
+
<div class="engine-grid" id="tradingEngineGrid"><div class="empty">Loading...</div></div>
|
|
270
300
|
</div>
|
|
271
301
|
</div>
|
|
272
302
|
|
|
@@ -274,7 +304,7 @@ canvas{display:block;width:100%;height:100%}
|
|
|
274
304
|
<div class="page" id="page-marketing">
|
|
275
305
|
<div class="section">
|
|
276
306
|
<div class="section-title"><span class="icon">📣</span> Marketing Pipeline</div>
|
|
277
|
-
<div class="pipeline"
|
|
307
|
+
<div class="pipeline">
|
|
278
308
|
<div class="pipe-stage"><div class="pipe-box" style="border-color:rgba(255,68,204,0.2)"><div class="pipe-icon">✍</div><div class="pipe-label">Posts</div><div class="pipe-value" id="mp-posts" style="color:var(--magenta)">0</div></div></div>
|
|
279
309
|
<div class="pipe-arrow" style="color:var(--magenta)">➡</div>
|
|
280
310
|
<div class="pipe-stage"><div class="pipe-box" style="border-color:rgba(255,68,204,0.2)"><div class="pipe-icon">📊</div><div class="pipe-label">Engagement</div><div class="pipe-value" id="mp-engagement" style="color:var(--magenta)">0</div></div></div>
|
|
@@ -284,16 +314,14 @@ canvas{display:block;width:100%;height:100%}
|
|
|
284
314
|
<div class="pipe-stage"><div class="pipe-box" style="border-color:rgba(255,68,204,0.2)"><div class="pipe-icon">🎯</div><div class="pipe-label">Strategie</div><div class="pipe-value" id="mp-campaigns" style="color:var(--magenta)">0</div></div></div>
|
|
285
315
|
</div>
|
|
286
316
|
</div>
|
|
287
|
-
|
|
288
317
|
<div class="grid grid-3">
|
|
289
318
|
<div class="card"><div class="card-title">Top Content</div><div class="card-value" id="marketingTopContent" style="color:var(--magenta)">--</div><div class="card-sub">Bester Post</div></div>
|
|
290
319
|
<div class="card"><div class="card-title">Kampagnen</div><div class="card-value" id="marketingCampaigns" style="color:var(--magenta)">--</div><div class="card-sub">Aktive Kampagnen</div></div>
|
|
291
320
|
<div class="card"><div class="card-title">Audience</div><div class="card-value" id="marketingAudience" style="color:var(--magenta)">--</div><div class="card-sub">Zielgruppe</div></div>
|
|
292
321
|
</div>
|
|
293
|
-
|
|
294
322
|
<div class="section" style="margin-top:16px">
|
|
295
323
|
<div class="section-title"><span class="icon">🏭</span> Marketing Engines</div>
|
|
296
|
-
<div class="engine-grid" id="marketingEngineGrid"><div class="empty">Loading
|
|
324
|
+
<div class="engine-grid" id="marketingEngineGrid"><div class="empty">Loading...</div></div>
|
|
297
325
|
</div>
|
|
298
326
|
</div>
|
|
299
327
|
|
|
@@ -305,7 +333,6 @@ canvas{display:block;width:100%;height:100%}
|
|
|
305
333
|
<canvas id="crossBrainCanvas"></canvas>
|
|
306
334
|
</div>
|
|
307
335
|
</div>
|
|
308
|
-
|
|
309
336
|
<div class="grid grid-2">
|
|
310
337
|
<div class="section">
|
|
311
338
|
<div class="section-title"><span class="icon">👾</span> Borg Sync</div>
|
|
@@ -320,20 +347,14 @@ canvas{display:block;width:100%;height:100%}
|
|
|
320
347
|
<div id="borgDetails"></div>
|
|
321
348
|
</div>
|
|
322
349
|
</div>
|
|
323
|
-
|
|
324
350
|
<div class="section">
|
|
325
351
|
<div class="section-title"><span class="icon">📜</span> Korrelationen</div>
|
|
326
|
-
<div class="card">
|
|
327
|
-
<div id="correlationList"><div class="empty">No correlations yet</div></div>
|
|
328
|
-
</div>
|
|
352
|
+
<div class="card"><div id="correlationList"><div class="empty">No correlations yet</div></div></div>
|
|
329
353
|
</div>
|
|
330
354
|
</div>
|
|
331
|
-
|
|
332
355
|
<div class="section">
|
|
333
356
|
<div class="section-title"><span class="icon">📃</span> Sync Historie</div>
|
|
334
|
-
<div class="card">
|
|
335
|
-
<div id="borgHistory"><div class="empty">No sync history</div></div>
|
|
336
|
-
</div>
|
|
357
|
+
<div class="card"><div id="borgHistory"><div class="empty">No sync history</div></div></div>
|
|
337
358
|
</div>
|
|
338
359
|
</div>
|
|
339
360
|
|
|
@@ -344,18 +365,15 @@ canvas{display:block;width:100%;height:100%}
|
|
|
344
365
|
<div class="section-title"><span class="icon">🛡</span> Watchdog</div>
|
|
345
366
|
<div class="card" id="watchdogCard"><div class="empty">Loading...</div></div>
|
|
346
367
|
</div>
|
|
347
|
-
|
|
348
368
|
<div class="section">
|
|
349
369
|
<div class="section-title"><span class="icon">🧩</span> Plugins</div>
|
|
350
370
|
<div class="card" id="pluginCard"><div class="empty">Loading...</div></div>
|
|
351
371
|
</div>
|
|
352
372
|
</div>
|
|
353
|
-
|
|
354
373
|
<div class="section">
|
|
355
374
|
<div class="section-title"><span class="icon">🏭</span> Alle Engines</div>
|
|
356
|
-
<div class="engine-grid" id="allEngineGrid"><div class="empty">Loading
|
|
375
|
+
<div class="engine-grid" id="allEngineGrid"><div class="empty">Loading...</div></div>
|
|
357
376
|
</div>
|
|
358
|
-
|
|
359
377
|
<div class="section">
|
|
360
378
|
<div class="section-title"><span class="icon">✅</span> System Health Check</div>
|
|
361
379
|
<div class="card" id="healthCheckCard"><div class="empty">Loading...</div></div>
|
|
@@ -367,589 +385,308 @@ canvas{display:block;width:100%;height:100%}
|
|
|
367
385
|
|
|
368
386
|
<script>
|
|
369
387
|
// ── State ─────────────────────────────────────────────────
|
|
370
|
-
const state = {
|
|
371
|
-
ecosystem: null,
|
|
372
|
-
engines: [],
|
|
373
|
-
watchdog: [],
|
|
374
|
-
plugins: [],
|
|
375
|
-
borg: null,
|
|
376
|
-
analytics: null,
|
|
377
|
-
events: [],
|
|
378
|
-
connected: false,
|
|
379
|
-
};
|
|
388
|
+
const state = { ecosystem:null, engines:[], watchdog:[], plugins:[], borg:null, analytics:null, llm:null, thoughts:[], connected:false, lastThoughtTime:0 };
|
|
380
389
|
|
|
381
390
|
// ── Navigation ────────────────────────────────────────────
|
|
382
|
-
const titles = {
|
|
383
|
-
overview: 'Ecosystem Overview',
|
|
384
|
-
learning: 'Der Lern-Kreislauf',
|
|
385
|
-
trading: 'Trading Flow',
|
|
386
|
-
marketing: 'Marketing Flow',
|
|
387
|
-
crossbrain: 'Cross-Brain & Borg',
|
|
388
|
-
infra: 'Infrastruktur',
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
+
const titles = { overview:'Ecosystem Overview', learning:'Der Lern-Kreislauf', trading:'Trading Flow', marketing:'Marketing Flow', crossbrain:'Cross-Brain & Borg', infra:'Infrastruktur' };
|
|
391
392
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
392
393
|
item.addEventListener('click', () => {
|
|
393
|
-
const page = item.dataset.page;
|
|
394
394
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
395
395
|
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
396
396
|
item.classList.add('active');
|
|
397
|
-
document.getElementById('page-' + page).classList.add('active');
|
|
398
|
-
document.getElementById('pageTitle').textContent = titles[page] ||
|
|
397
|
+
document.getElementById('page-' + item.dataset.page).classList.add('active');
|
|
398
|
+
document.getElementById('pageTitle').textContent = titles[item.dataset.page] || '';
|
|
399
399
|
});
|
|
400
400
|
});
|
|
401
401
|
|
|
402
402
|
// ── SSE Connection ────────────────────────────────────────
|
|
403
|
-
let es;
|
|
404
403
|
function connectSSE() {
|
|
405
|
-
es = new EventSource('/events');
|
|
406
|
-
|
|
407
|
-
es.addEventListener('
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
es.addEventListener('
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
es.addEventListener('engines', (e) => {
|
|
418
|
-
state.engines = JSON.parse(e.data);
|
|
419
|
-
renderEngines();
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
es.addEventListener('watchdog', (e) => {
|
|
423
|
-
state.watchdog = JSON.parse(e.data);
|
|
424
|
-
renderWatchdog();
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
es.addEventListener('borg', (e) => {
|
|
428
|
-
state.borg = JSON.parse(e.data);
|
|
429
|
-
renderBorg();
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
es.addEventListener('analytics', (e) => {
|
|
433
|
-
state.analytics = JSON.parse(e.data);
|
|
434
|
-
renderAnalytics();
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
es.onerror = () => {
|
|
438
|
-
state.connected = false;
|
|
439
|
-
updateConnectionBadge();
|
|
440
|
-
};
|
|
404
|
+
const es = new EventSource('/events');
|
|
405
|
+
es.addEventListener('connected', () => { state.connected = true; updateConnection(); });
|
|
406
|
+
es.addEventListener('ecosystem', e => { state.ecosystem = JSON.parse(e.data); renderEcosystem(); });
|
|
407
|
+
es.addEventListener('engines', e => { state.engines = JSON.parse(e.data); renderEngines(); });
|
|
408
|
+
es.addEventListener('watchdog', e => { state.watchdog = JSON.parse(e.data); renderWatchdog(); });
|
|
409
|
+
es.addEventListener('borg', e => { state.borg = JSON.parse(e.data); renderBorg(); });
|
|
410
|
+
es.addEventListener('analytics', e => { state.analytics = JSON.parse(e.data); renderAnalytics(); });
|
|
411
|
+
es.addEventListener('llm', e => { state.llm = JSON.parse(e.data); renderLLM(); });
|
|
412
|
+
es.addEventListener('thought', e => { addThought(JSON.parse(e.data)); });
|
|
413
|
+
es.onerror = () => { state.connected = false; updateConnection(); };
|
|
441
414
|
}
|
|
442
415
|
|
|
443
|
-
function
|
|
416
|
+
function updateConnection() {
|
|
444
417
|
const b = document.getElementById('connectionBadge');
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
b.className = 'badge badge-ok';
|
|
448
|
-
} else {
|
|
449
|
-
b.textContent = 'Disconnected';
|
|
450
|
-
b.className = 'badge badge-err';
|
|
451
|
-
}
|
|
418
|
+
b.textContent = state.connected ? 'Connected' : 'Disconnected';
|
|
419
|
+
b.className = 'badge ' + (state.connected ? 'badge-ok' : 'badge-err');
|
|
452
420
|
}
|
|
453
421
|
|
|
454
422
|
// ── Initial Load ──────────────────────────────────────────
|
|
455
423
|
async function loadInitial() {
|
|
456
424
|
try {
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
state.
|
|
460
|
-
state.
|
|
461
|
-
state.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
425
|
+
const data = await (await fetch('/api/state')).json();
|
|
426
|
+
state.ecosystem = data.ecosystem; state.engines = data.engines || [];
|
|
427
|
+
state.watchdog = data.watchdog || []; state.plugins = data.plugins || [];
|
|
428
|
+
state.borg = data.borg; state.analytics = data.analytics; state.llm = data.llm;
|
|
429
|
+
if (data.thoughts) { state.thoughts = data.thoughts; renderThoughts(); }
|
|
430
|
+
renderEcosystem(); renderEngines(); renderWatchdog(); renderPlugins(); renderBorg(); renderAnalytics(); renderLLM();
|
|
431
|
+
} catch {}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Thought Stream ────────────────────────────────────────
|
|
435
|
+
function addThought(t) {
|
|
436
|
+
state.thoughts.unshift(t);
|
|
437
|
+
if (state.thoughts.length > 100) state.thoughts.length = 100;
|
|
438
|
+
state.lastThoughtTime = Date.now();
|
|
439
|
+
updatePulse();
|
|
440
|
+
renderThoughts();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function renderThoughts() {
|
|
444
|
+
const el = document.getElementById('thoughtStream');
|
|
445
|
+
const cnt = document.getElementById('thoughtCount');
|
|
446
|
+
if (!state.thoughts.length) { el.innerHTML = '<div class="empty">Waiting for thoughts...</div>'; cnt.textContent = ''; return; }
|
|
447
|
+
cnt.textContent = `(${state.thoughts.length})`;
|
|
448
|
+
el.innerHTML = state.thoughts.slice(0, 50).map(t => {
|
|
449
|
+
const time = new Date(t.timestamp).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit', second:'2-digit' });
|
|
450
|
+
const sigClass = t.significance === 'breakthrough' ? 'sig-breakthrough' : t.significance === 'notable' ? 'sig-notable' : 'sig-routine';
|
|
451
|
+
const sigLabel = t.significance === 'breakthrough' ? 'Durchbruch' : t.significance === 'notable' ? 'Bemerkenswert' : '';
|
|
452
|
+
return `<div class="thought-item">
|
|
453
|
+
<span class="thought-time">${time}</span>
|
|
454
|
+
<span class="thought-engine" style="color:var(--cyan)">${t.engine}</span>
|
|
455
|
+
<span class="thought-content">${escHtml(t.content)}</span>
|
|
456
|
+
${sigLabel ? `<span class="thought-sig ${sigClass}">${sigLabel}</span>` : ''}
|
|
457
|
+
</div>`;
|
|
458
|
+
}).join('');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── Activity Pulse ────────────────────────────────────────
|
|
462
|
+
function updatePulse() {
|
|
463
|
+
const dot = document.getElementById('pulseDot');
|
|
464
|
+
const label = document.getElementById('pulseLabel');
|
|
465
|
+
const age = Date.now() - state.lastThoughtTime;
|
|
466
|
+
if (state.lastThoughtTime === 0) { dot.className = 'pulse-dot off'; label.textContent = 'Waiting...'; return; }
|
|
467
|
+
if (age < 30000) { dot.className = 'pulse-dot'; label.textContent = 'Brain arbeitet'; }
|
|
468
|
+
else if (age < 120000) { dot.className = 'pulse-dot idle'; label.textContent = 'Idle'; }
|
|
469
|
+
else { dot.className = 'pulse-dot off'; label.textContent = 'Ruhig'; }
|
|
470
|
+
}
|
|
471
|
+
setInterval(updatePulse, 5000);
|
|
472
|
+
|
|
473
|
+
// ── LLM Stats ─────────────────────────────────────────────
|
|
474
|
+
function renderLLM() {
|
|
475
|
+
const el = document.getElementById('llmCard');
|
|
476
|
+
const s = state.llm;
|
|
477
|
+
if (!s) { el.innerHTML = '<div class="empty">Kein LLM Service</div>'; return; }
|
|
478
|
+
|
|
479
|
+
const hourPct = s.tokensThisHour && s.budgetRemainingHour != null ? Math.min(100, (s.tokensThisHour / (s.tokensThisHour + s.budgetRemainingHour)) * 100) : 0;
|
|
480
|
+
const dayPct = s.tokensToday && s.budgetRemainingDay != null ? Math.min(100, (s.tokensToday / (s.tokensToday + s.budgetRemainingDay)) * 100) : 0;
|
|
481
|
+
|
|
482
|
+
el.innerHTML = `
|
|
483
|
+
<div class="llm-grid">
|
|
484
|
+
<div class="llm-stat"><div class="llm-stat-value">${s.totalCalls ?? 0}</div><div class="llm-stat-label">API Calls</div></div>
|
|
485
|
+
<div class="llm-stat"><div class="llm-stat-value">${formatK(s.totalTokens ?? 0)}</div><div class="llm-stat-label">Tokens Total</div></div>
|
|
486
|
+
<div class="llm-stat"><div class="llm-stat-value">${s.cacheHitRate != null ? (s.cacheHitRate * 100).toFixed(0) + '%' : '--'}</div><div class="llm-stat-label">Cache Hit Rate</div></div>
|
|
487
|
+
<div class="llm-stat"><div class="llm-stat-value">${s.callsThisHour ?? 0}</div><div class="llm-stat-label">Calls/Stunde</div></div>
|
|
488
|
+
<div class="llm-stat"><div class="llm-stat-value">${s.averageLatencyMs ? Math.round(s.averageLatencyMs) + 'ms' : '--'}</div><div class="llm-stat-label">Latenz</div></div>
|
|
489
|
+
<div class="llm-stat"><div class="llm-stat-value">${s.errors ?? 0}</div><div class="llm-stat-label">Fehler</div></div>
|
|
490
|
+
</div>
|
|
491
|
+
<div style="margin-top:12px">
|
|
492
|
+
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--text-dim);margin-bottom:2px"><span>Budget Stunde</span><span>${formatK(s.tokensThisHour ?? 0)} / ${formatK((s.tokensThisHour ?? 0) + (s.budgetRemainingHour ?? 0))}</span></div>
|
|
493
|
+
<div class="llm-bar"><div class="llm-bar-fill" style="width:${hourPct}%;background:${hourPct > 80 ? 'var(--red)' : hourPct > 50 ? 'var(--orange)' : 'var(--cyan)'}"></div></div>
|
|
494
|
+
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--text-dim);margin:8px 0 2px"><span>Budget Tag</span><span>${formatK(s.tokensToday ?? 0)} / ${formatK((s.tokensToday ?? 0) + (s.budgetRemainingDay ?? 0))}</span></div>
|
|
495
|
+
<div class="llm-bar"><div class="llm-bar-fill" style="width:${dayPct}%;background:${dayPct > 80 ? 'var(--red)' : dayPct > 50 ? 'var(--orange)' : 'var(--green)'}"></div></div>
|
|
496
|
+
</div>
|
|
497
|
+
${s.model ? `<div style="margin-top:10px;font-size:10px;color:var(--text-dim)">Model: ${s.model}</div>` : ''}
|
|
498
|
+
${s.providers ? `<div style="margin-top:4px;font-size:10px;color:var(--text-dim)">Provider: ${s.providers.map(p => `<span style="color:${p.available ? 'var(--green)' : 'var(--red)'}">${p.name}</span>`).join(', ')}</div>` : ''}
|
|
499
|
+
`;
|
|
473
500
|
}
|
|
474
501
|
|
|
475
502
|
// ── Render: Ecosystem ─────────────────────────────────────
|
|
476
503
|
function renderEcosystem() {
|
|
477
|
-
const eco = state.ecosystem;
|
|
478
|
-
if (!eco) return;
|
|
479
|
-
|
|
480
|
-
// Health badge
|
|
504
|
+
const eco = state.ecosystem; if (!eco) return;
|
|
481
505
|
const hb = document.getElementById('healthBadge');
|
|
482
506
|
const h = eco.health;
|
|
483
507
|
if (h) {
|
|
484
508
|
hb.textContent = h.status === 'healthy' ? 'Healthy' : h.status === 'degraded' ? 'Degraded' : 'Critical';
|
|
485
509
|
hb.className = 'badge ' + (h.status === 'healthy' ? 'badge-ok' : h.status === 'degraded' ? 'badge-warn' : 'badge-err');
|
|
486
|
-
|
|
487
|
-
// Gauge
|
|
488
510
|
const score = h.score || 0;
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
arc.setAttribute('stroke-dasharray', `${(score / 100) * maxLen} ${maxLen}`);
|
|
492
|
-
arc.setAttribute('stroke', score > 70 ? 'var(--green)' : score > 40 ? 'var(--orange)' : 'var(--red)');
|
|
511
|
+
document.getElementById('gaugeArc').setAttribute('stroke-dasharray', `${(score / 100) * 172} 172`);
|
|
512
|
+
document.getElementById('gaugeArc').setAttribute('stroke', score > 70 ? 'var(--green)' : score > 40 ? 'var(--orange)' : 'var(--red)');
|
|
493
513
|
document.getElementById('gaugeValue').textContent = Math.round(score);
|
|
494
514
|
}
|
|
495
|
-
|
|
496
515
|
// Brain cards
|
|
497
|
-
const brains = eco.brains || [];
|
|
498
516
|
const container = document.getElementById('brainCards');
|
|
499
|
-
container.innerHTML = '';
|
|
500
517
|
const brainNames = ['brain', 'trading-brain', 'marketing-brain'];
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
card.className = `card brain-card ${brainClasses[name] || ''}`;
|
|
508
|
-
card.innerHTML = `
|
|
509
|
-
<div class="brain-status">
|
|
510
|
-
<span class="dot ${b.available ? 'dot-on' : 'dot-off'}"></span>
|
|
511
|
-
<span class="brain-name">${brainLabels[name] || name}</span>
|
|
512
|
-
</div>
|
|
518
|
+
const labels = { brain:'Brain', 'trading-brain':'Trading-Brain', 'marketing-brain':'Marketing-Brain' };
|
|
519
|
+
const classes = { brain:'brain-brain', 'trading-brain':'brain-trading', 'marketing-brain':'brain-marketing' };
|
|
520
|
+
container.innerHTML = brainNames.map(name => {
|
|
521
|
+
const b = (eco.brains || []).find(x => x.name === name) || { name, available:false };
|
|
522
|
+
return `<div class="card brain-card ${classes[name]}">
|
|
523
|
+
<div class="brain-status"><span class="dot ${b.available ? 'dot-on' : 'dot-off'}"></span><span class="brain-name">${labels[name]}</span></div>
|
|
513
524
|
<div class="brain-meta">
|
|
514
525
|
<span>${b.available ? 'Online' : 'Offline'}</span>
|
|
515
|
-
${b.version ? `<span>v${b.version}</span>` : ''}
|
|
516
|
-
${b.
|
|
517
|
-
|
|
518
|
-
${b.methods ? `<span>${b.methods} methods</span>` : ''}
|
|
519
|
-
</div>`;
|
|
520
|
-
container.appendChild(card);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Events
|
|
524
|
-
const events = eco.recentEvents || [];
|
|
525
|
-
if (events.length > 0) {
|
|
526
|
-
state.events = events;
|
|
527
|
-
renderEventFeed();
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Peer canvas
|
|
531
|
-
drawPeerGraph(brains);
|
|
532
|
-
|
|
533
|
-
// Correlations
|
|
534
|
-
renderCorrelations(eco.correlations || []);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function renderEventFeed() {
|
|
538
|
-
const feed = document.getElementById('eventFeed');
|
|
539
|
-
if (state.events.length === 0) { feed.innerHTML = '<div class="empty">No events yet</div>'; return; }
|
|
540
|
-
feed.innerHTML = state.events.slice(-20).reverse().map(ev => {
|
|
541
|
-
const time = new Date(ev.timestamp).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
542
|
-
const srcClass = ev.source === 'brain' ? 'src-brain' : ev.source === 'trading-brain' ? 'src-trading' : 'src-marketing';
|
|
543
|
-
return `<div class="feed-item"><span class="feed-time">${time}</span><span class="feed-source ${srcClass}">${ev.source}</span><span class="feed-msg">${ev.event}</span></div>`;
|
|
526
|
+
${b.version ? `<span>v${b.version}</span>` : ''}${b.pid ? `<span>PID ${b.pid}</span>` : ''}
|
|
527
|
+
${b.uptime ? `<span>${fmtUp(b.uptime)}</span>` : ''}${b.methods ? `<span>${b.methods} methods</span>` : ''}
|
|
528
|
+
</div></div>`;
|
|
544
529
|
}).join('');
|
|
530
|
+
drawPeerGraph(eco.brains || []);
|
|
531
|
+
renderCorrelations(eco.correlations || []);
|
|
545
532
|
}
|
|
546
533
|
|
|
547
|
-
function renderCorrelations(
|
|
534
|
+
function renderCorrelations(c) {
|
|
548
535
|
const el = document.getElementById('correlationList');
|
|
549
|
-
if (!
|
|
550
|
-
el.innerHTML =
|
|
551
|
-
<
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
<span style="color:var(--magenta)">${c.sourceB}/${c.eventB}</span>
|
|
555
|
-
<span style="color:var(--text-dim);float:right">${(c.strength * 100).toFixed(0)}%</span>
|
|
556
|
-
</div>`).join('');
|
|
536
|
+
if (!c.length) { el.innerHTML = '<div class="empty">No correlations yet</div>'; return; }
|
|
537
|
+
el.innerHTML = c.slice(0, 10).map(x => `<div style="padding:6px 0;border-bottom:1px solid var(--border);font-size:12px">
|
|
538
|
+
<span style="color:var(--cyan)">${x.sourceA}/${x.eventA}</span> <span style="color:var(--text-dim)">↔</span>
|
|
539
|
+
<span style="color:var(--magenta)">${x.sourceB}/${x.eventB}</span>
|
|
540
|
+
<span style="color:var(--text-dim);float:right">${(x.strength * 100).toFixed(0)}%</span></div>`).join('');
|
|
557
541
|
}
|
|
558
542
|
|
|
559
543
|
// ── Render: Engines ───────────────────────────────────────
|
|
560
544
|
function renderEngines() {
|
|
561
|
-
const results = state.engines || [];
|
|
562
|
-
|
|
563
|
-
// Flatten: each result has { name, result } where result is an array of EngineActivity
|
|
564
545
|
const byBrain = {};
|
|
565
|
-
for (const r of
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
renderEngineGrid('brainEngineGrid', byBrain['brain'] || byBrain['trading-brain'] || byBrain['marketing-brain'] || []);
|
|
571
|
-
renderEngineGrid('tradingEngineGrid', byBrain['trading-brain'] || []);
|
|
572
|
-
renderEngineGrid('marketingEngineGrid', byBrain['marketing-brain'] || []);
|
|
573
|
-
|
|
574
|
-
// All engines on infra page
|
|
546
|
+
for (const r of (state.engines || [])) byBrain[r.name] = Array.isArray(r.result) ? r.result : [];
|
|
547
|
+
renderGrid('brainEngineGrid', byBrain['brain'] || []);
|
|
548
|
+
renderGrid('tradingEngineGrid', byBrain['trading-brain'] || []);
|
|
549
|
+
renderGrid('marketingEngineGrid', byBrain['marketing-brain'] || []);
|
|
575
550
|
const all = [];
|
|
576
|
-
for (const [brain, engines] of Object.entries(byBrain)) {
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
renderEngineGrid('allEngineGrid', all, true);
|
|
580
|
-
|
|
581
|
-
// Learning pipeline numbers from brain engines
|
|
551
|
+
for (const [brain, engines] of Object.entries(byBrain)) for (const e of engines) all.push({ ...e, brain });
|
|
552
|
+
renderGrid('allEngineGrid', all, true);
|
|
582
553
|
updateLearningPipeline(byBrain['brain'] || []);
|
|
583
554
|
}
|
|
584
555
|
|
|
585
|
-
function
|
|
586
|
-
const el = document.getElementById(
|
|
587
|
-
if (!
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const label = showBrain && eng.brain ? `${eng.brain}/${eng.engine}` : (eng.engine || eng.name || 'unknown');
|
|
593
|
-
return `<div class="engine-chip"><span class="engine-dot ${status}"></span><span class="engine-name" title="${label}">${label}</span><span class="engine-count">${eng.thoughtCount || 0}</span></div>`;
|
|
556
|
+
function renderGrid(id, engines, showBrain = false) {
|
|
557
|
+
const el = document.getElementById(id); if (!el) return;
|
|
558
|
+
if (!engines.length) { el.innerHTML = '<div class="empty">No engines</div>'; return; }
|
|
559
|
+
el.innerHTML = engines.map(e => {
|
|
560
|
+
const st = e.lastActivity && (Date.now() - e.lastActivity < 60000) ? 'active' : e.thoughtCount > 0 ? 'idle' : 'off';
|
|
561
|
+
const lbl = showBrain && e.brain ? `${e.brain}/${e.engine}` : (e.engine || e.name || '?');
|
|
562
|
+
return `<div class="engine-chip"><span class="engine-dot ${st}"></span><span class="engine-name" title="${lbl}">${lbl}</span><span class="engine-count">${e.thoughtCount || 0}</span></div>`;
|
|
594
563
|
}).join('');
|
|
595
564
|
}
|
|
596
565
|
|
|
597
566
|
function updateLearningPipeline(engines) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (n.includes('
|
|
604
|
-
else if (n.includes('
|
|
605
|
-
else if (n.includes('
|
|
606
|
-
else if (n.includes('
|
|
607
|
-
else if (n.includes('knowledge') || n.includes('principle') || n.includes('distiller')) prin += c;
|
|
608
|
-
else if (n.includes('strategy') || n.includes('responder') || n.includes('selfmod')) act += c;
|
|
567
|
+
let d=0,a=0,h=0,e=0,p=0,ac=0;
|
|
568
|
+
for (const x of engines) {
|
|
569
|
+
const n = (x.engine||'').toLowerCase(), c = x.thoughtCount||0;
|
|
570
|
+
if (n.includes('data')||n.includes('scout')||n.includes('miner')||n.includes('scanner')) d+=c;
|
|
571
|
+
else if (n.includes('anomaly')||n.includes('observer')||n.includes('causal')) a+=c;
|
|
572
|
+
else if (n.includes('hypothesis')) h+=c;
|
|
573
|
+
else if (n.includes('experiment')) e+=c;
|
|
574
|
+
else if (n.includes('knowledge')||n.includes('principle')||n.includes('distiller')) p+=c;
|
|
575
|
+
else if (n.includes('strategy')||n.includes('responder')||n.includes('selfmod')) ac+=c;
|
|
609
576
|
}
|
|
610
|
-
setText('lp-data',
|
|
611
|
-
setText('lp-
|
|
612
|
-
setText('lp-hypotheses', hyp);
|
|
613
|
-
setText('lp-experiments', exp);
|
|
614
|
-
setText('lp-principles', prin);
|
|
615
|
-
setText('lp-actions', act);
|
|
577
|
+
setText('lp-data',d); setText('lp-analysis',a); setText('lp-hypotheses',h);
|
|
578
|
+
setText('lp-experiments',e); setText('lp-principles',p); setText('lp-actions',ac);
|
|
616
579
|
}
|
|
617
580
|
|
|
618
581
|
// ── Render: Analytics ─────────────────────────────────────
|
|
619
582
|
function renderAnalytics() {
|
|
620
|
-
const a = state.analytics;
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
// Trading stats
|
|
624
|
-
if (a.trading) {
|
|
625
|
-
setText('tp-signals', a.trading.signals || 0);
|
|
626
|
-
setText('tp-trades', a.trading.trades || 0);
|
|
627
|
-
setText('tradingWinRate', a.trading.winRate ? (a.trading.winRate * 100).toFixed(1) + '%' : '--');
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Marketing stats
|
|
631
|
-
if (a.marketing) {
|
|
632
|
-
setText('mp-posts', a.marketing.posts || 0);
|
|
633
|
-
setText('mp-engagement', a.marketing.engagement || 0);
|
|
634
|
-
setText('marketingCampaigns', a.marketing.campaigns || 0);
|
|
635
|
-
}
|
|
583
|
+
const a = state.analytics; if (!a) return;
|
|
584
|
+
if (a.trading) { setText('tp-signals', a.trading.signals||0); setText('tp-trades', a.trading.trades||0); setText('tradingWinRate', a.trading.winRate ? (a.trading.winRate*100).toFixed(1)+'%' : '--'); }
|
|
585
|
+
if (a.marketing) { setText('mp-posts', a.marketing.posts||0); setText('mp-engagement', a.marketing.engagement||0); setText('marketingCampaigns', a.marketing.campaigns||0); }
|
|
636
586
|
}
|
|
637
587
|
|
|
638
588
|
// ── Render: Watchdog ──────────────────────────────────────
|
|
639
589
|
function renderWatchdog() {
|
|
640
590
|
const el = document.getElementById('watchdogCard');
|
|
641
|
-
const
|
|
642
|
-
if (
|
|
643
|
-
|
|
591
|
+
const d = state.watchdog || [];
|
|
592
|
+
if (!d.length) { el.innerHTML = '<div class="empty">No watchdog configured</div>'; return; }
|
|
644
593
|
el.innerHTML = `<table class="tbl"><thead><tr><th>Daemon</th><th>Status</th><th>PID</th><th>Uptime</th><th>Restarts</th></tr></thead><tbody>` +
|
|
645
|
-
|
|
646
|
-
<td>${d.name}</td>
|
|
647
|
-
<td><span class="dot ${d.running ? 'dot-on' : 'dot-off'}" style="margin-right:6px"></span>${d.running ? (d.healthy ? 'Healthy' : 'Unhealthy') : 'Stopped'}</td>
|
|
648
|
-
<td>${d.pid || '-'}</td>
|
|
649
|
-
<td>${d.uptime ? formatUptime(d.uptime / 1000) : '-'}</td>
|
|
650
|
-
<td>${d.restarts || 0}</td>
|
|
651
|
-
</tr>`).join('') + '</tbody></table>';
|
|
652
|
-
|
|
653
|
-
// Health check summary
|
|
594
|
+
d.map(x => `<tr><td>${x.name}</td><td><span class="dot ${x.running?'dot-on':'dot-off'}" style="margin-right:6px"></span>${x.running?(x.healthy?'Healthy':'Unhealthy'):'Stopped'}</td><td>${x.pid||'-'}</td><td>${x.uptime?fmtUp(x.uptime/1000):'-'}</td><td>${x.restarts||0}</td></tr>`).join('') + '</tbody></table>';
|
|
654
595
|
const hc = document.getElementById('healthCheckCard');
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
const unhealthy = daemons.filter(d => d.running && !d.healthy).length;
|
|
658
|
-
hc.innerHTML = `
|
|
659
|
-
<div style="display:flex;gap:20px;align-items:center">
|
|
660
|
-
<span style="font-size:32px">${allOk ? '✅' : offCount > 0 ? '❌' : '⚠'}</span>
|
|
661
|
-
<div>
|
|
662
|
-
<div style="font-size:15px;font-weight:600;color:${allOk ? 'var(--green)' : 'var(--orange)'}">${allOk ? 'All Systems Operational' : `${offCount} offline, ${unhealthy} unhealthy`}</div>
|
|
663
|
-
<div style="font-size:12px;color:var(--text-dim);margin-top:4px">${daemons.length} daemons monitored</div>
|
|
664
|
-
</div>
|
|
665
|
-
</div>`;
|
|
596
|
+
const ok = d.every(x => x.running && x.healthy), off = d.filter(x => !x.running).length;
|
|
597
|
+
hc.innerHTML = `<div style="display:flex;gap:20px;align-items:center"><span style="font-size:32px">${ok?'✅':'⚠'}</span><div><div style="font-size:15px;font-weight:600;color:${ok?'var(--green)':'var(--orange)'}">${ok?'All Systems Operational':`${off} offline`}</div><div style="font-size:12px;color:var(--text-dim);margin-top:4px">${d.length} daemons</div></div></div>`;
|
|
666
598
|
}
|
|
667
599
|
|
|
668
|
-
// ── Render: Plugins ───────────────────────────────────────
|
|
669
600
|
function renderPlugins() {
|
|
670
601
|
const el = document.getElementById('pluginCard');
|
|
671
|
-
const
|
|
672
|
-
if (
|
|
673
|
-
el.innerHTML =
|
|
674
|
-
<div style="padding:8px 0;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
|
|
675
|
-
<div>
|
|
676
|
-
<div style="font-weight:600;font-size:13px">${p.name}</div>
|
|
677
|
-
<div style="font-size:11px;color:var(--text-dim)">${p.description || ''} v${p.version}</div>
|
|
678
|
-
</div>
|
|
679
|
-
<span class="tag tag-green">${p.status || 'loaded'}</span>
|
|
680
|
-
</div>`).join('');
|
|
602
|
+
const p = state.plugins || [];
|
|
603
|
+
if (!p.length) { el.innerHTML = '<div class="empty">No plugins</div>'; return; }
|
|
604
|
+
el.innerHTML = p.map(x => `<div style="padding:8px 0;border-bottom:1px solid var(--border);display:flex;justify-content:space-between"><div><div style="font-weight:600;font-size:13px">${x.name}</div><div style="font-size:11px;color:var(--text-dim)">${x.description||''} v${x.version}</div></div><span class="tag tag-green">${x.status||'loaded'}</span></div>`).join('');
|
|
681
605
|
}
|
|
682
606
|
|
|
683
607
|
// ── Render: Borg ──────────────────────────────────────────
|
|
684
608
|
function renderBorg() {
|
|
685
|
-
const
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const s = borg.status;
|
|
698
|
-
statusEl.innerHTML = `<span class="dot ${s.enabled ? 'dot-on' : 'dot-off'}" style="margin-right:6px"></span>${s.enabled ? 'Active' : 'Disabled'} — Mode: ${s.mode}`;
|
|
699
|
-
toggleBtn.disabled = false;
|
|
700
|
-
toggleBtn.className = s.enabled ? 'btn btn-active' : 'btn';
|
|
701
|
-
toggleBtn.textContent = s.enabled ? 'Disable' : 'Enable';
|
|
702
|
-
|
|
703
|
-
detailsEl.innerHTML = `
|
|
704
|
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;font-size:12px;margin-top:8px">
|
|
705
|
-
<div><span style="color:var(--text-dim)">Total Syncs</span><br><strong>${s.totalSyncs || 0}</strong></div>
|
|
706
|
-
<div><span style="color:var(--text-dim)">Sent</span><br><strong>${s.totalSent || 0}</strong></div>
|
|
707
|
-
<div><span style="color:var(--text-dim)">Received</span><br><strong>${s.totalReceived || 0}</strong></div>
|
|
708
|
-
</div>`;
|
|
709
|
-
|
|
710
|
-
// History
|
|
711
|
-
const history = borg.history || [];
|
|
712
|
-
if (history.length === 0) { historyEl.innerHTML = '<div class="empty">No sync history</div>'; return; }
|
|
713
|
-
historyEl.innerHTML = `<table class="tbl"><thead><tr><th>Time</th><th>Direction</th><th>Peer</th><th>Items</th><th>Accepted</th></tr></thead><tbody>` +
|
|
714
|
-
history.slice(-15).reverse().map(h => `<tr>
|
|
715
|
-
<td>${new Date(h.timestamp).toLocaleTimeString('de-DE')}</td>
|
|
716
|
-
<td>${h.direction === 'sent' ? '⬆' : '⬇'} ${h.direction}</td>
|
|
717
|
-
<td>${h.peer}</td>
|
|
718
|
-
<td>${h.itemCount}</td>
|
|
719
|
-
<td>${h.accepted}</td>
|
|
720
|
-
</tr>`).join('') + '</tbody></table>';
|
|
609
|
+
const b = state.borg;
|
|
610
|
+
const sEl = document.getElementById('borgStatus'), btn = document.getElementById('borgToggle'), det = document.getElementById('borgDetails'), hist = document.getElementById('borgHistory');
|
|
611
|
+
if (!b || !b.status) { sEl.textContent = 'Not available'; btn.disabled = true; return; }
|
|
612
|
+
const s = b.status;
|
|
613
|
+
sEl.innerHTML = `<span class="dot ${s.enabled?'dot-on':'dot-off'}" style="margin-right:6px"></span>${s.enabled?'Active':'Disabled'} — ${s.mode}`;
|
|
614
|
+
btn.disabled = false; btn.className = s.enabled ? 'btn btn-active' : 'btn'; btn.textContent = s.enabled ? 'Disable' : 'Enable';
|
|
615
|
+
det.innerHTML = `<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;font-size:12px;margin-top:8px"><div><span style="color:var(--text-dim)">Syncs</span><br><strong>${s.totalSyncs||0}</strong></div><div><span style="color:var(--text-dim)">Sent</span><br><strong>${s.totalSent||0}</strong></div><div><span style="color:var(--text-dim)">Received</span><br><strong>${s.totalReceived||0}</strong></div></div>`;
|
|
616
|
+
const h = b.history || [];
|
|
617
|
+
if (!h.length) { hist.innerHTML = '<div class="empty">No sync history</div>'; return; }
|
|
618
|
+
hist.innerHTML = `<table class="tbl"><thead><tr><th>Zeit</th><th>Richtung</th><th>Peer</th><th>Items</th><th>Akzeptiert</th></tr></thead><tbody>` +
|
|
619
|
+
h.slice(-15).reverse().map(x => `<tr><td>${new Date(x.timestamp).toLocaleTimeString('de-DE')}</td><td>${x.direction==='sent'?'⬆':'⬇'} ${x.direction}</td><td>${x.peer}</td><td>${x.itemCount}</td><td>${x.accepted}</td></tr>`).join('') + '</tbody></table>';
|
|
721
620
|
}
|
|
722
|
-
|
|
723
|
-
// Borg toggle
|
|
724
621
|
document.getElementById('borgToggle').addEventListener('click', async () => {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const newState = !borg.status.enabled;
|
|
728
|
-
try {
|
|
729
|
-
await fetch('/api/borg/toggle', {
|
|
730
|
-
method: 'POST',
|
|
731
|
-
headers: { 'Content-Type': 'application/json' },
|
|
732
|
-
body: JSON.stringify({ enabled: newState }),
|
|
733
|
-
});
|
|
734
|
-
} catch { /* ignore */ }
|
|
622
|
+
if (!state.borg?.status) return;
|
|
623
|
+
try { await fetch('/api/borg/toggle', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({enabled:!state.borg.status.enabled}) }); } catch {}
|
|
735
624
|
});
|
|
736
625
|
|
|
737
626
|
// ── Canvas: Peer Graph ────────────────────────────────────
|
|
738
627
|
function drawPeerGraph(brains) {
|
|
739
|
-
const canvas = document.getElementById('peerCanvas');
|
|
740
|
-
if (!canvas) return;
|
|
628
|
+
const canvas = document.getElementById('peerCanvas'); if (!canvas) return;
|
|
741
629
|
const rect = canvas.parentElement.getBoundingClientRect();
|
|
742
|
-
canvas.width = rect.width * 2;
|
|
743
|
-
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
'marketing-brain': { x: (3 * w) / 4, y: h - 35 },
|
|
754
|
-
};
|
|
755
|
-
const colors = { brain: '#00e5ff', 'trading-brain': '#00ff88', 'marketing-brain': '#ff44cc' };
|
|
756
|
-
const labels = { brain: 'Brain', 'trading-brain': 'Trading', 'marketing-brain': 'Marketing' };
|
|
757
|
-
const brainMap = {};
|
|
758
|
-
for (const b of (brains || [])) brainMap[b.name] = b;
|
|
759
|
-
|
|
760
|
-
// Draw connections
|
|
761
|
-
const names = Object.keys(positions);
|
|
762
|
-
for (let i = 0; i < names.length; i++) {
|
|
763
|
-
for (let j = i + 1; j < names.length; j++) {
|
|
764
|
-
const a = positions[names[i]], b = positions[names[j]];
|
|
765
|
-
const aOnline = brainMap[names[i]]?.available;
|
|
766
|
-
const bOnline = brainMap[names[j]]?.available;
|
|
767
|
-
ctx.beginPath();
|
|
768
|
-
ctx.moveTo(a.x, a.y);
|
|
769
|
-
ctx.lineTo(b.x, b.y);
|
|
770
|
-
ctx.strokeStyle = (aOnline && bOnline) ? 'rgba(0,229,255,0.3)' : 'rgba(100,140,255,0.08)';
|
|
771
|
-
ctx.lineWidth = (aOnline && bOnline) ? 2 : 1;
|
|
772
|
-
ctx.stroke();
|
|
773
|
-
}
|
|
630
|
+
canvas.width = rect.width * 2; canvas.height = rect.height * 2;
|
|
631
|
+
const ctx = canvas.getContext('2d'); ctx.scale(2,2);
|
|
632
|
+
const w = rect.width, h = rect.height; ctx.clearRect(0,0,w,h);
|
|
633
|
+
const pos = { brain:{x:w/2,y:30}, 'trading-brain':{x:w/4,y:h-30}, 'marketing-brain':{x:3*w/4,y:h-30} };
|
|
634
|
+
const col = { brain:'#00e5ff', 'trading-brain':'#00ff88', 'marketing-brain':'#ff44cc' };
|
|
635
|
+
const lbl = { brain:'Brain', 'trading-brain':'Trading', 'marketing-brain':'Marketing' };
|
|
636
|
+
const map = {}; for (const b of brains) map[b.name] = b;
|
|
637
|
+
const names = Object.keys(pos);
|
|
638
|
+
for (let i=0;i<names.length;i++) for (let j=i+1;j<names.length;j++) {
|
|
639
|
+
const a=pos[names[i]],b=pos[names[j]]; ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y);
|
|
640
|
+
ctx.strokeStyle = (map[names[i]]?.available && map[names[j]]?.available) ? 'rgba(0,229,255,0.3)' : 'rgba(100,140,255,0.08)'; ctx.lineWidth = 2; ctx.stroke();
|
|
774
641
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
// Glow
|
|
782
|
-
if (online) {
|
|
783
|
-
ctx.beginPath();
|
|
784
|
-
ctx.arc(pos.x, pos.y, 22, 0, Math.PI * 2);
|
|
785
|
-
const grd = ctx.createRadialGradient(pos.x, pos.y, 0, pos.x, pos.y, 22);
|
|
786
|
-
grd.addColorStop(0, color + '30');
|
|
787
|
-
grd.addColorStop(1, color + '00');
|
|
788
|
-
ctx.fillStyle = grd;
|
|
789
|
-
ctx.fill();
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Circle
|
|
793
|
-
ctx.beginPath();
|
|
794
|
-
ctx.arc(pos.x, pos.y, 14, 0, Math.PI * 2);
|
|
795
|
-
ctx.fillStyle = online ? color + '20' : 'rgba(100,140,255,0.05)';
|
|
796
|
-
ctx.strokeStyle = online ? color : 'rgba(100,140,255,0.2)';
|
|
797
|
-
ctx.lineWidth = 2;
|
|
798
|
-
ctx.fill();
|
|
799
|
-
ctx.stroke();
|
|
800
|
-
|
|
801
|
-
// Inner dot
|
|
802
|
-
ctx.beginPath();
|
|
803
|
-
ctx.arc(pos.x, pos.y, 4, 0, Math.PI * 2);
|
|
804
|
-
ctx.fillStyle = online ? color : 'rgba(100,140,255,0.3)';
|
|
805
|
-
ctx.fill();
|
|
806
|
-
|
|
807
|
-
// Label
|
|
808
|
-
ctx.fillStyle = online ? '#fff' : 'rgba(120,136,168,0.6)';
|
|
809
|
-
ctx.font = '11px Segoe UI, sans-serif';
|
|
810
|
-
ctx.textAlign = 'center';
|
|
811
|
-
ctx.fillText(labels[name], pos.x, pos.y + 28);
|
|
642
|
+
for (const [name,p] of Object.entries(pos)) {
|
|
643
|
+
const on = map[name]?.available, c = col[name];
|
|
644
|
+
if (on) { ctx.beginPath(); ctx.arc(p.x,p.y,22,0,Math.PI*2); const g=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,22); g.addColorStop(0,c+'30'); g.addColorStop(1,c+'00'); ctx.fillStyle=g; ctx.fill(); }
|
|
645
|
+
ctx.beginPath(); ctx.arc(p.x,p.y,14,0,Math.PI*2); ctx.fillStyle=on?c+'20':'rgba(100,140,255,0.05)'; ctx.strokeStyle=on?c:'rgba(100,140,255,0.2)'; ctx.lineWidth=2; ctx.fill(); ctx.stroke();
|
|
646
|
+
ctx.beginPath(); ctx.arc(p.x,p.y,4,0,Math.PI*2); ctx.fillStyle=on?c:'rgba(100,140,255,0.3)'; ctx.fill();
|
|
647
|
+
ctx.fillStyle=on?'#fff':'rgba(120,136,168,0.6)'; ctx.font='11px Segoe UI,sans-serif'; ctx.textAlign='center'; ctx.fillText(lbl[name],p.x,p.y+28);
|
|
812
648
|
}
|
|
813
649
|
}
|
|
814
650
|
|
|
815
651
|
// ── Canvas: Cross-Brain Animation ─────────────────────────
|
|
816
652
|
let particles = [];
|
|
817
653
|
function initCrossBrainCanvas() {
|
|
818
|
-
const canvas = document.getElementById('crossBrainCanvas');
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
function resize() {
|
|
822
|
-
const rect = canvas.parentElement.getBoundingClientRect();
|
|
823
|
-
canvas.width = rect.width * 2;
|
|
824
|
-
canvas.height = rect.height * 2;
|
|
825
|
-
}
|
|
826
|
-
resize();
|
|
827
|
-
window.addEventListener('resize', resize);
|
|
828
|
-
|
|
654
|
+
const canvas = document.getElementById('crossBrainCanvas'); if (!canvas) return;
|
|
655
|
+
function resize() { const r=canvas.parentElement.getBoundingClientRect(); canvas.width=r.width*2; canvas.height=r.height*2; }
|
|
656
|
+
resize(); window.addEventListener('resize', resize);
|
|
829
657
|
const positions = {};
|
|
830
|
-
function
|
|
831
|
-
|
|
832
|
-
positions.brain = { x: w / 2, y: 50, color: '#00e5ff', label: 'Brain' };
|
|
833
|
-
positions['trading-brain'] = { x: w * 0.2, y: h - 50, color: '#00ff88', label: 'Trading' };
|
|
834
|
-
positions['marketing-brain'] = { x: w * 0.8, y: h - 50, color: '#ff44cc', label: 'Marketing' };
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function spawnParticle() {
|
|
838
|
-
const names = Object.keys(positions);
|
|
839
|
-
if (names.length < 2) return;
|
|
840
|
-
const src = names[Math.floor(Math.random() * names.length)];
|
|
841
|
-
let dst = names[Math.floor(Math.random() * names.length)];
|
|
842
|
-
while (dst === src) dst = names[Math.floor(Math.random() * names.length)];
|
|
843
|
-
const s = positions[src], d = positions[dst];
|
|
844
|
-
particles.push({ x: s.x, y: s.y, tx: d.x, ty: d.y, color: s.color, progress: 0, speed: 0.008 + Math.random() * 0.012 });
|
|
845
|
-
}
|
|
846
|
-
|
|
658
|
+
function getPos() { const w=canvas.width/2,h=canvas.height/2; positions.brain={x:w/2,y:50,color:'#00e5ff',label:'Brain'}; positions['trading-brain']={x:w*.2,y:h-50,color:'#00ff88',label:'Trading'}; positions['marketing-brain']={x:w*.8,y:h-50,color:'#ff44cc',label:'Marketing'}; }
|
|
659
|
+
function spawn() { const n=Object.keys(positions); if(n.length<2)return; let s=n[Math.random()*n.length|0],d=n[Math.random()*n.length|0]; while(d===s)d=n[Math.random()*n.length|0]; const a=positions[s],b=positions[d]; particles.push({x:a.x,y:a.y,tx:b.x,ty:b.y,color:a.color,progress:0,speed:.008+Math.random()*.012}); }
|
|
847
660
|
function draw() {
|
|
848
|
-
const ctx
|
|
849
|
-
const
|
|
850
|
-
ctx.
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const names = Object.keys(positions);
|
|
857
|
-
for (let i = 0; i < names.length; i++) {
|
|
858
|
-
for (let j = i + 1; j < names.length; j++) {
|
|
859
|
-
const a = positions[names[i]], b = positions[names[j]];
|
|
860
|
-
ctx.beginPath();
|
|
861
|
-
ctx.moveTo(a.x, a.y);
|
|
862
|
-
ctx.lineTo(b.x, b.y);
|
|
863
|
-
ctx.strokeStyle = 'rgba(100,140,255,0.1)';
|
|
864
|
-
ctx.lineWidth = 1;
|
|
865
|
-
ctx.stroke();
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// Particles
|
|
870
|
-
if (Math.random() < 0.05 && state.connected) spawnParticle();
|
|
871
|
-
|
|
872
|
-
particles = particles.filter(p => p.progress < 1);
|
|
873
|
-
for (const p of particles) {
|
|
874
|
-
p.progress += p.speed;
|
|
875
|
-
const t = p.progress;
|
|
876
|
-
const cx = p.x + (p.tx - p.x) * t;
|
|
877
|
-
const cy = p.y + (p.ty - p.y) * t - Math.sin(t * Math.PI) * 20;
|
|
878
|
-
|
|
879
|
-
ctx.beginPath();
|
|
880
|
-
ctx.arc(cx, cy, 3, 0, Math.PI * 2);
|
|
881
|
-
ctx.fillStyle = p.color;
|
|
882
|
-
ctx.fill();
|
|
883
|
-
|
|
884
|
-
// Trail
|
|
885
|
-
ctx.beginPath();
|
|
886
|
-
ctx.arc(cx, cy, 6, 0, Math.PI * 2);
|
|
887
|
-
const grd = ctx.createRadialGradient(cx, cy, 0, cx, cy, 6);
|
|
888
|
-
grd.addColorStop(0, p.color + '40');
|
|
889
|
-
grd.addColorStop(1, p.color + '00');
|
|
890
|
-
ctx.fillStyle = grd;
|
|
891
|
-
ctx.fill();
|
|
661
|
+
const ctx=canvas.getContext('2d'),w=canvas.width/2,h=canvas.height/2; ctx.save(); ctx.scale(2,2); ctx.clearRect(0,0,w,h); getPos();
|
|
662
|
+
const n=Object.keys(positions);
|
|
663
|
+
for(let i=0;i<n.length;i++) for(let j=i+1;j<n.length;j++) { const a=positions[n[i]],b=positions[n[j]]; ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.strokeStyle='rgba(100,140,255,0.1)'; ctx.lineWidth=1; ctx.stroke(); }
|
|
664
|
+
if(Math.random()<.05&&state.connected) spawn();
|
|
665
|
+
particles=particles.filter(p=>p.progress<1);
|
|
666
|
+
for(const p of particles) { p.progress+=p.speed; const t=p.progress,cx=p.x+(p.tx-p.x)*t,cy=p.y+(p.ty-p.y)*t-Math.sin(t*Math.PI)*20;
|
|
667
|
+
ctx.beginPath(); ctx.arc(cx,cy,3,0,Math.PI*2); ctx.fillStyle=p.color; ctx.fill();
|
|
668
|
+
ctx.beginPath(); ctx.arc(cx,cy,6,0,Math.PI*2); const g=ctx.createRadialGradient(cx,cy,0,cx,cy,6); g.addColorStop(0,p.color+'40'); g.addColorStop(1,p.color+'00'); ctx.fillStyle=g; ctx.fill();
|
|
892
669
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
ctx.
|
|
898
|
-
ctx.arc(pos.x, pos.y, 28, 0, Math.PI * 2);
|
|
899
|
-
const grd = ctx.createRadialGradient(pos.x, pos.y, 0, pos.x, pos.y, 28);
|
|
900
|
-
grd.addColorStop(0, pos.color + '25');
|
|
901
|
-
grd.addColorStop(1, pos.color + '00');
|
|
902
|
-
ctx.fillStyle = grd;
|
|
903
|
-
ctx.fill();
|
|
904
|
-
|
|
905
|
-
ctx.beginPath();
|
|
906
|
-
ctx.arc(pos.x, pos.y, 18, 0, Math.PI * 2);
|
|
907
|
-
ctx.fillStyle = pos.color + '15';
|
|
908
|
-
ctx.strokeStyle = pos.color;
|
|
909
|
-
ctx.lineWidth = 2;
|
|
910
|
-
ctx.fill();
|
|
911
|
-
ctx.stroke();
|
|
912
|
-
|
|
913
|
-
ctx.beginPath();
|
|
914
|
-
ctx.arc(pos.x, pos.y, 5, 0, Math.PI * 2);
|
|
915
|
-
ctx.fillStyle = pos.color;
|
|
916
|
-
ctx.fill();
|
|
917
|
-
|
|
918
|
-
ctx.fillStyle = '#fff';
|
|
919
|
-
ctx.font = '12px Segoe UI, sans-serif';
|
|
920
|
-
ctx.textAlign = 'center';
|
|
921
|
-
ctx.fillText(pos.label, pos.x, pos.y + 34);
|
|
670
|
+
for(const[,p]of Object.entries(positions)) {
|
|
671
|
+
ctx.beginPath(); ctx.arc(p.x,p.y,28,0,Math.PI*2); const g=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,28); g.addColorStop(0,p.color+'25'); g.addColorStop(1,p.color+'00'); ctx.fillStyle=g; ctx.fill();
|
|
672
|
+
ctx.beginPath(); ctx.arc(p.x,p.y,18,0,Math.PI*2); ctx.fillStyle=p.color+'15'; ctx.strokeStyle=p.color; ctx.lineWidth=2; ctx.fill(); ctx.stroke();
|
|
673
|
+
ctx.beginPath(); ctx.arc(p.x,p.y,5,0,Math.PI*2); ctx.fillStyle=p.color; ctx.fill();
|
|
674
|
+
ctx.fillStyle='#fff'; ctx.font='12px Segoe UI,sans-serif'; ctx.textAlign='center'; ctx.fillText(p.label,p.x,p.y+34);
|
|
922
675
|
}
|
|
923
|
-
|
|
924
|
-
ctx.restore();
|
|
925
|
-
requestAnimationFrame(draw);
|
|
676
|
+
ctx.restore(); requestAnimationFrame(draw);
|
|
926
677
|
}
|
|
927
|
-
|
|
928
678
|
draw();
|
|
929
679
|
}
|
|
930
680
|
|
|
931
681
|
// ── Helpers ───────────────────────────────────────────────
|
|
932
|
-
function setText(id,
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function formatUptime(seconds) {
|
|
938
|
-
if (seconds < 60) return Math.round(seconds) + 's';
|
|
939
|
-
if (seconds < 3600) return Math.round(seconds / 60) + 'm';
|
|
940
|
-
if (seconds < 86400) return (seconds / 3600).toFixed(1) + 'h';
|
|
941
|
-
return (seconds / 86400).toFixed(1) + 'd';
|
|
942
|
-
}
|
|
682
|
+
function setText(id, v) { const el=document.getElementById(id); if(el)el.textContent=String(v); }
|
|
683
|
+
function fmtUp(s) { if(s<60) return Math.round(s)+'s'; if(s<3600) return Math.round(s/60)+'m'; if(s<86400) return (s/3600).toFixed(1)+'h'; return (s/86400).toFixed(1)+'d'; }
|
|
684
|
+
function formatK(n) { if(n>=1e6) return (n/1e6).toFixed(1)+'M'; if(n>=1e3) return (n/1e3).toFixed(1)+'K'; return String(n); }
|
|
685
|
+
function escHtml(s) { const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
|
|
943
686
|
|
|
944
687
|
// ── Init ──────────────────────────────────────────────────
|
|
945
|
-
connectSSE();
|
|
946
|
-
|
|
947
|
-
initCrossBrainCanvas();
|
|
948
|
-
|
|
949
|
-
// Handle resize for peer canvas
|
|
950
|
-
window.addEventListener('resize', () => {
|
|
951
|
-
if (state.ecosystem && state.ecosystem.brains) drawPeerGraph(state.ecosystem.brains);
|
|
952
|
-
});
|
|
688
|
+
connectSSE(); loadInitial(); initCrossBrainCanvas();
|
|
689
|
+
window.addEventListener('resize', () => { if(state.ecosystem?.brains) drawPeerGraph(state.ecosystem.brains); });
|
|
953
690
|
</script>
|
|
954
691
|
</body>
|
|
955
692
|
</html>
|