From aaf5c3990a43d5041a9ac5ec05fd3e8a2d6abfd2 Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Mon, 27 Jul 2026 13:55:45 +0700 Subject: [PATCH] chore: ignore the go build artifact, mark two accepted limitations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `go build ./...` in backend/ emits `backend/backend` (the module is `mangabm/backend`), but .gitignore only listed `backend/server` — the name the Dockerfile uses for its container-internal build. The real artifact was untracked and unignored, and came close to being committed twice. Keeps the `server` entry, since a local `go build -o server` still matches the Dockerfile's naming. Also records two deliberate limitations as `ponytail:` comments so they are greppable rather than living only in prose: - latest.go: the poller's Store.Get + Store.Upsert is not wrapped in a transaction, so a client PUT committing between the two is lost to the stale re-read. Already documented in CLAUDE.md; the marker names the upgrade path (wrap in a tx) and the trigger (more than one user). Note this now costs a status change, not just read progress. - web.go: a swapped card stays on screen when its new status no longer matches the active tab. The alternative is a full list round trip per toggle; the card showing its new state is the feedback that matters. Comments only — no logic change. Tests pass, CGO_ENABLED=0 builds. Co-Authored-By: Claude Opus 5 --- .gitignore | 1 + backend/latest.go | 7 +++++++ backend/web.go | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/.gitignore b/.gitignore index 5c85376..1762a52 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.db-shm *.db-wal backend/server +backend/backend .playwright-mcp/ graphify-out/ plans/ diff --git a/backend/latest.go b/backend/latest.go index 4d08d0d..40a36aa 100644 --- a/backend/latest.go +++ b/backend/latest.go @@ -148,6 +148,13 @@ func (p *latestPoller) checkOne(ctx context.Context, b Bookmark) { // Re-read: the row may have been updated or deleted while the fetch was in // flight, and writing b back wholesale would undo that. + // + // ponytail: non-transactional read-modify-write, wrap Get+Upsert in a tx if + // this ever runs for more than one user. A client PUT that commits between + // these two statements is lost to the stale re-read — reverting read + // progress or a status change, and moving updated_at because the stored + // value now differs. Accepted for a single-user deployment: the window is + // milliseconds and the loser is one poll cycle. cur, found, err := p.store.Get(b.Key) if err != nil { log.Printf("latest poll %q: reread: %v", b.Key, err) diff --git a/backend/web.go b/backend/web.go index 7a5487d..61f5840 100644 --- a/backend/web.go +++ b/backend/web.go @@ -262,6 +262,13 @@ func (h *webHandler) loadForMutation(w http.ResponseWriter, r *http.Request) (Bo // saveAndRenderCard upserts and renders the row as stored. Upsert decides // whether updated_at moves, so the argument's timestamp is only a candidate and // the response must come from the return value. +// +// ponytail: the swapped card stays put even when its new status no longer +// matches the active tab, add an hx-swap-oob list refresh if that reads as a +// bug rather than as feedback. Archiving from the All tab leaves the card on +// screen until the next list load. The alternative costs a full list round +// trip on every toggle, and the card visibly showing its new state is the +// feedback the user needs. func (h *webHandler) saveAndRenderCard(w http.ResponseWriter, b Bookmark) { stored, err := h.store.Upsert(b) if err != nil {