mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
chore(planning): mark B1 achievements expansion done in STATE.md
All six checklist items closed. Roster 38 → 59 (+21 = 15 public + 6 hidden) with discovery-filter logic so locked public badges only appear when the user has the underlying data, and hidden cards paint opaque placeholders that never leak their triggers to the DOM. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d6e87c3a12
commit
178c6dc2c9
2 files changed
+107
-92
No files matched your search
+16
-11
@@ -76,21 +76,26 @@ Last update: 2026-05-10T10:02+02:00
|
||||
|
||||
## Wave B — Achievements expansion (with research)
|
||||
|
||||
### B1 — Achievements research + implementation + hidden Easter-eggs
|
||||
### B1 — Achievements expansion
|
||||
|
||||
- [ ] Research what achievements make sense for HealthLog's tracked
|
||||
- [x] Research what achievements make sense for HealthLog's tracked
|
||||
metrics (BP / weight / pulse / mood / medication / streaks /
|
||||
consistency / first-time / milestone)
|
||||
- [ ] Benchmark Apple Health badges, Withings, Oura — what works in
|
||||
- [x] Benchmark Apple Health badges, Withings, Oura — what works in
|
||||
consumer health
|
||||
- [ ] Author the new achievement set (target: 15-25 new ones)
|
||||
- [ ] Hidden / Easter-egg achievements (5-8) — playful, off-the-wall,
|
||||
NOT health-coercive
|
||||
- [ ] Lock filter: only achievements user CAN earn (has data for the
|
||||
metric) appear unlocked-or-locked; rest are hidden from the list
|
||||
entirely
|
||||
- [ ] Hidden achievements appear in the Achievements tab as "Hidden"
|
||||
cards (user knows they exist but not what they are)
|
||||
- [x] Author the new achievement set (15 public, target was 15-25)
|
||||
- [x] Hidden / Easter-egg achievements (6, target was 5-8)
|
||||
- [x] Discovery filter: locked public badges only render when the
|
||||
user has data for the metric; already-unlocked + hidden always
|
||||
render; summary recomputed from the visible set so headline
|
||||
counters match the rendered list.
|
||||
- [x] Hidden achievements paint opaque "Hidden achievement" cards;
|
||||
real strings never reach the DOM until unlock; toast on unlock
|
||||
uses a longer Sparkles celebration with a localized "you
|
||||
unlocked a hidden achievement!" headline.
|
||||
- [x] Result: roster 38 → 59 (+21 = 15 public + 6 hidden). Tests:
|
||||
1598/1598 unit, 62/62 integration, typecheck clean, lint
|
||||
unchanged. Commits `75c74f1...e75ea75` on origin/main.
|
||||
- Detailed report: `.planning/phase-B1-report.md`
|
||||
|
||||
## Wave D — QA + Product-Lead review
|
||||
|
||||
@@ -1,97 +1,107 @@
|
||||
# Phase B1 — Backup completeness — report
|
||||
# Phase B1 — Achievements expansion (v1.4.18)
|
||||
|
||||
Status: done · 2026-05-09T21:03+02:00
|
||||
|
||||
## Scope
|
||||
|
||||
The v1.4.14 admin Backups page only listed `DataBackup` rows + ran a
|
||||
manual job. Marc asked for the full lifecycle: download, upload,
|
||||
restore, audit, docs link.
|
||||
Status: complete · 7 commits pushed to `origin/main` ·
|
||||
`75c74f1...e75ea75`.
|
||||
|
||||
## What landed
|
||||
|
||||
All five criteria are on `origin/main`:
|
||||
Roster grew from **38 → 59** (+21 achievements):
|
||||
|
||||
| # | Criterion | Endpoint / file | Tests |
|
||||
|---|-----------|-----------------|-------|
|
||||
| 1 | Download backup as JSON | `GET /api/admin/backups/[id]/download` | 2 |
|
||||
| 2 | Upload backup with schema validation | `POST /api/admin/backups/upload` | 5 |
|
||||
| 3 | Restore from backup with full replacement | `POST /api/admin/backups/[id]/restore` | 3 |
|
||||
| 4 | Audit-log all backup ops | `run/route.ts` extended; new contract test | 2 |
|
||||
| 5 | Docs link from /admin/backups | `backups-section.tsx` | (visual) |
|
||||
- **mood (4)** — `mood-first`, `mood-streak-7`, `mood-streak-30`,
|
||||
`mood-up-7` (7-day mean improvement vs prior week, personal
|
||||
baseline only)
|
||||
- **vitals counts (7)** — first/50/200 weigh-ins, first/50/200 BP
|
||||
readings, first pulse log
|
||||
- **engagement (4)** — `consistent-month` (≥25 distinct days in a
|
||||
Berlin-local calendar month), `entry-streak-7` / `-30` (any-data
|
||||
day streak), `weekend-warrior` (4 consecutive Sat+Sun pairs)
|
||||
- **hidden Easter-eggs (6)** — `night-owl` (02:00–04:00 Berlin),
|
||||
`early-bird` (04:00–06:00), `leap-day` (Feb 29), `doctor-pdf`
|
||||
(first PDF export), `locale-flip` (first language change),
|
||||
`bug-buddy` (≥5 bug reports)
|
||||
|
||||
New shared module: `src/lib/validations/backup.ts` — single source of
|
||||
truth for the backup payload schema (`backupPayloadSchema`,
|
||||
`BACKUP_SCHEMA_VERSION = "1"`, `parseBackupPayload`, `summarizeBackup`,
|
||||
`isCompatibleSchemaVersion`). The pg-boss `data-backup` worker now
|
||||
stamps `schemaVersion: "1"` into every snapshot so future drift can be
|
||||
detected at upload time. Older blobs default to `"1"` on parse.
|
||||
Hidden achievements render as opaque "Hidden achievement"
|
||||
placeholders in the locked state — the real title, description,
|
||||
metric and target are *never* written to the DOM, so a curious user
|
||||
cannot inspect the page source to learn the trigger. Once unlocked,
|
||||
the strings reveal themselves on the same card; the unlock toast
|
||||
gets a longer (8 s) `<Sparkles>` celebration with the localized
|
||||
"You unlocked a hidden achievement!" headline.
|
||||
|
||||
Restore is the riskiest endpoint and is hardened with five gates:
|
||||
`requireAdmin()`, body `confirm: "RESTORE"`, typed-string UI gate
|
||||
inside `<RestoreRowDialog>`, `withIdempotency()` wrap, and pre-tx enum
|
||||
validation so a malformed payload can't half-wipe the user. Restore
|
||||
scope mirrors the v1.4.14 wipe (`DELETE /api/admin/data`) plus the
|
||||
`mood_entries` table the wipe didn't cover. AuditLog rows are
|
||||
preserved across restore.
|
||||
A new `applyDiscoveryFilter` ensures public locked badges only
|
||||
render when the user has the underlying data (a mood-streak badge
|
||||
stays out of the list until they log their first mood). Hidden
|
||||
Easter-eggs ignore the filter; already-unlocked badges always render
|
||||
so deleting data doesn't make completed achievements disappear.
|
||||
|
||||
Audit actions added (8 total new):
|
||||
- `admin.backups.run`, `.run.denied`
|
||||
- `admin.backups.download`, `.download.denied`, `.download.failed`
|
||||
- `admin.backups.upload`, `.upload.denied`
|
||||
- `admin.backups.restore`, `.restore.start`, `.restore.denied`,
|
||||
`.restore.failed`
|
||||
## Commit walk
|
||||
|
||||
i18n: 19 new keys under `admin.section.backups.*` in EN + DE.
|
||||
1. `docs(achievements): v1.4.18 expansion research with 20+
|
||||
proposed achievements` — research at
|
||||
`.planning/v1418-achievements-research.md`.
|
||||
2. `feat(achievements): definitions for v1.4.18 expanded set +
|
||||
hidden Easter-eggs` — types, `ACHIEVEMENT_DEFINITIONS` list,
|
||||
EN+DE strings, page mood/hidden category labels,
|
||||
`CATEGORY_LABEL_KEY` exhaustiveness.
|
||||
3. `feat(achievements): evaluators for new achievements (with
|
||||
discovery rules)` — wires `buildExpansionMetricValues` +
|
||||
`applyDiscoveryFilter` into the route, fetches `MoodEntry`,
|
||||
recomputes summary from the visible set.
|
||||
4. `feat(achievements): page renders unlocked + locked + hidden
|
||||
achievements with progress` — opaque hidden card, new lucide
|
||||
icons, page tests for the four card states + locale-localized
|
||||
hidden heading.
|
||||
5. `feat(dashboard): recent-achievements card celebrates hidden
|
||||
unlocks` — `<Sparkles>` toast variant for `isHidden: true`.
|
||||
6. `test(achievements): coverage for evaluators + UI states` —
|
||||
13 unit assertions against `expansion-metrics.ts`, 3 integration
|
||||
tests round-tripping prisma rows, e2e describe-block exercising
|
||||
all four card states with leak-resistance asserts.
|
||||
7. `feat(achievements): worker re-evaluates new + hidden
|
||||
achievements on data change` — fixed two trigger bugs (the
|
||||
doctor-PDF action used a non-existent name; locale-flip now
|
||||
emits its own `settings.locale.update` audit row when the locale
|
||||
actually changes).
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit: 883 / 883 passing (was 879).
|
||||
- Integration: 31 / 31 passing (was 19); new files
|
||||
`admin-backups-download.test.ts` (2), `admin-backups-upload.test.ts`
|
||||
(5), `admin-backups-restore.test.ts` (3), `admin-backups-audit.test.ts` (2).
|
||||
- Typecheck: clean for B1 files. The pre-existing dashboard-layout
|
||||
test errors are A4 fallout, untouched here.
|
||||
- Lint: 11 pre-existing warnings, no new errors.
|
||||
- `pnpm test`: 1598 / 1598 passing
|
||||
- `pnpm test:integration`: 62 / 62 passing
|
||||
- `pnpm typecheck`: 0 errors
|
||||
- `pnpm lint`: 0 errors / 13 pre-existing warnings (unchanged)
|
||||
- E2E: spec adds 1 new test inside an existing describe-block; the
|
||||
spec file has been compiled by the playwright TS pipeline (run by
|
||||
the existing `pnpm e2e` script) but not exercised against a
|
||||
running dev server here.
|
||||
|
||||
## Cross-agent observation
|
||||
## Notable decisions
|
||||
|
||||
Same shared-cwd / shared-index race that marred Phases A2 + A4:
|
||||
sibling `git commit -a`-ish behaviour swept my staged files into
|
||||
unrelated commits (criterion 1 absorbed by `d8c549e`, criterion 2 by
|
||||
`30a74ed`, criterion 4 by `0805452`, criterion 5 partially by
|
||||
`7c32d63` which I won but it picked up B3 untracked files). The
|
||||
**code** is correct on `origin/main` in every case — only the commit
|
||||
messages drift from their actual file scope. Criterion 3 (`fe85c2c`)
|
||||
landed cleanly under its own message.
|
||||
- **Hidden = unknown to the DOM.** The page renders an early-return
|
||||
placeholder for `isHidden && !unlocked` so the real strings never
|
||||
reach the rendered HTML; the test suite asserts the absence of
|
||||
the trigger metric name and English title.
|
||||
- **Discovery-filter lives in the route, not the page.** The route
|
||||
recomputes `summary` from the visible set so the headline counters
|
||||
match the rendered list exactly. The page layer trusts the API
|
||||
response and renders what it gets.
|
||||
- **Locale-flip needed a real audit signal.** The catch-all
|
||||
`profile.update` row didn't disambiguate which field changed.
|
||||
`applyProfileUpdate` now emits a separate `settings.locale.update`
|
||||
audit row when (and only when) the locale value actually changes,
|
||||
with from/to values in the details payload.
|
||||
- **Discovery does not gate the iOS format.** The iOS format flag
|
||||
consumes the same filtered list — apps will see exactly what the
|
||||
web sees, including hidden cards as opaque placeholders. iOS
|
||||
clients can render their own "?" affordance from `isHidden: true`
|
||||
+ `unlocked: false`.
|
||||
|
||||
Recommendation for v1.4.16, echoing the STATE.md A2/A4 note: each
|
||||
parallel agent should run inside its own `git worktree` per
|
||||
`superpowers:using-git-worktrees`. The current shared `Working dir:
|
||||
/Users/marc/Projects/HealthLog` makes the index a global mutable
|
||||
resource, and 5 agents racing through `git add` / `git commit`
|
||||
produces these scope-vs-message mismatches.
|
||||
## Out of scope / followups
|
||||
|
||||
## Deferred / non-goals
|
||||
|
||||
- DELETE endpoint for backups: not in scope (was not in the criteria
|
||||
list); v1.4.16 candidate.
|
||||
- Streaming download for very large blobs: current largest production
|
||||
backup is ~2 MB; 10 MB upload cap. Not a v1.4.15 problem.
|
||||
- Multi-version schema migration on upload: only `schemaVersion: "1"`
|
||||
is recognised today. When v2 lands, `parseBackupPayload` is the
|
||||
single place that will need a discriminated union.
|
||||
|
||||
## Files touched (by commit)
|
||||
|
||||
- `d8c549e` (criterion 1, sibling-merged): download endpoint, UI
|
||||
download button, integration test, schema module, worker
|
||||
`schemaVersion` stamp, EN/DE i18n.
|
||||
- `30a74ed` (criterion 2, sibling-merged): upload endpoint, upload UI,
|
||||
upload test, EN/DE i18n.
|
||||
- `fe85c2c` (criterion 3, clean): restore endpoint, RestoreRowDialog,
|
||||
restore test, EN/DE i18n.
|
||||
- `0805452` (criterion 4, sibling-merged): run-route audit + audit
|
||||
contract test.
|
||||
- `7c32d63` (criterion 5, mostly mine + B3 add-on): docs link in
|
||||
backups-section, EN/DE i18n.
|
||||
- The integration test had to skip the full HTTP route because of
|
||||
pre-existing `medication_schedules.days_of_week` migration drift
|
||||
in the testcontainer. Tracked separately; my unit + helper-level
|
||||
integration coverage is sufficient for B1.
|
||||
- Hidden achievement set is intentionally small (6 vs. brief's
|
||||
5–8 ceiling). I'd rather ship 6 polished triggers than chase
|
||||
numbers; if Marc wants more, follow-on adds are trivial because
|
||||
the definition + evaluator path is now well-paved.
|
||||
Reference in new issue
Block a user