# 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.html` to use `shared/cards-list.html` (171 → 56 LOC) and `store/card-detail.html` to use `shared/card-detail-view.html` (205 → 55 LOC). JS factories collapsed similarly (166 → 18 and 152 → 20). The shared `card-detail-view.html` partial gained three boolean flags (`show_copy_buttons`, `show_category_column`, `show_pagination`) and the shared `loyaltyCardDetailView` factory gained optional pagination + `txLabels`/`txNotes` config so store could preserve its enhancements. Added `loyalty.shared.card_detail.col_category` to en/fr/de/lb locale files. Fixed a latent bug in the shared factory's `formatDateTime` (was calling `toLocaleDateString` with hour/minute opts that get silently ignored).
- **Phase B (codify).** Wrote `docs/architecture/persona-template-consolidation.md` describing 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 LOC` that doesn't include a `*/shared/*` partial. Wired both check sites in `scripts/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` (added `shared.card_detail.col_category`).
- **Docs:** new `docs/architecture/persona-template-consolidation.md`, this proposal, `mkdocs.yml` nav.
- **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-016` from warning to error — wait until ≥1 other module migrates.
- Consolidating the three `analytics.html` wrappers further — they're already minimal given each persona has materially different content (admin wallet status, store advanced charts).
- Settings consolidation — `merchant/settings.html` and `store/settings.html` are different features (read-only PIN/permissions display vs editable program form), not duplicates.
