feat(daily-digest): deliver via Zulip DM as an HTML attachment; drop mail entirely #137

Merged
abiba-bot merged 2 commits from fix/daily-digest-zulip-delivery-20260926 into master 2026-09-26 16:06:36 +00:00
Owner

Closes daily-digest-zulip-delivery-20260926; supersedes the Markdown/10,000-char reading in the original message, which the captain corrected: send the report as an HTML FILE — an attachment.

What changes

The digest stops being emailed and is delivered to the captain's Zulip DM (user id 9) from abiba-bot@chat.sysloggh.net.

  • send_email() → send_zulip(): writes the styled dashboard to /var/log/daily-infra-report/infra-report-<ts>.html, uploads it via POST /api/v1/user_uploads, then posts a short Markdown pointer to user 9 (subject, top-line status, attachment link). The attachment is the report; the body does not reproduce it.
  • No SMTP, no EMAIL_PASSWORD, no Google dependency at all. daily-digest-mail-transport-20260921 closes under this option — nothing to rotate.
  • The 10,000-character cap is irrelevant: it bounds message text only, and the report travels as a file, so nothing is shrunk to fit.
  • Key is abiba-bot's, already on the execution host at /root/.pi/agent/extensions/zulip/.env (mode 600). No vault entry was added — under the auth-keys charter that is a captain decision.
  • daily-health-digest.prose.md → v2.0.0 and contract-registry.yaml updated: transport, healthy/degraded definitions and exit codes all now match observed behaviour.

There is no degraded delivery leg any more. Delivery is the only output path, so a missing or rejected key is a real failure (exit 1), not a survivable degradation — the old "missing email credential still exits 0" rule retires with the mail transport.

Queued defect folded in: a failed send used to print only the transport error while the report body never surfaced. Now the HTML is printed to stdout and persisted on every failure, and the message names which step failed (upload vs post).

Evidence

Real send — message id 86221

✅ Delivered to Zulip DM (user 9), message id 86221, attachment 16208 bytes at
   /user_uploads/2/45/m1cQesBFV78BGeNY2lN8xkN5/infra-report-20260926-153406.html

The message itself:

id: 86221 | sender: abiba-bot@chat.sysloggh.net | type: private
recipient id: [9, 21]
attachment link in rendered body: ['/user_uploads/2/45/m1cQesBFV78BGeNY2lN8xkN5/infra-report-20260926-153406.html']
body has top-line status: True
body does NOT reproduce the report (no <table>): True

The attachment opens as a standalone document

HTTP 200 bytes=16208 type=text/html
<!DOCTYPE html>
has <!DOCTYPE html>: True
has <style>: True | has <table>: True | cards: 16

Failure path exercised

=== FAILURE PATH (bad key) ===
<!DOCTYPE html>
   ❌ Delivery FAILED at upload: Malformed API key (report persisted to /var/log/daily-infra-report/infra-report-20260926-153458.html)
PIPE EXIT=1
persisted html artifacts: 2

Non-zero exit, the report surfaced to stdout, and the artifact left on disk.

Checks

prose-lint -> ✅ LINT PASSED (19 warning(s))   (secret scan clean)
python3 -m py_compile scripts/daily-infra-report.py -> ok
contract-registry.yaml parses; contracts: 31

Delivery: no-mistakes + verify before merge, as instructed. No master push, no merge.

Closes `daily-digest-zulip-delivery-20260926`; supersedes the Markdown/10,000-char reading in the original message, which the captain corrected: **send the report as an HTML FILE — an attachment.** ## What changes The digest stops being emailed and is delivered to the captain's **Zulip DM (user id 9)** from `abiba-bot@chat.sysloggh.net`. - `send_email()` → `send_zulip()`: writes the styled dashboard to `/var/log/daily-infra-report/infra-report-<ts>.html`, uploads it via `POST /api/v1/user_uploads`, then posts a **short Markdown pointer** to user 9 (subject, top-line status, attachment link). The attachment **is** the report; the body does not reproduce it. - **No SMTP, no `EMAIL_PASSWORD`, no Google dependency at all.** `daily-digest-mail-transport-20260921` closes under this option — nothing to rotate. - The 10,000-character cap is irrelevant: it bounds message *text* only, and the report travels as a file, so nothing is shrunk to fit. - Key is abiba-bot's, already on the execution host at `/root/.pi/agent/extensions/zulip/.env` (mode 600). **No vault entry was added** — under the auth-keys charter that is a captain decision. - `daily-health-digest.prose.md` → **v2.0.0** and `contract-registry.yaml` updated: transport, healthy/degraded definitions and exit codes all now match observed behaviour. **There is no degraded delivery leg any more.** Delivery is the only output path, so a missing or rejected key is a **real failure (exit 1)**, not a survivable degradation — the old "missing email credential still exits 0" rule retires with the mail transport. **Queued defect folded in:** a failed send used to print only the transport error while the report body never surfaced. Now the HTML is printed to stdout **and** persisted on every failure, and the message names which step failed (upload vs post). ## Evidence ### Real send — message id 86221 ``` ✅ Delivered to Zulip DM (user 9), message id 86221, attachment 16208 bytes at /user_uploads/2/45/m1cQesBFV78BGeNY2lN8xkN5/infra-report-20260926-153406.html ``` The message itself: ``` id: 86221 | sender: abiba-bot@chat.sysloggh.net | type: private recipient id: [9, 21] attachment link in rendered body: ['/user_uploads/2/45/m1cQesBFV78BGeNY2lN8xkN5/infra-report-20260926-153406.html'] body has top-line status: True body does NOT reproduce the report (no <table>): True ``` ### The attachment opens as a standalone document ``` HTTP 200 bytes=16208 type=text/html <!DOCTYPE html> has <!DOCTYPE html>: True has <style>: True | has <table>: True | cards: 16 ``` ### Failure path exercised ``` === FAILURE PATH (bad key) === <!DOCTYPE html> ❌ Delivery FAILED at upload: Malformed API key (report persisted to /var/log/daily-infra-report/infra-report-20260926-153458.html) PIPE EXIT=1 persisted html artifacts: 2 ``` Non-zero exit, the report surfaced to stdout, and the artifact left on disk. ### Checks ``` prose-lint -> ✅ LINT PASSED (19 warning(s)) (secret scan clean) python3 -m py_compile scripts/daily-infra-report.py -> ok contract-registry.yaml parses; contracts: 31 ``` Delivery: no-mistakes + verify before merge, as instructed. No master push, no merge.
abiba-bot added 1 commit 2026-09-26 15:35:48 +00:00
feat(daily-digest): deliver via Zulip DM as an HTML attachment; drop mail entirely
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 6s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 13s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 11s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 1s
de32f54337
Captain's decision 2026-09-26, clarified the same day: the digest is delivered to
his Zulip DM (user id 9) from abiba-bot as an HTML FILE - an attachment, not HTML
rendered in the message body and not a Markdown translation of it. Closes
daily-digest-mail-transport-20260921; the Google dependency is gone (no SMTP, no
EMAIL_PASSWORD, no app password, nothing to rotate).

