diff --git a/ui/src/components/admin/ConfirmDeleteDialog.module.css b/ui/src/components/admin/ConfirmDeleteDialog.module.css
new file mode 100644
index 00000000..481aea74
--- /dev/null
+++ b/ui/src/components/admin/ConfirmDeleteDialog.module.css
@@ -0,0 +1,103 @@
+.overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.dialog {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ padding: 24px;
+ width: 420px;
+ max-width: 90vw;
+}
+
+.title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0 0 12px;
+}
+
+.message {
+ font-size: 13px;
+ color: var(--text-secondary);
+ margin-bottom: 16px;
+ line-height: 1.5;
+}
+
+.label {
+ display: block;
+ font-size: 12px;
+ color: var(--text-muted);
+ margin-bottom: 6px;
+}
+
+.input {
+ width: 100%;
+ background: var(--bg-base);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 10px 14px;
+ color: var(--text-primary);
+ font-family: var(--font-mono);
+ font-size: 13px;
+ outline: none;
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.input:focus {
+ border-color: var(--amber-dim);
+ box-shadow: 0 0 0 3px var(--amber-glow);
+}
+
+.actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 16px;
+}
+
+.btnCancel {
+ padding: 8px 20px;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text-secondary);
+ font-family: var(--font-body);
+ font-size: 13px;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.btnCancel:hover {
+ border-color: var(--amber-dim);
+ color: var(--text-primary);
+}
+
+.btnDelete {
+ padding: 8px 20px;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ border: 1px solid var(--rose-dim);
+ color: var(--rose);
+ font-family: var(--font-body);
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.btnDelete:hover:not(:disabled) {
+ background: var(--rose-glow);
+}
+
+.btnDelete:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
diff --git a/ui/src/components/admin/ConfirmDeleteDialog.tsx b/ui/src/components/admin/ConfirmDeleteDialog.tsx
new file mode 100644
index 00000000..f81d2d92
--- /dev/null
+++ b/ui/src/components/admin/ConfirmDeleteDialog.tsx
@@ -0,0 +1,70 @@
+import { useState } from 'react';
+import styles from './ConfirmDeleteDialog.module.css';
+
+interface ConfirmDeleteDialogProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ resourceName: string;
+ resourceType: string;
+}
+
+export function ConfirmDeleteDialog({
+ isOpen,
+ onClose,
+ onConfirm,
+ resourceName,
+ resourceType,
+}: ConfirmDeleteDialogProps) {
+ const [confirmText, setConfirmText] = useState('');
+
+ if (!isOpen) return null;
+
+ const canDelete = confirmText === resourceName;
+
+ function handleClose() {
+ setConfirmText('');
+ onClose();
+ }
+
+ function handleConfirm() {
+ if (!canDelete) return;
+ setConfirmText('');
+ onConfirm();
+ }
+
+ return (
+
+
e.stopPropagation()}>
+
Confirm Deletion
+
+ Delete {resourceType} ‘{resourceName}’? This cannot be undone.
+
+
+
setConfirmText(e.target.value)}
+ placeholder={resourceName}
+ autoFocus
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/ui/src/components/admin/RefreshableCard.module.css b/ui/src/components/admin/RefreshableCard.module.css
new file mode 100644
index 00000000..15bf02a9
--- /dev/null
+++ b/ui/src/components/admin/RefreshableCard.module.css
@@ -0,0 +1,96 @@
+.card {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ margin-bottom: 16px;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.headerClickable {
+ cursor: pointer;
+ user-select: none;
+}
+
+.headerClickable:hover {
+ background: var(--bg-hover);
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
+}
+
+.titleRow {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.chevron {
+ font-size: 10px;
+ color: var(--text-muted);
+ transition: transform 0.2s;
+}
+
+.chevronOpen {
+ transform: rotate(90deg);
+}
+
+.title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.autoIndicator {
+ font-size: 10px;
+ color: var(--text-muted);
+ background: var(--bg-raised);
+ border: 1px solid var(--border);
+ border-radius: 99px;
+ padding: 1px 6px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.refreshBtn {
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-muted);
+ font-size: 16px;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.refreshBtn:hover {
+ border-color: var(--amber-dim);
+ color: var(--text-primary);
+}
+
+.refreshBtn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.refreshing {
+ animation: spin 1s linear infinite;
+}
+
+.body {
+ padding: 20px;
+}
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
diff --git a/ui/src/components/admin/RefreshableCard.tsx b/ui/src/components/admin/RefreshableCard.tsx
new file mode 100644
index 00000000..10e9ba80
--- /dev/null
+++ b/ui/src/components/admin/RefreshableCard.tsx
@@ -0,0 +1,70 @@
+import { type ReactNode, useState } from 'react';
+import styles from './RefreshableCard.module.css';
+
+interface RefreshableCardProps {
+ title: string;
+ onRefresh?: () => void;
+ isRefreshing?: boolean;
+ autoRefresh?: boolean;
+ collapsible?: boolean;
+ defaultCollapsed?: boolean;
+ children: ReactNode;
+}
+
+export function RefreshableCard({
+ title,
+ onRefresh,
+ isRefreshing,
+ autoRefresh,
+ collapsible,
+ defaultCollapsed,
+ children,
+}: RefreshableCardProps) {
+ const [collapsed, setCollapsed] = useState(defaultCollapsed ?? false);
+
+ const headerProps = collapsible
+ ? {
+ onClick: () => setCollapsed((c) => !c),
+ className: `${styles.header} ${styles.headerClickable}`,
+ role: 'button' as const,
+ tabIndex: 0,
+ onKeyDown: (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ setCollapsed((c) => !c);
+ }
+ },
+ }
+ : { className: styles.header };
+
+ return (
+
+
+
+ {collapsible && (
+
+ ▶
+
+ )}
+
{title}
+ {autoRefresh && auto}
+
+ {onRefresh && (
+
+ )}
+
+ {!collapsed &&
{children}
}
+
+ );
+}
diff --git a/ui/src/components/admin/StatusBadge.module.css b/ui/src/components/admin/StatusBadge.module.css
new file mode 100644
index 00000000..e2a4c9a6
--- /dev/null
+++ b/ui/src/components/admin/StatusBadge.module.css
@@ -0,0 +1,34 @@
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.healthy {
+ background: #22c55e;
+}
+
+.warning {
+ background: #eab308;
+}
+
+.critical {
+ background: #ef4444;
+}
+
+.unknown {
+ background: #6b7280;
+}
+
+.label {
+ font-size: 13px;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
diff --git a/ui/src/components/admin/StatusBadge.tsx b/ui/src/components/admin/StatusBadge.tsx
new file mode 100644
index 00000000..9d92f1ad
--- /dev/null
+++ b/ui/src/components/admin/StatusBadge.tsx
@@ -0,0 +1,17 @@
+import styles from './StatusBadge.module.css';
+
+export type Status = 'healthy' | 'warning' | 'critical' | 'unknown';
+
+interface StatusBadgeProps {
+ status: Status;
+ label?: string;
+}
+
+export function StatusBadge({ status, label }: StatusBadgeProps) {
+ return (
+
+
+ {label && {label}}
+
+ );
+}