import { useState, useEffect, type FormEvent } from "react"; import { useAuth } from "../contexts/AuthContext"; import { useNavigate } from "react-router"; import { Masthead } from "../components/Masthead"; import { useUserSettings } from "../hooks/useUserSettings"; import { getApiKey, createApiKey, refreshApiKey, revokeApiKey, changePassword, changeEmail, deleteAccount, listRepositoryHistory, deleteRepositoryHistory, type ApiKeyInfo, type CreateApiKeyResponse, type RepositoryHistoryEntry, } from "../lib/api"; // ============================================================================= // Password Strength Indicator // ============================================================================= interface PasswordStrength { score: number; label: string; color: string; requirements: { met: boolean; text: string }[]; } function getPasswordStrength(password: string): PasswordStrength { const requirements = [ { met: password.length >= 6, text: "At least 6 characters" }, ]; const score = requirements.filter((r) => r.met).length; const label = score === 1 ? "Valid" : "Too short"; const color = score === 1 ? "bg-green-500" : "bg-red-500"; return { score, label, color, requirements }; } // ============================================================================= // Confirmation Dialog Component // ============================================================================= interface ConfirmDialogProps { isOpen: boolean; title: string; message: string; confirmText: string; confirmButtonClass?: string; requireInput?: string; inputPlaceholder?: string; onConfirm: () => void; onCancel: () => void; } function ConfirmDialog({ isOpen, title, message, confirmText, confirmButtonClass = "bg-red-900/50 border-red-700 hover:bg-red-800/50", requireInput, inputPlaceholder, onConfirm, onCancel, }: ConfirmDialogProps) { const [inputValue, setInputValue] = useState(""); useEffect(() => { if (!isOpen) { setInputValue(""); } }, [isOpen]); if (!isOpen) return null; const canConfirm = !requireInput || inputValue === requireInput; return (

{title}

{message}

{requireInput && (
setInputValue(e.target.value)} placeholder={inputPlaceholder} className="w-full px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]" />
)}
); } // ============================================================================= // Section Header Component // ============================================================================= function SectionHeader({ children }: { children: React.ReactNode }) { return (

