Files
fuj-management/CHANGELOG.md
kacerrandClaude Opus 5.5 5b162d0030 fix(ci): remove kubernetes-deploy workflow that leaked secrets to logs
The workflow ran on every push to every branch and deployed nothing.
It echoed the raw Vault login response, the Kanidm API token response
and the OIDC token response to the job log, dumped the environment,
and configured kubectl with --insecure-skip-tls-verify.

Deployment is handled by gitops-update.yaml. Docs updated to describe
that flow; T1 marked done in the cleanup task list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:48:35 +02:00

36 KiB
Raw Permalink Blame History

Changelog

2026-10-08 11:48 CEST — fix(ci): remove kubernetes-deploy workflow that leaked secrets to logs

  • Deleted .gitea/workflows/kubernetes-deploy.yaml. It ran on every push to every branch, deployed nothing, and echoed the Vault login response, the Kanidm API token response and the OIDC token response to the job log.
  • Deployment is handled by gitops-update.yaml. Repo secrets VAULT_ROLE_ID, VAULT_SECRET_ID and K8S_CA_CERT are now unused.
  • Follow-up outside the repo: rotate the Vault AppRole secret and the Kanidm API token, and clear old job logs.

2026-10-08 11:34 CEST — fix(python): return months_to_show from /api/adults and /api/juniors

  • Go already returned months_to_show in the JSON API; Python only passed it to its HTML templates. Backported the key so the API contract matches and make parity reports 3/3 routes.
  • Key files: legacy/app.py, legacy/tests/test_app.py.

2026-10-08 11:34 CEST — fix: VS is always a string, on read and on write

  • Go rendered numeric sheet cells with fmt.Sprint, so a numeric VS came out as 2.609767878e+09. Text columns of the payments sheet now go through sheets.CellString (plain digits); bank inference uses it too.
  • Both sync implementations now append VS as a text cell (leading apostrophe under USER_ENTERED), so Sheets no longer stores it as a number or drops leading zeros. Existing rows are unchanged.
  • Key files: go/internal/io/sheets/cell.go, go/internal/services/membership/sources.go, go/internal/services/banksync/{sync,infer}.go, legacy/scripts/sync_fio_to_sheets.py.

2026-10-08 11:33 CEST — refactor: move Python implementation to legacy/, Go is primary

  • Go (go/) is now the primary implementation. The original Python version moved to legacy/ (app.py, scripts/, templates/, tests/, pyproject.toml, uv.lock, build/) and is considered legacy; new features are still built in both.
  • CLAUDE.md rewritten for the Go-first layout, with a "Dual implementation rule" section.
  • Makefile Python targets keep their names and point at legacy/; virtualenv is legacy/.venv, Python cache is legacy/tmp/; CREDENTIALS_PATH is exported pointing at the root .secret/ file.
  • CI builds the Python image with legacy/ as context; the image no longer contains the Makefile. legacy/.dockerignore now takes effect and excludes nested __pycache__.
  • Go comments, fixture README, capture script and VS Code launch config updated to the new paths.

2026-06-12 19:32 CEST — feat(ci): gitops image-update PR workflow

  • Added .gitea/workflows/gitops-update.yaml: after each successful Go image build, uh-cli gitops deployment update opens a PR in kacerr/home-kubernetes bumping the fuj-management Deployment (namespace fuj) to the new image tag.
  • Supports workflow_run auto-trigger and workflow_dispatch with dry_run / uh_cli_version inputs.
  • Requires GITOPS_TOKEN repo secret (Gitea PAT with write+PR access to home-kubernetes).

2026-05-24 21:58 CEST — feat(fees): update adult monthly rates for 2026-05 through 2026-08

  • 2026-05: 700 → 450 CZK; 2026-06/07/08: 600 CZK (new months added).
  • Mirrored in both scripts/attendance.py and go/internal/domain/fees/fees.go.

2026-05-24 21:42 CEST — feat: multi-account Fio sync + switch QR default to 2502035405/2010

  • Added second bank account 2502035405/2010 (IBAN CZ0820100000002502035405) to sync.
  • Both accounts are fetched on every sync; dedup by existing sync_id keeps the payments sheet clean.
  • QR codes now default to the new account (CZ0820100000002502035405).
  • Go: config.go gains hardcoded Accounts/LoadedAccount slice; Config.BankAccount renamed to Config.QRAccount; FioAPIToken removed (tokens are per-account via FIO_API_TOKEN_NEW / FIO_API_TOKEN_OLD).
  • Go: SyncToSheets now accepts []fio.Client; new TestSyncToSheets_MultiAccount test.
  • Python: config.py gains ACCOUNTS / LOADED_ACCOUNTS; fio_utils.py adds fetch_transactions_for and fetch_transactions_all; sync_fio_to_sheets.py uses fetch_transactions_all.
  • Key files: go/internal/config/config.go, go/internal/services/banksync/sync.go, go/cmd/fuj/main.go, scripts/config.py, scripts/fio_utils.py, scripts/sync_fio_to_sheets.py.

