Jun 29 - Update CLAUDE.md and create SERVER_DUPLICATION.md

This commit is contained in:
2026-06-29 09:45:30 -04:00
parent a42fc31fc5
commit 42790c499c
2 changed files with 1559 additions and 8 deletions
+161 -8
View File
@@ -2,7 +2,7 @@
> **Audience:** AI assistants and developers working on this codebase. > **Audience:** AI assistants and developers working on this codebase.
> **Purpose:** Authoritative reference for architecture, conventions, gotchas, and decisions. > **Purpose:** Authoritative reference for architecture, conventions, gotchas, and decisions.
> **Last reviewed:** June 2026 (Phase 19 complete + mobile API gap-fill Phases AE + customer UI refinements + Phase 22 comment visibility + Phase 23 support chat/tickets + inspector performance Excel export + inspection list filters + customer issue logging + AI chatbot + dashboard grouped sections + issues/inspections PDF export + date/ID filters + Reports expansion Phases R1R4 + Phase 24 issue_created notify defaults + Phase 25 inspection GPS + Phase 26 issue vendor fields + Phase 27 facility score alerts + **MT-0 through MT-7 complete; MT-8 flag-gated; MT-9 iOS pending**) > **Last reviewed:** June 2026 (Phase 19 complete + mobile API gap-fill Phases AE + customer UI refinements + Phase 22 comment visibility + Phase 23 support chat/tickets + inspector performance Excel export + inspection list filters + customer issue logging + AI chatbot + dashboard grouped sections + issues/inspections PDF export + date/ID filters + Reports expansion Phases R1R4 + Phase 24 issue_created notify defaults + Phase 25 inspection GPS + Phase 26 issue vendor fields + Phase 27 facility score alerts + **MT-0 through MT-8 complete; self-service signup; trial enforcement; billing emails; invoice history; superadmin billing controls; per-tenant backup CLI; health dashboard; MT-9 iOS pending**)
--- ---
@@ -29,8 +29,11 @@
19. [Infrastructure](#19-infrastructure) 19. [Infrastructure](#19-infrastructure)
20. [Known Constraints & Hard Rules](#20-known-constraints--hard-rules) 20. [Known Constraints & Hard Rules](#20-known-constraints--hard-rules)
21. [Change Philosophy](#21-change-philosophy) 21. [Change Philosophy](#21-change-philosophy)
22. [Multi-Tenant Architecture (MT-0 → MT-7)](#22-multi-tenant-architecture-mt-0--mt-7) 22. [Multi-Tenant Architecture (MT-0 → MT-8)](#22-multi-tenant-architecture-mt-0--mt-8)
23. [Coding Rules for AI Assistants](#23-coding-rules-for-ai-assistants) 23. [Billing System (MT-8)](#23-billing-system-mt-8)
24. [Backup CLI](#24-backup-cli-controlbackuppy)
25. [Health Dashboard](#25-health-dashboard-health-on-panel)
26. [Coding Rules for AI Assistants](#26-coding-rules-for-ai-assistants)
--- ---
@@ -48,6 +51,8 @@
- **Support chat** — Groq AI chatbot for customers with preset FAQ chips; escalation to admin via ticketing system; customers can view and reply to their own tickets; admins manage tickets at `/support/admin/tickets` - **Support chat** — Groq AI chatbot for customers with preset FAQ chips; escalation to admin via ticketing system; customers can view and reply to their own tickets; admins manage tickets at `/support/admin/tickets`
- **Mobile API** — JWT-authenticated REST layer for the iPad native app - **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) - **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)
The application is actively deployed in production and maintained by a single developer/administrator. The application is actively deployed in production and maintained by a single developer/administrator.
@@ -115,11 +120,23 @@ lt_janitorial_quality_control/
│ │ └── ... │ │ └── ...
│ ├── routes/ │ ├── routes/
│ │ ├── support.py # /support/* — AI chat, ticket submit/list/detail (Phase 23) │ │ ├── support.py # /support/* — AI chat, ticket submit/list/detail (Phase 23)
│ │ ├── tenant_settings.py # MT-7: /settings/* — branding, plan view, domain mgmt (@admin_required) │ │ ├── tenant_settings.py # MT-7: /settings/* — branding, plan view (+ invoice history), domain mgmt (@admin_required)
│ │ ├── signup.py # public /signup — self-service tenant signup (no auth, no tenant context)
│ │ └── ... │ │ └── ...
│ ├── templates/ │ ├── templates/
│ │ ├── _quota_warning.html # MT-5: reusable quota exceeded banner partial │ │ ├── _quota_warning.html # MT-5: reusable quota exceeded banner partial
│ │ ├── tenant_settings/ # MT-7: branding.html, plan.html, domains.html │ │ ├── tenant_settings/ # MT-7: branding.html, plan.html, domains.html
│ │ ├── billing/
│ │ │ ├── _billing_banner.html # past_due + trial_ending warning banners
│ │ │ ├── plan_picker.html # plan selection page (Starter/Pro/Enterprise cards)
│ │ │ └── email/ # HTML billing lifecycle emails
│ │ │ ├── base.html # shared branded email layout
│ │ │ ├── payment_failed.html
│ │ │ ├── trial_ending.html
│ │ │ └── subscription_cancelled.html
│ │ ├── signup/
│ │ │ ├── index.html # self-service signup form (standalone, no base.html)
│ │ │ └── success.html # post-signup confirmation
│ │ └── ... │ │ └── ...
│ ├── static/ │ ├── static/
│ │ └── uploads/ # UPLOAD_FOLDER root │ │ └── uploads/ # UPLOAD_FOLDER root
@@ -163,6 +180,7 @@ lt_janitorial_quality_control/
│ ├── models.py # Plan, PlanFeature, Tenant, TenantDomain, │ ├── models.py # Plan, PlanFeature, Tenant, TenantDomain,
│ │ # Superadmin, ProvisioningJob, TenantAudit │ │ # Superadmin, ProvisioningJob, TenantAudit
│ ├── provision.py # MT-3 — create_tenant / register_tenant_zero / delete_tenant │ ├── provision.py # MT-3 — create_tenant / register_tenant_zero / delete_tenant
│ ├── backup.py # per-tenant mysqldump backup CLI (python -m control.backup)
│ ├── seed.py # idempotent plan seeder │ ├── seed.py # idempotent plan seeder
│ ├── tenant_migrate.py # MT-2 — upgrade_tenant / bootstrap_tenant / chain_head CLI │ ├── tenant_migrate.py # MT-2 — upgrade_tenant / bootstrap_tenant / chain_head CLI
│ ├── time_utils.py # now_eastern() mirror (no app import) │ ├── time_utils.py # now_eastern() mirror (no app import)
@@ -171,9 +189,10 @@ lt_janitorial_quality_control/
│ │ ├── auth.py # /login, /logout (session-based, no Flask-Login) │ │ ├── auth.py # /login, /logout (session-based, no Flask-Login)
│ │ ├── decorators.py # @superadmin_required │ │ ├── decorators.py # @superadmin_required
│ │ ├── impersonate.py # HMAC-SHA256 signed token generator/validator │ │ ├── impersonate.py # HMAC-SHA256 signed token generator/validator
│ │ ├── tenants.py # CRUD + plan/suspend/resume/domain/migrate/provision routes │ │ ├── tenants.py # CRUD + plan/suspend/resume/domain/migrate/provision/billing routes
│ │ ├── health.py # GET /health/ — monitoring dashboard (tenant status, schema, trials)
│ │ ├── wsgi_panel.py # WSGI entry point — Gunicorn on port 8001 │ │ ├── wsgi_panel.py # WSGI entry point — Gunicorn on port 8001
│ │ └── templates/panel/ # base.html, login.html, tenants_list.html, tenant_detail.html, provision.html │ │ └── templates/panel/ # base.html, login.html, tenants_list.html, tenant_detail.html, provision.html, health.html
│ └── migrations/ # control Alembic chain (control{N}_…) │ └── migrations/ # control Alembic chain (control{N}_…)
│ └── versions/control0001_init.py ← HEAD │ └── versions/control0001_init.py ← HEAD
└── ... └── ...
@@ -210,6 +229,13 @@ lt_janitorial_quality_control/
| `TENANT_ENGINE_POOL_RECYCLE` | Pool recycle in seconds (default 1800). | | `TENANT_ENGINE_POOL_RECYCLE` | Pool recycle in seconds (default 1800). |
| `PANEL_SECRET_KEY` | Flask secret for the superadmin panel app (separate from `SECRET_KEY`). Generate: `python -c "import secrets; print(secrets.token_hex(32))"` | | `PANEL_SECRET_KEY` | Flask secret for the superadmin panel app (separate from `SECRET_KEY`). Generate: `python -c "import secrets; print(secrets.token_hex(32))"` |
| `PANEL_IMPERSONATE_KEY` | HMAC key for impersonation tokens. Must be identical between panel and main app env. Generate same way. | | `PANEL_IMPERSONATE_KEY` | HMAC key for impersonation tokens. Must be identical between panel and main app env. Generate same way. |
| `BILLING_ENABLED` | `false` by default. Set `true` to activate Stripe billing UI, gate, and webhooks. |
| `STRIPE_SECRET_KEY` | Stripe secret key (`sk_live_…` or `sk_test_…`). Required when `BILLING_ENABLED=true`. |
| `STRIPE_PUBLISHABLE_KEY` | Stripe publishable key (`pk_…`). Used in Checkout redirect. |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret (`whsec_…`). Validates webhook payloads. |
| `STRIPE_PRICE_STARTER` | Stripe Price ID for the Starter plan (`price_…`). Set via `python -m control.cli seed`. |
| `STRIPE_PRICE_PRO` | Stripe Price ID for the Pro plan. |
| `STRIPE_PRICE_ENTERPRISE` | Stripe Price ID for the Enterprise plan. |
### Email SSL Auto-Detection ### Email SSL Auto-Detection
@@ -439,6 +465,8 @@ api_device_tokens: id, user_id, device_id, apns_token, device_name, app_version
| `api_comments` | `/api/v1` | `GET /issues/<id>/comments`, `POST /issues/<id>/comments` (Phase D) | | `api_comments` | `/api/v1` | `GET /issues/<id>/comments`, `POST /issues/<id>/comments` (Phase D) |
| `api_discovery` | `/api/v1` | `GET /discover?subdomain=` or `?email=`, `GET /tenant` — MT-9 server side (public, no auth, exempt from tenant middleware) | | `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` | | `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) |
--- ---
@@ -1174,7 +1202,7 @@ set -a; . /etc/jqc/control.env; set +a
--- ---
## 22. Multi-Tenant Architecture (MT-0 → MT-7) ## 22. Multi-Tenant Architecture (MT-0 → MT-8)
See `MULTI_TENANT_PLAN.md` for the full phased roadmap. This section summarises what is built and operational. See `MULTI_TENANT_PLAN.md` for the full phased roadmap. This section summarises what is built and operational.
@@ -1331,7 +1359,132 @@ curl -sI -H "Host: ztest.jqc.app" http://127.0.0.1:8000/ | head -2
--- ---
## 23. Coding Rules for AI Assistants ## 23. Billing System (MT-8)
### Overview
Stripe-backed subscription billing. Controlled by `BILLING_ENABLED` env var (default `false`). When enabled, a `_billing_gate()` runs in `init_tenancy()` before every request and enforces trial expiry and cancellation state.
### Key files
| File | Purpose |
|---|---|
| `app/billing/__init__.py` | Blueprint registration |
| `app/billing/routes.py` | `subscribe`, `portal`, `webhook`, `suspended` routes |
| `app/billing/webhooks.py` | Stripe event handlers (`payment_failed`, `subscription_updated`, `subscription_deleted`) |
| `app/billing/emails.py` | `send_billing_email()` — HTML + plain text dunning emails in background thread |
| `app/templates/billing/` | Plan picker, billing banner partial, HTML email templates |
| `control/seed.py` | Reads `STRIPE_PRICE_*` from env and writes to `Plan.stripe_price_id` |
### Billing gate (`_billing_gate()` in `app/tenancy/middleware.py`)
| `subscription_status` | Behaviour |
|---|---|
| `None` or `active` | Allow through |
| `trial` (not expired) | Allow; set `g.billing_warning = 'trial_ending'` if ≤ 3 days remain |
| `trial` (expired) | Redirect to `/billing/subscribe` (except `/billing/` and `/settings/`) |
| `past_due` | Allow; set `g.billing_warning = 'past_due'` |
| `cancelled` | Redirect to `/billing/suspended` |
### 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.
- Trial enforcement is in `_billing_gate()` — no Stripe required until they subscribe.
### Billing emails (`app/billing/emails.py`)
`send_billing_email(to_addr, event_type, context_dict)` sends multipart HTML + plain text.
| `event_type` | Trigger | Required context keys |
|---|---|---|
| `payment_failed` | `invoice.payment_failed` Stripe webhook | `portal_url` |
| `trial_ending` | Cron (not yet wired — see §11 cron table) | `trial_ends_at`, `subscribe_url` |
| `subscription_cancelled` | `customer.subscription.deleted` Stripe webhook | `portal_url` |
HTML templates live in `app/templates/billing/email/`. `base.html` provides the branded layout; each event type extends it.
### Plan seeding
```bash
# Add to .env:
STRIPE_PRICE_STARTER=price_...
STRIPE_PRICE_PRO=price_...
STRIPE_PRICE_ENTERPRISE=price_...
# Then run:
python -m control.cli seed
python -m control.cli list-plans # verify price IDs
```
### Superadmin billing controls (panel `POST /tenants/<id>/billing`)
| Action | Form field | Effect |
|---|---|---|
| `set_trial` | `trial_days` (int) | Sets `subscription_status=trial`, `trial_ends_at=now+N` |
| `set_status` | `subscription_status` | Overrides status directly |
| `apply_coupon` | `coupon_id` | Calls `stripe.Customer.modify(..., coupon=id)` |
All three actions write a `TenantAudit` row.
### Invoice history
`GET /settings/plan` fetches last 10 Stripe invoices for the tenant's `stripe_customer_id` and passes them to `plan.html`. Shows date, amount, status badge, PDF link, hosted invoice link.
### Self-service signup (`/signup`)
Public route, exempt from tenant middleware. `SignupForm` validates:
- Company name, full name, email
- Subdomain: DNS label regex + uniqueness check against control DB
- Plan picker (Starter / Pro / Enterprise)
- Password + confirm
On submit calls `create_tenant(admin_password=pw, trial_days=14)`. Shows `signup/success.html` with workspace URL and trial end date.
---
## 24. Backup CLI (`control/backup.py`)
Per-tenant MySQL backup using `mysqldump`.
```bash
# Back up all tenants
python -m control.backup --tenant all --output-dir /var/backups/jqc
# Back up one tenant
python -m control.backup --tenant acme --output-dir /var/backups/jqc
# List tenants
python -m control.backup --list
```
Output: `<slug>_YYYYMMDD_HHMMSS.sql.gz` (gzip-compressed SQL dump).
Flags passed to `mysqldump`: `--single-transaction`, `--routines`, `--triggers`, `--set-gtid-purged=OFF`.
**Cron for nightly backup:**
```bash
0 2 * * * set -a; . /etc/jqc/control.env; set +a; \
python -m control.backup --tenant all --output-dir /var/backups/jqc
```
---
## 25. Health Dashboard (`/health/` on panel)
New nav item in the superadmin panel sidebar. Reads control DB only — no per-tenant DB queries.
Shows:
- Summary cards: total tenants, active, suspended, schema behind
- Subscription breakdown (count per status)
- Trial alerts: expired trials, trials expiring within 3 days
- Full tenant table: slug, name, status, plan, subscription status, trial expiry countdown, Stripe presence, schema freshness, created date
Row highlights: yellow = suspended, red = trial expired.
---
## 26. Coding Rules for AI Assistants
These rules apply to every change made to this codebase, without exception. These rules apply to every change made to this codebase, without exception.
File diff suppressed because it is too large Load Diff