mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:27:16 +01:00
Move TODOs to todo app
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
crdt_sync_dart tests / test (push) Has been cancelled
crdt_sync_flutter tests / test (push) Has been cancelled
music_theory tests / test (push) Has been cancelled
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
crdt_sync_dart tests / test (push) Has been cancelled
crdt_sync_flutter tests / test (push) Has been cancelled
music_theory tests / test (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fd013abf67
commit
af3c98a50e
@ -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 /<prefix> | 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
|
||||
@ -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
|
||||
@ -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
|
||||
`<file>.<pid>.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.<pid>.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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
`<uses-permission android:name="android.permission.VIBRATE"/>` 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
|
||||
@ -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 <changed 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
|
||||
@ -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
|
||||
@ -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 && <p className="muted">Loading…</p>}`,
|
||||
~: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 `<uses-permission
|
||||
android:name="android.permission.VIBRATE"/>` 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
|
||||
@ -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
|
||||
`<uses-permission android:name="android.permission.VIBRATE"/>` 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
|
||||
@ -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
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user