@chrysb/alphaclaw 0.7.0 → 0.7.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/lib/public/css/cron.css +26 -17
  2. package/lib/public/css/explorer.css +12 -0
  3. package/lib/public/css/theme.css +14 -0
  4. package/lib/public/js/components/cron-tab/cron-calendar.js +17 -12
  5. package/lib/public/js/components/cron-tab/cron-job-list.js +11 -1
  6. package/lib/public/js/components/cron-tab/cron-overview.js +2 -1
  7. package/lib/public/js/components/cron-tab/cron-run-history-panel.js +65 -49
  8. package/lib/public/js/components/cron-tab/index.js +16 -2
  9. package/lib/public/js/components/icons.js +11 -0
  10. package/lib/public/js/components/routes/watchdog-route.js +1 -1
  11. package/lib/public/js/components/sidebar.js +14 -2
  12. package/lib/public/js/components/update-modal.js +173 -0
  13. package/lib/public/js/components/watchdog-tab/console/index.js +115 -0
  14. package/lib/public/js/components/watchdog-tab/console/use-console.js +137 -0
  15. package/lib/public/js/components/watchdog-tab/helpers.js +106 -0
  16. package/lib/public/js/components/watchdog-tab/incidents/index.js +56 -0
  17. package/lib/public/js/components/watchdog-tab/incidents/use-incidents.js +33 -0
  18. package/lib/public/js/components/watchdog-tab/index.js +84 -0
  19. package/lib/public/js/components/watchdog-tab/resource-bar.js +76 -0
  20. package/lib/public/js/components/watchdog-tab/resources/index.js +85 -0
  21. package/lib/public/js/components/watchdog-tab/resources/use-resources.js +13 -0
  22. package/lib/public/js/components/watchdog-tab/settings/index.js +44 -0
  23. package/lib/public/js/components/watchdog-tab/settings/use-settings.js +117 -0
  24. package/lib/public/js/components/watchdog-tab/terminal/index.js +20 -0
  25. package/lib/public/js/components/watchdog-tab/terminal/use-terminal.js +263 -0
  26. package/lib/public/js/components/watchdog-tab/use-watchdog-tab.js +55 -0
  27. package/lib/public/js/lib/api.js +75 -0
  28. package/lib/server/constants.js +3 -0
  29. package/lib/server/init/register-server-routes.js +240 -0
  30. package/lib/server/init/runtime-init.js +44 -0
  31. package/lib/server/init/server-lifecycle.js +55 -0
  32. package/lib/server/routes/system.js +98 -0
  33. package/lib/server/routes/watchdog.js +62 -0
  34. package/lib/server/watchdog-terminal-ws.js +114 -0
  35. package/lib/server/watchdog-terminal.js +262 -0
  36. package/lib/server.js +89 -215
  37. package/package.json +3 -2
  38. package/lib/public/js/components/watchdog-tab.js +0 -535
