docs: add implementation plan for HTTP compatibility fix

7 tasks covering generateId() utility, copyToClipboard() utility,
replacement of all 10 call sites, and final verification.
This commit is contained in:
Siddharth Kumar Sah
2026-04-04 16:19:23 +08:00
parent 2422baec30
commit 2fc42ebd14
@@ -0,0 +1,494 @@
# HTTP/Non-Secure Context Compatibility - Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make all frontend features work over plain HTTP on non-localhost addresses (LAN/NAS deployments).
**Architecture:** Add `generateId()` and `copyToClipboard()` utilities to `apps/web/src/lib/utils.ts`, then replace all 10 call sites across 7 files. Test the utilities with Vitest.
**Tech Stack:** TypeScript, Vitest (jsdom), `crypto.getRandomValues()`, `document.execCommand("copy")`
**Spec:** `docs/superpowers/specs/2026-04-04-http-compatibility-design.md`
---
### Task 1: Add `generateId()` utility and test
**Files:**
- Modify: `apps/web/src/lib/utils.ts`
- Create: `tests/unit/web/utils.test.ts`
- [ ] **Step 1: Write the test file**
Create `tests/unit/web/utils.test.ts`:
```ts
// @vitest-environment jsdom
import { describe, expect, it } from "vitest";
import { generateId } from "../../../apps/web/src/lib/utils";
describe("generateId", () => {
it("returns a valid UUID v4 string", () => {
const id = generateId();
expect(id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
});
it("returns unique values on successive calls", () => {
const ids = new Set(Array.from({ length: 100 }, () => generateId()));
expect(ids.size).toBe(100);
});
});
```
- [ ] **Step 2: Run test to verify it fails**
Run: `pnpm test:unit -- tests/unit/web/utils.test.ts`
Expected: FAIL - `generateId` is not exported from utils.
- [ ] **Step 3: Implement `generateId` in utils.ts**
Add to `apps/web/src/lib/utils.ts` after the existing `cn` function:
```ts
export function generateId(): string {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `pnpm test:unit -- tests/unit/web/utils.test.ts`
Expected: PASS (2 tests)
- [ ] **Step 5: Commit**
```bash
git add apps/web/src/lib/utils.ts tests/unit/web/utils.test.ts
git commit -m "feat: add generateId() utility for non-secure context compatibility"
```
---
### Task 2: Add `copyToClipboard()` utility and test
**Files:**
- Modify: `apps/web/src/lib/utils.ts`
- Modify: `tests/unit/web/utils.test.ts`
- [ ] **Step 1: Write the tests**
Append to `tests/unit/web/utils.test.ts`:
```ts
import { afterEach, describe, expect, it, vi } from "vitest";
import { copyToClipboard } from "../../../apps/web/src/lib/utils";
describe("copyToClipboard", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("returns true when clipboard API succeeds", async () => {
Object.assign(navigator, {
clipboard: { writeText: vi.fn().mockResolvedValue(undefined) },
});
expect(await copyToClipboard("hello")).toBe(true);
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("hello");
});
it("falls back to execCommand when clipboard API fails", async () => {
Object.assign(navigator, { clipboard: undefined });
const execCommand = vi.spyOn(document, "execCommand").mockReturnValue(true);
expect(await copyToClipboard("hello")).toBe(true);
expect(execCommand).toHaveBeenCalledWith("copy");
});
it("returns false when both approaches fail", async () => {
Object.assign(navigator, { clipboard: undefined });
vi.spyOn(document, "execCommand").mockImplementation(() => {
throw new Error("not supported");
});
expect(await copyToClipboard("hello")).toBe(false);
});
});
```
Note: update the `import` line at the top of the file to also import `afterEach` and `vi` alongside the existing `describe, expect, it`, and import `copyToClipboard` from the same utils path.
- [ ] **Step 2: Run test to verify it fails**
Run: `pnpm test:unit -- tests/unit/web/utils.test.ts`
Expected: FAIL - `copyToClipboard` is not exported from utils.
- [ ] **Step 3: Implement `copyToClipboard` in utils.ts**
Add to `apps/web/src/lib/utils.ts` after `generateId`:
```ts
export async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
try {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
const ok = document.execCommand("copy");
document.body.removeChild(textarea);
return ok;
} catch {
return false;
}
}
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `pnpm test:unit -- tests/unit/web/utils.test.ts`
Expected: PASS (5 tests)
- [ ] **Step 5: Commit**
```bash
git add apps/web/src/lib/utils.ts tests/unit/web/utils.test.ts
git commit -m "feat: add copyToClipboard() utility with execCommand fallback"
```
---
### Task 3: Replace `crypto.randomUUID()` in `use-tool-processor.ts`
**Files:**
- Modify: `apps/web/src/hooks/use-tool-processor.ts`
- [ ] **Step 1: Add import**
Add `generateId` to imports at the top of `apps/web/src/hooks/use-tool-processor.ts`:
```ts
import { generateId } from "@/lib/utils";
```
- [ ] **Step 2: Replace line 91**
Change:
```ts
const clientJobId = crypto.randomUUID();
```
To:
```ts
const clientJobId = generateId();
```
- [ ] **Step 3: Replace line 260**
Change:
```ts
const clientJobId = crypto.randomUUID();
```
To:
```ts
const clientJobId = generateId();
```
- [ ] **Step 4: Verify no remaining `crypto.randomUUID` references**
Run: `grep -n "crypto.randomUUID" apps/web/src/hooks/use-tool-processor.ts`
Expected: No output.
- [ ] **Step 5: Run typecheck**
Run: `pnpm typecheck`
Expected: PASS
- [ ] **Step 6: Commit**
```bash
git add apps/web/src/hooks/use-tool-processor.ts
git commit -m "fix: replace crypto.randomUUID with generateId in use-tool-processor"
```
---
### Task 4: Replace `crypto.randomUUID()` in AI tool settings
**Files:**
- Modify: `apps/web/src/components/tools/ocr-settings.tsx`
- Modify: `apps/web/src/components/tools/erase-object-settings.tsx`
- [ ] **Step 1: Update `ocr-settings.tsx`**
Add import at the top:
```ts
import { generateId } from "@/lib/utils";
```
Replace line 52:
```ts
const clientJobId = crypto.randomUUID();
```
With:
```ts
const clientJobId = generateId();
```
- [ ] **Step 2: Update `erase-object-settings.tsx`**
Add import at the top:
```ts
import { generateId } from "@/lib/utils";
```
Replace line 52:
```ts
const clientJobId = crypto.randomUUID();
```
With:
```ts
const clientJobId = generateId();
```
- [ ] **Step 3: Run typecheck**
Run: `pnpm typecheck`
Expected: PASS
- [ ] **Step 4: Commit**
```bash
git add apps/web/src/components/tools/ocr-settings.tsx apps/web/src/components/tools/erase-object-settings.tsx
git commit -m "fix: replace crypto.randomUUID with generateId in AI tool settings"
```
---
### Task 5: Replace `crypto.randomUUID()` in pipeline/automation
**Files:**
- Modify: `apps/web/src/components/tools/pipeline-builder.tsx`
- Modify: `apps/web/src/pages/automate-page.tsx`
- [ ] **Step 1: Update `pipeline-builder.tsx`**
Add import at the top:
```ts
import { generateId } from "@/lib/utils";
```
Replace line 104:
```ts
id: crypto.randomUUID(),
```
With:
```ts
id: generateId(),
```
- [ ] **Step 2: Update `automate-page.tsx`**
Add import at the top:
```ts
import { generateId } from "@/lib/utils";
```
Replace line 147:
```ts
id: crypto.randomUUID(),
```
With:
```ts
id: generateId(),
```
- [ ] **Step 3: Run typecheck**
Run: `pnpm typecheck`
Expected: PASS
- [ ] **Step 4: Commit**
```bash
git add apps/web/src/components/tools/pipeline-builder.tsx apps/web/src/pages/automate-page.tsx
git commit -m "fix: replace crypto.randomUUID with generateId in pipeline/automation"
```
---
### Task 6: Replace `navigator.clipboard` in all 4 call sites
**Files:**
- Modify: `apps/web/src/components/settings/settings-dialog.tsx`
- Modify: `apps/web/src/components/tools/color-palette-settings.tsx`
- Modify: `apps/web/src/components/tools/ocr-settings.tsx`
- Modify: `apps/web/src/components/tools/barcode-read-settings.tsx`
- [ ] **Step 1: Update `settings-dialog.tsx`**
Add `copyToClipboard` to imports (the file already imports from `@/lib/utils` if `cn` is used, otherwise add a new import):
```ts
import { copyToClipboard } from "@/lib/utils";
```
Replace lines 1147-1152:
```ts
const copyKey = useCallback((key: string) => {
navigator.clipboard.writeText(key).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
}, []);
```
With:
```ts
const copyKey = useCallback(async (key: string) => {
const ok = await copyToClipboard(key);
if (ok) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}, []);
```
- [ ] **Step 2: Update `color-palette-settings.tsx`**
Add import:
```ts
import { copyToClipboard } from "@/lib/utils";
```
Replace lines 45-53:
```ts
const copyColor = async (color: string, idx: number) => {
try {
await navigator.clipboard.writeText(color);
setCopiedIdx(idx);
setTimeout(() => setCopiedIdx(null), 1500);
} catch {
// Fallback: silent fail
}
};
```
With:
```ts
const copyColor = async (color: string, idx: number) => {
const ok = await copyToClipboard(color);
if (ok) {
setCopiedIdx(idx);
setTimeout(() => setCopiedIdx(null), 1500);
}
};
```
- [ ] **Step 3: Update `ocr-settings.tsx`**
This file already has a `generateId` import from Task 4. Add `copyToClipboard` to the same import:
```ts
import { copyToClipboard, generateId } from "@/lib/utils";
```
Replace lines 118-124:
```ts
const handleCopy = async () => {
if (text) {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
```
With:
```ts
const handleCopy = async () => {
if (text) {
const ok = await copyToClipboard(text);
if (ok) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}
};
```
- [ ] **Step 4: Update `barcode-read-settings.tsx`**
Add import:
```ts
import { copyToClipboard } from "@/lib/utils";
```
Replace lines 45-54:
```ts
const copyText = async () => {
if (!result?.text) return;
try {
await navigator.clipboard.writeText(result.text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// Fallback: silent fail
}
};
```
With:
```ts
const copyText = async () => {
if (!result?.text) return;
const ok = await copyToClipboard(result.text);
if (ok) {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
};
```
- [ ] **Step 5: Run typecheck**
Run: `pnpm typecheck`
Expected: PASS
- [ ] **Step 6: Commit**
```bash
git add apps/web/src/components/settings/settings-dialog.tsx apps/web/src/components/tools/color-palette-settings.tsx apps/web/src/components/tools/ocr-settings.tsx apps/web/src/components/tools/barcode-read-settings.tsx
git commit -m "fix: replace navigator.clipboard with copyToClipboard utility"
```
---
### Task 7: Final verification
- [ ] **Step 1: Verify no remaining direct usages**
Run: `grep -rn "crypto\.randomUUID\|navigator\.clipboard" apps/web/src/`
Expected: No output.
- [ ] **Step 2: Run full lint**
Run: `pnpm lint`
Expected: PASS
- [ ] **Step 3: Run full test suite**
Run: `pnpm test`
Expected: All existing tests pass, plus the 5 new tests in `utils.test.ts`.
- [ ] **Step 4: Commit any lint fixes if needed**
```bash
git add -A
git commit -m "fix: lint fixes for http compatibility changes"
```
(Skip if lint passed cleanly.)