@chrysb/alphaclaw 0.4.2 → 0.4.4
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/js/components/action-button.js +16 -2
- package/lib/public/js/components/envars.js +50 -3
- package/lib/public/js/components/file-tree.js +37 -8
- package/lib/public/js/components/file-viewer/toolbar.js +27 -15
- package/lib/public/js/components/gateway.js +74 -42
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/usage-tab/overview-section.js +100 -26
- package/lib/public/js/lib/api.js +31 -0
- package/lib/server/db/usage/index.js +35 -0
- package/lib/server/db/usage/pricing.js +82 -0
- package/lib/server/db/usage/schema.js +87 -0
- package/lib/server/db/usage/sessions.js +217 -0
- package/lib/server/db/usage/shared.js +139 -0
- package/lib/server/db/usage/summary.js +280 -0
- package/lib/server/db/usage/timeseries.js +64 -0
- package/lib/server/{watchdog-db.js → db/watchdog/index.js} +1 -18
- package/lib/server/db/watchdog/schema.js +21 -0
- package/lib/server/{webhooks-db.js → db/webhooks/index.js} +1 -22
- package/lib/server/db/webhooks/schema.js +25 -0
- package/lib/server/routes/browse/index.js +29 -0
- package/lib/server.js +3 -3
- package/package.json +1 -1
- package/lib/server/usage-db.js +0 -838
|
@@ -31,6 +31,11 @@ const kSizeClassBySize = {
|
|
|
31
31
|
md: "h-9 text-sm font-medium leading-none px-4 rounded-xl",
|
|
32
32
|
lg: "h-10 text-sm font-medium leading-none px-5 rounded-lg",
|
|
33
33
|
};
|
|
34
|
+
const kIconOnlySizeClassBySize = {
|
|
35
|
+
sm: "h-7 w-7 p-0 rounded-lg",
|
|
36
|
+
md: "h-9 w-9 p-0 rounded-xl",
|
|
37
|
+
lg: "h-10 w-10 p-0 rounded-lg",
|
|
38
|
+
};
|
|
34
39
|
|
|
35
40
|
export const ActionButton = ({
|
|
36
41
|
onClick,
|
|
@@ -45,11 +50,16 @@ export const ActionButton = ({
|
|
|
45
50
|
className = "",
|
|
46
51
|
idleIcon = null,
|
|
47
52
|
idleIconClassName = "h-3 w-3",
|
|
53
|
+
iconOnly = false,
|
|
54
|
+
title = "",
|
|
55
|
+
ariaLabel = "",
|
|
48
56
|
}) => {
|
|
49
57
|
const isDisabled = disabled || loading;
|
|
50
58
|
const isInteractive = !isDisabled;
|
|
51
59
|
const toneClass = getToneClass(tone, isInteractive);
|
|
52
|
-
const sizeClass =
|
|
60
|
+
const sizeClass = iconOnly
|
|
61
|
+
? kIconOnlySizeClassBySize[size] || kIconOnlySizeClassBySize.sm
|
|
62
|
+
: kSizeClassBySize[size] || kSizeClassBySize.sm;
|
|
53
63
|
const loadingClass = loading
|
|
54
64
|
? `cursor-not-allowed ${
|
|
55
65
|
tone === "warning"
|
|
@@ -60,7 +70,9 @@ export const ActionButton = ({
|
|
|
60
70
|
const spinnerSizeClass = size === "md" || size === "lg" ? "h-4 w-4" : "h-3 w-3";
|
|
61
71
|
const isInlineLoading = loadingMode === "inline";
|
|
62
72
|
const IdleIcon = idleIcon;
|
|
63
|
-
const idleContent = IdleIcon
|
|
73
|
+
const idleContent = iconOnly && IdleIcon
|
|
74
|
+
? html`<${IdleIcon} className=${idleIconClassName} />`
|
|
75
|
+
: IdleIcon
|
|
64
76
|
? html`
|
|
65
77
|
<span class="inline-flex items-center gap-1.5 leading-none">
|
|
66
78
|
<${IdleIcon} className=${idleIconClassName} />
|
|
@@ -75,6 +87,8 @@ export const ActionButton = ({
|
|
|
75
87
|
type=${type}
|
|
76
88
|
onclick=${onClick}
|
|
77
89
|
disabled=${isDisabled}
|
|
90
|
+
title=${title}
|
|
91
|
+
aria-label=${ariaLabel || null}
|
|
78
92
|
class="inline-flex items-center justify-center transition-colors whitespace-nowrap ${sizeClass} ${toneClass} ${loadingClass} ${className}"
|
|
79
93
|
>
|
|
80
94
|
${isInlineLoading
|
|
@@ -17,6 +17,17 @@ const kGroupLabels = {
|
|
|
17
17
|
|
|
18
18
|
const kGroupOrder = ["github", "channels", "tools", "custom"];
|
|
19
19
|
const normalizeEnvVarKey = (raw) => raw.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
20
|
+
const stripSurroundingQuotes = (raw) => {
|
|
21
|
+
const value = String(raw || "").trim();
|
|
22
|
+
if (value.length < 2) return value;
|
|
23
|
+
const startsWithDouble = value.startsWith('"');
|
|
24
|
+
const endsWithDouble = value.endsWith('"');
|
|
25
|
+
if (startsWithDouble && endsWithDouble) return value.slice(1, -1);
|
|
26
|
+
const startsWithSingle = value.startsWith("'");
|
|
27
|
+
const endsWithSingle = value.endsWith("'");
|
|
28
|
+
if (startsWithSingle && endsWithSingle) return value.slice(1, -1);
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
20
31
|
const getVarsSignature = (items) =>
|
|
21
32
|
JSON.stringify(
|
|
22
33
|
(items || [])
|
|
@@ -27,6 +38,20 @@ const getVarsSignature = (items) =>
|
|
|
27
38
|
.sort((a, b) => a.key.localeCompare(b.key)),
|
|
28
39
|
);
|
|
29
40
|
|
|
41
|
+
const sortCustomVarsAlphabetically = (items) => {
|
|
42
|
+
const list = Array.isArray(items) ? [...items] : [];
|
|
43
|
+
const customSorted = list
|
|
44
|
+
.filter((item) => (item?.group || "custom") === "custom")
|
|
45
|
+
.sort((a, b) => String(a?.key || "").localeCompare(String(b?.key || "")));
|
|
46
|
+
let customIdx = 0;
|
|
47
|
+
return list.map((item) => {
|
|
48
|
+
if ((item?.group || "custom") !== "custom") return item;
|
|
49
|
+
const next = customSorted[customIdx];
|
|
50
|
+
customIdx += 1;
|
|
51
|
+
return next;
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
30
55
|
const kHintByKey = {
|
|
31
56
|
ANTHROPIC_API_KEY: html`from <a href="https://console.anthropic.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.anthropic.com</a>`,
|
|
32
57
|
ANTHROPIC_TOKEN: html`from <code class="text-xs bg-black/30 px-1 rounded">claude setup-token</code>`,
|
|
@@ -85,6 +110,8 @@ const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
|
85
110
|
export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
86
111
|
const [vars, setVars] = useState([]);
|
|
87
112
|
const [reservedKeys, setReservedKeys] = useState(() => new Set());
|
|
113
|
+
const [pendingCustomKeys, setPendingCustomKeys] = useState([]);
|
|
114
|
+
const [secretMaskEpoch, setSecretMaskEpoch] = useState(0);
|
|
88
115
|
const [dirty, setDirty] = useState(false);
|
|
89
116
|
const [saving, setSaving] = useState(false);
|
|
90
117
|
const [newKey, setNewKey] = useState("");
|
|
@@ -93,9 +120,10 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
93
120
|
const load = useCallback(async () => {
|
|
94
121
|
try {
|
|
95
122
|
const data = await fetchEnvVars();
|
|
96
|
-
const nextVars = data.vars || [];
|
|
123
|
+
const nextVars = sortCustomVarsAlphabetically(data.vars || []);
|
|
97
124
|
baselineSignatureRef.current = getVarsSignature(nextVars);
|
|
98
125
|
setVars(nextVars);
|
|
126
|
+
setPendingCustomKeys([]);
|
|
99
127
|
setReservedKeys(new Set(data.reservedKeys || []));
|
|
100
128
|
onRestartRequired(!!data.restartRequired);
|
|
101
129
|
} catch (err) {
|
|
@@ -117,6 +145,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
117
145
|
|
|
118
146
|
const handleDelete = (key) => {
|
|
119
147
|
setVars((prev) => prev.filter((v) => v.key !== key));
|
|
148
|
+
setPendingCustomKeys((prev) => prev.filter((pendingKey) => pendingKey !== key));
|
|
120
149
|
};
|
|
121
150
|
|
|
122
151
|
const handleSave = async () => {
|
|
@@ -135,7 +164,11 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
135
164
|
: "Environment variables saved",
|
|
136
165
|
"success",
|
|
137
166
|
);
|
|
138
|
-
|
|
167
|
+
const sortedVars = sortCustomVarsAlphabetically(vars);
|
|
168
|
+
setVars(sortedVars);
|
|
169
|
+
setPendingCustomKeys([]);
|
|
170
|
+
setSecretMaskEpoch((prev) => prev + 1);
|
|
171
|
+
baselineSignatureRef.current = getVarsSignature(sortedVars);
|
|
139
172
|
setDirty(false);
|
|
140
173
|
} catch (err) {
|
|
141
174
|
showToast("Failed to save: " + err.message, "error");
|
|
@@ -158,7 +191,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
158
191
|
if (eqIdx > 0)
|
|
159
192
|
pairs.push({
|
|
160
193
|
key: line.slice(0, eqIdx).trim(),
|
|
161
|
-
value: line.slice(eqIdx + 1)
|
|
194
|
+
value: stripSurroundingQuotes(line.slice(eqIdx + 1)),
|
|
162
195
|
});
|
|
163
196
|
}
|
|
164
197
|
return pairs;
|
|
@@ -167,6 +200,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
167
200
|
const addVars = (pairs) => {
|
|
168
201
|
let added = 0;
|
|
169
202
|
const blocked = [];
|
|
203
|
+
const addedCustomKeys = [];
|
|
170
204
|
setVars((prev) => {
|
|
171
205
|
const next = [...prev];
|
|
172
206
|
for (const { key: rawKey, value } of pairs) {
|
|
@@ -189,11 +223,15 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
189
223
|
source: "env_file",
|
|
190
224
|
editable: true,
|
|
191
225
|
});
|
|
226
|
+
addedCustomKeys.push(key);
|
|
192
227
|
}
|
|
193
228
|
added++;
|
|
194
229
|
}
|
|
195
230
|
return next;
|
|
196
231
|
});
|
|
232
|
+
if (addedCustomKeys.length) {
|
|
233
|
+
setPendingCustomKeys((prev) => [...prev, ...addedCustomKeys]);
|
|
234
|
+
}
|
|
197
235
|
return { added, blocked };
|
|
198
236
|
};
|
|
199
237
|
|
|
@@ -264,6 +302,14 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
264
302
|
if (!grouped[g]) grouped[g] = [];
|
|
265
303
|
grouped[g].push(v);
|
|
266
304
|
}
|
|
305
|
+
if (grouped.custom?.length) {
|
|
306
|
+
const pending = new Set(pendingCustomKeys);
|
|
307
|
+
const nonPending = grouped.custom
|
|
308
|
+
.filter((item) => !pending.has(item.key))
|
|
309
|
+
.sort((a, b) => String(a?.key || "").localeCompare(String(b?.key || "")));
|
|
310
|
+
const pendingAtBottom = grouped.custom.filter((item) => pending.has(item.key));
|
|
311
|
+
grouped.custom = [...nonPending, ...pendingAtBottom];
|
|
312
|
+
}
|
|
267
313
|
|
|
268
314
|
return html`
|
|
269
315
|
<div class="space-y-4">
|
|
@@ -295,6 +341,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
295
341
|
${grouped[g].map(
|
|
296
342
|
(v) =>
|
|
297
343
|
html`<${EnvRow}
|
|
344
|
+
key=${`${secretMaskEpoch}:${v.key}`}
|
|
298
345
|
envVar=${v}
|
|
299
346
|
onChange=${handleChange}
|
|
300
347
|
onDelete=${handleDelete}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
createBrowseFile,
|
|
14
14
|
createBrowseFolder,
|
|
15
15
|
moveBrowsePath,
|
|
16
|
+
downloadBrowseFile,
|
|
16
17
|
} from "../lib/api.js";
|
|
17
18
|
import {
|
|
18
19
|
kDraftIndexChangedEventName,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
FileAddLineIcon,
|
|
41
42
|
FolderAddLineIcon,
|
|
42
43
|
DeleteBinLineIcon,
|
|
44
|
+
DownloadLineIcon,
|
|
43
45
|
} from "./icons.js";
|
|
44
46
|
import { LoadingSpinner } from "./loading-spinner.js";
|
|
45
47
|
import { ConfirmDialog } from "./confirm-dialog.js";
|
|
@@ -224,6 +226,7 @@ const TreeContextMenu = ({
|
|
|
224
226
|
isLocked,
|
|
225
227
|
onNewFile,
|
|
226
228
|
onNewFolder,
|
|
229
|
+
onDownload,
|
|
227
230
|
onDelete,
|
|
228
231
|
onClose,
|
|
229
232
|
}) => {
|
|
@@ -252,6 +255,7 @@ const TreeContextMenu = ({
|
|
|
252
255
|
const isRoot = targetType === "root";
|
|
253
256
|
const contextFolder = isFolder ? targetPath : "";
|
|
254
257
|
const canCreate = !isLocked && (isFolder || isRoot);
|
|
258
|
+
const canDownload = isFile && targetPath;
|
|
255
259
|
const canDelete = !isLocked && (isFolder || isFile) && targetPath;
|
|
256
260
|
|
|
257
261
|
return html`
|
|
@@ -278,18 +282,33 @@ const TreeContextMenu = ({
|
|
|
278
282
|
</button>
|
|
279
283
|
`
|
|
280
284
|
: null}
|
|
281
|
-
${canDelete
|
|
285
|
+
${canDownload || canDelete
|
|
282
286
|
? html`
|
|
283
287
|
${canCreate
|
|
284
288
|
? html`<div class="tree-context-menu-sep"></div>`
|
|
285
289
|
: null}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
${canDownload
|
|
291
|
+
? html`
|
|
292
|
+
<button
|
|
293
|
+
class="tree-context-menu-item"
|
|
294
|
+
onclick=${() => { onDownload(targetPath); onClose(); }}
|
|
295
|
+
>
|
|
296
|
+
<${DownloadLineIcon} className="tree-context-menu-icon" />
|
|
297
|
+
<span>Download</span>
|
|
298
|
+
</button>
|
|
299
|
+
`
|
|
300
|
+
: null}
|
|
301
|
+
${canDelete
|
|
302
|
+
? html`
|
|
303
|
+
<button
|
|
304
|
+
class="tree-context-menu-item"
|
|
305
|
+
onclick=${() => { onDelete(targetPath); onClose(); }}
|
|
306
|
+
>
|
|
307
|
+
<${DeleteBinLineIcon} className="tree-context-menu-icon" />
|
|
308
|
+
<span>Delete</span>
|
|
309
|
+
</button>
|
|
310
|
+
`
|
|
311
|
+
: null}
|
|
293
312
|
`
|
|
294
313
|
: null}
|
|
295
314
|
${isLocked
|
|
@@ -942,6 +961,15 @@ export const FileTree = ({
|
|
|
942
961
|
setContextMenu(null);
|
|
943
962
|
};
|
|
944
963
|
|
|
964
|
+
const requestDownload = async (targetPath) => {
|
|
965
|
+
try {
|
|
966
|
+
await downloadBrowseFile(targetPath);
|
|
967
|
+
showToast("Download started", "success");
|
|
968
|
+
} catch (downloadError) {
|
|
969
|
+
showToast(downloadError.message || "Could not download file", "error");
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
|
|
945
973
|
const handleDragDrop = async (action, sourcePath, targetFolder) => {
|
|
946
974
|
if (action === "start") {
|
|
947
975
|
setDragSourcePath(sourcePath);
|
|
@@ -1208,6 +1236,7 @@ export const FileTree = ({
|
|
|
1208
1236
|
isLocked=${!!contextMenu.isLocked}
|
|
1209
1237
|
onNewFile=${(folder) => requestCreate(folder, "file")}
|
|
1210
1238
|
onNewFolder=${(folder) => requestCreate(folder, "folder")}
|
|
1239
|
+
onDownload=${requestDownload}
|
|
1211
1240
|
onDelete=${requestDelete}
|
|
1212
1241
|
onClose=${closeContextMenu}
|
|
1213
1242
|
/>
|
|
@@ -39,18 +39,25 @@ export const FileViewerToolbar = ({
|
|
|
39
39
|
${pathSegments.map(
|
|
40
40
|
(segment, index) => html`
|
|
41
41
|
<span class="file-viewer-breadcrumb-item">
|
|
42
|
-
<span
|
|
42
|
+
<span
|
|
43
|
+
class=${index === pathSegments.length - 1 ? "is-current" : ""}
|
|
44
|
+
>
|
|
43
45
|
${segment}
|
|
44
46
|
</span>
|
|
45
|
-
${index < pathSegments.length - 1 &&
|
|
47
|
+
${index < pathSegments.length - 1 &&
|
|
48
|
+
html`<span class="file-viewer-sep">></span>`}
|
|
46
49
|
</span>
|
|
47
50
|
`,
|
|
48
51
|
)}
|
|
49
52
|
</span>
|
|
50
|
-
${isDirty
|
|
53
|
+
${isDirty
|
|
54
|
+
? html`<span class="file-viewer-dirty-dot" aria-hidden="true"></span>`
|
|
55
|
+
: null}
|
|
51
56
|
</div>
|
|
52
57
|
<div class="file-viewer-tabbar-spacer"></div>
|
|
53
|
-
${isPreviewOnly
|
|
58
|
+
${isPreviewOnly
|
|
59
|
+
? html`<div class="file-viewer-preview-pill">Preview</div>`
|
|
60
|
+
: null}
|
|
54
61
|
${!isDiffView &&
|
|
55
62
|
isMarkdownFile &&
|
|
56
63
|
html`
|
|
@@ -69,27 +76,32 @@ export const FileViewerToolbar = ({
|
|
|
69
76
|
? html`
|
|
70
77
|
${!isProtectedFile
|
|
71
78
|
? html`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
onclick=${onRequestDelete}
|
|
79
|
+
<${ActionButton}
|
|
80
|
+
onClick=${onRequestDelete}
|
|
75
81
|
disabled=${!canDeleteFile || deleting}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
82
|
+
tone="secondary"
|
|
83
|
+
size="sm"
|
|
84
|
+
iconOnly=${true}
|
|
85
|
+
idleLabel=""
|
|
86
|
+
idleIcon=${DeleteBinLineIcon}
|
|
87
|
+
idleIconClassName="file-viewer-icon-action-icon"
|
|
88
|
+
className="file-viewer-save-action"
|
|
79
89
|
title=${isDeleteBlocked
|
|
80
90
|
? "Locked files cannot be deleted"
|
|
81
91
|
: "Delete file"}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<${DeleteBinLineIcon} className="file-viewer-icon-action-icon" />
|
|
85
|
-
</button>
|
|
92
|
+
ariaLabel="Delete file"
|
|
93
|
+
/>
|
|
86
94
|
`
|
|
87
95
|
: null}
|
|
88
96
|
${isDirty
|
|
89
97
|
? html`
|
|
90
98
|
<${ActionButton}
|
|
91
99
|
onClick=${handleDiscard}
|
|
92
|
-
disabled=${loading ||
|
|
100
|
+
disabled=${loading ||
|
|
101
|
+
!canEditFile ||
|
|
102
|
+
isEditBlocked ||
|
|
103
|
+
deleting ||
|
|
104
|
+
saving}
|
|
93
105
|
tone="secondary"
|
|
94
106
|
size="sm"
|
|
95
107
|
idleLabel="Discard changes"
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
|
-
import {
|
|
5
|
-
fetchOpenclawVersion,
|
|
6
|
-
updateOpenclaw,
|
|
7
|
-
} from "../lib/api.js";
|
|
4
|
+
import { fetchOpenclawVersion, updateOpenclaw } from "../lib/api.js";
|
|
8
5
|
import { UpdateActionButton } from "./update-action-button.js";
|
|
9
6
|
import { ConfirmDialog } from "./confirm-dialog.js";
|
|
10
7
|
import { showToast } from "./toast.js";
|
|
@@ -38,7 +35,8 @@ const VersionRow = ({
|
|
|
38
35
|
const [hasUpdate, setHasUpdate] = useState(false);
|
|
39
36
|
const [error, setError] = useState("");
|
|
40
37
|
const [hasViewedChangelog, setHasViewedChangelog] = useState(false);
|
|
41
|
-
const [confirmWithoutChangelogOpen, setConfirmWithoutChangelogOpen] =
|
|
38
|
+
const [confirmWithoutChangelogOpen, setConfirmWithoutChangelogOpen] =
|
|
39
|
+
useState(false);
|
|
42
40
|
const simulateUpdate = (() => {
|
|
43
41
|
try {
|
|
44
42
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -124,7 +122,9 @@ const VersionRow = ({
|
|
|
124
122
|
setChecking(true);
|
|
125
123
|
setError("");
|
|
126
124
|
try {
|
|
127
|
-
const data = isUpdateAction
|
|
125
|
+
const data = isUpdateAction
|
|
126
|
+
? await applyUpdate()
|
|
127
|
+
: await fetchVersion(true);
|
|
128
128
|
setVersion(data.currentVersion || version);
|
|
129
129
|
setLatestVersion(data.latestVersion || null);
|
|
130
130
|
setHasUpdate(!!data.hasUpdate);
|
|
@@ -158,10 +158,14 @@ const VersionRow = ({
|
|
|
158
158
|
} catch (err) {
|
|
159
159
|
setError(
|
|
160
160
|
err.message ||
|
|
161
|
-
(isUpdateAction
|
|
161
|
+
(isUpdateAction
|
|
162
|
+
? `Could not update ${label}`
|
|
163
|
+
: "Could not check updates"),
|
|
162
164
|
);
|
|
163
165
|
showToast(
|
|
164
|
-
isUpdateAction
|
|
166
|
+
isUpdateAction
|
|
167
|
+
? `Could not update ${label}`
|
|
168
|
+
: "Could not check updates",
|
|
165
169
|
"error",
|
|
166
170
|
);
|
|
167
171
|
await onActionComplete({
|
|
@@ -201,7 +205,8 @@ const VersionRow = ({
|
|
|
201
205
|
? `${version}`
|
|
202
206
|
: "..."}
|
|
203
207
|
</p>
|
|
204
|
-
${error &&
|
|
208
|
+
${error &&
|
|
209
|
+
effectiveHasUpdate &&
|
|
205
210
|
html`<div
|
|
206
211
|
class="mt-1 text-xs text-red-300 bg-red-900/30 border border-red-800 rounded-lg px-2 py-1"
|
|
207
212
|
>
|
|
@@ -209,7 +214,9 @@ const VersionRow = ({
|
|
|
209
214
|
</div>`}
|
|
210
215
|
</div>
|
|
211
216
|
<div class="flex items-center gap-3 shrink-0">
|
|
212
|
-
${effectiveHasUpdate &&
|
|
217
|
+
${effectiveHasUpdate &&
|
|
218
|
+
effectiveLatestVersion &&
|
|
219
|
+
html`
|
|
213
220
|
<a
|
|
214
221
|
href=${changelogUrl}
|
|
215
222
|
target="_blank"
|
|
@@ -228,7 +235,9 @@ const VersionRow = ({
|
|
|
228
235
|
idleLabel=${isUpdateActionActive
|
|
229
236
|
? updateIdleLabel
|
|
230
237
|
: "Check updates"}
|
|
231
|
-
loadingLabel=${isUpdateActionActive
|
|
238
|
+
loadingLabel=${isUpdateActionActive
|
|
239
|
+
? "Updating..."
|
|
240
|
+
: "Checking..."}
|
|
232
241
|
className="hidden md:inline-flex"
|
|
233
242
|
/>
|
|
234
243
|
`
|
|
@@ -240,12 +249,15 @@ const VersionRow = ({
|
|
|
240
249
|
idleLabel=${isUpdateActionActive
|
|
241
250
|
? updateIdleLabel
|
|
242
251
|
: "Check updates"}
|
|
243
|
-
loadingLabel=${isUpdateActionActive
|
|
252
|
+
loadingLabel=${isUpdateActionActive
|
|
253
|
+
? "Updating..."
|
|
254
|
+
: "Checking..."}
|
|
244
255
|
/>
|
|
245
256
|
`}
|
|
246
257
|
</div>
|
|
247
258
|
</div>
|
|
248
|
-
${showMobileUpdateRow &&
|
|
259
|
+
${showMobileUpdateRow &&
|
|
260
|
+
html`
|
|
249
261
|
<div class="mt-2 md:hidden flex items-center gap-2">
|
|
250
262
|
<a
|
|
251
263
|
href=${changelogUrl}
|
|
@@ -296,24 +308,25 @@ export const Gateway = ({
|
|
|
296
308
|
const dotClass = isRunning
|
|
297
309
|
? "ac-status-dot ac-status-dot--healthy"
|
|
298
310
|
: "w-2 h-2 rounded-full bg-yellow-500 animate-pulse";
|
|
299
|
-
const watchdogHealth =
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
? "
|
|
306
|
-
: watchdogHealth === "
|
|
307
|
-
? "bg-
|
|
308
|
-
: "
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
311
|
+
const watchdogHealth =
|
|
312
|
+
watchdogStatus?.lifecycle === "crash_loop"
|
|
313
|
+
? "crash_loop"
|
|
314
|
+
: watchdogStatus?.health;
|
|
315
|
+
const watchdogDotClass =
|
|
316
|
+
watchdogHealth === "healthy"
|
|
317
|
+
? "ac-status-dot ac-status-dot--healthy ac-status-dot--healthy-offset"
|
|
318
|
+
: watchdogHealth === "degraded"
|
|
319
|
+
? "bg-yellow-500"
|
|
320
|
+
: watchdogHealth === "unhealthy" || watchdogHealth === "crash_loop"
|
|
321
|
+
? "bg-red-500"
|
|
322
|
+
: "bg-gray-500";
|
|
323
|
+
const watchdogLabel =
|
|
324
|
+
watchdogHealth === "unknown" ? "initializing" : watchdogHealth || "unknown";
|
|
312
325
|
const isRepairInProgress = repairing || !!watchdogStatus?.operationInProgress;
|
|
326
|
+
const showInspectButton = watchdogHealth === "degraded" && !!onOpenWatchdog;
|
|
313
327
|
const showRepairButton =
|
|
314
328
|
isRepairInProgress ||
|
|
315
329
|
watchdogStatus?.lifecycle === "crash_loop" ||
|
|
316
|
-
watchdogStatus?.health === "degraded" ||
|
|
317
330
|
watchdogStatus?.health === "unhealthy" ||
|
|
318
331
|
watchdogStatus?.health === "crashed";
|
|
319
332
|
const liveUptimeMs = useMemo(() => {
|
|
@@ -339,7 +352,9 @@ export const Gateway = ({
|
|
|
339
352
|
<div class="min-w-0 flex items-center gap-2 text-sm">
|
|
340
353
|
<span class=${dotClass}></span>
|
|
341
354
|
<span class="font-semibold">Gateway:</span>
|
|
342
|
-
<span class="text-gray-400"
|
|
355
|
+
<span class="text-gray-400"
|
|
356
|
+
>${restarting ? "restarting..." : status || "checking..."}</span
|
|
357
|
+
>
|
|
343
358
|
</div>
|
|
344
359
|
<div class="flex items-center gap-3 shrink-0">
|
|
345
360
|
${!restarting && isRunning
|
|
@@ -367,18 +382,22 @@ export const Gateway = ({
|
|
|
367
382
|
onclick=${onOpenWatchdog}
|
|
368
383
|
title="Open Watchdog tab"
|
|
369
384
|
>
|
|
370
|
-
<span
|
|
371
|
-
|
|
372
|
-
|
|
385
|
+
<span
|
|
386
|
+
class=${watchdogDotClass.startsWith("ac-status-dot")
|
|
387
|
+
? watchdogDotClass
|
|
388
|
+
: `w-2 h-2 rounded-full ${watchdogDotClass}`}
|
|
389
|
+
></span>
|
|
373
390
|
<span class="font-semibold">Watchdog:</span>
|
|
374
391
|
<span class="text-gray-400">${watchdogLabel}</span>
|
|
375
392
|
</button>
|
|
376
393
|
`
|
|
377
394
|
: html`
|
|
378
395
|
<div class="inline-flex items-center gap-2 text-sm">
|
|
379
|
-
<span
|
|
380
|
-
|
|
381
|
-
|
|
396
|
+
<span
|
|
397
|
+
class=${watchdogDotClass.startsWith("ac-status-dot")
|
|
398
|
+
? watchdogDotClass
|
|
399
|
+
: `w-2 h-2 rounded-full ${watchdogDotClass}`}
|
|
400
|
+
></span>
|
|
382
401
|
<span class="font-semibold">Watchdog:</span>
|
|
383
402
|
<span class="text-gray-400">${watchdogLabel}</span>
|
|
384
403
|
</div>
|
|
@@ -386,24 +405,37 @@ export const Gateway = ({
|
|
|
386
405
|
${onRepair
|
|
387
406
|
? html`
|
|
388
407
|
<div class="shrink-0 w-32 flex justify-end">
|
|
389
|
-
${
|
|
408
|
+
${showInspectButton
|
|
390
409
|
? html`
|
|
391
410
|
<${UpdateActionButton}
|
|
392
|
-
onClick=${
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
loadingLabel="Repairing..."
|
|
411
|
+
onClick=${onOpenWatchdog}
|
|
412
|
+
warning=${false}
|
|
413
|
+
idleLabel="Inspect"
|
|
414
|
+
loadingLabel="Inspect"
|
|
397
415
|
className="w-full justify-center"
|
|
398
416
|
/>
|
|
399
417
|
`
|
|
400
|
-
:
|
|
418
|
+
: showRepairButton
|
|
419
|
+
? html`
|
|
420
|
+
<${UpdateActionButton}
|
|
421
|
+
onClick=${onRepair}
|
|
422
|
+
loading=${isRepairInProgress}
|
|
423
|
+
warning=${true}
|
|
424
|
+
idleLabel="Repair"
|
|
425
|
+
loadingLabel="Repairing..."
|
|
426
|
+
className="w-full justify-center"
|
|
427
|
+
/>
|
|
428
|
+
`
|
|
429
|
+
: html`<span
|
|
430
|
+
class="inline-flex h-7 w-full"
|
|
431
|
+
aria-hidden="true"
|
|
432
|
+
></span>`}
|
|
401
433
|
</div>
|
|
402
434
|
`
|
|
403
435
|
: null}
|
|
404
436
|
</div>
|
|
405
437
|
</div>
|
|
406
|
-
<div class="mt-3
|
|
438
|
+
<div class="mt-3">
|
|
407
439
|
<${VersionRow}
|
|
408
440
|
label="OpenClaw"
|
|
409
441
|
currentVersion=${openclawVersion}
|
|
@@ -288,6 +288,19 @@ export const FolderAddLineIcon = ({ className = "" }) => html`
|
|
|
288
288
|
</svg>
|
|
289
289
|
`;
|
|
290
290
|
|
|
291
|
+
export const DownloadLineIcon = ({ className = "" }) => html`
|
|
292
|
+
<svg
|
|
293
|
+
class=${className}
|
|
294
|
+
viewBox="0 0 24 24"
|
|
295
|
+
fill="currentColor"
|
|
296
|
+
aria-hidden="true"
|
|
297
|
+
>
|
|
298
|
+
<path
|
|
299
|
+
d="M3 19H21V21H3V19ZM13 13.1716L19.0711 7.1005L20.4853 8.51472L12 17L3.51472 8.51472L4.92893 7.1005L11 13.1716V2H13V13.1716Z"
|
|
300
|
+
/>
|
|
301
|
+
</svg>
|
|
302
|
+
`;
|
|
303
|
+
|
|
291
304
|
export const RestartLineIcon = ({ className = "" }) => html`
|
|
292
305
|
<svg
|
|
293
306
|
class=${className}
|