import { useEffect, useMemo, useRef, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { getApiBaseUrl, getAgentPublicBaseUrl, getHostMonitoring, createUser, createGitProvider, deleteGitProvider, getCurrentUserProfile, listAuditLogs, listAgents, listBuilds, listGitProviders, listProjects, createAgentToken, listAgentTokens, revokeAgentToken, createUserToken, listUserTokens, revokeUserToken, listDatabases, databaseAction, updateDatabaseBackupSchedule, createDatabaseBackup, restoreDatabaseBackup, createManagedDatabase, listServicesByProject, listServiceVariables, updateServiceVariables, getGitHubAppInstallUrl, connectGitHubApp, updateCurrentUserProfile, getPlatformSettings, updatePlatformSettings, getHAStatus, setHAEnabled, triggerFailover, listFailoverPolicies, setFailoverPolicy, deleteFailoverPolicy, listActiveAlerts, resolveAlert, listHealthResults, startSecurityScan, getSecurityHistory, listVulnerabilities, updateVulnerability, getSecurityMetrics, type AgentAuthTokenCreated, type UserTokenCreated, type UserTokenScope, type CreateDatabaseInput, type DatabaseEntity, type FailoverPolicy, } from '@/lib/api-client'; import { demoBuilds, demoDatabases, demoHAAlerts, demoHAHealthResults, demoHAPolicies, demoHAStatus, demoHostMonitoring, demoProjects, demoSecurityMetrics, demoSecurityScans, demoVulnerabilities, getDemoServicesByProject, } from '@/lib/demo-data'; import { ServiceIcon } from '@/features/workspace/canvas/ServiceIcon'; import { serviceAccent } from '@/features/workspace/canvas/service-visuals'; import { DocsBrowser } from '@/features/docs/DocsBrowser'; import { useAuthSession } from '@/lib/use-auth-session'; import { useDemoMode } from '@/lib/demo-mode'; import { formatRelative } from '@/lib/time'; import { getAuthBaseUrl, signOutAuthSession } from '@/lib/auth-client'; import { useBuildUpdates } from '@/lib/use-build-updates'; import { EnhancedMetricCard, LineAreaChart, DonutChart, SegmentedBar, BarChart, DemoRestricted } from '@/shared/components'; import { Clock, Activity, Gauge, Users, Shield, FileText, Settings, User, UserPlus, Key, Database, RefreshCw, Trash2, LogOut, Check, AlertCircle, Loader2, Terminal, Radio, Server, HardDrive, GitBranch, Link2, Unlink, Cpu, MemoryStick, ScrollText, ChevronRight, ChevronDown, Copy, Play, Square, Archive, ShieldCheck, Zap, HeartPulse, Cloud, ExternalLink, } from 'lucide-react'; function SecondaryPageHeader({ title, description }: { title: string; description: string }) { return (

{title}_

{description}

); } function StatCard({ title, value, description, icon: Icon, color = 'default', }: { title: string; value: string; description: string; icon: typeof Clock; color?: 'default' | 'success' | 'warning' | 'error'; }) { const colorClasses = { default: 'text-[var(--text-primary)]', success: 'text-[var(--success)]', warning: 'text-[var(--warning)]', error: 'text-[var(--error)]', }; return (

{value}

{title}

{description}

); } type LocalStorageSummary = { canvasKeys: string[]; totalKeys: number; }; function getLocalStorageSummary(): LocalStorageSummary { let storage: Storage | null = null; try { storage = typeof window === 'undefined' ? null : window.localStorage; } catch { storage = null; } if (!storage) { return { canvasKeys: [], totalKeys: 0 }; } const canvasKeys: string[] = []; for (let index = 0; index < storage.length; index += 1) { const key = storage.key(index); if (key && key.startsWith('containr.canvas.v1.')) { canvasKeys.push(key); } } canvasKeys.sort((left, right) => left.localeCompare(right)); return { canvasKeys, totalKeys: storage.length, }; } function formatBytes(value: number): string { if (!Number.isFinite(value) || value <= 0) { return '0 B'; } const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = value; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex += 1; } return `${size.toFixed(size >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; } function formatUptime(seconds: number): string { if (!Number.isFinite(seconds) || seconds <= 0) { return '—'; } const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); if (days > 0) { return `${days}d ${hours}h`; } return `${hours}h ${Math.floor((seconds % 3600) / 60)}m`; } export function UsagePage() { const queryClient = useQueryClient(); const isDemoMode = useDemoMode(); const sessionQuery = useAuthSession({ enabled: !isDemoMode }); const profileQuery = useQuery({ queryKey: ['user-profile'], queryFn: getCurrentUserProfile, enabled: !isDemoMode && Boolean(sessionQuery.data), retry: false, }); const signedIn = isDemoMode || Boolean(sessionQuery.data); const isAdmin = !isDemoMode && Boolean(profileQuery.data?.isAdmin); const buildsQuery = useQuery({ queryKey: ['usage-builds'], queryFn: () => listBuilds({ page: 1, limit: 100 }), enabled: signedIn && !isDemoMode, retry: false, }); const hostQuery = useQuery({ queryKey: ['usage-host-monitoring'], queryFn: getHostMonitoring, refetchInterval: 15_000, enabled: !isDemoMode, }); const agentsQuery = useQuery({ queryKey: ['usage-agents'], queryFn: listAgents, refetchInterval: 15_000, enabled: isAdmin, retry: false, }); const agentTokensQuery = useQuery({ queryKey: ['agent-tokens'], queryFn: listAgentTokens, enabled: isAdmin, retry: false, }); const [tokenLabel, setTokenLabel] = useState(''); const [issuedToken, setIssuedToken] = useState(null); const createTokenMutation = useMutation({ mutationFn: () => createAgentToken(tokenLabel.trim()), onSuccess: (data) => { setIssuedToken(data); setTokenLabel(''); queryClient.invalidateQueries({ queryKey: ['agent-tokens'] }); }, }); const revokeTokenMutation = useMutation({ mutationFn: (id: string) => revokeAgentToken(id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['agent-tokens'] }), }); const builds = useMemo( () => (isDemoMode ? demoBuilds : buildsQuery.data?.builds ?? []), [isDemoMode, buildsQuery.data], ); const liveStatus = useBuildUpdates( isDemoMode ? [] : builds.map((build) => build.id), () => { queryClient.invalidateQueries({ queryKey: ['usage-builds'] }); }, ); const successfulBuilds = builds.filter((build) => build.status === 'success').length; const failedBuilds = builds.filter((build) => build.status === 'failed').length; const runningBuilds = builds.filter((build) => build.status === 'running').length; const pendingBuilds = builds.filter((build) => build.status === 'pending').length; const completedDurationsMs = builds .filter((build) => Boolean(build.startedAt && build.completedAt)) .map((build) => { const started = new Date(build.startedAt as string).getTime(); const completed = new Date(build.completedAt as string).getTime(); return Math.max(0, completed - started); }) .filter((duration) => Number.isFinite(duration) && duration > 0); const totalBuildDurationMs = completedDurationsMs.reduce((sum, duration) => sum + duration, 0); const averageBuildDurationMs = completedDurationsMs.length > 0 ? totalBuildDurationMs / completedDurationsMs.length : 0; const totalBuildHours = totalBuildDurationMs / 3_600_000; const avgBuildMinutes = averageBuildDurationMs / 60_000; const activeBuildPressure = runningBuilds + pendingBuilds; const uniqueServices = new Set(builds.map((build) => build.serviceId).filter(Boolean)).size; const host = isDemoMode ? demoHostMonitoring : hostQuery.data; const agents = agentsQuery.data ?? []; // Accumulate host samples for the load sparkline (polled every 15s). const [loadHistory, setLoadHistory] = useState([]); useEffect(() => { if (!host || host.cpu.cores <= 0) { return; } const loadPercent = Math.min(100, (host.load.load1m / host.cpu.cores) * 100); // eslint-disable-next-line react-hooks/set-state-in-effect -- accumulating polled samples is a legitimate sync-to-external pattern setLoadHistory((prev) => [...prev, loadPercent].slice(-24)); }, [host]); const hostLoadPercent = host && host.cpu.cores > 0 ? Math.min(100, (host.load.load1m / host.cpu.cores) * 100) : 0; const hostStatus = (percent: number): 'good' | 'average' | 'warning' => percent < 50 ? 'good' : percent < 80 ? 'average' : 'warning'; // Builds per day over the last 14 days, normalized to bar heights. // Anchored on the newest build timestamp so the chart is render-pure. const buildsPerDay = useMemo(() => { const days = 14; const counts = new Array(days).fill(0); const timestamps = builds .map((build) => new Date((build.startedAt ?? build.completedAt) as string).getTime()) .filter((t) => Number.isFinite(t)); if (timestamps.length === 0) { return counts; } const anchor = Math.max(...timestamps); builds.forEach((build) => { const timestamp = build.startedAt ?? build.completedAt; if (!timestamp) { return; } const created = new Date(timestamp).getTime(); const dayIndex = days - 1 - Math.floor((anchor - created) / 86_400_000); if (dayIndex >= 0 && dayIndex < days) { counts[dayIndex] += 1; } }); const peak = Math.max(...counts, 1); return counts.map((count) => (count / peak) * 100); }, [builds]); const onlineAgents = agents.filter((agent) => agent.status === 'online' || agent.status === 'connecting').length; const totalAgentMemory = agents.reduce((sum, agent) => sum + agent.resources.memory.total, 0); const availableAgentMemory = agents.reduce((sum, agent) => sum + agent.resources.memory.available, 0); const agentEndpoint = getAgentPublicBaseUrl(); const connectCommand = `CONTAINR_API_URL=${getApiBaseUrl().replace(/\/api\/v1$/, '')} \\ CONTAINR_AGENT_AUTH_TOKEN=${issuedToken?.token ?? ''} \\ containr-agent`; let buildActivityBody = 'Track deployment frequency and failed rollouts over time.'; let runtimeBody = 'Build duration telemetry will appear after completed builds are available.'; let capacityBody = 'No active build pressure detected.'; if (buildsQuery.isLoading) { buildActivityBody = 'Loading recent build activity from Containr API.'; runtimeBody = 'Loading build runtime metrics from Containr API.'; capacityBody = 'Loading active queue depth and service fan-out.'; } else if (buildsQuery.isError) { buildActivityBody = 'Unable to fetch build telemetry right now. Check API connectivity and auth.'; runtimeBody = 'Unable to calculate runtime metrics while build telemetry is unavailable.'; capacityBody = 'Unable to evaluate queue pressure while build telemetry is unavailable.'; } else if (builds.length > 0) { buildActivityBody = `${builds.length} recent builds, ${successfulBuilds} successful, ${failedBuilds} failed${runningBuilds > 0 ? `, ${runningBuilds} running` : ''}.`; if (completedDurationsMs.length > 0) { runtimeBody = `${totalBuildHours.toFixed(1)}h total build runtime across ${completedDurationsMs.length} completed builds (avg ${avgBuildMinutes.toFixed(1)}m).`; } else { runtimeBody = 'Builds exist, but completed duration samples are not available yet.'; } if (activeBuildPressure > 0) { capacityBody = `${activeBuildPressure} active jobs in queue (${runningBuilds} running, ${pendingBuilds} pending) across ${uniqueServices} service${uniqueServices === 1 ? '' : 's'}.`; } else { capacityBody = `No pending or running jobs. Last sampled ${builds.length} builds touched ${uniqueServices} service${uniqueServices === 1 ? '' : 's'}.`; } } else { buildActivityBody = 'No builds recorded yet. Trigger a service deployment to populate this view.'; runtimeBody = 'No completed builds yet, so runtime totals are not available.'; capacityBody = 'Queue depth is empty because no build history is available yet.'; } return (
{liveStatus === 'live' ? 'Live sync active' : liveStatus === 'offline' ? 'Reconnecting...' : 'Polling for updates'}
{!isDemoMode && buildsQuery.isLoading ? (

Loading usage data...

) : !isDemoMode && buildsQuery.isError ? (

Failed to load usage data

) : !signedIn ? (

Sign in to view build activity and capacity.

) : (
0 ? `${totalBuildHours.toFixed(1)}h` : '—'} statusText={completedDurationsMs.length > 0 ? 'Active' : ''} status="good" subtitle={runtimeBody} icon={} /> 0 ? 'average' : 'good'} statusText={failedBuilds > 0 ? `${failedBuilds} failed` : 'Healthy'} subtitle={buildActivityBody} icon={} chart={} /> 0 ? 'average' : 'good'} statusText={activeBuildPressure > 0 ? 'Busy' : 'Idle'} subtitle={capacityBody} icon={} />

Host Monitoring

{host ? `${host.hostname} · ${host.os}/${host.architecture} · uptime ${formatUptime(host.uptimeSeconds)}${host.dockerAvailable ? '' : ' · Docker unavailable'}` : 'Autoscaling base signal from this Containr host.'}

{!isDemoMode && hostQuery.isError ? (
Host monitoring unavailable.
) : (
} value={host ? host.load.load1m.toFixed(2) : '—'} status={hostStatus(hostLoadPercent)} statusText={host ? { good: 'Good', average: 'Average', warning: 'High' }[hostStatus(hostLoadPercent)] : ''} subtitle={host ? `${host.cpu.cores} cores · 5m ${host.load.load5m.toFixed(2)} · 15m ${host.load.load15m.toFixed(2)}` : 'Loading CPU telemetry.'} chart={ 0 ? loadHistory : [0]} color="var(--accent-primary)" height={72} />} /> } value={host ? `${host.memory.usagePercent.toFixed(0)}%` : '—'} status={hostStatus(host?.memory.usagePercent ?? 0)} statusText={host ? { good: 'Good', average: 'Average', warning: 'High' }[hostStatus(host.memory.usagePercent)] : ''} subtitle={host ? `${formatBytes(host.memory.available)} free` : 'Loading memory telemetry.'} chart={
Used
{host ? `${formatBytes(host.memory.used)} / ${formatBytes(host.memory.total)}` : '—'}
} /> } value={host ? `${host.storage.usagePercent.toFixed(0)}%` : '—'} status={hostStatus(host?.storage.usagePercent ?? 0)} statusText={host ? { good: 'Good', average: 'Average', warning: 'High' }[hostStatus(host.storage.usagePercent)] : ''} subtitle={host ? `Mounted at ${host.storage.path}` : 'Loading disk telemetry.'} chart={
{host ? `${formatBytes(host.storage.used)} used` : '—'} {host ? `${formatBytes(host.storage.available)} free` : '—'}
} />
)}

Node Agents

Remote VPS, VM, and LXC capacity for placement and autoscaling.

0 ? 'success' : 'default'} /> 0 ? 'success' : 'default'} /> 0 ? 'success' : 'default'} />
{agents.length > 0 ? ( agents.slice(0, 5).map((agent) => (

{agent.name}

{agent.hostname} · {agent.ipAddress}:{agent.port}

{agent.status}

{formatBytes(agent.resources.memory.available)} free

)) ) : (
No remote agents registered yet.
)}
Connect Node
{isAdmin ? ( <>
setTokenLabel(e.target.value)} placeholder="Token label (e.g. node name)" className="h-8 flex-1 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-xs text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)] transition-all" />
{issuedToken && (
{issuedToken.token}

Shown once — store it now. Only the hash is kept server-side.

)} {createTokenMutation.isError && (

{createTokenMutation.error instanceof Error ? createTokenMutation.error.message : 'Failed to issue token'}

)}
{connectCommand}

Agent endpoint: {agentEndpoint}

Use an issued token above, or set CONTAINR_AGENT_AUTH_TOKENS on the backend.

Agent sends host resources, polls pending Docker commands, and reports command results.

{(agentTokensQuery.data ?? []).length > 0 && (

Issued tokens

{(agentTokensQuery.data ?? []).map((t) => (
{t.label || t.id?.slice(0, 8)} {t.revoked ? 'revoked' : t.last_used_at ? `used ${formatRelative(t.last_used_at)}` : 'unused'}
{!t.revoked && ( )}
))}
)} ) : (

Node registration and token issuance are admin operations. {signedIn ? 'Your account does not have admin rights.' : 'Sign in as an admin to manage nodes.'}

)}
)}
); } export function PeoplePage() { const queryClient = useQueryClient(); const isDemoMode = useDemoMode(); const profileQuery = useQuery({ queryKey: ['user-profile'], queryFn: getCurrentUserProfile, enabled: !isDemoMode, }); const [newUserName, setNewUserName] = useState(''); const [newUserEmail, setNewUserEmail] = useState(''); const [newUserPassword, setNewUserPassword] = useState(''); const createUserMutation = useMutation({ mutationFn: () => createUser({ name: newUserName.trim(), email: newUserEmail.trim(), password: newUserPassword, }), onSuccess: () => { setNewUserName(''); setNewUserEmail(''); setNewUserPassword(''); queryClient.invalidateQueries({ queryKey: ['user-profile'] }); queryClient.invalidateQueries({ queryKey: ['people-audit-logs'] }); }, }); const projectsQuery = useQuery({ queryKey: ['people-projects'], queryFn: () => listProjects(), enabled: !isDemoMode, }); const auditLogsQuery = useQuery({ queryKey: ['people-audit-logs'], queryFn: () => listAuditLogs({ page: 1, limit: 20 }), enabled: !isDemoMode, }); if (isDemoMode) { return ; } let membersBody = 'Owner-only mode currently. Multi-user collaboration is planned.'; if (profileQuery.isLoading) { membersBody = 'Loading authenticated profile from Containr API.'; } else if (profileQuery.isError) { membersBody = 'Unable to fetch current user profile. Verify API connectivity and Better Auth session.'; } else if (profileQuery.data) { membersBody = `${profileQuery.data.name} (${profileQuery.data.email}) is currently authenticated as platform owner.`; } let rolesBody = 'Predefined roles will map to service and project permissions.'; if (projectsQuery.isLoading) { rolesBody = 'Loading project access footprint for current user.'; } else if (projectsQuery.isError) { rolesBody = 'Project visibility check failed. Role scope cannot be calculated right now.'; } else { const projectCount = projectsQuery.data?.length ?? 0; rolesBody = `Current account can access ${projectCount} project${projectCount === 1 ? '' : 's'} in owner mode.`; } let auditBody = 'Human-readable action log for project and service changes.'; if (auditLogsQuery.isLoading) { auditBody = 'Loading recent audit events from Containr API.'; } else if (auditLogsQuery.isError) { auditBody = 'Unable to load recent audit events. Verify API connectivity and Better Auth session.'; } else { const auditLogs = auditLogsQuery.data ?? []; if (auditLogs.length > 0) { const latest = auditLogs[0]; auditBody = `${auditLogs.length} recent events. Latest action: ${latest.action} on ${latest.resource}${ latest.resourceId ? ` (${latest.resourceId})` : '' }.`; } else { auditBody = 'No audit events recorded yet for this account.'; } } return (
{profileQuery.isLoading ? (

Loading team data...

) : (

Manual User Creation

Public registration is closed after bootstrap.

{ event.preventDefault(); createUserMutation.mutate(); }} > setNewUserName(event.target.value)} required className="h-10 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm" placeholder="Name" /> setNewUserEmail(event.target.value)} type="email" required className="h-10 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm" placeholder="email@example.com" /> setNewUserPassword(event.target.value)} type="password" minLength={8} required className="h-10 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm" placeholder="Temporary password" />
{createUserMutation.isError ? (
{createUserMutation.error instanceof Error ? createUserMutation.error.message : 'Failed to create user'}
) : null} {createUserMutation.isSuccess ? (
User created. Share credentials through secure channel.
) : null}
)}
); } const gitProviderTypes = [ { value: 'github', label: 'GitHub', tokenHint: 'Personal access token with repo scope' }, { value: 'gitlab', label: 'GitLab', tokenHint: 'Personal access token with read_api scope' }, { value: 'gitea', label: 'Gitea', tokenHint: 'Access token from your Gitea instance' }, { value: 'bitbucket', label: 'Bitbucket', tokenHint: 'App password with repository read' }, ] as const; function GitProvidersSection() { const queryClient = useQueryClient(); const [searchParams, setSearchParams] = useSearchParams(); const providersQuery = useQuery({ queryKey: ['git-providers'], queryFn: listGitProviders, }); // GitHub redirects back here with ?installation_id= after app install. const installIdParam = searchParams.get('installation_id'); const connectAppMutation = useMutation({ mutationFn: (installationId: number) => connectGitHubApp(installationId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['git-providers'] }); const next = new URLSearchParams(searchParams); next.delete('installation_id'); next.delete('setup_action'); setSearchParams(next, { replace: true }); }, }); const connectFiredFor = useRef(null); useEffect(() => { const id = Number(installIdParam); if (id > 0 && connectFiredFor.current !== id) { connectFiredFor.current = id; connectAppMutation.mutate(id); } // eslint-disable-next-line react-hooks/exhaustive-deps -- fire once per installation id }, [installIdParam]); const [installUrlError, setInstallUrlError] = useState(null); const installApp = async () => { setInstallUrlError(null); try { window.open(await getGitHubAppInstallUrl(), '_blank', 'noopener'); } catch (err) { setInstallUrlError(err instanceof Error ? err.message : 'GitHub App is not configured'); } }; const [form, setForm] = useState({ name: 'github' as (typeof gitProviderTypes)[number]['value'], displayName: '', accessToken: '', apiUrl: '', }); const [formOpen, setFormOpen] = useState(false); const createMutation = useMutation({ mutationFn: () => createGitProvider({ name: form.name, display_name: form.displayName.trim() || gitProviderTypes.find((t) => t.value === form.name)!.label, access_token: form.accessToken.trim(), api_url: form.apiUrl.trim() || undefined, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['git-providers'] }); setForm({ name: 'github', displayName: '', accessToken: '', apiUrl: '' }); setFormOpen(false); }, }); const deleteMutation = useMutation({ mutationFn: (providerId: string) => deleteGitProvider(providerId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['git-providers'] }); }, }); const providers = providersQuery.data ?? []; const selectedType = gitProviderTypes.find((t) => t.value === form.name)!; return (

Git Providers

Connect accounts to deploy services from repositories

{installUrlError && (
{installUrlError}
)} {connectAppMutation.isPending && (
Completing GitHub App installation…
)} {connectAppMutation.isError && (
{connectAppMutation.error instanceof Error ? connectAppMutation.error.message : 'Failed to connect GitHub App'}
)} {providersQuery.isLoading ? (
) : providersQuery.isError ? (
Failed to load providers
) : providers.length === 0 ? (

No Git providers connected

Connect GitHub, GitLab, Gitea, or Bitbucket to deploy services from source

) : (
{providers.map((provider) => (

{provider.display_name}

{provider.name.replace('_', ' ')}

))}
)} {formOpen && (
setForm((p) => ({ ...p, displayName: e.target.value }))} className="w-full h-10 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-sm focus:border-[var(--accent-primary)] transition-colors" placeholder={selectedType.label} />
setForm((p) => ({ ...p, accessToken: e.target.value }))} className="w-full h-10 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-sm mono focus:border-[var(--accent-primary)] transition-colors" placeholder="••••••••••••••••" />

{selectedType.tokenHint}

{form.name === 'gitea' && (
setForm((p) => ({ ...p, apiUrl: e.target.value }))} className="w-full h-10 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-sm mono focus:border-[var(--accent-primary)] transition-colors" placeholder="https://gitea.example.com" />
)} {createMutation.isError && (
{createMutation.error instanceof Error ? createMutation.error.message : 'Failed to connect provider'}
)}
)}
); } function PlatformSettingsSection() { const queryClient = useQueryClient(); const settingsQuery = useQuery({ queryKey: ['platform-settings'], queryFn: getPlatformSettings, }); const [signupDraft, setSignupDraft] = useState(null); const [tokenDraft, setTokenDraft] = useState(''); const [feedback, setFeedback] = useState(null); const [error, setError] = useState(null); const signupEnabled = signupDraft ?? settingsQuery.data?.signupEnabled ?? false; const tunnel = settingsQuery.data?.cloudflareTunnel; const saveMutation = useMutation({ mutationFn: updatePlatformSettings, onSuccess: (settings) => { queryClient.setQueryData(['platform-settings'], settings); setSignupDraft(null); setTokenDraft(''); setFeedback('Settings saved.'); setError(null); }, onError: (err) => { setError(err instanceof Error ? err.message : 'Failed to save settings'); setFeedback(null); }, }); const saveSignup = (enabled: boolean) => { setSignupDraft(enabled); saveMutation.mutate({ signupEnabled: enabled }); }; const saveToken = () => { if (!tokenDraft.trim()) return; saveMutation.mutate({ cloudflareTunnelToken: tokenDraft.trim() }); }; const clearToken = () => { saveMutation.mutate({ cloudflareTunnelToken: '' }); }; return (

Platform

Owner only - values set here override environment variables

{settingsQuery.isLoading ? (
) : (
Cloudflare Tunnel {tunnel?.tokenSet ? `token saved (${tunnel.source}) - ${tunnel.container}` : tunnel?.container === 'unavailable' ? 'Docker socket unavailable' : 'not configured'}

Expose this instance through a Cloudflare Tunnel. Create a tunnel in the dashboard, copy the token, paste it here - the cloudflared sidecar is managed for you.

setTokenDraft(e.target.value)} className="flex-1 h-9 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--bg-void)] text-sm focus:border-[var(--accent-primary)] transition-colors" placeholder={tunnel?.tokenSet ? 'Token saved - paste a new one to rotate' : 'eyJhIjo... tunnel token'} /> {tunnel?.tokenSet && tunnel.source === 'app' ? ( ) : null}
Open Cloudflare Zero Trust dashboard - Networks → Tunnels → Create
{feedback ?

{feedback}

: null} {error ?

{error}

: null}
)}
); } const PAT_EXPIRY_OPTIONS: { label: string; days: number }[] = [ { label: 'Never', days: 0 }, { label: '30 days', days: 30 }, { label: '90 days', days: 90 }, { label: '1 year', days: 365 }, ]; function UserTokensSection({ isAdmin }: { isAdmin: boolean }) { const queryClient = useQueryClient(); const tokensQuery = useQuery({ queryKey: ['user-tokens'], queryFn: listUserTokens, }); const [tokenName, setTokenName] = useState(''); const [tokenScope, setTokenScope] = useState('write'); const [expiryDays, setExpiryDays] = useState(0); const [issuedToken, setIssuedToken] = useState(null); const createMutation = useMutation({ mutationFn: () => createUserToken({ name: tokenName.trim(), scope: tokenScope, expiresInDays: expiryDays, }), onSuccess: (data) => { setIssuedToken(data); setTokenName(''); queryClient.invalidateQueries({ queryKey: ['user-tokens'] }); }, }); const revokeMutation = useMutation({ mutationFn: (id: string) => revokeUserToken(id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['user-tokens'] }), }); const tokens = tokensQuery.data ?? []; const scopeOptions: { value: UserTokenScope; label: string }[] = isAdmin ? [ { value: 'read', label: 'Read - GET requests only' }, { value: 'write', label: 'Write - read and manage resources' }, { value: 'admin', label: 'Admin - everything, incl. platform settings' }, ] : [ { value: 'read', label: 'Read - GET requests only' }, { value: 'write', label: 'Write - read and manage resources' }, ]; return (

Personal Access Tokens

Authenticate the CLI, MCP server, and scripts - same rights as you, bounded by scope

setTokenName(e.target.value)} placeholder="Token name (e.g. laptop cli)" className="h-9 flex-1 min-w-[180px] rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent-primary)] transition-all" />
{issuedToken ? (
{issuedToken.token}

Shown once - store it now. Use with containr auth login --token or as a Bearer token.

) : null} {createMutation.isError ? (

{createMutation.error instanceof Error ? createMutation.error.message : 'Failed to create token'}

) : null} {tokensQuery.isLoading ? (
) : tokens.length === 0 ? (

No tokens yet. Create one to connect the CLI or an agent.

) : (
{tokens.map((token) => (
{token.name} {token.key_prefix}… {token.scope} {token.last_used_at ? `used ${formatRelative(token.last_used_at)}` : 'unused'} {token.expires_at ? ` · expires ${formatRelative(token.expires_at)}` : ''}
))}
)}
); } export function SettingsPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const isDemoMode = useDemoMode(); const profileQuery = useQuery({ queryKey: ['settings-profile'], queryFn: getCurrentUserProfile, enabled: !isDemoMode, }); const [nameDraft, setNameDraft] = useState(null); const [avatarDraft, setAvatarDraft] = useState(null); const [storageSummary, setStorageSummary] = useState(() => getLocalStorageSummary(), ); const resolvedNameDraft = nameDraft ?? profileQuery.data?.name ?? ''; const resolvedAvatarDraft = avatarDraft ?? profileQuery.data?.avatarUrl ?? ''; const hasProfileChanges = useMemo(() => { if (!profileQuery.data) { return false; } return ( resolvedNameDraft.trim() !== profileQuery.data.name || resolvedAvatarDraft.trim() !== (profileQuery.data.avatarUrl ?? '') ); }, [profileQuery.data, resolvedAvatarDraft, resolvedNameDraft]); const updateProfileMutation = useMutation({ mutationFn: () => updateCurrentUserProfile({ name: resolvedNameDraft.trim(), avatarUrl: resolvedAvatarDraft.trim() || undefined, }), onSuccess: (profile) => { queryClient.setQueryData(['settings-profile'], profile); queryClient.setQueryData(['user-profile'], profile); setNameDraft(null); setAvatarDraft(null); }, }); const refreshStorage = () => { setStorageSummary(getLocalStorageSummary()); }; const clearCanvasCache = () => { try { for (const key of storageSummary.canvasKeys) { window.localStorage.removeItem(key); } } catch { // Storage unavailable — nothing to clear. } refreshStorage(); }; const signOutLocalSession = async () => { try { await signOutAuthSession(); } catch { // Continue with local cleanup even if remote sign-out call fails. } queryClient.removeQueries(); await queryClient.invalidateQueries({ queryKey: ['auth-session'] }); navigate('/auth/sign-in', { replace: true }); refreshStorage(); }; if (isDemoMode) { return ; } return (
{/* Profile Section */}

Profile

Backed by GET/PUT /user/profile

{profileQuery.isLoading ? (
) : profileQuery.isError ? (
Failed to load profile
) : (
setNameDraft(e.target.value)} className="w-full h-10 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-sm focus:border-[var(--accent-primary)] transition-colors" placeholder="Your name" />
setAvatarDraft(e.target.value)} className="w-full h-10 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-sm focus:border-[var(--accent-primary)] transition-colors" placeholder="https://example.com/avatar.png" />
{updateProfileMutation.isError && (
{updateProfileMutation.error instanceof Error ? updateProfileMutation.error.message : 'Failed to update profile'}
)} {updateProfileMutation.isSuccess && (
Profile updated successfully
)}
)}
{/* Personal Access Tokens */} {/* Platform Settings - owner only */} {profileQuery.data?.isAdmin ? : null} {/* Git Providers Section */} {/* Audit Logs Section */}

Audit Logs

Every authenticated action recorded by the platform

Filter the full audit trail by resource, action, actor, and time range.

{/* Runtime & Local State Section */}

Runtime & Local State

API endpoints, cookie-session mode, and storage diagnostics

API Base

{getApiBaseUrl()}

Auth Base

{getAuthBaseUrl()}

Session Mode

HttpOnly Better Auth cookie (no local auth token)

Canvas Cache

{storageSummary.canvasKeys.length} cached canvas entries ({storageSummary.totalKeys} total keys)

{storageSummary.canvasKeys.length > 0 && (

Cached Keys

    {storageSummary.canvasKeys.slice(0, 5).map((key) => (
  • {key}
  • ))} {storageSummary.canvasKeys.length > 5 && (
  • +{storageSummary.canvasKeys.length - 5} more...
  • )}
)}
); } export function DocsPage() { return (
); } export { ComponentShowcase } from './pages/ComponentShowcase'; const dbStatusClass: Record = { running: 'bg-[var(--success-soft)] text-[var(--success)]', stopped: 'bg-[var(--surface-muted)] text-[var(--text-muted)]', building: 'bg-[var(--warning-soft)] text-[var(--warning)]', error: 'bg-[var(--error-soft)] text-[var(--error)]', }; const DATABASE_TYPES = ['postgresql', 'mysql', 'mariadb', 'mongodb', 'redis', 'dragonfly', 'clickhouse'] as const; const DATABASE_PLANS = ['hobby', 'starter', 'standard', 'business'] as const; export function DatabasesPage() { const queryClient = useQueryClient(); const isDemoMode = useDemoMode(); const sessionQuery = useAuthSession({ enabled: !isDemoMode }); const signedIn = isDemoMode || Boolean(sessionQuery.data); const [expandedId, setExpandedId] = useState(null); const [bindDb, setBindDb] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [createForm, setCreateForm] = useState({ name: '', type: 'postgresql', plan: 'hobby', region: 'local' }); const [createError, setCreateError] = useState(null); const databasesQuery = useQuery({ queryKey: ['databases'], queryFn: listDatabases, refetchInterval: 15_000, enabled: !isDemoMode, }); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['databases'] }); const createMutation = useMutation({ mutationFn: () => createManagedDatabase({ name: createForm.name.trim(), type: createForm.type as CreateDatabaseInput['type'], plan: createForm.plan as CreateDatabaseInput['plan'], region: createForm.region.trim() || 'local', }), onSuccess: () => { setCreateOpen(false); setCreateForm({ name: '', type: 'postgresql', plan: 'hobby', region: 'local' }); setCreateError(null); invalidate(); }, onError: (error) => { setCreateError(error instanceof Error ? error.message : 'Failed to create database'); }, }); const actionMutation = useMutation({ mutationFn: ({ id, action }: { id: string; action: 'start' | 'stop' | 'restart' }) => databaseAction(id, action), onSuccess: invalidate, }); const backupMutation = useMutation({ mutationFn: (id: string) => createDatabaseBackup(id), onSuccess: () => window.setTimeout(invalidate, 3000), }); const restoreMutation = useMutation({ mutationFn: ({ id, backupId }: { id: string; backupId: string }) => restoreDatabaseBackup(id, backupId), onSuccess: () => window.setTimeout(invalidate, 3000), }); const databases = isDemoMode ? demoDatabases : databasesQuery.data ?? []; return (

{databases.length} {databases.length === 1 ? 'database' : 'databases'}

{signedIn && !isDemoMode ? ( ) : null}
{databasesQuery.isLoading ? (
) : databases.length === 0 ? (

No managed databases yet.

{signedIn ? ( ) : (

Sign in to provision a managed database.

)}
) : (
{databases.map((db: DatabaseEntity) => { const expanded = expandedId === db.id; const running = db.status === 'running'; const backups = db.backups?.backups ?? []; return (
{expanded && (

Connection URL

{db.connection_url || '—'} {db.connection_url && ( )}
{signedIn ? ( <> Actions {actionMutation.isError && ( {actionMutation.error instanceof Error ? actionMutation.error.message : 'Action failed'} )} ) : ( Sign in to manage this database. )}
{bindDb?.id === db.id && db.connection_url && ( setBindDb(null)} /> )} {db.metrics && (
{([ ['CPU', `${db.metrics.cpu ?? 0}%`], ['Memory', `${db.metrics.memory ?? 0}%`], ['Storage', `${db.metrics.storage ?? 0}%`], ['Connections', `${db.metrics.connections ?? 0}`], ] as const).map(([label, value]) => (

{label}

{value}

))}
)}

Backups

{signedIn ? ( ) : null}
{backups.length === 0 ? (

No backups yet.

) : (
{backups.map((b) => (
{formatRelative(b.created_at)} {b.size} {b.status} {b.status === 'completed' && ( Download )}
))}
)} {restoreMutation.isError && (

{restoreMutation.error instanceof Error ? restoreMutation.error.message : 'Restore failed'}

)}
)}
); })}
)}
{createOpen ? (

New database

Provision a managed database service.

setCreateForm((f) => ({ ...f, name: e.target.value }))} placeholder="my-postgres" className="h-10 w-full rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm focus:border-[var(--accent-primary)]" />
setCreateForm((f) => ({ ...f, region: e.target.value }))} placeholder="local" className="h-10 w-full rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm focus:border-[var(--accent-primary)]" />
{createError ? (
{createError}
) : null}
) : null}
); } function BindDatabasePanel({ connectionUrl, onClose, }: { connectionUrl: string; onClose: () => void; }) { const [projectId, setProjectId] = useState(''); const [serviceId, setServiceId] = useState(''); const [varKey, setVarKey] = useState('DATABASE_URL'); const [bound, setBound] = useState(false); const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects() }); const servicesQuery = useQuery({ queryKey: ['services', projectId], queryFn: () => listServicesByProject(projectId), enabled: projectId !== '', }); const bindMutation = useMutation({ mutationFn: async () => { const existing = await listServiceVariables(serviceId); const merged = existing .filter((v) => v.key !== varKey) .map((v) => ({ key: v.key, value: v.value, is_secret: v.isSecret })); merged.push({ key: varKey, value: connectionUrl, is_secret: true }); await updateServiceVariables(serviceId, merged); }, onSuccess: () => setBound(true), }); const services = servicesQuery.data ?? []; return (

Bind to service

Injects this database's connection URL as an environment variable on the selected service.

{bound && (

Bound — {varKey} is set on the service. Redeploy the service for it to take effect.

)} {bindMutation.isError && (

{bindMutation.error instanceof Error ? bindMutation.error.message : 'Bind failed'}

)}
); } function BackupScheduleRow({ databaseId, schedule, nextBackupAt, }: { databaseId: string; schedule: string; nextBackupAt?: string; }) { const queryClient = useQueryClient(); const [value, setValue] = useState(schedule); const mutation = useMutation({ mutationFn: (expr: string) => updateDatabaseBackupSchedule(databaseId, expr), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['databases'] }), }); return (
setValue(e.target.value)} placeholder="cron schedule, e.g. 0 3 * * *" className="w-44 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-2 py-1.5 text-xs mono text-[var(--text-primary)]" /> {schedule && ( )} {schedule ? `auto-backup ${schedule}${nextBackupAt ? ` — next ${formatRelative(nextBackupAt)}` : ''}` : 'manual only'} {mutation.isError && ( {mutation.error instanceof Error ? mutation.error.message : 'Failed to save schedule'} )}
); } const failoverStrategies = [ { value: 'active_passive', label: 'Active / passive' }, { value: 'active_active', label: 'Active / active' }, { value: 'graceful', label: 'Graceful' }, ] as const; export function HighAvailabilityPage() { const queryClient = useQueryClient(); const isDemoMode = useDemoMode(); const sessionQuery = useAuthSession({ enabled: !isDemoMode }); const profileQuery = useQuery({ queryKey: ['user-profile'], queryFn: getCurrentUserProfile, enabled: !isDemoMode && Boolean(sessionQuery.data), retry: false, }); const isAdmin = !isDemoMode && Boolean(profileQuery.data?.isAdmin); const [failoverReason, setFailoverReason] = useState(''); const [policyForm, setPolicyForm] = useState<{ serviceId: string; strategy: string; minHealthyNodes: string; maxFailures: string; enabled: boolean; editing: boolean; } | null>(null); const [policyProjectId, setPolicyProjectId] = useState(''); const statusQuery = useQuery({ queryKey: ['ha-status'], queryFn: getHAStatus, refetchInterval: 15_000, enabled: !isDemoMode }); const policiesQuery = useQuery({ queryKey: ['ha-policies'], queryFn: listFailoverPolicies, enabled: !isDemoMode }); const alertsQuery = useQuery({ queryKey: ['ha-alerts'], queryFn: listActiveAlerts, refetchInterval: 15_000, enabled: !isDemoMode }); const healthQuery = useQuery({ queryKey: ['ha-health'], queryFn: listHealthResults, refetchInterval: 30_000, enabled: !isDemoMode }); const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects(), enabled: !isDemoMode }); const servicesQuery = useQuery({ queryKey: ['services', policyProjectId], queryFn: () => listServicesByProject(policyProjectId), enabled: Boolean(policyProjectId), }); const invalidate = () => { queryClient.invalidateQueries({ queryKey: ['ha-status'] }); queryClient.invalidateQueries({ queryKey: ['ha-policies'] }); queryClient.invalidateQueries({ queryKey: ['ha-alerts'] }); }; const toggleMutation = useMutation({ mutationFn: (enabled: boolean) => setHAEnabled(enabled), onSuccess: invalidate, }); const failoverMutation = useMutation({ mutationFn: () => triggerFailover(failoverReason.trim()), onSuccess: () => { setFailoverReason(''); invalidate(); }, }); const savePolicyMutation = useMutation({ mutationFn: async () => { if (!policyForm) return; await setFailoverPolicy({ service_id: policyForm.serviceId, enabled: policyForm.enabled, failover_strategy: policyForm.strategy, min_healthy_nodes: Number(policyForm.minHealthyNodes) || 1, max_failures: Number(policyForm.maxFailures) || 3, failover_timeout: 30e9, recovery_timeout: 60e9, }); }, onSuccess: () => { setPolicyForm(null); invalidate(); }, }); const deletePolicyMutation = useMutation({ mutationFn: (serviceId: string) => deleteFailoverPolicy(serviceId), onSuccess: invalidate, }); const resolveMutation = useMutation({ mutationFn: (alertId: string) => resolveAlert(alertId), onSuccess: invalidate, }); const status = isDemoMode ? demoHAStatus : statusQuery.data; const alerts = isDemoMode ? demoHAAlerts : alertsQuery.data ?? []; const policies = isDemoMode ? demoHAPolicies : policiesQuery.data ?? []; const healthResults = isDemoMode ? demoHAHealthResults : healthQuery.data ?? []; const pageError = statusQuery.error ?? policiesQuery.error ?? alertsQuery.error ?? healthQuery.error ?? toggleMutation.error ?? failoverMutation.error ?? savePolicyMutation.error ?? deletePolicyMutation.error ?? resolveMutation.error; return (
HA state feeds the platform security posture. Open Security →
{pageError && (
{pageError instanceof Error ? pageError.message : 'Request failed'}
)}
{isAdmin ? (

Manager controls

setFailoverReason(e.target.value)} placeholder="Manual failover" className="px-3 py-2 rounded-[var(--radius-md)] bg-[var(--surface-muted)] border border-[var(--border-subtle)] text-sm text-[var(--text-primary)] w-56" />
) : null}

Active alerts

{!isDemoMode && alertsQuery.isLoading ? (

Loading…

) : alerts.length === 0 ? (

No active alerts.

) : (
{alerts.map((alert) => (

{alert.message ?? alert.id}

{alert.severity ?? 'info'} · since {alert.starts_at ? formatRelative(alert.starts_at) : '—'}

{isAdmin ? ( ) : null}
))}
)}

Failover policies

{policyForm === null && isAdmin && ( )}
{policyForm !== null && (
{policyForm.editing ? (

{policyForm.serviceId}

) : ( <>
)}
setPolicyForm({ ...policyForm, minHealthyNodes: e.target.value })} className="w-full px-3 py-2 rounded-[var(--radius-md)] bg-[var(--surface-muted)] border border-[var(--border-subtle)] text-sm text-[var(--text-primary)]" />
setPolicyForm({ ...policyForm, maxFailures: e.target.value })} className="w-full px-3 py-2 rounded-[var(--radius-md)] bg-[var(--surface-muted)] border border-[var(--border-subtle)] text-sm text-[var(--text-primary)]" />
)} {!isDemoMode && policiesQuery.isLoading ? (

Loading…

) : policies.length === 0 ? (

No failover policies configured.

) : (
{policies.map((policy: FailoverPolicy) => (

{policy.service_id}

{policy.enabled ? 'enabled' : 'disabled'} · {policy.failover_strategy ?? 'default'} · min nodes {policy.min_healthy_nodes ?? '—'} · max failures {policy.max_failures ?? '—'}

{isAdmin ? ( <> {policy.enabled && ( )} ) : null}
))}
)}

Health check results

{!isDemoMode && healthQuery.isLoading ? (

Loading…

) : healthResults.length === 0 ? (

No health checks configured.

) : (
{healthResults.slice(0, 20).map((result, i) => (

{result.check_id}

{result.message || '—'}

{result.status ?? 'unknown'}

{result.timestamp ? formatRelative(result.timestamp) : ''}

))}
)}
); } const scanTypes = [ { value: 'dependency', label: 'Dependency' }, { value: 'configuration', label: 'Configuration' }, { value: 'comprehensive', label: 'Comprehensive' }, ] as const; const severityColor: Record = { critical: 'text-[var(--error)]', high: 'text-[var(--error)]', medium: 'text-[var(--warning)]', low: 'text-[var(--text-tertiary)]', }; export function SecurityPage() { const queryClient = useQueryClient(); const isDemoMode = useDemoMode(); const sessionQuery = useAuthSession({ enabled: !isDemoMode }); const signedIn = !isDemoMode && Boolean(sessionQuery.data); const [projectId, setProjectId] = useState(isDemoMode ? 'demo-project-core' : ''); const [scanType, setScanType] = useState('comprehensive'); const [scanServiceId, setScanServiceId] = useState(''); const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects(), enabled: !isDemoMode }); const projects = isDemoMode ? demoProjects : projectsQuery.data ?? []; const servicesQuery = useQuery({ queryKey: ['services', projectId], queryFn: () => listServicesByProject(projectId), enabled: !isDemoMode && Boolean(projectId), }); const services = isDemoMode ? getDemoServicesByProject(projectId) : servicesQuery.data ?? []; const metricsQuery = useQuery({ queryKey: ['security-metrics', projectId], queryFn: () => getSecurityMetrics(projectId), enabled: !isDemoMode && Boolean(projectId), refetchInterval: 15_000, }); const historyQuery = useQuery({ queryKey: ['security-history', projectId], queryFn: () => getSecurityHistory(projectId), enabled: !isDemoMode && Boolean(projectId), refetchInterval: 15_000, }); const vulnsQuery = useQuery({ queryKey: ['security-vulns', projectId], queryFn: () => listVulnerabilities(projectId), enabled: !isDemoMode && Boolean(projectId), }); const invalidate = () => { queryClient.invalidateQueries({ queryKey: ['security-metrics', projectId] }); queryClient.invalidateQueries({ queryKey: ['security-history', projectId] }); queryClient.invalidateQueries({ queryKey: ['security-vulns', projectId] }); }; const scanMutation = useMutation({ mutationFn: () => startSecurityScan({ project_id: projectId, service_id: scanServiceId || undefined, scan_type: scanType as 'dependency' | 'configuration' | 'comprehensive', }), onSuccess: invalidate, }); const vulnMutation = useMutation({ mutationFn: ({ id, status }: { id: string; status: 'open' | 'resolved' | 'ignored' }) => updateVulnerability(id, status), onSuccess: invalidate, }); const metrics = isDemoMode ? demoSecurityMetrics : metricsQuery.data; const scanHistory = isDemoMode ? demoSecurityScans.filter((s) => s.project_id === projectId) : historyQuery.data ?? []; const vulns = (isDemoMode ? demoVulnerabilities.filter((v) => v.project_id === projectId) : vulnsQuery.data ?? [] ).filter((v) => v.status !== 'resolved'); const pageError = projectsQuery.error ?? metricsQuery.error ?? historyQuery.error ?? vulnsQuery.error ?? scanMutation.error ?? vulnMutation.error; return (
Platform availability and failover live under High availability. Open HA →
{pageError && (
{pageError instanceof Error ? pageError.message : 'Request failed'}
)}
{projectId && ( <>
{signedIn ? ( ) : ( {isDemoMode ? 'Read-only in demo.' : 'Sign in to run scans.'} )} )}
{projectId && metrics && (
)} {projectId && (

Findings

{!isDemoMode && vulnsQuery.isLoading ? (

Loading…

) : vulns.length === 0 ? (

No open findings.

) : (
{vulns.map((v) => (
{v.severity}

{v.title}

{v.description}

{v.type} · {v.status} · found {v.found_at ? formatRelative(v.found_at) : '—'}

{v.status === 'open' && v.id && signedIn && (
)}
))}
)}
)} {projectId && (

Scan history

{!isDemoMode && historyQuery.isLoading ? (

Loading…

) : scanHistory.length === 0 ? (

No scans yet.

) : (
{scanHistory.map((scan) => (

{scan.scan_type} · {scan.findings_count ?? 0} findings

{scan.started_at ? formatRelative(scan.started_at) : '—'}

{scan.status}
))}
)}
)}
); }