myop 0.1.42

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