7 tasks covering generateId() utility, copyToClipboard() utility, replacement of all 10 call sites, and final verification.
12 KiB
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:
// @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
generateIdin utils.ts
Add to apps/web/src/lib/utils.ts after the existing cn function:
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
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:
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
copyToClipboardin utils.ts
Add to apps/web/src/lib/utils.ts after generateId:
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
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:
import { generateId } from "@/lib/utils";
- Step 2: Replace line 91
Change:
const clientJobId = crypto.randomUUID();
To:
const clientJobId = generateId();
- Step 3: Replace line 260
Change:
const clientJobId = crypto.randomUUID();
To:
const clientJobId = generateId();
- Step 4: Verify no remaining
crypto.randomUUIDreferences
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
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:
import { generateId } from "@/lib/utils";
Replace line 52:
const clientJobId = crypto.randomUUID();
With:
const clientJobId = generateId();
- Step 2: Update
erase-object-settings.tsx
Add import at the top:
import { generateId } from "@/lib/utils";
Replace line 52:
const clientJobId = crypto.randomUUID();
With:
const clientJobId = generateId();
- Step 3: Run typecheck
Run: pnpm typecheck
Expected: PASS
- Step 4: Commit
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:
import { generateId } from "@/lib/utils";
Replace line 104:
id: crypto.randomUUID(),
With:
id: generateId(),
- Step 2: Update
automate-page.tsx
Add import at the top:
import { generateId } from "@/lib/utils";
Replace line 147:
id: crypto.randomUUID(),
With:
id: generateId(),
- Step 3: Run typecheck
Run: pnpm typecheck
Expected: PASS
- Step 4: Commit
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):
import { copyToClipboard } from "@/lib/utils";
Replace lines 1147-1152:
const copyKey = useCallback((key: string) => {
navigator.clipboard.writeText(key).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
}, []);
With:
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:
import { copyToClipboard } from "@/lib/utils";
Replace lines 45-53:
const copyColor = async (color: string, idx: number) => {
try {
await navigator.clipboard.writeText(color);
setCopiedIdx(idx);
setTimeout(() => setCopiedIdx(null), 1500);
} catch {
// Fallback: silent fail
}
};
With:
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:
import { copyToClipboard, generateId } from "@/lib/utils";
Replace lines 118-124:
const handleCopy = async () => {
if (text) {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
With:
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:
import { copyToClipboard } from "@/lib/utils";
Replace lines 45-54:
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:
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
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
git add -A
git commit -m "fix: lint fixes for http compatibility changes"
(Skip if lint passed cleanly.)