docs: wave 11 accepted; SPEC records auth refresh + why the 404 rule needs it
HANDOFF: expired-token incident, local data loss, verification incl. live sync against the public URL, phone recovery path, HAZARD #12 (slow storage). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
009cb9a7cc
commit
4695731f91
4 files changed
+77
-1
No files matched your search
@@ -835,3 +835,64 @@ time with the monotonic clock (`/proc/uptime`), which does not advance while fro
|
||||
Also seen 2026-09-16: Claude Code's background-task runner killed Gradle runs "because the system
|
||||
is running low on memory" even with ~6GB available (8GB box, no swap; Kotlin daemon ~2.1GB,
|
||||
Gradle daemon ~1.2GB). Foreground runs of the same command succeeded.
|
||||
|
||||
## Wave 11 — M-authexpiry: sync broken by an expired token — ACCEPTED 2026-09-17
|
||||
**User report:** every sync on the phone said "Sync failed: HTTP 400". Server and public
|
||||
URL were both fine. PocketBase's request log (`pb_data/auxiliary.db`, `_logs`) showed
|
||||
every phone request with `auth: ""`: POST books -> 400 "create rule failure", PATCH
|
||||
books -> 404. **Root cause:** user tokens lasted PocketBase's default 5 days, the app
|
||||
logged in once and never refreshed, and PocketBase does NOT reject a bad token; it
|
||||
silently serves the request anonymously. Last good sync was 09-13.
|
||||
|
||||
**The worse half: data loss on the phone.** `SyncEngine.update*` hard-deletes the local
|
||||
row on 404 (SPEC rule). The anonymous PATCHes 404'd on five books that still exist on the
|
||||
server, so they were deleted locally, and the pull cursor was already past them.
|
||||
Server copies intact (kclh1e51t7hmj0z, insggwycbrd6o48, 9tp7rumid3rzsn9, 9plhmyj5s68f58e,
|
||||
mle7o641pn7zzvt). Any unsynced local edits to those five on the phone are lost.
|
||||
**Lesson: when you debug a server's 4xx, check the app's 4xx handling for side effects.**
|
||||
|
||||
**Fixes:**
|
||||
- `ab1d294` (orchestrator): migration sets user token duration to 180 days (the user chose it).
|
||||
Verified: fresh login token exp = 180.0 days; refresh also returns 180 days.
|
||||
- `009cb9a` (worker + chain fix-worker, $4.89 + $1.34): refresh at the top of every sync;
|
||||
401/403 -> `SyncResult.AuthExpired` (token cleared, URL/email kept, nothing pushed);
|
||||
library sync bar "Signed out — sign in again to sync" (tap -> setup); settings "Sign in"
|
||||
button; setup pre-fills URL + email; login clears pull cursors; one-time full re-pull
|
||||
(`full_repull_2026_09_17_done` flag) to restore the five books; cancellation rethrown in
|
||||
`sync()`. First gate RED: a pre-existing race in `LibraryViewModelTest` (cancelled
|
||||
search job not joined). The fix worker made the test join it. That was a test fix, not a
|
||||
logic change.
|
||||
|
||||
**Orchestrator verification:** read the full data-layer + VM diff; assembleDebug /
|
||||
testDebugUnitTest (355, 2 skipped, 0 fail; was 337) / verifyPaparazziDebug all exit 0; no
|
||||
"always 'false'"; eyeballed the 4 new PNGs (fine, light+dark). **LiveSyncTest run against
|
||||
the PUBLIC URL** (`PB_URL=https://bookshelf-dev-b2jqx.sprites.app server/live-sync-test.sh`):
|
||||
passed, not skipped; server log shows 4 auth-refresh calls, all 200, all authenticated.
|
||||
Anonymous LIST on all four collections still 403.
|
||||
|
||||
**Recovery on the phone:** install the new build. The first sync gets 401 on refresh and
|
||||
shows "Signed out". The user signs in (password only), which clears the cursors, and the
|
||||
next sync re-pulls everything, which brings back the five books. The queued PENDING_CREATE
|
||||
books from the failed syncs push normally. NOT verified on-device yet.
|
||||
|
||||
**Known soft spots (accepted):**
|
||||
- The 401/403 "belt and braces" catch mid-sync would NOT fire for a token that dies
|
||||
mid-sync. PocketBase would 404 anonymously, not 401. The window is one sync right after
|
||||
a successful refresh that issued a 180-day token, so it's negligible in practice. It would
|
||||
matter if tokens are ever revoked server-side (password change, tokenKey reset) during a sync.
|
||||
- The library sync bar is tappable, but nothing visual marks it as tappable. The label
|
||||
says what to do.
|
||||
- `SetupViewModel` pre-fill is async and could overwrite text the user types in the first
|
||||
few ms. Negligible.
|
||||
- One unnecessary full pull on fresh installs (the flag starts false). Harmless.
|
||||
- `SettingsStore.resetFullRepullDoneForTesting` is an internal test-only method.
|
||||
|
||||
### HAZARD #12 — sprite storage degrades; restart fixes it
|
||||
2026-09-17 ~01:30Z: `testDebugUnitTest` ran more than 20 min (normally ~75s).
|
||||
`ComponentGalleryPaparazziTest` took 763s and `BookDaoTest` 327s, and git was slow too.
|
||||
Nothing was hung: result XML was still being written, just very slowly. The user restarted
|
||||
the sprite and the same run took 74s. If builds or git crawl, suspect the box, not the
|
||||
code: compare against the gate logs' "BUILD SUCCESSFUL in" times and ask the user to restart.
|
||||
|
||||
`tasks/wave-chain.sh` now reads its commit subject from `tasks/<task>.subject`
|
||||
(it was hard-coded for waves 9/10).
|
||||
@@ -82,6 +82,16 @@ Pull: GET /api/collections/{c}/records?filter=(updated>'{cursor}')&sort=updated
|
||||
Push: records where syncState != SYNCED. PENDING_CREATE -> POST (with our id),
|
||||
PENDING_UPDATE -> PATCH, PENDING_DELETE -> PATCH {deleted:true}.
|
||||
On 404 for update/delete: drop local record. On 409/duplicate id: switch to PATCH.
|
||||
Auth: EVERY sync starts with POST /api/collections/users/auth-refresh and stores the
|
||||
new token, BEFORE any push or pull. 401/403 (from refresh or any later call)
|
||||
-> clear the token only (keep URL + email), stop, report AuthExpired: the UI
|
||||
says "sign in again" and routes to setup, pre-filled. Offline -> ordinary
|
||||
failure, token kept. WHY: PocketBase does not reject an expired token, it
|
||||
treats the request as anonymous, and the rules then 404 on records that
|
||||
exist — so the 404 rule above is only safe on a freshly-proven token
|
||||
(2026-09-17: five books hard-deleted locally this way). User tokens last
|
||||
180 days server-side (migration 1789608448).
|
||||
Sign-in clears all pull cursors, so every fresh session reconciles fully.
|
||||
Order: push THEN pull (so our writes come back canonical).
|
||||
Conflict: last-write-wins on `updated`. Document this in README; do not build
|
||||
anything cleverer.
|
||||
@@ -147,6 +157,7 @@ scan Camera + reticle; on hit -> bottom sheet w/ fetched book + shelf picker
|
||||
locations Bookcases -> shelves tree. CRUD + reorder. Book counts per shelf.
|
||||
Tap a shelf -> library filtered to it. "Move books" bulk action.
|
||||
settings Server, account, sign out, manual sync + last-sync time, book/cover counts.
|
||||
Session expired -> "sign in again" + Sign in button instead of Sync now.
|
||||
|
||||
## Quality bar
|
||||
- No emulator on this box (no KVM). Verify via: `./gradlew assembleDebug`,
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
chain complete 2026-09-15T18:28:39+00:00
|
||||
chain complete 2026-09-17T02:54:43+00:00
|
||||
@@ -0,0 +1,4 @@
|
||||
=== WAVE 11 (M-authexpiry) passed the chain gate 2026-09-17T02:54:43+00:00 ===
|
||||
commit 009cb9a (pushed); tests 355; worker $4.886013000000001, 123 turns
|
||||
Mechanical gate only. Orchestrator must still review: read logs/M-authexpiry.summary,
|
||||
eyeball new Paparazzi PNGs, and read the diff (git show --stat 009cb9a).
|
||||
Reference in new issue
Block a user