Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(kora): KR-WEBHOOK-EVENTS-PANEL — recent webhook events lens (stub)#109

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-WEBHOOK-EVENTS-PANEL
May 22, 2026
Merged

feat(kora): KR-WEBHOOK-EVENTS-PANEL — recent webhook events lens (stub)#109
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-WEBHOOK-EVENTS-PANEL

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Operator-facing observability for public-port traffic on `/api/webhooks/*` once the daemon deploys to Fly. When Joshua needs to see "what's hitting the public port" — verified events, dead-lettered HMAC failures, rate-limited bursts — without `flyctl logs`.

Stub-then-real per the proven CC#2 pattern. CC#3 will wire per-event chain-event recording (or a substrate `webhook_events` table) in a follow-on, blocked on the substrate-team coord ask for the dead-letter ledger shape. Until then `stub:true` keeps the FE banner visible.

Branch base: `feature/phase2-upgrades` per bucket §0. Bucket spec: `kora_docs/17_cc_bucket_prompts/KR-WEBHOOK-EVENTS-PANEL_stub.md`.

SECURITY contract — 3-layer pattern from KR-MCP-3 #106

`source_ip` values are OCTET-MASKED on the wire (e.g. `"54.203.x.x"` never `"54.203.99.142"`) — operator gets geolocation hint without full PII exposure.

  1. Backend payload — bucket §3 stub uses masked literals (`54.203.x.x`, `203.0.113.x`, `198.51.100.x`) that are RFC5737/example-IP-shaped to be unambiguously not-real
  2. TS interface — `source_ip: string` (the wire contract is enforced by the backend test, not the type)
  3. Backend tests — TWO regex guards:
    • each event's `source_ip` matches the octet-mask shape (must contain literal `"x"` octet)
    • walk-the-whole-payload guard rejects any 4-octet IPv4 appearing anywhere — catches future drift that adds a "real_source_ip" diagnostic field, embeds a full IP in details, etc.

CC#3 will enforce the same masking when real data flips in.

Backend

`GET /api/webhooks/events/recent` — stub returns 4 representative events per bucket §3 verbatim:

  • 2 verified slack events (`message`, `url_verification`)
  • 1 dead-letter email (`hmac_invalid`, `signature_mismatch`)
  • 1 rate-limited slack (`event_type: null` per "request never reached the handler" semantics)

`stub: true` + `total_recent_24h: 4`.

Frontend

`pages/WebhookEventsPanel.tsx`:

  • Stats strip: total + per-status counts + italic "Source IPs octet-masked for PII" reminder
  • Filter pills: All / Verified / Dead Letter / Rate Limited / Handler Error (FE-only scoping)
  • Timeline rows newest-first: status icon + status badge + short endpoint + event_type + Globe-icon masked source_ip + Clock-icon relative timestamp
  • Expandable detail: id + full endpoint + absolute received_at + source_ip with italic (octet-masked for PII) note + pretty-printed details JSON
  • STUB banner with CC#3 follow-on note (mentions substrate-team coord ask blocker)
  • Empty state guides operator: "No webhook events yet. Public webhook plane is on port 9118; verify daemon is running."

`lib/api.ts` — `WebhookEventStatus` type + `WebhookEvent` + `WebhookEventsResponse` + `getRecentWebhookEvents` client.

`App.tsx` — `/webhook-events` route + nav entry (Inbox icon) between `/heartbeat` and `/mcp-clients` (operator scans public-port activity near service-health cluster).

`DashboardPage.tsx` — new Webhook Events card on row 2 (7th card; grid stays `lg:grid-cols-3` for visual symmetry). Card body: `total_24h` headline + per-status pills (only non-zero shown). Headline tone goes destructive when `dead_letter > 5` in 24h per bucket §3(c) operator-attention contract — card visually screams from dashboard glance.

Test plan

  • `tests/kora_cli/test_web_server_webhook_events.py` — 10/10 green, covers all 6 §4 scenarios plus security/contract guards:
    • 4-event-pin (bucket §3 canonical stub list)
    • Status-diversity guard (verified/dead_letter/rate_limited all present so FE color rendering is exercised)
    • Per-entry shape + null-event_type-for-rate_limited contract
    • SECURITY: source_ip octet-mask regex (must contain "x" octet)
    • SECURITY: walk-all guard rejects any full IPv4 anywhere in payload
    • Dead-letter event carries reason field
    • Endpoints reference real CC#3 ST3 routes only
    • Cron-regression sanity
  • Full admin-panel suite: 178/178 green across 17 test files (was 168 + 10 new)
  • `npx tsc -b` on `web/` — clean
  • `npx vite build` on `web/` — clean
  • Manual smoke: navigate to `/webhook-events`, verify 4 stub events render with correct status badges; expand the dead-letter row and verify the masked IP shows + "octet-masked for PII" copy; click filter pills and verify FE-only filtering works; verify dashboard `/` shows the new Webhook Events card with stub data and the headline number neutral (4 total, only 1 dead-letter so under the 5-threshold)

Flip-over plan

When CC#3 wires per-event recording (substrate-team coord ask unblocks), the endpoint body swaps to project from real chain events or a webhook_events table. Payload shape pinned, FE auto-renders real data, STUB banner auto-disappears, dashboard card destructive-tone trigger fires when dead_letter actually exceeds 5/24h.

Known follow-on (out of scope)

Per bucket §5: PII redaction in `details` body is out of scope here — stub uses representative fake data. Real implementation will need a redaction pass on the details payload (slack channel IDs, email subjects, etc.) when CC#3 wires real recording. Flagged here so it doesn't get lost.

