@timmeck/brain-core 2.36.12 → 2.36.15
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 +955 -0
- package/dist/cross-brain/__tests__/borg-sync-engine.test.d.ts +1 -0
- package/dist/cross-brain/__tests__/borg-sync-engine.test.js +240 -0
- package/dist/cross-brain/__tests__/borg-sync-engine.test.js.map +1 -0
- package/dist/cross-brain/borg-sync-engine.d.ts +62 -0
- package/dist/cross-brain/borg-sync-engine.js +215 -0
- package/dist/cross-brain/borg-sync-engine.js.map +1 -0
- package/dist/cross-brain/borg-types.d.ts +37 -0
- package/dist/cross-brain/borg-types.js +9 -0
- package/dist/cross-brain/borg-types.js.map +1 -0
- package/dist/dashboard/__tests__/command-center-server.test.d.ts +1 -0
- package/dist/dashboard/__tests__/command-center-server.test.js +298 -0
- package/dist/dashboard/__tests__/command-center-server.test.js.map +1 -0
- package/dist/dashboard/command-center-server.d.ts +38 -0
- package/dist/dashboard/command-center-server.js +289 -0
- package/dist/dashboard/command-center-server.js.map +1 -0
- package/dist/embeddings/engine.js +2 -1
- package/dist/embeddings/engine.js.map +1 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/__tests__/anthropic-provider.test.d.ts +1 -0
- package/dist/llm/__tests__/anthropic-provider.test.js +121 -0
- package/dist/llm/__tests__/anthropic-provider.test.js.map +1 -0
- package/dist/llm/__tests__/llm-service.test.js +181 -40
- package/dist/llm/__tests__/llm-service.test.js.map +1 -1
- package/dist/llm/__tests__/ollama-embedding.test.d.ts +1 -0
- package/dist/llm/__tests__/ollama-embedding.test.js +128 -0
- package/dist/llm/__tests__/ollama-embedding.test.js.map +1 -0
- package/dist/llm/__tests__/ollama-provider.test.d.ts +1 -0
- package/dist/llm/__tests__/ollama-provider.test.js +213 -0
- package/dist/llm/__tests__/ollama-provider.test.js.map +1 -0
- package/dist/llm/__tests__/provider.test.d.ts +1 -0
- package/dist/llm/__tests__/provider.test.js +126 -0
- package/dist/llm/__tests__/provider.test.js.map +1 -0
- package/dist/llm/anthropic-provider.d.ts +41 -0
- package/dist/llm/anthropic-provider.js +86 -0
- package/dist/llm/anthropic-provider.js.map +1 -0
- package/dist/llm/index.d.ts +9 -1
- package/dist/llm/index.js +4 -0
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/llm-service.d.ts +55 -7
- package/dist/llm/llm-service.js +184 -82
- package/dist/llm/llm-service.js.map +1 -1
- package/dist/llm/ollama-embedding.d.ts +46 -0
- package/dist/llm/ollama-embedding.js +93 -0
- package/dist/llm/ollama-embedding.js.map +1 -0
- package/dist/llm/ollama-provider.d.ts +80 -0
- package/dist/llm/ollama-provider.js +178 -0
- package/dist/llm/ollama-provider.js.map +1 -0
- package/dist/llm/provider.d.ts +120 -0
- package/dist/llm/provider.js +104 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/missions/mission-engine.d.ts +4 -0
- package/dist/missions/mission-engine.js +30 -8
- package/dist/missions/mission-engine.js.map +1 -1
- package/dist/notifications/__tests__/notification-service.test.d.ts +1 -0
- package/dist/notifications/__tests__/notification-service.test.js +176 -0
- package/dist/notifications/__tests__/notification-service.test.js.map +1 -0
- package/dist/notifications/discord-provider.d.ts +30 -0
- package/dist/notifications/discord-provider.js +89 -0
- package/dist/notifications/discord-provider.js.map +1 -0
- package/dist/notifications/email-provider.d.ts +41 -0
- package/dist/notifications/email-provider.js +101 -0
- package/dist/notifications/email-provider.js.map +1 -0
- package/dist/notifications/index.d.ts +8 -0
- package/dist/notifications/index.js +5 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/notification-provider.d.ts +75 -0
- package/dist/notifications/notification-provider.js +47 -0
- package/dist/notifications/notification-provider.js.map +1 -0
- package/dist/notifications/notification-service.d.ts +85 -0
- package/dist/notifications/notification-service.js +184 -0
- package/dist/notifications/notification-service.js.map +1 -0
- package/dist/notifications/telegram-provider.d.ts +30 -0
- package/dist/notifications/telegram-provider.js +78 -0
- package/dist/notifications/telegram-provider.js.map +1 -0
- package/dist/plugin/__tests__/plugin-registry.test.d.ts +1 -0
- package/dist/plugin/__tests__/plugin-registry.test.js +166 -0
- package/dist/plugin/__tests__/plugin-registry.test.js.map +1 -0
- package/dist/plugin/plugin-registry.d.ts +38 -0
- package/dist/plugin/plugin-registry.js +185 -0
- package/dist/plugin/plugin-registry.js.map +1 -0
- package/dist/plugin/types.d.ts +59 -0
- package/dist/plugin/types.js +2 -0
- package/dist/plugin/types.js.map +1 -0
- package/dist/research/adapters/__tests__/web-adapters.test.d.ts +1 -0
- package/dist/research/adapters/__tests__/web-adapters.test.js +106 -0
- package/dist/research/adapters/__tests__/web-adapters.test.js.map +1 -0
- package/dist/research/adapters/firecrawl-adapter.d.ts +57 -0
- package/dist/research/adapters/firecrawl-adapter.js +137 -0
- package/dist/research/adapters/firecrawl-adapter.js.map +1 -0
- package/dist/research/adapters/index.d.ts +3 -0
- package/dist/research/adapters/index.js +2 -0
- package/dist/research/adapters/index.js.map +1 -1
- package/dist/research/adapters/playwright-adapter.d.ts +54 -0
- package/dist/research/adapters/playwright-adapter.js +130 -0
- package/dist/research/adapters/playwright-adapter.js.map +1 -0
- package/dist/research/research-orchestrator.d.ts +3 -0
- package/dist/research/research-orchestrator.js +19 -1
- package/dist/research/research-orchestrator.js.map +1 -1
- package/dist/techradar/__tests__/techradar-engine.test.d.ts +1 -0
- package/dist/techradar/__tests__/techradar-engine.test.js +246 -0
- package/dist/techradar/__tests__/techradar-engine.test.js.map +1 -0
- package/dist/techradar/daily-digest.d.ts +18 -0
- package/dist/techradar/daily-digest.js +100 -0
- package/dist/techradar/daily-digest.js.map +1 -0
- package/dist/techradar/index.d.ts +5 -0
- package/dist/techradar/index.js +5 -0
- package/dist/techradar/index.js.map +1 -0
- package/dist/techradar/relevance-scorer.d.ts +29 -0
- package/dist/techradar/relevance-scorer.js +139 -0
- package/dist/techradar/relevance-scorer.js.map +1 -0
- package/dist/techradar/repo-watcher.d.ts +24 -0
- package/dist/techradar/repo-watcher.js +87 -0
- package/dist/techradar/repo-watcher.js.map +1 -0
- package/dist/techradar/techradar-engine.d.ts +69 -0
- package/dist/techradar/techradar-engine.js +382 -0
- package/dist/techradar/techradar-engine.js.map +1 -0
- package/dist/techradar/types.d.ts +87 -0
- package/dist/techradar/types.js +5 -0
- package/dist/techradar/types.js.map +1 -0
- package/dist/watchdog/__tests__/watchdog-service.test.d.ts +1 -0
- package/dist/watchdog/__tests__/watchdog-service.test.js +113 -0
- package/dist/watchdog/__tests__/watchdog-service.test.js.map +1 -0
- package/dist/watchdog/watchdog-service.d.ts +60 -0
- package/dist/watchdog/watchdog-service.js +275 -0
- package/dist/watchdog/watchdog-service.js.map +1 -0
- package/dist/watchdog/windows-service.d.ts +39 -0
- package/dist/watchdog/windows-service.js +179 -0
- package/dist/watchdog/windows-service.js.map +1 -0
- package/package.json +20 -2
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Brain Ecosystem — Command Center</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ── Reset & Variables ─────────────────────────────────── */
|
|
9
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
10
|
+
:root{
|
|
11
|
+
--bg-deep:#050810;--bg-card:rgba(15,20,40,0.7);--bg-card-hover:rgba(20,30,60,0.85);
|
|
12
|
+
--border:rgba(100,140,255,0.12);--border-bright:rgba(100,200,255,0.3);
|
|
13
|
+
--text:#e0e8f8;--text-dim:#7888a8;--text-bright:#ffffff;
|
|
14
|
+
--cyan:#00e5ff;--green:#00ff88;--magenta:#ff44cc;--orange:#ff9933;
|
|
15
|
+
--red:#ff4466;--yellow:#ffcc00;--purple:#aa88ff;
|
|
16
|
+
--brain-color:#00e5ff;--trading-color:#00ff88;--marketing-color:#ff44cc;
|
|
17
|
+
--glass:rgba(15,20,40,0.6);--glass-border:rgba(100,140,255,0.15);
|
|
18
|
+
--radius:12px;--radius-sm:8px;
|
|
19
|
+
}
|
|
20
|
+
html,body{height:100%;font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg-deep);color:var(--text);overflow:hidden}
|
|
21
|
+
a{color:var(--cyan);text-decoration:none}
|
|
22
|
+
|
|
23
|
+
/* ── Layout ────────────────────────────────────────────── */
|
|
24
|
+
.app{display:flex;height:100vh}
|
|
25
|
+
.sidebar{width:220px;background:var(--glass);border-right:1px solid var(--glass-border);display:flex;flex-direction:column;padding:16px 0;flex-shrink:0;backdrop-filter:blur(20px)}
|
|
26
|
+
.sidebar-brand{padding:12px 20px;font-size:15px;font-weight:700;color:var(--cyan);letter-spacing:1px;border-bottom:1px solid var(--border);margin-bottom:8px}
|
|
27
|
+
.sidebar-brand span{color:var(--text-dim);font-weight:400;font-size:12px;display:block;margin-top:2px}
|
|
28
|
+
.nav-item{padding:10px 20px;cursor:pointer;color:var(--text-dim);font-size:13px;transition:all .2s;display:flex;align-items:center;gap:10px;border-left:3px solid transparent}
|
|
29
|
+
.nav-item:hover{background:rgba(0,229,255,0.05);color:var(--text)}
|
|
30
|
+
.nav-item.active{color:var(--cyan);border-left-color:var(--cyan);background:rgba(0,229,255,0.08)}
|
|
31
|
+
.nav-icon{font-size:16px;width:22px;text-align:center}
|
|
32
|
+
.nav-section{padding:8px 20px 4px;font-size:10px;text-transform:uppercase;letter-spacing:2px;color:var(--text-dim);opacity:.6;margin-top:8px}
|
|
33
|
+
|
|
34
|
+
.main{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
|
35
|
+
.header{padding:12px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--glass);backdrop-filter:blur(20px)}
|
|
36
|
+
.header-title{font-size:16px;font-weight:600}
|
|
37
|
+
.header-badges{display:flex;gap:8px;align-items:center}
|
|
38
|
+
.badge{padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600;border:1px solid}
|
|
39
|
+
.badge-ok{color:var(--green);border-color:rgba(0,255,136,0.3);background:rgba(0,255,136,0.08)}
|
|
40
|
+
.badge-warn{color:var(--orange);border-color:rgba(255,153,51,0.3);background:rgba(255,153,51,0.08)}
|
|
41
|
+
.badge-err{color:var(--red);border-color:rgba(255,68,102,0.3);background:rgba(255,68,102,0.08)}
|
|
42
|
+
.badge-off{color:var(--text-dim);border-color:var(--border);background:rgba(100,140,255,0.04)}
|
|
43
|
+
|
|
44
|
+
.content{flex:1;overflow-y:auto;padding:20px 24px}
|
|
45
|
+
.page{display:none}
|
|
46
|
+
.page.active{display:block}
|
|
47
|
+
|
|
48
|
+
/* ── Cards ─────────────────────────────────────────────── */
|
|
49
|
+
.grid{display:grid;gap:16px}
|
|
50
|
+
.grid-3{grid-template-columns:repeat(3,1fr)}
|
|
51
|
+
.grid-2{grid-template-columns:repeat(2,1fr)}
|
|
52
|
+
.grid-4{grid-template-columns:repeat(4,1fr)}
|
|
53
|
+
.card{background:var(--bg-card);border:1px solid var(--glass-border);border-radius:var(--radius);padding:16px;backdrop-filter:blur(12px);transition:all .3s}
|
|
54
|
+
.card:hover{background:var(--bg-card-hover);border-color:var(--border-bright)}
|
|
55
|
+
.card-title{font-size:12px;text-transform:uppercase;letter-spacing:1.5px;color:var(--text-dim);margin-bottom:12px}
|
|
56
|
+
.card-value{font-size:28px;font-weight:700;line-height:1}
|
|
57
|
+
.card-sub{font-size:11px;color:var(--text-dim);margin-top:6px}
|
|
58
|
+
|
|
59
|
+
/* ── Brain Cards ───────────────────────────────────────── */
|
|
60
|
+
.brain-card{position:relative;overflow:hidden}
|
|
61
|
+
.brain-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px}
|
|
62
|
+
.brain-card.brain-brain::before{background:var(--brain-color)}
|
|
63
|
+
.brain-card.brain-trading::before{background:var(--trading-color)}
|
|
64
|
+
.brain-card.brain-marketing::before{background:var(--marketing-color)}
|
|
65
|
+
.brain-status{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
|
66
|
+
.brain-name{font-size:15px;font-weight:600}
|
|
67
|
+
.dot{width:8px;height:8px;border-radius:50%;display:inline-block}
|
|
68
|
+
.dot-on{background:var(--green);box-shadow:0 0 8px var(--green)}
|
|
69
|
+
.dot-off{background:var(--red);box-shadow:0 0 8px var(--red)}
|
|
70
|
+
.brain-meta{font-size:11px;color:var(--text-dim)}
|
|
71
|
+
.brain-meta span{margin-right:12px}
|
|
72
|
+
|
|
73
|
+
/* ── Gauge ─────────────────────────────────────────────── */
|
|
74
|
+
.gauge-wrap{text-align:center;padding:12px 0}
|
|
75
|
+
.gauge-svg{width:140px;height:80px}
|
|
76
|
+
.gauge-label{font-size:11px;color:var(--text-dim);margin-top:6px}
|
|
77
|
+
|
|
78
|
+
/* ── Pipeline ──────────────────────────────────────────── */
|
|
79
|
+
.pipeline{display:flex;align-items:center;gap:0;padding:20px 0;overflow-x:auto}
|
|
80
|
+
.pipe-stage{text-align:center;flex:1;min-width:110px}
|
|
81
|
+
.pipe-box{background:var(--bg-card);border:1px solid var(--glass-border);border-radius:var(--radius-sm);padding:12px 8px;margin:0 auto;width:100px;transition:all .3s}
|
|
82
|
+
.pipe-box:hover{border-color:var(--cyan);box-shadow:0 0 12px rgba(0,229,255,0.15)}
|
|
83
|
+
.pipe-icon{font-size:22px;margin-bottom:6px}
|
|
84
|
+
.pipe-label{font-size:11px;color:var(--text-dim);margin-bottom:4px}
|
|
85
|
+
.pipe-value{font-size:18px;font-weight:700;color:var(--text-bright)}
|
|
86
|
+
.pipe-arrow{color:var(--cyan);font-size:20px;opacity:.5;flex-shrink:0;animation:pulse-arrow 2s ease-in-out infinite}
|
|
87
|
+
@keyframes pulse-arrow{0%,100%{opacity:.3;transform:translateX(0)}50%{opacity:1;transform:translateX(4px)}}
|
|
88
|
+
|
|
89
|
+
/* ── Event Feed ────────────────────────────────────────── */
|
|
90
|
+
.feed{max-height:300px;overflow-y:auto;font-size:12px}
|
|
91
|
+
.feed-item{padding:6px 0;border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:flex-start}
|
|
92
|
+
.feed-time{color:var(--text-dim);white-space:nowrap;font-size:11px;min-width:60px}
|
|
93
|
+
.feed-source{font-weight:600;min-width:70px}
|
|
94
|
+
.feed-source.src-brain{color:var(--brain-color)}
|
|
95
|
+
.feed-source.src-trading{color:var(--trading-color)}
|
|
96
|
+
.feed-source.src-marketing{color:var(--marketing-color)}
|
|
97
|
+
.feed-msg{color:var(--text)}
|
|
98
|
+
|
|
99
|
+
/* ── Engine Grid ───────────────────────────────────────── */
|
|
100
|
+
.engine-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px}
|
|
101
|
+
.engine-chip{background:var(--bg-card);border:1px solid var(--glass-border);border-radius:var(--radius-sm);padding:8px 10px;font-size:11px;display:flex;align-items:center;gap:6px}
|
|
102
|
+
.engine-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
|
103
|
+
.engine-dot.active{background:var(--green)}
|
|
104
|
+
.engine-dot.idle{background:var(--yellow)}
|
|
105
|
+
.engine-dot.off{background:var(--text-dim)}
|
|
106
|
+
.engine-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
107
|
+
.engine-count{margin-left:auto;color:var(--text-dim);font-size:10px}
|
|
108
|
+
|
|
109
|
+
/* ── Canvas ────────────────────────────────────────────── */
|
|
110
|
+
.canvas-wrap{position:relative;border-radius:var(--radius);overflow:hidden;background:rgba(5,8,16,0.5);border:1px solid var(--glass-border)}
|
|
111
|
+
canvas{display:block;width:100%;height:100%}
|
|
112
|
+
|
|
113
|
+
/* ── Table ─────────────────────────────────────────────── */
|
|
114
|
+
.tbl{width:100%;border-collapse:collapse;font-size:12px}
|
|
115
|
+
.tbl th{text-align:left;padding:8px 10px;color:var(--text-dim);font-weight:600;border-bottom:1px solid var(--border);font-size:11px;text-transform:uppercase;letter-spacing:1px}
|
|
116
|
+
.tbl td{padding:8px 10px;border-bottom:1px solid rgba(100,140,255,0.06)}
|
|
117
|
+
.tbl tr:hover td{background:rgba(0,229,255,0.03)}
|
|
118
|
+
|
|
119
|
+
/* ── Misc ──────────────────────────────────────────────── */
|
|
120
|
+
.section{margin-bottom:24px}
|
|
121
|
+
.section-title{font-size:14px;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
|
122
|
+
.section-title .icon{font-size:16px}
|
|
123
|
+
.tag{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600}
|
|
124
|
+
.tag-cyan{color:var(--cyan);background:rgba(0,229,255,0.1)}
|
|
125
|
+
.tag-green{color:var(--green);background:rgba(0,255,136,0.1)}
|
|
126
|
+
.tag-magenta{color:var(--magenta);background:rgba(255,68,204,0.1)}
|
|
127
|
+
.empty{color:var(--text-dim);font-style:italic;padding:20px;text-align:center;font-size:13px}
|
|
128
|
+
.btn{padding:6px 16px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg-card);color:var(--text);cursor:pointer;font-size:12px;transition:all .2s}
|
|
129
|
+
.btn:hover{border-color:var(--cyan);color:var(--cyan)}
|
|
130
|
+
.btn.btn-active{border-color:var(--green);color:var(--green);background:rgba(0,255,136,0.08)}
|
|
131
|
+
|
|
132
|
+
/* ── Scrollbar ─────────────────────────────────────────── */
|
|
133
|
+
::-webkit-scrollbar{width:6px}
|
|
134
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
135
|
+
::-webkit-scrollbar-thumb{background:rgba(100,140,255,0.2);border-radius:3px}
|
|
136
|
+
::-webkit-scrollbar-thumb:hover{background:rgba(100,140,255,0.4)}
|
|
137
|
+
|
|
138
|
+
/* ── Responsive ────────────────────────────────────────── */
|
|
139
|
+
@media(max-width:900px){
|
|
140
|
+
.grid-3{grid-template-columns:1fr 1fr}
|
|
141
|
+
.grid-4{grid-template-columns:1fr 1fr}
|
|
142
|
+
.sidebar{width:56px}
|
|
143
|
+
.sidebar-brand span,.nav-label,.nav-section{display:none}
|
|
144
|
+
.nav-item{justify-content:center;padding:10px 0}
|
|
145
|
+
}
|
|
146
|
+
@media(max-width:600px){
|
|
147
|
+
.grid-3,.grid-2,.grid-4{grid-template-columns:1fr}
|
|
148
|
+
}
|
|
149
|
+
</style>
|
|
150
|
+
</head>
|
|
151
|
+
<body>
|
|
152
|
+
<div class="app">
|
|
153
|
+
<!-- ── Sidebar ───────────────────────────────────────── -->
|
|
154
|
+
<div class="sidebar">
|
|
155
|
+
<div class="sidebar-brand">COMMAND CENTER<span>Brain Ecosystem</span></div>
|
|
156
|
+
<div class="nav-section">Views</div>
|
|
157
|
+
<div class="nav-item active" data-page="overview"><span class="nav-icon">🌍</span><span class="nav-label">Ecosystem</span></div>
|
|
158
|
+
<div class="nav-item" data-page="learning"><span class="nav-icon">🧠</span><span class="nav-label">Lern-Kreislauf</span></div>
|
|
159
|
+
<div class="nav-item" data-page="trading"><span class="nav-icon">📈</span><span class="nav-label">Trading Flow</span></div>
|
|
160
|
+
<div class="nav-item" data-page="marketing"><span class="nav-icon">📣</span><span class="nav-label">Marketing Flow</span></div>
|
|
161
|
+
<div class="nav-section">System</div>
|
|
162
|
+
<div class="nav-item" data-page="crossbrain"><span class="nav-icon">🔗</span><span class="nav-label">Cross-Brain</span></div>
|
|
163
|
+
<div class="nav-item" data-page="infra"><span class="nav-icon">⚙</span><span class="nav-label">Infrastruktur</span></div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- ── Main ──────────────────────────────────────────── -->
|
|
167
|
+
<div class="main">
|
|
168
|
+
<div class="header">
|
|
169
|
+
<div class="header-title" id="pageTitle">Ecosystem Overview</div>
|
|
170
|
+
<div class="header-badges">
|
|
171
|
+
<span class="badge badge-off" id="healthBadge">--</span>
|
|
172
|
+
<span class="badge badge-off" id="connectionBadge">Connecting...</span>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div class="content">
|
|
177
|
+
<!-- ════ Page 1: Ecosystem Overview ═════════════════ -->
|
|
178
|
+
<div class="page active" id="page-overview">
|
|
179
|
+
<div class="section">
|
|
180
|
+
<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>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="grid grid-2">
|
|
189
|
+
<div class="section">
|
|
190
|
+
<div class="section-title"><span class="icon">💡</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>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div class="section">
|
|
204
|
+
<div class="section-title"><span class="icon">📋</span> Live Events</div>
|
|
205
|
+
<div class="card">
|
|
206
|
+
<div class="feed" id="eventFeed"><div class="empty">Waiting for events...</div></div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div class="section">
|
|
212
|
+
<div class="section-title"><span class="icon">🌐</span> Peer Connection Graph</div>
|
|
213
|
+
<div class="card canvas-wrap" style="height:200px">
|
|
214
|
+
<canvas id="peerCanvas"></canvas>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- ════ Page 2: Lern-Kreislauf ═════════════════════ -->
|
|
220
|
+
<div class="page" id="page-learning">
|
|
221
|
+
<div class="section">
|
|
222
|
+
<div class="section-title"><span class="icon">🔄</span> Der Lern-Kreislauf</div>
|
|
223
|
+
<div class="pipeline" id="learnPipeline">
|
|
224
|
+
<div class="pipe-stage"><div class="pipe-box"><div class="pipe-icon">📥</div><div class="pipe-label">Daten rein</div><div class="pipe-value" id="lp-data">0</div></div></div>
|
|
225
|
+
<div class="pipe-arrow">➡</div>
|
|
226
|
+
<div class="pipe-stage"><div class="pipe-box"><div class="pipe-icon">🔍</div><div class="pipe-label">Analyse</div><div class="pipe-value" id="lp-analysis">0</div></div></div>
|
|
227
|
+
<div class="pipe-arrow">➡</div>
|
|
228
|
+
<div class="pipe-stage"><div class="pipe-box"><div class="pipe-icon">💡</div><div class="pipe-label">Hypothesen</div><div class="pipe-value" id="lp-hypotheses">0</div></div></div>
|
|
229
|
+
<div class="pipe-arrow">➡</div>
|
|
230
|
+
<div class="pipe-stage"><div class="pipe-box"><div class="pipe-icon">🧪</div><div class="pipe-label">Experimente</div><div class="pipe-value" id="lp-experiments">0</div></div></div>
|
|
231
|
+
<div class="pipe-arrow">➡</div>
|
|
232
|
+
<div class="pipe-stage"><div class="pipe-box"><div class="pipe-icon">📜</div><div class="pipe-label">Prinzipien</div><div class="pipe-value" id="lp-principles">0</div></div></div>
|
|
233
|
+
<div class="pipe-arrow">➡</div>
|
|
234
|
+
<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
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div class="section">
|
|
239
|
+
<div class="section-title"><span class="icon">🏭</span> Engine Stationen</div>
|
|
240
|
+
<div class="engine-grid" id="brainEngineGrid"><div class="empty">Loading engines...</div></div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<!-- ════ Page 3: Trading Flow ═══════════════════════ -->
|
|
245
|
+
<div class="page" id="page-trading">
|
|
246
|
+
<div class="section">
|
|
247
|
+
<div class="section-title"><span class="icon">📈</span> Trading Pipeline</div>
|
|
248
|
+
<div class="pipeline" id="tradingPipeline">
|
|
249
|
+
<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
|
+
<div class="pipe-arrow" style="color:var(--green)">➡</div>
|
|
251
|
+
<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>
|
|
252
|
+
<div class="pipe-arrow" style="color:var(--green)">➡</div>
|
|
253
|
+
<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">Paper Trade</div><div class="pipe-value" id="tp-trades" style="color:var(--green)">0</div></div></div>
|
|
254
|
+
<div class="pipe-arrow" style="color:var(--green)">➡</div>
|
|
255
|
+
<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">Ergebnis</div><div class="pipe-value" id="tp-pnl" style="color:var(--green)">$0</div></div></div>
|
|
256
|
+
<div class="pipe-arrow" style="color:var(--green)">➡</div>
|
|
257
|
+
<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
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div class="grid grid-3">
|
|
262
|
+
<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
|
+
<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
|
+
<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
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div class="section" style="margin-top:16px">
|
|
268
|
+
<div class="section-title"><span class="icon">🏭</span> Trading Engines</div>
|
|
269
|
+
<div class="engine-grid" id="tradingEngineGrid"><div class="empty">Loading engines...</div></div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<!-- ════ Page 4: Marketing Flow ═════════════════════ -->
|
|
274
|
+
<div class="page" id="page-marketing">
|
|
275
|
+
<div class="section">
|
|
276
|
+
<div class="section-title"><span class="icon">📣</span> Marketing Pipeline</div>
|
|
277
|
+
<div class="pipeline" id="marketingPipeline">
|
|
278
|
+
<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
|
+
<div class="pipe-arrow" style="color:var(--magenta)">➡</div>
|
|
280
|
+
<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>
|
|
281
|
+
<div class="pipe-arrow" style="color:var(--magenta)">➡</div>
|
|
282
|
+
<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">Muster</div><div class="pipe-value" id="mp-patterns" style="color:var(--magenta)">0</div></div></div>
|
|
283
|
+
<div class="pipe-arrow" style="color:var(--magenta)">➡</div>
|
|
284
|
+
<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
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div class="grid grid-3">
|
|
289
|
+
<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
|
+
<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
|
+
<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
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div class="section" style="margin-top:16px">
|
|
295
|
+
<div class="section-title"><span class="icon">🏭</span> Marketing Engines</div>
|
|
296
|
+
<div class="engine-grid" id="marketingEngineGrid"><div class="empty">Loading engines...</div></div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<!-- ════ Page 5: Cross-Brain & Borg ═════════════════ -->
|
|
301
|
+
<div class="page" id="page-crossbrain">
|
|
302
|
+
<div class="section">
|
|
303
|
+
<div class="section-title"><span class="icon">🔗</span> Cross-Brain Kommunikation</div>
|
|
304
|
+
<div class="card canvas-wrap" style="height:280px">
|
|
305
|
+
<canvas id="crossBrainCanvas"></canvas>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div class="grid grid-2">
|
|
310
|
+
<div class="section">
|
|
311
|
+
<div class="section-title"><span class="icon">👾</span> Borg Sync</div>
|
|
312
|
+
<div class="card" id="borgCard">
|
|
313
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
|
|
314
|
+
<div>
|
|
315
|
+
<div class="card-title" style="margin-bottom:4px">Collective Sync</div>
|
|
316
|
+
<div id="borgStatus" style="font-size:13px;color:var(--text-dim)">Not available</div>
|
|
317
|
+
</div>
|
|
318
|
+
<button class="btn" id="borgToggle" disabled>Toggle</button>
|
|
319
|
+
</div>
|
|
320
|
+
<div id="borgDetails"></div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div class="section">
|
|
325
|
+
<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>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div class="section">
|
|
333
|
+
<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>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<!-- ════ Page 6: Infrastruktur ══════════════════════ -->
|
|
341
|
+
<div class="page" id="page-infra">
|
|
342
|
+
<div class="grid grid-2">
|
|
343
|
+
<div class="section">
|
|
344
|
+
<div class="section-title"><span class="icon">🛡</span> Watchdog</div>
|
|
345
|
+
<div class="card" id="watchdogCard"><div class="empty">Loading...</div></div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<div class="section">
|
|
349
|
+
<div class="section-title"><span class="icon">🧩</span> Plugins</div>
|
|
350
|
+
<div class="card" id="pluginCard"><div class="empty">Loading...</div></div>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<div class="section">
|
|
355
|
+
<div class="section-title"><span class="icon">🏭</span> Alle Engines</div>
|
|
356
|
+
<div class="engine-grid" id="allEngineGrid"><div class="empty">Loading engines...</div></div>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<div class="section">
|
|
360
|
+
<div class="section-title"><span class="icon">✅</span> System Health Check</div>
|
|
361
|
+
<div class="card" id="healthCheckCard"><div class="empty">Loading...</div></div>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<script>
|
|
369
|
+
// ── 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
|
+
};
|
|
380
|
+
|
|
381
|
+
// ── 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
|
+
document.querySelectorAll('.nav-item').forEach(item => {
|
|
392
|
+
item.addEventListener('click', () => {
|
|
393
|
+
const page = item.dataset.page;
|
|
394
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
395
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
396
|
+
item.classList.add('active');
|
|
397
|
+
document.getElementById('page-' + page).classList.add('active');
|
|
398
|
+
document.getElementById('pageTitle').textContent = titles[page] || page;
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// ── SSE Connection ────────────────────────────────────────
|
|
403
|
+
let es;
|
|
404
|
+
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
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function updateConnectionBadge() {
|
|
444
|
+
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
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── Initial Load ──────────────────────────────────────────
|
|
455
|
+
async function loadInitial() {
|
|
456
|
+
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 */ }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Render: Ecosystem ─────────────────────────────────────
|
|
476
|
+
function renderEcosystem() {
|
|
477
|
+
const eco = state.ecosystem;
|
|
478
|
+
if (!eco) return;
|
|
479
|
+
|
|
480
|
+
// Health badge
|
|
481
|
+
const hb = document.getElementById('healthBadge');
|
|
482
|
+
const h = eco.health;
|
|
483
|
+
if (h) {
|
|
484
|
+
hb.textContent = h.status === 'healthy' ? 'Healthy' : h.status === 'degraded' ? 'Degraded' : 'Critical';
|
|
485
|
+
hb.className = 'badge ' + (h.status === 'healthy' ? 'badge-ok' : h.status === 'degraded' ? 'badge-warn' : 'badge-err');
|
|
486
|
+
|
|
487
|
+
// Gauge
|
|
488
|
+
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)');
|
|
493
|
+
document.getElementById('gaugeValue').textContent = Math.round(score);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Brain cards
|
|
497
|
+
const brains = eco.brains || [];
|
|
498
|
+
const container = document.getElementById('brainCards');
|
|
499
|
+
container.innerHTML = '';
|
|
500
|
+
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>
|
|
513
|
+
<div class="brain-meta">
|
|
514
|
+
<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>`;
|
|
544
|
+
}).join('');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function renderCorrelations(correlations) {
|
|
548
|
+
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)"> ↔ </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('');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ── Render: Engines ───────────────────────────────────────
|
|
560
|
+
function renderEngines() {
|
|
561
|
+
const results = state.engines || [];
|
|
562
|
+
|
|
563
|
+
// Flatten: each result has { name, result } where result is an array of EngineActivity
|
|
564
|
+
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
|
|
575
|
+
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
|
|
582
|
+
updateLearningPipeline(byBrain['brain'] || []);
|
|
583
|
+
}
|
|
584
|
+
|
|
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>`;
|
|
594
|
+
}).join('');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
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;
|
|
609
|
+
}
|
|
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);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ── Render: Analytics ─────────────────────────────────────
|
|
619
|
+
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
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ── Render: Watchdog ──────────────────────────────────────
|
|
639
|
+
function renderWatchdog() {
|
|
640
|
+
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
|
+
|
|
644
|
+
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
|
|
654
|
+
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 ? '✅' : 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>`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ── Render: Plugins ───────────────────────────────────────
|
|
669
|
+
function renderPlugins() {
|
|
670
|
+
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('');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ── Render: Borg ──────────────────────────────────────────
|
|
684
|
+
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'} — 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>';
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Borg toggle
|
|
724
|
+
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 */ }
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// ── Canvas: Peer Graph ────────────────────────────────────
|
|
738
|
+
function drawPeerGraph(brains) {
|
|
739
|
+
const canvas = document.getElementById('peerCanvas');
|
|
740
|
+
if (!canvas) return;
|
|
741
|
+
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
|
+
}
|
|
774
|
+
}
|
|
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);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ── Canvas: Cross-Brain Animation ─────────────────────────
|
|
816
|
+
let particles = [];
|
|
817
|
+
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
|
+
|
|
829
|
+
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
|
+
|
|
847
|
+
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();
|
|
892
|
+
}
|
|
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);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
ctx.restore();
|
|
925
|
+
requestAnimationFrame(draw);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
draw();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── 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
|
+
}
|
|
943
|
+
|
|
944
|
+
// ── 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
|
+
});
|
|
953
|
+
</script>
|
|
954
|
+
</body>
|
|
955
|
+
</html>
|