2026-05-11 23:58 CEST — fix(reconcile): fill earliest month deficit first in multi-month allocations

  • Multi-month payment allocation now fills the earliest in-window deficit first and spills any remainder to later months, accounting for prior transactions' contributions to each month. Previously a single transaction was split proportionally to each month's total expected fee, ignoring what earlier transactions had already paid — surfaced by Matyáš Thér's 200+550 case showing 566/183 instead of 500/250.
  • Files: scripts/match_payments.py, go/internal/domain/reconcile/reconcile.go, tests, parity fixtures.

2026-05-11 22:56 CEST — fix(python): parse Fio 2-digit-year dates + add make sync-debug dry-run tool

  • Fix: scripts/fio_utils.py parse_czech_date now accepts DD.MM.YY / D.M.YY in addition to the 4-digit-year variants. Fio's transparent page now mixes both forms in the same response; the 2-digit rows were being silently dropped, which caused make sync-2026 to miss every recent transfer. Mirrors the Go-side fix from 2026-05-07 (CHANGELOG entry below).
  • Added --dry-run and --print-fio-table flags to scripts/sync_fio_to_sheets.py, plus a make sync-debug [DAYS=N] Makefile target. Mirrors make go-sync-debug: fetches from Fio and dedupes against the sheet, prints STATUS=NEW/DUP per transaction, and prints per-row Dry run: would append … lines + would sort by date instead of touching the sheet.
  • Added always-on stderr diagnostics in scripts/fio_utils.py: which fetcher was selected (authenticated API vs. transparent-page scraper with FIO_API_TOKEN-unset warning), and raw-vs-after-filter transaction counts on both paths — so this class of "scraper drops everything" bug surfaces immediately.

2026-05-08 15:24 CEST — feat(go): M6.7 — single-binary embed verification

  • Confirmed embed.FS wiring is complete: templates parsed via template.ParseFS(templateFS, ...), static assets served via http.FileServerFS(fs.Sub(staticFS, "static")).
  • Added go/internal/web/assets_test.go with two tests: TestEmbedCompleteness (walks disk vs embed.FS to catch forgotten files) and TestStaticAssetsServed (hits /static/css/app.css and all JS files through the mux, asserts 200 + Content-Type + non-empty body + 404 for unknown paths).
  • Closes M6; single binary confirmed self-contained with no adjacent templates/ or static/ required at runtime.
  • Key files: go/internal/web/assets_test.go (new).

2026-05-08 14:55 CEST — feat(go): M6.6.1 — Pay-button QR popup modal

  • Restored the Python showPayQR in-page modal UX that was lost in M6.6 (Pay buttons were navigating the tab to the raw /qr PNG).
  • Replaced <a href="{{qrHref ...}}">Pay</a> with <button data-name|amount|month|raw-month> on /adults and /juniors; click is handled by a new static/js/payment-qr.js IIFE module that opens #qrModal with title, account, amount, message, and the QR image.
  • Added #qrModal markup to both templates; CSS display:none / .active{display:flex} rules added (content rules were already present from M6.1). Esc, [close], and outside-click all dismiss; coexists with the M6.5 member-detail modal.
  • Removed the now-dead qrHref / qrHrefAll template helpers from render.go.
  • Markup tests in html_handler_test.go assert modal IDs, script tag, data-bank-account, and that no bare href="/qr" links remain.
  • Key files: go/internal/web/static/js/payment-qr.js, go/internal/web/templates/adults.tmpl, go/internal/web/templates/juniors.tmpl, go/internal/web/render.go, go/internal/web/static/css/app.css.

