diff --git a/apps/ui/src/components/views/analysis-view.tsx b/apps/ui/src/components/views/analysis-view.tsx index f551e0a8..e235a9e9 100644 --- a/apps/ui/src/components/views/analysis-view.tsx +++ b/apps/ui/src/components/views/analysis-view.tsx @@ -20,7 +20,7 @@ import { AlertCircle, ListChecks, } from 'lucide-react'; -import { cn } from '@/lib/utils'; +import { cn, generateUUID } from '@/lib/utils'; const logger = createLogger('AnalysisView'); @@ -638,7 +638,7 @@ ${Object.entries(projectAnalysis.filesByExtension) for (const detectedFeature of detectedFeatures) { await api.features.create(currentProject.path, { - id: crypto.randomUUID(), + id: generateUUID(), category: detectedFeature.category, description: detectedFeature.description, status: 'backlog', diff --git a/apps/ui/src/components/views/github-issues-view.tsx b/apps/ui/src/components/views/github-issues-view.tsx index 03275b02..0ae6e1e8 100644 --- a/apps/ui/src/components/views/github-issues-view.tsx +++ b/apps/ui/src/components/views/github-issues-view.tsx @@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { LoadingState } from '@/components/ui/loading-state'; import { ErrorState } from '@/components/ui/error-state'; -import { cn, pathsEqual } from '@/lib/utils'; +import { cn, pathsEqual, generateUUID } from '@/lib/utils'; import { toast } from 'sonner'; import { useGithubIssues, useIssueValidation, useIssuesFilter } from './github-issues-view/hooks'; import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components'; @@ -137,7 +137,7 @@ export function GitHubIssuesView() { .join('\n'); const feature = { - id: `issue-${issue.number}-${crypto.randomUUID()}`, + id: `issue-${issue.number}-${generateUUID()}`, title: issue.title, description, category: 'From GitHub', diff --git a/apps/ui/src/components/views/interview-view.tsx b/apps/ui/src/components/views/interview-view.tsx index 75d8ab27..b9b9997e 100644 --- a/apps/ui/src/components/views/interview-view.tsx +++ b/apps/ui/src/components/views/interview-view.tsx @@ -6,7 +6,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Bot, Send, User, Loader2, Sparkles, FileText, ArrowLeft, CheckCircle } from 'lucide-react'; -import { cn } from '@/lib/utils'; +import { cn, generateUUID } from '@/lib/utils'; import { getElectronAPI } from '@/lib/electron'; import { Markdown } from '@/components/ui/markdown'; import { useFileBrowser } from '@/contexts/file-browser-context'; @@ -345,7 +345,7 @@ export function InterviewView() { // Create initial feature in the features folder const initialFeature: Feature = { - id: crypto.randomUUID(), + id: generateUUID(), category: 'Core', description: 'Initial project setup', status: 'backlog' as const, diff --git a/apps/ui/src/components/views/settings-view/event-hooks/event-hook-dialog.tsx b/apps/ui/src/components/views/settings-view/event-hooks/event-hook-dialog.tsx index 68233b5a..3997d424 100644 --- a/apps/ui/src/components/views/settings-view/event-hooks/event-hook-dialog.tsx +++ b/apps/ui/src/components/views/settings-view/event-hooks/event-hook-dialog.tsx @@ -28,6 +28,7 @@ import type { EventHookHttpAction, } from '@automaker/types'; import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types'; +import { generateUUID } from '@/lib/utils'; interface EventHookDialogProps { open: boolean; @@ -108,7 +109,7 @@ export function EventHookDialog({ open, onOpenChange, editingHook, onSave }: Eve const handleSave = () => { const hook: EventHook = { - id: editingHook?.id || crypto.randomUUID(), + id: editingHook?.id || generateUUID(), name: name.trim() || undefined, trigger, enabled: editingHook?.enabled ?? true, diff --git a/apps/ui/src/lib/utils.ts b/apps/ui/src/lib/utils.ts index e96d587c..27da4859 100644 --- a/apps/ui/src/lib/utils.ts +++ b/apps/ui/src/lib/utils.ts @@ -124,3 +124,39 @@ export const isMac = : typeof navigator !== 'undefined' && (/Mac/.test(navigator.userAgent) || (navigator.platform ? navigator.platform.toLowerCase().includes('mac') : false)); + +/** + * Generate a UUID v4 string. + * + * Uses crypto.randomUUID() when available (secure contexts: HTTPS or localhost). + * Falls back to crypto.getRandomValues() for non-secure contexts (e.g., Docker via HTTP). + * + * @returns A RFC 4122 compliant UUID v4 string (e.g., "550e8400-e29b-41d4-a716-446655440000") + */ +export function generateUUID(): string { + // Use native randomUUID if available (secure contexts: HTTPS or localhost) + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + // Fallback using crypto.getRandomValues() (works in all modern browsers, including non-secure contexts) + if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + + // Set version (4) and variant (RFC 4122) bits + bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4 + bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC 4122 + + // Convert to hex string with proper UUID format + 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)}`; + } + + // Last resort fallback using Math.random() - less secure but ensures functionality + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +}