Files
HealthLog/docs/audit/v1415-empty-states.md
T
2026-05-10 18:23:20 +02:00

98 lines
13 KiB
Markdown

# 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 `<tbody>` 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 `<EmptyState>` 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 `<p>No backups recorded yet.</p>`. | No icon, no CTA, no signal that the "Run backup now" button at the top is what to click. | Replaced with `<EmptyState>` (`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 `<p>No entries found.</p>` (filter-dependent). | When "Failed only" is selected on a healthy installation the user sees nothing — looked like a bug. | `<EmptyState>` with `ScrollText` icon, filter-aware description, and a "Show all events" CTA when the failed-only filter is active. |
| `/admin/api-tokens` | `ApiTokenOverviewSection` | Plain `<p>No API tokens found.</p>`. | Doesn't tell admins that tokens are issued at login from native clients — they wonder if they need to create them somewhere. | `<EmptyState>` with `Key` icon and a description pointing to native sign-in / Settings → Account API tokens. |
| `/admin/feedback` | `FeedbackInboxSection` | Plain `<p>No feedback in this category.</p>` per tab. | OK as text but the tab strip already shows `0` badges; the inner panel deserves the same compactness as other admin sections. | `<EmptyState size="compact">` with `Inbox` icon. Per-tab description. |
| `/admin` overview | `RecentAuditPreview` | Plain `<p>No recent audit entries.</p>`. | Compact but icon-less — visually inconsistent with the audit-table fixes. | `<EmptyState size="compact" variant="plain">` with `ScrollText` icon. |
| `/measurements` | `MeasurementList` | `<div class="border-dashed">{t("measurements.noMeasurements")}</div>`. | No CTA and the top-level "Add measurement" button is the only way out. | `<EmptyState>` with `Activity` icon, filter-aware description, "Add your first measurement" CTA. |
| `/mood` | `MoodList` | `<div class="border-dashed">{t("mood.noEntries")}</div>`. | No CTA. | `<EmptyState>` 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 `<EmptyState>` with `Pill` icon, "Add first medication" CTA. Visual parity with the rest. |
| `/insights` (top-level) | `InsightsPage` | Plain `<div class="text-muted-foreground py-20 text-center">{t("common.noData")}</div>` when `data` is undefined. | Looked like a hung page. | `<EmptyState>` with `TrendingUp` icon, "Log a measurement" CTA → `/measurements`. |
| `/insights` (BMI no-height) | Inline `<p>` | "no data" when `user.heightCm == null`. | The user might wonder why the BMI section is empty. | `<EmptyState size="compact" variant="plain">` with `Ruler` icon, copy points to `/settings/account` and a "Set height" CTA. |
| `/achievements` | `AchievementsPage` (no-grouped state) | Plain `<p>No achievements unlocked yet.</p>` inside a card. | Icon-less. The card after summary tiles needs the same visual cue we use elsewhere. | `<EmptyState>` 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 `<GettingStartedChecklist>` 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 `<EmptyState>` 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 `<p>{t("common.noData")}</p>` 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 `<EmptyState
size="compact">` per panel; not duplicating in this audit.
## Tests added
- `src/app/__tests__/measurement-list-empty.test.tsx` — confirms
`<EmptyState>` 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).