{children}

); } // ============================================================================= // Form Input Component // ============================================================================= function FormInput({ label, type = "text", value, onChange, placeholder, required, disabled, }: { label: string; type?: string; value: string; onChange: (value: string) => void; placeholder?: string; required?: boolean; disabled?: boolean; }) { return (
onChange(e.target.value)} placeholder={placeholder} required={required} disabled={disabled} className="w-full px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3] disabled:opacity-50" />
); } // ============================================================================= // Alert Components // ============================================================================= function ErrorAlert({ children }: { children: React.ReactNode }) { return (
{children}
); } function SuccessAlert({ children }: { children: React.ReactNode }) { return (
{children}
); } // ============================================================================= // Button Components // ============================================================================= function PrimaryButton({ children, onClick, disabled, type = "button", }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; type?: "button" | "submit"; }) { return ( ); } function SecondaryButton({ children, onClick, disabled, }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; }) { return ( ); } function DangerButton({ children, onClick, disabled, }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; }) { return ( ); } // ============================================================================= // Main Settings Page // ============================================================================= export default function SettingsPage() { const { user, isAuthConfigured, signOut } = useAuth(); const navigate = useNavigate(); // User settings (feature flags) state const { settings: userSettings, loading: userSettingsLoading, update: updateUserSettings } = useUserSettings(); const [featureFlagSaving, setFeatureFlagSaving] = useState(false); const [featureFlagError, setFeatureFlagError] = useState(null); // API Key state const [apiKeyInfo, setApiKeyInfo] = useState(null); const [newKey, setNewKey] = useState(null); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(false); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); // Password change state const [passwordForm, setPasswordForm] = useState({ currentPassword: "", newPassword: "", confirmPassword: "", }); const [passwordLoading, setPasswordLoading] = useState(false); const [passwordError, setPasswordError] = useState(null); const [passwordSuccess, setPasswordSuccess] = useState(null); // Email change state const [emailForm, setEmailForm] = useState({ password: "", newEmail: "", }); const [emailLoading, setEmailLoading] = useState(false); const [emailError, setEmailError] = useState(null); const [emailSuccess, setEmailSuccess] = useState(null); // Account deletion state const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deletePassword, setDeletePassword] = useState(""); const [deleteLoading, setDeleteLoading] = useState(false); const [deleteError, setDeleteError] = useState(null); // Repository history state const [repoHistory, setRepoHistory] = useState([]); const [repoHistoryLoading, setRepoHistoryLoading] = useState(true); const [repoHistoryError, setRepoHistoryError] = useState(null); const [deletingRepoId, setDeletingRepoId] = useState(null); useEffect(() => { loadApiKey(); loadRepoHistory(); }, []); const loadApiKey = async () => { try { setLoading(true); setError(null); const key = await getApiKey(); setApiKeyInfo(key); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load API key"); } finally { setLoading(false); } }; const loadRepoHistory = async () => { try { setRepoHistoryError(null); const response = await listRepositoryHistory(); setRepoHistory(response.entries); } catch (err) { setRepoHistoryError(err instanceof Error ? err.message : "Failed to load repository history"); } finally { setRepoHistoryLoading(false); } }; const handleDeleteRepoHistory = async (id: string) => { try { setDeletingRepoId(id); await deleteRepositoryHistory(id); setRepoHistory((prev) => prev.filter((entry) => entry.id !== id)); } catch (err) { setRepoHistoryError(err instanceof Error ? err.message : "Failed to delete entry"); } finally { setDeletingRepoId(null); } }; const handleCreate = async () => { try { setActionLoading(true); setError(null); setNewKey(null); const response: CreateApiKeyResponse = await createApiKey("Web UI"); setNewKey(response.key); setApiKeyInfo({ id: response.id, prefix: response.prefix, name: response.name, lastUsedAt: null, createdAt: response.createdAt, }); } catch (err) { setError(err instanceof Error ? err.message : "Failed to create API key"); } finally { setActionLoading(false); } }; const handleRefresh = async () => { try { setActionLoading(true); setError(null); setNewKey(null); const response = await refreshApiKey("Web UI (Refreshed)"); setNewKey(response.key); setApiKeyInfo({ id: response.id, prefix: response.prefix, name: response.name, lastUsedAt: null, createdAt: response.createdAt, }); } catch (err) { setError(err instanceof Error ? err.message : "Failed to refresh API key"); } finally { setActionLoading(false); } }; const handleRevoke = async () => { if (!confirm("Are you sure you want to revoke this API key? Any applications using it will stop working.")) { return; } try { setActionLoading(true); setError(null); setNewKey(null); await revokeApiKey(); setApiKeyInfo(null); } catch (err) { setError(err instanceof Error ? err.message : "Failed to revoke API key"); } finally { setActionLoading(false); } }; const copyToClipboard = async () => { if (!newKey) return; try { await navigator.clipboard.writeText(newKey); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error("Failed to copy:", err); } }; // Password change handlers const handlePasswordChange = async (e: FormEvent) => { e.preventDefault(); setPasswordError(null); setPasswordSuccess(null); if (passwordForm.newPassword !== passwordForm.confirmPassword) { setPasswordError("New passwords do not match"); return; } const strength = getPasswordStrength(passwordForm.newPassword); if (strength.score < 1) { setPasswordError("Password must be at least 6 characters"); return; } try { setPasswordLoading(true); await changePassword(passwordForm.currentPassword, passwordForm.newPassword); setPasswordSuccess("Password changed successfully. Please sign in with your new password."); setPasswordForm({ currentPassword: "", newPassword: "", confirmPassword: "" }); setTimeout(async () => { await signOut(); navigate("/login"); }, 1500); } catch (err) { setPasswordError(err instanceof Error ? err.message : "Failed to change password"); } finally { setPasswordLoading(false); } }; // Email change handlers const handleEmailChange = async (e: FormEvent) => { e.preventDefault(); setEmailError(null); setEmailSuccess(null); if (!emailForm.newEmail.includes("@")) { setEmailError("Please enter a valid email address"); return; } try { setEmailLoading(true); await changeEmail(emailForm.password, emailForm.newEmail); setEmailSuccess("Email changed successfully"); setEmailForm({ password: "", newEmail: "" }); } catch (err) { setEmailError(err instanceof Error ? err.message : "Failed to change email"); } finally { setEmailLoading(false); } }; // Account deletion handlers const DELETE_CONFIRMATION = "DELETE MY ACCOUNT"; const handleDeleteAccount = async () => { try { setDeleteLoading(true); setDeleteError(null); await deleteAccount(deletePassword, DELETE_CONFIRMATION); await signOut(); navigate("/login"); } catch (err) { setDeleteError(err instanceof Error ? err.message : "Failed to delete account"); setDeleteLoading(false); } }; // Feature flag toggle handlers const handleToggleDocumentMode = async () => { if (featureFlagSaving) return; setFeatureFlagError(null); setFeatureFlagSaving(true); try { const next = !(userSettings?.documentModeEnabled ?? false); await updateUserSettings({ documentModeEnabled: next }); } catch (err) { setFeatureFlagError(err instanceof Error ? err.message : "Failed to update setting"); } finally { setFeatureFlagSaving(false); } }; const passwordStrength = getPasswordStrength(passwordForm.newPassword); return (
{/* Page Header */}

