shokupan 0.6.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +55 -2
  2. package/dist/{openapi-analyzer-Bei1sVWp.cjs → analyzer-Bei1sVWp.cjs} +1 -1
  3. package/dist/analyzer-Bei1sVWp.cjs.map +1 -0
  4. package/dist/{openapi-analyzer-Ce_7JxZh.js → analyzer-Ce_7JxZh.js} +1 -1
  5. package/dist/analyzer-Ce_7JxZh.js.map +1 -0
  6. package/dist/cli.cjs +2 -2
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/cli.js.map +1 -1
  10. package/dist/context.d.ts +58 -23
  11. package/dist/{server-adapter-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
  12. package/dist/http-server-BEMPIs33.cjs.map +1 -0
  13. package/dist/{server-adapter-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
  14. package/dist/http-server-CCeagTyU.js.map +1 -0
  15. package/dist/index.cjs +1940 -917
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.ts +18 -17
  18. package/dist/index.js +1948 -925
  19. package/dist/index.js.map +1 -1
  20. package/dist/middleware.d.ts +1 -1
  21. package/dist/plugins/{auth.d.ts → application/auth.d.ts} +72 -3
  22. package/dist/plugins/application/cluster.d.ts +33 -0
  23. package/dist/plugins/{failed-request-recorder.d.ts → application/dashboard/failed-request-recorder.d.ts} +1 -1
  24. package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
  25. package/dist/plugins/application/dashboard/plugin.d.ts +42 -0
  26. package/dist/plugins/application/dashboard/static/charts.js +328 -0
  27. package/dist/plugins/application/dashboard/static/failures.js +85 -0
  28. package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
  29. package/dist/plugins/application/dashboard/static/poll.js +146 -0
  30. package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
  31. package/dist/plugins/application/dashboard/static/registry.css +131 -0
  32. package/dist/plugins/application/dashboard/static/registry.js +269 -0
  33. package/dist/plugins/application/dashboard/static/requests.js +118 -0
  34. package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
  35. package/dist/plugins/application/dashboard/static/styles.css +175 -0
  36. package/dist/plugins/application/dashboard/static/tables.js +92 -0
  37. package/dist/plugins/application/dashboard/static/tabs.js +113 -0
  38. package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
  39. package/dist/plugins/application/dashboard/template.eta +246 -0
  40. package/dist/plugins/{server-adapter.d.ts → application/http-server.d.ts} +1 -1
  41. package/dist/plugins/{idempotency → application/idempotency}/plugin.d.ts +7 -1
  42. package/dist/plugins/{openapi.d.ts → application/openapi/openapi.d.ts} +2 -2
  43. package/dist/plugins/application/scalar.d.ts +36 -0
  44. package/dist/plugins/application/socket-io.d.ts +14 -0
  45. package/dist/plugins/middleware/compression.d.ts +17 -0
  46. package/dist/plugins/middleware/cors.d.ts +34 -0
  47. package/dist/plugins/{express.d.ts → middleware/express.d.ts} +1 -1
  48. package/dist/plugins/{openapi-validator.d.ts → middleware/openapi-validator.d.ts} +2 -2
  49. package/dist/plugins/middleware/proxy.d.ts +37 -0
  50. package/dist/plugins/middleware/rate-limit.d.ts +58 -0
  51. package/dist/plugins/{security-headers.d.ts → middleware/security-headers.d.ts} +51 -1
  52. package/dist/plugins/{serve-static.d.ts → middleware/serve-static.d.ts} +1 -1
  53. package/dist/plugins/{session.d.ts → middleware/session.d.ts} +89 -3
  54. package/dist/plugins/{validation.d.ts → middleware/validation.d.ts} +6 -1
  55. package/dist/router.d.ts +17 -5
  56. package/dist/shokupan.d.ts +31 -5
  57. package/dist/util/async-hooks.d.ts +8 -2
  58. package/dist/util/datastore.d.ts +4 -3
  59. package/dist/{decorators.d.ts → util/decorators.d.ts} +6 -1
  60. package/dist/util/http-error.d.ts +38 -0
  61. package/dist/util/http-status.d.ts +32 -0
  62. package/dist/util/instrumentation.d.ts +1 -1
  63. package/dist/{request.d.ts → util/request.d.ts} +1 -1
  64. package/dist/util/symbol.d.ts +34 -0
  65. package/dist/{router → util}/trie.d.ts +1 -1
  66. package/dist/{types.d.ts → util/types.d.ts} +38 -2
  67. package/package.json +9 -6
  68. package/dist/openapi-analyzer-Bei1sVWp.cjs.map +0 -1
  69. package/dist/openapi-analyzer-Ce_7JxZh.js.map +0 -1
  70. package/dist/plugins/compression.d.ts +0 -5
  71. package/dist/plugins/cors.d.ts +0 -11
  72. package/dist/plugins/debugview/plugin.d.ts +0 -29
  73. package/dist/plugins/proxy.d.ts +0 -11
  74. package/dist/plugins/rate-limit.d.ts +0 -15
  75. package/dist/plugins/scalar.d.ts +0 -15
  76. package/dist/server-adapter-0xH174zz.js.map +0 -1
  77. package/dist/server-adapter-DFhwlK8e.cjs.map +0 -1
  78. package/dist/symbol.d.ts +0 -15
  79. /package/dist/{analysis/openapi-analyzer.d.ts → plugins/application/openapi/analyzer.d.ts} +0 -0
  80. /package/dist/{di.d.ts → util/di.d.ts} +0 -0
  81. /package/dist/{response.d.ts → util/response.d.ts} +0 -0
@@ -0,0 +1,523 @@
1
+ import { Background, Controls, Handle, MarkerType, ReactFlow, useEdgesState, useNodesState } from 'https://esm.sh/@xyflow/react@12.3.6?deps=react@18.3.1,react-dom@18.3.1';
2
+ import ELK from 'https://esm.sh/elkjs@0.9.3/lib/elk.bundled.js';
3
+ import { createRoot } from 'https://esm.sh/react-dom@18.3.1/client?deps=react@18.3.1';
4
+ import React, { useEffect, useState } from 'https://esm.sh/react@18.3.1';
5
+
6
+ const elk = new ELK();
7
+
8
+ const NODE_STYLES = {
9
+ router: { background: '#22c58a10', color: '#22c58a', border: '1px solid #22c58a', borderRadius: '8px' },
10
+ controller: { background: 'rgba(124, 58, 237, 0.1)', color: '#a78bfa', border: '1px solid #0a090aff', borderRadius: '6px' },
11
+ middleware: { background: '#7e22ce', color: '#fff', border: '1px solid #6b21a8', borderRadius: '9px', padding: '6px 12px', fontSize: '10px' },
12
+ entrypoint: { background: '#3b82f6', color: '#fff', border: '1px solid #306cce', borderRadius: '12px', padding: '' }
13
+ };
14
+
15
+
16
+ function renderPath(path) {
17
+ const parts = path.split('/').slice(1);
18
+
19
+ let out = '';
20
+ parts.forEach((part, index) => {
21
+ if (part.startsWith(":")) {
22
+ out += `/<span class="path-segment path-param">${part}</span>`;
23
+ return;
24
+ }
25
+ if (index === parts.length - 1) {
26
+ out += `/<span class="path-segment path-end">${part}</span>`;
27
+ return;
28
+ };
29
+ out += `/<span class="path-segment">${part}</span>`;
30
+ });
31
+
32
+ return out;
33
+ }
34
+
35
+ const GroupNode = ({ data }) => {
36
+ return React.createElement('div', { style: { padding: '10px', height: '100%' } },
37
+ React.createElement('div', { style: { fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '5px', marginBottom: '5px' } },
38
+ data.type === "controller" ? data.label : "Router: " + data.label
39
+ ),
40
+ data.data?.children?.routes?.map((r, i) =>
41
+ React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', margin: '2px 0' } },
42
+ React.createElement('span', {
43
+ style: {
44
+ padding: '2px 6px',
45
+ borderRadius: '4px',
46
+ background: '#0f172a',
47
+ border: `1px solid ${r.method === 'GET' ? '#3b82f6' :
48
+ r.method === 'POST' ? '#22c55e' :
49
+ r.method === 'PUT' ? '#eab308' :
50
+ r.method === 'DELETE' ? '#ef4444' : '#64748b'
51
+ }`,
52
+ color: r.method === 'GET' ? '#3b82f6' :
53
+ r.method === 'POST' ? '#22c55e' :
54
+ r.method === 'PUT' ? '#eab308' :
55
+ r.method === 'DELETE' ? '#ef4444' : '#f8fafc',
56
+ fontWeight: 'bold',
57
+ fontSize: '10px',
58
+ minWidth: '40px',
59
+ textAlign: 'center'
60
+ }
61
+ }, r.method),
62
+ React.createElement('span', {
63
+ style: { fontFamily: 'monospace', color: r.isFailed ? '#ef4444' : '#cbd5e1', fontWeight: r.isFailed ? 'bold' : 'normal' },
64
+ dangerouslySetInnerHTML: { __html: renderPath(r.path) }
65
+ })
66
+ )
67
+ ),
68
+ React.createElement(Handle, { type: 'target', position: 'top' }),
69
+ React.createElement(Handle, { type: 'source', position: 'bottom' })
70
+ );
71
+ };
72
+
73
+ const EntrypointNode = ({ data }) => {
74
+ return React.createElement('div', { style: { padding: '10px', height: '100%' } },
75
+ React.createElement(
76
+ "svg",
77
+ {
78
+ height: "100%",
79
+ width: "100%",
80
+ version: "1.1",
81
+ xmlns: "http://www.w3.org/2000/svg",
82
+ xmlnsXlink: "http://www.w3.org/1999/xlink",
83
+ viewBox: "0 0 512 512",
84
+ xmlSpace: "preserve",
85
+ },
86
+ React.createElement(
87
+ "style",
88
+ { type: "text/css" },
89
+ ".st0{fill:currentColor;}"
90
+ ),
91
+ React.createElement(
92
+ "g",
93
+ null,
94
+ React.createElement("path", {
95
+ className: "st0",
96
+ d: "M255.994,0.006C114.607,0.013,0.012,114.612,0,256c0.012,141.387,114.607,255.986,255.994,255.994 C397.393,511.986,511.992,397.387,512,256C511.992,114.612,397.393,0.013,255.994,0.006z M97.607,97.612 c23.34-23.328,51.761-41.475,83.455-52.725c-15.183,18.375-27.84,41.906-37.757,69.116H82.772 C87.452,108.308,92.396,102.824,97.607,97.612z M65.612,138.003h69.986c-9.008,31.929-14.41,67.834-15.363,105.997H32.327 C34.374,205.196,46.3,169.088,65.612,138.003z M65.612,373.997C46.3,342.912,34.374,306.804,32.327,268h87.991 c0.961,38.124,6.21,74.092,15.206,105.998H65.612z M97.607,414.386c-5.211-5.211-10.156-10.695-14.836-16.39h60.573 c4.28,11.774,9.019,22.944,14.312,33.21c6.954,13.438,14.758,25.468,23.348,35.89C149.332,455.846,120.931,437.699,97.607,414.386z M243.998,479.667c-3.746-0.196-7.469-0.477-11.164-0.86c-5.89-2.64-11.722-6.25-17.5-10.961 c-17.632-14.359-33.976-38.671-46.398-69.85h75.061V479.667z M243.998,373.997h-83.436c-9.477-31.171-15.316-67.311-16.328-105.998 h99.763V373.997z M243.998,244H144.31c1.008-38.71,6.875-74.819,16.359-105.997h83.33V244z M243.998,114.003h-74.951 c3.109-7.79,6.367-15.312,9.934-22.195c10.64-20.625,23.17-36.89,36.354-47.656c5.777-4.71,11.609-8.32,17.5-10.96 c3.695-0.382,7.417-0.664,11.164-0.859V114.003z M446.392,138.003c19.312,31.085,31.234,67.194,33.281,105.997h-87.991 c-0.961-38.124-6.21-74.092-15.21-105.997H446.392z M414.393,97.612c5.211,5.211,10.156,10.696,14.836,16.391h-60.577 c-4.281-11.773-9.023-22.945-14.312-33.21c-6.953-13.437-14.758-25.468-23.347-35.89C362.668,56.16,391.065,74.301,414.393,97.612z M267.998,32.333c3.746,0.195,7.469,0.484,11.16,0.859c5.89,2.649,11.723,6.25,17.504,10.96 c17.636,14.359,33.976,38.671,46.397,69.85h-75.061V32.333z M267.998,138.003h83.436c9.476,31.171,15.32,67.31,16.328,105.997 h-99.764V138.003z M267.998,268h99.685c-1.007,38.71-6.874,74.818-16.359,105.998h-83.326V268z M296.661,467.846 c-5.781,4.711-11.614,8.313-17.504,10.961c-3.691,0.375-7.414,0.664-11.16,0.86v-81.67h74.951 c-3.109,7.789-6.367,15.312-9.933,22.195C322.376,440.816,309.845,457.081,296.661,467.846z M414.393,414.386 c-23.336,23.328-51.764,41.476-83.459,52.725c15.187-18.375,27.835-41.905,37.757-69.115h60.538 C424.548,403.692,419.604,409.176,414.393,414.386z M446.392,373.997h-69.998c9.008-31.929,14.414-67.842,15.367-105.998h87.912 C477.626,306.804,465.704,342.912,446.392,373.997z",
97
+ })
98
+ )
99
+ ),
100
+ React.createElement(Handle, { type: 'source', position: 'bottom' })
101
+ );
102
+ };
103
+
104
+ const MiddlewareNode = ({ data }) => {
105
+ return React.createElement('div', { style: { padding: '10px', height: '100%' } },
106
+ React.createElement('div', { style: { fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '5px', marginBottom: '5px' } },
107
+ data.label
108
+ ),
109
+ React.createElement(Handle, { type: 'target', position: 'top' }),
110
+ React.createElement(Handle, { type: 'source', position: 'bottom' })
111
+ );
112
+ };
113
+
114
+ const nodeTypes = {
115
+ controller: GroupNode,
116
+ router: GroupNode,
117
+ middleware: MiddlewareNode,
118
+ entrypoint: EntrypointNode
119
+ };
120
+
121
+ const GraphComponent = () => {
122
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
123
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
124
+ const [loading, setLoading] = useState(true);
125
+ const [searchTerm, setSearchTerm] = useState('');
126
+ const [zoom, setZoom] = useState(1);
127
+ const [isFullscreen, setIsFullscreen] = useState(false);
128
+
129
+ // Search Filter
130
+ useEffect(() => {
131
+ if (!searchTerm) {
132
+ setNodes(nds => nds.map(node => ({ ...node, style: { ...node.style, opacity: 1 } })));
133
+ setEdges(eds => eds.map(edge => ({ ...edge, style: { ...edge.style, opacity: 1 } })));
134
+ return;
135
+ }
136
+
137
+ const query = searchTerm.toLowerCase();
138
+ const matchedIds = new Set();
139
+
140
+ // Find matches in labels OR in internal routes
141
+ nodes.forEach(node => {
142
+ const labelMatch = node.data.label && node.data.label.toLowerCase().includes(query);
143
+ const routeMatch = node.data.routes && node.data.routes.some(r => r.path.toLowerCase().includes(query));
144
+
145
+ if (labelMatch || routeMatch) matchedIds.add(node.id);
146
+ });
147
+
148
+ setNodes(nds => nds.map(node => ({
149
+ ...node,
150
+ style: { ...node.style, opacity: matchedIds.has(node.id) ? 1 : 0.1 }
151
+ })));
152
+
153
+ setEdges(eds => eds.map(edge => ({
154
+ ...edge,
155
+ style: { ...edge.style, opacity: 0.1 }
156
+ })));
157
+
158
+ }, [searchTerm]);
159
+
160
+
161
+ useEffect(() => {
162
+ const buildGraph = async () => {
163
+ const registryData = window.registryData;
164
+ if (!registryData) return;
165
+
166
+ const makeId = (type, parent, idx, name) => `${type}_${parent || 'root'}_${idx}_${(name || '').replace(/[^a-zA-Z0-9]/g, '')}`;
167
+
168
+ function getNodeStyle(id) {
169
+ const m = window.metrics?.nodeMetrics && window.metrics.nodeMetrics[id];
170
+ if (!m) return {};
171
+ if (m.failures > 0) {
172
+ // Red intensity based on failures
173
+ const intensity = Math.min(1, m.failures / 5);
174
+ return { backgroundColor: `rgba(239, 68, 68, ${0.2 + intensity * 0.8})`, color: 'white' };
175
+ }
176
+ return {};
177
+ }
178
+ function getEdgeStyle(item) {
179
+ return {
180
+
181
+ };
182
+ }
183
+
184
+ function calculateNodeBounds(container) {
185
+ const routes = container.children?.routes || [];
186
+
187
+ // Create a temporary container that matches GroupNode styling
188
+ const wrapper = document.createElement("div");
189
+ wrapper.style.visibility = "hidden";
190
+ wrapper.style.position = "absolute";
191
+ wrapper.style.width = "fit-content";
192
+ wrapper.style.maxWidth = "500px"; // Arbitrary max width
193
+ document.body.appendChild(wrapper);
194
+
195
+ // Mimic GroupNode container
196
+ const nodeEl = document.createElement("div");
197
+ nodeEl.style.padding = "10px";
198
+ nodeEl.style.fontFamily = "Inter, system-ui, sans-serif"; // App font
199
+ nodeEl.style.fontSize = "12px";
200
+ wrapper.appendChild(nodeEl);
201
+
202
+ // Mimic Header
203
+ const header = document.createElement("div");
204
+ header.style.fontWeight = "bold";
205
+ header.style.borderBottom = "1px solid rgba(255,255,255,0.1)";
206
+ header.style.paddingBottom = "5px";
207
+ header.style.marginBottom = "5px";
208
+ // Label logic matches GroupNode
209
+ header.textContent = container.type === "controller"
210
+ ? (container.name + (container.metadata?.pluginName ? `\n[${container.metadata.pluginName}]` : ''))
211
+ : "Router: " + container.path;
212
+ nodeEl.appendChild(header);
213
+
214
+ // Mimic Routes
215
+ for (const route of routes) {
216
+ const row = document.createElement("div");
217
+ row.style.display = "flex";
218
+ row.style.alignItems = "center";
219
+ row.style.gap = "8px";
220
+ row.style.margin = "2px 0";
221
+
222
+ const badge = document.createElement("span");
223
+ badge.textContent = route.method;
224
+ badge.style.padding = "2px 6px";
225
+ badge.style.fontSize = "10px";
226
+ badge.style.fontWeight = "bold";
227
+ row.appendChild(badge);
228
+
229
+ const path = document.createElement("span");
230
+ path.textContent = route.path;
231
+ path.style.fontFamily = "monospace";
232
+ // path.style.color... doesn't affect size
233
+ row.appendChild(path);
234
+
235
+ nodeEl.appendChild(row);
236
+ }
237
+
238
+ const rect = nodeEl.getBoundingClientRect();
239
+ const width = Math.ceil(rect.width) + 20; // Safety buffer
240
+ const height = Math.ceil(rect.height);
241
+
242
+ document.body.removeChild(wrapper);
243
+
244
+ return { width, height };
245
+ }
246
+
247
+ function addRecursedLevel({ middleware, routes, routers, controllers }, parentId) {
248
+ const restChildrenNodes = [];
249
+ const restChildrenEdges = [];
250
+
251
+ const elkEdges = [];
252
+ const elkNodes = [
253
+ ...(middleware || []).map((mw, idx) => {
254
+ const id = makeId("middleware", parentId, idx, mw.name);
255
+ return {
256
+ id,
257
+ label: mw.metadata?.pluginName || mw.name || "Unknown Middleware",
258
+ ...calculateNodeBounds(mw),
259
+ // width: 140,
260
+ // height: 40,
261
+ type: "middleware",
262
+ style: getNodeStyle(id),
263
+ data: mw
264
+ };
265
+ }),
266
+ ...(routers || []).map((r, idx) => {
267
+ const id = makeId("router", parentId, idx, r.path);
268
+ const { nodes, edges } = addRecursedLevel(r.children, id);
269
+ restChildrenNodes.push(...nodes);
270
+ restChildrenEdges.push(...edges);
271
+
272
+ return {
273
+ id,
274
+ label: r.path,
275
+ ...calculateNodeBounds(r),
276
+ type: "router",
277
+ style: {
278
+ ...getNodeStyle(id),
279
+ backgroundColor: 'red'
280
+ },
281
+ data: r
282
+ };
283
+ }),
284
+ ...(controllers || []).map((ctrl, idx) => {
285
+ const id = makeId("controller", parentId, idx, ctrl.path);
286
+
287
+ const { nodes, edges } = addRecursedLevel(ctrl.children, id);
288
+ restChildrenNodes.push(...nodes);
289
+ restChildrenEdges.push(...edges);
290
+
291
+ return {
292
+ id,
293
+ label: ctrl.name + (ctrl.metadata?.pluginName ? `\n[${ctrl.metadata.pluginName}]` : ''),
294
+ ...calculateNodeBounds(ctrl),
295
+ type: "controller",
296
+ style: getNodeStyle(id),
297
+ data: ctrl
298
+ };
299
+ })
300
+ ].map(n => {
301
+ return {
302
+ style: {},
303
+ ...n,
304
+ draggable: false
305
+ };
306
+ });
307
+
308
+ let lastMiddlewareId = "";
309
+ // Create middleware edges
310
+ middleware?.forEach((mw, idx) => {
311
+ const id = makeId("middleware", parentId, idx, mw.name);
312
+
313
+ let sourceId = idx === 0 ?
314
+ parentId ? parentId : "entrypoint-http"
315
+ : makeId("middleware", parentId, idx - 1, middleware[idx - 1].name);
316
+
317
+ elkEdges.push({
318
+ id,
319
+ sources: [sourceId],
320
+ targets: [id],
321
+ type: "straight",
322
+ style: {
323
+ ...getEdgeStyle(mw),
324
+ backgroundColor: 'blue'
325
+ }
326
+ });
327
+ lastMiddlewareId = id;
328
+ });
329
+
330
+ routers?.forEach((r, idx) => {
331
+ const id = makeId("router", parentId, idx, r.path);
332
+ elkEdges.push({
333
+ id,
334
+ sources: [lastMiddlewareId || parentId],
335
+ targets: [id],
336
+ style: {
337
+ ...getEdgeStyle(r),
338
+ backgroundColor: 'blue'
339
+ }
340
+ });
341
+ });
342
+ controllers?.forEach((ctrl, idx) => {
343
+ const id = makeId("controller", parentId, idx, ctrl.path);
344
+ console.log({ id, lastMiddlewareId });
345
+ elkEdges.push({
346
+ id,
347
+ sources: [lastMiddlewareId || parentId],
348
+ targets: [id],
349
+ style: {
350
+ ...getEdgeStyle(ctrl),
351
+ backgroundColor: 'blue'
352
+ }
353
+ });
354
+ });
355
+
356
+ const nodes = elkNodes.concat(restChildrenNodes);
357
+ const edges = elkEdges.concat(restChildrenEdges);
358
+
359
+ return { nodes, edges };
360
+ }
361
+
362
+ const { nodes: elkNodes, edges: elkEdges } = addRecursedLevel(registryData);
363
+ elkNodes.push({
364
+ id: "entrypoint-http", width: 64, height: 64, type: "entrypoint"
365
+ });
366
+
367
+ const nodeNodeGap = '20';
368
+ const nodeEdgeGap = '20';
369
+ const graph = {
370
+ id: 'root',
371
+ layoutOptions: {
372
+ 'elk.algorithm': 'layered',
373
+ 'elk.direction': 'DOWN',
374
+ 'elk.spacing.nodeNode': nodeNodeGap,
375
+ 'elk.layered.spacing.nodeNodeBetweenLayers': nodeNodeGap,
376
+ 'elk.spacing.edgeNode': nodeEdgeGap,
377
+ 'elk.layered.spacing.edgeEdgeBetweenLayers': nodeEdgeGap,
378
+ 'elk.layered.spacing.edgeNodeBetweenLayers': nodeEdgeGap,
379
+ 'elk.layered.wrapping.additionalEdgeSpacing': nodeEdgeGap,
380
+ 'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
381
+ },
382
+ children: elkNodes,
383
+ edges: elkEdges
384
+ };
385
+
386
+ const layoutedGraph = await elk.layout(graph);
387
+
388
+ const flowNodes = [];
389
+
390
+ const processLayoutedNode = (node, parentId = undefined) => {
391
+ if (node.id === 'root') {
392
+ node.children?.forEach(c => processLayoutedNode(c));
393
+ return;
394
+ }
395
+
396
+ const style = NODE_STYLES[node.type] || {};
397
+ const isGroup = node.children && node.children.length > 0;
398
+
399
+ flowNodes.push({
400
+ id: node.id,
401
+ position: { x: node.x, y: node.y },
402
+ data: node,
403
+ style: {
404
+ ...style,
405
+ width: node.width,
406
+ height: node.height,
407
+ zIndex: isGroup ? -1 : 1
408
+ },
409
+ type: node.type,
410
+ parentNode: parentId,
411
+ draggable: false,
412
+ extent: 'parent'
413
+ });
414
+
415
+ if (node.children) {
416
+ node.children.forEach(c => processLayoutedNode(c, node.id));
417
+ }
418
+ };
419
+
420
+ processLayoutedNode(layoutedGraph);
421
+
422
+ const flowEdges = elkEdges.map(e => ({
423
+ id: e.id,
424
+ source: e.sources[0],
425
+ target: e.targets[0],
426
+ type: e.type || 'smoothstep',
427
+ animated: true,
428
+ style: { stroke: '#475569', strokeWidth: 2 },
429
+ markerEnd: {
430
+ type: MarkerType.ArrowClosed,
431
+ },
432
+ }));
433
+
434
+ setNodes(flowNodes);
435
+ setEdges(flowEdges);
436
+
437
+ setLoading(false);
438
+ };
439
+
440
+ buildGraph();
441
+ }, []);
442
+
443
+ if (loading) return React.createElement('div', { style: { color: '#fff', padding: '20px' } }, 'Laying out graph...');
444
+
445
+ return React.createElement('div', {
446
+ style: isFullscreen ? {
447
+ position: 'fixed',
448
+ top: 0,
449
+ left: 0,
450
+ width: '100vw',
451
+ height: '100vh',
452
+ zIndex: 9999,
453
+ backgroundColor: '#1e293b' // Match theme
454
+ } : {
455
+ width: '100%',
456
+ height: '600px', // Ensure explicit height if container doesn't provide it, though normally #cy does.
457
+ position: 'relative'
458
+ }
459
+ },
460
+ React.createElement(ReactFlow, {
461
+ nodes,
462
+ edges,
463
+ onNodesChange,
464
+ onEdgesChange,
465
+ nodeTypes, // Register custom types
466
+ fitView: true,
467
+ minZoom: 0.1,
468
+ onMove: (_, viewport) => setZoom(viewport.zoom),
469
+ onInit: (instance) => setZoom(instance.getZoom())
470
+ },
471
+ React.createElement(Background, { color: '#334155', gap: 16 }),
472
+ React.createElement(Controls),
473
+ React.createElement('div', {
474
+ style: {
475
+ position: 'absolute',
476
+ top: '10px',
477
+ right: '10px',
478
+ zIndex: 5,
479
+ display: 'flex',
480
+ gap: '8px',
481
+ alignItems: 'center'
482
+ }
483
+ },
484
+ React.createElement('div', {
485
+ style: {
486
+ background: 'rgba(30, 41, 59, 0.8)',
487
+ color: 'white',
488
+ padding: '6px 12px',
489
+ borderRadius: '6px',
490
+ fontSize: '12px',
491
+ fontFamily: 'monospace',
492
+ border: '1px solid #475569'
493
+ }
494
+ }, `${Math.round(zoom * 100)}%`),
495
+ React.createElement('button', {
496
+ onClick: () => setIsFullscreen(!isFullscreen),
497
+ style: {
498
+ background: '#3b82f6',
499
+ color: 'white',
500
+ border: 'none',
501
+ padding: '6px 12px',
502
+ borderRadius: '6px',
503
+ cursor: 'pointer',
504
+ fontSize: '12px',
505
+ fontWeight: 'bold',
506
+ transition: 'background 0.2s'
507
+ }
508
+ }, isFullscreen ? "Exit Fullscreen" : "Fullscreen")
509
+ )
510
+ )
511
+ );
512
+ };
513
+
514
+ function mountReact() {
515
+ const container = document.getElementById('cy');
516
+ if (container && !container._reactRoot) {
517
+ const root = createRoot(container);
518
+ container._reactRoot = root;
519
+ root.render(React.createElement(GraphComponent));
520
+ }
521
+ }
522
+ window.initGraph = mountReact;
523
+
@@ -0,0 +1,146 @@
1
+
2
+ function printDuration(deltaMs/** number in ms */) {
3
+
4
+ // More than 1 week
5
+ if (deltaMs > (7 * 24 * 60 * 60 * 1000)) {
6
+ const weeks = (deltaMs / (7 * 24 * 60 * 60 * 1000));
7
+ const weeksR = (deltaMs % (7 * 24 * 60 * 60 * 1000));
8
+
9
+ if (weeks >= 10) {
10
+ return weeks.toFixed(0) + " weeks";
11
+ }
12
+ else {
13
+ const days = (weeksR / (24 * 60 * 60 * 1000));
14
+
15
+ if (days >= 1) {
16
+ return weeks.toFixed(0) + " weeks " + days.toFixed(0) + " days";
17
+ }
18
+
19
+ return weeks.toFixed(0) + " weeks";
20
+ }
21
+ }
22
+ // More than 1 day
23
+ else if (deltaMs > (24 * 60 * 60 * 1000)) {
24
+ const days = (deltaMs / (24 * 60 * 60 * 1000));
25
+ const daysR = (deltaMs % (24 * 60 * 60 * 1000));
26
+
27
+ if (days >= 10) {
28
+ return days.toFixed(0) + " days";
29
+ }
30
+ else {
31
+ const hours = (daysR / (60 * 60 * 1000));
32
+
33
+ if (hours >= 1) {
34
+ return days.toFixed(0) + " days " + hours.toFixed(0) + " hours";
35
+ }
36
+
37
+ return days.toFixed(0) + " days";
38
+ }
39
+ }
40
+ // More than 1 hour
41
+ else if (deltaMs > (60 * 60 * 1000)) {
42
+ const hours = (deltaMs / (60 * 60 * 1000));
43
+ const hoursR = (deltaMs % (60 * 60 * 1000));
44
+
45
+ if (hours >= 10) {
46
+ return hours.toFixed(0) + " hours";
47
+ }
48
+ else {
49
+ const minutes = (hoursR / (60 * 1000));
50
+
51
+ if (minutes >= 1) {
52
+ return hours.toFixed(0) + " hours " + minutes.toFixed(0) + " minutes";
53
+ }
54
+
55
+ return hours.toFixed(0) + " hours";
56
+ }
57
+ }
58
+ // less than an hour, print minutes
59
+ else if (deltaMs > 60 * 1000) {
60
+ const minutes = (deltaMs / (60 * 1000));
61
+ const minutesR = (deltaMs % (60 * 1000));
62
+
63
+ if (minutes >= 10) {
64
+ return minutes.toFixed(0) + " minutes";
65
+ }
66
+ else {
67
+ const seconds = (minutesR / (1000));
68
+
69
+ if (seconds >= 1) {
70
+ return minutes.toFixed(0) + " minutes " + seconds.toFixed(0) + " seconds";
71
+ }
72
+
73
+ return minutes.toFixed(0) + " minutes";
74
+ }
75
+ }
76
+ // Seconds (whoa)
77
+ else if (deltaMs > 1000) {
78
+ const seconds = (deltaMs / (60 * 1000));
79
+
80
+ if (seconds >= 10) {
81
+ return seconds.toFixed(0) + " seconds";
82
+ }
83
+ else {
84
+ return seconds.toFixed(2) + " seconds";
85
+ }
86
+ }
87
+ // Milliseconds
88
+ else if (deltaMs > 1) {
89
+ const ms = deltaMs;
90
+ return ms.toFixed(0) + " ms";
91
+ }
92
+ // Microseconds
93
+ else if (deltaMs > 0.0001) {
94
+ const us = deltaMs;
95
+ return us.toFixed(0) + " us";
96
+ }
97
+ else return "N/A";
98
+ }
99
+
100
+ async function updateDashboard() {
101
+ try {
102
+ const headers = getRequestHeaders ? getRequestHeaders() : {};
103
+ const interval = document.getElementById('time-range-selector')?.value || '1m';
104
+ // Handle relative path issue when accessing /admin without trailing slash
105
+ const metricsUrl = (window.location.pathname.endsWith('/') ? 'metrics' : window.location.pathname + '/metrics') + `?interval=${interval}`;
106
+ const res = await fetch(metricsUrl, { headers });
107
+ if (!res.ok) return;
108
+
109
+ const data = await res.json();
110
+ const metrics = data.metrics;
111
+ window.metrics = metrics; // Update global metrics for tooltips
112
+
113
+ // Refresh Registry if active
114
+ if (document.getElementById('tab-registry').classList.contains('active')) {
115
+ const registryContainer = document.getElementById('registry-tree');
116
+ // Simple clear and re-render (optimization: could just update tooltips)
117
+ registryContainer.innerHTML = '';
118
+ window.renderRegistry(window.registryData, registryContainer);
119
+ }
120
+
121
+ document.getElementById('uptime').innerText = data.uptime;
122
+ document.getElementById('total-requests').innerText = metrics.totalRequests;
123
+ document.getElementById('active-requests').innerText = metrics.activeRequests;
124
+ document.getElementById('successful-requests').innerText = metrics.successfulRequests;
125
+ document.getElementById('failed-requests').innerText = metrics.failedRequests;
126
+ document.getElementById('avg-latency').innerText = metrics.averageTotalTime_ms.toFixed(2);
127
+
128
+ // Recalc rates
129
+ const finishedRequests = metrics.totalRequests - metrics.activeRequests;
130
+ const successRate = finishedRequests ? Math.round((metrics.successfulRequests / finishedRequests) * 100) : 100;
131
+ const failRate = finishedRequests ? Math.round((metrics.failedRequests / finishedRequests) * 100) : 0;
132
+
133
+ document.getElementById('success-rate').innerText = successRate + '%';
134
+ document.getElementById('fail-rate').innerText = failRate + '%';
135
+
136
+ } catch (err) {
137
+ console.error("Failed to update dashboard", err);
138
+ }
139
+ }
140
+
141
+ // Auto-refresh every 2 seconds
142
+ setInterval(updateDashboard, 2000);
143
+ // Initial load
144
+ updateDashboard();
145
+
146
+
@@ -0,0 +1,18 @@
1
+ .react-flow__controls-button {
2
+ color: #000 !important;
3
+ fill: #000 !important;
4
+ border-bottom: 1px solid #eee;
5
+ }
6
+
7
+ .react-flow__controls-button svg {
8
+ fill: currentColor;
9
+ }
10
+
11
+ .react-flow__node.react-flow__node-router {
12
+ height: fit-content !important;
13
+ }
14
+
15
+ .react-flow__handle {
16
+ opacity: 0;
17
+ pointer-events: none;
18
+ }