@myop/cli 0.1.3 → 0.1.5

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,790 @@
1
+ const SVG_NS = 'http://www.w3.org/2000/svg';
2
+
3
+ // Initialize data
4
+ let components = []; // Array of {id, path, name}
5
+ let origins = [];
6
+ let totalRequests = 0;
7
+ let localRequests = 0;
8
+ let proxiedRequests = 0;
9
+ let requestLog = [];
10
+ let componentLines = new Map(); // Map of componentId -> {origin, servedLocally, count}
11
+
12
+ // Drag state
13
+ let draggedElement = null;
14
+ let dragOffset = { x: 0, y: 0 };
15
+ let nodePositions = new Map(); // Store custom positions for nodes
16
+ let expandedLabels = new Set(); // Track which component labels are expanded
17
+
18
+ // Zoom and pan state
19
+ let zoomLevel = 1;
20
+ let panX = 0;
21
+ let panY = 0;
22
+ let isPanning = false;
23
+ let panStart = { x: 0, y: 0 };
24
+
25
+ // Connect to SSE
26
+ const eventSource = new EventSource('/events');
27
+
28
+ eventSource.onmessage = (event) => {
29
+ const data = JSON.parse(event.data);
30
+
31
+ if (data.type === 'components') {
32
+ components = data.components;
33
+ updateComponentsList();
34
+ renderArchitecture();
35
+ } else if (data.type === 'origins') {
36
+ origins = data.origins;
37
+ renderArchitecture();
38
+ } else if (data.type === 'requestLog') {
39
+ requestLog = data.log;
40
+ updateActivityLog();
41
+ updateStats();
42
+ } else if (data.type === 'request') {
43
+ handleNewRequest(data);
44
+ }
45
+ };
46
+
47
+ eventSource.onerror = () => {
48
+ console.error('SSE connection lost, will retry...');
49
+ };
50
+
51
+ // Drag functions
52
+ function startDrag(evt, nodeId) {
53
+ draggedElement = nodeId;
54
+ const svg = document.getElementById('architecture-svg');
55
+ const pt = svg.createSVGPoint();
56
+ pt.x = evt.clientX;
57
+ pt.y = evt.clientY;
58
+ const svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
59
+
60
+ const currentPos = nodePositions.get(nodeId) || getDefaultPosition(nodeId);
61
+ dragOffset.x = svgP.x - currentPos.x;
62
+ dragOffset.y = svgP.y - currentPos.y;
63
+
64
+ evt.preventDefault();
65
+ }
66
+
67
+ function drag(evt) {
68
+ if (!draggedElement) return;
69
+
70
+ const svg = document.getElementById('architecture-svg');
71
+ const pt = svg.createSVGPoint();
72
+ pt.x = evt.clientX;
73
+ pt.y = evt.clientY;
74
+ const svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
75
+
76
+ const newX = svgP.x - dragOffset.x;
77
+ const newY = svgP.y - dragOffset.y;
78
+
79
+ nodePositions.set(draggedElement, { x: newX, y: newY });
80
+
81
+ // Update node position
82
+ const node = document.querySelector(`[data-node-id="${draggedElement}"]`);
83
+ if (node) {
84
+ node.setAttribute('transform', `translate(${newX}, ${newY})`);
85
+ }
86
+
87
+ // Redraw lines
88
+ redrawRequestLines();
89
+
90
+ evt.preventDefault();
91
+ }
92
+
93
+ function endDrag(evt) {
94
+ draggedElement = null;
95
+ evt.preventDefault();
96
+ }
97
+
98
+ function getDefaultPosition(nodeId) {
99
+ // Return default positions based on nodeId
100
+ const svgHeight = parseInt(document.getElementById('architecture-svg').style.height) || 800;
101
+ const originCount = origins.length;
102
+ const verticalSpacing = 150;
103
+ const originX = 100;
104
+ const localServerX = 100 + 350;
105
+ const remoteServerX = 100 + 700;
106
+ const centerY = svgHeight / 2;
107
+
108
+ if (nodeId === 'local-server') {
109
+ return { x: localServerX, y: centerY };
110
+ } else if (nodeId === 'remote-server') {
111
+ return { x: remoteServerX, y: centerY };
112
+ } else if (nodeId.startsWith('origin-')) {
113
+ const index = parseInt(nodeId.replace('origin-', ''));
114
+ return { x: originX, y: 100 + (index * verticalSpacing) };
115
+ }
116
+ return { x: 0, y: 0 };
117
+ }
118
+
119
+ // Add global mouse event listeners
120
+ document.addEventListener('mousemove', drag);
121
+ document.addEventListener('mouseup', endDrag);
122
+
123
+ // Zoom and pan functions
124
+ function updateViewBox() {
125
+ const svg = document.getElementById('architecture-svg');
126
+ const svgHeight = parseInt(svg.style.height) || 800;
127
+ const svgWidth = 1400;
128
+
129
+ const viewBoxWidth = svgWidth / zoomLevel;
130
+ const viewBoxHeight = svgHeight / zoomLevel;
131
+ const viewBoxX = panX;
132
+ const viewBoxY = panY;
133
+
134
+ svg.setAttribute('viewBox', `${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}`);
135
+
136
+ // Update zoom level display
137
+ document.getElementById('zoom-level').textContent = `${Math.round(zoomLevel * 100)}%`;
138
+ }
139
+
140
+ function zoomIn() {
141
+ zoomLevel = Math.min(zoomLevel * 1.2, 5);
142
+ updateViewBox();
143
+ }
144
+
145
+ function zoomOut() {
146
+ zoomLevel = Math.max(zoomLevel / 1.2, 0.5);
147
+ updateViewBox();
148
+ }
149
+
150
+ function resetZoom() {
151
+ zoomLevel = 1;
152
+ panX = 0;
153
+ panY = 0;
154
+ updateViewBox();
155
+ }
156
+
157
+ function handleWheel(e) {
158
+ e.preventDefault();
159
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
160
+ zoomLevel = Math.max(0.5, Math.min(5, zoomLevel * delta));
161
+ updateViewBox();
162
+ }
163
+
164
+ function startPan(e) {
165
+ if (draggedElement) return; // Don't pan if dragging a node
166
+ isPanning = true;
167
+ const svg = document.getElementById('architecture-svg');
168
+ svg.classList.add('panning');
169
+ panStart.x = e.clientX + panX;
170
+ panStart.y = e.clientY + panY;
171
+ }
172
+
173
+ function pan(e) {
174
+ if (!isPanning) return;
175
+ panX = panStart.x - e.clientX;
176
+ panY = panStart.y - e.clientY;
177
+ updateViewBox();
178
+ }
179
+
180
+ function endPan() {
181
+ if (isPanning) {
182
+ isPanning = false;
183
+ const svg = document.getElementById('architecture-svg');
184
+ svg.classList.remove('panning');
185
+ }
186
+ }
187
+
188
+ // Setup zoom controls
189
+ document.getElementById('zoom-in').addEventListener('click', zoomIn);
190
+ document.getElementById('zoom-out').addEventListener('click', zoomOut);
191
+ document.getElementById('zoom-reset').addEventListener('click', resetZoom);
192
+
193
+ // Setup wheel zoom
194
+ const svg = document.getElementById('architecture-svg');
195
+ svg.addEventListener('wheel', handleWheel, { passive: false });
196
+
197
+ // Setup pan on SVG (only when not dragging nodes)
198
+ svg.addEventListener('mousedown', (e) => {
199
+ if (e.target === svg || e.target.closest('.request-line') || e.target.closest('.connection-line')) {
200
+ startPan(e);
201
+ }
202
+ });
203
+ document.addEventListener('mousemove', pan);
204
+ document.addEventListener('mouseup', endPan);
205
+
206
+ function renderArchitecture() {
207
+ const svg = document.getElementById('architecture-svg');
208
+ svg.innerHTML = '';
209
+
210
+ const originCount = Math.max(1, origins.length);
211
+
212
+ // Professional layout with more space
213
+ const nodeWidth = 200;
214
+ const nodeHeight = 80;
215
+ const horizontalSpacing = 350;
216
+ const verticalSpacing = 150;
217
+
218
+ // Calculate canvas size based on content
219
+ const canvasWidth = 1400;
220
+ const svgHeight = Math.max(800, originCount * verticalSpacing + 200);
221
+
222
+ svg.setAttribute('viewBox', `0 0 ${canvasWidth} ${svgHeight}`);
223
+ svg.setAttribute('width', canvasWidth);
224
+ svg.setAttribute('height', svgHeight);
225
+ svg.style.height = svgHeight + 'px';
226
+ svg.style.width = '100%';
227
+
228
+ // Position nodes in clear columns
229
+ const originX = 100;
230
+ const localServerX = 100 + horizontalSpacing;
231
+ const remoteServerX = 100 + horizontalSpacing * 2;
232
+ const centerY = svgHeight / 2;
233
+
234
+ // Draw connection lines to local server
235
+ const linesGroup = document.createElementNS(SVG_NS, 'g');
236
+ linesGroup.setAttribute('id', 'connection-lines');
237
+ svg.appendChild(linesGroup);
238
+
239
+ // Draw grid lines for visual reference
240
+ const gridGroup = document.createElementNS(SVG_NS, 'g');
241
+ gridGroup.setAttribute('opacity', '0.1');
242
+ for (let i = 0; i < canvasWidth; i += 100) {
243
+ const line = document.createElementNS(SVG_NS, 'line');
244
+ line.setAttribute('x1', i);
245
+ line.setAttribute('y1', 0);
246
+ line.setAttribute('x2', i);
247
+ line.setAttribute('y2', svgHeight);
248
+ line.setAttribute('stroke', '#858585');
249
+ line.setAttribute('stroke-width', '1');
250
+ gridGroup.appendChild(line);
251
+ }
252
+ svg.appendChild(gridGroup);
253
+
254
+ // Draw origin nodes (left column)
255
+ origins.forEach((origin, index) => {
256
+ const nodeId = `origin-${index}`;
257
+ const defaultY = 100 + (index * verticalSpacing);
258
+ const pos = nodePositions.get(nodeId) || { x: originX, y: defaultY };
259
+
260
+ // Origin node group
261
+ const g = document.createElementNS(SVG_NS, 'g');
262
+ g.setAttribute('class', 'node draggable');
263
+ g.setAttribute('transform', `translate(${pos.x}, ${pos.y})`);
264
+ g.setAttribute('data-origin', origin.url);
265
+ g.setAttribute('data-node-id', nodeId);
266
+ g.style.cursor = 'move';
267
+ g.onmousedown = (e) => startDrag(e, nodeId);
268
+
269
+ // Node background with shadow effect
270
+ const shadow = document.createElementNS(SVG_NS, 'rect');
271
+ shadow.setAttribute('width', nodeWidth);
272
+ shadow.setAttribute('height', nodeHeight);
273
+ shadow.setAttribute('rx', '8');
274
+ shadow.setAttribute('fill', '#000000');
275
+ shadow.setAttribute('opacity', '0.3');
276
+ shadow.setAttribute('transform', 'translate(4, 4)');
277
+ g.appendChild(shadow);
278
+
279
+ const rect = document.createElementNS(SVG_NS, 'rect');
280
+ rect.setAttribute('class', 'node-rect');
281
+ rect.setAttribute('width', nodeWidth);
282
+ rect.setAttribute('height', nodeHeight);
283
+ rect.setAttribute('rx', '8');
284
+ rect.setAttribute('fill', '#1e1e1e');
285
+ rect.setAttribute('stroke', '#007acc');
286
+ rect.setAttribute('stroke-width', '3');
287
+ g.appendChild(rect);
288
+
289
+ // Header section
290
+ const headerRect = document.createElementNS(SVG_NS, 'rect');
291
+ headerRect.setAttribute('width', nodeWidth);
292
+ headerRect.setAttribute('height', '30');
293
+ headerRect.setAttribute('rx', '8');
294
+ headerRect.setAttribute('fill', '#007acc');
295
+ headerRect.setAttribute('opacity', '0.2');
296
+ g.appendChild(headerRect);
297
+
298
+ // Title
299
+ const text1 = document.createElementNS(SVG_NS, 'text');
300
+ text1.setAttribute('x', '12');
301
+ text1.setAttribute('y', '20');
302
+ text1.setAttribute('fill', '#4fc3f7');
303
+ text1.setAttribute('font-size', '11');
304
+ text1.setAttribute('font-weight', 'bold');
305
+ text1.textContent = 'REQUEST ORIGIN';
306
+ text1.style.pointerEvents = 'none';
307
+ g.appendChild(text1);
308
+
309
+ // Origin label
310
+ const text2 = document.createElementNS(SVG_NS, 'text');
311
+ text2.setAttribute('x', '12');
312
+ text2.setAttribute('y', '48');
313
+ text2.setAttribute('fill', '#cccccc');
314
+ text2.setAttribute('font-size', '13');
315
+ text2.setAttribute('font-weight', '600');
316
+ text2.textContent = truncateText(origin.label, 25);
317
+ text2.style.pointerEvents = 'none';
318
+ g.appendChild(text2);
319
+
320
+ // Request count
321
+ const text3 = document.createElementNS(SVG_NS, 'text');
322
+ text3.setAttribute('x', '12');
323
+ text3.setAttribute('y', '66');
324
+ text3.setAttribute('fill', '#858585');
325
+ text3.setAttribute('font-size', '10');
326
+ text3.textContent = `${origin.requestCount} total requests`;
327
+ text3.style.pointerEvents = 'none';
328
+ g.appendChild(text3);
329
+
330
+ // Status indicator
331
+ const statusCircle = document.createElementNS(SVG_NS, 'circle');
332
+ statusCircle.setAttribute('cx', nodeWidth - 12);
333
+ statusCircle.setAttribute('cy', '20');
334
+ statusCircle.setAttribute('r', '5');
335
+ statusCircle.setAttribute('fill', '#4ec9b0');
336
+ statusCircle.style.pointerEvents = 'none';
337
+ g.appendChild(statusCircle);
338
+
339
+ svg.appendChild(g);
340
+ });
341
+
342
+ // Local Server (center column)
343
+ const localPos = nodePositions.get('local-server') || { x: localServerX, y: centerY };
344
+
345
+ const localG = document.createElementNS(SVG_NS, 'g');
346
+ localG.setAttribute('class', 'node draggable');
347
+ localG.setAttribute('transform', `translate(${localPos.x}, ${localPos.y})`);
348
+ localG.setAttribute('data-node-id', 'local-server');
349
+ localG.style.cursor = 'move';
350
+ localG.onmousedown = (e) => startDrag(e, 'local-server');
351
+
352
+ // Shadow
353
+ const localShadow = document.createElementNS(SVG_NS, 'rect');
354
+ localShadow.setAttribute('width', nodeWidth);
355
+ localShadow.setAttribute('height', nodeHeight);
356
+ localShadow.setAttribute('rx', '8');
357
+ localShadow.setAttribute('fill', '#000000');
358
+ localShadow.setAttribute('opacity', '0.3');
359
+ localShadow.setAttribute('transform', 'translate(4, 4)');
360
+ localG.appendChild(localShadow);
361
+
362
+ const localRect = document.createElementNS(SVG_NS, 'rect');
363
+ localRect.setAttribute('class', 'node-rect');
364
+ localRect.setAttribute('width', nodeWidth);
365
+ localRect.setAttribute('height', nodeHeight);
366
+ localRect.setAttribute('rx', '8');
367
+ localRect.setAttribute('fill', '#1e1e1e');
368
+ localRect.setAttribute('stroke', '#4ec9b0');
369
+ localRect.setAttribute('stroke-width', '3');
370
+ localG.appendChild(localRect);
371
+
372
+ // Header
373
+ const localHeader = document.createElementNS(SVG_NS, 'rect');
374
+ localHeader.setAttribute('width', nodeWidth);
375
+ localHeader.setAttribute('height', '30');
376
+ localHeader.setAttribute('rx', '8');
377
+ localHeader.setAttribute('fill', '#4ec9b0');
378
+ localHeader.setAttribute('opacity', '0.2');
379
+ localG.appendChild(localHeader);
380
+
381
+ const localTitle = document.createElementNS(SVG_NS, 'text');
382
+ localTitle.setAttribute('x', '12');
383
+ localTitle.setAttribute('y', '20');
384
+ localTitle.setAttribute('fill', '#4ec9b0');
385
+ localTitle.setAttribute('font-size', '11');
386
+ localTitle.setAttribute('font-weight', 'bold');
387
+ localTitle.textContent = 'LOCAL DEV SERVER';
388
+ localTitle.style.pointerEvents = 'none';
389
+ localG.appendChild(localTitle);
390
+
391
+ const localLabel = document.createElementNS(SVG_NS, 'text');
392
+ localLabel.setAttribute('x', '12');
393
+ localLabel.setAttribute('y', '48');
394
+ localLabel.setAttribute('fill', '#cccccc');
395
+ localLabel.setAttribute('font-size', '13');
396
+ localLabel.setAttribute('font-weight', '600');
397
+ localLabel.textContent = `localhost:${window.PORT}`;
398
+ localLabel.style.pointerEvents = 'none';
399
+ localG.appendChild(localLabel);
400
+
401
+ const localCount = document.createElementNS(SVG_NS, 'text');
402
+ localCount.setAttribute('x', '12');
403
+ localCount.setAttribute('y', '66');
404
+ localCount.setAttribute('fill', '#858585');
405
+ localCount.setAttribute('font-size', '10');
406
+ localCount.textContent = `${components.length} components loaded`;
407
+ localCount.style.pointerEvents = 'none';
408
+ localG.appendChild(localCount);
409
+
410
+ const localStatus = document.createElementNS(SVG_NS, 'circle');
411
+ localStatus.setAttribute('cx', nodeWidth - 12);
412
+ localStatus.setAttribute('cy', '20');
413
+ localStatus.setAttribute('r', '5');
414
+ localStatus.setAttribute('fill', '#4ec9b0');
415
+ localStatus.style.pointerEvents = 'none';
416
+ localG.appendChild(localStatus);
417
+
418
+ svg.appendChild(localG);
419
+
420
+ // Remote Server (right column)
421
+ const remotePos = nodePositions.get('remote-server') || { x: remoteServerX, y: centerY };
422
+ const remoteG = document.createElementNS(SVG_NS, 'g');
423
+ remoteG.setAttribute('class', 'node draggable');
424
+ remoteG.setAttribute('transform', `translate(${remotePos.x}, ${remotePos.y})`);
425
+ remoteG.setAttribute('data-node-id', 'remote-server');
426
+ remoteG.style.cursor = 'move';
427
+ remoteG.onmousedown = (e) => startDrag(e, 'remote-server');
428
+
429
+ // Shadow
430
+ const remoteShadow = document.createElementNS(SVG_NS, 'rect');
431
+ remoteShadow.setAttribute('width', nodeWidth);
432
+ remoteShadow.setAttribute('height', nodeHeight);
433
+ remoteShadow.setAttribute('rx', '8');
434
+ remoteShadow.setAttribute('fill', '#000000');
435
+ remoteShadow.setAttribute('opacity', '0.3');
436
+ remoteShadow.setAttribute('transform', 'translate(4, 4)');
437
+ remoteG.appendChild(remoteShadow);
438
+
439
+ const remoteRect = document.createElementNS(SVG_NS, 'rect');
440
+ remoteRect.setAttribute('class', 'node-rect');
441
+ remoteRect.setAttribute('width', nodeWidth);
442
+ remoteRect.setAttribute('height', nodeHeight);
443
+ remoteRect.setAttribute('rx', '8');
444
+ remoteRect.setAttribute('fill', '#1e1e1e');
445
+ remoteRect.setAttribute('stroke', '#dcdcaa');
446
+ remoteRect.setAttribute('stroke-width', '3');
447
+ remoteG.appendChild(remoteRect);
448
+
449
+ // Header
450
+ const remoteHeader = document.createElementNS(SVG_NS, 'rect');
451
+ remoteHeader.setAttribute('width', nodeWidth);
452
+ remoteHeader.setAttribute('height', '30');
453
+ remoteHeader.setAttribute('rx', '8');
454
+ remoteHeader.setAttribute('fill', '#dcdcaa');
455
+ remoteHeader.setAttribute('opacity', '0.2');
456
+ remoteG.appendChild(remoteHeader);
457
+
458
+ const remoteTitle = document.createElementNS(SVG_NS, 'text');
459
+ remoteTitle.setAttribute('x', '12');
460
+ remoteTitle.setAttribute('y', '20');
461
+ remoteTitle.setAttribute('fill', '#dcdcaa');
462
+ remoteTitle.setAttribute('font-size', '11');
463
+ remoteTitle.setAttribute('font-weight', 'bold');
464
+ remoteTitle.textContent = 'CLOUD SERVER';
465
+ remoteTitle.style.pointerEvents = 'none';
466
+ remoteG.appendChild(remoteTitle);
467
+
468
+ const remoteLabel = document.createElementNS(SVG_NS, 'text');
469
+ remoteLabel.setAttribute('x', '12');
470
+ remoteLabel.setAttribute('y', '48');
471
+ remoteLabel.setAttribute('fill', '#cccccc');
472
+ remoteLabel.setAttribute('font-size', '13');
473
+ remoteLabel.setAttribute('font-weight', '600');
474
+ remoteLabel.textContent = 'cloud.myop.dev';
475
+ remoteLabel.style.pointerEvents = 'none';
476
+ remoteG.appendChild(remoteLabel);
477
+
478
+ const remoteInfo = document.createElementNS(SVG_NS, 'text');
479
+ remoteInfo.setAttribute('x', '12');
480
+ remoteInfo.setAttribute('y', '66');
481
+ remoteInfo.setAttribute('fill', '#858585');
482
+ remoteInfo.setAttribute('font-size', '10');
483
+ remoteInfo.textContent = 'Production environment';
484
+ remoteInfo.style.pointerEvents = 'none';
485
+ remoteG.appendChild(remoteInfo);
486
+
487
+ const remoteStatus = document.createElementNS(SVG_NS, 'circle');
488
+ remoteStatus.setAttribute('cx', nodeWidth - 12);
489
+ remoteStatus.setAttribute('cy', '20');
490
+ remoteStatus.setAttribute('r', '5');
491
+ remoteStatus.setAttribute('fill', '#dcdcaa');
492
+ remoteStatus.style.pointerEvents = 'none';
493
+ remoteG.appendChild(remoteStatus);
494
+
495
+ svg.appendChild(remoteG);
496
+
497
+ // Redraw all request lines
498
+ redrawRequestLines();
499
+ }
500
+
501
+ function redrawRequestLines() {
502
+ const svg = document.getElementById('architecture-svg');
503
+ const linesGroup = document.getElementById('connection-lines');
504
+ if (!linesGroup) return;
505
+
506
+ // Clear existing request lines and labels
507
+ svg.querySelectorAll('.request-line, .request-label, .connection-line').forEach(el => el.remove());
508
+
509
+ const svgHeight = parseInt(svg.style.height) || 800;
510
+ const centerY = svgHeight / 2;
511
+ const localServerX = 100 + 350;
512
+ const remoteServerX = 100 + 700;
513
+
514
+ // Get actual node positions
515
+ const localServerPos = nodePositions.get('local-server') || { x: localServerX, y: centerY };
516
+ const remoteServerPos = nodePositions.get('remote-server') || { x: remoteServerX, y: centerY };
517
+
518
+ // Draw static connection from local to remote using actual positions
519
+ const lineToRemote = document.createElementNS(SVG_NS, 'path');
520
+ lineToRemote.setAttribute('class', 'connection-line');
521
+ const connStartX = localServerPos.x + 200;
522
+ const connStartY = localServerPos.y + 40;
523
+ const connEndX = remoteServerPos.x;
524
+ const connEndY = remoteServerPos.y + 40;
525
+ const connMidX = (connStartX + connEndX) / 2;
526
+ lineToRemote.setAttribute('d', `M ${connStartX} ${connStartY} C ${connMidX} ${connStartY}, ${connMidX} ${connEndY}, ${connEndX} ${connEndY}`);
527
+ lineToRemote.setAttribute('stroke', '#3e3e42');
528
+ lineToRemote.setAttribute('stroke-width', '1');
529
+ lineToRemote.setAttribute('stroke-dasharray', '5 5');
530
+ lineToRemote.setAttribute('fill', 'none');
531
+ lineToRemote.setAttribute('opacity', '0.5');
532
+ svg.insertBefore(lineToRemote, linesGroup.nextSibling);
533
+
534
+ // Group component lines by origin to calculate proper offsets
535
+ const linesByOrigin = new Map();
536
+ componentLines.forEach((compData, key) => {
537
+ const origin = compData.origin;
538
+ if (!linesByOrigin.has(origin)) {
539
+ linesByOrigin.set(origin, []);
540
+ }
541
+ linesByOrigin.get(origin).push({ key, compData });
542
+ });
543
+
544
+ // Draw lines for each component
545
+ componentLines.forEach((compData, key) => {
546
+ const originIndex = origins.findIndex(o => o.url === compData.origin);
547
+ if (originIndex === -1) return;
548
+
549
+ const nodeId = `origin-${originIndex}`;
550
+ const originPos = nodePositions.get(nodeId) || getDefaultPosition(nodeId);
551
+
552
+ const color = compData.servedLocally ? '#4ec9b0' : '#dcdcaa';
553
+ const componentId = compData.componentId;
554
+
555
+ // Calculate vertical offset for this line within its origin
556
+ const originLines = linesByOrigin.get(compData.origin);
557
+ const lineIndexInOrigin = originLines.findIndex(l => l.key === key);
558
+ const totalLinesForOrigin = originLines.length;
559
+
560
+ // Center the lines vertically around the origin's center point
561
+ const lineSpacing = 25; // Increased from 12 to 25 for better separation
562
+ const totalHeight = (totalLinesForOrigin - 1) * lineSpacing;
563
+ const startOffset = -totalHeight / 2;
564
+ const yOffset = startOffset + (lineIndexInOrigin * lineSpacing);
565
+
566
+ // Line from origin to local server (connect from right edge of origin to left edge of local)
567
+ const line1 = document.createElementNS(SVG_NS, 'path');
568
+ line1.setAttribute('class', 'request-line');
569
+ const startX = originPos.x + 200; // Right edge of origin node
570
+ const startY = originPos.y + 40 + yOffset; // Middle of node + offset
571
+ const endX = localServerPos.x; // Left edge of local server
572
+ const endY = localServerPos.y + 40 + yOffset; // Middle of node + offset
573
+
574
+ // Create curved path for professional look
575
+ const midX = (startX + endX) / 2;
576
+ line1.setAttribute('d', `M ${startX} ${startY} C ${midX} ${startY}, ${midX} ${endY}, ${endX} ${endY}`);
577
+ line1.setAttribute('stroke', color);
578
+ line1.setAttribute('stroke-width', '2.5');
579
+ line1.setAttribute('fill', 'none');
580
+ line1.setAttribute('opacity', '0.8');
581
+ svg.insertBefore(line1, linesGroup.nextSibling);
582
+
583
+ // Component label on the line
584
+ const labelMidX = (startX + endX) / 2;
585
+ const labelMidY = startY;
586
+
587
+ const labelKey = `${key}-1`;
588
+ const isExpanded = expandedLabels.has(labelKey);
589
+ const labelText = isExpanded
590
+ ? (compData.count > 1 ? `${componentId} (${compData.count})` : componentId)
591
+ : (compData.count > 1 ? `${truncateText(componentId, 8)} (${compData.count})` : truncateText(componentId, 10));
592
+ const labelWidth = Math.max(80, labelText.length * 6.5);
593
+
594
+ const labelGroup = document.createElementNS(SVG_NS, 'g');
595
+ labelGroup.setAttribute('class', 'request-label');
596
+ labelGroup.style.cursor = 'pointer';
597
+ labelGroup.onclick = (e) => {
598
+ e.stopPropagation();
599
+ if (expandedLabels.has(labelKey)) {
600
+ expandedLabels.delete(labelKey);
601
+ } else {
602
+ expandedLabels.add(labelKey);
603
+ }
604
+ redrawRequestLines();
605
+ };
606
+
607
+ const labelBg = document.createElementNS(SVG_NS, 'rect');
608
+ labelBg.setAttribute('x', labelMidX - labelWidth / 2);
609
+ labelBg.setAttribute('y', labelMidY - 12);
610
+ labelBg.setAttribute('width', labelWidth);
611
+ labelBg.setAttribute('height', '20');
612
+ labelBg.setAttribute('rx', '3');
613
+ labelBg.setAttribute('fill', '#1e1e1e');
614
+ labelBg.setAttribute('fill-opacity', '0.95');
615
+ labelBg.setAttribute('stroke', color);
616
+ labelBg.setAttribute('stroke-width', '1.5');
617
+ labelGroup.appendChild(labelBg);
618
+
619
+ const label = document.createElementNS(SVG_NS, 'text');
620
+ label.setAttribute('x', labelMidX);
621
+ label.setAttribute('y', labelMidY + 3);
622
+ label.setAttribute('text-anchor', 'middle');
623
+ label.setAttribute('fill', '#cccccc');
624
+ label.setAttribute('font-size', '10');
625
+ label.setAttribute('font-weight', '600');
626
+ label.style.pointerEvents = 'none';
627
+ label.textContent = labelText;
628
+ labelGroup.appendChild(label);
629
+
630
+ svg.appendChild(labelGroup);
631
+
632
+ // If proxied to remote, draw line to remote
633
+ if (!compData.servedLocally) {
634
+ const line2 = document.createElementNS(SVG_NS, 'path');
635
+ line2.setAttribute('class', 'request-line');
636
+
637
+ const start2X = localServerPos.x + 200; // Right edge of local server
638
+ const start2Y = localServerPos.y + 40 + yOffset;
639
+ const end2X = remoteServerPos.x; // Left edge of remote server
640
+ const end2Y = remoteServerPos.y + 40 + yOffset;
641
+
642
+ const mid2X = (start2X + end2X) / 2;
643
+ line2.setAttribute('d', `M ${start2X} ${start2Y} C ${mid2X} ${start2Y}, ${mid2X} ${end2Y}, ${end2X} ${end2Y}`);
644
+ line2.setAttribute('stroke', color);
645
+ line2.setAttribute('stroke-width', '2.5');
646
+ line2.setAttribute('fill', 'none');
647
+ line2.setAttribute('opacity', '0.8');
648
+ svg.insertBefore(line2, linesGroup.nextSibling);
649
+
650
+ // Label on remote line
651
+ const labelMidX2 = (start2X + end2X) / 2;
652
+ const labelMidY2 = start2Y;
653
+
654
+ const labelKey2 = `${key}-2`;
655
+ const isExpanded2 = expandedLabels.has(labelKey2);
656
+ const labelText2 = isExpanded2
657
+ ? (compData.count > 1 ? `${componentId} (${compData.count})` : componentId)
658
+ : (compData.count > 1 ? `${truncateText(componentId, 8)} (${compData.count})` : truncateText(componentId, 10));
659
+ const labelWidth2 = Math.max(80, labelText2.length * 6.5);
660
+
661
+ const labelGroup2 = document.createElementNS(SVG_NS, 'g');
662
+ labelGroup2.setAttribute('class', 'request-label');
663
+ labelGroup2.style.cursor = 'pointer';
664
+ labelGroup2.onclick = (e) => {
665
+ e.stopPropagation();
666
+ if (expandedLabels.has(labelKey2)) {
667
+ expandedLabels.delete(labelKey2);
668
+ } else {
669
+ expandedLabels.add(labelKey2);
670
+ }
671
+ redrawRequestLines();
672
+ };
673
+
674
+ const labelBg2 = document.createElementNS(SVG_NS, 'rect');
675
+ labelBg2.setAttribute('x', labelMidX2 - labelWidth2 / 2);
676
+ labelBg2.setAttribute('y', labelMidY2 - 12);
677
+ labelBg2.setAttribute('width', labelWidth2);
678
+ labelBg2.setAttribute('height', '20');
679
+ labelBg2.setAttribute('rx', '3');
680
+ labelBg2.setAttribute('fill', '#1e1e1e');
681
+ labelBg2.setAttribute('fill-opacity', '0.95');
682
+ labelBg2.setAttribute('stroke', color);
683
+ labelBg2.setAttribute('stroke-width', '1.5');
684
+ labelGroup2.appendChild(labelBg2);
685
+
686
+ const label2 = document.createElementNS(SVG_NS, 'text');
687
+ label2.setAttribute('x', labelMidX2);
688
+ label2.setAttribute('y', labelMidY2 + 3);
689
+ label2.setAttribute('text-anchor', 'middle');
690
+ label2.setAttribute('fill', '#cccccc');
691
+ label2.setAttribute('font-size', '10');
692
+ label2.setAttribute('font-weight', '600');
693
+ label2.style.pointerEvents = 'none';
694
+ label2.textContent = labelText2;
695
+ labelGroup2.appendChild(label2);
696
+
697
+ svg.appendChild(labelGroup2);
698
+ }
699
+ });
700
+ }
701
+
702
+ function truncateText(text, maxLen) {
703
+ return text.length > maxLen ? text.substring(0, maxLen - 3) + '...' : text;
704
+ }
705
+
706
+ function updateComponentsList() {
707
+ const list = document.getElementById('components');
708
+
709
+ if (components.length === 0) {
710
+ list.innerHTML = '<div class="empty-state">No components registered</div>';
711
+ } else {
712
+ list.innerHTML = components.map(comp => {
713
+ const displayName = comp.name ? `<span style="color: #4fc3f7; font-weight: 600;">${comp.name}</span>` : '';
714
+ return `<li class="component-item">
715
+ <a href="/view/${comp.id}/">${comp.id}</a>
716
+ ${displayName ? `<div style="margin-top: 2px;">${displayName}</div>` : ''}
717
+ <div class="component-path">${comp.path}</div>
718
+ </li>`;
719
+ }).join('');
720
+ }
721
+ }
722
+
723
+ function updateActivityLog() {
724
+ const log = document.getElementById('activity-log');
725
+
726
+ if (requestLog.length === 0) {
727
+ log.innerHTML = '<div class="empty-state">No activity logged</div>';
728
+ } else {
729
+ const recentLog = requestLog.slice(-20).reverse();
730
+ log.innerHTML = recentLog.map(entry => {
731
+ const time = new Date(entry.timestamp).toLocaleTimeString();
732
+ const iconClass = entry.servedLocally ? 'log-icon success' : 'log-icon warning';
733
+ const icon = entry.servedLocally ? '●' : '◆';
734
+ const status = entry.servedLocally ? 'LOCAL' : 'PROXY';
735
+
736
+ return `
737
+ <div class="log-entry">
738
+ <div class="${iconClass}">${icon}</div>
739
+ <div class="log-content">
740
+ <div class="log-component">${entry.componentId}</div>
741
+ <div class="log-status">[${status}] from <span class="log-origin">${entry.originLabel || 'Unknown'}</span></div>
742
+ <div class="log-time">${time}</div>
743
+ </div>
744
+ </div>
745
+ `;
746
+ }).join('');
747
+ }
748
+ }
749
+
750
+ function updateStats() {
751
+ totalRequests = requestLog.length;
752
+ localRequests = requestLog.filter(r => r.servedLocally).length;
753
+ proxiedRequests = requestLog.filter(r => !r.servedLocally).length;
754
+
755
+ document.getElementById('total-requests').textContent = totalRequests;
756
+ document.getElementById('local-requests').textContent = localRequests;
757
+ document.getElementById('proxied-requests').textContent = proxiedRequests;
758
+ document.getElementById('origins-count').textContent = origins.length;
759
+ }
760
+
761
+ function handleNewRequest(data) {
762
+ requestLog.push(data);
763
+ updateActivityLog();
764
+ updateStats();
765
+
766
+ // Track component requests
767
+ const key = `${data.origin}|${data.componentId}`;
768
+ if (componentLines.has(key)) {
769
+ // Increment count for existing component
770
+ componentLines.get(key).count++;
771
+ } else {
772
+ // Add new component line
773
+ componentLines.set(key, {
774
+ origin: data.origin,
775
+ componentId: data.componentId,
776
+ servedLocally: data.servedLocally,
777
+ count: 1
778
+ });
779
+ }
780
+
781
+ // Redraw lines
782
+ redrawRequestLines();
783
+ }
784
+
785
+ // Initial render
786
+ renderArchitecture();
787
+ updateComponentsList();
788
+ updateActivityLog();
789
+ updateStats();
790
+ updateViewBox();