Appearance
Managing the corpus (admin)
In the admin: Corpus — the screen this page documents (opens in the running app).
What/why: Content & media — what this screen manages and why it exists.
The Corpus screen under /admin/corpus is where data editors keep the Product Expertise Corpus (#47) current without a code change or deploy: uploading technical documentation and marketing materials, replacing a document with a refreshed version, and retiring superseded items. It is restricted to the data editor capability (edit_data) — a rules-editor is refused. See admin-access.md for how sign-in and roles work.
Everything here is enforced server-side: the UI reflects your capability, but the load and every action re-check it, so a hand-crafted request can't bypass the guard. Every change (upload, replace, retire, restore) is recorded in the append-only audit trail under the corpus entity type.
The corpus list (/admin/corpus)
- In retrieval lists every active document with its title, stable id, content type, source (URL or uploaded filename), version, and fetch date — each with a Retire action.
- Stale lists the excluded set: retired documents (with a Restore action) and superseded ones (badged with the id of their replacement). Rows and provenance are preserved — nothing is hard-deleted.
Uploading a document
Upload document reveals the ingest form. It takes:
- Document (JSON) — required. The same corpus ingestion document contract the
corpus:ingestCLI consumes: source, title, content type, version, and the extracted passages (the retrieval units). Validation is shared with the CLI, so both writers reject identical problems — every problem is listed inline and nothing is written on a rejected upload. - Source binary — optional. The original file (PDF, collateral) is stored in R2 for provenance. For an
upload-sourced document that omitssourceFilename, the attached binary's filename fills it in.
The submission routes through the ingestion pipeline (#64), so its idempotency contract applies unchanged:
| The store already has… | Result | Audit action |
|---|---|---|
| no item for this source | created | create |
| the same content and version | unchanged — a no-op | (none — nothing was written) |
| anything different | replaced — the existing item updates in place | replace |
A replacement is version-aware and never a duplicate: the item keeps its stable id (derived from the source URL or filename), version/fetched_at move to the new capture, and the passage set is swapped atomically.
Source links in the table (#1427)
Each corpus row's Source cell links to the original the document was ingested from — the canonical nanawall.com URL a url / Drupal view / Drupal dump item carries (the same source_url field the buyer-facing citation resolver consumes, #1422) — so retrieval quality can be spot-checked against the source in one click (new tab). This admin surface is deliberately not liveness-gated: discovering a dead source is part of the job here. An uploaded document shows its filename; a row with nothing resolvable shows an em-dash.
Rechecking source links (no dead links, #1008)
The results view never renders a link that 404s: a document's link only appears to buyers when its liveness flag (source_url_ok) is verified live. The flag is stamped automatically when a url-sourced document is ingested or refreshed; Recheck links (in the page header) probes active URL-bearing documents — direct URL captures and Drupal view/dump ingests (#1422) — and reports how many are live vs. dead. Each run checks one bounded batch (never-checked documents first, then the stalest checks); when the flash says some documents weren't reached, run it again to continue the sweep. Run it after a Drupal dump load (dump documents start unchecked, so their citations render without links until probed) or when public content is restructured. A dead or not-yet-checked URL degrades to the document title with no link (the citation itself still renders). The pass is audited under the corpus entity.
Caution: probes run server-side; if the public site's WAF blocks them, live URLs can read as dead (links omitted — safe but conservative). Re-running the pass from a context the WAF allows repairs the flags.
Retire vs. delete (soft delete)
Retiring a document is a soft delete: it drops out of retrieval (#66) immediately, but the row, its passages, and its provenance are preserved. Restore returns a retired document to retrieval. Superseded documents are also excluded from retrieval but are not restorable from this screen — their superseded by pointer names the current capture (see the corpus data model).
No deploy needed
Retrieval reads the corpus store live, so an uploaded or replaced document is retrievable — and a retired one is excluded — immediately. There is no build, deploy, or cache step between an admin change and the selector's expertise.
Relation to the CLI workflow
The operator-run npm run corpus:ingest script (corpus-refresh.md) feeds the same pipeline from data/corpus/ files and remains the bulk-refresh path for nanawall.com captures. The admin screen is the no-tooling alternative for one-off uploads, replacements, and lifecycle changes.