diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0236c0df..e044dfd5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
### Added
+- Ingester control plane gains `GET /database` (LanceDB snapshot: stored version, embeddings, per-table counts/sizes, vector index, pending migrations, package versions — the data `haiku-rag info` prints) and `GET /config` (full effective config as YAML, secrets redacted). The dashboard surfaces both as on-demand collapsible Database and Configuration panels.
- `HaikuRAG.import_documents(imports)` batch-imports prepared documents (`DocumentImport`), writing the `documents`, `chunks`, and `document_items` tables once each regardless of batch size. `DocumentRepository.create` accepts `Document | list[Document]`.
### Fixed
diff --git a/docs/ingester.md b/docs/ingester.md
index 12019bd4..b99492dc 100644
--- a/docs/ingester.md
+++ b/docs/ingester.md
@@ -305,13 +305,17 @@ token; without one the API stays open and the service logs a warning.
| `GET` | `/dlq` | dead jobs |
| `POST` | `/dlq/{id}/retry` | resurrect from DLQ |
| `GET` | `/stats` | rolling throughput (5m / 30m / 1h succeeded), worker occupancy, oldest queued age, per-source DLQ + backlog |
+| `GET` | `/database` | LanceDB snapshot — stored version, embeddings, per-table row counts/sizes, vector index status, pending migrations, package versions (same data as `haiku-rag info`) |
+| `GET` | `/config` | full effective configuration (defaults filled in) as YAML, with secrets redacted |
OpenAPI docs at `http://localhost:8765/docs`. The dashboard at `/` polls
the JSON endpoints above every few seconds and surfaces the same data
visually — queue depth chips, per-source health with a `queue busy` badge
when sweeps are skipped, throughput counters, active jobs with a Cancel
button, recent failures with a Retry button, and the last-completed
-feed.
+feed. The Database and Configuration panels are collapsed and load on
+demand (the Database panel has a Refresh button) rather than on the poll
+loop.

diff --git a/haiku_rag_slim/haiku/rag/ingester/api/static/index.html b/haiku_rag_slim/haiku/rag/ingester/api/static/index.html
index 7f740f5b..42fcae7b 100644
--- a/haiku_rag_slim/haiku/rag/ingester/api/static/index.html
+++ b/haiku_rag_slim/haiku/rag/ingester/api/static/index.html
@@ -161,6 +161,33 @@
}
#tooltip.visible { opacity: 1; }
[data-tip] { cursor: help; }
+
+ details.panel { padding: 0; }
+ details.panel > summary {
+ list-style: none; cursor: pointer; user-select: none;
+ display: flex; align-items: center; gap: 8px; padding: 16px;
+ font-size: 13px; font-weight: 600; letter-spacing: 0.04em;
+ text-transform: uppercase; color: var(--muted);
+ }
+ details.panel > summary::-webkit-details-marker { display: none; }
+ details.panel > summary::before { content: "▸"; color: var(--muted); }
+ details.panel[open] > summary::before { content: "▾"; }
+ details.panel > summary .spacer { flex: 1; }
+ details.panel > summary button {
+ background: var(--bg); color: var(--text); border: 1px solid var(--border);
+ padding: 4px 10px; border-radius: 4px; font: inherit; font-size: 11px;
+ text-transform: none; letter-spacing: 0; cursor: pointer;
+ }
+ details.panel > summary button:hover { background: var(--border); }
+ details.panel > .body { padding: 0 16px 16px; }
+
+ pre.config {
+ margin: 0; max-height: 600px; overflow: auto;
+ background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
+ padding: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 12px; line-height: 1.5; color: var(--text);
+ white-space: pre; word-break: normal;
+ }
@@ -255,6 +282,26 @@
loading…
+
+
+
+ Database
+
+
+
+
+
expand to load…
+
+
+
+
+
+ Configuration
+
+
+
expand to load…
+
+
@@ -321,6 +368,18 @@
return id ? id.slice(0, 8) : "—";
}
+ function formatBytes(n) {
+ if (n == null) return "—";
+ const units = ["B", "KB", "MB", "GB", "TB"];
+ let i = 0;
+ let v = n;
+ while (v >= 1024 && i < units.length - 1) {
+ v /= 1024;
+ i++;
+ }
+ return `${i === 0 ? v : v.toFixed(1)} ${units[i]}`;
+ }
+
function escapeHtml(s) {
if (s == null) return "";
return String(s)
@@ -668,6 +727,113 @@
});
})();
+ // Database and Configuration panels are on-demand, not part of the
+ // POLL_MS loop: their data is database state, not live queue telemetry,
+ // and gathering it re-reads table manifests. Loaded lazily on first
+ // expand; the Database panel can be re-fetched with its Refresh button.
+
+ function renderDatabase(info) {
+ if (!info.exists) {
+ $("db-body").innerHTML =
+ '