erne-universal 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -26
- package/agents/feature-builder.md +88 -0
- package/agents/senior-developer.md +77 -0
- package/bin/cli.js +4 -2
- package/dashboard/package.json +10 -0
- package/dashboard/public/agents.js +329 -0
- package/dashboard/public/canvas.js +275 -0
- package/dashboard/public/index.html +113 -0
- package/dashboard/public/sidebar.js +107 -0
- package/dashboard/public/ws-client.js +69 -0
- package/dashboard/server.js +191 -0
- package/docs/assets/dashboard-preview.png +0 -0
- package/docs/superpowers/plans/2026-03-11-agent-dashboard.md +1537 -0
- package/docs/superpowers/specs/2026-03-11-agent-dashboard-design.md +275 -0
- package/hooks/hooks.json +14 -0
- package/lib/dashboard.js +156 -0
- package/lib/init.js +294 -0
- package/lib/start.js +26 -0
- package/lib/update.js +60 -0
- package/package.json +3 -1
- package/scripts/daily-news/scan-ai-agents.js +222 -0
- package/scripts/daily-news/scan-rn-expo.js +233 -0
- package/scripts/hooks/dashboard-event.js +89 -0
- package/scripts/sync/issue-to-clickup.js +108 -0
- package/scripts/validate-all.js +1 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ERNE Dashboard — Pixel-art office canvas renderer
|
|
3
|
+
* Draws 4 rooms with walls, floors, doors, desks, and computers.
|
|
4
|
+
*/
|
|
5
|
+
(function () {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const TILE_SIZE = 16;
|
|
9
|
+
const OFFICE_COLS = 52;
|
|
10
|
+
const OFFICE_ROWS = 36;
|
|
11
|
+
const CANVAS_W = OFFICE_COLS * TILE_SIZE; // 832
|
|
12
|
+
const CANVAS_H = OFFICE_ROWS * TILE_SIZE; // 576
|
|
13
|
+
|
|
14
|
+
const COLORS = {
|
|
15
|
+
wall: '#2C2137',
|
|
16
|
+
wallLight: '#4A3F5C',
|
|
17
|
+
floor: '#8B7355',
|
|
18
|
+
floorAlt: '#7A6548',
|
|
19
|
+
desk: '#5C4033',
|
|
20
|
+
deskTop: '#8B6914',
|
|
21
|
+
computer: '#1a1a2e',
|
|
22
|
+
computerScreen: '#16213e',
|
|
23
|
+
door: '#6B4226',
|
|
24
|
+
doorFrame: '#8B5A2B',
|
|
25
|
+
whiteboard: '#E8E8E8',
|
|
26
|
+
whiteboardFrame: '#666666',
|
|
27
|
+
chair: '#4A3F5C',
|
|
28
|
+
headerBg: '#1a1a2e',
|
|
29
|
+
headerText: '#E0E0E0',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ROOMS = {
|
|
33
|
+
development: { x: 1, y: 3, w: 24, h: 14 },
|
|
34
|
+
review: { x: 27, y: 3, w: 24, h: 14 },
|
|
35
|
+
testing: { x: 1, y: 19, w: 24, h: 14 },
|
|
36
|
+
conference: { x: 27, y: 19, w: 24, h: 14 },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const DESK_POSITIONS = {
|
|
40
|
+
development: [
|
|
41
|
+
{ x: 4, y: 4, agent: 'architect' },
|
|
42
|
+
{ x: 12, y: 4, agent: 'native-bridge-builder' },
|
|
43
|
+
{ x: 4, y: 9, agent: 'expo-config-resolver' },
|
|
44
|
+
{ x: 12, y: 9, agent: 'ui-designer' },
|
|
45
|
+
{ x: 20, y: 4, agent: 'senior-developer' },
|
|
46
|
+
{ x: 20, y: 9, agent: 'feature-builder' },
|
|
47
|
+
],
|
|
48
|
+
review: [
|
|
49
|
+
{ x: 4, y: 4, agent: 'code-reviewer' },
|
|
50
|
+
{ x: 12, y: 4, agent: 'upgrade-assistant' },
|
|
51
|
+
],
|
|
52
|
+
testing: [
|
|
53
|
+
{ x: 4, y: 4, agent: 'tdd-guide' },
|
|
54
|
+
{ x: 12, y: 4, agent: 'performance-profiler' },
|
|
55
|
+
],
|
|
56
|
+
conference: [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const ROOM_LABELS = {
|
|
60
|
+
development: 'DEVELOPMENT',
|
|
61
|
+
review: 'CODE REVIEW',
|
|
62
|
+
testing: 'TESTING',
|
|
63
|
+
conference: 'CONFERENCE',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/* ---- Drawing helpers ---- */
|
|
67
|
+
|
|
68
|
+
const fillTile = (ctx, col, row, color) => {
|
|
69
|
+
ctx.fillStyle = color;
|
|
70
|
+
ctx.fillRect(col * TILE_SIZE, row * TILE_SIZE, TILE_SIZE, TILE_SIZE);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const drawRoom = (ctx, room, name) => {
|
|
74
|
+
const rx = room.x;
|
|
75
|
+
const ry = room.y;
|
|
76
|
+
const rw = room.w;
|
|
77
|
+
const rh = room.h;
|
|
78
|
+
|
|
79
|
+
// Floor — checkerboard
|
|
80
|
+
for (let r = ry; r < ry + rh; r++) {
|
|
81
|
+
for (let c = rx; c < rx + rw; c++) {
|
|
82
|
+
const alt = (r + c) % 2 === 0;
|
|
83
|
+
fillTile(ctx, c, r, alt ? COLORS.floor : COLORS.floorAlt);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Walls — top and sides
|
|
88
|
+
for (let c = rx; c < rx + rw; c++) {
|
|
89
|
+
fillTile(ctx, c, ry, COLORS.wall);
|
|
90
|
+
// Highlight strip
|
|
91
|
+
ctx.fillStyle = COLORS.wallLight;
|
|
92
|
+
ctx.fillRect(c * TILE_SIZE, ry * TILE_SIZE + TILE_SIZE - 3, TILE_SIZE, 3);
|
|
93
|
+
}
|
|
94
|
+
for (let r = ry; r < ry + rh; r++) {
|
|
95
|
+
fillTile(ctx, rx, r, COLORS.wall);
|
|
96
|
+
fillTile(ctx, rx + rw - 1, r, COLORS.wall);
|
|
97
|
+
}
|
|
98
|
+
// Bottom wall
|
|
99
|
+
for (let c = rx; c < rx + rw; c++) {
|
|
100
|
+
fillTile(ctx, c, ry + rh - 1, COLORS.wall);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Door — bottom center
|
|
104
|
+
const doorC = rx + Math.floor(rw / 2);
|
|
105
|
+
fillTile(ctx, doorC, ry + rh - 1, COLORS.door);
|
|
106
|
+
fillTile(ctx, doorC - 1, ry + rh - 1, COLORS.doorFrame);
|
|
107
|
+
fillTile(ctx, doorC + 1, ry + rh - 1, COLORS.doorFrame);
|
|
108
|
+
// Door knob
|
|
109
|
+
ctx.fillStyle = '#C0A060';
|
|
110
|
+
ctx.fillRect(doorC * TILE_SIZE + 10, (ry + rh - 1) * TILE_SIZE + 7, 3, 3);
|
|
111
|
+
|
|
112
|
+
// Room label
|
|
113
|
+
ctx.fillStyle = COLORS.headerText;
|
|
114
|
+
ctx.font = 'bold 10px monospace';
|
|
115
|
+
ctx.textAlign = 'center';
|
|
116
|
+
ctx.fillText(
|
|
117
|
+
ROOM_LABELS[name] || name.toUpperCase(),
|
|
118
|
+
(rx + rw / 2) * TILE_SIZE,
|
|
119
|
+
ry * TILE_SIZE + 11
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Desks
|
|
123
|
+
const desks = DESK_POSITIONS[name] || [];
|
|
124
|
+
for (const d of desks) {
|
|
125
|
+
drawDesk(ctx, (rx + d.x) * TILE_SIZE, (ry + d.y) * TILE_SIZE);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Whiteboards for development and testing
|
|
129
|
+
if (name === 'development' || name === 'testing') {
|
|
130
|
+
drawWhiteboard(ctx, (rx + 20) * TILE_SIZE, (ry + 1) * TILE_SIZE);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Conference table
|
|
134
|
+
if (name === 'conference') {
|
|
135
|
+
drawConferenceTable(ctx, rx, ry, rw, rh);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const drawDesk = (ctx, x, y) => {
|
|
140
|
+
// Desk body
|
|
141
|
+
ctx.fillStyle = COLORS.desk;
|
|
142
|
+
ctx.fillRect(x, y + 12, 32, 18);
|
|
143
|
+
// Desk top
|
|
144
|
+
ctx.fillStyle = COLORS.deskTop;
|
|
145
|
+
ctx.fillRect(x - 2, y + 10, 36, 4);
|
|
146
|
+
// Computer monitor
|
|
147
|
+
ctx.fillStyle = COLORS.computer;
|
|
148
|
+
ctx.fillRect(x + 8, y, 16, 12);
|
|
149
|
+
// Screen
|
|
150
|
+
ctx.fillStyle = COLORS.computerScreen;
|
|
151
|
+
ctx.fillRect(x + 10, y + 2, 12, 8);
|
|
152
|
+
// Screen glow
|
|
153
|
+
ctx.fillStyle = '#0f3460';
|
|
154
|
+
ctx.fillRect(x + 11, y + 3, 10, 6);
|
|
155
|
+
// Keyboard
|
|
156
|
+
ctx.fillStyle = '#333';
|
|
157
|
+
ctx.fillRect(x + 6, y + 14, 20, 4);
|
|
158
|
+
// Chair
|
|
159
|
+
ctx.fillStyle = COLORS.chair;
|
|
160
|
+
ctx.fillRect(x + 10, y + 22, 12, 8);
|
|
161
|
+
ctx.fillRect(x + 12, y + 18, 8, 4);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const drawConferenceTable = (ctx, rx, ry, rw, rh) => {
|
|
165
|
+
const cx = (rx + rw / 2) * TILE_SIZE;
|
|
166
|
+
const cy = (ry + rh / 2) * TILE_SIZE;
|
|
167
|
+
const tw = 10 * TILE_SIZE;
|
|
168
|
+
const th = 5 * TILE_SIZE;
|
|
169
|
+
// Table shadow
|
|
170
|
+
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
|
171
|
+
ctx.fillRect(cx - tw / 2 + 3, cy - th / 2 + 3, tw, th);
|
|
172
|
+
// Table
|
|
173
|
+
ctx.fillStyle = COLORS.desk;
|
|
174
|
+
ctx.fillRect(cx - tw / 2, cy - th / 2, tw, th);
|
|
175
|
+
ctx.fillStyle = COLORS.deskTop;
|
|
176
|
+
ctx.fillRect(cx - tw / 2 + 2, cy - th / 2 + 2, tw - 4, th - 4);
|
|
177
|
+
// Chairs around table
|
|
178
|
+
ctx.fillStyle = COLORS.chair;
|
|
179
|
+
for (let i = 0; i < 4; i++) {
|
|
180
|
+
const chairX = cx - tw / 2 + 20 + i * 40;
|
|
181
|
+
ctx.fillRect(chairX, cy - th / 2 - 14, 12, 10);
|
|
182
|
+
ctx.fillRect(chairX, cy + th / 2 + 4, 12, 10);
|
|
183
|
+
}
|
|
184
|
+
for (let i = 0; i < 2; i++) {
|
|
185
|
+
const chairY = cy - th / 2 + 14 + i * 30;
|
|
186
|
+
ctx.fillRect(cx - tw / 2 - 14, chairY, 10, 12);
|
|
187
|
+
ctx.fillRect(cx + tw / 2 + 4, chairY, 10, 12);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const drawWhiteboard = (ctx, x, y) => {
|
|
192
|
+
// Frame
|
|
193
|
+
ctx.fillStyle = COLORS.whiteboardFrame;
|
|
194
|
+
ctx.fillRect(x - 2, y - 2, 36, 22);
|
|
195
|
+
// Board
|
|
196
|
+
ctx.fillStyle = COLORS.whiteboard;
|
|
197
|
+
ctx.fillRect(x, y, 32, 18);
|
|
198
|
+
// Some scribbles
|
|
199
|
+
ctx.fillStyle = '#888';
|
|
200
|
+
ctx.fillRect(x + 4, y + 4, 12, 2);
|
|
201
|
+
ctx.fillRect(x + 4, y + 8, 18, 2);
|
|
202
|
+
ctx.fillRect(x + 4, y + 12, 8, 2);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const drawHeader = (ctx) => {
|
|
206
|
+
ctx.fillStyle = COLORS.headerBg;
|
|
207
|
+
ctx.fillRect(0, 0, CANVAS_W, 3 * TILE_SIZE);
|
|
208
|
+
ctx.fillStyle = COLORS.headerText;
|
|
209
|
+
ctx.font = 'bold 20px monospace';
|
|
210
|
+
ctx.textAlign = 'center';
|
|
211
|
+
ctx.fillText('ERNE HQ', CANVAS_W / 2, 30);
|
|
212
|
+
// Decorative line
|
|
213
|
+
ctx.fillStyle = COLORS.wallLight;
|
|
214
|
+
ctx.fillRect(CANVAS_W / 2 - 60, 38, 120, 2);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const drawOffice = (ctx) => {
|
|
218
|
+
// Background
|
|
219
|
+
ctx.fillStyle = '#0a0a1a';
|
|
220
|
+
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
|
|
221
|
+
|
|
222
|
+
drawHeader(ctx);
|
|
223
|
+
|
|
224
|
+
// Hallway floor between rooms
|
|
225
|
+
for (let r = 0; r < OFFICE_ROWS; r++) {
|
|
226
|
+
for (let c = 0; c < OFFICE_COLS; c++) {
|
|
227
|
+
// Only fill hallway areas (outside rooms, below header)
|
|
228
|
+
if (r >= 3) {
|
|
229
|
+
let inRoom = false;
|
|
230
|
+
for (const room of Object.values(ROOMS)) {
|
|
231
|
+
if (c >= room.x && c < room.x + room.w && r >= room.y && r < room.y + room.h) {
|
|
232
|
+
inRoom = true;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!inRoom) {
|
|
237
|
+
fillTile(ctx, c, r, (r + c) % 2 === 0 ? '#3a3040' : '#332a3a');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Draw rooms
|
|
244
|
+
for (const [name, room] of Object.entries(ROOMS)) {
|
|
245
|
+
drawRoom(ctx, room, name);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const getAgentDeskPosition = (agentName) => {
|
|
250
|
+
for (const [roomName, desks] of Object.entries(DESK_POSITIONS)) {
|
|
251
|
+
for (const d of desks) {
|
|
252
|
+
if (d.agent === agentName) {
|
|
253
|
+
const room = ROOMS[roomName];
|
|
254
|
+
return {
|
|
255
|
+
x: (room.x + d.x) * TILE_SIZE + 10,
|
|
256
|
+
y: (room.y + d.y) * TILE_SIZE + 20,
|
|
257
|
+
room: roomName,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
window.OfficeCanvas = {
|
|
266
|
+
TILE_SIZE,
|
|
267
|
+
CANVAS_W,
|
|
268
|
+
CANVAS_H,
|
|
269
|
+
COLORS,
|
|
270
|
+
ROOMS,
|
|
271
|
+
DESK_POSITIONS,
|
|
272
|
+
drawOffice,
|
|
273
|
+
getAgentDeskPosition,
|
|
274
|
+
};
|
|
275
|
+
})();
|
|
@@ -0,0 +1,113 @@
|
|
|
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.0">
|
|
6
|
+
<title>ERNE Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { background: #0a0a1a; display: flex; justify-content: center; align-items: center; min-height: 100vh; overflow: hidden; }
|
|
10
|
+
canvas { image-rendering: pixelated; image-rendering: crisp-edges; }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<canvas id="dashboard"></canvas>
|
|
15
|
+
<script src="canvas.js"></script>
|
|
16
|
+
<script src="agents.js"></script>
|
|
17
|
+
<script src="sidebar.js"></script>
|
|
18
|
+
<script src="ws-client.js"></script>
|
|
19
|
+
<script>
|
|
20
|
+
(function () {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
var CANVAS_W = OfficeCanvas.CANVAS_W;
|
|
24
|
+
var CANVAS_H = OfficeCanvas.CANVAS_H;
|
|
25
|
+
var SIDEBAR_W = Sidebar.SIDEBAR_W;
|
|
26
|
+
var totalW = CANVAS_W + SIDEBAR_W;
|
|
27
|
+
var totalH = CANVAS_H;
|
|
28
|
+
|
|
29
|
+
var canvas = document.getElementById('dashboard');
|
|
30
|
+
var ctx = canvas.getContext('2d');
|
|
31
|
+
|
|
32
|
+
// Scale to fit viewport
|
|
33
|
+
var scaleX = window.innerWidth / totalW;
|
|
34
|
+
var scaleY = window.innerHeight / totalH;
|
|
35
|
+
var scale = Math.min(scaleX, scaleY, 2);
|
|
36
|
+
|
|
37
|
+
canvas.width = totalW;
|
|
38
|
+
canvas.height = totalH;
|
|
39
|
+
canvas.style.width = Math.floor(totalW * scale) + 'px';
|
|
40
|
+
canvas.style.height = Math.floor(totalH * scale) + 'px';
|
|
41
|
+
|
|
42
|
+
// Handle resize
|
|
43
|
+
window.addEventListener('resize', function () {
|
|
44
|
+
var sx = window.innerWidth / totalW;
|
|
45
|
+
var sy = window.innerHeight / totalH;
|
|
46
|
+
var s = Math.min(sx, sy, 2);
|
|
47
|
+
canvas.style.width = Math.floor(totalW * s) + 'px';
|
|
48
|
+
canvas.style.height = Math.floor(totalH * s) + 'px';
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Init sprites
|
|
52
|
+
AgentSprites.initAgentSprites();
|
|
53
|
+
|
|
54
|
+
// Agent server state
|
|
55
|
+
var agentServerState = {};
|
|
56
|
+
var wsConnected = false;
|
|
57
|
+
|
|
58
|
+
// WebSocket connection
|
|
59
|
+
WSClient.createWSClient(
|
|
60
|
+
function onStateUpdate(agents) {
|
|
61
|
+
agentServerState = agents;
|
|
62
|
+
// Sync sprite states with server
|
|
63
|
+
for (var name in agents) {
|
|
64
|
+
if (agents.hasOwnProperty(name)) {
|
|
65
|
+
var status = agents[name].status || 'idle';
|
|
66
|
+
if (AgentSprites.agentSprites[name] &&
|
|
67
|
+
AgentSprites.agentSprites[name].status !== status) {
|
|
68
|
+
AgentSprites.updateAgentState(name, status);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
function onConnectionChange(connected) {
|
|
74
|
+
wsConnected = connected;
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Main render loop
|
|
79
|
+
var lastTime = performance.now();
|
|
80
|
+
|
|
81
|
+
var render = function (now) {
|
|
82
|
+
var dt = (now - lastTime) / 1000;
|
|
83
|
+
lastTime = now;
|
|
84
|
+
|
|
85
|
+
// Cap dt to avoid jumps
|
|
86
|
+
if (dt > 0.1) dt = 0.1;
|
|
87
|
+
|
|
88
|
+
// Update
|
|
89
|
+
AgentSprites.updateAgentSprites(dt);
|
|
90
|
+
|
|
91
|
+
// Clear
|
|
92
|
+
ctx.clearRect(0, 0, totalW, totalH);
|
|
93
|
+
|
|
94
|
+
// Draw office
|
|
95
|
+
OfficeCanvas.drawOffice(ctx);
|
|
96
|
+
|
|
97
|
+
// Draw agents
|
|
98
|
+
AgentSprites.drawAgentSprites(ctx);
|
|
99
|
+
|
|
100
|
+
// Draw sidebar
|
|
101
|
+
Sidebar.drawSidebar(ctx, agentServerState, CANVAS_W, CANVAS_H);
|
|
102
|
+
|
|
103
|
+
// Draw connection indicator
|
|
104
|
+
Sidebar.drawConnectionIndicator(ctx, CANVAS_W, CANVAS_H, wsConnected);
|
|
105
|
+
|
|
106
|
+
requestAnimationFrame(render);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
requestAnimationFrame(render);
|
|
110
|
+
})();
|
|
111
|
+
</script>
|
|
112
|
+
</body>
|
|
113
|
+
</html>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ERNE Dashboard — Sidebar status panel
|
|
3
|
+
*/
|
|
4
|
+
(function () {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const SIDEBAR_W = 220;
|
|
8
|
+
const SIDEBAR_PAD = 12;
|
|
9
|
+
const ROW_H = 52;
|
|
10
|
+
|
|
11
|
+
const STATUS_COLORS = {
|
|
12
|
+
idle: '#9E9E9E',
|
|
13
|
+
working: '#4CAF50',
|
|
14
|
+
moving: '#FF9800',
|
|
15
|
+
done: '#2196F3',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const STATUS_LABELS = {
|
|
19
|
+
idle: 'IDLE',
|
|
20
|
+
working: 'WORKING',
|
|
21
|
+
moving: 'MOVING',
|
|
22
|
+
done: 'DONE',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const drawSidebar = (ctx, agents, canvasW, canvasH) => {
|
|
26
|
+
const sx = canvasW;
|
|
27
|
+
|
|
28
|
+
// Background
|
|
29
|
+
ctx.fillStyle = '#12121e';
|
|
30
|
+
ctx.fillRect(sx, 0, SIDEBAR_W, canvasH);
|
|
31
|
+
|
|
32
|
+
// Header
|
|
33
|
+
ctx.fillStyle = '#1a1a2e';
|
|
34
|
+
ctx.fillRect(sx, 0, SIDEBAR_W, 36);
|
|
35
|
+
ctx.fillStyle = '#E0E0E0';
|
|
36
|
+
ctx.font = 'bold 14px monospace';
|
|
37
|
+
ctx.textAlign = 'center';
|
|
38
|
+
ctx.fillText('AGENTS', sx + SIDEBAR_W / 2, 24);
|
|
39
|
+
|
|
40
|
+
// Separator
|
|
41
|
+
ctx.fillStyle = '#4A3F5C';
|
|
42
|
+
ctx.fillRect(sx + SIDEBAR_PAD, 36, SIDEBAR_W - SIDEBAR_PAD * 2, 1);
|
|
43
|
+
|
|
44
|
+
// Agent list
|
|
45
|
+
const agentNames = Object.keys(agents);
|
|
46
|
+
let y = 40;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < agentNames.length; i++) {
|
|
49
|
+
const name = agentNames[i];
|
|
50
|
+
const agent = agents[name];
|
|
51
|
+
const status = agent.status || 'idle';
|
|
52
|
+
const task = agent.task || '';
|
|
53
|
+
|
|
54
|
+
// Alternating row background
|
|
55
|
+
ctx.fillStyle = i % 2 === 0 ? '#16162a' : '#1a1a30';
|
|
56
|
+
ctx.fillRect(sx, y, SIDEBAR_W, ROW_H);
|
|
57
|
+
|
|
58
|
+
// Status dot
|
|
59
|
+
ctx.fillStyle = STATUS_COLORS[status] || STATUS_COLORS.idle;
|
|
60
|
+
ctx.beginPath();
|
|
61
|
+
ctx.arc(sx + SIDEBAR_PAD + 6, y + 16, 5, 0, Math.PI * 2);
|
|
62
|
+
ctx.fill();
|
|
63
|
+
|
|
64
|
+
// Agent name
|
|
65
|
+
ctx.fillStyle = '#E0E0E0';
|
|
66
|
+
ctx.font = 'bold 11px monospace';
|
|
67
|
+
ctx.textAlign = 'left';
|
|
68
|
+
ctx.fillText(name, sx + SIDEBAR_PAD + 18, y + 18);
|
|
69
|
+
|
|
70
|
+
// Status label
|
|
71
|
+
ctx.fillStyle = STATUS_COLORS[status] || STATUS_COLORS.idle;
|
|
72
|
+
ctx.font = '9px monospace';
|
|
73
|
+
ctx.fillText(STATUS_LABELS[status] || 'IDLE', sx + SIDEBAR_PAD + 18, y + 30);
|
|
74
|
+
|
|
75
|
+
// Task text (truncated)
|
|
76
|
+
if (task) {
|
|
77
|
+
ctx.fillStyle = '#888';
|
|
78
|
+
ctx.font = '8px monospace';
|
|
79
|
+
const truncated = task.length > 24 ? task.substring(0, 23) + '\u2026' : task;
|
|
80
|
+
ctx.fillText(truncated, sx + SIDEBAR_PAD + 18, y + 42);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
y += ROW_H;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const drawConnectionIndicator = (ctx, canvasW, canvasH, connected) => {
|
|
88
|
+
const sx = canvasW + SIDEBAR_W - SIDEBAR_PAD - 10;
|
|
89
|
+
const sy = canvasH - SIDEBAR_PAD - 5;
|
|
90
|
+
|
|
91
|
+
ctx.fillStyle = connected ? '#4CAF50' : '#f44336';
|
|
92
|
+
ctx.beginPath();
|
|
93
|
+
ctx.arc(sx, sy, 5, 0, Math.PI * 2);
|
|
94
|
+
ctx.fill();
|
|
95
|
+
|
|
96
|
+
ctx.fillStyle = '#888';
|
|
97
|
+
ctx.font = '8px monospace';
|
|
98
|
+
ctx.textAlign = 'right';
|
|
99
|
+
ctx.fillText(connected ? 'LIVE' : 'OFFLINE', sx - 10, sy + 3);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
window.Sidebar = {
|
|
103
|
+
SIDEBAR_W,
|
|
104
|
+
drawSidebar,
|
|
105
|
+
drawConnectionIndicator,
|
|
106
|
+
};
|
|
107
|
+
})();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ERNE Dashboard — WebSocket client with exponential backoff reconnect
|
|
3
|
+
*/
|
|
4
|
+
(function () {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const createWSClient = (onStateUpdate, onConnectionChange) => {
|
|
8
|
+
let ws = null;
|
|
9
|
+
let connected = false;
|
|
10
|
+
let reconnectDelay = 1000;
|
|
11
|
+
|
|
12
|
+
const connect = () => {
|
|
13
|
+
try {
|
|
14
|
+
ws = new WebSocket('ws://' + location.host);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
scheduleReconnect();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ws.onopen = () => {
|
|
21
|
+
connected = true;
|
|
22
|
+
reconnectDelay = 1000;
|
|
23
|
+
if (onConnectionChange) onConnectionChange(true);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
ws.onmessage = (event) => {
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(event.data);
|
|
29
|
+
// Handle both wrapped {type:'state', agents:{...}} and raw agent state
|
|
30
|
+
if (data.type === 'state' && data.agents) {
|
|
31
|
+
onStateUpdate(data.agents);
|
|
32
|
+
} else if (!data.type) {
|
|
33
|
+
// Raw agent state object from server
|
|
34
|
+
onStateUpdate(data);
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// Ignore malformed messages
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
ws.onclose = () => {
|
|
42
|
+
connected = false;
|
|
43
|
+
if (onConnectionChange) onConnectionChange(false);
|
|
44
|
+
scheduleReconnect();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
ws.onerror = () => {
|
|
48
|
+
if (ws) ws.close();
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const scheduleReconnect = () => {
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
connect();
|
|
55
|
+
}, reconnectDelay);
|
|
56
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
connect();
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
isConnected: () => connected,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
window.WSClient = {
|
|
67
|
+
createWSClient,
|
|
68
|
+
};
|
|
69
|
+
})();
|