@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.
Files changed (31) hide show
  1. package/lib/public/css/cron.css +26 -17
  2. package/lib/public/css/theme.css +14 -0
  3. package/lib/public/js/components/cron-tab/cron-calendar.js +17 -12
  4. package/lib/public/js/components/cron-tab/cron-job-list.js +11 -1
  5. package/lib/public/js/components/cron-tab/index.js +16 -2
  6. package/lib/public/js/components/icons.js +11 -0
  7. package/lib/public/js/components/routes/watchdog-route.js +1 -1
  8. package/lib/public/js/components/watchdog-tab/console/index.js +115 -0
  9. package/lib/public/js/components/watchdog-tab/console/use-console.js +137 -0
  10. package/lib/public/js/components/watchdog-tab/helpers.js +106 -0
  11. package/lib/public/js/components/watchdog-tab/incidents/index.js +56 -0
  12. package/lib/public/js/components/watchdog-tab/incidents/use-incidents.js +33 -0
  13. package/lib/public/js/components/watchdog-tab/index.js +84 -0
  14. package/lib/public/js/components/watchdog-tab/resource-bar.js +76 -0
  15. package/lib/public/js/components/watchdog-tab/resources/index.js +85 -0
  16. package/lib/public/js/components/watchdog-tab/resources/use-resources.js +13 -0
  17. package/lib/public/js/components/watchdog-tab/settings/index.js +44 -0
  18. package/lib/public/js/components/watchdog-tab/settings/use-settings.js +117 -0
  19. package/lib/public/js/components/watchdog-tab/terminal/index.js +20 -0
  20. package/lib/public/js/components/watchdog-tab/terminal/use-terminal.js +263 -0
  21. package/lib/public/js/components/watchdog-tab/use-watchdog-tab.js +55 -0
  22. package/lib/public/js/lib/api.js +39 -0
  23. package/lib/server/init/register-server-routes.js +239 -0
  24. package/lib/server/init/runtime-init.js +44 -0
  25. package/lib/server/init/server-lifecycle.js +55 -0
  26. package/lib/server/routes/watchdog.js +62 -0
  27. package/lib/server/watchdog-terminal-ws.js +114 -0
  28. package/lib/server/watchdog-terminal.js +262 -0
  29. package/lib/server.js +89 -215
  30. package/package.json +3 -2
  31. 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
- };