Files
SnapOtter/docs/superpowers/plans/2026-04-04-http-compatibility.md
T
Siddharth Kumar Sah 2fc42ebd14 docs: add implementation plan for HTTP compatibility fix
7 tasks covering generateId() utility, copyToClipboard() utility,
replacement of all 10 call sites, and final verification.
2026-04-04 16:19:23 +08:00

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 generateId in 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 copyToClipboard in 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.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
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.)