@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,36 @@
|
|
|
1
|
+
import { h } from 'https://esm.sh/preact';
|
|
2
|
+
import htm from 'https://esm.sh/htm';
|
|
3
|
+
import { Badge } from './badge.js';
|
|
4
|
+
const html = htm.bind(h);
|
|
5
|
+
|
|
6
|
+
const ALL_CHANNELS = ['telegram', 'discord'];
|
|
7
|
+
|
|
8
|
+
export function Channels({ channels, onSwitchTab }) {
|
|
9
|
+
return html`
|
|
10
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
11
|
+
<h2 class="font-semibold mb-3">Channels</h2>
|
|
12
|
+
<div class="space-y-2">
|
|
13
|
+
${channels ? ALL_CHANNELS.map(ch => {
|
|
14
|
+
const info = channels[ch];
|
|
15
|
+
let badge;
|
|
16
|
+
if (!info) {
|
|
17
|
+
badge = html`<a
|
|
18
|
+
href="#"
|
|
19
|
+
onclick=${(e) => { e.preventDefault(); onSwitchTab?.('envars'); }}
|
|
20
|
+
class="text-xs text-gray-500 hover:text-gray-300"
|
|
21
|
+
>Add token</a>`;
|
|
22
|
+
} else if (info.status === 'paired') {
|
|
23
|
+
badge = html`<${Badge} tone="success">Paired (${info.paired})</${Badge}>`;
|
|
24
|
+
} else {
|
|
25
|
+
badge = html`<${Badge} tone="warning">Awaiting pairing</${Badge}>`;
|
|
26
|
+
}
|
|
27
|
+
return html`<div class="flex justify-between items-center py-1.5">
|
|
28
|
+
<span class="font-medium text-sm">${ch.charAt(0).toUpperCase() + ch.slice(1)}</span>
|
|
29
|
+
${badge}
|
|
30
|
+
</div>`;
|
|
31
|
+
}) : html`<div class="text-gray-500 text-sm text-center py-2">Loading...</div>`}
|
|
32
|
+
</div>
|
|
33
|
+
</div>`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { ALL_CHANNELS };
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useState, useRef } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { saveGoogleCredentials } from "../lib/api.js";
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
8
|
+
const [clientId, setClientId] = useState("");
|
|
9
|
+
const [clientSecret, setClientSecret] = useState("");
|
|
10
|
+
const [email, setEmail] = useState("");
|
|
11
|
+
const [error, setError] = useState("");
|
|
12
|
+
const [saving, setSaving] = useState(false);
|
|
13
|
+
const [instrType, setInstrType] = useState("personal");
|
|
14
|
+
const [redirectUriCopied, setRedirectUriCopied] = useState(false);
|
|
15
|
+
const fileRef = useRef(null);
|
|
16
|
+
|
|
17
|
+
if (!visible) return null;
|
|
18
|
+
|
|
19
|
+
const redirectUri = `${window.location.origin}/auth/google/callback`;
|
|
20
|
+
|
|
21
|
+
const copyRedirectUri = async () => {
|
|
22
|
+
try {
|
|
23
|
+
await navigator.clipboard.writeText(redirectUri);
|
|
24
|
+
setRedirectUriCopied(true);
|
|
25
|
+
window.setTimeout(() => setRedirectUriCopied(false), 1500);
|
|
26
|
+
} catch {
|
|
27
|
+
setError("Unable to copy redirect URI");
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleFile = async (e) => {
|
|
32
|
+
const file = e.target.files[0];
|
|
33
|
+
if (!file) return;
|
|
34
|
+
try {
|
|
35
|
+
const text = await file.text();
|
|
36
|
+
const json = JSON.parse(text);
|
|
37
|
+
const creds = json.installed || json.web || json;
|
|
38
|
+
if (creds.client_id) setClientId(creds.client_id);
|
|
39
|
+
if (creds.client_secret) setClientSecret(creds.client_secret);
|
|
40
|
+
} catch {
|
|
41
|
+
setError("Invalid JSON file");
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const submit = async () => {
|
|
46
|
+
setError("");
|
|
47
|
+
if (!clientId || !clientSecret || !email) {
|
|
48
|
+
setError("All fields required");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
setSaving(true);
|
|
52
|
+
try {
|
|
53
|
+
const data = await saveGoogleCredentials(clientId, clientSecret, email);
|
|
54
|
+
if (data.ok) {
|
|
55
|
+
onClose();
|
|
56
|
+
onSaved();
|
|
57
|
+
} else setError(data.error || "Failed to save credentials");
|
|
58
|
+
} catch {
|
|
59
|
+
setError("Request failed");
|
|
60
|
+
} finally {
|
|
61
|
+
setSaving(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const btnCls = (type) =>
|
|
66
|
+
`px-2 py-1 rounded text-xs ${instrType === type ? "bg-gray-700 text-gray-200" : "bg-gray-800 text-gray-400 hover:text-gray-200"}`;
|
|
67
|
+
|
|
68
|
+
const renderRedirectUriInstruction = () => html`
|
|
69
|
+
<div class="mt-1 flex items-center gap-2">
|
|
70
|
+
<input
|
|
71
|
+
type="text"
|
|
72
|
+
readonly
|
|
73
|
+
value=${redirectUri}
|
|
74
|
+
onFocus=${(e) => e.target.select()}
|
|
75
|
+
onclick=${(e) => e.target.select()}
|
|
76
|
+
class="flex-1 min-w-0 bg-black/40 border border-border rounded px-2 py-1 text-gray-300 text-xs focus:outline-none focus:border-gray-500"
|
|
77
|
+
/>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onclick=${copyRedirectUri}
|
|
81
|
+
class="shrink-0 px-2 py-1 rounded border border-border text-xs text-gray-300 hover:border-gray-500"
|
|
82
|
+
>
|
|
83
|
+
${redirectUriCopied ? "Copied" : "Copy"}
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
return html` <div
|
|
89
|
+
class="fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-50"
|
|
90
|
+
onclick=${(e) => {
|
|
91
|
+
if (e.target === e.currentTarget) onClose();
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<div
|
|
95
|
+
class="bg-surface border border-border rounded-xl p-6 max-w-md w-full space-y-4"
|
|
96
|
+
>
|
|
97
|
+
<h2 class="text-lg font-semibold">Connect Google Workspace</h2>
|
|
98
|
+
<div class="space-y-3">
|
|
99
|
+
<div>
|
|
100
|
+
<p class="text-gray-400 text-sm mb-3">
|
|
101
|
+
You'll need a Google Cloud OAuth app.${" "}
|
|
102
|
+
<a
|
|
103
|
+
href="https://console.cloud.google.com/apis/credentials"
|
|
104
|
+
target="_blank"
|
|
105
|
+
class="text-blue-400 hover:text-blue-300"
|
|
106
|
+
>Create one here →</a
|
|
107
|
+
>
|
|
108
|
+
</p>
|
|
109
|
+
<details
|
|
110
|
+
class="text-sm text-gray-400 mb-3 bg-black/20 border border-border rounded-lg px-3 py-2"
|
|
111
|
+
>
|
|
112
|
+
<summary class="cursor-pointer font-medium hover:text-gray-200">
|
|
113
|
+
Step-by-step instructions
|
|
114
|
+
</summary>
|
|
115
|
+
<div class="mt-2 mb-2 flex gap-2">
|
|
116
|
+
<button
|
|
117
|
+
onclick=${() => setInstrType("workspace")}
|
|
118
|
+
class=${btnCls("workspace")}
|
|
119
|
+
>
|
|
120
|
+
Google Workspace
|
|
121
|
+
</button>
|
|
122
|
+
<button
|
|
123
|
+
onclick=${() => setInstrType("personal")}
|
|
124
|
+
class=${btnCls("personal")}
|
|
125
|
+
>
|
|
126
|
+
Personal Gmail
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
${instrType === "personal"
|
|
130
|
+
? html`
|
|
131
|
+
<div>
|
|
132
|
+
<ol class="list-decimal list-inside space-y-1.5 ml-1">
|
|
133
|
+
<li>
|
|
134
|
+
${" "}
|
|
135
|
+
<a
|
|
136
|
+
href="https://console.cloud.google.com/projectcreate"
|
|
137
|
+
target="_blank"
|
|
138
|
+
class="text-blue-400 hover:text-blue-300"
|
|
139
|
+
>Create a Google Cloud project</a
|
|
140
|
+
>
|
|
141
|
+
(or use existing)
|
|
142
|
+
</li>
|
|
143
|
+
<li>
|
|
144
|
+
Go to${" "}
|
|
145
|
+
<a
|
|
146
|
+
href="https://console.cloud.google.com/auth/audience"
|
|
147
|
+
target="_blank"
|
|
148
|
+
class="text-blue-400 hover:text-blue-300"
|
|
149
|
+
>OAuth consent screen</a
|
|
150
|
+
>
|
|
151
|
+
→ set to <strong>External</strong>
|
|
152
|
+
</li>
|
|
153
|
+
<li>
|
|
154
|
+
Under${" "}
|
|
155
|
+
<a
|
|
156
|
+
href="https://console.cloud.google.com/auth/audience"
|
|
157
|
+
target="_blank"
|
|
158
|
+
class="text-blue-400 hover:text-blue-300"
|
|
159
|
+
>Test users</a
|
|
160
|
+
>, <strong>add your own email</strong>
|
|
161
|
+
</li>
|
|
162
|
+
<li>
|
|
163
|
+
${" "}
|
|
164
|
+
<a
|
|
165
|
+
href="https://console.cloud.google.com/apis/library"
|
|
166
|
+
target="_blank"
|
|
167
|
+
class="text-blue-400 hover:text-blue-300"
|
|
168
|
+
>Enable APIs</a
|
|
169
|
+
>
|
|
170
|
+
for the services you selected below
|
|
171
|
+
</li>
|
|
172
|
+
<li>
|
|
173
|
+
Go to${" "}
|
|
174
|
+
<a
|
|
175
|
+
href="https://console.cloud.google.com/apis/credentials"
|
|
176
|
+
target="_blank"
|
|
177
|
+
class="text-blue-400 hover:text-blue-300"
|
|
178
|
+
>Credentials</a
|
|
179
|
+
>
|
|
180
|
+
→ Create OAuth 2.0 Client ID (Web application)
|
|
181
|
+
</li>
|
|
182
|
+
<li>
|
|
183
|
+
Add redirect URI:
|
|
184
|
+
${renderRedirectUriInstruction()}
|
|
185
|
+
</li>
|
|
186
|
+
<li>
|
|
187
|
+
Copy Client ID + Secret (or download credentials JSON)
|
|
188
|
+
</li>
|
|
189
|
+
</ol>
|
|
190
|
+
<p class="mt-2 text-yellow-500/80">
|
|
191
|
+
⚠️ App will be in "Testing" mode. Only emails added as
|
|
192
|
+
Test Users can sign in (up to 100).
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
`
|
|
196
|
+
: html`
|
|
197
|
+
<div>
|
|
198
|
+
<ol class="list-decimal list-inside space-y-1.5 ml-1">
|
|
199
|
+
<li>
|
|
200
|
+
${" "}
|
|
201
|
+
<a
|
|
202
|
+
href="https://console.cloud.google.com/projectcreate"
|
|
203
|
+
target="_blank"
|
|
204
|
+
class="text-blue-400 hover:text-blue-300"
|
|
205
|
+
>Create a Google Cloud project</a
|
|
206
|
+
>
|
|
207
|
+
(or use existing)
|
|
208
|
+
</li>
|
|
209
|
+
<li>
|
|
210
|
+
Go to${" "}
|
|
211
|
+
<a
|
|
212
|
+
href="https://console.cloud.google.com/auth/audience"
|
|
213
|
+
target="_blank"
|
|
214
|
+
class="text-blue-400 hover:text-blue-300"
|
|
215
|
+
>OAuth consent screen</a
|
|
216
|
+
>
|
|
217
|
+
→ set to <strong>Internal</strong> (Workspace only)
|
|
218
|
+
</li>
|
|
219
|
+
<li>
|
|
220
|
+
${" "}
|
|
221
|
+
<a
|
|
222
|
+
href="https://console.cloud.google.com/apis/library"
|
|
223
|
+
target="_blank"
|
|
224
|
+
class="text-blue-400 hover:text-blue-300"
|
|
225
|
+
>Enable APIs</a
|
|
226
|
+
>
|
|
227
|
+
for the services you selected below
|
|
228
|
+
</li>
|
|
229
|
+
<li>
|
|
230
|
+
Go to${" "}
|
|
231
|
+
<a
|
|
232
|
+
href="https://console.cloud.google.com/apis/credentials"
|
|
233
|
+
target="_blank"
|
|
234
|
+
class="text-blue-400 hover:text-blue-300"
|
|
235
|
+
>Credentials</a
|
|
236
|
+
>
|
|
237
|
+
→ Create OAuth 2.0 Client ID (Web application)
|
|
238
|
+
</li>
|
|
239
|
+
<li>
|
|
240
|
+
Add redirect URI:
|
|
241
|
+
${renderRedirectUriInstruction()}
|
|
242
|
+
</li>
|
|
243
|
+
<li>
|
|
244
|
+
Copy Client ID + Secret (or download credentials JSON)
|
|
245
|
+
</li>
|
|
246
|
+
</ol>
|
|
247
|
+
<p class="mt-2 text-green-500/80">
|
|
248
|
+
✓ Internal apps skip test users and verification. Only
|
|
249
|
+
users in your Workspace org can authorize this Google app.
|
|
250
|
+
</p>
|
|
251
|
+
</div>
|
|
252
|
+
`}
|
|
253
|
+
</details>
|
|
254
|
+
</div>
|
|
255
|
+
<div
|
|
256
|
+
class="bg-black/20 border border-border rounded-lg p-3 space-y-3 mt-2"
|
|
257
|
+
>
|
|
258
|
+
<div class="flex flex-col items-center text-center gap-2 py-2">
|
|
259
|
+
<label class="text-sm text-gray-300 font-medium"
|
|
260
|
+
>Upload credentials.json</label
|
|
261
|
+
>
|
|
262
|
+
<input
|
|
263
|
+
type="file"
|
|
264
|
+
ref=${fileRef}
|
|
265
|
+
accept=".json"
|
|
266
|
+
onchange=${handleFile}
|
|
267
|
+
class="hidden"
|
|
268
|
+
/>
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
onclick=${() => fileRef.current?.click()}
|
|
272
|
+
class="text-sm px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
|
|
273
|
+
>
|
|
274
|
+
Choose file
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="flex items-center gap-3 py-1">
|
|
278
|
+
<div class="h-px flex-1 bg-border"></div>
|
|
279
|
+
<span class="text-gray-500 text-xs">or enter manually</span>
|
|
280
|
+
<div class="h-px flex-1 bg-border"></div>
|
|
281
|
+
</div>
|
|
282
|
+
<div>
|
|
283
|
+
<label class="text-sm text-gray-400 block mb-1">Client ID</label>
|
|
284
|
+
<input
|
|
285
|
+
type="text"
|
|
286
|
+
value=${clientId}
|
|
287
|
+
onInput=${(e) => setClientId(e.target.value)}
|
|
288
|
+
placeholder="xxxx.apps.googleusercontent.com"
|
|
289
|
+
class="w-full bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
<div>
|
|
293
|
+
<label class="text-sm text-gray-400 block mb-1"
|
|
294
|
+
>Client Secret</label
|
|
295
|
+
>
|
|
296
|
+
<input
|
|
297
|
+
type="password"
|
|
298
|
+
value=${clientSecret}
|
|
299
|
+
onInput=${(e) => setClientSecret(e.target.value)}
|
|
300
|
+
placeholder="GOCSPX-..."
|
|
301
|
+
class="w-full bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
<div>
|
|
305
|
+
<label class="text-sm text-gray-400 block mb-1"
|
|
306
|
+
>Email (Google account to authorize)</label
|
|
307
|
+
>
|
|
308
|
+
<input
|
|
309
|
+
type="email"
|
|
310
|
+
value=${email}
|
|
311
|
+
onInput=${(e) => setEmail(e.target.value)}
|
|
312
|
+
placeholder="you@gmail.com"
|
|
313
|
+
class="w-full bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="flex gap-2 pt-2">
|
|
319
|
+
<button
|
|
320
|
+
onclick=${submit}
|
|
321
|
+
disabled=${saving}
|
|
322
|
+
class="flex-1 bg-green-500 text-black font-medium py-2 rounded-lg hover:opacity-85 transition-opacity text-sm"
|
|
323
|
+
>
|
|
324
|
+
${saving ? "Saving..." : "Connect Google"}
|
|
325
|
+
</button>
|
|
326
|
+
<button
|
|
327
|
+
onclick=${onClose}
|
|
328
|
+
class="px-4 bg-gray-800 text-gray-300 py-2 rounded-lg hover:bg-gray-700 transition-colors text-sm"
|
|
329
|
+
>
|
|
330
|
+
Cancel
|
|
331
|
+
</button>
|
|
332
|
+
</div>
|
|
333
|
+
${error ? html`<div class="text-red-400 text-xs">${error}</div>` : null}
|
|
334
|
+
</div>
|
|
335
|
+
</div>`;
|
|
336
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { h } from 'https://esm.sh/preact';
|
|
2
|
+
import { useState } from 'https://esm.sh/preact/hooks';
|
|
3
|
+
import htm from 'https://esm.sh/htm';
|
|
4
|
+
const html = htm.bind(h);
|
|
5
|
+
|
|
6
|
+
const kModeLabels = {
|
|
7
|
+
webchat: 'Browser',
|
|
8
|
+
cli: 'CLI',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const formatTitle = (d) => kModeLabels[d.clientMode] || d.clientId || 'Device';
|
|
12
|
+
|
|
13
|
+
const formatSubtitle = (d) => {
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (d.platform) parts.push(d.platform);
|
|
16
|
+
if (d.role) parts.push(d.role);
|
|
17
|
+
return parts.join(' · ');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DeviceRow = ({ d, onApprove, onReject }) => {
|
|
21
|
+
const [busy, setBusy] = useState(null);
|
|
22
|
+
|
|
23
|
+
const handle = async (action) => {
|
|
24
|
+
setBusy(action);
|
|
25
|
+
try {
|
|
26
|
+
if (action === 'approve') await onApprove(d.id);
|
|
27
|
+
else await onReject(d.id);
|
|
28
|
+
} catch {
|
|
29
|
+
setBusy(null);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const title = formatTitle(d);
|
|
34
|
+
const subtitle = formatSubtitle(d);
|
|
35
|
+
|
|
36
|
+
if (busy === 'approve') {
|
|
37
|
+
return html`
|
|
38
|
+
<div class="bg-black/30 rounded-lg p-3 mb-2 flex items-center gap-2">
|
|
39
|
+
<span class="text-green-400 text-sm">Approved</span>
|
|
40
|
+
<span class="text-gray-500 text-xs">${title}</span>
|
|
41
|
+
</div>`;
|
|
42
|
+
}
|
|
43
|
+
if (busy === 'reject') {
|
|
44
|
+
return html`
|
|
45
|
+
<div class="bg-black/30 rounded-lg p-3 mb-2 flex items-center gap-2">
|
|
46
|
+
<span class="text-gray-400 text-sm">Rejected</span>
|
|
47
|
+
<span class="text-gray-500 text-xs">${title}</span>
|
|
48
|
+
</div>`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return html`
|
|
52
|
+
<div class="bg-black/30 rounded-lg p-3 mb-2">
|
|
53
|
+
<div class="flex items-center gap-2 mb-2">
|
|
54
|
+
<span class="font-medium text-sm">${title}</span>
|
|
55
|
+
${subtitle && html`<span class="text-xs text-gray-500">${subtitle}</span>`}
|
|
56
|
+
</div>
|
|
57
|
+
<div class="flex gap-2">
|
|
58
|
+
<button onclick=${() => handle('approve')} class="bg-green-500 text-black text-xs font-medium px-3 py-1.5 rounded-lg hover:opacity-85">Approve</button>
|
|
59
|
+
<button onclick=${() => handle('reject')} class="bg-gray-800 text-gray-300 text-xs px-3 py-1.5 rounded-lg hover:bg-gray-700">Reject</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>`;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const DevicePairings = ({ pending, onApprove, onReject }) => {
|
|
65
|
+
if (!pending || pending.length === 0) return null;
|
|
66
|
+
|
|
67
|
+
return html`
|
|
68
|
+
<div class="mt-3 pt-3 border-t border-border">
|
|
69
|
+
<p class="text-xs text-gray-500 mb-2">Pending device pairings</p>
|
|
70
|
+
${pending.map((d) => html`<${DeviceRow} key=${d.id} d=${d} onApprove=${onApprove} onReject=${onReject} />`)}
|
|
71
|
+
</div>`;
|
|
72
|
+
};
|