diff --git a/ui/src/pages/admin/DatabaseAdminPage.module.css b/ui/src/pages/admin/DatabaseAdminPage.module.css
new file mode 100644
index 00000000..944e0246
--- /dev/null
+++ b/ui/src/pages/admin/DatabaseAdminPage.module.css
@@ -0,0 +1,317 @@
+.page {
+ max-width: 960px;
+ margin: 0 auto;
+ padding: 32px 16px;
+}
+
+.pageTitle {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ margin-bottom: 24px;
+}
+
+.headerInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.headerMeta {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.metaItem {
+ font-size: 12px;
+ color: var(--text-muted);
+ font-family: var(--font-mono);
+}
+
+.globalRefresh {
+ padding: 8px 16px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border);
+ background: var(--bg-raised);
+ color: var(--text-secondary);
+ font-size: 13px;
+ cursor: pointer;
+ transition: all 0.15s;
+ white-space: nowrap;
+}
+
+.globalRefresh:hover {
+ border-color: var(--amber-dim);
+ color: var(--text-primary);
+}
+
+.loading {
+ text-align: center;
+ padding: 32px;
+ color: var(--text-muted);
+ font-size: 14px;
+}
+
+.accessDenied {
+ text-align: center;
+ padding: 64px 16px;
+ color: var(--text-muted);
+ font-size: 14px;
+}
+
+/* ─── Progress Bar ─── */
+.progressContainer {
+ margin-bottom: 16px;
+}
+
+.progressLabel {
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-bottom: 6px;
+}
+
+.progressPct {
+ font-weight: 600;
+ font-family: var(--font-mono);
+}
+
+.progressBar {
+ height: 8px;
+ background: var(--bg-raised);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.progressFill {
+ height: 100%;
+ border-radius: 4px;
+ transition: width 0.3s ease;
+}
+
+/* ─── Metrics Grid ─── */
+.metricsGrid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 12px;
+}
+
+.metric {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 12px;
+ background: var(--bg-raised);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+}
+
+.metricValue {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ font-family: var(--font-mono);
+}
+
+.metricLabel {
+ font-size: 11px;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-top: 4px;
+}
+
+/* ─── Tables ─── */
+.tableWrapper {
+ overflow-x: auto;
+}
+
+.table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+}
+
+.table th {
+ text-align: left;
+ padding: 8px 12px;
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--text-muted);
+ border-bottom: 1px solid var(--border-subtle);
+ white-space: nowrap;
+}
+
+.table td {
+ padding: 8px 12px;
+ color: var(--text-secondary);
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.table tbody tr:hover {
+ background: var(--bg-hover);
+}
+
+.mono {
+ font-family: var(--font-mono);
+ font-size: 12px;
+}
+
+.queryCell {
+ max-width: 300px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: var(--font-mono);
+ font-size: 11px;
+}
+
+.rowWarning {
+ background: rgba(234, 179, 8, 0.06);
+}
+
+.killBtn {
+ padding: 4px 10px;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ border: 1px solid var(--rose-dim);
+ color: var(--rose);
+ font-size: 11px;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.killBtn:hover {
+ background: var(--rose-glow);
+}
+
+.emptyState {
+ text-align: center;
+ padding: 24px;
+ color: var(--text-muted);
+ font-size: 13px;
+}
+
+/* ─── Maintenance ─── */
+.maintenanceGrid {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.maintenanceBtn {
+ padding: 8px 16px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border);
+ background: var(--bg-raised);
+ color: var(--text-muted);
+ font-size: 13px;
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+/* ─── Thresholds ─── */
+.thresholdGrid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.thresholdField {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.thresholdLabel {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--text-muted);
+}
+
+.thresholdInput {
+ width: 100%;
+ background: var(--bg-base);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 8px 12px;
+ color: var(--text-primary);
+ font-family: var(--font-mono);
+ font-size: 13px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.thresholdInput:focus {
+ border-color: var(--amber-dim);
+ box-shadow: 0 0 0 3px var(--amber-glow);
+}
+
+.thresholdActions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.btnPrimary {
+ padding: 8px 20px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--amber);
+ background: var(--amber);
+ color: #0a0e17;
+ font-family: var(--font-body);
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.btnPrimary:hover {
+ background: var(--amber-hover);
+ border-color: var(--amber-hover);
+}
+
+.btnPrimary:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.successMsg {
+ font-size: 12px;
+ color: var(--green);
+}
+
+.errorMsg {
+ font-size: 12px;
+ color: var(--rose);
+}
+
+@media (max-width: 640px) {
+ .metricsGrid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .thresholdGrid {
+ grid-template-columns: 1fr;
+ }
+
+ .header {
+ flex-direction: column;
+ gap: 12px;
+ }
+}
diff --git a/ui/src/pages/admin/DatabaseAdminPage.tsx b/ui/src/pages/admin/DatabaseAdminPage.tsx
new file mode 100644
index 00000000..3edbff07
--- /dev/null
+++ b/ui/src/pages/admin/DatabaseAdminPage.tsx
@@ -0,0 +1,379 @@
+import { useState } from 'react';
+import { useAuthStore } from '../../auth/auth-store';
+import { StatusBadge } from '../../components/admin/StatusBadge';
+import { RefreshableCard } from '../../components/admin/RefreshableCard';
+import { ConfirmDeleteDialog } from '../../components/admin/ConfirmDeleteDialog';
+import {
+ useDatabaseStatus,
+ useDatabasePool,
+ useDatabaseTables,
+ useDatabaseQueries,
+ useKillQuery,
+} from '../../api/queries/admin/database';
+import { useThresholds, useSaveThresholds, type Thresholds } from '../../api/queries/admin/thresholds';
+import styles from './DatabaseAdminPage.module.css';
+
+export function DatabaseAdminPage() {
+ const roles = useAuthStore((s) => s.roles);
+
+ if (!roles.includes('ADMIN')) {
+ return (
+
+
+ Access Denied — this page requires the ADMIN role.
+
+
+ );
+ }
+
+ return ;
+}
+
+function DatabaseAdminContent() {
+ const status = useDatabaseStatus();
+ const pool = useDatabasePool();
+ const tables = useDatabaseTables();
+ const queries = useDatabaseQueries();
+ const thresholds = useThresholds();
+
+ if (status.isLoading) {
+ return (
+
+
Database Administration
+
Loading...
+
+ );
+ }
+
+ const db = status.data;
+
+ return (
+
+
+
+
Database Administration
+
+
+ {db?.version && {db.version}}
+ {db?.host && {db.host}}
+ {db?.schema && Schema: {db.schema}}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function PoolSection({
+ pool,
+ warningPct,
+ criticalPct,
+}: {
+ pool: ReturnType;
+ warningPct?: number;
+ criticalPct?: number;
+}) {
+ const data = pool.data;
+ if (!data) return null;
+
+ const usagePct = data.maxConnections > 0
+ ? Math.round((data.activeConnections / data.maxConnections) * 100)
+ : 0;
+ const barColor =
+ criticalPct && usagePct >= criticalPct ? '#ef4444'
+ : warningPct && usagePct >= warningPct ? '#eab308'
+ : '#22c55e';
+
+ return (
+ pool.refetch()}
+ isRefreshing={pool.isFetching}
+ autoRefresh
+ >
+
+
+ {data.activeConnections} / {data.maxConnections} connections
+ {usagePct}%
+
+
+
+
+
+ {data.activeConnections}
+ Active
+
+
+ {data.idleConnections}
+ Idle
+
+
+ {data.pendingConnections}
+ Pending
+
+
+ {data.maxWaitMillis}ms
+ Max Wait
+
+
+
+ );
+}
+
+function TablesSection({ tables }: { tables: ReturnType }) {
+ const data = tables.data;
+
+ return (
+ tables.refetch()}
+ isRefreshing={tables.isFetching}
+ >
+ {!data ? (
+ Loading...
+ ) : (
+
+
+
+
+ | Table |
+ Rows |
+ Data Size |
+ Index Size |
+
+
+
+ {data.map((t) => (
+
+ | {t.tableName} |
+ {t.rowEstimate.toLocaleString()} |
+ {t.dataSize} |
+ {t.indexSize} |
+
+ ))}
+
+
+
+ )}
+
+ );
+}
+
+function QueriesSection({
+ queries,
+ warningSeconds,
+}: {
+ queries: ReturnType;
+ warningSeconds?: number;
+}) {
+ const [killTarget, setKillTarget] = useState(null);
+ const killMutation = useKillQuery();
+ const data = queries.data;
+
+ const warningMs = (warningSeconds ?? 30) * 1000;
+
+ return (
+ queries.refetch()}
+ isRefreshing={queries.isFetching}
+ autoRefresh
+ >
+ {!data || data.length === 0 ? (
+ No active queries
+ ) : (
+
+
+
+
+ | PID |
+ Duration |
+ State |
+ Query |
+ |
+
+
+
+ {data.map((q) => (
+ warningMs ? styles.rowWarning : undefined}
+ >
+ | {q.pid} |
+ {formatDuration(q.durationMs)} |
+ {q.state} |
+
+ {q.query.length > 100 ? `${q.query.slice(0, 100)}...` : q.query}
+ |
+
+
+ |
+
+ ))}
+
+
+
+ )}
+ setKillTarget(null)}
+ onConfirm={() => {
+ if (killTarget !== null) {
+ killMutation.mutate(killTarget);
+ setKillTarget(null);
+ }
+ }}
+ resourceName={String(killTarget ?? '')}
+ resourceType="query (PID)"
+ />
+
+ );
+}
+
+function MaintenanceSection() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function ThresholdsSection({ thresholds }: { thresholds?: Thresholds }) {
+ const [form, setForm] = useState(null);
+ const saveMutation = useSaveThresholds();
+ const [status, setStatus] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
+
+ const current = form ?? thresholds;
+ if (!current) return null;
+
+ function update(key: keyof Thresholds, value: number) {
+ setForm((prev) => ({ ...(prev ?? thresholds!), [key]: value }));
+ }
+
+ async function handleSave() {
+ if (!form && !thresholds) return;
+ const data = form ?? thresholds!;
+ try {
+ await saveMutation.mutateAsync(data);
+ setStatus({ type: 'success', msg: 'Thresholds saved.' });
+ setTimeout(() => setStatus(null), 3000);
+ } catch {
+ setStatus({ type: 'error', msg: 'Failed to save thresholds.' });
+ }
+ }
+
+ return (
+
+
+
+
+ {status && (
+
+ {status.msg}
+
+ )}
+
+
+ );
+}
+
+function formatDuration(ms: number): string {
+ if (ms < 1000) return `${ms}ms`;
+ const s = Math.floor(ms / 1000);
+ if (s < 60) return `${s}s`;
+ const m = Math.floor(s / 60);
+ return `${m}m ${s % 60}s`;
+}