Jun 29 - Update CLAUDE.md and create SERVER_DUPLICATION.md
This commit is contained in:
@@ -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 A–E + 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 R1–R4 + 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 A–E + 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 R1–R4 + 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
Reference in New Issue
Block a user