@poolzin/pool-bot 2026.3.22 → 2026.3.24

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 (159) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/bindings-store.js +209 -0
  4. package/dist/acp/control-plane/runtime-cache.js +54 -0
  5. package/dist/acp/control-plane/runtime-options.js +215 -0
  6. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  7. package/dist/acp/policy.js +52 -0
  8. package/dist/acp/runtime/errors.js +47 -0
  9. package/dist/acp/runtime/registry.js +86 -0
  10. package/dist/acp/runtime/types.js +1 -0
  11. package/dist/acp/translator.js +97 -0
  12. package/dist/agents/btw.js +280 -0
  13. package/dist/agents/failover-error.js +145 -47
  14. package/dist/agents/fast-mode.js +24 -0
  15. package/dist/agents/live-model-errors.js +23 -0
  16. package/dist/agents/model-auth-env-vars.js +44 -0
  17. package/dist/agents/model-auth-markers.js +69 -0
  18. package/dist/agents/models-config.providers.discovery.js +180 -0
  19. package/dist/agents/models-config.providers.static.js +480 -0
  20. package/dist/auto-reply/reply/typing-policy.js +15 -0
  21. package/dist/browser/browser-profile-manager.js +319 -0
  22. package/dist/browser/cdp-proxy-bypass.js +129 -0
  23. package/dist/browser/cdp-timeouts.js +41 -0
  24. package/dist/browser/chrome-extension-validator.js +406 -0
  25. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  26. package/dist/browser/chrome-mcp.js +421 -0
  27. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  28. package/dist/browser/errors.js +67 -0
  29. package/dist/browser/form-fields.js +22 -0
  30. package/dist/browser/output-atomic.js +44 -0
  31. package/dist/browser/profile-capabilities.js +47 -0
  32. package/dist/browser/safe-filename.js +25 -0
  33. package/dist/browser/snapshot-roles.js +60 -0
  34. package/dist/build-info.json +3 -3
  35. package/dist/channels/account-snapshot-fields.js +176 -0
  36. package/dist/channels/draft-stream-controls.js +89 -0
  37. package/dist/channels/inbound-debounce-policy.js +28 -0
  38. package/dist/channels/typing-lifecycle.js +39 -0
  39. package/dist/cli/program/command-registry.js +52 -0
  40. package/dist/commands/agent-binding.js +123 -0
  41. package/dist/commands/agents.commands.bind.js +280 -0
  42. package/dist/commands/backup-shared.js +186 -0
  43. package/dist/commands/backup-verify.js +236 -0
  44. package/dist/commands/backup.js +166 -0
  45. package/dist/commands/channel-account-context.js +15 -0
  46. package/dist/commands/channel-account.js +190 -0
  47. package/dist/commands/gateway-install-token.js +117 -0
  48. package/dist/commands/oauth-tls-preflight.js +121 -0
  49. package/dist/commands/ollama-setup.js +402 -0
  50. package/dist/commands/security-owner-only.js +86 -0
  51. package/dist/commands/self-hosted-provider-setup.js +207 -0
  52. package/dist/commands/session-store-targets.js +12 -0
  53. package/dist/commands/sessions-cleanup.js +97 -0
  54. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  55. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  56. package/dist/control-ui/index.html +1 -1
  57. package/dist/cron/cron-filters.js +150 -0
  58. package/dist/cron/heartbeat-policy.js +26 -0
  59. package/dist/gateway/device-pairing-security.js +197 -0
  60. package/dist/gateway/event-deduplication.js +167 -0
  61. package/dist/gateway/hooks-mapping.js +46 -7
  62. package/dist/gateway/run-tracker.js +253 -0
  63. package/dist/gateway/server-methods/nodes.js +14 -0
  64. package/dist/gateway/websocket-preauth-security.js +188 -0
  65. package/dist/hooks/module-loader.js +28 -0
  66. package/dist/infra/agent-command-binding.js +144 -0
  67. package/dist/infra/backup.js +328 -0
  68. package/dist/infra/channel-account-context.js +173 -0
  69. package/dist/infra/errors.js +53 -13
  70. package/dist/infra/exec-approvals-security.js +217 -0
  71. package/dist/infra/security/command-analyzer.js +257 -0
  72. package/dist/infra/session-cleanup.js +143 -0
  73. package/dist/plugins/loader.js +16 -8
  74. package/dist/security/external-content.js +51 -1
  75. package/dist/sessions/session-costs.js +228 -0
  76. package/dist/shared/param-key.js +16 -0
  77. package/dist/shared/poll-params.js +58 -0
  78. package/dist/shared/polls.js +55 -0
  79. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  80. package/docs/FEATURES.md +523 -0
  81. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  82. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  83. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  84. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  85. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  86. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  87. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  88. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  89. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  90. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  91. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  92. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  93. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  94. package/docs/PHASE-7-SUMMARY.md +144 -0
  95. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  96. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  97. package/docs/README.md +116 -0
  98. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  99. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  100. package/docs/channels/googlechat.md +235 -206
  101. package/docs/channels/irc.md +332 -0
  102. package/docs/channels/nostr.md +255 -168
  103. package/docs/components/command-palette.md +166 -0
  104. package/docs/components/login-gate.md +219 -0
  105. package/docs/getting-started/installation.md +191 -0
  106. package/docs/getting-started/introduction.md +120 -0
  107. package/docs/improvements/USAGE-GUIDE.md +359 -0
  108. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  109. package/docs/reference/deadcode-detection.md +72 -0
  110. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  111. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  112. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  113. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  114. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  115. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  116. package/extensions/googlechat/package.json +11 -28
  117. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  118. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  119. package/extensions/googlechat/src/index.ts +14 -0
  120. package/extensions/irc/node_modules/.bin/tsc +21 -0
  121. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  122. package/extensions/irc/node_modules/.bin/vitest +21 -0
  123. package/extensions/irc/package.json +16 -8
  124. package/extensions/irc/src/index.ts +14 -0
  125. package/extensions/irc/src/irc-channel.test.ts +43 -0
  126. package/extensions/irc/src/irc-channel.ts +191 -0
  127. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  128. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  129. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  130. package/extensions/keyed-async-queue/package.json +20 -0
  131. package/extensions/keyed-async-queue/src/index.ts +14 -0
  132. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  133. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  134. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  135. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  136. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  137. package/extensions/memory-core/package.json +11 -8
  138. package/extensions/memory-core/src/index.ts +14 -0
  139. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  140. package/extensions/memory-core/src/memory-manager.ts +186 -0
  141. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  142. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  143. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  144. package/extensions/nostr/package.json +15 -24
  145. package/extensions/nostr/src/index.ts +14 -0
  146. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  147. package/extensions/nostr/src/nostr-channel.ts +228 -0
  148. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  149. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  150. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  151. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  152. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  153. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  154. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  155. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  156. package/package.json +2 -1
  157. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  158. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  159. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -0,0 +1,1632 @@
