Replace bare <a href=/qr> Pay buttons with <button data-*> elements that
open an in-page #qrModal (matching Python's showPayQR UX), driven by a
new payment-qr.js vanilla-JS IIFE module. Remove the now-dead qrHref /
qrHrefAll template helpers from render.go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds static/js/member-detail.js: fetches /api/<page> once on page load,
caches the response, and renders a per-member detail modal on [i] row click.
Keyboard nav: Esc closes, ↑/↓ walk visible (filtered) rows. All modal CSS
was already in place from M6.1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extract AssemblePayments(ctx) from ServePayments in api/handler.go,
mirroring the AssembleAdults/AssembleJuniors pattern
- Add PaymentsPageData view-model wrapper in render.go
- Rewire html_handler.go ServePayments to call AssemblePayments and
render with PaymentsPageData
- Replace payments.tmpl placeholder with real grouped-by-person ledger:
alphabetical member blocks, txn-table (Date/Amount/Purpose/Message),
newest-first rows, Unmatched/Unknown bucket
- Append ledger CSS classes to app.css (.ledger-container, .member-block,
.txn-table, .txn-date/amount/purpose/message, tr:hover)
- Add TestPaymentsPage markup test
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extract AssembleJuniors(ctx) from ServeJuniors JSON handler so HTML
and JSON share the same view-model path (mirrors AssembleAdults pattern)
- Add JuniorsPageData wrapper in render.go
- Wire HTMLHandler.ServeJuniors to AssembleJuniors + render template
- Replace 4-line placeholder juniors.tmpl with full template:
member table, name filter, month-range filter, totals row,
Credits + Debts sections, Pay / Pay All buttons via /qr links
(no Unmatched section — matches Python juniors.html parity)
- J/A attendance breakdown ("3/500 CZK (4:2J,1A)") and "?" sentinel
rendered via MonthCell.Text from buildJuniorMemberRow, no extra
template logic needed
- All tests pass; make parity reports 3/3 routes OK
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two regressions made older periods invisible on the adults dashboard:
- 1257f0d (Mar 9) commented out ADULT_MERGED_MONTHS, removing the
Sep+Oct 2025 merged label. Restored only the 2025-09 → 2025-10
mapping (Dec and Jan are billed separately for adults; the
Dec → Jan mapping stays disabled per product decision). Mirrored
on the Go side. Test fixtures in sources_test.go now assert Sep
dates land in merged 2025-10 instead of 2025-09.
- 7774301 (Apr 9) added a JS onload default that set the From
selector to maxMonthIdx − 4 and immediately filtered the table,
hiding everything older than 5 months on first load. Dropped that
default in templates/adults.html and templates/juniors.html so
the From-selector starts at the oldest available month. Future
months are still removed from the dropdowns and hidden in the
table — only the past-month truncation is gone.
Note: the live adults attendance sheet had also been pruned to
start at 02.12.2025; restoring Sep/Oct/Nov 2025 columns from
Sheets version history is required to actually see those periods.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extract AssembleAdults(ctx) from ServeAdults so HTML and JSON API share one reconcile path.
- HTMLHandler gains *api.Handler; ServeAdults loads real data and renders adults.tmpl.
- AdultsPageData view model + qrHref/qrHrefAll funcMap (URL-encode /qr params, YYYY-MM→MM/YYYY).
- adults.tmpl: full reconcile table, per-cell status classes + cell-unpaid-current, Pay button hrefs,
totals row, credits/debts/unmatched sections, filter controls, sheet links.
- static/js/filters.js: NFD-normalize name filter + month-range column hiding; future months hidden by default.
- TestAdultsPage asserts member name and cell text against fixture data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stand up the Go-native HTML frontend foundation:
- base.tmpl layout + nav/footer partials (three-tier nav, active-link highlighting)
- terminal-green-on-black theme extracted to static/css/app.css (served via embed.FS)
- HTMLHandler with stub pages for all five routes; / redirects to /adults
- NewRenderer parses per-page template sets at startup so parse failures abort boot
- Smoke test: each route returns 200 text/html with exactly one class="active" link
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/api/version returns each binary's own tag/commit/build_date, which
differs by design between independently built backends. Diffing it
always produces a false positive. Drop it from allRoutes; the route
remains reachable via `make parity ARGS="-route /api/version"`.
Also remove the vestigial `build_meta` allowlist entry (Python returns
the build dict as the top-level response body, not nested under
build_meta, so the scrubber never matched anything).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds cmd/parity/main.go: a standalone Go binary that GETs
/api/version, /api/adults, /api/juniors, /api/payments from both
the Python (:5001) and Go (:8080) backends, scrubs an allowlist
(render_time.total, build_meta), and prints cmp.Diff for any
remaining differences. Exits 0 on full match, 1 on diffs, 2 on
fetch/parse errors — CI-friendly for M7.2.
- go/cmd/parity/main.go: flags (-py, -go, -route, -timeout), fetch
helper, allowlist scrubber (dotted-path aware), exit-code logic.
- go/cmd/parity/scrub_test.go: 4 unit tests for the scrubber.
- go/go.mod: promote github.com/google/go-cmp to direct dep.
- Makefile: parity target + help entry.
- Progress tracker: M5.4 ticked; milestone updated to M5 complete.
- Plan archived to docs/plans/2026-05-07-2254-m5-4-parity-binary.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add internal/web/api package with Go structs for every /api/X route:
AdultsResponse, JuniorsResponse, PaymentsResponse, VersionResponse.
All fields carry explicit json: tags matching the Python view-model keys.
Key design choices:
- member_data / month_labels / raw_payments are nested objects (not
the pre-serialised JSON strings used in Jinja templates)
- Expected{Value int; Unknown bool} with custom MarshalJSON emits int
or the string "?" for junior single-attendance months
- RawTransaction covers the full 11-column payments sheet row
schemagen_test.go reflects all four response types via
github.com/invopop/jsonschema and golden-compares against committed
schemas in tests/fixtures/api-schema/. The JSONSchema() method on
Expected lives in the test file so the prod binary has no jsonschema
dependency.
Closes M5.1 in docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pull 350+ lines of inline per-row computation out of adults_view,
juniors_view, and payments into three pure builder functions with no
Flask globals or IO dependencies. Route handlers now contain only
cache/IO calls and a single render_template. No behaviour change —
all 27 tests pass.
Also moves get_month_labels, group_payments_by_person, and
adapt_junior_members out of app.py. Prep for /api/* shadow endpoints
(M5 Go parity).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prints an aligned tabwriter table of every Fio transaction in the
look-back window, with a STATUS column showing NEW (would be appended)
or DUP (already in sheet). Only fires when --dry-run is also set, so
it can't affect real syncs. Refactors Sync ID computation into a single
pre-pass shared by both the table printer and the row builder.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror fuj infer's read-only mode: SyncOpts.DryRun skips WriteHeader,
AppendValues, and SortByDateColumn, printing one "Dry run: would …"
line per planned operation instead. ID-dedup still runs so the output
reflects exactly what the next real sync would write.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes M3.1–M3.6. Parity safety net proving Go output matches Python
for every ported pure-domain function (M2.1–M2.9) and reconcile (M2.10).
Capture pipeline:
- scripts/capture_fixtures.py: calls each Python function with seeded
inputs, emits JSON fixtures to stdout (never writes files directly).
- scripts/scrub_fixtures.py: deterministic PII scrubber — SHA-256
pseudonyms for member names, digit-preserving hashes for VS/account/
bank_id, name-sweep in message text. Idempotent; no salt.
- scripts/_fixture_seeds.py: handcrafted seeds for all 11 functions;
synthetic names throughout (no real roster members).
- scripts/capture_all_fixtures.sh: convenience wrapper for full corpus
regeneration outside of make.
Fixture corpus (98 files, all PII-free):
- go/tests/fixtures/pure/<func>/<case>.json — 10 function directories.
- go/tests/fixtures/reconcile/<NN>_<case>.json — 10 branch-coverage
cases: greedy, overpayment credit, proportional remainder, even-split,
out-of-window, exception override, other: purpose, junior ?, multi-
person+month fan-out, unmatched.
Go parity tests (//go:build parity):
- go/tests/parity/parityio.go: generic LoadDir/RunAll helpers + typed
In/Out struct pairs for all 10 pure functions; Envelope decoder for
int/float/none disambiguation.
- 10 pure-function test packages + bespoke reconcile test with per-cell
float tolerance (math.Abs <= 0.01 for `paid` values).
Makefile: go-parity, go-test-all, capture-fixtures targets.
go/tests/fixtures/README.md: refresh workflow + PII audit guide.
Gate: make go-test green, make go-parity green (11/11 packages),
make go-lint clean (parity tag), make go-build clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add internal/services/membership package: AttendanceLoader,
TransactionLoader, ExceptionLoader interfaces + NewStubSources stub
(returns ErrIOPending until M4 lands real Sheets loaders).
FeesReport and ReconcileReport orchestrate domain/fees + domain/reconcile
and write fixed-width text reports matching Python calculate_fees.py and
match_payments.py print_report output. 13 unit tests cover all formatter
branches and orchestration wiring via fake loaders.
cmd/fuj/main.go: fees and reconcile subcommands now dispatch; sync/infer
retain the [M4] placeholder.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
infer_payments was building member_names from get_members_with_fees()
(adults sheet only). Junior-only members were invisible to the matcher,
so a payment message containing an exact junior name would produce a
fuzzy review match against a different adult sharing the same first name.
Fix: union the adult and junior rosters (deduped via canonical_member_key)
so all members are candidates. The existing exact-name short-circuit in
match_members then handles precedence correctly.
Two regression tests added for the Jáchym Kubík / Jáchym Hrušák case.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New go/internal/domain/matching package porting three helpers from
scripts/match_payments.py:
- BuildNameVariants: normalized ASCII variants from a member name (nickname
in parens, last/first split, len<3 filtered); variants[0] is always the
full base name — MatchMembers relies on this invariant.
- MatchMembers: auto/review confidence matching with an exact-name
short-circuit pass that prevents nickname substrings (tov) from firing
inside longer surnames (ottova); common-surname filter for review tier.
- FormatDate: nil/empty/""/serial int/float64 (since 1899-12-30, fractional
days supported)/YYYY-MM-DD passthrough/garbage → never errors.
- InferTransactionDetails: composes BuildNameVariants+MatchMembers+
ParseMonthReferences; falls back to sender-only member match and
date-derived month when text carries no signal.
21 table-driven tests; all expected values verified against live Python
on 2026-05-06. go-build, go-test, go-lint all clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
SHA-256 dedup hash from sync_fio_to_sheets.py generate_sync_id.
Key subtlety: Python str(float) emits "500.0" for whole-valued floats
and switches to scientific notation at |f|>=1e16 or |f|<1e-4 —
replicated via formatAmount using 'f'/'e' format selection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Port scripts/infer_payments.py parse_czk_amount to Go as
internal/domain/money.ParseCZK. Preserves the Czech-locale heuristic
(comma = decimal sep; 2+ dots = thousand seps; single dot = decimal)
and returns (float64, error) so callers can opt into Python's
silent-zero contract via v, _ := money.ParseCZK(s).
All expected values verified against live Python on 2026-05-06.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three-pass regex parser matching python/czech_utils.py parse_month_references:
1. Numeric slash notation — "11+12/2025", "01/26"; 2-digit year → +2000
2. Dot notation — "12.2025" (4-digit year only)
3. Czech month names — range walk (listopad-leden wrap logic) then
standalone with m≥10 → defaultYear-1 heuristic; longest-match
alternation (sorted desc by name length) handles cervenec vs cerven
35 table-driven tests, all expected outputs verified against live Python
on 2026-05-05 before locking. Plan at
docs/plans/2026-05-05-2337-go-rewrite-m2-2-parse-month-references.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds internal/domain/czech.Normalize, the first pure-domain function in
the Go rewrite (M2 milestone). Matches Python czech_utils.normalize byte-
for-byte: NFKD decompose via golang.org/x/text/unicode/norm, drop Mn-
category combining marks (unicode.Mn, not IsMark, to match Python's
unicodedata.combining() semantics), then strings.ToLower.
Includes 13-case table-driven test; all inputs spot-checked against the
Python implementation before locking. Adds golang.org/x/text v0.36.0 as
first external dependency.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Feature work now goes on feat/<slug> branches; Claude pushes and prints
the Gitea compare URL for the user to open the MR. Exceptions documented
for small fixes and typo tweaks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add canonical_member_key() in match_payments.py to normalize names via
NFKD + lowercase + whitespace-collapse before ledger lookup; resolves
payments attributed to e.g. "Maria Maco" to canonical "Mária Maco".
Emits logger.info when a non-canonical cell is rescued so sheet typos
are visible in logs without losing the payment allocation.
- Extend group_payments_by_person() in app.py to accept member_names and
re-key raw-payment groups under the canonical attendance-sheet name so
the modal's Raw Payments debug section also finds the row correctly.
- Add raw payments collapsible section to member detail modal in adults.html
and juniors.html for debugging payment attribution issues.
- Remove 4 obsolete tests targeting routes /fees, /fees-juniors, /reconcile,
/reconcile-juniors that no longer exist; add test_match_payments.py
covering canonical key equivalence and reconcile() tolerance end-to-end.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
match_members() now short-circuits on whole-word full-name hits and
uses word-boundary regex everywhere else, so a nickname that is a
substring of another member's surname (e.g. "tov" inside "ottova")
no longer produces false positives. Adds tests/test_match_members.py.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>