@myop/cli 0.1.4 → 0.1.6
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 +335 -0
- package/dist/commands/dev/management-website/app.js +790 -0
- package/dist/commands/dev/management-website/index.js +98 -0
- package/dist/commands/dev/management-website/styles.css +346 -0
- package/dist/myop-cli.js +780 -0
- package/package.json +3 -4
- package/src/myop-cli.js +0 -157
|
@@ -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();
|