Goal: Bring
archiveto feature parity with pgAdmin 4, DBeaver CE, TablePlus, and DataGrip across all three frontends (ratatui, browser, egui).Constraint: Every user-visible action must flow through the
Established<P>proof chain and the AccessKit IR. UI layers readArchiveDisplay::to_ak_nodesoutput; they never produce widgets or HTML from raw data. Adding a new feature means: implementArchiveDisplayfor the type, extendPanelModeor the nav tree, wirebuild_content_nodes, done — all three frontends pick it up for free.
| Phase | Feature |
|---|---|
| 1.1 | Data grid with pagination |
| 1.2 | SQL editor + results |
| 1.3 | Live tree refresh (r) |
| 1.4 | Nav filter (/) |
| 2.1 | DDL viewer (d) |
| 2.2 | FK descriptors (ForeignKeyDescriptor) |
| 2.5 | Column statistics (ColumnStats) |
| 2.6 | EXPLAIN plan viewer |
| 3.1 | Inline row edit/insert/delete |
| 3.2 | Query history |
| 3.3 | Saved queries |
| 3.4 | CSV/JSON export |
| 3.5 | Multi-connection (Ctrl+Tab) |
| 4.1 | Function browser (nav tree + MCP) |
| 4.3 | Sequence browser (nav tree + MCP) |
| 4.4 | Type browser — enum, domain, composite (nav tree + MCP) |
| 4.5 | Extension list (in Admin panel) |
| 5.1 | Monitor panel — sessions, roles, cache, backups (m) |
| 5.2–5.4 | Admin panel — roles, backups, WAL, extensions, settings (a) |
| 6 | ERD diagram — table list + FK edges (g) |
- 2.3 Constraint viewer —
ConstraintDescriptor+ArchiveConstraintPluginexist; no panel - 2.4 Index details panel —
IndexDescriptorfetched inside inspect path; never rendered standalone - 4.2 Trigger browser —
TriggerDescriptor+list_triggersMCP tool exist; no nav tree node - 4.5 Extension nav node — extensions visible in admin panel only; no top-level nav tree node
- 5.1 Monitor depth —
MonitorSnapshotonly shows sessions/roles/cache/backups;slow_queries,lock_waits,table_bloat,index_usageare MCP tools but not in the panel UI - 6 ERD is_fk —
ErdColumn.is_fkhardcodedfalse; FK participation not back-propagated from edges
- Data-grid pagination keybindings (
PgDn/PgUp, first/last page) - SSE live polling for monitor (browser frontend only)
- Syntax highlighting in SQL editor (all frontends)
- SSH tunnel + SSL cert fields in
ConnectionProfile - Visual ERD layout (petgraph + force-directed, SVG browser, egui Painter)
- Query plan comparison (DBeaver/DataGrip feature)
- Theme toggle (dark/light; currently hardcoded Catppuccin Mocha in egui, none in ratatui/browser)
The ArchiveDisplay trait (to_ak_nodes) is the contract that lets the IR
pipeline surface any type in all three frontends. Only 5 of ~25 types in
types.rs implement it. Every type without an impl is invisible to the UI and
to ArchiveDisplayPlugin (MCP rendering). This is the root cause of most
"partial" items above.
| Type | Has ArchiveDisplay |
|---|---|
ColumnDescriptor |
✅ |
DatabaseDescriptor |
✅ |
QueryResult |
✅ |
SchemaDescriptor |
✅ |
TableDescriptor |
✅ |
ForeignKeyDescriptor |
❌ |
ConstraintDescriptor |
❌ |
DdlDescriptor |
❌ |
TableInspection |
❌ |
IndexDescriptor |
❌ |
ColumnStats |
❌ |
ExplainNode |
❌ |
QueryHistoryEntry |
❌ |
SavedQuery |
❌ |
StagedEdit / RowEditState |
❌ |
ConnectionProfile |
❌ |
FunctionDescriptor |
❌ |
TriggerDescriptor |
❌ |
SequenceDescriptor |
❌ |
EnumDescriptor |
❌ |
DomainDescriptor |
❌ |
CompositeTypeDescriptor |
❌ |
MonitorSnapshot |
❌ |
AdminSnapshot |
❌ |
ErdDiagram / ErdNode / ErdEdge / ErdColumn |
❌ |
The display pipeline is:
Descriptor type
│
└── impl ArchiveDisplay { to_ak_nodes(&self, mode, id_base) }
│
▼
build_content_nodes() ← inserts nodes into the shared tree map
│
▼
to_verified_tree() ← mints Established<IrSourced> proof token
│
┌─────┼──────────────────────────┐
│ │ │
ratatui egui leptos
bridge bridge (Axum server — rebuilds IR per HTTP request)
Every new feature follows this flow. Implementing ArchiveDisplay is the
first step for any type. Once the impl exists, build_content_nodes can
call it and all three frontends render it. However, Leptos is not free —
it is a server-side request cycle, not a live event loop. Every new
PanelMode variant requires explicit Leptos work:
- A new
A::Open*arm indispatch_action_on_model() - A new
async fn api_*handler function - A new
GET /api/*route registered inbuild_router()
ratatui and egui pick up new PanelMode arms automatically from
build_content_nodes via the shared in-process model. Leptos must be
wired separately for each panel variant.
This is the prerequisite for Phases 8–10. Each type gets a new file in
crates/elicit_server/src/archive/display/ and is registered in display/mod.rs.
// display/index.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, JsonSchema)]
pub enum IndexDescriptorMode {
#[default]
Row, // single list item for embedding in a parent list
Detailed, // expanded: name + columns + type + unique flag
}
impl ArchiveDisplay for IndexDescriptor {
type Mode = IndexDescriptorMode;
fn root_role(mode: &Self::Mode) -> Role { … }
fn to_ak_nodes(&self, mode: &Self::Mode, id_base: u64) -> (NodeId, Vec<(NodeId, NodeJson)>) { … }
}| Type | Modes | Root role |
|---|---|---|
ForeignKeyDescriptor |
Inline / Detailed |
Row / Group |
ConstraintDescriptor |
Inline / Detailed |
Row / Group |
DdlDescriptor |
Block |
GenericContainer (preformatted text) |
TableInspection |
FkList / ConstraintList / IndexList |
List |
IndexDescriptor |
Row / Detailed |
Row / Group |
ColumnStats |
Summary / Detailed |
Article / Group |
ExplainNode |
TreeNode (recursive) |
TreeItem |
QueryHistoryEntry |
Row / Detailed |
Row / Article |
SavedQuery |
Row / Detailed |
Row / Article |
StagedEdit |
Row |
Row |
ConnectionProfile |
Card / Row |
Article / Row |
FunctionDescriptor |
Row / Detailed |
Row / Group |
TriggerDescriptor |
Row / Detailed |
Row / Group |
SequenceDescriptor |
Row / Detailed |
Row / Group |
EnumDescriptor |
Row / Detailed |
Row / Group |
DomainDescriptor |
Row / Detailed |
Row / Group |
CompositeTypeDescriptor |
Row / Detailed |
Row / Group |
MonitorSnapshot |
Dashboard / SessionList |
Group / List |
AdminSnapshot |
RoleList / BackupList / WalStatus / ExtList / Settings |
List / Group |
ErdDiagram |
NodeList / EdgeList / Visual |
List / Group / Figure |
ErdNode |
TableBox |
Group |
ErdEdge |
Row |
Row |
ErdColumn |
Row |
Row |
ExplainNode is recursive. The impl must allocate a block of id_base
space large enough to cover the subtree, or use a counter passed by mutable
reference. Recommended: pass a &mut u64 counter through a private helper:
fn node_to_ak(node: &ExplainNode, counter: &mut u64) -> (NodeId, Vec<(NodeId, NodeJson)>) { … }DdlDescriptor is verbatim text. Use Role::GenericContainer with the
DDL as the label. Syntax highlighting is a Phase 12 concern — Phase 7 just
gets the text into the tree.
MonitorSnapshot and AdminSnapshot should produce self-contained
subtrees so that build_content_nodes can call them directly without
rebuilding the entire panel by hand.
ErdDiagram::Visual mode is a placeholder in Phase 7 — it emits a
Role::Figure with a description field containing a text summary. Phase 10
replaces it with actual coordinate data.
Once impls are added, register them as new MCP display tools:
archive_display__foreign_keys → Vec<ForeignKeyDescriptor>
archive_display__constraints → Vec<ConstraintDescriptor>
archive_display__indexes → Vec<IndexDescriptor>
archive_display__column_stats → ColumnStats
archive_display__explain → ExplainNode (recursive tree)
archive_display__history → Vec<QueryHistoryEntry>
archive_display__saved → Vec<SavedQuery>
archive_display__function → FunctionDescriptor
archive_display__trigger → TriggerDescriptor
archive_display__sequence → SequenceDescriptor
archive_display__type → EnumDescriptor | DomainDescriptor | CompositeTypeDescriptor
archive_display__monitor → MonitorSnapshot
archive_display__admin → AdminSnapshot
archive_display__erd → ErdDiagramWith Phase 7 done, wiring each feature into the UI is a build_content_nodes
arm + optional PanelMode variant + key binding. Each item below is a
self-contained diff.
Key binding: c — show constraints for selected table
New PanelMode: ConstraintPanel { table: TableDescriptor, constraints: Vec<ConstraintDescriptor> }
New FetchRequest: FetchConstraints { schema: String, table: String }
New PanelEvent: ConstraintsReady(Vec<ConstraintDescriptor>)
New HTMX route: GET /api/constraints?schema=&table=
build_content_nodes arm: call ConstraintDescriptor::to_ak_nodes per row in a Role::List
Key binding: i — show indexes for selected table (currently i = column detail; remap or use I)
New PanelMode: IndexPanel { table: TableDescriptor, indexes: Vec<IndexDescriptor> }
New FetchRequest: FetchIndexes { schema: String, table: String }
New HTMX route: GET /api/indexes?schema=&table=
build_content_nodes arm: call IndexDescriptor::to_ak_nodes per row in a Role::List
TriggerDescriptor already fetched by inspect_table_direct. The nav tree needs:
FlatItem::TriggersGroup(si)— collapsible group under each schemaFlatItem::Trigger(si, ti)— individual triggerSchemaEntryextended withtriggers: Vec<TriggerDescriptor>nav_tree.rs: queryinformation_schema.triggersalongside functions/sequences/typestoggle_expandcoversTriggersGroupvia the same expand-flag pattern asFunctionsGroupbuild_nav_nodesarm forTrigger→ callsTriggerDescriptor::to_ak_nodes(&mode, id_base)- Leptos:
A::ExpandTriggersaction +dispatch_action_on_modelarm; no dedicated panel, so no new/api/route needed (nav tree is rebuilt on tree-expand events shared across all three frontends)
Extensions are global (not per-schema). The plan calls for a top-level "Extensions" node under the database node:
FlatItem::ExtensionsGroup— top-level collapsibleFlatItem::Extension(idx)— one per installed extensionDatabaseEntryextended withextensions: Vec<(String, String)>(name, version)nav_tree.rs: populate fromDbServerAdmin::list_extensions()inbuild_nav_treebuild_nav_nodesarm: list items usingRole::ListItem- Leptos: same as 8.3 — nav tree expand is a model-side action; no separate
/api/route needed since extensions are loaded at tree build time
Currently page navigation in DataGrid exists in the model
(page_next, page_prev) but has no key bindings. Add to ArchiveKeyMap:
KeyMapEntry::nav(p(K::PageDown), A::PageNext, "PgDn", "Next page", true),
KeyMapEntry::nav(p(K::PageUp), A::PagePrev, "PgUp", "Prev page", true),
KeyMapEntry::nav(p(K::Home), A::PageFirst, "Home", "First page", false),
KeyMapEntry::nav(p(K::End), A::PageLast, "End", "Last page", false),Add PageNext, PagePrev, PageFirst, PageLast to ArchiveAction.
All three frontends pick up the bindings automatically via resolve().
In nav_tree::fetch_erd, after building edges, enrich ErdColumn.is_fk:
for edge in &edges {
if let Some(node) = nodes.iter_mut().find(|n| n.table_name == edge.from_table) {
if let Some(col) = node.columns.iter_mut().find(|c| c.name == edge.from_col) {
col.is_fk = true;
}
}
}The MonitorSnapshot today only captures sessions, roles, cache hit, and
backups. ArchiveMonitorPlugin already has MCP tools for slow_queries,
lock_waits, table_bloat, and index_usage. Phase 9 brings these into the
panel UI.
pub struct MonitorSnapshot {
// existing fields …
pub slow_queries: Vec<DbSlowQuery>,
pub lock_waits: Vec<DbLockWait>,
pub table_bloat: Vec<DbTableBloat>,
pub index_usage: Vec<DbIndexUsage>,
}Add the four sub-types to types.rs with #[derive(Elicit)] and
ArchiveDisplay impls in Phase 7.
pub enum MonitorTab {
Sessions,
SlowQueries,
LockWaits,
TableBloat,
IndexUsage,
}MonitorPanel variant gains active_tab: MonitorTab. [/] cycle tabs
(same key bindings already used for AdminPanel — they're mode-aware).
FetchMonitor tasks in ratatui + egui call the four new backend methods.
Leptos: GET /api/monitor already exists (Phase 5). Extend the handler to call the four
new ArchiveMonitorPlugin tools and populate the new MonitorSnapshot fields. Add a
MonitorTab query param so the response renders the correct tab. No new route needed —
existing route gains tab-aware rendering.
The MonitorPanel arm calls MonitorSnapshot::to_ak_nodes with the active
tab as the mode discriminant, producing a Role::List per tab.
The current ERD is a text IR: a list of table nodes and a list of FK edges.
Phase 10 replaces ErdDiagram::Visual mode with a real spatial layout.
Add petgraph (already a transitive dep via elicit_rstar; confirm direct
dep) to elicit_server. Use petgraph::Graph<ErdNode, ErdEdge> as the
intermediate representation.
Layout: Sugiyama/layer-based (for DAG-like FK graphs) or a simple grid layout (row × col based on table count) as a first pass. Force-directed is visually nicer but harder to implement stably — defer to a later polish iteration.
pub struct ErdLayout {
pub positions: HashMap<String, (f32, f32)>, // table_name → (x, y)
pub diagram: ErdDiagram,
}ErdDiagram::to_ak_nodes in Visual mode produces:
Role::Figure
├─ Role::Group (table box for each ErdNode)
│ label: "<table_name>\n<col1>: <type>\n…"
│ value: "x=120,y=340,w=200,h=120" ← bounding box in description
└─ Role::Group (edge for each ErdEdge)
label: "from_table.from_col → to_table.to_col"
value: "x1=320,y1=380,x2=500,y2=200" ← line endpoints
The Leptos renderer translates Role::Figure children with coordinate
value fields into SVG <rect>, <text>, and <line> elements. FK edges
get clickable <a> anchors that navigate to the target table.
This keeps the SVG in the IR pipeline: the AccessKit tree holds the layout data; the renderer decides the output format.
The egui bridge checks for Role::Figure children and uses egui::Painter
to draw rectangles and Bézier lines. Pan/zoom via egui::ScrollArea. Click
on a table box navigates the nav tree to that table.
ratatui cannot render SVG. The ratatui bridge for ErdPanel renders a
simple grid of box-drawing characters:
┌──────────────┐ ┌──────────────┐
│ users │────▶│ orders │
│ id: int4 PK │ │ user_id: FK │
└──────────────┘ └──────────────┘
This is best-effort; the text layout uses the same coordinate data from
ErdLayout::positions, snapped to character cells.
The browser monitor panel currently requires a manual HTMX reload. Phase 11 adds a Server-Sent Events stream so the browser auto-refreshes at a configurable interval.
GET /api/monitor-stream
│
├─ Axum: returns Response<Body> with Content-Type: text/event-stream
├─ tokio: spawns task that loops every 5 s
│ calls DbMonitor + DbRoleManager + DbBackupManager
│ emits SSE: "data: <JSON MonitorSnapshot>\n\n"
└─ Browser: EventSource('/api/monitor-stream')
onmessage: parse snapshot, call HTMX.trigger("#monitor-panel", "refresh")
- Use
axum::response::sse::{Event, Sse}+tokio_stream::wrappers::IntervalStream AppStatemust carry the DB URL asArc<Mutex<Option<String>>>(already the case)- The HTMX panel subscribes on open; unsubscribes on panel close (browser JS)
- ratatui / egui: periodic
FetchRequest::FetchMonitoralready triggered from atokio::time::intervalin the event loop — no change needed
GET /api/monitor-stream → SSE stream (text/event-stream)
All three frontends currently show the SQL editor as plain text.
Use CodeMirror 6. Loaded from a CDN bundle or
vendored JS. The <textarea> is replaced with a CodeMirror editor; the
Leptos backend only sees the textarea value on submit, so the IR pipeline is
unaffected. The JS integration is entirely frontend-local.
Use egui_code_editor (crate) or a hand-rolled egui::TextEdit with a custom
layouter that tokenises SQL keywords and applies Catppuccin Mocha colours.
The custom layouter approach avoids an additional heavy dependency.
Use tui-textarea (already in the ecosystem notes) which supports syntax
highlighting via ratatui_syntax_highlight or manual span markup. Minimal:
keyword highlighting only (SELECT, FROM, WHERE, JOIN, etc.) in the accent
colour.
Syntax highlighting is not part of the IR. The AccessKit tree carries the
raw SQL text. Highlighting is a pure rendering concern applied by each
frontend's text display primitive. No changes to ArchiveDisplay or
build_content_nodes.
ConnectionProfile today stores url_env_key: String only. Production use
requires SSH tunnels (pgAdmin's most-used feature) and SSL certificate paths.
pub struct ConnectionProfile {
pub name: String,
pub url_env_key: String,
pub backend: BackendKind,
pub color: Option<String>,
// SSH tunnel (all optional; if host is set, tunnel is active)
pub ssh_host: Option<String>,
pub ssh_port: Option<u16>,
pub ssh_user: Option<String>,
pub ssh_key_env: Option<String>, // env var naming the private key path
// SSL
pub ssl_mode: SslMode,
pub ssl_cert_env: Option<String>, // env var naming client cert path
pub ssl_key_env: Option<String>,
pub ssl_ca_env: Option<String>,
// Display
pub color: Option<String>,
}
pub enum SslMode { Disable, Allow, Prefer, Require, VerifyCa, VerifyFull }ConnectionProfilenever stores raw paths or passwords — only env var names. The actual secrets stay in the environment /.envfile.- SSH tunnel is established before
AnyPoolcreation usingopensshcrate (orssh2). The tunnel binds a random local port; the pool connects tolocalhost:<local_port>. ArchiveDbBackend::connect_profile(profile)replaces the currentconnect(url_str)for profile-based connections.- The UI has a connection editor panel (new
PanelMode::ConnectionEditor) gated on a newArchiveAction::EditConnection.
pub enum ConnectionProfileMode { Card, Row, Editor }Editor mode emits a Role::Form subtree with labelled Role::TextInput
fields for each editable property — the IR-native form pattern.
DataGrip and DBeaver both show side-by-side EXPLAIN plans for two queries.
pub struct ExplainComparison {
pub left: ExplainNode,
pub right: ExplainNode,
pub label_left: String,
pub label_right: String,
}PanelMode::ExplainCompare { left: ExplainNode, right: ExplainNode } is set
when the user runs the second EXPLAIN (toggled by Ctrl+Shift+E).
ExplainComparison::to_ak_nodes produces two Role::Tree children inside
a Role::Group. Each tree is built by ExplainNode::to_ak_nodes — the same
impl used for the single-plan view.
- First
Ctrl+E→ opensExplainPlanpanel (existing) - Second
Ctrl+E(with plan already open) → upgrades toExplainCompare, showing the new plan on the right and preserving the old on the left Ctrl+Shift+E→ clear comparison, return to single plan
Cost-delta annotations: if left.total_cost and right.total_cost differ
by > 10%, the changed node label gains a ▲/▼ prefix. Implemented in
ExplainComparison::to_ak_nodes as label suffix, not as renderer-specific
colour — the IR stays clean; renderers can add colour on top.
Currently: egui hardcodes Catppuccin Mocha; ratatui and browser have no theme awareness.
A ColorTheme type (already in elicit_accesskit) should drive all three
frontends. Extend it with named semantic tokens:
pub struct ArchiveTheme {
pub base: catppuccin::Flavour, // Latte | Frappe | Macchiato | Mocha
}ArchiveNavModel carries pub theme: ArchiveTheme. All three frontends read
model.theme at render time.
- egui:
apply_theme(ctx, &model.theme)— already partially done (hardcoded Mocha) - ratatui:
Style::fg(Color::Rgb(r,g,b))from theme tokens; applied inTuiAccessKitConverter::convert - browser: CSS custom properties injected in the
<head>from the Leptos renderer, derived frommodel.theme; no hardcoded hex values in HTML
T → cycle theme (Latte → Frappe → Macchiato → Mocha → Latte)
New ArchiveAction::CycleTheme.
Phase 7 (ArchiveDisplay impls) ← foundation for everything
│
├─ Phase 8 (surface partial features) ← high value, low risk, uses Phase 7
│ 8.1 constraint panel
│ 8.2 index panel
│ 8.3 trigger browser nav node
│ 8.4 extension nav node
│ 8.5 pagination keybindings
│ 8.6 ERD is_fk enrichment
│
├─ Phase 9 (monitor depth) ← extends existing panel, uses Phase 7
│
├─ Phase 10 (visual ERD) ← petgraph layout, SVG/Painter/text-art
│
├─ Phase 11 (SSE live monitor) ← browser only, no IR changes
│
├─ Phase 12 (syntax highlighting) ← pure rendering, no IR changes
│
├─ Phase 13 (connection config) ← type + ArchiveDisplay + UI panel
│
├─ Phase 14 (plan comparison) ← type + ArchiveDisplay + new PanelMode
│
└─ Phase 15 (theme system) ← egui already started, propagate everywhere
Even at full parity with pgAdmin, archive has properties its competitors
cannot match:
-
Proof-carrying operations — every destructive action (DROP, TRUNCATE, DELETE, GRANT) returns
Established<AuditLogged>. You can't accidentally drop a table without the event being recorded in the proof chain. -
MCP-first — every operation is also an MCP tool. An AI agent can browse your database schema, run explains, compare query plans, and generate DDL just by calling the same tools the UI does. pgAdmin has no equivalent.
-
Three parallel renderers, one model —
ArchiveNavModeland all descriptor types are frontend-agnostic. Adding a new object type automatically gets ratatui + egui + browser rendering with no extra code beyond the three rendering leaves. -
Formal verification coverage — core operations in
elicit_db/elicit_sqlxcarry Kani / Creusot / Prusti proofs. Production correctness guarantees that pgAdmin (Python + Flask) cannot provide.