diff --git a/CLAUDE.md b/CLAUDE.md index 8bffdfe..72831d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 A–E + 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 R1–R4) +> **Last reviewed:** June 2026 (Phase 19 complete + mobile API gap-fill Phases A–E + 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 R1–R4 + 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 24–27:** +```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" ``` --- diff --git a/app/models/notification_matrix.py b/app/models/notification_matrix.py index 1902a88..7d6780d 100644 --- a/app/models/notification_matrix.py +++ b/app/models/notification_matrix.py @@ -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, }