2026-05-08 13:57 CEST — feat(go): M6.6 — /qr, /sync-bank, /flush-cache, /version

  • Added GET /qr: generates Czech QR Platba PNG from SPD payload (account, amount, message query params); ports Python's qr_code() handler exactly including account validation, amount clamping, and * stripping.
  • Implemented GET /sync-bank: runs Fio sync → infer payments → cache flush, captures output into sync.tmpl page with success/error banner.
  • Implemented GET /flush-cache + POST /flush-cache: form + action that deletes cache files and shows deleted count.
  • Added GET /version as a JSON alias of GET /api/version (Python parity).
  • Added FlushCache() (int, error) to membership.Sources interface; implemented on realSources via cache.FileCache.Flush().
  • Introduced web.ActionHandlers{BankSync} — closure-based dep injection for sync, constructed in serverCmd with fio + sheets clients.
  • New dependency: github.com/skip2/go-qrcode.
  • Key files: go/internal/web/qr.go, go/internal/web/html_handler.go, go/internal/web/server.go, go/internal/services/membership/loader.go, go/internal/web/templates/sync.tmpl, go/internal/web/templates/flush_cache.tmpl.

2026-05-08 13:19 CEST — feat(go): M6.5 — member-detail modal JS module

  • Added static/js/member-detail.js: fetches /api/adults or /api/juniors once on page load, caches the response, renders a per-member detail modal on [i] row click.
  • Modal sections: status-per-month table, fee exceptions, other transactions, matched payment history, toggleable raw-payments debug view.
  • Keyboard nav: Esc closes, ↑/↓ walk visible (name-filtered) rows; click outside modal content closes it.
  • Added [i] info icon and #memberModal markup to adults.tmpl and juniors.tmpl; all CSS was already in place from M6.1.
  • Added TestModalMarkup assertions to html_handler_test.go.

2026-05-08 12:48 CEST — feat(go): M6.4 — Go-native /payments page

  • Extracted AssemblePayments(ctx) from the inlined JSON handler body, following the M6.2/M6.3 AssembleAdults/AssembleJuniors pattern.
  • Added PaymentsPageData to render.go; wired HTMLHandler.ServePayments to call AssemblePayments and render the new template.
  • Replaced the "Coming in M6.4" placeholder in payments.tmpl with the full grouped-by-person ledger: alphabetical <h2> member blocks, each with a txn-table (Date / Amount / Purpose / Bank Message), newest-first rows, "Unmatched / Unknown" bucket.
  • Appended ledger CSS classes to app.css (.ledger-container, .member-block, .txn-table, .txn-{date,amount,purpose,message}, tr:hover).
  • Added TestPaymentsPage markup test. No filters, no JS — matches the Python page.

2026-05-08 11:25 CEST — feat(go): M6.3 — Go-native /juniors page

  • Extracted AssembleJuniors(ctx) from the JSON handler so HTML and JSON share one view-model path (mirrors AssembleAdults).
  • Added JuniorsPageData to render.go; wired HTMLHandler.ServeJuniors to call AssembleJuniors and render the new template.
  • Replaced the 4-line "Coming in M6.3" placeholder in juniors.tmpl with the full template: member table, name/month-range filters, totals row, Credits + Debts sections, Pay / Pay All links to /qr. No Unmatched section (parity with Python).
  • J/A attendance breakdown and "?" sentinel rendered via MonthCell.Text produced by buildJuniorMemberRow — no extra template logic.
  • make parity reports 3/3 routes OK; all Go tests pass.

2026-05-08 11:11 CEST — fix: period selector showed only Dec 2025+ on adults

  • Restored the 2025-09 → 2025-10 adult merge in scripts/attendance.py and go/internal/services/membership/sources.go (commented-out by 1257f0d); the 2025-12 → 2026-01 mapping stays disabled per product decision (Dec and Jan are billed separately for adults).
  • Dropped the defaultFrom = maxMonthIdx − 4 JS auto-default in templates/adults.html and templates/juniors.html (introduced by 7774301); the From-selector now starts at the oldest available month so all non-future periods render on first load. Future-month removal is preserved.
  • go/internal/services/membership/sources_test.go: TestLoadAdults / TestLoadAdults_Fee now assert that Sep dates land in the merged 2025-10 bucket.
  • Independent of these code changes: the live adults attendance Google Sheet header had been pruned to start at 02.12.2025 (Sep/Oct/Nov 2025 columns deleted); restoring those columns from Sheets version history is required to actually see those periods on the dashboard.

2026-05-08 10:15 CEST — fix(go): adults template — use lifted CSS classes for visual parity

  • Use existing .balance-pos / .balance-neg (drop invented balance-cell / balance-negative); Pay-All button now lives inside the balance cell with position: relative (matches Python; no separate trailing column).
  • Credits / Debts / Unmatched sections rewritten from <ul> / <table> to <div class="list-container"><div class="list-item">… / <div class="unmatched-row">… so the lifted CSS actually applies.
  • Section headings get the descriptive Python text: "Credits (Advance Payments / Surplus)", "Debts (Missing Payments)".
  • Source links moved from page bottom to a <div class="description"> block under the h1, matching Python's "Source: Attendance Sheet | Payments Ledger".
  • Total row uses cell-{{Status}} plus the small "received / expected" caption span and Python's inline-styled bold/dark background.
  • Drop the redundant class="cell" wrapper; debts amount turns red via inline style; balance value drops the trailing "CZK".

