@timmeck/brain-core 2.36.15 → 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.
@@ -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">&#x1F916;</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 class="section">
190
- <div class="section-title"><span class="icon">&#x1F4A1;</span> Health</div>
191
- <div class="card">
192
- <div class="gauge-wrap" id="healthGauge">
193
- <svg class="gauge-svg" viewBox="0 0 140 80">
194
- <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"/>
195
- <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"/>
196
- <text x="70" y="65" text-anchor="middle" fill="var(--text-bright)" font-size="22" font-weight="700" id="gaugeValue">--</text>
197
- <text x="70" y="78" text-anchor="middle" fill="var(--text-dim)" font-size="9" id="gaugeLabel">Health Score</text>
198
- </svg>
216
+ <div>
217
+ <div class="section">
218
+ <div class="section-title"><span class="icon">&#x1F4A1;</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">&#x1F4CA;</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">&#x1F4CB;</span> Live Events</div>
205
- <div class="card">
206
- <div class="feed" id="eventFeed"><div class="empty">Waiting for events...</div></div>
237
+ <div class="section-title"><span class="icon">&#x1F4AD;</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">&#x1F310;</span> Peer Connection Graph</div>
213
- <div class="card canvas-wrap" style="height:200px">
245
+ <div class="section-title"><span class="icon">&#x1F310;</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">&#x26A1;</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">&#x1F3ED;</span> Engine Stationen</div>
240
- <div class="engine-grid" id="brainEngineGrid"><div class="empty">Loading engines...</div></div>
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">&#x1F4C8;</span> Trading Pipeline</div>
248
- <div class="pipeline" id="tradingPipeline">
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">&#x1F4E1;</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)">&#x27A1;</div>
251
283
  <div class="pipe-stage"><div class="pipe-box" style="border-color:rgba(0,255,136,0.2)"><div class="pipe-icon">&#x1F50E;</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">&#x1F393;</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">&#x1F3ED;</span> Trading Engines</div>
269
- <div class="engine-grid" id="tradingEngineGrid"><div class="empty">Loading engines...</div></div>
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">&#x1F4E3;</span> Marketing Pipeline</div>
277
- <div class="pipeline" id="marketingPipeline">
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">&#x270D;</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)">&#x27A1;</div>
280
310
  <div class="pipe-stage"><div class="pipe-box" style="border-color:rgba(255,68,204,0.2)"><div class="pipe-icon">&#x1F4CA;</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">&#x1F3AF;</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">&#x1F3ED;</span> Marketing Engines</div>
