diff --git a/README.md b/README.md index e2c0b22..1220530 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,19 @@ volume. Agents send with `scripts/whatsapp-push.py` and resolve contacts with `scripts/whatsapp-contacts.py`. +Sends to a **first-contact recipient** (a number whose device list the bridge +has not cached) can stall in the usync device-list lookup while the link — and +`/health` — stay green. The gateway handles this itself: on a usync failure it +retries against the recipient's **LID** when the store knows one (any contact +who has messaged this account before — that path delivers where the raw-number +lookup stalls), then once more after a backoff (`WHATSAPP_SEND_USYNC_RETRIES`, +default 1; `WHATSAPP_SEND_USYNC_BACKOFF`, default 15 s), without ever re-sending +the parts of a multi-part send that already went out. If every attempt fails, +the pending send terminates with a clear error on `/sends`, and `/health` +reports `recipient_lookup_ok: false` (shown as a warning on `/gateways`; the +evidence decays after `WHATSAPP_RECIPIENT_LOOKUP_TTL`, default 30 min, since it +cannot be re-probed safely). + ### Telegram accounts Telegram is reached through the sibling `telegram-gateway` service, which logs in diff --git a/scripts/web-gateway.py b/scripts/web-gateway.py index 96eb9c1..5e5593f 100644 --- a/scripts/web-gateway.py +++ b/scripts/web-gateway.py @@ -1621,6 +1621,7 @@ def _start_conv_turn(cid: str) -> None: _SEND_SINGLE_RE = re.compile(r"^/sends/([^/]+)/([^/]+?)/?$") _SEND_ACTION_RE = re.compile(r"^/sends/([^/]+)/([^/]+)/(approve|reject)/?$") +_SEND_STATUS_RE = re.compile(r"^/sends/([^/]+)/([^/]+)/status/?$") _GATEWAY_HEALTH_RE = re.compile(r"^/gateways/([A-Za-z0-9._-]+)/health/?$") _GATEWAY_QR_RE = re.compile(r"^/gateways/([A-Za-z0-9._-]+)/qr/?$") @@ -1793,7 +1794,10 @@ def _render_channel_send_html(detail: dict, channel: str, request_id: str, next_ recipient = html.escape(detail.get("recipient") or detail.get("to") or "") cat = html.escape(detail.get("category") or "") msg = html.escape(detail.get("message") or "") - skip = html.escape(next_url) if next_url else "/sends" + # "Skip" jumps to the next pending request — rendered only when one exists + # (the nav already links back to /sends and the dashboard). + skip_btn = (f' Skip\n' + if next_url else "") meta_rows = [ f"Channel{label_e}", f"To{recipient}", @@ -1801,37 +1805,97 @@ def _render_channel_send_html(detail: dict, channel: str, request_id: str, next_ ] status = detail.get("status") or "pending" if status != "pending": + # Status page: a "sending" entry shows a spinner and polls the JSON + # status endpoint client-side — no full-page refresh flicker. Success + # flips the spinner to a green check and auto-advances a moment later: + # to the next pending request when one exists, else the page tries to + # close itself (falling back to /sends — window.close() only works for + # script-opened windows). The next-request button is rendered only when + # a next request actually exists; a failure shows the gateway's real + # error and stays put so the user can read it. if status == "sending": - headline = "Sending…" - note = ("The gateway accepted the send and is delivering it in the " - "background. This page refreshes until it completes.") - extra_head = '\n' + icon = '
' + note = "Delivering in the background…" elif status == "approved": - headline = "Sent ✓" - note = "The message was delivered to the channel." - extra_head = "" + icon = '
' + note = "Sent." elif status == "rejected": - headline = "Rejected" + icon = '
' note = "The message was discarded without sending." - extra_head = "" else: # "error" - headline = "Send failed" + icon = '
' note = ("The gateway could not deliver the message: " + (detail.get("error") or "unknown error")) - extra_head = "" return ( _HTML_HEAD + f"Retinue — {label_e} Send {rid}\n" - + extra_head + # No-JS fallback only: with scripting available the page polls + # instead of reloading. + + ('\n' + if status == "sending" else "") + + "\n" + "\n" - + f"

{label_e} send: {html.escape(headline)}

\n" + + f"

{label_e} send

\n" + f'\n' + '\n' + "\n".join(meta_rows) + "\n
\n" + f'
{msg}
\n' - + f'

{html.escape(note)}

\n' + + f'
{icon}
' + + f'

{html.escape(note)}

\n' + '
\n' - + ' Next pending send\n' + + f' Next pending send\n' + "
\n" + + "\n" + "\n\n" ) return ( @@ -1847,7 +1911,7 @@ def _render_channel_send_html(detail: dict, channel: str, request_id: str, next_ f'\n' + f'
' f'
\n' - + f' Skip\n' + + skip_btn + "\n" + "