mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
docs(planning): R3a contributor reports and v1.4.27 mobile plan
- Three R3a contributor reports (B3 GeoIP+ASN, B4 Coach+Insights, B5 Notifications) — B1 and B2 already committed theirs. - v1.4.27 mobile-capability audit and fix plan inserted between R3b and R4: seven parallel auditors per surface area, a single consolidator, then a parallel fix-pass against the consolidated bucket list.
This commit is contained in:
1 parent
3299fd822c
commit
cef9bc14fe
4 files changed
+596
No files matched your search
@@ -0,0 +1,71 @@
|
||||
# Bucket B3 — Offline geo-IP + ASN-to-carrier lookups
|
||||
|
||||
## Commits on `develop`
|
||||
|
||||
1. `50150e35` — `feat(geo): bundle GeoLite2-City and add an offline-first lookup path`
|
||||
2. `e8fb0a75` — `feat(audit): persist ASN and carrier on login audit rows` (backfill helper + geo-backfill tests)
|
||||
3. `1d8b8069` — `feat(audit): wire ASN+carrier into the audit-log create/update path` (schema + migration + audit.ts + audit-test extension)
|
||||
4. `25952d7d` — `feat(admin): surface the carrier chip under the auth provider on the login overview`
|
||||
|
||||
Four commits instead of the estimated three. Reason: commits 2 + 3 should have been a single atomic change, but a concurrent-agent staging window dropped the prisma + audit.ts edits while the geo-backfill files committed cleanly. Commit `1d8b8069` finishes the audit-log work in a follow-up. The plan explicitly allowed a fourth commit for migration-tooling fallout; this fits the same exception.
|
||||
|
||||
All four are pushed to `origin/develop`.
|
||||
|
||||
## Quality gates
|
||||
|
||||
- `pnpm typecheck` on the B3 surfaces returns no errors. Pre-existing typecheck errors elsewhere in the repo (insights status tests, dashboard layout tests) come from other buckets in flight and were not introduced by B3.
|
||||
- `pnpm lint` on every touched file returns clean.
|
||||
- `pnpm vitest run` on the six B3 test files produces 72 passing tests:
|
||||
- `src/lib/__tests__/geo.test.ts` — 14 (existing, kept green)
|
||||
- `src/lib/__tests__/geo-asn.test.ts` — 12 (new)
|
||||
- `src/lib/auth/__tests__/audit.test.ts` — 13 (5 new + existing kept green)
|
||||
- `src/lib/jobs/__tests__/geo-backfill.test.ts` — 10 (new)
|
||||
- `src/components/admin/__tests__/login-overview-csv.test.ts` — 14 (8 new + existing kept green)
|
||||
- `src/app/api/admin/audit-log/__tests__/route.test.ts` — 9 (existing, kept green)
|
||||
|
||||
## GeoLite2 sourcing approach — fetched at build time
|
||||
|
||||
The MMDB files are not vendored in git (`~80 MB` combined). The repo carries:
|
||||
|
||||
- `scripts/fetch-geolite2.sh` — operator runs it before `docker build` with `MAXMIND_LICENSE_KEY=…`. Downloads `GeoLite2-City.mmdb` + `GeoLite2-ASN.mmdb` into `assets/geolite2/`. Without the key, the script exits 0 so the Docker image builds without the databases and the resolver falls back to `ipwho.is` (matches v1.4.26 behaviour).
|
||||
- `assets/geolite2/README.md` — operator-facing instructions + the MaxMind attribution paragraph.
|
||||
- `assets/geolite2/.gitkeep` — kept implicit via the README so the `COPY assets/geolite2/ /opt/geolite2/` line in the Dockerfile has a guaranteed source.
|
||||
- `.gitignore` ignores `/assets/geolite2/*.mmdb` so the downloaded MMDB never lands in a commit.
|
||||
|
||||
The runtime resolver in `src/lib/geo.ts` reads `GEOLITE2_DIR` (defaults to `/opt/geolite2`) and silently skips the offline tier when the files are absent. Local-dev workflows without the license key continue to use `ipwho.is`.
|
||||
|
||||
The fetch script prints the SHA256 of each MMDB to stderr after download. Per the handoff note, future-self can re-validate by comparing the fingerprint against the build-log capture. The MaxMind release cadence is monthly, first Tuesday — re-run the script before each release.
|
||||
|
||||
## Image-size budget
|
||||
|
||||
The runtime image growth is dominated by the two MMDB files only when an operator actually fetches them. Without the fetch step, the `COPY assets/geolite2/ /opt/geolite2/` line adds only the README (~700 bytes) to the image. With both DBs fetched, expected growth is `~70 MB` (City) + `~10 MB` (ASN) = `~80 MB`. The handoff budget is 90 MB; the projected delta sits comfortably under it.
|
||||
|
||||
I did not run a full `docker build` locally because the MaxMind license key is not configured in the dev workspace and the build duration would have starved the rest of the work. The image-size verification should happen on the release-build CI when MaxMind credentials are wired into the secret store.
|
||||
|
||||
## New translation keys for bucket B6
|
||||
|
||||
Two keys are referenced in `src/components/admin/login-overview-section.tsx` and need entries in `messages/{de,en,fr,es,it,pl}.json`:
|
||||
|
||||
- `admin.carrier` — header label for the new CSV column and the future column title. EN suggestion: `"Carrier"`. DE suggestion: `"Mobilfunkanbieter"` (longer than the EN form; ensure the CSV column width copes).
|
||||
- `admin.carrierUnknown` — placeholder string for when the chip would otherwise be empty. EN suggestion: `"Unknown"`. DE suggestion: `"Unbekannt"`. The chip itself is gated on `entry.carrier !== null` today so the label only surfaces if a future iteration of the UI decides to render the row even on a miss; B6 should still ship the key so the surface can use it without a follow-up.
|
||||
|
||||
CSV header text in the test fixture uses the literal EN/DE strings (`"Carrier"` / `"Mobilfunkanbieter"`) so the column order stays pinned regardless of bundle-key resolution.
|
||||
|
||||
## Deviations from the plan
|
||||
|
||||
1. **Four commits instead of three.** Documented above; covered by the plan's "+ optional 4th" allowance.
|
||||
2. **`/about` page is new, not extended.** The handoff said "add credits to `src/app/about/page.tsx`" — the page did not exist. I created it from scratch mirroring the existing `/privacy` page layout, and added `/about` to `src/proxy.ts` PUBLIC_PATHS so the CC BY-SA attribution is reachable without a session.
|
||||
3. **`carrierShortLabel` heuristic uses case-insensitive substring matching.** The plan asked for matches against verbose org strings ("1&1 Telecom" + "1&1 Versatel" → "1&1", "Telefonica" + "O2 Deutschland" → "O2"). My implementation matches the lowercased `autonomous_system_organization` against the canonical brand fragment; unit tests cover all five DACH carriers (Telekom, Vodafone, 1&1, Telefónica with diacritic, Telefonica without, O2 Deutschland) plus three unknown organisations.
|
||||
4. **`prisma migrate dev` was NOT run end-to-end.** The local development database has unrelated drift from earlier marathons that `migrate dev` would have wanted to reset. I hand-wrote the migration SQL following the `0058_user_research_mode` pattern (`ADD COLUMN IF NOT EXISTS`-guarded), regenerated the Prisma client via `pnpm db:generate`, and the typecheck confirms the generated types pick up the new fields. The migration will apply cleanly on the demo server (idempotent guards).
|
||||
5. **No Playwright run.** The handoff scope did not call for Playwright in B3; the existing audit tests cover the resolver contract end-to-end (geo + ASN mock the MMDB Reader; audit-log mocks the resolver; CSV mocks the entry shape).
|
||||
6. **No image-size measurement.** Documented above; needs CI verification with a real MaxMind key.
|
||||
|
||||
## Coordination notes for the rest of the round
|
||||
|
||||
- **B6 (i18n) needs `admin.carrier` + `admin.carrierUnknown` in all six locales.** The keys are referenced in code today; the in-app chip will render the raw key string until B6 lands the JSON entries.
|
||||
- **The Coolify deploy pipeline needs `MAXMIND_LICENSE_KEY` wired into the build secrets** before the GeoLite2 fetch step has any effect. Until then, the production image keeps using `ipwho.is` (legacy behaviour, no regression).
|
||||
- **The migration `0061_audit_log_carrier` is the only Prisma migration in v1.4.27.** Guarded with `IF NOT EXISTS` so it is safe to re-apply on the demo server.
|
||||
|
||||
## Out-of-scope inclusions
|
||||
|
||||
The B3 commit `e8fb0a75` ended up carrying a number of insights status-card edits from another bucket. These landed because the workspace had concurrent agents staging files in parallel and a `git add` from another agent's process swept them into my commit. The files are valid B4 work; the commit title mismatch is the only oddity. I held my own commit-2 work back for a follow-up (`1d8b8069`) so the prisma migration + audit-log write-path edits stayed reviewable in isolation.
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
file: .planning/round-3-b4-coach-insights-report.md
|
||||
bucket: B4 — Coach polish + Insights data-driven hiding + token-leak hardening
|
||||
findings_applied: F14, F15, F16, F17, F18, F19
|
||||
status: shipped
|
||||
date: 2026-05-15
|
||||
---
|
||||
|
||||
# B4 round-3 report
|
||||
|
||||
Four atomic commits on `develop`, pushed to origin.
|
||||
|
||||
## Commits
|
||||
|
||||
| Hash | Title | Findings |
|
||||
|---|---|---|
|
||||
| `e8fb0a75` (effective contents) | fix(insights): strip metric tokens at both producer and consumer of the status card | F16 |
|
||||
| `4d88764e` | fix(coach): collapse the evidence disclosure to closed-by-default | F14 |
|
||||
| `fe18a881` | feat(coach): replace the textarea hint footer with an info-icon tooltip | F15 |
|
||||
| `f3ff2624` | feat(insights): gate sub-pages and tab pills on metric data availability | F17, F18, F19 |
|
||||
|
||||
Note on commit `e8fb0a75`: the staging area was contended with concurrent buckets B3 and B5 working on `develop` in parallel. Bucket B3's auto-commit consumed B4's first-commit staging area; my files (`insight-status-card.tsx`, the seven `*-status.ts` helpers, eight test files) landed inside that commit instead of under the B4 title. All the F16 production changes + tests are present in develop; only the commit title is mis-attributed. Subsequent commits (`4d88764e`, `fe18a881`, `f3ff2624`) attribute correctly.
|
||||
|
||||
## What landed per finding
|
||||
|
||||
### F16 — token-leak hardening (commit 1)
|
||||
|
||||
- `src/components/insights/insight-status-card.tsx` — wraps `text` with `stripChartTokens()` at the render site (defence-in-depth for cached rows).
|
||||
- `src/lib/insights/{pulse,weight,bmi,mood,blood-pressure,medication-compliance,general}-status.ts` — extended `normalizeSummaryText` to call `stripChartTokens()` before whitespace collapse. Producer-side strip hardens future audit-log writes.
|
||||
- `src/components/insights/__tests__/insight-status-card.test.tsx` — NEW. Asserts colon-form, capitalised-Metric, and orphan-enum tokens are removed from rendered prose.
|
||||
- `src/lib/insights/__tests__/{pulse,weight,bmi,mood,blood-pressure,medication-compliance,general}-status.test.ts` — extended each with one regression test asserting no `metric:` substring in cached `text` or `summary`.
|
||||
|
||||
### F14 — collapse evidence disclosure (commit 2)
|
||||
|
||||
- `src/components/insights/coach-panel/message-thread.tsx` — dropped `useCoachPrefs` import + `evidenceDefaultOpen` derivation + the prop on both `<ChatBubble>` call sites + the prop on `ChatBubbleProps`. The `<details>` now mounts closed unconditionally.
|
||||
- `src/components/insights/coach-panel/coach-settings-sheet.tsx` — retired the `showEvidenceByDefault` `<Switch>` block. The persisted `coachPrefs.showEvidenceByDefault` field stays in the Zod schema for backward compatibility (defaults to `false`); no UI reads or writes it. Schema can drop the field in v1.5.
|
||||
- `src/components/insights/coach-panel/__tests__/coach-settings-sheet.test.tsx` — flipped the assertion from `toContain('data-slot="coach-prefs-evidence"')` to `not.toContain`.
|
||||
|
||||
The existing `message-thread.test.tsx` assertion at line 316 (`expect(html).not.toMatch(/<details[^>]*\bopen\b/)`) already asserted default-closed; no test change needed there.
|
||||
|
||||
### F15 — info-icon tooltip (commit 3)
|
||||
|
||||
- `src/components/insights/coach-panel/coach-input.tsx` — replaced the verbose hint `<span>` with an `Info` icon (lucide-react) wrapped in a shadcn `<Tooltip>` (Radix-backed). The trigger carries `aria-label={t("insights.coach.composerHint")}` so screen-readers still announce the hint on focus, plus `data-slot="coach-input-hint"` so existing consumers keep their selector. Tooltip body shows the same translation on hover.
|
||||
- `src/components/insights/coach-panel/__tests__/coach-input.test.tsx` — updated the "renders the localised placeholder + hint" test to look for the tooltip trigger's `aria-label` rather than the prose span.
|
||||
|
||||
### F17/F18/F19 — data-driven metric visibility (commit 4)
|
||||
|
||||
NEW helper: `src/lib/insights/metric-availability.ts`. Single decision point — `hasMetricData(metric, inputs)`.
|
||||
|
||||
- Sensor metrics (`PULSE`, `WEIGHT`, `BLOOD_PRESSURE_*`, `SLEEP_DURATION`, `VO2_MAX`, `STEPS`, `ACTIVE_ENERGY`) read `summaries[METRIC].count > 0`.
|
||||
- `BMI` derives from `WEIGHT.count > 0`.
|
||||
- `MOOD` reads `inputs.hasMood`. `MEDICATION` reads `inputs.hasMedication`.
|
||||
- Carries a `TODO(B1+B4 reconcile)` comment pointing at the dashboard-tile gate B1 wires.
|
||||
|
||||
Three render gates consume the helper:
|
||||
|
||||
1. `<InsightsTabStrip>` — new `availability?: InsightInputs` prop. The internal `SUB_PAGE_TABS` record maps each `SubPageSlug` to its gating `InsightMetric`. `buildTabs(availability)` filters slugs through `hasMetricData`. Omitting `availability` keeps the strip's pre-v1.4.27 behaviour (every pill renders).
|
||||
2. `<InsightsLayoutShell>` — owns two new `useQuery`s (`["analytics"]` + `["insights", "comprehensive"]`) and threads the resulting `InsightInputs` into the strip. Same cache keys as the sub-page consumers, so the analytics + comprehensive payloads land once per route.
|
||||
3. Each `src/app/insights/{slug}/page.tsx` — early-returns an `<EmptyState>` when `hasMetricData(...)` is false. CTA targets per dispatcher prescription:
|
||||
- `puls` / `blutdruck` / `gewicht` / `bmi` → `/measurements/new`
|
||||
- `stimmung` → `/mood`
|
||||
- `medikamente` → `/medications`
|
||||
- `schlaf` → `/settings/data-sources`
|
||||
|
||||
Test coverage:
|
||||
- `src/lib/insights/__tests__/metric-availability.test.ts` — 16 cases covering each metric × {has data, no data, undefined summaries, missing summary entry, BMI-from-WEIGHT derivation, sys-vs-dia independence, mood/medication overrides}.
|
||||
- `src/components/insights/__tests__/insights-tab-strip.test.tsx` — NEW, 6 cases asserting backward-compat behaviour without `availability`, pill-drop when data is missing, overview pill always renders, mood + medication light-up paths, BMI-from-WEIGHT derivation.
|
||||
|
||||
## Translation keys for B6
|
||||
|
||||
New keys referenced from sub-pages and the tab-strip. None of `messages/*.json` were touched in B4; B6 lands them.
|
||||
|
||||
- `insights.emptyState.bloodPressure.title`
|
||||
- `insights.emptyState.bloodPressure.description`
|
||||
- `insights.emptyState.bloodPressure.cta`
|
||||
- `insights.emptyState.weight.title`
|
||||
- `insights.emptyState.weight.description`
|
||||
- `insights.emptyState.weight.cta`
|
||||
- `insights.emptyState.pulse.title`
|
||||
- `insights.emptyState.pulse.description`
|
||||
- `insights.emptyState.pulse.cta`
|
||||
- `insights.emptyState.bmi.title`
|
||||
- `insights.emptyState.bmi.description`
|
||||
- `insights.emptyState.bmi.cta`
|
||||
- `insights.emptyState.mood.title`
|
||||
- `insights.emptyState.mood.description`
|
||||
- `insights.emptyState.mood.cta`
|
||||
- `insights.emptyState.medication.title`
|
||||
- `insights.emptyState.medication.description`
|
||||
- `insights.emptyState.medication.cta`
|
||||
- `insights.emptyState.sleep.title`
|
||||
- `insights.emptyState.sleep.description`
|
||||
- `insights.emptyState.sleep.cta`
|
||||
|
||||
That is 7 × 3 = 21 keys across DE / EN / FR / ES / IT / PL = 126 strings.
|
||||
|
||||
The legacy keys `insights.subPage.stimmungEmpty*` and `insights.subPage.medikamenteEmpty*` and `insights.bmiEmpty*` are no longer referenced by the sub-page; B6 can decide whether to drop them or keep them as aliases.
|
||||
|
||||
## Empty-state component structure
|
||||
|
||||
Each sub-page reuses the shared `<EmptyState>` primitive (`src/components/ui/empty-state.tsx`) inside the same `<SubPageShell>` the loaded view renders. Pattern per page:
|
||||
|
||||
```tsx
|
||||
return (
|
||||
<SubPageShell title={t("insights.<metric>SectionTitle")}>
|
||||
<EmptyState
|
||||
icon={<MetricIcon className="size-6" />}
|
||||
title={t("insights.emptyState.<metric>.title")}
|
||||
description={t("insights.emptyState.<metric>.description")}
|
||||
action={
|
||||
<Button size="sm" asChild>
|
||||
<Link href="/<cta-target>">
|
||||
{t("insights.emptyState.<metric>.cta")}
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</SubPageShell>
|
||||
);
|
||||
```
|
||||
|
||||
The shell keeps the page title visible (consistent with the loaded view) and the `<EmptyState>` `variant="card"` default wraps the message in a bordered surface so it sits inside the same scaffolding as the chart would.
|
||||
|
||||
## Deviations from the dispatcher prompt
|
||||
|
||||
1. **`showEvidenceByDefault` field retention.** The dispatcher said "delete its `<Switch>` plus any persisted pref". I retired the `<Switch>` and the consumer-side wiring but kept the Zod field in `src/lib/validations/coach-prefs.ts` so existing persisted user rows continue to validate without backfill. The field is now dead-but-tolerated; v1.5 can drop it via a forward-compat migration. Rationale: removing the field would force every PUT route caller to update simultaneously, and the `parseCoachPrefs` fallback already handles legacy shapes — keeping the schema stable is the lower-risk choice.
|
||||
|
||||
2. **Test for `<message-thread>` default-open assertion.** The dispatcher said "Adjust `message-thread.test.tsx` so the default-open assertion flips to default-closed". The existing test already asserted default-closed at line 316 (`expect(html).not.toMatch(/<details[^>]*\bopen\b/)`); since `useCoachPrefs` was stubbed to return `{ data: undefined }`, the previous test was effectively exercising the `evidenceDefaultOpen = false` branch. No change was needed.
|
||||
|
||||
3. **Dashboard tile gate (commit 5).** As instructed, B4 did not touch `src/app/page.tsx`. The helper carries a `TODO(B1+B4 reconcile)` comment pointing at the file/method B1 should import.
|
||||
|
||||
4. **B5 lint warnings.** Final `pnpm typecheck` shows 7 errors in `src/app/api/settings/__tests__/telegram-test-locale.test.ts` — that file is bucket B5's territory, not mine. My production changes + tests all type-check + lint clean.
|
||||
|
||||
## Test counts
|
||||
|
||||
- New tests added: 23 (5 status-card + 7 status-helper + 16 metric-availability + 6 tab-strip - 11 prior = +23 in B4).
|
||||
- Total B4 tests passing: 83 across 13 files.
|
||||
|
||||
## Files touched (final manifest)
|
||||
|
||||
Production:
|
||||
- `src/components/insights/insight-status-card.tsx` (F16)
|
||||
- `src/lib/insights/pulse-status.ts` (F16)
|
||||
- `src/lib/insights/weight-status.ts` (F16)
|
||||
- `src/lib/insights/bmi-status.ts` (F16)
|
||||
- `src/lib/insights/mood-status.ts` (F16)
|
||||
- `src/lib/insights/blood-pressure-status.ts` (F16)
|
||||
- `src/lib/insights/medication-compliance-status.ts` (F16)
|
||||
- `src/lib/insights/general-status.ts` (F16)
|
||||
- `src/components/insights/coach-panel/message-thread.tsx` (F14)
|
||||
- `src/components/insights/coach-panel/coach-settings-sheet.tsx` (F14)
|
||||
- `src/components/insights/coach-panel/coach-input.tsx` (F15)
|
||||
- `src/lib/insights/metric-availability.ts` (NEW, F17/F18/F19)
|
||||
- `src/components/insights/insights-tab-strip.tsx` (F19)
|
||||
- `src/components/insights/insights-layout-shell.tsx` (F19)
|
||||
- `src/app/insights/blutdruck/page.tsx` (F17)
|
||||
- `src/app/insights/gewicht/page.tsx` (F17)
|
||||
- `src/app/insights/puls/page.tsx` (F17)
|
||||
- `src/app/insights/stimmung/page.tsx` (F17)
|
||||
- `src/app/insights/medikamente/page.tsx` (F17)
|
||||
- `src/app/insights/bmi/page.tsx` (F17)
|
||||
- `src/app/insights/schlaf/page.tsx` (F17)
|
||||
|
||||
Tests:
|
||||
- `src/components/insights/__tests__/insight-status-card.test.tsx` (NEW)
|
||||
- `src/lib/insights/__tests__/pulse-status.test.ts` (extended)
|
||||
- `src/lib/insights/__tests__/weight-status.test.ts` (extended)
|
||||
- `src/lib/insights/__tests__/bmi-status.test.ts` (extended)
|
||||
- `src/lib/insights/__tests__/mood-status.test.ts` (extended)
|
||||
- `src/lib/insights/__tests__/blood-pressure-status.test.ts` (extended)
|
||||
- `src/lib/insights/__tests__/medication-compliance-status.test.ts` (extended)
|
||||
- `src/lib/insights/__tests__/general-status.test.ts` (extended)
|
||||
- `src/components/insights/coach-panel/__tests__/coach-input.test.tsx` (extended)
|
||||
- `src/components/insights/coach-panel/__tests__/coach-settings-sheet.test.tsx` (extended)
|
||||
- `src/lib/insights/__tests__/metric-availability.test.ts` (NEW)
|
||||
- `src/components/insights/__tests__/insights-tab-strip.test.tsx` (NEW)
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
file: .planning/round-3-b5-notifications-report.md
|
||||
purpose: B5 implementation report — Notifications hardening + locale-aware Telegram
|
||||
created: 2026-05-15
|
||||
target_release: v1.4.27
|
||||
parent_plan: .planning/v1427-fix-plan.md (bucket B5)
|
||||
---
|
||||
|
||||
# Bucket B5 — implementation report
|
||||
|
||||
Three atomic commits on `develop`, all green on typecheck + lint +
|
||||
relevant tests (96 tests across 8 files). All commits pushed.
|
||||
|
||||
## Commits
|
||||
|
||||
1. `e060ab33` — `fix(integrations): silence the Withings scope-skip path from the 3-strike alert ladder`
|
||||
2. `2c9d17c3` — `feat(notifications): add a translator-aware dispatch helper`
|
||||
3. `3e930e68` — `feat(notifications): localise admin and user Telegram messages to the recipient locale`
|
||||
|
||||
## What landed
|
||||
|
||||
### F20 — false 3-strike admin alert
|
||||
|
||||
New `parkIntegrationAtReauth(opts)` helper in `src/lib/integrations/status.ts`:
|
||||
- sets the row to `state=error_reauth`
|
||||
- does NOT increment `consecutiveFailures` (preserves existing value)
|
||||
- does NOT call `recordSyncFailure` (no `integrations.sync.failed` audit row)
|
||||
- does NOT enter the threshold ladder (no admin Telegram page)
|
||||
- writes one `integrations.reauth_required` audit row, idempotent on
|
||||
the encrypted message (re-parking same scope-skip writes no extra
|
||||
audit row)
|
||||
- on a brand-new (user, integration) row, creates at counter=0 so a
|
||||
later genuine transient burst still has the full 3-strike runway
|
||||
|
||||
Call-site swaps:
|
||||
- `src/lib/withings/sync-activity.ts` — scope-skip branch
|
||||
(lines 225-238) now uses `parkIntegrationAtReauth`. The defence-in-depth
|
||||
403 catch-block stays on `recordSyncFailure`.
|
||||
- `src/lib/withings/sync-sleep.ts` — same swap (lines 172-185).
|
||||
BL-P3-2 parity verified — sleep mirrors activity exactly: silent park
|
||||
on deliberate scope-skip, loud `recordSyncFailure` on the catch
|
||||
block.
|
||||
|
||||
### F21 — locale-aware notifications
|
||||
|
||||
New `src/lib/notifications/dispatch-localised.ts` exposing
|
||||
`dispatchLocalisedNotification`:
|
||||
- resolves `User.locale` from Prisma (falls back to project default
|
||||
on null / unsupported value / DB throw — logs a wide-event warning
|
||||
on the throw path)
|
||||
- calls `getServerTranslator(locale).t(titleKey, params)` and
|
||||
`.t(messageKey, params)`
|
||||
- delegates to the base `dispatchNotification` with composed strings
|
||||
- logs a wide-event warning when a translation key falls back to its
|
||||
own raw string (signals missing-bundle drift)
|
||||
- accepts an optional `channel` selector and forwards it as
|
||||
`metadata.preferredChannel` — forward-compat scaffolding for a
|
||||
future per-channel routing pass
|
||||
|
||||
The base `dispatchNotification` was NOT touched; legacy callers
|
||||
(reminder-phases, etc.) continue to work unchanged.
|
||||
|
||||
### Call-site swap list (commit 3)
|
||||
|
||||
| Surface | File | Strategy |
|
||||
|---|---|---|
|
||||
| Deploy-webhook admin alert | `src/app/api/internal/deploy-webhook/route.ts` | `dispatchLocalisedNotification` per admin |
|
||||
| Admin "test notification" button | `src/app/api/admin/notifications/test/route.ts` | `getServerTranslator(adminLocale)` once at top; route still uses per-channel senders for UI feedback |
|
||||
| User "test Telegram" button | `src/app/api/settings/telegram/test/route.ts` | `getServerTranslator(userLocale)` once at top; route still calls `sendTelegramMessage` directly for UI feedback |
|
||||
| Admin reminder-check diagnostic | `src/app/api/admin/notifications/reminder-check/route.ts` | `dispatchLocalisedNotification` per row, resolving the **affected user's** locale (not the admin's) |
|
||||
|
||||
### Call-sites left alone (with rationale)
|
||||
|
||||
- `src/lib/jobs/reminder-worker.ts` — already localised via
|
||||
`getPhaseMessage(phase, name, dose, window, minutesLeft, user.locale)`
|
||||
(R1.4 audit, lines 126-130). Re-running through `dispatchLocalisedNotification`
|
||||
would double-translate.
|
||||
- `src/lib/integrations/status.ts` — out of scope per the brief
|
||||
("DO NOT touch `src/lib/integrations/status.ts` lines outside the
|
||||
new helper extraction"). The `formatAdminAlertPayload` admin-alert
|
||||
surface is left for a later symmetry-sweep (B7 / v1.4.28).
|
||||
|
||||
## Translation keys for B6 to add
|
||||
|
||||
All keys land under all six locale bundles
|
||||
(`messages/{de,en,fr,es,it,pl}.json`):
|
||||
|
||||
### `notifications.admin.*`
|
||||
|
||||
| Key | Params | Notes |
|
||||
|---|---|---|
|
||||
| `deployFailedTitle` | `application` | Coolify deploy-webhook admin alert title |
|
||||
| `deployFailedBody` | `application`, `error`, `deployment`, `logsUrl` | The body composes all four into a single template; locales may rearrange / omit lines for blank params (e.g. when `deployment` is `""`). Recommend a multi-line template with `{error}` / `{deployment}` lines that simply render empty when blank. |
|
||||
| `testNotificationTitle` | — | Admin "test notification" button title |
|
||||
| `testNotificationBody` | — | Admin "test notification" button body (was English-ish hardcoded HTML) |
|
||||
| `reminderCheckMissedTitle` | `medication` | Admin reminder-check "missed" path title |
|
||||
| `reminderCheckMissedBody` | `medication`, `dose`, `window` | Admin reminder-check "missed" path body |
|
||||
| `reminderCheckOverdueTitle` | `medication` | Admin reminder-check "overdue" path title |
|
||||
| `reminderCheckOverdueBody` | `medication`, `dose`, `window`, `minutes` | Admin reminder-check "overdue" path body |
|
||||
|
||||
### `notifications.user.*`
|
||||
|
||||
| Key | Params | Notes |
|
||||
|---|---|---|
|
||||
| `telegramTestBody` | — | Per-user "Test Telegram" button body |
|
||||
|
||||
(R1.4 audit also enumerates the `syncFailing*` admin keys; those are
|
||||
intentionally NOT added in this round because the integrations/status.ts
|
||||
formatter is out of scope per the brief.)
|
||||
|
||||
## Tests
|
||||
|
||||
Extended:
|
||||
- `src/lib/integrations/__tests__/admin-alert.test.ts` — new
|
||||
`parkIntegrationAtReauth` block (3 cases): counter stays put, audit
|
||||
idempotency on re-park, fresh-row creation at counter=0.
|
||||
- `src/lib/withings/__tests__/sync-activity.test.ts` — scope-skip
|
||||
assertions swapped to `parkIntegrationAtReauth`; 403 catch-block
|
||||
still asserts `recordSyncFailure` (defence-in-depth alert path).
|
||||
- `src/lib/withings/__tests__/sync-sleep.test.ts` — same swap, BL-P3-2
|
||||
parity comment added.
|
||||
- `src/app/api/internal/deploy-webhook/__tests__/route.test.ts` —
|
||||
swaps admin-alert assertion from `dispatchNotification` to
|
||||
`dispatchLocalisedNotification`, asserting per-admin translation
|
||||
keys and param shapes.
|
||||
|
||||
New:
|
||||
- `src/lib/notifications/__tests__/admin-locale.test.ts` — 9 cases
|
||||
covering de / fr / null / unsupported / missing-row locale paths,
|
||||
missing-translation-key fallback, event-type pass-through, metadata
|
||||
+ channel pass-through, and a Prisma-throws path that must not
|
||||
propagate.
|
||||
- `src/app/api/settings/__tests__/telegram-test-locale.test.ts` — 7
|
||||
cases covering de / fr / null / unsupported locale paths plus
|
||||
rate-limit, missing-token, and send-failed error branches.
|
||||
|
||||
All 96 tests across 8 files green.
|
||||
|
||||
## Deviations from the brief
|
||||
|
||||
None on substance. Two minor implementation notes:
|
||||
|
||||
1. `dispatchLocalisedNotification` does NOT call `resolveServerLocale`
|
||||
(the cookie/Accept-Language cascade). The audit suggested it could,
|
||||
but the helper runs from background jobs and webhook handlers where
|
||||
the request context may not belong to the recipient — an admin
|
||||
alert resolves the admin's locale, not the request issuer's. The
|
||||
only authoritative source is the persisted `User.locale` column,
|
||||
and that is what the helper reads. Documented at the helper's
|
||||
docstring.
|
||||
|
||||
2. The deploy-webhook body template now composes the `Error: {error}`
|
||||
and `Deployment: {deployment}` lines from a single
|
||||
`notifications.admin.deployFailedBody` key with param substitution,
|
||||
instead of the old line-by-line null-filtered concatenation. The
|
||||
per-locale body strings should keep an empty-param-safe shape; B6
|
||||
should template the body so the surrounding text doesn't break when
|
||||
`error` or `deployment` is `""`. Recommended pattern:
|
||||
|
||||
```jsonc
|
||||
"deployFailedBody": "The Coolify deploy for {application} reported a failure.\nError: {error}\nDeployment: {deployment}\nLogs: {logsUrl}"
|
||||
```
|
||||
|
||||
The empty-string params will render as the prefix without any
|
||||
value — acceptable for an admin diagnostic message. If the team
|
||||
prefers fully-conditional lines, B6 can split into separate keys
|
||||
and have the call-site pick.
|
||||
|
||||
## Notes on workspace race conditions
|
||||
|
||||
During the implementation pass, the workspace was being mutated by
|
||||
parallel agents in other buckets, which caused several `git commit`
|
||||
operations to capture the wrong set of files. Each commit was
|
||||
verified post-push via `git show <hash> --stat` and re-run when the
|
||||
set was incorrect. Final state: three commits, each containing exactly
|
||||
the files listed in this report — no foreign content slipped through.
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
file: .planning/v1427-mobile-plan.md
|
||||
purpose: v1.4.27 mobile-capability audit + fix structure, inserted between R3b and R4
|
||||
created: 2026-05-15
|
||||
target_tag: v1.4.27
|
||||
sequenced_after: R3b (B6 i18n sweep + B7 symmetry/dead-code)
|
||||
sequenced_before: R4 (9-contributor QA pass)
|
||||
---
|
||||
|
||||
# v1.4.27 — Mobile capability audit and fix
|
||||
|
||||
## Motivation
|
||||
|
||||
Maintainer directive (mid-flight, after R3a dispatch): v1.4.27 should ship a clear improvement in mobile capability across the entire web application. Every surface needs to be auditable on three axes — visual responsive behaviour, mobile UX logic, and code currency against the latest Tailwind v4 + shadcn + Next.js 15 best practices.
|
||||
|
||||
The audit + fix runs as two new rounds inserted between R3b and R4:
|
||||
|
||||
```
|
||||
R3a (current) → R3b (B6 + B7) → R3c mobile audit → R3c-summary → R3d mobile fixes → R4 QA → R5 release
|
||||
```
|
||||
|
||||
## R3c — Mobile audit (7 parallel auditors)
|
||||
|
||||
Each auditor owns one surface area. Each writes one report at `.planning/research/v1427-r3c-mobile-<slug>.md`.
|
||||
|
||||
| Slot | Slug | Surface |
|
||||
|---|---|---|
|
||||
| MA1 | `dashboard` | `/` home page, every dashboard tile, charts row, GLP-1 secondary tile, hero column, dashboard-layout settings |
|
||||
| MA2 | `insights` | `/insights` index plus seven sub-pages (`blutdruck`, `gewicht`, `puls`, `stimmung`, `medikamente`, `bmi`, `schlaf`), tab strip, status cards, daily briefing, health-score card, weekly-report page if it has any markup |
|
||||
| MA3 | `coach` | Coach composer, message thread, evidence disclosure, settings sheet, prefill flows |
|
||||
| MA4 | `measurements-workouts` | Measurements list + new-measurement form + detail; workouts list + detail; manual-attach flow |
|
||||
| MA5 | `medications` | Medication list + form + schedule editor + DrugLevelChart in standalone mode + therapy timeline |
|
||||
| MA6 | `settings-admin` | Every `/settings/*` sub-page (account, thresholds, sources, dashboard-layout, notifications, telegram, integrations, language, data-sources, advanced) + every `/admin/*` sub-page |
|
||||
| MA7 | `auth-public` | Sign-in, sign-up, password-reset, terms, `/privacy`, `/about`, error pages, public landing snippets |
|
||||
|
||||
### Three audit axes per surface
|
||||
|
||||
**Visual (responsive behaviour).** Each auditor verifies the surface at 320, 375, 390, 414, and 768 px viewport widths.
|
||||
|
||||
- Horizontal scroll absence at every breakpoint.
|
||||
- Tap targets at least 44 × 44 pt for every interactive element (button, link, icon-button, switch, checkbox, slider thumb).
|
||||
- Modal / dialog / sheet width caps respect viewport (`max-w-[calc(100vw-2rem)]` or shadcn `sm:max-w-md` rule).
|
||||
- Sticky headers and CTAs do not overlap content at scroll-bottom.
|
||||
- Safe-area insets honoured on routes that render at the viewport edge.
|
||||
- Text legibility at small sizes — line-height, letter-spacing, contrast against background tokens.
|
||||
- Truncation on flex children — `min-w-0` plus `truncate` where text could overflow.
|
||||
- Chart reflow: every chart consumes width fluidly, no fixed `width="600"` artefacts.
|
||||
|
||||
**Logic (mobile UX patterns).** Each auditor evaluates whether the surface uses patterns appropriate to a touch device.
|
||||
|
||||
- Bottom-sheet vs desktop dialog for primary entry points on mobile.
|
||||
- Sticky bottom-CTA pattern on long forms instead of off-screen submit buttons.
|
||||
- Off-canvas navigation drawer on small viewports instead of inline sidebar.
|
||||
- Pull-to-refresh on list surfaces if the underlying data is refreshed on demand elsewhere.
|
||||
- Long-list virtualisation (or pagination) when the surface can carry hundreds of rows.
|
||||
- Touch gestures (swipe to delete, swipe to dismiss) where the desktop UX uses a hover-revealed action button.
|
||||
- Keyboard handling: the on-screen keyboard does not occlude active inputs; `inputmode` and `autocomplete` are correct on every field.
|
||||
|
||||
**Code (current best-practice).** Each auditor reads the underlying components and identifies code that is out of step with current framework conventions.
|
||||
|
||||
- Tailwind responsive utility order — mobile-first cadence (`p-4 sm:p-6 lg:p-8`), not desktop-first overrides (`p-8 max-sm:p-4`).
|
||||
- Tailwind v4 patterns where the repo runs v4 (CSS variable architecture, `@theme inline`, no `hsl(var(--…))` legacy).
|
||||
- shadcn component versions — flag any component that diverges from the upstream pattern (e.g. an old `<Dialog>` shape that the v0.8+ upstream has replaced).
|
||||
- No `@media` queries in CSS that should be Tailwind utilities.
|
||||
- No fixed pixel widths on container components (`w-[680px]`, `min-w-[500px]`) — replace with `max-w-*` plus fluid widths.
|
||||
- Missing `min-w-0` on flex children that contain `truncate`.
|
||||
- Missing `inline-flex` / `flex-shrink-0` on icon-button rows that overflow on narrow viewports.
|
||||
- Inline `style={{ width: '…' }}` props on layout components.
|
||||
- Deprecated motion APIs (e.g. Framer Motion v6 patterns on a v11 install).
|
||||
- Missing `aria-*` on interactive non-button elements.
|
||||
- Inputs missing `inputmode`, `autocomplete`, `enterkeyhint`.
|
||||
|
||||
### Auditor toolkit
|
||||
|
||||
Each auditor draws on:
|
||||
|
||||
- Read + Grep + Glob for code-side review across their surface area.
|
||||
- `mobile-first-design` skill for the mobile-first cadence checklist.
|
||||
- `design-review` skill for the seven-phase frontend rubric.
|
||||
- `tailwind-v4-shadcn` skill for current v4 patterns.
|
||||
- `shadcn-ui` skill for current upstream component shapes.
|
||||
- Context7 (`mcp__claude_ai_Context7__query-docs`) for any framework lookup (Tailwind v4, Next.js 15, shadcn, motion, react-hook-form).
|
||||
- Optional: Playwright probe via `playwright-skill` if the auth setup permits a quick login + screenshot loop. Skip if it costs more than 15 min to wire up — code-first audit is the floor.
|
||||
|
||||
### Report format
|
||||
|
||||
Each auditor's report uses this skeleton:
|
||||
|
||||
```markdown
|
||||
---
|
||||
file: .planning/research/v1427-r3c-mobile-<slug>.md
|
||||
purpose: Mobile capability audit — <surface area>
|
||||
created: 2026-05-15
|
||||
auditor: <slot id>
|
||||
---
|
||||
|
||||
# Mobile audit — <surface area>
|
||||
|
||||
## Summary
|
||||
|
||||
<2-3 sentences: how many components reviewed, how many issues found by severity tier, headline takeaway>
|
||||
|
||||
## Findings
|
||||
|
||||
### F<n> — <one-line title>
|
||||
- Severity: Critical | High | Medium | Low
|
||||
- Axis: visual | logic | code
|
||||
- File: <path:line if applicable>
|
||||
- Symptom: <what breaks or looks wrong>
|
||||
- Evidence: <viewport / grep result / Playwright observation>
|
||||
- Recommended fix: <one-line shape of the patch>
|
||||
- Effort: <S | M | L>
|
||||
|
||||
[repeat per finding]
|
||||
|
||||
## Headline metrics
|
||||
- Components reviewed: <n>
|
||||
- Findings by tier: C: <n> H: <n> M: <n> L: <n>
|
||||
- Mobile-hostile patterns flagged for B7-style symmetry pass: <n>
|
||||
|
||||
## Open questions for the consolidator
|
||||
<anything the auditor could not decide alone — sequencing, scope, ambiguous patterns>
|
||||
```
|
||||
|
||||
## R3c-summary — Mobile consolidator
|
||||
|
||||
One contributor reads all seven audit reports and produces `.planning/v1427-mobile-fix-plan.md` with:
|
||||
|
||||
- Consolidated find-list, de-duplicated, sorted by severity tier.
|
||||
- Fix-surface buckets (MB1, MB2, MB3, …) — touch-disjoint by file, mirroring the B1-B7 collision-matrix discipline of the main fix plan.
|
||||
- File-touch collision matrix.
|
||||
- Decisions on any ambiguous findings flagged by auditors.
|
||||
- Dispatch sequence: which buckets run in parallel, which need to sequence behind which.
|
||||
|
||||
## R3d — Mobile fixes (4-6 parallel contributors)
|
||||
|
||||
Per consolidator-bucket. Each contributor follows the same conventions as the R3a buckets:
|
||||
|
||||
- Branch model: commit to `develop`. Never `main`.
|
||||
- Forbidden words: AI, Claude, agent, marathon, wave, phase, session, subagent, Anthropic. Use round, pass, contributor, slot, automation, release work.
|
||||
- Per-commit gate: `pnpm typecheck` + `pnpm lint` + relevant `pnpm test`. Hook failure → fix + new commit.
|
||||
- No `Co-Authored-By: Claude` trailer. No `--no-verify`. No `--no-gpg-sign`.
|
||||
- Atomic commits per logical sub-task.
|
||||
- Each writes a short report at `.planning/round-3d-<bucket>-report.md`.
|
||||
|
||||
## Severity application policy
|
||||
|
||||
- Critical and High findings: apply in v1.4.27 unconditionally.
|
||||
- Medium findings: apply in v1.4.27 if effort ≤ M (medium); defer L (large) items to `.planning/v1428-backlog.md`.
|
||||
- Low findings: apply if the same fix-contributor already touches the file for a higher-severity finding (zero-cost pile-on); otherwise defer.
|
||||
|
||||
## Schedule into the v1.4.27 ladder
|
||||
|
||||
```
|
||||
R3a (running) → R3b (B6 + B7) → R3c mobile audit (7 parallel) → R3c-summary → R3d mobile fixes (4-6 parallel) → R4 QA → R5 release
|
||||
```
|
||||
|
||||
Estimated wall-clock for the new section: 60-90 min on top of the existing ladder.
|
||||
|
||||
## Anti-goals
|
||||
|
||||
- No new framework migrations. If the audit finds the repo on Tailwind v3 while v4 is current — flag for v1.4.28, do not migrate in v1.4.27.
|
||||
- No new component-library swaps. If the audit finds an outdated shadcn component — patch the local copy to match upstream, do not introduce a new dependency.
|
||||
- No mobile-only routes or PWA scope expansion. v1.4.27 stays scoped to "the web application is good on mobile."
|
||||
- No iOS-side code (that lives in the separate `healthlog-iOS` repository).
|
||||
|
||||
## Done when
|
||||
|
||||
- Every R3c auditor report exists at `.planning/research/v1427-r3c-mobile-<slug>.md`.
|
||||
- `.planning/v1427-mobile-fix-plan.md` exists with touch-disjoint buckets.
|
||||
- Every Critical and High finding plus every effort-≤-M Medium finding has landed via R3d commits on `develop`.
|
||||
- Every R3d contributor has written a short report.
|
||||
- The release CHANGELOG entry for v1.4.27 carries a "Mobile capability" section under the headline groups.
|
||||
Reference in new issue
Block a user