Files
fuj-management/docs/plans/2026-06-08-1118-months-to-show.md
Jan Novak c0487e3af0
All checks were successful
Deploy to K8s / deploy (push) Successful in 17s
feat(display): limit /adults and /juniors to last N months by default
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>
2026-06-08 11:18:55 +02:00

6.0 KiB

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(...))):

    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:

    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:

    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:

    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
    }
    
    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:

    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):

    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.goMonthsToShow field + envInt helper.
  • go/internal/web/api/handler.golastNMonths 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.