@chrysb/alphaclaw 0.1.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 +338 -0
- package/lib/public/icons/chevron-down.svg +9 -0
- package/lib/public/js/app.js +325 -0
- package/lib/public/js/components/badge.js +16 -0
- package/lib/public/js/components/channels.js +36 -0
- package/lib/public/js/components/credentials-modal.js +336 -0
- package/lib/public/js/components/device-pairings.js +72 -0
- package/lib/public/js/components/envars.js +354 -0
- package/lib/public/js/components/gateway.js +163 -0
- package/lib/public/js/components/google.js +223 -0
- package/lib/public/js/components/icons.js +23 -0
- package/lib/public/js/components/models.js +461 -0
- package/lib/public/js/components/pairings.js +74 -0
- package/lib/public/js/components/scope-picker.js +106 -0
- package/lib/public/js/components/toast.js +31 -0
- package/lib/public/js/components/welcome.js +541 -0
- package/lib/public/js/hooks/usePolling.js +29 -0
- package/lib/public/js/lib/api.js +196 -0
- package/lib/public/js/lib/model-config.js +88 -0
- package/lib/public/login.html +90 -0
- package/lib/public/setup.html +33 -0
- package/lib/scripts/systemctl +56 -0
- package/lib/server/auth-profiles.js +101 -0
- package/lib/server/commands.js +84 -0
- package/lib/server/constants.js +282 -0
- package/lib/server/env.js +78 -0
- package/lib/server/gateway.js +262 -0
- package/lib/server/helpers.js +192 -0
- package/lib/server/login-throttle.js +86 -0
- package/lib/server/onboarding/cron.js +51 -0
- package/lib/server/onboarding/github.js +49 -0
- package/lib/server/onboarding/index.js +127 -0
- package/lib/server/onboarding/openclaw.js +171 -0
- package/lib/server/onboarding/validation.js +107 -0
- package/lib/server/onboarding/workspace.js +52 -0
- package/lib/server/openclaw-version.js +179 -0
- package/lib/server/routes/auth.js +80 -0
- package/lib/server/routes/codex.js +204 -0
- package/lib/server/routes/google.js +390 -0
- package/lib/server/routes/models.js +68 -0
- package/lib/server/routes/onboarding.js +116 -0
- package/lib/server/routes/pages.js +21 -0
- package/lib/server/routes/pairings.js +134 -0
- package/lib/server/routes/proxy.js +29 -0
- package/lib/server/routes/system.js +213 -0
- package/lib/server.js +161 -0
- package/lib/setup/core-prompts/AGENTS.md +22 -0
- package/lib/setup/core-prompts/TOOLS.md +18 -0
- package/lib/setup/env.template +19 -0
- package/lib/setup/gitignore +12 -0
- package/lib/setup/hourly-git-sync.sh +86 -0
- package/lib/setup/skills/control-ui/SKILL.md +70 -0
- package/package.json +34 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useState, useEffect, useCallback } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { fetchEnvVars, saveEnvVars, restartGateway } from "../lib/api.js";
|
|
5
|
+
import { showToast } from "./toast.js";
|
|
6
|
+
const html = htm.bind(h);
|
|
7
|
+
|
|
8
|
+
const kGroupLabels = {
|
|
9
|
+
github: "GitHub",
|
|
10
|
+
channels: "Channels",
|
|
11
|
+
tools: "Tools",
|
|
12
|
+
custom: "Custom",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const kGroupOrder = ["github", "channels", "tools", "custom"];
|
|
16
|
+
|
|
17
|
+
const kHintByKey = {
|
|
18
|
+
ANTHROPIC_API_KEY: html`From <a href="https://console.anthropic.com" target="_blank" class="text-blue-400 hover:underline">console.anthropic.com</a>`,
|
|
19
|
+
ANTHROPIC_TOKEN: html`From <code class="text-xs bg-black/30 px-1 rounded">claude setup-token</code>`,
|
|
20
|
+
OPENAI_API_KEY: html`From <a href="https://platform.openai.com" target="_blank" class="text-blue-400 hover:underline">platform.openai.com</a>`,
|
|
21
|
+
GEMINI_API_KEY: html`From <a href="https://aistudio.google.com" target="_blank" class="text-blue-400 hover:underline">aistudio.google.com</a>`,
|
|
22
|
+
GITHUB_TOKEN: html`Use a <strong>classic PAT</strong> with <code class="text-xs bg-black/30 px-1 rounded">repo</code> scope from <a href="https://github.com/settings/tokens" target="_blank" class="text-blue-400 hover:underline">github.com/settings/tokens</a>. Fine-grained tokens can fail unless they include equivalent repo write permissions.`,
|
|
23
|
+
GITHUB_WORKSPACE_REPO: html`Use <code class="text-xs bg-black/30 px-1 rounded">owner/repo</code> or <code class="text-xs bg-black/30 px-1 rounded">https://github.com/owner/repo</code>`,
|
|
24
|
+
TELEGRAM_BOT_TOKEN: html`From <a href="https://t.me/BotFather" target="_blank" class="text-blue-400 hover:underline">@BotFather</a> · <a href="https://docs.openclaw.ai/channels/telegram" target="_blank" class="text-blue-400 hover:underline">full guide</a>`,
|
|
25
|
+
DISCORD_BOT_TOKEN: html`From <a href="https://discord.com/developers/applications" target="_blank" class="text-blue-400 hover:underline">Developer Portal</a> · <a href="https://docs.openclaw.ai/channels/discord" target="_blank" class="text-blue-400 hover:underline">full guide</a>`,
|
|
26
|
+
BRAVE_API_KEY: html`From <a href="https://brave.com/search/api/" target="_blank" class="text-blue-400 hover:underline">brave.com/search/api</a> — free tier available`,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getHintContent = (envVar) => kHintByKey[envVar.key] || envVar.hint || "";
|
|
30
|
+
|
|
31
|
+
const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
32
|
+
const [visible, setVisible] = useState(false);
|
|
33
|
+
const isSecret = !!envVar.value;
|
|
34
|
+
|
|
35
|
+
return html`
|
|
36
|
+
<div class="flex items-center gap-2">
|
|
37
|
+
<div class="flex-1 min-w-0">
|
|
38
|
+
<div class="flex items-center gap-2 mb-1">
|
|
39
|
+
<span
|
|
40
|
+
class="inline-block w-2 h-2 rounded-full ${envVar.value
|
|
41
|
+
? "bg-green-500"
|
|
42
|
+
: "bg-gray-600"}"
|
|
43
|
+
/>
|
|
44
|
+
<label class="text-xs font-medium text-gray-400">
|
|
45
|
+
${envVar.label || envVar.key}
|
|
46
|
+
</label>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="flex items-center gap-1">
|
|
49
|
+
<input
|
|
50
|
+
type=${isSecret && !visible ? "password" : "text"}
|
|
51
|
+
value=${envVar.value}
|
|
52
|
+
placeholder=${envVar.value ? "" : "not set"}
|
|
53
|
+
onInput=${(e) => onChange(envVar.key, e.target.value)}
|
|
54
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
55
|
+
disabled=${disabled}
|
|
56
|
+
/>
|
|
57
|
+
${isSecret
|
|
58
|
+
? html`<button
|
|
59
|
+
onclick=${() => setVisible(!visible)}
|
|
60
|
+
class="text-gray-500 hover:text-gray-300 px-1 text-xs shrink-0"
|
|
61
|
+
title=${visible ? "Hide" : "Show"}
|
|
62
|
+
>
|
|
63
|
+
${visible ? "Hide" : "Show"}
|
|
64
|
+
</button>`
|
|
65
|
+
: null}
|
|
66
|
+
${envVar.group === "custom"
|
|
67
|
+
? html`<button
|
|
68
|
+
onclick=${() => onDelete(envVar.key)}
|
|
69
|
+
class="text-gray-600 hover:text-red-400 px-1 text-xs shrink-0"
|
|
70
|
+
title="Delete"
|
|
71
|
+
>
|
|
72
|
+
✕
|
|
73
|
+
</button>`
|
|
74
|
+
: null}
|
|
75
|
+
</div>
|
|
76
|
+
${getHintContent(envVar)
|
|
77
|
+
? html`<p class="text-xs text-gray-600 mt-1">${getHintContent(envVar)}</p>`
|
|
78
|
+
: null}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
`;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const Envars = () => {
|
|
85
|
+
const [vars, setVars] = useState([]);
|
|
86
|
+
const [dirty, setDirty] = useState(false);
|
|
87
|
+
const [saving, setSaving] = useState(false);
|
|
88
|
+
const [restartingGateway, setRestartingGateway] = useState(false);
|
|
89
|
+
const [restartRequired, setRestartRequired] = useState(false);
|
|
90
|
+
const [newKey, setNewKey] = useState("");
|
|
91
|
+
|
|
92
|
+
const load = useCallback(async () => {
|
|
93
|
+
try {
|
|
94
|
+
const data = await fetchEnvVars();
|
|
95
|
+
setVars(data.vars || []);
|
|
96
|
+
setDirty(false);
|
|
97
|
+
setRestartRequired(!!data.restartRequired);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error("Failed to load env vars:", err);
|
|
100
|
+
}
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
load();
|
|
105
|
+
}, [load]);
|
|
106
|
+
|
|
107
|
+
const handleChange = (key, value) => {
|
|
108
|
+
setVars((prev) => prev.map((v) => (v.key === key ? { ...v, value } : v)));
|
|
109
|
+
setDirty(true);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleDelete = (key) => {
|
|
113
|
+
setVars((prev) => prev.filter((v) => v.key !== key));
|
|
114
|
+
setDirty(true);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleSave = async () => {
|
|
118
|
+
if (saving) return;
|
|
119
|
+
setSaving(true);
|
|
120
|
+
try {
|
|
121
|
+
const toSave = vars.filter((v) => v.editable).map((v) => ({ key: v.key, value: v.value }));
|
|
122
|
+
const result = await saveEnvVars(toSave);
|
|
123
|
+
const needsRestart = !!result?.restartRequired;
|
|
124
|
+
setRestartRequired(needsRestart);
|
|
125
|
+
showToast(
|
|
126
|
+
needsRestart
|
|
127
|
+
? "Environment variables saved. Restart gateway to apply."
|
|
128
|
+
: "Environment variables saved",
|
|
129
|
+
"success",
|
|
130
|
+
);
|
|
131
|
+
setDirty(false);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
showToast("Failed to save: " + err.message, "error");
|
|
134
|
+
} finally {
|
|
135
|
+
setSaving(false);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const handleRestartGateway = async () => {
|
|
140
|
+
if (restartingGateway) return;
|
|
141
|
+
setRestartingGateway(true);
|
|
142
|
+
try {
|
|
143
|
+
await restartGateway();
|
|
144
|
+
setRestartRequired(false);
|
|
145
|
+
showToast("Gateway restarted", "success");
|
|
146
|
+
} catch (err) {
|
|
147
|
+
showToast("Restart failed: " + err.message, "error");
|
|
148
|
+
} finally {
|
|
149
|
+
setRestartingGateway(false);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const [newVal, setNewVal] = useState("");
|
|
154
|
+
|
|
155
|
+
const parsePaste = (input) => {
|
|
156
|
+
const lines = input.split("\n").map((l) => l.trim()).filter(Boolean).filter((l) => !l.startsWith("#"));
|
|
157
|
+
const pairs = [];
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
const eqIdx = line.indexOf("=");
|
|
160
|
+
if (eqIdx > 0) pairs.push({ key: line.slice(0, eqIdx).trim(), value: line.slice(eqIdx + 1).trim() });
|
|
161
|
+
}
|
|
162
|
+
return pairs;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const addVars = (pairs) => {
|
|
166
|
+
let added = 0;
|
|
167
|
+
setVars((prev) => {
|
|
168
|
+
const next = [...prev];
|
|
169
|
+
for (const { key: rawKey, value } of pairs) {
|
|
170
|
+
const key = rawKey.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
171
|
+
if (!key) continue;
|
|
172
|
+
const existing = next.find((v) => v.key === key);
|
|
173
|
+
if (existing) {
|
|
174
|
+
existing.value = value;
|
|
175
|
+
} else {
|
|
176
|
+
next.push({ key, value, label: key, group: "custom", hint: "", source: "env_file", editable: true });
|
|
177
|
+
}
|
|
178
|
+
added++;
|
|
179
|
+
}
|
|
180
|
+
return next;
|
|
181
|
+
});
|
|
182
|
+
if (added) setDirty(true);
|
|
183
|
+
return added;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const handlePaste = (e, fallbackField) => {
|
|
187
|
+
const text = (e.clipboardData || window.clipboardData).getData("text");
|
|
188
|
+
const pairs = parsePaste(text);
|
|
189
|
+
if (pairs.length > 1) {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
const added = addVars(pairs);
|
|
192
|
+
setNewKey("");
|
|
193
|
+
setNewVal("");
|
|
194
|
+
showToast(`Added ${added} variable${added !== 1 ? "s" : ""}`, "success");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (pairs.length === 1) {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
setNewKey(pairs[0].key);
|
|
200
|
+
setNewVal(pairs[0].value);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handleKeyInput = (raw) => {
|
|
206
|
+
const pairs = parsePaste(raw);
|
|
207
|
+
if (pairs.length === 1) {
|
|
208
|
+
setNewKey(pairs[0].key);
|
|
209
|
+
setNewVal(pairs[0].value);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
setNewKey(raw);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const handleValInput = (raw) => {
|
|
216
|
+
const pairs = parsePaste(raw);
|
|
217
|
+
if (pairs.length === 1) {
|
|
218
|
+
setNewKey(pairs[0].key);
|
|
219
|
+
setNewVal(pairs[0].value);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
setNewVal(raw);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const handleAddVar = () => {
|
|
226
|
+
const key = newKey.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
227
|
+
if (!key) return;
|
|
228
|
+
addVars([{ key, value: newVal }]);
|
|
229
|
+
setNewKey("");
|
|
230
|
+
setNewVal("");
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Group vars
|
|
234
|
+
const grouped = {};
|
|
235
|
+
for (const v of vars) {
|
|
236
|
+
const g = v.group || "custom";
|
|
237
|
+
if (!grouped[g]) grouped[g] = [];
|
|
238
|
+
grouped[g].push(v);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return html`
|
|
242
|
+
<div class="space-y-4">
|
|
243
|
+
${kGroupOrder
|
|
244
|
+
.filter((g) => grouped[g]?.length)
|
|
245
|
+
.map(
|
|
246
|
+
(g) => html`
|
|
247
|
+
<div
|
|
248
|
+
class="bg-surface border border-border rounded-xl p-4 space-y-3"
|
|
249
|
+
>
|
|
250
|
+
<h3 class="text-sm font-medium text-gray-400">
|
|
251
|
+
${kGroupLabels[g] || g}
|
|
252
|
+
</h3>
|
|
253
|
+
${grouped[g].map(
|
|
254
|
+
(v) =>
|
|
255
|
+
html`<${EnvRow}
|
|
256
|
+
envVar=${v}
|
|
257
|
+
onChange=${handleChange}
|
|
258
|
+
onDelete=${handleDelete}
|
|
259
|
+
disabled=${saving}
|
|
260
|
+
/>`
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
`
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
267
|
+
<div class="flex items-center justify-between">
|
|
268
|
+
<h3 class="text-sm font-medium text-gray-400">Add Variable</h3>
|
|
269
|
+
<span class="text-xs text-gray-600">Paste KEY=VALUE or multiple lines</span>
|
|
270
|
+
</div>
|
|
271
|
+
<input
|
|
272
|
+
type="text"
|
|
273
|
+
value=${newKey}
|
|
274
|
+
placeholder="VARIABLE_NAME"
|
|
275
|
+
onInput=${(e) => handleKeyInput(e.target.value)}
|
|
276
|
+
onPaste=${(e) => handlePaste(e, "key")}
|
|
277
|
+
onKeyDown=${(e) => e.key === "Enter" && handleAddVar()}
|
|
278
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono uppercase"
|
|
279
|
+
/>
|
|
280
|
+
<div class="flex gap-2">
|
|
281
|
+
<input
|
|
282
|
+
type="text"
|
|
283
|
+
value=${newVal}
|
|
284
|
+
placeholder="value"
|
|
285
|
+
onInput=${(e) => handleValInput(e.target.value)}
|
|
286
|
+
onPaste=${(e) => handlePaste(e, "val")}
|
|
287
|
+
onKeyDown=${(e) => e.key === "Enter" && handleAddVar()}
|
|
288
|
+
class="flex-1 bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
289
|
+
/>
|
|
290
|
+
<button
|
|
291
|
+
onclick=${handleAddVar}
|
|
292
|
+
class="text-sm px-3 py-1.5 rounded-lg border border-border text-gray-400 hover:text-gray-200 hover:border-gray-500 shrink-0"
|
|
293
|
+
>
|
|
294
|
+
+ Add
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
${restartRequired
|
|
300
|
+
? html`<div
|
|
301
|
+
class="bg-yellow-500/10 border border-yellow-500/30 rounded-xl p-4 flex items-center justify-between gap-3"
|
|
302
|
+
>
|
|
303
|
+
<p class="text-sm text-yellow-200">
|
|
304
|
+
Gateway restart required to apply env changes.
|
|
305
|
+
</p>
|
|
306
|
+
<button
|
|
307
|
+
onclick=${handleRestartGateway}
|
|
308
|
+
disabled=${restartingGateway}
|
|
309
|
+
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
|
|
310
|
+
? "opacity-60 cursor-not-allowed"
|
|
311
|
+
: ""}"
|
|
312
|
+
>
|
|
313
|
+
${restartingGateway ? "Restarting..." : "Restart Gateway"}
|
|
314
|
+
</button>
|
|
315
|
+
</div>`
|
|
316
|
+
: null}
|
|
317
|
+
|
|
318
|
+
<button
|
|
319
|
+
onclick=${handleSave}
|
|
320
|
+
disabled=${!dirty || saving || restartingGateway}
|
|
321
|
+
class="w-full text-sm font-medium px-4 py-2.5 rounded-xl transition-all ${dirty &&
|
|
322
|
+
!saving &&
|
|
323
|
+
!restartingGateway
|
|
324
|
+
? "bg-white text-black hover:opacity-85"
|
|
325
|
+
: "bg-gray-800 text-gray-500 cursor-not-allowed"}"
|
|
326
|
+
>
|
|
327
|
+
${saving
|
|
328
|
+
? html`<span class="flex items-center justify-center gap-2">
|
|
329
|
+
<svg
|
|
330
|
+
class="animate-spin h-4 w-4"
|
|
331
|
+
viewBox="0 0 24 24"
|
|
332
|
+
fill="none"
|
|
333
|
+
>
|
|
334
|
+
<circle
|
|
335
|
+
class="opacity-25"
|
|
336
|
+
cx="12"
|
|
337
|
+
cy="12"
|
|
338
|
+
r="10"
|
|
339
|
+
stroke="currentColor"
|
|
340
|
+
stroke-width="4"
|
|
341
|
+
/>
|
|
342
|
+
<path
|
|
343
|
+
class="opacity-75"
|
|
344
|
+
fill="currentColor"
|
|
345
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
346
|
+
/>
|
|
347
|
+
</svg>
|
|
348
|
+
Saving...
|
|
349
|
+
</span>`
|
|
350
|
+
: "Save Changes"}
|
|
351
|
+
</button>
|
|
352
|
+
</div>
|
|
353
|
+
`;
|
|
354
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import {
|
|
5
|
+
fetchOpenclawVersion,
|
|
6
|
+
restartGateway,
|
|
7
|
+
updateOpenclaw,
|
|
8
|
+
} from "../lib/api.js";
|
|
9
|
+
import { showToast } from "./toast.js";
|
|
10
|
+
const html = htm.bind(h);
|
|
11
|
+
|
|
12
|
+
export function Gateway({ status, openclawVersion }) {
|
|
13
|
+
const [restarting, setRestarting] = useState(false);
|
|
14
|
+
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
|
15
|
+
const [currentVersion, setCurrentVersion] = useState(openclawVersion || null);
|
|
16
|
+
const [latestVersion, setLatestVersion] = useState(null);
|
|
17
|
+
const [hasUpdate, setHasUpdate] = useState(false);
|
|
18
|
+
const [updateError, setUpdateError] = useState("");
|
|
19
|
+
const isRunning = status === "running" && !restarting;
|
|
20
|
+
const dotClass = isRunning
|
|
21
|
+
? "w-2 h-2 rounded-full bg-green-500"
|
|
22
|
+
: "w-2 h-2 rounded-full bg-yellow-500 animate-pulse";
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setCurrentVersion(openclawVersion || null);
|
|
26
|
+
}, [openclawVersion]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let active = true;
|
|
30
|
+
const loadLatest = async () => {
|
|
31
|
+
try {
|
|
32
|
+
const data = await fetchOpenclawVersion(false);
|
|
33
|
+
if (!active) return;
|
|
34
|
+
setCurrentVersion(data.currentVersion || openclawVersion || null);
|
|
35
|
+
setLatestVersion(data.latestVersion || null);
|
|
36
|
+
setHasUpdate(!!data.hasUpdate);
|
|
37
|
+
setUpdateError(data.ok ? "" : data.error || "");
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (!active) return;
|
|
40
|
+
setUpdateError(err.message || "Could not check updates");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
loadLatest();
|
|
44
|
+
return () => {
|
|
45
|
+
active = false;
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const handleRestart = async () => {
|
|
50
|
+
if (restarting) return;
|
|
51
|
+
setRestarting(true);
|
|
52
|
+
try {
|
|
53
|
+
await restartGateway();
|
|
54
|
+
showToast("Gateway restarted", "success");
|
|
55
|
+
} catch (err) {
|
|
56
|
+
showToast("Restart failed: " + err.message, "error");
|
|
57
|
+
}
|
|
58
|
+
setRestarting(false);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleUpdate = async () => {
|
|
62
|
+
if (checkingUpdate) return;
|
|
63
|
+
setCheckingUpdate(true);
|
|
64
|
+
setUpdateError("");
|
|
65
|
+
try {
|
|
66
|
+
const data = hasUpdate
|
|
67
|
+
? await updateOpenclaw()
|
|
68
|
+
: await fetchOpenclawVersion(true);
|
|
69
|
+
setCurrentVersion(data.currentVersion || currentVersion);
|
|
70
|
+
setLatestVersion(data.latestVersion || null);
|
|
71
|
+
setHasUpdate(!!data.hasUpdate);
|
|
72
|
+
setUpdateError(data.ok ? "" : data.error || "");
|
|
73
|
+
if (hasUpdate) {
|
|
74
|
+
if (!data.ok) {
|
|
75
|
+
showToast(data.error || "OpenClaw update failed", "error");
|
|
76
|
+
} else if (data.updated) {
|
|
77
|
+
showToast(
|
|
78
|
+
data.restarted
|
|
79
|
+
? `Updated to ${data.currentVersion} and restarted gateway`
|
|
80
|
+
: `Updated to ${data.currentVersion}`,
|
|
81
|
+
"success",
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
showToast("Already at latest OpenClaw version", "success");
|
|
85
|
+
}
|
|
86
|
+
} else if (data.hasUpdate && data.latestVersion) {
|
|
87
|
+
showToast(`Update available: ${data.latestVersion}`, "warning");
|
|
88
|
+
} else {
|
|
89
|
+
showToast("OpenClaw is up to date", "success");
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
setUpdateError(
|
|
93
|
+
err.message ||
|
|
94
|
+
(hasUpdate ? "Could not update OpenClaw" : "Could not check updates"),
|
|
95
|
+
);
|
|
96
|
+
showToast(
|
|
97
|
+
hasUpdate ? "Could not update OpenClaw" : "Could not check updates",
|
|
98
|
+
"error",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
setCheckingUpdate(false);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return html` <div class="bg-surface border border-border rounded-xl p-4">
|
|
105
|
+
<div class="flex items-start justify-between gap-3">
|
|
106
|
+
<div class="min-w-0">
|
|
107
|
+
<div class="flex items-center gap-2">
|
|
108
|
+
<span class=${dotClass}></span>
|
|
109
|
+
<span class="font-semibold">Gateway:</span>
|
|
110
|
+
<span class="text-gray-400"
|
|
111
|
+
>${restarting ? "restarting..." : status || "checking..."}</span
|
|
112
|
+
>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<button
|
|
116
|
+
onclick=${handleRestart}
|
|
117
|
+
disabled=${restarting || !status}
|
|
118
|
+
class="text-xs px-2.5 py-1 rounded-lg border border-border text-gray-500 hover:text-gray-300 hover:border-gray-500 transition-colors ${restarting ||
|
|
119
|
+
!status
|
|
120
|
+
? "opacity-50 cursor-not-allowed"
|
|
121
|
+
: ""}"
|
|
122
|
+
>
|
|
123
|
+
Restart
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="mt-3 pt-3 border-t border-border">
|
|
127
|
+
<div class="flex items-center justify-between gap-3">
|
|
128
|
+
<div class="min-w-0">
|
|
129
|
+
<p class="text-sm text-gray-300 truncate">
|
|
130
|
+
v${currentVersion || openclawVersion || "unknown"}
|
|
131
|
+
</p>
|
|
132
|
+
${updateError &&
|
|
133
|
+
html`<p class="text-xs text-yellow-500 mt-1">${updateError}</p>`}
|
|
134
|
+
</div>
|
|
135
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
136
|
+
${hasUpdate &&
|
|
137
|
+
latestVersion &&
|
|
138
|
+
html`<a
|
|
139
|
+
href="https://github.com/openclaw/openclaw/tags"
|
|
140
|
+
target="_blank"
|
|
141
|
+
class="text-xs text-yellow-500 hover:text-yellow-300 transition-colors"
|
|
142
|
+
>${latestVersion} available</a
|
|
143
|
+
>`}
|
|
144
|
+
<button
|
|
145
|
+
onclick=${handleUpdate}
|
|
146
|
+
disabled=${checkingUpdate}
|
|
147
|
+
class="text-xs px-2.5 py-1 rounded-lg border border-border text-gray-500 hover:text-gray-300 hover:border-gray-500 transition-colors ${checkingUpdate
|
|
148
|
+
? "opacity-50 cursor-not-allowed"
|
|
149
|
+
: ""}"
|
|
150
|
+
>
|
|
151
|
+
${checkingUpdate
|
|
152
|
+
? hasUpdate
|
|
153
|
+
? "Updating..."
|
|
154
|
+
: "Checking..."
|
|
155
|
+
: hasUpdate
|
|
156
|
+
? "Update"
|
|
157
|
+
: "Check updates"}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>`;
|
|
163
|
+
}
|