@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.
Files changed (66) hide show
  1. package/LICENSE +166 -0
  2. package/README.md +95 -0
  3. package/lib/bodycapture.d.ts +66 -0
  4. package/lib/bodycapture.js +162 -0
  5. package/lib/bodycapture.js.map +1 -0
  6. package/lib/debugservice.service.d.ts +166 -0
  7. package/lib/debugservice.service.js +609 -0
  8. package/lib/debugservice.service.js.map +1 -0
  9. package/lib/index.d.ts +7 -0
  10. package/lib/index.js +7 -0
  11. package/lib/index.js.map +1 -0
  12. package/lib/introspection.d.ts +115 -0
  13. package/lib/introspection.js +260 -0
  14. package/lib/introspection.js.map +1 -0
  15. package/lib/logbuffer.d.ts +75 -0
  16. package/lib/logbuffer.js +98 -0
  17. package/lib/logbuffer.js.map +1 -0
  18. package/lib/requestlog.d.ts +181 -0
  19. package/lib/requestlog.js +153 -0
  20. package/lib/requestlog.js.map +1 -0
  21. package/lib/tui/client.d.ts +165 -0
  22. package/lib/tui/client.js +220 -0
  23. package/lib/tui/client.js.map +1 -0
  24. package/lib/tui/panels/config.d.ts +79 -0
  25. package/lib/tui/panels/config.js +291 -0
  26. package/lib/tui/panels/config.js.map +1 -0
  27. package/lib/tui/panels/logs.d.ts +106 -0
  28. package/lib/tui/panels/logs.js +342 -0
  29. package/lib/tui/panels/logs.js.map +1 -0
  30. package/lib/tui/panels/models.d.ts +47 -0
  31. package/lib/tui/panels/models.js +193 -0
  32. package/lib/tui/panels/models.js.map +1 -0
  33. package/lib/tui/panels/operations.d.ts +55 -0
  34. package/lib/tui/panels/operations.js +241 -0
  35. package/lib/tui/panels/operations.js.map +1 -0
  36. package/lib/tui/panels/panel.d.ts +55 -0
  37. package/lib/tui/panels/panel.js +31 -0
  38. package/lib/tui/panels/panel.js.map +1 -0
  39. package/lib/tui/panels/requests.d.ts +156 -0
  40. package/lib/tui/panels/requests.js +512 -0
  41. package/lib/tui/panels/requests.js.map +1 -0
  42. package/lib/tui/panels/routes.d.ts +45 -0
  43. package/lib/tui/panels/routes.js +154 -0
  44. package/lib/tui/panels/routes.js.map +1 -0
  45. package/lib/tui/panels/services.d.ts +49 -0
  46. package/lib/tui/panels/services.js +266 -0
  47. package/lib/tui/panels/services.js.map +1 -0
  48. package/lib/tui/tui.d.ts +58 -0
  49. package/lib/tui/tui.js +236 -0
  50. package/lib/tui/tui.js.map +1 -0
  51. package/package.json +60 -0
  52. package/webui/app.js +177 -0
  53. package/webui/components/logs.js +128 -0
  54. package/webui/components/model-graph.js +292 -0
  55. package/webui/components/models.js +213 -0
  56. package/webui/components/operations.js +315 -0
  57. package/webui/components/requests.js +264 -0
  58. package/webui/components/schema-form.js +268 -0
  59. package/webui/components/services.js +248 -0
  60. package/webui/index.html +22 -0
  61. package/webui/logo.svg +1 -0
  62. package/webui/styles.css +685 -0
  63. package/webui/vendor/htm.js +1 -0
  64. package/webui/vendor/preact-hooks.js +2 -0
  65. package/webui/vendor/preact.js +2 -0
  66. package/webui/webda.png +0 -0
