diff --git a/TODO-crdt-e2e-encryption.md b/TODO-crdt-e2e-encryption.md deleted file mode 100644 index 6828da1..0000000 --- a/TODO-crdt-e2e-encryption.md +++ /dev/null @@ -1,41 +0,0 @@ -# Encrypt crdt_sync payloads end to end - -## what -Firebase RTDB stores every crdt_sync record in plaintext, so Google can read -all app data. Encrypt record payloads on the device before push and decrypt -after pull, so RTDB stays a blind relay (the "ownership" meaning of -local-first: hologram.page/blog/local-first-means-three-different-things). - -## open decisions (ask kuhy before coding) -- Key source: derived from the shared crdt-sync password (Argon2id) or a - random key stored in the OS keystore and copied to new devices. -- Migration: re-encrypt existing logs in place, or new prefix + one-time copy. -- Scope: payload only (keep HLC/ids clear for merge) or whole record. -- diet_guard's `export_keys` document (random data-export keys, synced as a - keyring) sits in plaintext in RTDB, so Firebase can read them: it must move - under E2E too, with its key source decided here first. -- tutor uses its own RTDB client (`~/src/tutor/app/lib/sync/rtdb_client.dart`): - in or out of scope. - -## steps -1. Add an encrypt/decrypt seam in `crdt_sync_dart/lib/src/remote_store.dart` - (push/pull), AEAD (XChaCha20-Poly1305), version byte per record. -2. Property test: decrypt(encrypt(r)) == r; merge result identical with and - without encryption (see property-and-differential-tests skill). -3. Read path accepts plaintext and v1 records during migration. -4. Roll out app by app (todo first), bump pins, verify on the phone. - -## done -`dart test` in `crdt_sync_dart` passes, and -`firebase database:get / | grep -c '"title"'` returns 0 for every -migrated app. - -## verify -Pixel 6a + desktop todo: edit on one, see it on the other; raw RTDB JSON shows -only ciphertext. - -## read first -- `crdt_sync_dart/lib/src/sync.dart`, `log.dart`, `remote_store.dart` -- `crdt_sync_flutter/lib/src/bootstrap.dart` - -REMOVE ME AFTER FINISH diff --git a/TODO-pylint-fail-under-10.md b/TODO-pylint-fail-under-10.md deleted file mode 100644 index 3c474d3..0000000 --- a/TODO-pylint-fail-under-10.md +++ /dev/null @@ -1,93 +0,0 @@ -# TODO — pylint `--fail-under=10` everywhere, and clear what that surfaces - -REMOVE ME AFTER FINISH - -## what - -Two pylint hooks (`leetcode-guard`, `steam-backlog-enforcer`) still pass at -`--fail-under=8.0`, which permanently tolerates two points of lint debt and -lets a score *regress* from 9.9 to 8.1 without failing anything. Move both to -`10`, and fix what that surfaces. Every other site already runs at `10`, so -this is adopting an existing standard, not inventing one. - -## what is left (updated 2026-11-02) - -Done and verified (`pre-commit run pylint --all-files`, exit 0, each at -`--fail-under=10`): `control-panel`, `screen-locker` (both hooks), -`testsAndMisc/meta`, `utils/crdt-sync`, `utils/freedays`, `utils/gatelock`, -`utils/music_theory`. - -Still at `--fail-under=8.0`: - -| Repo | Site | -|---|---| -| `leetcode-guard` | `.pre-commit-config.yaml:126` | -| `steam-backlog-enforcer` | `.pre-commit-config.yaml:126` (ignore its `.claude/worktrees/` copies) | - -Both live outside `utils`, so this file belongs to whichever session works -those repos. The method is the fleet standard: pylint grades source, ruff -grades tests. Copy the `[tool.pylint]` block (tests basename ignore, -`init-hook = "import sys; sys.path.insert(0, '.')"`) from `wake-alarm` / -`diet-guard`, bump to `10`, fix the residue in source. Read the `must` -rules below first. - -## must - -- Every site at `--fail-under=10`. No `9.5`, no per-repo exception. -- Adopt the reference config from `wake-alarm` / `diet-guard`: the `tests` - basename ignore, the `init-hook`, and only then the residue fixes. -- Fix findings in source. Prefer the fix to the disable, every time. -- must not: add a `# pylint: disable=` comment, or extend `disable = [...]` - beyond wake-alarm's three implicit-booleaness entries, **without asking - kuhy first — every time, per `memories/code-quality.md`.** All seven - "unavoidable" disables in a past konbini-67 session were reverted. -- must not: exclude a source file from pylint to raise the score, or drop - `enable = "all"`. -- must not: touch the five sites already at `10`. -- optional: `duplicate-code` findings may be genuine shared-code candidates - for `utils`. Note them, do not force a refactor to hit the number. - -## done - -Both commands, from `~`: - -```bash -# 1. no site left below 10 (cov-fail-under is coverage, not pylint) -! grep -rn --exclude-dir=worktrees --include=.pre-commit-config.yaml --include=.pylintrc \ - -E 'fail-under=(8|9)' ~/src ~/praca_magisterska 2>/dev/null \ - | grep -v cov-fail-under - -# 2. both remaining packages pass its own pylint hook -for d in ~/src/leetcode-guard ~/src/steam-backlog-enforcer; do - (cd "$d" && pre-commit run pylint --all-files) || echo "FAIL $d" -done -``` - -Command 1 prints nothing and exits 0; command 2 prints no `FAIL` line. - -## verify - -Desktop. Per repo, `pre-commit run --all-files` must be green — not just the -pylint hook, since a fix that satisfies pylint can break `ruff`, `mypy` or the -250-line cap. Then the test suite: these are behaviour-affecting edits -(`broad-exception-caught` and `import-outside-toplevel` fixes especially), and -every one of these packages holds a 100% coverage bar. - -`utils` and `testsAndMisc` each have CI; push and confirm green rather than -trusting the local run. `gatelock`'s Tk suite is flaky on a live X session — -run it under `xvfb-run -a` and compare against a baseline worktree before -blaming a change for a failure. - -## read first - -- `~/src/wake-alarm/pyproject.toml` — `[tool.pylint]`, the reference config. - `diet-guard` is byte-similar; `build-your-x` also ignores `conftest.py`. -- `~/.claude/memories/code-quality.md` — the suppression rule that governs - every `disable` decision in this task. -- `~/src/utils/rules/` is not involved; this is not a dependency change. - -Each repo is its own git repo: commit per repo, work on `main`, stage -narrowly. `utils` additionally runs the shared `md-naming` and file-length -gates, so delete this file in that repo's final commit. - -REMOVE ME AFTER FINISH diff --git a/crdt_sync_dart/TODO-fix-file-log-persistence-shared-tmp.md b/crdt_sync_dart/TODO-fix-file-log-persistence-shared-tmp.md deleted file mode 100644 index 582d958..0000000 --- a/crdt_sync_dart/TODO-fix-file-log-persistence-shared-tmp.md +++ /dev/null @@ -1,27 +0,0 @@ -# Make FileLogPersistence.write safe under concurrent calls - -## what -`FileLogPersistence.write` (lib/src/io_store.dart) writes to a tmp file named -`..tmp` and renames it over the target. The name is per process, -not per call, so two overlapping writes in one process (per-keystroke upserts -in the todo app) share one tmp path: the first rename moves it away and the -second throws an unhandled `PathNotFoundException` on -`rename` of `todo_notes.json..tmp`. Seen in the todo app during -concurrent per-keystroke upserts. -Fix by serializing writes (a chained Future per instance, last write wins) -or by using a unique tmp name per call; keep the atomic rename. - -## done -A regression test in test/ fires 50 overlapping `write()` calls on one -instance, all complete without error, and the file then holds the text of a -call that was issued (not a torn mix). `dart test` in this package exits 0 -with coverage still 100%. - -## verify -`cd ~/src/utils/crdt_sync_dart && dart test --coverage=coverage`; then bump -the todo app's `crdt_sync` ref and run its full suite. - -## read first -- lib/src/io_store.dart - -REMOVE ME AFTER FINISH diff --git a/dopamine-ux/DOCS-dopamine-ux-index.md b/dopamine-ux/DOCS-dopamine-ux-index.md index 216002f..f6790bf 100644 --- a/dopamine-ux/DOCS-dopamine-ux-index.md +++ b/dopamine-ux/DOCS-dopamine-ux-index.md @@ -37,13 +37,13 @@ beating a PB) and todo (reward capture, not completion count). |---|---|---|---| | 01 | `01-motion-tokens.md` | `~/src/utils` | 3 | | 02 | `02-structural-check.md` | `~/src/utils` | 2 | -| 03 | `03-diet-guard.md` | `~/src/diet-guard` | 4 | -| 04 | `04-screen-locker.md` | `~/src/screen-locker` | 3 | -| 05 | `05-workout-app.md` | `~/src/screen-locker/stronglift_replacement/workout_app` | 4 | +| 03 | todo note `85ca329b-9809-412c-a47f-eddf055322cf` | `~/src/diet-guard` | 4 | +| 04 | todo note `73a20837-6480-4b31-b966-1898a4e7272e` | `~/src/screen-locker` | 3 | +| 05 | todo note `e50d6a03-ae7d-4da5-bbd3-c33b17f6f074` | `~/src/screen-locker/stronglift_replacement/workout_app` | 4 | | 06 | `06-todo.md` | `~/src/todo` | 3 | | 07 | `07-dufs-theme.md` | `~/src/dufs-cloud` | 2 | -| 08 | `08-dufs-motion.md` | `~/src/dufs-cloud` | 4 | -| 09 | `09-wake-alarm.md` | `~/src/wake-alarm` | 2 | +| 08 | todo note `e301e090-b2ee-4e26-8387-00f356b63521` | `~/src/dufs-cloud` | 4 | +| 09 | todo note `9e1e4922-9f29-4bad-bcf4-96826aa8ffa4` | `~/src/wake-alarm` | 2 | **27 ranked findings total.** @@ -100,13 +100,13 @@ what remains instead. |---|---|---|---| | 01-motion-tokens.md | DONE 2026-08-22 | `f741896` | `motion.md` frozen; `AppDuration`/`AppCurve` + `--duration-*`/`--ease-*` shipped. Tags: `design_system-v0.2.0`, `web_ui-v0.3.2`. | | 02-structural-check.md | not started | — | — | -| 03-diet-guard.md | not started | — | — | -| 04-screen-locker.md | DONE 2026-11-01 | `89c65bd` | Status window `_section_streak` + week-transition reward text (kept in `extra_benefits_state.json`); the dead 09:00 early-bird claim is not shown. Per kuhy: no daily-streak state, no shutdown time, no freezes. Commit is on a worktree branch, unpushed. | -| 05-workout-app.md | DONE 2026-11-01 | `e938201` | Set/warmup `selectionClick`, finish `mediumImpact` + generated chime (music_theory, pinned tag) + dialog arrival, `AppDuration`/`AppCurve` in theme.dart, streak counts up, reduced motion honoured, one Settings "Sound" switch for chime and break-end (foreground service too). Worktree commits, unpushed; `62c1c2c` covers old export/import gaps. | +| todo note `85ca329b-9809-412c-a47f-eddf055322cf` | not started | — | — | +| todo note `73a20837-6480-4b31-b966-1898a4e7272e` | DONE 2026-11-01 | `89c65bd` | Status window `_section_streak` + week-transition reward text (kept in `extra_benefits_state.json`); the dead 09:00 early-bird claim is not shown. Per kuhy: no daily-streak state, no shutdown time, no freezes. Commit is on a worktree branch, unpushed. | +| todo note `e50d6a03-ae7d-4da5-bbd3-c33b17f6f074` | DONE 2026-11-01 | `e938201` | Set/warmup `selectionClick`, finish `mediumImpact` + generated chime (music_theory, pinned tag) + dialog arrival, `AppDuration`/`AppCurve` in theme.dart, streak counts up, reduced motion honoured, one Settings "Sound" switch for chime and break-end (foreground service too). Worktree commits, unpushed; `62c1c2c` covers old export/import gaps. | | 06-todo.md | PARTIAL 2026-11-02 (step 1 DONE; step 2 sound REJECTED by kuhy) | `kuhyx/todo-app 2ffae26` | haptic + checkmark on capture, appRoute motion tokens, animated wizard bar; no sound by decision | | 07-dufs-theme.md | not started | — | — | -| 08-dufs-motion.md | not started | — | — | -| 09-wake-alarm.md | not started | — | — | +| todo note `e301e090-b2ee-4e26-8387-00f356b63521` | not started | — | — | +| todo note `9e1e4922-9f29-4bad-bcf4-96826aa8ffa4` | not started | — | — | ## Keeping these prompts honest diff --git a/dopamine-ux/TODO-03-diet-guard.md b/dopamine-ux/TODO-03-diet-guard.md deleted file mode 100644 index 09bfb9b..0000000 --- a/dopamine-ux/TODO-03-diet-guard.md +++ /dev/null @@ -1,170 +0,0 @@ -Make logging a meal feel satisfying in diet_guard_app - -## findings (4, ranked by value over effort) - -1. **`_onLogMeal()` gives zero feedback** — no haptic, no sound, no animation, no - confirmation. The flagship gap. Fix needs no new dependency. -2. **Streak text is inert** — "Adherence streak: N days" is a plain `Text` that - blinks to a new value. Real data, no salience. -3. **No VIBRATE permission** — blocks haptics until added to the manifest. -4. **No audio capability at all** — no package, no `assets:` block. Step 2 only. - -## what - -Logging a meal currently produces **no feedback whatsoever**. `_onLogMeal()` in -`lib/screens/log_meal_screen.dart` writes the meal, syncs it, clears the fields, -and calls `setState(() { _progress = buildTodayProgress(log); })`. The progress -card silently appears. No SnackBar, no haptic, no sound, no animation. - -That is the whole problem. In kuhy's words: *"noting what you ate should be -SATISFYING and ADDICTING so you WANT TO ACTUALLY note down what you ate."* The -data quality of the entire app depends on the logging habit surviving the days -you don't feel like it, and right now the act of logging gives nothing back. - -Make the moment of logging land: an immediate haptic, a real confirmation, and -the streak/progress reacting visibly rather than blinking into existence. - -## where - -Repo: `~/src/diet-guard`. Flutter app: `~/src/diet-guard/app` (package `diet_guard_app`). - -Primary: -- `app/lib/screens/log_meal_screen.dart` — `_onLogMeal()` (line 122-168 as of - 2026-08-16; **grep the symbol, the line will drift**). The `setState` that - silently swaps in the progress card is at ~:164-166. Card rendered at ~:240. -- `app/lib/widgets/today_progress_card.dart` — `TodayProgress` model (~:15), - `TodayProgressCard` (~:64). Consumed/budget kcal ~:100, P/C/F macro row ~:122, - **"Adherence streak: N days"** ~:129-130 — all plain `Text`, no ring, no bar. -- `app/lib/widgets/streak_summary_row.dart` — `StreakSummaryRow`: logging streak, - adherence streak, YTD tally. Also text-only. -- `app/lib/screens/log_meal_progress.dart` — `buildTodayProgress(DayLog)` (~:18), - `adherenceStreak(...)` (~:31). - -Theme (local hand-copy, **no `design_system` dependency**): -- `app/lib/ui/theme.dart` — `buildAppTheme()` ~:12, `AppStatusColors` - ThemeExtension ~:89, redeclared `AppSpacing` ~:124, `AppRadius` ~:146, - `AppTextSize`, plus an `AppWidth` class the shared package does not have. - -Settings (for the sound opt-out in step 2): -- `app/lib/screens/settings_screen.dart` — body is a `ListView` inside - `Center > ConstrainedBox(maxWidth: AppWidth.prose)` at ~:161-165. - **No `SwitchListTile` exists anywhere in this app** — you are adding the first. -- `app/lib/services/app_settings_service.dart` — the store. **Not - SharedPreferences**: a singleton over a `DocumentStore` writing JSON to - `app_settings.json` (~:26). `init()` ~:49, `_load()` ~:95-112, setters funnel - through `_persist`/`_writeToDisk` ~:147-165 (read the comment at ~:153-155 — - `_writeToDisk` is the single write path every setter must use). - `resetForTesting`/`initForTesting` at ~:66/:74. - -## must - -**Step 1 — motion + haptics (no new dependency; land this first).** - -- On successful meal log, fire `HapticFeedback.mediumImpact()` (or the token - vocabulary's "complete" cue if prompt 01 defined one). `HapticFeedback` comes - from `package:flutter/services.dart` — **no pubspec change needed**. Add - `` to - `app/android/app/src/main/AndroidManifest.xml`; it is currently absent. -- Fire it **immediately on the tap**, before the write/sync completes. The - article's point about micro-animations is that they replace latency with - feedback; a haptic that waits for a network round-trip defeats it. -- Animate the progress card's arrival and the numbers' change rather than - snapping: the kcal figure and the streak count should visibly move to their new - value. Use the motion tokens from prompt 01. -- Show an explicit confirmation of what was recorded — the user should not have - to infer success from a card appearing. -- Honour `MediaQuery.of(context).disableAnimations`: collapse durations to zero. - Haptics still fire (they are not motion). - -**Step 2 — sound (separate, independently revertable).** - -- Add an audio dependency and an `assets:` block to `app/pubspec.yaml` (neither - exists today), plus a short confirmation cue asset. -- Sound ships **on**, with an opt-out `SwitchListTile` in `settings_screen.dart`, - backed by a new bool in `app_settings_service.dart` following the existing - field pattern: private field + static getter, parsed in `_load()`, written via - `_writeToDisk`. -- The toggle controls **UI sound only**. Never route notification-channel - behaviour through it. - -**Both steps:** - -- must not: **reward the calorie number.** The satisfying moment fires on - *logging*, not on being under budget. Rewarding a low number teaches the user - to eat less rather than record honestly, and corrupts the app's own data. A - meal that blows the budget gets the same confirmation as one that doesn't. -- must not: fabricate or round up any displayed value. The streak counts real - consecutive days; the progress bar reflects the true ratio. Framing, timing - and salience are fair game — the numbers are not. -- must not: add a celebratory cue to *deleting* or editing a meal. Only the - record-creating act gets it. -- must not: touch `~/src/diet-guard/diet_guard/` (the Python desktop side) — it has - its own streak logic and is out of scope here. -- optional: a progress ring instead of the plain kcal text. The article's - near-miss point applies honestly here — a ring at 80% of budget is truthful - and more motivating than a number. Only if it stays accurate. - -## done - -1. Logging a meal on the phone produces a haptic within ~50ms of the tap, an - explicit confirmation, and a visible (not instant) update of the streak/progress. -2. `cd ~/src/diet-guard/app && flutter analyze` is clean. -3. `cd ~/src/diet-guard/app && flutter test` passes. -4. With OS "remove animations" enabled, the screen still works and durations are - zero — verified by toggling it, not by reading the code. -5. Step 2 only: the settings toggle flips, persists across an app restart, and - silences the cue. - -## verify - -**On the phone — this app is mobile-primary; desktop is not sufficient.** - -``` -adb devices # confirm 23181JEGR08034 is attached -cd ~/src/diet-guard/app -flutter build apk --release -adb install -r build/app/outputs/flutter-apk/app-release.apk -``` - -Never `flutter install`, never `adb uninstall`, never `pm clear` — those wipe -real logged food data. `adb install -r` preserves it. - -Then log a real meal on the device and report what you felt and saw. A haptic is -a physical outcome: state what the phone did, do not claim it worked from code. -If `com.kuhy.*` focus-mode kills the app on launch, the app needs a focus -whitelist entry before it can be tested. - -## read first - -- `app/lib/screens/log_meal_screen.dart` — `_onLogMeal()`. Read the whole method - before editing; the sync call and the field-clearing both matter for where the - haptic goes. -- `app/lib/services/app_settings_service.dart` — the `dailyKcalGoal` field - (~:34/:40) is the pattern to copy for a new bool. Note the `_writeToDisk` - comment. -- `~/src/diet-guard/diet_guard/_calendar_view.py` — `streaks_text()` ~:136 and - `ytd_text()` ~:154. The Dart `streak_summary_row.dart` docstring says it mirrors - this formatting; keep them consistent if you change wording. -- `~/src/utils/unified-design-system/motion.md` — the motion/haptic vocabulary from - prompt 01. **Prompt 01 must have run first.** - -## context you would otherwise rediscover - -- This app has **no `design_system` dependency** — `app/lib/ui/theme.dart` is a - hand-transcription of the shared tokens (values are byte-identical, names in - comments). Motion tokens from prompt 01 must be **transcribed into that local - theme file**, matching the existing convention. Do not add the dependency here; - that is a separate, larger refactor. -- Capability baseline, verified 2026-08-16: **no audio package, no `assets:` - block, no `vibration` package, no VIBRATE permission.** The only existing - audio-adjacent thing is the `diet_guard_due_slot` notification channel - (`app/lib/services/notification_backend_io.dart` ~:17-18, :52-55) at - `Importance.high` with no explicit sound/vibration override — leave it alone. -- The app already computes both a **logging streak** and an **adherence streak** - (`_daystatus.py:144`/`:153` on the Python side, mirrored in Dart). You are - making existing true data feel like something, not inventing a new metric. -- diet-guard state lives under XDG (`~/.local/share/diet_guard`), unlike - screen-locker's in-repo JSON. Tests isolate it via - `~/src/diet-guard/diet_guard/tests/conftest.py:97` `_isolate_state`. - -REMOVE ME AFTER FINISH diff --git a/dopamine-ux/TODO-04-screen-locker.md b/dopamine-ux/TODO-04-screen-locker.md deleted file mode 100644 index 375a5e7..0000000 --- a/dopamine-ux/TODO-04-screen-locker.md +++ /dev/null @@ -1,173 +0,0 @@ -Surface the workout streak that screen-locker already tracks - -## findings (3, ranked by value over effort) - -1. **`snapshot.streak` is populated and no renderer reads it** — the cheapest - win in the whole project. Zero new state, zero conftest change, zero deps. -2. **Week-transition reward strings are discarded into `_logger.info`** — - fully-formed celebratory sentences that never reach a user. -3. **The success moment exists only on the lock screen**, for 1.5s. The status - window — the surface visited *voluntarily* — shows only enforcement. - -## what - -screen-locker computes a real workout streak and celebratory reward text, then -throws almost all of it away. Verified 2026-08-16: - -- `StatusSnapshot` (`screen_locker/_status_types.py` ~:88-101) carries populated - `streak` and `early_bird_extended` fields, filled by - `_status_data.py` ~:210-214. **Grep confirms no renderer reads them.** -- `_extra_benefits.py` `process_week_transition()` (~:82, reward strings built at - ~:130-139) produces fully-formed celebratory sentences — and - `_startup_checks.py` ~:57-58 sends them to `_logger.info` and nowhere else. -- The one success moment that exists is on the **lock screen**: - `_unlock_view.py` `unlock_screen()` (~:25) paints "Great job! 💪" and - `🔥 {streak}-week streak`, for `_UNLOCK_DELAY_MS = 1500` before routing away. - -So the surface the user visits **voluntarily** — the status window, opened from -the tray icon — shows enforcement and budget depletion, and no evidence of -success at all. This is the cheapest high-value change in the whole project: -the data already exists, is already true, and needs no new state. - -## where - -Repo: `~/src/screen-locker`. - -Primary: -- `screen_locker/_status_sections.py` — the section renderers used by the status - window (`_section_*` functions). **177 lines; has headroom.** Add - `_section_streak` here. -- `screen_locker/status_view.py` — `StatusWindow`, `render()`. Call the new - section from here. ⚠️ **246 lines — only 4 left under the 250 cap.** Expect to - extract something, or add the call in a way that costs ~1 line. -- `screen_locker/_status_types.py` — `StatusSnapshot` (~:88-101), the fields you - are rendering. Read-only for this task. -- `screen_locker/_extra_benefits.py` (179 lines) — `process_week_transition()` - and its reward strings; `current_streak()`, `has_extended_early_bird()`. -- `screen_locker/_startup_checks.py` ~:57-58 — where the reward strings are - currently discarded into a log call. -- `screen_locker/_unlock_view.py` (72 lines, lots of headroom) — the existing - success moment, if you extend it. - -## must - -- **Render the already-populated fields.** Add `_section_streak` to - `_status_sections.py` showing the weekly streak and the extended-early-bird - state, and call it from `status_view.py`'s `render()`. - Zero new state, zero conftest change, zero new dependency. -- **Stop discarding the reward strings.** `process_week_transition()` returns - human-readable celebratory sentences that currently only reach the log. Surface - them where the user will actually see them — the status window is the obvious - home; a milestone is worth more than a silent counter increment. -- Match the existing positive-status styling rather than inventing one: - `_status_sections.py` ~:73-78 already renders `"{n} above the weekly minimum!"` - in success green, and uses `✓` marks at ~:36 and ~:57. -- Keep the streak **true**. It counts real ISO weeks with ≥5 counted workouts - (`_weekly_check.py`: `WEEKLY_WORKOUT_MINIMUM = 5`, `COUNTED_WORKOUT_TYPES = - {phone_verified, runnerup_verified, manual_workout}`). Do not round, pad, or - "encourage" the number. - -- must not: **make any reward reduce enforcement.** This is recorded in the - repo as a deliberate decision: - > `_startup_checks.py` — "there is no banked 'skip a workout' credit — - > that mechanic works against the goal of maximizing weekly workouts, so it was - > removed." - - Every reward here is **display only**. Do not reintroduce a - banked skip, a streak freeze, a grace day, or any mechanic that lowers the - floor. A dopamine loop that lets you skip the workout defeats the app. -- must not: **grant shutdown time for a streak, a week or a milestone.** The - weekly banked shutdown bonus (and the 4-week `_MILESTONE_INTERVAL` +1h) was - removed on purpose (2026-10-10): every shutdown minute comes from the - `earned_time` ladder and stops at its daily ceiling. Rewards here are display - only — no "+Nh" weekly reward, no bonus-hours field, no new shutdown source. -- must not: add **sound** to any enforcement or lock path. screen-locker is a - self-restriction tool; a chime on lock is punishment audio, and a chime on - unlock trains the wrong association. This repo stays silent. (Sound is - opt-out-on elsewhere in the fleet; here it is simply absent.) -- must not: add a write tool to the MCP server. `_mcp.py` ~:14-26 states it is - read-only by design. Adding `streak` to the existing `get_status` output is - fine; a new action tool violates a stated invariant. -- must not: add `# noqa` (banned repo-wide) or a silent `except` - (`scripts/check_silent_failures.py` — every handler re-raises or logs at - warning or above). -- must not: create a new on-disk state file unless genuinely unavoidable. If you - do, you **must** add it to `_ISOLATED_STATE` in - `screen_locker/tests/conftest.py` (~:134), listing *every* module that binds - the path at import time — a missed binding lets tests write to kuhy's real - state. Read the comment at ~:131-133 and the fixture docstring at ~:170-175. - -- optional: a daily streak alongside the weekly one. Note this **would** be new - state (the existing streak is consecutive-*weeks*), so it triggers the conftest - requirement above. diet-guard's consecutive-*day* streak - (`~/src/diet-guard/diet_guard/_daystatus.py` ~:144) is the precedent to mirror. - -## done - -1. Opening the status window (tray icon → click, or `screen-locker-status`) shows - the current streak and early-bird state — with the real values - from `extra_benefits_state.json`, not placeholders. -2. A week-transition milestone reaches the UI instead of only `_logger.info`. -3. `cd ~/src/screen-locker && python -m pytest` passes. -4. `pre-commit run --files ` is clean — including the 250-line cap - (`scripts/check_file_length.py`) and the silent-failure check. -5. `git diff` touches no enforcement logic: no change to lock decisions, weekly - minimums, or shutdown floors. - -## verify - -Desktop — this is a desktop Python app. - -``` -cd ~/src/screen-locker -python -m screen_locker --status # CLI: confirm the values it prints -screen-locker-status # the Tk window: confirm they render -``` - -Paste both outputs. The CLI already prints the streak and early-bird state -(`_status.py`), so it is your ground truth: the window must show the **same numbers**. -If they disagree, the renderer is wrong, not the data. - -Current real state for comparison: `screen_locker/extra_benefits_state.json` and -`screen_locker/workout_log.json` (12 days of entries, 2026-07-12 → 2026-08-15). - -## read first - -- `screen_locker/_status_sections.py` — every existing `_section_*`. Match their - signature and style; note the success-green precedent at ~:73-78. -- `screen_locker/_status_types.py` ~:88-101 — the three fields you are rendering. -- `screen_locker/_extra_benefits.py` — especially the module docstring ~:5-8 - (the enforcement invariant) and the reward strings ~:130-139. -- `screen_locker/_unlock_view.py` — the existing celebration, for tone. -- `screen_locker/status_view.py` — check its line count **before** editing. -- `~/src/screen-locker/CLAUDE.md` — repo rules (no silent failures, ruff `select=ALL`). - -## context you would otherwise rediscover - -- **`workout_log.json` is the single source of truth**, written only through - `_log_mixin.py` `write_signed_entry()` (~:82), HMAC-signed via - `gatelock.log_integrity`, deduped on `workout_id`. The Flutter app converges on - it via `_manual_push.py`. There is no second history — a streak computed from - it is authoritative. -- All persistence is **JSON files, no sqlite**; paths declared in - `screen_locker/_constants.py`. The streak store is - `screen_locker/extra_benefits_state.json` (`consecutive_5plus_weeks`, - `last_processed_iso_week`, `extended_early_bird_iso_weeks`; a legacy - `weekly_shutdown_bonus_hours` map is ignored and dropped on the next write). -- The threshold lives in `_extra_benefits.py`: `_BONUS_THRESHOLD = 5` (workouts - per week, for the streak and the early-bird extension). There is no milestone - constant any more. -- **There is no config system** — no argparse, no env vars, no config file. Every - tunable is a module-level constant. An opt-out toggle has no existing home; - for a self-restriction tool, requiring a code edit to disable is arguably - correct, so prefer a constant in `_constants.py` over new state. -- `~/gatelock` is **not** a repo — it is `~/src/utils/gatelock/gatelock/`, consumed as - a pinned git dep, and it holds no workout state. -- Manual workouts were **not** removed: `_manual_workout.py` implements a - rate-limited, evidence-gated subsystem (budget 2 per 7 days, 10 per 30). -- Three UI surfaces exist: the Tk status window (`status_view.py`), a GTK tray - icon at `~/.config/i3/scripts/screen_locker_tray.py` (outside the repo, shells - out to the CLI, refreshes every 60s), and the lock screen itself. No i3blocks - integration exists despite `format_summary_line` being described as one. - -REMOVE ME AFTER FINISH diff --git a/dopamine-ux/TODO-05-workout-app.md b/dopamine-ux/TODO-05-workout-app.md deleted file mode 100644 index 1373200..0000000 --- a/dopamine-ux/TODO-05-workout-app.md +++ /dev/null @@ -1,194 +0,0 @@ -Close the feedback asymmetry in workout_app - -## findings (4, ranked by value over effort) - -1. **Finishing a workout gets no cue** — while the rest timer gets sound *and* - an 800ms vibration. The app celebrates waiting and ignores achieving. All - infrastructure is already present; only the call is missing. -2. **Set completion has no haptic** — the app's most repeated action, silent. -3. **The fleet's only animation is an inline magic number** — - `Duration(milliseconds: 200)` in `exercise_tile_rows.dart`. -4. **No sound opt-out exists** — the SQLite settings table is ready, but no - public accessor and no `SwitchListTile` anywhere. Step 2 only. - -## what - -workout_app is the only app in kuhy's fleet that can already play a sound and -vibrate — and it spends that capability on the wrong moment. Verified 2026-08-16: - -- **Rest-timer expiry** gets a full cue: `_playBreakEndCue()` in - `lib/screens/workout_screen_session.dart` (~:128-145) plays - `assets/sounds/break_end.mp3` and fires `Vibration.vibrate(duration: 800)`. -- **Finishing an entire workout** gets nothing. `_persistFinishedWorkout()` in - `lib/screens/workout_screen_finish.dart` (~:99-108) ends by showing - `WorkoutSummaryDialog` with no sound, no haptic, no animation. -- **Completing a set** — the core repeated action of the whole app — gets no - haptic either. The `GestureDetector` at `lib/widgets/exercise_tile_rows.dart` - (~:145) toggles state silently. - -So the app celebrates *waiting* and ignores *achieving*. Fix the asymmetry: the -moments worth reinforcing are completing a set and finishing a workout. - -This app also contains the fleet's **only animation** — a lone -`AnimatedContainer(duration: Duration(milliseconds: 200))` at -`exercise_tile_rows.dart` ~:146-147, with an inline magic number. Replace that -number with a motion token while you are in the file. - -## where - -Repo: `~/src/screen-locker`. App: `~/src/screen-locker/stronglift_replacement/workout_app` -(a Flutter app nested inside the screen-locker repo; that directory contains **no -Python** — only the app and two design docs). - -Primary: -- `lib/widgets/exercise_tile_rows.dart` — set-completion `GestureDetector` ~:145, - the `AnimatedContainer` ~:146-147. -- `lib/screens/workout_screen_finish.dart` — `_persistFinishedWorkout()` ~:99-108. -- `lib/widgets/workout_summary_dialog.dart` — `WorkoutSummaryDialog` ~:10. -- `lib/screens/workout_screen_session.dart` — `_playBreakEndCue()` ~:128-145, the - existing cue to model the new ones on. Note its `.catchError` only - `debugPrint`s (~:131-137). -- `lib/screens/workout_screen.dart` — `_audio = AudioPlayer()` ~:78, `vibration` - import ~:9. -- `lib/widgets/rep_circle.dart` — `RepCircle` ~:34, per-set completion indicator. - -Progress/history surfaces (already rich; animate rather than rebuild): -- `lib/screens/history_screen_charts.dart` — `_ProgressStatsCard` ~:71, - **`_StreakRow` ~:139**, `_WeightChart` ~:192. -- `lib/screens/history_screen_painter.dart` — `_ChartPainter` ~:9, `paint()` ~:46, - `shouldRepaint` ~:121 (static; no animation today). -- Streak data: `successStreak`/`failStreak` on `ExerciseState` - (`lib/services/storage_service.dart` ~:42-46). - -Settings (for the sound opt-out): -- `lib/screens/settings_screen.dart` — `build()` ~:166, `ListView` ~:174-208. - Parts: `settings_screen_rows.dart`, `settings_screen_sections.dart`, - `settings_screen_exercise_sections.dart`, `settings_screen_thresholds.dart`, - `settings_screen_widget.dart`, `settings_screen_actions.dart`. - **No `SwitchListTile` exists** — model a new section on `_SyncSection` - (`settings_screen_sections.dart` ~:14) or `_OfflineBackupSection`. -- Store: **SQLite**, not SharedPreferences. There is a ready-made key/value table - — schema `lib/services/storage_service_schema.dart` ~:34-36 - (`CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)`, migration - ~:64-65), accessors `lib/services/storage_service.dart` ~:122-137 - (`_getSetting`/`_setSetting`, both **private** — add a public typed wrapper like - the existing `setLastWorkoutType`). Already backed up/restored via - `lib/services/storage_service_backup.dart` ~:23, :43, :156-163. - -Theme: `lib/ui/theme.dart` — local hand-copy, **no `design_system` dependency** -(`buildAppTheme()` ~:12, `AppStatusColors` ~:76, `AppSpacing` ~:111, -`AppRadius` ~:133, `AppTextSize`). Wired at `lib/main.dart` ~:115. - -## must - -**Step 1 — motion + haptics (no new dependency).** - -- Add `HapticFeedback.selectionClick()` on **set completion** — the tap at - `exercise_tile_rows.dart` ~:145. This is the app's most repeated action; it - should feel like flipping a physical switch. Fire on tap, not after the write. -- Add a completion cue when a **workout finishes** — haptic plus a visible - arrival for `WorkoutSummaryDialog`, rather than it appearing instantly. -- Replace the inline `Duration(milliseconds: 200)` at ~:146-147 with the motion - token from prompt 01, transcribed into `lib/ui/theme.dart`. -- Animate the streak/stat numbers on the history screen to their new values - instead of snapping. `_ChartPainter.shouldRepaint` currently returns a static - result — leave the chart itself alone unless animating it is cheap. -- Honour `MediaQuery.of(context).disableAnimations` — durations collapse to zero; - haptics and sound still fire. -- `VIBRATE` is already granted (`android/app/src/main/AndroidManifest.xml` ~:10) - and `vibration: ^3.1.0` is already a dependency. Prefer `HapticFeedback` from - `flutter/services.dart` for short UI taps and reserve the `vibration` package - for the long timed buzz it already does well. - -**Step 2 — sound (separate, independently revertable).** - -- Add a finish-workout cue asset alongside `assets/sounds/break_end.mp3` - (declared at `pubspec.yaml` ~:52-53 — **this is the fleet's only `assets:` - block**, so you are adding a file, not the block). -- Sound ships **on**, with an opt-out. Add a public - `getSoundEnabled()`/`setSoundEnabled(bool)` on `StorageService` wrapping - `_getSetting`/`_setSetting` (store `'true'`/`'false'`), then a settings section - with a `SwitchListTile`. -- The toggle must silence the **new** cues and the existing break-end cue alike — - one switch, not two behaviours. - -**Both steps:** - -- must not: **reward the weight or rep number.** The cue fires on *logging a set* - and *finishing a workout*, not on hitting a PB or beating last week. Rewarding - the number pushes toward misreporting; rewarding the record keeps the data - honest. A deloaded set gets the same confirmation as a PB. -- must not: fabricate the streak. `successStreak`/`failStreak` count real - outcomes; display them as they are. -- must not: add a cue to *deleting* a set or workout. -- must not: silently swallow an audio error. The existing `.catchError` → - `debugPrint` at ~:131-137 is the local precedent; do not make it quieter. -- must not: break the **100% coverage gate** — new public methods on - `StorageService` need tests. See the comments at - `settings_screen_sections.dart` ~:1-11. -- must not: exceed the **250-line cap** on any file. - -- optional: a subtle near-miss cue on the history screen when a streak is one - workout short of a milestone — legitimate only because the number is true. - -## done - -1. Completing a set produces an immediate haptic on the phone. -2. Finishing a workout produces a haptic and a visible dialog arrival; step 2 - adds a sound. -3. No inline animation duration remains in `exercise_tile_rows.dart`. -4. `cd ~/src/screen-locker/stronglift_replacement/workout_app && flutter analyze` clean. -5. `flutter test` passes **and coverage stays at 100%**. -6. With OS "remove animations" on, the app works and durations are zero. -7. Step 2: the settings toggle flips, persists across restart, and silences both - the new cues and the existing break-end cue. - -## verify - -**On the phone.** - -``` -adb devices # confirm 23181JEGR08034 -cd ~/src/screen-locker/stronglift_replacement/workout_app -flutter build apk --release -adb install -r build/app/outputs/flutter-apk/app-release.apk -``` - -Never `flutter install`, never `adb uninstall`, never `pm clear` — those wipe the -SQLite workout history. `adb install -r` preserves it. - -Then run a real set on the device: tap a set complete, finish a workout, and -report what the phone actually did. Haptics and sound are physical outcomes — -state what you felt and heard; do not infer them from the code. - -## read first - -- `lib/screens/workout_screen_session.dart` ~:128-145 — `_playBreakEndCue()`, the - working cue. Your new cues should look like it. -- `lib/widgets/exercise_tile_rows.dart` — the set-completion tap and the fleet's - only `AnimatedContainer`. -- `lib/services/storage_service.dart` ~:122-137 — `_getSetting`/`_setSetting`, and - `setLastWorkoutType` as the public-wrapper pattern. -- `lib/screens/settings_screen_sections.dart` ~:1-11 — the coverage/line-cap - comments, and `_SyncSection` as the section pattern. -- `~/src/utils/unified-design-system/motion.md` — motion/haptic vocabulary from - prompt 01. **Prompt 01 must have run first.** - -## context you would otherwise rediscover - -- This app has **no `design_system` dependency**; `lib/ui/theme.dart` is a - hand-transcription of the shared tokens. Motion tokens must be transcribed - there, matching the existing convention. Do not add the dependency here. -- `shared_preferences` **is** a dependency but is used only for the device id - (`lib/services/sync_device_id.dart` ~:37-38). Settings go in SQLite. -- Non-motion `Duration`s that are **not** animation and must not be touched: - rest tickers (`workout_screen.dart` ~:91, `workout_screen_breaks.dart` ~:20, - `workout_screen_session.dart` ~:37 — 1s), settings debounces - (`settings_screen.dart` ~:140, :148 — 600ms), `main.dart` ~:56 (20s). -- The app syncs to the screen-locker Python side, which treats - `screen_locker/workout_log.json` as the single source of truth. **Do not change - what counts as a workout** — enforcement depends on it, and it is out of scope. -- The repo enforces a 250-line file cap and bans `# noqa`; ruff runs - `select = ["ALL"]` on the Python side. - -REMOVE ME AFTER FINISH diff --git a/dopamine-ux/TODO-08-dufs-motion.md b/dopamine-ux/TODO-08-dufs-motion.md deleted file mode 100644 index 8b2291d..0000000 --- a/dopamine-ux/TODO-08-dufs-motion.md +++ /dev/null @@ -1,198 +0,0 @@ -Give dufs-cloud real latency feedback on its slow operations - -## findings (4, ranked by value over effort) - -1. **No cancel on the unbounded index walk or the ZIP build** — the real - usability failure, and worth more than any animation. -2. **Slow operations report one static string** — the walk and the zip both know - real counts and discard them. -3. **No `prefers-reduced-motion` block anywhere in 806 lines of CSS** — a - straight accessibility gap. -4. **Long-press multi-select has no haptic** — the canonical place for one. - -## what - -dufs-cloud has genuinely slow operations and tells the user almost nothing while -they run. Verified 2026-08-16: - -- The **recursive cloud index walk** lists every directory in the cloud, - unbounded. Web shows one static string `"Indexing cloud…"`; Flutter shows one - indeterminate `CircularProgressIndicator`. **No count, no current path, no - cancel** on either side. -- The **multi-file ZIP build** downloads every selected file's bytes into memory - and then zips them, with `setBusy('Preparing N item(s)…')` — a single static - string for the entire operation. Neither `buildSelectionZip` signature even - accepts a progress callback. -- Web's entire 806-line `index.css` contains **exactly one transition** - (`.tile-actions { transition: opacity 0.12s }` ~:539-544), zero `@keyframes`, - and **no `prefers-reduced-motion` block at all**. -- Flutter side has **zero animations and zero haptics** — notably - `browser_screen.dart` (~:95) enters multi-select on **long-press** with no - `HapticFeedback.selectionClick()`, the canonical place for one. - -This is the article's micro-animation point in its most defensible form: replace -dead latency with honest, informative feedback. Nothing here needs a manufactured -reward — the operations are genuinely slow and the progress is genuinely knowable. - -## where - -Repo: `~/src/dufs-cloud`. Web: `web/`. Flutter: `app/` (package `dufs_client`). - -**First step — bump both dependencies** to pick up prompt 01's motion tokens: -- `web/package.json`: `"@kuhyx/web-ui": "github:kuhyx/utils#web_ui-v0.3.1&path:/web_ui"` - → bump to **`web_ui-v0.3.2`** (or whatever tag prompt 01 printed). - **pnpm only** — npm cannot install a monorepo subdirectory and will lock the repo. -- `app/pubspec.yaml`: bump `design_system` to the tag prompt 01 cut. - **Prompt 07 must have run first** — it is what adds this dependency at all. - -Slow operations (the highest-value targets): -- Index walk — web `web/src/hooks/use-cloud-index.ts` (`walkInto` ~:36-43, root - walk ~:56-63, `loading` derived ~:113); Flutter - `app/lib/services/cloud_index.dart` (`buildCloudIndex` ~:25, `_walk` ~:31-41), - driven by `app/lib/screens/browser_screen.dart` `_ensureIndex()` ~:184-194. -- ZIP build — web `web/src/lib/download.ts` (`gatherFiles` ~:24, - `buildSelectionZip` ~:37-47, `saveBytes` ~:55; encoder `web/src/lib/zip.ts` - `zipStore` ~:32), triggered from `web/src/components/gallery.tsx` ~:307; - Flutter `app/lib/services/download_zip.dart` (`gatherFiles` ~:15, - `buildSelectionZip` ~:29-37). -- Upload — web `gallery.tsx` ~:186; Flutter - `app/lib/services/dufs_client.dart` `upload()` ~:73-81 (a plain `_http.put` - with the full body — **not** a `StreamedRequest`, so byte progress is not - currently observable), called from `browser_screen.dart` ~:339. -- Bulk mutations, all single static strings in `gallery.tsx`: move ~:211/:271, - delete ~:229/:290 (the bulk delete loops per item and reports only a final - failure count), rename ~:243, create dir ~:257. -- Directory listing — web `web/src/hooks/use-listing.ts` ~:31-72; Flutter - `browser_screen.dart` ~:120/:140/:149/:155. - -Other surfaces: -- `web/src/index.css` (806 lines; `.busy` styled ~:87 — static text, no shimmer). -- `web/src/components/filter-bar.tsx` ~:75-83 — the search input fires `onFilter` - on **every keystroke with no debounce**. -- `web/src/components/gallery.tsx` ~:454 `{loading &&

