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, pruneAgent, updateAgent, listBuilds, listGitProviders, listProjects, createAgentToken, listAgentTokens, revokeAgentToken, createUserToken, listUserTokens, revokeUserToken, listRegistries, createRegistry, deleteRegistry, type Registry, listDatabases, databaseAction, updateDatabaseBackupSchedule, createDatabaseBackup, restoreDatabaseBackup, createManagedDatabase, registerExternalDatabase, importDatabaseBackup, updateDatabase, listBackupTargets, createBackupTarget, deleteBackupTarget, testBackupTarget, testDatabaseConnection, testDatabaseConnectionByID, type ExternalDatabaseInput, listServicesByProject, listServiceVariables, updateServiceVariables, getGitHubAppInstallUrl, getGitHubAppStatus, createGitHubAppManifest, convertGitHubAppCode, connectGitHubApp, updateCurrentUserProfile, getPlatformSettings, updatePlatformSettings, type Branding, type BrandingInput, 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, listNotificationChannels, createNotificationChannel, deleteNotificationChannel, testNotificationChannel, type NotificationChannel, } 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, Upload, Bell, Send, } 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 pruneMutation = useMutation({ mutationFn: (id: string) => pruneAgent(id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['usage-agents'] }), }); const autoPruneMutation = useMutation({ mutationFn: ({ id, on }: { id: string; on: boolean }) => updateAgent(id, { auto_prune: on }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['usage-agents'] }), }); 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 apiBase = getApiBaseUrl().replace(/\/api\/v1$/, ''); const installCommand = `curl -fsSL ${apiBase}/api/agents/install.sh | bash -s -- --url ${apiBase} --token ${issuedToken?.token ?? ''}`; const sshCommand = `containr nodes add --ssh ${issuedToken?.token ? '' : ' # run on a host with the CLI'}`; 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}

{isAdmin && ( <> )}

{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'}

)}
{installCommand}
{sshCommand}

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'); } }; // Admins can self-provision the instance's GitHub App via the manifest // flow when GITHUB_APP_* env vars were never configured. const profileQuery = useQuery({ queryKey: ['user-profile'], queryFn: getCurrentUserProfile, retry: false }); const isAdmin = Boolean(profileQuery.data?.isAdmin); const ghAppQuery = useQuery({ queryKey: ['github-app-status'], queryFn: getGitHubAppStatus, enabled: isAdmin, retry: false, }); const provisionMutation = useMutation({ mutationFn: async () => { const { url, manifest } = await createGitHubAppManifest({ base_url: window.location.origin }); // GitHub requires a form POST carrying the manifest JSON. const form = document.createElement('form'); form.method = 'POST'; form.action = url; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'manifest'; input.value = manifest; form.appendChild(input); document.body.appendChild(form); form.submit(); }, }); const ghAppStatus = ghAppQuery.data; const showProvision = isAdmin && ghAppStatus && !ghAppStatus.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

{showProvision ? ( ) : ( )}
{installUrlError && (
{installUrlError}
)} {provisionMutation.isError && (
{provisionMutation.error instanceof Error ? provisionMutation.error.message : 'Failed to start GitHub App setup'}
)} {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'}
)}
)}
); } // GitHub redirects here after the admin completes the manifest form — // /git/github-app/callback?code=…&state=… export function GitHubAppCallbackPage() { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const queryClient = useQueryClient(); const code = searchParams.get('code'); const convertMutation = useMutation({ mutationFn: (appCode: string) => convertGitHubAppCode(appCode), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['github-app-status'] }); // The setup wizard stashes a return marker before submitting the // manifest form — resume there instead of the settings page. const inSetup = sessionStorage.getItem('containr.setup_return') === '1'; sessionStorage.removeItem('containr.setup_return'); navigate(inSetup ? '/setup' : '/settings', { replace: true }); }, }); const firedRef = useRef(false); useEffect(() => { if (code && !firedRef.current) { firedRef.current = true; convertMutation.mutate(code); } // eslint-disable-next-line react-hooks/exhaustive-deps -- fire once per code }, [code]); return (
{!code ? ( <>

Missing code

GitHub did not return an app code. Start the setup again from Git providers.

) : convertMutation.isError ? ( <>

Setup failed

{convertMutation.error instanceof Error ? convertMutation.error.message : 'Could not provision the GitHub App'}

) : ( <>

Setting up GitHub App

Exchanging the app credentials…

)}
); } function PlatformSettingsSection() { const queryClient = useQueryClient(); const settingsQuery = useQuery({ queryKey: ['platform-settings'], queryFn: getPlatformSettings, }); const [signupDraft, setSignupDraft] = useState(null); const [tokenDraft, setTokenDraft] = useState(''); const [brandDraft, setBrandDraft] = 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); queryClient.invalidateQueries({ queryKey: ['branding'] }); setSignupDraft(null); setTokenDraft(''); setBrandDraft({}); 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
Branding leave blank to keep Containr defaults

