# Plan: Limit /adults and /juniors to last N months (default 5) ## Context The `/adults` and `/juniors` dashboard tables render one column per month of attendance/fee history. As the season accumulates months, the tables grow wider than the screen. The goal is to show only the **last N months** by default (N = 5), with N configurable via an env var, so the tables fit on screen. This must be implemented in **both** the Python/Flask version and the Go version, keeping their behavior identical. ### Key correctness requirement Member **balances, credits, and debts** must continue to reflect *all* history, not just the visible window. Hiding older columns must not hide older debt. This is naturally satisfied because in both codebases the balance math iterates over the full per-member month map produced by reconcile, while only the *column rendering* iterates over the passed-in month list: - Python `scripts/views.py`: `settled_balance` / `_settled_balance` / credits / debts loop over `data["months"].items()` (full), whereas columns, totals, and per-row cells loop over `sorted_months`. - Go `build_adults.go` / `build_juniors.go`: `settledBalance(mr, ...)` loops over `mr.Months` (full); columns/totals/cells loop over `sortedMonths`. Therefore the correct seam is: run `reconcile()` / `Reconcile()` on the **full** month list, then trim the list to the last N **only for the view-model builder**. The member-details modal also keeps full history because it reads the untrimmed `member_data` / `MemberData`. ## Approach Add a `MONTHS_TO_SHOW` tunable (default 5; `<= 0` means "show all" as an escape hatch). Trim `sorted_months`/`sortedMonths` to the last N immediately before the view-model builder, leaving reconcile on the full list. ### Python 1. **`scripts/config.py`** — add, next to the existing `CACHE_TTL_SECONDS` pattern (`int(os.environ.get(...))`): ```python MONTHS_TO_SHOW = int(os.environ.get("MONTHS_TO_SHOW", 5)) ``` 2. **`app.py`** — add a small helper (module-level) and apply it in all four routes that build the adults/juniors view models: ```python from config import MONTHS_TO_SHOW # add to existing config import def _last_n_months(months): return months[-MONTHS_TO_SHOW:] if MONTHS_TO_SHOW > 0 else months ``` In each route, keep `reconcile(members, sorted_months, ...)` on the full list, then pass the trimmed list to the builder: ```python result = reconcile(members, sorted_months, transactions, exceptions) display_months = _last_n_months(sorted_months) vm = build_adults_view_model(members, display_months, result, ...) ``` Apply to: `adults_view()` (~`app.py:226`), `juniors_view()` (~`app.py:260`), and the JSON twins `/api/adults` (~`app.py:161`) and `/api/juniors` (~`app.py:184`) for parity. No changes to `scripts/views.py` — it already derives `months`, `raw_months`, `totals`, and per-row `row.months` from whatever month list it receives, and balances/credits/debts from the full `result`. ### Go 1. **`go/internal/config/config.go`** — add `MonthsToShow int` to the `Config` struct (~line 57-67), populate it in `Load()` (~line 80-90) with a new integer helper modeled on `envDuration`: ```go func envInt(key string, fallback int) int { if v := os.Getenv(key); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return fallback } ``` ```go MonthsToShow: envInt("MONTHS_TO_SHOW", 5), ``` (Note: unlike `envDuration`, accept `<= 0` so it can mean "show all".) 2. **`go/internal/web/api/handler.go`** — in `AssembleAdults` (lines 50-57) and `AssembleJuniors` (lines 71-78), keep `Reconcile` on the full `sortedMonths`, then trim before the builder: ```go result := domreconcile.Reconcile(members, sortedMonths, txns, exceptions, time.Now().Year()) displayMonths := lastNMonths(sortedMonths, h.Config.MonthsToShow) return buildAdultsResponse(members, displayMonths, result, txns, h.Config, time.Now().Format("2006-01")), nil ``` Add a small helper (e.g. in handler.go): ```go func lastNMonths(months []string, n int) []string { if n > 0 && len(months) > n { return months[len(months)-n:] } return months } ``` No changes to `build_adults.go` / `build_juniors.go` or the templates — they already derive `Months`, `RawMonths`, `Totals`, and per-row cells from the passed-in `sortedMonths`, and balances/credits/debts from the full `result.Members[...]`. ## Critical files - `scripts/config.py` — new `MONTHS_TO_SHOW` constant. - `app.py` — trim helper + apply in 4 routes (HTML + JSON, adults + juniors). - `go/internal/config/config.go` — `MonthsToShow` field + `envInt` helper. - `go/internal/web/api/handler.go` — `lastNMonths` helper + apply in `AssembleAdults` / `AssembleJuniors`. No template or `views.py` / `build_*.go` changes required. ## Verification Python: - `make test` (and a targeted run, e.g. `PYTHONPATH=scripts:. python -m unittest tests.test_app`). - `make web`, open `/adults` and `/juniors`: confirm exactly 5 month columns by default, and that the **Balance** column / credits / debts are unchanged from before (compare against an untrimmed run, e.g. `MONTHS_TO_SHOW=0`). - `MONTHS_TO_SHOW=3 make web` → 3 columns; `MONTHS_TO_SHOW=0` → all columns. - Spot-check that the month-range filter dropdowns and a member-details modal (full history) still work. Go: - `cd go && go build ./... && go test ./...`. - Run the Go server, open `/adults` and `/juniors`: same checks as above (default 5 columns, balances unchanged, `MONTHS_TO_SHOW` env override works). - Confirm Python and Go render the same number of columns and identical balances for the same data. ## Housekeeping - Per `CLAUDE.md`, copy this plan to `docs/plans/YYYY-MM-DD-HHMM-months-to-show.md` during implementation (create `docs/plans/` if missing) and add a `CHANGELOG.md` entry once verified. - Per `CLAUDE.md`, this is a feature → do it on a `feat/months-to-show` branch and open a Gitea MR with `tea`; do not commit to `main`.