@@ -0,0 +1,268 @@
1
+ import { h } from "preact";
2
+ import { useState } from "preact/hooks";
3
+ import htm from "htm";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ /**
8
+ * Resolve a $ref in a schema against a definitions map.
9
+ * Merges the referenced schema with any sibling properties (description, etc).
10
+ */
11
+ function resolveRef(prop, definitions) {
12
+ if (!prop) return prop;
13
+ if (prop.$ref) {
14
+ const refName = prop.$ref.replace("#/definitions/", "");
15
+ const resolved = definitions?.[refName] || definitions?.[decodeURIComponent(refName)];
16
+ if (resolved) {
17
+ // Merge: sibling properties (like description) override the ref
18
+ const siblings = Object.fromEntries(Object.entries(prop).filter(([k]) => k !== "$ref"));
19
+ return { ...resolved, ...siblings, definitions: { ...resolved.definitions, ...definitions } };
20
+ }
21
+ }
22
+ return { ...prop, definitions: { ...prop.definitions, ...definitions } };
23
+ }
24
+
25
+ /**
26
+ * Collapsible section wrapper.
27
+ */
28
+ function Collapsible({ title, badge, defaultOpen, children }) {
29
+ const [open, setOpen] = useState(defaultOpen ?? true);
30
+ return html`
31
+ <div class="sf-collapsible">
32
+ <div class="sf-collapsible-header" onClick=${() => setOpen(!open)}>
33
+ <span class="sf-chevron ${open ? "sf-open" : ""}">\u25B6</span>
34
+ <span class="sf-collapsible-title">${title}</span>
35
+ ${badge && html`<span class="sf-badge">${badge}</span>`}
36
+ </div>
37
+ ${open && html`<div class="sf-collapsible-body">${children}</div>`}
38
+ </div>
39
+ `;
40
+ }
41
+
42
+ /**
43
+ * Render a single field based on its schema type.
44
+ */
45
+ function SchemaField({ name, prop, value, onChangeValue, readOnly, definitions, path }) {
46
+ const resolved = resolveRef(prop, definitions);
47
+ if (!resolved) return null;
48
+ const defs = resolved.definitions || definitions;
49
+
50
+ // Nested object with properties → recursive form
51
+ if (resolved.type === "object" && resolved.properties) {
52
+ const objValue = value || {};
53
+ return html`
54
+ <${Collapsible} title=${name} badge="object" defaultOpen=${Object.keys(objValue).length > 0}>
55
+ <${SchemaFields}
56
+ schema=${resolved}
57
+ definitions=${defs}
58
+ values=${objValue}
59
+ onChange=${v => onChangeValue(v)}
60
+ readOnly=${readOnly}
61
+ path=${path}
62
+ />
63
+ <//>
64
+ `;
65
+ }
66
+
67
+ // Array field
68
+ if (resolved.type === "array") {
69
+ return html`<${ArrayField}
70
+ name=${name} prop=${resolved} value=${value} onChangeValue=${onChangeValue}
71
+ readOnly=${readOnly} definitions=${defs} path=${path}
72
+ />`;
73
+ }
74
+
75
+ // Enum → select
76
+ if (resolved.enum) {
77
+ return html`
78
+ <select class="sf-input" value=${value ?? ""} disabled=${readOnly}
79
+ onChange=${e => onChangeValue(e.target.value || undefined)}>
80
+ <option value="">Select...</option>
81
+ ${resolved.enum.map(v => html`<option key=${v} value=${v}>${v}</option>`)}
82
+ </select>
83
+ `;
84
+ }
85
+
86
+ // Boolean → toggle-style select
87
+ if (resolved.type === "boolean") {
88
+ return html`
89
+ <select class="sf-input" value=${value == null ? "" : String(value)} disabled=${readOnly}
90
+ onChange=${e => onChangeValue(e.target.value === "" ? undefined : e.target.value === "true")}>
91
+ <option value="">-</option>
92
+ <option value="true">true</option>
93
+ <option value="false">false</option>
94
+ </select>
95
+ `;
96
+ }
97
+
98
+ // Number / integer
99
+ if (resolved.type === "number" || resolved.type === "integer") {
100
+ return html`
101
+ <input class="sf-input" type="number" value=${value ?? ""}
102
+ step=${resolved.type === "integer" ? "1" : "any"}
103
+ min=${resolved.minimum} max=${resolved.maximum} readOnly=${readOnly}
104
+ onInput=${e => {
105
+ const v = e.target.value;
106
+ onChangeValue(v === "" ? undefined : resolved.type === "integer" ? parseInt(v) : parseFloat(v));
107
+ }}
108
+ placeholder=${resolved.default != null ? `Default: ${resolved.default}` : name} />
109
+ `;
110
+ }
111
+
112
+ // Object without properties (map) or unknown → JSON textarea
113
+ if (resolved.type === "object") {
114
+ return html`
115
+ <textarea class="sf-input sf-textarea" rows="3"
116
+ value=${typeof value === "object" && value !== null ? JSON.stringify(value, null, 2) : value ?? ""}
117
+ readOnly=${readOnly}
118
+ onInput=${e => { try { onChangeValue(JSON.parse(e.target.value)); } catch {} }}
119
+ placeholder="JSON object" />
120
+ `;
121
+ }
122
+
123
+ // String (default)
124
+ const inputType = resolved.format === "email" ? "email"
125
+ : resolved.format === "uri" || resolved.format === "url" ? "url"
126
+ : resolved.format === "password" ? "password"
127
+ : "text";
128
+
129
+ return html`
130
+ <input class="sf-input" type=${inputType}
131
+ value=${value ?? ""} readOnly=${readOnly}
132
+ onInput=${e => onChangeValue(e.target.value || undefined)}
133
+ placeholder=${resolved.default != null ? `Default: ${resolved.default}` : (resolved.pattern ? `Pattern: ${resolved.pattern}` : name)}
134
+ minlength=${resolved.minLength} maxlength=${resolved.maxLength} />
135
+ `;
136
+ }
137
+
138
+ /**
139
+ * Render an array field with add/remove controls.
140
+ */
141
+ function ArrayField({ name, prop, value, onChangeValue, readOnly, definitions, path }) {
142
+ const items = Array.isArray(value) ? value : [];
143
+ const itemSchema = resolveRef(prop.items, definitions) || { type: "string" };
144
+ const isSimple = itemSchema.type === "string" || itemSchema.type === "number" || itemSchema.type === "integer";
145
+
146
+ return html`
147
+ <${Collapsible} title=${name} badge=${`array[${items.length}]`} defaultOpen=${items.length > 0}>
148
+ <div class="sf-array">
149
+ ${items.map((item, i) => html`
150
+ <div class="sf-array-item" key=${i}>
151
+ <div class="sf-array-item-content">
152
+ ${isSimple
153
+ ? html`<input class="sf-input" type=${itemSchema.type === "string" ? "text" : "number"}
154
+ value=${item ?? ""} readOnly=${readOnly}
155
+ onInput=${e => {
156
+ const v = e.target.value;
157
+ const newItems = [...items];
158
+ newItems[i] = itemSchema.type === "string" ? v : (itemSchema.type === "integer" ? parseInt(v) : parseFloat(v));
159
+ onChangeValue(newItems);
160
+ }}
161
+ placeholder=${`Item ${i + 1}`} />`
162
+ : itemSchema.type === "object" && itemSchema.properties
163
+ ? html`<${SchemaFields}
164
+ schema=${itemSchema} definitions=${definitions}
165
+ values=${item || {}}
166
+ onChange=${v => { const newItems = [...items]; newItems[i] = v; onChangeValue(newItems); }}
167
+ readOnly=${readOnly} path=${[...path, i]}
168
+ />`
169
+ : html`<textarea class="sf-input sf-textarea" rows="2"
170
+ value=${typeof item === "object" ? JSON.stringify(item, null, 2) : String(item ?? "")}
171
+ readOnly=${readOnly}
172
+ onInput=${e => { try { const newItems = [...items]; newItems[i] = JSON.parse(e.target.value); onChangeValue(newItems); } catch {} }}
173
+ placeholder="JSON" />`
174
+ }
175
+ </div>
176
+ ${!readOnly && html`
177
+ <button class="sf-btn sf-btn-remove" onClick=${() => { const newItems = items.filter((_, j) => j !== i); onChangeValue(newItems.length ? newItems : undefined); }}
178
+ title="Remove item">\u00D7</button>
179
+ `}
180
+ </div>
181
+ `)}
182
+ ${!readOnly && html`
183
+ <button class="sf-btn sf-btn-add" onClick=${() => {
184
+ const empty = isSimple ? (itemSchema.type === "string" ? "" : 0) : {};
185
+ onChangeValue([...items, empty]);
186
+ }}>+ Add item</button>
187
+ `}
188
+ </div>
189
+ <//>
190
+ `;
191
+ }
192
+
193
+ /**
194
+ * Render all fields for a schema's properties.
195
+ */
196
+ function SchemaFields({ schema, definitions, values, onChange, readOnly, path }) {
197
+ const defs = { ...definitions, ...schema.definitions };
198
+ const required = new Set(schema.required || []);
199
+ const props = schema.properties || {};
200
+
201
+ return html`
202
+ <div class="sf-fields">
203
+ ${Object.entries(props).map(([name, prop]) => {
204
+ const resolved = resolveRef(prop, defs);
205
+ if (!resolved) return null;
206
+ const isReq = required.has(name);
207
+ const hasValue = values[name] !== undefined && values[name] !== null && values[name] !== "";
208
+ const fieldPath = [...(path || []), name];
209
+
210
+ return html`
211
+ <div class="sf-field ${hasValue ? "sf-has-value" : ""}" key=${name}>
212
+ <div class="sf-label-row">
213
+ <label class="sf-label">
214
+ ${name}
215
+ ${isReq && html`<span class="sf-required">*</span>`}
216
+ </label>
217
+ <span class="sf-type">${resolved.type || "ref"}${resolved.format ? `:${resolved.format}` : ""}</span>
218
+ </div>
219
+ ${resolved.description && html`<div class="sf-description">${resolved.description}</div>`}
220
+ <${SchemaField}
221
+ name=${name} prop=${prop} value=${values[name]} definitions=${defs}
222
+ readOnly=${readOnly} path=${fieldPath}
223
+ onChangeValue=${v => {
224
+ const next = { ...values };
225
+ if (v === undefined || v === null || v === "") delete next[name];
226
+ else next[name] = v;
227
+ onChange(next);
228
+ }}
229
+ />
230
+ ${resolved.default !== undefined && !hasValue && html`
231
+ <div class="sf-default">Default: <code>${JSON.stringify(resolved.default)}</code></div>
232
+ `}
233
+ </div>
234
+ `;
235
+ })}
236
+ </div>
237
+ `;
238
+ }
239
+
240
+ /**
241
+ * Top-level SchemaForm component.
242
+ *
243
+ * @param {object} props
244
+ * @param {object} props.schema - JSON Schema object
245
+ * @param {object} props.values - current form values
246
+ * @param {function} props.onChange - called with updated values
247
+ * @param {boolean} props.readOnly - disable all inputs
248
+ * @param {object} props.definitions - external definitions map for $ref resolution
249
+ */
250
+ export function SchemaForm({ schema, values, onChange, readOnly, definitions }) {
251
+ if (!schema?.properties) {
252
+ return html`<div class="sf-empty">No fields defined in schema</div>`;
253
+ }
254
+
255
+ const allDefs = { ...definitions, ...schema.definitions };
256
+
257
+ return html`
258
+ <div class="sf-root">
259
+ ${schema.title && html`<div class="sf-title">${schema.title}</div>`}
260
+ ${schema.description && html`<div class="sf-schema-desc">${schema.description}</div>`}
261
+ <${SchemaFields}
262
+ schema=${schema} definitions=${allDefs}
263
+ values=${values || {}} onChange=${onChange}
264
+ readOnly=${readOnly} path=${[]}
265
+ />
266
+ </div>
267
+ `;
268
+ }
@@ -0,0 +1,248 @@
1
+ import { h } from "preact";
2
+ import { useState } from "preact/hooks";
3
+ import htm from "htm";
4
+ import { SchemaForm } from "./schema-form.js";
5
+
6
+ const html = htm.bind(h);
7
+
8
+ function stateBadge(state) {
9
+ if (!state) return html`<span class="badge badge-muted">unknown</span>`;
10
+ const s = state.toLowerCase();
11
+ if (s === "running" || s === "resolved") return html`<span class="badge badge-green">${state}</span>`;
12
+ if (s === "stopped" || s === "error" || s === "failed") return html`<span class="badge badge-red">${state}</span>`;
13
+ if (s === "initializing" || s === "created") return html`<span class="badge badge-yellow">${state}</span>`;
14
+ return html`<span class="badge badge-muted">${state}</span>`;
15
+ }
16
+
17
+ export function ServicesPanel({ data, config }) {
18
+ const [selected, setSelected] = useState(null);
19
+ const [filter, setFilter] = useState("");
20
+ const services = data || [];
21
+ const detail = selected ? services.find((s) => s.name === selected) || null : null;
22
+ const globalParams = config?.parameters || {};
23
+ const paramKeys = Object.keys(globalParams);
24
+
25
+ const filtered = services.filter(
26
+ (s) =>
27
+ (s.name || "").toLowerCase().includes(filter.toLowerCase()) ||
28
+ (s.type || "").toLowerCase().includes(filter.toLowerCase())
29
+ );
30
+
31
+ return html`
32
+ <div class="split-panel">
33
+ <div class="split-left">
34
+ <input
35
+ class="search-input"
36
+ placeholder="Filter services..."
37
+ value=${filter}
38
+ onInput=${(e) => setFilter(e.target.value)}
39
+ />
40
+ ${paramKeys.length > 0 && html`
41
+ <div
42
+ class="list-item ${selected === "__globalParams" ? "active" : ""}"
43
+ onClick=${() => setSelected("__globalParams")}
44
+ style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border);margin-bottom:0.5rem;padding-bottom:0.625rem"
45
+ >
46
+ <span style="font-weight:500">Global Parameters</span>
47
+ <span class="badge badge-muted">${paramKeys.length}</span>
48
+ </div>
49
+ `}
50
+ ${filtered.map(
51
+ (s) => html`
52
+ <div
53
+ key=${s.name}
54
+ class="list-item ${selected === s.name ? "active" : ""}"
55
+ onClick=${() => setSelected(s.name)}
56
+ style="display: flex; justify-content: space-between; align-items: center;"
57
+ >
58
+ <span>${s.name}</span>
59
+ ${stateBadge(s.state)}
60
+ </div>
61
+ `
62
+ )}
63
+ ${filtered.length === 0 && html`<div style="color: var(--text-muted); padding: 0.5rem;">No services found</div>`}
64
+ </div>
65
+ <div class="split-right">
66
+ ${selected === "__globalParams" ? html`<${GlobalParamsDetail} params=${globalParams} />`
67
+ : detail ? html`<${ServiceDetail} service=${detail} />`
68
+ : html`
69
+ <div style="color: var(--text-muted); padding: 2rem; text-align: center;">
70
+ Select a service to view details
71
+ </div>
72
+ `}
73
+ </div>
74
+ </div>
75
+ `;
76
+ }
77
+
78
+ function GlobalParamsDetail({ params }) {
79
+ const keys = Object.keys(params);
80
+ return html`
81
+ <div>
82
+ <h2 style="margin-bottom:1rem;color:var(--accent)">Global Parameters</h2>
83
+ <div class="table-container">
84
+ <table>
85
+ <thead>
86
+ <tr>
87
+ <th style="width:200px">Parameter</th>
88
+ <th>Value</th>
89
+ </tr>
90
+ </thead>
91
+ <tbody>
92
+ ${keys.map(key => html`
93
+ <tr key=${key}>
94
+ <td class="mono" style="font-weight:500">${key}</td>
95
+ <td class="mono" style="font-size:0.8125rem;word-break:break-all">
96
+ ${typeof params[key] === "object"
97
+ ? html`<pre style="margin:0;white-space:pre-wrap;font-size:0.8125rem">${JSON.stringify(params[key], null, 2)}</pre>`
98
+ : String(params[key])}
99
+ </td>
100
+ </tr>
101
+ `)}
102
+ </tbody>
103
+ </table>
104
+ </div>
105
+ </div>
106
+ `;
107
+ }
108
+
109
+ function ServiceDetail({ service }) {
110
+ const [activeTab, setActiveTab] = useState("config");
111
+ const [formValues, setFormValues] = useState({});
112
+
113
+ const config = service.configuration || {};
114
+ const configKeys = Object.keys(config).filter((k) => !k.startsWith("_"));
115
+ const schema = service.schema;
116
+
117
+ const tabs = ["config"];
118
+ if (schema) tabs.push("schema-form", "schema-json");
119
+ if (service.metrics?.length) tabs.push("metrics");
120
+
121
+ const currentTab = tabs.includes(activeTab) ? activeTab : tabs[0];
122
+
123
+ return html`
124
+ <div>
125
+ <h2 style="margin-bottom: 0.5rem; color: var(--accent);">${service.name}</h2>
126
+
127
+ <div style="display: flex; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap;">
128
+ <div style="background: var(--bg-tertiary); padding: 0.5rem 0.75rem; border-radius: 4px; font-size: 0.875rem;">
129
+ <span style="color: var(--text-muted);">Type: </span>
130
+ <span class="mono">${service.type || "unknown"}</span>
131
+ </div>
132
+ <div style="background: var(--bg-tertiary); padding: 0.5rem 0.75rem; border-radius: 4px; font-size: 0.875rem;">
133
+ <span style="color: var(--text-muted);">State: </span>
134
+ ${stateBadge(service.state)}
135
+ </div>
136
+ </div>
137
+
138
+ ${Object.keys(service.capabilities || {}).length > 0 && html`
139
+ <div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem;">
140
+ ${Object.keys(service.capabilities).map(
141
+ (c) => html`<span key=${c} class="badge badge-purple">${c}</span>`
142
+ )}
143
+ </div>
144
+ `}
145
+
146
+ <!-- Tabs -->
147
+ <div style="display:flex;gap:0;margin-bottom:0.75rem;border-bottom:1px solid var(--border)">
148
+ ${tabs.map(tab => {
149
+ const label = tab === "config" ? "Configuration" : tab === "schema-form" ? "Schema Form" : tab === "schema-json" ? "Schema JSON" : tab === "metrics" ? "Metrics" : tab;
150
+ return html`
151
+ <button key=${tab} onClick=${() => { setActiveTab(tab); if (tab === "schema-form") setFormValues({...config}); }} style="
152
+ padding:6px 16px;
153
+ background:${currentTab === tab ? "var(--bg-tertiary)" : "transparent"};
154
+ color:${currentTab === tab ? "var(--accent)" : "var(--text-muted)"};
155
+ border:none;
156
+ border-bottom:2px solid ${currentTab === tab ? "var(--accent)" : "transparent"};
157
+ cursor:pointer;font-size:0.875rem;
158
+ font-weight:${currentTab === tab ? "600" : "400"};
159
+ ">${label}</button>
160
+ `;
161
+ })}
162
+ </div>
163
+
164
+ <!-- Tab Content -->
165
+ ${currentTab === "config" && html`
166
+ <div>
167
+ ${configKeys.length > 0 ? html`
168
+ <div class="table-container">
169
+ <table>
170
+ <thead>
171
+ <tr>
172
+ <th style="width: 200px;">Parameter</th>
173
+ <th>Value</th>
174
+ </tr>
175
+ </thead>
176
+ <tbody>
177
+ ${configKeys.map(
178
+ (key) => html`
179
+ <tr key=${key}>
180
+ <td class="mono" style="font-weight: 500;">${key}</td>
181
+ <td class="mono" style="font-size: 0.8125rem; word-break: break-all;">
182
+ ${typeof config[key] === "object"
183
+ ? html`<pre style="margin: 0; white-space: pre-wrap; font-size: 0.8125rem;">${JSON.stringify(config[key], null, 2)}</pre>`
184
+ : String(config[key])}
185
+ </td>
186
+ </tr>
187
+ `
188
+ )}
189
+ </tbody>
190
+ </table>
191
+ </div>
192
+ ` : html`<div style="color: var(--text-muted); padding: 0.5rem;">No configuration parameters</div>`}
193
+ </div>
194
+ `}
195
+
196
+ ${currentTab === "schema-form" && schema && html`
197
+ <div>
198
+ <${SchemaForm} schema=${schema} values=${formValues} onChange=${setFormValues} />
199
+ ${Object.keys(formValues).length > 0 && html`
200
+ <div style="margin-top:0.75rem">
201
+ <strong style="color:var(--text-muted);font-size:0.75rem;text-transform:uppercase">Configuration Preview</strong>
202
+ <pre class="mono" style="
203
+ background:var(--bg-secondary);padding:0.75rem;border-radius:4px;
204
+ margin-top:0.25rem;font-size:0.8125rem;overflow:auto;max-height:300px;
205
+ ">${JSON.stringify(formValues, null, 2)}</pre>
206
+ </div>
207
+ `}
208
+ </div>
209
+ `}
210
+
211
+ ${currentTab === "schema-json" && schema && html`
212
+ <div>
213
+ <pre class="mono" style="
214
+ background:var(--bg-secondary);padding:1rem;border-radius:4px;
215
+ overflow:auto;max-height:500px;font-size:0.8125rem;line-height:1.5;
216
+ ">${JSON.stringify(schema, null, 2)}</pre>
217
+ </div>
218
+ `}
219
+
220
+ ${currentTab === "metrics" && service.metrics && html`
221
+ <div>
222
+ <div class="table-container">
223
+ <table>
224
+ <thead>
225
+ <tr>
226
+ <th>Name</th>
227
+ <th>Type</th>
228
+ <th>Help</th>
229
+ <th style="text-align:right">Value</th>
230
+ </tr>
231
+ </thead>
232
+ <tbody>
233
+ ${service.metrics.map(m => html`
234
+ <tr key=${m.name}>
235
+ <td class="mono" style="font-weight:500">${m.name}</td>
236
+ <td><span class="badge ${m.type === "counter" ? "badge-blue" : m.type === "gauge" ? "badge-green" : m.type === "histogram" ? "badge-purple" : "badge-muted"}">${m.type}</span></td>
237
+ <td style="color:var(--text-muted);font-size:0.8125rem">${m.help || "-"}</td>
238
+ <td class="mono" style="text-align:right;font-weight:500">${m.values?.length ? m.values.map(v => v.value).reduce((a, b) => a + b, 0).toLocaleString() : "0"}</td>
239
+ </tr>
240
+ `)}
241
+ </tbody>
242
+ </table>
243
+ </div>
244
+ </div>
245
+ `}
246
+ </div>
247
+ `;
248
+ }
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Webda Debug Dashboard</title>
7
+ <link rel="stylesheet" href="/styles.css" />
8
+ <script type="importmap">
9
+ {
10
+ "imports": {
11
+ "preact": "/vendor/preact.js",
12
+ "preact/hooks": "/vendor/preact-hooks.js",
13
+ "htm": "/vendor/htm.js"
14
+ }
15
+ }
16
+ </script>
17
+ </head>
18
+ <body>
19
+ <div id="app"></div>
20
+ <script type="module" src="/app.js"></script>
21
+ </body>
22
+ </html>
package/webui/logo.svg ADDED
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M10,0h118.51v128H0V10C0,4.48,4.48,0,10,0Z" style="fill:#4557a4; opacity:.5;"/><path d="M128.51,0h127.49v128h-117.49c-5.52,0-10-4.48-10-10V0h0Z" transform="translate(384.51 128) rotate(-180)" style="fill:#f7992c; opacity:.5;"/><path d="M0,128h128.51v128H10C4.48,256,0,251.52,0,246v-118H0Z" style="fill:#81bf6b; opacity:.5;"/><path d="M138.51,128h117.49v128h-127.49v-118c0-5.52,4.48-10,10-10Z" transform="translate(384.51 384) rotate(180)" style="fill:#f4f4f4; opacity:.5;"/><g id="b"><path d="M128.51,83.92v42.96h.1v-40.99c0-.69,0-1.38-.1-1.97Z" style="fill:#fff;"/><path d="M128.61,4h-.1v43.45c.1-.59.1-1.28.1-2.06V4Z" style="fill:#fff;"/><path d="M205.39,115.47c2.75-2.75,6.39-5.01,6.39-11.8,0-7.57-7.67-17.2-20.74-17.2s-20.74,9.63-20.74,17.2c0,6.78,3.64,9.04,6.39,11.8,4.23,4.23,3.93,9.24-.39,11.4h29.59c-4.42-2.16-4.72-7.18-.49-11.4Z" style="fill:#fff;"/><path d="M128.61,127.96h42.17c2.26,0,4.03-.39,5.41-1.08h-47.58v1.08Z" style="fill:#f7992c;"/><path d="M251.39,127.96v-1.08h-45.81c1.38.69,3.24,1.08,5.41,1.08h40.4Z" style="fill:#fff;"/><path d="M175.9,126.88h-47.38v1.08h41.98c2.16,0,4.03-.39,5.41-1.08Z" style="fill:#fff;"/><path d="M140.9,204.73c2.75,2.75,5.01,6.39,11.8,6.39,7.57,0,17.2-7.67,17.2-20.74s-9.63-20.74-17.2-20.74c-6.78,0-9.04,3.64-11.8,6.39-5.21,5.21-11.5,3.54-12.39-3.83v36.47c.98-7.47,7.27-9.14,12.39-3.93Z" style="fill:#fff;"/><path d="M128.42,127.96v42.17c0,.69.1,1.38.1,2.06v-44.14h-.1v-.1Z" style="fill:#fff;"/><path d="M128.42,249.76h.1v-41.19c-.1.59-.1,1.28-.1,2.06v39.12Z" style="fill:#fff;"/><path d="M51.25,126.88H5.63v1.08h40.11c2.26,0,4.03-.39,5.51-1.08Z" style="fill:#4557a4;"/><path d="M128.42,127.96v42.17c0,.69.1,1.38.1,2.06v-45.22h-47.68c1.38.69,3.24,1.08,5.41,1.08h42.17v-.1Z" style="fill:#fff;"/><path d="M117.11,79.1c-.39.29-.69.59-.98.88.29-.29.69-.59.98-.88Z" style="fill:#f7992c;"/><path d="M118.19,78.32c-.2.2-.49.29-.69.49.2-.1.39-.29.69-.49Z" style="fill:#f7992c;"/><path d="M116.13,51.28c.29.29.69.69.98.88-.29-.29-.69-.59-.98-.88Z" style="fill:#f7992c;"/><path d="M119.17,77.73c-.2.1-.39.2-.59.29.2,0,.39-.1.59-.29Z" style="fill:#f7992c;"/><path d="M118.58,53.15l.59.29c-.2,0-.39-.1-.59-.29Z" style="fill:#f7992c;"/><path d="M120.75,54.03c.2,0,.29.1.49.1-.1,0-.29,0-.49-.1Z" style="fill:#f7992c;"/><path d="M117.5,52.46c.2.2.49.29.69.49-.29-.2-.49-.29-.69-.49Z" style="fill:#f7992c;"/><path d="M119.67,53.74c.2.1.39.1.49.2-.1-.1-.29-.1-.49-.2Z" style="fill:#f7992c;"/><path d="M124.88,77.63c-.1,0-.2-.1-.2-.1q.1.1.2.1Z" style="fill:#f7992c;"/><path d="M128.61,127.96h41.78c2.26,0,4.03-.39,5.41-1.08h-47.28v1.08h.1Z" style="fill:#f7992c;"/><path d="M128.61,85.88c0-.39,0-.88-.1-1.28.1.49.1.88.1,1.28Z" style="fill:#f7992c;"/><path d="M80.74,115.47c2.75-2.75,6.39-5.01,6.39-11.8,0-7.57-7.67-17.2-20.74-17.2h-.1c12.98.1,20.64,9.63,20.64,17.2,0,6.78-3.64,9.04-6.39,11.8-4.23,4.23-3.93,9.24.39,11.4,1.38.69,3.24,1.08,5.41,1.08h.29c-9.14,0-11.6-6.88-5.9-12.48Z" style="fill:#f7992c;"/><path d="M128.42,170.13c0,.69.1,1.38.1,2.06v-44.14h-.1v42.07Z" style="fill:#f7992c;"/><path d="M123.8,54.03c.1,0,.2,0,.2-.1,0,.1-.1.1-.2.1Z" style="fill:#f7992c;"/><path d="M120.26,77.33c-.2.1-.39.1-.59.2.2,0,.39-.1.59-.2Z" style="fill:#f7992c;"/><path d="M121.24,77.14c-.2,0-.39.1-.49.1.2-.1.39-.1.49-.1Z" style="fill:#f7992c;"/><path d="M123.21,77.14c-.1,0-.29,0-.39-.1.1,0,.29,0,.39.1Z" style="fill:#f7992c;"/><path d="M124.09,77.33c-.1,0-.2-.1-.29-.1.1,0,.2,0,.29.1Z" style="fill:#f7992c;"/><path d="M140.9,204.73c-5.21-5.21-11.5-3.54-12.39,3.83-.1.59-.1,1.28-.1,2.06v39.12h.1v-41.19c.98-7.37,7.27-9.04,12.39-3.83Z" style="fill:#f7992c;"/><path d="M205.39,115.47c2.75-2.75,6.39-5.01,6.39-11.8,0-7.57-7.67-17.2-20.74-17.2h-.1c12.98.1,20.64,9.63,20.64,17.2,0,6.78-3.64,9.04-6.39,11.8-4.23,4.23-3.93,9.24.39,11.4,1.38.69,3.24,1.08,5.41,1.08h40.4v-1.08h-45.61c-4.33-2.16-4.62-7.18-.39-11.4Z" style="fill:#f7992c;"/><path d="M128.61,46.66c0-.39.1-.79.1-1.28-.1.39-.1.88-.1,1.28Z" style="fill:#f7992c;"/><path d="M124.68,53.74s.1,0,.1-.1q0,.1-.1.1Z" style="fill:#f7992c;"/><path d="M51.25,126.88c4.33-2.16,4.72-7.18.39-11.4-2.75-2.75-6.39-5.01-6.39-11.8,0-7.57,7.67-17.2,20.74-17.2h.2c13.07,0,20.74,9.63,20.74,17.2,0,6.78-3.64,9.04-6.39,11.8-5.7,5.6-3.24,12.58,5.9,12.58h41.98v-42.17c0-.39,0-.88-.1-1.28,0-.29,0-.49-.1-.79-.39-2.75-1.47-4.62-2.95-5.8l-.1-.1c-.2-.1-.39-.29-.59-.39-.1,0-.2-.1-.2-.1-.2-.1-.39-.2-.59-.2-.1,0-.2-.1-.29-.1-.2,0-.39-.1-.59-.1-.1,0-.29,0-.39-.1h-.98c-.2,0-.29,0-.49.1-.2,0-.39.1-.49.1-.2,0-.29.1-.49.1-.2.1-.39.1-.59.2s-.29.1-.49.2-.39.2-.59.29c-.1.1-.29.2-.39.2-.2.2-.49.29-.69.49-.1.1-.2.2-.39.29-.39.29-.69.59-.98.88-2.75,2.75-5.01,6.39-11.8,6.39-7.57,0-17.2-7.67-17.2-20.74s9.63-20.74,17.2-20.74c6.78,0,9.04,3.64,11.8,6.39.29.29.69.69.98.88.1.1.2.2.39.29.2.2.49.29.69.49.2.1.29.2.49.29.2.1.39.2.59.29.2.1.29.1.49.2.2.1.39.1.49.2.2.1.39.1.49.1.2,0,.29.1.49.1s.39.1.59.1h1.28c.2,0,.39-.1.69-.1.1,0,.2,0,.2-.1.2-.1.49-.2.69-.29,0,0,.1,0,.1-.1,1.87-.98,3.24-3.05,3.74-6.29,0-.29.1-.49.1-.79,0-.39.1-.79.1-1.28V4H15.46c-5.43,0-9.83,4.4-9.83,9.83v114.13h40.11c2.26,0,4.03-.39,5.51-1.08Z" style="fill:#4557a4;"/><path d="M251.39,127.96h-40.4c-2.26,0-4.03-.39-5.41-1.08-4.33-2.16-4.72-7.18-.39-11.4,2.75-2.75,6.39-5.01,6.39-11.8,0-7.47-7.67-17.1-20.64-17.2-12.98.1-20.64,9.63-20.64,17.2,0,6.78,3.64,9.04,6.39,11.8,4.23,4.23,3.93,9.24-.39,11.4h-.29c-1.38.69-3.24,1.08-5.41,1.08h-42.07v44.14c.98,7.37,7.27,9.04,12.39,3.83,2.75-2.75,5.01-6.39,11.8-6.39,7.57,0,17.2,7.67,17.2,20.74s-9.63,20.74-17.2,20.74c-6.78,0-9.04-3.64-11.8-6.39-5.21-5.21-11.5-3.54-12.39,3.83v41.29h113.05c5.43,0,9.83-4.4,9.83-9.83v-111.97Z" style="fill:#f4f4f4;"/><path d="M128.51,208.57c.98-7.37,7.27-9.04,12.39-3.83,2.75,2.75,5.01,6.39,11.8,6.39,7.57,0,17.2-7.67,17.2-20.74s-9.63-20.74-17.2-20.74c-6.78,0-9.04,3.64-11.8,6.39-5.21,5.21-11.5,3.54-12.39-3.83h0c-.1-.59-.1-1.28-.1-2.06v-42.07h-42.17c-2.26,0-4.03-.39-5.41-1.08-4.33-2.16-4.72-7.18-.39-11.4,2.75-2.75,6.39-5.01,6.39-11.8,0-7.47-7.67-17.1-20.64-17.2-12.98.1-20.64,9.63-20.64,17.2,0,6.78,3.64,9.04,6.39,11.8,5.7,5.6,3.24,12.58-5.9,12.58H5.63v111.77c0,5.43,4.4,9.83,9.83,9.83h112.95v-39.12c0-.69,0-1.38.1-2.06Z" style="fill:#7fbf6b;"/><path d="M45.25,103.58c0,6.78,3.64,9.04,6.39,11.8,4.23,4.23,3.93,9.24-.39,11.4-1.38.69-3.24,1.08-5.41,1.08h.29c9.14,0,11.5-6.88,5.9-12.58-2.75-2.75-6.39-5.01-6.39-11.8,0-7.47,7.67-17.1,20.64-17.2h-.1c-13.27.2-20.94,9.83-20.94,17.3Z" style="fill:#7fbf6b;"/><path d="M140.9,175.93c-5.21,5.21-11.5,3.54-12.39-3.83h0c.98,7.47,7.27,9.04,12.39,3.83Z" style="fill:#7fbf6b;"/><path d="M128.61,4v41.39c0,.39,0,.88-.1,1.28-.29,3.64-1.77,6-3.74,7.08,0,0-.1,0-.1.1-.2.1-.49.2-.69.29-.1,0-.2.1-.2.1-.2.1-.39.1-.69.1h-1.28c-.2,0-.39,0-.59-.1-.2,0-.29-.1-.49-.1s-.39-.1-.49-.1c-.2-.1-.39-.1-.49-.2-.2-.1-.29-.1-.49-.2l-.59-.29c-.2-.1-.29-.2-.49-.29s-.49-.29-.69-.49c-.1-.1-.2-.2-.39-.29-.39-.29-.69-.59-.98-.88-2.75-2.75-5.01-6.39-11.8-6.39-7.57,0-17.2,7.67-17.2,20.74s9.63,20.74,17.2,20.74c6.78,0,9.04-3.64,11.8-6.39.29-.29.69-.69.98-.88.1-.1.2-.2.39-.29.2-.2.49-.39.69-.49.1-.1.29-.2.39-.2.2-.1.39-.2.59-.29.2-.1.29-.1.49-.2s.39-.2.59-.2c.2-.1.29-.1.49-.1s.39-.1.49-.1c.2,0,.29,0,.49-.1h.98c.1,0,.29,0,.39.1.2,0,.39.1.59.1.1,0,.2.1.29.1.2.1.39.1.59.2.1,0,.2.1.2.1.2.1.39.2.59.39l.1.1c1.47,1.08,2.56,3.05,2.95,5.8h0c0,.29.1.49.1.79,0,.39.1.79.1,1.28v40.7h47.28c4.33-2.16,4.72-7.18.39-11.4-2.75-2.75-6.39-5.01-6.39-11.8,0-7.57,7.67-17.2,20.74-17.2h.2c13.07,0,20.74,9.63,20.74,17.2,0,6.78-3.64,9.04-6.39,11.8-4.23,4.23-3.93,9.24.39,11.4h45.81V13.83c0-5.43-4.4-9.83-9.83-9.83h-112.95Z" style="fill:#f7992c;"/><path d="M125.47,78.02c-.2-.1-.39-.29-.59-.39.2.2.39.29.59.39Z" style="fill:#f7992c;"/><path d="M124.68,77.53c-.2-.1-.39-.2-.59-.2.2.1.39.1.59.2Z" style="fill:#f7992c;"/><path d="M123.21,54.23c.2,0,.39-.1.69-.1-.29,0-.49,0-.69.1Z" style="fill:#f7992c;"/><path d="M128.61,46.66c0,.29,0,.49-.1.79-.39,3.15-1.87,5.31-3.74,6.29,2.06-1.08,3.54-3.44,3.83-7.08Z" style="fill:#f7992c;"/><path d="M123.99,54.03c.2-.1.49-.2.69-.29-.2.1-.39.2-.69.29Z" style="fill:#f7992c;"/><path d="M118.19,52.95c.2.1.29.2.49.29-.2-.1-.39-.2-.49-.29Z" style="fill:#f7992c;"/><path d="M120.26,53.94c.2.1.39.1.49.1-.1,0-.29,0-.49-.1Z" style="fill:#f7992c;"/><path d="M117.11,52.17c.1.1.2.2.39.29-.1-.1-.29-.2-.39-.29Z" style="fill:#f7992c;"/><path d="M128.61,84.71c0-.29,0-.49-.1-.79,0,.2.1.49.1.79Z" style="fill:#f7992c;"/><path d="M119.17,53.54c.2.1.29.1.49.2-.1-.1-.29-.1-.49-.2Z" style="fill:#f7992c;"/><path d="M121.24,54.13c.2,0,.39.1.59.1-.2,0-.39,0-.59-.1Z" style="fill:#f7992c;"/><path d="M125.56,78.12c1.47,1.08,2.56,3.05,2.95,5.8-.39-2.75-1.47-4.72-2.95-5.8Z" style="fill:#f7992c;"/><path d="M118.58,78.12c-.1.1-.29.2-.39.2.1,0,.29-.1.39-.2Z" style="fill:#f7992c;"/><path d="M117.5,78.91c-.1.1-.2.2-.39.29.1-.2.2-.29.39-.29Z" style="fill:#f7992c;"/><path d="M123.8,77.23c-.2,0-.39-.1-.59-.1.2,0,.39,0,.59.1Z" style="fill:#f7992c;"/><path d="M119.67,77.63c-.2.1-.29.1-.49.2.2-.1.39-.2.49-.2Z" style="fill:#f7992c;"/><path d="M121.83,77.04c-.2,0-.29,0-.49.1.1-.1.29-.1.49-.1Z" style="fill:#f7992c;"/><path d="M120.75,77.23c-.2,0-.29.1-.49.1.2,0,.29-.1.49-.1Z" style="fill:#f7992c;"/><path d="M169.9,103.58c0,6.78,3.64,9.04,6.39,11.8,4.23,4.23,3.93,9.24-.39,11.4h.29c4.33-2.16,4.72-7.18.39-11.4-2.75-2.75-6.39-5.01-6.39-11.8,0-7.47,7.67-17.1,20.64-17.2h-.1c-13.07.1-20.84,9.73-20.84,17.2Z" style="fill:#f7992c;"/><path d="M103.84,98.86s13.27-26.54,36.96-26.54,42.57,17.99,41.68,47.38c14.16,1.87,27.43,13.27,25.56,32.15-1.87,18.87-13.27,25.56-20.84,27.43s-110.79,0-110.79,0c0,0-25.56-.98-26.54-28.41-.98-27.43,19.86-34.11,19.86-34.11,0,0,2.85-16.12,13.27-20.84,10.42-4.72,20.84,2.95,20.84,2.95Z" style="fill:#fff; stroke:#464646; stroke-miterlimit:10; stroke-width:.75px;"/></g></svg>