- 2026-08 added to the junior rate table at 0 CZK, same as adults
- A single practice in a 0-rate month is 0 instead of "?", since a
free month needs no manual review
- Mirrored in Go and legacy Python; new calculate_junior_fee parity
fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the bank message starts with "other:" (case-insensitive),
inference does not look for a month, neither in the text nor from the
payment date. The trimmed message is written to Purpose, so reconcile
books the payment as an "other" transaction for the person instead of
a monthly fee. Messages containing "other:" elsewhere are unchanged.
- matching.IsOtherPurpose / is_other_purpose shared by inference and
reconcile
- reconcile applies the rule to rows not yet processed by inference
- no person found: Purpose is still written, row shows as unmatched
- implemented in Go and legacy Python; four new parity fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A junior month with one session has an unknown expected fee. When such
a month had a payment, the Go tooltip printed "Expected: 0" where
Python prints "Expected: ?", which made make parity fail on
/api/juniors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Go server now runs sync -> infer -> flush-cache by itself at the
times in SYNC_SCHEDULE (comma-separated HH:MM, default 04:00, "off"
disables), read in SYNC_TZ (default Europe/Prague). Nothing runs at
startup; an invalid value stops the server with an error.
- internal/scheduler: time-of-day parsing, Next, and the run loop
- banksync.Runner: one lock shared by /sync-bank and the scheduler so
runs never overlap, plus the status of the last run
- footer shows the last sync since start and the next scheduled run
- binary embeds time/tzdata because the Alpine image has no zone data
Deliberately not implemented in the legacy Python app, which is being
phased out; recorded in CLAUDE.md under "Deliberately Go-only features".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/sync-bank now uses one shared default window in both implementations:
1 November of the previous year to 31 December of the current year.
Payments dated around New Year are picked up in January; duplicates
are still filtered by Sync ID.
Python used a hardcoded 2026 window and would have stopped syncing on
2027-01-01; Go used the current calendar year only.
Adds make sync-year [YEAR=N] with the same window. The CLI default
without flags (last 30 days) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Default adult monthly rate 700 -> 600 CZK
- 2026-07 and 2026-08 set to 0; 2026-09 through 2026-12 added at 600
- Single-session fee is capped at the month's rate, so a 0-rate month
is free for one session as well
- Mirrored in Go and legacy Python; new calculate_fee parity fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Task list from the 2026-10-08 repo audit, plus the plan for the two
parity fixes found while validating the legacy/ move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Go returns months_to_show in /api/adults and /api/juniors; the Python
API did not, so make parity failed on both routes. The Python HTML
pages already had the feature.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Go stringified sheet cells with fmt.Sprint, so a numeric VS read with
UNFORMATTED_VALUE was rendered as 2.609767878e+09. Text columns of the
payments sheet now go through sheets.CellString, which never uses
exponent form; bank inference uses the same helper.
Both sync implementations now append VS as a text cell so Sheets keeps
it a string and leading zeros survive. Existing rows are not rewritten.
Python already read VS as a digit string; a regression test pins that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Go (go/) becomes the primary implementation. The original Python
version moves to legacy/ and is kept runnable; new features are still
built in both (see the Dual implementation rule in CLAUDE.md).
- git mv app.py, scripts/, templates/, tests/, pyproject.toml, uv.lock
and build/ into legacy/
- Makefile: Python targets point at legacy/, venv in legacy/.venv,
CREDENTIALS_PATH exported for the root .secret/ file
- CI and make image build the Python image with legacy/ as context;
the Makefile is no longer copied into the image
- legacy/.dockerignore moved to the context root so it takes effect
- CLAUDE.md, docs/README.md, Go comments, fixture README, capture
script and VS Code launch config updated to the new layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
upload/download-artifact@v4 is not supported on Gitea (GHES). Replace
with a direct Gitea API call in gitops-update: look up the tag name
whose commit SHA matches workflow_run.head_sha. Reverts the artifact
upload from build.yaml; no changes to build.yaml logic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github.event.workflow_run.head_branch is not populated for tag pushes
in Gitea Actions, causing the image tag to resolve to empty (-go suffix
with no version). Fix: build-go uploads the full image reference as a
one-line artifact; gitops-update downloads it via download-artifact@v4
with run-id from the workflow_run event.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tea pr create matches the remote URL against the configured login URL to
auto-detect owner/repo. Embedding credentials in the URL (user:token@host)
breaks that match and produces "path segment [0] is empty". Store creds
via git credential helper instead and pass a clean URL to uh-cli.
Also adds set -x to the PR step for shell-level tracing in CI logs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After a successful Go image build, uh-cli opens a PR against
kacerr/home-kubernetes that bumps the fuj-management Deployment
(namespace fuj) to the newly published image tag. Supports
workflow_run auto-trigger and workflow_dispatch with dry-run option.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Configured-month cases now read expected values from AdultFeeMonthlyRate /
JuniorFeeMonthlyRate via a mustRate helper that panics if a test month is
removed from the map. Fallback cases use AdultFeeDefault / JuniorFeeDefault.
This way the tests verify dispatch logic (0/1/2+ branching, map vs. fallback)
without breaking when rates are intentionally updated in the map.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Instead of hiding older months entirely, show all months in the from/to
selectors but default the from-select to the last MONTHS_TO_SHOW months
on page load. The "All" button resets to full history as before.
Python: passes months_to_show to render_template, IIFE sets fromSelect.value.
Go: adds MonthsToShow to response structs, data-months-to-show attr in
templates, filters.js reads it and defaults fromSelect after hideFutureMonths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show only the last MONTHS_TO_SHOW months (default 5) in the fee table columns
so the page fits on screen without horizontal scrolling. Reconciliation still
runs over the full month history so balances, credits, and debts are unaffected.
Set MONTHS_TO_SHOW=0 to show all months. Implemented in both Python and Go.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Juniors with exactly 1 session get expected='?' (manual-review marker
from attendance.py). The fill-first allocation block summed and cast
expected values numerically without guarding against this, causing a
TypeError: unsupported operand type(s) for +: 'int' and 'str' on the
/juniors route whenever any matched payment landed on such a month.
Add _expected_amount() helper that coerces non-numeric markers to 0
(same convention the final-balance calculation at line 512 already used)
and apply it in the two failing spots plus the existing isinstance check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 2026-05: 700 → 450 CZK
- 2026-06, 07, 08: 600 CZK (new months)
Changes are mirrored in both Python (scripts/attendance.py) and Go (go/internal/domain/fees/fees.go).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add second Fio account (CZ0820100000002502035405 / 2502035405/2010).
Both accounts are fetched on every sync run and combined before dedup,
so the payments sheet accumulates transactions from either account.
QR codes now default to the new account.
Go:
- config.go: hardcoded Accounts/LoadedAccount slice replaces scalar
BankAccount + FioAPIToken; Config.BankAccount renamed QRAccount;
per-account tokens via FIO_API_TOKEN_NEW / FIO_API_TOKEN_OLD
- banksync.SyncToSheets: accepts []fio.Client, loops to combine txns
- cmd/fuj/main.go: buildFioClients helper; both sync call sites updated
- html_handler + build_adults/juniors: use Config.QRAccount
- New TestSyncToSheets_MultiAccount covers cross-account dedup
Python:
- config.py: ACCOUNTS list + LOADED_ACCOUNTS (tokens from env)
- fio_utils.py: fetch_transactions_for (per-account) +
fetch_transactions_all (loops all accounts)
- sync_fio_to_sheets.py: uses fetch_transactions_all
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace proportional split with a fill-first loop that allocates
min(remaining, deficit) to each matched month in user-supplied order,
where deficit = expected - already_paid. Prior transactions' contributions
are now properly accounted for, so a second payment on overlapping months
fills only what's still owed instead of splitting proportionally by total
expected. Surplus after all deficits are covered goes to the credit bucket.
Fixes: Matyáš Thér 200+550 showing 566/183 instead of 500/250.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fio's transparent page now serves recent rows as DD.MM.YY while older
rows stay DD.MM.YYYY. parse_czech_date only knew the 4-digit form so
every recent transfer was silently dropped — make sync-2026 reported
zero new transactions. Adds %d.%m.%y and %d/%m/%y to the format list,
mirroring the Go-side fix from 2026-05-07.
Also adds a Python analog of make go-sync-debug:
- --dry-run skips header write / append / sort and prints "would …" lines
- --print-fio-table prints aligned per-txn table with NEW/DUP status
- make sync-debug [DAYS=N] wrapper (default DAYS=30)
- always-on stderr diagnostics in fio_utils: which fetcher was chosen
(with FIO_API_TOKEN-unset lag warning) + raw-vs-filtered counts, so
this class of "scraper drops everything" bug surfaces immediately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add TestEmbedCompleteness and TestStaticAssetsServed in
go/internal/web/assets_test.go. The completeness guard walks the
on-disk templates/ and static/ directories and asserts every file is
present in the corresponding embed.FS, catching forgotten files on
future additions. The static mux test hits /static/css/app.css and all
JS files through the same http.FileServerFS wiring used in server.go,
confirming assets are served from the embedded FS with correct
Content-Type and a 404 for unknown paths.
Standalone binary smoke test passed manually: binary copied to /tmp
(no adjacent templates/ or static/), assets served correctly.
Closes M6.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace bare <a href=/qr> Pay buttons with <button data-*> elements that
open an in-page #qrModal (matching Python's showPayQR UX), driven by a
new payment-qr.js vanilla-JS IIFE module. Remove the now-dead qrHref /
qrHrefAll template helpers from render.go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>