@@ -0,0 +1,115 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import {
4
+ kWatchdogConsoleTabLogs,
5
+ kWatchdogConsoleTabTerminal,
6
+ } from "../helpers.js";
7
+ import { WatchdogTerminal } from "../terminal/index.js";
8
+
9
+ const html = htm.bind(h);
10
+
11
+ export const WatchdogConsoleCard = ({
12
+ activeConsoleTab = kWatchdogConsoleTabLogs,
13
+ stickToBottom = true,
14
+ onSetStickToBottom = () => {},
15
+ onSelectConsoleTab = () => {},
16
+ connectingTerminal = false,
17
+ terminalConnected = false,
18
+ terminalEnded = false,
19
+ terminalStatusText = "",
20
+ terminalUiSettling = false,
21
+ onRestartTerminalSession = () => {},
22
+ logsRef = null,
23
+ logs = "",
24
+ loadingLogs = true,
25
+ terminalPanelRef = null,
26
+ terminalHostRef = null,
27
+ terminalInstanceRef = null,
28
+ logsPanelHeightPx = 320,
29
+ }) => html`
30
+ <div class="bg-surface border border-border rounded-xl p-4">
31
+ <div class="flex items-center justify-between gap-2 mb-3">
32
+ <div
33
+ class="inline-flex items-center rounded-lg border border-border bg-black/20 p-0.5"
34
+ >
35
+ <button
36
+ type="button"
37
+ class=${`px-2.5 py-1 text-xs rounded-md ${activeConsoleTab === kWatchdogConsoleTabLogs ? "bg-surface text-gray-100" : "text-gray-400 hover:text-gray-200"}`}
38
+ onClick=${() => onSelectConsoleTab(kWatchdogConsoleTabLogs)}
39
+ >
40
+ Logs
41
+ </button>
42
+ <button
43
+ type="button"
44
+ class=${`px-2.5 py-1 text-xs rounded-md ${activeConsoleTab === kWatchdogConsoleTabTerminal ? "bg-surface text-gray-100" : "text-gray-400 hover:text-gray-200"}`}
45
+ onClick=${() => onSelectConsoleTab(kWatchdogConsoleTabTerminal)}
46
+ >
47
+ Terminal
48
+ </button>
49
+ </div>
50
+ <div class="flex items-center gap-2">
51
+ ${activeConsoleTab === kWatchdogConsoleTabLogs
52
+ ? html`
53
+ <label class="inline-flex items-center gap-2 text-xs text-gray-400">
54
+ <input
55
+ type="checkbox"
56
+ checked=${stickToBottom}
57
+ onchange=${(event) =>
58
+ onSetStickToBottom(!!event.currentTarget?.checked)}
59
+ />
60
+ Stick to bottom
61
+ </label>
62
+ `
63
+ : html`
64
+ <div class="flex items-center gap-2 pr-1">
65
+ ${terminalUiSettling
66
+ ? null
67
+ : html`
68
+ <span class="text-xs text-gray-500">
69
+ ${connectingTerminal
70
+ ? "Connecting..."
71
+ : terminalEnded
72
+ ? "Session ended"
73
+ : terminalConnected
74
+ ? "Connected"
75
+ : terminalStatusText || "Disconnected"}
76
+ </span>
77
+ ${connectingTerminal || terminalConnected
78
+ ? null
79
+ : html`
80
+ <button
81
+ type="button"
82
+ class="ac-btn-secondary text-xs px-2.5 py-1 rounded-lg"
83
+ onClick=${onRestartTerminalSession}
84
+ >
85
+ New session
86
+ </button>
87
+ `}
88
+ `}
89
+ </div>
90
+ `}
91
+ </div>
92
+ </div>
93
+ <div class=${activeConsoleTab === kWatchdogConsoleTabLogs ? "" : "hidden"}>
94
+ <pre
95
+ ref=${logsRef}
96
+ 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"
97
+ style=${{ height: `${logsPanelHeightPx}px` }}
98
+ >
99
+ ${loadingLogs ? "Loading logs..." : logs || "No logs yet."}</pre
100
+ >
101
+ </div>
102
+ <div
103
+ class=${activeConsoleTab === kWatchdogConsoleTabTerminal
104
+ ? "space-y-2"
105
+ : "hidden"}
106
+ >
107
+ <${WatchdogTerminal}
108
+ panelRef=${terminalPanelRef}
109
+ hostRef=${terminalHostRef}
110
+ terminalInstanceRef=${terminalInstanceRef}
111
+ panelHeightPx=${logsPanelHeightPx}
112
+ />
113
+ </div>
114
+ </div>
115
+ `;
@@ -0,0 +1,137 @@
1
+ import { useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
2
+ import { fetchWatchdogLogs } from "../../../lib/api.js";
3
+ import { readUiSettings, writeUiSettings } from "../../../lib/ui-settings.js";
4
+ import {
5
+ clampWatchdogLogsPanelHeight,
6
+ kWatchdogConsoleTabLogs,
7
+ kWatchdogConsoleTabTerminal,
8
+ kWatchdogConsoleTabUiSettingKey,
9
+ kWatchdogLogsPanelHeightUiSettingKey,
10
+ normalizeWatchdogConsoleTab,
11
+ readCssHeightPx,
12
+ } from "../helpers.js";
13
+ import { useWatchdogTerminal } from "../terminal/use-terminal.js";
14
+
15
+ export const useWatchdogConsole = () => {
16
+ const [logs, setLogs] = useState("");
17
+ const [loadingLogs, setLoadingLogs] = useState(true);
18
+ const [stickToBottom, setStickToBottom] = useState(true);
19
+ const [activeConsoleTab, setActiveConsoleTab] = useState(() => {
20
+ const settings = readUiSettings();
21
+ return normalizeWatchdogConsoleTab(settings?.[kWatchdogConsoleTabUiSettingKey]);
22
+ });
23
+ const [logsPanelHeightPx, setLogsPanelHeightPx] = useState(() => {
24
+ const settings = readUiSettings();
25
+ return clampWatchdogLogsPanelHeight(
26
+ settings?.[kWatchdogLogsPanelHeightUiSettingKey],
27
+ );
28
+ });
29
+ const logsRef = useRef(null);
30
+ const terminalPanelRef = useRef(null);
31
+ const terminalHostRef = useRef(null);
32
+ const terminal = useWatchdogTerminal({
33
+ active: activeConsoleTab === kWatchdogConsoleTabTerminal,
34
+ panelRef: terminalPanelRef,
35
+ hostRef: terminalHostRef,
36
+ });
37
+
38
+ useEffect(() => {
39
+ const settings = readUiSettings();
40
+ settings[kWatchdogConsoleTabUiSettingKey] =
41
+ normalizeWatchdogConsoleTab(activeConsoleTab);
42
+ writeUiSettings(settings);
43
+ }, [activeConsoleTab]);
44
+
45
+ useEffect(() => {
46
+ let active = true;
47
+ let timer = null;
48
+ const pollLogs = async () => {
49
+ try {
50
+ const text = await fetchWatchdogLogs(65536);
51
+ if (!active) return;
52
+ setLogs(text || "");
53
+ setLoadingLogs(false);
54
+ } catch {
55
+ if (!active) return;
56
+ setLoadingLogs(false);
57
+ }
58
+ if (!active) return;
59
+ timer = setTimeout(pollLogs, 3000);
60
+ };
61
+ pollLogs();
62
+ return () => {
63
+ active = false;
64
+ if (timer) clearTimeout(timer);
65
+ };
66
+ }, []);
67
+
68
+ useEffect(() => {
69
+ const logsElement = logsRef.current;
70
+ if (!logsElement || !stickToBottom) return;
71
+ logsElement.scrollTop = logsElement.scrollHeight;
72
+ }, [logs, stickToBottom]);
73
+
74
+ useEffect(() => {
75
+ const panelElement =
76
+ activeConsoleTab === kWatchdogConsoleTabLogs
77
+ ? logsRef.current
78
+ : terminalPanelRef.current;
79
+ if (!panelElement || typeof ResizeObserver === "undefined") return () => {};
80
+ let saveTimer = null;
81
+ const observer = new ResizeObserver((entries) => {
82
+ const entry = entries?.[0];
83
+ const nextHeight = clampWatchdogLogsPanelHeight(
84
+ readCssHeightPx(entry?.target),
85
+ );
86
+ setLogsPanelHeightPx((currentValue) =>
87
+ Math.abs(currentValue - nextHeight) >= 1 ? nextHeight : currentValue,
88
+ );
89
+ if (saveTimer) window.clearTimeout(saveTimer);
90
+ saveTimer = window.setTimeout(() => {
91
+ const settings = readUiSettings();
92
+ settings[kWatchdogLogsPanelHeightUiSettingKey] = nextHeight;
93
+ writeUiSettings(settings);
94
+ }, 120);
95
+ if (activeConsoleTab === kWatchdogConsoleTabTerminal) {
96
+ window.requestAnimationFrame(() => {
97
+ terminal.fitNow();
98
+ });
99
+ }
100
+ });
101
+ observer.observe(panelElement);
102
+ return () => {
103
+ observer.disconnect();
104
+ if (saveTimer) window.clearTimeout(saveTimer);
105
+ };
106
+ }, [activeConsoleTab]);
107
+
108
+ const handleSelectConsoleTab = (nextTab = kWatchdogConsoleTabLogs) => {
109
+ const normalizedTab = normalizeWatchdogConsoleTab(nextTab);
110
+ if (normalizedTab === kWatchdogConsoleTabTerminal) {
111
+ terminal.prepareForActivate();
112
+ } else {
113
+ terminal.clearSettling();
114
+ }
115
+ setActiveConsoleTab(normalizedTab);
116
+ };
117
+
118
+ const onRestartTerminalSession = () => {
119
+ terminal.restartSession();
120
+ setActiveConsoleTab(kWatchdogConsoleTabTerminal);
121
+ };
122
+
123
+ return {
124
+ logs,
125
+ loadingLogs,
126
+ stickToBottom,
127
+ setStickToBottom,
128
+ activeConsoleTab,
129
+ handleSelectConsoleTab,
130
+ logsPanelHeightPx,
131
+ logsRef,
132
+ terminalPanelRef,
133
+ terminalHostRef,
134
+ onRestartTerminalSession,
135
+ ...terminal,
136
+ };
137
+ };
@@ -0,0 +1,106 @@
1
+ export const kWatchdogConsoleTabLogs = "logs";
2
+ export const kWatchdogConsoleTabTerminal = "terminal";
3
+ export const kWatchdogConsoleTabUiSettingKey = "watchdogConsoleTab";
4
+ export const kWatchdogLogsPanelHeightUiSettingKey = "watchdogLogsPanelHeightPx";
5
+ export const kWatchdogLogsPanelDefaultHeightPx = 320;
6
+ export const kWatchdogLogsPanelMinHeightPx = 160;
7
+ export const kXtermCssUrl =
8
+ "https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css";
9
+ export const kWatchdogTerminalWsPath = "/api/watchdog/terminal/ws";
10
+
11
+ let xtermModulesPromise = null;
12
+
13
+ export const loadXtermModules = () => {
14
+ if (!xtermModulesPromise) {
15
+ xtermModulesPromise = Promise.all([
16
+ import("https://esm.sh/@xterm/xterm@5.5.0"),
17
+ import("https://esm.sh/@xterm/addon-fit@0.10.0"),
18
+ ]);
19
+ }
20
+ return xtermModulesPromise;
21
+ };
22
+
23
+ export const ensureXtermStylesheet = () => {
24
+ if (typeof document === "undefined") return;
25
+ if (document.getElementById("ac-xterm-css")) return;
26
+ const link = document.createElement("link");
27
+ link.id = "ac-xterm-css";
28
+ link.rel = "stylesheet";
29
+ link.href = kXtermCssUrl;
30
+ document.head.appendChild(link);
31
+ };
32
+
33
+ export const fitTerminalWhenVisible = ({
34
+ panel = null,
35
+ fitAddon = null,
36
+ minWidthPx = 120,
37
+ minHeightPx = 80,
38
+ } = {}) => {
39
+ if (!panel || !fitAddon) return false;
40
+ const panelWidth = Number(panel.clientWidth || 0);
41
+ const panelHeight = Number(panel.clientHeight || 0);
42
+ if (panelWidth < minWidthPx || panelHeight < minHeightPx) return false;
43
+ fitAddon.fit();
44
+ return true;
45
+ };
46
+
47
+ export const normalizeWatchdogConsoleTab = (value) =>
48
+ value === kWatchdogConsoleTabTerminal
49
+ ? kWatchdogConsoleTabTerminal
50
+ : kWatchdogConsoleTabLogs;
51
+
52
+ export const clampWatchdogLogsPanelHeight = (value) => {
53
+ const parsed = Number(value);
54
+ const normalized = Number.isFinite(parsed)
55
+ ? Math.round(parsed)
56
+ : kWatchdogLogsPanelDefaultHeightPx;
57
+ return Math.max(kWatchdogLogsPanelMinHeightPx, normalized);
58
+ };
59
+
60
+ export const readCssHeightPx = (element) => {
61
+ if (!element) return 0;
62
+ const computedHeight = Number.parseFloat(
63
+ window.getComputedStyle(element).height || "0",
64
+ );
65
+ return Number.isFinite(computedHeight) ? computedHeight : 0;
66
+ };
67
+
68
+ export const formatBytes = (bytes) => {
69
+ if (bytes == null) return "—";
70
+ if (bytes < 1024) return `${bytes} B`;
71
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
72
+ if (bytes < 1024 * 1024 * 1024)
73
+ return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
74
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
75
+ };
76
+
77
+ export const getIncidentStatusTone = (event) => {
78
+ const eventType = String(event?.eventType || "")
79
+ .trim()
80
+ .toLowerCase();
81
+ const status = String(event?.status || "")
82
+ .trim()
83
+ .toLowerCase();
84
+ if (status === "failed") {
85
+ return {
86
+ dotClass: "bg-red-500/90",
87
+ label: "Failed",
88
+ };
89
+ }
90
+ if (status === "ok" && eventType === "health_check") {
91
+ return {
92
+ dotClass: "bg-green-500/90",
93
+ label: "Healthy",
94
+ };
95
+ }
96
+ if (status === "warn" || status === "warning") {
97
+ return {
98
+ dotClass: "bg-yellow-400/90",
99
+ label: "Warning",
100
+ };
101
+ }
102
+ return {
103
+ dotClass: "bg-gray-500/70",
104
+ label: "Unknown",
105
+ };
106
+ };
@@ -0,0 +1,56 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { getIncidentStatusTone } from "../helpers.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const WatchdogIncidentsCard = ({
8
+ events = [],
9
+ onRefresh = () => {},
10
+ }) => html`
11
+ <div class="bg-surface border border-border rounded-xl p-4">
12
+ <div class="flex items-center justify-between gap-2 mb-3">
13
+ <h2 class="card-label">Recent incidents</h2>
14
+ <button class="text-xs text-gray-400 hover:text-gray-200" onclick=${onRefresh}>
15
+ Refresh
16
+ </button>
17
+ </div>
18
+ <div class="ac-history-list">
19
+ ${events.length === 0 &&
20
+ html`<p class="text-xs text-gray-500">No incidents recorded.</p>`}
21
+ ${events.map((event) => {
22
+ const tone = getIncidentStatusTone(event);
23
+ return html`
24
+ <details class="ac-history-item">
25
+ <summary class="ac-history-summary">
26
+ <div class="ac-history-summary-row">
27
+ <span class="inline-flex items-center gap-2 min-w-0">
28
+ <span class="ac-history-toggle shrink-0" aria-hidden="true"
29
+ >▸</span
30
+ >
31
+ <span class="truncate">
32
+ ${event.createdAt || ""} · ${event.eventType || "event"} ·
33
+ ${event.status || "unknown"}
34
+ </span>
35
+ </span>
36
+ <span
37
+ class=${`h-2.5 w-2.5 shrink-0 rounded-full ${tone.dotClass}`}
38
+ title=${tone.label}
39
+ aria-label=${tone.label}
40
+ ></span>
41
+ </div>
42
+ </summary>
43
+ <div class="ac-history-body text-xs text-gray-400">
44
+ <div>Source: ${event.source || "unknown"}</div>
45
+ <pre class="mt-2 bg-black/30 rounded p-2 whitespace-pre-wrap break-words">
46
+ ${typeof event.details === "string"
47
+ ? event.details
48
+ : JSON.stringify(event.details || {}, null, 2)}</pre
49
+ >
50
+ </div>
51
+ </details>
52
+ `;
53
+ })}
54
+ </div>
55
+ </div>
56
+ `;
@@ -0,0 +1,33 @@
1
+ import { useEffect } from "https://esm.sh/preact/hooks";
2
+ import { usePolling } from "../../../hooks/usePolling.js";
3
+ import { fetchWatchdogEvents } from "../../../lib/api.js";
4
+
5
+ export const useWatchdogIncidents = ({
6
+ restartSignal = 0,
7
+ onRefreshStatuses = () => {},
8
+ } = {}) => {
9
+ const eventsPoll = usePolling(() => fetchWatchdogEvents(20), 15000);
10
+
11
+ useEffect(() => {
12
+ if (!restartSignal) return;
13
+ onRefreshStatuses();
14
+ eventsPoll.refresh();
15
+ const t1 = setTimeout(() => {
16
+ onRefreshStatuses();
17
+ eventsPoll.refresh();
18
+ }, 1200);
19
+ const t2 = setTimeout(() => {
20
+ onRefreshStatuses();
21
+ eventsPoll.refresh();
22
+ }, 3500);
23
+ return () => {
24
+ clearTimeout(t1);
25
+ clearTimeout(t2);
26
+ };
27
+ }, [restartSignal, onRefreshStatuses, eventsPoll.refresh]);
28
+
29
+ return {
30
+ events: eventsPoll.data?.events || [],
31
+ refreshEvents: eventsPoll.refresh,
32
+ };
33
+ };
@@ -0,0 +1,84 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { Gateway } from "../gateway.js";
4
+ import { useWatchdogTab } from "./use-watchdog-tab.js";
5
+ import { WatchdogResourcesCard } from "./resources/index.js";
6
+ import { WatchdogSettingsCard } from "./settings/index.js";
7
+ import { WatchdogConsoleCard } from "./console/index.js";
8
+ import { WatchdogIncidentsCard } from "./incidents/index.js";
9
+
10
+ const html = htm.bind(h);
11
+
12
+ export const WatchdogTab = ({
13
+ gatewayStatus = null,
14
+ openclawVersion = null,
15
+ watchdogStatus = null,
16
+ onRefreshStatuses = () => {},
17
+ restartingGateway = false,
18
+ onRestartGateway,
19
+ restartSignal = 0,
20
+ openclawUpdateInProgress = false,
21
+ onOpenclawVersionActionComplete = () => {},
22
+ onOpenclawUpdate,
23
+ }) => {
24
+ const state = useWatchdogTab({
25
+ watchdogStatus,
26
+ onRefreshStatuses,
27
+ restartSignal,
28
+ });
29
+
30
+ return html`
31
+ <div class="space-y-4">
32
+ <${Gateway}
33
+ status=${gatewayStatus}
34
+ openclawVersion=${openclawVersion}
35
+ restarting=${restartingGateway}
36
+ onRestart=${onRestartGateway}
37
+ watchdogStatus=${state.currentWatchdogStatus}
38
+ onRepair=${state.onRepair}
39
+ repairing=${state.isRepairInProgress}
40
+ openclawUpdateInProgress=${openclawUpdateInProgress}
41
+ onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
42
+ onOpenclawUpdate=${onOpenclawUpdate}
43
+ />
44
+
45
+ <${WatchdogResourcesCard}
46
+ resources=${state.resources}
47
+ memoryExpanded=${state.memoryExpanded}
48
+ onSetMemoryExpanded=${state.setMemoryExpanded}
49
+ />
50
+
51
+ <${WatchdogSettingsCard}
52
+ settings=${state.settings}
53
+ savingSettings=${state.savingSettings}
54
+ onToggleAutoRepair=${state.onToggleAutoRepair}
55
+ onToggleNotifications=${state.onToggleNotifications}
56
+ />
57
+
58
+ <${WatchdogConsoleCard}
59
+ activeConsoleTab=${state.activeConsoleTab}
60
+ stickToBottom=${state.stickToBottom}
61
+ onSetStickToBottom=${state.setStickToBottom}
62
+ onSelectConsoleTab=${state.handleSelectConsoleTab}
63
+ connectingTerminal=${state.connectingTerminal}
64
+ terminalConnected=${state.terminalConnected}
65
+ terminalEnded=${state.terminalEnded}
66
+ terminalStatusText=${state.terminalStatusText}
67
+ terminalUiSettling=${state.terminalUiSettling}
68
+ onRestartTerminalSession=${state.onRestartTerminalSession}
69
+ logsRef=${state.logsRef}
70
+ logs=${state.logs}
71
+ loadingLogs=${state.loadingLogs}
72
+ terminalPanelRef=${state.terminalPanelRef}
73
+ terminalHostRef=${state.terminalHostRef}
74
+ terminalInstanceRef=${state.terminalInstanceRef}
75
+ logsPanelHeightPx=${state.logsPanelHeightPx}
76
+ />
77
+
78
+ <${WatchdogIncidentsCard}
79
+ events=${state.events}
80
+ onRefresh=${state.refreshEvents}
81
+ />
82
+ </div>
83
+ `;
84
+ };
@@ -0,0 +1,76 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+
4
+ const html = htm.bind(h);
5
+
6
+ const barColor = (percent) => {
7
+ if (percent == null) return "bg-gray-600";
8
+ return "bg-cyan-400";
9
+ };
10
+
11
+ export const ResourceBar = ({
12
+ label,
13
+ percent,
14
+ detail,
15
+ segments = null,
16
+ expanded = false,
17
+ onToggle = null,
18
+ }) => html`
19
+ <div
20
+ class=${onToggle ? "cursor-pointer group" : ""}
21
+ onclick=${onToggle || undefined}
22
+ >
23
+ <span
24
+ class=${`text-xs text-gray-400 ${onToggle ? "group-hover:text-gray-200 transition-colors" : ""}`}
25
+ >${label}</span
26
+ >
27
+ <div
28
+ 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" : ""}`}
29
+ >
30
+ ${expanded && segments
31
+ ? segments.map(
32
+ (seg) => html`
33
+ <div
34
+ class="h-full"
35
+ style=${{
36
+ width: `${Math.min(100, seg.percent ?? 0)}%`,
37
+ backgroundColor: seg.color,
38
+ transition:
39
+ "width 0.8s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.5s ease",
40
+ }}
41
+ ></div>
42
+ `,
43
+ )
44
+ : html`
45
+ <div
46
+ class=${`h-full rounded-full ${barColor(percent)}`}
47
+ style=${{
48
+ width: `${Math.min(100, percent ?? 0)}%`,
49
+ transition:
50
+ "width 0.8s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.5s ease",
51
+ }}
52
+ ></div>
53
+ `}
54
+ </div>
55
+ <div class="flex flex-wrap items-center gap-x-3 mt-2.5">
56
+ <span class="text-xs text-gray-500 font-mono flex-1">${detail}</span>
57
+ ${expanded &&
58
+ segments &&
59
+ segments
60
+ .filter((segment) => segment.label)
61
+ .map(
62
+ (segment) => html`
63
+ <span
64
+ class="inline-flex items-center gap-1 text-xs text-gray-500 font-mono"
65
+ >
66
+ <span
67
+ class="inline-block w-1.5 h-1.5 rounded-full"
68
+ style=${{ backgroundColor: segment.color }}
69
+ ></span>
70
+ ${segment.label}
71
+ </span>
72
+ `,
73
+ )}
74
+ </div>
75
+ </div>
76
+ `;
@@ -0,0 +1,85 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { formatBytes } from "../helpers.js";
4
+ import { ResourceBar } from "../resource-bar.js";
5
+
6
+ const html = htm.bind(h);
7
+
8
+ export const WatchdogResourcesCard = ({
9
+ resources = null,
10
+ memoryExpanded = false,
11
+ onSetMemoryExpanded = () => {},
12
+ }) => {
13
+ if (!resources) return null;
14
+ const memorySegments = (() => {
15
+ const processes = resources.processes;
16
+ const totalBytes = resources.memory?.totalBytes;
17
+ const usedBytes = resources.memory?.usedBytes;
18
+ if (!processes || !totalBytes || !usedBytes) return null;
19
+ const segments = [];
20
+ let trackedBytes = 0;
21
+ if (processes.gateway?.rssBytes != null) {
22
+ trackedBytes += processes.gateway.rssBytes;
23
+ segments.push({
24
+ percent: (processes.gateway.rssBytes / totalBytes) * 100,
25
+ color: "#22d3ee",
26
+ label: `Gateway ${formatBytes(processes.gateway.rssBytes)}`,
27
+ });
28
+ }
29
+ if (processes.alphaclaw?.rssBytes != null) {
30
+ trackedBytes += processes.alphaclaw.rssBytes;
31
+ segments.push({
32
+ percent: (processes.alphaclaw.rssBytes / totalBytes) * 100,
33
+ color: "#a78bfa",
34
+ label: `AlphaClaw ${formatBytes(processes.alphaclaw.rssBytes)}`,
35
+ });
36
+ }
37
+ const otherBytes = Math.max(0, usedBytes - trackedBytes);
38
+ if (otherBytes > 0) {
39
+ segments.push({
40
+ percent: (otherBytes / totalBytes) * 100,
41
+ color: "#4b5563",
42
+ label: `Other ${formatBytes(otherBytes)}`,
43
+ });
44
+ }
45
+ return segments.length ? segments : null;
46
+ })();
47
+
48
+ return html`
49
+ <div class="bg-surface border border-border rounded-xl p-4">
50
+ ${memoryExpanded
51
+ ? html`
52
+ <${ResourceBar}
53
+ label="Memory"
54
+ detail=${`${formatBytes(resources.memory?.usedBytes)} / ${formatBytes(resources.memory?.totalBytes)}`}
55
+ percent=${resources.memory?.percent}
56
+ expanded=${true}
57
+ onToggle=${() => onSetMemoryExpanded(false)}
58
+ segments=${memorySegments}
59
+ />
60
+ `
61
+ : html`
62
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
63
+ <${ResourceBar}
64
+ label="Memory"
65
+ percent=${resources.memory?.percent}
66
+ detail=${`${formatBytes(resources.memory?.usedBytes)} / ${formatBytes(resources.memory?.totalBytes)}`}
67
+ onToggle=${() => onSetMemoryExpanded(true)}
68
+ />
69
+ <${ResourceBar}
70
+ label=${`Disk${resources.disk?.path ? ` (${resources.disk.path})` : ""}`}
71
+ percent=${resources.disk?.percent}
72
+ detail=${`${formatBytes(resources.disk?.usedBytes)} / ${formatBytes(resources.disk?.totalBytes)}`}
73
+ />
74
+ <${ResourceBar}
75
+ label=${`CPU${resources.cpu?.cores ? ` (${resources.cpu.cores} vCPU)` : ""}`}
76
+ percent=${resources.cpu?.percent}
77
+ detail=${resources.cpu?.percent != null
78
+ ? `${resources.cpu.percent}%`
79
+ : "—"}
80
+ />
81
+ </div>
82
+ `}
83
+ </div>
84
+ `;
85
+ };