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.
@@ -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
+ })();