@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.
- package/CHANGELOG.md +111 -0
- package/dist/.buildstamp +1 -1
- package/dist/acp/bindings-store.js +209 -0
- package/dist/acp/control-plane/runtime-cache.js +54 -0
- package/dist/acp/control-plane/runtime-options.js +215 -0
- package/dist/acp/control-plane/session-actor-queue.js +36 -0
- package/dist/acp/policy.js +52 -0
- package/dist/acp/runtime/errors.js +47 -0
- package/dist/acp/runtime/registry.js +86 -0
- package/dist/acp/runtime/types.js +1 -0
- package/dist/acp/translator.js +97 -0
- package/dist/agents/btw.js +280 -0
- package/dist/agents/failover-error.js +145 -47
- package/dist/agents/fast-mode.js +24 -0
- package/dist/agents/live-model-errors.js +23 -0
- package/dist/agents/model-auth-env-vars.js +44 -0
- package/dist/agents/model-auth-markers.js +69 -0
- package/dist/agents/models-config.providers.discovery.js +180 -0
- package/dist/agents/models-config.providers.static.js +480 -0
- package/dist/auto-reply/reply/typing-policy.js +15 -0
- package/dist/browser/browser-profile-manager.js +319 -0
- package/dist/browser/cdp-proxy-bypass.js +129 -0
- package/dist/browser/cdp-timeouts.js +41 -0
- package/dist/browser/chrome-extension-validator.js +406 -0
- package/dist/browser/chrome-mcp-snapshot.js +222 -0
- package/dist/browser/chrome-mcp.js +421 -0
- package/dist/browser/chrome-mcp.snapshot.js +133 -0
- package/dist/browser/errors.js +67 -0
- package/dist/browser/form-fields.js +22 -0
- package/dist/browser/output-atomic.js +44 -0
- package/dist/browser/profile-capabilities.js +47 -0
- package/dist/browser/safe-filename.js +25 -0
- package/dist/browser/snapshot-roles.js +60 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/account-snapshot-fields.js +176 -0
- package/dist/channels/draft-stream-controls.js +89 -0
- package/dist/channels/inbound-debounce-policy.js +28 -0
- package/dist/channels/typing-lifecycle.js +39 -0
- package/dist/cli/program/command-registry.js +52 -0
- package/dist/commands/agent-binding.js +123 -0
- package/dist/commands/agents.commands.bind.js +280 -0
- package/dist/commands/backup-shared.js +186 -0
- package/dist/commands/backup-verify.js +236 -0
- package/dist/commands/backup.js +166 -0
- package/dist/commands/channel-account-context.js +15 -0
- package/dist/commands/channel-account.js +190 -0
- package/dist/commands/gateway-install-token.js +117 -0
- package/dist/commands/oauth-tls-preflight.js +121 -0
- package/dist/commands/ollama-setup.js +402 -0
- package/dist/commands/security-owner-only.js +86 -0
- package/dist/commands/self-hosted-provider-setup.js +207 -0
- package/dist/commands/session-store-targets.js +12 -0
- package/dist/commands/sessions-cleanup.js +97 -0
- package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
- package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/cron/cron-filters.js +150 -0
- package/dist/cron/heartbeat-policy.js +26 -0
- package/dist/gateway/device-pairing-security.js +197 -0
- package/dist/gateway/event-deduplication.js +167 -0
- package/dist/gateway/hooks-mapping.js +46 -7
- package/dist/gateway/run-tracker.js +253 -0
- package/dist/gateway/server-methods/nodes.js +14 -0
- package/dist/gateway/websocket-preauth-security.js +188 -0
- package/dist/hooks/module-loader.js +28 -0
- package/dist/infra/agent-command-binding.js +144 -0
- package/dist/infra/backup.js +328 -0
- package/dist/infra/channel-account-context.js +173 -0
- package/dist/infra/errors.js +53 -13
- package/dist/infra/exec-approvals-security.js +217 -0
- package/dist/infra/security/command-analyzer.js +257 -0
- package/dist/infra/session-cleanup.js +143 -0
- package/dist/plugins/loader.js +16 -8
- package/dist/security/external-content.js +51 -1
- package/dist/sessions/session-costs.js +228 -0
- package/dist/shared/param-key.js +16 -0
- package/dist/shared/poll-params.js +58 -0
- package/dist/shared/polls.js +55 -0
- package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
- package/docs/FEATURES.md +523 -0
- package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
- package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
- package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
- package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
- package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
- package/docs/MIKRODASH-ANALYSIS.md +412 -0
- package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
- package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
- package/docs/PHASE-7-SUMMARY.md +144 -0
- package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
- package/docs/PROJECT-FINAL-STATUS.md +237 -0
- package/docs/README.md +116 -0
- package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
- package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
- package/docs/channels/googlechat.md +235 -206
- package/docs/channels/irc.md +332 -0
- package/docs/channels/nostr.md +255 -168
- package/docs/components/command-palette.md +166 -0
- package/docs/components/login-gate.md +219 -0
- package/docs/getting-started/installation.md +191 -0
- package/docs/getting-started/introduction.md +120 -0
- package/docs/improvements/USAGE-GUIDE.md +359 -0
- package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
- package/docs/reference/deadcode-detection.md +72 -0
- package/extensions/acpx/node_modules/.bin/acpx +21 -0
- package/extensions/agency-agents/node_modules/.bin/vite +4 -4
- package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
- package/extensions/googlechat/node_modules/.bin/tsc +21 -0
- package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
- package/extensions/googlechat/node_modules/.bin/vitest +21 -0
- package/extensions/googlechat/package.json +11 -28
- package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
- package/extensions/googlechat/src/googlechat-channel.ts +120 -0
- package/extensions/googlechat/src/index.ts +14 -0
- package/extensions/irc/node_modules/.bin/tsc +21 -0
- package/extensions/irc/node_modules/.bin/tsserver +21 -0
- package/extensions/irc/node_modules/.bin/vitest +21 -0
- package/extensions/irc/package.json +16 -8
- package/extensions/irc/src/index.ts +14 -0
- package/extensions/irc/src/irc-channel.test.ts +43 -0
- package/extensions/irc/src/irc-channel.ts +191 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
- package/extensions/keyed-async-queue/package.json +20 -0
- package/extensions/keyed-async-queue/src/index.ts +14 -0
- package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
- package/extensions/keyed-async-queue/src/queue.ts +200 -0
- package/extensions/memory-core/node_modules/.bin/tsc +21 -0
- package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
- package/extensions/memory-core/node_modules/.bin/vitest +21 -0
- package/extensions/memory-core/package.json +11 -8
- package/extensions/memory-core/src/index.ts +14 -0
- package/extensions/memory-core/src/memory-manager.test.ts +124 -0
- package/extensions/memory-core/src/memory-manager.ts +186 -0
- package/extensions/nostr/node_modules/.bin/tsc +2 -2
- package/extensions/nostr/node_modules/.bin/tsserver +2 -2
- package/extensions/nostr/node_modules/.bin/vitest +21 -0
- package/extensions/nostr/package.json +15 -24
- package/extensions/nostr/src/index.ts +14 -0
- package/extensions/nostr/src/nostr-channel.test.ts +55 -0
- package/extensions/nostr/src/nostr-channel.ts +228 -0
- package/extensions/page-agent/node_modules/.bin/vitest +2 -2
- package/extensions/test-utils/node_modules/.bin/jiti +21 -0
- package/extensions/test-utils/node_modules/.bin/playwright +21 -0
- package/extensions/test-utils/node_modules/.bin/tsx +21 -0
- package/extensions/test-utils/node_modules/.bin/vite +21 -0
- package/extensions/test-utils/node_modules/.bin/vitest +21 -0
- package/extensions/test-utils/node_modules/.bin/yaml +21 -0
- package/extensions/xyops/node_modules/.bin/vitest +2 -2
- package/package.json +2 -1
- package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
- package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
- 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
|