Files
SnapOtter/docs/superpowers/specs/2026-04-04-http-compatibility-design.md
T
Siddharth Kumar Sah 2422baec30 docs: add execCommand fallback to clipboard utility in design spec
Without the fallback, every Copy button is dead on HTTP. The
execCommand approach is deprecated but works in all current browsers
and does not require a secure context.
2026-04-04 16:17:11 +08:00

101 lines
4.3 KiB
Markdown

# Fix HTTP/Non-Secure Context Compatibility
**Date:** 2026-04-04
**Issues:** [#4](https://github.com/stirling-image/stirling-image/issues/4), [#5](https://github.com/stirling-image/stirling-image/issues/5)
## Problem
`crypto.randomUUID()` and `navigator.clipboard.writeText()` are secure-context-only Web APIs. They throw or are undefined when the app is accessed over plain HTTP on non-localhost addresses (e.g., `http://192.168.1.x:1349`). This breaks all tool operations and copy-to-clipboard functionality.
Stirling-Image is a self-hosted tool where many users deploy on NAS/homelab devices over plain HTTP. This must be a first-class supported deployment mode.
## Solution
### Part 1: `generateId()` utility
Add to `apps/web/src/lib/utils.ts`:
```ts
export function generateId(): string {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10
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)}`;
}
```
Produces a standard UUID v4 string using `crypto.getRandomValues()`, which is available in all modern browsers regardless of secure context.
**6 call sites replaced** (`crypto.randomUUID()` -> `generateId()`):
| File | Line | Purpose |
|------|------|---------|
| `apps/web/src/hooks/use-tool-processor.ts` | 91 | Single-file job correlation ID |
| `apps/web/src/hooks/use-tool-processor.ts` | 260 | Batch job correlation ID |
| `apps/web/src/components/tools/ocr-settings.tsx` | 52 | OCR job ID |
| `apps/web/src/components/tools/erase-object-settings.tsx` | 52 | Erase object job ID |
| `apps/web/src/components/tools/pipeline-builder.tsx` | 104 | Pipeline step ID |
| `apps/web/src/pages/automate-page.tsx` | 147 | Automation step ID |
### Part 2: `copyToClipboard()` utility
Add to `apps/web/src/lib/utils.ts`:
```ts
export async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Fallback for non-secure contexts (HTTP on LAN)
// document.execCommand is deprecated but works in all current browsers
// and does not require a secure context
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;
}
}
}
```
Tries the Clipboard API first (works on HTTPS/localhost). Falls back to `document.execCommand("copy")` which is deprecated but works in all current browsers including non-secure contexts. This is the standard clipboard compatibility pattern used by GitHub, Stack Overflow, etc. Returns `true`/`false` so callers can decide whether to show a "Copied!" confirmation. Only returns `false` if both approaches fail.
**4 call sites replaced** (`navigator.clipboard.writeText()` -> `copyToClipboard()`):
| File | Line | Current error handling |
|------|------|-----------------------|
| `apps/web/src/components/settings/settings-dialog.tsx` | 1148 | None - needs wrapping |
| `apps/web/src/components/tools/color-palette-settings.tsx` | 47 | Has try-catch, simplify |
| `apps/web/src/components/tools/ocr-settings.tsx` | 120 | None - needs wrapping |
| `apps/web/src/components/tools/barcode-read-settings.tsx` | 48 | Has try-catch, simplify |
## What does NOT change
- Server-side code: Node's `crypto.randomUUID()` works fine outside browsers
- `vitest.config.ts`: Runs in Node, not a browser
- No new dependencies added
- No config file changes
## Testing
- `pnpm typecheck` - verify all imports resolve
- `pnpm lint` - verify Biome formatting
- `pnpm test` - catch regressions
- Manual: access over HTTP on a non-localhost address, confirm tools work
## Alternatives considered
1. **`uuid` npm package** - rejected, adds a dependency for 5 lines of code that does the same thing internally
2. **Try-catch fallback wrapper** (try native `randomUUID()`, fall back to `getRandomValues()`) - rejected, two code paths for zero meaningful performance benefit