Jul 10 - Update codes to catch up with the single tenant project (Medium)

This commit is contained in:
2026-07-10 12:58:57 -04:00
parent e41998b561
commit aa749107c7
12 changed files with 815 additions and 68 deletions
+42 -4
View File
@@ -2,7 +2,7 @@
> **Audience:** AI assistants and developers working on this codebase.
> **Purpose:** Authoritative reference for architecture, conventions, gotchas, and decisions.
> **Last reviewed:** July 2026 (doc-reconciliation pass — verified against code on disk. Adds previously-undocumented phase28 notify-fix, phase29 broadcasts, phase3032 device registry; `broadcast` + `devices` + `api_devices` blueprints; Broadcast + DeviceRegistration models; corrected MT-8 billing status to DONE; resolved the device-registration collision (rule 84 — removed duplicate `api_devices` blueprint + `DeviceRegistration` model, consolidated on `DeviceToken`). Prior: Phase 19 + mobile API Phases AE + Phase 22 comment visibility + Phase 23 support chat/tickets + inspector Excel export + inspection list filters + customer issue logging + AI chatbot + dashboard grouped sections + issues/inspections PDF export + Reports R1R4 + Phase 24 notify defaults + Phase 25 GPS + Phase 26 vendor fields + Phase 27 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; fail2ban; welcome email; dunning day-3/7/14; ProxyFix middleware; QR occupant issue reporting; issue handler type (phase39); MT-9 iOS pending**)
> **Last reviewed:** July 2026 (doc-reconciliation pass — verified against code on disk. Adds previously-undocumented phase28 notify-fix, phase29 broadcasts, phase3032 device registry; `broadcast` + `devices` + `api_devices` blueprints; Broadcast + DeviceRegistration models; corrected MT-8 billing status to DONE; resolved the device-registration collision (rule 84 — removed duplicate `api_devices` blueprint + `DeviceRegistration` model, consolidated on `DeviceToken`). Prior: Phase 19 + mobile API Phases AE + Phase 22 comment visibility + Phase 23 support chat/tickets + inspector Excel export + inspection list filters + customer issue logging + AI chatbot + dashboard grouped sections + issues/inspections PDF export + Reports R1R4 + Phase 24 notify defaults + Phase 25 GPS + Phase 26 vendor fields + Phase 27 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; fail2ban; welcome email; dunning day-3/7/14; ProxyFix middleware; QR occupant issue reporting; issue handler type (phase39); support chat persistence + knowledge base (phase40); MT-9 iOS pending**)
---
@@ -381,6 +381,27 @@ facility_score_alerts: id, facility_id (FK→facilities CASCADE), sent_at DATETI
Records each score-trend alert dispatched for a facility. `send_score_alerts()` queries this table to skip re-alerting a facility within the last 24 hours, preventing notification storms on persistent score drops.
### SupportChatSession / SupportChatMessage (phase40)
```
support_chat_sessions: id, customer_id (FK→users SET NULL), title VARCHAR(200) nullable,
created_at DATETIME, last_msg_at DATETIME
support_chat_messages: id, session_id (FK→support_chat_sessions CASCADE),
role VARCHAR(20) ('user'/'assistant'), content TEXT, created_at DATETIME
```
AI chat conversations are now persisted to the DB. `GET /support/chat?session_id=N` loads a prior session's history. The `POST /support/chat/message` endpoint creates a new session (via `flush()`) on first message and persists both turns after the Groq call succeeds; it rolls back if Groq fails (no empty sessions). The session's `title` is auto-set from the first user message (truncated to 100 chars). History is loaded from DB for Groq context (last 40 messages) — the client no longer sends the history array.
### SupportKnowledge (phase40)
```
support_knowledge: id, title VARCHAR(200), body TEXT, active BOOL DEFAULT TRUE,
created_by (FK→users SET NULL), created_at DATETIME, updated_at DATETIME
```
Admin-curated knowledge base entries. Active entries are appended to the Groq system prompt via `_system_prompt_with_kb()`, capped at `_KB_MAX_CHARS = 6000`. Managed at `/support/admin/knowledge` (`@supervisor_required`): add, edit, toggle active/inactive, delete. Deactivated entries are preserved but skipped from the system prompt.
### SupportTicket / SupportTicketReply
```
@@ -520,7 +541,7 @@ The `DeviceRegistration` model and the duplicate `api_devices` blueprint were **
| `scheduled_reports` | `/scheduled-reports` | CRUD + manual trigger (accessible via Reports sub-nav) |
| `inspection_schedules` | `/inspection-schedules` | phase34 — recurring inspection CRUD (`@project_manager_required`) + `POST /run-now` (manual) + `POST /run` (token-protected cron materialiser) |
| `work_orders` | `/work-orders` | phase36 — **public, login-less** vendor pages: `GET /<token>` (contractor view) + `POST /<token>` (acknowledge/complete). Token is the authorization. Staff dispatch is `POST /issues/<id>/work-order` on the `issues` blueprint (`@project_manager_required`). |
| `support` | `/support` | `GET /chat`, `POST /chat/message` (AJAX→Groq), `POST /tickets`, `GET /my-tickets`, `GET/POST /my-tickets/<id>`, `GET /admin/tickets`, `GET/POST /admin/tickets/<id>` |
| `support` | `/support` | `GET /chat` (accepts `?session_id=`), `POST /chat/message` (AJAX→Groq, persists turns, returns `session_id`), `POST /tickets`, `GET /my-tickets`, `GET/POST /my-tickets/<id>`, `GET /my-conversations`, `GET /my-conversations/<id>`, `GET /admin/tickets`, `GET/POST /admin/tickets/<id>`, `GET /admin/conversations`, `GET /admin/conversations/<id>`, `GET /admin/knowledge`, `POST /admin/knowledge/add`, `GET/POST /admin/knowledge/<id>/edit`, `POST /admin/knowledge/<id>/toggle`, `POST /admin/knowledge/<id>/delete` (phase40) |
| `broadcast` | `/admin/broadcast` | `GET /` (compose + history), `POST /send` — admin-only push to iOS via Notification rows (phase29) |
| `devices` | `/admin/devices` | `GET /` (registered device list), `POST /notify` — notify users on outdated app versions (reads `api_device_tokens`) |
| `api` | `/api/v1` | parent blueprint |
@@ -791,7 +812,7 @@ limiter = Limiter(
## 17. Alembic Migration Chain
**Current HEAD:** `phase38_facility_qr` (36 migrations total).
**Current HEAD:** `phase40_support_chat_kb` (38 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`.
@@ -825,7 +846,22 @@ limiter = Limiter(
→ phase36_issue_work_orders
→ phase37_contract_recipients
→ phase38_facility_qr
→ phase39_issue_handler_type ← HEAD
→ phase39_issue_handler_type → phase40_support_chat_kb ← HEAD
```
### phase40_support_chat_kb
Creates three tables backing support chat persistence and the AI knowledge base:
- `support_chat_sessions` — one row per customer chat thread (`customer_id`, `title`, `created_at`, `last_msg_at`)
- `support_chat_messages` — individual turns (`session_id` CASCADE, `role` user/assistant, `content`, `created_at`)
- `support_knowledge` — admin-curated chatbot context entries (`title`, `body`, `active`, `created_by`, `created_at`, `updated_at`)
All three tables guarded with `INFORMATION_SCHEMA` table-existence checks — safe to re-run. `down_revision = 'phase39_issue_handler_type'`.
**Deploy order:**
```bash
flask db upgrade
sudo systemctl restart gunicorn
```
### phase39_issue_handler_type
@@ -1370,6 +1406,8 @@ set -a; . /etc/jqc/control.env; set +a
| 92 | **`POST /f/<token>/report` creates issues with `reported_by=None` — honeypot protects it** | phase39. The occupant report endpoint shares the same authorization model as rule 91 (token = credential, no login). The honeypot field (`name="website"`, CSS-hidden, `position:absolute;left:-9999px`) silently drops bot submissions by redirecting to the success URL without creating an issue. Rate-limited `5/hr` per IP. The notification fires `notify_by_matrix('issue_created', issue_id=..., facility_id=...)` so admins are notified via the standard matrix. Do not add login gates, photo upload, or internal fields (assignee, comments) to this form — it is intentionally minimal. |
| 93 | **ProxyFix must wrap `app.wsgi_app` — without it, rate limiting and fail2ban are broken** | `app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)` reads `X-Forwarded-For` set by Nginx. Without it, `get_remote_address()` returns `127.0.0.1` for every request — Flask-Limiter shares one counter across all users and fail2ban can never ban an attacker's real IP. Always set in `create_app()` immediately after `app = Flask(__name__)`. |
| 94 | **`handler_type` NULL and `'internal'` are equivalent** | NULL means the column was not set (pre-phase39 row or unmodified new row); the application treats both as "Janitorial Staff". The dashboard `handler_breakdown['internal']` counter and the `?handler_type=internal` issues-list filter both use `db.or_(Issue.handler_type == 'internal', Issue.handler_type.is_(None))`. Never coerce NULL to 'internal' at the DB layer — the nullable default is intentional for backwards compatibility. |
| 95 | **Chat history is loaded from DB — never pass client-sent history to Groq** | phase40. `POST /support/chat/message` loads prior turns from `SupportChatMessage` (newest-first, limit 40, reversed). The JSON body sends only `{ message, session_id }` — no history array. This prevents history tampering by clients and ensures accuracy across page reloads. |
| 96 | **`db.session.flush()` to get session ID before first message insert** | When creating a new `SupportChatSession` in `chat_message()`, call `db.session.flush()` after `db.session.add(chat_session)` to get the autoincrement `id` before constructing `SupportChatMessage` rows. If Groq fails, `db.session.rollback()` undoes the flush — no orphaned empty session is left in the DB. |
---