Aug 27 - Update code to follow-up with ST functions

This commit is contained in:
2026-08-27 11:54:07 -04:00
parent 3bfd81c84c
commit 2d68bad966
10 changed files with 617 additions and 16 deletions
+82 -3
View File
@@ -843,7 +843,7 @@ limiter = Limiter(
## 17. Alembic Migration Chain
**Current HEAD:** `phase55_template_contracts`.
**Current HEAD:** `phase56_followup_assignee`.
**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`.
@@ -884,7 +884,8 @@ limiter = Limiter(
→ phase48_schedule_parent_inspection → phase49_followup_requested_by
→ phase50_sched_acknowledged → phase51_external_inspector
→ phase52_user_ui_theme → phase53_knowledge_sort_order
→ phase54_user_notif_matrix → phase55_template_contracts ← HEAD
→ phase54_user_notif_matrix → phase55_template_contracts
→ phase56_followup_assignee ← HEAD
```
`phase54` / `phase55` port the ST August-2026 work (ST calls them phase51 /
@@ -907,7 +908,22 @@ means SHARED**, so every pre-existing form stays available everywhere and the
migration cannot change behaviour on deploy. Table-existence check — safe to
re-run.
**Deploy order for both (tenant DBs):**
#### phase56 — assign a follow-up to another inspector
Adds `inspections.follow_up_assigned_to` (FK → `users.id`, ON DELETE SET NULL)
— see §27c. **No backfill:** NULL means the follow-up belongs to the
inspection's own inspector, which is what every existing row already means, so
deploying cannot change who owns anything.
**This is the THIRD FK from `inspections` to `users`** (`inspector_id`,
`follow_up_requested_by`, and now this). Every relationship spanning the two
tables must pin `foreign_keys` explicitly or the mapper is ambiguous — and it
raises on first ORM *use*, not at import, so the app starts cleanly and then
every request 500s. Column + constraint checks — safe to re-run.
ST calls this phase53; MT's chain was already past that number. Match by NAME.
**Deploy order (tenant DBs):**
```bash
python -m control.tenant_migrate upgrade --tenant all
sudo systemctl restart gunicorn
@@ -1507,6 +1523,8 @@ set -a; . /etc/jqc/control.env; set +a
| 105 | **The flag-issue assignee list is contract-scoped, and BOTH call sites must use `_assignable_staff_for()`; a failed flag-issue POST must return non-2xx** | `execute()` renders the dropdown, `flag_issue()` builds the choices that validate the POST — the choices are the security boundary. An org-wide list let anyone assign another client's Customer Inspector, who was then emailed the facility name and issue description. And the offcanvas JS branches on `res.ok`, so a 200 re-render of an invalid form reads as success: the panel closes, the page reloads, and no issue exists. |
| 106 | **Name the Groq model in the chat error log, and keep `_DEFAULT_GROQ_MODEL` current** | Groq retires models without notice; when the configured one disappears the API 404s and EVERY question returns the generic "problem reaching the AI assistant" reply, with nothing else broken — invisible until a customer complains. The fix needs no deploy, only `GROQ_MODEL`, which is exactly what the log line must say. |
| 107 | **`viewer_is_our_staff` in `issues/view.html` is an explicit role ALLOWLIST, and `external_inspector` is absent on purpose** | `not current_user.is_customer_account` fails OPEN — a missing attribute yields Jinja `Undefined`, `not Undefined` is true, and the internal-process chrome renders for exactly the accounts it must be hidden from. This is not a rule-87 violation: rule 87 governs capability/scoping, where a Customer Inspector must behave like our inspector; this asks "does this person work for us?", the one place the two genuinely differ. |
| 108 | **A follow-up has exactly ONE owner: use `follow_up_owner` (row) / `follow_up_owned_by()` (query) — never re-derive it** | Assignee when set, original inspector otherwise. The API's two arms must be mutually exclusive (`follow_up_assigned_to == me` OR `assigned_to IS NULL AND inspector_id == me`); drop the `IS NULL` and two people turn up for the same re-inspection. The authorship filter must be DEFERRED when `follow_up_required=true` is requested, or the rows the assignee needs are hidden before the ownership test runs. |
| 109 | **Inspector READ access is facility scope; WRITE access is authorship** | `index()` lists by facility (rule 58), so `view()`/`export_pdf()` must too — scoping reads by authorship made the list offer rows that said "Access denied" on click, and locked the follow-up assignee out of the parent inspection. `execute`, `save_draft_ajax`, `upload_photo_ajax` and `flag_issue` keep the authorship check: readable is not editable. |
| 98 | **Reports R1 + R2 contract cascade is client-side only — facility_id is the sole DB filter** | The Contract dropdown in `reports/issues_aging.html` and `reports/sla_compliance.html` has no `name` attribute and is never submitted. It exists only to narrow the Facility `<select>` in the browser via `GET /inspections/facilities_for_project/<id>`. The routes receive and filter on `facility_id`; `contract_id` plays no role server-side. Do not add server-side `contract_id` filtering to these routes — it would duplicate what `facility_id` already provides. |
---
@@ -1968,6 +1986,67 @@ copies the restrictions.
---
## 27c. Follow-up assignment + inspector read access (Aug 2026 ST parity)
Ported from ST (its phase53 + the two fixes shipped beside it).
### Assigning a follow-up (phase56)
A follow-up used to belong implicitly to whoever performed the original
inspection. `inspections.follow_up_assigned_to` lets a director — or a
**Customer Director**, for their own facilities — hand the re-inspection to
someone else. `Inspection.follow_up_owner` (assignee *or* inspector) is the
single definition of ownership, so the web display, the notification and the
mobile API filter cannot disagree.
**The assignee takes over**: only the owner is notified, and only the owner
sees it. In `GET /api/v1/inspections?follow_up_required=true` the two arms are
mutually exclusive — without `is_(None)` on the second arm the original
inspector keeps seeing a follow-up handed to someone else and two people turn
up to do it. The generic "inspectors see only their own inspections" filter is
**deferred** when follow-ups are requested, because an assigned follow-up lives
on an inspection somebody else performed.
The picker (`_followup_assignees_for()`) is contract-scoped for the same reason
the flag-issue list is (rule 105), offers only the two INSPECTOR roles, and the
POST re-validates against it. A facility with no contract offers nobody —
fail-closed, the follow-up stays with the original inspector.
**MT-only gap closed on the way:** MT's `GET /api/v1/inspections` had no
`follow_up_required` filter at all, so the iPad's Follow-up Requests screen
received the inspector's entire history. The filter now matches the web's
definition of "follow-up" — flagged, completed, and not yet answered by a
linked re-inspection (`~follow_ups.any()`).
### Inspector READ access follows the list, not authorship
`index()` scopes an inspector by FACILITY (rule 58), but `view()` and
`export_pdf()` scoped by authorship — so the list offered rows that answered
"Access denied" on click, and the follow-up assignee could not open the parent
inspection they had just been asked to re-inspect. Both reads now use
`_inspector_may_read()` (facility scope). **Writes stay owner-only**: `execute`,
`save_draft_ajax`, `upload_photo_ajax` and `flag_issue` keep the authorship
check. `reinspect()` belongs to the follow-up's owner; the buttons render only
for `is_own_inspection or owns_follow_up`, so the page never shows a control
that fails on click.
### One rule, two expressions, three callers
Ownership has to be stated twice — once for a loaded row, once in SQL — so both
live together in `models/inspection.py`:
* `follow_up_owner` — the property (assignee, else inspector)
* `follow_up_owned_by(user_id)` — the query predicate
Every query that scopes follow-ups calls the predicate: the mobile list filter,
the web dashboard card, and the iPad stats KPI. They each used to write their
own version and three tested AUTHORSHIP, so an assignee saw the work in their
list while both dashboards read 0 — the stats KPI sitting directly above the
Follow-up Requests list it disagreed with. Fixed in ST at the same time.
Pinned by `tests/test_followup_ownership.py`.
---
## 28. Coding Rules for AI Assistants
These rules apply to every change made to this codebase, without exception.