This repository was archived by the owner on May 26, 2026. It is now read-only.
feat(kora): KR-FE-OUTBOUND-EMAIL-LOG-PANEL — outbound email audit stream in cockpit#183
Merged
rafe-walker merged 1 commit intoMay 24, 2026
Conversation
…eam in cockpit Symmetric follow-on to PR #180 (email-intent log panel). Surfaces the tool.email_to_operator_sent audit seam (PR #179 — the kora__send_email_to_operator reasoning-loop tool). Completes the cockpit's email-surface story: inbound (PR #180) + outbound (this PR) both visible. K-DG drift caught: spec assumed subject string in audit ======================================================== Spec said: "subject CAN be shown (it IS in audit details per #179 sample entry)." K-DG of kora_cli/tools/email_to_operator.py lines 365-369 showed that PR #179 was MORE privacy-conscious than the spec claimed — the audit row carries only: * subject_chars (length, not the string) * body_chars (length, not the content) * attachment_count + attachment_total_bytes * status + per-status diagnostic fields No subject string. No body text. No recipient. PR #179's implementation is the stricter/correct posture; the spec description was inaccurate. Adopted the privacy reality — panel shows sizes + status + smtp_message_id (sent) or rejection_reason + truncated rejection_detail (rejected) or error type (smtp_failure). This is the safest choice and pinned by test_privacy_response_never_contains_subject_or_body_text + test_page_does_not_render_subject_or_body_text against hostile-field injection (future writer adding subject/body to details must NOT leak through projection or page render). Backend (kora_cli/web_server.py) ================================ GET /api/outbound-email/recent?limit=N Symmetric pattern to /api/email-intent/recent (PR #180): * read_audit_entries(seam="tool.email_to_operator_sent") * per-status field whitelist projection * 24h-window by-status counts (sent/rejected/smtp_failure) * daily-sent sparkline points over 14 days * status_values echoed for the 3-source drift guard Projection whitelist (_project_outbound_email_audit): All branches: subject_chars, body_chars, attachment_count, attachment_total_bytes, status, caller_session_id status=sent: smtp_message_id (≤200), sent_at status=rejected: rejection_reason (≤120), rejection_detail (JSON-serialized ≤200) status=smtp_failure: error (≤200), smtp_status Unknown status values coerced to "unknown" defensively. Frontend ======== * web/src/pages/OutboundEmailLogPage.tsx (new): - usePanelView("OutboundEmailLogPage") - SummaryChips: 24h counts per status - Sparkline: plain SVG bars (same discipline as PR #180 / CostTelemetryPage — no chart library) - FilterChips iterating OUTBOUND_EMAIL_STATUS_VALUES - Per-row card: relative ts + privacy-preserved size row (subject_chars / body_chars / attachment info) + status badge + per-status detail line (smtp_message_id or rejection_reason+detail or error) - Calm empty state (regression-guarded) * api.ts: api.getOutboundEmailRecent + OutboundEmailEvent / OutboundEmailEventsResponse / OutboundEmailStatus / OutboundEmailDailyCount types + OUTBOUND_EMAIL_STATUS_VALUES canonical constant * App.tsx: /outbound-email-log route + sidebar nav entry placed right after /email-intent-log (inbound/outbound sidebar pair). Send icon completes the Inbox/Send pair. Drift-guard pin (marquee — symmetric to PR #180's discipline) ============================================================= test_status_values_drift_guard pins the 3 canonical status values (sent / rejected / smtp_failure) across THREE sources: 1. BE projection allow-list: _OUTBOUND_EMAIL_STATUS_VALUES in kora_cli/web_server.py 2. BE emitter STATUS_* constants in kora_cli/tools/email_to_operator.py (STATUS_SENT, STATUS_REJECTED, STATUS_SMTP_FAILURE — regex-asserted each has the matching string literal) 3. FE constant: OUTBOUND_EMAIL_STATUS_VALUES in web/src/lib/api.ts Drift in any one fails CI. Tests (23 in test_outbound_email_panel.py — all pass) ===================================================== Backend (10): * endpoint registered, empty zero-response, by_status_24h shape-stable across all 3 known statuses, per-status projection (sent/rejected/smtp_failure), unknown coerced, daily sparkline math (14 buckets ending today, chronological, correct totals), 24h cutoff * PRIVACY: response never contains subject string / body content even when hostile fields injected * SECURITY: arbitrary details fields don't leak * rejection_detail + error truncated to 200 chars Drift guard (2): * 3-way status_values drift guard (marquee) * SeamName Literal includes tool.email_to_operator_sent FE source-pins (11): * api wrapper exists with correct URL * 4 TS types declared, status constant exported * Page exists + usePanelView * Route + nav entry * Filter chips iterate canonical list * Sparkline plain SVG (no chart-library imports) * PRIVACY: page does NOT reference event.subject / event.body / event.body_text / event.to / event.from / event.recipient (only subject_chars + body_chars + attachment_* allowed — regression guard against future patches sneaking in text-content render) * Empty state copy committed Combined with PR #180's existing tests: 47/47 pass (24 from #180 + 23 new). tsc -b clean. vite build clean. Screenshots =========== web/docs/outbound-email-log-panel/populated.png — 5 events: 3 sent (with SMTP-ids + sizes + 1 with attachment), 1 rejected (hourly_cap_exceeded with truncated detail), 1 smtp_failure (ConnectionRefusedError). Summary chips + sparkline above the cards. web/docs/outbound-email-log-panel/empty_filtered.png — operator filtered to "SMTP-failure" with zero failures; calm green-toned empty state with one-click "All" link. Shared-utility extraction recommendation (spec §4 follow-on) ============================================================ PR #180 (EmailIntentLogPage) + this PR (OutboundEmailLogPage) have substantial overlap in five small components/helpers: 1. Sparkline (plain SVG, daily counts) — IDENTICAL shape; only the data type's field name differs (DailyCount). Could parameterize over <T extends {date: string; count: number}>. 2. SummaryChips (24h counts by enum-keyed dict) — visual shape identical; only the values + visual map differ. 3. FilterChips (All + per-enum-value buttons with counts) — same shape; only the canonical-values list + visual map differ. 4. formatTimestamp / formatRelative / truncate helpers — verbatim copies. 5. BadgeTone TS type alias — verbatim copy. Per spec §4 license to "copy first, refactor later" — kept copies in both files this round. Recommend extracting into web/src/components/AuditPanelKit/ (or similar) in a follow-on KR-FE-AUDIT-PANEL-KIT-REFACTOR bucket once a 3rd consumer appears (3 = the threshold where extraction is clearly worth the abstraction cost vs duplication). Refs ==== * rafe-walker/kora-docs 17_cc_bucket_prompts/KR-FE-OUTBOUND-EMAIL-LOG-PANEL_audit_stream_for_email_sent_seam.md * PR #155 — KR-AUDIT-PANEL-ENDPOINTS (per-seam endpoint pattern) * PR #164 — CostTelemetryPage (plain-SVG charts discipline) * PR #179 — tool.email_to_operator_sent (audit source) * PR #180 — KR-FE-EMAIL-INTENT-LOG-PANEL (symmetric inbound panel) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
rafe-walker
added a commit
that referenced
this pull request
May 24, 2026
…utofix-log + kora-actions-aggregate (#187) 4 deliverables: (A) Extracted AuditPanelKit (Sparkline/SummaryChips/FilterChips/formatters/BadgeTone/EmptyFilteredMessage); (B) #180+#183 retrofit (zero visual diff); (C) KR-FE-AUTOFIX-LOG-PANEL (3rd consumer; surfaces tool.probe_autofix_attempted); (D) KR-FE-KORA-ACTIONS-AGGREGATED-PANEL (apex timeline joining all 4+ mutating-action seams; action_category chip filter). Drift-guards: 4 pin tests (status / categories / seam_literal_includes / badge_tone_matches_library). Existing #180+#183 drift guards still pass. 98/98 panel-suite tests + tsc + vite build clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Symmetric follow-on to PR #180 (email-intent log panel). Surfaces the
tool.email_to_operator_sentaudit seam (PR #179 — thekora__send_email_to_operatorreasoning-loop tool). Completes the cockpit's email-surface story: inbound (PR #180) + outbound (this PR) both visible side-by-side in the sidebar.Screenshots
Populated state — 5 events: 3 sent (SMTP-ids + sizes + 1 with attachment), 1 rejected (
hourly_cap_exceededwith truncated detail), 1 SMTP-failure (ConnectionRefusedError). Summary chips + 14-day sparkline above.Actually use this corrected URL:
Empty filtered state — operator filtered to "SMTP-failure" but zero failures in the window. Calm reassurance copy + one-click "All" link.
Drift-guard pin confirmation
test_status_values_drift_guardpins the 3 canonical status values across three sources (mirrors PR #180's discipline):_OUTBOUND_EMAIL_STATUS_VALUESinkora_cli/web_server.pySTATUS_SENT/STATUS_REJECTED/STATUS_SMTP_FAILUREinkora_cli/tools/email_to_operator.py(regex-asserts each constant assigns the matching string literal)OUTBOUND_EMAIL_STATUS_VALUESinweb/src/lib/api.tsThe 3 canonical values:
sent/rejected/smtp_failure. Drift in any one fails CI.⚠ K-DG drift caught: spec assumed subject string in audit
Spec said: "subject CAN be shown (it IS in audit details per #179 sample entry)." K-DG of
kora_cli/tools/email_to_operator.py:365-369showed that PR #179 was MORE privacy-conscious than the spec claimed — the audit row carries onlysubject_chars(length),body_chars(length),attachment_count,attachment_total_bytes,status, and per-status diagnostic fields. No subject string. No body text. No recipient.PR #179's implementation is the stricter/correct posture; the spec description was inaccurate. Adopted the privacy reality — panel renders sizes + status + smtp_message_id (sent) or rejection_reason+truncated detail (rejected) or error type (smtp_failure). Pinned by:
test_privacy_response_never_contains_subject_or_body_text— hostile-field injection (future writer adding subject/body/recipient to details must NOT leak through projection)test_page_does_not_render_subject_or_body_text— regex-asserts the page never referencesevent.subject/event.body/event.body_text/event.to/event.from/event.recipient(only_charssuffixes allowed). Regression guard against future patches sneaking in text-content render.What's in here
Backend (
kora_cli/web_server.py):GET /api/outbound-email/recent— symmetric to/api/email-intent/recent. Returns newest-first projected events + 24h-window by-status counts + 14-day daily-sent sparkline points._project_outbound_email_audit()— per-status field whitelist. Unknown statuses coerced to\"unknown\"defensively._OUTBOUND_EMAIL_STATUS_VALUES— BE projection allow-list (drift-guarded).Frontend:
web/src/pages/OutboundEmailLogPage.tsx(new) —usePanelView, SummaryChips, plain-SVG Sparkline, FilterChips, per-row card with privacy-preserved size row, per-status detail line, calm empty stateapi.getOutboundEmailRecent+ 4 new TS types +OUTBOUND_EMAIL_STATUS_VALUEScanonical constant/outbound-email-logroute + sidebar nav entry placed right after/email-intent-log— Inbox/Send icon pair completes the inbound/outbound sidebar adjacencyShared-utility extraction recommendation
PR #180 (EmailIntentLogPage) + this PR (OutboundEmailLogPage) have substantial overlap in five small components/helpers:
Sparkline(plain SVG, daily counts)SummaryChips(24h enum-keyed counts)FilterChips(All + per-enum-value buttons)formatTimestamp/formatRelative/truncatehelpersBadgeToneTS type aliasPer spec §4 license to "copy first, refactor later" — kept copies in both files this round to keep the PR focused. Recommended follow-on bucket:
KR-FE-AUDIT-PANEL-KIT-REFACTOR— extract intoweb/src/components/AuditPanelKit/once a 3rd consumer appears. (3 = the established threshold where extraction's clearly worth the abstraction cost vs duplication.)SECURITY + privacy
rejection_detailJSON-serialize-then-substring to 200;errortruncated to 200;smtp_message_idtruncated to 200.detailsdon't leak.Test plan
tests/kora_cli/test_outbound_email_panel.py(10 BE + 2 drift guard + 11 FE source-pins, including the 2 PRIVACY pins)pnpm tsc -bcleanpnpm buildclean/outbound-email-logagainst a daemon with at least one PR KR-EMAIL-OUTBOUND-COMPOSE-TOOL — Kora can email operator (R3 Q8a outbound) #179 audit row → cards render with privacy-preserved sizes + status chips + SMTP-id or rejection_reason as appropriate → switch filter chips → counts narrow correctly → switch to a zero-count status → empty state renders.Refs
rafe-walker/kora-docs→17_cc_bucket_prompts/KR-FE-OUTBOUND-EMAIL-LOG-PANEL_audit_stream_for_email_sent_seam.mdtool.email_to_operator_sentaudit source🤖 Generated with Claude Code