chore: preserve database management prototype

This commit is contained in:
Codex
2026-08-31 15:59:08 +02:00
parent 9c697dc062
commit ded66fde9c
17 changed files with 6377 additions and 0 deletions
+2
View File
@@ -4,6 +4,8 @@
**/__pycache__
**/.pytest_cache
**/dist
frontend/prototypes/
frontend/vite.database-management-prototype.config.ts
**/*.pyc
.git
.worktrees
+1
View File
@@ -4,6 +4,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"prototype:database-management": "vite --config vite.database-management-prototype.config.ts",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "vitest run",
@@ -0,0 +1,173 @@
import { useEffect, useMemo, useState } from "react";
import {
AlertTriangle,
CheckCircle2,
ExternalLink,
FlaskConical,
PlayCircle,
RotateCcw,
} from "lucide-react";
import { FALLBACK_SNAPSHOT, loadCatalogSnapshot, VARIANTS } from "./data";
import type { PanelKind, PrototypeState, ScenarioKey, VariantKey } from "./model";
import { ExplorerVariant } from "./variants/ExplorerVariant";
import { InventoryVariant } from "./variants/InventoryVariant";
import { OperationsVariant } from "./variants/OperationsVariant";
const VARIANT_COMPONENTS = { A: InventoryVariant, B: ExplorerVariant, C: OperationsVariant } as const;
const INITIAL_STATE: PrototypeState = {
scenario: "steady",
inventoryLevel: "databases",
selectedDatabaseId: FALLBACK_SNAPSHOT.databases[0].id,
selectedTableId: FALLBACK_SNAPSHOT.databases[0].tables[0]?.id ?? null,
selectedRowIds: [],
search: "",
panel: "none",
panelTargetId: null,
sensitiveDrafts: {},
};
function initialVariant(): VariantKey {
const candidate = new URLSearchParams(window.location.search).get("variant")?.toUpperCase();
return candidate === "B" || candidate === "C" ? candidate : "A";
}
function initialFrame(): boolean {
return new URLSearchParams(window.location.search).get("frame") !== "off";
}
function updateQuery(updates: Record<string, string | null>): void {
const url = new URL(window.location.href);
Object.entries(updates).forEach(([key, value]) => value === null ? url.searchParams.delete(key) : url.searchParams.set(key, value));
window.history.replaceState({}, "", url);
}
function isTypingTarget(target: EventTarget | null): boolean {
return target instanceof HTMLElement && target.matches("input, textarea, select, [contenteditable='true']");
}
const scenarioCopy: Record<ScenarioKey, { label: string; detail: string }> = {
steady: { label: "Live state", detail: "Current catalog snapshot" },
running: { label: "Sync preview", detail: "Running operation" },
changes: { label: "Diff preview", detail: "Destructive confirmation" },
};
export function PrototypeApp() {
const [variant, setVariant] = useState<VariantKey>(initialVariant);
const [framed, setFramed] = useState(initialFrame);
const [snapshot, setSnapshot] = useState(FALLBACK_SNAPSHOT);
const [catalogLoading, setCatalogLoading] = useState(true);
const [catalogError, setCatalogError] = useState<string | null>(null);
const [state, setState] = useState<PrototypeState>(INITIAL_STATE);
const [notice, setNotice] = useState<string | null>(null);
const databases = snapshot.databases;
const selectedDatabase = useMemo(
() => databases.find((database) => database.id === state.selectedDatabaseId) ?? databases[0],
[databases, state.selectedDatabaseId],
);
const selectedTable = useMemo(
() => selectedDatabase.tables.find((table) => table.id === state.selectedTableId) ?? selectedDatabase.tables[0] ?? null,
[selectedDatabase, state.selectedTableId],
);
useEffect(() => {
let active = true;
loadCatalogSnapshot().then((liveSnapshot) => {
if (!active || !liveSnapshot.databases.length) return;
setSnapshot(liveSnapshot);
setState((current) => {
const database = liveSnapshot.databases.find((item) => item.id === current.selectedDatabaseId) ?? liveSnapshot.databases[0];
const table = database.tables.find((item) => item.id === current.selectedTableId) ?? database.tables[0] ?? null;
return { ...current, selectedDatabaseId: database.id, selectedTableId: table?.id ?? null, selectedRowIds: [] };
});
setCatalogError(null);
}).catch((error: unknown) => {
if (active) setCatalogError(error instanceof Error ? error.message : "Live catalog unavailable");
}).finally(() => { if (active) setCatalogLoading(false); });
return () => { active = false; };
}, []);
useEffect(() => {
const onPopState = () => { setVariant(initialVariant()); setFramed(initialFrame()); };
window.addEventListener("popstate", onPopState);
return () => window.removeEventListener("popstate", onPopState);
}, []);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (isTypingTarget(event.target)) return;
const currentIndex = VARIANTS.findIndex((item) => item.key === variant);
let next: VariantKey | null = null;
if (event.key === "1") next = "A";
if (event.key === "2") next = "B";
if (event.key === "3") next = "C";
if (event.key === "ArrowRight") next = VARIANTS[(currentIndex + 1) % VARIANTS.length].key;
if (event.key === "ArrowLeft") next = VARIANTS[(currentIndex - 1 + VARIANTS.length) % VARIANTS.length].key;
if (!next) return;
event.preventDefault();
setVariant(next);
updateQuery({ variant: next });
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [variant]);
useEffect(() => {
if (!notice) return;
const timeout = window.setTimeout(() => setNotice(null), 3000);
return () => window.clearTimeout(timeout);
}, [notice]);
const chooseVariant = (next: VariantKey) => { setVariant(next); updateQuery({ variant: next }); };
const chooseDatabase = (databaseId: string) => {
const database = databases.find((item) => item.id === databaseId) ?? databases[0];
setState((current) => ({ ...current, selectedDatabaseId: database.id, selectedTableId: database.tables[0]?.id ?? null, selectedRowIds: [], search: "", panelTargetId: null }));
};
const chooseTable = (tableId: string) => setState((current) => ({ ...current, selectedTableId: tableId, selectedRowIds: [], search: "" }));
const toggleRow = (rowId: string) => setState((current) => ({ ...current, selectedRowIds: current.selectedRowIds.includes(rowId) ? current.selectedRowIds.filter((id) => id !== rowId) : [...current.selectedRowIds, rowId] }));
const chooseScenario = (scenario: ScenarioKey) => setState((current) => ({ ...current, scenario }));
const simulateAction = (message: string, scenario?: ScenarioKey) => { if (scenario) chooseScenario(scenario); setNotice(message); };
const openPanel = (panel: PanelKind, panelTargetId?: string) => setState((current) => ({ ...current, panel, panelTargetId: panelTargetId ?? null }));
const closePanel = () => setState((current) => ({ ...current, panel: "none", panelTargetId: null }));
const toggleSensitive = (columnId: string, value: boolean) => setState((current) => ({ ...current, sensitiveDrafts: { ...current.sensitiveDrafts, [columnId]: value } }));
const saveSensitive = () => {
const count = Object.keys(state.sensitiveDrafts).length;
setState((current) => ({ ...current, sensitiveDrafts: {}, panel: "none", panelTargetId: null }));
setNotice(`${count} sensitive-field decision${count === 1 ? "" : "s"} saved in prototype only`);
};
const resetState = () => {
const database = databases[0];
setState({ ...INITIAL_STATE, selectedDatabaseId: database.id, selectedTableId: database.tables[0]?.id ?? null });
};
const toggleFrame = () => { const next = !framed; setFramed(next); updateQuery({ frame: next ? null : "off" }); };
const VariantComponent = VARIANT_COMPONENTS[variant];
const activeVariant = VARIANTS.find((item) => item.key === variant) ?? VARIANTS[0];
const variantProps = {
snapshot, databases, state, selectedDatabase, selectedTable,
onSelectDatabase: chooseDatabase, onSelectTable: chooseTable, onToggleRow: toggleRow,
onClearSelection: () => setState((current) => ({ ...current, selectedRowIds: [] })),
onSearch: (search: string) => setState((current) => ({ ...current, search })),
onInventoryLevel: (inventoryLevel: PrototypeState["inventoryLevel"]) => setState((current) => ({ ...current, inventoryLevel, search: "", selectedRowIds: [], panel: "none", panelTargetId: null })),
onOpenPanel: openPanel, onClosePanel: closePanel, onToggleSensitive: toggleSensitive, onSaveSensitive: saveSensitive,
onScenario: chooseScenario, onAction: simulateAction,
};
return (
<div className={`prototype-shell ${framed ? "" : "prototype-shell--unframed"}`}>
{framed ? <aside className="prototype-rail" aria-label="Prototype controls">
<div className="prototype-identity"><div className="prototype-mark" aria-hidden="true"><FlaskConical size={17} /></div><div><p className="prototype-kicker">UI lab</p><strong>Fleet inventory</strong></div></div>
<section className="prototype-question"><span>Question</span><p>Where should real fleet actions, sensitive review, and operation history live?</p></section>
<nav className="prototype-variants" aria-label="Design hypotheses"><p className="prototype-nav-label">Fleet ideas</p>{VARIANTS.map((item) => <button key={item.key} type="button" data-tooltip={item.name} className="prototype-variant-button" aria-current={variant === item.key ? "page" : undefined} onClick={() => chooseVariant(item.key)}><span className="prototype-variant-key">{item.key}</span><span><strong>{item.name}</strong><small>{item.bestFor}</small></span></button>)}</nav>
<section className="prototype-scenarios" aria-labelledby="scenario-heading"><p id="scenario-heading" className="prototype-nav-label">Operation preview</p><button type="button" data-tooltip="Live state" aria-pressed={state.scenario === "steady"} onClick={() => chooseScenario("steady")}><CheckCircle2 size={15} /> Live state</button><button type="button" data-tooltip="Synchronization" aria-pressed={state.scenario === "running"} onClick={() => chooseScenario("running")}><PlayCircle size={15} /> Sync running</button><button type="button" data-tooltip="Destructive changes" aria-pressed={state.scenario === "changes"} onClick={() => chooseScenario("changes")}><AlertTriangle size={15} /> Destructive diff</button></section>
<section className="prototype-debug" aria-label="Current prototype state"><p className="prototype-nav-label">Current state</p><dl><div><dt>Source</dt><dd>{snapshot.source}</dd></div><div><dt>Database</dt><dd>{selectedDatabase.workspaceId}</dd></div><div><dt>Table</dt><dd>{selectedTable?.name ?? "None"}</dd></div><div><dt>View</dt><dd>{state.inventoryLevel}</dd></div><div><dt>Selected</dt><dd>{state.selectedRowIds.length}</dd></div><div><dt>Panel</dt><dd>{state.panel}</dd></div><div><dt>Status</dt><dd>{scenarioCopy[state.scenario].label}</dd></div></dl></section>
<div className="prototype-rail-actions"><button type="button" data-tooltip="Reset prototype" onClick={resetState}><RotateCcw size={14} /> Reset state</button><button type="button" data-tooltip="Unframed view" onClick={toggleFrame}><ExternalLink size={14} /> Open unframed</button></div>
<p className="prototype-footnote">Development only · {catalogLoading ? "loading live data" : catalogError ? "saved real snapshot" : "live local data"} · no API writes</p>
</aside> : <button className="prototype-restore" type="button" data-tooltip="UI lab controls" onClick={toggleFrame}><FlaskConical size={15} /> Show UI lab</button>}
<main className="prototype-stage" aria-label={`${activeVariant.name} prototype`}><VariantComponent {...variantProps} /></main>
{notice && <div className="prototype-toast" role="status">{notice}</div>}
</div>
);
}
@@ -0,0 +1,39 @@
# Database management UI prototype
> Throwaway, development-only prototype. It reads a secret-excluding metadata snapshot from the
> local catalog PostgreSQL container and never persists changes.
Question being tested: **where should the real database-management actions, sensitive-field
review, and operation histories live inside the Fleet inventory information architecture?**
Run it from `frontend/`:
```bash
npm run prototype:database-management
```
Then open <http://127.0.0.1:5173/?variant=A>. The left prototype rail switches among:
- `A`, Fleet Ledger: one dense grid at a time, selection actions inline, detail in a right drawer
- `B`, Fleet Workbench: the same grid beside a persistent contextual rail
- `C`, Fleet Operations Deck: the same grid with actions and history in a bottom console
Every variant uses the same live read-only dataset and the same real action vocabulary. Row actions
match production exactly; batch actions appear only after selection. Simulated actions, edits, and
sensitive decisions remain in memory and reset on reload. The current local catalog contains one
database, 163 tables, 2,275 columns, 5 sensitive flags, 0 relationships, and 21 sync runs. Description
and sensitive-suggestion histories are honestly empty because no runs are persisted yet.
The active variant is stored in the URL. Edit files in this directory while Vite is running to see
changes immediately through hot reload. Set `THT_PROTOTYPE_CATALOG_CONTAINER` before starting Vite
to read a different local catalog container; if the container is unavailable, a small saved snapshot
is displayed and labelled as such. The live snapshot excludes credentials and source rows, but it
does include catalog host/base URL metadata, comments, descriptions, and sanitized operation errors.
## Generated theme asset
All three Fleet ideas use `assets/thoth-cosmic-archive.png`, generated with the built-in ImageGen tool in
`stylized-concept` mode. Final prompt: a restrained wide fantasy mural of Thoth as guardian of a
celestial archive, with papyrus, stone, and constellation paths suggesting schema relationships;
twilight lapis-charcoal and warm porcelain palette; quiet central negative space; no text, logos,
watermarks, interface elements, neon, excessive gold, or busy detail behind the data surface.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

@@ -0,0 +1,215 @@
import type {
CatalogColumn,
CatalogDatabase,
CatalogRelationship,
CatalogRunEvent,
CatalogSnapshot,
CatalogSyncRun,
CatalogTable,
DescriptionGenerationRun,
SensitiveSuggestionRun,
VariantDefinition,
} from "./model";
export const VARIANTS: VariantDefinition[] = [
{
key: "A",
name: "Fleet ledger",
premise: "Dense inventory with selection actions inline and operational detail in a drawer.",
bestFor: "Fast catalog administration",
},
{
key: "B",
name: "Fleet workbench",
premise: "The inventory remains visible beside a persistent contextual action and history rail.",
bestFor: "Comparing data and consequences",
},
{
key: "C",
name: "Fleet operations deck",
premise: "A full-width inventory with actions, review, and run history in a bottom console.",
bestFor: "Long-running operational work",
},
];
interface RawSnapshot {
capturedAt: string;
databases: Array<Record<string, unknown>>;
tables: Array<Record<string, unknown>>;
columns: Array<Record<string, unknown>>;
relationships: Array<Record<string, unknown>>;
relationshipColumns: Array<Record<string, unknown>>;
syncRuns: Array<Record<string, unknown>>;
syncEvents: Array<Record<string, unknown>>;
descriptionRuns: Array<Record<string, unknown>>;
descriptionEvents: Array<Record<string, unknown>>;
sensitiveRuns: Array<Record<string, unknown>>;
sensitiveEvents: Array<Record<string, unknown>>;
}
const text = (value: unknown, fallback = "") => typeof value === "string" ? value : fallback;
const nullableText = (value: unknown) => typeof value === "string" && value.length ? value : null;
const number = (value: unknown, fallback = 0) => typeof value === "number" ? value : Number(value) || fallback;
const boolean = (value: unknown) => value === true || value === "true";
function titleWorkspace(value: string): string {
if (value === "psd-clinical") return "Policlinico San Donato";
return value.split(/[-_]/).map((part) => part.length <= 3 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
}
function transportLabel(value: string): CatalogDatabase["transport"] {
if (value === "rest_api") return "REST API";
if (value === "postgres_direct") return "Direct PostgreSQL";
if (value === "ssh_tunnel") return "SSH tunnel";
return "Unconfigured";
}
function runState(value: unknown): CatalogSyncRun["state"] {
const candidate = text(value) as CatalogSyncRun["state"];
return ["queued", "running", "awaiting_confirmation", "applying", "succeeded", "failed", "cancelled", "interrupted"].includes(candidate) ? candidate : "failed";
}
function normalizeEvents(rows: Array<Record<string, unknown>>): CatalogRunEvent[] {
return rows.map((row) => ({
runId: text(row.runId),
sequence: number(row.sequence),
level: (text(row.level, "info") as CatalogRunEvent["level"]),
message: text(row.message),
createdAt: text(row.createdAt),
}));
}
export function normalizeSnapshot(raw: RawSnapshot): CatalogSnapshot {
const columnsByTable = new Map<string, CatalogColumn[]>();
raw.columns.forEach((row) => {
const tableId = text(row.tableId);
const column: CatalogColumn = {
id: text(row.id), tableId, name: text(row.name), ordinalPosition: number(row.ordinalPosition),
dataType: text(row.dataType), isNullable: boolean(row.isNullable),
defaultExpression: nullableText(row.defaultExpression), primaryKeyPosition: row.primaryKeyPosition === null ? null : number(row.primaryKeyPosition),
sourceComment: nullableText(row.sourceComment), description: nullableText(row.description),
generatedDescription: nullableText(row.generatedDescription), sensitive: boolean(row.sensitive),
lastSyncedAt: nullableText(row.lastSyncedAt), version: number(row.version, 1),
};
const list = columnsByTable.get(tableId) ?? [];
list.push(column);
columnsByTable.set(tableId, list);
});
const tablesByDatabase = new Map<string, CatalogTable[]>();
raw.tables.forEach((row) => {
const id = text(row.id);
const databaseId = text(row.databaseId);
const columns = (columnsByTable.get(id) ?? []).sort((a, b) => a.ordinalPosition - b.ordinalPosition);
const described = columns.filter((column) => column.description || column.generatedDescription).length;
const table: CatalogTable = {
id, databaseId, name: text(row.name), sourceComment: nullableText(row.sourceComment),
description: nullableText(row.description), generatedDescription: nullableText(row.generatedDescription),
lastSyncedAt: nullableText(row.lastSyncedAt), version: number(row.version, 1), columns,
columnCount: columns.length, sensitiveCount: columns.filter((column) => column.sensitive).length,
descriptionCoverage: columns.length ? Math.round((described / columns.length) * 100) : 0,
};
const list = tablesByDatabase.get(databaseId) ?? [];
list.push(table);
tablesByDatabase.set(databaseId, list);
});
const relationshipColumns = new Map<string, CatalogRelationship["columns"]>();
raw.relationshipColumns.forEach((row) => {
const relationshipId = text(row.relationshipId);
const list = relationshipColumns.get(relationshipId) ?? [];
list.push({ sourceColumnName: text(row.sourceColumnName), targetColumnName: text(row.targetColumnName), ordinalPosition: number(row.ordinalPosition) });
relationshipColumns.set(relationshipId, list);
});
const relationshipsByDatabase = new Map<string, CatalogRelationship[]>();
raw.relationships.forEach((row) => {
const id = text(row.id);
const databaseId = text(row.databaseId);
const relationship: CatalogRelationship = {
id, databaseId, constraintName: text(row.constraintName), sourceTableName: text(row.sourceTableName),
targetTableName: text(row.targetTableName), updateRule: text(row.updateRule), deleteRule: text(row.deleteRule),
columns: relationshipColumns.get(id) ?? [],
};
const list = relationshipsByDatabase.get(databaseId) ?? [];
list.push(relationship);
relationshipsByDatabase.set(databaseId, list);
});
const syncRuns: CatalogSyncRun[] = raw.syncRuns.map((row) => ({
id: text(row.id), databaseId: text(row.databaseId), scope: text(row.scope, "all") as CatalogSyncRun["scope"],
state: runState(row.state), phase: text(row.phase), counts: (row.counts ?? {}) as Record<string, number>,
plannedDiff: (row.plannedDiff ?? null) as CatalogSyncRun["plannedDiff"], errorCode: nullableText(row.errorCode),
errorMessage: nullableText(row.errorMessage), createdAt: text(row.createdAt), startedAt: nullableText(row.startedAt),
finishedAt: nullableText(row.finishedAt),
}));
const databases = raw.databases.map((row): CatalogDatabase => {
const id = text(row.id);
const tables = (tablesByDatabase.get(id) ?? []).sort((a, b) => a.name.localeCompare(b.name));
const relationships = relationshipsByDatabase.get(id) ?? [];
const transport = transportLabel(text(row.transport));
const connectionStatus = text(row.connectionStatus);
const version = number(row.version, 1);
const testedVersion = row.testedVersion === null ? null : number(row.testedVersion);
const status = transport === "Unconfigured" ? "Not configured" : connectionStatus === "reachable" && testedVersion === version ? "Ready" : "Attention";
const describedTargets = tables.reduce((total, table) => total + (table.description || table.generatedDescription ? 1 : 0) + table.columns.filter((column) => column.description || column.generatedDescription).length, 0);
const totalTargets = tables.length + tables.reduce((total, table) => total + table.columns.length, 0);
const host = nullableText(row.host);
const baseUrl = nullableText(row.baseUrl);
const port = row.port === null ? null : number(row.port);
return {
id, displayName: titleWorkspace(text(row.workspaceId)), workspaceId: text(row.workspaceId), engine: text(row.engine),
databaseName: text(row.databaseName), schemaName: text(row.schemaName), version, transport,
endpoint: baseUrl ?? (host ? `${host}${port ? `:${port}` : ""}` : "No connection binding"),
connectionStatus, testedVersion, status, lastSync: nullableText(row.schemaSyncedAt), updatedAt: text(row.updatedAt),
tableCount: tables.length, columnCount: tables.reduce((total, table) => total + table.columns.length, 0),
relationshipCount: relationships.length, sensitiveCount: tables.reduce((total, table) => total + table.sensitiveCount, 0),
descriptionCoverage: totalTargets ? Math.round((describedTargets / totalTargets) * 100) : 0,
tables, relationships, activeSyncRun: syncRuns.find((run) => run.databaseId === id && ["queued", "running", "awaiting_confirmation", "applying"].includes(run.state)) ?? null,
};
});
const descriptionRuns = raw.descriptionRuns as unknown as DescriptionGenerationRun[];
const sensitiveRuns = raw.sensitiveRuns as unknown as SensitiveSuggestionRun[];
return {
source: "live", capturedAt: raw.capturedAt, databases, syncRuns,
syncEvents: normalizeEvents(raw.syncEvents), descriptionRuns, descriptionEvents: normalizeEvents(raw.descriptionEvents),
sensitiveRuns, sensitiveEvents: normalizeEvents(raw.sensitiveEvents),
};
}
export async function loadCatalogSnapshot(): Promise<CatalogSnapshot> {
const response = await fetch("/__prototype/catalog-snapshot", { headers: { Accept: "application/json" } });
if (!response.ok) throw new Error(`Live catalog returned ${response.status}`);
return normalizeSnapshot(await response.json() as RawSnapshot);
}
const fallbackColumns: CatalogColumn[] = [
{ id: "f1-cod-paz", tableId: "f1", name: "cod_paz", ordinalPosition: 1, dataType: "bigint", isNullable: false, defaultExpression: null, primaryKeyPosition: 1, sourceComment: null, description: null, generatedDescription: null, sensitive: true, lastSyncedAt: "2026-08-27T23:11:39Z", version: 1 },
{ id: "f1-num", tableId: "f1", name: "num", ordinalPosition: 2, dataType: "bigint", isNullable: false, defaultExpression: null, primaryKeyPosition: 2, sourceComment: null, description: null, generatedDescription: null, sensitive: false, lastSyncedAt: "2026-08-27T23:11:39Z", version: 1 },
{ id: "f1-note", tableId: "f1", name: "note", ordinalPosition: 8, dataType: "text", isNullable: true, defaultExpression: null, primaryKeyPosition: null, sourceComment: null, description: null, generatedDescription: null, sensitive: true, lastSyncedAt: "2026-08-27T23:11:39Z", version: 1 },
];
const fallbackTable: CatalogTable = {
id: "f1", databaseId: "psd-live", name: "fact_chiusura_defibrillatore_impiantabile",
sourceComment: null, description: null, generatedDescription: null, lastSyncedAt: "2026-08-27T23:11:39Z", version: 1,
columns: fallbackColumns, columnCount: 12, sensitiveCount: 3, descriptionCoverage: 0,
};
const fallbackRun: CatalogSyncRun = {
id: "sync-2026-08-27", databaseId: "psd-live", scope: "all", state: "succeeded", phase: "completed",
counts: { tables: 163, columns: 2275, relationships: 0, created: 0, updated: 0, deleted: 0 }, plannedDiff: null,
errorCode: null, errorMessage: null, createdAt: "2026-08-27T23:11:39Z", startedAt: "2026-08-27T23:11:39Z", finishedAt: "2026-08-27T23:11:40Z",
};
export const FALLBACK_SNAPSHOT: CatalogSnapshot = {
source: "fallback", capturedAt: "2026-08-27T23:11:40Z",
databases: [{
id: "psd-live", displayName: "Policlinico San Donato", workspaceId: "psd-clinical", engine: "postgres", databaseName: "postgres",
schemaName: "datawarehouse", version: 4, transport: "REST API", endpoint: "https://supabase-aritmolab.policlinicosandonato.it/dwh/",
connectionStatus: "reachable", testedVersion: 4, status: "Ready", lastSync: "2026-08-27T23:11:39Z", updatedAt: "2026-08-27T23:11:39Z",
tableCount: 163, columnCount: 2275, relationshipCount: 0, sensitiveCount: 5, descriptionCoverage: 0,
tables: [fallbackTable], relationships: [], activeSyncRun: null,
}],
syncRuns: [fallbackRun], syncEvents: [], descriptionRuns: [], descriptionEvents: [], sensitiveRuns: [], sensitiveEvents: [],
};
@@ -0,0 +1,469 @@
import { useMemo, useState, type ReactNode } from "react";
import {
Activity,
ArrowLeft,
CheckCircle2,
ChevronDown,
ChevronRight,
CircleAlert,
Clock3,
Columns3,
Database,
FileClock,
FileText,
History,
KeyRound,
LockKeyhole,
Pencil,
Plus,
RefreshCw,
Rows3,
Save,
Search,
Settings2,
ShieldCheck,
Sparkles,
Table2,
TestTube2,
Trash2,
Unlock,
X,
} from "lucide-react";
import type {
CatalogColumn,
CatalogDatabase,
CatalogSnapshot,
CatalogSyncRun,
CatalogTable,
InventoryLevel,
PanelKind,
VariantProps,
} from "./model";
import { Coverage, StatusBadge } from "./shared";
export function formatMoment(value: string | null): string {
if (!value) return "Never";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat("en-GB", {
day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit",
}).format(date);
}
function humanize(value: string): string {
return value.replaceAll("_", " ").replace(/^./, (letter) => letter.toUpperCase());
}
export function LiveCatalogBadge({ snapshot }: { snapshot: CatalogSnapshot }) {
const live = snapshot.source === "live";
return (
<span className={`live-catalog-badge${live ? "" : " live-catalog-badge--fallback"}`}>
<span className="live-catalog-dot" aria-hidden="true" />
<span><strong>{live ? "Live local catalog" : "Saved catalog snapshot"}</strong><small>Read only · captured {formatMoment(snapshot.capturedAt)}</small></span>
</span>
);
}
export function FleetHeader({ snapshot, variantLabel, onOpenPanel }: {
snapshot: CatalogSnapshot;
variantLabel: string;
onOpenPanel: VariantProps["onOpenPanel"];
}) {
return (
<header className="fleet-header">
<div className="fleet-title-block">
<p className="eyebrow">Thoth catalog · {variantLabel}</p>
<h1>Database management</h1>
<LiveCatalogBadge snapshot={snapshot} />
</div>
<div className="fleet-global-controls">
<button className="button" type="button" data-tooltip="Description history" onClick={() => onOpenPanel("description-history")}><FileClock size={15} /> Description history</button>
<button className="button" type="button" data-tooltip="Sensitive history" onClick={() => onOpenPanel("sensitive-history")}><ShieldCheck size={15} /> Sensitive history</button>
<button className="button button--primary" type="button" data-tooltip="Add database" onClick={() => onOpenPanel("database-form")}><Plus size={15} /> Add database</button>
</div>
</header>
);
}
export function FleetBreadcrumb({ level, database, table, onLevel }: {
level: InventoryLevel;
database: CatalogDatabase;
table: CatalogTable | null;
onLevel: (level: InventoryLevel) => void;
}) {
const parentLevel: InventoryLevel | null = level === "columns" ? "tables" : level === "tables" || level === "relationships" ? "databases" : null;
return (
<div className="fleet-path-row">
{parentLevel ? (
<button className="inventory-back-button" type="button" onClick={() => onLevel(parentLevel)} aria-label={`Back to ${parentLevel}`} data-tooltip={`Back to ${parentLevel}`}>
<ArrowLeft size={19} strokeWidth={2.4} />
</button>
) : null}
<nav className="inventory-breadcrumb" aria-label="Catalog hierarchy">
<button type="button" data-tooltip="Databases" aria-current={level === "databases" ? "page" : undefined} onClick={() => onLevel("databases")}>Databases</button>
{level !== "databases" ? <><ChevronRight size={13} /><button type="button" data-tooltip="Tables" aria-label={`View tables in ${database.displayName}`} aria-current={level === "tables" || level === "relationships" ? "page" : undefined} onClick={() => onLevel("tables")}>{database.displayName}</button></> : null}
{level === "columns" && table ? <><ChevronRight size={13} /><span aria-current="page">{table.name}</span></> : null}
</nav>
{level === "tables" || level === "relationships" ? (
<div className="database-object-tabs" role="group" aria-label="Database objects">
<button type="button" data-tooltip="Tables" aria-label={`Show tables in ${database.displayName}`} aria-pressed={level === "tables"} onClick={() => onLevel("tables")}><Table2 size={14} /> Tables</button>
<button type="button" data-tooltip="Relationships" aria-label={`Show relationships in ${database.displayName}`} aria-pressed={level === "relationships"} onClick={() => onLevel("relationships")}><Activity size={14} /> Relationships</button>
</div>
) : null}
</div>
);
}
interface RowActionProps {
label: string;
tooltip: string;
icon: ReactNode;
onClick: () => void;
disabled?: boolean;
destructive?: boolean;
}
function RowAction({ label, tooltip, icon, onClick, disabled = false, destructive = false }: RowActionProps) {
return (
<button className={`row-action-button${destructive ? " row-action-button--destructive" : ""}`} type="button" aria-label={label} data-tooltip={tooltip} disabled={disabled} onClick={onClick}>
{icon}
</button>
);
}
function RowActions({ label, children }: { label: string; children: ReactNode }) {
return <div className="row-action-group" role="group" aria-label={label}>{children}</div>;
}
export function FleetGrid(props: VariantProps) {
const {
databases, state, selectedDatabase, selectedTable, onSelectDatabase, onSelectTable,
onToggleRow, onSearch, onInventoryLevel, onOpenPanel, onAction, onToggleSensitive,
} = props;
const query = state.search.trim().toLocaleLowerCase();
const level = state.inventoryLevel;
const rows = useMemo(() => {
if (level === "databases") return databases.filter((database) => `${database.displayName} ${database.workspaceId} ${database.databaseName} ${database.schemaName} ${database.transport}`.toLocaleLowerCase().includes(query));
if (level === "tables") return selectedDatabase.tables.filter((table) => `${table.name} ${table.sourceComment ?? ""} ${table.description ?? ""}`.toLocaleLowerCase().includes(query));
if (level === "columns") return (selectedTable?.columns ?? []).filter((column) => `${column.name} ${column.dataType} ${column.sourceComment ?? ""}`.toLocaleLowerCase().includes(query));
return selectedDatabase.relationships.filter((relationship) => `${relationship.constraintName} ${relationship.sourceTableName} ${relationship.targetTableName}`.toLocaleLowerCase().includes(query));
}, [databases, level, query, selectedDatabase, selectedTable]);
const total = level === "databases" ? databases.length : level === "tables" ? selectedDatabase.tables.length : level === "columns" ? selectedTable?.columns.length ?? 0 : selectedDatabase.relationships.length;
const heading = level === "databases" ? "Workspace databases" : level === "tables" ? `Tables in ${selectedDatabase.displayName}` : level === "columns" ? `Columns in ${selectedTable?.name ?? "selected table"}` : `Relationships in ${selectedDatabase.displayName}`;
return (
<section className="fleet-grid-region" aria-labelledby="fleet-grid-heading">
<div className="fleet-grid-toolbar">
<div>
<p className="eyebrow">{level === "databases" ? "Fleet inventory" : level === "columns" ? `Table · ${selectedTable?.name ?? ""}` : `Workspace · ${selectedDatabase.workspaceId}`}</p>
<h2 id="fleet-grid-heading">{heading}</h2>
<span>{rows.length} of {total} objects shown</span>
</div>
<label className="search-field fleet-search">
<Search size={15} aria-hidden="true" />
<span className="sr-only">Search {level}</span>
<input value={state.search} onChange={(event) => onSearch(event.target.value)} placeholder={`Search ${level}`} />
</label>
</div>
<div className="table-scroll fleet-table-scroll">
{level === "databases" ? (
<table className="data-table fleet-data-table database-table">
<caption className="sr-only">Workspace databases</caption>
<thead><tr><th><span className="sr-only">Select</span></th><th>Workspace</th><th>Database</th><th>Schema</th><th>Transport</th><th>Endpoint</th><th>Status</th><th>Updated</th><th className="inventory-actions-heading">Actions</th></tr></thead>
<tbody>{(rows as CatalogDatabase[]).map((database) => <tr key={database.id} data-selected={state.selectedRowIds.includes(database.id) || undefined}>
<td><input type="checkbox" aria-label={`Select ${database.displayName}`} checked={state.selectedRowIds.includes(database.id)} onChange={() => onToggleRow(database.id)} /></td>
<td><span className="inventory-object-name"><span className="object-icon"><Database size={15} /></span><span><strong>{database.displayName}</strong><small>{database.workspaceId}</small></span></span></td>
<td><code>{database.databaseName}</code></td><td><code>{database.schemaName}</code></td><td>{database.transport}</td>
<td><span className="truncate-cell" title={database.endpoint}>{database.endpoint}</span></td><td><StatusBadge status={database.status} /></td>
<td>{formatMoment(database.updatedAt)}</td>
<td className="inventory-actions-cell"><RowActions label={`Actions for ${database.displayName}`}>
{database.activeSyncRun ? <RowAction label={`View synchronization for ${database.displayName}`} tooltip="Synchronization" icon={<RefreshCw size={15} />} onClick={() => { onSelectDatabase(database.id); onOpenPanel("sync-history", database.activeSyncRun?.id); }} /> : null}
<RowAction label={`View tables in ${database.displayName}`} tooltip="Tables" icon={<Rows3 size={15} />} onClick={() => { onSelectDatabase(database.id); onInventoryLevel("tables"); }} />
<RowAction label={`Edit ${database.displayName}`} tooltip="Edit database" icon={<Pencil size={15} />} onClick={() => { onSelectDatabase(database.id); onOpenPanel("database-form", database.id); }} />
<RowAction label={`Delete ${database.displayName}`} tooltip="Delete database" icon={<Trash2 size={15} />} destructive onClick={() => onAction(`Delete confirmation opened for ${database.displayName}`)} />
</RowActions></td>
</tr>)}</tbody>
</table>
) : null}
{level === "tables" ? (
<table className="data-table fleet-data-table catalog-table-table">
<caption className="sr-only">Catalog tables</caption>
<thead><tr><th><span className="sr-only">Select</span></th><th>Name</th><th>Source comment</th><th>Generated description</th><th>Description</th><th className="inventory-actions-heading">Actions</th></tr></thead>
<tbody>{(rows as CatalogTable[]).map((table) => <tr key={table.id} data-selected={state.selectedRowIds.includes(table.id) || undefined}>
<td><input type="checkbox" aria-label={`Select ${table.name}`} checked={state.selectedRowIds.includes(table.id)} onChange={() => onToggleRow(table.id)} /></td>
<td><span className="inventory-object-name"><span className="object-icon"><Table2 size={15} /></span><span><code>{table.name}</code><small>{table.columnCount} columns · {table.sensitiveCount} sensitive</small></span></span></td>
<td><span className="catalog-copy">{table.sourceComment ?? <span className="quiet-value">No source comment</span>}</span></td>
<td><span className="catalog-copy">{table.generatedDescription ?? <span className="quiet-value">Not generated</span>}</span></td>
<td><span className="catalog-copy">{table.description ?? <span className="quiet-value">Not curated</span>}</span></td>
<td className="inventory-actions-cell"><RowActions label={`Actions for table ${table.name}`}>
<RowAction label={`View columns in ${table.name}`} tooltip="Columns" icon={<Columns3 size={15} />} onClick={() => { onSelectTable(table.id); onInventoryLevel("columns"); }} />
<RowAction label={`Edit metadata for ${table.name}`} tooltip="Edit table metadata" icon={<Pencil size={15} />} onClick={() => { onSelectTable(table.id); onOpenPanel("metadata-editor", table.id); }} />
</RowActions></td>
</tr>)}</tbody>
</table>
) : null}
{level === "columns" ? (
<table className="data-table fleet-data-table catalog-column-table">
<caption className="sr-only">Catalog columns</caption>
<thead><tr><th><span className="sr-only">Select</span></th><th>#</th><th>Sensitive</th><th>Name</th><th>Type</th><th>Keys</th><th>Nullable</th><th>Source comment</th><th>Generated description</th><th>Description</th><th className="inventory-actions-heading">Actions</th></tr></thead>
<tbody>{(rows as CatalogColumn[]).map((column) => {
const sensitive = state.sensitiveDrafts[column.id] ?? column.sensitive;
return <tr key={column.id} data-selected={state.selectedRowIds.includes(column.id) || undefined}>
<td><input type="checkbox" aria-label={`Select ${column.name}`} checked={state.selectedRowIds.includes(column.id)} onChange={() => onToggleRow(column.id)} /></td>
<td>{column.ordinalPosition}</td>
<td><label className="sensitive-toggle"><input type="checkbox" checked={sensitive} onChange={(event) => onToggleSensitive(column.id, event.target.checked)} /><span className="sr-only">Sensitive flag for {column.name}</span></label></td>
<td><code>{column.name}</code></td>
<td><code className="type-token">{column.dataType}</code></td>
<td>{column.primaryKeyPosition ? <span className="column-key"><KeyRound size={13} /> PK {column.primaryKeyPosition}</span> : <span className="quiet-value">None</span>}</td>
<td>{column.isNullable ? "Yes" : "No"}</td>
<td><span className="catalog-copy">{column.sourceComment ?? <span className="quiet-value">No source comment</span>}</span></td>
<td><span className="catalog-copy">{column.generatedDescription ?? <span className="quiet-value">Not generated</span>}</span></td>
<td><span className="catalog-copy">{column.description ?? <span className="quiet-value">Not curated</span>}</span></td>
<td className="inventory-actions-cell"><RowActions label={`Actions for column ${column.name}`}>
<RowAction label={`Edit metadata for ${column.name}`} tooltip="Edit column metadata" icon={<Pencil size={15} />} onClick={() => onOpenPanel("metadata-editor", column.id)} />
</RowActions></td>
</tr>;
})}</tbody>
</table>
) : null}
{level === "relationships" ? (
<table className="data-table fleet-data-table relationship-table">
<caption className="sr-only">Catalog relationships</caption>
<thead><tr><th>Constraint</th><th>Source table</th><th>Source columns</th><th>Target table</th><th>Target columns</th><th>On update</th><th>On delete</th><th className="inventory-actions-heading">Actions</th></tr></thead>
<tbody>{selectedDatabase.relationships.length ? selectedDatabase.relationships.map((relationship) => <tr key={relationship.id}>
<td><code>{relationship.constraintName}</code></td><td>{relationship.sourceTableName}</td><td>{relationship.columns.map((pair) => pair.sourceColumnName).join(", ")}</td>
<td>{relationship.targetTableName}</td><td>{relationship.columns.map((pair) => pair.targetColumnName).join(", ")}</td><td>{relationship.updateRule}</td><td>{relationship.deleteRule}</td>
<td className="inventory-actions-cell"><span className="no-row-actions">No row actions</span></td>
</tr>) : <tr><td colSpan={8}><div className="fleet-empty-state"><Activity size={20} /><strong>No relationships in the live catalog</strong><span>The current PSD catalog contains 163 tables and 0 synchronized relationships.</span></div></td></tr>}</tbody>
</table>
) : null}
</div>
</section>
);
}
type ActionGroup = "Connection" | "Synchronization" | "Descriptions" | "Sensitive data" | "Cleanup";
type ActionDefinition = {
id: string;
group: ActionGroup;
label: string;
icon: ReactNode;
onClick: () => void;
executeLabel?: string;
destructive?: boolean;
disabled?: boolean;
};
const actionGroupOrder: ActionGroup[] = ["Connection", "Synchronization", "Descriptions", "Sensitive data", "Cleanup"];
export function FleetSelectionActions({ props, direction = "row" }: { props: VariantProps; direction?: "row" | "column" }) {
const { state, selectedDatabase, onAction, onOpenPanel, onClearSelection, onSaveSensitive } = props;
const count = state.selectedRowIds.length;
const disabled = count === 0;
const [selectedCommandId, setSelectedCommandId] = useState("");
const run = (message: string, scenario?: "running" | "changes") => () => onAction(message, scenario);
let actions: ActionDefinition[] = [];
if (state.inventoryLevel === "databases") actions = [
{ id: "databases-test", group: "Connection", label: "Test connections", icon: <TestTube2 size={14} />, disabled, onClick: run(`Testing ${count} selected connection${count === 1 ? "" : "s"}`) },
{ id: "databases-sync-tables", group: "Synchronization", label: "Synchronize tables", icon: <RefreshCw size={14} />, disabled, onClick: run("Table synchronization started", "running") },
{ id: "databases-sync-columns", group: "Synchronization", label: "Synchronize all columns", icon: <Columns3 size={14} />, disabled, onClick: run("Column synchronization started", "running") },
{ id: "databases-sync-relationships", group: "Synchronization", label: "Synchronize relationships", icon: <Activity size={14} />, disabled, onClick: run("Relationship synchronization started", "running") },
{ id: "databases-sync-all", group: "Synchronization", label: "Synchronize all", icon: <RefreshCw size={14} />, disabled, onClick: run("Full schema synchronization started", "running") },
{ id: "databases-generate-all", group: "Descriptions", label: "Generate all descriptions", icon: <Sparkles size={14} />, disabled, onClick: run("Generate all confirmation opened") },
{ id: "databases-generate-missing", group: "Descriptions", label: "Generate missing descriptions", icon: <FileText size={14} />, disabled, onClick: run("Missing-description generation started") },
{ id: "databases-suggest-sensitive", group: "Sensitive data", label: "Suggest sensitive fields", icon: <ShieldCheck size={14} />, executeLabel: "Open review", disabled: count !== 1, onClick: () => onOpenPanel("sensitive-review") },
{ id: "databases-delete-tables", group: "Cleanup", label: "Delete all catalog tables", icon: <Trash2 size={14} />, executeLabel: "Review deletion", destructive: true, disabled, onClick: run("Delete catalog tables confirmation opened", "changes") },
{ id: "databases-delete-relationships", group: "Cleanup", label: "Delete all relationships", icon: <Trash2 size={14} />, executeLabel: "Review deletion", destructive: true, disabled, onClick: run("Delete relationships confirmation opened", "changes") },
];
if (state.inventoryLevel === "tables") actions = [
{ id: "tables-generate", group: "Descriptions", label: "Generate descriptions", icon: <Sparkles size={14} />, disabled, onClick: run(`Description generation started for ${count} table${count === 1 ? "" : "s"}`) },
{ id: "tables-sync-columns", group: "Synchronization", label: "Synchronize columns", icon: <RefreshCw size={14} />, disabled, onClick: run("Selected table column synchronization started", "running") },
{ id: "tables-consolidate", group: "Descriptions", label: "Move generated to Description", icon: <Save size={14} />, disabled, onClick: run("Generated descriptions moved to curated Description") },
{ id: "tables-suggest-sensitive", group: "Sensitive data", label: "Suggest sensitive fields", icon: <ShieldCheck size={14} />, executeLabel: "Open review", disabled, onClick: () => onOpenPanel("sensitive-review") },
{ id: "tables-delete-columns", group: "Cleanup", label: "Delete all columns", icon: <Trash2 size={14} />, executeLabel: "Review deletion", destructive: true, disabled, onClick: run("Delete table columns confirmation opened", "changes") },
{ id: "tables-delete-relationships", group: "Cleanup", label: "Delete all relationships", icon: <Trash2 size={14} />, executeLabel: "Review deletion", destructive: true, disabled, onClick: run("Delete table relationships confirmation opened", "changes") },
];
if (state.inventoryLevel === "columns") actions = [
{ id: "columns-generate", group: "Descriptions", label: "Generate descriptions", icon: <Sparkles size={14} />, disabled, onClick: run(`Description generation started for ${count} column${count === 1 ? "" : "s"}`) },
{ id: "columns-consolidate", group: "Descriptions", label: "Move generated to Description", icon: <Save size={14} />, disabled, onClick: run("Generated column descriptions consolidated") },
{ id: "columns-suggest-sensitive", group: "Sensitive data", label: "Suggest sensitive fields", icon: <ShieldCheck size={14} />, executeLabel: "Open review", disabled, onClick: () => onOpenPanel("sensitive-review") },
{ id: "columns-save-sensitive", group: "Sensitive data", label: `Save sensitive fields (${Object.keys(state.sensitiveDrafts).length})`, icon: <LockKeyhole size={14} />, executeLabel: "Save changes", disabled: Object.keys(state.sensitiveDrafts).length === 0, onClick: onSaveSensitive },
];
if (state.inventoryLevel === "relationships") actions = [
{ id: "relationships-refresh", group: "Synchronization", label: "Refresh relationships", icon: <RefreshCw size={14} />, onClick: run("Relationships refreshed from the live catalog") },
{ id: "relationships-sync", group: "Synchronization", label: "Synchronize relationships", icon: <Activity size={14} />, onClick: run("Relationship synchronization started", "running") },
{ id: "relationships-sync-all", group: "Synchronization", label: "Synchronize all", icon: <RefreshCw size={14} />, onClick: run("Full schema synchronization started", "running") },
];
const selectedAction = actions.find((action) => action.id === selectedCommandId);
const selectedValue = selectedAction ? selectedCommandId : "";
const executeSelectedAction = () => {
if (!selectedAction || selectedAction.disabled) return;
selectedAction.onClick();
setSelectedCommandId("");
};
return (
<div className={`fleet-selection-actions fleet-selection-actions--${direction}`}>
<div className="selection-summary"><strong>{count ? `${count} selected` : "Select rows to act"}</strong><small>{count ? `${humanize(state.inventoryLevel)} scope · ${selectedDatabase.displayName}` : "Batch actions match the production DB management"}</small></div>
<div className="selection-command-picker">
<label className="selection-command-field">
<span>Command</span>
<select aria-label={`Command for ${humanize(state.inventoryLevel)}`} value={selectedValue} onChange={(event) => setSelectedCommandId(event.target.value)}>
<option value="">Choose a command…</option>
{actionGroupOrder.map((group) => {
const groupActions = actions.filter((action) => action.group === group);
return groupActions.length ? <optgroup key={group} label={group}>{groupActions.map((action) => <option key={action.id} value={action.id} disabled={action.disabled}>{action.label}</option>)}</optgroup> : null;
})}
</select>
<ChevronDown size={14} aria-hidden="true" />
</label>
<button
className={`selection-execute-button${selectedAction?.destructive ? " destructive-action" : ""}`}
type="button"
disabled={!selectedAction || selectedAction.disabled}
onClick={executeSelectedAction}
>
{selectedAction?.icon}
<span>{selectedAction?.executeLabel ?? "Run command"}</span>
</button>
</div>
{count ? <button className="clear-selection-button" type="button" onClick={() => { setSelectedCommandId(""); onClearSelection(); }}><X size={13} /> Clear</button> : null}
</div>
);
}
function PanelHeader({ eyebrow, title, onClose }: { eyebrow: string; title: string; onClose: () => void }) {
return <header className="fleet-panel-header"><div><p className="eyebrow">{eyebrow}</p><h2>{title}</h2></div><button type="button" aria-label="Close panel" data-tooltip="Close panel" onClick={onClose}><X size={17} /></button></header>;
}
function EmptyHistory({ kind, detail }: { kind: string; detail: string }) {
return <div className="history-empty"><History size={24} /><strong>No {kind} runs yet</strong><p>{detail}</p><span>0 live records</span></div>;
}
function SyncHistory({ snapshot, database, targetId, onAction }: { snapshot: CatalogSnapshot; database: CatalogDatabase; targetId: string | null; onAction: VariantProps["onAction"] }) {
const runs = snapshot.syncRuns.filter((run) => run.databaseId === database.id);
const [selectedId, setSelectedId] = useState(targetId ?? runs[0]?.id ?? "");
const selected = runs.find((run) => run.id === selectedId) ?? runs[0];
if (!selected) return <EmptyHistory kind="synchronization" detail="Run a schema synchronization to populate this history." />;
const events = snapshot.syncEvents.filter((event) => event.runId === selected.id).sort((a, b) => a.sequence - b.sequence);
const destructiveCount = (selected.plannedDiff?.deletedTables?.length ?? 0) + (selected.plannedDiff?.deletedColumns?.length ?? 0) + (selected.plannedDiff?.deletedRelationships?.length ?? 0);
return <div className="run-history-layout">
<section className="run-detail">
<div className="run-status-row"><span className={`run-state run-state--${selected.state}`}>{humanize(selected.state)}</span><span>{humanize(selected.scope)} scope</span></div>
<div className="run-count-grid">{["tables", "columns", "relationships", "created", "updated", "deleted"].map((key) => <div key={key}><span>{humanize(key)}</span><strong>{selected.counts[key] ?? 0}</strong></div>)}</div>
<dl className="run-timestamps"><div><dt>Started</dt><dd>{formatMoment(selected.startedAt ?? selected.createdAt)}</dd></div><div><dt>Finished</dt><dd>{formatMoment(selected.finishedAt)}</dd></div><div><dt>Phase</dt><dd>{humanize(selected.phase)}</dd></div></dl>
{destructiveCount ? <div className="destructive-summary"><CircleAlert size={16} /><span><strong>{destructiveCount} removals require confirmation</strong><small>The source diff has not been applied.</small></span><button type="button" onClick={() => onAction("Destructive catalog changes confirmed in prototype", "steady")}>Confirm removals</button></div> : null}
{selected.state === "running" || selected.state === "queued" ? <button className="button" type="button" onClick={() => onAction("Synchronization cancel requested in prototype", "steady")}>Cancel</button> : null}
{["failed", "cancelled", "interrupted"].includes(selected.state) ? <button className="button" type="button" onClick={() => onAction("Synchronization retry started in prototype", "running")}>Retry</button> : null}
<div className="event-log"><h3>Event log</h3>{events.length ? events.map((event) => <div key={`${event.runId}-${event.sequence}`} data-level={event.level}><span>{event.sequence}</span><code>{event.message}</code><time>{formatMoment(event.createdAt)}</time></div>) : <p>No persisted events for this run.</p>}</div>
</section>
<aside className="recent-runs"><h3>Recent runs</h3>{runs.slice(0, 10).map((run) => <button type="button" key={run.id} data-tooltip="View run" aria-label={`View ${humanize(run.scope).toLocaleLowerCase()} synchronization from ${formatMoment(run.createdAt)}`} aria-current={run.id === selected.id ? "true" : undefined} onClick={() => setSelectedId(run.id)}><span><strong>{humanize(run.scope)}</strong><small>{formatMoment(run.createdAt)}</small></span><span className={`run-state run-state--${run.state}`}>{humanize(run.state)}</span></button>)}</aside>
</div>;
}
function DescriptionHistory({ snapshot, onAction }: { snapshot: CatalogSnapshot; onAction: VariantProps["onAction"] }) {
if (!snapshot.descriptionRuns.length) return <><EmptyHistory kind="description generation" detail="The live catalog currently has no description-generation history. The empty state is real." /><button className="button history-unlock" type="button" onClick={() => onAction("Stale description-generation lock check requested") }><Unlock size={14} /> Unlock stale run</button></>;
return <div className="simple-history-list">{snapshot.descriptionRuns.map((run) => <article key={run.id}><div><span className={`run-state run-state--${run.status}`}>{humanize(run.status)}</span><strong>{humanize(run.scope)}</strong><small>{run.modelId} · {run.language}</small></div><div className="history-progress"><span>{run.processed}/{run.total} processed</span><progress max={Math.max(1, run.total)} value={run.processed} /><small>{run.generated} generated · {run.nonGeneratable} not generated · {run.failed} failed</small></div>{["queued", "running"].includes(run.status) ? <button type="button" onClick={() => onAction("Description generation stop requested")}>Stop</button> : null}</article>)}</div>;
}
function SensitiveHistory({ snapshot }: { snapshot: CatalogSnapshot }) {
if (!snapshot.sensitiveRuns.length) return <EmptyHistory kind="sensitive suggestion" detail="The live catalog currently has no sensitive-data suggestion history. The empty state is real." />;
return <div className="simple-history-list">{snapshot.sensitiveRuns.map((run) => <article key={run.id}><div><span className={`run-state run-state--${run.status}`}>{humanize(run.status)}</span><strong>{humanize(run.scope)}</strong><small>{run.modelId} · {formatMoment(run.createdAt)}</small></div><div className="run-count-grid"><div><span>Total</span><strong>{run.total}</strong></div><div><span>Sensitive</span><strong>{run.suggestedSensitive}</strong></div><div><span>Not sensitive</span><strong>{run.suggestedNonSensitive}</strong></div></div></article>)}</div>;
}
function SensitiveReview({ props }: { props: VariantProps }) {
const { selectedDatabase, selectedTable, state, onToggleSensitive, onSaveSensitive, onClosePanel } = props;
const [search, setSearch] = useState("");
const [showAll, setShowAll] = useState(false);
const selected = new Set(state.selectedRowIds);
const candidates = selectedDatabase.tables.flatMap((table) => table.columns.map((column) => ({ table, column }))).filter(({ table, column }) => {
const inScope = state.inventoryLevel === "databases"
? true
: state.inventoryLevel === "tables"
? (!selected.size ? table.id === selectedTable?.id : selected.has(table.id))
: state.inventoryLevel === "columns"
? (!selected.size ? table.id === selectedTable?.id : selected.has(column.id))
: true;
const changed = state.sensitiveDrafts[column.id] !== undefined;
return inScope && (showAll || column.sensitive || changed || /cod_paz|note|numero_serie|nome|email|telefono|indirizzo/i.test(column.name));
}).filter(({ table, column }) => `${table.name}.${column.name}`.toLocaleLowerCase().includes(search.toLocaleLowerCase())).slice(0, 80);
return <div className="sensitive-review-panel">
<div className="review-controls"><label><Search size={14} /><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search table or column" /></label><label className="show-all-toggle"><input type="checkbox" checked={showAll} onChange={(event) => setShowAll(event.target.checked)} /> Show all classified columns</label></div>
<div className="review-explainer"><ShieldCheck size={17} /><span><strong>Review suggested flags</strong><small>Actual catalog columns, prototype decisions. Nothing is written.</small></span></div>
<div className="sensitive-review-list">{candidates.map(({ table, column }) => {
const current = column.sensitive;
const proposed = state.sensitiveDrafts[column.id] ?? (current || /cod_paz|numero_serie|note/i.test(column.name));
return <label key={column.id}><input type="checkbox" checked={proposed} onChange={(event) => onToggleSensitive(column.id, event.target.checked)} /><span><code>{table.name}.{column.name}</code><small>{column.dataType} · {column.isNullable ? "nullable" : "required"}</small></span><span className="review-current">Current<strong>{current ? "Protected" : "Allowed"}</strong></span><span className="review-proposed">Proposed<strong>{proposed ? "Protect" : "Allow"}</strong></span></label>;
})}{!candidates.length ? <div className="fleet-empty-state"><ShieldCheck size={20} /><strong>No proposed changes in this view</strong></div> : null}</div>
<footer><button className="button" type="button" onClick={onClosePanel}>Cancel</button><button className="button button--primary" type="button" disabled={!Object.keys(state.sensitiveDrafts).length} onClick={onSaveSensitive}><Save size={14} /> Save {Object.keys(state.sensitiveDrafts).length || ""}</button></footer>
</div>;
}
function MetadataEditor({ props }: { props: VariantProps }) {
const { state, selectedDatabase, onAction } = props;
const table = selectedDatabase.tables.find((item) => item.id === state.panelTargetId);
const column = selectedDatabase.tables.flatMap((item) => item.columns).find((item) => item.id === state.panelTargetId);
const target = column ?? table;
if (!target) return <div className="fleet-empty-state"><FileText size={20} /><strong>Select a table or column to edit metadata</strong></div>;
return <form className="metadata-editor" onSubmit={(event) => { event.preventDefault(); onAction(`${column ? "Column" : "Table"} metadata saved in prototype`); }}>
<label><span>Physical name</span><input value={target.name} readOnly /><small>Physical fields are read only.</small></label>
<label><span>Source comment</span><textarea value={target.sourceComment ?? ""} readOnly rows={4} /></label>
<label><span>Generated description</span><textarea defaultValue={target.generatedDescription ?? ""} rows={4} placeholder="No generated description" /></label>
<label><span>Description</span><textarea defaultValue={target.description ?? ""} rows={5} placeholder="Add the curated human description" /></label>
<button className="button button--primary" type="submit"><Save size={14} /> Save metadata</button>
</form>;
}
function DatabaseForm({ database, onAction }: { database: CatalogDatabase; onAction: VariantProps["onAction"] }) {
return <form className="metadata-editor" onSubmit={(event) => { event.preventDefault(); onAction("Database configuration validated in prototype"); }}>
<label><span>Workspace</span><input defaultValue={database.workspaceId} /></label><label><span>Transport</span><select defaultValue={database.transport}><option>REST API</option><option>Direct PostgreSQL</option><option>SSH tunnel</option></select></label>
<label><span>Database</span><input defaultValue={database.databaseName} /></label><label><span>Schema</span><input defaultValue={database.schemaName} /></label><label><span>Endpoint</span><input defaultValue={database.endpoint} /></label>
<div className="prototype-write-warning"><LockKeyhole size={16} /><span><strong>Prototype, read-only</strong><small>Connection changes are not sent to the backend.</small></span></div>
<div className="form-actions"><button className="button" type="button" onClick={() => onAction("Connection test passed in prototype")}><TestTube2 size={14} /> Test connection</button><button className="button button--primary" type="submit"><Save size={14} /> Validate configuration</button></div>
</form>;
}
export function FleetPanel({ props, includeHeader = true }: { props: VariantProps; includeHeader?: boolean }) {
const { state, snapshot, selectedDatabase, onClosePanel, onAction } = props;
if (state.panel === "none" || state.panel === "actions") return null;
const titles: Record<Exclude<PanelKind, "none" | "actions">, [string, string]> = {
"sync-history": ["Operations", "Synchronization history"],
"description-history": ["Generation", "Description history"],
"sensitive-history": ["Classification", "Sensitive suggestion history"],
"sensitive-review": ["Human review", "Sensitive field review"],
"database-form": ["Connection", state.panelTargetId ? `Edit ${selectedDatabase.displayName}` : "Add database"],
"metadata-editor": ["Catalog metadata", "Review description"],
};
const [eyebrow, title] = titles[state.panel];
return <section className="fleet-panel" aria-label={title}>
{includeHeader ? <PanelHeader eyebrow={eyebrow} title={title} onClose={onClosePanel} /> : null}
<div className="fleet-panel-body">
{state.panel === "sync-history" ? <SyncHistory snapshot={snapshot} database={selectedDatabase} targetId={state.panelTargetId} onAction={onAction} /> : null}
{state.panel === "description-history" ? <DescriptionHistory snapshot={snapshot} onAction={onAction} /> : null}
{state.panel === "sensitive-history" ? <SensitiveHistory snapshot={snapshot} /> : null}
{state.panel === "sensitive-review" ? <SensitiveReview props={props} /> : null}
{state.panel === "metadata-editor" ? <MetadataEditor props={props} /> : null}
{state.panel === "database-form" ? <DatabaseForm database={selectedDatabase} onAction={onAction} /> : null}
</div>
</section>;
}
export function FleetSummary({ database }: { database: CatalogDatabase }) {
return <dl className="fleet-summary">
<div><dt>Tables</dt><dd>{database.tableCount.toLocaleString("en-GB")}</dd></div>
<div><dt>Columns</dt><dd>{database.columnCount.toLocaleString("en-GB")}</dd></div>
<div><dt>Sensitive</dt><dd>{database.sensitiveCount.toLocaleString("en-GB")}</dd></div>
<div><dt>Relationships</dt><dd>{database.relationshipCount.toLocaleString("en-GB")}</dd></div>
<div><dt>Description coverage</dt><dd><Coverage value={database.descriptionCoverage} /></dd></div>
</dl>;
}
export function FleetPanelShortcut({ icon, label, detail, onClick }: { icon: ReactNode; label: string; detail: string; onClick: () => void }) {
return <button className="fleet-panel-shortcut" type="button" data-tooltip={label} aria-label={`${label}: ${detail}`} onClick={onClick}><span>{icon}</span><span><strong>{label}</strong><small>{detail}</small></span><ChevronRight size={15} /></button>;
}
export function ActiveRunShortcut({ run, onClick }: { run: CatalogSyncRun | null; onClick: () => void }) {
return <FleetPanelShortcut icon={run ? <RefreshCw className="spin" size={16} /> : <CheckCircle2 size={16} />} label={run ? `${humanize(run.scope)} sync ${humanize(run.state)}` : "Synchronization history"} detail={run ? `Phase: ${humanize(run.phase)}` : "Inspect the 21 persisted local runs"} onClick={onClick} />;
}
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light" />
<title>Database management UI prototype</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&family=Fraunces:opsz,wght@9..144,500;9..144,600&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>
@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { PrototypeApp } from "./App";
import "./prototype.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<PrototypeApp />
</StrictMode>,
);
@@ -0,0 +1,198 @@
export type VariantKey = "A" | "B" | "C";
export type ScenarioKey = "steady" | "running" | "changes";
export type DatabaseStatus = "Ready" | "Attention" | "Not configured";
export type InventoryLevel = "databases" | "tables" | "columns" | "relationships";
export type PanelKind =
| "none"
| "actions"
| "sync-history"
| "description-history"
| "sensitive-history"
| "sensitive-review"
| "database-form"
| "metadata-editor";
export interface CatalogColumn {
id: string;
tableId: string;
name: string;
ordinalPosition: number;
dataType: string;
isNullable: boolean;
defaultExpression: string | null;
primaryKeyPosition: number | null;
sourceComment: string | null;
description: string | null;
generatedDescription: string | null;
sensitive: boolean;
lastSyncedAt: string | null;
version: number;
}
export interface CatalogTable {
id: string;
databaseId: string;
name: string;
sourceComment: string | null;
description: string | null;
generatedDescription: string | null;
lastSyncedAt: string | null;
version: number;
columns: CatalogColumn[];
columnCount: number;
sensitiveCount: number;
descriptionCoverage: number;
}
export interface CatalogRelationshipColumn {
sourceColumnName: string;
targetColumnName: string;
ordinalPosition: number;
}
export interface CatalogRelationship {
id: string;
databaseId: string;
constraintName: string;
sourceTableName: string;
targetTableName: string;
updateRule: string;
deleteRule: string;
columns: CatalogRelationshipColumn[];
}
export interface CatalogSyncRun {
id: string;
databaseId: string;
scope: "tables" | "columns" | "relationships" | "all";
state: "queued" | "running" | "awaiting_confirmation" | "applying" | "succeeded" | "failed" | "cancelled" | "interrupted";
phase: string;
counts: Record<string, number>;
plannedDiff: {
deletedTables?: string[];
deletedColumns?: Array<{ tableName: string; columnName: string }>;
deletedRelationships?: Array<{ sourceTableName: string; constraintName: string }>;
} | null;
errorCode: string | null;
errorMessage: string | null;
createdAt: string;
startedAt: string | null;
finishedAt: string | null;
}
export interface CatalogRunEvent {
runId: string;
sequence: number;
level: "info" | "warning" | "error";
message: string;
createdAt: string;
}
export interface DescriptionGenerationRun {
id: string;
databaseId: string;
scope: "selected_columns" | "selected_tables" | "all" | "missing";
modelId: string;
language: string;
status: "queued" | "running" | "completed" | "completed_with_errors" | "cancelled" | "failed" | "interrupted";
total: number;
processed: number;
generated: number;
nonGeneratable: number;
failed: number;
createdAt: string;
startedAt: string | null;
finishedAt: string | null;
errorSummary: string | null;
}
export interface SensitiveSuggestionRun {
id: string;
databaseId: string;
scope: "all" | "selected_tables" | "selected_columns";
modelId: string;
status: "running" | "completed" | "failed" | "interrupted";
total: number;
suggestedSensitive: number;
suggestedNonSensitive: number;
createdAt: string;
startedAt: string | null;
finishedAt: string | null;
errorSummary: string | null;
}
export interface CatalogDatabase {
id: string;
displayName: string;
workspaceId: string;
engine: string;
databaseName: string;
schemaName: string;
version: number;
transport: "REST API" | "Direct PostgreSQL" | "SSH tunnel" | "Unconfigured";
endpoint: string;
connectionStatus: string;
testedVersion: number | null;
status: DatabaseStatus;
lastSync: string | null;
updatedAt: string;
tableCount: number;
columnCount: number;
relationshipCount: number;
sensitiveCount: number;
descriptionCoverage: number;
tables: CatalogTable[];
relationships: CatalogRelationship[];
activeSyncRun: CatalogSyncRun | null;
}
export interface CatalogSnapshot {
source: "live" | "fallback";
capturedAt: string;
databases: CatalogDatabase[];
syncRuns: CatalogSyncRun[];
syncEvents: CatalogRunEvent[];
descriptionRuns: DescriptionGenerationRun[];
descriptionEvents: CatalogRunEvent[];
sensitiveRuns: SensitiveSuggestionRun[];
sensitiveEvents: CatalogRunEvent[];
}
export interface PrototypeState {
scenario: ScenarioKey;
inventoryLevel: InventoryLevel;
selectedDatabaseId: string;
selectedTableId: string | null;
selectedRowIds: string[];
search: string;
panel: PanelKind;
panelTargetId: string | null;
sensitiveDrafts: Record<string, boolean>;
}
export interface VariantProps {
snapshot: CatalogSnapshot;
databases: CatalogDatabase[];
state: PrototypeState;
selectedDatabase: CatalogDatabase;
selectedTable: CatalogTable | null;
onSelectDatabase: (databaseId: string) => void;
onSelectTable: (tableId: string) => void;
onToggleRow: (rowId: string) => void;
onClearSelection: () => void;
onSearch: (value: string) => void;
onInventoryLevel: (level: InventoryLevel) => void;
onOpenPanel: (panel: PanelKind, targetId?: string) => void;
onClosePanel: () => void;
onToggleSensitive: (columnId: string, value: boolean) => void;
onSaveSensitive: () => void;
onScenario: (scenario: ScenarioKey) => void;
onAction: (message: string, scenario?: ScenarioKey) => void;
}
export interface VariantDefinition {
key: VariantKey;
name: string;
premise: string;
bestFor: string;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,69 @@
import {
AlertTriangle,
CheckCircle2,
Clock3,
LoaderCircle,
Search,
} from "lucide-react";
import type { DatabaseStatus, ScenarioKey } from "./model";
export function StatusBadge({ status }: { status: DatabaseStatus }) {
const Icon = status === "Ready" ? CheckCircle2 : status === "Attention" ? AlertTriangle : Clock3;
return <span className={`status-badge status-badge--${status.toLowerCase().replace(" ", "-")}`}><Icon size={13} />{status}</span>;
}
export function SearchField({ value, onChange, placeholder = "Search catalog" }: {
value: string;
onChange: (value: string) => void;
placeholder?: string;
}) {
return (
<label className="search-field">
<Search size={15} aria-hidden="true" />
<span className="sr-only">{placeholder}</span>
<input value={value} onChange={(event) => onChange(event.target.value)} placeholder={placeholder} />
</label>
);
}
export function Coverage({ value }: { value: number }) {
return (
<span className="coverage" aria-label={`${value}% description coverage`}>
<span className="coverage-track"><span style={{ width: `${value}%` }} /></span>
<span>{value}%</span>
</span>
);
}
export function ScenarioNotice({ scenario, onAction }: {
scenario: ScenarioKey;
onAction: (message: string, scenario?: ScenarioKey) => void;
}) {
if (scenario === "steady") return null;
if (scenario === "running") {
return (
<div className="scenario-notice scenario-notice--running" role="status">
<LoaderCircle className="spin" size={17} />
<div><strong>Full database synchronization is running</strong><span>42% scanned · 18 of 42 tables · no catalog changes applied yet</span></div>
<button type="button" onClick={() => onAction("Prototype run stopped", "steady")}>Stop run</button>
</div>
);
}
return (
<div className="scenario-notice scenario-notice--changes" role="alert">
<AlertTriangle size={17} />
<div><strong>Source changes need confirmation</strong><span>2 tables removed · 4 columns changed · 3 relationships affected</span></div>
<button type="button" onClick={() => onAction("Diff review opened in prototype")}>Review changes</button>
</div>
);
}
export function EmptyDatabase() {
return (
<div className="empty-database">
<Clock3 size={22} />
<strong>No database configured</strong>
<p>Add a connection binding to inspect tables, relationships, and catalog operations.</p>
</div>
);
}
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"types": ["vite/client"]
},
"include": ["./**/*.ts", "./**/*.tsx"]
}
@@ -0,0 +1,185 @@
import {
Columns3,
Database,
FileClock,
Pencil,
RefreshCw,
Settings2,
ShieldCheck,
Table2,
} from "lucide-react";
import type { InventoryLevel, VariantProps } from "../model";
import {
ActiveRunShortcut,
FleetBreadcrumb,
FleetGrid,
FleetHeader,
FleetPanel,
FleetPanelShortcut,
FleetSelectionActions,
FleetSummary,
formatMoment,
} from "../fleet";
import { ScenarioNotice } from "../shared";
function contextCopy(level: InventoryLevel, props: VariantProps): {
eyebrow: string;
title: string;
detail: string;
icon: typeof Database;
} {
const { databases, selectedDatabase, selectedTable } = props;
if (level === "databases") {
return {
eyebrow: "Fleet context",
title: selectedDatabase.displayName,
detail: `${databases.length} repository workspace${databases.length === 1 ? "" : "s"} in the read-only catalog`,
icon: Database,
};
}
if (level === "tables") {
return {
eyebrow: `Workspace · ${selectedDatabase.workspaceId}`,
title: selectedDatabase.displayName,
detail: `${selectedDatabase.databaseName}.${selectedDatabase.schemaName} · ${selectedDatabase.tableCount.toLocaleString("en-GB")} tables`,
icon: Table2,
};
}
if (level === "columns") {
return {
eyebrow: `Table · ${selectedDatabase.workspaceId}`,
title: selectedTable?.name ?? "No table selected",
detail: selectedTable
? `${selectedTable.columnCount.toLocaleString("en-GB")} columns · last synchronized ${formatMoment(selectedTable.lastSyncedAt)}`
: "Choose a table from the parent inventory",
icon: Columns3,
};
}
return {
eyebrow: `Workspace · ${selectedDatabase.workspaceId}`,
title: "Physical relationships",
detail: `${selectedDatabase.relationshipCount.toLocaleString("en-GB")} synchronized constraints`,
icon: RefreshCw,
};
}
export function ExplorerVariant(props: VariantProps) {
const {
snapshot,
state,
selectedDatabase,
selectedTable,
onInventoryLevel,
onOpenPanel,
onAction,
} = props;
const panelOpen = state.panel !== "none" && state.panel !== "actions";
const context = contextCopy(state.inventoryLevel, props);
const ContextIcon = context.icon;
const syncRunCount = snapshot.syncRuns.filter((run) => run.databaseId === selectedDatabase.id).length;
return (
<div className="candidate candidate--explorer candidate--fleet-workbench">
<FleetHeader
snapshot={snapshot}
variantLabel="Fleet workbench"
onOpenPanel={onOpenPanel}
/>
<ScenarioNotice scenario={state.scenario} onAction={onAction} />
<section className="fleet-workbench">
<FleetBreadcrumb
level={state.inventoryLevel}
database={selectedDatabase}
table={selectedTable}
onLevel={onInventoryLevel}
/>
<div className="fleet-workbench-layout">
<main className="fleet-workbench-grid" aria-label="Fleet inventory">
<FleetGrid {...props} />
</main>
<aside className="fleet-workbench-rail" aria-label="Fleet context and actions">
<header className="fleet-workbench-context">
<span className="large-object-icon" aria-hidden="true"><ContextIcon size={20} /></span>
<div>
<p className="eyebrow">{context.eyebrow}</p>
<h2>{context.title}</h2>
<p>{context.detail}</p>
</div>
</header>
<FleetSummary database={selectedDatabase} />
{panelOpen ? (
<FleetPanel props={props} />
) : (
<>
<section className="fleet-workbench-actions" aria-labelledby="fleet-workbench-actions-heading">
<div className="fleet-workbench-section-heading">
<p className="eyebrow">Selection</p>
<h3 id="fleet-workbench-actions-heading">Batch actions</h3>
</div>
<FleetSelectionActions props={props} direction="column" />
</section>
<section className="fleet-workbench-shortcuts" aria-labelledby="fleet-workbench-shortcuts-heading">
<div className="fleet-workbench-section-heading">
<p className="eyebrow">Read-only operations</p>
<h3 id="fleet-workbench-shortcuts-heading">Inspect and review</h3>
</div>
{selectedDatabase.activeSyncRun ? (
<ActiveRunShortcut
run={selectedDatabase.activeSyncRun}
onClick={() => onOpenPanel("sync-history", selectedDatabase.activeSyncRun?.id)}
/>
) : (
<FleetPanelShortcut
icon={<RefreshCw size={16} />}
label="Synchronization history"
detail={`${syncRunCount} persisted run${syncRunCount === 1 ? "" : "s"} for this workspace`}
onClick={() => onOpenPanel("sync-history")}
/>
)}
<FleetPanelShortcut
icon={<FileClock size={16} />}
label="Description history"
detail={`${snapshot.descriptionRuns.length} persisted run${snapshot.descriptionRuns.length === 1 ? "" : "s"}`}
onClick={() => onOpenPanel("description-history")}
/>
<FleetPanelShortcut
icon={<ShieldCheck size={16} />}
label="Sensitive history"
detail={`${snapshot.sensitiveRuns.length} persisted run${snapshot.sensitiveRuns.length === 1 ? "" : "s"}`}
onClick={() => onOpenPanel("sensitive-history")}
/>
{state.inventoryLevel === "columns" && selectedTable ? (
<FleetPanelShortcut
icon={<Pencil size={16} />}
label="Edit table metadata"
detail={`Review generated and curated text for ${selectedTable.name}`}
onClick={() => onOpenPanel("metadata-editor", selectedTable.id)}
/>
) : (
<FleetPanelShortcut
icon={<Settings2 size={16} />}
label="Connection configuration"
detail={`${selectedDatabase.transport} · prototype changes are not persisted`}
onClick={() => onOpenPanel("database-form", selectedDatabase.id)}
/>
)}
</section>
</>
)}
</aside>
</div>
</section>
</div>
);
}
@@ -0,0 +1,41 @@
import { FileClock, ShieldCheck } from "lucide-react";
import type { VariantProps } from "../model";
import {
ActiveRunShortcut,
FleetBreadcrumb,
FleetGrid,
FleetHeader,
FleetPanel,
FleetPanelShortcut,
FleetSelectionActions,
FleetSummary,
} from "../fleet";
import { ScenarioNotice } from "../shared";
export function InventoryVariant(props: VariantProps) {
const { snapshot, state, selectedDatabase, selectedTable, onInventoryLevel, onOpenPanel, onAction } = props;
const panelOpen = state.panel !== "none" && state.panel !== "actions";
return (
<div className="candidate candidate--inventory candidate--fleet-ledger">
<FleetHeader snapshot={snapshot} variantLabel="Fleet ledger" onOpenPanel={onOpenPanel} />
<ScenarioNotice scenario={state.scenario} onAction={onAction} />
<section className="fleet-ledger-workbench">
<FleetBreadcrumb level={state.inventoryLevel} database={selectedDatabase} table={selectedTable} onLevel={onInventoryLevel} />
<div className="fleet-ledger-summary-row">
<FleetSummary database={selectedDatabase} />
<div className="fleet-ledger-shortcuts">
<ActiveRunShortcut run={selectedDatabase.activeSyncRun} onClick={() => onOpenPanel("sync-history", selectedDatabase.activeSyncRun?.id)} />
<FleetPanelShortcut icon={<FileClock size={16} />} label="Description history" detail={`${snapshot.descriptionRuns.length} persisted runs`} onClick={() => onOpenPanel("description-history")} />
<FleetPanelShortcut icon={<ShieldCheck size={16} />} label="Sensitive history" detail={`${snapshot.sensitiveRuns.length} persisted runs`} onClick={() => onOpenPanel("sensitive-history")} />
</div>
</div>
<FleetSelectionActions props={props} />
<FleetGrid {...props} />
</section>
{panelOpen ? <aside className="fleet-drawer"><FleetPanel props={props} /></aside> : null}
</div>
);
}
@@ -0,0 +1,166 @@
import { useState } from "react";
import {
Activity,
ChevronDown,
ChevronUp,
FileClock,
ShieldCheck,
TerminalSquare,
} from "lucide-react";
import type { VariantProps } from "../model";
import {
ActiveRunShortcut,
FleetBreadcrumb,
FleetGrid,
FleetHeader,
FleetPanel,
FleetPanelShortcut,
FleetSelectionActions,
FleetSummary,
} from "../fleet";
import { ScenarioNotice } from "../shared";
const PANEL_TITLES = {
"sync-history": "Synchronization history",
"description-history": "Description history",
"sensitive-history": "Sensitive suggestion history",
"sensitive-review": "Sensitive field review",
"database-form": "Database configuration",
"metadata-editor": "Catalog metadata",
} as const;
export function OperationsVariant(props: VariantProps) {
const {
snapshot,
state,
selectedDatabase,
selectedTable,
onInventoryLevel,
onOpenPanel,
onClosePanel,
onAction,
} = props;
const [consoleExpanded, setConsoleExpanded] = useState(false);
const panelActive = state.panel !== "none" && state.panel !== "actions";
const consoleOpen = consoleExpanded || panelActive;
const panelTitle = panelActive
? PANEL_TITLES[state.panel as keyof typeof PANEL_TITLES]
: null;
const selectedCount = state.selectedRowIds.length;
const collapseConsole = () => {
if (panelActive) onClosePanel();
setConsoleExpanded(false);
};
return (
<div className="candidate candidate--operations candidate--fleet-deck fleet-shell fleet-operations-deck">
<FleetHeader
snapshot={snapshot}
variantLabel="C · Fleet Operations Deck"
onOpenPanel={onOpenPanel}
/>
<ScenarioNotice scenario={state.scenario} onAction={onAction} />
<main className="operations-deck-main">
<section className="operations-command-strip" aria-label="Fleet context">
<div className="operations-command-copy">
<p className="eyebrow">Fleet operations</p>
<h2>One inventory surface, one operational console</h2>
<p>
Browse one catalog grid at a time. The live catalog remains read only;
prototype actions are simulated locally and never modify source data.
</p>
</div>
<FleetSummary database={selectedDatabase} />
</section>
<FleetBreadcrumb
level={state.inventoryLevel}
database={selectedDatabase}
table={selectedTable}
onLevel={onInventoryLevel}
/>
<section className="operations-grid-deck" aria-label="Fleet inventory work area">
<FleetGrid {...props} />
</section>
</main>
<section
className={`operations-console${consoleOpen ? " operations-console--expanded" : ""}`}
aria-label="Fleet operations console"
>
<header className="operations-console-header">
<div className="operations-console-title">
<span className="operations-console-icon" aria-hidden="true">
<TerminalSquare size={17} />
</span>
<span>
<strong>{panelTitle ?? "Operations console"}</strong>
<small>
{panelActive
? "Read-only run detail and simulated prototype controls"
: selectedCount
? `${selectedCount} selected · actions use the current ${state.inventoryLevel} scope`
: "Selection actions and persisted catalog histories"}
</small>
</span>
</div>
<button
className="operations-console-toggle"
type="button"
aria-expanded={consoleOpen}
aria-label={consoleOpen ? "Collapse operations console" : "Expand operations console"}
title={consoleOpen ? "Collapse operations console" : "Expand operations console"}
onClick={() => consoleOpen ? collapseConsole() : setConsoleExpanded(true)}
>
{consoleOpen ? <ChevronDown size={17} /> : <ChevronUp size={17} />}
</button>
</header>
<div className="operations-console-actions">
<FleetSelectionActions props={props} />
</div>
{consoleOpen ? (
<div className="operations-console-expanded-body">
{panelActive ? (
<FleetPanel props={props} includeHeader={false} />
) : (
<div className="operations-console-shortcuts" aria-label="Operation histories">
<ActiveRunShortcut
run={selectedDatabase.activeSyncRun}
onClick={() => onOpenPanel("sync-history", selectedDatabase.activeSyncRun?.id)}
/>
<FleetPanelShortcut
icon={<FileClock size={16} />}
label="Description history"
detail={snapshot.descriptionRuns.length
? `${snapshot.descriptionRuns.length} persisted run${snapshot.descriptionRuns.length === 1 ? "" : "s"}`
: "No persisted runs in the live catalog"}
onClick={() => onOpenPanel("description-history")}
/>
<FleetPanelShortcut
icon={<ShieldCheck size={16} />}
label="Sensitive suggestion history"
detail={snapshot.sensitiveRuns.length
? `${snapshot.sensitiveRuns.length} persisted run${snapshot.sensitiveRuns.length === 1 ? "" : "s"}`
: "No persisted runs in the live catalog"}
onClick={() => onOpenPanel("sensitive-history")}
/>
<FleetPanelShortcut
icon={<Activity size={16} />}
label="Relationship operations"
detail={`${selectedDatabase.relationshipCount.toLocaleString("en-GB")} synchronized relationships`}
onClick={() => onInventoryLevel("relationships")}
/>
</div>
)}
</div>
) : null}
</section>
</div>
);
}
@@ -0,0 +1,134 @@
import path from "path";
import { execFileSync } from "node:child_process";
import react from "@vitejs/plugin-react";
import { defineConfig, type Plugin } from "vite";
const CATALOG_CONTAINER = process.env.THT_PROTOTYPE_CATALOG_CONTAINER
?? "thothii-catalog-db-1";
const LIVE_CATALOG_SQL = String.raw`
select json_build_object(
'capturedAt', now(),
'databases', coalesce((select json_agg(json_build_object(
'id', wd.id, 'workspaceId', wd.workspace_id, 'engine', wd.engine,
'databaseName', wd.database_name, 'schemaName', wd.schema_name,
'version', wd.version, 'schemaSyncedVersion', wd.schema_synced_version,
'schemaSyncedAt', wd.schema_synced_at, 'updatedAt', wd.updated_at,
'transport', b.transport, 'host', b.host, 'port', b.port,
'baseUrl', b.base_url, 'restPath', b.rest_path,
'connectionStatus', b.connection_status, 'testedVersion', b.tested_version,
'lastTestedAt', b.last_tested_at
) order by wd.workspace_id) from workspace_databases wd left join database_bindings b on b.database_id = wd.id), '[]'::json),
'tables', coalesce((select json_agg(json_build_object(
'id', t.id, 'databaseId', t.database_id, 'name', t.name,
'sourceComment', t.source_comment, 'description', t.description,
'generatedDescription', t.generated_description, 'version', t.version,
'lastSyncedAt', t.last_synced_at
) order by t.name) from catalog_tables t), '[]'::json),
'columns', coalesce((select json_agg(json_build_object(
'id', c.id, 'tableId', c.table_id, 'name', c.name,
'ordinalPosition', c.ordinal_position, 'dataType', c.data_type,
'isNullable', c.is_nullable, 'defaultExpression', c.default_expression,
'primaryKeyPosition', c.primary_key_position, 'sourceComment', c.source_comment,
'description', c.description, 'generatedDescription', c.generated_description,
'sensitive', c.sensitive, 'lastSyncedAt', c.last_synced_at, 'version', c.version
) order by c.table_id, c.ordinal_position) from catalog_columns c), '[]'::json),
'relationships', coalesce((select json_agg(json_build_object(
'id', r.id, 'databaseId', r.database_id, 'constraintName', r.constraint_name,
'sourceTableName', st.name, 'targetTableName', tt.name,
'updateRule', r.update_rule, 'deleteRule', r.delete_rule
) order by r.constraint_name) from catalog_relationships r
join catalog_tables st on st.id = r.source_table_id
join catalog_tables tt on tt.id = r.target_table_id), '[]'::json),
'relationshipColumns', coalesce((select json_agg(json_build_object(
'relationshipId', rc.relationship_id, 'ordinalPosition', rc.position,
'sourceColumnName', sc.name, 'targetColumnName', tc.name
) order by rc.relationship_id, rc.position) from catalog_relationship_columns rc
join catalog_columns sc on sc.id = rc.source_column_id
join catalog_columns tc on tc.id = rc.target_column_id), '[]'::json),
'syncRuns', coalesce((select json_agg(json_build_object(
'id', r.id, 'databaseId', r.database_id, 'scope', r.scope,
'state', r.state, 'phase', r.phase, 'counts', r.counts,
'plannedDiff', r.planned_diff, 'errorCode', r.error_code,
'errorMessage', r.error_message, 'createdAt', r.created_at,
'startedAt', r.started_at, 'finishedAt', r.finished_at
) order by r.created_at desc) from (select * from catalog_sync_runs order by created_at desc limit 50) r), '[]'::json),
'syncEvents', coalesce((select json_agg(json_build_object(
'runId', e.run_id, 'sequence', e.sequence, 'level', e.level,
'message', e.message, 'createdAt', e.created_at
) order by e.created_at desc) from (select * from catalog_sync_events order by created_at desc limit 300) e), '[]'::json),
'descriptionRuns', coalesce((select json_agg(json_build_object(
'id', r.id, 'databaseId', r.database_id, 'scope', r.scope,
'modelId', r.model_id, 'language', r.language, 'status', r.status,
'total', r.total, 'processed', r.processed, 'generated', r.generated,
'nonGeneratable', r.non_generatable, 'failed', r.failed,
'createdAt', r.created_at, 'startedAt', r.started_at,
'finishedAt', r.finished_at, 'errorSummary', r.error_summary
) order by r.created_at desc) from (select * from description_generation_runs order by created_at desc limit 50) r), '[]'::json),
'descriptionEvents', coalesce((select json_agg(json_build_object(
'runId', e.run_id, 'sequence', e.sequence, 'level', e.level,
'message', e.message, 'createdAt', e.created_at
) order by e.created_at desc) from (select * from description_generation_events order by created_at desc limit 300) e), '[]'::json),
'sensitiveRuns', coalesce((select json_agg(json_build_object(
'id', r.id, 'databaseId', r.database_id, 'scope', r.scope,
'modelId', r.model_id, 'status', r.status, 'total', r.total,
'suggestedSensitive', r.suggested_sensitive,
'suggestedNonSensitive', r.suggested_non_sensitive,
'createdAt', r.created_at, 'startedAt', r.started_at,
'finishedAt', r.finished_at, 'errorSummary', r.error_summary
) order by r.created_at desc) from (select * from sensitive_data_suggestion_runs order by created_at desc limit 50) r), '[]'::json),
'sensitiveEvents', coalesce((select json_agg(json_build_object(
'runId', e.run_id, 'sequence', e.sequence, 'level', e.level,
'message', e.message, 'createdAt', e.created_at
) order by e.created_at desc) from (select * from sensitive_data_suggestion_events order by created_at desc limit 300) e), '[]'::json)
)::text;
`;
function liveCatalogPlugin(): Plugin {
return {
name: "database-management-prototype-live-catalog",
configureServer(server) {
server.middlewares.use("/__prototype/catalog-snapshot", (_request, response) => {
try {
const payload = execFileSync("docker", [
"exec",
"--env",
`THT_PROTOTYPE_SQL=${LIVE_CATALOG_SQL}`,
CATALOG_CONTAINER,
"sh",
"-lc",
'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "$THT_PROTOTYPE_SQL"',
], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 }).trim();
response.statusCode = 200;
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.end(payload);
} catch (error) {
response.statusCode = 503;
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.end(JSON.stringify({
error: "live_catalog_unavailable",
detail: error instanceof Error ? error.message.split("\n")[0] : "Unknown error",
}));
}
});
},
};
}
export default defineConfig(({ command }) => {
if (command !== "serve") {
throw new Error("The database-management prototype is dev-only. Run npm run prototype:database-management.");
}
return {
root: path.resolve(__dirname, "prototypes/database-management"),
plugins: [react(), liveCatalogPlugin()],
appType: "spa",
server: {
host: "127.0.0.1",
port: 5173,
strictPort: true,
},
};
});