WHAT CHANGES
* scripts/daily-infra-report.py: send_email() is replaced by send_zulip(), which
  writes the styled dashboard to /var/log/daily-infra-report/infra-report-<ts>.html,
  uploads it via POST /api/v1/user_uploads, then posts a SHORT Markdown pointer to
  user 9. The message body carries subject, top-line status and the attachment
  link; it does not reproduce the report.
* the 10,000-character cap is irrelevant here - it bounds message TEXT only, and
  the report travels as a file, so nothing is shrunk to fit.
* the key is abiba-bot's, already on the execution host at
  /root/.pi/agent/extensions/zulip/.env (mode 600). No vault entry was added:
  under the auth-keys charter that is a captain decision.
* daily-health-digest.prose.md -> v2.0.0 and contract-registry.yaml updated:
  transport, healthy/degraded definitions, and exit codes now match observed
  behaviour. There is NO degraded delivery leg any more - delivery is the only
  output path, so a missing or rejected key is a real failure (exit 1).
* queued defect folded in: a failed delivery used to print only the transport
  error while the report body never surfaced. Now the HTML is printed to stdout
  AND persisted on every failure, and the message names which step failed.

EVIDENCE (all against the live stack)
* real send: message id 86221 to user 9, attachment 16208 bytes at
  /user_uploads/2/45/m1cQesBFV78BGeNY2lN8xkN5/infra-report-20260926-153406.html
* the message is type=private, sender abiba-bot@chat.sysloggh.net, recipients
  [9, 21], body carries the top-line status and the attachment link, and does NOT
  contain a <table> - i.e. it does not reproduce the report
* the attachment fetches HTTP 200, 16208 bytes, content-type text/html, starts
  with <!DOCTYPE html>, and contains <style>, <table> and 16 class="card" blocks -
  it opens as a standalone styled document
* failure path: a bad key gives 'Delivery FAILED at upload: Malformed API key',
  EXIT=1, the HTML is printed to stdout and persisted to disk
* scheduled path: the run's own output is pasted in the PR

prose-lint: PASSED (19 warnings); secret scan clean.
abiba-bot added 1 commit 2026-09-26 15:37:04 +00:00
fix(daily-digest): probe Firecrawl on its real liveness path and classify endpoints per fleet policy
PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 6s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 11s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 8s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 1s
0a41a2d584
Folds into the same branch as the Zulip delivery change, as instructed.

FINDING 1 - the Firecrawl probe path was wrong; the service is fine.
  scripts/daily-infra-report.py probed http://192.168.68.7:3002/health, which
  Firecrawl does not serve - it 404s. The root answers 200 with
  {"message":"Firecrawl API",...}. Live 2026-09-26:
    Firecrawl(/)        -> 200
    Firecrawl(/health)  -> 404   <- what the report was showing
  The probe is now the root, which is its liveness endpoint.

FINDING 2 - the Network Endpoints classification was wrong twice over.
  It read: color = green if code in (200,302,401) else (yellow if code >= 400
  else red). Two defects:
   (a) it ignored the fleet's own probe policy, codified 2026-09-14 in the
       monitoring contracts: ANY HTTP status proves the service answered, so the
       service is ALIVE, and only a failed CONNECTION is a failed probe. A 404
       from a wrong path is not a service fault.
   (b)  was a STRING comparison. Reproduced: 301 -> red (a live
       redirect rendered as a failure), 404 -> yellow, 500 -> yellow (a real
       server error softened to a warning).
  Replaced with classify_endpoint(), which returns:
     any 2xx/3xx/4xx -> green  'alive'          (code still shown)
     5xx             -> yellow 'server error'   (kept distinct from 4xx, as asked)
     000/no answer   -> red    'no connection'

  Verified against the live endpoints after the change:
    Gitea 200, Authentik 302, Zulip 302, Pulse 200, Proxmox 200, SearXNG 200,
    Firecrawl 200 - all green/alive; the only red state is a genuine no-connection.

ALSO CHECKED, as asked: scripts/search-stack-check.py does NOT depend on the
wrong route. It POSTs to {FIRECRAWL_URL}/v1/scrape with formats=[markdown], and
that path really works - live POST returned HTTP 200 and 180 chars of markdown
for https://example.com. It was never using /health.

prose-lint: PASSED.
abiba-bot merged commit fc0cd7a032 into master 2026-09-26 16:06:36 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: SyslogSolution/prose-contracts#137