← All manuals Operator Guide for this screen IM2

Rush Alert Contacts

Rush Alert Contacts
Technical Spec Ver 1 Updated 2026-09-06 Matches the live screen

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

MethodPathAuthNotes
GET/settings/rush-contactsrole == '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/addadminname (required, trimmed), phone. Inserts active=true, returns {contact_id, phone}.
POST/api/rush-contacts/{contact_id:int}adminname, 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():

  1. Builds "RUSH RO {order_no} - {job_name} - {n} line(s)[ - vendor: X] - from {tech}".
  2. If setting_bool("rush_notify_slack")notify_slack(text + " - " + IM2_PUBLIC_URL + "/po/workbench?priority=R"), posting to RUSH_SLACK_WEBHOOK_URL. Does not use this table.
  3. If setting_bool("rush_notify_sms")_active_contact_phones() (select phone from rush_alert_contact where active order by name) → notify_sms() → ClickSend POST https://rest.clicksend.com/v3/sms/send, basic auth CLICKSEND_USERNAME/CLICKSEND_API_KEY, from CLICKSEND_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

  1. rush_notify_sms ships No and the ClickSend credentials are not set in this package, so this list is currently inert. Slack is the only live channel.
  2. 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.
  3. No duplicate protection — the same phone can be added twice under two names and gets two texts.
  4. notify_release() (text the requesting tech when his Rush RO is ordered) is complete but wired to nothing: po.py never creates a real QB PO and never stamps po_buy_list.released_at, so there is no release event to hook. The rush_notify_tech_sms setting therefore does nothing today.
  5. No CSRF token on either POST (cookie auth + samesite=lax).
  6. Phone numbers are stored and displayed in clear to any admin, and appear in audit_log values. Fine internally; worth knowing before this box is ever exposed more widely.