From a701b19a03f91d530a1727fbf2d5c3ac32927665 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 26 Aug 2026 17:53:23 +0200 Subject: [PATCH] feat(frontend): add database management surface --- CONTEXT.md | 11 +- docs/adr/0001-postgres-metadata-catalog.md | 21 + ...026-08-26-metadata-catalog-from-thothai.md | 492 ++++++++++++++++++ .../AppShell.database-management.test.tsx | 139 +++++ frontend/src/shell/AppShell.tsx | 36 +- frontend/src/shell/DatabaseManagementPage.tsx | 8 + frontend/src/shell/ModelActivityPanel.tsx | 4 +- frontend/src/shell/SessionDocumentsPanel.tsx | 5 +- 8 files changed, 708 insertions(+), 8 deletions(-) create mode 100644 docs/adr/0001-postgres-metadata-catalog.md create mode 100644 docs/plans/2026-08-26-metadata-catalog-from-thothai.md create mode 100644 frontend/src/shell/AppShell.database-management.test.tsx create mode 100644 frontend/src/shell/DatabaseManagementPage.tsx diff --git a/CONTEXT.md b/CONTEXT.md index a53c99e3..484a1796 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -246,9 +246,10 @@ precedente o di un altro workspace. ## Catalogo dei metadati -**Workspace Database** — Il database associato a un workspace, considerato nella sua -interezza fisica: tutte le tabelle, le colonne e le relazioni disponibili, anche quando -solo un loro sottoinsieme è destinato al core di ThothII. +**Workspace Database** — Il database associato in modo uno-a-uno a un workspace, +considerato nella sua interezza fisica: tutte le tabelle, le colonne e le relazioni +disponibili. La sua struttura fisica viene acquisita interrogando il database; il +Metadata Catalog non crea né possiede l'identità del workspace. **Metadata Catalog** — Il contesto amministrativo che raccoglie e cura i metadati di un Workspace Database. Non definisce quali elementi partecipano al workflow SQL. @@ -256,6 +257,10 @@ Workspace Database. Non definisce quali elementi partecipano al workflow SQL. **Database Profile** — L'insieme curato di scope, descrizioni e metadati semantici associato a un Workspace Database. +**Physical Schema Snapshot** — L'inventario della struttura fisica osservata in un +Workspace Database durante una specifica introspezione. Non è un progetto dello schema +né un'autorizzazione a modificarne la struttura. + **AI Proposal** — Un contenuto generato con l'ausilio dell'AI che non è ancora stato approvato come contenuto canonico. diff --git a/docs/adr/0001-postgres-metadata-catalog.md b/docs/adr/0001-postgres-metadata-catalog.md new file mode 100644 index 00000000..dfc857f9 --- /dev/null +++ b/docs/adr/0001-postgres-metadata-catalog.md @@ -0,0 +1,21 @@ +# PostgreSQL come autorità dei metadati dei database dei workspace + +ThothII userà un Metadata Catalog PostgreSQL interno per conservare, per ogni workspace, la +struttura fisica acquisita interrogando il relativo database e i metadati semantici generati con +l'AI. Ogni workspace avrà un solo Workspace Database; identità e lista dei workspace resteranno +autorevoli in `thoth-workspaces.yaml`, mentre il catalogo ne conserverà soltanto il riferimento +stabile. `schema/annotations.yaml` verrà sostituito come input del core in uno step successivo; +l'interfaccia e il lifecycle amministrativi resteranno separati dal workflow NL→SQL. + +## Considered Options + +- Conservare `annotations.yaml` come fonte di verità avrebbe mantenuto il revisionamento Git, ma + non avrebbe fornito il CRUD e il processo di introspezione/generazione richiesti. +- Leggere metadati mutabili dal catalogo senza un confine esplicito avrebbe accoppiato il workflow + alla disponibilità del nuovo servizio e consentito viste parziali durante gli aggiornamenti. + +## Consequences + +PSD importerà le annotations esistenti; gli altri workspace genereranno i metadati da zero. Il +cutover futuro dovrà sostituire consapevolmente i consumatori delle annotations e verificarne +l'equivalenza semantica. Le sessioni di test esistenti non sono un vincolo di migrazione. diff --git a/docs/plans/2026-08-26-metadata-catalog-from-thothai.md b/docs/plans/2026-08-26-metadata-catalog-from-thothai.md new file mode 100644 index 00000000..333c5640 --- /dev/null +++ b/docs/plans/2026-08-26-metadata-catalog-from-thothai.md @@ -0,0 +1,492 @@ +# Metadata Catalog di ThothII: ricognizione ThothAI e percorso incrementale + +Data: 2026-08-26 +Stato: ricognizione completata; step 1 implementato; scelte tecnologiche degli step successivi +deliberatamente rinviate. + +## Obiettivo + +ThothII deve introdurre un contesto amministrativo separato, il **Metadata Catalog**, per gestire +il database associato a ciascun workspace, la sua struttura fisica introspezionata e i metadati +semantici oggi rappresentati da `schema/annotations.yaml`. + +Il programma procede per step indipendenti. Il primo step aggiunge soltanto l'accesso dalla sidebar +destra a una superficie centrale vuota. Non introduce PostgreSQL, API CRUD, introspezione o +integrazioni con il workflow core. + +Questa analisi usa come riferimento il working tree legacy osservato in +`Thoth/ThothAI`. Non è stato verificato che quel contenuto corrisponda a una release o a un tag +canonico; i percorsi e i comportamenti descrivono il sorgente disponibile il 2026-08-26. + +## Decisioni già confermate + +1. Ogni workspace è associato a un solo Workspace Database e ogni Workspace Database appartiene a + un solo workspace. +2. Il CRUD non crea né rinomina workspace. Identità e lista ordinata dei workspace restano + autorevoli in `thoth-workspaces.yaml`; il catalogo conserva il loro identificatore stabile. +3. La struttura fisica viene acquisita interrogando il database esterno tramite i dati di + connessione registrati per il Workspace Database. +4. I contenuti semantici equivalenti a `annotations.yaml` vengono generati con l'AI e conservati nel + PostgreSQL interno. +5. Per PSD è prevista l'importazione delle annotations esistenti. Gli altri database partiranno + dalla struttura introspezionata e genereranno i metadati semantici da zero. +6. `annotations.yaml` sarà sostituito anche come input del core in uno step futuro. Il repository è + in fase di test e non è richiesta la conservazione delle sessioni esistenti durante il cutover. +7. La gestione catalogo resta una superficie separata dal processo NL→SQL. La futura integrazione + deve essere esplicita e non deve modificare fasi, gate o semantica del workflow. +8. Il link iniziale è visibile agli utenti con `workspace.manage`, usa stato React locale e non + introduce un router. +9. La pagina iniziale è vuota, segue il tema, nasconde l'intera colonna core e non interrompe una + sessione live. Le azioni di apertura, resume o creazione sessione riportano al core. + +## Correzione del modello mentale corrente + +`schema/annotations.yaml` non contiene l'intero schema del database. + +- `physical.yaml` è un artefatto derivato dall'introspezione. Contiene database, schema, timestamp, + tabelle, colonne, tipi, nullability, default, primary key, commenti sorgente, esempi, foreign key + fisiche e indici. +- `annotations.yaml` contiene metadati curati: descrizioni e concetti delle tabelle; descrizioni, + sinonimi, concetti, evidence, note e override `eligible` delle colonne; foreign key logiche. +- Il rendering M-Schema fonde questi due input. Le annotations prevalgono sui commenti sorgente e + le relazioni logiche vengono unite alle foreign key fisiche. + +La sostituzione del solo file annotations non elimina automaticamente l'introspezione fisica. Il +nuovo catalogo dovrà conservare una distinzione esplicita fra fatti osservati nel database e +contenuto semantico modificabile. + +## Architettura ThothII rilevante + +### Autorità e revisionamento attuali + +Il repository dei workspace contiene: + +```text +thoth-workspaces.yaml +/workspace.yaml +/schema/annotations.yaml +/evidence/** +``` + +Il backend legge descriptor e annotations allo stesso commit Git. Durante l'attivazione valida il +blob, lo copia atomicamente nello snapshot immutabile della revisione e registra commit, blob ID e +digest. Le nuove sessioni vengono legate a quella revisione; resume e SQL salvato riaprono lo stesso +snapshot. + +Punti principali: + +- `backend/src/workspaces/schema.ts`: descriptor v3 e singolo `dwh.database`/`dwh.schema`; +- `backend/src/workspaces/git-repository.ts`: lettura sicura del blob annotations al commit; +- `backend/src/workspaces/registry.ts`: validazione e attivazione atomica; +- `backend/src/workspaces/annotations-sync.ts`: materializzazione revision-qualified; +- `backend/src/workspaces/runtime-config-lease.ts`: binding dello snapshot al runtime; +- `harness/tht/mschema/models.py`: contratti `PhysicalSchema` e `Annotations`; +- `harness/tht/mschema/render.py`: fusione fisico/semantico; +- `harness/tht/cli/vector_cmd.py`: indicizzazione schema in Qdrant. + +### Consumatori da preservare al cutover futuro + +Le annotations incidono oggi su: + +- override `eligible` prima del campionamento LSH; +- suggerimento, controllo e accettazione delle foreign key logiche; +- descrizioni, concetti e sinonimi dei record schema in Qdrant; +- retrieval delle tabelle e colonne candidate; +- rendering M-Schema usato dal gate F4 e dalla generazione SQL; +- digest della revisione accettata durante il preprocessing. + +Il futuro cutover non potrà limitarsi a rimuovere il file: dovrà fornire al core lo stesso contenuto +effettivo, con un'identità coerente e test di equivalenza. Poiché non occorre preservare le sessioni +di test esistenti, non serve progettare compatibilità con i vecchi manifest, ma resta necessario +evitare letture parziali o semanticamente incoerenti. + +## Inventario ThothAI + +### Modelli legacy + +I modelli sono definiti in `Thoth/ThothAI/backend/thoth_core/models.py`. + +#### `SqlDb` + +Campi di connessione osservati: + +- `name`; +- `db_host`, `db_port`; +- `db_type`; +- `db_name`, `schema`; +- `user_name`, `password`; +- `db_mode`; +- configurazione SSH e Informix opzionale. + +Il modello contiene anche scope, JSON dello scope, ERD, direttive, campi GDPR, collegamento a +`VectorDb` e numerosi campi di stato/task/log per lavori AI asincroni. + +I tipi legacy dichiarati sono Informix, MariaDB, MySQL, Oracle, PostgreSQL, SQL Server e SQLite. +Questo elenco non costituisce automaticamente un requisito per ThothII: il core corrente supporta +PostgreSQL e l'estensione ad altri dialetti dovrà essere decisa separatamente. + +#### `SqlTable` + +- `name`; +- `description`; +- `generated_comment`; +- foreign key obbligatoria a `SqlDb`, con cancellazione cascade. + +#### `SqlColumn` + +- `original_column_name` e alias `column_name`; +- `data_format` normalizzato; +- `column_description`; +- `generated_comment`; +- `value_description`; +- stringhe denormalizzate `pk_field` e `fk_field`; +- foreign key obbligatoria a `SqlTable`, con cancellazione cascade. + +#### `Relationship` + +Contiene quattro foreign key obbligatorie: + +- `source_table` e `source_column`; +- `target_table` e `target_column`. + +Il form admin verifica che le tabelle appartengano allo stesso database e che ogni colonna +appartenga alla tabella selezionata. Il database non impone però gli stessi check. + +#### `Workspace` + +ThothAI usa `Workspace.sql_db` come foreign key nullable verso `SqlDb`: un workspace seleziona un +solo DB, mentre lo stesso DB può essere riusato da più workspace. ThothII adotterà invece una +relazione uno-a-uno: `workspace_id` deve essere unico nel catalogo. + +### Lacune dei constraint legacy + +Non risultano constraint database-level per: + +- unicità del nome database nel workspace; +- unicità `(database, table name)`; +- unicità `(table, column name)`; +- unicità degli estremi di una relationship; +- appartenenza degli estremi della relationship allo stesso database; +- corrispondenza fra colonna e tabella dichiarata. + +ThothII deve applicare queste invarianti sia nel database interno sia nel servizio applicativo. La +sola validazione del form non è sufficiente perché API, import e job la possono aggirare. + +### Django Admin e UX da replicare concettualmente + +ThothAI espone il CRUD tramite il Django Admin standard, registrato da +`backend/thoth_core/admin.py` e pubblicato su `/admin/`. + +Capacità utili: + +- lista database con ricerca per nome, host, tipo, database e schema; +- fieldset separati per identità, connessione, autenticazione, SSH e stato; +- lista tabelle filtrabile per database; +- lista colonne filtrabile in cascata per database e tabella; +- lista relazioni con estremi leggibili e filtri per database e tabelle; +- form relazione con dropdown dipendenti database → tabella → colonna; +- validazione degli estremi prima del salvataggio; +- azioni separate per test connessione, introspezione, import/export e generazione AI; +- azioni bulk sulle righe selezionate. + +ThothII deve replicare i contratti di interazione e validazione, non il rendering server-side o i +template Django. + +### Introspezione legacy + +`Thoth/ThothAI/backend/thoth_core/dbmanagement.py` usa `thoth-dbmanager` per: + +1. costruire l'adapter del dialetto; +2. acquisire tabelle; +3. acquisire e normalizzare colonne e tipi; +4. acquisire relazioni; +5. creare le eventuali colonne mancanti necessarie alle relazioni; +6. aggiornare i campi PK/FK denormalizzati. + +Il comportamento è principalmente additivo: usa `get_or_create` o controlli `exists`, aggiorna +alcuni commenti, ma non riconcilia in modo completo rename, rimozioni o drift. Non va copiato così +com'è. Il futuro processo ThothII dovrà almeno distinguere scansione, differenze osservate e +applicazione della nuova snapshot. + +### Generazione AI legacy + +ThothAI dispone di azioni e workflow per: + +- commenti delle tabelle; +- commenti delle colonne; +- scope del database; +- ERD Mermaid; +- documentazione del database; +- analisi GDPR. + +Per il requisito attuale sono direttamente rilevanti descrizioni di tabelle e colonne, scope e +metadati semantici. ERD, documentazione aggregata e GDPR sono estensioni future, non prerequisiti +del CRUD iniziale. + +La separazione `description`/`generated_comment` del legacy non offre versioning o approvazione +robusti. Nei passi successivi andrà deciso se l'output AI è una proposta revisionabile o diventa +immediatamente il valore editabile corrente. + +### Import ed export legacy + +ThothAI offre: + +- CSV di database, tabelle, colonne e relazioni; +- export di struttura per workspace; +- import mediante `import_db_structure`; +- script SQL dei commenti per più dialetti; +- aggiornamento delle descrizioni colonna da CSV. + +Il futuro import PSD dovrà leggere il contratto YAML corrente e convertirlo su chiavi naturali, +non riutilizzare gli ID numerici Django. Deve essere idempotente e produrre un report di elementi +creati, aggiornati, ignorati o non risolti. + +## Comandi osservati in ThothAI + +### Backend locale + +Eseguiti da `Thoth/ThothAI/backend`: + +```sh +uv sync +uv run python manage.py migrate +uv run python manage.py createsuperuser +uv run python manage.py runserver 8200 +uv run pytest +``` + +Import catalogo legacy: + +```sh +uv run python manage.py import_db_structure --source local +uv run python manage.py load_defaults --only-level 4 --source local +``` + +Test mirati rilevanti: + +```sh +uv run pytest tests/test_relational_database_operations.py -v +uv run pytest tests/test_ssh_tunnel_configuration.py -v +``` + +### Stack Docker legacy + +ThothAI dichiara `postgres:16-alpine` nel profilo `internal-db`, con volume persistente e +healthcheck `pg_isready`. + +```sh +docker compose --profile internal-db up --build +``` + +Il wrapper legacy abilita lo stesso profilo quando `POSTGRES_INTERNAL=true`: + +```sh +POSTGRES_INTERNAL=true ./docker-up.sh +``` + +Questi comandi documentano il riferimento osservato; non sono comandi di installazione per +ThothII. + +## Cosa copiare in ThothII + +### Parità necessaria + +- gerarchia Workspace Database → Table → Column; +- relazione strutturale fra colonne sorgente e destinazione; +- navigazione e filtri dipendenti workspace/database/tabella; +- test di connessione separato dal salvataggio; +- introspezione esplicita e ripetibile; +- descrizioni generate dall'AI ma modificabili dall'utente; +- validazione cross-entity delle relazioni; +- azioni di import/export senza segreti; +- stato leggibile dei job lunghi; +- PostgreSQL interno persistente con migrazioni esplicite; +- test di CRUD, cardinalità, cascade/restrict, isolamento per workspace e idempotenza. + +### Parità semantica con `annotations.yaml` + +Il modello futuro deve poter rappresentare almeno: + +- descrizione, concetti e note per tabella; +- descrizione, sinonimi, concetti, evidence, note ed `eligible` per colonna; +- foreign key logiche; +- distinzione fra commento fisico osservato e descrizione curata; +- provenienza del contenuto importato o generato. + +L'eventuale esclusione di uno di questi campi deve essere una decisione esplicita perché cambia +rendering, retrieval, LSH o SQL generation. + +### Vincoli minimi da progettare + +- `workspace_id` unico sul Workspace Database; +- nome tabella unico nel database e schema appropriato; +- nome colonna unico nella tabella; +- relationship unica secondo il modello, anche per chiavi composite; +- estremi della relationship nello stesso Workspace Database; +- appartenenza certa della colonna alla tabella; +- mutazioni aggregate transazionali; +- gestione esplicita di concorrenza fra CRUD e introspezione. + +## Cosa non copiare + +- Django, Django Admin, Django ORM, DRF, template admin e frontend Next; +- modello Workspace legacy e condivisione dello stesso DB fra più workspace; +- password o passphrase come normali campi testuali; +- password incluse in CSV o export completi; +- token SSO inseriti nella query string; +- migrazioni generate automaticamente all'avvio; +- validazioni presenti soltanto nel form; +- `pk_field` e `fk_field` testuali come fonte di verità; +- duplicazione di tabella e colonna negli estremi senza constraint coerenti; +- introspezione additiva che non segnala rename, delete o drift; +- azioni admin che possono mostrare successo dopo output AI non valido; +- dipendenza del workflow core dalla disponibilità della UI o del PostgreSQL amministrativo. + +## Aspetti di sicurezza da non ereditare + +L'export legacy della struttura include username e password in chiaro. Il modello conserva inoltre +password, passphrase SSH e altri segreti in `CharField`; non è stata trovata cifratura applicativa, +nonostante un testo admin affermi il contrario. + +Per ThothII resta da decidere nello step infrastrutturale quali dati di connessione siano normali +metadati e quali siano secret reference. In ogni caso: + +- nessun endpoint o export deve restituire segreti; +- log ed errori devono sanificare DSN e credenziali; +- le credenziali di migrazione non devono essere disponibili al runtime CRUD; +- il catalogo non deve riusare credenziali del DWH, delle sessioni o di Qdrant; +- test connessione e introspezione devono usare timeout e privilegi read-only. + +## Percorso incrementale + +### Step 1: accesso alla superficie vuota + +Implementato in questo worktree: + +- pulsante `Database management` nella sidebar destra; +- visibilità legata a `workspace.manage`; +- superficie centrale React separata e vuota; +- nessun router, endpoint, fetch o stato catalogo; +- sessione e SSE conservati in background; +- ritorno al core tramite creazione, apertura o resume di una sessione; +- test frontend dedicati. + +Comandi di verifica: + +```sh +cd frontend +npx vitest run src/shell/AppShell.database-management.test.tsx +npx vitest run src/shell/AppShell.new-session.test.tsx \ + src/shell/AppShell.session-target.test.tsx \ + src/shell/AppShell.session-mgmt.test.tsx +npx tsc -b +``` + +### Step 2: contratto di dominio e schema relazionale + +Da progettare con un nuovo round decisionale: campi, secret reference, dialetti supportati, +namespace/schema, snapshot fisiche, relazioni fisiche/logiche e lifecycle dell'output AI. Nessuna +tecnologia ORM o migration tool è stata scelta in questo documento. + +### Step 3: PostgreSQL interno e migrazioni + +Da progettare separatamente dal core: servizio, volume, ruoli runtime/migrator/backup, health e +readiness dedicati, backup/restore e diagnostica. La sua indisponibilità non dovrà cambiare +`core /health` o interrompere una sessione. + +### Step 4: API CRUD + +Contratti HTTP, autorizzazione, paginazione, filtri, errori, optimistic concurrency e transazioni. +Gli endpoint dovranno vivere sotto un namespace catalogo e non riutilizzare le route sessione. + +### Step 5: UI CRUD + +Liste e form per Workspace Database, tabelle, colonne e relazioni, costruiti con React/Vite e il +design system ThothII. La gerarchia e i filtri ThothAI sono il riferimento funzionale; Django Admin +non è il riferimento tecnologico o visuale. + +### Step 6: introspezione + +Connessione read-only, preview delle differenze, acquisizione di una Physical Schema Snapshot, +policy per rename/rimozioni e stato del job. Nessuna chiamata lunga dovrà mantenere aperta una +transazione CRUD. + +### Step 7: generazione AI dei metadati + +Generazione di descrizioni e altri campi equivalenti alle annotations, editing umano e gestione +esplicita di errori o output non validi. Approvazione/versioning saranno decisi in questo step. + +### Step 8: migrazione PSD + +Import idempotente delle annotations PSD, riconciliazione contro la struttura introspezionata, +report degli orfani e confronto semantico con il rendering corrente. Gli altri workspace non +ricevono import legacy. + +### Step 9: sostituzione dell'input core + +Rimuovere la dipendenza da `annotations.yaml` soltanto dopo avere un contratto equivalente, +test di rendering/search/Qdrant e una policy di disponibilità. Le sessioni di test esistenti +possono essere eliminate, ma le nuove sessioni non devono osservare aggiornamenti parziali. + +### Step 10: operazioni e accettazione + +Backup/restore reale, diagnostica, metriche, audit, permessi definitivi, hardening degli export e +test di failure isolation fra catalogo e workflow. + +## Verifiche del core da conservare per il cutover + +Comandi attuali rilevanti: + +```sh +tht --installation /thothii-installation.yaml workspace preprocess dwh \ + --workspace --json +tht --installation /thothii-installation.yaml workspace schema suggest-fks \ + --workspace --json +tht --installation /thothii-installation.yaml workspace schema check \ + --workspace --json +tht --installation /thothii-installation.yaml workspace schema accept \ + --workspace --run --yes --json +tht --installation /thothii-installation.yaml workspace index-schema \ + --workspace --json +``` + +Suite che documentano il comportamento da preservare: + +```sh +cd backend +npx vitest run test/workspaces-git-annotations.test.ts \ + test/registry-annotations.test.ts \ + test/annotations-sync.test.ts \ + test/workspace-runtime-config-lease.test.ts \ + test/workspace-preprocessing-service.test.ts +npx tsc --noEmit -p . + +cd ../harness +.venv/bin/pytest -q \ + tests/test_annotations_root.py \ + tests/test_schema_fk_annotations.py \ + tests/test_mschema_render.py \ + tests/test_qdrant_cli_commands.py +``` + +Questi test non implicano che la futura implementazione debba continuare a usare file YAML. +Definiscono gli effetti semantici e le guardie da mantenere o sostituire consapevolmente. + +## Decisioni rinviate + +Le seguenti scelte non appartengono allo step 1: + +- framework del servizio catalogo e libreria di accesso PostgreSQL; +- collocazione e protezione delle credenziali dei Workspace Database; +- supporto iniziale di dialetti diversi da PostgreSQL; +- uno o più schema namespace per database; +- policy di reconciliation per rename e delete; +- modello delle foreign key composite; +- distinzione persistente fra relationship fisiche e logiche; +- lifecycle draft/review/approval dell'output AI; +- versionamento, audit e rollback; +- formato e momento del cutover dal file al database interno; +- permission definitiva separata da `workspace.manage`. + +Ognuna sarà affrontata nel relativo step, senza anticipare scelte tecnologiche nel presente +documento. diff --git a/frontend/src/shell/AppShell.database-management.test.tsx b/frontend/src/shell/AppShell.database-management.test.tsx new file mode 100644 index 00000000..1ccb08bc --- /dev/null +++ b/frontend/src/shell/AppShell.database-management.test.tsx @@ -0,0 +1,139 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { http, HttpResponse } from "msw"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { server } from "../test/msw"; +import { FakeEventSource } from "../test/fakeEventSource"; +import { useSessionStore } from "../store/sessionStore"; +import { clearAuthState, setAuthState } from "../auth/authState"; +import { AppShell } from "./AppShell"; + +function renderShell() { + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render( + + + , + ); +} + +beforeEach(() => { + clearAuthState(); + setAuthState({ + issuer: "test", + subject: "test", + roles: ["user"], + permissions: ["session.use", "workspace.manage"], + isAdmin: false, + csrfToken: null, + session: null, + }); + localStorage.clear(); + FakeEventSource.instances = []; + (globalThis as any).EventSource = FakeEventSource; + useSessionStore.getState().resetSession(); + server.use( + http.get("/api/me", () => HttpResponse.json({ + issuer: "test", + subject: "test", + displayName: "Test", + isAdmin: false, + })), + http.get("/api/sessions", () => HttpResponse.json([])), + http.get("/api/settings", () => HttpResponse.json({ + workspace: "default", + provider: "test", + model: "test", + thinking: "low", + })), + http.get("/api/workspaces", () => HttpResponse.json([])), + http.get("/api/models", () => HttpResponse.json({ models: [] })), + http.post("/api/runtime/prewarm", () => new HttpResponse(null, { status: 202 })), + ); +}); + +test("opens the blank database management surface and returns to the core", async () => { + renderShell(); + + const composer = screen.getByRole("textbox", { name: /new question/i }); + await userEvent.type(composer, "Unsent draft"); + + const databaseManagement = screen.getByRole("button", { name: "Database management" }); + await userEvent.click(databaseManagement); + + expect(screen.getByRole("main", { name: "Database management" })).toBeVisible(); + expect(databaseManagement).toHaveAttribute("aria-current", "page"); + expect(databaseManagement).toHaveClass("bg-sidebar-accent", "font-bold"); + expect(screen.queryByRole("textbox", { name: /new question/i })).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole("button", { name: "New session" })); + + await waitFor(() => { + expect(screen.queryByRole("main", { name: "Database management" })).not.toBeInTheDocument(); + }); + expect(screen.getByRole("textbox", { name: /new question/i })).toHaveValue("Unsent draft"); +}); + +test("keeps a live core session connected and returns when that session is opened", async () => { + server.use( + http.get("/api/sessions", () => HttpResponse.json([{ + id: "s1", + status: "open", + question: "Active question", + summary: null, + created_at: "2026-01-02T00:00:00Z", + updated_at: null, + author: "test", + name: null, + group: null, + archived: false, + active: true, + }])), + http.post("/api/sessions/:id/resume", ({ params }) => HttpResponse.json({ + id: params.id, + alreadyActive: true, + })), + http.get("/api/sessions/:id", ({ params }) => HttpResponse.json({ + id: params.id, + status: "open", + phase: 1, + })), + ); + renderShell(); + + const session = await screen.findByTestId("session-item-s1"); + await userEvent.click(session); + await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); + const source = FakeEventSource.instances[0]; + + await userEvent.click(screen.getByRole("button", { name: "Database management" })); + + expect(screen.getByRole("main", { name: "Database management" })).toBeVisible(); + expect(source.closed).toBe(false); + expect(FakeEventSource.instances).toHaveLength(1); + + await userEvent.click(session); + + await waitFor(() => { + expect(screen.queryByRole("main", { name: "Database management" })).not.toBeInTheDocument(); + }); + expect(source.closed).toBe(false); + expect(FakeEventSource.instances).toHaveLength(1); +}); + +test("hides database management from users without workspace management permission", () => { + clearAuthState(); + setAuthState({ + issuer: "test", + subject: "test", + roles: ["user"], + permissions: ["session.use"], + isAdmin: false, + csrfToken: null, + session: null, + }); + + renderShell(); + + expect(screen.queryByRole("button", { name: "Database management" })).not.toBeInTheDocument(); +}); diff --git a/frontend/src/shell/AppShell.tsx b/frontend/src/shell/AppShell.tsx index f41b9160..17bcd602 100644 --- a/frontend/src/shell/AppShell.tsx +++ b/frontend/src/shell/AppShell.tsx @@ -15,6 +15,7 @@ import { DeleteConfirmDialog } from "./DeleteConfirmDialog"; import { StopConfirmDialog } from "./StopConfirmDialog"; import { SteerInput, ComposerFooter } from "./SteerInput"; import { WorkflowBar } from "./WorkflowBar"; +import { DatabaseManagementPage } from "./DatabaseManagementPage"; import { Pencil, ArrowLeft, ArrowRight, Trash2 } from "lucide-react"; import { Button } from "../components/ui/button"; import { Checkbox } from "../components/ui/checkbox"; @@ -39,6 +40,8 @@ interface AppShellProps { canLogout: boolean; } +type ActiveSurface = "core" | "database-management"; + export function AppShell({ canLogout }: AppShellProps) { const authenticatedUser = useAuthUser(); const [panelSession, setPanelSession] = useState(null); @@ -118,6 +121,7 @@ export function AppShell({ canLogout }: AppShellProps) { const queryClient = useQueryClient(); const [showActivity, setShowActivity] = useState(false); + const [activeSurface, setActiveSurface] = useState("core"); const [workspaceManagerOpen, setWorkspaceManagerOpen] = useState(false); const [piManagementOpen, setPiManagementOpen] = useState(false); const [activeOpen, setActiveOpen] = useState(true); @@ -195,6 +199,7 @@ export function AppShell({ canLogout }: AppShellProps) { function openPanel(id: string) { const s = sessions.find((x) => x.id === id); if (!s) return; + setActiveSurface("core"); // A session with a live Pi runtime opens straight into its live view: doResume // reconnects to the already-active runtime and replays its pending gate, so an // in-progress session never shows an empty screen that reads as "stopped". Cold or @@ -214,6 +219,7 @@ export function AppShell({ canLogout }: AppShellProps) { }); } async function doResume(id: string) { + setActiveSurface("core"); const guard = captureAuthOperation({ sessionId: id, disposalEpoch: operationEpochRef.current }); if (!guard) return; const token = ++resumeInvocationRef.current; @@ -495,6 +501,7 @@ export function AppShell({ canLogout }: AppShellProps) { }, [lastSystemEvent]); function startNewSession() { + setActiveSurface("core"); invalidateResumeIntent(); newSessionOperationRef.current = null; resetSession(); @@ -580,9 +587,10 @@ export function AppShell({ canLogout }: AppShellProps) { onClose={() => setPanelSession(null)} onResume={doResume} desktopSplit={sessionDesktopSplit} + hidden={activeSurface !== "core"} /> )} - {panelSession && sessionDesktopSplit && ( + {activeSurface === "core" && panelSession && sessionDesktopSplit && (
)} - {showActivity && setShowActivity(false)} onOpenWorkspaceManager={() => setWorkspaceManagerOpen(true)} />} - {showActivity && desktopSplit && ( + {showActivity && setShowActivity(false)} onOpenWorkspaceManager={() => setWorkspaceManagerOpen(true)} hidden={activeSurface !== "core"} />} + {activeSurface === "core" && showActivity && desktopSplit && (
+ {activeSurface === "database-management" && } +
{activeSessionId && (
{/* Arrow toggle for the left Model-activity panel: → opens it, ← closes it. */} @@ -686,6 +699,7 @@ export function AppShell({ canLogout }: AppShellProps) {
+
{/* Right session rail */} @@ -731,6 +745,22 @@ export function AppShell({ canLogout }: AppShellProps) { > Workspace management + {canManageWorkspace && ( + + )} {canManagePi && (