Loading…

}`, - ~:355 the `busy` span; `folder-picker.tsx` ~:97. -- Flutter spinners: `browser_screen.dart` ~:717, ~:732, `RefreshIndicator` ~:748 - (the one built-in motion affordance), plus `text_editor_screen.dart` ~:101/:80, - `audio_screen.dart` ~:104, `video_screen.dart` ~:169, `pdf_screen.dart` ~:184, - `app/lib/widgets/folder_picker.dart` ~:92. -- Settings: `app/lib/screens/settings_screen.dart` (93 lines — room for a toggle), - store `app/lib/services/settings.dart`. - -## must - -**Step 1 — motion + honest progress (the valuable half).** - -- Make the index walk report **real progress**: directories walked, files found, - and ideally the current path. The walk already knows these numbers — it just - discards them. Thread a progress callback through `buildCloudIndex`/`walkInto`. -- Make the ZIP build report **per-file progress** (`3 of 47`). Both - `buildSelectionZip` signatures need a progress callback added; today neither - accepts one. -- Add a **cancel** to both. A long unbounded operation with no way out is the - actual usability failure here, and it is more valuable than any animation. -- Add `HapticFeedback.selectionClick()` on the Flutter **long-press to - multi-select** (`browser_screen.dart` ~:95). No dependency needed - (`flutter/services.dart`); add `` to the manifest if absent. -- Add a `@media (prefers-reduced-motion: reduce)` block to `web/src/index.css`. - **There is none today** — this is a straight accessibility gap, not a nicety. - Flutter side: honour `MediaQuery.of(context).disableAnimations`. -- Use the shared motion tokens for any transition you add. Replace the inline - `0.12s` at `index.css` ~:539-544 with the token while you are there. -- Debounce the search input (`filter-bar.tsx` ~:75-83). Firing on every keystroke - over a large index is the cause of jank, and no animation fixes that. - -**Step 2 — sound (separate, independently revertable).** - -- A completion cue for long operations only (index built, ZIP ready, upload - finished) — **not** for navigation or selection. A file manager that chirps on - every click is intolerable. -- Sound ships **on**, with an opt-out toggle in - `app/lib/screens/settings_screen.dart` backed by `app/lib/services/settings.dart`, - and an equivalent on web. -- Consider suppressing the cue when the operation finishes in under ~2s — a - completion sound for something that felt instant is noise. - -**Both steps:** - -- must not: **fake progress.** No indeterminate bar dressed up as determinate, no - bar that advances on a timer, no "estimated" percentage that is really elapsed - time. If a total is unknown, show the count that *is* known ("47 files found") - rather than a fabricated fraction. This is the project's first test and the - easiest place in the fleet to violate it. -- must not: add a variable/random reward. There is no real pool here; any - variance would be manufactured. -- must not: change what the index actually contains, or filter/sort semantics - (`web/src/lib/filter-sort.ts`, `app/lib/util/filter_sort.dart` mirror each - other — a change to one desyncs the port). -- must not: use npm in `web/`. **pnpm only.** -- must not: let web and Flutter drift further apart. If you add a progress - callback on one side, add the equivalent on the other, or state explicitly in - the summary why not. - -- optional: skeleton screens for the directory listing instead of "Loading…". - Genuinely useful for perceived latency, but lower value than cancel + real - counts. Do it only after step 1 lands. - -## done - -1. Starting a cloud index shows a live, **true** count and can be cancelled — on - both web and Flutter. -2. A multi-file ZIP shows per-file progress and can be cancelled. -3. `web/src/index.css` has a `prefers-reduced-motion` block; with the OS setting - on, transitions are suppressed. -4. Long-press multi-select produces a haptic on the phone. -5. `cd ~/src/dufs-cloud/web && pnpm install && pnpm build && pnpm test` passes - (use the repo's actual script names). -6. `cd ~/src/dufs-cloud/app && flutter analyze && flutter test` clean. -7. No fabricated progress value anywhere in the diff. - -## verify - -**Both surfaces — this repo has two and they drift.** - -Web: run the dev server, open it in a real browser, and index a directory large -enough to take several seconds. Confirm the count moves, the cancel works, and -the numbers are real (cross-check the final count against the actual file count). -A jsdom test cannot verify this — it has no layout and no perceived latency. - -Flutter, on the phone: -``` -adb devices -cd ~/src/dufs-cloud/app -flutter build apk --release -adb install -r build/app/outputs/flutter-apk/app-release.apk -``` -Never `adb uninstall`/`pm clear` — the app holds a stored credential. - -Then trigger a real index build and a real multi-file ZIP on the device and -report what you saw. State the haptic as a physical observation. - -## read first - -- `web/src/hooks/use-cloud-index.ts` and `app/lib/services/cloud_index.dart` — - read both before changing either; they mirror each other deliberately. -- `web/src/lib/download.ts` and `app/lib/services/download_zip.dart` — same. -- `web/src/components/gallery.tsx` — the `setBusy` call sites; they are the - inventory of every operation currently reduced to one static string. -- `~/src/dufs-cloud/DESIGN_AUDIT_TODO.md` — prior findings for this repo. -- `~/src/utils/unified-design-system/motion.md` — vocabulary from prompt 01. - -## context you would otherwise rediscover - -- **Prompt 07 must have run first.** Until it does, `app/` has no - `design_system` dependency and hand-restates the palette in - `app/lib/ui/theme.dart`. -- Web tokens come from `@kuhyx/web-ui` imported in `web/src/main.tsx`; - `web/src/index.css` declares only `--overlay`. New tokens arrive **only** after - the tag bump — this is the step that is easy to forget and silently produces - "the variable doesn't exist". -- `upload()` uses a non-streaming `_http.put`, so real byte-level upload progress - needs a `StreamedRequest` refactor. That may be more than this prompt wants — - if you skip it, say so explicitly and report per-**file** progress instead of - per-byte rather than faking a byte percentage. -- Media playback (`web/src/hooks/use-dash-player.ts`, `web/src/lib/dash-buffer.ts`, - `app/lib/services/app_player.dart`) has its own buffering states. Out of scope - unless trivially adjacent. - -REMOVE ME AFTER FINISH diff --git a/dopamine-ux/TODO-09-wake-alarm.md b/dopamine-ux/TODO-09-wake-alarm.md deleted file mode 100644 index cd2e811..0000000 --- a/dopamine-ux/TODO-09-wake-alarm.md +++ /dev/null @@ -1,164 +0,0 @@ -Add motion and haptics to the wake-alarm phone app - -## findings (2, ranked by value over effort) - -1. **Setting an alarm gives no confirmation or haptic** — and since the real - alarm lives in another app entirely, confirming the handoff is the only - reassurance this app can offer. -2. **Zero motion, and the thinnest local theme in the fleet** — it lacks - `AppSpacing`/`AppRadius`/`AppTextSize`, so widgets copied from sibling apps - will not compile here. - -## what - -`~/src/wake-alarm/phone_app` is the smallest surface in this project: 12 Dart files, -a sync companion that sets a time, syncs it, and hands off to the system clock -app. It has **zero motion, zero haptics, zero audio, zero progress feedback** — -verified 2026-08-16. Its settings screen is a 56-line `StatelessWidget` with two -`ListTile`s and no state at all. - -Bring it up to the fleet's baseline: a confirmation the alarm was actually set, -motion tokens on its transitions, and a haptic on the one action that matters. - -**This app is deliberately last and deliberately small.** It also carries the -project's sharpest hazard — see `must not` about alarm audio. - -## where - -Repo: `~/src/wake-alarm`. Flutter app: `~/src/wake-alarm/phone_app` (**underscore**, not -`phone-app`). The repo root is a Python package; the Flutter app is only the -`phone_app/` subdirectory. - -Primary: -- `phone_app/lib/screens/home_screen.dart` — `_setPhoneAlarm()` ~:86-96 builds an - `AndroidIntent(action: 'android.intent.action.SET_ALARM', arguments: {HOUR, - MINUTES, SKIP_UI: false, MESSAGE: 'Wake Alarm'})` via `android_intent_plus`. - The `CircularProgressIndicator` is at ~:189. The only `Duration` in the whole - app is `.timeout(const Duration(seconds: 3))` ~:95 — **an intent timeout, not - animation. Do not tokenise it.** -- `phone_app/lib/screens/settings_screen.dart` — 56 lines, `StatelessWidget`, - `ListView` ~:21-53, `MaterialPageRoute` ~:30, :49. -- `phone_app/lib/services/sync_settings.dart` — holds **only** the GitHub token - in the keystore (`FlutterSecureStorage`, `_secureTokenKey = 'github_token'` - ~:31, `load()` ~:36, `save()` ~:61). There is no general app-preferences class. -- `phone_app/lib/ui/theme.dart` — local hand-copy, **no `design_system` - dependency**. `buildAppTheme()` ~:12, `AppStatusColors` ~:54. ⚠️ Unlike the - other apps it does **not** declare `AppSpacing`/`AppRadius`/`AppTextSize`, so a - widget copied from diet-guard or workout_app **will not compile here**. Wired at - `phone_app/lib/main.dart` ~:18. - -## must - -**Step 1 — motion + haptics (no new dependency).** - -- Add `HapticFeedback.mediumImpact()` when the alarm is successfully set — the - single most important action in the app. Fire it on the tap, not after the - intent round-trips. `HapticFeedback` is in `package:flutter/services.dart`; add - `` to - `phone_app/android/app/src/main/AndroidManifest.xml` if absent. -- Give setting the alarm a **clear confirmation**: which time was set, and that - the handoff to the system clock succeeded. Today the user gets a `_status` - string and a spinner. Since the actual alarm lives in another app, confirming - the handoff is the only reassurance this app can offer — make it unambiguous. -- Transcribe the motion tokens from prompt 01 into `phone_app/lib/ui/theme.dart`, - matching the existing hand-copy convention. Add `AppSpacing`/`AppRadius`/ - `AppTextSize` **only if** you need them; otherwise leave the file's shape alone. -- Apply motion tokens to the two `MaterialPageRoute` transitions. -- Honour `MediaQuery.of(context).disableAnimations`. - -**Step 2 — sound (optional here; consider skipping).** - -- A UI confirmation cue would need an audio dependency, an `assets:` block, a - settings toggle, **and** a preferences class that does not exist yet (see - below). For an app whose entire job is to hand off to another app, that is a - poor value ratio. -- **Recommendation: skip sound in this app** and say so explicitly in the session - summary. If you do add it, it needs the full opt-out toggle like everywhere - else — and the toggle work below. -- To add any toggle here you must do three things the other apps do not require: - (a) convert `SettingsScreen` from `StatelessWidget` to `StatefulWidget`, - (b) create a preferences class — there is no `AppSettings` equivalent; model it - on `~/src/todo/lib/data/app_settings.dart`, and note `shared_preferences: ^2.3.0` - is **already a dependency**, (c) add the `SwitchListTile` to the `ListView`. - -**Both steps:** - -- must not: **touch anything under `~/src/wake-alarm/wake_alarm/`.** That is the - desktop Python daemon that produces the real alarm noise — sine-tone WAVs via - `paplay` → `aplay` → `speaker-test` (`_audio.py`, `_play_on_all_sinks` ~:218, - pcspkr evdev fallback `_beep_pcspkr` ~:81), escalating 440Hz → 1000Hz → loud in - the loop at `_alarm.py` ~:393-407, with sink volume save/restore in `_sinks.py`. - **Never route a UI-sound opt-out through any of it.** A toggle that silences a - wake-up alarm is a bug that makes someone late for work. -- The two systems never share a process, package, or file — `phone_app` has no - audio engine at all — so a UI sound layer here **structurally cannot** touch - alarm playback. Keep it that way; do not build a bridge between them. -- must not: change the alarm-setting intent, its arguments, or the 3s timeout. - Feedback wraps that call; it does not modify it. -- must not: add a streak, a counter, or any reward for setting an alarm. Waking - up is not a metric this app owns, and rewarding "alarms set" rewards the wrong - act entirely. -- must not: copy a widget from another app without checking it only uses symbols - this app's thinner theme actually declares. - -## done - -1. Setting an alarm produces a haptic and an unambiguous confirmation naming the - time that was set. -2. Page transitions use the shared motion tokens; no inline animation `Duration` - remains (the 3s intent timeout stays). -3. `cd ~/src/wake-alarm/phone_app && flutter analyze` is clean. -4. `cd ~/src/wake-alarm/phone_app && flutter test` passes. -5. With OS "remove animations" enabled, the app works and durations are zero. -6. `git diff` shows **no changes under `~/src/wake-alarm/wake_alarm/`**. -7. The summary states whether sound was added or deliberately skipped. - -## verify - -**On the phone.** - -``` -adb devices # confirm 23181JEGR08034 -cd ~/src/wake-alarm/phone_app -flutter build apk --release -adb install -r build/app/outputs/flutter-apk/app-release.apk -``` - -Never `flutter install`, never `adb uninstall`, never `pm clear` — the app stores -a GitHub token in the keystore and wiping it means re-authenticating. - -Then set a real alarm on the device: confirm the haptic fired (state it as a -physical observation), the confirmation named the right time, and **the system -Clock app actually received the alarm**. The last one is the real test — this -app's job is the handoff, so verify the alarm exists in the Clock app, not just -that this screen said so. - -Do **not** test by letting an alarm actually fire unless kuhy asks — that -involves the desktop daemon and real noise. - -## read first - -- `phone_app/lib/screens/home_screen.dart` — `_setPhoneAlarm()` in full, - including the timeout and the `_status` string handling. -- `phone_app/lib/ui/theme.dart` — note what it does **not** declare before - writing any widget. -- `~/src/todo/lib/data/app_settings.dart` — the preferences-class pattern, only if - you decide to do step 2. -- `~/src/utils/unified-design-system/motion.md` — vocabulary from prompt 01. - **Prompt 01 must have run first.** - -## context you would otherwise rediscover - -- **The Flutter app produces no audio whatsoever.** No audio package, no - `assets:` block; `assets/` contains only launcher icons. It fires an Android - `SET_ALARM` intent and the **system Clock app** rings. The desktop Python - daemon is an entirely separate program on a different machine. -- The repo root (`~/src/wake-alarm/wake_alarm/`, `wake_alarm.egg-info`) is the Python - package. Only `phone_app/` is Flutter. -- This app has **no `design_system` dependency** and the thinnest local theme in - the fleet. Adding the dependency is out of scope here (see prompt 07 for what - that migration looks like when done properly). -- No streaks, no charts, no progress, no completion counts exist — and none - should be added. This is a two-button utility. - -REMOVE ME AFTER FINISH diff --git a/music_theory/TODO-multivoice-score.md b/music_theory/TODO-multivoice-score.md deleted file mode 100644 index 8699131..0000000 --- a/music_theory/TODO-multivoice-score.md +++ /dev/null @@ -1,31 +0,0 @@ -# Add a plain-text multi-voice score format - -## what -Add a plain-text score format that `compose.py` and `sequencer.py` can -render: named voices, a noise-drum voice, a `meter` field (3/4, 6/8) and -A/B sections. Today `sequencer.BEATS_PER_BAR` is fixed at 4 and a `Recipe` -has one bass, one arpeggio and one melody. -Source: simonwillison.net/2026/Oct/6/scrimshaw-jukebox. - -## done -``` -cd ~/src/utils/music_theory && PYTHONPATH=. python -m pytest music_theory/tests/test_score.py music_theory/tests/test_sequencer.py music_theory/tests/test_compose.py --cov=music_theory.score --cov=music_theory.sequencer --cov=music_theory.compose --cov-branch --cov-fail-under=100 -``` -exits 0. It covers: a 3/4 and a 6/8 score render bar-exact loops, two named -voices stack, the noise-drum voice is deterministic for a fixed seed, and an -A/B-section score renders A then B in order. - -## verify -Desktop. Render one 3/4 and one 6/8 example score with -`PYTHONPATH=. python -m music_theory render --score examples/waltz.txt --out /tmp/waltz.wav`, -check `analysis.dominant_frequency` and the frame count equal -`sequencer.loop_frames` for the bar count, and re-render for byte-identical -output. The final listen is kuhy's. - -## read first -- `music_theory/compose.py` (`Recipe`), `music_theory/sequencer.py` - (`NoteEvent`, `BEATS_PER_BAR`), `music_theory/noise.py` (drum source). -- `DOCS-article-walkthrough.md` (article section 10, notation as format). -- `~/.claude/skills/procedural-music/SKILL.md`. - -REMOVE ME AFTER FINISH diff --git a/vmbox/TODO-discovery-test.md b/vmbox/DOCS-discovery-test.md similarity index 99% rename from vmbox/TODO-discovery-test.md rename to vmbox/DOCS-discovery-test.md index c80c8f4..077ed3c 100644 --- a/vmbox/TODO-discovery-test.md +++ b/vmbox/DOCS-discovery-test.md @@ -209,5 +209,3 @@ All four, in this order: why the midnight-shutdown timer stays rejected: a Fail powers the machine off mid-session, regardless of how clean its prose neighbourhood is. 4. A written undo recipe captured *before* the subject runs. - -REMOVE ME AFTER FINISH