Summary
The Home dashboard's Export jobs card shows a hardcoded number. Hook it up to real job state, add an equivalent Import jobs card, and settle where these figures should come from.
1. Export jobs is not hooked up
crates/ui/src/lib.rs:1367-1379 says so outright:
// Not part of the resource-count read path — placeholder until the bulk
// export job-state and availability read paths are wired.
export_jobs: "13".to_string(),
export_jobs_queued: 1,
uptime_percent: "99.98".to_string(),
So the card at crates/ui/templates/pages/index.html:17-21 always reads "13 / running (1 queued)", on every server, in every tenant.
2. Add an Import jobs card
Bulk Data Submit already has a full UI at /ui/bulk-import (a nav entry at base.html:82), but nothing on the dashboard. Add a card alongside Export jobs, and link it to the existing page.
Note the layout: .stat-grid is repeat(4, 1fr) (app.css:594-599), currently exactly full. A fifth card needs a deliberate layout decision rather than being dropped in — there's a --2 modifier precedent at app.css:789 if a different column count is wanted.
3. "Investigate using metrics if applicable" — mostly not applicable, and there's a reason
Two things to know before reaching for Prometheus:
a. There are no job metrics today. The entire emitted set is http_requests_total and http_request_duration_seconds (crates/observability/src/middleware.rs:62-68) plus uptime_seconds (crates/observability/src/metrics.rs:69). Nothing counts exports or submissions, so metrics would have to be added first.
b. These cards are per-tenant, and /metrics must not carry tenant data. crates/observability/src/metrics.rs states the rule explicitly:
per-tenant resource-count gauges are intentionally NOT exported here. The /metrics endpoint is public (unauthenticated, for Prometheus scraping), and tenant is never a metric label — exporting per-tenant counts would leak cross-tenant data to any anonymous scraper.
The dashboard is tenant-scoped (dashboard::snapshot(window, &tenant.id), crates/ui/src/lib.rs:1294), so per-tenant job counts must not be sourced from /metrics. The established pattern for exactly this situation is the DashboardProvider / DashboardSnapshot path, which is tenant-aware and served through the authenticated console.
Process-wide job metrics (total jobs run, durations, failure counts, with no tenant label) would still be a reasonable separate addition for operators scraping Prometheus — just not the source for these cards.
4. The data already exists — this is plumbing, not new persistence
Both counts are already on the storage traits, tenant-scoped:
count_active_exports(&self, tenant) — crates/persistence/src/core/bulk_export.rs:995, "counts active (accepted or in_progress) jobs for a tenant"
count_active_submissions(&self, tenant) — crates/persistence/src/core/bulk_submit_worker.rs:302
So the work is: extend DashboardSnapshot (crates/observability/src/dashboard.rs:136-147, which has no job fields today) and StorageDashboardProvider (crates/rest/src/dashboard.rs:180-260) to carry these, then render them.
Two gaps to handle:
- Backend coverage is uneven.
count_active_submissions is implemented for sqlite, postgres, mongodb, and s3; count_active_exports only for sqlite and postgres. The cards need a graceful "unavailable" state rather than showing zero or a fabricated number where the backend can't answer.
- "running" vs "queued". The card renders two figures (
card-export-jobs-sub = running ({ $queued } queued), locales/en/main.ftl:140-141), but count_active_exports returns a single count covering both accepted and in_progress. Either split the query or change the copy — don't invent the breakdown.
5. "Investigate if Uptime is connected" — it is not
uptime_percent: "99.98" is hardcoded on the same line as the export placeholders (lib.rs:1375). A real uptime_seconds gauge does exist (crates/observability/src/uptime.rs, exported at metrics.rs:69), but the card claims an availability percentage over 30 days, which the in-process gauge cannot produce.
Unlike the job counts, uptime is process-wide, so /metrics is a legitimate source for it. This is already tracked separately in #540 — coordinate rather than fixing it twice here.
Acceptance criteria
Pointers
Summary
The Home dashboard's Export jobs card shows a hardcoded number. Hook it up to real job state, add an equivalent Import jobs card, and settle where these figures should come from.
1. Export jobs is not hooked up
crates/ui/src/lib.rs:1367-1379says so outright:So the card at
crates/ui/templates/pages/index.html:17-21always reads "13 / running (1 queued)", on every server, in every tenant.2. Add an Import jobs card
Bulk Data Submit already has a full UI at
/ui/bulk-import(a nav entry atbase.html:82), but nothing on the dashboard. Add a card alongside Export jobs, and link it to the existing page.Note the layout:
.stat-gridisrepeat(4, 1fr)(app.css:594-599), currently exactly full. A fifth card needs a deliberate layout decision rather than being dropped in — there's a--2modifier precedent atapp.css:789if a different column count is wanted.3. "Investigate using metrics if applicable" — mostly not applicable, and there's a reason
Two things to know before reaching for Prometheus:
a. There are no job metrics today. The entire emitted set is
http_requests_totalandhttp_request_duration_seconds(crates/observability/src/middleware.rs:62-68) plusuptime_seconds(crates/observability/src/metrics.rs:69). Nothing counts exports or submissions, so metrics would have to be added first.b. These cards are per-tenant, and
/metricsmust not carry tenant data.crates/observability/src/metrics.rsstates the rule explicitly:The dashboard is tenant-scoped (
dashboard::snapshot(window, &tenant.id),crates/ui/src/lib.rs:1294), so per-tenant job counts must not be sourced from/metrics. The established pattern for exactly this situation is theDashboardProvider/DashboardSnapshotpath, which is tenant-aware and served through the authenticated console.Process-wide job metrics (total jobs run, durations, failure counts, with no tenant label) would still be a reasonable separate addition for operators scraping Prometheus — just not the source for these cards.
4. The data already exists — this is plumbing, not new persistence
Both counts are already on the storage traits, tenant-scoped:
count_active_exports(&self, tenant)—crates/persistence/src/core/bulk_export.rs:995, "counts active (acceptedorin_progress) jobs for a tenant"count_active_submissions(&self, tenant)—crates/persistence/src/core/bulk_submit_worker.rs:302So the work is: extend
DashboardSnapshot(crates/observability/src/dashboard.rs:136-147, which has no job fields today) andStorageDashboardProvider(crates/rest/src/dashboard.rs:180-260) to carry these, then render them.Two gaps to handle:
count_active_submissionsis implemented for sqlite, postgres, mongodb, and s3;count_active_exportsonly for sqlite and postgres. The cards need a graceful "unavailable" state rather than showing zero or a fabricated number where the backend can't answer.card-export-jobs-sub = running ({ $queued } queued),locales/en/main.ftl:140-141), butcount_active_exportsreturns a single count covering bothacceptedandin_progress. Either split the query or change the copy — don't invent the breakdown.5. "Investigate if Uptime is connected" — it is not
uptime_percent: "99.98"is hardcoded on the same line as the export placeholders (lib.rs:1375). A realuptime_secondsgauge does exist (crates/observability/src/uptime.rs, exported atmetrics.rs:69), but the card claims an availability percentage over 30 days, which the in-process gauge cannot produce.Unlike the job counts, uptime is process-wide, so
/metricsis a legitimate source for it. This is already tracked separately in #540 — coordinate rather than fixing it twice here.Acceptance criteria
/ui/bulk-import.max-width: 1081pxbreakpoint./metrics.locales/{en,es,de}/main.ftl.crates/ui/e2e/tests/dashboard.spec.ts, which today asserts only that the stat cards render.Pointers
crates/ui/src/lib.rs:1367-1379— the placeholders;:325-340—DashboardMetricscrates/ui/templates/pages/index.html:6-27— the stat gridcrates/observability/src/dashboard.rs:136-160—DashboardSnapshot/DashboardProvidercrates/rest/src/dashboard.rs:180-260— the live providercrates/persistence/src/core/bulk_export.rs:995,crates/persistence/src/core/bulk_submit_worker.rs:302crates/observability/src/metrics.rs— the public-endpoint / no-tenant-label rulelocales/en/main.ftl:140-143,crates/ui/assets/app.css:594-599