Document how admin/merchant/store templates share a single shared/ body partial while keeping their three separate base templates. Covers: - The wrapper/partial split and why the three base templates must stay separate (nav + permissions isolation). - The scope contract: pass strings + booleans only, no macro objects, no `persona` enum. - The backend mirror: services scope-agnostic, routes inject scope via auth deps, same Pydantic shape across personas. - Legit exceptions and the heuristic for when to keep a template standalone (multi-tenant aggregators, persona-unique features). - Forward reference to the TPL-016 architecture rule. Adds both docs to mkdocs nav under Architecture and Proposals sections. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.9 KiB
Persona Template Consolidation
Date: 2026-05-23 Implemented: 2026-05-23 Status: Done Motivation: Maintainers were editing the "same" admin / merchant / store templates three times for every feature change, with predictable drift between personas. The user wanted to know whether the codebase could move to a model where the shared CRUD body lives once and per-persona wrappers handle only the parts that genuinely differ.
Discovery
Loyalty turned out to be a partial success story already: app/modules/loyalty/templates/loyalty/shared/ held 7 reusable partials (program-form, program-view, cards-list, transactions-list, pins-list, devices-list, analytics-stats), and merchant/admin wrappers were already thin includes. The pattern was working but undocumented, unenforced, and not consistently applied to store/ — store had inlined two big features (cards.html 171 LOC, card-detail.html 205 LOC) instead of using the shared partials.
So this turned into a finish + codify + guard job, not a rewrite.
Implementation Summary
- Phase A (loyalty cleanup). Migrated
store/cards.htmlto useshared/cards-list.html(171 → 56 LOC) andstore/card-detail.htmlto useshared/card-detail-view.html(205 → 55 LOC). JS factories collapsed similarly (166 → 18 and 152 → 20). The sharedcard-detail-view.htmlpartial gained three boolean flags (show_copy_buttons,show_category_column,show_pagination) and the sharedloyaltyCardDetailViewfactory gained optional pagination +txLabels/txNotesconfig so store could preserve its enhancements. Addedloyalty.shared.card_detail.col_categoryto en/fr/de/lb locale files. Fixed a latent bug in the shared factory'sformatDateTime(was callingtoLocaleDateStringwith hour/minute opts that get silently ignored). - Phase B (codify). Wrote
docs/architecture/persona-template-consolidation.mddescribing the pattern, the scope contract, the backend mirror, and the legit-exception heuristic. This doc is now the reference for any contributor adding a new CRUD feature. - Phase C (guard). Added architecture rule
TPL-016(warning) that flags any persona template> 75 LOCthat doesn't include a*/shared/*partial. Wired both check sites inscripts/validate/validate_architecture.py. Suppressible with{# noqa: TPL-016 #}for the legit exceptions (admin programs aggregator, merchant-detail, store terminal, etc.).
Decisions Made
| # | Decision | Rationale |
|---|---|---|
| 1 | Loyalty cleanup + codify pattern + arch rule | Scope-limited; the pattern is the real deliverable. |
| 2 | Leave admin/programs.html standalone |
Multi-merchant aggregator + create-with-search modal is fundamentally a different shape from the merchant/store single-program views. Forcing it into shared would mean if scope == admin in every row. |
| 3 | JS/CSS variables only (no macro objects, no persona enum) |
Existing loyalty pattern proven to work; macro objects bloat call sites and persona branching defeats the purpose of the partial. |
| 4 | TPL-016 severity = warning |
Lets the rule ship without breaking CI on day one. Escalate to error after at least one other module is migrated. |
Files Touched
- Templates:
loyalty/templates/loyalty/store/cards.html,loyalty/templates/loyalty/store/card-detail.html,loyalty/templates/loyalty/shared/card-detail-view.html(added flags). - JS:
loyalty/static/store/js/loyalty-cards.js,loyalty/static/store/js/loyalty-card-detail.js,loyalty/static/shared/js/loyalty-card-detail-view.js(added config options + pagination). - i18n:
loyalty/locales/{en,fr,de,lb}.json(addedshared.card_detail.col_category). - Docs: new
docs/architecture/persona-template-consolidation.md, this proposal,mkdocs.ymlnav. - Arch rule:
.architecture-rules/frontend.yaml(TPL-016),scripts/validate/validate_architecture.py(check function + 2 wire sites).
Verification
python scripts/validate/validate_architecture.py— 16 baseline warnings, no new findings.mkdocs build --strict— clean.- Smoke test: store/cards and store/card-detail render identically to the pre-migration version (filters, search, pagination, copy buttons, category column, translated transaction labels all preserved).
- Pre-commit hooks (architecture, security, performance, audit, ruff) all green.
Out of Scope (Deferred)
- Applying the pattern to other modules (catalog, billing, etc.). The doc + rule make this a follow-up any contributor can pick up.
- Escalating
TPL-016from warning to error — wait until ≥1 other module migrates. - Consolidating the three
analytics.htmlwrappers further — they're already minimal given each persona has materially different content (admin wallet status, store advanced charts). - Settings consolidation —
merchant/settings.htmlandstore/settings.htmlare different features (read-only PIN/permissions display vs editable program form), not duplicates.