Jul 10 - Update codes to catch up with the single tenant project
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:** July 2026 (doc-reconciliation pass — verified against code on disk. Adds previously-undocumented phase28 notify-fix, phase29 broadcasts, phase30–32 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 A–E + 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 R1–R4 + 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; MT-9 iOS pending**)
|
||||
> **Last reviewed:** July 2026 (doc-reconciliation pass — verified against code on disk. Adds previously-undocumented phase28 notify-fix, phase29 broadcasts, phase30–32 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 A–E + 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 R1–R4 + 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**)
|
||||
|
||||
---
|
||||
|
||||
@@ -333,9 +333,15 @@ issues: id, inspection_id (nullable), area_id, facility_id (nullable), severity
|
||||
mobile_local_id VARCHAR(64) nullable indexed, ← Phase B
|
||||
vendor_name VARCHAR(100) nullable, ← Phase 26
|
||||
vendor_contact VARCHAR(200) nullable, ← Phase 26
|
||||
vendor_notes TEXT nullable ← Phase 26
|
||||
vendor_notes TEXT nullable, ← Phase 26
|
||||
handler_type ENUM('internal','facility','vendor') nullable, ← phase39
|
||||
facility_handler_name VARCHAR(100) nullable, ← phase39
|
||||
facility_handler_contact VARCHAR(200) nullable, ← phase39
|
||||
facility_handler_notes TEXT nullable ← phase39
|
||||
```
|
||||
|
||||
**`handler_type` (phase39):** Who is responsible for resolving the issue. `NULL` and `'internal'` both mean janitorial staff (the default). `'facility'` activates the `facility_handler_*` sub-fields (contact at the building). `'vendor'` indicates an external contractor and cross-references the existing `vendor_*` fields and work orders. Editable by admin/director/project_manager on the issue update form. Dashboard shows a three-card handler breakdown for staff roles. Issues list accepts `?handler_type=` filter. `Issue.HANDLER_LABELS` maps enum values to display names.
|
||||
|
||||
**Photo columns — three distinct fields with different semantics:**
|
||||
|
||||
| Column | Type | Populated by | Displayed as |
|
||||
@@ -818,7 +824,18 @@ limiter = Limiter(
|
||||
→ phase35_user_mfa
|
||||
→ phase36_issue_work_orders
|
||||
→ phase37_contract_recipients
|
||||
→ phase38_facility_qr ← HEAD
|
||||
→ phase38_facility_qr
|
||||
→ phase39_issue_handler_type ← HEAD
|
||||
```
|
||||
|
||||
### phase39_issue_handler_type
|
||||
|
||||
Adds four nullable columns to `issues`: `handler_type ENUM('internal','facility','vendor')`, `facility_handler_name VARCHAR(100)`, `facility_handler_contact VARCHAR(200)`, `facility_handler_notes TEXT`. Also enables the `POST /f/<token>/report` occupant issue reporting endpoint on the `facility_qr` blueprint (no schema change needed — reuses the `issues` table). Guarded with `INFORMATION_SCHEMA` existence checks — safe to re-run.
|
||||
|
||||
**Deploy order:**
|
||||
```bash
|
||||
flask db upgrade
|
||||
sudo systemctl restart gunicorn
|
||||
```
|
||||
|
||||
### phase38_facility_qr
|
||||
@@ -1350,6 +1367,9 @@ set -a; . /etc/jqc/control.env; set +a
|
||||
| 88 | **Password strength enforced by one shared `strong_password()` validator** | Lives in `app/utils/forms.py`: ≥8 chars, at least one letter AND one digit, and not in a small common-password blocklist. Applied to every password-setting form — `ProfileForm`, `UserForm`, `CustomerForm`, `ResetPasswordForm`, `SetPasswordForm`, and `signup.SignupForm` (imports it). Sits after `Optional()` on edit forms (skips blank = "leave unchanged"). Do not re-introduce ad-hoc `Length(min=6)` password rules — route new password fields through `strong_password()` so the policy stays consistent. |
|
||||
| 90 | **Per-contract recipients dispatch INSIDE `notify_by_matrix()` — never call `_notify_project_recipients()` from routes** | phase37. Contract-scoped recipients (`ProjectNotificationRecipient`) are dispatched automatically at the end of `notify_by_matrix()`, after matrix roles + global custom emails, with dedup against both. The contract is resolved from `facility_id` arg → `issue.resolved_facility` → `inspection.facility_id`; events fired without any facility context reach matrix recipients only. New `notify_by_matrix()` call sites should pass `facility_id` (or `issue_id`/`inspection_id`) so contract recipients fire. The `score_alert` cron call in `sla.py` now passes `facility_id=fid` for this reason (side effect: if the matrix ever enables `customer` for `score_alert`, customers are facility-scoped instead of org-wide — a strict improvement). Staff recipients use `respect_preferences=False` (contract config is the authority, same as matrix broadcasts). |
|
||||
| 91 | **Facility QR scan page is public and token-authorized — counts + scores ONLY** | phase38, same authorization class as rule 89: `GET /f/<token>` has NO `@login_required`; the unguessable `facilities.qr_token` is the sole credential, because QR posters hang in public hallways. The page must NEVER render free-text issue descriptions, inspector/staff names, photos, or comments — only aggregate counts, scores, dates, template names, and severity/SLA counts. Rate-limited `60/hr`. Inactive facilities 404. The hybrid full-view button appears only when `current_user` is authenticated AND their role scope covers the facility (`_can_view_full()` — staff always; inspector/customer via scope utils); the internal page re-enforces scope anyway. QR URLs are built from `request.host_url` (rule 64 pattern) so each tenant's posters carry its own domain — the route resolves by Host and is NOT tenant-exempt. `qr_svg()` lives in `app/utils/qr.py` (general-purpose; the TOTP-specific `mfa.qr_svg()` mirrors stay untouched). Rotate a leaked poster with `POST /facilities/<id>/qr/regenerate`. |
|
||||
| 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. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user