July 3rd - Update landing page

This commit is contained in:
2026-07-03 15:38:31 -04:00
parent 4c275fc0e5
commit 4b6401be43
12 changed files with 414 additions and 19 deletions
+13 -9
View File
@@ -52,7 +52,8 @@
- **Mobile API** — JWT-authenticated REST layer for the iPad native app
- **iPad native app** — SwiftUI + SwiftData offline-first inspection tool (Phase A + B + C complete)
- **Billing** — Stripe-backed subscription system with plan picker, Checkout, Customer Portal, dunning emails (HTML + plain text), invoice history, and trial-period enforcement
- **Self-service signup** — public `/signup` page provisions a new tenant immediately with a 14-day trial (no Stripe required at signup)
- **Public landing page** — `jqc.app` apex serves a marketing/landing page (`landing.index`, canonical path `/welcome`) introducing the product and plans, funneling to signup
- **Self-service signup** — public `/signup` page provisions a new tenant immediately: Free plan = free-forever (`active`), paid plans = 14-day trial (no Stripe required at signup)
The application is actively deployed in production and maintained by a single developer/administrator.
@@ -488,7 +489,8 @@ The `DeviceRegistration` model and the duplicate `api_devices` blueprint were **
| `api_discovery` | `/api/v1` | `GET /discover?subdomain=` or `?email=`, `GET /tenant` — MT-9 server side (public, no auth, exempt from tenant middleware) |
| `tenant_settings` | `/settings` | MT-7: `GET/POST /branding`, `GET /plan`, `GET /domains`, `POST /domains/request`, `POST /domains/<id>/delete` |
| `billing` | `/billing` | `GET /subscribe`, `POST /subscribe`, `GET /portal`, `POST /webhook`, `GET /suspended` |
| `signup` | `/signup` | `GET /`, `POST /` — public self-service signup (exempt from tenant middleware) |
| `signup` | `/signup` | `GET /`, `POST /` — public self-service signup (exempt from tenant middleware). Free plan → provisioned `active` (free forever); paid plans → 14-day trial |
| `landing` | `/` (apex only) | `GET /welcome` — public marketing/landing page (tenant-exempt). In MT mode the middleware serves this for the apex host's `/` and funnels to `/signup` |
---
@@ -1235,6 +1237,8 @@ set -a; . /etc/jqc/control.env; set +a
| 75 | **`delete_tenant()` + `_add_domains()` are retry-safe** | `_add_domains` uses `_upsert_domain()` (delete-then-insert) to handle orphan rows from failed partial runs. `delete_tenant()` also purges by derived domain string, not only by `tenant_id`, catching orphans whose parent tenant row was rolled back. |
| 76 | **`register-tenant-zero` never bootstraps and never drops the DB** | `register_tenant_zero()` only reads the existing head, inserts control rows, and maps domains. `delete_tenant()` on tenant-zero must never use `--drop-db` — the guard checks `db_name == db_name_for(slug)` and refuses non-provisioner-named DBs (LT's DB name is `jqc_lt`, not `jqc_lts`). |
| 84 | **Device registration is consolidated on `DeviceToken` / `api_device_tokens` — one handler only** | RESOLVED. There is exactly one `POST /api/v1/devices/register`, in `app/api/auth.py` (blueprint `api_auth`); it upserts `DeviceToken` (device_id, device_name, app_version, ios_version, apns_token, last_seen_at) which the admin Devices page reads. The former duplicate `api_devices` blueprint (`app/api/devices.py`) and the orphaned `DeviceRegistration` model / `device_registrations` table were **deleted** — that path wrote to a table phase31/32 drop. Do not reintroduce a second `/devices/register` route or a `device_registrations`-backed model. |
| 85 | **Apex host serves the public landing page; the landing route lives at `/welcome`, NOT `/`** | The dashboard owns `/` (login-gated) on tenant hosts, so the landing page cannot register a second `/` route (same collision class as rule 84). Instead the tenant middleware detects the apex host (`TENANT_BASE_DOMAIN` + `www.`) and calls `landing.index` directly for `/`, redirecting other non-exempt apex paths to `/`. `/welcome`, `/signup`, `/static/` are tenant-exempt. The apex handling is inert when `MULTI_TENANT_ENABLED=false`, so `/welcome` is the always-reachable preview URL. Requires the Nginx apex block to **proxy** (not 301-redirect) to port 8000 with `Host` passed through. |
| 86 | **Free plan is free-forever, not a trial** | `signup.index()` passes `trial_days=0` for `plan_code == 'free'`; `create_tenant()` then sets `subscription_status='active'` (no `trial_ends_at`) so `_billing_gate()` never blocks it. Paid plans keep the 14-day trial (`trial_days=14`). The welcome email adapts via `trial_note` and hides the trial row when `trial_ends_at` is blank. Do not reintroduce a hardcoded `trial_days=14` in the signup path. |
---
@@ -1400,7 +1404,7 @@ curl -sI -H "Host: ztest.jqc.app" http://127.0.0.1:8000/ | head -2
| 76 | `register-tenant-zero` never bootstraps and never drops the DB |
| 77 | `CONTROL_DATABASE_URL` not in interactive shell — always `set -a; . /etc/jqc/control.env; set +a` before CLI |
| 78 | Nginx `admin.jqc.app` must be a separate `server {}` block before `*.jqc.app` — exact name wins only in its own block |
| 79 | `jqc.app` apex has no registered tenant — Nginx redirects to `lts.jqc.app` at the server level |
| 79 | `jqc.app` apex has no registered tenant — Nginx **proxies** it to the app (port 8000) and the tenant middleware serves the public landing page (`landing.index`) for `/`. (Was a server-level 301 to `lts.jqc.app`; changed when the marketing landing page was added — see rule 85 + §10 of MULTI_TENANT_PLAN.md.) |
| 80 | `@feature_required` before `@quota_soft_check` in decorator stack — no point counting if feature is blocked |
| 81 | `TenantSettings.get_or_default()` returns a transient (non-persisted) default instance — `db.session.add(row)` required before first save |
| 82 | `inject_tenant_branding()` context processor wraps the DB call in try/except — branding failure must never break page rendering |
@@ -1437,9 +1441,9 @@ Stripe-backed subscription billing. Controlled by `BILLING_ENABLED` env var (def
### Trial period
- New tenants provisioned via `create_tenant()` get `subscription_status='trial'`, `trial_ends_at = now + 14 days` by default.
- Self-service signup (`/signup`) provisions a tenant immediately with a 14-day trial and sends a `welcome` email.
- Trial enforcement is in `_billing_gate()` — no Stripe required until they subscribe.
- New tenants provisioned via `create_tenant()` with `trial_days > 0` get `subscription_status='trial'`, `trial_ends_at = now + N days`. With `trial_days=0` they are provisioned `subscription_status='active'` (no trial) — used by the Free plan (rule 86).
- Self-service signup (`/signup`) provisions paid plans with a 14-day trial and the **Free plan as active/free-forever**, then sends a `welcome` email (wording adapts per plan).
- Trial enforcement is in `_billing_gate()` — no Stripe required until they subscribe; `active` and `None` pass through indefinitely.
### Billing emails (`app/billing/emails.py`)
@@ -1497,13 +1501,13 @@ All three actions write a `TenantAudit` row.
### Self-service signup (`/signup`)
Public route, exempt from tenant middleware. `SignupForm` validates:
Public route, exempt from tenant middleware. Reached from the public landing page (`landing.index`) CTA buttons. `SignupForm` validates:
- Company name, full name, email
- Subdomain: DNS label regex + uniqueness check against control DB
- Plan picker (Starter / Pro / Enterprise)
- Plan picker (Free / Starter / Pro / Enterprise — choices loaded from the control DB, cheapest-first so **Free is the default**)
- Password + confirm
On submit calls `create_tenant(admin_password=pw, trial_days=14)`. Shows `signup/success.html` with workspace URL and trial end date.
On submit calls `create_tenant(admin_password=pw, trial_days=0 if free else 14)` (rule 86). Shows `signup/success.html` with workspace URL and (for paid plans) trial end date.
---