06/18 Update CLAUDE.md & notification_matrix.py

This commit is contained in:
Nguyen Ngo
2026-06-18 14:13:59 -04:00
parent d8880ba463
commit bf9a81ce5f
2 changed files with 72 additions and 3 deletions
+63 -3
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:** June 2026 (Phase 19 complete + mobile API gap-fill Phases AE + 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 R1R4)
> **Last reviewed:** June 2026 (Phase 19 complete + mobile API gap-fill Phases AE + 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 R1R4 + Phase 24 issue_created notify defaults + Phase 25 inspection GPS + Phase 26 issue vendor fields + Phase 27 facility score alerts)
---
@@ -217,6 +217,8 @@ inspections: id, template_id, facility_id, area_id, inspector_id, inspection_dat
overall_score, status (in_progress/completed/flagged), notes, form_data (JSON),
completed_at, parent_inspection_id (self-FK), follow_up_required, follow_up_note,
mobile_local_id VARCHAR(64) nullable indexed ← Phase B
submit_latitude DECIMAL(10,7) nullable ← Phase 25
submit_longitude DECIMAL(10,7) nullable ← Phase 25
```
**`mobile_local_id`:** UUID string generated on the iPad. Used for idempotency — if a submission arrives twice (network retry), the server returns the existing record without creating a duplicate. Set `NULL` for all web-created inspections.
@@ -232,7 +234,10 @@ issues: id, inspection_id (nullable), area_id, facility_id (nullable), severity
reported_at, resolved_at, result_notes, result_photos (JSON),
mobile_photo_paths (JSON), ← Phase 19
verified_by, verified_at, verification_note, sla_notified,
mobile_local_id VARCHAR(64) nullable indexed ← Phase B
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
```
**Photo columns — three distinct fields with different semantics:**
@@ -264,6 +269,16 @@ issue_comments: id, issue_id (FK), user_id (FK), body, created_at,
**`is_customer_visible`:** Staff comments are hidden from customers by default (`False`). Staff can tick "Share with customer" at post time to set `True`. Customer-authored comments are always stored as `True`. Customers see only `is_customer_visible=True` comments; staff see all.
### FacilityScoreAlert
```
facility_score_alerts: id, facility_id (FK→facilities CASCADE), sent_at DATETIME,
current_avg DECIMAL(5,2), prior_avg DECIMAL(5,2), delta DECIMAL(5,2)
INDEX ix_fsa_facility_sent (facility_id, sent_at)
```
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.
### SupportTicket / SupportTicketReply
```
@@ -552,6 +567,7 @@ EVENT_SLA_ALERT = 'sla_alert'
EVENT_ISSUE_FLAGGED = 'issue_flagged'
EVENT_CUSTOMER_INSPECTION_DONE = 'customer_inspection_completed'
EVENT_CUSTOMER_ISSUE_UPDATED = 'customer_issue_updated'
EVENT_SCORE_ALERT = 'score_alert' ← Phase 27
```
### Cron Endpoints (all require `token=DIGEST_SECRET`)
@@ -561,6 +577,7 @@ EVENT_CUSTOMER_ISSUE_UPDATED = 'customer_issue_updated'
| `POST /notifications/send-digest` | Digest email delivery | `0 7 * * *` |
| `POST /notifications/check-sla` | SLA breach/at-risk alerts | `*/30 * * * *` |
| `POST /notifications/cleanup-tokens` | Purge expired API tokens | `0 3 * * *` |
| `POST /notifications/check-score-trends` | Facility score drop alerts (Phase 27) | `0 8 * * *` |
---
@@ -628,7 +645,11 @@ phase1_projects_roles → phase6_features → phase7_mobile_api → phase8_notif
→ phase21_template_active
→ phase21_performance_indexes
→ phase22_comment_visibility
→ phase23_support_tickets ← HEAD
→ phase23_support_tickets
→ phase24_notify_defaults
→ phase25_inspection_gps
→ phase26_issue_vendor
→ phase27_score_alerts ← HEAD
```
### phase21_performance_indexes
@@ -657,6 +678,43 @@ pip install groq # if not already installed
sudo systemctl restart gunicorn
```
### phase24_notify_defaults
Data-only migration. Sets `enabled=True` for `('issue_created', 'admin')` and `('issue_created', 'director')` rows in `notification_matrix` if they already exist. Rows that do not yet exist are seeded at runtime by `MATRIX_DEFAULTS`. No schema change — no existence check needed, `UPDATE` on missing rows is a no-op.
### phase25_inspection_gps
Adds `submit_latitude DECIMAL(10,7) NULL` and `submit_longitude DECIMAL(10,7) NULL` to `inspections`. Populated at submit time — by browser Geolocation API (web) or CoreLocation (iPad). Null for all existing rows. Uses `INFORMATION_SCHEMA` column existence check — safe to re-run.
`inspections/view.html` shows a Google Maps embed (admin/director only) when both columns are non-null.
**iPad behaviour:** `InspectionLocationManager` begins acquiring a fix when the submit confirm dialog appears. GPS is captured into `LocalInspection.submitLatitude`/`submitLongitude` and sent in the `POST /api/v1/inspections` body. The `PATCH` endpoint does not accept GPS — creation-time capture only.
### phase26_issue_vendor
Adds three nullable columns to `issues`:
| Column | Type | Purpose |
|---|---|---|
| `vendor_name` | `VARCHAR(100)` | External contractor or vendor name |
| `vendor_contact` | `VARCHAR(200)` | Phone or email for the vendor |
| `vendor_notes` | `TEXT` | Notes about what the vendor is handling |
Displayed in `issues/view.html` and editable via `IssueForm` (`form.html`). Staff-only — not exposed in mobile API. Uses `INFORMATION_SCHEMA` existence check — safe to re-run.
### phase27_score_alerts
Creates `facility_score_alerts` table. Used by `send_score_alerts()` in `sla.py` for 24-hour deduplication of score-drop notifications. Uses table existence check — safe to re-run.
**Deploy order for phases 2427:**
```bash
flask db upgrade
sudo systemctl restart gunicorn
# Add to cron:
# 0 8 * * * curl -s -X POST https://yourdomain.com/notifications/check-score-trends \
# -d "token=YOUR_DIGEST_SECRET"
```
### phase22_comment_visibility
Adds `is_customer_visible BOOLEAN NOT NULL DEFAULT FALSE` to `issue_comments`. Existing comments default to staff-only visibility. Uses `INFORMATION_SCHEMA` column existence check — safe to re-run.
@@ -898,6 +956,8 @@ timeout = 30
-d "token=SECRET"
0 8 * * * curl -s -X POST https://your-domain.com/scheduled-reports/run \
-d "secret=SECRET"
0 8 * * * curl -s -X POST https://your-domain.com/notifications/check-score-trends \
-d "token=SECRET"
```
---
+9
View File
@@ -28,6 +28,7 @@ issue_created : admin ✗ director ✗ inspector ✗ pm ✗ cust
issue_updated_customer : admin director inspector pm customer
verification_requested : admin director inspector pm customer
sla_alert : admin director inspector pm customer (assignee + followers implicit)
score_alert : admin director inspector pm customer (facility score drop cron)
"""
import json
@@ -58,6 +59,7 @@ MATRIX_EVENTS = {
'issue_updated_customer': 'Issue updated (customer)',
'verification_requested': 'Verification requested',
'sla_alert': 'SLA at-risk / breached',
'score_alert': 'Facility score trend alert (significant drop)',
}
# Default enabled state: (event_key, role_key) → True/False
@@ -147,6 +149,13 @@ MATRIX_DEFAULTS = {
('sla_alert', 'project_manager'): False,
('sla_alert', 'customer'): False,
('sla_alert', 'custom'): False,
# score_alert — facility rolling-avg score drop detected by cron
('score_alert', 'admin'): True,
('score_alert', 'director'): True,
('score_alert', 'inspector'): False,
('score_alert', 'project_manager'): False,
('score_alert', 'customer'): False,
('score_alert', 'custom'): False,
}