""" app/routes/landing.py ---------------------- Public marketing / landing page served at the apex domain (e.g. jqc.app). Canonical path is GET /welcome (always reachable, tenant-exempt) so the page can be previewed directly in any environment. In multi-tenant production the tenant middleware delegates the apex host's `/` to this view (see app/tenancy/middleware.py) — the apex is NOT a tenant, so `/` cannot be owned by the login-gated dashboard route. The page introduces the product, lists plans (read live from the control DB when available, with a static fallback), and funnels visitors to the existing public self-service signup at /signup (plan picker, Free selectable/default). """ import logging from flask import Blueprint, render_template, current_app, url_for bp = Blueprint('landing', __name__) logger = logging.getLogger(__name__) # Static fallback mirrors control/seed.py PLAN_DEFS so the page still renders if # the control DB is unreachable or multi-tenancy is disabled (dev preview). _FALLBACK_PLANS = [ dict(code='free', name='Free', price_label='$0', max_users=3, max_facilities=2, max_inspections_month=50, max_issues_month=50, allow_mobile_api=False, allow_scheduled_reports=False, allow_branding=False, allow_custom_domain=False), dict(code='starter', name='Starter', price_label='Contact us', max_users=15, max_facilities=10, max_inspections_month=500, max_issues_month=500, allow_mobile_api=True, allow_scheduled_reports=False, allow_branding=False, allow_custom_domain=False), dict(code='pro', name='Pro', price_label='Contact us', max_users=50, max_facilities=50, max_inspections_month=5000, max_issues_month=5000, allow_mobile_api=True, allow_scheduled_reports=True, allow_branding=True, allow_custom_domain=True), dict(code='enterprise', name='Enterprise', price_label='Contact us', max_users=None, max_facilities=None, max_inspections_month=None, max_issues_month=None, allow_mobile_api=True, allow_scheduled_reports=True, allow_branding=True, allow_custom_domain=True), ] def _plan_dict(p): """Flatten a control Plan ORM row into a template-safe dict (read inside the open session so no lazy attribute access happens after it closes).""" if p.price_cents == 0: price_label = '$0' elif p.price_cents: price_label = f'${p.price_cents // 100}/mo' else: price_label = 'Contact us' return dict( code=p.code, name=p.name, price_label=price_label, max_users=p.max_users, max_facilities=p.max_facilities, max_inspections_month=p.max_inspections_month, max_issues_month=p.max_issues_month, allow_mobile_api=p.allow_mobile_api, allow_scheduled_reports=p.allow_scheduled_reports, allow_branding=p.allow_branding, allow_custom_domain=p.allow_custom_domain, ) def _load_plans(): """Return plan dicts from the control DB, ordered cheapest-first, with a static fallback so the landing page never fails to render.""" try: from control.base import control_session from control.models import Plan with control_session() as s: plans = (s.query(Plan) .order_by(Plan.price_cents.nullslast(), Plan.id) .all()) if plans: return [_plan_dict(p) for p in plans] except Exception as exc: logger.debug('LANDING | plan_load_failed | err=%s', exc) return _FALLBACK_PLANS @bp.route('/welcome') def index(): return render_template( 'landing/index.html', plans=_load_plans(), signup_url=url_for('signup.index'), base_domain=current_app.config.get('TENANT_BASE_DOMAIN', 'jqc.app'), )