leedab 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.
Files changed (71) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +85 -0
  3. package/bin/leedab.js +626 -0
  4. package/dist/analytics.d.ts +20 -0
  5. package/dist/analytics.js +57 -0
  6. package/dist/audit.d.ts +15 -0
  7. package/dist/audit.js +46 -0
  8. package/dist/brand.d.ts +9 -0
  9. package/dist/brand.js +57 -0
  10. package/dist/channels/index.d.ts +5 -0
  11. package/dist/channels/index.js +47 -0
  12. package/dist/config/index.d.ts +10 -0
  13. package/dist/config/index.js +49 -0
  14. package/dist/config/schema.d.ts +58 -0
  15. package/dist/config/schema.js +21 -0
  16. package/dist/dashboard/routes.d.ts +5 -0
  17. package/dist/dashboard/routes.js +410 -0
  18. package/dist/dashboard/server.d.ts +2 -0
  19. package/dist/dashboard/server.js +80 -0
  20. package/dist/dashboard/static/app.js +351 -0
  21. package/dist/dashboard/static/console.html +252 -0
  22. package/dist/dashboard/static/favicon.png +0 -0
  23. package/dist/dashboard/static/index.html +815 -0
  24. package/dist/dashboard/static/logo-dark.png +0 -0
  25. package/dist/dashboard/static/logo-light.png +0 -0
  26. package/dist/dashboard/static/sessions.html +182 -0
  27. package/dist/dashboard/static/settings.html +274 -0
  28. package/dist/dashboard/static/style.css +493 -0
  29. package/dist/dashboard/static/team.html +215 -0
  30. package/dist/gateway.d.ts +8 -0
  31. package/dist/gateway.js +213 -0
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.js +5 -0
  34. package/dist/license.d.ts +27 -0
  35. package/dist/license.js +92 -0
  36. package/dist/memory/index.d.ts +9 -0
  37. package/dist/memory/index.js +41 -0
  38. package/dist/onboard/index.d.ts +4 -0
  39. package/dist/onboard/index.js +263 -0
  40. package/dist/onboard/oauth-server.d.ts +13 -0
  41. package/dist/onboard/oauth-server.js +73 -0
  42. package/dist/onboard/steps/google.d.ts +12 -0
  43. package/dist/onboard/steps/google.js +178 -0
  44. package/dist/onboard/steps/provider.d.ts +10 -0
  45. package/dist/onboard/steps/provider.js +292 -0
  46. package/dist/onboard/steps/teams.d.ts +5 -0
  47. package/dist/onboard/steps/teams.js +51 -0
  48. package/dist/onboard/steps/telegram.d.ts +6 -0
  49. package/dist/onboard/steps/telegram.js +88 -0
  50. package/dist/onboard/steps/welcome.d.ts +1 -0
  51. package/dist/onboard/steps/welcome.js +10 -0
  52. package/dist/onboard/steps/whatsapp.d.ts +2 -0
  53. package/dist/onboard/steps/whatsapp.js +76 -0
  54. package/dist/openclaw.d.ts +9 -0
  55. package/dist/openclaw.js +20 -0
  56. package/dist/team.d.ts +13 -0
  57. package/dist/team.js +49 -0
  58. package/dist/templates/verticals/supply-chain/HEARTBEAT.md +12 -0
  59. package/dist/templates/verticals/supply-chain/SOUL.md +49 -0
  60. package/dist/templates/verticals/supply-chain/WORKFLOWS.md +148 -0
  61. package/dist/templates/verticals/supply-chain/vault-template.json +18 -0
  62. package/dist/templates/workspace/AGENTS.md +181 -0
  63. package/dist/templates/workspace/BOOTSTRAP.md +32 -0
  64. package/dist/templates/workspace/HEARTBEAT.md +9 -0
  65. package/dist/templates/workspace/IDENTITY.md +14 -0
  66. package/dist/templates/workspace/SOUL.md +32 -0
  67. package/dist/templates/workspace/TOOLS.md +40 -0
  68. package/dist/templates/workspace/USER.md +26 -0
  69. package/dist/vault.d.ts +24 -0
  70. package/dist/vault.js +123 -0
  71. package/package.json +58 -0
