# v1.4.15 — Empty-states audit Date: 2026-05-09 Owner: Stage C5 work-stream (parallel with C1 + C4). Primitive: `src/components/ui/empty-state.tsx` (already shipped earlier in v1.4.15; sweep below tracks adoption). The audit walks every list / table / tile surface a brand-new user hits, classifies the empty path, and records the fix. "Useful" means icon + title + description + (where it makes sense) a primary action, not a single grey sentence floating in a card. ## Findings | Route / surface | Component | State before | What was wrong | What changed | | ----------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `/admin/users` | `UserManagementSection` | Loading-only — when `users` resolved to `[]` the table simply rendered an empty `` with the header still visible. | Empty case never specified; on a fresh deployment with one admin self-row it never reproduced for the live tenant, but the `admin/user` filter did regularly. | New `` mounts inside the card when `filteredUsers.length === 0`, with `Users` icon, filter-aware copy, and a "Reset filter" CTA when a filter is active. | | `/admin/backups` | `BackupsSection` | Plain `

No backups recorded yet.

`. | No icon, no CTA, no signal that the "Run backup now" button at the top is what to click. | Replaced with `` (`Database` icon) that includes a primary "Backup now" button — duplicates the header CTA but inside the empty card so the new admin doesn't need to look up. | | `/admin/login-overview` | `LoginOverviewSection` | Plain `

No entries found.

` (filter-dependent). | When "Failed only" is selected on a healthy installation the user sees nothing — looked like a bug. | `` with `ScrollText` icon, filter-aware description, and a "Show all events" CTA when the failed-only filter is active. | | `/admin/api-tokens` | `ApiTokenOverviewSection` | Plain `

No API tokens found.

`. | Doesn't tell admins that tokens are issued at login from native clients — they wonder if they need to create them somewhere. | `` with `Key` icon and a description pointing to native sign-in / Settings → Account API tokens. | | `/admin/feedback` | `FeedbackInboxSection` | Plain `

No feedback in this category.

` per tab. | OK as text but the tab strip already shows `0` badges; the inner panel deserves the same compactness as other admin sections. | `` with `Inbox` icon. Per-tab description. | | `/admin` overview | `RecentAuditPreview` | Plain `

No recent audit entries.

`. | Compact but icon-less — visually inconsistent with the audit-table fixes. | `` with `ScrollText` icon. | | `/measurements` | `MeasurementList` | `
{t("measurements.noMeasurements")}
`. | No CTA and the top-level "Add measurement" button is the only way out. | `` with `Activity` icon, filter-aware description, "Add your first measurement" CTA. | | `/mood` | `MoodList` | `
{t("mood.noEntries")}
`. | No CTA. | `` with `Smile` icon, filter-aware description, "Log your first mood" CTA. | | `/medications` | `MedicationsPage` | Custom inline icon + button block. | Already friendly but didn't share the new primitive — diverging styles. | Refactored to `` with `Pill` icon, "Add first medication" CTA. Visual parity with the rest. | | `/insights` (top-level) | `InsightsPage` | Plain `
{t("common.noData")}
` when `data` is undefined. | Looked like a hung page. | `` with `TrendingUp` icon, "Log a measurement" CTA → `/measurements`. | | `/insights` (BMI no-height) | Inline `

` | "no data" when `user.heightCm == null`. | The user might wonder why the BMI section is empty. | `` with `Ruler` icon, copy points to `/settings/account` and a "Set height" CTA. | | `/achievements` | `AchievementsPage` (no-grouped state) | Plain `

No achievements unlocked yet.

` inside a card. | Icon-less. The card after summary tiles needs the same visual cue we use elsewhere. | `` with `Trophy` icon — keeps the same `noneUnlockedYet` string to avoid C4 i18n churn. | | Dashboard tile-strip / charts | `DashboardPage` | Tile strip + chart row both hidden when every metric has zero data; only `` plus the welcome banner show. The strip itself stays a 0-px row. | Brand-new users see a near-blank page below the welcome banner — feels broken even though the checklist is technically present. | New `` block rendered when `trendCards.length === 0 && charts.length === 0`. Routed to "Add your first measurement" via the existing quick-entry dialog (no nav). Does not race the checklist (which has its own self-gating). | ## Out of scope (deferred) - `/insights` correlation-card empty branches (`scatterData.length < 5`) already use a custom in-card empty surface (icon + 2 lines) that's tighter than the generic primitive — refactor would only churn classes, no UX win. Leave for v1.4.16 if C4 i18n sweep wants the consistency. - `/admin/danger-zone` and `/admin/services` are not lists — irrelevant. - `/admin/system-status` shows operational data not user records — empty paths handled by per-card "—" + status text. - `/admin/reminders` is a settings form, not a list. - Dashboard `/insights` BMI `

{t("common.noData")}

` for a user with height but no weight is intentionally minimal — the chart itself shows the empty axis. - Tab strip `0` badges on `/admin/feedback` use the same `` per panel; not duplicating in this audit. ## Tests added - `src/app/__tests__/measurement-list-empty.test.tsx` — confirms `` renders with the "Log your first measurement" CTA on a fresh account (mocked TanStack Query result of `{ measurements: [], meta: { total: 0 }}`). - `src/components/admin/__tests__/users-section-empty.test.tsx` — confirms the user-management empty state renders the filter-aware description and a "Reset filter" action when the admin filter is active. - `src/components/admin/__tests__/backups-section-empty.test.tsx` — confirms the backups empty state surfaces the secondary "Backup now" CTA inside the empty card. - Existing `src/components/ui/__tests__/empty-state.test.tsx` covers the primitive (4 variants). ## Counts - **Empty-states added or upgraded**: 13. - admin sub-routes: 6 (users, backups, login-overview, api-tokens, feedback, overview-preview). - feature lists: 4 (measurements, mood, medications, achievements). - insights: 2 (top-level no-data, BMI no-height). - dashboard: 1 (tile-strip + charts both empty). - **i18n keys added**: 18 in EN + DE under `emptyStates.*` (1), `measurements.empty.*` (4), `mood.empty.*` (4), `medications.empty.*` (3), `admin.section.users.empty.*` (3), `admin.section.backups.empty.*` (2), `admin.section.login-overview.empty.*` (3), `admin.section.api-tokens.empty.*` (2), `admin.section.feedback.empty.*` (2), `admin.overview.auditEmpty*` (1 reuses existing key + new description), `insights.empty.*` (3), `dashboard.empty.*` (3), `achievements.empty.*` (reuses `noneUnlockedYet`). - **No new dependencies**. ## Verification - `pnpm test` after each commit (target: 0 regressions). - `pnpm typecheck` after each commit. - `pnpm lint` after each commit. ## Reference Stage C5 work order: `.planning/STATE.md` § Stage C5. Primitive intro PR: see git log for `feat(ui): generic EmptyState primitive` (already on `main` before this stage started — section 1 of the C5 brief satisfied without a new commit).