@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.
- package/lib/public/css/cron.css +26 -17
- package/lib/public/css/explorer.css +12 -0
- 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/cron-overview.js +2 -1
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +65 -49
- 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/sidebar.js +14 -2
- package/lib/public/js/components/update-modal.js +173 -0
- 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 +75 -0
- package/lib/server/constants.js +3 -0
- package/lib/server/init/register-server-routes.js +240 -0
- package/lib/server/init/runtime-init.js +44 -0
- package/lib/server/init/server-lifecycle.js +55 -0
- package/lib/server/routes/system.js +98 -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
|
@@ -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
|
+
};
|