'use client' import { useCallback, useEffect, useState } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Panel, PanelContent, PanelHeader, PanelTitle } from '@/components/ui/panel' // #38 — direct DB CRUD for the admin. ponytail: deliberately unauthenticated (explicit request); // add auth before this is reachable beyond the dev host. interface AdminColumnInfo { name: string; type: string; pk: number } interface AdminTableInfo { name: string; columns: AdminColumnInfo[]; count: number } interface AdminRows { rows: Record[]; total: number } // Form state is all strings (one input per column); parsed on save against the column type. function parseValue(raw: string, col: AdminColumnInfo): number | string | null { const v = raw.trim() if (v === '' || v.toLowerCase() === 'null') return null if (/INT|REAL|FLOA|DOUB/.test(col.type)) { const n = Number(v) if (!Number.isFinite(n)) throw new Error(`'${col.name}' must be a number, got '${v}'`) return col.type.includes('INT') ? Math.trunc(n) : n } return v } function cellText(value: unknown): string { return value === null || value === undefined ? '—' : String(value) } export function AdminPanel({ onClose }: { onClose: () => void }) { const [tables, setTables] = useState(null) const [selected, setSelected] = useState(null) const [rows, setRows] = useState(null) // null = form closed; { key: null } = add row; { key: {...} } = edit that row. const [editing, setEditing] = useState<{ key: Record | null } | null>(null) const [form, setForm] = useState>({}) const [error, setError] = useState(null) const table = tables?.find((t) => t.name === selected) ?? null const refreshTables = useCallback(async () => { try { const res = await fetch('/api/admin/tables') if (!res.ok) throw new Error(`HTTP ${res.status}`) setTables((await res.json()) as AdminTableInfo[]) } catch (err) { setError(err instanceof Error ? err.message : String(err)) } }, []) const refreshRows = useCallback(async (name: string) => { try { const res = await fetch(`/api/admin/tables/${encodeURIComponent(name)}/rows`) if (!res.ok) throw new Error(`HTTP ${res.status}`) setRows((await res.json()) as AdminRows) } catch (err) { setError(err instanceof Error ? err.message : String(err)) } }, []) useEffect(() => { void refreshTables() }, [refreshTables]) useEffect(() => { if (!selected) return setRows(null) void refreshRows(selected) }, [selected, refreshRows]) const pkColumns = (t: AdminTableInfo): AdminColumnInfo[] => t.columns.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk) const rowKey = (t: AdminTableInfo, row: Record): Record => Object.fromEntries(pkColumns(t).map((c) => [c.name, Number(row[c.name])])) const openAdd = () => { if (!table) return setEditing({ key: null }) setForm(Object.fromEntries(table.columns.map((c) => [c.name, '']))) setError(null) } const openEdit = (row: Record) => { if (!table) return setEditing({ key: rowKey(table, row) }) setForm(Object.fromEntries(table.columns.map((c) => [c.name, row[c.name] == null ? '' : String(row[c.name])]))) setError(null) } const save = async () => { if (!table || !editing) return try { const payload: Record = {} for (const col of table.columns) payload[col.name] = parseValue(form[col.name] ?? '', col) const url = `/api/admin/tables/${encodeURIComponent(table.name)}/rows` const res = await fetch(url, { method: editing.key === null ? 'POST' : 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(editing.key === null ? payload : { key: editing.key, values: payload }), }) const body = (await res.json()) as { error?: string } & Record if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`) setEditing(null) setError(null) await Promise.all([refreshRows(table.name), refreshTables()]) } catch (err) { setError(err instanceof Error ? err.message : String(err)) } } const remove = async (row: Record) => { if (!table) return try { const res = await fetch(`/api/admin/tables/${encodeURIComponent(table.name)}/rows`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: rowKey(table, row) }), }) const body = (await res.json()) as { error?: string } if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`) await Promise.all([refreshRows(table.name), refreshTables()]) } catch (err) { setError(err instanceof Error ? err.message : String(err)) } } const inputStyle: React.CSSProperties = { width: '100%', background: 'var(--background)', border: '1px solid var(--border)', color: 'var(--text-secondary)', fontFamily: 'inherit', fontSize: '0.68rem', padding: '0.3rem 0.4rem', outline: 'none', } return (
ADMIN // DB CRUD NO AUTH
{/* ── TABLE LIST ── */}
{!tables ? ( LOADING… ) : ( tables.map((t) => ( )) )}
{/* ── ROWS / EDITOR ── */}
{!table ? ( SELECT A TABLE ) : ( <>
{table.name} {rows && ( {rows.rows.length} / {rows.total} )}
{!editing && } {error && ( {error} )}
{editing && (
{table.columns.map((c) => ( ))}
)}
{!rows ? ( LOADING… ) : rows.rows.length === 0 ? ( EMPTY TABLE ) : ( {table.columns.map((c) => ( ))} {rows.rows.map((row) => ( {table.columns.map((c) => ( ))} ))}
{c.name} {c.pk > 0 ? ` (PK${c.pk})` : ''}
{cellText(row[c.name])}
)}
)}
) }