🤖 Generated with Claude Code

Operator-facing observability for public-port traffic on
/api/webhooks/* once the daemon deploys to Fly. Joshua needs to see
"what's hitting the public port" — verified events, dead-lettered
HMAC failures, rate-limited bursts — without `flyctl logs`.

Stub-then-real per the proven CC#2 pattern (HB-PANEL #103,
KR-MCP-3 #106). CC#3 will wire per-event chain-event recording OR
a substrate webhook_events table in a follow-on — blocked on the
substrate-team coord ask for the dead-letter ledger shape. Until
then the stub:true flag keeps the FE banner visible.

Branch base: feature/phase2-upgrades per bucket §0.

SECURITY: source_ip values are OCTET-MASKED in the response
(e.g. "54.203.x.x" never "54.203.99.142") — operator gets the
geolocation hint without full PII exposure on the panel.
Three-layer contract from KR-MCP-3 (#106 b34b268) applied here too:

  1. Backend payload — bucket §3 stub uses masked literals
     ("54.203.x.x", "203.0.113.x", "198.51.100.x") that are
     RFC5737/example-IP-shaped to be unambiguously not-real
  2. TS interface — source_ip: string (no value-level type info,
     but the wire contract is enforced by the backend test)
  3. Backend tests — two regex guards:
       * each event's source_ip matches the octet-mask shape
         (must contain literal "x" octet)
       * walk-the-whole-payload guard rejects any 4-octet IPv4
         appearing anywhere (catches future drift that adds a
         "real_source_ip" diagnostic field, embeds a full IP in
         details, etc.)
  CC#3 will enforce the same masking when real data flips in.

§2 K-DG verifications:
  * kora_cli/listeners/webhooks.py + webhook_dead_letter.py confirmed
    (CC#3 KR-D-DAEMON ST3 PR #104)
  * No collision with existing /api/webhooks/* listener routes — the
    new GET /api/webhooks/events/recent is a sibling read endpoint;
    POST /slack/events + POST /email/inbound are untouched
  * Dashboard layout currently lg:grid-cols-3 (2-row symmetry) —
    adding a 7th card pushes row 2 to 7 cards; keep cols-3 for
    visual stability (3 rows of 3 with the last row half-full looks
    cleaner than cols-7 single-row narrowness)

Backend (kora_cli/web_server.py):
  * GET /api/webhooks/events/recent — stub returns 4 representative
    events per bucket §3 verbatim:
      - 2 verified slack events (message, url_verification)
      - 1 dead-letter email (hmac_invalid, signature_mismatch)
      - 1 rate-limited slack (event_type:null per "request never
        reached the handler" semantics)
    stub:true + total_recent_24h:4 + generated_at.

Frontend:
  * pages/WebhookEventsPanel.tsx —
    - Stats strip: total_recent_24h + per-status counts + italic
      "Source IPs octet-masked for PII" reminder
    - Filter pills: All / Verified / Dead Letter / Rate Limited /
      Handler Error (FE-only scoping; operator narrows fetched
      data without re-hitting backend)
    - Timeline rows newest-first: status icon + status badge +
      short endpoint (/slack/events vs /api/webhooks/slack/events)
      + event_type + Globe-icon masked source_ip + Clock-icon
      relative timestamp
    - Expandable detail: id + full endpoint + absolute received_at
      + source_ip with italic "(octet-masked for PII)" note +
      pretty-printed details JSON
    - STUB banner with CC#3 follow-on note (mentions substrate-team
      coord ask blocker)
    - Empty state guides operator: "No webhook events yet. Public
      webhook plane is on port 9118; verify daemon is running."
  * lib/api.ts — WebhookEventStatus type alias + WebhookEvent +
    WebhookEventsResponse interfaces + getRecentWebhookEvents client
  * App.tsx — /webhook-events route + nav entry (Inbox icon)
    between /heartbeat and /mcp-clients (operator scans public-port
    activity near service-health cluster)
  * DashboardPage.tsx — new Webhook Events card on row 2 (7th card;
    grid stays lg:grid-cols-3 for visual symmetry). Card body:
    total_24h headline + per-status pills (verified/dead-letter/
    rate-limited/handler-error when non-zero). Headline tone goes
    destructive when dead_letter > 5 in 24h per bucket §3(c)
    operator-attention contract — card visually screams from
    dashboard glance. ALL_SOURCES extended → footer stubbed count
    adds 1.

Tests: tests/kora_cli/test_web_server_webhook_events.py — 10 tests
covering all 6 §4 scenarios plus security/contract guards:
  * 4-event-pin (bucket §3 canonical stub list)
  * Status diversity guard (3 statuses must be present so FE color
    rendering is exercised)
  * Per-entry shape + null-event_type-for-rate_limited contract
  * SECURITY: source_ip octet-mask regex (must contain "x" octet)
  * SECURITY: walk-all guard rejects any full IPv4 anywhere in payload
  * Dead-letter event carries reason field
  * Endpoints reference real CC#3 ST3 routes only
  * Cron-regression sanity

178/178 admin-panel suite green (was 168 + 10 new). tsc -b + vite
build clean.

PII redaction in `details` body is OUT OF SCOPE here (per bucket
§5 — flagged for follow-on when CC#3 wires real data and the
substrate-team coord ask lands).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker merged commit 655403d into feature/phase2-upgrades May 22, 2026
@rafe-walker rafe-walker deleted the feat/kora-KR-WEBHOOK-EVENTS-PANEL branch May 22, 2026 06:10
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant