Adds static/js/member-detail.js: fetches /api/<page> once on page load,
caches the response, and renders a per-member detail modal on [i] row click.
Keyboard nav: Esc closes, ↑/↓ walk visible (filtered) rows. All modal CSS
was already in place from M6.1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extract AssemblePayments(ctx) from ServePayments in api/handler.go,
mirroring the AssembleAdults/AssembleJuniors pattern
- Add PaymentsPageData view-model wrapper in render.go
- Rewire html_handler.go ServePayments to call AssemblePayments and
render with PaymentsPageData
- Replace payments.tmpl placeholder with real grouped-by-person ledger:
alphabetical member blocks, txn-table (Date/Amount/Purpose/Message),
newest-first rows, Unmatched/Unknown bucket
- Append ledger CSS classes to app.css (.ledger-container, .member-block,
.txn-table, .txn-date/amount/purpose/message, tr:hover)
- Add TestPaymentsPage markup test
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extract AssembleJuniors(ctx) from ServeJuniors JSON handler so HTML
and JSON share the same view-model path (mirrors AssembleAdults pattern)
- Add JuniorsPageData wrapper in render.go
- Wire HTMLHandler.ServeJuniors to AssembleJuniors + render template
- Replace 4-line placeholder juniors.tmpl with full template:
member table, name filter, month-range filter, totals row,
Credits + Debts sections, Pay / Pay All buttons via /qr links
(no Unmatched section — matches Python juniors.html parity)
- J/A attendance breakdown ("3/500 CZK (4:2J,1A)") and "?" sentinel
rendered via MonthCell.Text from buildJuniorMemberRow, no extra
template logic needed
- All tests pass; make parity reports 3/3 routes OK
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two regressions made older periods invisible on the adults dashboard:
- 1257f0d (Mar 9) commented out ADULT_MERGED_MONTHS, removing the
Sep+Oct 2025 merged label. Restored only the 2025-09 → 2025-10
mapping (Dec and Jan are billed separately for adults; the
Dec → Jan mapping stays disabled per product decision). Mirrored
on the Go side. Test fixtures in sources_test.go now assert Sep
dates land in merged 2025-10 instead of 2025-09.
- 7774301 (Apr 9) added a JS onload default that set the From
selector to maxMonthIdx − 4 and immediately filtered the table,
hiding everything older than 5 months on first load. Dropped that
default in templates/adults.html and templates/juniors.html so
the From-selector starts at the oldest available month. Future
months are still removed from the dropdowns and hidden in the
table — only the past-month truncation is gone.
Note: the live adults attendance sheet had also been pruned to
start at 02.12.2025; restoring Sep/Oct/Nov 2025 columns from
Sheets version history is required to actually see those periods.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Map unpaid|partial → cell-unpaid (or cell-unpaid-current for current
month) and surplus → cell-overridden, matching Python's Jinja logic;
avoids emitting non-existent cell-partial/cell-surplus classes that
caused Pay buttons to escape the table.
- Add text-decoration: none to .pay-btn so anchor-based Pay links don't
show the default underline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The lifted CSS defines .table-container with border: 1px solid #333 and
max-width: 1200px — without the wrapper the table stretched to full
viewport width and showed no border. Mirrors templates/adults.html.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extract AssembleAdults(ctx) from ServeAdults so HTML and JSON API share one reconcile path.
- HTMLHandler gains *api.Handler; ServeAdults loads real data and renders adults.tmpl.
- AdultsPageData view model + qrHref/qrHrefAll funcMap (URL-encode /qr params, YYYY-MM→MM/YYYY).
- adults.tmpl: full reconcile table, per-cell status classes + cell-unpaid-current, Pay button hrefs,
totals row, credits/debts/unmatched sections, filter controls, sheet links.
- static/js/filters.js: NFD-normalize name filter + month-range column hiding; future months hidden by default.
- TestAdultsPage asserts member name and cell text against fixture data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stand up the Go-native HTML frontend foundation:
- base.tmpl layout + nav/footer partials (three-tier nav, active-link highlighting)
- terminal-green-on-black theme extracted to static/css/app.css (served via embed.FS)
- HTMLHandler with stub pages for all five routes; / redirects to /adults
- NewRenderer parses per-page template sets at startup so parse failures abort boot
- Smoke test: each route returns 200 text/html with exactly one class="active" link
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Go's build_juniors sets cellText = "?" + countStr whenever
md.IsUnknown is true, regardless of whether an exception overrides the
expected amount. Python was checking expected == "?" for this branch,
but reconcile replaces expected with the exception amount (e.g. 0)
before the view builder runs, so the "?" was silently dropped to "-".
Fix: derive is_unknown from original_expected == "?" (set before
exception substitution) instead of expected == "?". Also align the
tooltip guard: Go only shows Received/Expected tooltip for non-unknown
months (or when paid > 0), matching the same is_unknown flag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two remaining make parity diffs vs Go:
- amount: non-numeric sheet values like "---" passed through as strings;
Go's parseFloat silently returns 0.0 for unparseable values. Add
get_float helper that matches that behaviour.
- message: numeric cell values (e.g. a bank reference in the message
column) passed through as float64; Go's getVal uses fmt.Sprint and
always emits a string. Apply get_str to the message field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Sheets API returns VS (variabilní symbol) cells as float64 when
the column is number-formatted, so Python was emitting vs: 0 (a JSON
number) while Go's getVal uses fmt.Sprint and always emits vs: "0"
(a string). Add get_str helper that converts whole-number floats via
int() first (matching Go's %g formatting), applied to the vs field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Python's fetch_sheet_data read 9 sheet columns but skipped VS and
Sync ID, causing make parity to report extra fields on every raw
payment row emitted by the Go backend. Both columns are already on
the sheet; add idx_vs / idx_sync_id lookups and the matching keys
to the tx dict so the Python /api/* wire shape matches Go's
RawTransaction.
Update /api/* test fixtures to include vs/sync_id keys for realism.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
parseDates was using "02.01.2006" / "01/02/2006" which require
zero-padded fields. The Czech attendance sheet headers contain dates
like "1.6.2026", "23.3.2026", "6.4.2026" — Go silently dropped those
columns while Python's strptime accepted them. Effect was a missing
2026-06 month on /api/juniors plus undercounted attendance in any month
with single-digit columns; surfaced via make parity.
Use the unpadded reference forms "2.1.2006" / "1/2/2006" instead — Go's
time.Parse accepts both padded and unpadded inputs against them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/api/version returns each binary's own tag/commit/build_date, which
differs by design between independently built backends. Diffing it
always produces a false positive. Drop it from allRoutes; the route
remains reachable via `make parity ARGS="-route /api/version"`.
Also remove the vestigial `build_meta` allowlist entry (Python returns
the build dict as the top-level response body, not nested under
build_meta, so the scrubber never matched anything).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The transaction-row parser in services/membership/sources.go used a
helper (`getVal`) that did `fmt.Sprint(row[i])` before passing to
`matching.FormatDate`. The Sheets API returns date-formatted cells
as `float64` (Sheets serial-day numbers); pre-stringifying defeated
`FormatDate`'s `case float64:` dispatch, so values like 46147 leaked
through unchanged as the string "46147" instead of being converted
to "2026-05-05".
Surfaced by `make parity` (M5.4) — every `transactions[].date` on
/api/adults and /api/juniors differed between Python and Go. Python
side passes the raw value through directly (`isinstance(val, (int,
float))` in scripts/match_payments.py format_date), so it was always
correct.
Added a `getRaw` helper that returns row[i] without stringifying;
only the date column needs it. Extended TestLoadTransactions with
a numeric-serial-day row to lock in the regression.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds cmd/parity/main.go: a standalone Go binary that GETs
/api/version, /api/adults, /api/juniors, /api/payments from both
the Python (:5001) and Go (:8080) backends, scrubs an allowlist
(render_time.total, build_meta), and prints cmp.Diff for any
remaining differences. Exits 0 on full match, 1 on diffs, 2 on
fetch/parse errors — CI-friendly for M7.2.
- go/cmd/parity/main.go: flags (-py, -go, -route, -timeout), fetch
helper, allowlist scrubber (dotted-path aware), exit-code logic.
- go/cmd/parity/scrub_test.go: 4 unit tests for the scrubber.
- go/go.mod: promote github.com/google/go-cmp to direct dep.
- Makefile: parity target + help entry.
- Progress tracker: M5.4 ticked; milestone updated to M5 complete.
- Plan archived to docs/plans/2026-05-07-2254-m5-4-parity-binary.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously both backends defaulted to `CacheDir=tmp` and used the
same cache keys (`attendance_regular`, `attendance_juniors`,
`payments_transactions`, `exceptions_dict`) but stored different
shapes: Python caches post-processed view-model tuples
(e.g. `(members, sorted_months)`), Go caches raw sheet rows.
Whichever backend wrote last poisoned the cache for the other,
producing `ValueError: too many values to unpack (expected 2,
got 68)` on Python's /adults after the Go side populated the
file with 68 raw CSV rows.
This breaks the M5.4 `make parity` workflow that requires both
backends running side-by-side.
Fix: change Go's default to `tmp/go` so the two cache trees
never overlap. `CACHE_DIR` env var override still works.
`os.MkdirAll` already handles creating the new subdirectory on
first write.
Recovery for users with poisoned `tmp/`: hit /flush-cache on
the Python side once after pulling, then restart the Go server.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four new JSON routes mirror the Go /api/* handlers so the M5.4 parity
tool can diff them: /api/version, /api/adults, /api/juniors,
/api/payments. A small _unwrap_view_model_for_api() helper in app.py
expands the three pre-serialised JSON strings in the view-model dicts
and renames month_labels_json → month_labels and
raw_payments_json → raw_payments to match the Go wire contract.
Tests in test_app.py assert top-level key sets match the Go API schema
and that member_data, month_labels, raw_payments are objects not strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add internal/web/api package with Go structs for every /api/X route:
AdultsResponse, JuniorsResponse, PaymentsResponse, VersionResponse.
All fields carry explicit json: tags matching the Python view-model keys.
Key design choices:
- member_data / month_labels / raw_payments are nested objects (not
the pre-serialised JSON strings used in Jinja templates)
- Expected{Value int; Unknown bool} with custom MarshalJSON emits int
or the string "?" for junior single-attendance months
- RawTransaction covers the full 11-column payments sheet row
schemagen_test.go reflects all four response types via
github.com/invopop/jsonschema and golden-compares against committed
schemas in tests/fixtures/api-schema/. The JSONSchema() method on
Expected lives in the test file so the prod binary has no jsonschema
dependency.
Closes M5.1 in docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Accidentally removed when moving group_payments_by_person to views.py;
re.match in qr_code caused a 500 on every QR request.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pull 350+ lines of inline per-row computation out of adults_view,
juniors_view, and payments into three pure builder functions with no
Flask globals or IO dependencies. Route handlers now contain only
cache/IO calls and a single render_template. No behaviour change —
all 27 tests pass.
Also moves get_month_labels, group_payments_by_person, and
adapt_junior_members out of app.py. Prep for /api/* shadow endpoints
(M5 Go parity).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fio's transparent account page now serves dates as DD.MM.YY (e.g.
07.05.26) rather than the previously expected 4-digit-year format.
Extends parseCzechDate to try all eight layout variants: padded and
non-padded, dot and slash separators, 4-digit and 2-digit years.
Go maps 2-digit year 00-68 → 2000-2068, so 26 → 2026.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wraps `LOG_LEVEL=DEBUG ./bin/fuj sync -dry-run -print-fio-table -days N`
behind a single make target. Default DAYS=30, override with
`make go-sync-debug DAYS=90`. Builds the Go binary first via go-build.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires slog.SetDefault to honour LOG_LEVEL in all CLI commands and adds
debug logs on the Fio fetch path so a silent "fetched 0 transaction(s)"
can be diagnosed without code changes:
- fio.New: which client variant (api/transparent) was selected
- apiClient: GET URL (token redacted as ****), HTTP status, body bytes,
parsed transaction count
- transparentClient: GET URL, HTTP status, body bytes, plus parser
stats (raw rows from second table, kept, dropped_bad_date,
dropped_nonpositive_amount)
Also suppresses the --print-fio-table block when zero transactions were
fetched, so the bare header no longer prints under that condition.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prints an aligned tabwriter table of every Fio transaction in the
look-back window, with a STATUS column showing NEW (would be appended)
or DUP (already in sheet). Only fires when --dry-run is also set, so
it can't affect real syncs. Refactors Sync ID computation into a single
pre-pass shared by both the table printer and the row builder.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
extractSecondTableRows tracked a boolean inTarget flag and exited on
the first </table> token while inside the target. Any nested <table>
(e.g. pagination markup in the real Fio page) would cause an early
return before reading any data rows, explaining the 0-transaction report.
Fixed by tracking targetDepth instead: depth increments on every <table>
inside the target and we only return when it reaches 0 again.
parseCzechDate also only tried zero-padded layouts ("02.01.2006").
The real Fio transparent page emits non-padded dates ("7.5.2026");
added "2.1.2006" and "2/1/2006" as the preferred layouts.
Also adds a dry-run diagnostic line ("fetched N transaction(s) from Fio")
so the fetch vs dedup split is visible without reading logs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When token is empty, New falls back to transparentClient with the
caller-supplied hc. main.go passes nil, so the first Do() call panicked.
Default to http.DefaultClient when hc is nil.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror fuj infer's read-only mode: SyncOpts.DryRun skips WriteHeader,
AppendValues, and SortByDateColumn, printing one "Dry run: would …"
line per planned operation instead. ID-dedup still runs so the output
reflects exactly what the next real sync would write.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes M3.1–M3.6. Parity safety net proving Go output matches Python
for every ported pure-domain function (M2.1–M2.9) and reconcile (M2.10).
Capture pipeline:
- scripts/capture_fixtures.py: calls each Python function with seeded
inputs, emits JSON fixtures to stdout (never writes files directly).
- scripts/scrub_fixtures.py: deterministic PII scrubber — SHA-256
pseudonyms for member names, digit-preserving hashes for VS/account/
bank_id, name-sweep in message text. Idempotent; no salt.
- scripts/_fixture_seeds.py: handcrafted seeds for all 11 functions;
synthetic names throughout (no real roster members).
- scripts/capture_all_fixtures.sh: convenience wrapper for full corpus
regeneration outside of make.
Fixture corpus (98 files, all PII-free):
- go/tests/fixtures/pure/<func>/<case>.json — 10 function directories.
- go/tests/fixtures/reconcile/<NN>_<case>.json — 10 branch-coverage
cases: greedy, overpayment credit, proportional remainder, even-split,
out-of-window, exception override, other: purpose, junior ?, multi-
person+month fan-out, unmatched.
Go parity tests (//go:build parity):
- go/tests/parity/parityio.go: generic LoadDir/RunAll helpers + typed
In/Out struct pairs for all 10 pure functions; Envelope decoder for
int/float/none disambiguation.
- 10 pure-function test packages + bespoke reconcile test with per-cell
float tolerance (math.Abs <= 0.01 for `paid` values).
Makefile: go-parity, go-test-all, capture-fixtures targets.
go/tests/fixtures/README.md: refresh workflow + PII audit guide.
Gate: make go-test green, make go-parity green (11/11 packages),
make go-lint clean (parity tag), make go-build clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add internal/services/membership package: AttendanceLoader,
TransactionLoader, ExceptionLoader interfaces + NewStubSources stub
(returns ErrIOPending until M4 lands real Sheets loaders).
FeesReport and ReconcileReport orchestrate domain/fees + domain/reconcile
and write fixed-width text reports matching Python calculate_fees.py and
match_payments.py print_report output. 13 unit tests cover all formatter
branches and orchestration wiring via fake loaders.
cmd/fuj/main.go: fees and reconcile subcommands now dispatch; sync/infer
retain the [M4] placeholder.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
infer_payments was building member_names from get_members_with_fees()
(adults sheet only). Junior-only members were invisible to the matcher,
so a payment message containing an exact junior name would produce a
fuzzy review match against a different adult sharing the same first name.
Fix: union the adult and junior rosters (deduped via canonical_member_key)
so all members are candidates. The existing exact-name short-circuit in
match_members then handles precedence correctly.
Two regression tests added for the Jáchym Kubík / Jáchym Hrušák case.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New go/internal/domain/matching package porting three helpers from
scripts/match_payments.py:
- BuildNameVariants: normalized ASCII variants from a member name (nickname
in parens, last/first split, len<3 filtered); variants[0] is always the
full base name — MatchMembers relies on this invariant.
- MatchMembers: auto/review confidence matching with an exact-name
short-circuit pass that prevents nickname substrings (tov) from firing
inside longer surnames (ottova); common-surname filter for review tier.
- FormatDate: nil/empty/""/serial int/float64 (since 1899-12-30, fractional
days supported)/YYYY-MM-DD passthrough/garbage → never errors.
- InferTransactionDetails: composes BuildNameVariants+MatchMembers+
ParseMonthReferences; falls back to sender-only member match and
date-derived month when text carries no signal.
21 table-driven tests; all expected values verified against live Python
on 2026-05-06. go-build, go-test, go-lint all clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
SHA-256 dedup hash from sync_fio_to_sheets.py generate_sync_id.
Key subtlety: Python str(float) emits "500.0" for whole-valued floats
and switches to scientific notation at |f|>=1e16 or |f|<1e-4 —
replicated via formatAmount using 'f'/'e' format selection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Port scripts/infer_payments.py parse_czk_amount to Go as
internal/domain/money.ParseCZK. Preserves the Czech-locale heuristic
(comma = decimal sep; 2+ dots = thousand seps; single dot = decimal)
and returns (float64, error) so callers can opt into Python's
silent-zero contract via v, _ := money.ParseCZK(s).
All expected values verified against live Python on 2026-05-06.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ports calculate_fee and calculate_junior_fee from scripts/attendance.py
into a new go/internal/domain/fees package. Introduces the Expected type
(Value int, Unknown bool) for the junior "?" sentinel, keeping the Go
API strictly typed instead of mirroring Python's str|int return.
All 20 table-driven tests pass with -race; golangci-lint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ADULT_FEE_DEFAULT is 700 CZK, not 750. The 750 appears in
ADULT_FEE_MONTHLY_RATE for most current months but is not the fallback.
Rephrase the member-tiers bullet to point at the dict rather than a
number that drifts each season; update the fee-calc bullet to match
the junior line's style (default 700 vs default 500).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three-pass regex parser matching python/czech_utils.py parse_month_references:
1. Numeric slash notation — "11+12/2025", "01/26"; 2-digit year → +2000
2. Dot notation — "12.2025" (4-digit year only)
3. Czech month names — range walk (listopad-leden wrap logic) then
standalone with m≥10 → defaultYear-1 heuristic; longest-match
alternation (sorted desc by name length) handles cervenec vs cerven
35 table-driven tests, all expected outputs verified against live Python
on 2026-05-05 before locking. Plan at
docs/plans/2026-05-05-2337-go-rewrite-m2-2-parse-month-references.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the "do not use tea/gh/Gitea API" rule with explicit guidance to
run `tea pr create` and print the resulting PR URL. tea is already
authenticated on this machine. Merging stays a manual user action in
Gitea — neither tea nor git CLI may merge or delete branches.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds internal/domain/czech.Normalize, the first pure-domain function in
the Go rewrite (M2 milestone). Matches Python czech_utils.normalize byte-
for-byte: NFKD decompose via golang.org/x/text/unicode/norm, drop Mn-
category combining marks (unicode.Mn, not IsMark, to match Python's
unicodedata.combining() semantics), then strings.ToLower.
Includes 13-case table-driven test; all inputs spot-checked against the
Python implementation before locking. Adds golang.org/x/text v0.36.0 as
first external dependency.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Feature work now goes on feat/<slug> branches; Claude pushes and prints
the Gitea compare URL for the user to open the MR. Exceptions documented
for small fixes and typo tweaks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add canonical_member_key() in match_payments.py to normalize names via
NFKD + lowercase + whitespace-collapse before ledger lookup; resolves
payments attributed to e.g. "Maria Maco" to canonical "Mária Maco".
Emits logger.info when a non-canonical cell is rescued so sheet typos
are visible in logs without losing the payment allocation.
- Extend group_payments_by_person() in app.py to accept member_names and
re-key raw-payment groups under the canonical attendance-sheet name so
the modal's Raw Payments debug section also finds the row correctly.
- Add raw payments collapsible section to member detail modal in adults.html
and juniors.html for debugging payment attribution issues.
- Remove 4 obsolete tests targeting routes /fees, /fees-juniors, /reconcile,
/reconcile-juniors that no longer exist; add test_match_payments.py
covering canonical key equivalence and reconcile() tolerance end-to-end.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
match_members() now short-circuits on whole-word full-name hits and
uses word-boundary regex everywhere else, so a nickname that is a
substring of another member's surname (e.g. "tov" inside "ottova")
no longer produces false positives. Adds tests/test_match_members.py.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pin Sep 2025 – Feb 2026 at 750 CZK in ADULT_FEE_MONTHLY_RATE so
historical billing is unchanged; new default 700 CZK applies to
2026-04 onward. March 2026 stays at 350.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous calculation derived balance from total_balance (which includes
current/future-month activity and out-of-window credits) plus a one-sided
debt-only adjustment. Current-month surplus leaked through, making the balance
appear less negative than actual past-month debt (e.g. Mauric Daniel -1250 vs
correct -1750). Pay-All is now max(0, -balance) so the two values share a
single source and cannot disagree.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
reconcile() previously split a multi-month payment evenly across months,
which falsely flagged months as underpaid when their expected fees
differed (e.g. 1250 CZK for 02+03+04 2026 with rates 750/350/150 was
shown as 416/month with two months red).
The allocation now runs per matched member: greedy when the share covers
the total expected (each month gets its expected fee, surplus -> credit),
proportional by expected fee otherwise. Out-of-window months keep the
previous even-split-to-credit behavior. 6 new test cases.
Also adds CHANGELOG.md and a changelog convention in CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hide Pay/Pay All buttons for months still in progress, exclude
current month debt from balance column, and show in-progress
month debt in a muted red color.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>