1
+ # OpenClaw Features Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Implement all missing OpenClaw features to make Pool Bot production-ready and feature-equivalent.
6
+
7
+ **Architecture:** Phased approach prioritizing critical UI/UX foundations (Phase 1), then control-plane improvements (Phase 2), channel expansion (Phase 3), and specialized features (Phase 4).
8
+
9
+ **Tech Stack:** TypeScript, Lit (UI), @mariozechner/pi-tui (TUI), Node.js 22+, Vitest (testing), pnpm (package management)
10
+
11
+ ---
12
+
13
+ ## PHASE 1: CRITICAL FOUNDATION (Weeks 1-4)
14
+
15
+ ### Task 1: TUI Integration (@mariozechner/pi-tui)
16
+
17
+ **Files:**
18
+ - Create: `src/tui/components/login-gate.ts`
19
+ - Create: `src/tui/components/nodes-list.ts`
20
+ - Create: `src/tui/components/exec-approvals-panel.ts`
21
+ - Create: `src/tui/components/device-pairing.ts`
22
+ - Modify: `src/tui/tui.ts:45-120` (add component registration)
23
+ - Test: `src/tui/components/*.test.ts` (one per component)
24
+
25
+ **Dependencies:** `@mariozechner/pi-tui@0.52.12` (already in package.json)
26
+
27
+ **Estimated Effort:** 16 hours (2 days)
28
+
29
+ **Success Criteria:**
30
+ - TUI renders all 4 new components
31
+ - Components respond to gateway events
32
+ - Keyboard navigation works (arrow keys, Enter, Escape)
33
+ - Tests pass with >80% coverage
34
+
35
+ **Step 1: Create login-gate component**
36
+
37
+ ```typescript
38
+ // src/tui/components/login-gate.ts
39
+ import { Component, Container, Text } from "@mariozechner/pi-tui";
40
+
41
+ export class LoginGate extends Component {
42
+ private status: "disconnected" | "connecting" | "authenticated" = "disconnected";
43
+ private error: string | null = null;
44
+
45
+ setStatus(status: typeof this.status) {
46
+ this.status = status;
47
+ this.markDirty();
48
+ }
49
+
50
+ setError(error: string | null) {
51
+ this.error = error;
52
+ this.markDirty();
53
+ }
54
+
55
+ render() {
56
+ if (this.status === "authenticated") return null;
57
+
58
+ return new Container([
59
+ new Text(`Gateway: ${this.status}`, { color: this.status === "disconnected" ? "red" : "yellow" }),
60
+ this.error ? new Text(this.error, { color: "red" }) : null,
61
+ ].filter(Boolean));
62
+ }
63
+ }
64
+ ```
65
+
66
+ **Step 2: Run test to verify component compiles**
67
+
68
+ ```bash
69
+ pnpm tsc --noEmit src/tui/components/login-gate.ts
70
+ ```
71
+ Expected: No errors
72
+
73
+ **Step 3: Create test file**
74
+
75
+ ```typescript
76
+ // src/tui/components/login-gate.test.ts
77
+ import { describe, it, expect } from "vitest";
78
+ import { LoginGate } from "./login-gate";
79
+
80
+ describe("LoginGate", () => {
81
+ it("renders disconnected status", () => {
82
+ const component = new LoginGate();
83
+ component.setStatus("disconnected");
84
+ expect(component.render()).toBeDefined();
85
+ });
86
+
87
+ it("shows error message", () => {
88
+ const component = new LoginGate();
89
+ component.setError("Auth failed");
90
+ const rendered = component.render();
91
+ expect(rendered).toBeDefined();
92
+ });
93
+ });
94
+ ```
95
+
96
+ **Step 4: Run test**
97
+
98
+ ```bash
99
+ pnpm test src/tui/components/login-gate.test.ts
100
+ ```
101
+ Expected: PASS
102
+
103
+ **Step 5: Create remaining components (nodes-list, exec-approvals-panel, device-pairing)**
104
+
105
+ Follow same pattern as Step 1-4 for each component.
106
+
107
+ **Step 6: Register components in TUI**
108
+
109
+ ```typescript
110
+ // src/tui/tui.ts - add to component initialization
111
+ import { LoginGate } from "./components/login-gate";
112
+ import { NodesList } from "./components/nodes-list";
113
+ import { ExecApprovalsPanel } from "./components/exec-approvals-panel";
114
+ import { DevicePairing } from "./components/device-pairing";
115
+
116
+ // In constructor or init method:
117
+ this.loginGate = new LoginGate();
118
+ this.nodesList = new NodesList();
119
+ this.execApprovals = new ExecApprovalsPanel();
120
+ this.devicePairing = new DevicePairing();
121
+ ```
122
+
123
+ **Step 7: Commit**
124
+
125
+ ```bash
126
+ git add src/tui/components/*.ts src/tui/tui.ts
127
+ git commit -m "feat(tui): add login-gate, nodes-list, exec-approvals, device-pairing components"
128
+ ```
129
+
130
+ ---
131
+
132
+ ### Task 2: Command Palette UI (Cmd+K Navigation)
133
+
134
+ **Files:**
135
+ - Modify: `ui/src/ui/views/command-palette.ts:16-82` (add more navigation items)
136
+ - Modify: `ui/src/ui/views/dashboard-v2.ts` (integrate command palette)
137
+ - Create: `ui/src/ui/views/command-palette.test.ts`
138
+ - Modify: `ui/src/ui/storage.ts` (persist cmd-k usage stats)
139
+
140
+ **Dependencies:** Lit (already installed)
141
+
142
+ **Estimated Effort:** 8 hours (1 day)
143
+
144
+ **Success Criteria:**
145
+ - Cmd+K opens palette from any view
146
+ - Search filters items correctly
147
+ - Arrow keys navigate, Enter selects
148
+ - Usage stats tracked in storage
149
+
150
+ **Step 1: Add navigation items for new views**
151
+
152
+ ```typescript
153
+ // ui/src/ui/views/command-palette.ts - extend NAVIGATION_ITEMS
154
+ const NAVIGATION_ITEMS: PaletteItem[] = [
155
+ // ... existing items ...
156
+ {
157
+ id: "nav-nodes",
158
+ label: "Nodes",
159
+ icon: "server",
160
+ category: "navigation",
161
+ action: "nav:nodes",
162
+ description: "Manage compute nodes",
163
+ },
164
+ {
165
+ id: "nav-exec-approvals",
166
+ label: "Exec Approvals",
167
+ icon: "shield",
168
+ category: "navigation",
169
+ action: "nav:exec-approvals",
170
+ description: "Pending command approvals",
171
+ },
172
+ {
173
+ id: "nav-device-pair",
174
+ label: "Pair Device",
175
+ icon: "smartphone",
176
+ category: "navigation",
177
+ action: "nav:device-pair",
178
+ description: "Pair new device",
179
+ },
180
+ ];
181
+ ```
182
+
183
+ **Step 2: Integrate with dashboard-v2**
184
+
185
+ ```typescript
186
+ // ui/src/ui/views/dashboard-v2.ts - add import and handler
187
+ import { renderCommandPalette, installGlobalCmdKHandler, getPaletteItemsForCmdK } from "./command-palette";
188
+
189
+ // In state:
190
+ private cmdKOpen = false;
191
+ private cmdKQuery = "";
192
+ private cmdKActiveIndex = 0;
193
+
194
+ // In render method:
195
+ ${renderCommandPalette({
196
+ open: this.cmdKOpen,
197
+ query: this.cmdKQuery,
198
+ activeIndex: this.cmdKActiveIndex,
199
+ onToggle: () => { this.cmdKOpen = !this.cmdKOpen; },
200
+ onQueryChange: (q) => { this.cmdKQuery = q; },
201
+ onActiveIndexChange: (i) => { this.cmdKActiveIndex = i; },
202
+ onNavigate: (tab) => { this.navigate(tab); },
203
+ onSlashCommand: (cmd) => { this.handleSlashCommand(cmd); },
204
+ })}
205
+ ```
206
+
207
+ **Step 3: Install global handler**
208
+
209
+ ```typescript
210
+ // In connectedCallback or init:
211
+ installGlobalCmdKHandler(() => {
212
+ this.cmdKOpen = !this.cmdKOpen;
213
+ });
214
+ ```
215
+
216
+ **Step 4: Create test file**
217
+
218
+ ```typescript
219
+ // ui/src/ui/views/command-palette.test.ts
220
+ import { describe, it, expect } from "vitest";
221
+ import { getPaletteItemsForCmdK, filteredItems } from "./command-palette";
222
+
223
+ describe("Command Palette", () => {
224
+ it("returns navigation items", () => {
225
+ const items = getPaletteItemsForCmdK();
226
+ expect(items.some(i => i.category === "navigation")).toBe(true);
227
+ });
228
+
229
+ it("filters by query", () => {
230
+ const items = filteredItems("nodes");
231
+ expect(items.every(i =>
232
+ i.label.toLowerCase().includes("nodes") ||
233
+ i.description?.toLowerCase().includes("nodes")
234
+ )).toBe(true);
235
+ });
236
+ });
237
+ ```
238
+
239
+ **Step 5: Run tests**
240
+
241
+ ```bash
242
+ pnpm test ui/src/ui/views/command-palette.test.ts
243
+ ```
244
+ Expected: PASS
245
+
246
+ **Step 6: Commit**
247
+
248
+ ```bash
249
+ git add ui/src/ui/views/command-palette.ts ui/src/ui/views/dashboard-v2.ts
250
+ git commit -m "feat(ui): enhance command palette with nodes/exec-approvals navigation"
251
+ ```
252
+
253
+ ---
254
+
255
+ ### Task 3: Login Gate Component
256
+
257
+ **Files:**
258
+ - Create: `ui/src/ui/components/login-gate.ts`
259
+ - Create: `ui/src/ui/components/login-gate.test.ts`
260
+ - Modify: `ui/src/ui/views/overview.ts` (embed login-gate)
261
+
262
+ **Dependencies:** Lit (already installed)
263
+
264
+ **Estimated Effort:** 6 hours (0.75 days)
265
+
266
+ **Success Criteria:**
267
+ - Login gate shows before dashboard access
268
+ - Validates token/password
269
+ - Shows clear error messages
270
+ - Persists auth state
271
+
272
+ **Step 1: Create login-gate component**
273
+
274
+ ```typescript
275
+ // ui/src/ui/components/login-gate.ts
276
+ import { LitElement, html, css } from "lit";
277
+ import { customElement, property } from "lit/decorators.js";
278
+
279
+ @customElement("login-gate")
280
+ export class LoginGate extends LitElement {
281
+ @property({ type: Boolean }) connected = false;
282
+ @property({ type: String }) error: string | null = null;
283
+ @property({ type: String }) token = "";
284
+ @property({ type: String }) password = "";
285
+
286
+ static styles = css`
287
+ :host {
288
+ display: block;
289
+ max-width: 400px;
290
+ margin: 2rem auto;
291
+ padding: 2rem;
292
+ border: 1px solid #e0e0e0;
293
+ border-radius: 8px;
294
+ }
295
+ .error { color: #d32f2f; margin-top: 1rem; }
296
+ .field { margin-bottom: 1rem; }
297
+ .field label { display: block; margin-bottom: 0.5rem; }
298
+ .field input { width: 100%; padding: 0.5rem; }
299
+ button {
300
+ width: 100%;
301
+ padding: 0.75rem;
302
+ background: #1976d2;
303
+ color: white;
304
+ border: none;
305
+ border-radius: 4px;
306
+ cursor: pointer;
307
+ }
308
+ `;
309
+
310
+ private handleSubmit(e: Event) {
311
+ e.preventDefault();
312
+ this.dispatchEvent(new CustomEvent("login", {
313
+ detail: { token: this.token, password: this.password }
314
+ }));
315
+ }
316
+
317
+ render() {
318
+ if (this.connected) {
319
+ return html`<div style="color: green;">✓ Authenticated</div>`;
320
+ }
321
+
322
+ return html`
323
+ <form @submit=${this.handleSubmit}>
324
+ <div class="field">
325
+ <label>Gateway Token</label>
326
+ <input
327
+ type="text"
328
+ .value=${this.token}
329
+ @input=${(e: Event) => this.token = (e.target as HTMLInputElement).value}
330
+ placeholder="CLAWDBOT_GATEWAY_TOKEN"
331
+ />
332
+ </div>
333
+ <div class="field">
334
+ <label>Password (optional)</label>
335
+ <input
336
+ type="password"
337
+ .value=${this.password}
338
+ @input=${(e: Event) => this.password = (e.target as HTMLInputElement).value}
339
+ />
340
+ </div>
341
+ ${this.error ? html`<div class="error">${this.error}</div>` : ""}
342
+ <button type="submit">Connect</button>
343
+ </form>
344
+ `;
345
+ }
346
+ }
347
+ ```
348
+
349
+ **Step 2: Create test**
350
+
351
+ ```typescript
352
+ // ui/src/ui/components/login-gate.test.ts
353
+ import { describe, it, expect } from "vitest";
354
+ import { LoginGate } from "./login-gate";
355
+
356
+ describe("LoginGate", () => {
357
+ it("renders login form when disconnected", () => {
358
+ const el = new LoginGate();
359
+ el.connected = false;
360
+ const shadow = el.renderRoot as ShadowRoot;
361
+ expect(shadow.querySelector("form")).toBeDefined();
362
+ });
363
+
364
+ it("shows success message when connected", () => {
365
+ const el = new LoginGate();
366
+ el.connected = true;
367
+ const shadow = el.renderRoot as ShadowRoot;
368
+ expect(shadow.textContent).toContain("Authenticated");
369
+ });
370
+
371
+ it("dispatches login event on submit", () => {
372
+ const el = new LoginGate();
373
+ let eventDispatched = false;
374
+ el.addEventListener("login", () => { eventDispatched = true; });
375
+ el.token = "test-token";
376
+ // Simulate form submit
377
+ const form = el.renderRoot.querySelector("form");
378
+ form?.dispatchEvent(new SubmitEvent("submit", { cancelable: true }));
379
+ expect(eventDispatched).toBe(true);
380
+ });
381
+ });
382
+ ```
383
+
384
+ **Step 3: Run test**
385
+
386
+ ```bash
387
+ pnpm test ui/src/ui/components/login-gate.test.ts
388
+ ```
389
+ Expected: PASS
390
+
391
+ **Step 4: Embed in overview**
392
+
393
+ ```typescript
394
+ // ui/src/ui/views/overview.ts - replace auth form section
395
+ import "../components/login-gate";
396
+
397
+ // In render method:
398
+ <login-gate
399
+ .connected=${props.connected}
400
+ .error=${props.lastError}
401
+ .token=${props.settings.token}
402
+ .password=${props.password}
403
+ @login=${(e: CustomEvent) => {
404
+ props.onSettingsChange({ ...props.settings, token: e.detail.token });
405
+ props.onPasswordChange(e.detail.password);
406
+ props.onConnect();
407
+ }}
408
+ ></login-gate>
409
+ ```
410
+
411
+ **Step 5: Commit**
412
+
413
+ ```bash
414
+ git add ui/src/ui/components/login-gate.ts ui/src/ui/views/overview.ts
415
+ git commit -m "feat(ui): add login-gate component for authentication"
416
+ ```
417
+
418
+ ---
419
+
420
+ ### Task 4: Nodes Management UI
421
+
422
+ **Files:**
423
+ - Modify: `ui/src/ui/views/nodes.ts` (add full CRUD UI)
424
+ - Create: `ui/src/ui/controllers/nodes-crud.ts`
425
+ - Create: `ui/src/ui/components/nodes-table.ts`
426
+ - Create: `ui/src/ui/components/nodes-form.ts`
427
+ - Test: `ui/src/ui/views/nodes.test.ts`
428
+
429
+ **Dependencies:** Existing nodes gateway methods (`src/gateway/server-methods/nodes.ts`)
430
+
431
+ **Estimated Effort:** 12 hours (1.5 days)
432
+
433
+ **Success Criteria:**
434
+ - List all nodes with status
435
+ - Add/edit/delete nodes
436
+ - Show node health metrics
437
+ - Real-time status updates
438
+
439
+ **Step 1: Read existing nodes view**
440
+
441
+ ```bash
442
+ cat ui/src/ui/views/nodes.ts
443
+ ```
444
+ Understand current structure before modifying.
445
+
446
+ **Step 2: Create nodes-table component**
447
+
448
+ ```typescript
449
+ // ui/src/ui/components/nodes-table.ts
450
+ import { LitElement, html, css } from "lit";
451
+ import { customElement, property } from "lit/decorators.js";
452
+
453
+ type Node = {
454
+ id: string;
455
+ name: string;
456
+ status: "online" | "offline" | "busy";
457
+ cpu: number;
458
+ memory: number;
459
+ lastSeen: number;
460
+ };
461
+
462
+ @customElement("nodes-table")
463
+ export class NodesTable extends LitElement {
464
+ @property({ type: Array }) nodes: Node[] = [];
465
+ @property({ type: Number }) selectedIndex = -1;
466
+
467
+ static styles = css`
468
+ table { width: 100%; border-collapse: collapse; }
469
+ th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #e0e0e0; }
470
+ th { background: #f5f5f5; font-weight: 600; }
471
+ tr.selected { background: #e3f2fd; }
472
+ .status { padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.875rem; }
473
+ .status-online { background: #c8e6c9; color: #2e7d32; }
474
+ .status-offline { background: #ffcdd2; color: #c62828; }
475
+ .status-busy { background: #fff9c4; color: #f57f17; }
476
+ `;
477
+
478
+ render() {
479
+ return html`
480
+ <table>
481
+ <thead>
482
+ <tr>
483
+ <th>Name</th>
484
+ <th>Status</th>
485
+ <th>CPU</th>
486
+ <th>Memory</th>
487
+ <th>Last Seen</th>
488
+ </tr>
489
+ </thead>
490
+ <tbody>
491
+ ${this.nodes.map((node, i) => html`
492
+ <tr
493
+ class=${i === this.selectedIndex ? "selected" : ""}
494
+ @click=${() => this.dispatchEvent(new CustomEvent("select", { detail: i }))}
495
+ >
496
+ <td>${node.name}</td>
497
+ <td><span class="status status-${node.status}">${node.status}</span></td>
498
+ <td>${node.cpu}%</td>
499
+ <td>${node.memory}%</td>
500
+ <td>${new Date(node.lastSeen).toLocaleString()}</td>
501
+ </tr>
502
+ `)}
503
+ </tbody>
504
+ </table>
505
+ `;
506
+ }
507
+ }
508
+ ```
509
+
510
+ **Step 3: Create nodes-form component**
511
+
512
+ ```typescript
513
+ // ui/src/ui/components/nodes-form.ts
514
+ import { LitElement, html, css } from "lit";
515
+ import { customElement, property } from "lit/decorators.js";
516
+
517
+ type NodeFormData = {
518
+ name: string;
519
+ endpoint: string;
520
+ token: string;
521
+ };
522
+
523
+ @customElement("nodes-form")
524
+ export class NodesForm extends LitElement {
525
+ @property({ type: Object }) data: NodeFormData = { name: "", endpoint: "", token: "" };
526
+
527
+ static styles = css`
528
+ .field { margin-bottom: 1rem; }
529
+ label { display: block; margin-bottom: 0.5rem; }
530
+ input { width: 100%; padding: 0.5rem; }
531
+ .actions { display: flex; gap: 1rem; margin-top: 1rem; }
532
+ button { padding: 0.5rem 1rem; }
533
+ `;
534
+
535
+ render() {
536
+ return html`
537
+ <form @submit=${(e: Event) => e.preventDefault()}>
538
+ <div class="field">
539
+ <label>Name</label>
540
+ <input
541
+ .value=${this.data.name}
542
+ @input=${(e: Event) => this.data.name = (e.target as HTMLInputElement).value}
543
+ />
544
+ </div>
545
+ <div class="field">
546
+ <label>Endpoint</label>
547
+ <input
548
+ .value=${this.data.endpoint}
549
+ @input=${(e: Event) => this.data.endpoint = (e.target as HTMLInputElement).value}
550
+ placeholder="https://node.example.com:18789"
551
+ />
552
+ </div>
553
+ <div class="field">
554
+ <label>Token</label>
555
+ <input
556
+ type="password"
557
+ .value=${this.data.token}
558
+ @input=${(e: Event) => this.data.token = (e.target as HTMLInputElement).value}
559
+ />
560
+ </div>
561
+ <div class="actions">
562
+ <button @click=${() => this.dispatchEvent(new CustomEvent("save", { detail: this.data }))}>
563
+ Save
564
+ </button>
565
+ <button type="button" @click=${() => this.dispatchEvent(new CustomEvent("cancel"))}>
566
+ Cancel
567
+ </button>
568
+ </div>
569
+ </form>
570
+ `;
571
+ }
572
+ }
573
+ ```
574
+
575
+ **Step 4: Create CRUD controller**
576
+
577
+ ```typescript
578
+ // ui/src/ui/controllers/nodes-crud.ts
579
+ import type { GatewayClient } from "../gateway";
580
+
581
+ export class NodesCrudController {
582
+ private client: GatewayClient;
583
+
584
+ constructor(client: GatewayClient) {
585
+ this.client = client;
586
+ }
587
+
588
+ async list() {
589
+ const response = await this.client.send({ method: "nodes:list", params: {} });
590
+ return response.result as any[];
591
+ }
592
+
593
+ async create(data: { name: string; endpoint: string; token: string }) {
594
+ const response = await this.client.send({
595
+ method: "nodes:create",
596
+ params: data
597
+ });
598
+ return response.result;
599
+ }
600
+
601
+ async update(id: string, data: Partial<{ name: string; endpoint: string; token: string }>) {
602
+ const response = await this.client.send({
603
+ method: "nodes:update",
604
+ params: { id, ...data }
605
+ });
606
+ return response.result;
607
+ }
608
+
609
+ async delete(id: string) {
610
+ const response = await this.client.send({
611
+ method: "nodes:delete",
612
+ params: { id }
613
+ });
614
+ return response.result;
615
+ }
616
+ }
617
+ ```
618
+
619
+ **Step 5: Update nodes view**
620
+
621
+ ```typescript
622
+ // ui/src/ui/views/nodes.ts - integrate components
623
+ import "../components/nodes-table";
624
+ import "../components/nodes-form";
625
+ import { NodesCrudController } from "../controllers/nodes-crud";
626
+
627
+ // In class:
628
+ private crud = new NodesCrudController(this.client);
629
+ private nodes = [];
630
+ private showForm = false;
631
+
632
+ async loadNodes() {
633
+ this.nodes = await this.crud.list();
634
+ this.requestUpdate();
635
+ }
636
+
637
+ render() {
638
+ return html`
639
+ <div class="toolbar">
640
+ <button @click=${() => this.showForm = true}>Add Node</button>
641
+ <button @click=${() => this.loadNodes()}>Refresh</button>
642
+ </div>
643
+ ${this.showForm ? html`
644
+ <nodes-form
645
+ @save=${async (e: CustomEvent) => {
646
+ await this.crud.create(e.detail);
647
+ this.showForm = false;
648
+ this.loadNodes();
649
+ }}
650
+ @cancel=${() => this.showForm = false}
651
+ ></nodes-form>
652
+ ` : html`
653
+ <nodes-table
654
+ .nodes=${this.nodes}
655
+ @select=${(e: CustomEvent) => this.selectNode(e.detail)}
656
+ ></nodes-table>
657
+ `}
658
+ `;
659
+ }
660
+ ```
661
+
662
+ **Step 6: Create tests**
663
+
664
+ ```typescript
665
+ // ui/src/ui/views/nodes.test.ts
666
+ import { describe, it, expect, vi } from "vitest";
667
+ import { NodesView } from "./nodes";
668
+
669
+ describe("NodesView", () => {
670
+ it("loads nodes on connect", async () => {
671
+ const view = new NodesView();
672
+ const mockClient = { send: vi.fn().mockResolvedValue({ result: [] }) };
673
+ view.client = mockClient as any;
674
+ await view.loadNodes();
675
+ expect(mockClient.send).toHaveBeenCalledWith({ method: "nodes:list", params: {} });
676
+ });
677
+
678
+ it("creates node via form", async () => {
679
+ const view = new NodesView();
680
+ const mockClient = { send: vi.fn().mockResolvedValue({ result: {} }) };
681
+ view.client = mockClient as any;
682
+ // Simulate form save
683
+ await view.crud.create({ name: "test", endpoint: "http://test", token: "abc" });
684
+ expect(mockClient.send).toHaveBeenCalledWith({
685
+ method: "nodes:create",
686
+ params: { name: "test", endpoint: "http://test", token: "abc" }
687
+ });
688
+ });
689
+ });
690
+ ```
691
+
692
+ **Step 7: Run tests**
693
+
694
+ ```bash
695
+ pnpm test ui/src/ui/views/nodes.test.ts
696
+ ```
697
+ Expected: PASS
698
+
699
+ **Step 8: Commit**
700
+
701
+ ```bash
702
+ git add ui/src/ui/views/nodes.ts ui/src/ui/components/nodes-*.ts ui/src/ui/controllers/nodes-crud.ts
703
+ git commit -m "feat(ui): add full CRUD UI for nodes management"
704
+ ```
705
+
706
+ ---
707
+
708
+ ### Task 5: Exec Approvals UI for Nodes
709
+
710
+ **Files:**
711
+ - Modify: `ui/src/ui/views/exec-approval.ts` (enhance with nodes support)
712
+ - Create: `ui/src/ui/components/exec-approvals-queue.ts`
713
+ - Create: `ui/src/ui/components/exec-approvals-history.ts`
714
+ - Test: `ui/src/ui/views/exec-approval.test.ts`
715
+
716
+ **Dependencies:** `src/gateway/server-methods/exec-approval.ts`, `src/infra/exec-approvals.ts`
717
+
718
+ **Estimated Effort:** 10 hours (1.25 days)
719
+
720
+ **Success Criteria:**
721
+ - Shows pending approvals queue
722
+ - Approve/reject buttons work
723
+ - Shows approval history
724
+ - Filters by node/status
725
+
726
+ **Step 1: Read existing exec-approval view**
727
+
728
+ ```bash
729
+ cat ui/src/ui/views/exec-approval.ts
730
+ ```
731
+
732
+ **Step 2: Create queue component**
733
+
734
+ ```typescript
735
+ // ui/src/ui/components/exec-approvals-queue.ts
736
+ import { LitElement, html, css } from "lit";
737
+ import { customElement, property } from "lit/decorators.js";
738
+
739
+ type PendingApproval = {
740
+ id: string;
741
+ nodeId: string;
742
+ command: string;
743
+ requestedAt: number;
744
+ requester: string;
745
+ };
746
+
747
+ @customElement("exec-approvals-queue")
748
+ export class ExecApprovalsQueue extends LitElement {
749
+ @property({ type: Array }) pending: PendingApproval[] = [];
750
+
751
+ static styles = css`
752
+ .approval {
753
+ border: 1px solid #e0e0e0;
754
+ padding: 1rem;
755
+ margin-bottom: 1rem;
756
+ border-radius: 4px;
757
+ }
758
+ .command {
759
+ font-family: monospace;
760
+ background: #f5f5f5;
761
+ padding: 0.5rem;
762
+ margin: 0.5rem 0;
763
+ }
764
+ .actions { display: flex; gap: 0.5rem; margin-top: 1rem; }
765
+ .approve { background: #4caf50; color: white; border: none; padding: 0.5rem 1rem; }
766
+ .reject { background: #f44336; color: white; border: none; padding: 0.5rem 1rem; }
767
+ `;
768
+
769
+ render() {
770
+ if (this.pending.length === 0) {
771
+ return html`<div class="empty">No pending approvals</div>`;
772
+ }
773
+
774
+ return html`
775
+ ${this.pending.map(approval => html`
776
+ <div class="approval">
777
+ <div><strong>Node:</strong> ${approval.nodeId}</div>
778
+ <div><strong>Command:</strong></div>
779
+ <div class="command">${approval.command}</div>
780
+ <div><strong>Requested:</strong> ${new Date(approval.requestedAt).toLocaleString()}</div>
781
+ <div><strong>Requester:</strong> ${approval.requester}</div>
782
+ <div class="actions">
783
+ <button
784
+ class="approve"
785
+ @click=${() => this.dispatchEvent(new CustomEvent("approve", { detail: approval.id }))}
786
+ >
787
+ Approve
788
+ </button>
789
+ <button
790
+ class="reject"
791
+ @click=${() => this.dispatchEvent(new CustomEvent("reject", { detail: approval.id }))}
792
+ >
793
+ Reject
794
+ </button>
795
+ </div>
796
+ </div>
797
+ `)}
798
+ `;
799
+ }
800
+ }
801
+ ```
802
+
803
+ **Step 3: Create history component**
804
+
805
+ ```typescript
806
+ // ui/src/ui/components/exec-approvals-history.ts
807
+ import { LitElement, html, css } from "lit";
808
+ import { customElement, property } from "lit/decorators.js";
809
+
810
+ type ApprovalHistory = {
811
+ id: string;
812
+ command: string;
813
+ status: "approved" | "rejected" | "expired";
814
+ decidedAt: number;
815
+ decisionBy: string;
816
+ };
817
+
818
+ @customElement("exec-approvals-history")
819
+ export class ExecApprovalsHistory extends LitElement {
820
+ @property({ type: Array }) history: ApprovalHistory[] = [];
821
+ @property({ type: String }) filter = "all";
822
+
823
+ static styles = css`
824
+ table { width: 100%; border-collapse: collapse; }
825
+ th, td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #e0e0e0; }
826
+ .status-approved { color: #4caf50; }
827
+ .status-rejected { color: #f44336; }
828
+ .status-expired { color: #ff9800; }
829
+ .filters { margin-bottom: 1rem; }
830
+ .filters button { margin-right: 0.5rem; }
831
+ `;
832
+
833
+ private get filteredHistory() {
834
+ if (this.filter === "all") return this.history;
835
+ return this.history.filter(h => h.status === this.filter);
836
+ }
837
+
838
+ render() {
839
+ return html`
840
+ <div class="filters">
841
+ <button @click=${() => this.filter = "all"}>All</button>
842
+ <button @click=${() => this.filter = "approved"}>Approved</button>
843
+ <button @click=${() => this.filter = "rejected"}>Rejected</button>
844
+ <button @click=${() => this.filter = "expired"}>Expired</button>
845
+ </div>
846
+ <table>
847
+ <thead>
848
+ <tr>
849
+ <th>Command</th>
850
+ <th>Status</th>
851
+ <th>Decided</th>
852
+ <th>By</th>
853
+ </tr>
854
+ </thead>
855
+ <tbody>
856
+ ${this.filteredHistory.map(h => html`
857
+ <tr>
858
+ <td><code>${h.command}</code></td>
859
+ <td class="status-${h.status}">${h.status}</td>
860
+ <td>${new Date(h.decidedAt).toLocaleString()}</td>
861
+ <td>${h.decisionBy}</td>
862
+ </tr>
863
+ `)}
864
+ </tbody>
865
+ </table>
866
+ `;
867
+ }
868
+ }
869
+ ```
870
+
871
+ **Step 4: Enhance exec-approval view**
872
+
873
+ ```typescript
874
+ // ui/src/ui/views/exec-approval.ts
875
+ import "../components/exec-approvals-queue";
876
+ import "../components/exec-approvals-history";
877
+
878
+ // Add state:
879
+ private pending = [];
880
+ private history = [];
881
+ private activeTab = "queue";
882
+
883
+ // Add methods:
884
+ async loadPending() {
885
+ const response = await this.client.send({ method: "exec-approvals:list", params: { status: "pending" } });
886
+ this.pending = response.result as any[];
887
+ }
888
+
889
+ async approve(id: string) {
890
+ await this.client.send({ method: "exec-approvals:approve", params: { id } });
891
+ this.loadPending();
892
+ this.loadHistory();
893
+ }
894
+
895
+ async reject(id: string) {
896
+ await this.client.send({ method: "exec-approvals:reject", params: { id } });
897
+ this.loadPending();
898
+ this.loadHistory();
899
+ }
900
+
901
+ // Render:
902
+ render() {
903
+ return html`
904
+ <div class="tabs">
905
+ <button class=${this.activeTab === "queue" ? "active" : ""}
906
+ @click=${() => this.activeTab = "queue"}>
907
+ Queue
908
+ </button>
909
+ <button class=${this.activeTab === "history" ? "active" : ""}
910
+ @click=${() => this.activeTab = "history"}>
911
+ History
912
+ </button>
913
+ </div>
914
+ ${this.activeTab === "queue" ? html`
915
+ <exec-approvals-queue
916
+ .pending=${this.pending}
917
+ @approve=${(e: CustomEvent) => this.approve(e.detail)}
918
+ @reject=${(e: CustomEvent) => this.reject(e.detail)}
919
+ ></exec-approvals-queue>
920
+ ` : html`
921
+ <exec-approvals-history .history=${this.history}></exec-approvals-history>
922
+ `}
923
+ `;
924
+ }
925
+ ```
926
+
927
+ **Step 5: Create tests**
928
+
929
+ ```typescript
930
+ // ui/src/ui/views/exec-approval.test.ts
931
+ import { describe, it, expect, vi } from "vitest";
932
+ import { ExecApprovalView } from "./exec-approval";
933
+
934
+ describe("ExecApprovalView", () => {
935
+ it("loads pending approvals", async () => {
936
+ const view = new ExecApprovalView();
937
+ const mockClient = { send: vi.fn().mockResolvedValue({ result: [] }) };
938
+ view.client = mockClient as any;
939
+ await view.loadPending();
940
+ expect(mockClient.send).toHaveBeenCalledWith({
941
+ method: "exec-approvals:list",
942
+ params: { status: "pending" }
943
+ });
944
+ });
945
+
946
+ it("approves pending request", async () => {
947
+ const view = new ExecApprovalView();
948
+ const mockClient = { send: vi.fn().mockResolvedValue({ result: {} }) };
949
+ view.client = mockClient as any;
950
+ await view.approve("approval-123");
951
+ expect(mockClient.send).toHaveBeenCalledWith({
952
+ method: "exec-approvals:approve",
953
+ params: { id: "approval-123" }
954
+ });
955
+ });
956
+ });
957
+ ```
958
+
959
+ **Step 6: Run tests**
960
+
961
+ ```bash
962
+ pnpm test ui/src/ui/views/exec-approval.test.ts
963
+ ```
964
+ Expected: PASS
965
+
966
+ **Step 7: Commit**
967
+
968
+ ```bash
969
+ git add ui/src/ui/views/exec-approval.ts ui/src/ui/components/exec-approvals-*.ts
970
+ git commit -m "feat(ui): add exec approvals queue and history UI"
971
+ ```
972
+
973
+ ---
974
+
975
+ ### Task 6: Device Pairing UI
976
+
977
+ **Files:**
978
+ - Create: `ui/src/ui/views/device-pair.ts`
979
+ - Create: `ui/src/ui/components/device-pair-qr.ts`
980
+ - Create: `ui/src/ui/components/device-pair-code.ts`
981
+ - Test: `ui/src/ui/views/device-pair.test.ts`
982
+
983
+ **Dependencies:** `src/infra/device-pairing.ts`, `src/gateway/device-pairing-security.ts`
984
+
985
+ **Estimated Effort:** 10 hours (1.25 days)
986
+
987
+ **Success Criteria:**
988
+ - Shows QR code for pairing
989
+ - Shows setup code alternative
990
+ - Pairing status updates in real-time
991
+ - Shows paired devices list
992
+
993
+ **Step 1: Create QR component**
994
+
995
+ ```typescript
996
+ // ui/src/ui/components/device-pair-qr.ts
997
+ import { LitElement, html, css } from "lit";
998
+ import { customElement, property } from "lit/decorators.js";
999
+
1000
+ @customElement("device-pair-qr")
1001
+ export class DevicePairQR extends LitElement {
1002
+ @property({ type: String }) pairingUrl = "";
1003
+ @property({ type: String }) setupCode = "";
1004
+
1005
+ static styles = css`
1006
+ .container { text-align: center; padding: 2rem; }
1007
+ .qr {
1008
+ width: 256px;
1009
+ height: 256px;
1010
+ margin: 1rem auto;
1011
+ background: white;
1012
+ border: 1px solid #e0e0e0;
1013
+ }
1014
+ .code {
1015
+ font-size: 2rem;
1016
+ font-family: monospace;
1017
+ letter-spacing: 0.5rem;
1018
+ margin: 1rem 0;
1019
+ }
1020
+ .instructions { color: #666; margin-top: 1rem; }
1021
+ `;
1022
+
1023
+ render() {
1024
+ return html`
1025
+ <div class="container">
1026
+ <div class="qr">
1027
+ <!-- Use QR code library or API -->
1028
+ <img src="https://api.qrserver.com/v1/create-qr-code/?size=256x256&data=${encodeURIComponent(this.pairingUrl)}" />
1029
+ </div>
1030
+ <div>Or enter setup code:</div>
1031
+ <div class="code">${this.setupCode}</div>
1032
+ <div class="instructions">
1033
+ Open Pool Bot mobile app → Settings → Pair Device → Scan QR or enter code
1034
+ </div>
1035
+ </div>
1036
+ `;
1037
+ }
1038
+ }
1039
+ ```
1040
+
1041
+ **Step 2: Create pairing code component**
1042
+
1043
+ ```typescript
1044
+ // ui/src/ui/components/device-pair-code.ts
1045
+ import { LitElement, html, css } from "lit";
1046
+ import { customElement, property } from "lit/decorators.js";
1047
+
1048
+ @customElement("device-pair-code")
1049
+ export class DevicePairCode extends LitElement {
1050
+ @property({ type: String }) code = "";
1051
+ @property({ type: Boolean }) loading = false;
1052
+ @property({ type: String }) error: string | null = null;
1053
+
1054
+ static styles = css`
1055
+ .form { max-width: 400px; margin: 0 auto; }
1056
+ .field { margin-bottom: 1rem; }
1057
+ label { display: block; margin-bottom: 0.5rem; }
1058
+ input { width: 100%; padding: 0.75rem; font-size: 1.25rem; letter-spacing: 0.25rem; }
1059
+ button {
1060
+ width: 100%;
1061
+ padding: 1rem;
1062
+ background: #1976d2;
1063
+ color: white;
1064
+ border: none;
1065
+ border-radius: 4px;
1066
+ }
1067
+ button:disabled { background: #ccc; }
1068
+ .error { color: #d32f2f; margin-top: 1rem; }
1069
+ `;
1070
+
1071
+ render() {
1072
+ return html`
1073
+ <form class="form" @submit=${(e: Event) => e.preventDefault()}>
1074
+ <div class="field">
1075
+ <label>Enter Setup Code from Device</label>
1076
+ <input
1077
+ .value=${this.code}
1078
+ @input=${(e: Event) => this.code = (e.target as HTMLInputElement).value.toUpperCase()}
1079
+ placeholder="ABC123"
1080
+ maxlength="6"
1081
+ />
1082
+ </div>
1083
+ ${this.error ? html`<div class="error">${this.error}</div>` : ""}
1084
+ <button
1085
+ type="submit"
1086
+ ?disabled=${this.loading || this.code.length !== 6}
1087
+ @click=${() => this.dispatchEvent(new CustomEvent("pair", { detail: this.code }))}
1088
+ >
1089
+ ${this.loading ? "Pairing..." : "Pair Device"}
1090
+ </button>
1091
+ </form>
1092
+ `;
1093
+ }
1094
+ }
1095
+ ```
1096
+
1097
+ **Step 3: Create device-pair view**
1098
+
1099
+ ```typescript
1100
+ // ui/src/ui/views/device-pair.ts
1101
+ import { LitElement, html, css } from "lit";
1102
+ import { customElement, state } from "lit/decorators.js";
1103
+ import "./components/device-pair-qr";
1104
+ import "./components/device-pair-code";
1105
+
1106
+ @customElement("device-pair")
1107
+ export class DevicePairView extends LitElement {
1108
+ @state() private pairingUrl = "";
1109
+ @state() private setupCode = "";
1110
+ @state() private loading = false;
1111
+ @state() private error: string | null = null;
1112
+ @state() private pairedDevices: Array<{ id: string; name: string; pairedAt: number }> = [];
1113
+
1114
+ static styles = css`
1115
+ .container { max-width: 800px; margin: 0 auto; padding: 2rem; }
1116
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 2rem; }
1117
+ .card { border: 1px solid #e0e0e0; padding: 1.5rem; border-radius: 8px; }
1118
+ h2 { margin-top: 0; }
1119
+ `;
1120
+
1121
+ async firstUpdated() {
1122
+ await this.generatePairingCode();
1123
+ await this.loadPairedDevices();
1124
+ }
1125
+
1126
+ async generatePairingCode() {
1127
+ this.loading = true;
1128
+ try {
1129
+ const response = await fetch("/api/device-pair/generate", { method: "POST" });
1130
+ const data = await response.json();
1131
+ this.pairingUrl = data.pairingUrl;
1132
+ this.setupCode = data.setupCode;
1133
+ } catch (e) {
1134
+ this.error = "Failed to generate pairing code";
1135
+ } finally {
1136
+ this.loading = false;
1137
+ }
1138
+ }
1139
+
1140
+ async loadPairedDevices() {
1141
+ const response = await fetch("/api/device-pair/list");
1142
+ this.pairedDevices = await response.json();
1143
+ }
1144
+
1145
+ async handlePair(e: CustomEvent) {
1146
+ this.loading = true;
1147
+ this.error = null;
1148
+ try {
1149
+ const response = await fetch("/api/device-pair/complete", {
1150
+ method: "POST",
1151
+ headers: { "Content-Type": "application/json" },
1152
+ body: JSON.stringify({ code: e.detail })
1153
+ });
1154
+ if (!response.ok) throw new Error("Pairing failed");
1155
+ await this.loadPairedDevices();
1156
+ await this.generatePairingCode();
1157
+ } catch (e) {
1158
+ this.error = e.message;
1159
+ } finally {
1160
+ this.loading = false;
1161
+ }
1162
+ }
1163
+
1164
+ render() {
1165
+ return html`
1166
+ <div class="container">
1167
+ <h2>Pair Device</h2>
1168
+ <div class="grid">
1169
+ <div class="card">
1170
+ <h3>Scan QR Code</h3>
1171
+ ${this.loading ? html`<div>Loading...</div>` : html`
1172
+ <device-pair-qr
1173
+ .pairingUrl=${this.pairingUrl}
1174
+ .setupCode=${this.setupCode}
1175
+ ></device-pair-qr>
1176
+ `}
1177
+ </div>
1178
+ <div class="card">
1179
+ <h3>Enter Code Manually</h3>
1180
+ <device-pair-code
1181
+ .loading=${this.loading}
1182
+ .error=${this.error}
1183
+ @pair=${this.handlePair}
1184
+ ></device-pair-code>
1185
+ </div>
1186
+ </div>
1187
+ <div class="card" style="margin-top: 2rem;">
1188
+ <h3>Paired Devices</h3>
1189
+ ${this.pairedDevices.length === 0 ? html`<div>No paired devices</div>` : html`
1190
+ <table>
1191
+ <thead><tr><th>Name</th><th>Paired</th><th>Actions</th></tr></thead>
1192
+ <tbody>
1193
+ ${this.pairedDevices.map(d => html`
1194
+ <tr>
1195
+ <td>${d.name}</td>
1196
+ <td>${new Date(d.pairedAt).toLocaleString()}</td>
1197
+ <td><button>Unpair</button></td>
1198
+ </tr>
1199
+ `)}
1200
+ </tbody>
1201
+ </table>
1202
+ `}
1203
+ </div>
1204
+ </div>
1205
+ `;
1206
+ }
1207
+ }
1208
+ ```
1209
+
1210
+ **Step 4: Create tests**
1211
+
1212
+ ```typescript
1213
+ // ui/src/ui/views/device-pair.test.ts
1214
+ import { describe, it, expect, vi } from "vitest";
1215
+ import { DevicePairView } from "./device-pair";
1216
+
1217
+ describe("DevicePairView", () => {
1218
+ it("generates pairing code on load", async () => {
1219
+ const view = new DevicePairView();
1220
+ global.fetch = vi.fn().mockResolvedValue({
1221
+ json: () => Promise.resolve({ pairingUrl: "ws://test", setupCode: "ABC123" })
1222
+ });
1223
+ await view.firstUpdated();
1224
+ expect(view.pairingUrl).toBe("ws://test");
1225
+ expect(view.setupCode).toBe("ABC123");
1226
+ });
1227
+
1228
+ it("completes pairing with code", async () => {
1229
+ const view = new DevicePairView();
1230
+ global.fetch = vi.fn()
1231
+ .mockResolvedValueOnce({ json: () => Promise.resolve({ pairingUrl: "", setupCode: "" }) })
1232
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]) });
1233
+ await view.firstUpdated();
1234
+ await view.handlePair({ detail: "ABC123" } as any);
1235
+ expect(global.fetch).toHaveBeenCalledWith("/api/device-pair/complete", expect.any(Object));
1236
+ });
1237
+ });
1238
+ ```
1239
+
1240
+ **Step 5: Run tests**
1241
+
1242
+ ```bash
1243
+ pnpm test ui/src/ui/views/device-pair.test.ts
1244
+ ```
1245
+ Expected: PASS
1246
+
1247
+ **Step 6: Commit**
1248
+
1249
+ ```bash
1250
+ git add ui/src/ui/views/device-pair.ts ui/src/ui/components/device-pair-*.ts
1251
+ git commit -m "feat(ui): add device pairing UI with QR and code support"
1252
+ ```
1253
+
1254
+ ---
1255
+
1256
+ ## PHASE 2: HIGH PRIORITY (Weeks 5-8)
1257
+
1258
+ ### Task 7: Refactor Overview into 6 Components
1259
+
1260
+ **Files:**
1261
+ - Create: `ui/src/ui/components/overview-gateway-status.ts`
1262
+ - Create: `ui/src/ui/components/overview-auth-form.ts`
1263
+ - Create: `ui/src/ui/components/overview-channels-status.ts`
1264
+ - Create: `ui/src/ui/components/overview-sessions-stats.ts`
1265
+ - Create: `ui/src/ui/components/overview-cron-status.ts`
1266
+ - Create: `ui/src/ui/components/overview-system-health.ts`
1267
+ - Modify: `ui/src/ui/views/overview.ts` (refactor to use components)
1268
+
1269
+ **Estimated Effort:** 16 hours (2 days)
1270
+
1271
+ **Success Criteria:**
1272
+ - Overview view < 200 LOC
1273
+ - Each component < 150 LOC
1274
+ - All components testable in isolation
1275
+ - No regression in functionality
1276
+
1277
+ **Implementation Steps:**
1278
+
1279
+ 1. Read current overview.ts to identify 6 logical sections
1280
+ 2. Extract each section into separate component
1281
+ 3. Define clear props/contracts for each component
1282
+ 4. Update overview.ts to compose components
1283
+ 5. Write tests for each component
1284
+ 6. Run full test suite to verify no regressions
1285
+
1286
+ ---
1287
+
1288
+ ### Task 8: Config Search Functionality
1289
+
1290
+ **Files:**
1291
+ - Modify: `ui/src/ui/views/config.ts` (add search input)
1292
+ - Create: `ui/src/ui/utils/config-search.ts`
1293
+ - Create: `ui/src/ui/utils/config-search.test.ts`
1294
+ - Modify: `src/config/config-store.ts` (add search method)
1295
+
1296
+ **Estimated Effort:** 8 hours (1 day)
1297
+
1298
+ **Success Criteria:**
1299
+ - Search filters config keys in real-time
1300
+ - Supports fuzzy matching
1301
+ - Highlights matched text
1302
+ - Works across nested config objects
1303
+
1304
+ ---
1305
+
1306
+ ### Task 9: Chrome MCP Snapshot
1307
+
1308
+ **Files:**
1309
+ - Create: `src/browser/chrome-mcp-snapshot.ts`
1310
+ - Create: `src/browser/chrome-mcp-snapshot.test.ts`
1311
+ - Modify: `src/browser/chrome.ts` (integrate snapshot)
1312
+
1313
+ **Dependencies:** Chrome DevTools Protocol (existing)
1314
+
1315
+ **Estimated Effort:** 12 hours (1.5 days)
1316
+
1317
+ **Success Criteria:**
1318
+ - Captures page state as structured snapshot
1319
+ - Includes DOM, network, console state
1320
+ - Snapshot serializable to JSON
1321
+ - Tests cover edge cases
1322
+
1323
+ ---
1324
+
1325
+ ### Task 10: Chrome Extension Validation
1326
+
1327
+ **Files:**
1328
+ - Create: `src/browser/chrome-extension-validator.ts`
1329
+ - Create: `src/browser/chrome-extension-validator.test.ts`
1330
+ - Modify: `assets/chrome-extension/manifest.json` (update if needed)
1331
+ - Create: `assets/chrome-extension/options.ts`
1332
+ - Create: `assets/chrome-extension/background.ts`
1333
+
1334
+ **Estimated Effort:** 10 hours (1.25 days)
1335
+
1336
+ **Success Criteria:**
1337
+ - Validates manifest.json structure
1338
+ - Validates options.html exists
1339
+ - Validates background.js/service worker
1340
+ - Provides clear error messages
1341
+
1342
+ ---
1343
+
1344
+ ### Task 11: Persistent Bindings (ACP)
1345
+
1346
+ **Files:**
1347
+ - Modify: `src/acp/translator.ts` (add bindings persistence)
1348
+ - Modify: `src/acp/session.ts` (store bindings)
1349
+ - Create: `src/acp/bindings-store.ts`
1350
+ - Create: `src/acp/bindings-store.test.ts`
1351
+
1352
+ **Estimated Effort:** 12 hours (1.5 days)
1353
+
1354
+ **Success Criteria:**
1355
+ - Bindings persist across restarts
1356
+ - Bindings scoped to session
1357
+ - Bindings validate on load
1358
+ - Tests cover persistence scenarios
1359
+
1360
+ ---
1361
+
1362
+ ### Task 12: Translator Features
1363
+
1364
+ **Files:**
1365
+ - Modify: `src/acp/translator.ts` (add prompt-prefix support)
1366
+ - Modify: `src/acp/translator.ts` (add cancel-scoping)
1367
+ - Create: `src/acp/translator.prompt-prefix.test.ts`
1368
+ - Create: `src/acp/translator.cancel-scoping.test.ts`
1369
+
1370
+ **Estimated Effort:** 10 hours (1.25 days)
1371
+
1372
+ **Success Criteria:**
1373
+ - prompt-prefix configurable per session
1374
+ - cancel-scoping prevents cascade cancels
1375
+ - Features documented in ACP docs
1376
+ - Tests cover edge cases
1377
+
1378
+ ---
1379
+
1380
+ ## PHASE 3: MEDIUM PRIORITY (Weeks 9-12)
1381
+
1382
+ ### Task 13: Google Chat Channel
1383
+
1384
+ **Files:**
1385
+ - Create: `extensions/googlechat/src/index.ts`
1386
+ - Create: `extensions/googlechat/src/googlechat-channel.ts`
1387
+ - Create: `extensions/googlechat/src/googlechat-auth.ts`
1388
+ - Create: `extensions/googlechat/package.json`
1389
+ - Test: `extensions/googlechat/src/*.test.ts`
1390
+
1391
+ **Estimated Effort:** 20 hours (2.5 days)
1392
+
1393
+ **Success Criteria:**
1394
+ - OAuth auth flow works
1395
+ - Sends/receives messages
1396
+ - Supports threads
1397
+ - Presence tracking
1398
+
1399
+ ---
1400
+
1401
+ ### Task 14: IRC Channel
1402
+
1403
+ **Files:**
1404
+ - Create: `extensions/irc/src/index.ts`
1405
+ - Create: `extensions/irc/src/irc-channel.ts`
1406
+ - Create: `extensions/irc/src/irc-connection.ts`
1407
+ - Create: `extensions/irc/package.json`
1408
+ - Test: `extensions/irc/src/*.test.ts`
1409
+
1410
+ **Estimated Effort:** 16 hours (2 days)
1411
+
1412
+ **Success Criteria:**
1413
+ - Connects to IRC servers
1414
+ - Joins channels
1415
+ - Sends/receives messages
1416
+ - Handles reconnects
1417
+
1418
+ ---
1419
+
1420
+ ### Task 15: Nostr Channel
1421
+
1422
+ **Files:**
1423
+ - Modify: `extensions/nostr/src/` (integrate as channel)
1424
+ - Create: `extensions/nostr/src/nostr-channel.ts`
1425
+ - Test: `extensions/nostr/src/nostr-channel.test.ts`
1426
+
1427
+ **Estimated Effort:** 16 hours (2 days)
1428
+
1429
+ **Success Criteria:**
1430
+ - Publishes to Nostr relays
1431
+ - Subscribes to events
1432
+ - Supports NIP-04 DMs
1433
+ - Profile management
1434
+
1435
+ ---
1436
+
1437
+ ### Task 16: Deadcode Detection
1438
+
1439
+ **Files:**
1440
+ - Modify: `package.json` (add scripts)
1441
+ - Create: `.knip.jsonc`
1442
+ - Create: `.ts-prune.jsonc`
1443
+ - Create: `.ts-unused.jsonc`
1444
+ - Modify: `.github/workflows/lint.yml` (add deadcode checks)
1445
+
1446
+ **Estimated Effort:** 6 hours (0.75 days)
1447
+
1448
+ **Success Criteria:**
1449
+ - `pnpm deadcode:knip` works
1450
+ - `pnpm deadcode:ts-prune` works
1451
+ - `pnpm deadcode:ts-unused` works
1452
+ - CI fails on deadcode
1453
+
1454
+ ---
1455
+
1456
+ ### Task 17: Protocol Generation for Swift
1457
+
1458
+ **Files:**
1459
+ - Create: `scripts/generate-swift-protocol.ts`
1460
+ - Create: `src/gateway/protocol/swift-generator.ts`
1461
+ - Create: `templates/swift/` (Swift templates)
1462
+ - Test: `scripts/generate-swift-protocol.test.ts`
1463
+
1464
+ **Estimated Effort:** 16 hours (2 days)
1465
+
1466
+ **Success Criteria:**
1467
+ - Generates Swift types from TypeScript
1468
+ - Generated code compiles
1469
+ - Round-trip validation
1470
+ - Integrated in build process
1471
+
1472
+ ---
1473
+
1474
+ ### Task 18: Advanced Cron Filters
1475
+
1476
+ **Files:**
1477
+ - Modify: `src/cron/cron-scheduler.ts` (add filters)
1478
+ - Modify: `ui/src/ui/views/cron.ts` (add filter UI)
1479
+ - Create: `src/cron/cron-filters.ts`
1480
+ - Create: `src/cron/cron-filters.test.ts`
1481
+
1482
+ **Estimated Effort:** 12 hours (1.5 days)
1483
+
1484
+ **Success Criteria:**
1485
+ - Filters by channel
1486
+ - Filters by user
1487
+ - Filters by session
1488
+ - UI for filter management
1489
+
1490
+ ---
1491
+
1492
+ ### Task 19: Session Cost Types
1493
+
1494
+ **Files:**
1495
+ - Modify: `src/config/sessions/types.ts` (add cost types)
1496
+ - Modify: `src/gateway/server-methods/sessions.ts` (track costs)
1497
+ - Modify: `ui/src/ui/views/sessions.ts` (show costs)
1498
+ - Create: `src/usage/session-costs.ts`
1499
+ - Create: `src/usage/session-costs.test.ts`
1500
+
1501
+ **Estimated Effort:** 10 hours (1.25 days)
1502
+
1503
+ **Success Criteria:**
1504
+ - Tracks token costs
1505
+ - Tracks time costs
1506
+ - Shows cost breakdown
1507
+ - Exportable cost reports
1508
+
1509
+ ---
1510
+
1511
+ ## PHASE 4: LOW PRIORITY (Weeks 13+)
1512
+
1513
+ ### Task 20: Additional Specialized Plugins
1514
+
1515
+ **Files:**
1516
+ - Create: `extensions/phone-control/` (4-6h)
1517
+ - Create: `extensions/llm-task/` (3-4h)
1518
+ - Create: `extensions/thread-ownership/` (3-4h)
1519
+ - Create: `extensions/open-prose/` (2-3h)
1520
+
1521
+ **Estimated Effort:** 14 hours (1.75 days)
1522
+
1523
+ **Success Criteria:**
1524
+ - Each plugin has README
1525
+ - Each plugin has tests
1526
+ - Plugins loadable via plugin system
1527
+ - Documented in plugin docs
1528
+
1529
+ ---
1530
+
1531
+ ### Task 21: Test Suite Expansion
1532
+
1533
+ **Files:**
1534
+ - Create: `vitest.channels.config.ts` (2h)
1535
+ - Create: `vitest.extensions.config.ts` (2h)
1536
+ - Create: `test:channels` script (1h)
1537
+ - Create: `test:extensions` script (1h)
1538
+ - Create: E2E test variants (4h)
1539
+
1540
+ **Estimated Effort:** 10 hours (1.25 days)
1541
+
1542
+ **Success Criteria:**
1543
+ - All test configs work
1544
+ - Scripts run correct tests
1545
+ - E2E tests cover key flows
1546
+ - CI runs all test suites
1547
+
1548
+ ---
1549
+
1550
+ ### Task 22: Specialized Lint Checks
1551
+
1552
+ **Files:**
1553
+ - Modify: `package.json` (add lint scripts)
1554
+ - Modify: `.oxlintrc.json` (add rules)
1555
+ - Create: `scripts/lint-security.ts` (4h)
1556
+ - Create: `scripts/lint-performance.ts` (4h)
1557
+
1558
+ **Estimated Effort:** 10 hours (1.25 days)
1559
+
1560
+ **Success Criteria:**
1561
+ - Security lint checks run
1562
+ - Performance lint checks run
1563
+ - CI fails on violations
1564
+ - Clear error messages
1565
+
1566
+ ---
1567
+
1568
+ ### Task 23: Channel Documentation Expansion
1569
+
1570
+ **Files:**
1571
+ - Create: `docs/channels/google-chat.md` (2h)
1572
+ - Create: `docs/channels/irc.md` (2h)
1573
+ - Create: `docs/channels/nostr.md` (2h)
1574
+ - Create: `docs/channels/line.md` (2h)
1575
+ - Create: `docs/channels/zalo.md` (2h)
1576
+
1577
+ **Estimated Effort:** 10 hours (1.25 days)
1578
+
1579
+ **Success Criteria:**
1580
+ - Each channel has setup guide
1581
+ - Each channel has config examples
1582
+ - Each channel has troubleshooting
1583
+ - Screenshots where applicable
1584
+
1585
+ ---
1586
+
1587
+ ## SUMMARY
1588
+
1589
+ | Phase | Tasks | Total Hours | Weeks |
1590
+ |-------|-------|-------------|-------|
1591
+ | **Phase 1 (Critical)** | 6 | 72h | 4 |
1592
+ | **Phase 2 (High)** | 6 | 66h | 4 |
1593
+ | **Phase 3 (Medium)** | 7 | 98h | 4 |
1594
+ | **Phase 4 (Low)** | 4 | 44h | 2+ |
1595
+ | **TOTAL** | **23** | **280h** | **14-16 weeks** |
1596
+
1597
+ ---
1598
+
1599
+ ## TESTING REQUIREMENTS
1600
+
1601
+ Every task must include:
1602
+ 1. Unit tests for new components/functions
1603
+ 2. Integration tests where applicable
1604
+ 3. Run `pnpm test` before commit
1605
+ 4. Coverage > 80% for new code
1606
+
1607
+ ## DOCUMENTATION REQUIREMENTS
1608
+
1609
+ Every user-facing feature must include:
1610
+ 1. Update to relevant docs/*.md file
1611
+ 2. Changelog entry (if user-facing)
1612
+ 3. Code comments for complex logic
1613
+ 4. JSDoc for public APIs
1614
+
1615
+ ## COMMIT GUIDELINES
1616
+
1617
+ - One commit per task (or sub-task for large tasks)
1618
+ - Conventional Commits format
1619
+ - Include test files in same commit
1620
+ - Keep commits under 500 LOC when possible
1621
+
1622
+ ---
1623
+
1624
+ **Plan complete and saved to `docs/plans/2026-03-15-openclaw-features-implementation.md`. Two execution options:**
1625
+
1626
+ **1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
1627
+
1628
+ **2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
1629
+
1630
+ **Which approach?**
1631
+
1632
+ https://docs.molt.bot/plans/2026-03-15-openclaw-features-implementation