05/29 Update document CLAUDE.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:** May 2026 (Phase 19 complete + mobile API gap-fill Phases A–E: issue comments, dashboard stats with severity breakdown, area_name/assigned_to_name in issue payload, notification inbox, area_id on issues, CSRF-exempt pattern for all new blueprints)
|
> **Last reviewed:** May 2026 (Phase 19 complete + mobile API gap-fill Phases A–E + customer UI refinements: contract filter scoping on Inspections/Issues list, dynamic invitation email domain from `request.host_url`, "Your Facilities" card-grid dashboard panel)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -637,6 +637,18 @@ Pages using this pattern: `issues/form.html` (create), `issues/list.html` (filte
|
|||||||
|
|
||||||
The issues list and inspections list both accept a `contract_id` query param that filters the DB query to facilities belonging to that contract (`facility.project_id == contract_id`) and narrows the facility dropdown in the rendered HTML.
|
The issues list and inspections list both accept a `contract_id` query param that filters the DB query to facilities belonging to that contract (`facility.project_id == contract_id`) and narrows the facility dropdown in the rendered HTML.
|
||||||
|
|
||||||
|
**Customer role — contract filter scoping:** In `inspections.index()` and `issues.index()`, the `projects` list passed to the template is scoped to contracts the customer is assigned to via `CustomerAssignment`. Non-customer roles still receive all active projects. This prevents customers from seeing contracts they have no assignment to in the Contract filter dropdown.
|
||||||
|
|
||||||
|
### Customer Dashboard — "Your Facilities" Panel
|
||||||
|
|
||||||
|
Rendered in `dashboard.html` for `current_user.role == 'customer'`. Uses a Bootstrap card grid instead of a table:
|
||||||
|
|
||||||
|
- **Grid:** `col-12 col-sm-6 col-lg-4` — 3 per row on large, 2 on medium, 1 on small.
|
||||||
|
- **Collapse (> 9 facilities):** First 9 cards are shown; a "Show all N facilities" toggle reveals the rest. Controlled by inline JS (`toggleFacilities` button, `VISIBLE = 9` constant).
|
||||||
|
- **Live search (> 6 facilities):** A `#facilitySearch` text input filters `.facility-col` cards in real time by matching against the card's full text content. The show-more bar hides while a search query is active.
|
||||||
|
- **Count badge:** The card header always shows the total facility count as a `badge bg-secondary rounded-pill`.
|
||||||
|
- The JS block is only emitted when `customer_facilities|length > 9`; the search input is only emitted when `customer_facilities|length > 6`.
|
||||||
|
|
||||||
### Inspection Execute Page — UX Patterns
|
### Inspection Execute Page — UX Patterns
|
||||||
|
|
||||||
- **Photo upload-on-select**: `uploadPhotoField(input)` fires immediately on `<input type="file">` change. XHR to `POST /<id>/upload-photo`. On success, the server path is written to `<input type="hidden" id="field_<fid>_server_path">` and a `<img id="thumb_<fid>">` is shown.
|
- **Photo upload-on-select**: `uploadPhotoField(input)` fires immediately on `<input type="file">` change. XHR to `POST /<id>/upload-photo`. On success, the server path is written to `<input type="hidden" id="field_<fid>_server_path">` and a `<img id="thumb_<fid>">` is shown.
|
||||||
@@ -735,6 +747,9 @@ timeout = 30
|
|||||||
| 60 | **`flag_issue` offcanvas form must include `<input type="hidden" name="facility_id">`** | `IssueForm.facility_id` has `DataRequired()`. The hand-written offcanvas form in `execute.html` is not rendered by WTForms, so it must explicitly send `facility_id`. Without it, `form.validate_on_submit()` silently returns `False`, the server responds `200 OK` with the `flag_issue.html` template, and the JS treats `res.ok` as success — no issue is ever saved. Fix: `<input type="hidden" name="facility_id" value="{{ inspection.facility_id }}">` inside `#flagIssueForm`. |
|
| 60 | **`flag_issue` offcanvas form must include `<input type="hidden" name="facility_id">`** | `IssueForm.facility_id` has `DataRequired()`. The hand-written offcanvas form in `execute.html` is not rendered by WTForms, so it must explicitly send `facility_id`. Without it, `form.validate_on_submit()` silently returns `False`, the server responds `200 OK` with the `flag_issue.html` template, and the JS treats `res.ok` as success — no issue is ever saved. Fix: `<input type="hidden" name="facility_id" value="{{ inspection.facility_id }}">` inside `#flagIssueForm`. |
|
||||||
| 61 | **Contract→Facility cascade UI pattern: contract selector is UI-only, not a WTForms field** | The "Log New Issue" form (`issues/form.html`) and both filter bars (`issues/list.html`, `inspections/list.html`) use a plain HTML `<select id="...contract...">` that triggers an AJAX call to `GET /inspections/facilities_for_project/<id>` on change, repopulating the facility dropdown. `IssueForm.facility_id.choices` is always set to ALL active facilities in the route so POST validation passes regardless of which contract was selected in the UI. On POST error re-render, the route derives `selected_project_id` from the submitted `facility_id`'s `project_id` and passes it to the template so JS can restore both selectors. |
|
| 61 | **Contract→Facility cascade UI pattern: contract selector is UI-only, not a WTForms field** | The "Log New Issue" form (`issues/form.html`) and both filter bars (`issues/list.html`, `inspections/list.html`) use a plain HTML `<select id="...contract...">` that triggers an AJAX call to `GET /inspections/facilities_for_project/<id>` on change, repopulating the facility dropdown. `IssueForm.facility_id.choices` is always set to ALL active facilities in the route so POST validation passes regardless of which contract was selected in the UI. On POST error re-render, the route derives `selected_project_id` from the submitted `facility_id`'s `project_id` and passes it to the template so JS can restore both selectors. |
|
||||||
| 62 | **`issue.resolved_facility.project` and `inspection.facility.project` give the contract** | `Project.facilities` declares `backref='project'`, so `facility.project` is a direct ORM attribute (not a dynamic query). Guard all template accesses: `ins.facility.project.name if ins.facility and ins.facility.project else '—'`. The contract name is displayed in the issues list, issues detail, and inspections list; the issues list also accepts a `contract_id` query param that pre-filters the facility dropdown server-side. |
|
| 62 | **`issue.resolved_facility.project` and `inspection.facility.project` give the contract** | `Project.facilities` declares `backref='project'`, so `facility.project` is a direct ORM attribute (not a dynamic query). Guard all template accesses: `ins.facility.project.name if ins.facility and ins.facility.project else '—'`. The contract name is displayed in the issues list, issues detail, and inspections list; the issues list also accepts a `contract_id` query param that pre-filters the facility dropdown server-side. |
|
||||||
|
| 63 | **Customer Contract filter scoped to assigned contracts only** | `inspections.index()` and `issues.index()` build the `projects` list differently for `customer` role: query `CustomerAssignment.query.filter_by(user_id=current_user.id)` to get assigned `project_id` values, then filter `Project` to that set. All other roles still receive all active projects. Pattern mirrors the existing inspector scoping in `inspections.start()`. |
|
||||||
|
| 64 | **Invitation email sender and link domain are derived from `request.host_url`** | `_send_invite_email(user, token, base_url=None)` in `customers.py` accepts an optional `base_url`. Both call sites (`invite` and `resend_invite`) pass `request.host_url`. Inside the function, `effective_base` is built from that value (falling back to `APP_BASE_URL`); `setup_link` uses `effective_base`; `sender` is `noreply@<netloc>` parsed from `effective_base`. The SMTP server and credentials are unchanged — only the `From` address and link URL vary per domain. |
|
||||||
|
| 65 | **Customer "Your Facilities" uses a card grid, not a table** | See §18 "Customer Dashboard — Your Facilities Panel". Never revert to a full-width table for this section. The show-more threshold is `VISIBLE = 9`; the search input threshold is `> 6`. Both thresholds live as JS/Jinja constants in `dashboard.html` and can be adjusted together if needed. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user