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>
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>
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>
Same accent-stripped pattern as the FR fix in b463c6bf: the DE
password_reset template was missing every umlaut. Restored throughout
the name, description, subject, body_html, and body_text:
- zurucksetzen → zurücksetzen
- Zurucksetzung → Zurücksetzung
- Passwortzurucksetzung → Passwortzurücksetzung
- Schaltflache → Schaltfläche
- lauft → läuft
- konnen → können
- Grussen → Grüßen
Same deploy step: re-run scripts/seed/seed_email_templates_core.py on
prod to upsert the existing row in place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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 (06e59f73, bb4c4004, dd1f9af8) so the rule
ships at error severity from day one.
- Rule definition in .architecture-rules/frontend.yaml under
javascript_rules; exceptions: i18n.js (defines the helper), vendor/.
- _check_hardcoded_locale in scripts/validate/validate_architecture.py
wired into both JS validation sites (full scan + per-file -f mode).
- Suppressible per-line with `// noqa: JS-016` for the rare case where
a specific locale is genuinely required (e.g., a US-only invoice
formatter that must use en-US regardless of UI language).
Validator output: 0 JS-016 hits across the codebase. Negative-tested
with a planted violation — rule fires correctly and clears on
removal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
Migration cms_003 dropped meta_keywords from content_pages but the
default-pages seed script still passed it to ContentPage(...), so
make db-reset / fresh seeding bombed on the first platform.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small storefront i18n improvements found during the FR
pre-launch walkthrough on FASHIONHUB:
- Store description (e.g. "Trendy clothing and accessories") was a
single English string rendering in the footer regardless of locale.
Added a description_translations JSON column on Store with the same
shape used elsewhere (CMS, Platform, Subscription), exposed via
get_translated_description(lang), and updated the footer + meta tag
to use it. Seeded FR/DE/LB/EN for FASHIONHUB and FASHIONOUTLET so
Fashion Group renders correctly out of the box. Other stores still
show the single description field as fallback.
- "Home" was a hardcoded English literal in both desktop and mobile
nav, even though the FR translation already existed at nav.home in
static/locales/fr.json. Now uses _('nav.home').
- <html lang="en"> was hardcoded, which made <input type="date"> show
in mm/dd/yyyy on the FR storefront. Now driven by current_language
so the browser's locale-aware date picker matches the page locale.
Migration tenancy_005 adds the description_translations column;
nullable, no backfill needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add async email notifications for 5 loyalty lifecycle events, using
the existing messaging module infrastructure (EmailService, EmailLog,
store template overrides).
- New seed script: scripts/seed/seed_email_templates_loyalty.py
Seeds 5 templates × 4 locales (en/fr/de/lb) = 20 rows. Idempotent.
Renamed existing script to seed_email_templates_core.py.
- Celery task: loyalty.send_notification_email — async dispatch with
3 retries and 60s backoff. Opens own DB session.
- Notification service: LoyaltyNotificationService with 5 methods
that resolve customer/card/program into template variables and
enqueue via Celery (never blocks request handlers).
- Enrollment: sends loyalty_enrollment + loyalty_welcome_bonus (if
bonus > 0) after card creation commit.
- Stamps: sends loyalty_reward_ready when stamp target reached.
- Expiration task: sends loyalty_points_expiring 14 days before expiry
(tracked via new last_expiration_warning_at column to prevent dupes),
and loyalty_points_expired after points are zeroed.
- Migration loyalty_005: adds last_expiration_warning_at to cards.
- 8 new unit tests for notification service dispatch.
- Fix: rate limiter autouse fixture in integration tests to prevent
state bleed between tests.
Templates: loyalty_enrollment, loyalty_welcome_bonus,
loyalty_points_expiring, loyalty_points_expired, loyalty_reward_ready.
All support store-level overrides via the existing email template UI.
Birthday + re-engagement emails deferred to future marketing module
(cross-platform: OMS, loyalty, hosting).
342 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The block name validation (scripts → extra_scripts, etc.) only checked
admin and store templates, missing merchant. Added is_merchant flag.
This would have caught the {% block scripts %} bug in merchant/team.html.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add docker-compose.override.yml exposing db/redis ports for local dev
- Remove override from .gitignore so all devs get port mappings
- Use explicit -f in deploy.sh to skip override in production
- Document production safety rule: always use -f on the server
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add wallet diagnostics page at /admin/loyalty/wallet-debug (super admin only)
with explorer-sidebar pattern: config validation, class status, card inspector,
save URL tester, recent enrollments, and Apple Wallet status panels
- Fix Google Wallet fat JWT: include both loyaltyClasses and loyaltyObjects in
payload, use UNDER_REVIEW instead of DRAFT for class reviewStatus
- Fix StorefrontProgramResponse schema: accept google_class_id values while
keeping exclude=True (was rejecting non-None values)
- Standardize all module configs to read from .env file directly
(env_file=".env", extra="ignore") matching core Settings pattern
- Add MOD-026 architecture rule enforcing env_file in module configs
- Add SVC-005 noqa support in architecture validator
- Add test files for dev_tools domain_health and isolation_audit services
- Add google_wallet_status.py script for querying Google Wallet API
- Use table_wrapper macro in wallet-debug.html (FE-005 compliance)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- deploy.sh writes .build-info with commit SHA and timestamp after git pull
- /health endpoint now returns version, commit, and deployed_at fields
- Admin sidebar footer shows version and commit SHA
- Hetzner docs updated: runner --config flag, swap, and runner timeout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix IPv6 host parsing with _strip_port() utility
- Remove dangerous StorePlatform→Store.subdomain silent fallback
- Close storefront gate bypass when frontend_type is None
- Add custom subdomain management UI and API for stores
- Add domain health diagnostic tool
- Convert db.add() in loops to db.add_all() (24 PERF-006 fixes)
- Add tests for all new functionality (18 subdomain service tests)
- Add .github templates for validator compliance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Convert storefront enrollment $t() calls to server-side _() to silence
dev-toolbar warnings (welcome bonus + join button)
- Fix store base template I18n.init() to use current_language (from middleware)
instead of dashboard_language (hardcoded store config) so language changes
take effect immediately
- Switch admin loyalty routes to use get_admin_context() for proper i18n support
- Switch store loyalty routes to use core get_store_context() from page_context
- Pass program object to storefront enrollment context for server-side rendering
- Add LANG-011 architecture rule: enforce $t()/_() over I18n.t() in templates
- Fix duplicate file_pattern key in LANG-004 rule (YAML validation error)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds WizaMart S.à r.l. as a demo merchant with:
- OMS platform subscription (essential tier, 30-day trial)
- Custom domain wizamart.com linked to OMS platform
- Idempotent: safe to run multiple times
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The setting `settings.platform_domain` (the global/main domain like "wizard.lu")
was easily confused with `platform.domain` (per-platform domain like "rewardflow.lu").
Renamed to `settings.main_domain` / `MAIN_DOMAIN` env var across the entire codebase.
Also updated docs to reflect the refactored store detection logic with
`is_platform_domain` / `is_subdomain_of_platform` guards.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add run_affected_tests.py script that uses module dependency graph to
run only tests for changed modules and their dependents. Fix CI and
Makefile to use pyproject.toml testpaths (was missing 9 of 18 modules).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add admin SQL query tool with saved queries, schema explorer presets,
and collapsible category sections (dev_tools module)
- Add platform debug tool for admin diagnostics
- Add loyalty settings page with owner-only access control
- Fix loyalty settings owner check (use currentUser instead of window.__userData)
- Replace HTTPException with AuthorizationException in loyalty routes
- Expand loyalty module with PIN service, Apple Wallet, program management
- Improve store login with platform detection and multi-platform support
- Update billing feature gates and subscription services
- Add store platform sync improvements and remove is_primary column
- Add unit tests for loyalty (PIN, points, stamps, program services)
- Update i18n translations across dev_tools locales
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 16 missing translations for: subscription_welcome, payment_failed,
subscription_cancelled, trial_ending, team_invite (fr/de/lb each) and
team_invitation (lb). All 11 email templates now have full coverage
across all 4 supported languages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The reset_all_data() function used `ContentPage.store_id is not None` which
is a Python identity check (always True), causing it to delete ALL content
pages including platform defaults. Same bug in print_summary() caused the
count to always show 0 platform pages. Fixed both to use proper SQLAlchemy
.is_(None) / .is_not(None) syntax.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add description_translations JSON column to Platform model + migration
- Add language tabs to platform admin edit form for multilingual descriptions
- Update API schemas to include description_translations in request/response
- Translate pricing section UI labels via _t() macro (monthly/annual/CTA/etc.)
- Add Luxembourgish (lb) support to all platforms (OMS, Main, Loyalty, Hosting)
- Seed description_translations, contact emails, and social links for all platforms
- Add LuxWeb Agency demo merchant with hosting stores, team, and content pages
- Fix language code typo: lu → lb in platform-edit.js availableLanguages
- Fix store content pages to use correct primary platform instead of hardcoded OMS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add name_translations JSON column to SubscriptionTier for multi-language
tier names. Pre-resolve tier names and build dynamic feature lists from
module providers in route handlers. Fix Jinja2 macro scoping by importing
pricing partial with context. Backfill content_translations for all 43
content pages across 4 platforms (en/fr/de/lb).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add sections covering CMS locale file structure, translated template
inventory, TranslatableText pattern for sections, and the new
title_translations/content_translations model API with migration cms_002.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Phase 5: Translate homepage-modern.html (~90 new locale keys, all
hardcoded strings replaced with _() calls for dashboard mock,
features, pricing tiers, testimonial sections)
- Phase 6: Translate homepage-minimal.html (17 new locale keys for
fallback content, features, and CTA sections)
- Phase 7: Add multi-language page.title/content support with
title_translations and content_translations JSON columns, Alembic
migration cms_002, translated title/content resolution in templates,
and seed script updates with tt() helper
- Phase 8: Complete lb.json audit — fill 6 missing keys (messages,
confirmations), also backfill same keys in fr.json and de.json
All 4 locale files now have 340 keys with full parity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ProductCard/ProductsSection schema and _products.html section macro
- Rewrite seed script with 3-platform homepage sections (wizard, OMS, loyalty),
platform marketing pages, and store defaults with {{store_name}} placeholders
- Add resolve_placeholders() to ContentPageService for store default pages
- Fix SQLAlchemy filter bugs: replace Python `is None` with `.is_(None)` across
all ContentPageService query methods (was silently breaking all platform page lookups)
- Remove hardcoded orion fallback and delete homepage-orion.html
- Add placeholder hint box with click-to-copy in admin content page editor
- Export ProductCard/ProductsSection from cms schemas __init__
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move core signup service from marketplace to billing module, add
automatic Stripe product/price sync for tiers, create loyalty-specific
signup wizard, and enforce that platform is always explicitly known
(no silent defaulting to primary/hardcoded ID).
Key changes:
- New billing SignupService with separated account/store creation steps
- Stripe auto-sync on tier create/update (new prices, archive old)
- Loyalty signup template (Plan → Account → Store → Payment)
- platform_code is now required throughout the signup flow
- Pricing/signup pages return 404 if platform not detected
- OMS-specific logic (Letzshop claiming) stays in marketplace module
- Bootstrap script: scripts/seed/sync_stripe_products.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migrates scanning pipeline from marketing-.lu-domains app into Orion module.
Supports digital (domain scan) and offline (manual capture) lead channels
with enrichment, scoring, campaign management, and interaction tracking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use SUDO_USER to resolve correct home directory when run with sudo.
Use --project-directory instead of -f for docker compose lookups.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- deploy.sh: add DB health wait before migrations, prune old Docker images
- restore.sh: add redis-exporter to stop list, replace sleep with DB health wait
- verify-server.sh: add redis-exporter to expected containers, add Sentry + Redis exporter checks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All route files (admin.py, store.py) now export `router` instead of
`admin_router`/`store_router`. Consumer code (definition.py, __init__.py)
imports as `router as admin_router` where distinction is needed.
ModuleDefinition fields remain admin_router/store_router.
64 files changed across all modules. Architecture rules, docs, and
migration plan updated. Added noqa:API001 support to validator for
pre-existing raw dict endpoints now visible with standardized router name.
All 1114 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All 84 import sites now use the canonical path
app.modules.tenancy.schemas.auth directly — no need
for backwards-compatibility re-exports.
Update audit validator to check module schemas locations
instead of only the legacy models/schema/ path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add create_oms_admin (admin@omsflow.lu) alongside existing loyalty admin,
both using a shared create_platform_admin helper. Rename "Dashboard" and
"Staff login" labels to "Store panel" and "Store login" across seed output.
Add customer login URLs to production-style access section.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reflect the production routing refactor (ce5b54f): document store dashboard
double-mounting, per-platform subdomain overrides via StorePlatform.custom_subdomain,
get_resolved_store_code dependency, and /merchants/ reserved path. Update seed
script to populate custom_subdomain and StoreDomain.platform_id for demo data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract store/platform context from Referer header for storefront API requests
(StoreContextMiddleware and PlatformContextMiddleware) so login POST works in
dev mode where API paths lack /platforms/{code}/ prefix
- Set customer token cookie path to "/" for cross-route compatibility
- Fix double storefront in URLs: replace {{ base_url }}storefront/ with {{ base_url }}
across all 24 storefront templates
- Fix auth error redirect to include platform prefix and use store_code
- Update seed script to output correct storefront login URLs
- Add 20 new unit tests covering all fixes; fix 9 pre-existing test failures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Seed default RBAC roles per store and assign role_id to StoreUser
records (was never implemented after RBAC Phase 1 cleanup)
- Handle nullable role in auth_service find_user_store and
get_user_store_role to prevent NoneType crash on login
- Use platform_clean_path instead of clean_path in FrontendTypeMiddleware
so /store/X/dashboard is detected as STORE, not STOREFRONT
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds SEC001 (hardcoded password) and SEC021 (password in print output)
suppressions for the loyalty admin seed data, consistent with existing
patterns in seed_demo.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>