shokupan 0.7.0 → 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.
- package/README.md +53 -0
- package/dist/context.d.ts +50 -15
- package/dist/{http-server-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
- package/dist/http-server-BEMPIs33.cjs.map +1 -0
- package/dist/{http-server-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
- package/dist/http-server-CCeagTyU.js.map +1 -0
- package/dist/index.cjs +998 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +996 -135
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
- package/dist/plugins/application/dashboard/plugin.d.ts +14 -8
- package/dist/plugins/application/dashboard/static/charts.js +328 -0
- package/dist/plugins/application/dashboard/static/failures.js +85 -0
- package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
- package/dist/plugins/application/dashboard/static/poll.js +146 -0
- package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
- package/dist/plugins/application/dashboard/static/registry.css +131 -0
- package/dist/plugins/application/dashboard/static/registry.js +269 -0
- package/dist/plugins/application/dashboard/static/requests.js +118 -0
- package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
- package/dist/plugins/application/dashboard/static/styles.css +175 -0
- package/dist/plugins/application/dashboard/static/tables.js +92 -0
- package/dist/plugins/application/dashboard/static/tabs.js +113 -0
- package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
- package/dist/plugins/application/dashboard/template.eta +246 -0
- package/dist/plugins/application/socket-io.d.ts +14 -0
- package/dist/router.d.ts +12 -0
- package/dist/shokupan.d.ts +21 -1
- package/dist/util/datastore.d.ts +4 -3
- package/dist/util/decorators.d.ts +5 -0
- package/dist/util/http-error.d.ts +38 -0
- package/dist/util/http-status.d.ts +30 -0
- package/dist/util/request.d.ts +1 -1
- package/dist/util/symbol.d.ts +19 -0
- package/dist/util/types.d.ts +30 -1
- package/package.json +6 -3
- package/dist/http-server-0xH174zz.js.map +0 -1
- package/dist/http-server-DFhwlK8e.cjs.map +0 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
.tree-node {
|
|
2
|
+
margin-left: 1rem;
|
|
3
|
+
border-left: 1px solid #40516a;
|
|
4
|
+
padding-left: 1rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.tree-item {
|
|
8
|
+
margin: 0.25rem 0;
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: 0.5rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.tree-icon {
|
|
15
|
+
color: var(--text-secondary);
|
|
16
|
+
font-size: 0.8em;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.tree-label {
|
|
20
|
+
color: var(--text-primary);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.tree-meta {
|
|
24
|
+
color: var(--text-secondary);
|
|
25
|
+
font-size: 0.8em;
|
|
26
|
+
margin-left: 0.5rem;
|
|
27
|
+
font-style: italic;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.path-param {
|
|
31
|
+
color: var(--text-emphasis);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.path-end,
|
|
35
|
+
.path-param {
|
|
36
|
+
font-weight: bold;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.badge {
|
|
40
|
+
padding: 2px 6px;
|
|
41
|
+
border-radius: 4px;
|
|
42
|
+
font-size: 0.7em;
|
|
43
|
+
font-weight: bold;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.badge-GET {
|
|
47
|
+
background: #0f172a;
|
|
48
|
+
color: #3b82f6;
|
|
49
|
+
border: 1px solid #3b82f6;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.badge-POST {
|
|
53
|
+
background: #0f172a;
|
|
54
|
+
color: #22c55e;
|
|
55
|
+
border: 1px solid #22c55e;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.badge-PUT {
|
|
59
|
+
background: #0f172a;
|
|
60
|
+
color: #eab308;
|
|
61
|
+
border: 1px solid #eab308;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.badge-PATCH {
|
|
65
|
+
background: #0f172a;
|
|
66
|
+
color: #08d3ea;
|
|
67
|
+
border: 1px solid #08eae6;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.badge-HEAD {
|
|
71
|
+
background: #0f172a;
|
|
72
|
+
color: #e3ea08;
|
|
73
|
+
border: 1px solid #e3ea08;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.badge-OPTIONS {
|
|
77
|
+
background: #0f172a;
|
|
78
|
+
color: #838383;
|
|
79
|
+
border: 1px solid #838383;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.badge-DELETE {
|
|
83
|
+
background: #0f172a;
|
|
84
|
+
color: #ef4444;
|
|
85
|
+
border: 1px solid #ef4444;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.badge-ROUTER {
|
|
89
|
+
background: #334155;
|
|
90
|
+
color: #f8fafc;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.badge-CONTROLLER {
|
|
94
|
+
background: #475569;
|
|
95
|
+
color: #f8fafc;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
/* Tooltip Styling */
|
|
100
|
+
.tooltip {
|
|
101
|
+
position: relative;
|
|
102
|
+
display: block;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.tooltip .tooltip-text {
|
|
106
|
+
visibility: hidden;
|
|
107
|
+
width: 220px;
|
|
108
|
+
background-color: var(--bg-primary);
|
|
109
|
+
color: var(--text-primary);
|
|
110
|
+
text-align: left;
|
|
111
|
+
border-radius: 6px;
|
|
112
|
+
padding: 8px;
|
|
113
|
+
position: absolute;
|
|
114
|
+
z-index: 1000;
|
|
115
|
+
bottom: 125%;
|
|
116
|
+
/* Position above */
|
|
117
|
+
left: 50%;
|
|
118
|
+
margin-left: -110px;
|
|
119
|
+
opacity: 0;
|
|
120
|
+
transition: opacity 0.2s;
|
|
121
|
+
border: 1px solid var(--card-border);
|
|
122
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
123
|
+
backdrop-filter: blur(12px);
|
|
124
|
+
font-size: 0.85rem;
|
|
125
|
+
pointer-events: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.tooltip:hover .tooltip-text {
|
|
129
|
+
visibility: visible;
|
|
130
|
+
opacity: 1;
|
|
131
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
|
|
2
|
+
window.renderRegistry = function renderRegistry(node, container) {
|
|
3
|
+
const rootPath = "<%~ it.rootPath %>";
|
|
4
|
+
const linkPattern = "<%~ it.linkPattern %>";
|
|
5
|
+
|
|
6
|
+
if (!node) {
|
|
7
|
+
container.innerHTML = '<div style="color: var(--text-secondary)">No registry data available</div>';
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const wrapper = document.createElement('div');
|
|
12
|
+
|
|
13
|
+
// Helper to clean paths
|
|
14
|
+
const cleanPath = (p) => {
|
|
15
|
+
if (!p) return '';
|
|
16
|
+
if (p.startsWith(rootPath)) return p.slice(rootPath.length + 1);
|
|
17
|
+
return p;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Helper to create file link
|
|
21
|
+
const createFileMeta = (metadata, defaultName) => {
|
|
22
|
+
if (metadata && metadata.file) {
|
|
23
|
+
const relative = cleanPath(metadata.file);
|
|
24
|
+
const absolute = metadata.file;
|
|
25
|
+
const line = metadata.line;
|
|
26
|
+
|
|
27
|
+
// Generate Link
|
|
28
|
+
let link = linkPattern
|
|
29
|
+
.replace('{{absolute}}', absolute)
|
|
30
|
+
.replace('{{relative}}', relative)
|
|
31
|
+
.replace('{{line}}', line);
|
|
32
|
+
|
|
33
|
+
const name = metadata.name || defaultName;
|
|
34
|
+
|
|
35
|
+
// Label Filtering
|
|
36
|
+
const blocklist = ['wrappedHandler', 'anonymous', 'finalHandler', 'routeHandler'];
|
|
37
|
+
let displayNameStr = '';
|
|
38
|
+
if (name && !blocklist.includes(name)) {
|
|
39
|
+
displayNameStr = ` <span style="color: #94a3b8; font-size: 0.8em;">(${name})</span>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const builtin = metadata.isBuiltin ? `<span class="badge" style="background: #059669; margin-left:10px;">BUILTIN</span>` : '';
|
|
43
|
+
const pluginName = metadata.pluginName ? `<span style="color: #6ee7b7; margin-left: 5px;">[${metadata.pluginName}]</span>` : '';
|
|
44
|
+
|
|
45
|
+
return `<a href="${link}" target="_blank" style="text-decoration: none; color: inherit;"><span class="tree-meta" style="cursor: pointer; text-decoration: underline;">${relative}:${line}</span></a>${displayNameStr} ${builtin} ${pluginName}`;
|
|
46
|
+
}
|
|
47
|
+
return '';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 1. Flatten all items
|
|
51
|
+
const allItems = [];
|
|
52
|
+
if (node.middleware) node.middleware.forEach(i => allItems.push({ ...i, kind: 'middleware' }));
|
|
53
|
+
if (node.routes) node.routes.forEach(i => allItems.push({ ...i, kind: 'route' }));
|
|
54
|
+
if (node.routers) node.routers.forEach(i => allItems.push({ ...i, kind: 'router' }));
|
|
55
|
+
if (node.controllers) node.controllers.forEach(i => allItems.push({ ...i, kind: 'controller' }));
|
|
56
|
+
|
|
57
|
+
// 2. Sort by Order
|
|
58
|
+
const kindPriority = { 'middleware': 0, 'router': 1, 'controller': 2, 'route': 3 };
|
|
59
|
+
allItems.sort((a, b) => {
|
|
60
|
+
const pA = kindPriority[a.kind] !== undefined ? kindPriority[a.kind] : 99;
|
|
61
|
+
const pB = kindPriority[b.kind] !== undefined ? kindPriority[b.kind] : 99;
|
|
62
|
+
if (pA !== pB) return pA - pB;
|
|
63
|
+
return (a.order || 0) - (b.order || 0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Deduplication (by ID if available, or path+method)
|
|
67
|
+
const uniqueItems = [];
|
|
68
|
+
const seenIds = new Set();
|
|
69
|
+
allItems.forEach(item => {
|
|
70
|
+
const uniqueKey = item.id || (item.kind + ':' + (item.path || item.name));
|
|
71
|
+
if (!seenIds.has(uniqueKey)) {
|
|
72
|
+
seenIds.add(uniqueKey);
|
|
73
|
+
uniqueItems.push(item);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// Replace allItems with deduplicated list
|
|
77
|
+
allItems.length = 0;
|
|
78
|
+
allItems.push(...uniqueItems);
|
|
79
|
+
|
|
80
|
+
// 3. Render
|
|
81
|
+
const renderedRoutes = new Set(); // Track rendered routes to avoid duplication when grouping
|
|
82
|
+
|
|
83
|
+
// Let's rebuild the controller groups map using the flattened objects
|
|
84
|
+
const controllerGroups = new Map();
|
|
85
|
+
allItems.forEach(item => {
|
|
86
|
+
if (item.kind === 'route' && item.tags && item.tags.length > 0) {
|
|
87
|
+
const tag = item.tags[0];
|
|
88
|
+
if (!controllerGroups.has(tag)) controllerGroups.set(tag, []);
|
|
89
|
+
controllerGroups.get(tag).push(item);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function getTooltipHtml(id) {
|
|
94
|
+
// Return default 0 metrics if not found, rather than empty string, so tooltip always appears if intended
|
|
95
|
+
const metrics = window.metrics || {};
|
|
96
|
+
const m = (metrics.nodeMetrics && metrics.nodeMetrics[id]) ? metrics.nodeMetrics[id] : { requests: 0, failures: 0 };
|
|
97
|
+
const totalReqs = metrics.totalRequests || 1; // avoid div/0
|
|
98
|
+
const percent = ((m.requests / totalReqs) * 100).toFixed(1);
|
|
99
|
+
const failRate = m.requests > 0 ? ((m.failures / m.requests) * 100).toFixed(1) : '0.0';
|
|
100
|
+
|
|
101
|
+
return `
|
|
102
|
+
<div class="tooltip-text">
|
|
103
|
+
<div style="font-weight:bold; margin-bottom:4px; border-bottom:1px solid var(--text-secondary); padding-bottom:2px;">Metrics</div>
|
|
104
|
+
<div style="display:flex; justify-content:space-between;"><span>Requests:</span> <span style="font-family:monospace">${m.requests}</span></div>
|
|
105
|
+
<div style="display:flex; justify-content:space-between;"><span>Traffic:</span> <span style="font-family:monospace">${percent}%</span></div>
|
|
106
|
+
<div style="display:flex; justify-content:space-between;"><span>Failures:</span> <span style="font-family:monospace; color:${m.failures > 0 ? '#ef4444' : 'inherit'}">${m.failures} (${failRate}%)</span></div>
|
|
107
|
+
</div>
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderPath(path) {
|
|
112
|
+
const parts = path.split('/').slice(1);
|
|
113
|
+
|
|
114
|
+
let out = '';
|
|
115
|
+
parts.forEach((part, index) => {
|
|
116
|
+
if (part.startsWith(":")) {
|
|
117
|
+
out += `/<span class="path-segment path-param">${part}</span>`;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (index === parts.length - 1) {
|
|
121
|
+
out += `/<span class="path-segment path-end">${part}</span>`;
|
|
122
|
+
return;
|
|
123
|
+
};
|
|
124
|
+
out += `/<span class="path-segment">${part}</span>`;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
allItems.forEach(item => {
|
|
131
|
+
// Middleware
|
|
132
|
+
if (item.kind === 'middleware') {
|
|
133
|
+
const div = document.createElement('div');
|
|
134
|
+
const mwContainer = document.createElement('div');
|
|
135
|
+
mwContainer.className = 'tree-node';
|
|
136
|
+
|
|
137
|
+
const mwDiv = document.createElement('div');
|
|
138
|
+
mwDiv.className = 'tree-item tooltip'; // Add tooltip class
|
|
139
|
+
const meta = createFileMeta(item.metadata, item.name);
|
|
140
|
+
const tooltipHtml = getTooltipHtml(item.id);
|
|
141
|
+
mwDiv.innerHTML = `<span class="badge" style="background: #9333ea; color: white;">MIDDLEWARE</span> <span class="tree-label">${item.name}</span>${meta}${tooltipHtml}`;
|
|
142
|
+
|
|
143
|
+
mwContainer.appendChild(mwDiv);
|
|
144
|
+
div.appendChild(mwDiv);
|
|
145
|
+
wrapper.appendChild(div);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Router
|
|
149
|
+
else if (item.kind === 'router') {
|
|
150
|
+
const div = document.createElement('div');
|
|
151
|
+
const header = document.createElement('div');
|
|
152
|
+
header.className = 'tree-item tooltip'; // Add tooltip class
|
|
153
|
+
const meta = createFileMeta(item.metadata, 'Router');
|
|
154
|
+
const tooltipHtml = getTooltipHtml(item.id);
|
|
155
|
+
header.innerHTML = `
|
|
156
|
+
<span class="badge badge-ROUTER">ROUTER</span>
|
|
157
|
+
<span class="tree-label">${renderPath(item.path)}</span>
|
|
158
|
+
${meta}
|
|
159
|
+
${tooltipHtml}
|
|
160
|
+
`;
|
|
161
|
+
div.appendChild(header);
|
|
162
|
+
|
|
163
|
+
if (item.children) {
|
|
164
|
+
const childrenContainer = document.createElement('div');
|
|
165
|
+
childrenContainer.className = 'tree-node';
|
|
166
|
+
renderRegistry(item.children, childrenContainer);
|
|
167
|
+
div.appendChild(childrenContainer);
|
|
168
|
+
}
|
|
169
|
+
wrapper.appendChild(div);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Controller
|
|
173
|
+
else if (item.kind === 'controller') {
|
|
174
|
+
// Render Controller Group
|
|
175
|
+
const name = item.name;
|
|
176
|
+
const cPath = item.path || '';
|
|
177
|
+
|
|
178
|
+
// Render Header
|
|
179
|
+
const div = document.createElement('div');
|
|
180
|
+
const header = document.createElement('div');
|
|
181
|
+
header.className = 'tree-item tooltip'; // Add tooltip class
|
|
182
|
+
const meta = createFileMeta(item.metadata, name);
|
|
183
|
+
const tooltipHtml = getTooltipHtml(item.id);
|
|
184
|
+
header.innerHTML = `
|
|
185
|
+
<span class="badge badge-CONTROLLER">CTRL</span>
|
|
186
|
+
<span class="tree-label" style="font-weight: bold;">${name} <span style="color:var(--text-secondary); font-weight:normal;">(${renderPath(cPath)})</span></span>
|
|
187
|
+
${meta}
|
|
188
|
+
${tooltipHtml}
|
|
189
|
+
`;
|
|
190
|
+
div.appendChild(header);
|
|
191
|
+
|
|
192
|
+
// Render Routes belonging to this controller
|
|
193
|
+
const routes = item.children.routes || [];
|
|
194
|
+
if (routes.length > 0) {
|
|
195
|
+
const routesContainer = document.createElement('div');
|
|
196
|
+
routesContainer.className = 'tree-node';
|
|
197
|
+
|
|
198
|
+
routes.forEach(r => {
|
|
199
|
+
renderedRoutes.add(r); // Mark as rendered
|
|
200
|
+
|
|
201
|
+
const rDiv = document.createElement('div');
|
|
202
|
+
rDiv.className = 'tree-item tooltip'; // Add tooltip class
|
|
203
|
+
const method = r.method.toUpperCase();
|
|
204
|
+
const badgeClass = `badge-${method}`;
|
|
205
|
+
const rMeta = createFileMeta(r.metadata, r.handlerName);
|
|
206
|
+
const tHtml = getTooltipHtml(r.id);
|
|
207
|
+
rDiv.innerHTML = `
|
|
208
|
+
<span class="badge ${badgeClass}" style="width: 50px; text-align: center; display: inline-block;">${method}</span>
|
|
209
|
+
<span class="tree-label">${renderPath(r.path)}</span>
|
|
210
|
+
${rMeta}
|
|
211
|
+
${tHtml}
|
|
212
|
+
`;
|
|
213
|
+
routesContainer.appendChild(rDiv);
|
|
214
|
+
});
|
|
215
|
+
div.appendChild(routesContainer);
|
|
216
|
+
} else {
|
|
217
|
+
// Empty controller or no tagged routes found
|
|
218
|
+
// It's still valid to show it
|
|
219
|
+
const note = document.createElement('div');
|
|
220
|
+
note.className = 'tree-node';
|
|
221
|
+
note.innerHTML = '<span style="color: #64748b; font-style: italic;">(No routes detected)</span>';
|
|
222
|
+
div.appendChild(note);
|
|
223
|
+
}
|
|
224
|
+
wrapper.appendChild(div);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Route (Loose)
|
|
228
|
+
else if (item.kind === 'route') {
|
|
229
|
+
if (renderedRoutes.has(item)) return; // Skip if already rendered in controller
|
|
230
|
+
|
|
231
|
+
const div = document.createElement('div');
|
|
232
|
+
div.className = 'tree-item tooltip'; // Add tooltip class
|
|
233
|
+
const method = item.method.toUpperCase();
|
|
234
|
+
const badgeClass = `badge-${method}`;
|
|
235
|
+
const meta = createFileMeta(item.metadata, item.handlerName);
|
|
236
|
+
const tHtml = getTooltipHtml(item.id);
|
|
237
|
+
|
|
238
|
+
div.innerHTML = `
|
|
239
|
+
<span class="badge ${badgeClass}" style="width: 50px; text-align: center; display: inline-block;">${method}</span>
|
|
240
|
+
<span class="tree-label">${renderPath(item.path)}</span>
|
|
241
|
+
${meta}
|
|
242
|
+
${tHtml}
|
|
243
|
+
`;
|
|
244
|
+
wrapper.appendChild(div);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
container.innerHTML = '';
|
|
248
|
+
container.appendChild(wrapper);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
window.fetchRegistry = async function fetchRegistry() {
|
|
252
|
+
const registryContainer = document.getElementById('registry-tree');
|
|
253
|
+
if (!registryContainer) return;
|
|
254
|
+
|
|
255
|
+
const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
|
|
256
|
+
const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
|
|
257
|
+
const url = basePath + '/registry';
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const res = await fetch(url, { headers });
|
|
261
|
+
const { registry } = await res.json();
|
|
262
|
+
renderRegistry(window.registryData = registry, registryContainer);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
console.error("Failed to fetch registry", e);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
document.addEventListener('DOMContentLoaded', fetchRegistry);
|
|
269
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
|
|
2
|
+
// Initialize Requests Table
|
|
3
|
+
let requestsTable;
|
|
4
|
+
|
|
5
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
6
|
+
requestsTable = new Tabulator("#requests-list-container", {
|
|
7
|
+
layout: "fitColumns",
|
|
8
|
+
placeholder: "No requests found",
|
|
9
|
+
selectable: 1,
|
|
10
|
+
columns: [
|
|
11
|
+
{ title: "Method", field: "method", width: 100 },
|
|
12
|
+
{ title: "URL", field: "url" },
|
|
13
|
+
{
|
|
14
|
+
title: "Status",
|
|
15
|
+
field: "status",
|
|
16
|
+
width: 100,
|
|
17
|
+
formatter: function (cell) {
|
|
18
|
+
const status = cell.getValue();
|
|
19
|
+
const color = status >= 500 ? 'red' : status >= 400 ? 'orange' : 'green';
|
|
20
|
+
return `<span style="color: ${color}; font-weight: bold;">${status}</span>`;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{ title: "Duration (ms)", field: "duration", width: 150, formatter: (cell) => printDuration(cell.getValue()) },
|
|
24
|
+
{
|
|
25
|
+
title: "Time",
|
|
26
|
+
field: "timestamp",
|
|
27
|
+
width: 200,
|
|
28
|
+
formatter: function (cell) {
|
|
29
|
+
return new Date(cell.getValue()).toLocaleString();
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: "",
|
|
34
|
+
width: 80,
|
|
35
|
+
formatter: function (cell) {
|
|
36
|
+
const el = document.createElement("div");
|
|
37
|
+
el.onclick = () => showRequestDetails(cell.getData());
|
|
38
|
+
el.innerHTML = "View";
|
|
39
|
+
return el;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
data: []
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Auto-fetch on load if tab is active (or just fetch initially)
|
|
47
|
+
fetchRequests();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function fetchRequests() {
|
|
51
|
+
const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
|
|
52
|
+
|
|
53
|
+
// Determine base path for API requests
|
|
54
|
+
const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
|
|
55
|
+
const url = basePath + '/';
|
|
56
|
+
|
|
57
|
+
fetch(url + 'requests', { headers })
|
|
58
|
+
.then(res => res.json())
|
|
59
|
+
.then(data => {
|
|
60
|
+
if (requestsTable) {
|
|
61
|
+
requestsTable.setData(data.requests);
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.catch(err => console.error("Failed to fetch requests", err));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function showRequestDetails(request) {
|
|
68
|
+
const container = document.getElementById('request-details-container');
|
|
69
|
+
const content = document.getElementById('request-details-content');
|
|
70
|
+
const traceContainer = document.getElementById('middleware-trace-container');
|
|
71
|
+
|
|
72
|
+
container.style.display = 'block';
|
|
73
|
+
|
|
74
|
+
// Render Summary
|
|
75
|
+
content.innerHTML = `
|
|
76
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
|
|
77
|
+
<div><strong>Method:</strong> ${request.method}</div>
|
|
78
|
+
<div><strong>URL:</strong> ${request.url}</div>
|
|
79
|
+
<div><strong>Status:</strong> ${request.status}</div>
|
|
80
|
+
<div><strong>Duration:</strong> ${printDuration(request.duration)} ms</div>
|
|
81
|
+
<div><strong>Timestamp:</strong> ${new Date(request.timestamp).toLocaleString()}</div>
|
|
82
|
+
</div>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
// Render Trace
|
|
86
|
+
if (request.handlerStack && request.handlerStack.length > 0) {
|
|
87
|
+
let html = '<div style="display: flex; flex-direction: column; gap: 4px;">';
|
|
88
|
+
|
|
89
|
+
request.handlerStack.forEach((item, index) => {
|
|
90
|
+
const duration = item.duration || 0;
|
|
91
|
+
|
|
92
|
+
if (index !== 0) {
|
|
93
|
+
html += `<div style="align-self: center">⬇︎</div>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
html += `
|
|
97
|
+
<div style="padding: 8px; border-radius: 4px; background: var(--bg-secondary);">
|
|
98
|
+
<div style="display: flex; justify-content: space-between;">
|
|
99
|
+
<span style="font-weight: bold;">${item.name}</span>
|
|
100
|
+
<span>${printDuration(duration)}</span>
|
|
101
|
+
</div>
|
|
102
|
+
<div style="font-size: 0.8rem; color: var(--text-secondary);">
|
|
103
|
+
${item.file}:${item.line}
|
|
104
|
+
</div>
|
|
105
|
+
${item.stateChanges ? `<div style="font-size: 0.8rem; margin-top: 4px; color: #aaa;">State Changes: ${Object.keys(item.stateChanges).join(', ')}</div>` : ''}
|
|
106
|
+
</div>
|
|
107
|
+
`;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
html += '</div>';
|
|
111
|
+
traceContainer.innerHTML = html;
|
|
112
|
+
} else {
|
|
113
|
+
traceContainer.innerHTML = '<div style="color: var(--text-secondary);">No middleware trace available.</div>';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Scroll to details
|
|
117
|
+
container.scrollIntoView({ behavior: 'smooth' });
|
|
118
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
::-webkit-scrollbar {
|
|
2
|
+
width: 8px;
|
|
3
|
+
height: 8px;
|
|
4
|
+
background-color: #22355a;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
::-webkit-scrollbar-thumb {
|
|
8
|
+
border-radius: 10px;
|
|
9
|
+
background-color: #2a406a;
|
|
10
|
+
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
*:hover::-webkit-scrollbar-thumb {
|
|
14
|
+
background-color: #22468a;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
::-webkit-scrollbar-track {
|
|
18
|
+
border-radius: 10px;
|
|
19
|
+
background-color: #0b0f17;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
::-webkit-scrollbar-corner {
|
|
23
|
+
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
|
24
|
+
}
|