@chrysb/alphaclaw 0.2.3 → 0.3.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/bin/alphaclaw.js +79 -0
- package/lib/public/css/shell.css +57 -2
- package/lib/public/css/theme.css +184 -0
- package/lib/public/js/app.js +330 -89
- package/lib/public/js/components/action-button.js +92 -0
- package/lib/public/js/components/channels.js +16 -7
- package/lib/public/js/components/confirm-dialog.js +25 -19
- package/lib/public/js/components/credentials-modal.js +32 -23
- package/lib/public/js/components/device-pairings.js +15 -2
- package/lib/public/js/components/envars.js +22 -65
- package/lib/public/js/components/features.js +1 -1
- package/lib/public/js/components/gateway.js +139 -32
- package/lib/public/js/components/global-restart-banner.js +31 -0
- package/lib/public/js/components/google.js +9 -9
- package/lib/public/js/components/icons.js +19 -0
- package/lib/public/js/components/info-tooltip.js +18 -0
- package/lib/public/js/components/loading-spinner.js +32 -0
- package/lib/public/js/components/modal-shell.js +42 -0
- package/lib/public/js/components/models.js +34 -29
- package/lib/public/js/components/onboarding/welcome-form-step.js +45 -32
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
- package/lib/public/js/components/onboarding/welcome-setup-step.js +7 -24
- package/lib/public/js/components/page-header.js +13 -0
- package/lib/public/js/components/pairings.js +15 -2
- package/lib/public/js/components/providers.js +216 -142
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/secret-input.js +1 -0
- package/lib/public/js/components/telegram-workspace.js +37 -49
- package/lib/public/js/components/toast.js +34 -5
- package/lib/public/js/components/toggle-switch.js +25 -0
- package/lib/public/js/components/update-action-button.js +13 -53
- package/lib/public/js/components/watchdog-tab.js +312 -0
- package/lib/public/js/components/webhooks.js +981 -0
- package/lib/public/js/components/welcome.js +2 -1
- package/lib/public/js/lib/api.js +102 -1
- package/lib/public/js/lib/model-config.js +0 -5
- package/lib/server/alphaclaw-version.js +5 -3
- package/lib/server/constants.js +33 -0
- package/lib/server/discord-api.js +48 -0
- package/lib/server/gateway.js +64 -4
- package/lib/server/log-writer.js +102 -0
- package/lib/server/onboarding/github.js +21 -1
- package/lib/server/openclaw-version.js +2 -6
- package/lib/server/restart-required-state.js +86 -0
- package/lib/server/routes/auth.js +9 -4
- package/lib/server/routes/proxy.js +12 -14
- package/lib/server/routes/system.js +61 -15
- package/lib/server/routes/telegram.js +17 -48
- package/lib/server/routes/watchdog.js +68 -0
- package/lib/server/routes/webhooks.js +214 -0
- package/lib/server/telegram-api.js +11 -0
- package/lib/server/watchdog-db.js +148 -0
- package/lib/server/watchdog-notify.js +93 -0
- package/lib/server/watchdog.js +585 -0
- package/lib/server/webhook-middleware.js +195 -0
- package/lib/server/webhooks-db.js +265 -0
- package/lib/server/webhooks.js +238 -0
- package/lib/server.js +119 -4
- package/lib/setup/core-prompts/AGENTS.md +84 -0
- package/lib/setup/core-prompts/TOOLS.md +13 -0
- package/lib/setup/core-prompts/UI-DRY-OPPORTUNITIES.md +50 -0
- package/lib/setup/gitignore +2 -0
- package/package.json +2 -1
|
@@ -3,6 +3,7 @@ import { useState, useEffect } from "https://esm.sh/preact/hooks";
|
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { Badge } from "./badge.js";
|
|
5
5
|
import { showToast } from "./toast.js";
|
|
6
|
+
import { ActionButton } from "./action-button.js";
|
|
6
7
|
|
|
7
8
|
const html = htm.bind(h);
|
|
8
9
|
|
|
@@ -14,11 +15,6 @@ const authFetch = async (url, opts = {}) => {
|
|
|
14
15
|
}
|
|
15
16
|
return res;
|
|
16
17
|
};
|
|
17
|
-
const encodePayloadQuery = (payload) =>
|
|
18
|
-
encodeURIComponent(
|
|
19
|
-
JSON.stringify(payload && typeof payload === "object" ? payload : {}),
|
|
20
|
-
);
|
|
21
|
-
|
|
22
18
|
const api = {
|
|
23
19
|
verifyBot: async () => {
|
|
24
20
|
const res = await authFetch("/api/telegram/bot");
|
|
@@ -35,15 +31,11 @@ const api = {
|
|
|
35
31
|
return res.json();
|
|
36
32
|
},
|
|
37
33
|
verifyGroup: async (groupId) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
headers: { "Content-Type": "application/json" },
|
|
44
|
-
body: JSON.stringify({ groupId }),
|
|
45
|
-
},
|
|
46
|
-
);
|
|
34
|
+
const res = await authFetch("/api/telegram/groups/verify", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ groupId }),
|
|
38
|
+
});
|
|
47
39
|
return res.json();
|
|
48
40
|
},
|
|
49
41
|
listTopics: async (groupId) => {
|
|
@@ -53,9 +45,8 @@ const api = {
|
|
|
53
45
|
return res.json();
|
|
54
46
|
},
|
|
55
47
|
createTopicsBulk: async (groupId, topics) => {
|
|
56
|
-
const queryPayload = encodePayloadQuery({ topics });
|
|
57
48
|
const res = await authFetch(
|
|
58
|
-
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics/bulk
|
|
49
|
+
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics/bulk`,
|
|
59
50
|
{
|
|
60
51
|
method: "POST",
|
|
61
52
|
headers: { "Content-Type": "application/json" },
|
|
@@ -72,9 +63,8 @@ const api = {
|
|
|
72
63
|
return res.json();
|
|
73
64
|
},
|
|
74
65
|
updateTopic: async (groupId, topicId, payload) => {
|
|
75
|
-
const queryPayload = encodePayloadQuery(payload);
|
|
76
66
|
const res = await authFetch(
|
|
77
|
-
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics/${encodeURIComponent(topicId)}
|
|
67
|
+
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics/${encodeURIComponent(topicId)}`,
|
|
78
68
|
{
|
|
79
69
|
method: "PUT",
|
|
80
70
|
headers: { "Content-Type": "application/json" },
|
|
@@ -84,9 +74,8 @@ const api = {
|
|
|
84
74
|
return res.json();
|
|
85
75
|
},
|
|
86
76
|
configureGroup: async (groupId, payload) => {
|
|
87
|
-
const queryPayload = encodePayloadQuery(payload);
|
|
88
77
|
const res = await authFetch(
|
|
89
|
-
`/api/telegram/groups/${encodeURIComponent(groupId)}/configure
|
|
78
|
+
`/api/telegram/groups/${encodeURIComponent(groupId)}/configure`,
|
|
90
79
|
{
|
|
91
80
|
method: "POST",
|
|
92
81
|
headers: { "Content-Type": "application/json" },
|
|
@@ -427,16 +416,16 @@ const AddBotStep = ({
|
|
|
427
416
|
placeholder="-100XXXXXXXXXX"
|
|
428
417
|
class="flex-1 bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500"
|
|
429
418
|
/>
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
disabled=${
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
419
|
+
<${ActionButton}
|
|
420
|
+
onClick=${verify}
|
|
421
|
+
disabled=${!input.trim() || loading}
|
|
422
|
+
loading=${loading}
|
|
423
|
+
tone="secondary"
|
|
424
|
+
size="md"
|
|
425
|
+
idleLabel="Verify"
|
|
426
|
+
loadingMode="inline"
|
|
427
|
+
className="rounded-lg"
|
|
428
|
+
/>
|
|
440
429
|
</div>
|
|
441
430
|
|
|
442
431
|
${verifyGroupError &&
|
|
@@ -652,16 +641,16 @@ const TopicsStep = ({ groupId, topics, setTopics, onNext, onBack }) => {
|
|
|
652
641
|
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500 resize-y"
|
|
653
642
|
/>
|
|
654
643
|
<div class="flex justify-end">
|
|
655
|
-
|
|
656
|
-
|
|
644
|
+
<${ActionButton}
|
|
645
|
+
onClick=${createSingle}
|
|
657
646
|
disabled=${creating || !newTopicName.trim()}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
647
|
+
loading=${creating}
|
|
648
|
+
tone="secondary"
|
|
649
|
+
size="lg"
|
|
650
|
+
idleLabel="Add"
|
|
651
|
+
loadingMode="inline"
|
|
652
|
+
className="min-w-[88px]"
|
|
653
|
+
/>
|
|
665
654
|
</div>
|
|
666
655
|
</div>
|
|
667
656
|
</div>
|
|
@@ -932,7 +921,7 @@ const ManageTelegramWorkspace = ({
|
|
|
932
921
|
String(id)}
|
|
933
922
|
class="text-xs px-2 py-1 rounded transition-all ac-btn-cyan ${renamingTopicId ===
|
|
934
923
|
String(id)
|
|
935
|
-
? "opacity-
|
|
924
|
+
? "opacity-50 cursor-not-allowed"
|
|
936
925
|
: ""}"
|
|
937
926
|
>
|
|
938
927
|
Save
|
|
@@ -1045,16 +1034,15 @@ const ManageTelegramWorkspace = ({
|
|
|
1045
1034
|
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500 resize-y"
|
|
1046
1035
|
/>
|
|
1047
1036
|
<div class="flex justify-end">
|
|
1048
|
-
|
|
1049
|
-
|
|
1037
|
+
<${ActionButton}
|
|
1038
|
+
onClick=${createSingle}
|
|
1050
1039
|
disabled=${creating || !newTopicName.trim()}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
</button>
|
|
1040
|
+
loading=${creating}
|
|
1041
|
+
tone="secondary"
|
|
1042
|
+
size="lg"
|
|
1043
|
+
idleLabel="Add topic"
|
|
1044
|
+
loadingLabel="Creating..."
|
|
1045
|
+
/>
|
|
1058
1046
|
</div>
|
|
1059
1047
|
</div>
|
|
1060
1048
|
</div>
|
|
@@ -6,11 +6,38 @@ const html = htm.bind(h);
|
|
|
6
6
|
let toastId = 0;
|
|
7
7
|
let addToastFn = null;
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const kToastTypeByAlias = {
|
|
10
|
+
success: "success",
|
|
11
|
+
error: "error",
|
|
12
|
+
warning: "warning",
|
|
13
|
+
info: "info",
|
|
14
|
+
green: "success",
|
|
15
|
+
red: "error",
|
|
16
|
+
yellow: "warning",
|
|
17
|
+
blue: "info",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const kToastClassByType = {
|
|
21
|
+
success: "bg-green-950/95 border border-green-700/80 text-green-200",
|
|
22
|
+
error: "bg-red-950/95 border border-red-700/80 text-red-200",
|
|
23
|
+
warning: "bg-yellow-950/95 border border-yellow-700/80 text-yellow-100",
|
|
24
|
+
info: "bg-cyan-950/95 border border-cyan-700/80 text-cyan-100",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const normalizeToastType = (type) => {
|
|
28
|
+
const normalized = String(type || "")
|
|
29
|
+
.trim()
|
|
30
|
+
.toLowerCase();
|
|
31
|
+
return kToastTypeByAlias[normalized] || "info";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function showToast(text, type = "info") {
|
|
35
|
+
if (addToastFn) addToastFn({ id: ++toastId, text, type: normalizeToastType(type) });
|
|
11
36
|
}
|
|
12
37
|
|
|
13
|
-
export function ToastContainer(
|
|
38
|
+
export function ToastContainer({
|
|
39
|
+
className = "fixed bottom-4 right-4 z-50 space-y-2",
|
|
40
|
+
}) {
|
|
14
41
|
const [toasts, setToasts] = useState([]);
|
|
15
42
|
|
|
16
43
|
useEffect(() => {
|
|
@@ -21,9 +48,11 @@ export function ToastContainer() {
|
|
|
21
48
|
return () => { addToastFn = null; };
|
|
22
49
|
}, []);
|
|
23
50
|
|
|
24
|
-
|
|
51
|
+
if (toasts.length === 0) return null;
|
|
52
|
+
|
|
53
|
+
return html`<div class=${className}>
|
|
25
54
|
${toasts.map(t => html`
|
|
26
|
-
<div key=${t.id} class="
|
|
55
|
+
<div key=${t.id} class="${kToastClassByType[normalizeToastType(t.type)]} px-4 py-2 rounded-lg text-sm">
|
|
27
56
|
${t.text}
|
|
28
57
|
</div>
|
|
29
58
|
`)}
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
export const ToggleSwitch = ({
|
|
7
|
+
checked = false,
|
|
8
|
+
disabled = false,
|
|
9
|
+
onChange = () => {},
|
|
10
|
+
label = "Enabled",
|
|
11
|
+
}) => html`
|
|
12
|
+
<label class="ac-toggle">
|
|
13
|
+
<input
|
|
14
|
+
class="ac-toggle-input"
|
|
15
|
+
type="checkbox"
|
|
16
|
+
checked=${!!checked}
|
|
17
|
+
disabled=${!!disabled}
|
|
18
|
+
onchange=${(e) => onChange(!!e.target.checked)}
|
|
19
|
+
/>
|
|
20
|
+
<span class="ac-toggle-track" aria-hidden="true">
|
|
21
|
+
<span class="ac-toggle-thumb"></span>
|
|
22
|
+
</span>
|
|
23
|
+
${label ? html`<span class="ac-toggle-label">${label}</span>` : null}
|
|
24
|
+
</label>
|
|
25
|
+
`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { ActionButton } from "./action-button.js";
|
|
3
4
|
|
|
4
5
|
const html = htm.bind(h);
|
|
5
6
|
|
|
@@ -11,56 +12,15 @@ export const UpdateActionButton = ({
|
|
|
11
12
|
idleLabel = "Check updates",
|
|
12
13
|
loadingLabel = "Checking...",
|
|
13
14
|
className = "",
|
|
14
|
-
}) =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
: "opacity-80"}`
|
|
27
|
-
: "";
|
|
28
|
-
|
|
29
|
-
return html`
|
|
30
|
-
<button
|
|
31
|
-
onclick=${onClick}
|
|
32
|
-
disabled=${disabled || loading}
|
|
33
|
-
class="inline-flex items-center justify-center h-7 text-xs leading-none px-2.5 py-1 rounded-lg border transition-colors whitespace-nowrap ${toneClass} ${loadingClass} ${className}"
|
|
34
|
-
>
|
|
35
|
-
${loading
|
|
36
|
-
? html`
|
|
37
|
-
<span class="inline-flex items-center gap-1.5 leading-none">
|
|
38
|
-
<svg
|
|
39
|
-
class="animate-spin"
|
|
40
|
-
width="12"
|
|
41
|
-
height="12"
|
|
42
|
-
viewBox="0 0 24 24"
|
|
43
|
-
fill="none"
|
|
44
|
-
aria-hidden="true"
|
|
45
|
-
>
|
|
46
|
-
<circle
|
|
47
|
-
class="opacity-30"
|
|
48
|
-
cx="12"
|
|
49
|
-
cy="12"
|
|
50
|
-
r="9"
|
|
51
|
-
stroke="currentColor"
|
|
52
|
-
stroke-width="3"
|
|
53
|
-
/>
|
|
54
|
-
<path
|
|
55
|
-
class="opacity-90"
|
|
56
|
-
fill="currentColor"
|
|
57
|
-
d="M12 3a9 9 0 0 1 9 9h-3a6 6 0 0 0-6-6V3z"
|
|
58
|
-
/>
|
|
59
|
-
</svg>
|
|
60
|
-
${loadingLabel}
|
|
61
|
-
</span>
|
|
62
|
-
`
|
|
63
|
-
: idleLabel}
|
|
64
|
-
</button>
|
|
65
|
-
`;
|
|
66
|
-
};
|
|
15
|
+
}) => html`
|
|
16
|
+
<${ActionButton}
|
|
17
|
+
onClick=${onClick}
|
|
18
|
+
disabled=${disabled}
|
|
19
|
+
loading=${loading}
|
|
20
|
+
tone=${warning ? "warning" : "neutral"}
|
|
21
|
+
size="sm"
|
|
22
|
+
idleLabel=${idleLabel}
|
|
23
|
+
loadingLabel=${loadingLabel}
|
|
24
|
+
className=${className}
|
|
25
|
+
/>
|
|
26
|
+
`;
|
|
@@ -0,0 +1,312 @@
|
|
|
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
|
+
fetchWatchdogSettings,
|
|
8
|
+
updateWatchdogSettings,
|
|
9
|
+
triggerWatchdogRepair,
|
|
10
|
+
} from "../lib/api.js";
|
|
11
|
+
import { usePolling } from "../hooks/usePolling.js";
|
|
12
|
+
import { Gateway } from "./gateway.js";
|
|
13
|
+
import { InfoTooltip } from "./info-tooltip.js";
|
|
14
|
+
import { ToggleSwitch } from "./toggle-switch.js";
|
|
15
|
+
import { showToast } from "./toast.js";
|
|
16
|
+
|
|
17
|
+
const html = htm.bind(h);
|
|
18
|
+
|
|
19
|
+
const getIncidentStatusTone = (event) => {
|
|
20
|
+
const eventType = String(event?.eventType || "").trim().toLowerCase();
|
|
21
|
+
const status = String(event?.status || "").trim().toLowerCase();
|
|
22
|
+
if (status === "failed") {
|
|
23
|
+
return {
|
|
24
|
+
dotClass: "bg-red-500/90",
|
|
25
|
+
label: "Failed",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (status === "ok" && eventType === "health_check") {
|
|
29
|
+
return {
|
|
30
|
+
dotClass: "bg-green-500/90",
|
|
31
|
+
label: "Healthy",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (status === "warn" || status === "warning") {
|
|
35
|
+
return {
|
|
36
|
+
dotClass: "bg-yellow-400/90",
|
|
37
|
+
label: "Warning",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
dotClass: "bg-gray-500/70",
|
|
42
|
+
label: "Unknown",
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const WatchdogTab = ({
|
|
47
|
+
gatewayStatus = null,
|
|
48
|
+
openclawVersion = null,
|
|
49
|
+
watchdogStatus = null,
|
|
50
|
+
onRefreshStatuses = () => {},
|
|
51
|
+
restartingGateway = false,
|
|
52
|
+
onRestartGateway,
|
|
53
|
+
restartSignal = 0,
|
|
54
|
+
}) => {
|
|
55
|
+
const eventsPoll = usePolling(() => fetchWatchdogEvents(20), 15000);
|
|
56
|
+
const [settings, setSettings] = useState({
|
|
57
|
+
autoRepair: false,
|
|
58
|
+
notificationsEnabled: true,
|
|
59
|
+
});
|
|
60
|
+
const [savingSettings, setSavingSettings] = useState(false);
|
|
61
|
+
const [repairing, setRepairing] = useState(false);
|
|
62
|
+
const [logs, setLogs] = useState("");
|
|
63
|
+
const [loadingLogs, setLoadingLogs] = useState(true);
|
|
64
|
+
const [stickToBottom, setStickToBottom] = useState(true);
|
|
65
|
+
const logsRef = useRef(null);
|
|
66
|
+
|
|
67
|
+
const currentWatchdogStatus = watchdogStatus || {};
|
|
68
|
+
const events = eventsPoll.data?.events || [];
|
|
69
|
+
const isRepairInProgress = repairing || !!currentWatchdogStatus?.operationInProgress;
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
let active = true;
|
|
73
|
+
const loadSettings = async () => {
|
|
74
|
+
try {
|
|
75
|
+
const data = await fetchWatchdogSettings();
|
|
76
|
+
if (!active) return;
|
|
77
|
+
setSettings(
|
|
78
|
+
data.settings || {
|
|
79
|
+
autoRepair: false,
|
|
80
|
+
notificationsEnabled: true,
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (!active) return;
|
|
85
|
+
showToast(err.message || "Could not load watchdog settings", "error");
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
loadSettings();
|
|
89
|
+
return () => {
|
|
90
|
+
active = false;
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
let active = true;
|
|
96
|
+
let timer = null;
|
|
97
|
+
const pollLogs = async () => {
|
|
98
|
+
try {
|
|
99
|
+
const text = await fetchWatchdogLogs(65536);
|
|
100
|
+
if (!active) return;
|
|
101
|
+
setLogs(text || "");
|
|
102
|
+
setLoadingLogs(false);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (!active) return;
|
|
105
|
+
setLoadingLogs(false);
|
|
106
|
+
}
|
|
107
|
+
if (!active) return;
|
|
108
|
+
timer = setTimeout(pollLogs, 3000);
|
|
109
|
+
};
|
|
110
|
+
pollLogs();
|
|
111
|
+
return () => {
|
|
112
|
+
active = false;
|
|
113
|
+
if (timer) clearTimeout(timer);
|
|
114
|
+
};
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const el = logsRef.current;
|
|
119
|
+
if (!el || !stickToBottom) return;
|
|
120
|
+
el.scrollTop = el.scrollHeight;
|
|
121
|
+
}, [logs, stickToBottom]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!restartSignal) return;
|
|
125
|
+
onRefreshStatuses();
|
|
126
|
+
eventsPoll.refresh();
|
|
127
|
+
const t1 = setTimeout(() => {
|
|
128
|
+
onRefreshStatuses();
|
|
129
|
+
eventsPoll.refresh();
|
|
130
|
+
}, 1200);
|
|
131
|
+
const t2 = setTimeout(() => {
|
|
132
|
+
onRefreshStatuses();
|
|
133
|
+
eventsPoll.refresh();
|
|
134
|
+
}, 3500);
|
|
135
|
+
return () => {
|
|
136
|
+
clearTimeout(t1);
|
|
137
|
+
clearTimeout(t2);
|
|
138
|
+
};
|
|
139
|
+
}, [restartSignal, onRefreshStatuses, eventsPoll.refresh]);
|
|
140
|
+
|
|
141
|
+
const onToggleAutoRepair = async (nextValue) => {
|
|
142
|
+
if (savingSettings) return;
|
|
143
|
+
setSavingSettings(true);
|
|
144
|
+
try {
|
|
145
|
+
const data = await updateWatchdogSettings({ autoRepair: !!nextValue });
|
|
146
|
+
setSettings(
|
|
147
|
+
data.settings || {
|
|
148
|
+
...settings,
|
|
149
|
+
autoRepair: !!nextValue,
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
onRefreshStatuses();
|
|
153
|
+
showToast(`Auto-repair ${nextValue ? "enabled" : "disabled"}`, "success");
|
|
154
|
+
} catch (err) {
|
|
155
|
+
showToast(err.message || "Could not update auto-repair", "error");
|
|
156
|
+
} finally {
|
|
157
|
+
setSavingSettings(false);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const onToggleNotifications = async (nextValue) => {
|
|
162
|
+
if (savingSettings) return;
|
|
163
|
+
setSavingSettings(true);
|
|
164
|
+
try {
|
|
165
|
+
const data = await updateWatchdogSettings({
|
|
166
|
+
notificationsEnabled: !!nextValue,
|
|
167
|
+
});
|
|
168
|
+
setSettings(
|
|
169
|
+
data.settings || {
|
|
170
|
+
...settings,
|
|
171
|
+
notificationsEnabled: !!nextValue,
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
onRefreshStatuses();
|
|
175
|
+
showToast(`Notifications ${nextValue ? "enabled" : "disabled"}`, "success");
|
|
176
|
+
} catch (err) {
|
|
177
|
+
showToast(err.message || "Could not update notifications", "error");
|
|
178
|
+
} finally {
|
|
179
|
+
setSavingSettings(false);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const onRepair = async () => {
|
|
184
|
+
if (isRepairInProgress) return;
|
|
185
|
+
setRepairing(true);
|
|
186
|
+
try {
|
|
187
|
+
const data = await triggerWatchdogRepair();
|
|
188
|
+
if (!data.ok) throw new Error(data.error || "Repair failed");
|
|
189
|
+
showToast("Repair triggered", "success");
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
onRefreshStatuses();
|
|
192
|
+
eventsPoll.refresh();
|
|
193
|
+
}, 800);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
showToast(err.message || "Could not run repair", "error");
|
|
196
|
+
} finally {
|
|
197
|
+
setRepairing(false);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return html`
|
|
202
|
+
<div class="space-y-4">
|
|
203
|
+
<${Gateway}
|
|
204
|
+
status=${gatewayStatus}
|
|
205
|
+
openclawVersion=${openclawVersion}
|
|
206
|
+
restarting=${restartingGateway}
|
|
207
|
+
onRestart=${onRestartGateway}
|
|
208
|
+
watchdogStatus=${currentWatchdogStatus}
|
|
209
|
+
onRepair=${onRepair}
|
|
210
|
+
repairing=${isRepairInProgress}
|
|
211
|
+
/>
|
|
212
|
+
|
|
213
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
214
|
+
<div class="flex items-center justify-between gap-3">
|
|
215
|
+
<div class="inline-flex items-center gap-2 text-xs text-gray-400">
|
|
216
|
+
<span>Auto-repair</span>
|
|
217
|
+
<${InfoTooltip}
|
|
218
|
+
text="Automatically runs OpenClaw doctor repair when watchdog detects gateway health failures or crash loops."
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
<${ToggleSwitch}
|
|
222
|
+
checked=${!!settings.autoRepair}
|
|
223
|
+
disabled=${savingSettings}
|
|
224
|
+
onChange=${onToggleAutoRepair}
|
|
225
|
+
label=""
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="flex items-center justify-between gap-3 mt-3">
|
|
229
|
+
<div class="inline-flex items-center gap-2 text-xs text-gray-400">
|
|
230
|
+
<span>Notifications</span>
|
|
231
|
+
<${InfoTooltip}
|
|
232
|
+
text="Sends Telegram notices for watchdog alerts and auto-repair outcomes."
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
<${ToggleSwitch}
|
|
236
|
+
checked=${!!settings.notificationsEnabled}
|
|
237
|
+
disabled=${savingSettings}
|
|
238
|
+
onChange=${onToggleNotifications}
|
|
239
|
+
label=""
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
245
|
+
<div class="flex items-center justify-between gap-2 mb-3">
|
|
246
|
+
<h2 class="font-semibold text-sm">Logs</h2>
|
|
247
|
+
<label class="inline-flex items-center gap-2 text-xs text-gray-400">
|
|
248
|
+
<input
|
|
249
|
+
type="checkbox"
|
|
250
|
+
checked=${stickToBottom}
|
|
251
|
+
onchange=${(e) => setStickToBottom(!!e.target.checked)}
|
|
252
|
+
/>
|
|
253
|
+
Stick to bottom
|
|
254
|
+
</label>
|
|
255
|
+
</div>
|
|
256
|
+
<pre
|
|
257
|
+
ref=${logsRef}
|
|
258
|
+
class="bg-black/40 border border-border rounded-lg p-3 h-72 overflow-auto text-xs text-gray-300 whitespace-pre-wrap break-words"
|
|
259
|
+
>${loadingLogs ? "Loading logs..." : logs || "No logs yet."}</pre>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
263
|
+
<div class="flex items-center justify-between gap-2 mb-3">
|
|
264
|
+
<h2 class="font-semibold text-sm">Recent incidents</h2>
|
|
265
|
+
<button
|
|
266
|
+
class="text-xs text-gray-400 hover:text-gray-200"
|
|
267
|
+
onclick=${() => eventsPoll.refresh()}
|
|
268
|
+
>
|
|
269
|
+
Refresh
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="space-y-2">
|
|
273
|
+
${events.length === 0 &&
|
|
274
|
+
html`<p class="text-xs text-gray-500">No incidents recorded.</p>`}
|
|
275
|
+
${events.map(
|
|
276
|
+
(event) => {
|
|
277
|
+
const tone = getIncidentStatusTone(event);
|
|
278
|
+
return html`
|
|
279
|
+
<details class="border border-border rounded-lg p-2">
|
|
280
|
+
<summary class="cursor-pointer text-xs text-gray-300 list-none [&::-webkit-details-marker]:hidden">
|
|
281
|
+
<div class="flex items-center justify-between gap-2">
|
|
282
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
283
|
+
<span class="text-gray-500 shrink-0" aria-hidden="true">▸</span>
|
|
284
|
+
<span class="truncate">
|
|
285
|
+
${event.createdAt || ""} · ${event.eventType || "event"} · ${event.status ||
|
|
286
|
+
"unknown"}
|
|
287
|
+
</span>
|
|
288
|
+
</span>
|
|
289
|
+
<span
|
|
290
|
+
class=${`h-2.5 w-2.5 shrink-0 rounded-full ${tone.dotClass}`}
|
|
291
|
+
title=${tone.label}
|
|
292
|
+
aria-label=${tone.label}
|
|
293
|
+
></span>
|
|
294
|
+
</div>
|
|
295
|
+
</summary>
|
|
296
|
+
<div class="mt-2 text-xs text-gray-400">
|
|
297
|
+
<div>Source: ${event.source || "unknown"}</div>
|
|
298
|
+
<pre class="mt-2 bg-black/30 rounded p-2 whitespace-pre-wrap break-words"
|
|
299
|
+
>${typeof event.details === "string"
|
|
300
|
+
? event.details
|
|
301
|
+
: JSON.stringify(event.details || {}, null, 2)}</pre
|
|
302
|
+
>
|
|
303
|
+
</div>
|
|
304
|
+
</details>
|
|
305
|
+
`;
|
|
306
|
+
},
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
`;
|
|
312
|
+
};
|