- 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>
6.4 KiB
M6.3 — Go-native /juniors page
Context
The Go rewrite is in milestone M6 (Go-native HTML frontend). M6.2 shipped the /adults page in commit c85748b; the progress tracker line 115 names M6.3 as:
/juniorspage: same structure + per-month J/A attendance breakdown +"?"sentinel rendering
The Go side already has buildJuniorsResponse (M5.2) that produces the full JSON view model, and the JuniorsResponse uses the same MemberRow/MonthCell/TotalCell types as AdultsResponse. Critically, buildJuniorMemberRow (go/internal/web/api/build_juniors.go:142) already bakes the J/A breakdown (e.g. "3/500 CZK (4:2J,2A)") and the "?" sentinel into MonthCell.Text — no template-level junior-specific logic is required for the M6.3 table.
Today juniors.tmpl is a 4-line "Coming in M6.3" placeholder and HTMLHandler.ServeJuniors (html_handler.go:35) just renders that placeholder. Goal of M6.3: produce a working /juniors page that mirrors the structure of /adults and reaches feature parity with the Python /juniors route.
Approach
Mirror the M6.2 pattern exactly:
- Extract
AssembleJuniors(ctx)from the JSON handler so HTML + JSON share one view-model assembly path (the parity contract). - Add
JuniorsPageDatawrapper type next toAdultsPageDatainrender.go. - Replace
juniors.tmplwith an adults-shaped layout. BecauseJuniorsResponseshares the same field names used byadults.tmpl(Months,RawMonths,Results,Totals,Credits,Debts,AttendanceURL,PaymentsURL,BankAccount,CurrentMonth), the template body is essentially copy/paste with one parity tweak (see "Decisions" below). - Wire
HTMLHandler.ServeJuniorsto callAssembleJuniorsand render the new template, parallel toServeAdults.
No new CSS, no new JS — filters.js and the cell-* classes (lifted in M6.1) already cover juniors.
Files to modify
| File | Change |
|---|---|
| go/internal/web/api/handler.go | Add AssembleJuniors(ctx) (JuniorsResponse, error); refactor existing ServeJuniors to call it (mirrors AssembleAdults at line 47). |
| go/internal/web/render.go | Add JuniorsPageData struct (PageData + Data api.JuniorsResponse + Error string). |
| go/internal/web/html_handler.go | Replace stub ServeJuniors with the same shape as ServeAdults (lines 20–33). |
| go/internal/web/templates/juniors.tmpl | Replace placeholder with a structurally identical copy of adults.tmpl, with the title swapped to "Juniors Dashboard" and the Unmatched section removed (parity with Python). |
| docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md | Tick M6.3 once merged, append merge SHA. |
| CHANGELOG.md | New top-most entry per CLAUDE.md format. |
Decisions captured
- No "Unmatched Transactions" section on
/juniors. Python's templates/juniors.html renders only Credits + Debts (no Unmatched block). AlthoughJuniorsResponse.Unmatchedis populated, parity says we don't render it on juniors. The data stays available for the modal in M6.5. - Reuse
qrHref/qrHrefAlltemplate funcs from render.go:35–57. They are name-only (not adult-specific) and already ontmplFuncs. - Reuse the existing
cell-empty / cell-unpaid / cell-unpaid-current / cell-ok / cell-overriddencell-class branching fromadults.tmpllines 60–67 verbatim. The "?" sentinel is rendered as plain{{$cell.Text}}text — no template branch needed becausebuildJuniorMemberRowproduces"?"or"? CZK"already. - No template helper extraction (e.g. shared
_table_section.tmpl). The two templates will be near-duplicates, but extracting a shared partial now is premature — wait until M6.4 or later when patterns settle. M6.2 didn't extract one either. - Branch:
feat/go-m6-3-juniors-page, MR viatea pr createper CLAUDE.md.
Verification
End-to-end smoke (parallel-running both backends as in M5/M6.2):
cd go && make run(or whatever target boots the Go server on :8080).- Browser-load
http://localhost:8080/juniors. Confirm:- Member rows render with cells containing fee + attendance breakdown like
0/500 CZK (3:2J,1A). - At least one
"?"cell visible (any junior with exactly one session in some month). - Filter input narrows rows by name; From/To month selects hide columns via
data-month-idx. Paybutton visible on past-month unpaid cells; clicking links to/qr?.... (The QR endpoint itself lands in M6.6 — only the link target is in scope here.)Pay Allvisible on rows withBalance < 0.- TOTAL row sums correctly per month.
- Credits + Debts sections render when present; no Unmatched section.
- Member rows render with cells containing fee + attendance breakdown like
- JSON parity:
make parityagainst the M3 fixture corpus must still report zero non-allowlisted diffs for/api/juniors(theAssembleJuniorsextraction must not change wire output). - Compare side-by-side against Python
/juniorson :5001 for the same fixture; the table cells should match cell-for-cell modulo the known M5 allowlist.
Out of scope
- Modal (
[i]info button +#memberModal) — that is M6.5. - QR endpoint,
/sync-bank,/flush-cache,/versionpages — M6.6. - Embed.FS deploy verification — M6.7.
Plan-file relocation
Per CLAUDE.md "Plans" section, plan files belong in docs/plans/YYYY-MM-DD-HHMM-<slug>.md inside the repo. Plan mode forced this draft into ~/.claude/plans/; first step after ExitPlanMode is to copy this file to docs/plans/2026-05-08-HHMM-go-m6-3-juniors-page.md (resolving the timestamp at that moment) and commit it on the feature branch alongside the implementation.