import { useEffect, useMemo, useRef, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { getApiBaseUrl, getAgentPublicBaseUrl, getHostMonitoring, createUser, createGitProvider, deleteGitProvider, getCurrentUserProfile, listAuditLogs, listAgents, listBuilds, listGitProviders, listProjects, listTemplates, createAgentToken, listAgentTokens, revokeAgentToken, listDatabases, databaseAction, updateDatabaseBackupSchedule, createDatabaseBackup, restoreDatabaseBackup, listServicesByProject, listServiceVariables, updateServiceVariables, getGitHubAppInstallUrl, connectGitHubApp, updateCurrentUserProfile, type AgentAuthTokenCreated, type DatabaseEntity, } from '@/lib/api-client'; 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 } from '@/shared/components'; import { Clock, Activity, Gauge, Users, Shield, FileText, Settings, User, UserPlus, Key, Database, RefreshCw, Trash2, LogOut, Check, AlertCircle, Loader2, BookOpen, Folder, Box, Terminal, Radio, Server, HardDrive, GitBranch, Link2, Unlink, Cpu, MemoryStick, ScrollText, ChevronRight, ChevronDown, Copy, Play, Square, Archive, } 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 { if (typeof window === 'undefined') { return { canvasKeys: [], totalKeys: 0 }; } const canvasKeys: string[] = []; for (let index = 0; index < localStorage.length; index += 1) { const key = localStorage.key(index); if (key && key.startsWith('containr.canvas.v1.')) { canvasKeys.push(key); } } canvasKeys.sort((left, right) => left.localeCompare(right)); return { canvasKeys, totalKeys: localStorage.length, }; } function endpointStateBadge(isLoading: boolean, isError: boolean): { label: string; toneClass: string; } { if (isLoading) { return { label: 'Loading', toneClass: 'text-[var(--warn)]' }; } if (isError) { return { label: 'Unavailable', toneClass: 'text-[var(--bad)]' }; } return { label: 'Available', toneClass: 'text-[var(--ok)]' }; } 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 buildsQuery = useQuery({ queryKey: ['usage-builds'], queryFn: () => listBuilds({ page: 1, limit: 100 }), }); const hostQuery = useQuery({ queryKey: ['usage-host-monitoring'], queryFn: getHostMonitoring, refetchInterval: 15_000, }); const agentsQuery = useQuery({ queryKey: ['usage-agents'], queryFn: listAgents, refetchInterval: 15_000, }); const agentTokensQuery = useQuery({ queryKey: ['agent-tokens'], queryFn: listAgentTokens, }); 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 = buildsQuery.data?.builds ?? []; const liveStatus = useBuildUpdates( 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 = hostQuery.data; const agents = agentsQuery.data ?? []; // Accumulate host samples for the load sparkline (polled every 15s). const [loadHistory, setLoadHistory] = useState([]); useEffect(() => { const sample = hostQuery.data; if (!sample || sample.cpu.cores <= 0) { return; } const loadPercent = Math.min(100, (sample.load.load1m / sample.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)); }, [hostQuery.data]); 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 = (buildsQuery.data?.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); (buildsQuery.data?.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); }, [buildsQuery.data]); 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'}
{buildsQuery.isLoading ? (

Loading usage data...

) : buildsQuery.isError ? (

Failed to load usage data

) : (
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.'}

{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="#b4e34a" 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
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 && ( )}
))}
)}
)}
); } export function PeoplePage() { const queryClient = useQueryClient(); const profileQuery = useQuery({ queryKey: ['user-profile'], queryFn: getCurrentUserProfile, }); 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, }); const auditLogsQuery = useQuery({ queryKey: ['people-audit-logs'], queryFn: () => listAuditLogs({ page: 1, limit: 20 }), }); 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'}
)}
)}
); } export function SettingsPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const profileQuery = useQuery({ queryKey: ['settings-profile'], queryFn: getCurrentUserProfile, }); 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 = () => { for (const key of storageSummary.canvasKeys) { localStorage.removeItem(key); } 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(); }; 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
)}
)}
{/* 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() { const profileQuery = useQuery({ queryKey: ['docs-profile'], queryFn: getCurrentUserProfile, }); const projectsQuery = useQuery({ queryKey: ['docs-projects'], queryFn: listProjects, }); const templatesQuery = useQuery({ queryKey: ['docs-templates'], queryFn: () => listTemplates(), }); const buildsQuery = useQuery({ queryKey: ['docs-builds'], queryFn: () => listBuilds({ page: 1, limit: 1 }), }); const profileStatus = endpointStateBadge(profileQuery.isLoading, profileQuery.isError); const projectStatus = endpointStateBadge(projectsQuery.isLoading, projectsQuery.isError); const templateStatus = endpointStateBadge(templatesQuery.isLoading, templatesQuery.isError); const buildStatus = endpointStateBadge(buildsQuery.isLoading, buildsQuery.isError); return (
{/* API Status Grid */}
Profile

{profileStatus.label}

{profileQuery.data ? `${profileQuery.data.name} authenticated` : 'GET /user/profile'}

Projects

{projectStatus.label}

{projectsQuery.data ? `${projectsQuery.data.length} projects` : 'GET /projects'}

Templates

{templateStatus.label}

{templatesQuery.data ? `${templatesQuery.data.length} templates` : 'GET /templates'}

Builds

{buildStatus.label}

{buildsQuery.data ? `${buildsQuery.data.total} builds` : 'GET /builds'}

{/* Repository Paths */}

Repository Paths

Source files and documentation

API Contract

docs/api/openapi.yaml

Generated Types

app/frontend/src/generated/api-types.ts

Primary Guides

README.md • DOCKER_SETUP.md • docs/guides/

UI References

docs/references/dashboard.png • projects.png

{/* Common Commands */}

Common Commands

Development and build scripts

{`# Regenerate frontend API types
npm --prefix app/frontend run generate:api

# Frontend type-check + build
npm --prefix app/frontend run build:check

# Backend API tests
cd app/backend && go test ./internal/api/...

# Build remote node agent
cd app/backend && go build -o bin/containr-agent ./cmd/agent`}
              
Active API Base

{getApiBaseUrl()}

); } 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)]', }; export function DatabasesPage() { const queryClient = useQueryClient(); const [expandedId, setExpandedId] = useState(null); const [bindDb, setBindDb] = useState(null); const databasesQuery = useQuery({ queryKey: ['databases'], queryFn: listDatabases, refetchInterval: 15_000, }); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['databases'] }); 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 = databasesQuery.data ?? []; return (
{databasesQuery.isLoading ? (
) : databases.length === 0 ? (

No managed databases yet.

Create one via Add Service → Database in a project workspace.

) : (
{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 && ( )}
Actions {actionMutation.isError && ( {actionMutation.error instanceof Error ? actionMutation.error.message : 'Action failed'} )}
{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

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

)}
)}
); })}
)}
); } 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'} )}
); }