296
- <div class="engine-grid" id="marketingEngineGrid"><div class="empty">Loading engines...</div></div>
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">&#x1F47E;</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">&#x1F4DC;</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">&#x1F4C3;</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">&#x1F6E1;</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">&#x1F9E9;</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">&#x1F3ED;</span> Alle Engines</div>
356
- <div class="engine-grid" id="allEngineGrid"><div class="empty">Loading engines...</div></div>
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">&#x2705;</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] || 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('connected', () => {
408
- state.connected = true;
409
- updateConnectionBadge();
410
- });
411
-
412
- es.addEventListener('ecosystem', (e) => {
413
- state.ecosystem = JSON.parse(e.data);
414
- renderEcosystem();
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 updateConnectionBadge() {
416
+ function updateConnection() {
444
417
  const b = document.getElementById('connectionBadge');
445
- if (state.connected) {
446
- b.textContent = 'Connected';
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 res = await fetch('/api/state');
458
- const data = await res.json();
459
- state.ecosystem = data.ecosystem;
460
- state.engines = data.engines || [];
461
- state.watchdog = data.watchdog || [];
462
- state.plugins = data.plugins || [];
463
- state.borg = data.borg;
464
- state.analytics = data.analytics;
465
-
466
- renderEcosystem();
467
- renderEngines();
468
- renderWatchdog();
469
- renderPlugins();
470
- renderBorg();
471
- renderAnalytics();
472
- } catch { /* ignore — SSE will update */ }
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
- const arc = document.getElementById('gaugeArc');
490
- const maxLen = 172; // approximate arc length
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 brainLabels = { brain: 'Brain', 'trading-brain': 'Trading-Brain', 'marketing-brain': 'Marketing-Brain' };
502
- const brainClasses = { brain: 'brain-brain', 'trading-brain': 'brain-trading', 'marketing-brain': 'brain-marketing' };
503
-
504
- for (const name of brainNames) {
505
- const b = brains.find(x => x.name === name) || { name, available: false };
506
- const card = document.createElement('div');
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.pid ? `<span>PID ${b.pid}</span>` : ''}
517
- ${b.uptime ? `<span>${formatUptime(b.uptime)}</span>` : ''}
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(correlations) {
534
+ function renderCorrelations(c) {
548
535
  const el = document.getElementById('correlationList');
549
- if (!correlations || correlations.length === 0) { el.innerHTML = '<div class="empty">No correlations yet</div>'; return; }
550
- el.innerHTML = correlations.slice(0, 10).map(c => `
551
- <div style="padding:6px 0;border-bottom:1px solid var(--border);font-size:12px">
552
- <span style="color:var(--cyan)">${c.sourceA}/${c.eventA}</span>
553
- <span style="color:var(--text-dim)"> &#x2194; </span>
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)">&#x2194;</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 results) {
566
- const engines = Array.isArray(r.result) ? r.result : [];
567
- byBrain[r.name] = engines;
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
- for (const eng of engines) all.push({ ...eng, brain });
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 renderEngineGrid(containerId, engines, showBrain = false) {
586
- const el = document.getElementById(containerId);
587
- if (!el) return;
588
- if (!engines || engines.length === 0) { el.innerHTML = '<div class="empty">No engines available</div>'; return; }
589
-
590
- el.innerHTML = engines.map(eng => {
591
- const status = eng.lastActivity && (Date.now() - eng.lastActivity < 60000) ? 'active' : eng.thoughtCount > 0 ? 'idle' : 'off';
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
- // Map engine names to pipeline stages (best effort)
599
- let data = 0, analysis = 0, hyp = 0, exp = 0, prin = 0, act = 0;
600
- for (const e of engines) {
601
- const n = (e.engine || '').toLowerCase();
602
- const c = e.thoughtCount || 0;
603
- if (n.includes('data') || n.includes('scout') || n.includes('miner') || n.includes('scanner')) data += c;
604
- else if (n.includes('anomaly') || n.includes('observer') || n.includes('causal')) analysis += c;
605
- else if (n.includes('hypothesis')) hyp += c;
606
- else if (n.includes('experiment')) exp += c;
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', data);
611
- setText('lp-analysis', analysis);
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 (!a) return;
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 daemons = state.watchdog || [];
642
- if (daemons.length === 0) { el.innerHTML = '<div class="empty">No watchdog configured</div>'; return; }
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
- daemons.map(d => `<tr>
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 allOk = daemons.every(d => d.running && d.healthy);
656
- const offCount = daemons.filter(d => !d.running).length;
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 ? '&#x2705;' : offCount > 0 ? '&#x274C;' : '&#x26A0;'}</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?'&#x2705;':'&#x26A0;'}</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 plugins = state.plugins || [];
672
- if (plugins.length === 0) { el.innerHTML = '<div class="empty">No plugins installed</div>'; return; }
673
- el.innerHTML = plugins.map(p => `
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 borg = state.borg;
686
- const statusEl = document.getElementById('borgStatus');
687
- const toggleBtn = document.getElementById('borgToggle');
688
- const detailsEl = document.getElementById('borgDetails');
689
- const historyEl = document.getElementById('borgHistory');
690
-
691
- if (!borg || !borg.status) {
692
- statusEl.textContent = 'Not available';
693
- toggleBtn.disabled = true;
694
- return;
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'} &mdash; 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' ? '&#x2B06;' : '&#x2B07;'} ${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'} &mdash; ${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'?'&#x2B06;':'&#x2B07;'} ${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
- const borg = state.borg;
726
- if (!borg || !borg.status) return;
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
- canvas.height = rect.height * 2;
744
- const ctx = canvas.getContext('2d');
745
- ctx.scale(2, 2);
746
- const w = rect.width, h = rect.height;
747
-
748
- ctx.clearRect(0, 0, w, h);
749
-
750
- const positions = {
751
- brain: { x: w / 2, y: 35 },
752
- 'trading-brain': { x: w / 4, y: h - 35 },
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
- // Draw nodes
777
- for (const [name, pos] of Object.entries(positions)) {
778
- const online = brainMap[name]?.available;
779
- const color = colors[name];
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
- if (!canvas) return;
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 getPositions() {
831
- const w = canvas.width / 2, h = canvas.height / 2;
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 = canvas.getContext('2d');
849
- const w = canvas.width / 2, h = canvas.height / 2;
850
- ctx.save();
851
- ctx.scale(2, 2);
852
- ctx.clearRect(0, 0, w, h);
853
- getPositions();
854
-
855
- // Connections
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
- // Nodes
895
- for (const [, pos] of Object.entries(positions)) {
896
- // Glow
897
- ctx.beginPath();
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, val) {
933
- const el = document.getElementById(id);
934
- if (el) el.textContent = String(val);
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
- loadInitial();
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>