Cut production over #26
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent
#18
What to build
The one-way door. Export fresh, import, verify, and confirm the whole system works end to end against real data — the owner's library intact, Discord login working, a userscript syncing, and an already-installed script still working on the old token.
There is no dual-write period and no going back except by restoring the retained volume.
Acceptance criteria
Blocked by
#24, #25
Cutover staged and rehearsed against production, but not executed — blocked on the four Discord values in the server's
.env.Branch:
feat/cut-production-over(2 commits offmain).Rehearsal against the real host
The runbook was dry-run against a warm copy of the live SQLite volume. Numbers match #25 exactly:
reading/ 11archived, allkind=manga(site, series_id)pairs → 29 SeriesI'm Being Misunderstood as a Soccer Genius,The Chaebeol's Youngest Son,The Nebula's Civilization) come out with balanced doubled quotesfavoriteemitted astrue/false, never0/1;reader_idnever a literalBEGIN+ temp table + 29 series + 29 bookmarks +COMMITGET /bookmarksreturns 29The generator lives only at
/tmp/cutover/gen_import.pyon the server. Not committed, per #25.Runbook defects the rehearsal found
CUTOVER.mdas merged would have failed mid-cutover. Fixed in this branch:bookmarkmanager_bookmarks-data. The compose project is the lowercased directory name, and the checkout is~/mangaBookmark, so the volume ismangabookmark_bookmarks-data. Now derived exactly — not withdocker volume ls --filter name=, which is a substring match that can return several volumes into a-vmount or anrm.-wal.compose stopSIGKILLs after 10s and the live volume currently carries a 1.1 MB-wal; copyingbookmarks.dbalone would have silently dropped whatever it held. Now asserted, with a fallback.jqis not installed on the server. §5's read-path check now usespython3, which the runbook already requires.$VOL. §6 removes the retired volume "once the Postgres data has been trusted for a while" — a month later, in a shell where$VOLis unset. Now self-contained.REDEPLOY.mdandDEPLOY.mdpointed at/opt/bookmarkmanager, so §6's handoff toREDEPLOY.md§1 sent the operator to a path that does not exist here.REDEPLOY.md§1 listed the pre-split schema —readersandsessionsmissing from both the\dtoutput and thepg_restore --listcontents.Environment contract (last AC)
README.md's config table listed 8 of the backend's 25 variables, omitting the entireDISCORD_*set the backend refuses to start without, plus the poller and userscript-path vars. Completed, with the compose-only variables (POSTGRES_PASSWORD,BOOKMARK_*_HOST,PROXY_NETWORK,TRAEFIK_*) called out as such. Retired variables were already clean:WEB_PASSWORDsurvives only in ADR-0002 as history, andAPI_TOKENis deliberately retained and labelled for the grace window.Also fixed
Production
git pullwas broken — the checkout's remote was the HTTPS clone URL with no credential helper, soREDEPLOY.md§2 died oncould not read Username. Switched to SSH on Gitea's port 2222 (port 22 is the host's own sshd and rejects every key). Recorded in the troubleshooting table.Staged on the server, nothing disruptive
2cc1e69;bookmarkmanager-backend:latestrebuilt;postgres:17-alpinepulled — so the downtime window is an import, not a build..envbacked up to.env.pre-cutover.bak;TOKEN_KEYandPOSTGRES_PASSWORDgenerated;API_TOKEN_GRACE_UNTIL=2026-08-22;DISCORD_REDIRECT_URIset.Blocked
OWNER_DISCORD_ID,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,DISCORD_GUILD_IDare empty in~/mangaBookmark/.env; compose refuses to interpolate. Once they are set, remaining ACs run in order: stop + fresh export, generate, review,up, import, verify, then the owner's Discord login, userscript install, chapter read, and the retired-token sync check.go test ./...green (uncached).Cutover executed
Production is on Postgres.
mainat1b1820d.§1 — fresh export
Old API stopped first, then exported. The WAL assertion earned its place: the live volume had been carrying a 1.1 MB
-wal, and the clean SIGTERM checkpointed it away, leavingbookmarks.dbalone at 24 KB — verified before copying rather than assumed.../mangaBookmark-backups/bookmarks-20260808-090657.dbThe earlier snapshot was not reused, and the freshness shows:
The-Outcast-Is-Too-Good-at-Martial-Artsreads Chapter 137 in this export where the rehearsal copy had 136.§2 — schema and owner Reader
Five tables built by the migration runner. Exactly one row in
readers,discord_id= the configured owner.bookmarksandseriesempty before the import.§3–4 — generated and reviewed
61 lines:
BEGIN, tempownertable, 29 series, 29 bookmarks,COMMIT. Read in full before applying. Apostrophes doubled (The Nebula''s Civilization,I''m Being Misunderstood as a Soccer Genius,The Chaebeol''s Youngest Son); Unicode’left alone;favoriteemittedtrue/false; everyreader_idresolved viaSELECT id ... FROM owner, never a literal; the%5C%27URL escapes survive verbatim.§5 — applied and verified
not_owned_by_ownerresolves the Reader by Discord id, independently of theORDER BY id LIMIT 1the import used, andreaders_totalis beside it so a NULL subquery cannot fake a zero.Rather than sampling, every migrated row was compared against the snapshot it came from — 29 rows × 13 columns, 0 mismatches, including the nullable
latest_chapter_numand theupdated_atordering key.Read path over HTTPS:
/healthzok,/bookmarks401 unauthenticated, 401 on a wrong credential, 29 rows on the retired global token, CORS preflight 204 echoinghttps://asurascans.com, web UI 200.The grace path is audited in production, not just in tests:
§6 — afterwards
mangabookmark_bookmarks-dataretained, still undeclared in compose.bookmarks-20260808-091332.dump,pg_restore --listshows all five tables./tmp/cutoverdeliberately left in place until the remaining checks pass — it holds the snapshot and the comparison script, which are the reference if anything looks wrong.rm -rf /tmp/cutoverafterwards.A blocker found on the way
The guild-membership check called
GET /guilds/{guild}/members/{user}— the Guild resource's Get Guild Member, which requires a Bot token and the application present in the guild. Handed a user Bearer token it answers 401, whichdiscordMemberreported as an error, so every sign-in would have rendered "Discord sign-in is unavailable right now". Found before the cutover rather than during it.guilds.members.readgrants Get Current User Guild Member,GET /users/@me/guilds/{guild}/member. Same single-guild question, same privacy property, and it accepts the token we hold. #18 flagged this as verified from Discord's documentation but never from a live flow — it was wrong.The test stub mirrored the implementation, so the suite was blind to it. It now serves the OAuth path and answers the bot path 401 the way Discord does; reverting the fix fails the test with the exact production symptom.
Still open
interval=10m cooldown=1h batch=14 stagger=20s— identical to the pre-cutover values — and the first wake was due 09:21:21 UTC. Owner is verifying.Owner-verified against production: Discord login works and shows the library, the poller is running normally, and an already-installed userscript is still syncing on the retired global token. Those three are ticked.
The install AC stays open, and it turned up a real bug. Install works on desktop, but on mobile Violentmonkey (Chromium) the Install link does nothing useful — Violentmonkey does not intercept navigation to a
.user.jsURL there, so the browser just renders the script as text and there is no path to installing it.Fixed in #35:
?download=1on the same session-gated install endpoint setsContent-Disposition: attachment, so the file saves and can be added from Violentmonkey's own menu. The setup panel now offers Download links alongside Install. The plain link stays inline deliberately — the updater polls the/u/path and an attachment disposition there would break auto-update.Remaining before this closes: install a script on mobile via the new Download link and record a chapter read end to end.
I verified that the userscript download is working perfectly fine. and the userscript install is good. i check the userscript install in a desktop browser. The poller working just fine.