Admin read-only Model explorer (catalog idiom) - #52
Merged
Merged
Conversation
Read-only /admin/model GET — a projection of load_organization: a datasource picker (>1 served), org overview (glossary + storage names/types, never storage_config), and a subject-area landing (tables + metric/entity cards). Adds model_store.list_datasources and the Model tab. Slice A of ACE-014.
Columns rendered grouped (authored column_groups, collapsible + labelled) or a flat 'show all N' list otherwise; per-column flags only where they carry signal (PK / FK→target / enum / unit / sensitive / caveat); table caveats elevated to an amber callout; ai_unvalidated marked; relationships + metrics that use the table. Browse tree expands the active area to its tables. Slice B of ACE-014.
ui.md is a tiny escape-first markdown subset (headings, bold/italic, inline + fenced code, lists, http(s)-only links) for the deployed ORGANIZATION.md — raw HTML in the doc is inert. The overview now surfaces org-level cross-area metrics/entities so they aren't dropped. Slice C of ACE-014.
Fold the catalog design into ui._CSS (browse tree, calm schema table + type pills, trust/sensitive/caveat badges, collapsible column groups, the caveat callout) — light rail, full-width, mobile. render_previews.py renders the model pages (overview / area / table flat + grouped / domain context) from the real builders over a neutral seeded model. README documents the read-only Model view. Slice D of ACE-014.
There was a problem hiding this comment.
Pull request overview
Adds an admin-only, read-only “Model” explorer at /admin/model that renders the deployed semantic model and domain context directly from the served DB-backed model tree, with an escape-first markdown subset for ORGANIZATION.md.
Changes:
- Introduces
GET /admin/model(session-gated, GET-only) with overview/area/table/context views and safe, encoded navigation URLs. - Adds a minimal, escape-first markdown renderer (
ui.md) with an http(s)-only link allowlist for rendering domain context safely. - Adds datasource listing support in
model_storeplus previews and a comprehensive test suite covering gating, safety (nostorage_configleak), rendering behavior, and markdown escaping.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_admin_model.py | New test suite covering auth gating, read-only contract, rendering behavior, safety/escaping, and markdown subset behavior. |
| render_previews.py | Generates static preview HTML pages for the new model explorer views using seeded neutral model data. |
| packages/agami-core/src/ui.py | Adds explorer CSS, safe markdown subset renderer, and updates admin tab routing to support /admin/model. |
| packages/agami-core/src/model_store.py | Adds list_datasources() to support selecting among served datasources. |
| packages/agami-core/src/admin.py | Implements the read-only model explorer handlers/builders and mounts the GET-only route. |
| packages/agami-core/README.md | Documents the new Model tab and its read-only/safety properties. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Pin status==200 on the fallback paths (distinguish a 500-with-substring), assert flat columns actually render (not just 'grp' absent), and add negatives: a table with no relationships omits the section, orders shows no stray caveat callout, description-less columns render the em-dash, relationship cardinality is pinned. Drop a vacuous 'Model'-tab assertion.
A 'Relationships' rail node (shown only when the model has cross-area joins) → a page listing org.cross_subject_area_relationships grouped by area-pair (from→to · join columns · cardinality · confidence). Makes the cross-area topology readable in one place; within-area joins stay on each table page. Motivated by real models in local testing (meridian 20, servicenow 66 across 16 areas). Adds 3 tests + a preview.
Comment on lines
+275
to
+290
| def _md_inline(s: str) -> str: | ||
| """Inline markdown on an ALREADY-escaped string: inline code, bold, italic, and scheme-checked | ||
| links. Code is substituted first so `**` inside backticks is left literal.""" | ||
| s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s) | ||
| s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s) | ||
| s = re.sub(r"(?<![*\w])\*([^*]+)\*(?![*\w])", r"<em>\1</em>", s) | ||
|
|
||
| def _link(m: "re.Match[str]") -> str: | ||
| text, url = m.group(1), m.group(2) | ||
| # Only http(s) links — the text is already escaped, so a `javascript:`/`data:` URL renders as | ||
| # plain text (never a live href). This is the one place a URL becomes an attribute. | ||
| if url.startswith(("http://", "https://")): | ||
| return f'<a href="{url}" rel="noopener noreferrer" target="_blank">{text}</a>' | ||
| return text | ||
|
|
||
| return re.sub(r"\[([^\]]+)\]\(([^)\s]+)\)", _link, s) |
Comment on lines
+861
to
+865
| picker = ( | ||
| '<form class="ds" method="get" action="/admin/model">' | ||
| '<span class="muted">Datasource</span>' | ||
| f'<select name="datasource" onchange="this.form.submit()">{opts}</select></form>' | ||
| ) |
…S fallback 1. ui._md_inline stashes inline-code spans before running bold/italic/link and restores them last, so a link/bold inside backticks stays literal (no live <a>/<strong> inside <code>). 2. The datasource picker gains a real submit button (the no-JS fallback), so it works with JavaScript disabled like the rest of the admin UI. Both with regression tests.
Contributor
Author
|
Thanks @copilot — both addressed in
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Spec: ACE-014
Summary
Adds the admin Model tab — a read-only Model explorer for the deployed semantic model. It's a pure projection of
model_store.load_organization(the same tree the MCP tools serve, so it can't drift) +load_memoryfor the domain doc. Replaces the cognitively-heavy "dump every column of every table with full per-column trust metadata" approach with a DataHub/Sundial-style catalog: a browse rail (datasource → subject area → table) and one page at a time.Changes
GET /admin/model(session-gated, GET-only — read-only by construction; no write path) with?datasource=,?area=,?table=,?view=context.confidencebadge; per-column flags only where they carry signal: PK / FK→target /sensitive/ unit / enum / caveat); caveats elevated to an amber callout + inline note;ai_unvalidatedmarked; relationships + metrics that use the table. Columns grouped when the table authorscolumn_groups(collapsible, labelled), else a flat "show all N" list (wide tables stay short).ui.md— a tiny escape-first markdown subset (headings, bold/italic, inline + fenced code, lists, http(s)-only links) forORGANIZATION.md; raw HTML in the doc is inert.model_store.list_datasources;_TABSgains a Model tab (its own/admin/modelhref); the catalog design folded intoui._CSS(light rail, full-width, mobile);render_previews.pyrenders the model pages from the real builders over neutral seeded data; README documents the view.Safety
/admin/model. Editing stays conversational in Claude (the in-app editor is Hosted).storage_config(hosts/credentials) is never rendered. Verified by a security pass.ui.esc; the markdown renderer is escape-first with an http(s) link allowlist that fails closed. No new egress.Test plan
tests/test_admin_model.py(27 tests): gating + GET-only (405 on POST); empty state; datasource picker (present >1, absent =1) + stale-param fallback; overview (glossary, storage names, neverstorage_config); area landing; the table page's every flag (PK/FK/enum/unit/sensitive/caveat/ai_unvalidated/low-confidence); wide-table "show all 14"; grouped-vs-flat columns; SQL-backed table; relationships + metrics;ui.mdsubset + escaping (a<script>in the doc is inert) + http(s)-only links; cross-area objects; markdown empty/unterminated-fence guards.Checklist
uv run dev.py checkgreen — 937 tests, ruff lint, gitleaksuv run dev.py cover)/code-review+ security review — 0 must-fix; XSS/secret surface verified cleanstorage_config/secrets; no new egressacme/SALES_DATA/you@example.com) — no customer datarender_previews.py