, replace = false) {
+ if (!canLeave()) return;
+ writeRoute({ ...route, ...next }, replace);
+ setDirty(false); setEditing(false); setFocused(false); setSection(0); setNotice(""); setAdminOpen(false);
+ }
+ useEffect(() => {
+ const onPop = () => {
+ if (dirtyRef.current && !window.confirm("Discard unsaved changes and follow browser history?")) {
+ history.pushState(history.state, "", currentUrl.current); return;
+ }
+ setRoute(readLocation()); currentUrl.current = location.href;
+ setDirty(false); setEditing(false); setFocused(false); setSection(0); setNotice("");
+ };
+ const onUnload = (e: BeforeUnloadEvent) => { if (dirtyRef.current) { e.preventDefault(); e.returnValue = ""; } };
+ window.addEventListener("popstate", onPop); window.addEventListener("beforeunload", onUnload);
+ return () => { window.removeEventListener("popstate", onPop); window.removeEventListener("beforeunload", onUnload); };
+ }, []);
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ if ((e.target as HTMLElement).closest("input,textarea,select,button,[contenteditable],[role=tablist]")) return;
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
+ e.preventDefault(); cycle(e.key === "ArrowRight" ? 1 : -1);
+ };
+ window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey);
+ });
+ function cycle(delta: number) { const keys: Variant[] = ["A", "B", "C"]; navigate({ variant: keys[(keys.indexOf(variant) + delta + 3) % 3] }, true); }
+ function choose(id: string) {
+ if (!canLeave()) return;
+ if (surface === "workspace") writeRoute({ ...route, workspace: id });
+ setSelection(s => ({ ...s, [scope]: id })); setFocused(true); setEditing(false); setDirty(false); setNotice("");
+ if (surface === "pi") setSection(inventory.pi.findIndex(r => r.id === id));
+ }
+ function changeSection(n: number) {
+ if (!canLeave()) return;
+ setEditing(false); setDirty(false); setSection(n); setFocused(true);
+ }
+ function setScenario(next: Scenario) { if (!canLeave()) return; setEditing(false); setDirty(false); setScenarioMap(s => ({ ...s, [scope]: next })); }
+ function beginOperation(label: string) {
+ setScenarioMap(s => ({ ...s, [scope]: "running" }));
+ notify(`${label}: simulated operation started. You can switch pages and return. Use “Complete operation” to finish the demonstration.`);
+ }
+ function beginEdit() { setEditing(true); setFocused(true); }
+ function primaryAction() {
+ if (surface === "memory") { if (!canLeave()) return; setSelection(s => ({ ...s, [scope]: "new" })); setEditing(true); setDirty(false); setFocused(true); setSection(0); }
+ else if (surface === "evidence") changeSection(2);
+ else beginOperation(modules[surface].primary);
+ }
+ const props: ContentProps = {
+ surface, workspace: currentWorkspace, scenario, item: selected === "new" ? undefined : item, section, editing,
+ connectionValues: connections[workspace],
+ onConnectionSave: (values) => { setConnections(s => ({ ...s, [workspace]: values })); setDirty(false); setEditing(false); notify("Connection saved in memory for this prototype. No real credentials or database were used."); },
+ onEdit: beginEdit, onDirty: setDirty, onCancel: () => { if (canLeave()) { setEditing(false); setDirty(false); } },
+ onSave: (updated) => {
+ if (surface === "memory" && updated) {
+ const next = selected === "new" ? [updated, ...(scenario === "empty" ? [] : rows)] : rows.map(r => r.id === updated.id ? updated : r);
+ setSavedCards(s => ({ ...s, [workspace]: next })); setSelection(s => ({ ...s, [scope]: updated.id }));
+ }
+ setScenarioMap(s => ({ ...s, [scope]: "ready" }));
+ setDirty(false); setEditing(false); notify("Saved in this prototype only. No installation data was changed.");
+ },
+ onDelete: () => { setSavedCards(s => ({ ...s, [workspace]: rows.filter(r => r.id !== selected) })); setSelection(s => ({ ...s, [scope]: rows.find(r => r.id !== selected)?.id ?? "" })); notify("Card and its links removed from the demo. Other cards are kept."); },
+ onNavigate: (next) => navigate({ surface: next }), onOperation: beginOperation, onNotice: notify,
+ onSection: changeSection, onScenario: setScenario,
+ };
+ const context = {surface === "pi" ? "Installation scope" : "Administrative workspace"} {surface === "pi" ? All workspaces : navigate({ workspace: e.target.value })}>{workspaces.map(w => {w.name} )} }Core workspace: PSD Clinical
;
+ const heading = ;
+ const sections = {modules[surface].sections.map((name, n) => changeSection(n)}>{name} )} ;
+ const index =
{modules[surface].noun} {scenario === "empty" ? 0 : rows.length} {surface !== "pi" &&
setFilters(s => ({ ...s, [scope]: e.target.value }))} />}
{scenario === "empty" ?
No records in this demonstration state.
: shownRows.length ? shownRows.map(r =>
choose(r.id)}>{r.category} {r.title} {r.meta} ) :
No matches. Try a shorter search.
}
;
+ const content = ;
+ const status = {surface === "pi" ? "Installation catalog" : `Scope: ${currentWorkspace.id}`} Illustrative records
;
+ const navigation = <>ThothII
Administration {order.map(s => { const Icon = icons[s]; return { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); navigate({ surface: s }); }} aria-current={surface === s ? "page" : undefined}>{s === "pi" ? "Pi runtime" : s[0].toUpperCase() + s.slice(1)} {surface === s && } ; })} Working context
{currentWorkspace.name} Browsing here does not change the core session. >;
+
+ return <>
+
{ setCollapsed(!collapsed); setHostOpen(!hostOpen); }} aria-label="Toggle portal sidebar"> Omics Portal Clinical data & research Administrator AD
+
PORTAL
{[[LayoutDashboard,"Overview"],[Users,"Patients"],[Database,"Clinical data"],[Settings2,"ThothII"]].map(([Icon,label]) => { const I = Icon as typeof Menu; return {String(label)}
; })}Host shell preview
+
Omics Portal ThothII Administration
+
ThothII administration setAdminOpen(!adminOpen)} aria-expanded={adminOpen} aria-label="Toggle administration navigation">{adminOpen ? : }
+
+
+ {context}{heading}{status}
+ {notice && {notice} setNotice("")}>
}
+ {dirty && Unsaved changes. Save or cancel before leaving.
}
+ {variant === "A" && {index}
{surface !== "pi" && sections}{content}
}
+ {variant === "B" && {sections}{!focused && section === 0 && surface !== "pi" ? <>
{modules[surface].noun} Select a record to open its full page. Search and selection are kept when you return.
{index}> : <>{surface !== "pi" &&
{ if (canLeave()) { setFocused(false); setEditing(false); setDirty(false); setSection(0); } }}> Back to {modules[surface].noun.toLowerCase()} }{content}>}
}
+ {variant === "C" && Next action
{scenario === "running" ? "An operation is in progress" : scenario === "attention" ? "Resolve the preparation step" : surface === "evidence" ? "Maintain the local knowledge archive" : "Ready for administrative work"} {surface === "workspace" ? "Check the catalog prerequisite, then build the reference indexes for this workspace." : surface === "database" ? "Synchronize the source schema before reviewing metadata and preparing the workspace." : surface === "memory" ? "Review the authoritative cards. Retry any pending retrieval-index updates separately." : surface === "evidence" ? "Edit the Markdown files externally, consolidate locally, then review and push your Git changes." : "Check the runtime and its configured default. Changes to models are made in the installation file."}
{modules[surface].sections.map((s,i) => changeSection(i)}>{String(i+1).padStart(2,"0")} {s} )} {section === 0 && surface !== "pi" && Choose {surface === "workspace" ? "workspace" : "record"}{item?.title} {index} }{content}
}
+ {surface === "pi" ? "Global installation controls" : "Workspace-scoped administration"} No active core session required
+
+
+
+ {import.meta.env.DEV && DESIGN REVIEW · V2 cycle(-1)}> Layout variant navigate({ variant: e.target.value as Variant },true)}>{Object.entries(variants).map(([key,name]) => {key} · {name} )} cycle(1)}> Synthetic data · No server writes Review settings Page state setScenario(e.target.value as Scenario)}>{["ready","attention","running","empty","error"].map(s => {s} )} Host width setWidth(e.target.value)}>Available width 960 px host 390 px host {scenario === "running" &&
{ setScenario("ready"); notify("Simulated operation completed."); }}>Complete operation }
States and edits are held in memory and reset on reload. The URL retains the page, administrative workspace and layout.
}
+ >;
+}
diff --git a/frontend/prototypes/administration-review/Content.tsx b/frontend/prototypes/administration-review/Content.tsx
new file mode 100644
index 00000000..ae54eabe
--- /dev/null
+++ b/frontend/prototypes/administration-review/Content.tsx
@@ -0,0 +1,95 @@
+import { useState, type ReactNode } from "react";
+import { ArrowUpRight, Check, CircleAlert, Clock3, Copy, Database, FileText, GitBranch, KeyRound, Play, RefreshCw, ShieldCheck } from "lucide-react";
+import { Button } from "./App";
+import { type Item, type Scenario, type Surface, type workspaces } from "./model";
+
+export function Status({ scenario }: { scenario: Scenario }) {
+ const Icon = scenario === "ready" ? Check : scenario === "running" ? Clock3 : CircleAlert;
+ return {{ ready: "Ready", attention: "Needs attention", running: "In progress", empty: "Not configured", error: "Unavailable" }[scenario]} ;
+}
+function Section({ title, children, label }: { title: string; children: ReactNode; label?: string }) {
+ return {label && {label}
}{title} {children} ;
+}
+function Facts({ data }: { data: Array<[string, ReactNode]> }) { return {data.map(([name,value]) =>
{name} {value} )} ; }
+function CopyValue({ value, label }: { value: string; label: string }) {
+ const [copied, setCopied] = useState(false);
+ return {value} { try { await navigator.clipboard.writeText(value); setCopied(true); } catch { setCopied(false); } }}>{copied ? : }
;
+}
+export type ConnectionValues = { host: string; port: string; database: string; user: string };
+export type ContentProps = {
+ surface: Surface; workspace: typeof workspaces[number]; scenario: Scenario; item?: Item; section: number; editing: boolean;
+ onEdit: () => void; onDirty: (v: boolean) => void; onCancel: () => void; onSave: (item?: Item) => void;
+ onDelete: () => void; onNavigate: (s: Surface) => void; onOperation: (label: string) => void;
+ onNotice: (text: string) => void; onSection: (section: number) => void; onScenario: (s: Scenario) => void;
+ connectionValues?: ConnectionValues; onConnectionSave: (values: ConnectionValues) => void;
+};
+export function Content(p: ContentProps) {
+ if (p.scenario === "error") return This page could not be loaded The service is unavailable. Your last saved data has not been changed.
p.onScenario("ready")}>Retry loading ;
+ if (p.scenario === "empty" && !p.editing) return {p.surface === "memory" ? "No Memory cards yet" : p.surface === "evidence" ? "No Evidence files in this workspace" : p.surface === "database" ? "Database not configured" : p.surface === "workspace" ? "Workspace preparation required" : "No available session models"} {p.surface === "memory" ? "Create a card to record a reusable clarification or an approved SQL rule." : p.surface === "evidence" ? "Create a Markdown file in the curated archive, then run consolidation to activate it." : p.surface === "database" ? "Configure one catalog binding for this workspace. The source database remains read-only." : p.surface === "workspace" ? "Check the source and configure the database before running preprocessing." : "Add eligible models to the Installation Model Catalog and reload the runtime."}
{p.surface === "database" && { p.onScenario("ready"); p.onSection(1); }}>Configure catalog }{p.surface === "workspace" && p.onNavigate("database")}>Open Database management }{p.surface === "evidence" && } ;
+ return
+ {p.scenario === "running" &&
Operation in progress The job belongs to {p.surface === "pi" ? "this installation" : p.workspace.name}. Changing pages does not cancel it.
{ p.onScenario("ready"); p.onNotice("Demonstration completed. The server was not contacted."); }}>Demo: complete operation }
+ {p.surface === "workspace" ?
: p.surface === "database" ?
: p.surface === "evidence" ?
: p.surface === "memory" ?
:
}
+
;
+}
+
+function Workspace(p: ContentProps) {
+ const [clear, setClear] = useState(false);
+ const [secret, setSecret] = useState("");
+ return <>Selected workspace
{p.workspace.name} {p.workspace.id}
+ {p.section === 0 && <>
+ Preprocessing builds the reference indexes for this workspace from its catalog snapshot and Evidence. Memory has its own independent index.
Workspace source Active Git revision · a71c9f2
Database catalog {p.workspace.database} · Metadata revision 12
p.onNavigate("database")} aria-label="Open database prerequisite"> {p.scenario === "attention" ? : }Reference indexes {p.scenario === "attention" ? "Built from revision 11. Run preprocessing to use revision 12." : "Current for metadata revision 12"}
+ Workspace-scoped operation Uses the catalog snapshot, prepares schema and reference indexes, and includes configured Evidence. It does not modify the source database.
p.onOperation("Workspace preprocessing")}>Run preprocessing p.onNavigate("database")}>Open database
+ Clear derived data Remove schema/Evidence vectors, LSH and derived checkpoints. Keep the curated files, catalog, source database, Memory and sessions.
{clear ? Clear derived data for {p.workspace.name}? { setClear(false); p.onScenario("attention"); p.onNotice("Derived data cleared in the simulation. Authoritative data was kept."); }}>Confirm clear setClear(false)}>Cancel
: setClear(true)}>Clear derived data… }
+ >}
+ {p.section === 1 && <>Change identity and Evidence source declarations in the workspace repository. Fetch and validate the candidate revision before activating it.
p.onNavigate("database")}>{p.workspace.database} ]]}/> p.onNotice("Simulated source validation passed. No repository files were changed.")}>Validate workspace source p.onNotice("Simulated connection check passed.")}>Test workspace connections
Repository updates apply to all workspaces listed in the registry. An invalid candidate leaves the previous revision active.
p.onOperation("Shared workspace repository update")}>Update workspace repository >}
+ {p.section === 2 && }
+ >;
+}
+function LayersMark() { return ; }
+
+function Catalog(p: ContentProps) {
+ const [tab, setTab] = useState("Columns");
+ const [connection, setConnection] = useState(p.connectionValues ?? { host: "warehouse.internal", port: "5432", database: p.workspace.database, user: "readonly_analyst" });
+ const [password, setPassword] = useState("");
+ return <>{p.workspace.name} / Catalog
{p.section === 0 ? `${p.workspace.schema}.${p.item?.title ?? "patients"}` : p.workspace.database} {p.section === 0 ? p.item?.detail : "One workspace binding · PostgreSQL · Read-only source access"}
+ Workspace preparation {p.scenario === "attention" ? "needs a refresh" : "uses metadata revision 12"}. p.onNavigate("workspace")}>Open workspace
+ {p.section === 0 && <>{["Columns","Relationships"].map(t => setTab(t)}>{t} )} {tab === "Columns" ? <>Catalog columns · illustrative excerpt Column Type Description Sensitivity {[["patient_id","uuid","Identificativo del paziente","Identifier"],["recorded_at","timestamp","Data di registrazione","Not marked"],["status","varchar","Stato del record","Not marked"],["source_code","varchar","Codice del sistema origine","Needs review"]].map(([name,type,description,sensitivity]) => {name}{type}{description} {sensitivity} )}
Column descriptions and sensitivity are catalog metadata. Source values are not displayed in this prototype.
> : Catalog relationships From To Kind visits.patient_idpatients.patient_idForeign key samples.patient_idpatients.patient_idForeign key
} p.onOperation("Schema synchronization")}>Synchronize schema p.onSection(2)}>Review metadata
>}
+ {p.section === 1 && Connection and credentials are scoped to this workspace binding. Saving a connection does not synchronize the schema.
}
+ {p.section === 2 && <>Generate draft descriptions from the catalog and permitted source samples. Review and consolidate them using the existing catalog workflow.
p.onOperation("Description generation")}>Generate descriptions Review column sensitivity before sampling and analytical use. Source synchronization, description generation and sensitivity analysis remain distinct operations.
p.onOperation("Sensitivity analysis")}>Analyze sensitivity p.onNotice("Sample review: source_code requires an administrator’s sensitivity classification.")}>Review suggestions
>}
+ >;
+}
+
+function Evidence(p: ContentProps) {
+ const [decision, setDecision] = useState("");
+ return <>
+ {p.section === 0 && <>{p.item?.category ?? "Domain knowledge"}
{p.item?.title} Active locally Canonical Markdown archive
Edit this document with your preferred external editor. Consolidation activates valid local changes; Git review and push are manual.
p.onSection(2)}>Maintenance instructions p.onSection(1)}>Sources & comparisons
>}
+ {p.section === 1 && <>Source maintenance
Refresh deliberately, review changes External sources are reacquired only when requested. A manual correction remains in force until you resolve the comparison.
p.onOperation("Evidence source refresh")}>Import or refresh sources
Current manual correction
Contare i pazienti distinti con almeno una visita valida.
Refreshed source proposal
Contare i record delle visite nel periodo selezionato.
Replacing the content requires an explicit decision. Refresh alone does not overwrite it.
{decision ? {decision} (prototype only)
: setDecision("Current correction kept")}>Keep current correction setDecision("Source proposal selected for activation")}>Use source proposal
} >}
+ {p.section === 2 && <>External editing
Maintain the Evidence archive The browser helps you locate and verify knowledge. Your editor maintains the Markdown files.
Edit the canonical files Use the installation host path shown by the real application. This demonstration shows relative paths only.
Validate and consolidate Correct validation errors, then activate the local index. Source refresh is a separate operation.
Review and publish with Git From the Evidence working tree, inspect the status and diff, then commit and push manually.
p.onNotice("File list refreshed in the demonstration. No host files were read.")}>Refresh files >}
+ >;
+}
+
+function Memory(p: ContentProps) {
+ const [title,setTitle] = useState(p.item?.title ?? "");
+ const [detail,setDetail] = useState(p.item?.detail ?? "");
+ const [category,setCategory] = useState(p.item?.category ?? "Domain clarification");
+ const [confirmDelete,setConfirmDelete] = useState(false);
+ if (p.editing) return ;
+ if (!p.item) return No cards to display Create a new card using the action above.
;
+ return <>{p.item.category}
{p.item.title} {p.scenario === "attention" ? "Index update pending" : "Indexed"} Authoritative card available
Edit card
+ {p.section === 0 && <>Il criterio rende esplicita l’unità di analisi e impedisce che le relazioni uno-a-molti alterino il risultato.
p.onSection(1)}>Dependencies & links setConfirmDelete(true)}>Delete card…
{confirmDelete && Delete “{p.item.title}” and its links? Other cards and Evidence are kept.
Confirm deletion setConfirmDelete(false)}>Keep card
}>}
+ {p.section === 1 && <>Structured dependencies connect this card to the catalog. A successful physical schema synchronization can remove cards whose dependencies no longer exist.
Evitare il doppio conteggio nei join Related explained error · sample link
Links belong to the Memory archive. They do not make this card an Evidence unit.
>}
+ {p.section === 2 && The authoritative card is preserved A failed projection does not delete saved content. Retry the retrieval-index update explicitly.
{p.scenario === "attention" ? p.onOperation("Memory index retry")}>Retry index update
: No pending updates for this demonstration.
} }
+ >;
+}
+
+function Pi(p: ContentProps) {
+ const [logs,setLogs] = useState(false);
+ const [platform,setPlatform] = useState("Linux");
+ return <>
+ Installation scope
{["Runtime & catalog default","Available session models","Sanitized diagnostics","Host maintenance"][p.section]} These settings apply across workspaces. The browser does not maintain a second model configuration.
+ {p.section === 0 && <>Bundled Pi
0.80.3 sample version Change modelCatalog.defaults.session in the installation YAML and reload Pi to apply it.
p.onOperation("Catalog default test")}>Test catalog default p.onSection(3)}>Host instructions
>}
+ {p.section === 1 && {[["GLM 5.3","zai / glm-5.3","Default"],["DeepSeek V4 Pro","deepseek / deepseek-v4-pro","Available"],["Local Qwen","local-qwen / qwen3.6-35b-a3b","Available"]].map(([name,id,status]) =>
)}
Illustrative catalog. Availability in production is read from the installation.
}
+ {p.section === 2 && Fetch up to 200 sanitized lines only when requested. Credentials and declared secret values are removed.
setLogs(true)}>Show sanitized logs {logs && {`[sample] Runtime ready\n[sample] Catalog default resolved\n[sample] Provider credential: [REDACTED]\n[sample] No active diagnostic errors`} } }
+ {p.section === 3 && <>{["Linux","macOS","Windows"].map(v=>setPlatform(v)} aria-current={platform===v?"page":undefined}>{v} )} Run these commands on the installation host. Review active sessions before a restart or version update.
Edit the single configuration source \\thothii-installation.yaml" : "deploy//thothii-installation.yaml"} label="installation path"/> Reload and verify Commands are examples to review and copy; nothing is executed from this prototype.
>}
+ >;
+}
diff --git a/frontend/prototypes/administration-review/README.md b/frontend/prototypes/administration-review/README.md
new file mode 100644
index 00000000..f2d8591c
--- /dev/null
+++ b/frontend/prototypes/administration-review/README.md
@@ -0,0 +1,122 @@
+# Administration design review V2
+
+Throwaway prototype, 2026-09-10. **No variant selected yet.**
+
+Question: which page structure best supports the five existing administrative
+workflows inside the space allocated by Omics Portal?
+
+## Run
+
+From `frontend/`:
+
+```sh
+npm run prototype:administration-review
+```
+
+Open `http://127.0.0.1:5175/?thoth_route=administration/workspace&variant=A`.
+The development-only configuration rejects production builds. No production
+imports, API requests, real credentials, patient records or host writes are used.
+Google Fonts loads the same Manrope/Fraunces families as the current frontend;
+system/Georgia fallbacks remain usable offline.
+
+The original prototype remains untouched at `../administration-pages/` on port
+5174, preserved in commit `debb63d8` on `codex/prototype-administration-pages`.
+The incomplete production changes already present in the working tree were not
+continued by this review. The previous specification/tickets are suspended drafts.
+
+## Confirmed after the renewed grill
+
+1. Preserve existing features and authoring/persistence contracts. Workspace
+ definitions are Git-managed; Evidence uses external Markdown editing and local
+ consolidation; Memory has web CRUD; Database owns its catalog binding and
+ metadata operations; Pi configuration remains installation-authored.
+2. Share the administrative Workspace selection across relevant pages, without
+ changing the Workspace selected for core work. Pi has installation scope.
+3. Share visual hierarchy and interaction conventions; allow each module's content
+ and internal structure to differ. Do not invent a resource list for Pi.
+4. Use explicit saves and unsaved-change protection, including browser traversal.
+ Preserve filters and selection. Leaving a page must not implicitly cancel an
+ existing server-owned job. Actual API/job recovery limits must be audited in the
+ subsequent spec; the prototype only models the intended experience.
+5. **Final mounting and integration happen on the server**, not in the owner's
+ local Omics Portal checkout. If Omics Portal code changes, the owner can pull
+ those changes locally afterward. Record server integration as an explicit
+ delivery/validation step, respecting existing operational migration gates.
+6. Five peer pages, right-side navigation, English UI chrome and workspace-language
+ content remain accepted. Namespaced `thoth_route` supports page restoration;
+ `thoth_workspace` holds the stable administrative selection.
+7. Full preprocessing belongs to Workspace readiness, with Database prerequisites
+ and cross-links. Each workspace currently has at most one catalog binding;
+ this design does not introduce shared database entities or a many-to-many model.
+8. Preserve all prototypes. Proceed to `to-spec` and `to-tickets` after the owner
+ selects the new layout(s). No new specification or tracker tickets were
+ published in this review turn.
+
+## Alternatives
+
+| Variant | Structure | Useful when | Tradeoff |
+| --- | --- | --- | --- |
+| A: Workbench | Persistent list/section navigation next to detail; Pi uses its installation sections | Frequent switching between records, catalog inspection and Memory curation | The detail pane has less width; narrow containers stack the areas |
+| B: Focused pages | Full-width list, followed by a full record page with section navigation and return to list | Sustained reading and editing; Evidence and complex forms | Switching records takes a return to the list |
+| C: Operations first | Contextual operational summary, section navigation, collapsible record choice and detail | Workspace preparation, diagnostics and occasional maintenance | More vertical space and less immediate access to the full record list |
+
+All five routes render domain-specific content in all three variants. This is a
+representative interaction prototype, not an exhaustive replacement for existing
+forms. Advanced Memory link editing, all Database grids, detailed review histories,
+permissions and full API validation are implementation/spec work, not new scope.
+
+The V2 prototype uses a separate development entry beside the previous prototype
+because the production worktree contains interrupted, unapproved implementation.
+Its portal frame represents header/sidebar geometry only; it is not a running
+Django/Bootstrap integration. No Omics Portal files were changed.
+
+## What to try
+
+- Switch all five modules from the right rail (the navigation button when narrow).
+- Change Administrative workspace, then open another module. Core workspace stays
+ PSD Clinical; Pi explicitly says All workspaces.
+- Open a Memory card, edit its title/content and save. Attempt navigation while
+ dirty and cancel the browser confirmation. Create/delete sample cards as well.
+- Open Database Connection, edit the sample host and save it in memory.
+- Follow Database to Workspace and back. The administrative selection is retained.
+- Start a simulated preprocessing job, visit Pi, return and complete the demo job.
+- Open Evidence maintenance/source comparison and Pi diagnostics/host instructions.
+- Use Review settings to simulate ready/attention/running/empty/error states and
+ 960/390px host containers independently of browser width.
+- Use Back/Forward and reload. Module, Workspace and variant are in the URL;
+ fixtures, edits and simulated jobs reset on reload. Real job resumption requires
+ the subsequent backend-aware implementation.
+
+Layout variants are selectable with the bottom review bar, or left/right arrows
+outside interactive controls. These controls are not proposed production UI.
+
+## Design decisions
+
+Impeccable's product guidance informs a daylight-office, warm-light presentation:
+institutional bordeaux host header, restrained action color, distinct neutral
+surfaces for navigation and content, readable statuses with icons and words.
+Manrope carries operational controls and A/C headings; B uses Fraunces for the page
+and record titles. Prose has bounded line lengths, forms use consistent labels,
+and responsive changes depend on container width. The host owns its red topbar;
+Thoth does not add a second red page banner.
+
+## Verification performed
+
+- Isolated TypeScript check of the V2 entry passed.
+- Chromium: 3 variants × 5 modules × 4 viewport widths (1600, 1280, 960, 390):
+ 60 initial views with no document-level horizontal overflow or runtime errors.
+ Tables can scroll inside their own bounded container.
+- A 390px simulated host within a 1600px browser had a 372px content area and no
+ overflow beyond the host. The inline navigation collapsed at container width.
+- Twelve browser behavior checks passed: unsaved navigation cancellation, Memory
+ save, shared administrative context/core isolation, Pi scope, job navigation,
+ connection save, Back, routed Workspace on reload, B detail/return, mobile
+ navigation, creation from an empty Memory state, and error/retry.
+- Desktop A/B/C and mobile screenshots were visually inspected. The prototype
+ was opened in the in-app browser for review.
+- Standalone Playwright WebKit was unavailable (browser executable not installed).
+ No cross-browser, real-device, Bootstrap collision, authentication, backend or
+ server-integration acceptance is claimed by these prototype checks.
+
+Verdict pending the owner's selection. The next specification must distinguish
+demonstrated UI decisions from simulated data and behavior.
diff --git a/frontend/prototypes/administration-review/index.html b/frontend/prototypes/administration-review/index.html
new file mode 100644
index 00000000..aecac66c
--- /dev/null
+++ b/frontend/prototypes/administration-review/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ ThothII · Administration design review
+
+
+
+
+
+
+
+
+
diff --git a/frontend/prototypes/administration-review/main.tsx b/frontend/prototypes/administration-review/main.tsx
new file mode 100644
index 00000000..9ce7fb11
--- /dev/null
+++ b/frontend/prototypes/administration-review/main.tsx
@@ -0,0 +1,6 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { App } from "./App";
+import "./review.css";
+
+createRoot(document.getElementById("root")!).render( );
diff --git a/frontend/prototypes/administration-review/model.ts b/frontend/prototypes/administration-review/model.ts
new file mode 100644
index 00000000..eefde667
--- /dev/null
+++ b/frontend/prototypes/administration-review/model.ts
@@ -0,0 +1,51 @@
+// Synthetic data only. These are presentation fixtures, never API payloads.
+export type Surface = "workspace" | "database" | "evidence" | "memory" | "pi";
+export type Variant = "A" | "B" | "C";
+export type Scenario = "ready" | "attention" | "running" | "empty" | "error";
+export type Item = { id: string; title: string; category: string; detail: string; meta: string };
+export const variants = { A: "Workbench", B: "Focused pages", C: "Operations first" };
+export const workspaces = [
+ { id: "psd-clinical", name: "PSD Clinical", database: "clinical_dwh", schema: "clinical" },
+ { id: "genetics", name: "Genetics research", database: "genetics_dwh", schema: "research" },
+ { id: "sandbox", name: "Research sandbox", database: "sandbox_dwh", schema: "sandbox" },
+];
+export const modules: Record = {
+ workspace: { title: "Workspace management", subtitle: "Source identity, runtime requirements and preparation for analysis.", noun: "Workspaces", primary: "Run preprocessing", sections: ["Readiness", "Source & configuration", "Runtime secrets"] },
+ database: { title: "Database management", subtitle: "Connection settings and the metadata used to understand your data.", noun: "Schema tables", primary: "Synchronize schema", sections: ["Schema", "Connection", "Descriptions & sensitivity"] },
+ evidence: { title: "Evidence management", subtitle: "Read domain knowledge, locate source files and maintain the local archive.", noun: "Evidence units", primary: "Maintenance instructions", sections: ["Library", "Sources & comparisons", "Maintenance"] },
+ memory: { title: "Memory management", subtitle: "Review and refine knowledge retained from analytical work.", noun: "Memory cards", primary: "New card", sections: ["Library", "Dependencies & links", "Index updates"] },
+ pi: { title: "Pi management", subtitle: "Inspect the installation runtime, available models and diagnostics.", noun: "Installation", primary: "Test catalog default", sections: ["Runtime", "Session models", "Diagnostics", "Host instructions"] },
+};
+export const evidence: Item[] = [
+ { id: "ev-population", title: "Popolazione clinica di riferimento", category: "Domain rule", detail: "Il conteggio della popolazione deve riferirsi ai pazienti distinti. La presenza di più visite non incrementa il numero di pazienti.", meta: "Manual declaration · Active locally" },
+ { id: "ev-visit", title: "Visita e follow-up", category: "Glossary", detail: "Una visita rappresenta un evento clinico. Il follow-up descrive un controllo successivo alla valutazione iniziale.", meta: "Source document · Active locally" },
+ { id: "ev-status", title: "Stato del campione", category: "Enumeration", detail: "Lo stato del campione distingue il materiale raccolto, in lavorazione e validato. I valori mancanti non equivalgono a un esito negativo.", meta: "Manual declaration · Active locally" },
+ { id: "ev-date", title: "Intervallo temporale di osservazione", category: "SQL example", detail: "Esplicitare sempre le date di inizio e fine dell’osservazione. Un intervallo semiaperto evita di escludere eventi dell’ultimo giorno.", meta: "Source document · Active locally" },
+];
+export const memories: Item[] = [
+ { id: "mem-count", title: "Conteggio dei pazienti distinti", category: "SQL rule", detail: "Quando la domanda riguarda il numero di pazienti, usare COUNT(DISTINCT patient_id) dopo il collegamento con le visite.", meta: "Approved in a session · Indexed" },
+ { id: "mem-active", title: "Significato di paziente attivo", category: "Domain clarification", detail: "Un paziente è attivo nel periodo osservato se presenta almeno una visita valida nel periodo richiesto.", meta: "Manual correction · Indexed" },
+ { id: "mem-question", title: "Pazienti con almeno due visite", category: "Solved question", detail: "Raggruppare per paziente e filtrare i gruppi con almeno due visite, mantenendo esplicito l’intervallo temporale.", meta: "Approved in a session · Indexed" },
+ { id: "mem-error", title: "Evitare il doppio conteggio nei join", category: "Explained error", detail: "Il join tra due relazioni uno-a-molti può moltiplicare le righe. Aggregare prima del collegamento quando la misura lo richiede.", meta: "Approved correction · Indexed" },
+];
+export const tables: Item[] = [
+ { id: "patients", title: "patients", category: "18 columns", detail: "Anagrafica dei pazienti e identificativi di collegamento con gli eventi clinici.", meta: "Primary key: patient_id · 3 relationships" },
+ { id: "visits", title: "visits", category: "24 columns", detail: "Eventi clinici associati ai pazienti, con data, tipo e stato della visita.", meta: "Primary key: visit_id · 2 relationships" },
+ { id: "samples", title: "samples", category: "12 columns", detail: "Campioni biologici raccolti durante il percorso clinico.", meta: "Primary key: sample_id · 2 relationships" },
+ { id: "observations", title: "observations", category: "16 columns", detail: "Osservazioni e misurazioni cliniche registrate durante le visite.", meta: "Primary key: observation_id · 1 relationship" },
+];
+export const piItems: Item[] = [
+ { id: "runtime", title: "Runtime & default", category: "Installation", detail: "Bundled Pi runtime and the catalog default for new sessions.", meta: "Read-only configuration" },
+ { id: "models", title: "Session models", category: "Model catalog", detail: "Models available to the interactive workflow.", meta: "Installation-defined eligibility" },
+ { id: "diagnostics", title: "Sanitized diagnostics", category: "On demand", detail: "Request a bounded excerpt without exposing declared secrets.", meta: "Up to 200 lines" },
+ { id: "host", title: "Host instructions", category: "Operator guide", detail: "Maintain the installation configuration with the native tht CLI.", meta: "Linux · macOS · Windows" },
+];
+export const workspaceItems: Item[] = workspaces.map(w => ({ id: w.id, title: w.name, category: "Git-managed workspace", detail: "Identity and Evidence sources are maintained in the workspace repository.", meta: w.database }));
+export const inventory: Record = { workspace: workspaceItems, database: tables, evidence, memory: memories, pi: piItems };
+export function readLocation() {
+ const q = new URLSearchParams(location.search);
+ const surface = q.get("thoth_route")?.split("/").at(-1) as Surface;
+ const variant = q.get("variant") as Variant;
+ const workspace = q.get("thoth_workspace");
+ return { surface: surface in modules ? surface : "workspace" as Surface, variant: variant in variants ? variant : "A" as Variant, workspace: workspaces.some(w => w.id === workspace) ? workspace! : "psd-clinical" };
+}
diff --git a/frontend/prototypes/administration-review/review.css b/frontend/prototypes/administration-review/review.css
new file mode 100644
index 00000000..990d3627
--- /dev/null
+++ b/frontend/prototypes/administration-review/review.css
@@ -0,0 +1,298 @@
+/* Product scene: an analyst maintaining clinical knowledge on a daylight office
+ workstation. Warm light surfaces match the host; bordeaux marks actions and
+ location. Fixed-size typography keeps forms predictable inside the portal. */
+:root {
+ --rv-red: oklch(55.87% .1881 23.2);
+ --rv-red-dark: oklch(47% .155 23.2);
+ --rv-red-soft: oklch(96% .018 23.2);
+ --rv-ink: oklch(28% .012 355);
+ --rv-muted: oklch(49% .012 355);
+ --rv-paper: oklch(99.2% .002 20);
+ --rv-worktop: oklch(96.5% .005 20);
+ --rv-sunken: oklch(93% .007 20);
+ --rv-line: oklch(87.5% .009 20);
+ --rv-green: oklch(41% .075 156);
+ --rv-green-bg: oklch(95% .02 156);
+ --rv-amber: oklch(42% .085 73);
+ --rv-amber-bg: oklch(96% .035 83);
+ --rv-blue: oklch(43% .065 246);
+ --rv-blue-bg: oklch(95% .018 246);
+ --rv-sans: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --rv-serif: "Fraunces", Georgia, serif;
+ --rv-mono: ui-monospace, "SF Mono", Menlo, monospace;
+ color: var(--rv-ink); background: var(--rv-sunken); font-family: var(--rv-sans);
+ font-size: 16px; font-synthesis: none;
+}
+body { margin: 0; }
+.rv-host *, .rv-lab * { box-sizing: border-box; }
+.rv-host, .rv-lab { line-height: 1.5; }
+.rv-host button, .rv-host input, .rv-host select, .rv-host textarea, .rv-lab button, .rv-lab select { font: inherit; }
+.rv-host button, .rv-lab button { cursor: pointer; }
+.rv-host button:disabled { cursor: not-allowed; opacity: .5; }
+.rv-host button, .rv-lab button { color: inherit; }
+.rv-host :is(button,a,input,select,textarea,summary):focus-visible, .rv-lab :is(button,select,summary):focus-visible { outline: 3px solid var(--rv-red); outline-offset: 3px; }
+.rv-host :is(h1,h2,h3,p) { margin: 0; }
+.rv-host h1 { font-size: 1.65rem; line-height: 1.2; letter-spacing: -.045em; font-weight: 800; text-wrap: balance; }
+.rv-host h2 { font-size: 1.25rem; line-height: 1.35; letter-spacing: -.025em; font-weight: 750; overflow-wrap: anywhere; }
+.rv-host h3 { font-size: 1rem; font-weight: 750; }
+.rv-host p { max-width: 72ch; }
+.rv-host code, .rv-mono { font-family: var(--rv-mono); font-size: .8rem; overflow-wrap: anywhere; }
+.rv-host svg { flex-shrink: 0; vertical-align: middle; }
+.rv-host { container: portal / inline-size; background: var(--rv-worktop); min-height: calc(100dvh - 72px); max-width: 100%; margin: 0 auto 110px; box-shadow: 0 0 0 1px var(--rv-line); }
+.rv-host-compact { width: 960px; }
+.rv-host-phone { width: 390px; }
+.rv-topbar { display: flex; align-items: center; gap: 18px; padding: 0 24px; min-height: 64px; color: var(--rv-paper); background: var(--rv-red); }
+.rv-topbar > button { border: 0; background: transparent; color: inherit; padding: 10px; }
+.rv-topbar strong { font-size: 1.25rem; letter-spacing: -.035em; white-space: nowrap; }
+.rv-topbar strong span { font-weight: 400; }
+.rv-topbar-location { font-size: .8rem; }
+.rv-topbar-divider { height: 22px; width: 1px; background: oklch(95% .005 20 / .35); }
+.rv-topbar-user { margin-left: auto; display: flex; align-items: center; gap: 14px; font-size: .75rem; }
+.rv-avatar { display: grid; place-items: center; width: 32px; height: 32px; background: oklch(95% .005 20 / .2); border: 1px solid oklch(95% .005 20 / .3); border-radius: 50%; font-size: .7rem; font-weight: 800; }
+.rv-host-body { display: grid; grid-template-columns: 208px minmax(0,1fr); }
+.rv-portal-nav { border-right: 1px solid var(--rv-line); padding: 28px 14px; background: var(--rv-paper); }
+.rv-portal-nav > p { font-size: .65rem; font-weight: 800; letter-spacing: .14em; padding: 0 14px 14px; color: var(--rv-muted); }
+.rv-portal-nav > div { display: flex; align-items: center; gap: 12px; padding: 13px 14px; font-size: .8rem; margin-bottom: 4px; border-radius: 5px; color: var(--rv-muted); }
+.rv-portal-nav > .rv-portal-current { color: var(--rv-red-dark); background: var(--rv-red-soft); font-weight: 800; }
+.rv-portal-nav > small { display: block; padding: 20px 14px; font-size: .65rem; color: var(--rv-muted); }
+.rv-host-collapsed .rv-host-body { grid-template-columns: 64px minmax(0,1fr); }
+.rv-host-collapsed .rv-portal-nav { padding-inline: 8px; }
+.rv-host-collapsed .rv-portal-nav > div { padding-inline: 14px; }
+.rv-host-collapsed .rv-portal-nav :is(span,p,small) { display: none; }
+.rv-mount { min-width: 0; padding: 0 22px 24px; }
+.rv-breadcrumb { min-height: 52px; display: flex; align-items: center; flex-wrap: wrap; gap: 9px; font-size: .7rem; color: var(--rv-muted); }
+.rv-breadcrumb > span:last-child { margin-left: auto; }
+.rv-thoth { display: grid; grid-template-columns: minmax(0,1fr) 174px; align-items: start; background: var(--rv-paper); border: 1px solid var(--rv-line); border-radius: 7px; container: thoth / inline-size; isolation: isolate; }
+.rv-page { min-width: 0; grid-column: 1; grid-row: 1; container: page / inline-size; }
+.rv-admin-rail { grid-column: 2; grid-row: 1; align-self: stretch; border-left: 1px solid var(--rv-line); background: var(--rv-worktop); padding: 25px 12px; }
+.rv-eyebrow { color: var(--rv-muted); font-size: .65rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
+.rv-eyebrow > span { font-weight: 500; letter-spacing: .02em; }
+.rv-nav-title { display: block; font-size: .8rem; margin: 5px 0 22px; }
+.rv-admin-rail nav { display: grid; gap: 5px; }
+.rv-admin-rail a { text-decoration: none; display: flex; align-items: center; gap: 9px; padding: 11px 9px; border: 1px solid transparent; border-radius: 5px; font-size: .78rem; font-weight: 650; color: var(--rv-muted); }
+.rv-admin-rail a > svg:last-child:not(:first-child) { margin-left: auto; }
+.rv-admin-rail a:hover { background: var(--rv-sunken); color: var(--rv-ink); }
+.rv-admin-rail a[aria-current] { color: var(--rv-red-dark); border-color: oklch(86% .055 23); background: var(--rv-red-soft); font-weight: 800; }
+.rv-rail-note { border-top: 1px solid var(--rv-line); margin-top: 30px; padding-top: 18px; font-size: .7rem; }
+.rv-rail-note strong { display: block; margin-top: 4px; }
+.rv-rail-note small { display: block; color: var(--rv-muted); margin-top: 10px; line-height: 1.6; }
+.rv-context { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 11px 25px; border-bottom: 1px solid var(--rv-line); font-size: .7rem; background: var(--rv-worktop); border-radius: 7px 0 0 0; }
+.rv-context > span:first-child { color: var(--rv-muted); }
+.rv-context select { background: var(--rv-paper); border: 1px solid var(--rv-line); padding: 6px 26px 6px 9px; border-radius: 4px; font-size: .75rem; font-weight: 700; max-width: 100%; }
+.rv-context-core { margin-left: auto; color: var(--rv-muted); font-size: .65rem; }
+.rv-page-heading { display: flex; align-items: flex-start; flex-wrap: wrap; justify-content: space-between; gap: 20px; padding: 30px 28px 22px; }
+.rv-page-heading > div { flex: 1 1 290px; min-width: 0; }
+.rv-page-heading h1 { margin: 8px 0 10px; }
+.rv-lede { color: var(--rv-muted); font-size: .8rem; line-height: 1.65; }
+.rv-button { min-height: 38px; border: 1px solid var(--rv-line); border-radius: 5px; padding: 9px 13px; display: inline-flex; align-items: center; justify-content: center; gap: 7px; font-size: .75rem !important; font-weight: 750 !important; background: var(--rv-paper); text-align: center; line-height: 1.35; }
+.rv-button:hover { border-color: var(--rv-muted); background: var(--rv-worktop); }
+.rv-button.rv-primary { background: var(--rv-red); color: var(--rv-paper); border-color: var(--rv-red); }
+.rv-button.rv-primary:hover { background: var(--rv-red-dark); border-color: var(--rv-red-dark); }
+.rv-statusline { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 11px 28px; border-block: 1px solid var(--rv-line); font-size: .68rem; color: var(--rv-muted); }
+.rv-sample-note { margin-left: auto; }
+.rv-status { display: inline-flex; align-items: center; gap: 5px; padding: 4px 7px; font-size: .68rem; line-height: 1.35; font-weight: 800; border-radius: 4px; color: var(--rv-green); background: var(--rv-green-bg); white-space: nowrap; }
+.rv-status-attention, .rv-status-empty { color: var(--rv-amber); background: var(--rv-amber-bg); }
+.rv-status-running { color: var(--rv-blue); background: var(--rv-blue-bg); }
+.rv-status-error { color: var(--rv-red-dark); background: var(--rv-red-soft); }
+.rv-tabs { display: flex; gap: 22px; padding: 0 24px; overflow-x: auto; border-bottom: 1px solid var(--rv-line); }
+.rv-tabs button, .rv-subtabs button { flex-shrink: 0; background: transparent; border: 0; border-bottom: 2px solid transparent; padding: 15px 0; font-size: .75rem; font-weight: 700; color: var(--rv-muted); }
+.rv-tabs button[aria-current], .rv-subtabs button[aria-current] { color: var(--rv-red-dark); border-bottom-color: var(--rv-red); }
+.rv-index { padding: 20px 16px; min-width: 0; }
+.rv-index-title { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 16px; }
+.rv-index-title h2 { font-size: .8rem; letter-spacing: 0; }
+.rv-index-title > span { font-family: var(--rv-mono); font-size: .7rem; color: var(--rv-muted); }
+.rv-search { display: flex; gap: 8px; align-items: center; background: var(--rv-paper); border: 1px solid var(--rv-line); border-radius: 4px; padding: 9px; color: var(--rv-muted); }
+.rv-search input { border: 0; background: transparent; padding: 0; min-width: 0; width: 100%; font-size: .7rem; color: var(--rv-ink); }
+.rv-records { display: grid; gap: 5px; margin-top: 16px; }
+.rv-record { display: block; position: relative; width: 100%; text-align: left; border: 1px solid transparent; border-radius: 5px; background: transparent; padding: 13px 24px 13px 12px; }
+.rv-record:hover { background: var(--rv-worktop); }
+.rv-record[aria-current] { border-color: oklch(84% .04 23); background: var(--rv-red-soft); }
+.rv-record small { display: block; font-size: .6rem; color: var(--rv-muted); margin-bottom: 4px; }
+.rv-record strong { display: block; font-size: .8rem; line-height: 1.5; overflow-wrap: anywhere; }
+.rv-record > span { display: block; font-size: .63rem; color: var(--rv-muted); margin-top: 6px; }
+.rv-record > svg { position: absolute; top: 42%; right: 6px; color: var(--rv-muted); }
+.rv-workbench { display: grid; grid-template-columns: minmax(0,1fr); }
+.rv-workbench > .rv-index { background: var(--rv-worktop); border-bottom: 1px solid var(--rv-line); }
+.rv-workbench-detail, .rv-operation-detail { min-width: 0; }
+.rv-content { padding: 24px; min-width: 0; }
+.rv-record-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 28px; }
+.rv-record-heading > div { min-width: 0; }
+.rv-record-heading h2 { margin: 7px 0 10px; }
+.rv-record-heading p:not(.rv-eyebrow) { font-size: .78rem; color: var(--rv-muted); }
+.rv-record-emblem { width: 44px; height: 44px; flex-shrink: 0; display: grid; place-items: center; color: var(--rv-red-dark); background: var(--rv-red-soft); border: 1px solid oklch(88% .03 23); border-radius: 8px; }
+.rv-section { margin: 26px 0; }
+.rv-section:first-child { margin-top: 0; }
+.rv-section > .rv-eyebrow { margin-bottom: 6px; }
+.rv-section > h3 { margin-bottom: 12px; }
+.rv-section > p, .rv-disclosure > p, .rv-instructions p { font-size: .85rem; line-height: 1.8; color: var(--rv-muted); margin: 10px 0 16px; }
+.rv-section > p.rv-reading { font-size: 1rem; color: var(--rv-ink); line-height: 1.85; }
+.rv-facts { display: grid; grid-template-columns: minmax(0,1fr); margin: 18px 0; gap: 0 24px; }
+.rv-facts > div { border-top: 1px solid var(--rv-line); padding: 13px 0; min-width: 0; }
+.rv-facts dt { font-size: .7rem; color: var(--rv-muted); margin-bottom: 5px; }
+.rv-facts dd { margin: 0; font-size: .8rem; font-weight: 650; overflow-wrap: anywhere; }
+.rv-actions { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 20px; }
+.rv-callout { display: flex; align-items: flex-start; gap: 13px; padding: 16px; background: var(--rv-blue-bg); border: 1px solid oklch(85% .025 246); border-radius: 5px; color: var(--rv-blue); }
+.rv-callout strong { display: block; font-size: .8rem; }
+.rv-callout p { font-size: .78rem; line-height: 1.75; margin-top: 6px; }
+.rv-checklist { list-style: none; margin: 18px 0; padding: 0; }
+.rv-checklist li { display: flex; align-items: flex-start; gap: 12px; padding: 14px 0; border-bottom: 1px solid var(--rv-line); }
+.rv-checklist li > svg { width: 17px; height: 17px; color: var(--rv-green); margin-top: 3px; }
+.rv-checklist li strong { font-size: .8rem; display: block; }
+.rv-checklist li span { display: block; font-size: .75rem; color: var(--rv-muted); margin-top: 4px; }
+.rv-checklist button { margin-left: auto; background: transparent; border: 1px solid var(--rv-line); border-radius: 4px; padding: 7px; }
+.rv-disclosure, .rv-choose-record { border: 1px solid var(--rv-line); border-radius: 5px; padding: 14px 16px; margin-top: 24px; }
+.rv-disclosure summary, .rv-choose-record summary { cursor: pointer; font-weight: 700; font-size: .78rem; }
+.rv-confirm { padding: 18px; background: var(--rv-red-soft); border: 1px solid oklch(85% .06 23); border-radius: 5px; font-size: .85rem; margin-top: 20px; }
+.rv-field { display: grid; min-width: 0; gap: 8px; margin-bottom: 18px; font-size: .8rem; font-weight: 700; }
+.rv-field :is(input,textarea,select) { min-width: 0; width: 100%; background: var(--rv-paper); border: 1px solid var(--rv-line); color: var(--rv-ink); padding: 10px 12px; border-radius: 4px; font-size: .9rem; line-height: 1.6; font-weight: 450; }
+.rv-field :is(input,textarea,select):disabled { opacity: 1; background: var(--rv-worktop); color: var(--rv-muted); }
+.rv-field textarea { resize: vertical; }
+.rv-form-grid { display: grid; grid-template-columns: minmax(0,1fr); column-gap: 18px; }
+.rv-span-all { grid-column: 1 / -1; }
+.rv-small { font-size: .72rem; color: var(--rv-muted); line-height: 1.65; font-weight: 450; }
+.rv-text-button, .rv-crosslink button { display: inline-flex; gap: 5px; align-items: center; background: transparent; color: var(--rv-red-dark) !important; border: 0; padding: 0; text-decoration: underline; text-underline-offset: 3px; text-align: left; }
+.rv-crosslink { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; background: var(--rv-worktop); padding: 12px 14px; border-radius: 4px; margin: 0 0 20px; font-size: .72rem; }
+.rv-subtabs { display: flex; gap: 24px; border-bottom: 1px solid var(--rv-line); margin-bottom: 14px; }
+.rv-table-scroll { overflow-x: auto; max-width: 100%; }
+.rv-table-scroll table { width: 100%; border-collapse: collapse; font-size: .75rem; }
+.rv-table-scroll caption { text-align: left; padding: 10px 0; font-size: .65rem; color: var(--rv-muted); }
+.rv-table-scroll th { background: var(--rv-worktop); font-size: .65rem; text-align: left; font-weight: 800; white-space: nowrap; }
+.rv-table-scroll td, .rv-table-scroll th { padding: 12px 11px; border-bottom: 1px solid var(--rv-line); vertical-align: top; }
+.rv-table-scroll td { min-width: 95px; }
+.rv-tag { display: inline-block; padding: 3px 6px; border-radius: 3px; font-size: .62rem; font-weight: 700; background: var(--rv-worktop); color: var(--rv-muted); }
+.rv-tag-warning { background: var(--rv-amber-bg); color: var(--rv-amber); }
+.rv-code { display: flex; align-items: flex-start; gap: 8px; min-width: 0; padding: 13px; background: var(--rv-worktop); border: 1px solid var(--rv-line); border-radius: 4px; margin: 12px 0; }
+.rv-code code { flex: 1; min-width: 0; white-space: pre-wrap; font-size: .73rem; }
+.rv-code button { flex-shrink: 0; padding: 4px; border: 0; background: transparent; }
+.rv-instructions { margin: 20px 0; padding-left: 24px; }
+.rv-instructions li { padding: 4px 0 20px 8px; font-size: .85rem; }
+.rv-instructions li::marker { color: var(--rv-red-dark); font-weight: 800; }
+.rv-comparison { display: grid; grid-template-columns: minmax(0,1fr); gap: 16px; }
+.rv-comparison > div { padding: 18px; border: 1px solid var(--rv-line); background: var(--rv-worktop); border-radius: 4px; }
+.rv-comparison p[lang] { margin-top: 10px; font-size: .9rem; line-height: 1.75; }
+.rv-linked { display: flex; align-items: flex-start; gap: 12px; padding: 16px; border: 1px solid var(--rv-line); font-size: .8rem; }
+.rv-linked p { color: var(--rv-muted); margin-top: 5px; font-size: .7rem; }
+.rv-model-list > div { padding: 20px 0; border-bottom: 1px solid var(--rv-line); display: flex; align-items: center; gap: 10px; justify-content: space-between; }
+.rv-model-list strong { font-size: .9rem; }
+.rv-model-list code { display: block; font-size: .7rem; margin-top: 5px; }
+.rv-runtime { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 22px; background: var(--rv-worktop); border: 1px solid var(--rv-line); border-radius: 5px; }
+.rv-runtime strong { display: block; font-size: 1.45rem; font-family: var(--rv-mono); margin-top: 8px; }
+.rv-runtime small { font: 400 .7rem var(--rv-sans); display: block; margin-top: 5px; color: var(--rv-muted); }
+.rv-terminal { padding: 18px; font: .75rem/1.85 var(--rv-mono); color: oklch(94% .01 20); background: oklch(28% .01 20); border-radius: 5px; white-space: pre-wrap; overflow-wrap: anywhere; }
+.rv-notice, .rv-dirty { padding: 14px 22px; background: var(--rv-blue-bg); color: var(--rv-blue); display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; font-size: .78rem; }
+.rv-notice button { border: 0; background: transparent; padding: 2px; }
+.rv-dirty { background: var(--rv-amber-bg); color: var(--rv-amber); }
+.rv-operation-banner { display: flex; align-items: flex-start; gap: 14px; padding: 18px; background: var(--rv-blue-bg); margin-bottom: 24px; border-radius: 5px; font-size: .78rem; color: var(--rv-blue); }
+.rv-operation-banner p { margin: 8px 0; }
+.rv-progress { height: 5px; background: oklch(86% .025 246); margin: 14px 0; border-radius: 3px; }
+.rv-progress > span { width: 55%; height: 100%; background: var(--rv-blue); display: block; border-radius: inherit; }
+.rv-empty { padding: 44px 12px; text-align: center; }
+.rv-empty > svg { color: var(--rv-muted); margin-bottom: 18px; }
+.rv-empty h2 { margin-bottom: 12px; }
+.rv-empty p { margin: 0 auto 22px; max-width: 48ch; font-size: .85rem; color: var(--rv-muted); }
+.rv-error { color: var(--rv-red-dark); }
+.rv-page-footer { display: flex; flex-wrap: wrap; gap: 12px; justify-content: space-between; padding: 18px 24px; border-top: 1px solid var(--rv-line); font-size: .63rem; color: var(--rv-muted); }
+/* B gives each record the full reading width; the list is a separate page. */
+.rv-variant-B .rv-page-heading h1 { font-family: var(--rv-serif); font-size: 1.875rem; font-weight: 500; letter-spacing: -.025em; }
+.rv-focus { padding-bottom: 24px; }
+.rv-focus-intro { padding: 26px 32px 0; }
+.rv-focus-intro p { font-size: .8rem; color: var(--rv-muted); margin-top: 10px; }
+.rv-focus > .rv-index { padding: 22px 32px; }
+.rv-focus > .rv-index .rv-index-title { display: none; }
+.rv-focus .rv-records { gap: 0; }
+.rv-focus .rv-record { border-radius: 0; border-bottom: 1px solid var(--rv-line); padding: 20px 30px 20px 16px; }
+.rv-focus .rv-record strong { font-size: .95rem; }
+.rv-focus > .rv-content { max-width: 860px; margin: auto; padding: 18px 32px 32px; }
+.rv-focus .rv-record-heading h2 { font-family: var(--rv-serif); font-size: 1.7rem; font-weight: 500; }
+.rv-focus .rv-section { margin-block: 34px; }
+.rv-back { margin: 22px 32px 6px; }
+/* C places status and operational sections before the record's content. */
+.rv-operations { padding: 22px; }
+.rv-operation-summary { background: var(--rv-worktop); border: 1px solid var(--rv-line); border-radius: 6px; display: flex; align-items: flex-start; gap: 20px; justify-content: space-between; padding: 22px; }
+.rv-operation-summary h2 { margin: 8px 0 10px; font-size: 1.125rem; }
+.rv-operation-summary p:not(.rv-eyebrow) { font-size: .78rem; color: var(--rv-muted); line-height: 1.75; }
+.rv-operation-body { display: grid; grid-template-columns: minmax(0,1fr); gap: 20px; margin-top: 24px; }
+.rv-step-nav { display: flex; flex-wrap: wrap; align-content: start; gap: 6px; }
+.rv-step-nav button { display: flex; align-items: center; text-align: left; gap: 10px; background: transparent; border: 1px solid var(--rv-line); border-radius: 4px; padding: 12px; font-size: .73rem; font-weight: 650; }
+.rv-step-nav button > span { font: .68rem var(--rv-mono); color: var(--rv-muted); }
+.rv-step-nav button[aria-current] { background: var(--rv-red-soft); border-color: oklch(84% .04 23); color: var(--rv-red-dark); }
+.rv-operation-detail > .rv-content { padding: 8px 0 0; }
+.rv-choose-record { margin-top: 0; margin-bottom: 22px; }
+.rv-choose-record summary span { display: block; font-size: .68rem; color: var(--rv-muted); margin: 5px 0 0 15px; font-weight: 450; }
+.rv-choose-record > .rv-index { padding: 16px 0 0; }
+.rv-admin-mobile { display: none; }
+@container page (min-width: 43rem) {
+ .rv-workbench { grid-template-columns: 220px minmax(0,1fr); }
+ .rv-workbench > .rv-index { border-bottom: 0; border-right: 1px solid var(--rv-line); }
+ .rv-facts, .rv-form-grid, .rv-comparison { grid-template-columns: repeat(2,minmax(0,1fr)); }
+ .rv-operation-body { grid-template-columns: 175px minmax(0,1fr); gap: 24px; }
+ .rv-step-nav { display: grid; }
+ .rv-step-nav button > svg { margin-left: auto; }
+ .rv-focus .rv-record { display: grid; grid-template-columns: 150px minmax(0,1fr); align-items: baseline; gap: 2px 20px; }
+ .rv-focus .rv-record > span { grid-column: 2; }
+}
+@container page (max-width: 42.999rem) {
+ .rv-workbench > .rv-index .rv-records { grid-template-columns: repeat(2,minmax(0,1fr)); }
+ .rv-workbench > .rv-index { max-height: 265px; overflow-y: auto; }
+ .rv-context-core { margin-left: 0; width: 100%; }
+ .rv-record-heading { flex-wrap: wrap; }
+}
+@container portal (max-width: 62rem) {
+ .rv-host-body { grid-template-columns: 64px minmax(0,1fr); }
+ .rv-portal-nav { padding-inline: 8px; }
+ .rv-portal-nav :is(span,p,small) { display: none; }
+ .rv-mount { padding-inline: 14px; }
+ .rv-topbar-location, .rv-topbar-divider { display: none; }
+}
+@container thoth (max-width: 47rem) {
+ .rv-page { grid-column: 1 / -1; grid-row: 3; }
+ .rv-admin-mobile { display: flex; grid-column: 1 / -1; grid-row: 1; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--rv-line); padding: 12px 16px; font-size: .75rem; }
+ .rv-admin-mobile button { background: transparent; border: 1px solid var(--rv-line); padding: 7px; border-radius: 4px; }
+ .rv-admin-rail { display: none; }
+ .rv-admin-rail.rv-admin-open { display: block; grid-column: 1 / -1; grid-row: 2; border-left: 0; border-bottom: 1px solid var(--rv-line); }
+ .rv-admin-open .rv-rail-note, .rv-admin-open > :is(p,strong) { display: none; }
+ .rv-admin-open nav { display: flex; flex-wrap: wrap; gap: 8px; }
+}
+@container portal (max-width: 42rem) {
+ .rv-host-body, .rv-host-collapsed .rv-host-body { grid-template-columns: minmax(0,1fr); }
+ .rv-portal-nav { display: none; }
+ .rv-portal-nav.rv-host-nav-open { display: flex; flex-wrap: wrap; gap: 6px; border-right: 0; border-bottom: 1px solid var(--rv-line); padding: 8px; }
+ .rv-portal-nav.rv-host-nav-open span { display: inline; }
+ .rv-topbar { padding-inline: 10px; gap: 8px; }
+ .rv-topbar-user { font-size: 0; gap: 0; }
+ .rv-mount { padding-inline: 8px; }
+ .rv-breadcrumb > span:last-child { display: none; }
+}
+@container page (max-width: 28rem) {
+ .rv-page-heading, .rv-content { padding: 22px 16px; }
+ .rv-page-heading h1 { font-size: 1.4rem; }
+ .rv-context, .rv-statusline { padding-inline: 16px; }
+ .rv-sample-note { margin-left: 0; }
+ .rv-tabs { padding-inline: 16px; gap: 18px; }
+ .rv-operation-summary { flex-direction: column; padding: 16px; }
+ .rv-operations { padding: 16px; }
+ .rv-workbench > .rv-index .rv-records { grid-template-columns: minmax(0,1fr); }
+ .rv-focus > .rv-content, .rv-focus > .rv-index { padding-inline: 16px; }
+ .rv-focus-intro { padding-inline: 16px; }
+ .rv-back { margin-left: 16px; }
+ .rv-record-emblem { display: none; }
+ .rv-runtime { flex-wrap: wrap; }
+ .rv-button { min-height: 44px; }
+ .rv-field :is(input,textarea,select) { font-size: 1rem; }
+}
+/* Review controls are outside the product shell and always explicitly labeled. */
+.rv-lab { position: fixed; z-index: 100; bottom: 12px; left: 50%; transform: translateX(-50%); width: max-content; max-width: calc(100% - 16px); padding: 9px 12px; background: oklch(27% .012 355); color: oklch(97% .004 20); border: 1px solid oklch(45% .012 355); border-radius: 9px; box-shadow: 0 6px 28px oklch(20% .01 355 / .2); }
+.rv-lab-main { display: flex; align-items: center; gap: 9px; }
+.rv-lab-main button { display: grid; place-items: center; width: 32px; height: 32px; background: oklch(34% .012 355); border: 1px solid oklch(48% .012 355); border-radius: 5px; }
+.rv-lab-tag { font-size: .55rem; font-weight: 800; letter-spacing: .08em; padding-right: 6px; }
+.rv-lab select { color: inherit; background: oklch(34% .012 355); border: 1px solid oklch(48% .012 355); border-radius: 4px; padding: 7px; font-size: .72rem; max-width: 100%; }
+.rv-lab-help { font-size: .6rem; color: oklch(80% .01 355); }
+.rv-lab details { font-size: .65rem; }
+.rv-lab summary { cursor: pointer; text-align: center; padding-top: 5px; color: oklch(83% .01 355); }
+.rv-lab-options { display: flex; align-items: end; flex-wrap: wrap; gap: 12px; padding-top: 14px; max-width: 620px; }
+.rv-lab-options label { display: grid; gap: 4px; }
+.rv-lab-options p { margin: 0; color: oklch(83% .01 355); max-width: 52ch; }
+.rv-lab-options .rv-button { background: oklch(38% .012 355); border-color: oklch(50% .012 355); color: inherit; }
+.rv-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
+@media(max-width: 680px) { .rv-lab-tag, .rv-lab-help { display: none; } .rv-lab-options { max-width: 290px; } }
+@media(prefers-reduced-motion: reduce) { .rv-host *, .rv-lab * { scroll-behavior: auto !important; } }
diff --git a/frontend/vite.administration-review.config.ts b/frontend/vite.administration-review.config.ts
new file mode 100644
index 00000000..7be45785
--- /dev/null
+++ b/frontend/vite.administration-review.config.ts
@@ -0,0 +1,13 @@
+import path from "node:path";
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vite";
+
+export default defineConfig(({ command }) => {
+ if (command !== "serve") throw new Error("Review prototype is development-only.");
+ return {
+ root: path.resolve(__dirname, "prototypes/administration-review"),
+ plugins: [react()],
+ appType: "spa",
+ server: { host: "127.0.0.1", port: 5175, strictPort: true },
+ };
+});