Rush Alert Contacts
Rush Alert Contacts — Technical Spec
app/im2.py (screen + API) and app/notify.py (delivery). Template app/templates/rush_contacts.html. Rush RO build, brief section 4: "a system-control table, not code and not a settings text box."
Routes
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /settings/rush-contacts | role == 'admin' (403 "Admins only") | select contact_id, name, phone, active, updated_by, updated_at from rush_alert_contact order by active desc, name. |
| POST | /api/rush-contacts/add | admin | name (required, trimmed), phone. Inserts active=true, returns {contact_id, phone}. |
| POST | /api/rush-contacts/{contact_id:int} | admin | name, phone, active — all optional. 404 if the row is gone. |
Partial-save rules on the edit endpoint: a blank name or phone leaves that column alone; active is only applied when the field is actually present and non-blank (1/true/yes/on = true), so a name-only save can never silently deactivate a contact. No delete endpoint exists — deactivate only, so the audit trail and alert history stay readable.
Table
rush_alert_contact(contact_id serial pk, name, phone, active bool, updated_by, updated_at). DDL ships in rush_ro.sql per notify.py's docstring — that file is not in this package (only scripts/schema.sql and scripts/seed_ro_demo.sql are), so the table must already exist on the target box before this screen works. Same migration adds app_users.phone.
Audit: audit_log with sku = 'RUSH_CONTACT:{contact_id}', source='rush_contacts_admin', field_name created/updated, new_value "{name} {phone} active={bool}".
Phone normalization
_normalize_rush_phone() — strips non-digits; 10 digits → +1XXXXXXXXXX; 11 digits starting 1 → +…; anything else → 400 "phone must be 10 digits (or 11 starting with 1)". Deliberately a duplicate of notify._normalize_phone() rather than an import, so im2.py never has to depend on notify.py to validate a number. (notify's copy returns None instead of raising.)
How the list is consumed
app/rush.py::rush_create commits the Rush RO, then calls notify.notify_rush() inside a try/except — order of operations is load-bearing (brief section 4): notification failure must never roll back a request. notify_rush():
- Builds
"RUSH RO {order_no} - {job_name} - {n} line(s)[ - vendor: X] - from {tech}". - If
setting_bool("rush_notify_slack")→notify_slack(text + " - " + IM2_PUBLIC_URL + "/po/workbench?priority=R"), posting toRUSH_SLACK_WEBHOOK_URL. Does not use this table. - If
setting_bool("rush_notify_sms")→_active_contact_phones()(select phone from rush_alert_contact where active order by name) →notify_sms()→ ClickSendPOST https://rest.clicksend.com/v3/sms/send, basic authCLICKSEND_USERNAME/CLICKSEND_API_KEY, fromCLICKSEND_SENDER_NUMBER.
Every function in notify.py degrades to a logged warning and returns False/None — no exception may cross its boundary. Missing env credentials = a logged warning and a dropped message; the screens behave identically.
Known limits / defects
rush_notify_smsships No and the ClickSend credentials are not set in this package, so this list is currently inert. Slack is the only live channel.- Delivery is fire-and-forget: no send log, no per-contact success/failure record, no retry. ClickSend's response is only inspected for a non-2xx and logged. Nothing in the UI ever shows that an alert failed.
- No duplicate protection — the same phone can be added twice under two names and gets two texts.
notify_release()(text the requesting tech when his Rush RO is ordered) is complete but wired to nothing:po.pynever creates a real QB PO and never stampspo_buy_list.released_at, so there is no release event to hook. Therush_notify_tech_smssetting therefore does nothing today.- No CSRF token on either POST (cookie auth +
samesite=lax). - Phone numbers are stored and displayed in clear to any admin, and appear in
audit_logvalues. Fine internally; worth knowing before this box is ever exposed more widely.