All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
33 KiB
33 KiB
Changelog
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).