@chrysb/alphaclaw 0.2.2 → 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/public/login.html +1 -0
- package/lib/public/setup.html +1 -0
- 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
|
@@ -23,10 +23,13 @@ export function Channels({ channels, onSwitchTab, onNavigate }) {
|
|
|
23
23
|
badge = html`<a
|
|
24
24
|
href="#"
|
|
25
25
|
onclick=${(e) => { e.preventDefault(); onSwitchTab?.('envars'); }}
|
|
26
|
-
class="text-xs
|
|
26
|
+
class="text-xs px-2 py-1 rounded-lg ac-btn-ghost"
|
|
27
27
|
>Add token</a>`;
|
|
28
28
|
} else if (info.status === 'paired') {
|
|
29
|
-
|
|
29
|
+
const pairedCount = Number(info.paired || 0);
|
|
30
|
+
badge = html`<${Badge} tone="success"
|
|
31
|
+
>${ch === 'telegram' || pairedCount <= 1 ? 'Paired' : `Paired (${pairedCount})`}</${Badge
|
|
32
|
+
}>`;
|
|
30
33
|
} else {
|
|
31
34
|
badge = html`<${Badge} tone="warning">Awaiting pairing</${Badge}>`;
|
|
32
35
|
}
|
|
@@ -39,15 +42,21 @@ export function Channels({ channels, onSwitchTab, onNavigate }) {
|
|
|
39
42
|
? html`<img src=${channelMeta.iconSrc} alt="" class="w-4 h-4 rounded-sm" aria-hidden="true" />`
|
|
40
43
|
: null}
|
|
41
44
|
${channelMeta.label}
|
|
42
|
-
</span>
|
|
43
|
-
<span class="flex items-center gap-2">
|
|
44
|
-
${badge}
|
|
45
45
|
${isClickable && html`
|
|
46
|
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="
|
|
47
|
-
<path
|
|
46
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" class="text-gray-600">
|
|
47
|
+
<path
|
|
48
|
+
d="M6 3.5L10.5 8L6 12.5"
|
|
49
|
+
stroke="currentColor"
|
|
50
|
+
stroke-width="2"
|
|
51
|
+
stroke-linecap="round"
|
|
52
|
+
stroke-linejoin="round"
|
|
53
|
+
/>
|
|
48
54
|
</svg>
|
|
49
55
|
`}
|
|
50
56
|
</span>
|
|
57
|
+
<span class="flex items-center gap-2">
|
|
58
|
+
${badge}
|
|
59
|
+
</span>
|
|
51
60
|
</div>`;
|
|
52
61
|
}) : html`<div class="text-gray-500 text-sm text-center py-2">Loading...</div>`}
|
|
53
62
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import { useEffect } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { ActionButton } from "./action-button.js";
|
|
4
5
|
|
|
5
6
|
const html = htm.bind(h);
|
|
6
7
|
|
|
@@ -8,11 +9,15 @@ export const ConfirmDialog = ({
|
|
|
8
9
|
visible = false,
|
|
9
10
|
title = "Confirm action",
|
|
10
11
|
message = "Are you sure you want to continue?",
|
|
12
|
+
details = null,
|
|
11
13
|
confirmLabel = "Confirm",
|
|
14
|
+
confirmLoadingLabel = "Working...",
|
|
12
15
|
cancelLabel = "Cancel",
|
|
13
16
|
onConfirm,
|
|
14
17
|
onCancel,
|
|
15
18
|
confirmTone = "primary",
|
|
19
|
+
confirmLoading = false,
|
|
20
|
+
confirmDisabled = false,
|
|
16
21
|
}) => {
|
|
17
22
|
useEffect(() => {
|
|
18
23
|
if (!visible) return;
|
|
@@ -28,11 +33,7 @@ export const ConfirmDialog = ({
|
|
|
28
33
|
}, [visible, onCancel]);
|
|
29
34
|
|
|
30
35
|
if (!visible) return null;
|
|
31
|
-
|
|
32
|
-
const confirmClass =
|
|
33
|
-
confirmTone === "warning"
|
|
34
|
-
? "border border-yellow-500/45 text-yellow-300 bg-[linear-gradient(180deg,rgba(234,179,8,0.22)_0%,rgba(234,179,8,0.12)_100%)] shadow-[inset_0_0_0_1px_rgba(234,179,8,0.18)] hover:border-yellow-300/75 hover:text-yellow-200 hover:bg-[linear-gradient(180deg,rgba(234,179,8,0.3)_0%,rgba(234,179,8,0.16)_100%)] hover:shadow-[inset_0_0_0_1px_rgba(234,179,8,0.26),0_0_12px_rgba(234,179,8,0.16)]"
|
|
35
|
-
: "ac-btn-cyan";
|
|
36
|
+
const actionTone = confirmTone === "warning" ? "warning" : "primary";
|
|
36
37
|
|
|
37
38
|
return html`
|
|
38
39
|
<div
|
|
@@ -44,21 +45,26 @@ export const ConfirmDialog = ({
|
|
|
44
45
|
<div class="bg-modal border border-border rounded-xl p-5 max-w-md w-full space-y-3">
|
|
45
46
|
<h2 class="text-base font-semibold">${title}</h2>
|
|
46
47
|
<p class="text-sm text-gray-400">${message}</p>
|
|
48
|
+
${details}
|
|
47
49
|
<div class="pt-1 flex items-center justify-end gap-2">
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
<${ActionButton}
|
|
51
|
+
onClick=${onCancel}
|
|
52
|
+
disabled=${confirmLoading}
|
|
53
|
+
tone="secondary"
|
|
54
|
+
size="md"
|
|
55
|
+
idleLabel=${cancelLabel}
|
|
56
|
+
className="px-4 py-2 rounded-lg text-sm"
|
|
57
|
+
/>
|
|
58
|
+
<${ActionButton}
|
|
59
|
+
onClick=${onConfirm}
|
|
60
|
+
disabled=${confirmDisabled}
|
|
61
|
+
loading=${confirmLoading}
|
|
62
|
+
tone=${actionTone}
|
|
63
|
+
size="md"
|
|
64
|
+
idleLabel=${confirmLabel}
|
|
65
|
+
loadingLabel=${confirmLoadingLabel}
|
|
66
|
+
className="px-4 py-2 rounded-lg text-sm"
|
|
67
|
+
/>
|
|
62
68
|
</div>
|
|
63
69
|
</div>
|
|
64
70
|
</div>
|
|
@@ -3,6 +3,10 @@ import { useState, useRef } from "https://esm.sh/preact/hooks";
|
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { saveGoogleCredentials } from "../lib/api.js";
|
|
5
5
|
import { SecretInput } from "./secret-input.js";
|
|
6
|
+
import { ModalShell } from "./modal-shell.js";
|
|
7
|
+
import { PageHeader } from "./page-header.js";
|
|
8
|
+
import { ActionButton } from "./action-button.js";
|
|
9
|
+
import { CloseIcon } from "./icons.js";
|
|
6
10
|
const html = htm.bind(h);
|
|
7
11
|
|
|
8
12
|
export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
@@ -93,16 +97,25 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
93
97
|
</div>
|
|
94
98
|
`;
|
|
95
99
|
|
|
96
|
-
return html`
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
return html` <${ModalShell}
|
|
101
|
+
visible=${visible}
|
|
102
|
+
onClose=${onClose}
|
|
103
|
+
closeOnOverlayClick=${false}
|
|
104
|
+
panelClassName="bg-modal border border-border rounded-xl p-6 max-w-lg w-full space-y-4"
|
|
101
105
|
>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
<${PageHeader}
|
|
107
|
+
title="Connect Google Workspace"
|
|
108
|
+
actions=${html`
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onclick=${onClose}
|
|
112
|
+
class="h-8 w-8 inline-flex items-center justify-center rounded-lg ac-btn-secondary"
|
|
113
|
+
aria-label="Close modal"
|
|
114
|
+
>
|
|
115
|
+
<${CloseIcon} className="w-3.5 h-3.5 text-gray-300" />
|
|
116
|
+
</button>
|
|
117
|
+
`}
|
|
118
|
+
/>
|
|
106
119
|
<div class="space-y-3">
|
|
107
120
|
<div>
|
|
108
121
|
<p class="text-gray-400 text-sm mb-3">
|
|
@@ -319,21 +332,17 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
319
332
|
</div>
|
|
320
333
|
</div>
|
|
321
334
|
<div class="flex gap-2 pt-2">
|
|
322
|
-
|
|
323
|
-
|
|
335
|
+
<${ActionButton}
|
|
336
|
+
onClick=${submit}
|
|
324
337
|
disabled=${saving}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
>
|
|
333
|
-
Cancel
|
|
334
|
-
</button>
|
|
338
|
+
loading=${saving}
|
|
339
|
+
tone="primary"
|
|
340
|
+
size="lg"
|
|
341
|
+
idleLabel="Connect Google"
|
|
342
|
+
loadingLabel="Saving..."
|
|
343
|
+
className="w-full px-4 py-2 rounded-lg text-sm"
|
|
344
|
+
/>
|
|
335
345
|
</div>
|
|
336
346
|
${error ? html`<div class="text-red-400 text-xs">${error}</div>` : null}
|
|
337
|
-
|
|
338
|
-
</div>`;
|
|
347
|
+
</${ModalShell}>`;
|
|
339
348
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { h } from 'https://esm.sh/preact';
|
|
2
2
|
import { useState } from 'https://esm.sh/preact/hooks';
|
|
3
3
|
import htm from 'https://esm.sh/htm';
|
|
4
|
+
import { ActionButton } from './action-button.js';
|
|
4
5
|
const html = htm.bind(h);
|
|
5
6
|
|
|
6
7
|
const kModeLabels = {
|
|
@@ -55,8 +56,20 @@ const DeviceRow = ({ d, onApprove, onReject }) => {
|
|
|
55
56
|
${subtitle && html`<span class="text-xs text-gray-500">${subtitle}</span>`}
|
|
56
57
|
</div>
|
|
57
58
|
<div class="flex gap-2">
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
<${ActionButton}
|
|
60
|
+
onClick=${() => handle('approve')}
|
|
61
|
+
tone="success"
|
|
62
|
+
size="sm"
|
|
63
|
+
idleLabel="Approve"
|
|
64
|
+
className="font-medium px-3 py-1.5"
|
|
65
|
+
/>
|
|
66
|
+
<${ActionButton}
|
|
67
|
+
onClick=${() => handle('reject')}
|
|
68
|
+
tone="secondary"
|
|
69
|
+
size="sm"
|
|
70
|
+
idleLabel="Reject"
|
|
71
|
+
className="font-medium px-3 py-1.5"
|
|
72
|
+
/>
|
|
60
73
|
</div>
|
|
61
74
|
</div>`;
|
|
62
75
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import { useState, useEffect, useCallback, useRef } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
|
-
import { fetchEnvVars, saveEnvVars
|
|
4
|
+
import { fetchEnvVars, saveEnvVars } from "../lib/api.js";
|
|
5
5
|
import { showToast } from "./toast.js";
|
|
6
6
|
import { SecretInput } from "./secret-input.js";
|
|
7
|
+
import { PageHeader } from "./page-header.js";
|
|
8
|
+
import { ActionButton } from "./action-button.js";
|
|
7
9
|
const html = htm.bind(h);
|
|
8
10
|
|
|
9
11
|
const kGroupLabels = {
|
|
@@ -80,13 +82,11 @@ const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
|
80
82
|
`;
|
|
81
83
|
};
|
|
82
84
|
|
|
83
|
-
export const Envars = () => {
|
|
85
|
+
export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
84
86
|
const [vars, setVars] = useState([]);
|
|
85
87
|
const [reservedKeys, setReservedKeys] = useState(() => new Set());
|
|
86
88
|
const [dirty, setDirty] = useState(false);
|
|
87
89
|
const [saving, setSaving] = useState(false);
|
|
88
|
-
const [restartingGateway, setRestartingGateway] = useState(false);
|
|
89
|
-
const [restartRequired, setRestartRequired] = useState(false);
|
|
90
90
|
const [newKey, setNewKey] = useState("");
|
|
91
91
|
const baselineSignatureRef = useRef("[]");
|
|
92
92
|
|
|
@@ -97,7 +97,7 @@ export const Envars = () => {
|
|
|
97
97
|
baselineSignatureRef.current = getVarsSignature(nextVars);
|
|
98
98
|
setVars(nextVars);
|
|
99
99
|
setReservedKeys(new Set(data.reservedKeys || []));
|
|
100
|
-
|
|
100
|
+
onRestartRequired(!!data.restartRequired);
|
|
101
101
|
} catch (err) {
|
|
102
102
|
console.error("Failed to load env vars:", err);
|
|
103
103
|
}
|
|
@@ -128,7 +128,7 @@ export const Envars = () => {
|
|
|
128
128
|
.map((v) => ({ key: v.key, value: v.value }));
|
|
129
129
|
const result = await saveEnvVars(toSave);
|
|
130
130
|
const needsRestart = !!result?.restartRequired;
|
|
131
|
-
|
|
131
|
+
if (needsRestart) onRestartRequired(true);
|
|
132
132
|
showToast(
|
|
133
133
|
needsRestart
|
|
134
134
|
? "Environment variables saved. Restart gateway to apply."
|
|
@@ -144,20 +144,6 @@ export const Envars = () => {
|
|
|
144
144
|
}
|
|
145
145
|
};
|
|
146
146
|
|
|
147
|
-
const handleRestartGateway = async () => {
|
|
148
|
-
if (restartingGateway) return;
|
|
149
|
-
setRestartingGateway(true);
|
|
150
|
-
try {
|
|
151
|
-
await restartGateway();
|
|
152
|
-
setRestartRequired(false);
|
|
153
|
-
showToast("Gateway restarted", "success");
|
|
154
|
-
} catch (err) {
|
|
155
|
-
showToast("Restart failed: " + err.message, "error");
|
|
156
|
-
} finally {
|
|
157
|
-
setRestartingGateway(false);
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
147
|
const [newVal, setNewVal] = useState("");
|
|
162
148
|
|
|
163
149
|
const parsePaste = (input) => {
|
|
@@ -281,6 +267,22 @@ export const Envars = () => {
|
|
|
281
267
|
|
|
282
268
|
return html`
|
|
283
269
|
<div class="space-y-4">
|
|
270
|
+
<${PageHeader}
|
|
271
|
+
title="Envars"
|
|
272
|
+
actions=${html`
|
|
273
|
+
<${ActionButton}
|
|
274
|
+
onClick=${handleSave}
|
|
275
|
+
disabled=${!dirty || saving}
|
|
276
|
+
loading=${saving}
|
|
277
|
+
tone="primary"
|
|
278
|
+
size="sm"
|
|
279
|
+
idleLabel="Save changes"
|
|
280
|
+
loadingLabel="Saving..."
|
|
281
|
+
className="transition-all"
|
|
282
|
+
/>
|
|
283
|
+
`}
|
|
284
|
+
/>
|
|
285
|
+
|
|
284
286
|
${kGroupOrder
|
|
285
287
|
.filter((g) => grouped[g]?.length)
|
|
286
288
|
.map(
|
|
@@ -341,51 +343,6 @@ export const Envars = () => {
|
|
|
341
343
|
</div>
|
|
342
344
|
</div>
|
|
343
345
|
|
|
344
|
-
${restartRequired
|
|
345
|
-
? html`<div
|
|
346
|
-
class="bg-yellow-500/10 border border-yellow-500/30 rounded-xl p-4 flex items-center justify-between gap-3"
|
|
347
|
-
>
|
|
348
|
-
<p class="text-sm text-yellow-200">
|
|
349
|
-
Gateway restart required to apply env changes.
|
|
350
|
-
</p>
|
|
351
|
-
<button
|
|
352
|
-
onclick=${handleRestartGateway}
|
|
353
|
-
disabled=${restartingGateway}
|
|
354
|
-
class="text-xs px-2.5 py-1 rounded-lg border border-yellow-500/40 text-yellow-200 hover:border-yellow-400 hover:text-yellow-100 transition-colors shrink-0 ${restartingGateway
|
|
355
|
-
? "opacity-60 cursor-not-allowed"
|
|
356
|
-
: ""}"
|
|
357
|
-
>
|
|
358
|
-
${restartingGateway ? "Restarting..." : "Restart Gateway"}
|
|
359
|
-
</button>
|
|
360
|
-
</div>`
|
|
361
|
-
: null}
|
|
362
|
-
|
|
363
|
-
<button
|
|
364
|
-
onclick=${handleSave}
|
|
365
|
-
disabled=${!dirty || saving || restartingGateway}
|
|
366
|
-
class="w-full text-sm font-medium px-4 py-2.5 rounded-xl transition-all ac-btn-cyan"
|
|
367
|
-
>
|
|
368
|
-
${saving
|
|
369
|
-
? html`<span class="flex items-center justify-center gap-2">
|
|
370
|
-
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
|
|
371
|
-
<circle
|
|
372
|
-
class="opacity-25"
|
|
373
|
-
cx="12"
|
|
374
|
-
cy="12"
|
|
375
|
-
r="10"
|
|
376
|
-
stroke="currentColor"
|
|
377
|
-
stroke-width="4"
|
|
378
|
-
/>
|
|
379
|
-
<path
|
|
380
|
-
class="opacity-75"
|
|
381
|
-
fill="currentColor"
|
|
382
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
383
|
-
/>
|
|
384
|
-
</svg>
|
|
385
|
-
Saving...
|
|
386
|
-
</span>`
|
|
387
|
-
: "Save changes"}
|
|
388
|
-
</button>
|
|
389
346
|
</div>
|
|
390
347
|
`;
|
|
391
348
|
};
|
|
@@ -63,7 +63,7 @@ export const Features = ({ onSwitchTab }) => {
|
|
|
63
63
|
e.preventDefault();
|
|
64
64
|
onSwitchTab?.("providers");
|
|
65
65
|
}}
|
|
66
|
-
class="text-xs
|
|
66
|
+
class="text-xs px-2 py-1 rounded-lg ac-btn-ghost"
|
|
67
67
|
>Add provider</a>
|
|
68
68
|
<${Badge} tone=${feature.hasDefault ? "neutral" : "danger"}>Disabled</${Badge}>
|
|
69
69
|
</span>
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import {
|
|
5
5
|
fetchOpenclawVersion,
|
|
6
|
-
restartGateway,
|
|
7
6
|
updateOpenclaw,
|
|
8
7
|
} from "../lib/api.js";
|
|
9
|
-
import { showToast } from "./toast.js";
|
|
10
8
|
import { UpdateActionButton } from "./update-action-button.js";
|
|
11
9
|
import { ConfirmDialog } from "./confirm-dialog.js";
|
|
10
|
+
import { showToast } from "./toast.js";
|
|
12
11
|
const html = htm.bind(h);
|
|
13
12
|
|
|
13
|
+
const formatDuration = (ms) => {
|
|
14
|
+
const safeMs = Number(ms || 0);
|
|
15
|
+
if (!Number.isFinite(safeMs) || safeMs <= 0) return "0s";
|
|
16
|
+
const totalSeconds = Math.floor(safeMs / 1000);
|
|
17
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
18
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
19
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
20
|
+
const seconds = totalSeconds % 60;
|
|
21
|
+
if (days > 0) return `${days}d ${hours % 24}h ${minutes}m ${seconds}s`;
|
|
22
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
23
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
24
|
+
return `${seconds}s`;
|
|
25
|
+
};
|
|
26
|
+
|
|
14
27
|
function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
15
28
|
const [checking, setChecking] = useState(false);
|
|
16
29
|
const [version, setVersion] = useState(currentVersion || null);
|
|
@@ -140,7 +153,12 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
140
153
|
? `${version}`
|
|
141
154
|
: "..."}
|
|
142
155
|
</p>
|
|
143
|
-
${error &&
|
|
156
|
+
${error && effectiveHasUpdate &&
|
|
157
|
+
html`<div
|
|
158
|
+
class="mt-1 text-xs text-red-300 bg-red-900/30 border border-red-800 rounded-lg px-2 py-1"
|
|
159
|
+
>
|
|
160
|
+
${error}
|
|
161
|
+
</div>`}
|
|
144
162
|
</div>
|
|
145
163
|
<div class="flex items-center gap-2 shrink-0">
|
|
146
164
|
${effectiveHasUpdate && effectiveLatestVersion && html`
|
|
@@ -212,44 +230,133 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
|
212
230
|
`;
|
|
213
231
|
}
|
|
214
232
|
|
|
215
|
-
export function Gateway({
|
|
216
|
-
|
|
233
|
+
export function Gateway({
|
|
234
|
+
status,
|
|
235
|
+
openclawVersion,
|
|
236
|
+
restarting = false,
|
|
237
|
+
onRestart,
|
|
238
|
+
watchdogStatus = null,
|
|
239
|
+
onOpenWatchdog,
|
|
240
|
+
onRepair,
|
|
241
|
+
repairing = false,
|
|
242
|
+
}) {
|
|
243
|
+
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
217
244
|
const isRunning = status === "running" && !restarting;
|
|
218
245
|
const dotClass = isRunning
|
|
219
|
-
? "
|
|
246
|
+
? "ac-status-dot ac-status-dot--healthy"
|
|
220
247
|
: "w-2 h-2 rounded-full bg-yellow-500 animate-pulse";
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
248
|
+
const watchdogHealth = watchdogStatus?.lifecycle === "crash_loop"
|
|
249
|
+
? "crash_loop"
|
|
250
|
+
: watchdogStatus?.health;
|
|
251
|
+
const watchdogDotClass = watchdogHealth === "healthy"
|
|
252
|
+
? "ac-status-dot ac-status-dot--healthy ac-status-dot--healthy-offset"
|
|
253
|
+
: watchdogHealth === "degraded"
|
|
254
|
+
? "bg-yellow-500"
|
|
255
|
+
: watchdogHealth === "unhealthy" || watchdogHealth === "crash_loop"
|
|
256
|
+
? "bg-red-500"
|
|
257
|
+
: "bg-gray-500";
|
|
258
|
+
const watchdogLabel = watchdogHealth === "unknown"
|
|
259
|
+
? "initializing"
|
|
260
|
+
: watchdogHealth || "unknown";
|
|
261
|
+
const isRepairInProgress = repairing || !!watchdogStatus?.operationInProgress;
|
|
262
|
+
const showRepairButton =
|
|
263
|
+
isRepairInProgress ||
|
|
264
|
+
watchdogStatus?.lifecycle === "crash_loop" ||
|
|
265
|
+
watchdogStatus?.health === "degraded" ||
|
|
266
|
+
watchdogStatus?.health === "unhealthy" ||
|
|
267
|
+
watchdogStatus?.health === "crashed";
|
|
268
|
+
const liveUptimeMs = useMemo(() => {
|
|
269
|
+
const startedAtMs = watchdogStatus?.uptimeStartedAt
|
|
270
|
+
? Date.parse(watchdogStatus.uptimeStartedAt)
|
|
271
|
+
: null;
|
|
272
|
+
if (Number.isFinite(startedAtMs)) {
|
|
273
|
+
return Math.max(0, nowMs - startedAtMs);
|
|
230
274
|
}
|
|
231
|
-
|
|
232
|
-
};
|
|
275
|
+
return watchdogStatus?.uptimeMs || 0;
|
|
276
|
+
}, [watchdogStatus?.uptimeStartedAt, watchdogStatus?.uptimeMs, nowMs]);
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
const id = setInterval(() => {
|
|
280
|
+
setNowMs(Date.now());
|
|
281
|
+
}, 1000);
|
|
282
|
+
return () => clearInterval(id);
|
|
283
|
+
}, []);
|
|
233
284
|
|
|
234
285
|
return html` <div class="bg-surface border border-border rounded-xl p-4">
|
|
235
|
-
<div class="
|
|
236
|
-
<div class="
|
|
237
|
-
<div class="flex items-center gap-2">
|
|
286
|
+
<div class="space-y-2">
|
|
287
|
+
<div class="flex items-center justify-between gap-3">
|
|
288
|
+
<div class="min-w-0 flex items-center gap-2 text-sm">
|
|
238
289
|
<span class=${dotClass}></span>
|
|
239
290
|
<span class="font-semibold">Gateway:</span>
|
|
240
|
-
<span class="text-gray-400"
|
|
241
|
-
|
|
242
|
-
|
|
291
|
+
<span class="text-gray-400">${restarting ? "restarting..." : status || "checking..."}</span>
|
|
292
|
+
${!restarting && isRunning
|
|
293
|
+
? html`
|
|
294
|
+
<span class="hidden md:inline text-gray-500">·</span>
|
|
295
|
+
<span class="hidden md:inline text-xs text-gray-500"
|
|
296
|
+
>Uptime: ${formatDuration(liveUptimeMs)}</span
|
|
297
|
+
>
|
|
298
|
+
`
|
|
299
|
+
: null}
|
|
243
300
|
</div>
|
|
301
|
+
<${UpdateActionButton}
|
|
302
|
+
onClick=${onRestart}
|
|
303
|
+
disabled=${!status}
|
|
304
|
+
loading=${restarting}
|
|
305
|
+
warning=${false}
|
|
306
|
+
idleLabel="Restart"
|
|
307
|
+
loadingLabel="On it..."
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
${!restarting && isRunning
|
|
311
|
+
? html`
|
|
312
|
+
<div class="md:hidden pl-4 text-xs text-gray-500">
|
|
313
|
+
Uptime: ${formatDuration(liveUptimeMs)}
|
|
314
|
+
</div>
|
|
315
|
+
`
|
|
316
|
+
: null}
|
|
317
|
+
<div class="flex items-center justify-between gap-3">
|
|
318
|
+
${onOpenWatchdog
|
|
319
|
+
? html`
|
|
320
|
+
<button
|
|
321
|
+
class="inline-flex items-center gap-2 text-sm hover:opacity-90"
|
|
322
|
+
onclick=${onOpenWatchdog}
|
|
323
|
+
title="Open Watchdog tab"
|
|
324
|
+
>
|
|
325
|
+
<span class=${watchdogDotClass.startsWith("ac-status-dot")
|
|
326
|
+
? watchdogDotClass
|
|
327
|
+
: `w-2 h-2 rounded-full ${watchdogDotClass}`}></span>
|
|
328
|
+
<span class="font-semibold">Watchdog:</span>
|
|
329
|
+
<span class="text-gray-400">${watchdogLabel}</span>
|
|
330
|
+
</button>
|
|
331
|
+
`
|
|
332
|
+
: html`
|
|
333
|
+
<div class="inline-flex items-center gap-2 text-sm">
|
|
334
|
+
<span class=${watchdogDotClass.startsWith("ac-status-dot")
|
|
335
|
+
? watchdogDotClass
|
|
336
|
+
: `w-2 h-2 rounded-full ${watchdogDotClass}`}></span>
|
|
337
|
+
<span class="font-semibold">Watchdog:</span>
|
|
338
|
+
<span class="text-gray-400">${watchdogLabel}</span>
|
|
339
|
+
</div>
|
|
340
|
+
`}
|
|
341
|
+
${onRepair
|
|
342
|
+
? html`
|
|
343
|
+
<div class="shrink-0 w-32 flex justify-end">
|
|
344
|
+
${showRepairButton
|
|
345
|
+
? html`
|
|
346
|
+
<${UpdateActionButton}
|
|
347
|
+
onClick=${onRepair}
|
|
348
|
+
loading=${isRepairInProgress}
|
|
349
|
+
warning=${true}
|
|
350
|
+
idleLabel="Repair"
|
|
351
|
+
loadingLabel="Repairing..."
|
|
352
|
+
className="w-full justify-center"
|
|
353
|
+
/>
|
|
354
|
+
`
|
|
355
|
+
: html`<span class="inline-flex h-7 w-full" aria-hidden="true"></span>`}
|
|
356
|
+
</div>
|
|
357
|
+
`
|
|
358
|
+
: null}
|
|
244
359
|
</div>
|
|
245
|
-
<${UpdateActionButton}
|
|
246
|
-
onClick=${handleRestart}
|
|
247
|
-
disabled=${!status}
|
|
248
|
-
loading=${restarting}
|
|
249
|
-
warning=${false}
|
|
250
|
-
idleLabel="Restart"
|
|
251
|
-
loadingLabel="On it..."
|
|
252
|
-
/>
|
|
253
360
|
</div>
|
|
254
361
|
<div class="mt-3 pt-3 border-t border-border">
|
|
255
362
|
<${VersionRow}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { UpdateActionButton } from "./update-action-button.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const GlobalRestartBanner = ({
|
|
8
|
+
visible = false,
|
|
9
|
+
restarting = false,
|
|
10
|
+
onRestart,
|
|
11
|
+
}) => {
|
|
12
|
+
if (!visible) return null;
|
|
13
|
+
return html`
|
|
14
|
+
<div class="global-restart-banner">
|
|
15
|
+
<div class="global-restart-banner__content">
|
|
16
|
+
<p class="global-restart-banner__text">
|
|
17
|
+
Gateway restart required to apply pending configuration changes.
|
|
18
|
+
</p>
|
|
19
|
+
<${UpdateActionButton}
|
|
20
|
+
onClick=${onRestart}
|
|
21
|
+
disabled=${restarting}
|
|
22
|
+
loading=${restarting}
|
|
23
|
+
warning=${true}
|
|
24
|
+
idleLabel="Restart Gateway"
|
|
25
|
+
loadingLabel="Restarting..."
|
|
26
|
+
className="global-restart-banner__button"
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
`;
|
|
31
|
+
};
|