White-label this instance — product name, logo, accent color and links apply to the shell, login page and document title.

{( [ ['productName', 'Product name'], ['logoUrl', 'Logo URL'], ['faviconUrl', 'Favicon URL'], ['accentColor', 'Accent color (#hex)'], ['docsUrl', 'Docs URL'], ['supportUrl', 'Support URL'], ] as const ).map(([field, label]) => ( ))}
{Object.keys(brandDraft).length > 0 ? ( ) : null}
{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)}` : ''}
))}
)}
); } function RegistriesSection() { const queryClient = useQueryClient(); const registriesQuery = useQuery({ queryKey: ['registries'], queryFn: listRegistries, }); const [name, setName] = useState(''); const [host, setHost] = useState(''); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const createMutation = useMutation({ mutationFn: () => createRegistry({ name: name.trim(), host: host.trim(), username: username.trim() || undefined, password: password || undefined, }), onSuccess: () => { setName(''); setHost(''); setUsername(''); setPassword(''); queryClient.invalidateQueries({ queryKey: ['registries'] }); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => deleteRegistry(id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['registries'] }), }); const registries = registriesQuery.data ?? []; return (

Registry credentials

Pull private images on deploy - matched by image host, stored encrypted

setName(e.target.value)} placeholder="Name (e.g. ghcr)" className="h-9 flex-1 min-w-[120px] 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" /> setHost(e.target.value)} placeholder="Host (ghcr.io, docker.io)" className="h-9 flex-1 min-w-[160px] 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" /> setUsername(e.target.value)} placeholder="Username" className="h-9 w-40 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" /> setPassword(e.target.value)} placeholder="Password / token" className="h-9 w-44 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" />
{createMutation.isError ? (

{createMutation.error instanceof Error ? createMutation.error.message : 'Failed to add registry'}

) : null} {registriesQuery.isLoading ? (
) : registries.length === 0 ? (

No credentials. Image pulls are anonymous unless a registry matching the image host exists here.

) : (
{registries.map((r: Registry) => (
{r.name} {r.host} {r.username ? {r.username} : null} {r.has_password ? 'password set' : 'anonymous'}
))}
)}
); } function BackupTargetsSection() { const queryClient = useQueryClient(); const targetsQuery = useQuery({ queryKey: ['backup-targets'], queryFn: listBackupTargets, }); const [form, setForm] = useState({ name: '', endpoint: '', bucket: '', region: '', prefix: '', accessKey: '', secretKey: '', noTls: false }); const [testResult, setTestResult] = useState>({}); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['backup-targets'] }); const createMutation = useMutation({ mutationFn: () => createBackupTarget({ name: form.name.trim(), endpoint: form.endpoint.trim(), bucket: form.bucket.trim(), region: form.region.trim() || undefined, prefix: form.prefix.trim() || undefined, access_key: form.accessKey || undefined, secret_key: form.secretKey || undefined, use_tls: !form.noTls, }), onSuccess: () => { setForm({ name: '', endpoint: '', bucket: '', region: '', prefix: '', accessKey: '', secretKey: '', noTls: false }); invalidate(); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => deleteBackupTarget(id), onSuccess: invalidate, }); const testMutation = useMutation({ mutationFn: (id: string) => testBackupTarget(id), onSuccess: (result, id) => setTestResult((prev) => ({ ...prev, [id]: result })), onError: (error, id) => setTestResult((prev) => ({ ...prev, [id]: { ok: false, error: error instanceof Error ? error.message : 'probe failed' } })), }); const targets = targetsQuery.data ?? []; const fieldCls = 'h-9 flex-1 min-w-[130px] 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'; return (

Backup targets

S3-compatible destinations for database archives — keys stored encrypted

setForm((f) => ({ ...f, name: e.target.value }))} placeholder="Name (e.g. garage)" className={fieldCls} /> setForm((f) => ({ ...f, endpoint: e.target.value }))} placeholder="Endpoint host:port" className={fieldCls} /> setForm((f) => ({ ...f, bucket: e.target.value }))} placeholder="Bucket" className="h-9 w-36 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" /> setForm((f) => ({ ...f, accessKey: e.target.value }))} placeholder="Access key" className={fieldCls} /> setForm((f) => ({ ...f, secretKey: e.target.value }))} placeholder="Secret key" className={fieldCls} />
{createMutation.isError ? (

{createMutation.error instanceof Error ? createMutation.error.message : 'Failed to add target'}

) : null} {targetsQuery.isLoading ? (
) : targets.length === 0 ? (

No backup targets. Database archives stay in the local backups volume until a target is configured.

) : (
{targets.map((t) => (
{t.name} {t.endpoint}/{t.bucket}{t.prefix ? `/${t.prefix}` : ''} {t.has_credentials ? 'keys set' : 'anonymous'}{t.use_tls ? '' : ' · no TLS'} {testResult[t.id ?? ''] && ( {testResult[t.id ?? '']?.ok ? `reachable (${testResult[t.id ?? '']?.latency_ms ?? 0}ms)` : `unreachable — ${testResult[t.id ?? '']?.error ?? 'probe failed'}`} )}
))}
)}
); } function NotificationChannelsSection() { const queryClient = useQueryClient(); const channelsQuery = useQuery({ queryKey: ['notification-channels'], queryFn: listNotificationChannels, }); const [kind, setKind] = useState<'ntfy' | 'gotify'>('ntfy'); const [endpoint, setEndpoint] = useState(''); const [token, setToken] = useState(''); const [testResult, setTestResult] = useState>({}); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['notification-channels'] }); const createMutation = useMutation({ mutationFn: () => createNotificationChannel({ kind, endpoint: endpoint.trim(), token: token.trim() || undefined, }), onSuccess: () => { setEndpoint(''); setToken(''); invalidate(); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => deleteNotificationChannel(id), onSuccess: invalidate, }); const testMutation = useMutation({ mutationFn: (id: string) => testNotificationChannel(id), onSuccess: (_d, id) => setTestResult((prev) => ({ ...prev, [id]: 'ok' })), onError: (err, id) => setTestResult((prev) => ({ ...prev, [id]: err instanceof Error ? err.message : 'delivery failed', })), }); const channels = channelsQuery.data ?? []; return (

Push notifications

Relay notifications to a self-hosted ntfy or Gotify server

setEndpoint(e.target.value)} placeholder={kind === 'ntfy' ? 'https://ntfy.example.com/my-alerts' : 'https://gotify.example.com'} className="h-9 flex-1 min-w-[220px] 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" /> setToken(e.target.value)} placeholder={kind === 'ntfy' ? 'Access token (optional)' : 'App token'} className="h-9 w-48 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" />
{createMutation.isError ? (

{createMutation.error instanceof Error ? createMutation.error.message : 'Failed to add channel'}

) : null} {channelsQuery.isLoading ? (
) : channels.length === 0 ? (

No push channels. Notifications stay in-app until you add one.

) : (
{channels.map((ch: NotificationChannel) => (
{ch.kind} {ch.endpoint} {testResult[ch.id] === 'ok' ? ( delivered ) : testResult[ch.id] ? ( {testResult[ch.id]} ) : null}
))}
)}
); } 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 [createMode, setCreateMode] = useState<'managed' | 'external'>('managed'); const [createForm, setCreateForm] = useState({ name: '', type: 'postgresql', plan: 'hobby', region: 'local', public: false }); const [externalForm, setExternalForm] = useState({ name: '', type: 'postgresql', host: '', port: '', database: '', username: '', password: '', ssl: false }); const [externalProbe, setExternalProbe] = useState<{ ok: boolean; latency_ms?: number; error?: string } | null>(null); const [externalTestResult, setExternalTestResult] = useState>({}); const [createError, setCreateError] = useState(null); const databasesQuery = useQuery({ queryKey: ['databases'], queryFn: listDatabases, refetchInterval: 15_000, enabled: !isDemoMode, }); const backupTargetsQuery = useQuery({ queryKey: ['backup-targets'], queryFn: listBackupTargets, 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', public_port: createForm.public, }), onSuccess: () => { setCreateOpen(false); setCreateForm({ name: '', type: 'postgresql', plan: 'hobby', region: 'local', public: false }); setCreateError(null); invalidate(); }, onError: (error) => { setCreateError(error instanceof Error ? error.message : 'Failed to create database'); }, }); const externalPayload = (): ExternalDatabaseInput => ({ name: externalForm.name.trim(), type: externalForm.type, host: externalForm.host.trim(), port: Number(externalForm.port) || undefined, database: externalForm.database.trim() || undefined, username: externalForm.username.trim() || undefined, password: externalForm.password || undefined, ssl: externalForm.ssl, }); const registerExternalMutation = useMutation({ mutationFn: () => registerExternalDatabase(externalPayload()), onSuccess: () => { setCreateOpen(false); setExternalForm({ name: '', type: 'postgresql', host: '', port: '', database: '', username: '', password: '', ssl: false }); setExternalProbe(null); setCreateError(null); invalidate(); }, onError: (error) => { setCreateError(error instanceof Error ? error.message : 'Failed to register database'); }, }); const probeMutation = useMutation({ mutationFn: () => testDatabaseConnection(externalPayload()), onSuccess: (result) => setExternalProbe(result), onError: (error) => setExternalProbe({ ok: false, error: error instanceof Error ? error.message : 'probe failed' }), }); const publicPortMutation = useMutation({ mutationFn: ({ id, publicPort }: { id: string; publicPort: boolean }) => updateDatabase(id, { public_port: publicPort }), onSuccess: () => window.setTimeout(invalidate, 1500), }); const backupTargetMutation = useMutation({ mutationFn: ({ id, targetId }: { id: string; targetId: string }) => updateDatabase(id, { backup_target_id: targetId }), onSuccess: invalidate, }); const importBackupMutation = useMutation({ mutationFn: ({ id, file }: { id: string; file: File }) => importDatabaseBackup(id, file), onSuccess: invalidate, }); const testByIdMutation = useMutation({ mutationFn: (id: string) => testDatabaseConnectionByID(id), onSuccess: (result, id) => setExternalTestResult((prev) => ({ ...prev, [id]: result })), onError: (error, id) => setExternalTestResult((prev) => ({ ...prev, [id]: { ok: false, error: error instanceof Error ? error.message : 'probe failed' } })), }); 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 {db.provider === 'external' ? ( <> {externalTestResult[db.id ?? ''] && ( {externalTestResult[db.id ?? '']?.ok ? `reachable (${externalTestResult[db.id ?? '']?.latency_ms ?? 0}ms)` : `unreachable — ${externalTestResult[db.id ?? '']?.error ?? 'probe failed'}`} )} ) : ( <> )} {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}

))}
)} {db.provider !== 'external' && (

Backups

{signedIn ? (
{(backupTargetsQuery.data?.length ?? 0) > 0 && ( )}
) : null}
{backups.length === 0 ? (

No backups yet.

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

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

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

New database

{(['managed', 'external'] as const).map((mode) => ( ))}

{createMode === 'managed' ? 'Provision a managed database service.' : 'Register a database hosted outside Containr.'}

(createMode === 'managed' ? setCreateForm((f) => ({ ...f, name: e.target.value })) : setExternalForm((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)]" />
{createMode === 'external' ? ( <>
setExternalForm((f) => ({ ...f, host: e.target.value }))} placeholder="db.example.com" 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)]" />
setExternalForm((f) => ({ ...f, port: e.target.value }))} placeholder="default" 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)]" />
setExternalForm((f) => ({ ...f, database: e.target.value }))} placeholder="optional" 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)]" />
setExternalForm((f) => ({ ...f, username: e.target.value }))} placeholder="optional" 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)]" />
setExternalForm((f) => ({ ...f, password: e.target.value }))} placeholder="optional" 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)]" />
{externalProbe && ( {externalProbe.ok ? `reachable (${externalProbe.latency_ms ?? 0}ms)` : `unreachable — ${externalProbe.error ?? 'probe failed'}`} )}
) : ( <>
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}
))}
)}
)}
); }