@@ -0,0 +1,351 @@
1
+ // Fetch status, vault, and allowlist on load
2
+ document.addEventListener("DOMContentLoaded", () => {
3
+ refreshStatus();
4
+ loadVault();
5
+ loadAllowlist();
6
+ });
7
+
8
+ async function refreshStatus() {
9
+ try {
10
+ const res = await fetch("/api/status");
11
+ const status = await res.json();
12
+
13
+ for (const [key, info] of Object.entries(status)) {
14
+ const card = document.getElementById(`card-${key}`);
15
+ const statusEl = document.getElementById(`status-${key}`);
16
+ const btn = card?.querySelector(".btn");
17
+
18
+ if (info.connected) {
19
+ card?.classList.add("connected");
20
+ if (statusEl) {
21
+ statusEl.innerHTML = '<span class="dot"></span> Connected';
22
+ }
23
+ if (btn) {
24
+ btn.textContent = "Connected";
25
+ btn.className = "btn btn-connected";
26
+ btn.disabled = true;
27
+ btn.onclick = null;
28
+ }
29
+ }
30
+ }
31
+ } catch (err) {
32
+ console.error("Failed to fetch status:", err);
33
+ }
34
+ }
35
+
36
+ // --- WhatsApp ---
37
+
38
+ function connectWhatsApp() {
39
+ const panel = document.getElementById("whatsapp-panel");
40
+ panel.classList.remove("hidden");
41
+ const qr = document.getElementById("whatsapp-qr");
42
+ qr.innerHTML = `<p>Generating QR code...</p>`;
43
+
44
+ fetch("/api/whatsapp/connect", { method: "POST" })
45
+ .then((r) => r.json())
46
+ .then((data) => {
47
+ if (data.error) {
48
+ qr.innerHTML = `<p style="color:#ef4444">${escapeHtml(data.error)}</p>`;
49
+ return;
50
+ }
51
+ if (data.qr) {
52
+ const lines = data.qr.split("\n").filter(
53
+ (l) => l.includes("\u2588") || l.includes("\u2584") || l.includes("\u2580")
54
+ );
55
+ const qrText = lines.join("\n");
56
+ qr.innerHTML = `<pre style="
57
+ font-family: monospace;
58
+ font-size: 4px;
59
+ line-height: 4px;
60
+ letter-spacing: 0px;
61
+ background: white;
62
+ color: black;
63
+ padding: 16px;
64
+ display: inline-block;
65
+ border-radius: 8px;
66
+ transform: scale(2);
67
+ transform-origin: center;
68
+ margin: 40px 0;
69
+ ">${escapeHtml(qrText)}</pre>`;
70
+ pollWhatsAppStatus();
71
+ } else {
72
+ qr.innerHTML = `<p>Waiting for QR code... try again in a moment.</p>`;
73
+ }
74
+ })
75
+ .catch(() => {
76
+ qr.innerHTML = `<p style="color:#ef4444">Connection failed. Try again.</p>`;
77
+ });
78
+ }
79
+
80
+ function pollWhatsAppStatus() {
81
+ const interval = setInterval(async () => {
82
+ const res = await fetch("/api/status");
83
+ const status = await res.json();
84
+ if (status.whatsapp.connected) {
85
+ clearInterval(interval);
86
+ hideWhatsAppPanel();
87
+ showToast("WhatsApp connected!", "success");
88
+ refreshStatus();
89
+ }
90
+ }, 3000);
91
+
92
+ setTimeout(() => clearInterval(interval), 120000);
93
+ }
94
+
95
+ function hideWhatsAppPanel() {
96
+ document.getElementById("whatsapp-panel").classList.add("hidden");
97
+ }
98
+
99
+ // --- Telegram ---
100
+
101
+ function showTelegramForm() {
102
+ document.getElementById("telegram-form").classList.remove("hidden");
103
+ }
104
+
105
+ function hideTelegramForm() {
106
+ document.getElementById("telegram-form").classList.add("hidden");
107
+ }
108
+
109
+ async function connectTelegram() {
110
+ const token = document.getElementById("telegram-token").value.trim();
111
+ if (!token) {
112
+ showToast("Bot token is required", "error");
113
+ return;
114
+ }
115
+
116
+ try {
117
+ const res = await fetch("/api/telegram/connect", {
118
+ method: "POST",
119
+ headers: { "Content-Type": "application/json" },
120
+ body: JSON.stringify({ token }),
121
+ });
122
+ const data = await res.json();
123
+ if (data.error) {
124
+ showToast(data.error, "error");
125
+ } else {
126
+ hideTelegramForm();
127
+ showToast("Telegram connected!", "success");
128
+ refreshStatus();
129
+ }
130
+ } catch {
131
+ showToast("Connection failed", "error");
132
+ }
133
+ }
134
+
135
+ // --- Teams ---
136
+
137
+ function showTeamsForm() {
138
+ document.getElementById("teams-form").classList.remove("hidden");
139
+ }
140
+
141
+ function hideTeamsForm() {
142
+ document.getElementById("teams-form").classList.add("hidden");
143
+ }
144
+
145
+ async function connectTeams() {
146
+ const appId = document.getElementById("teams-app-id").value.trim();
147
+ const tenantId = document.getElementById("teams-tenant-id").value.trim();
148
+
149
+ if (!appId || !tenantId) {
150
+ showToast("All fields are required", "error");
151
+ return;
152
+ }
153
+
154
+ try {
155
+ const res = await fetch("/api/teams/connect", {
156
+ method: "POST",
157
+ headers: { "Content-Type": "application/json" },
158
+ body: JSON.stringify({ appId, tenantId }),
159
+ });
160
+ const data = await res.json();
161
+ if (data.error) {
162
+ showToast(data.error, "error");
163
+ } else {
164
+ hideTeamsForm();
165
+ showToast("Teams credentials saved!", "success");
166
+ refreshStatus();
167
+ }
168
+ } catch {
169
+ showToast("Connection failed", "error");
170
+ }
171
+ }
172
+
173
+ // --- Vault ---
174
+
175
+ async function loadVault() {
176
+ const container = document.getElementById("vault-list");
177
+ if (!container) return;
178
+
179
+ try {
180
+ const res = await fetch("/api/vault");
181
+ const entries = await res.json();
182
+
183
+ if (!entries.length) {
184
+ container.innerHTML = '<div class="vault-empty">No credentials stored yet.</div>';
185
+ return;
186
+ }
187
+
188
+ container.innerHTML = `
189
+ <table class="vault-table">
190
+ <thead>
191
+ <tr><th>Service</th><th>URL</th><th>Username</th><th></th></tr>
192
+ </thead>
193
+ <tbody>
194
+ ${entries.map(e => `
195
+ <tr>
196
+ <td>${escapeHtml(e.service)}</td>
197
+ <td>${e.url ? escapeHtml(e.url) : '<span style="color:var(--text-faint)">—</span>'}</td>
198
+ <td>${e.username ? escapeHtml(e.username) : '<span style="color:var(--text-faint)">—</span>'}</td>
199
+ <td><button class="btn btn-danger" onclick="removeVaultEntry('${escapeHtml(e.service)}')">Remove</button></td>
200
+ </tr>
201
+ `).join("")}
202
+ </tbody>
203
+ </table>`;
204
+ } catch {
205
+ container.innerHTML = '<div class="vault-empty">Could not load vault.</div>';
206
+ }
207
+ }
208
+
209
+ async function addVaultEntry() {
210
+ const service = document.getElementById("vault-service").value.trim();
211
+ const url = document.getElementById("vault-url").value.trim();
212
+ const username = document.getElementById("vault-username").value.trim();
213
+ const password = document.getElementById("vault-password").value;
214
+ const notes = document.getElementById("vault-notes").value.trim();
215
+
216
+ if (!service) {
217
+ showToast("Service name is required", "error");
218
+ return;
219
+ }
220
+
221
+ try {
222
+ await fetch("/api/vault", {
223
+ method: "POST",
224
+ headers: { "Content-Type": "application/json" },
225
+ body: JSON.stringify({ service, url, username, password, notes }),
226
+ });
227
+
228
+ // Clear form
229
+ document.getElementById("vault-service").value = "";
230
+ document.getElementById("vault-url").value = "";
231
+ document.getElementById("vault-username").value = "";
232
+ document.getElementById("vault-password").value = "";
233
+ document.getElementById("vault-notes").value = "";
234
+
235
+ showToast(`Added ${service} to vault`, "success");
236
+ loadVault();
237
+ } catch {
238
+ showToast("Failed to add credential", "error");
239
+ }
240
+ }
241
+
242
+ async function removeVaultEntry(service) {
243
+ try {
244
+ await fetch(`/api/vault?service=${encodeURIComponent(service)}`, {
245
+ method: "DELETE",
246
+ });
247
+ showToast(`Removed ${service}`, "success");
248
+ loadVault();
249
+ } catch {
250
+ showToast("Failed to remove credential", "error");
251
+ }
252
+ }
253
+
254
+ // --- Allowlist ---
255
+
256
+ async function loadAllowlist() {
257
+ const container = document.getElementById("allowlist-entries");
258
+ const channelSelect = document.getElementById("allowlist-channel");
259
+ if (!container || !channelSelect) return;
260
+
261
+ const channel = channelSelect.value;
262
+
263
+ try {
264
+ const res = await fetch(`/api/allowlist?channel=${encodeURIComponent(channel)}`);
265
+ const data = await res.json();
266
+
267
+ if (!data.allowFrom || data.allowFrom.length === 0) {
268
+ container.innerHTML = `<div class="vault-empty">No users in ${channel} allowlist. Policy: ${data.dmPolicy}</div>`;
269
+ return;
270
+ }
271
+
272
+ container.innerHTML = `
273
+ <table class="vault-table">
274
+ <thead>
275
+ <tr><th>User ID</th><th>Policy</th><th></th></tr>
276
+ </thead>
277
+ <tbody>
278
+ ${data.allowFrom.map(id => `
279
+ <tr>
280
+ <td>${escapeHtml(id)}</td>
281
+ <td style="color:var(--text-dim)">${data.dmPolicy}</td>
282
+ <td><button class="btn btn-danger" onclick="removeAllowlistEntry('${escapeHtml(channel)}', '${escapeHtml(id)}')">Remove</button></td>
283
+ </tr>
284
+ `).join("")}
285
+ </tbody>
286
+ </table>`;
287
+ } catch {
288
+ container.innerHTML = '<div class="vault-empty">Could not load allowlist.</div>';
289
+ }
290
+ }
291
+
292
+ async function addAllowlistEntry() {
293
+ const channel = document.getElementById("allowlist-channel").value;
294
+ const userId = document.getElementById("allowlist-userid").value.trim();
295
+
296
+ if (!userId) {
297
+ showToast("User ID is required", "error");
298
+ return;
299
+ }
300
+
301
+ try {
302
+ const res = await fetch("/api/allowlist", {
303
+ method: "POST",
304
+ headers: { "Content-Type": "application/json" },
305
+ body: JSON.stringify({ channel, userId }),
306
+ });
307
+ const data = await res.json();
308
+ if (data.error) {
309
+ showToast(data.error, "error");
310
+ } else {
311
+ document.getElementById("allowlist-userid").value = "";
312
+ showToast(`Added ${userId} to ${channel}.`, "success");
313
+ loadAllowlist();
314
+ }
315
+ } catch {
316
+ showToast("Failed to add user", "error");
317
+ }
318
+ }
319
+
320
+ async function removeAllowlistEntry(channel, userId) {
321
+ try {
322
+ await fetch(`/api/allowlist?channel=${encodeURIComponent(channel)}&userId=${encodeURIComponent(userId)}`, {
323
+ method: "DELETE",
324
+ });
325
+ showToast(`Removed ${userId}.`, "success");
326
+ loadAllowlist();
327
+ } catch {
328
+ showToast("Failed to remove user", "error");
329
+ }
330
+ }
331
+
332
+ // --- Helpers ---
333
+
334
+ function showToast(message, type = "success") {
335
+ const toast = document.getElementById("toast");
336
+ if (!toast) return;
337
+ toast.textContent = message;
338
+ toast.className = `toast ${type}`;
339
+
340
+ setTimeout(() => {
341
+ toast.classList.add("hidden");
342
+ }, 4000);
343
+ }
344
+
345
+ function escapeHtml(str) {
346
+ return str
347
+ .replace(/&/g, "&amp;")
348
+ .replace(/</g, "&lt;")
349
+ .replace(/>/g, "&gt;")
350
+ .replace(/"/g, "&quot;");
351
+ }
@@ -0,0 +1,252 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="icon" type="image/png" href="/favicon.png">
7
+ <title>LeedAB — Console</title>
8
+ <link rel="stylesheet" href="/style.css">
9
+ </head>
10
+ <body>
11
+ <style>
12
+ .console-header {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: space-between;
16
+ padding: 0 20px;
17
+ height: 52px;
18
+ border-bottom: 1px solid var(--border);
19
+ background: var(--bg);
20
+ }
21
+ .console-header-left {
22
+ display: flex;
23
+ align-items: center;
24
+ gap: 10px;
25
+ }
26
+ .console-header-title {
27
+ font-size: 14px;
28
+ font-weight: 600;
29
+ letter-spacing: -0.01em;
30
+ }
31
+ .console-badge {
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 5px;
35
+ padding: 3px 8px;
36
+ border-radius: 20px;
37
+ font-size: 11px;
38
+ font-weight: 500;
39
+ }
40
+ .console-badge.on { background: rgba(34, 197, 94, 0.1); color: #22c55e; }
41
+ .console-badge.on .bdot { background: #22c55e; }
42
+ .console-badge.off { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
43
+ .console-badge.off .bdot { background: #ef4444; }
44
+ .bdot { width: 6px; height: 6px; border-radius: 50%; }
45
+ .console-nav {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 2px;
49
+ }
50
+ .console-nav a {
51
+ color: var(--text-dim);
52
+ text-decoration: none;
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 5px;
56
+ padding: 6px 12px;
57
+ border-radius: 8px;
58
+ font-size: 13px;
59
+ font-weight: 450;
60
+ transition: all 0.15s;
61
+ }
62
+ .console-nav a:hover {
63
+ color: var(--text-secondary);
64
+ background: var(--surface-raised);
65
+ }
66
+ </style>
67
+
68
+ <div class="console-header">
69
+ <div class="console-header-left">
70
+ <span class="console-header-title">LeedAB</span>
71
+ <div class="console-badge off" id="agent-badge">
72
+ <span class="bdot"></span>
73
+ <span id="agent-status-text">...</span>
74
+ </div>
75
+ </div>
76
+ <div class="console-nav">
77
+ <a href="/">
78
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4 20-7Z"/><path d="M22 2 11 13"/></svg>
79
+ Chat
80
+ </a>
81
+ <a href="/team.html">
82
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
83
+ Team
84
+ </a>
85
+ <button onclick="toggleTheme()" title="Toggle theme" style="background:none;border:1px solid var(--border);border-radius:8px;padding:5px 8px;cursor:pointer;color:var(--text-dim);display:flex;align-items:center;transition:all 0.15s">
86
+ <svg id="theme-icon-sun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
87
+ <svg id="theme-icon-moon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
88
+ </button>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="container">
93
+ <main>
94
+ <!-- Channel Health -->
95
+ <section class="section">
96
+ <div style="display:flex;align-items:center;justify-content:space-between">
97
+ <h2 class="section-title">Channels</h2>
98
+ <a href="/settings.html" style="font-size:12px;color:var(--text-faint);text-decoration:none;transition:color 0.15s" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--text-faint)'">Settings</a>
99
+ </div>
100
+ <p class="section-desc">How your team talks to the agent.</p>
101
+ <div class="cards" id="channel-cards">
102
+ <div class="card" id="card-telegram">
103
+ <div class="card-icon">
104
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
105
+ <path d="m22 2-7 20-4-9-9-4 20-7Z"/><path d="M22 2 11 13"/>
106
+ </svg>
107
+ </div>
108
+ <div class="card-body">
109
+ <h3>Telegram <span style="font-size:0.75rem;color:var(--success);font-weight:400">recommended</span></h3>
110
+ <div class="card-status" id="status-telegram">
111
+ <span class="dot"></span> <span>Checking...</span>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ <div class="card" id="card-whatsapp">
116
+ <div class="card-icon">
117
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
118
+ <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
119
+ </svg>
120
+ </div>
121
+ <div class="card-body">
122
+ <h3>WhatsApp</h3>
123
+ <div class="card-status" id="status-whatsapp">
124
+ <span class="dot"></span> <span>Checking...</span>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ <div class="card" id="card-teams">
129
+ <div class="card-icon">
130
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
131
+ <rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
132
+ <path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
133
+ </svg>
134
+ </div>
135
+ <div class="card-body">
136
+ <h3>Microsoft Teams</h3>
137
+ <div class="card-status" id="status-teams">
138
+ <span class="dot"></span> <span>Checking...</span>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ <div class="card" style="border-style:dashed;opacity:0.5">
143
+ <div class="card-icon" style="background:var(--surface-raised);color:var(--text-faint)">
144
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
145
+ <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>
146
+ </svg>
147
+ </div>
148
+ <div class="card-body">
149
+ <h3 style="color:var(--text-dim)">More coming soon</h3>
150
+ <div class="card-status"><span style="color:var(--text-faint)">Slack, Discord, Email</span></div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </section>
155
+
156
+ <!-- Vault Summary -->
157
+ <section class="section">
158
+ <div style="display:flex;align-items:center;justify-content:space-between">
159
+ <h2 class="section-title">Credential Vault</h2>
160
+ <a href="/settings.html" style="font-size:12px;color:var(--text-faint);text-decoration:none;transition:color 0.15s" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--text-faint)'">Manage</a>
161
+ </div>
162
+ <p class="section-desc">Services the agent can access via browser.</p>
163
+ <div id="vault-summary" class="info-grid">
164
+ <div class="info-item">
165
+ <span class="info-label">Stored</span>
166
+ <span class="info-value" id="vault-count">...</span>
167
+ </div>
168
+ </div>
169
+ </section>
170
+ </main>
171
+
172
+ <footer>
173
+ <p>Your files, credentials, and memory stay on this device.</p>
174
+ </footer>
175
+ </div>
176
+
177
+ <script>
178
+ // Theme toggle
179
+ function initTheme() {
180
+ const saved = localStorage.getItem("leedab-theme") || "dark";
181
+ document.documentElement.setAttribute("data-theme", saved);
182
+ updateThemeIcon(saved);
183
+ }
184
+ function toggleTheme() {
185
+ const current = document.documentElement.getAttribute("data-theme") || "dark";
186
+ const next = current === "dark" ? "light" : "dark";
187
+ document.documentElement.setAttribute("data-theme", next);
188
+ localStorage.setItem("leedab-theme", next);
189
+ updateThemeIcon(next);
190
+ }
191
+ function updateThemeIcon(theme) {
192
+ const sun = document.getElementById("theme-icon-sun");
193
+ const moon = document.getElementById("theme-icon-moon");
194
+ if (sun && moon) {
195
+ sun.style.display = theme === "dark" ? "block" : "none";
196
+ moon.style.display = theme === "light" ? "block" : "none";
197
+ }
198
+ }
199
+ initTheme();
200
+
201
+ document.addEventListener("DOMContentLoaded", () => {
202
+ refreshStatus();
203
+ refreshVault();
204
+ setInterval(refreshStatus, 30000);
205
+ });
206
+
207
+ async function refreshStatus() {
208
+ try {
209
+ const res = await fetch("/api/status");
210
+ const status = await res.json();
211
+
212
+ const badge = document.getElementById("agent-badge");
213
+ const statusText = document.getElementById("agent-status-text");
214
+
215
+ // If we got a response, agent/dashboard is running
216
+ badge.className = "console-badge on";
217
+ statusText.textContent = "On";
218
+
219
+ for (const [key, info] of Object.entries(status)) {
220
+ const card = document.getElementById(`card-${key}`);
221
+ const statusEl = document.getElementById(`status-${key}`);
222
+ if (!card || !statusEl) continue;
223
+
224
+ if (info.connected) {
225
+ card.classList.add("connected");
226
+ statusEl.innerHTML = '<span class="dot"></span> Connected';
227
+ } else {
228
+ card.classList.remove("connected");
229
+ statusEl.innerHTML = '<span class="dot"></span> Not connected';
230
+ }
231
+ }
232
+ } catch {
233
+ const badge = document.getElementById("agent-badge");
234
+ const statusText = document.getElementById("agent-status-text");
235
+ badge.className = "console-badge off";
236
+ statusText.textContent = "Off";
237
+ }
238
+ }
239
+
240
+ async function refreshVault() {
241
+ try {
242
+ const res = await fetch("/api/vault");
243
+ const entries = await res.json();
244
+ document.getElementById("vault-count").textContent =
245
+ entries.length > 0 ? `${entries.length} service(s)` : "None";
246
+ } catch {
247
+ document.getElementById("vault-count").textContent = "—";
248
+ }
249
+ }
250
+ </script>
251
+ </body>
252
+ </html>