All checks were successful
Deploy to K8s / deploy (push) Successful in 17s
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>
147 lines
6.0 KiB
Markdown
147 lines
6.0 KiB
Markdown
# 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`.
|