64c8a0ec2c5380d37a6cc8d9e5a24828ba28a1ee
1276 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 64c8a0ec2c |
chore(ops): prune build cache in deploy.sh + document rescale & disk maintenance
All checks were successful
deploy.sh already pruned old images but never build cache — the larger half of disk creep from CI rebuilds (root fs hit 83% on prod). Add `docker builder prune --filter until=168h` alongside the existing image prune so cleanup happens every deploy, version-controlled, no host cron. Docs (hetzner-server-setup.md, Maintenance section): - New "Rescaling / Upgrading the Server" — when/why, same-arch (Arm/CAX) + CPU-RAM-only vs irreversible disk-expand constraints, poweroff→rescale→ power-on→verify steps, and the Arm-capacity-unavailable-in-DC caveat. - New "Disk Maintenance (Docker Pruning)" — emergency manual prune + the automated deploy.sh approach. - Fixed stale Resource Budget: cadvisor 128→192 MB (matches compose), total 672→736 MB, and "live-upgrade" wording (rescale needs a power-off). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| ac7850b880 |
docs(auth): record 2026-05-31 401-redirect review + dev/prod symmetry proposal
Some checks failed
Reviewed commit
|
|||
| 4423f0a5ed |
fix(api-client): generalize 401 redirect from /account/* to all 4 personas
All checks were successful
Yesterday's redirectIfCustomerAreaUnauthorized was scoped to /account/*
only. Admin, store, and merchant pages still hit the same UX gap when
an AJAX call returned 401 on token expiry: apiClient cleared tokens
and threw, leaving the page in a broken state with whatever generic
error UI the caller had wired up — no redirect, no `?next=` round-trip,
identical bug to the customer flicker we fixed in `b04b36a2` /
`6564f138`.
Rename and dispatch by path:
- /account/* (not /account/login) → /account/login?next=…
- /admin/* (not /admin/login) → /admin/login?next=…
- /merchants/* (not /merchants/login) → /merchants/login?next=…
- /store/{code}/* (not /store/{code}/login) → /store/{code}/login?next=…
- anything else → return false (caller throws)
Store paths include the per-store code, so the helper does a small regex
to extract `{code}` from the current pathname and builds the persona's
login URL with the right prefix.
All three 401 handlers in apiClient (request, requestFormData, getBlob)
already wrap this with the `return new Promise(() => {})` pattern from
|
|||
| 947ca43c7b |
docs(loyalty): record 2026-05-30 afternoon — prod-readiness 1-3 done + alerting back online
All checks were successful
Picked up the morning's carry-over and ran the full prod-readiness chain end-to-end. Resolution: SG credential out of git permanently via untrack + .example template ( |
|||
| 1227567d08 |
docs(hetzner): record 25/465 egress block + mail1 SMTP setup (5h debug payback)
Some checks failed
Hetzner Cloud silently blocks outbound TCP 25 and 465 on every Cloud Server. The block sits upstream of the VM — UFW and iptables look completely clean — so it presents as a generic "connection times out" that's easy to misdiagnose as a credential or DNS issue. Spent ~5 hours on 2026-05-30 working through swaks/tcpdump/auth-backend hypotheses before finding Hetzner's own docs that mention the policy. Two doc additions: - Step 4 (Firewall Configuration) gets a warning admonition right after the UFW status check. Explains the upstream nature of the block, gives the symptom signature (nc to 587 succeeds, nc to 465 silently times out), and includes the auto-approved unblock ticket template with sample text. - Step 19.5 (Alertmanager SMTP) gets a "live prod uses mail1.myservices.hosting:465" callout reflecting the reality that the SendGrid setup documented in that section is no longer how this prod env is wired. The callout captures the actual smarthost config (with smtp_auth_password kept gitignored, only .example ships in repo), the two prerequisites (Hetzner unblock + implicit-TLS-aware smarthost port), and the redacted swaks verification command. The rest of §19.5 stays as a reference for greenfield deploys that prefer SendGrid. Saves the next person from repeating the same hours-long detour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| e44f5c0458 |
chore(alertmanager): untrack alertmanager.yml + ship .example template (post-SMTP migration)
All checks were successful
Yesterday's deploy debug surfaced a SendGrid API key pasted into the
tracked monitoring/alertmanager/alertmanager.yml on prod, with the
in-repo file literally captioning the field "TODO: Paste your SG.xxx
API key here" — actively encouraging the anti-pattern. Forensic
follow-up (bash history lines 290-357) confirmed it was a user-driven
nano edit that was never committed, just left as a long-running local
mod. Three problems collapsed into this finding:
1. Real SMTP credential lived in a tracked git file on prod.
2. The SendGrid → mail1.myservices.hosting SMTP migration never
touched alertmanager — it still pointed at smtp.sendgrid.net.
3. The alertmanager container has been Up 13 days with the
pre-paste empty smtp_auth_password loaded from disk, so prod's
email alerting has been silently failing.
Resolution shipped here:
- `git rm --cached monitoring/alertmanager/alertmanager.yml` so the
prod-edited file on each host stops being a tracked file and the
credential can't accidentally reach git again.
- Add `monitoring/alertmanager/alertmanager.yml` to .gitignore.
- Ship `monitoring/alertmanager/alertmanager.yml.example` as the
template — pre-filled with the post-migration non-secret routing
(`mail1.myservices.hosting:587`, `support@wizard.lu` auth,
`alerts@wizard.lu` From for inbox clarity), only `smtp_auth_password`
left as `CHANGEME`. Includes inline guidance for the From-vs-auth
rule that some SMTP relays enforce.
Per-host steps (Hetzner): backup the prod-edited file → revert local
change → pull → copy the template over the old file → fill in the
password → SIGHUP alertmanager. Doc reference will follow in the next
commit (Hetzner deploy doc still needs an "alertmanager.yml lives
outside git" footnote).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| cff0b3f911 |
docs(loyalty): record 2026-05-30 widget i18n + cache-bust + 401 redirect + alertmanager finding
All checks were successful
Nine code commits shipped today ( |
|||
| c13e8e29b5 |
feat(deploy): scripts/deploy-api-only.sh + Hetzner doc for manual code-only redeploys
Some checks failed
Manual deploys had been using a bare `git pull && docker compose up -d
--build api` sequence, which works for the container itself but silently
skipped writing `.build-info`. The stale `.build-info` left
`?v=<commit-sha>` pointing at the previous deploy's SHA on every shared
JS/CSS URL — so browsers happily kept cached pre-fix assets even after
a successful rebuild. Bit us today: ~5 hours of "is this even deployed?"
debugging on the loyalty-dashboard redirect-flicker fix.
deploy.sh wasn't a substitute because it's a CI/CD script: stashes
working tree, runs alembic, restarts every service in the full profile
(db, redis, api, celery-worker, celery-beat, flower), 60s health budget.
Heavy and disruptive for an api-only hotfix.
New scripts/deploy-api-only.sh fills the gap with the narrow path:
- Refuses if working tree is dirty (no silent stash → no pop conflicts).
- git pull --ff-only.
- Writes .build-info (the critical missing step).
- docker compose -f docker-compose.yml --profile full up -d --build api
(only the api service — db/redis/celery untouched).
- Tight 30s health budget since DB doesn't need to come back up.
- Exit codes 0/1/2/3 for clean automation.
docs/deployment/hetzner-server-setup.md §16.5 split into 16.5a
(code-only — points at the new script as the default) and 16.5b
(full deploy fallback — kept the existing deploy.sh path for migrations
/ Dockerfile / docker-compose / requirements changes). §12 footnote on
.build-info refreshed to mention both scripts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| c9fe717184 |
fix(login-i18n): translate the 3 hardcoded JS toasts in customer login
Some checks failed
Three more hardcoded English strings in customers/storefront/login.html were still bypassing i18n because they were emitted from Alpine showAlert() calls in <script>: - "Account created successfully! Please sign in." (post-register toast) - "Login successful! Redirecting..." (post-login toast) - "Invalid email or password" (login-error fallback) Same pattern as the earlier forgot/reset-password sweep: defined window.__customerLoginI18n with `tojson` server-rendered values, read them once at function entry as `const i18n = ...`, and swapped each hardcoded string for an i18n property. Two new auth.* keys × 4 locales (registration_success_signin, login_success_redirecting). The third reuses the existing auth.invalid_credentials. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| bbb481aad4 |
fix(login-i18n): translate the "Welcome back ..." subtitle on customer login
Some checks failed
Last unilingual string in customers/storefront/login.html was the
"Welcome back to your shopping experience" line on the branding side.
Was flagged as scope-creep-skip during the original Test 5 i18n sweep
since the user only complained about reset-password and dashboard
pages; surfaced now that the 401 redirect lands users on /account/login
mid-French-session.
Replaced with {{ _("auth.login_subtitle") }} and added the key to all
4 core locales (en/fr/de/lb). Reworded slightly from "shopping
experience" since loyalty storefronts have no catalog — same reasoning
as the auth.continue_shopping → auth.back_to_home rename earlier.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 6564f13898 |
fix(api-client): never-resolving promise on 401 redirect kills the wrong-UI flash
Some checks failed
The
|
|||
| b04b36a232 |
fix(loyalty-storefront): initial loading=true so 401 redirect doesn't flash wrong UI
Some checks failed
Loyalty dashboard's "Rejoignez notre programme" CTA flashed for one render tick on a 401-triggered redirect: Alpine initialised the component with loading=false + card=null, the template rendered `x-show="!loading && !card"`, then the async API call completed with 401, apiClient.redirectIfCustomerAreaUnauthorized fired, and the browser navigated away. Flip the initial state to loading=true so both the card view (x-show="!loading && card") and the join CTA (x-show="!loading && !card") stay hidden until the API call resolves. The template's existing `x-show="loading"` spinner branch covers the in-flight window. Same fix in loyalty-history.js (same x-show pattern). Customer profile + addresses already initialise loading=true, so no flicker there. User repro'd by deleting localStorage.customer_token + F5 on /account/loyalty: pre-fix flashed the CTA for ~half a second before redirect; post-fix should jump straight to the spinner, then to /account/login. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 856db328b5 |
chore(api-client): drop dead /shop/ path branches (migrated to /storefront long ago)
Some checks failed
The customer-area used to be mounted at /shop/* and was migrated to
/storefront long ago, but apiClient.js still carried the dead /shop/
checks alongside the live ones added in
|
|||
| a0ae638821 |
fix(storefront-auth): apiClient redirects to login on 401 from /account/*
Some checks failed
When the customer's JWT (30-min TTL via JWT_EXPIRE_MINUTES) expires in localStorage, subsequent API calls from a customer-area page returned 401 → callers showed an unrelated error UI (loyalty dashboard rendered the "join now" CTA because card came back null on the catch path). Three changes in static/shared/js/api-client.js: 1. Path detection in getToken() + clearTokens() now recognises /account/* and /api/v1/storefront/* as customer-area routes (the only existing checks were for /shop/* which was never used in this codebase). Also clears customer_user alongside customer_token. 2. New redirectIfCustomerAreaUnauthorized() helper: on a /account/* page, sends the browser to /account/login?next=<current path> (with a guard to skip the redirect when already on the login page, avoiding loops). Called from all three 401 paths (request, requestFormData, getBlob). 3. login.html now honours the ?next= query param (in addition to the legacy ?return=), so the redirect lands the user back where their session expired. Other personas (admin/store/merchant) are unaffected — the helper is a no-op outside /account/*. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 3ce9468397 |
fix(cache-bust): close FE-024 gaps so every JS/CSS gets ?v=<sha>
Some checks failed
The 2026-05-18 cache-busting system was only catching a fraction of
includes because:
1. FE-024 anti-pattern only matched `'<module>_static'` mount names
(e.g. `'core_static'`, `'billing_static'`). The bare `'static'`
mount — which is what every persona base.html uses for shared JS,
CSS, and Tailwind output — never matched.
2. The rule explicitly excluded `base.html` files, which are exactly
where most of the shared JS/CSS includes live.
User reported only a handful of files had `?v=` in their Network tab.
Swept 5 persona base.html + 15 standalone templates (login/register/
forgot/reset password, error pages, onboarding, invitation-accept,
admin module-info/config, etc.) — 53 url_for('static', ...) refs for
.js/.css converted to static_v(request, 'static', ...).
Then tightened FE-024:
- Added an anti-pattern for the bare `'static'` mount.
- Dropped `base.html` from exceptions (kept `partials/`).
Re-running the validator: same 126-warning baseline, 0 FE-024 hits.
Now every deploy flips the `?v=<sha>` query string on every shared
asset; browsers refetch automatically without a hard refresh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 5f359283bc |
fix(storefront-i18n): dashboard widgets translate + correct customer-module key paths
Some checks failed
Two bugs from Test 5.1 on FR storefront dashboard: 1. Loyalty + Orders dashboard cards (`StorefrontDashboardCard.title`/ `subtitle`/`value_label`) were hardcoded English. Added `language` to `WidgetContext`; customer dashboard route passes `request.state.language` through; loyalty and orders widget providers now call `translate(..., context.language)` with new `widget.*` i18n keys × 4 locales each. 2. Customer-module locale JSON has redundant top-level `customers` wrapper, so after the module-locale loader auto-namespaces under module code `customers`, the actual key path is `customers.customers.customer_number` (matches the existing `loyalty.loyalty.wallet.apple` pattern). My earlier sweep used the single-prefix path for 8 references — fixed all to double-prefix. Both bugs were visible end-of-day yesterday after the api container recreate landed `1bade6e6`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| acbe2eff1a |
docs(loyalty): record 2026-05-29 Test 5.0 i18n sweep + FR/DE email accents
All checks were successful
Five-issue triage shipped as four commits today: storefront i18n sweep ( |
|||
| 213a60533c |
fix(seed): correct sys.path so seed_email_templates_core can import app.*
Some checks failed
Script lived at scripts/seed/seed_email_templates_core.py and inserted Path(__file__).parent.parent into sys.path — that resolves to scripts/, not the project root, so `from app.core.database import get_db` raises ModuleNotFoundError when run from inside the container. The loyalty sibling already uses parent.parent.parent correctly. Hit during the FR/DE password_reset reseed on prod. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 7e3a1cb93f |
chore(skills): generalize loyalty-wrap into topic-agnostic /wrap
Some checks failed
Rewrites the end-of-day wrap skill so it works for any session topic (not just the loyalty E2E walkthrough) and is safe to run with other agents working on the same repo concurrently. Detects the session's topic, creates a memory file if no matching one exists, only edits a proposal doc if one already exists for the topic, and pushes via fetch + pull --rebase + retry-once instead of bare push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 1bade6e6b2 |
fix(storefront): broken Alpine x-text from |tojson inside double-quoted attribute
Some checks failed
Dashboard unread-messages count crashed Alpine with
"expected expression, got '}'" because I emitted Jinja {{ _(...) | tojson }}
directly inside x-text="..." — the JSON's double quotes broke out of the
attribute, leaving Alpine to parse a malformed expression.
The messages card has its own nested x-data="{ unreadCount: 0 }" scope,
so the parent component's i18n property isn't reachable. Moved the
singular/plural strings onto window.__accountDashboardI18n (added next
to logoutSuccess/logoutFailed) and referenced them by global path from
x-text — no quoting collision and the nested scope sees window fine.
Other touched templates (login/register/forgot/reset-password) already
use x-data='...' (single-quoted outer attribute) for their |tojson
language-selector args, so this collision only existed in dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 36fd37813b |
fix(email): DE password_reset umlauts (zurucksetzen → zurücksetzen etc.)
Some checks failed
Same accent-stripped pattern as the FR fix in
|
|||
| b463c6bf48 |
fix(email): FR password_reset accents + store-name signature on all 4 locales
Some checks failed
User reported the password_reset email body had unaccented French
("demande" instead of "demandé", "L'equipe" instead of "L'équipe") and
the signature was the generic "L'équipe" without the store name.
FR template was missing accents throughout — fixed all of them:
Envoye→Envoyé, Reinitialiser→Réinitialiser, recu→reçu, reinitialisation
→réinitialisation, creer→créer, demande→demandé, equipe→équipe.
Signature on all 4 locales now includes {{ store_name }} (auto-injected
by EmailService.get_branding), so users see "L'équipe Fashion Hub" /
"The Fashion Hub Team" / "Das Fashion Hub Team" / "D'Fashion Hub Team"
instead of an unbranded "The Team".
The seeder is idempotent (upsert on code+language), so re-running
seed_email_templates_core.py on prod will update the existing rows in
place — no DB wipe needed.
Note: DE template still has missing umlauts (zurucksetzen→zurücksetzen,
Schaltflache→Schaltfläche, lauft→läuft, etc.) — left for a separate
DE/LB quality sweep since the user only reported FR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 10a99f98fe |
fix(storefront): i18n sweep + locale-aware reset-password and welcome email
Some checks failed
Test 5 (storefront password reset + customer dashboard) surfaced five
issues that all traced back to missing i18n plumbing:
- Forgot-password email arrived in EN regardless of storefront locale —
handler now prefers request.state.language over customer.preferred_language,
and loyalty self-enrollment backfills preferred_language for new + returning
customers so future locale-sensitive flows hit the right language without
being told twice.
- reset-password.html rendered "undefined" icon boxes because $icon magic
wasn't loaded in the standalone page — replaced with inline SVGs matching
the forgot-password.html convention.
- reset-password.html was hardcoded English: added lang attr, full _()
sweep (22 new auth.* keys × 4 locales), language selector, and JS
validation strings exposed via tojson.
- "Continue shopping" CTA renamed to "Back to Home" (auth.back_to_home,
4 locales) on login + forgot + reset — loyalty storefronts have no
catalog to continue to, mirroring the earlier enroll-success rename.
- /account dashboard, profile, addresses were hardcoded English in the
body (menu was FR because base layout uses _()). New customers.storefront
.pages.{dashboard,profile,addresses}.* namespace (~80 keys × 4 locales),
templates updated, Alpine JS strings injected via window.__*I18n.
18 files, 18 changed; arch validation: 126 warnings before = 126 after,
mkdocs --strict clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| f04cbb8ca2 |
docs(audit): lessons learned from loyalty migration
All checks were successful
Adds a post-audit section to the persona-template consolidation audit capturing what came out of the in-prod card-detail test on rewardflow.lu vs fashionhub.rewardflow.lu: - Template alignment != data alignment: shared partial guarantees the markup is the same per persona, NOT that the API response is. Loyalty's category column rendered empty on merchant + admin because only the store route enriched category_names. Future migrations should diff API response shapes per persona, not just templates. Fixed in |
|||
| eaf180c64f |
feat(arch-rules): JS-016 blocks hardcoded 'en-US' in JS at error severity
Some checks failed
Architecture rule that fails CI on any new toLocaleDateString / toLocaleString / toLocaleTimeString / new Intl.* call that hardcodes 'en-US' instead of using I18n.locale. The whole codebase was cleaned in the preceding commits ( |
|||
| bb4c400436 |
fix(loyalty): sweep remaining hardcoded 'en-US' in persona JS files
Follow-up to |
|||
| 06e59f73b3 |
fix(i18n): sweep hardcoded 'en-US' from non-loyalty JS to I18n.locale
Some checks failed
Follow-up to
|
|||
| a21dbbcddf |
docs(loyalty): record 2026-05-24 Test 4 + storefront auth body-schema fix
Some checks failed
End-of-day update. - Test 4 (cross-store redemption) verified: card #5's transaction history now spans store_id=4 (FASHIONHUB, all the earnings) and store_id=5 (FASHIONOUTLET, today's -100 redemption). Cross-location flow confirmed. - Bug found + fixed ( |
|||
| 478c3a9c50 |
fix(storefront-auth): forgot/reset password accept JSON body, not query
Some checks failed
POST /api/v1/storefront/auth/forgot-password and .../reset-password were
both declared with bare `email: str` / `reset_token: str, new_password: str`
parameters. FastAPI treats unannotated str params as query parameters, so
the frontend's JSON body was ignored and the endpoint 422'd with
"missing query parameter 'email'". The docstrings on both endpoints
already said "Request Body" — intent was clear, implementation drifted.
Add two new Pydantic body schemas in tenancy/schemas/auth.py:
PasswordResetRequest { email: str } (forgot)
PasswordResetConfirm { reset_token: str, new_password: str } (reset)
Re-export from tenancy/schemas/__init__.py, import in
customers/routes/api/storefront.py, and switch both endpoint signatures
to take `body: <Schema>`. Internal usage reads body.email / body.reset_token
/ body.new_password.
Surfaced during Test 5 when user clicked "forgot password" on the customer
storefront login page to set a password for the first time after a
self-enrollment flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| dd1f9af811 |
fix(i18n): locale-aware date/number formatting in loyalty factories
Some checks failed
Dates rendered in English even when the dashboard language was set to French (or any other locale). The 5 shared loyalty Alpine factories hardcoded 'en-US' in every toLocaleDateString / toLocaleString / Intl.NumberFormat call, ignoring the user's selected language. - Add `I18n.locale` getter to static/shared/js/i18n.js that returns the current dashboard language code (en/fr/de/lb). Falls back to 'en' if I18n isn't initialised yet. - Replace 'en-US' with I18n.locale in 5 loyalty shared factories: loyalty-cards-list, loyalty-card-detail-view, loyalty-transactions- list, loyalty-pins-list, loyalty-devices-list. - Also fix a latent bug in loyalty-transactions-list.formatDateTime that called toLocaleDateString with hour/minute opts (silently ignored — same bug previously fixed in loyalty-card-detail-view). Scoped to loyalty per session decision; other modules with the same hardcoded 'en-US' pattern (catalog, billing, etc.) are tracked as a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| d32c1fd545 |
fix(loyalty): show category column on card-detail for all 3 personas
The transaction-history table on the card-detail page rendered a
Category column only on the store frontend. Merchant and admin saw
five columns instead of six, even though the merchant report
prompted the audit (rewardflow.lu/merchants/loyalty/cards/6 vs
fashionhub.rewardflow.lu/store/.../cards/6).
Root cause was two layers:
- API: only store's GET /cards/{id}/transactions enriched
tx.category_names from tx.category_ids; merchant's and admin's
endpoints returned raw rows with category_names=null.
- Template: the shared partial's show_category_column flag was set
to true only on the store wrapper.
Backfill the same `category_service.validate_category_for_store`
lookup loop into merchant.py::get_card_transactions and
admin.py::get_merchant_card_transactions, accepting Request to read
request.state.language for localised category names. Add
`{% set show_category_column = true %}` to the merchant and admin
card-detail wrappers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 58a9e3f740 |
docs(proposals): cross-module persona-template consolidation audit
All checks were successful
Walks every multi-persona module's templates/{admin,merchant,store}/
and classifies each feature cluster as YES / PARTIAL / NO (legit
exception) / N/A for consolidation. Produces a prioritized 10-item
backlog across 3 waves (~8-9 days of focused work, ~3,100-3,500 LOC
removable).
Headline findings:
- 141 persona templates across 9 modules; loyalty already migrated
with 8 shared partials.
- Wave 1 (low risk, ~1,190 LOC): messaging.messages,
messaging.notifications, billing.billing-history.
- Wave 2 (3-persona my-account is the marquee item, ~1,430 LOC):
tenancy.my-account, tenancy.profile, messaging.email-templates.
- Wave 3 (higher complexity, ~1,820 LOC): tenancy.team,
catalog.store-products lists, customers.customers, tenancy.login
(security-gated).
- Anti-candidates documented inline so contributors don't try to
force-fit them (catalog product forms, marketplace admin vs store,
cms content-page-edit, etc.).
Backend services are uniformly scope-agnostic for every top-10
candidate -- no service/route work required.
Added to mkdocs nav under Proposals.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 82939c0005 |
docs(api): document apiClient error.status / errorCode / details surface
All checks were successful
Adds a "Frontend Error Handling (apiClient)" section to the error handling guide. Callers can branch on `error.errorCode` and read `error.details` to localise toasts instead of rendering the raw English `message`, as already done in the loyalty terminal cooldown handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| d03b96da1c |
chore(skill): add loyalty-wrap end-of-day skill
Some checks failed
Mechanises the closing routine we've been doing manually every session in the loyalty pre-launch walkthrough: update the persistent memory file + the go-live readiness proposal with today's progress, run mkdocs strict + architecture validation, commit and push the doc changes, then print a concise recap with next-session carry-over. Skill triggers on phrases like "call it a night", "save memory and docs", "wrap up", etc. when the session has been on the loyalty walkthrough. SKILL.md documents exact file paths, section templates, commit message shape, target remote (gitea), and the edge cases (TPL-016 noise from another agent's in-flight work, architecture errors blocking push, mkdocs strict failures). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 78621cb7bb |
docs(loyalty): record 2026-05-23 Test 3 + cooldown bug + routing investigation
Some checks failed
End-of-day update. Three things in this session: - Test 3 (staff stamps/points at terminal) — all 6 sub-steps verified on prod, including the new cooldown rejection and its localised toast. Tests 1-3 now done; Tests 4-8 ahead. - Cooldown bug ( |
|||
| aa8ca59493 |
fix(loyalty-terminal): localise cooldown toast (was raw English)
Some checks failed
When earn-points or add-stamp was rejected by the new cooldown
enforcement, the terminal showed the raw English error message from
the backend in the toast, even on FR / DE / LB locales:
"Transaction failed: Please wait 15 minutes between point-earning..."
Two-part fix:
1. static/shared/js/api-client.js — when raising apiError on non-OK
responses, also propagate the `details` payload from the response
body (alongside the existing errorCode). Without this the catch
sites had no structured access to e.g. cooldown_minutes.
2. loyalty-terminal.js — in the catch around the transaction dispatch,
when error.errorCode is POINTS_COOLDOWN or STAMP_COOLDOWN, render a
new localised key loyalty.store.terminal.cooldown_wait_minutes with
{minutes} interpolated from error.details.cooldown_minutes (with a
fallback to this.program.cooldown_minutes). Toast type switches to
'warning' since the rejection is soft (try again later) rather than
a hard failure. Other errors keep the existing 'transaction_failed'
path so nothing else regresses.
Added the new key in en / fr / de / lb under the existing
loyalty.store.terminal.* namespace (sibling of the existing
cooldown_active label).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| f9a15deed7 |
feat(arch-rules): TPL-016 flags large persona templates that skip shared/
Some checks failed
Architecture rule that warns on any template under
app/modules/<m>/templates/<m>/{admin,merchant,store}/*.html that
exceeds 75 LOC AND does not {% include %} a `*/shared/*` partial.
Catches new persona-specific templates that inline body content rather
than sharing it with sibling personas (the project-wide pain point that
prompted the persona-template-consolidation work).
- Rule definition in .architecture-rules/frontend.yaml at warning
severity. Suppressible per-file with `{# noqa: TPL-016 #}`.
- Check function `_check_persona_template_shared_include` in
scripts/validate/validate_architecture.py, wired at both template
validation sites (full scan + per-file -f mode).
- Loyalty was migrated under this rule and reports clean (5 legit
exceptions carry noqa with reason).
- First run surfaces ~110 warnings across other modules — the
migration backlog. Severity stays at warning until at least one
non-loyalty module is migrated, then escalate to error.
See docs/architecture/persona-template-consolidation.md for the
pattern this rule guards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| f82dce30ca |
docs(architecture): persona template consolidation pattern + proposal
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> |
|||
| c661c1e394 |
refactor(loyalty): migrate store cards + card-detail to shared partials
The store frontend was inlining two CRUD bodies that already had shared
equivalents under loyalty/templates/loyalty/shared/. Migrate them to the
established pattern (thin per-persona wrapper + shared body partial).
- store/cards.html: 171 -> 56 LOC. Now sets cards_api_prefix /
cards_base_url / show_store_filter=false and includes
shared/cards-list.html (same partial merchant already uses).
- store/card-detail.html: 205 -> 55 LOC. Includes
shared/card-detail-view.html with new flags show_copy_buttons,
show_category_column, show_pagination so its extras survive.
- shared/card-detail-view.html: gain those three boolean flags plus
reads txLabels/txNotes from the Alpine factory (empty defaults so
admin/merchant callers still get raw values).
- shared/loyalty-card-detail-view.js: factory accepts txLabels, txNotes,
paginate config; exposes pagination state unconditionally so the
partial's pagination macro resolves; fix latent bug where
formatDateTime called toLocaleDateString with ignored hour/minute
opts.
- store/loyalty-cards.js + loyalty-card-detail.js: now thin wrappers
calling the shared factories.
- locales/{en,fr,de,lb}.json: add loyalty.shared.card_detail.col_category
for the new optional column.
- Add `noqa: TPL-016` on the 5 legit-exception loyalty templates
(admin/programs aggregator, admin/merchant-settings, admin/wallet-debug,
store/enroll, store/terminal) ahead of the rule landing in a follow-up
commit. Note the per-file reason inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 93ab072f55 |
fix(loyalty): enforce cooldown on earn-points (was silently skipped)
Some checks failed
stamp_service.add_stamp checks card.last_stamp_at + cooldown_minutes before crediting and raises StampCooldownException if too soon. The parallel points_service.earn_points writes card.last_points_at but never reads it for enforcement — so cooldown_minutes was silently ignored for points-based programs. Mirror the stamps check in points_service.earn_points: after acquiring the row lock, compare now vs last_points_at + cooldown_minutes and raise the new PointsCooldownException if the cashier is inside the window. Add PointsCooldownException alongside StampCooldownException in exceptions.py with parity wording / error code POINTS_COOLDOWN. Surfaced during Test 3 step 3.6 — repeated earn-points calls for the same card kept crediting the customer with no rate limit even though the program's cooldown_minutes was set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 4b64233b5f |
docs(loyalty): record 2026-05-19 Test 2 complete + subtitle fix
All checks were successful
End-of-day update. Test 2 (cross-store re-enrollment at FASHIONOUTLET
with the email from Test 1) walked cleanly with all behavioral checks
green:
- exactly 1 loyalty_cards row, no duplicate
- zero new email_logs rows (no duplicate welcome email)
- cross-location locations block lists both stores
- title already-enrolled branch renders correctly
One copy bug surfaced and was fixed in
|
|||
| dee2eab266 |
fix(loyalty): conditional subtitle on enrollment success page
Some checks failed
When re-enrolling (already a member), the success page showed two
contradicting messages stacked:
Title: "Vous êtes déjà membre !" (correct, conditional)
Subtitle: "Vous êtes maintenant membre..." (wrong — static)
The title was already x-text-conditional based on
enrollContext.already_enrolled, but the subtitle was a server-side
{{ _('success.message') }} so it always rendered the "you're now a
member" copy regardless of branch.
Make the subtitle conditional the same way:
- new i18n key already_enrolled_message in en/fr/de/lb
("Welcome back — your card is ready whenever you are." and
locale-appropriate equivalents)
- expose success_message + already_enrolled_message in i18nStrings
- subtitle becomes x-text="already_enrolled ? msg2 : msg1"
Found during Test 2 round 2 — cross-store re-enrollment at
FASHIONOUTLET with the email from Test 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| f68a30a820 |
docs(loyalty): record 2026-05-18 Test 1 round 2 cleanup + admin polish
All checks were successful
End-of-day update. Adds a new section to the go-live readiness doc covering today's three shipped commits: - |
|||
| 78e098d4da |
fix(static-assets): also cache-bust raw /static/ refs (fonts CSS, store JS)
All checks were successful
The initial codemod only converted url_for('*_static', path='*.js'|'*.css')
patterns and missed 19 raw /static/... references — most importantly the
shared/fonts/inter.css link in all four base.html files, plus a handful
of <script src="/static/modules/..."> tags in marketplace/billing/orders
templates and the storefront login/register/forgot/reset pages.
Result: deploys now flip ?v=<sha> on every JS/CSS asset that reaches the
browser, not just the ones loaded via url_for().
FE-024 rule extended to flag src="/static/...*.(js|css)" patterns too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 5f2885023c |
fix(loyalty-admin): require at least one terms field on program form
Some checks failed
If a merchant saves a loyalty program with both terms_text and terms_cms_page_slug empty, the storefront enrollment page renders the "Terms & Conditions" link as a non-clickable <span> (see enroll.html template branch) — customers can't read what they're agreeing to. Two changes to the shared program-form to make this impossible to ship by accident: 1. Yellow warning banner inside the Terms section, visible only when both fields are empty. Tells the admin what the storefront will look like and what to fix. 2. Save button is disabled until at least one of the two terms fields is filled. The button gets a localised :title tooltip explaining why it's disabled, and disabled:cursor-not-allowed so the disabled state is obvious on hover. Added three i18n keys (terms_required_warning, terms_text_hint, terms_required_tooltip) in en/fr/de/lb, plus a small "either this or the slug above is required" hint under the textarea so each field is self-explanatory in isolation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| ab3e133af7 |
fix(loyalty): use flatpickr for birthday so Firefox honors dd/mm/yyyy on FR
Some checks failed
Native <input type="date"> defers display format to the browser's locale, which most engines pick up from the <html lang> attribute. Firefox is the exception — it ignores lang and uses the OS locale instead (Mozilla bug #1344625, still open). So FR users on Firefox still saw mm/dd/yyyy even after the lang fix from earlier this week. Swap to flatpickr for both the customer storefront enrollment page and the staff terminal enrollment page. Configure: - dateFormat: 'Y-m-d' (what gets sent to the API — ISO, what Pydantic's date field expects) - altInput: true (flatpickr creates a separate visible input) - altFormat: 'd/m/Y' (what the user sees — universal in Europe) - locale: current_language (FR/DE/LB month + day names) - maxDate: 'today' (no future birthdays) Load flatpickr core + the optional locale JS via the existing {% block extra_head %} / {% block extra_scripts %} hooks. The loyalty/store/enroll.html template didn't have those blocks before, added them in the same commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 54247ca4f0 |
feat(static-assets): cache-bust JS/CSS via ?v=<commit-sha>, immutable in prod
All checks were successful
Adds a `static_v(request, name, path=...)` Jinja helper that appends
?v=<commit-sha> from app.core.build_info, plus a CachedStaticFiles
subclass that serves Cache-Control: public, max-age=31536000, immutable
in production and no-cache in development. Browsers refetch JS/CSS
automatically on every deploy without the user having to hard-reload.
- New: app/core/static_files.py (CachedStaticFiles)
- Updated: app/templates_config.py (static_v helper)
- Updated: main.py (use CachedStaticFiles for *_static mounts)
- Codemod: 143 url_for('*_static', path='*.js'|'*.css') → static_v(...)
across 123 templates. Images/fonts/JSON locales intentionally
unchanged (out of scope).
- Arch rule: FE-024 (warning) flags raw url_for on JS/CSS to prevent
drift. Note: FE-008 was already taken by the number_stepper rule.
- docs/proposals/static-asset-cache-busting.md marked Done.
Closes plan from docs/proposals/static-asset-cache-busting.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 236fee015e |
fix(loyalty): rename enrollment-success CTA from "continue shopping" to "back to home"
Some checks failed
The loyalty storefront is a registration / wallet endpoint, not a
catalog — there's nothing to continue shopping toward. The button
already navigates to {{ base_url }} (the homepage), so the
destination was correct; only the wording was wrong.
Rename the i18n key continue_shopping -> back_to_home in
loyalty/enroll-success.html and all four locale files (en/fr/de/lb):
EN: "Continue Shopping" -> "Back to Home"
FR: "Continuer mes achats" -> "Retour à l'accueil"
DE: "Weiter einkaufen" -> "Zurück zur Startseite"
LB: "Weider akafen" -> "Zréck op d'Haaptsäit"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 7cf2420bba |
docs(loyalty): record B1-F resolution + 6 follow-ups for next session
All checks were successful
End-of-day 2026-05-17 update to the go-live readiness doc. Welcome email B1-F is now fully resolved end-to-end (enrollment -> celery dispatch -> email_logs status=sent -> emails landing). The issue was a chain of four nested bugs each masking the next — the doc lists all four with commit hashes ( |
|||
| 5b21908ba4 |
fix(celery): preload every module's models so mapper resolution succeeds
Some checks failed
Celery workers were failing every task that touched the DB with
InvalidRequestError: "expression 'ContentPage' failed to locate a name".
Root cause: the worker process only loads task modules during startup,
not routes or services. Models reached only via SQLAlchemy
string-based relationships (e.g. Platform.relationship("ContentPage"))
were never imported, so the mapper couldn't resolve the name when the
first task tried to open a DB session. The FastAPI process avoids this
because api_router transitively imports the world; the worker doesn't.
Add _preload_all_module_models() in celery_config.py that walks the
module registry and importlib.import_module's each "app.modules.<code>.models"
package. Called at module import time, before any task runs.
Surfaced while finally getting loyalty.send_notification_email registered
on the worker — the task ran, hit the DB to load email settings, and
exploded on the unresolved Platform -> ContentPage relationship.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|