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>
36 KiB
36 KiB
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 secretsVAULT_ROLE_ID,VAULT_SECRET_IDandK8S_CA_CERTare 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_showin the JSON API; Python only passed it to its HTML templates. Backported the key so the API contract matches andmake parityreports 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 as2.609767878e+09. Text columns of the payments sheet now go throughsheets.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 tolegacy/(app.py,scripts/,templates/,tests/,pyproject.toml,uv.lock,build/) and is considered legacy; new features are still built in both. CLAUDE.mdrewritten for the Go-first layout, with a "Dual implementation rule" section.- Makefile Python targets keep their names and point at
legacy/; virtualenv islegacy/.venv, Python cache islegacy/tmp/;CREDENTIALS_PATHis exported pointing at the root.secret/file. - CI builds the Python image with
legacy/as context; the image no longer contains the Makefile.legacy/.dockerignorenow 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 updateopens a PR inkacerr/home-kubernetesbumping thefuj-managementDeployment (namespacefuj) to the new image tag. - Supports
workflow_runauto-trigger andworkflow_dispatchwithdry_run/uh_cli_versioninputs. - Requires
GITOPS_TOKENrepo secret (Gitea PAT with write+PR access tohome-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.pyandgo/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(IBANCZ0820100000002502035405) 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.gogains hardcodedAccounts/LoadedAccountslice;Config.BankAccountrenamed toConfig.QRAccount;FioAPITokenremoved (tokens are per-account viaFIO_API_TOKEN_NEW/FIO_API_TOKEN_OLD). - Go:
SyncToSheetsnow accepts[]fio.Client; newTestSyncToSheets_MultiAccounttest. - Python:
config.pygainsACCOUNTS/LOADED_ACCOUNTS;fio_utils.pyaddsfetch_transactions_forandfetch_transactions_all;sync_fio_to_sheets.pyusesfetch_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.pyparse_czech_datenow acceptsDD.MM.YY/D.M.YYin 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 causedmake sync-2026to miss every recent transfer. Mirrors the Go-side fix from 2026-05-07 (CHANGELOG entry below). - Added
--dry-runand--print-fio-tableflags toscripts/sync_fio_to_sheets.py, plus amake sync-debug [DAYS=N]Makefile target. Mirrorsmake go-sync-debug: fetches from Fio and dedupes against the sheet, printsSTATUS=NEW/DUPper transaction, and prints per-rowDry run: would append …lines +would sort by dateinstead of touching the sheet. - Added always-on stderr diagnostics in
scripts/fio_utils.py: which fetcher was selected (authenticated API vs. transparent-page scraper withFIO_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.FSwiring is complete: templates parsed viatemplate.ParseFS(templateFS, ...), static assets served viahttp.FileServerFS(fs.Sub(staticFS, "static")). - Added
go/internal/web/assets_test.gowith two tests:TestEmbedCompleteness(walks disk vs embed.FS to catch forgotten files) andTestStaticAssetsServed(hits/static/css/app.cssand 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/orstatic/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
showPayQRin-page modal UX that was lost in M6.6 (Pay buttons were navigating the tab to the raw/qrPNG). - Replaced
<a href="{{qrHref ...}}">Pay</a>with<button data-name|amount|month|raw-month>on/adultsand/juniors; click is handled by a newstatic/js/payment-qr.jsIIFE module that opens#qrModalwith title, account, amount, message, and the QR image. - Added
#qrModalmarkup to both templates; CSSdisplay: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/qrHrefAlltemplate helpers fromrender.go. - Markup tests in
html_handler_test.goassert modal IDs, script tag,data-bank-account, and that no barehref="/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'sqr_code()handler exactly including account validation, amount clamping, and*stripping. - Implemented
GET /sync-bank: runs Fio sync → infer payments → cache flush, captures output intosync.tmplpage with success/error banner. - Implemented
GET /flush-cache+POST /flush-cache: form + action that deletes cache files and shows deleted count. - Added
GET /versionas a JSON alias ofGET /api/version(Python parity). - Added
FlushCache() (int, error)tomembership.Sourcesinterface; implemented onrealSourcesviacache.FileCache.Flush(). - Introduced
web.ActionHandlers{BankSync}— closure-based dep injection for sync, constructed inserverCmdwith 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/adultsor/api/juniorsonce 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:
Esccloses,↑/↓walk visible (name-filtered) rows; click outside modal content closes it. - Added
[i]info icon and#memberModalmarkup toadults.tmplandjuniors.tmpl; all CSS was already in place from M6.1. - Added
TestModalMarkupassertions tohtml_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.3AssembleAdults/AssembleJuniorspattern. - Added
PaymentsPageDatatorender.go; wiredHTMLHandler.ServePaymentsto callAssemblePaymentsand render the new template. - Replaced the "Coming in M6.4" placeholder in
payments.tmplwith the full grouped-by-person ledger: alphabetical<h2>member blocks, each with atxn-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
TestPaymentsPagemarkup 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 (mirrorsAssembleAdults). - Added
JuniorsPageDatatorender.go; wiredHTMLHandler.ServeJuniorsto callAssembleJuniorsand render the new template. - Replaced the 4-line "Coming in M6.3" placeholder in
juniors.tmplwith 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 viaMonthCell.Textproduced bybuildJuniorMemberRow— no extra template logic. make parityreports 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-10adult merge inscripts/attendance.pyandgo/internal/services/membership/sources.go(commented-out by1257f0d); the2025-12 → 2026-01mapping stays disabled per product decision (Dec and Jan are billed separately for adults). - Dropped the
defaultFrom = maxMonthIdx − 4JS auto-default intemplates/adults.htmlandtemplates/juniors.html(introduced by7774301); 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_Feenow assert that Sep dates land in the merged2025-10bucket.- 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 inventedbalance-cell/balance-negative); Pay-All button now lives inside the balance cell withposition: 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: extractedServeAdultsbody intoAssembleAdults(ctx)— shared by the JSON API route and the new HTML handler.go/internal/web/render.go: addedAdultsPageDataview model (PageData+api.AdultsResponse+Error);tmplFuncswithqrHref/qrHrefAll(URL-encode QR Platba params, convert YYYY-MM → MM/YYYY).go/internal/web/html_handler.go:HTMLHandlergains*api.Handler;ServeAdultsloads 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 bydata-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 Pythontemplates/adults.html— shared by all Go HTML pages via<link>.go/internal/web/assets.go://go:embed templates staticfor single-binary deployment.go/internal/web/render.go:Rendererparses a fresh*template.Templateper page at startup;Render(w, name, data)executes the "base" template block.go/internal/web/html_handler.go:HTMLHandlerwith one method per route (ServeAdults,ServeJuniors,ServePayments,ServeSync,ServeFlushCache).go/internal/web/server.go: dropshelloHandler;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 200text/htmlwith exactly oneclass="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: addedget_floathelper — non-numericamountvalues (e.g."---"placeholder rows) now coerce to0.0matching Go'sparseFloatbehaviour;messagefield now goes throughget_strso numeric cell values (bank references) are emitted as strings, matching Go'sfmt.Sprint.scripts/views.py: junior month cell"?"text is now sticky across exception overrides. Previouslyreconcilereplacedexpectedwith the exception amount before the view builder ran, silently turning"?"into"-"when the override was 0. Fixed by derivingis_unknownfromoriginal_expected == "?"instead ofexpected == "?". Also aligned tooltip guard: only show Received/Expected for non-unknown months (or when paid > 0), matching Go's!md.IsUnknowncondition.
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_datanow readsVSandSync IDcolumns and includesvs/sync_idkeys in every tx dict. Previously only 9 columns were projected, causingmake parityto report extravs/sync_idfields on every raw payment row emitted by the Go backend. Values flow throughgroup_payments_by_person→_unwrap_view_model_for_apitoraw_payments(adults/juniors) andgrouped_payments(payments) automatically.tests/test_app.py: updated/api/*mock fixtures to includevs/sync_idkeys for realism.- Cache note: after deploying, hit
POST /flush-cacheonce 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:parseDatesnow uses Go time formats2.1.2006and1/2/2006(single-digit reference forms, which accept both padded and unpadded inputs) instead of02.01.2006and01/02/2006. The Czech attendance sheet headers contain dates like1.6.2026,23.3.2026,6.4.2026— Go silently dropped those columns under the strict zero-padded format, while Python'sstrptime("%d.%m.%Y")accepted them. Effect was a missing2026-06month entirely on/api/juniorsplus undercounted attendance for any month with single-digit columns; both surfaced as diffs inmake parity.sources_test.go::TestParseDates_SingleDigitDayMonthadded 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 passesrow[idxDate]directly tomatching.FormatDate(via a newgetRawhelper) instead of stringifying first viagetVal. The Sheets API returns numeric serial-day values asfloat64for date-formatted cells; pre-stringifying them defeatedFormatDate'scase float64:dispatch, causing all numeric dates to leak through as"46147"style strings instead of"2026-05-05".- Surfaced by
make parity(M5.4): everytransactions[].datefield on/api/adultsand/api/juniorsdiffered between Python and Go. sources_test.go::TestLoadTransactionsextended 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:CacheDirdefault changed fromtmptotmp/go. Override viaCACHE_DIRenv var still works.- Why: both backends used
tmp/<key>_cache.jsonwith 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 producedValueError: too many values to unpack (expected 2, got 68)on Python's/adultsafter the Go server populatedattendance_regular_cache.jsonwith raw CSV rows. - After upgrading: stop the Go server, hit
/flush-cacheon the Python side once (rewritestmp/*.jsonwith correct shapes), then restartmake web-go— it will usetmp/go/going forward. Required for the M5.4make parityworkflow 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/paymentsfrom both Python (:5001) and Go (:8080) backends, scrubs an allowlist (render_time.total), and printscmp.Difffor any remaining differences. Exits 0 on full match, 1 on diffs, 2 on fetch/parse errors — CI-friendly for M7.2./api/versionis excluded by design (returns binary identity — tag/commit/build_date — which differs between independently built backends); still accessible viamake 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-cmppromoted to direct dependency.Makefile:paritytarget 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 renamesmonth_labels_json→month_labels,raw_payments_json→raw_paymentsto 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:Handlerstruct +ServeAdults,ServeJuniors,ServePayments,ServeVersionusingmembership.Sources.web/api/build_{adults,juniors,payments,common}.go: ports ofscripts/views.pyview-model builders;buildJuniorMemberRowhandles"?"sentinel,:NJ,MAbreakdown, unknown-month skip.- Extended
reconcile.FeeData/MonthDatawithIsUnknown,JuniorAttendance,AdultAttendance;TransactionwithManualFix,VS,BankID,SyncID. sources.goexportsAdultMergedMonths/JuniorMergedMonths; parses new FeeData and transaction columns.web/server.go+cmd/fuj/main.gowired 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,VersionResponsewith explicitjson:tags matching Python view-model keys. Expected{Value int; Unknown bool}customMarshalJSONemits integer or"?"for junior single-attendance months.schemagen_test.gogolden-tests four JSON Schemas committed togo/tests/fixtures/api-schema/.JSONSchema()onExpectedlives 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, andpaymentsinto three pure functions inscripts/views.py:build_adults_view_model,build_juniors_view_model,build_payments_view_model. - Moved
get_month_labels,group_payments_by_person,adapt_junior_membersfromapp.pytoscripts/views.py. Route handlers now ~25 lines each. - Hotfixed missing
import rethat caused 500 on/qrafter 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-tableflag tofuj sync --dry-run: prints an aligned table of every Fio transaction in the window withSTATUS=NEW/DUP, usingtext/tabwriter. Key files:go/internal/services/banksync/fio_table.go,sync.go,cmd/fuj/main.go. - Added
LOG_LEVEL=DEBUGdebug 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
parseCzechDateto acceptDD.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 booladded; when true,SyncToSheetsprints planned writes (would write header row,would append date=… amount=… sender=…,would sort by date) and returns without callingWriteHeader,AppendValues, orSortByDateColumn.fuj sync --dry-runflag wired incmd/fuj/main.go; mirrors existingfuj infer --dry-runbehaviour.TestSyncToSheets_DryRunadded 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 +Fakefor both adult and junior tabs.go/internal/io/drive: thin Drive v3 wrapper formodifiedTimereads +Fake.go/internal/io/sheets: Sheets v4 client (GetValues,AppendValues,BatchUpdateValues,WriteHeader,SortByDateColumn) +Fakewith call-capture for assertions.go/internal/io/cache: Drive-modifiedTime-gatedFileCachewith two TTL knobs, atomic writes, and genericGet[T]; Python-compatible JSON format;Flush()support.go/internal/io/fio:Clientinterface backed by Fio REST API (apiClient) and HTML-scraper (transparentClient);Fakefor tests. Fixtures intestdata/.go/internal/services/membership/sources.go:NewSourceswires attendance CSV + Sheets + cache intoLoadAdults,LoadJuniors,LoadTransactions,LoadExceptions. Includes Czech month/merged-month parsing logic.go/internal/services/banksync:SyncToSheets(dedup via SHA-256 Sync ID, optional sort) andInferPayments(name-match +[?]review prefix, dry-run) — fully tested with fakes.go/cmd/fuj/main.go:syncandinfersubcommands wired to real clients;feesandreconcilenow use realNewSources.- 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>/andgo/tests/fixtures/reconcile/; all PII-free. go/tests/parity/parityio.go: shared loader with genericLoadDir/RunAllhelpers and typedIn/Outstructs 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-fixturestargets. 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/membershippackage:AttendanceLoader,TransactionLoader,ExceptionLoaderinterfaces, a stub (NewStubSources) that returnsErrIOPending, andFeesReport/ReconcileReportorchestration functions backed by realdomain/fees+domain/reconcilelogic. - Text formatters
printFeesTable/printReconcileReportport the output ofcalculate_fees.pyandmatch_payments.py print_reportverbatim. cmd/fuj/main.go:fuj feesandfuj reconcilesubcommands now dispatch properly;fuj sync/fuj inferretain 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 frommember_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/reconcilepackage porting the three-phase payment allocation fromscripts/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/matchingpackage porting three helpers fromscripts/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-formattedYYYY-MM-DDstrings, 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/synchpackage withGenerateSyncID(Transaction) stringported fromscripts/sync_fio_to_sheets.pygenerate_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| >= 1e16or|f| < 1e-4— replicated informatAmountusing'f'/'e'format selection. - 6 table-driven hash tests + 9
formatAmounttests; 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/moneypackage withParseCZK(string) (float64, error)ported fromscripts/infer_payments.pyparse_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 usev, _ := 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/feespackage with adult and junior fee calculators ported fromscripts/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;
-raceclean;golangci-lintclean.
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 andm≥10 → previousYearheuristic, 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 Pythonczech_utils.normalize. - Adds
golang.org/x/text v0.36.0as 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
insubstring 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). Affectsscripts/match_payments.py.
2026-05-04 23:08 CEST — feat: lower adult monthly fee to 700 CZK from April 2026
ADULT_FEE_DEFAULTreduced from 750 → 700 CZK.ADULT_FEE_MONTHLY_RATEnow pins Sep 2025 – Feb 2026 at 750 to preserve historical billing; Mar 2026 stays 350; Apr–May 2026 at 700. Affectsscripts/attendance.py.
2026-05-04 12:02 CEST — Go rewrite M1: skeleton + tooling
- Created
go/tree with modulefuj-management/go(Go 1.26). cmd/fuj: stdlib-flag subcommand dispatcher;serverandversionimplemented, stubs for M2/M4 commands.internal/config: env loader mirroringscripts/config.py(same env var names and defaults).internal/logging: slog setup accepting log level from config.internal/web:net/httpServeMux on:8080;middleware/timer.gologs method/path/status/ms.go/build/Dockerfile: multi-stage (golang:1.26→alpine:3) producing a static binary image.- Makefile:
web→web-pyalias; addedweb-go,go-build,go-test,go-run,go-lint. .gitea/workflows/build.yaml: parallelbuild-gojob pushing<tag>-goimage.- Gate:
make go-build,make go-lint,make go-test,curl :8080all 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 fromreconcile(). - 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()andjuniors_view()inapp.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 Amountstill holds the full bank amount. - Files: scripts/match_payments.py, tests/test_reconcile_exceptions.py (6 new test cases).