Files
fuj-management/docs/plans/2026-05-08-1334-go-m6-6-action-pages.md
Jan Novak fe935235e8
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
feat(go): M6.6 — /qr, /sync-bank, /flush-cache, /version pages
- 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>
2026-05-08 14:26:54 +02:00

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.md per the repo's CLAUDE.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 level qrcode.Medium, size 256.

Scope per endpoint

1. GET /qr — PNG only

Match Python (app.py:321-356) verbatim semantics:

  1. Read account (default cfg.BankAccount), amount (default "0"), message (default "").
  2. Validate account against ^[A-Z]{2}\d{2,34}$|^\d{1,16}/\d{4}$; on miss, silently fall back to cfg.BankAccount.
  3. Parse amount as float64; clamp to [0, 10_000_000]; format %.2f; on parse error → "0.00".
  4. Truncate message to 60 runes; strip all *.
  5. If account contains /, split into {number}*BC:{bankcode}; else use raw.
  6. SPD payload: SPD*1.0*ACC:{acc}*AM:{amount}*CC:CZK*MSG:{msg} (no BC: key when IBAN — Python doesn't either).
  7. qrcode.Encode(payload, qrcode.Medium, 256) → write bytes with Content-Type: image/png.

No template, no HTML.

2. GET /sync-bank — triggers + renders

Match Python (app.py:124-151):

  1. Build a *bytes.Buffer to capture step output.
  2. 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".
  3. On any error: append err.Error() + a stack trace via runtime/debug.Stack(), set success=false, continue rendering.
  4. Render sync template with SyncPageData{ 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 → render flush_cache with Flushed=false.
  • POST → call cacheFlusher.Flush(), render with Flushed=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

Modified files

  • go/go.mod / go.sum — add github.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. Widen Run(...) signature to accept fio.Client, sheets.Client, and a cache.Flusher — these flow into a new Deps struct on HTMLHandler.
  • go/internal/web/html_handler.go — add ServeQR, real ServeSync, split ServeFlushCacheGET / ServeFlushCachePOST, add ServeVersion (delegates to api package).
  • go/internal/web/render.go — add SyncPageData{ PageData; Output string; Success bool } and FlushPageData{ PageData; Flushed bool; Deleted int } view-models. (pageNames already 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 CacheFlusher interface (Flush() (int, error)); have Sources aggregate include it. Alternative considered: pass *cache.FileCache straight into web.Run. Choose whichever needs fewer test-stub edits — likely the direct-pass approach, since stubs in tests don't need cache behavior. Decision: pass cache.Flusher directly into web.Run as a separate dependency, not through Sources. Simpler, fewer interface changes.
  • go/internal/services/banksync/sync.go + infer.go — accept an optional io.Writer (in SyncOpts.Logger / InferOpts.Logger) and route progress prints through it; default to os.Stdout when nil for CLI parity.
  • go/cmd/fuj/main.go — in the server subcommand, build fio.Client, sheets.Client, and cache.Flusher (already constructed inside membership.NewSources — expose it or construct a sibling) and pass them into web.Run.

Reuse (already in the codebase)

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 -10.00; amount 999999999999999.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" and Flushed.
  • 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 — assert application/json, body {"tag":"v0","commit":"abc1234","build_date":"2026-01-01"}.
  • Extend the TestHTMLHandlerSmoke table to cover /qr and /version.

Verification (end-to-end)

  1. cd go && go build ./... && go test ./...
  2. cd go && go run ./cmd/fuj server — open http://localhost:8080
  3. Smoke:
    • /adults → click Pay next 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 Run signature change).
  4. Run the parity diff: go run ./cmd/parity/api/version must still match Python.
  5. Append a CHANGELOG entry per CLAUDE.md; tick M6.6 in 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-bank and /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.