@chrysb/alphaclaw 0.7.0 → 0.7.1
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/lib/public/css/cron.css +26 -17
- package/lib/public/css/theme.css +14 -0
- package/lib/public/js/components/cron-tab/cron-calendar.js +17 -12
- package/lib/public/js/components/cron-tab/cron-job-list.js +11 -1
- package/lib/public/js/components/cron-tab/index.js +16 -2
- package/lib/public/js/components/icons.js +11 -0
- package/lib/public/js/components/routes/watchdog-route.js +1 -1
- package/lib/public/js/components/watchdog-tab/console/index.js +115 -0
- package/lib/public/js/components/watchdog-tab/console/use-console.js +137 -0
- package/lib/public/js/components/watchdog-tab/helpers.js +106 -0
- package/lib/public/js/components/watchdog-tab/incidents/index.js +56 -0
- package/lib/public/js/components/watchdog-tab/incidents/use-incidents.js +33 -0
- package/lib/public/js/components/watchdog-tab/index.js +84 -0
- package/lib/public/js/components/watchdog-tab/resource-bar.js +76 -0
- package/lib/public/js/components/watchdog-tab/resources/index.js +85 -0
- package/lib/public/js/components/watchdog-tab/resources/use-resources.js +13 -0
- package/lib/public/js/components/watchdog-tab/settings/index.js +44 -0
- package/lib/public/js/components/watchdog-tab/settings/use-settings.js +117 -0
- package/lib/public/js/components/watchdog-tab/terminal/index.js +20 -0
- package/lib/public/js/components/watchdog-tab/terminal/use-terminal.js +263 -0
- package/lib/public/js/components/watchdog-tab/use-watchdog-tab.js +55 -0
- package/lib/public/js/lib/api.js +39 -0
- package/lib/server/init/register-server-routes.js +239 -0
- package/lib/server/init/runtime-init.js +44 -0
- package/lib/server/init/server-lifecycle.js +55 -0
- package/lib/server/routes/watchdog.js +62 -0
- package/lib/server/watchdog-terminal-ws.js +114 -0
- package/lib/server/watchdog-terminal.js +262 -0
- package/lib/server.js +89 -215
- package/package.json +3 -2
- package/lib/public/js/components/watchdog-tab.js +0 -535
|
@@ -1,535 +0,0 @@
|
|
|
1
|
-
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
-
import htm from "https://esm.sh/htm";
|
|
4
|
-
import {
|
|
5
|
-
fetchWatchdogEvents,
|
|
6
|
-
fetchWatchdogLogs,
|
|
7
|
-
fetchWatchdogResources,
|
|
8
|
-
fetchWatchdogSettings,
|
|
9
|
-
updateWatchdogSettings,
|
|
10
|
-
triggerWatchdogRepair,
|
|
11
|
-
} from "../lib/api.js";
|
|
12
|
-
import { usePolling } from "../hooks/usePolling.js";
|
|
13
|
-
import { Gateway } from "./gateway.js";
|
|
14
|
-
import { InfoTooltip } from "./info-tooltip.js";
|
|
15
|
-
import { ToggleSwitch } from "./toggle-switch.js";
|
|
16
|
-
import { showToast } from "./toast.js";
|
|
17
|
-
import { readUiSettings, writeUiSettings } from "../lib/ui-settings.js";
|
|
18
|
-
|
|
19
|
-
const html = htm.bind(h);
|
|
20
|
-
const kWatchdogLogsPanelHeightUiSettingKey = "watchdogLogsPanelHeightPx";
|
|
21
|
-
const kWatchdogLogsPanelDefaultHeightPx = 288;
|
|
22
|
-
const kWatchdogLogsPanelMinHeightPx = 160;
|
|
23
|
-
const clampWatchdogLogsPanelHeight = (value) => {
|
|
24
|
-
const parsed = Number(value);
|
|
25
|
-
const normalized = Number.isFinite(parsed)
|
|
26
|
-
? Math.round(parsed)
|
|
27
|
-
: kWatchdogLogsPanelDefaultHeightPx;
|
|
28
|
-
return Math.max(kWatchdogLogsPanelMinHeightPx, normalized);
|
|
29
|
-
};
|
|
30
|
-
const readCssHeightPx = (element) => {
|
|
31
|
-
if (!element) return 0;
|
|
32
|
-
const computedHeight = Number.parseFloat(window.getComputedStyle(element).height || "0");
|
|
33
|
-
return Number.isFinite(computedHeight) ? computedHeight : 0;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const formatBytes = (bytes) => {
|
|
37
|
-
if (bytes == null) return "—";
|
|
38
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
39
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
40
|
-
if (bytes < 1024 * 1024 * 1024)
|
|
41
|
-
return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
|
|
42
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const barColor = (percent) => {
|
|
46
|
-
if (percent == null) return "bg-gray-600";
|
|
47
|
-
return "bg-cyan-400";
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const ResourceBar = ({
|
|
51
|
-
label,
|
|
52
|
-
percent,
|
|
53
|
-
detail,
|
|
54
|
-
segments = null,
|
|
55
|
-
expanded = false,
|
|
56
|
-
onToggle = null,
|
|
57
|
-
}) => html`
|
|
58
|
-
<div
|
|
59
|
-
class=${onToggle ? "cursor-pointer group" : ""}
|
|
60
|
-
onclick=${onToggle || undefined}
|
|
61
|
-
>
|
|
62
|
-
<span
|
|
63
|
-
class=${`text-xs text-gray-400 ${onToggle ? "group-hover:text-gray-200 transition-colors" : ""}`}
|
|
64
|
-
>${label}</span
|
|
65
|
-
>
|
|
66
|
-
<div
|
|
67
|
-
class=${`h-0.5 w-full bg-white/15 rounded-full overflow-hidden mt-1.5 flex ${onToggle ? "group-hover:bg-white/10 transition-colors" : ""}`}
|
|
68
|
-
>
|
|
69
|
-
${expanded && segments
|
|
70
|
-
? segments.map(
|
|
71
|
-
(seg) => html`
|
|
72
|
-
<div
|
|
73
|
-
class="h-full"
|
|
74
|
-
style=${{
|
|
75
|
-
width: `${Math.min(100, seg.percent ?? 0)}%`,
|
|
76
|
-
backgroundColor: seg.color,
|
|
77
|
-
transition:
|
|
78
|
-
"width 0.8s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.5s ease",
|
|
79
|
-
}}
|
|
80
|
-
></div>
|
|
81
|
-
`,
|
|
82
|
-
)
|
|
83
|
-
: html`
|
|
84
|
-
<div
|
|
85
|
-
class=${`h-full rounded-full ${barColor(percent)}`}
|
|
86
|
-
style=${{
|
|
87
|
-
width: `${Math.min(100, percent ?? 0)}%`,
|
|
88
|
-
transition:
|
|
89
|
-
"width 0.8s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.5s ease",
|
|
90
|
-
}}
|
|
91
|
-
></div>
|
|
92
|
-
`}
|
|
93
|
-
</div>
|
|
94
|
-
<div class="flex flex-wrap items-center gap-x-3 mt-2.5">
|
|
95
|
-
<span class="text-xs text-gray-500 font-mono flex-1">${detail}</span>
|
|
96
|
-
${expanded &&
|
|
97
|
-
segments &&
|
|
98
|
-
segments
|
|
99
|
-
.filter((s) => s.label)
|
|
100
|
-
.map(
|
|
101
|
-
(seg) => html`
|
|
102
|
-
<span
|
|
103
|
-
class="inline-flex items-center gap-1 text-xs text-gray-500 font-mono"
|
|
104
|
-
>
|
|
105
|
-
<span
|
|
106
|
-
class="inline-block w-1.5 h-1.5 rounded-full"
|
|
107
|
-
style=${{ backgroundColor: seg.color }}
|
|
108
|
-
></span>
|
|
109
|
-
${seg.label}
|
|
110
|
-
</span>
|
|
111
|
-
`,
|
|
112
|
-
)}
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
`;
|
|
116
|
-
|
|
117
|
-
const getIncidentStatusTone = (event) => {
|
|
118
|
-
const eventType = String(event?.eventType || "")
|
|
119
|
-
.trim()
|
|
120
|
-
.toLowerCase();
|
|
121
|
-
const status = String(event?.status || "")
|
|
122
|
-
.trim()
|
|
123
|
-
.toLowerCase();
|
|
124
|
-
if (status === "failed") {
|
|
125
|
-
return {
|
|
126
|
-
dotClass: "bg-red-500/90",
|
|
127
|
-
label: "Failed",
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
if (status === "ok" && eventType === "health_check") {
|
|
131
|
-
return {
|
|
132
|
-
dotClass: "bg-green-500/90",
|
|
133
|
-
label: "Healthy",
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
if (status === "warn" || status === "warning") {
|
|
137
|
-
return {
|
|
138
|
-
dotClass: "bg-yellow-400/90",
|
|
139
|
-
label: "Warning",
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
return {
|
|
143
|
-
dotClass: "bg-gray-500/70",
|
|
144
|
-
label: "Unknown",
|
|
145
|
-
};
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
export const WatchdogTab = ({
|
|
149
|
-
gatewayStatus = null,
|
|
150
|
-
openclawVersion = null,
|
|
151
|
-
watchdogStatus = null,
|
|
152
|
-
onRefreshStatuses = () => {},
|
|
153
|
-
restartingGateway = false,
|
|
154
|
-
onRestartGateway,
|
|
155
|
-
restartSignal = 0,
|
|
156
|
-
openclawUpdateInProgress = false,
|
|
157
|
-
onOpenclawVersionActionComplete = () => {},
|
|
158
|
-
onOpenclawUpdate,
|
|
159
|
-
}) => {
|
|
160
|
-
const eventsPoll = usePolling(() => fetchWatchdogEvents(20), 15000);
|
|
161
|
-
const resourcesPoll = usePolling(() => fetchWatchdogResources(), 5000);
|
|
162
|
-
const [memoryExpanded, setMemoryExpanded] = useState(false);
|
|
163
|
-
const [settings, setSettings] = useState({
|
|
164
|
-
autoRepair: false,
|
|
165
|
-
notificationsEnabled: true,
|
|
166
|
-
});
|
|
167
|
-
const [savingSettings, setSavingSettings] = useState(false);
|
|
168
|
-
const [repairing, setRepairing] = useState(false);
|
|
169
|
-
const [logs, setLogs] = useState("");
|
|
170
|
-
const [loadingLogs, setLoadingLogs] = useState(true);
|
|
171
|
-
const [stickToBottom, setStickToBottom] = useState(true);
|
|
172
|
-
const [logsPanelHeightPx, setLogsPanelHeightPx] = useState(() => {
|
|
173
|
-
const settings = readUiSettings();
|
|
174
|
-
return clampWatchdogLogsPanelHeight(settings?.[kWatchdogLogsPanelHeightUiSettingKey]);
|
|
175
|
-
});
|
|
176
|
-
const logsRef = useRef(null);
|
|
177
|
-
|
|
178
|
-
const currentWatchdogStatus = watchdogStatus || {};
|
|
179
|
-
const events = eventsPoll.data?.events || [];
|
|
180
|
-
const isRepairInProgress =
|
|
181
|
-
repairing || !!currentWatchdogStatus?.operationInProgress;
|
|
182
|
-
|
|
183
|
-
useEffect(() => {
|
|
184
|
-
let active = true;
|
|
185
|
-
const loadSettings = async () => {
|
|
186
|
-
try {
|
|
187
|
-
const data = await fetchWatchdogSettings();
|
|
188
|
-
if (!active) return;
|
|
189
|
-
setSettings(
|
|
190
|
-
data.settings || {
|
|
191
|
-
autoRepair: false,
|
|
192
|
-
notificationsEnabled: true,
|
|
193
|
-
},
|
|
194
|
-
);
|
|
195
|
-
} catch (err) {
|
|
196
|
-
if (!active) return;
|
|
197
|
-
showToast(err.message || "Could not load watchdog settings", "error");
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
loadSettings();
|
|
201
|
-
return () => {
|
|
202
|
-
active = false;
|
|
203
|
-
};
|
|
204
|
-
}, []);
|
|
205
|
-
|
|
206
|
-
useEffect(() => {
|
|
207
|
-
let active = true;
|
|
208
|
-
let timer = null;
|
|
209
|
-
const pollLogs = async () => {
|
|
210
|
-
try {
|
|
211
|
-
const text = await fetchWatchdogLogs(65536);
|
|
212
|
-
if (!active) return;
|
|
213
|
-
setLogs(text || "");
|
|
214
|
-
setLoadingLogs(false);
|
|
215
|
-
} catch (err) {
|
|
216
|
-
if (!active) return;
|
|
217
|
-
setLoadingLogs(false);
|
|
218
|
-
}
|
|
219
|
-
if (!active) return;
|
|
220
|
-
timer = setTimeout(pollLogs, 3000);
|
|
221
|
-
};
|
|
222
|
-
pollLogs();
|
|
223
|
-
return () => {
|
|
224
|
-
active = false;
|
|
225
|
-
if (timer) clearTimeout(timer);
|
|
226
|
-
};
|
|
227
|
-
}, []);
|
|
228
|
-
|
|
229
|
-
useEffect(() => {
|
|
230
|
-
const el = logsRef.current;
|
|
231
|
-
if (!el || !stickToBottom) return;
|
|
232
|
-
el.scrollTop = el.scrollHeight;
|
|
233
|
-
}, [logs, stickToBottom]);
|
|
234
|
-
|
|
235
|
-
useEffect(() => {
|
|
236
|
-
const logsElement = logsRef.current;
|
|
237
|
-
if (!logsElement || typeof ResizeObserver === "undefined") return () => {};
|
|
238
|
-
let saveTimer = null;
|
|
239
|
-
const observer = new ResizeObserver((entries) => {
|
|
240
|
-
const entry = entries?.[0];
|
|
241
|
-
const nextHeight = clampWatchdogLogsPanelHeight(readCssHeightPx(entry?.target));
|
|
242
|
-
setLogsPanelHeightPx((currentValue) =>
|
|
243
|
-
Math.abs(currentValue - nextHeight) >= 1 ? nextHeight : currentValue
|
|
244
|
-
);
|
|
245
|
-
if (saveTimer) window.clearTimeout(saveTimer);
|
|
246
|
-
saveTimer = window.setTimeout(() => {
|
|
247
|
-
const settings = readUiSettings();
|
|
248
|
-
settings[kWatchdogLogsPanelHeightUiSettingKey] = nextHeight;
|
|
249
|
-
writeUiSettings(settings);
|
|
250
|
-
}, 120);
|
|
251
|
-
});
|
|
252
|
-
observer.observe(logsElement);
|
|
253
|
-
return () => {
|
|
254
|
-
observer.disconnect();
|
|
255
|
-
if (saveTimer) window.clearTimeout(saveTimer);
|
|
256
|
-
};
|
|
257
|
-
}, []);
|
|
258
|
-
|
|
259
|
-
useEffect(() => {
|
|
260
|
-
if (!restartSignal) return;
|
|
261
|
-
onRefreshStatuses();
|
|
262
|
-
eventsPoll.refresh();
|
|
263
|
-
const t1 = setTimeout(() => {
|
|
264
|
-
onRefreshStatuses();
|
|
265
|
-
eventsPoll.refresh();
|
|
266
|
-
}, 1200);
|
|
267
|
-
const t2 = setTimeout(() => {
|
|
268
|
-
onRefreshStatuses();
|
|
269
|
-
eventsPoll.refresh();
|
|
270
|
-
}, 3500);
|
|
271
|
-
return () => {
|
|
272
|
-
clearTimeout(t1);
|
|
273
|
-
clearTimeout(t2);
|
|
274
|
-
};
|
|
275
|
-
}, [restartSignal, onRefreshStatuses, eventsPoll.refresh]);
|
|
276
|
-
|
|
277
|
-
const onToggleAutoRepair = async (nextValue) => {
|
|
278
|
-
if (savingSettings) return;
|
|
279
|
-
setSavingSettings(true);
|
|
280
|
-
try {
|
|
281
|
-
const data = await updateWatchdogSettings({ autoRepair: !!nextValue });
|
|
282
|
-
setSettings(
|
|
283
|
-
data.settings || {
|
|
284
|
-
...settings,
|
|
285
|
-
autoRepair: !!nextValue,
|
|
286
|
-
},
|
|
287
|
-
);
|
|
288
|
-
onRefreshStatuses();
|
|
289
|
-
showToast(`Auto-repair ${nextValue ? "enabled" : "disabled"}`, "success");
|
|
290
|
-
} catch (err) {
|
|
291
|
-
showToast(err.message || "Could not update auto-repair", "error");
|
|
292
|
-
} finally {
|
|
293
|
-
setSavingSettings(false);
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
const onToggleNotifications = async (nextValue) => {
|
|
298
|
-
if (savingSettings) return;
|
|
299
|
-
setSavingSettings(true);
|
|
300
|
-
try {
|
|
301
|
-
const data = await updateWatchdogSettings({
|
|
302
|
-
notificationsEnabled: !!nextValue,
|
|
303
|
-
});
|
|
304
|
-
setSettings(
|
|
305
|
-
data.settings || {
|
|
306
|
-
...settings,
|
|
307
|
-
notificationsEnabled: !!nextValue,
|
|
308
|
-
},
|
|
309
|
-
);
|
|
310
|
-
onRefreshStatuses();
|
|
311
|
-
showToast(
|
|
312
|
-
`Notifications ${nextValue ? "enabled" : "disabled"}`,
|
|
313
|
-
"success",
|
|
314
|
-
);
|
|
315
|
-
} catch (err) {
|
|
316
|
-
showToast(err.message || "Could not update notifications", "error");
|
|
317
|
-
} finally {
|
|
318
|
-
setSavingSettings(false);
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
const onRepair = async () => {
|
|
323
|
-
if (isRepairInProgress) return;
|
|
324
|
-
setRepairing(true);
|
|
325
|
-
try {
|
|
326
|
-
const data = await triggerWatchdogRepair();
|
|
327
|
-
if (!data.ok) throw new Error(data.error || "Repair failed");
|
|
328
|
-
showToast("Repair triggered", "success");
|
|
329
|
-
setTimeout(() => {
|
|
330
|
-
onRefreshStatuses();
|
|
331
|
-
eventsPoll.refresh();
|
|
332
|
-
}, 800);
|
|
333
|
-
} catch (err) {
|
|
334
|
-
showToast(err.message || "Could not run repair", "error");
|
|
335
|
-
} finally {
|
|
336
|
-
setRepairing(false);
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
return html`
|
|
341
|
-
<div class="space-y-4">
|
|
342
|
-
<${Gateway}
|
|
343
|
-
status=${gatewayStatus}
|
|
344
|
-
openclawVersion=${openclawVersion}
|
|
345
|
-
restarting=${restartingGateway}
|
|
346
|
-
onRestart=${onRestartGateway}
|
|
347
|
-
watchdogStatus=${currentWatchdogStatus}
|
|
348
|
-
onRepair=${onRepair}
|
|
349
|
-
repairing=${isRepairInProgress}
|
|
350
|
-
openclawUpdateInProgress=${openclawUpdateInProgress}
|
|
351
|
-
onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
|
|
352
|
-
onOpenclawUpdate=${onOpenclawUpdate}
|
|
353
|
-
/>
|
|
354
|
-
|
|
355
|
-
${(() => {
|
|
356
|
-
const r = resourcesPoll.data?.resources;
|
|
357
|
-
if (!r) return null;
|
|
358
|
-
return html`
|
|
359
|
-
<div class="bg-surface border border-border rounded-xl p-4">
|
|
360
|
-
${memoryExpanded
|
|
361
|
-
? html`
|
|
362
|
-
<${ResourceBar}
|
|
363
|
-
label="Memory"
|
|
364
|
-
detail=${`${formatBytes(r.memory?.usedBytes)} / ${formatBytes(r.memory?.totalBytes)}`}
|
|
365
|
-
percent=${r.memory?.percent}
|
|
366
|
-
expanded=${true}
|
|
367
|
-
onToggle=${() => setMemoryExpanded(false)}
|
|
368
|
-
segments=${(() => {
|
|
369
|
-
const p = r.processes;
|
|
370
|
-
const total = r.memory?.totalBytes;
|
|
371
|
-
const used = r.memory?.usedBytes;
|
|
372
|
-
if (!p || !total || !used) return null;
|
|
373
|
-
const segs = [];
|
|
374
|
-
let tracked = 0;
|
|
375
|
-
if (p.gateway?.rssBytes != null) {
|
|
376
|
-
tracked += p.gateway.rssBytes;
|
|
377
|
-
segs.push({
|
|
378
|
-
percent: (p.gateway.rssBytes / total) * 100,
|
|
379
|
-
color: "#22d3ee",
|
|
380
|
-
label: `Gateway ${formatBytes(p.gateway.rssBytes)}`,
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
if (p.alphaclaw?.rssBytes != null) {
|
|
384
|
-
tracked += p.alphaclaw.rssBytes;
|
|
385
|
-
segs.push({
|
|
386
|
-
percent: (p.alphaclaw.rssBytes / total) * 100,
|
|
387
|
-
color: "#a78bfa",
|
|
388
|
-
label: `AlphaClaw ${formatBytes(p.alphaclaw.rssBytes)}`,
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
const other = Math.max(0, used - tracked);
|
|
392
|
-
if (other > 0) {
|
|
393
|
-
segs.push({
|
|
394
|
-
percent: (other / total) * 100,
|
|
395
|
-
color: "#4b5563",
|
|
396
|
-
label: `Other ${formatBytes(other)}`,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
return segs.length ? segs : null;
|
|
400
|
-
})()}
|
|
401
|
-
/>
|
|
402
|
-
`
|
|
403
|
-
: html`
|
|
404
|
-
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
405
|
-
<${ResourceBar}
|
|
406
|
-
label="Memory"
|
|
407
|
-
percent=${r.memory?.percent}
|
|
408
|
-
detail=${`${formatBytes(r.memory?.usedBytes)} / ${formatBytes(r.memory?.totalBytes)}`}
|
|
409
|
-
onToggle=${() => setMemoryExpanded(true)}
|
|
410
|
-
/>
|
|
411
|
-
<${ResourceBar}
|
|
412
|
-
label=${`Disk${r.disk?.path ? ` (${r.disk.path})` : ""}`}
|
|
413
|
-
percent=${r.disk?.percent}
|
|
414
|
-
detail=${`${formatBytes(r.disk?.usedBytes)} / ${formatBytes(r.disk?.totalBytes)}`}
|
|
415
|
-
/>
|
|
416
|
-
<${ResourceBar}
|
|
417
|
-
label=${`CPU${r.cpu?.cores ? ` (${r.cpu.cores} vCPU)` : ""}`}
|
|
418
|
-
percent=${r.cpu?.percent}
|
|
419
|
-
detail=${r.cpu?.percent != null
|
|
420
|
-
? `${r.cpu.percent}%`
|
|
421
|
-
: "—"}
|
|
422
|
-
/>
|
|
423
|
-
</div>
|
|
424
|
-
`}
|
|
425
|
-
</div>
|
|
426
|
-
`;
|
|
427
|
-
})()}
|
|
428
|
-
|
|
429
|
-
<div class="bg-surface border border-border rounded-xl p-4">
|
|
430
|
-
<div class="flex items-center justify-between gap-3">
|
|
431
|
-
<div class="inline-flex items-center gap-2 text-xs text-gray-400">
|
|
432
|
-
<span>Auto-repair</span>
|
|
433
|
-
<${InfoTooltip}
|
|
434
|
-
text="Automatically runs OpenClaw doctor repair when watchdog detects gateway health failures or crash loops."
|
|
435
|
-
/>
|
|
436
|
-
</div>
|
|
437
|
-
<${ToggleSwitch}
|
|
438
|
-
checked=${!!settings.autoRepair}
|
|
439
|
-
disabled=${savingSettings}
|
|
440
|
-
onChange=${onToggleAutoRepair}
|
|
441
|
-
label=""
|
|
442
|
-
/>
|
|
443
|
-
</div>
|
|
444
|
-
<div class="flex items-center justify-between gap-3 mt-3">
|
|
445
|
-
<div class="inline-flex items-center gap-2 text-xs text-gray-400">
|
|
446
|
-
<span>Notifications</span>
|
|
447
|
-
<${InfoTooltip}
|
|
448
|
-
text="Sends channel notices for watchdog alerts and auto-repair outcomes."
|
|
449
|
-
/>
|
|
450
|
-
</div>
|
|
451
|
-
<${ToggleSwitch}
|
|
452
|
-
checked=${!!settings.notificationsEnabled}
|
|
453
|
-
disabled=${savingSettings}
|
|
454
|
-
onChange=${onToggleNotifications}
|
|
455
|
-
label=""
|
|
456
|
-
/>
|
|
457
|
-
</div>
|
|
458
|
-
</div>
|
|
459
|
-
|
|
460
|
-
<div class="bg-surface border border-border rounded-xl p-4">
|
|
461
|
-
<div class="flex items-center justify-between gap-2 mb-3">
|
|
462
|
-
<h2 class="card-label">Logs</h2>
|
|
463
|
-
<label class="inline-flex items-center gap-2 text-xs text-gray-400">
|
|
464
|
-
<input
|
|
465
|
-
type="checkbox"
|
|
466
|
-
checked=${stickToBottom}
|
|
467
|
-
onchange=${(e) => setStickToBottom(!!e.target.checked)}
|
|
468
|
-
/>
|
|
469
|
-
Stick to bottom
|
|
470
|
-
</label>
|
|
471
|
-
</div>
|
|
472
|
-
<pre
|
|
473
|
-
ref=${logsRef}
|
|
474
|
-
class="watchdog-logs-panel bg-black/40 border border-border rounded-lg p-3 overflow-auto text-xs text-gray-300 whitespace-pre-wrap break-words"
|
|
475
|
-
style=${{ height: `${logsPanelHeightPx}px` }}
|
|
476
|
-
>
|
|
477
|
-
${loadingLogs ? "Loading logs..." : logs || "No logs yet."}</pre
|
|
478
|
-
>
|
|
479
|
-
</div>
|
|
480
|
-
|
|
481
|
-
<div class="bg-surface border border-border rounded-xl p-4">
|
|
482
|
-
<div class="flex items-center justify-between gap-2 mb-3">
|
|
483
|
-
<h2 class="card-label">Recent incidents</h2>
|
|
484
|
-
<button
|
|
485
|
-
class="text-xs text-gray-400 hover:text-gray-200"
|
|
486
|
-
onclick=${() => eventsPoll.refresh()}
|
|
487
|
-
>
|
|
488
|
-
Refresh
|
|
489
|
-
</button>
|
|
490
|
-
</div>
|
|
491
|
-
<div class="ac-history-list">
|
|
492
|
-
${events.length === 0 &&
|
|
493
|
-
html`<p class="text-xs text-gray-500">No incidents recorded.</p>`}
|
|
494
|
-
${events.map((event) => {
|
|
495
|
-
const tone = getIncidentStatusTone(event);
|
|
496
|
-
return html`
|
|
497
|
-
<details class="ac-history-item">
|
|
498
|
-
<summary class="ac-history-summary">
|
|
499
|
-
<div class="ac-history-summary-row">
|
|
500
|
-
<span class="inline-flex items-center gap-2 min-w-0">
|
|
501
|
-
<span
|
|
502
|
-
class="ac-history-toggle shrink-0"
|
|
503
|
-
aria-hidden="true"
|
|
504
|
-
>▸</span
|
|
505
|
-
>
|
|
506
|
-
<span class="truncate">
|
|
507
|
-
${event.createdAt || ""} · ${event.eventType || "event"}
|
|
508
|
-
· ${event.status || "unknown"}
|
|
509
|
-
</span>
|
|
510
|
-
</span>
|
|
511
|
-
<span
|
|
512
|
-
class=${`h-2.5 w-2.5 shrink-0 rounded-full ${tone.dotClass}`}
|
|
513
|
-
title=${tone.label}
|
|
514
|
-
aria-label=${tone.label}
|
|
515
|
-
></span>
|
|
516
|
-
</div>
|
|
517
|
-
</summary>
|
|
518
|
-
<div class="ac-history-body text-xs text-gray-400">
|
|
519
|
-
<div>Source: ${event.source || "unknown"}</div>
|
|
520
|
-
<pre
|
|
521
|
-
class="mt-2 bg-black/30 rounded p-2 whitespace-pre-wrap break-words"
|
|
522
|
-
>
|
|
523
|
-
${typeof event.details === "string"
|
|
524
|
-
? event.details
|
|
525
|
-
: JSON.stringify(event.details || {}, null, 2)}</pre
|
|
526
|
-
>
|
|
527
|
-
</div>
|
|
528
|
-
</details>
|
|
529
|
-
`;
|
|
530
|
-
})}
|
|
531
|
-
</div>
|
|
532
|
-
</div>
|
|
533
|
-
</div>
|
|
534
|
-
`;
|
|
535
|
-
};
|