Jun 28 Optimize code
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
> **Audience:** AI assistants and developers working on this codebase.
|
||||
> **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-3 multi-tenant control plane**)
|
||||
> **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**)
|
||||
|
||||
---
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
19. [Infrastructure](#19-infrastructure)
|
||||
20. [Known Constraints & Hard Rules](#20-known-constraints--hard-rules)
|
||||
21. [Change Philosophy](#21-change-philosophy)
|
||||
22. [Multi-Tenant Architecture (MT-0 → MT-3)](#22-multi-tenant-architecture-mt-0--mt-3)
|
||||
22. [Multi-Tenant Architecture (MT-0 → MT-7)](#22-multi-tenant-architecture-mt-0--mt-7)
|
||||
23. [Coding Rules for AI Assistants](#23-coding-rules-for-ai-assistants)
|
||||
|
||||
---
|
||||
|
||||
@@ -85,11 +86,13 @@ lt_janitorial_quality_control/
|
||||
├── app/
|
||||
│ ├── __init__.py # Application factory — limiter, csrf, db (RoutingSession), mail, login_manager, init_tenancy
|
||||
│ ├── tenancy/ # MT-1 — tenant resolution + DB routing (inert unless MULTI_TENANT_ENABLED=true)
|
||||
│ │ ├── __init__.py # public exports: RoutingSession, init_tenancy, TenantContext
|
||||
│ │ ├── context.py # TenantContext frozen dataclass (g.tenant)
|
||||
│ │ ├── __init__.py # public exports: RoutingSession, init_tenancy, TenantContext, feature_required, quota_soft_check
|
||||
│ │ ├── context.py # TenantContext frozen dataclass (g.tenant) — MT-5: +9 plan fields
|
||||
│ │ ├── engine_cache.py # per-tenant SQLAlchemy engine cache + invalidate()
|
||||
│ │ ├── middleware.py # init_tenancy() — before_request Host→tenant resolver
|
||||
│ │ ├── resolver.py # resolve_tenant(host) → TenantContext | None
|
||||
│ │ ├── gates.py # MT-5: @feature_required (hard 403) + @quota_soft_check (soft warn)
|
||||
│ │ ├── middleware.py # init_tenancy() — before_request Host→tenant resolver + MT-4 impersonation override
|
||||
│ │ ├── quota.py # MT-5: live quota counters (inspections/issues/users/facilities)
|
||||
│ │ ├── resolver.py # resolve_tenant(host) → TenantContext | None — MT-5: loads plan fields
|
||||
│ │ └── routing.py # RoutingSession — routes db.session to g.tenant_engine
|
||||
│ ├── api/ # Mobile REST API
|
||||
│ │ ├── __init__.py # api_bp parent blueprint + register_api()
|
||||
@@ -108,9 +111,15 @@ lt_janitorial_quality_control/
|
||||
│ │ ├── inspection.py # Inspection — mobile_local_id column (Phase B)
|
||||
│ │ ├── issue.py # Issue — mobile_local_id (Phase B), reported_by (Phase 18), mobile_photo_paths (Phase 19)
|
||||
│ │ ├── support.py # SupportTicket, SupportTicketReply (Phase 23)
|
||||
│ │ ├── tenant_settings.py # MT-7: TenantSettings — per-tenant branding (one row per tenant DB)
|
||||
│ │ └── ...
|
||||
│ ├── routes/
|
||||
│ │ ├── support.py # /support/* — AI chat, ticket submit/list/detail (Phase 23)
|
||||
│ │ ├── tenant_settings.py # MT-7: /settings/* — branding, plan view, domain mgmt (@admin_required)
|
||||
│ │ └── ...
|
||||
│ ├── templates/
|
||||
│ │ ├── _quota_warning.html # MT-5: reusable quota exceeded banner partial
|
||||
│ │ ├── tenant_settings/ # MT-7: branding.html, plan.html, domains.html
|
||||
│ │ └── ...
|
||||
│ ├── static/
|
||||
│ │ └── uploads/ # UPLOAD_FOLDER root
|
||||
@@ -157,6 +166,14 @@ lt_janitorial_quality_control/
|
||||
│ ├── seed.py # idempotent plan seeder
|
||||
│ ├── tenant_migrate.py # MT-2 — upgrade_tenant / bootstrap_tenant / chain_head CLI
|
||||
│ ├── time_utils.py # now_eastern() mirror (no app import)
|
||||
│ ├── panel/ # MT-4 — superadmin control panel (standalone Flask app)
|
||||
│ │ ├── __init__.py # create_panel_app() factory
|
||||
│ │ ├── auth.py # /login, /logout (session-based, no Flask-Login)
|
||||
│ │ ├── decorators.py # @superadmin_required
|
||||
│ │ ├── impersonate.py # HMAC-SHA256 signed token generator/validator
|
||||
│ │ ├── tenants.py # CRUD + plan/suspend/resume/domain/migrate/provision routes
|
||||
│ │ ├── wsgi_panel.py # WSGI entry point — Gunicorn on port 8001
|
||||
│ │ └── templates/panel/ # base.html, login.html, tenants_list.html, tenant_detail.html, provision.html
|
||||
│ └── migrations/ # control Alembic chain (control{N}_…)
|
||||
│ └── versions/control0001_init.py ← HEAD
|
||||
└── ...
|
||||
@@ -187,10 +204,12 @@ lt_janitorial_quality_control/
|
||||
| `CONTROL_FERNET_KEY` | Fernet key for encrypting tenant DB passwords. Generate once; store in `/etc/jqc/control.env`. |
|
||||
| `PROVISION_DB_URL` | MySQL account that can `CREATE DATABASE` / `CREATE USER` / `GRANT`. e.g. `mysql+pymysql://jqc_provisioner:pw@127.0.0.1/`. |
|
||||
| `TENANT_BASE_DOMAIN` | Apex domain for subdomains, e.g. `jqc.app`. Used by provisioner to build `<slug>.jqc.app`. |
|
||||
| `MULTI_TENANT_EXEMPT_PATHS` | Comma-separated path prefixes that bypass the tenant gate (e.g. `/health`). `/static/` is always exempt. |
|
||||
| `MULTI_TENANT_EXEMPT_PATHS` | Comma-separated path prefixes that bypass the tenant gate (e.g. `/health,/api/v1/discover`). `/static/` is always exempt. |
|
||||
| `TENANT_ENGINE_POOL_SIZE` | Per-tenant engine pool size (default 5). |
|
||||
| `TENANT_ENGINE_MAX_OVERFLOW` | Per-tenant pool max overflow (default 5). |
|
||||
| `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_IMPERSONATE_KEY` | HMAC key for impersonation tokens. Must be identical between panel and main app env. Generate same way. |
|
||||
|
||||
### Email SSL Auto-Detection
|
||||
|
||||
@@ -418,6 +437,8 @@ api_device_tokens: id, user_id, device_id, apns_token, device_name, app_version
|
||||
| `api_notifications` | `/api/v1` | `GET /notifications`, `PATCH /notifications/mark-read` |
|
||||
| `api_stats` | `/api/v1` | `GET /stats/dashboard` — inspector-scoped KPIs with severity breakdown (Phase B) |
|
||||
| `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) |
|
||||
| `tenant_settings` | `/settings` | MT-7: `GET/POST /branding`, `GET /plan`, `GET /domains`, `POST /domains/request`, `POST /domains/<id>/delete` |
|
||||
|
||||
---
|
||||
|
||||
@@ -666,7 +687,7 @@ limiter = Limiter(
|
||||
|
||||
## 17. Alembic Migration Chain
|
||||
|
||||
**Current HEAD:** `phase32_device_token_columns` (30 migrations total).
|
||||
**Current HEAD:** `phase33_tenant_settings` (31 migrations total).
|
||||
|
||||
**Chain root:** `0003_add_user_active` — a guarded squashed baseline (MT-2) that recreates the full 25-table schema with INFORMATION_SCHEMA guards. The original baseline migrations (0001/0002/0003) were lost; this file restores the chain root so Alembic can build the revision map. `down_revision = None`.
|
||||
|
||||
@@ -689,7 +710,8 @@ limiter = Limiter(
|
||||
→ phase25_inspection_gps
|
||||
→ phase26_issue_vendor
|
||||
→ phase27_score_alerts
|
||||
→ ... → phase32_device_token_columns ← HEAD
|
||||
→ ... → phase32_device_token_columns
|
||||
→ phase33_tenant_settings ← HEAD
|
||||
```
|
||||
|
||||
### Fresh DB provisioning (multi-tenant)
|
||||
@@ -1152,7 +1174,7 @@ set -a; . /etc/jqc/control.env; set +a
|
||||
|
||||
---
|
||||
|
||||
## 22. Multi-Tenant Architecture (MT-0 → MT-3)
|
||||
## 22. Multi-Tenant Architecture (MT-0 → MT-7)
|
||||
|
||||
See `MULTI_TENANT_PLAN.md` for the full phased roadmap. This section summarises what is built and operational.
|
||||
|
||||
@@ -1164,6 +1186,199 @@ Shared codebase + **database-per-tenant**. One Flask/Gunicorn process serves all
|
||||
|
||||
Self-contained package, own `ControlBase` + engine/session, own Alembic chain. No imports from `app/`.
|
||||
|
||||
| Module | Purpose |
|
||||
|---|---|
|
||||
| `base.py` | `ControlBase`, `control_session()` context manager, engine from `CONTROL_DATABASE_URL` |
|
||||
| `models.py` | `Plan`, `PlanFeature`, `Tenant`, `TenantDomain`, `Superadmin`, `ProvisioningJob`, `TenantAudit` |
|
||||
| `crypto.py` | Fernet encrypt/decrypt for `tenants.db_password_enc` |
|
||||
| `seed.py` | Idempotent plan seeder (Free / Starter / Pro / Enterprise) |
|
||||
| `cli.py` | `seed`, `create-superadmin`, `list-plans` |
|
||||
| `tenant_migrate.py` | `bootstrap_tenant()`, `upgrade_tenant()`, `chain_head()`, `current_revision()` + CLI |
|
||||
| `provision.py` | `create_tenant()`, `register_tenant_zero()`, `delete_tenant()` + CLI |
|
||||
| `panel/` | Standalone Flask app for superadmin — see MT-4 below |
|
||||
|
||||
### Superadmin control panel (MT-4)
|
||||
|
||||
Separate Flask WSGI app at `admin.jqc.app`, Gunicorn port 8001, `jqc-panel.service` systemd unit.
|
||||
|
||||
**Critical Nginx ordering:** the `admin.jqc.app` server block must appear **before** the `*.jqc.app` wildcard block. If the wildcard catches `admin.jqc.app` first, requests go to port 8000 (main app) which returns 404 "Workspace not found".
|
||||
|
||||
**Impersonation:** panel generates HMAC-SHA256 signed token (`PANEL_IMPERSONATE_KEY`, TTL 60 s) → redirects to `/auth/impersonate?token=<t>` on the tenant → main app validates + sets `session['impersonating_tenant_id']` → tenancy middleware short-circuits Host resolution → "End impersonation" banner clears key.
|
||||
|
||||
### Data-plane changes (`app/`)
|
||||
|
||||
| File / Package | Change |
|
||||
|---|---|
|
||||
| `app/__init__.py` | `db = SQLAlchemy(session_options={'class_': RoutingSession})` + `init_tenancy(app)` + `inject_tenant_branding()` context processor + `hex_to_rgb` Jinja2 filter + `tenant_settings` blueprint |
|
||||
| `config.py` | `MULTI_TENANT_ENABLED` (default `false`) + pool tunables |
|
||||
| `app/tenancy/` | MT-1: resolution + routing. MT-5: `gates.py`, `quota.py`, extended `context.py` + `resolver.py` |
|
||||
| `app/models/tenant_settings.py` | MT-7: per-tenant branding settings (one row per tenant DB) |
|
||||
| `app/routes/tenant_settings.py` | MT-7: `/settings/` blueprint — branding, plan view, domain mgmt |
|
||||
| `app/templates/tenant_settings/` | MT-7: branding, plan, domains templates |
|
||||
| `app/templates/_quota_warning.html` | MT-5: reusable quota exceeded banner partial |
|
||||
| `app/templates/base.html` | MT-7: tenant logo/name in navbar, CSS colour vars injection |
|
||||
|
||||
### Plan tiers
|
||||
|
||||
| Axis | Free | Starter | Pro | Enterprise |
|
||||
|---|---|---|---|---|
|
||||
| Max users | 3 | 15 | 50 | unlimited |
|
||||
| Max facilities | 2 | 10 | 50 | unlimited |
|
||||
| Inspections / month | 50 | 500 | 5 000 | unlimited |
|
||||
| Issues / month | 50 | 500 | 5 000 | unlimited |
|
||||
| Mobile API | ✗ | ✓ | ✓ | ✓ |
|
||||
| Scheduled reports | ✗ | ✗ | ✓ | ✓ |
|
||||
| Branding | ✗ | ✗ | ✓ | ✓ |
|
||||
| Custom domain | ✗ | ✗ | ✓ | ✓ |
|
||||
| Subdomain | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
Quota-exceed behaviour: **soft warn** — allow submit, flag for upgrade, never reject.
|
||||
Feature gate (`@feature_required`): hard 403 — route is blocked.
|
||||
Both decorators are inert when `MULTI_TENANT_ENABLED=false`.
|
||||
|
||||
### Gate decorator stack order
|
||||
|
||||
```python
|
||||
@bp.route(...)
|
||||
@login_required # 1 — must be authenticated
|
||||
@admin_required # 2 — role check (if applicable)
|
||||
@feature_required('x') # 3 — plan feature gate (hard 403)
|
||||
@quota_soft_check('y') # 4 — quota warn (never blocks)
|
||||
def my_route(): ...
|
||||
```
|
||||
|
||||
### Tenant self-service (MT-7) — `/settings/`
|
||||
|
||||
| Tab | Route | What it does |
|
||||
|---|---|---|
|
||||
| Branding | `/settings/branding` | Company name, logo upload, primary/accent colours, support email |
|
||||
| Plan & Usage | `/settings/plan` | Read-only plan info, live quota progress bars |
|
||||
| Domains | `/settings/domains` | List domains, request custom domain (TXT/CNAME verification), delete pending requests |
|
||||
|
||||
`TenantSettings.get_or_default()` returns safe defaults when no row exists — zero migration burden for tenant-zero.
|
||||
|
||||
### Provisioner MySQL account (required grants)
|
||||
|
||||
```sql
|
||||
GRANT ALL PRIVILEGES ON *.* TO 'jqc_provisioner'@'localhost' WITH GRANT OPTION;
|
||||
GRANT CREATE USER ON *.* TO 'jqc_provisioner'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
### CLI quick-reference
|
||||
|
||||
```bash
|
||||
# Source env first — CONTROL_DATABASE_URL not in interactive shell by default
|
||||
set -a; . /etc/jqc/control.env; set +a
|
||||
|
||||
# Control schema + plans + first superadmin (run once)
|
||||
alembic -c control/migrations/alembic.ini upgrade head
|
||||
python -m control.cli seed
|
||||
python -m control.cli create-superadmin --username admin --email you@example.com
|
||||
|
||||
# Adopt existing LT database as tenant-zero (run once, no data move)
|
||||
python -m control.provision register-tenant-zero \
|
||||
--slug lts --name "LT Services" --plan enterprise \
|
||||
--db-host 127.0.0.1 --db-name <LT_DB> --db-user <LT_USER> --db-password '<pw>' \
|
||||
--custom-domain jqc.ltservicesinc.com --base-domain jqc.app
|
||||
|
||||
# Provision a new tenant
|
||||
python -m control.provision create-tenant \
|
||||
--slug acme --name "Acme Corp" --plan pro --admin-email ops@acme.com
|
||||
|
||||
# Delete / deregister a tenant
|
||||
python -m control.provision delete-tenant --slug ztest --drop-db --yes
|
||||
python -m control.provision delete-tenant --slug lts --yes # adopted DB — no --drop-db
|
||||
|
||||
# Migration status + upgrades
|
||||
python -m control.tenant_migrate heads
|
||||
python -m control.tenant_migrate current --tenant all
|
||||
python -m control.tenant_migrate upgrade --tenant all # incremental (phase33+)
|
||||
python -m control.tenant_migrate bootstrap --tenant acme # fresh DB only
|
||||
```
|
||||
|
||||
### Enable multi-tenancy (cutover sequence)
|
||||
|
||||
```bash
|
||||
# 1. Register LT as tenant-zero (see above)
|
||||
# 2. Smoke-test routing (flag still off)
|
||||
curl -sI -H "Host: lts.jqc.app" http://127.0.0.1:8000/ | head -2
|
||||
curl -sI -H "Host: ztest.jqc.app" http://127.0.0.1:8000/ | head -2
|
||||
# 3. Wildcard DNS: *.jqc.app A <SERVER_IP>
|
||||
# 4. Nginx config — admin.jqc.app block BEFORE *.jqc.app wildcard block (see §19)
|
||||
# 5. Wildcard TLS cert (DNS-01, certbot)
|
||||
# 6. Add to /etc/jqc/control.env: MULTI_TENANT_ENABLED=true
|
||||
# 7. sudo systemctl daemon-reload && sudo systemctl restart jqc jqc-panel
|
||||
```
|
||||
|
||||
### Known MT-specific gotchas (rules 71–83)
|
||||
|
||||
| # | Rule |
|
||||
|---|---|
|
||||
| 71 | `CONTROL_FERNET_KEY` must be identical between CLI sessions and Gunicorn — use the same `/etc/jqc/control.env` |
|
||||
| 72 | `MULTI_TENANT_ENABLED` must be flipped only after tenant-zero is registered and domains verified |
|
||||
| 73 | Tenant DB passwords use `_gen_password()` — MySQL `validate_password` MEDIUM requires lower+upper+digit+special |
|
||||
| 74 | `bootstrap_tenant()` for fresh DBs; `upgrade_tenant()` for incremental (phase33+) |
|
||||
| 75 | `delete_tenant()` + `_add_domains()` are retry-safe |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 83 | `hex_to_rgb` Jinja2 filter required for `--bs-primary-rgb` CSS var — register in `app/__init__.py` |
|
||||
|
||||
---
|
||||
|
||||
## 23. Coding Rules for AI Assistants
|
||||
|
||||
These rules apply to every change made to this codebase, without exception.
|
||||
|
||||
### Before Writing Any Code
|
||||
|
||||
**Rule 1 — Read the actual file on disk first.**
|
||||
Use the view or read tool on every file that will be modified. Never rely on output from a previous turn — a prior edit invalidates earlier view output. Always re-read before a second edit to the same file.
|
||||
|
||||
**Rule 2 — Trace the full request path.**
|
||||
For any bug, follow the request from the browser through: decorator → form validation → route logic → DB write → template render. Identify the exact layer where the failure occurs before proposing a fix.
|
||||
|
||||
**Rule 3 — Find the root cause. No assumptions.**
|
||||
"It might be X" is not sufficient. Confirm X by reading the relevant code. State the root cause explicitly in the response.
|
||||
|
||||
**Rule 4 — Check both layers of enforcement.**
|
||||
Feature gates and quota checks must be applied in BOTH web routes AND `/api/v1` API endpoints. A web-only check is bypassable by the iPad app.
|
||||
|
||||
### Making Changes
|
||||
|
||||
**Rule 5 — Make the smallest possible change.**
|
||||
Do not rewrite surrounding code. Do not rename variables, restructure functions, or reformat blocks unless explicitly requested.
|
||||
|
||||
**Rule 6 — Never remove functionality** that was not explicitly asked to be removed.
|
||||
|
||||
**Rule 7 — Preserve all route names, function names, and variable names** unless explicitly directed otherwise.
|
||||
|
||||
**Rule 8 — Keep all log handlers.**
|
||||
|
||||
**Rule 9 — One atomic commit per logical transaction.**
|
||||
|
||||
### Before Providing the Output
|
||||
|
||||
**Rule 10 — Verify the fix in the file on disk.**
|
||||
After applying a patch, read the changed section back and confirm the intended change is present, no surrounding code was accidentally removed, and import statements are consistent.
|
||||
|
||||
**Rule 11 — Check for introduced bugs.**
|
||||
Ask: Does this change break any other code path that uses the modified function, field, or query?
|
||||
|
||||
**Rule 12 — State the root cause explicitly** in the deployment instructions. "Changed X to Y" is not enough — explain *why* the old code failed and *how* the fix resolves it.
|
||||
|
||||
**Rule 13 — List every file changed** with the exact location of each change (function name and what was modified).
|
||||
|
||||
**Rule 14 — Migrations are required for any schema change.**
|
||||
Follow the `phase{N}_description.py` naming convention. The new migration's `down_revision` must point to the current HEAD (`phase33_tenant_settings`). Use `INFORMATION_SCHEMA` existence checks so migrations are safe to re-run. Never use `batch_alter_table` for MySQL.
|
||||
|
||||
Self-contained package, own `ControlBase` + engine/session, own Alembic chain. No imports from `app/`.
|
||||
|
||||
| Module | Purpose |
|
||||
|---|---|
|
||||
| `base.py` | `ControlBase`, `control_session()` context manager, engine from `CONTROL_DATABASE_URL` |
|
||||
|
||||
Reference in New Issue
Block a user