Settings

{/* Left Column */}
{/* Account Info */}
Account {isAuthConfigured && user ? (
Email {user.email}
User ID {user.id}
) : (

{isAuthConfigured ? "Not signed in" : "Authentication not configured (API key mode)"}

)}
{/* API Key Section */}
API Key

Authenticate daemon and CLI tools. One active key at a time.

{error && {error}} {newKey && (

Key created. Copy now - won't be shown again.

{newKey}
)} {loading ? (

Loading...

) : apiKeyInfo ? (
Prefix {apiKeyInfo.prefix}...
Created {new Date(apiKeyInfo.createdAt).toLocaleDateString()}
{apiKeyInfo.lastUsedAt && (
Last used {new Date(apiKeyInfo.lastUsedAt).toLocaleDateString()}
)}
{actionLoading ? "..." : "Rotate"} {actionLoading ? "..." : "Revoke"}
) : (

No API key configured.

{actionLoading ? "Creating..." : "Create API Key"}
)}
{/* Daemons Link */}
Daemons

Daemon management has moved to its own page.

Go to Daemons →
{/* Repository History */}

Repository History

{repoHistory.length > 0 && ( ({repoHistory.length} saved) )}

Previously used repositories will appear as suggestions when adding repos to contracts.

{repoHistoryError && {repoHistoryError}} {repoHistoryLoading && repoHistory.length === 0 ? (

Loading...

) : repoHistory.length === 0 ? (

No repository history

Add repositories to contracts to build history

) : (
{repoHistory.map((entry) => (
{entry.name} {entry.sourceType}
{entry.repositoryUrl || entry.localPath}
Used {entry.useCount}× Last: {new Date(entry.lastUsedAt).toLocaleDateString()}
))}
)}
{/* Right Column */}
{/* Password Change */} {isAuthConfigured && user && (
Change Password {passwordError && {passwordError}} {passwordSuccess && {passwordSuccess}}
setPasswordForm({ ...passwordForm, currentPassword: v })} required /> setPasswordForm({ ...passwordForm, newPassword: v })} required /> {passwordForm.newPassword && (
{passwordStrength.label}
)} setPasswordForm({ ...passwordForm, confirmPassword: v })} required /> {passwordForm.confirmPassword && passwordForm.newPassword !== passwordForm.confirmPassword && (

Passwords do not match

)}
{passwordLoading ? "Changing..." : "Change Password"}
)} {/* Email Change */} {isAuthConfigured && user && (
Change Email {emailError && {emailError}} {emailSuccess && {emailSuccess}}
setEmailForm({ ...emailForm, newEmail: v })} placeholder="new@example.com" required /> setEmailForm({ ...emailForm, password: v })} required />
{emailLoading ? "Changing..." : "Change Email"}
)} {/* Feature Flags (POC) */}
Feature Flags (POC) {featureFlagError && {featureFlagError}}
Document Mode for directives

Replaces the tabular directives UI with a Lexical-based interactive document editor. Proof of concept; expect rough edges.

{(userSettingsLoading || featureFlagSaving) && (

{userSettingsLoading ? "Loading..." : "Saving..."}

)}
{/* Danger Zone */} {isAuthConfigured && user && (

Danger Zone

Permanently delete your account and all data. This cannot be undone.

{deleteError && {deleteError}}
setDeleteDialogOpen(true)} disabled={!deletePassword || deleteLoading} > {deleteLoading ? "Deleting..." : "Delete Account"}
)}
{/* Delete Confirmation Dialog */} { setDeleteDialogOpen(false); handleDeleteAccount(); }} onCancel={() => setDeleteDialogOpen(false)} />
); }