- GET /qr: Czech QR Platba PNG; ports Python qr_code() exactly
(account validation, amount clamping, * stripping, SPD format)
- GET /sync-bank: Fio sync → infer → cache flush with captured log
- GET+POST /flush-cache: form + action, shows deleted count
- GET /version: JSON alias of /api/version (Python parity)
- FlushCache() added to membership.Sources; wired through api.Handler
- web.ActionHandlers{BankSync} closure-based dep injection for sync
- New dep: github.com/skip2/go-qrcode
- TestQRBuildSPD (9 cases), TestServeQR, TestServeFlushCache{GET,POST},
TestServeSync, TestServeVersion added
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
11 KiB
M6.6 — /qr, /sync-bank, /flush-cache, /version (Go parity)
On approval, relocate this file to
docs/plans/YYYY-MM-DD-HHMM-go-m6-6-action-pages.mdper the repo'sCLAUDE.md"Plans" convention before starting work.
Context
The Go-rewrite progress tracker (2026-05-03-2349-go-backend-rewrite-progress.md:118) lists M6.6 as:
/qr, /sync-bank, /flush-cache, /version pages
After M6.4 (payments page) and M6.5 (member-detail modal JS), the only remaining pre-deployment HTML/action endpoints in Python that the Go binary doesn't yet implement are these four. The adults/juniors templates already produce /qr?... URLs via the qrHref / qrHrefAll helpers (render.go:47-71) — the matching handler is the missing piece. sync and flush_cache templates exist as "Coming in M6.6" stubs and need real markup + working handlers. /api/version already serves build metadata; /version just needs to alias it (Python parity).
After M6.6 the M6 gate ("Browser smoke on :8080: all pages render … QR loads, sync/flush work end-to-end") is reachable.
Confirmed design choices
/sync-bank: Python-faithful — GET both renders and triggers the sync, captures stdout/stderr into a log buffer, surfaces success/failure via a flag. Matches existing nav[Sync Bank Data]link./version: JSON alias of/api/version. No HTML page.- QR library:
github.com/skip2/go-qrcode. Default levelqrcode.Medium, size 256.
Scope per endpoint
1. GET /qr — PNG only
Match Python (app.py:321-356) verbatim semantics:
- Read
account(defaultcfg.BankAccount),amount(default"0"),message(default""). - Validate account against
^[A-Z]{2}\d{2,34}$|^\d{1,16}/\d{4}$; on miss, silently fall back tocfg.BankAccount. - Parse amount as float64; clamp to
[0, 10_000_000]; format%.2f; on parse error →"0.00". - Truncate message to 60 runes; strip all
*. - If account contains
/, split into{number}*BC:{bankcode}; else use raw. - SPD payload:
SPD*1.0*ACC:{acc}*AM:{amount}*CC:CZK*MSG:{msg}(noBC:key when IBAN — Python doesn't either). qrcode.Encode(payload, qrcode.Medium, 256)→ write bytes withContent-Type: image/png.
No template, no HTML.
2. GET /sync-bank — triggers + renders
Match Python (app.py:124-151):
- Build a
*bytes.Bufferto capture step output. - For each step write a header line then call:
banksync.SyncToSheets(ctx, cfg.PaymentsSheetID, fioClient, sheetsClient, banksync.SyncOpts{From: jan1ThisYear, To: dec31ThisYear, Sort: true})banksync.InferPayments(ctx, cfg.PaymentsSheetID, sheetsClient, sources, banksync.InferOpts{})cacheFlusher.Flush()— append"%d cache files deleted".
- On any error: append
err.Error()+ a stack trace viaruntime/debug.Stack(), setsuccess=false, continue rendering. - Render
synctemplate withSyncPageData{ PageData; Output string; Success bool }.
The banksync package's stdout-printing functions can be redirected by passing an io.Writer — confirm during implementation; if they only print to os.Stdout, capture by tee'ing through a slog handler or briefly swapping os.Stdout (or extend the package with a *Writer option). Preferred: add a Logger io.Writer field to banksync.SyncOpts / InferOpts. This is a minor extension to existing services, not a rewrite.
3. /flush-cache — GET form, POST action
Match Python (app.py:117-122):
GET→ renderflush_cachewithFlushed=false.POST→ callcacheFlusher.Flush(), render withFlushed=true, Deleted=n.
4. GET /version — JSON
One-liner: register the existing api.VersionHandler (or its inline equivalent in api/version.go) on the /version path as well as /api/version. Output: {"tag", "commit", "build_date"}.
Files to create / modify
New files
- go/internal/web/templates/qr.tmpl — not actually needed; remove from plan if
/qris PNG-only. (Decision: skip — no template.) - Replace stub go/internal/web/templates/sync.tmpl with full markup: title, status banner colored by
Success,<pre>ofOutput, "Run again" link. - Replace stub go/internal/web/templates/flush_cache.tmpl with: form (
POST /flush-cache→<button>[Flush Cache]</button>), success banner whenFlushed.
Modified files
- go/go.mod /
go.sum— addgithub.com/skip2/go-qrcode. - go/internal/web/server.go — register
GET /qr,GET /sync-bank(replace stub),GET /flush-cache(form),POST /flush-cache(action),GET /version. WidenRun(...)signature to acceptfio.Client,sheets.Client, and acache.Flusher— these flow into a newDepsstruct onHTMLHandler. - go/internal/web/html_handler.go — add
ServeQR, realServeSync, splitServeFlushCacheGET/ServeFlushCachePOST, addServeVersion(delegates to api package). - go/internal/web/render.go — add
SyncPageData{ PageData; Output string; Success bool }andFlushPageData{ PageData; Flushed bool; Deleted int }view-models. (pageNamesalready includes"sync"and"flush_cache"; no change.) - go/internal/web/qr.go (new file) — pure helper:
BuildSPD(account, amount, message string, defaultAccount string) (payload string)+RenderQRCode(payload string) ([]byte, error). Easier to unit-test than a method on the handler. - go/internal/services/membership/loader.go — add a small
CacheFlusherinterface (Flush() (int, error)); haveSourcesaggregate include it. Alternative considered: pass*cache.FileCachestraight intoweb.Run. Choose whichever needs fewer test-stub edits — likely the direct-pass approach, since stubs in tests don't need cache behavior. Decision: passcache.Flusherdirectly intoweb.Runas a separate dependency, not throughSources. Simpler, fewer interface changes. - go/internal/services/banksync/sync.go +
infer.go— accept an optionalio.Writer(inSyncOpts.Logger/InferOpts.Logger) and route progress prints through it; default toos.Stdoutwhen nil for CLI parity. - go/cmd/fuj/main.go — in the
serversubcommand, buildfio.Client,sheets.Client, andcache.Flusher(already constructed insidemembership.NewSources— expose it or construct a sibling) and pass them intoweb.Run.
Reuse (already in the codebase)
- Bank sync:
banksync.SyncToSheets(go/internal/services/banksync/sync.go) andbanksync.InferPayments(go/internal/services/banksync/infer.go) — same calls the CLIsync/infersubcommands use (go/cmd/fuj/main.go:184, :222). - Cache flush:
(*cache.FileCache).Flush() (int, error)(go/internal/io/cache/filecache.go:92) — already returns the deleted count. - Build metadata:
BuildInfo{Version, Commit, BuildDate}(go/internal/web/server.go:28-35) is already plumbed everywhere;/api/versionJSON shape is already correct (go/internal/web/api/version.go). - Template render pattern:
Renderer.Render(w, name, data)(go/internal/web/render.go:98-108). - Test scaffolding:
web.NewRenderer()+httptest.NewRequest/NewRecorder(go/internal/web/html_handler_test.go:52-94).
Tests
Add to go/internal/web/html_handler_test.go (and one new qr_test.go for the SPD builder):
TestQRBuildSPD(unit, in go/internal/web/qr_test.go) — table-driven: invalid account → fallback;/-account →{num}*BC:{bc}; IBAN → raw; amount-1→0.00; amount99999999→9999999.00; amount"abc"→0.00; message with*stripped; message length-clamped to 60.TestServeQR— request/qr?account=…&amount=10&message=Hi; assert 200,Content-Type: image/png, body starts with PNG magic\x89PNG\r\n\x1a\n.TestServeFlushCacheGet— assert form rendered, no banner.TestServeFlushCachePost— fixture flusher returns 3; assert banner says "3" andFlushed.TestServeSync— fixture banksync stubs return(2, nil)and(1, nil), fixture flusher returns 4; assert page renders, status is success, output contains the section headers.TestServeVersion— assertapplication/json, body{"tag":"v0","commit":"abc1234","build_date":"2026-01-01"}.- Extend the
TestHTMLHandlerSmoketable to cover/qrand/version.
Verification (end-to-end)
cd go && go build ./... && go test ./...cd go && go run ./cmd/fuj server— open http://localhost:8080- Smoke:
- /adults → click
Paynext to any unpaid month → modal shows QR image (this exercises/qr). Confirm a Czech banking app can scan it (visual check). - Direct
/qr?account=2702008874/2010&amount=700&message=Test→ PNG renders. - /sync-bank → click
[Sync Bank Data]in nav → page shows "Syncing…", then sections + "Inferred Payments", "Cache Flushed", green status banner. - /flush-cache → click
[Flush Cache]→ success banner with file count. - /version in browser → JSON visible.
- Adults / juniors / payments still render (no regressions from
Runsignature change).
- /adults → click
- Run the parity diff:
go run ./cmd/parity—/api/versionmust still match Python. - Append a CHANGELOG entry per
CLAUDE.md; tickM6.6in docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md:118.
Branch / MR
Per CLAUDE.md workflow: feat/go-m6-6-action-pages off main, push with -u, open with tea pr create --base main --head feat/go-m6-6-action-pages.
Out of scope (deferred)
- Auth / CSRF protection on
/sync-bankand/flush-cache— Python has none either; deferred to M8 hardening. - M6.7 (single-binary embed verification) — separate ticket.
- Streaming progress for
/sync-bank(currently buffered until completion, like Python). - Configurable QR error-correction level / size.