@webda/debug 4.0.0-beta.3
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/LICENSE +166 -0
- package/README.md +95 -0
- package/lib/bodycapture.d.ts +66 -0
- package/lib/bodycapture.js +162 -0
- package/lib/bodycapture.js.map +1 -0
- package/lib/debugservice.service.d.ts +166 -0
- package/lib/debugservice.service.js +609 -0
- package/lib/debugservice.service.js.map +1 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -0
- package/lib/introspection.d.ts +115 -0
- package/lib/introspection.js +260 -0
- package/lib/introspection.js.map +1 -0
- package/lib/logbuffer.d.ts +75 -0
- package/lib/logbuffer.js +98 -0
- package/lib/logbuffer.js.map +1 -0
- package/lib/requestlog.d.ts +181 -0
- package/lib/requestlog.js +153 -0
- package/lib/requestlog.js.map +1 -0
- package/lib/tui/client.d.ts +165 -0
- package/lib/tui/client.js +220 -0
- package/lib/tui/client.js.map +1 -0
- package/lib/tui/panels/config.d.ts +79 -0
- package/lib/tui/panels/config.js +291 -0
- package/lib/tui/panels/config.js.map +1 -0
- package/lib/tui/panels/logs.d.ts +106 -0
- package/lib/tui/panels/logs.js +342 -0
- package/lib/tui/panels/logs.js.map +1 -0
- package/lib/tui/panels/models.d.ts +47 -0
- package/lib/tui/panels/models.js +193 -0
- package/lib/tui/panels/models.js.map +1 -0
- package/lib/tui/panels/operations.d.ts +55 -0
- package/lib/tui/panels/operations.js +241 -0
- package/lib/tui/panels/operations.js.map +1 -0
- package/lib/tui/panels/panel.d.ts +55 -0
- package/lib/tui/panels/panel.js +31 -0
- package/lib/tui/panels/panel.js.map +1 -0
- package/lib/tui/panels/requests.d.ts +156 -0
- package/lib/tui/panels/requests.js +512 -0
- package/lib/tui/panels/requests.js.map +1 -0
- package/lib/tui/panels/routes.d.ts +45 -0
- package/lib/tui/panels/routes.js +154 -0
- package/lib/tui/panels/routes.js.map +1 -0
- package/lib/tui/panels/services.d.ts +49 -0
- package/lib/tui/panels/services.js +266 -0
- package/lib/tui/panels/services.js.map +1 -0
- package/lib/tui/tui.d.ts +58 -0
- package/lib/tui/tui.js +236 -0
- package/lib/tui/tui.js.map +1 -0
- package/package.json +60 -0
- package/webui/app.js +177 -0
- package/webui/components/logs.js +128 -0
- package/webui/components/model-graph.js +292 -0
- package/webui/components/models.js +213 -0
- package/webui/components/operations.js +315 -0
- package/webui/components/requests.js +264 -0
- package/webui/components/schema-form.js +268 -0
- package/webui/components/services.js +248 -0
- package/webui/index.html +22 -0
- package/webui/logo.svg +1 -0
- package/webui/styles.css +685 -0
- package/webui/vendor/htm.js +1 -0
- package/webui/vendor/preact-hooks.js +2 -0
- package/webui/vendor/preact.js +2 -0
- package/webui/webda.png +0 -0
package/lib/tui/tui.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import termkit from "terminal-kit";
|
|
2
|
+
import { DebugClient } from "./client.js";
|
|
3
|
+
import { ModelsPanel } from "./panels/models.js";
|
|
4
|
+
import { ServicesPanel } from "./panels/services.js";
|
|
5
|
+
import { OperationsPanel } from "./panels/operations.js";
|
|
6
|
+
import { RequestsPanel } from "./panels/requests.js";
|
|
7
|
+
import { LogsPanel } from "./panels/logs.js";
|
|
8
|
+
const { terminal: term } = termkit;
|
|
9
|
+
/**
|
|
10
|
+
* Terminal UI dashboard for the Webda debug server.
|
|
11
|
+
*
|
|
12
|
+
* Provides a tab-based interface with seven panels (Logs, Models, Services,
|
|
13
|
+
* Operations, Routes, Config, Requests) and live WebSocket updates.
|
|
14
|
+
* Connect to an already-running debug server started with `webda debug`.
|
|
15
|
+
*/
|
|
16
|
+
export class DebugTui {
|
|
17
|
+
/**
|
|
18
|
+
* Create a new TUI instance.
|
|
19
|
+
*
|
|
20
|
+
* @param port - Port of the debug server to connect to
|
|
21
|
+
*/
|
|
22
|
+
constructor(port = 18181) {
|
|
23
|
+
this.panels = [];
|
|
24
|
+
this.activeTab = 0;
|
|
25
|
+
this.running = false;
|
|
26
|
+
this.lastRefresh = "";
|
|
27
|
+
this.appInfo = null;
|
|
28
|
+
this.client = new DebugClient(`http://localhost:${port}`);
|
|
29
|
+
this.logsPanel = new LogsPanel(this.client);
|
|
30
|
+
this.panels = [
|
|
31
|
+
this.logsPanel,
|
|
32
|
+
new ModelsPanel(this.client),
|
|
33
|
+
new ServicesPanel(this.client),
|
|
34
|
+
new OperationsPanel(this.client),
|
|
35
|
+
new RequestsPanel(this.client)
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Start the TUI: initialize terminal, load data, and begin rendering.
|
|
40
|
+
*/
|
|
41
|
+
async start() {
|
|
42
|
+
this.running = true;
|
|
43
|
+
// Set up terminal
|
|
44
|
+
term.fullscreen(true);
|
|
45
|
+
term.hideCursor();
|
|
46
|
+
term.grabInput(true);
|
|
47
|
+
// Handle input
|
|
48
|
+
term.on("key", (key) => this.handleKey(key));
|
|
49
|
+
// Connect WebSocket for live events
|
|
50
|
+
this.client.connectWebSocket();
|
|
51
|
+
// Listen for WebSocket events: restart triggers refetch, log events go to LogsPanel
|
|
52
|
+
this.wsUnsubscribe = this.client.onEvent(event => {
|
|
53
|
+
if (event.type === "restart") {
|
|
54
|
+
this.refreshAll();
|
|
55
|
+
}
|
|
56
|
+
else if (event.type === "log") {
|
|
57
|
+
this.logsPanel.onWsEvent(event);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// Initial data load
|
|
61
|
+
await this.refreshAll();
|
|
62
|
+
// Periodic render (for live updates like requests panel)
|
|
63
|
+
this.renderTimer = setInterval(() => {
|
|
64
|
+
if (this.running)
|
|
65
|
+
this.render();
|
|
66
|
+
}, 500);
|
|
67
|
+
// Initial render
|
|
68
|
+
this.render();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Refresh data for all panels.
|
|
72
|
+
*/
|
|
73
|
+
async refreshAll() {
|
|
74
|
+
const now = new Date();
|
|
75
|
+
this.lastRefresh =
|
|
76
|
+
String(now.getHours()).padStart(2, "0") +
|
|
77
|
+
":" +
|
|
78
|
+
String(now.getMinutes()).padStart(2, "0") +
|
|
79
|
+
":" +
|
|
80
|
+
String(now.getSeconds()).padStart(2, "0");
|
|
81
|
+
const refreshPromises = this.panels.map(p => (p.refresh ? p.refresh() : Promise.resolve()));
|
|
82
|
+
refreshPromises.push(this.client.getAppInfo().then(info => { this.appInfo = info; }));
|
|
83
|
+
await Promise.allSettled(refreshPromises);
|
|
84
|
+
if (this.running)
|
|
85
|
+
this.render();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Handle keyboard input.
|
|
89
|
+
* @param key - key name from terminal-kit
|
|
90
|
+
*/
|
|
91
|
+
handleKey(key) {
|
|
92
|
+
// Quit
|
|
93
|
+
if (key === "q" || key === "CTRL_C") {
|
|
94
|
+
this.stop();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Tab switching with number keys
|
|
98
|
+
const tabKeys = ["1", "2", "3", "4", "5", "6", "7"];
|
|
99
|
+
const tabIdx = tabKeys.indexOf(key);
|
|
100
|
+
if (tabIdx >= 0 && tabIdx < this.panels.length) {
|
|
101
|
+
this.activeTab = tabIdx;
|
|
102
|
+
this.render();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Tab switching with left/right arrows (shift+arrow or tab/shift+tab)
|
|
106
|
+
if (key === "TAB" || key === "RIGHT") {
|
|
107
|
+
this.activeTab = (this.activeTab + 1) % this.panels.length;
|
|
108
|
+
this.render();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (key === "SHIFT_TAB" || key === "LEFT") {
|
|
112
|
+
this.activeTab = (this.activeTab - 1 + this.panels.length) % this.panels.length;
|
|
113
|
+
this.render();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Refresh
|
|
117
|
+
if (key === "r" || key === "R") {
|
|
118
|
+
this.refreshAll();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Forward to active panel
|
|
122
|
+
const panel = this.panels[this.activeTab];
|
|
123
|
+
if (panel.onKey) {
|
|
124
|
+
panel.onKey(key);
|
|
125
|
+
this.render();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Full render: header, panel content, status bar.
|
|
130
|
+
*/
|
|
131
|
+
render() {
|
|
132
|
+
if (!this.running)
|
|
133
|
+
return;
|
|
134
|
+
const width = term.width;
|
|
135
|
+
const height = term.height;
|
|
136
|
+
// Header bar (row 1)
|
|
137
|
+
this.renderHeader(width);
|
|
138
|
+
// Content area (row 3 to height - 1)
|
|
139
|
+
const panel = this.panels[this.activeTab];
|
|
140
|
+
for (let row = 3; row < height; row++) {
|
|
141
|
+
term.moveTo(1, row);
|
|
142
|
+
term.eraseLine();
|
|
143
|
+
}
|
|
144
|
+
panel.render(term, 3, height - 1, width);
|
|
145
|
+
// Status bar (last row)
|
|
146
|
+
this.renderStatusBar(width, height);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Render the header/tab bar.
|
|
150
|
+
* @param width - terminal width in columns
|
|
151
|
+
*/
|
|
152
|
+
renderHeader(width) {
|
|
153
|
+
term.moveTo(1, 1);
|
|
154
|
+
term.eraseLine();
|
|
155
|
+
term.white("web");
|
|
156
|
+
term.yellow("da");
|
|
157
|
+
term.white(" debug");
|
|
158
|
+
if (this.appInfo) {
|
|
159
|
+
const name = this.appInfo.package?.name || "";
|
|
160
|
+
const cwd = (this.appInfo.workingDirectory || "").replace(/^\/(Users|home)\/[^/]+\//, "~/");
|
|
161
|
+
term.dim(` | ${name} ${cwd}`);
|
|
162
|
+
}
|
|
163
|
+
term(" ");
|
|
164
|
+
for (let i = 0; i < this.panels.length; i++) {
|
|
165
|
+
const label = ` ${i + 1}:${this.panels[i].name} `;
|
|
166
|
+
if (i === this.activeTab) {
|
|
167
|
+
term.bgCyan.black.bold(label);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
term.dim(label);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Separator line
|
|
174
|
+
term.moveTo(1, 2);
|
|
175
|
+
term.eraseLine();
|
|
176
|
+
term.dim("-".repeat(width));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Render the status bar at the bottom.
|
|
180
|
+
* @param width - terminal width in columns
|
|
181
|
+
* @param height - terminal height in rows
|
|
182
|
+
*/
|
|
183
|
+
renderStatusBar(width, height) {
|
|
184
|
+
term.moveTo(1, height);
|
|
185
|
+
term.eraseLine();
|
|
186
|
+
// Connection status
|
|
187
|
+
if (this.client.connected) {
|
|
188
|
+
term.bgGreen.black(" CONNECTED ");
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
term.bgRed.white(" DISCONNECTED ");
|
|
192
|
+
}
|
|
193
|
+
term(" ");
|
|
194
|
+
term.dim(`Last refresh: ${this.lastRefresh || "never"}`);
|
|
195
|
+
// Right-aligned help
|
|
196
|
+
const help = " q:Quit r:Refresh 1-7:Tabs Arrows:Navigate ";
|
|
197
|
+
const pos = Math.max(1, width - help.length);
|
|
198
|
+
term.moveTo(pos, height);
|
|
199
|
+
term.dim(help);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Stop the TUI and clean up.
|
|
203
|
+
*/
|
|
204
|
+
stop() {
|
|
205
|
+
if (!this.running)
|
|
206
|
+
return;
|
|
207
|
+
this.running = false;
|
|
208
|
+
if (this.renderTimer) {
|
|
209
|
+
clearInterval(this.renderTimer);
|
|
210
|
+
this.renderTimer = undefined;
|
|
211
|
+
}
|
|
212
|
+
if (this.wsUnsubscribe) {
|
|
213
|
+
this.wsUnsubscribe();
|
|
214
|
+
this.wsUnsubscribe = undefined;
|
|
215
|
+
}
|
|
216
|
+
// Destroy all panels
|
|
217
|
+
for (const panel of this.panels) {
|
|
218
|
+
panel.destroy();
|
|
219
|
+
}
|
|
220
|
+
// Disconnect from server
|
|
221
|
+
this.client.disconnect();
|
|
222
|
+
// Restore terminal
|
|
223
|
+
term.hideCursor(false);
|
|
224
|
+
term.grabInput(false);
|
|
225
|
+
term.fullscreen(false);
|
|
226
|
+
term.clear();
|
|
227
|
+
if (this.onStop) {
|
|
228
|
+
// The command owning the TUI stops the application, then exits
|
|
229
|
+
this.onStop();
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
term.processExit(0);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=tui.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui.js","sourceRoot":"","sources":["../../src/tui/tui.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webda/debug",
|
|
3
|
+
"version": "4.0.0-beta.3",
|
|
4
|
+
"description": "Development debug server for Webda applications",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"webda",
|
|
7
|
+
"debug"
|
|
8
|
+
],
|
|
9
|
+
"author": "Remi Cattiau <remi@cattiau.com>",
|
|
10
|
+
"repository": "git://github.com/loopingz/webda.io.git",
|
|
11
|
+
"main": "lib/index.js",
|
|
12
|
+
"typings": "lib/index.d.ts",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"terminal-kit": "^3.1.2",
|
|
15
|
+
"ws": "^8.18.0",
|
|
16
|
+
"@webda/utils": "4.0.0-beta.3",
|
|
17
|
+
"@webda/workout": "4.0.0-beta.3"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@webda/core": "^4.0.0-beta.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "25.5.0",
|
|
24
|
+
"@types/ws": "^8.5.13",
|
|
25
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
26
|
+
"jscodeshift": "^17.3.0",
|
|
27
|
+
"typescript": "7.1.0-dev.20260929.1",
|
|
28
|
+
"vite": "^6.0.0",
|
|
29
|
+
"vitest": "^4.1.2",
|
|
30
|
+
"@webda/compiler": "^4.0.0-beta.3",
|
|
31
|
+
"@webda/core": "^4.0.0-beta.3",
|
|
32
|
+
"@webda/test": "4.0.0-beta.3"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"lib",
|
|
36
|
+
"webui"
|
|
37
|
+
],
|
|
38
|
+
"homepage": "https://webda.io",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"type": "module",
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22.0.0"
|
|
45
|
+
},
|
|
46
|
+
"license": "LGPL-3.0-only",
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "webdac build",
|
|
49
|
+
"build:watch": "webdac build --watch",
|
|
50
|
+
"pretest": "npm run build",
|
|
51
|
+
"format": "prettier --check src/**/*",
|
|
52
|
+
"format:fix": "prettier --write src/**/*",
|
|
53
|
+
"lint": "eslint",
|
|
54
|
+
"lint:fix": "eslint --fix",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"test:debug": "vitest debug",
|
|
58
|
+
"test:coverage": "vitest run --coverage"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/webui/app.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { h, render } from "preact";
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from "preact/hooks";
|
|
3
|
+
import htm from "htm";
|
|
4
|
+
|
|
5
|
+
import { ModelsPanel } from "./components/models.js";
|
|
6
|
+
import { ServicesPanel } from "./components/services.js";
|
|
7
|
+
import { OperationsPanel } from "./components/operations.js";
|
|
8
|
+
import { RequestsPanel } from "./components/requests.js";
|
|
9
|
+
import { LogsPanel } from "./components/logs.js";
|
|
10
|
+
|
|
11
|
+
const html = htm.bind(h);
|
|
12
|
+
|
|
13
|
+
const TABS = [
|
|
14
|
+
{ id: "logs", label: "Logs", color: "#f7992c" },
|
|
15
|
+
{ id: "models", label: "Models", color: "#81bf6b" },
|
|
16
|
+
{ id: "services", label: "Services", color: "#6b8fd4" },
|
|
17
|
+
{ id: "operations", label: "Operations", color: "#f4f4f4" },
|
|
18
|
+
{ id: "requests", label: "Requests", color: "#6b8fd4" }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fetch JSON from the debug API.
|
|
23
|
+
* @param {string} path
|
|
24
|
+
* @returns {Promise<any>}
|
|
25
|
+
*/
|
|
26
|
+
async function fetchApi(path) {
|
|
27
|
+
const res = await fetch(path);
|
|
28
|
+
return res.json();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function App() {
|
|
32
|
+
const [tab, setTab] = useState("logs");
|
|
33
|
+
const [wsConnected, setWsConnected] = useState(false);
|
|
34
|
+
const [wsEvents, setWsEvents] = useState([]);
|
|
35
|
+
const [dataVersion, setDataVersion] = useState(0);
|
|
36
|
+
const [cache, setCache] = useState({});
|
|
37
|
+
const wsRef = useRef(null);
|
|
38
|
+
const reconnectTimer = useRef(null);
|
|
39
|
+
|
|
40
|
+
// Load all data upfront and on rebuild (restart event)
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
Promise.all([
|
|
43
|
+
fetchApi("/api/info").catch(() => null),
|
|
44
|
+
fetchApi("/api/models").catch(() => []),
|
|
45
|
+
fetchApi("/api/services").catch(() => []),
|
|
46
|
+
fetchApi("/api/operations").catch(() => []),
|
|
47
|
+
fetchApi("/api/config").catch(() => {}),
|
|
48
|
+
fetchApi("/api/requests").catch(() => []),
|
|
49
|
+
fetchApi("/api/logs").catch(() => [])
|
|
50
|
+
]).then(([info, models, services, operations, config, requests, logs]) => {
|
|
51
|
+
setCache({ info, models, services, operations, config, requests, logs });
|
|
52
|
+
});
|
|
53
|
+
}, [dataVersion]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const onKeyDown = (e) => {
|
|
57
|
+
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT") return;
|
|
58
|
+
if (e.key === "ArrowLeft") {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
setTab((prev) => {
|
|
61
|
+
const idx = TABS.findIndex((t) => t.id === prev);
|
|
62
|
+
return TABS[(idx - 1 + TABS.length) % TABS.length].id;
|
|
63
|
+
});
|
|
64
|
+
} else if (e.key === "ArrowRight") {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
setTab((prev) => {
|
|
67
|
+
const idx = TABS.findIndex((t) => t.id === prev);
|
|
68
|
+
return TABS[(idx + 1) % TABS.length].id;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
window.addEventListener("keydown", onKeyDown);
|
|
73
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const connectWs = useCallback(() => {
|
|
77
|
+
if (wsRef.current && wsRef.current.readyState <= 1) return;
|
|
78
|
+
|
|
79
|
+
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
80
|
+
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
|
|
81
|
+
wsRef.current = ws;
|
|
82
|
+
|
|
83
|
+
ws.addEventListener("open", () => {
|
|
84
|
+
setWsConnected(true);
|
|
85
|
+
if (reconnectTimer.current) {
|
|
86
|
+
clearTimeout(reconnectTimer.current);
|
|
87
|
+
reconnectTimer.current = null;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
ws.addEventListener("message", (e) => {
|
|
92
|
+
try {
|
|
93
|
+
const data = JSON.parse(e.data);
|
|
94
|
+
if (data.type === "restart") {
|
|
95
|
+
// Trigger data refetch across all panels
|
|
96
|
+
setDataVersion((v) => v + 1);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Dispatch custom event so individual panels can listen
|
|
100
|
+
window.dispatchEvent(new CustomEvent("debug-ws-event", { detail: data }));
|
|
101
|
+
setWsEvents((prev) => [data, ...prev].slice(0, 500));
|
|
102
|
+
} catch {
|
|
103
|
+
// ignore parse errors
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
ws.addEventListener("close", () => {
|
|
108
|
+
setWsConnected(false);
|
|
109
|
+
wsRef.current = null;
|
|
110
|
+
// Auto-reconnect after 2 seconds
|
|
111
|
+
reconnectTimer.current = setTimeout(connectWs, 2000);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
ws.addEventListener("error", () => {
|
|
115
|
+
ws.close();
|
|
116
|
+
});
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
connectWs();
|
|
121
|
+
return () => {
|
|
122
|
+
if (wsRef.current) wsRef.current.close();
|
|
123
|
+
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
|
|
124
|
+
};
|
|
125
|
+
}, [connectWs]);
|
|
126
|
+
|
|
127
|
+
const renderPanel = () => {
|
|
128
|
+
switch (tab) {
|
|
129
|
+
case "models":
|
|
130
|
+
return html`<${ModelsPanel} data=${cache.models} fetchApi=${fetchApi} dataVersion=${dataVersion} />`;
|
|
131
|
+
case "services":
|
|
132
|
+
return html`<${ServicesPanel} data=${cache.services} config=${cache.config} fetchApi=${fetchApi} dataVersion=${dataVersion} />`;
|
|
133
|
+
case "operations":
|
|
134
|
+
return html`<${OperationsPanel} data=${cache.operations} fetchApi=${fetchApi} dataVersion=${dataVersion} />`;
|
|
135
|
+
case "requests":
|
|
136
|
+
return html`<${RequestsPanel} data=${cache.requests} fetchApi=${fetchApi} dataVersion=${dataVersion} wsEvents=${wsEvents} />`;
|
|
137
|
+
case "logs":
|
|
138
|
+
return html`<${LogsPanel} data=${cache.logs} fetchApi=${fetchApi} dataVersion=${dataVersion} />`;
|
|
139
|
+
default:
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return html`
|
|
145
|
+
<div class="header ${wsConnected ? "" : "disconnected"}">
|
|
146
|
+
<div class="header-logo">
|
|
147
|
+
<img src="/logo.svg" alt="Webda" />
|
|
148
|
+
<div class="header-title">web<span>da</span> debug</div>
|
|
149
|
+
${cache.info && html`<div class="header-app-info">
|
|
150
|
+
<div class="header-app-name">${cache.info.package?.name || "unknown"}</div>
|
|
151
|
+
<div class="header-cwd">${(cache.info.workingDirectory || "").replace(/^\/(Users|home)\/[^/]+\//, "~/")}</div>
|
|
152
|
+
</div>`}
|
|
153
|
+
</div>
|
|
154
|
+
<nav class="nav-tabs">
|
|
155
|
+
${TABS.map(
|
|
156
|
+
(t) => html`
|
|
157
|
+
<button
|
|
158
|
+
key=${t.id}
|
|
159
|
+
class="nav-tab ${tab === t.id ? "active" : ""}"
|
|
160
|
+
style="${tab === t.id ? `color:${t.color};border-bottom-color:${t.color}` : ""}"
|
|
161
|
+
onClick=${() => setTab(t.id)}
|
|
162
|
+
>
|
|
163
|
+
${t.label}
|
|
164
|
+
</button>
|
|
165
|
+
`
|
|
166
|
+
)}
|
|
167
|
+
</nav>
|
|
168
|
+
<div class="ws-indicator">
|
|
169
|
+
<span class="ws-dot ${wsConnected ? "" : "disconnected"}"></span>
|
|
170
|
+
<span>${wsConnected ? "Connected" : "Reconnecting..."}</span>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="content">${renderPanel()}</div>
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
render(html`<${App} />`, document.getElementById("app"));
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { h } from "preact";
|
|
2
|
+
import { useState, useEffect, useRef, useMemo } from "preact/hooks";
|
|
3
|
+
import htm from "htm";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
const LEVEL_COLORS = {
|
|
8
|
+
ERROR: "#ef4444",
|
|
9
|
+
WARN: "#eab308",
|
|
10
|
+
INFO: "#22c55e",
|
|
11
|
+
DEBUG: "#6b7280",
|
|
12
|
+
TRACE: "#4b5563"
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const LEVEL_ORDER = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];
|
|
16
|
+
|
|
17
|
+
function formatTime(ts) {
|
|
18
|
+
if (!ts) return "-";
|
|
19
|
+
return new Date(ts).toISOString().substring(11, 23);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function LogsPanel({ data }) {
|
|
23
|
+
const [liveEntries, setLiveEntries] = useState([]);
|
|
24
|
+
const [search, setSearch] = useState("");
|
|
25
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
26
|
+
const [levelFilter, setLevelFilter] = useState(() => localStorage.getItem("webda-log-level") || "INFO");
|
|
27
|
+
const listRef = useRef(null);
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
// Merge cached + live, deduplicate by id
|
|
31
|
+
const allEntries = useMemo(() => {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const merged = [];
|
|
34
|
+
for (const e of liveEntries) {
|
|
35
|
+
if (!seen.has(e.id)) { seen.add(e.id); merged.push(e); }
|
|
36
|
+
}
|
|
37
|
+
for (const e of (data || [])) {
|
|
38
|
+
if (!seen.has(e.id)) { seen.add(e.id); merged.push(e); }
|
|
39
|
+
}
|
|
40
|
+
return merged;
|
|
41
|
+
}, [data, liveEntries]);
|
|
42
|
+
|
|
43
|
+
const entries = search
|
|
44
|
+
? allEntries.filter(e => e.message?.toLowerCase().includes(search.toLowerCase()) || e.level?.toLowerCase().includes(search.toLowerCase()))
|
|
45
|
+
: allEntries;
|
|
46
|
+
|
|
47
|
+
// Listen for live log events via custom event dispatched from WebSocket handler
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const handler = (event) => {
|
|
50
|
+
if (event.detail?.type === "log") {
|
|
51
|
+
setLiveEntries((prev) => [event.detail, ...prev].slice(0, 2000));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
window.addEventListener("debug-ws-event", handler);
|
|
55
|
+
return () => window.removeEventListener("debug-ws-event", handler);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (autoScroll && listRef.current) {
|
|
60
|
+
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
61
|
+
}
|
|
62
|
+
}, [entries, autoScroll]);
|
|
63
|
+
|
|
64
|
+
const minLevel = LEVEL_ORDER.indexOf(levelFilter);
|
|
65
|
+
const filtered = minLevel <= 0 ? entries : entries.filter((e) => LEVEL_ORDER.indexOf(e.level) >= minLevel);
|
|
66
|
+
|
|
67
|
+
return html`
|
|
68
|
+
<div>
|
|
69
|
+
<div style="display:flex;gap:8px;margin-bottom:12px;align-items:center">
|
|
70
|
+
<input
|
|
71
|
+
type="text"
|
|
72
|
+
placeholder="Search logs..."
|
|
73
|
+
value=${search}
|
|
74
|
+
onInput=${(e) => setSearch(e.target.value)}
|
|
75
|
+
style="flex:1;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-primary);border-radius:4px;font-family:monospace"
|
|
76
|
+
/>
|
|
77
|
+
<select
|
|
78
|
+
value=${levelFilter}
|
|
79
|
+
onChange=${(e) => { setLevelFilter(e.target.value); localStorage.setItem("webda-log-level", e.target.value); }}
|
|
80
|
+
style="padding:6px;background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-primary);border-radius:4px"
|
|
81
|
+
>
|
|
82
|
+
<option value="ERROR">ERROR</option>
|
|
83
|
+
<option value="WARN">WARN+</option>
|
|
84
|
+
<option value="INFO">INFO+</option>
|
|
85
|
+
<option value="DEBUG">DEBUG+</option>
|
|
86
|
+
<option value="TRACE">TRACE+</option>
|
|
87
|
+
</select>
|
|
88
|
+
<label style="display:flex;align-items:center;gap:4px;color:var(--text-muted);font-size:12px">
|
|
89
|
+
<input type="checkbox" checked=${autoScroll} onChange=${(e) => setAutoScroll(e.target.checked)} />
|
|
90
|
+
Auto-scroll
|
|
91
|
+
</label>
|
|
92
|
+
</div>
|
|
93
|
+
<div style="margin-bottom:0.75rem;color:var(--text-muted);font-size:0.8125rem">
|
|
94
|
+
${filtered.length} log${filtered.length !== 1 ? "s" : ""}
|
|
95
|
+
</div>
|
|
96
|
+
<div ref=${listRef} class="table-container" style="overflow-y:auto;max-height:calc(100vh - 200px)">
|
|
97
|
+
<table>
|
|
98
|
+
<thead>
|
|
99
|
+
<tr>
|
|
100
|
+
<th style="width:120px">Time</th>
|
|
101
|
+
<th style="width:70px">Level</th>
|
|
102
|
+
<th>Message</th>
|
|
103
|
+
</tr>
|
|
104
|
+
</thead>
|
|
105
|
+
<tbody>
|
|
106
|
+
${filtered.map(
|
|
107
|
+
(entry) => html`
|
|
108
|
+
<tr key=${entry.id}>
|
|
109
|
+
<td class="mono" style="white-space:nowrap;color:var(--text-muted);font-size:12px">${formatTime(entry.timestamp)}</td>
|
|
110
|
+
<td><span class="badge" style="background:${LEVEL_COLORS[entry.level] || "#666"}">${entry.level}</span></td>
|
|
111
|
+
<td class="mono" style="font-size:13px;white-space:pre-wrap;word-break:break-all">${entry.message}</td>
|
|
112
|
+
</tr>
|
|
113
|
+
`
|
|
114
|
+
)}
|
|
115
|
+
${filtered.length === 0 &&
|
|
116
|
+
html`
|
|
117
|
+
<tr>
|
|
118
|
+
<td colspan="3" style="color:var(--text-muted);text-align:center;padding:2rem">
|
|
119
|
+
No logs${search ? " matching search" : " yet"}. Logs will appear here in real-time.
|
|
120
|
+
</td>
|
|
121
|
+
</tr>
|
|
122
|
+
`}
|
|
123
|
+
</tbody>
|
|
124
|
+
</table>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
`;
|
|
128
|
+
}
|