2026-05-08 01:09 CEST — feat(go): M6.2 — adults page (table, filters, Pay buttons)

  • go/internal/web/api/handler.go: extracted ServeAdults body into AssembleAdults(ctx) — shared by the JSON API route and the new HTML handler.
  • go/internal/web/render.go: added AdultsPageData view model (PageData + api.AdultsResponse + Error); tmplFuncs with qrHref / qrHrefAll (URL-encode QR Platba params, convert YYYY-MM → MM/YYYY).
  • go/internal/web/html_handler.go: HTMLHandler gains *api.Handler; ServeAdults loads real reconcile data and renders the full adults page.
  • go/internal/web/templates/adults.tmpl: full table (per-member rows, per-cell status classes, data-month-idx, Pay button hrefs to /qr), totals row, credits/debts/unmatched sections, filter controls, sheet links.
  • go/internal/web/static/js/filters.js: name filter (NFD-normalize) + month-range hide/show by data-month-idx; future months hidden by default.

2026-05-08 00:44 CEST — feat(go): M6.1 — template skeleton + embed.FS

  • go/internal/web/templates/: base.tmpl (full HTML layout), partials/nav.tmpl (three-tier nav with active-link highlighting), partials/footer.tmpl (build meta), and stub pages for each route (adults/juniors/payments/sync/flush_cache).
  • go/internal/web/static/css/app.css: terminal-green-on-black theme extracted once from Python templates/adults.html — shared by all Go HTML pages via <link>.
  • go/internal/web/assets.go: //go:embed templates static for single-binary deployment.
  • go/internal/web/render.go: Renderer parses a fresh *template.Template per page at startup; Render(w, name, data) executes the "base" template block.
  • go/internal/web/html_handler.go: HTMLHandler with one method per route (ServeAdults, ServeJuniors, ServePayments, ServeSync, ServeFlushCache).
  • go/internal/web/server.go: drops helloHandler; GET /{$} now redirects to /adults; HTML + /static/ routes registered alongside the existing /api/* routes.
  • go/internal/web/html_handler_test.go: smoke test — each route returns 200 text/html with exactly one class="active" on the matching nav link.

2026-05-08 00:26 CEST — fix(py): parity coercions — amount/message types + junior '?' sticky

  • scripts/match_payments.py: added get_float helper — non-numeric amount values (e.g. "---" placeholder rows) now coerce to 0.0 matching Go's parseFloat behaviour; message field now goes through get_str so numeric cell values (bank references) are emitted as strings, matching Go's fmt.Sprint.
  • scripts/views.py: junior month cell "?" text is now sticky across exception overrides. Previously reconcile replaced expected with the exception amount before the view builder ran, silently turning "?" into "-" when the override was 0. Fixed by deriving is_unknown from original_expected == "?" instead of expected == "?". Also aligned tooltip guard: only show Received/Expected for non-unknown months (or when paid > 0), matching Go's !md.IsUnknown condition.

2026-05-07 23:51 CEST — feat(py): M5.4 fix #2 — add vs and sync_id to payments tx projection

  • scripts/match_payments.py: fetch_sheet_data now reads VS and Sync ID columns and includes vs/sync_id keys in every tx dict. Previously only 9 columns were projected, causing make parity to report extra vs/sync_id fields on every raw payment row emitted by the Go backend. Values flow through group_payments_by_person → _unwrap_view_model_for_api to raw_payments (adults/juniors) and grouped_payments (payments) automatically.
  • tests/test_app.py: updated /api/* mock fixtures to include vs/sync_id keys for realism.
  • Cache note: after deploying, hit POST /flush-cache once so the in-process cache is cleared and the next request picks up the new column lookups.

2026-05-07 23:37 CEST — fix(go): accept single-digit day/month in attendance date headers

  • go/internal/services/membership/sources.go: parseDates now uses Go time formats 2.1.2006 and 1/2/2006 (single-digit reference forms, which accept both padded and unpadded inputs) instead of 02.01.2006 and 01/02/2006. The Czech attendance sheet headers contain dates like 1.6.2026, 23.3.2026, 6.4.2026 — Go silently dropped those columns under the strict zero-padded format, while Python's strptime("%d.%m.%Y") accepted them. Effect was a missing 2026-06 month entirely on /api/juniors plus undercounted attendance for any month with single-digit columns; both surfaced as diffs in make parity.
  • sources_test.go::TestParseDates_SingleDigitDayMonth added as a regression guard covering both Czech and US format flavours with and without leading zeros.

2026-05-07 23:17 CEST — fix(go): pass raw value to FormatDate so numeric serial-day dates format

  • go/internal/services/membership/sources.go: transaction-row parser now passes row[idxDate] directly to matching.FormatDate (via a new getRaw helper) instead of stringifying first via getVal. The Sheets API returns numeric serial-day values as float64 for date-formatted cells; pre-stringifying them defeated FormatDate's case float64: dispatch, causing all numeric dates to leak through as "46147" style strings instead of "2026-05-05".
  • Surfaced by make parity (M5.4): every transactions[].date field on /api/adults and /api/juniors differed between Python and Go.
  • sources_test.go::TestLoadTransactions extended with a numeric-serial-day row covering the regression.

2026-05-07 23:05 CEST — fix(go): default CacheDir to tmp/go to avoid Python collision

  • go/internal/config/config.go: CacheDir default changed from tmp to tmp/go. Override via CACHE_DIR env var still works.
  • Why: both backends used tmp/<key>_cache.json with the same keys (attendance_regular, attendance_juniors, payments_transactions, exceptions_dict) but different shapes — Python caches post-processed view-model tuples, Go caches raw rows. Whichever wrote last poisoned the cache; running both in parallel produced ValueError: too many values to unpack (expected 2, got 68) on Python's /adults after the Go server populated attendance_regular_cache.json with raw CSV rows.
  • After upgrading: stop the Go server, hit /flush-cache on the Python side once (rewrites tmp/*.json with correct shapes), then restart make web-go — it will use tmp/go/ going forward. Required for the M5.4 make parity workflow which assumes both backends run side-by-side.

2026-05-07 22:55 CEST — feat(go): M5.4 — parity diff binary + make parity

  • go/cmd/parity/main.go: new standalone binary that GETs /api/adults, /api/juniors, /api/payments from both Python (:5001) and Go (:8080) backends, scrubs an allowlist (render_time.total), 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. /api/version is excluded by design (returns binary identity — tag/commit/build_date — which differs between independently built backends); still accessible via make parity ARGS="-route /api/version".
  • go/cmd/parity/scrub_test.go: 4 unit tests covering top-level delete, nested delete, missing path, and non-map parent.
  • go/go.mod: github.com/google/go-cmp promoted to direct dependency.
  • Makefile: parity target added (.PHONY, help, cd go && go run ./cmd/parity).
  • docs/plans/2026-05-07-2254-m5-4-parity-binary.md: plan archived.

2026-05-07 22:37 CEST — feat(py): M5.3 — Python /api/* shadow endpoints

  • app.py: four new JSON routes (/api/version, /api/adults, /api/juniors, /api/payments) mirroring the Go /api/* handlers; _unwrap_view_model_for_api() helper expands pre-serialised JSON strings and renames month_labels_json → month_labels, raw_payments_json → raw_payments to match Go wire contract.
  • tests/test_app.py: four new smoke tests asserting top-level key sets and that unwrapped fields are objects (not strings).

2026-05-07 20:13 CEST — feat(go): M5.2 — HTTP handlers for /api/adults, /api/juniors, /api/payments, /api/version

  • web/api/handler.go: Handler struct + ServeAdults, ServeJuniors, ServePayments, ServeVersion using membership.Sources.
  • web/api/build_{adults,juniors,payments,common}.go: ports of scripts/views.py view-model builders; buildJuniorMemberRow handles "?" sentinel, :NJ,MA breakdown, unknown-month skip.
  • Extended reconcile.FeeData/MonthData with IsUnknown, JuniorAttendance, AdultAttendance; Transaction with ManualFix, VS, BankID, SyncID.
  • sources.go exports AdultMergedMonths/JuniorMergedMonths; parses new FeeData and transaction columns.
  • web/server.go + cmd/fuj/main.go wired to register /api/* routes.
  • PR #17.

2026-05-07 17:37 CEST — feat(go): M5.1 — /api/* wire types + JSON Schemas

  • New go/internal/web/api/ package: AdultsResponse, JuniorsResponse, PaymentsResponse, VersionResponse with explicit json: tags matching Python view-model keys.
  • Expected{Value int; Unknown bool} custom MarshalJSON emits integer or "?" for junior single-attendance months.
  • schemagen_test.go golden-tests four JSON Schemas committed to go/tests/fixtures/api-schema/. JSONSchema() on Expected lives in the test file — production binary has no jsonschema dep.
  • PR #16.

2026-05-07 15:26 CEST — refactor(app): extract view-model builders into scripts/views.py

  • Pulled ~350 lines of inline per-row computation out of adults_view, juniors_view, and payments into three pure functions in scripts/views.py: build_adults_view_model, build_juniors_view_model, build_payments_view_model.
  • Moved get_month_labels, group_payments_by_person, adapt_junior_members from app.py to scripts/views.py. Route handlers now ~25 lines each.
  • Hotfixed missing import re that caused 500 on /qr after the refactor.
  • No behaviour change; all 27 tests pass. Prep for /api/* shadow endpoints (M5).

2026-05-07 14:13 CEST — feat(go): --print-fio-table + Fio debug logging + date parser fix

  • Added --print-fio-table flag to fuj sync --dry-run: prints an aligned table of every Fio transaction in the window with STATUS=NEW/DUP, using text/tabwriter. Key files: go/internal/services/banksync/fio_table.go, sync.go, cmd/fuj/main.go.
  • Added LOG_LEVEL=DEBUG debug logging on the Fio fetch path: client variant selected, full GET URL (token redacted on API path), HTTP status, body bytes, and per-parse drop-reason counters (raw_rows, kept, dropped_bad_date, dropped_nonpositive_amount). Key files: go/internal/io/fio/{client,api,transparent}.go.
  • Fixed parseCzechDate to accept DD.MM.YY (2-digit year) in addition to the 4-digit variant — Fio's transparent page now serves this format. Key file: go/internal/io/fio/transparent.go.
  • Added make go-sync-debug [DAYS=N] Makefile target (default 30 days).

2026-05-07 10:32 CEST — feat(go): --dry-run for fuj sync

  • SyncOpts.DryRun bool added; when true, SyncToSheets prints planned writes (would write header row, would append date=… amount=… sender=…, would sort by date) and returns without calling WriteHeader, AppendValues, or SortByDateColumn.
  • fuj sync --dry-run flag wired in cmd/fuj/main.go; mirrors existing fuj infer --dry-run behaviour.
  • TestSyncToSheets_DryRun added to banksync test suite.

2026-05-07 01:03 CEST — feat(go/M4): IO layer behind interfaces

  • go/internal/io/attendance: CSV-over-public-URL client + Fake for both adult and junior tabs.
  • go/internal/io/drive: thin Drive v3 wrapper for modifiedTime reads + Fake.
  • go/internal/io/sheets: Sheets v4 client (GetValues, AppendValues, BatchUpdateValues, WriteHeader, SortByDateColumn) + Fake with call-capture for assertions.
  • go/internal/io/cache: Drive-modifiedTime-gated FileCache with two TTL knobs, atomic writes, and generic Get[T]; Python-compatible JSON format; Flush() support.
  • go/internal/io/fio: Client interface backed by Fio REST API (apiClient) and HTML-scraper (transparentClient); Fake for tests. Fixtures in testdata/.
  • go/internal/services/membership/sources.go: NewSources wires attendance CSV + Sheets + cache into LoadAdults, LoadJuniors, LoadTransactions, LoadExceptions. Includes Czech month/merged-month parsing logic.
  • go/internal/services/banksync: SyncToSheets (dedup via SHA-256 Sync ID, optional sort) and InferPayments (name-match + [?] review prefix, dry-run) — fully tested with fakes.
  • go/cmd/fuj/main.go: sync and infer subcommands wired to real clients; fees and reconcile now use real NewSources.
  • All packages lint-clean (golangci-lint v1.64.8, gofumpt extra-rules).

2026-05-06 23:25 CEST — feat(go/M3): fixture capture + parity test framework

  • scripts/capture_fixtures.py: dispatcher CLI that calls each ported function with seeded inputs and emits captured output as JSON fixtures.
  • scripts/scrub_fixtures.py: deterministic PII scrubber (SHA-256 pseudonyms, digit-preserving account/VS hashes, name-sweep in free text).
  • scripts/_fixture_seeds.py: handcrafted seed registry for all 10 pure functions + 10 reconcile branch-coverage cases.
  • 98 fixture files committed under go/tests/fixtures/pure/<func>/ and go/tests/fixtures/reconcile/; all PII-free.
  • go/tests/parity/parityio.go: shared loader with generic LoadDir/RunAll helpers and typed In/Out structs for all 10 functions.
  • 11 parity test packages under //go:build parity: 10 pure-function tests + bespoke reconcile test with per-cell float tolerance.
  • Makefile: go-parity, go-test-all, capture-fixtures targets.
  • go/tests/fixtures/README.md: refresh workflow, PII audit guide, adding-a-fixture steps.

2026-05-06 17:49 CEST — feat(go/M2.11-12): wire fuj fees + fuj reconcile subcommands

  • New go/internal/services/membership package: AttendanceLoader, TransactionLoader, ExceptionLoader interfaces, a stub (NewStubSources) that returns ErrIOPending, and FeesReport / ReconcileReport orchestration functions backed by real domain/fees + domain/reconcile logic.
  • Text formatters printFeesTable / printReconcileReport port the output of calculate_fees.py and match_payments.py print_report verbatim.
  • cmd/fuj/main.go: fuj fees and fuj reconcile subcommands now dispatch properly; fuj sync / fuj infer retain the [M4] placeholder.
  • Both subcommands exit 1 with a clean "io layer not yet wired up; lands in milestone M4" message until real Sheets loaders are injected in M4.
  • 13 unit tests covering stubs, all formatter branches (OK/partial/UNPAID/dash cells, credits, debts, unmatched, review annotation), and orchestration wiring via fake loaders.

2026-05-06 16:38 CEST — fix: include juniors in payment-inference roster

  • scripts/infer_payments.py: union adults + junior rosters so junior-only members are visible to the matcher.
  • Root cause: get_members_with_fees() reads only the adults sheet; junior-only kids like Jáchym Kubík were absent from member_names, causing the exact-match short-circuit to never fire and a different adult sharing the first name to win via fuzzy review.
  • Two regression tests added to tests/test_match_members.py.

2026-05-06 16:05 CEST — feat(go/M2.10): port domain/reconcile.Reconcile

  • New go/internal/domain/reconcile package porting the three-phase payment allocation from scripts/match_payments.py reconcile().
  • 12 unit tests covering all Python test cases plus Go-only extras (diacritics tolerance, [?] stripping, other: purpose, out-of-window credit, inference fallback, unmatched, no-transaction guard).

2026-05-06 13:18 CEST — feat(go/M2.7-2.9): port domain/matching package

  • New go/internal/domain/matching package porting three helpers from scripts/match_payments.py.
  • BuildNameVariants — extracts normalized ASCII search variants from a member name, including nickname (from parens) and separate first/last; filters variants shorter than 3 chars; variants[0] is always the full normalized base name.
  • MatchMembers — finds members in free text with "auto" or "review" confidence; exact-name short-circuit prevents nickname substrings (e.g. tov) from matching inside surnames (e.g. ottova).
  • FormatDate — normalizes Google Sheets date values: handles nil, empty, int/float64 serial-days since 1899-12-30 (supports fractional serials), pre-formatted YYYY-MM-DD strings, and garbage input — never errors.
  • InferTransactionDetails — composes name + month matching over sender/message/user_id; falls back to sender-only member match and date-derived month when text gives no signal.
  • 21 table-driven tests; all expected values verified against live Python on 2026-05-06.

2026-05-06 12:43 CEST — feat(go/M2.6): port domain/synch.GenerateSyncID

  • New go/internal/domain/synch package with GenerateSyncID(Transaction) string ported from scripts/sync_fio_to_sheets.py generate_sync_id.
  • Byte-stable SHA-256 hash over date|amount|currency|sender|vs|message|bank_id (lowercased, UTF-8); Currency: "" defaults to "CZK" matching the Python missing-key fallback.
  • Key subtlety: Python's str(float) emits "500.0" for whole-valued floats and switches to scientific notation at |f| >= 1e16 or |f| < 1e-4 — replicated in formatAmount using 'f'/'e' format selection.
  • 6 table-driven hash tests + 9 formatAmount tests; all expected values verified against live Python on 2026-05-06.

2026-05-06 09:38 CEST — feat(go/M2.5): port domain/money.ParseCZK

  • New go/internal/domain/money package with ParseCZK(string) (float64, error) ported from scripts/infer_payments.py parse_czk_amount.
  • Preserves the Czech-locale heuristic: comma → decimal sep; 2+ dots → thousand seps; single dot → decimal (so "1.500" → 1.5).
  • Returns (0, ErrInvalidAmount) on parse failure; callers wanting Python's silent-zero contract use v, _ := ParseCZK(s).
  • 15 table-driven tests plus a silent-zero contract test; all expected values verified against live Python on 2026-05-06.

2026-05-06 09:24 CEST — feat(go/M2.3+M2.4): port domain/fees.CalculateFee and CalculateJuniorFee

  • New go/internal/domain/fees package with adult and junior fee calculators ported from scripts/attendance.py.
  • CalculateFee(count, monthKey) int — 0→0, 1→200, 2+→AdultFeeMonthlyRate[month] (fallback 700 CZK).
  • CalculateJuniorFee(count, monthKey) Expected — 0→{0}, 1→{Unknown:true} (the "?" sentinel, now strictly typed), 2+→JuniorFeeMonthlyRate[month] (fallback 500 CZK).
  • 20 table-driven tests, all verified against live Python; -race clean; golangci-lint clean.

2026-05-06 00:07 CEST — feat(go/M2.2): port czech.ParseMonthReferences

  • internal/domain/czech.ParseMonthReferences: three-pass regex (numeric slash, dot, Czech month names) with range wrap-around and m≥10 → previousYear heuristic, byte-equivalent to Python.
  • 35 table-driven tests; all expected outputs verified against live Python before locking (addresses risk #4 from the rewrite plan).

2026-05-05 23:33 CEST — feat(go/M2.1): port czech.Normalize

  • First M2 pure-domain task: internal/domain/czech.Normalize (NFKD + Mn-strip + lowercase), byte-equivalent to Python czech_utils.normalize.
  • Adds golang.org/x/text v0.36.0 as first external Go dependency.
  • 13-case table-driven test, all spot-checked against Python before locking.

2026-05-04 23:08 CEST — fix: payment inference exact-match short-circuit

  • match_members() now short-circuits on whole-word full-name hits; nickname/partial checks only run when no full name is present.
  • Replaced bare in substring checks with _word_in() word-boundary regex throughout, closing the class of bugs where a short nickname (e.g. tov) matches inside another member's surname (ottova).
  • Added tests/test_match_members.py (6 cases). Affects scripts/match_payments.py.

2026-05-04 23:08 CEST — feat: lower adult monthly fee to 700 CZK from April 2026

  • ADULT_FEE_DEFAULT reduced from 750 → 700 CZK.
  • ADULT_FEE_MONTHLY_RATE now pins Sep 2025 – Feb 2026 at 750 to preserve historical billing; Mar 2026 stays 350; Apr–May 2026 at 700. Affects scripts/attendance.py.

2026-05-04 12:02 CEST — Go rewrite M1: skeleton + tooling

  • Created go/ tree with module fuj-management/go (Go 1.26).
  • cmd/fuj: stdlib-flag subcommand dispatcher; server and version implemented, stubs for M2/M4 commands.
  • internal/config: env loader mirroring scripts/config.py (same env var names and defaults).
  • internal/logging: slog setup accepting log level from config.
  • internal/web: net/http ServeMux on :8080; middleware/timer.go logs method/path/status/ms.
  • go/build/Dockerfile: multi-stage (golang:1.26 → alpine:3) producing a static binary image.
  • Makefile: web → web-py alias; added web-go, go-build, go-test, go-run, go-lint.
  • .gitea/workflows/build.yaml: parallel build-go job pushing <tag>-go image.
  • Gate: make go-build, make go-lint, make go-test, curl :8080 all pass.

2026-05-03 20:37 CEST — Fix Balance column to correctly reflect past-month debt

  • Balance (and Pay-All) are now computed as sum(paid − expected) over past months only, iterating directly over the ledger entries from reconcile().
  • Previously the balance used total_balance (which includes current/future-month activity and out-of-window credits) plus a one-sided current-month debt adjustment. Current-month surplus leaked through, making the balance appear less negative than the actual past-month debt.
  • Pay-All is now max(0, −balance) so the two values are derived from a single source and can never disagree.
  • Affected: adults_view() and juniors_view() in app.py.

2026-05-03 19:26 CEST — Fee-aware allocation for multi-month payments

  • reconcile() no longer splits a multi-month payment evenly. Allocation is now per-member with two phases: greedy (if amount ≥ total expected, each month gets exactly its expected fee and overflow → credit) and proportional (otherwise distribute by each month's expected). Fixes the case where e.g. 1250 CZK covering 3 months with mixed fees (750/350/150) marked two months red.
  • Out-of-window months keep the previous even-split-to-credit behavior. Fallback to even split when all matched months have expected = 0 (prepayment before attendance is recorded).
  • Display layer only — no changes to how payments are stored in Google Sheets; Inferred Amount still holds the full bank amount.
  • Files: scripts/match_payments.py, tests/test_reconcile_exceptions.py (6 new test cases).