This repository was archived by the owner on May 26, 2026. It is now read-only.
feat(kora): KR-FE-EMAIL-INTENT-LOG-PANEL — intent audit stream in cockpit#180
Merged
rafe-walker merged 1 commit intoMay 24, 2026
Conversation
…kpit Build-list follow-on to PR #176 (KR-INTENT-EMAIL-TO-SEA-TICKET). Operator-facing lens onto the intent.email_to_sea_ticket audit stream so the substrate-level "what did Kora decide to do with each inbound email" is visible at a glance: * created → became a Sea_Ticket (deep-linkable) * logged_only → future promotion-loop training-data * dry_run → preview-mode evaluation, no write * cap_exceeded → hourly throttle hit * failed → triage surface (Sea_Ticket write errored) K-DG drift caught: spec deviation from FE-only ============================================= Bucket title says "FE-only PR — no backend changes (uses existing /api/audit-events?seam=intent.email_to_sea_ticket from PR #163 era)." K-DG showed that endpoint DOES NOT EXIST — the established discipline (PR #155 KR-AUDIT-PANEL-ENDPOINTS) is one endpoint per audit seam (/api/webhooks/events/recent, /api/agent-activity/recent, /api/reasoning/recent, etc.), NOT a generic /api/audit-events surface. Spec §4 STOP-ASK clause explicitly anticipated this case + licensed a "small BE addition might be needed." Added the minimal per-seam endpoint (/api/email-intent/recent) following the established pattern rather than escalating + creating a coord cycle. Backend (kora_cli/web_server.py — ~150 lines) ============================================= * GET /api/email-intent/recent Reads kora_audit_log.jsonl filtered to seam=intent.email_to_sea_ticket, projects each row to the EmailIntentEvent FE shape (per-branch field whitelist — SECURITY tested against arbitrary-field leak), returns newest-first with: - events: projected entry list - total_recent_24h + by_action_24h: aggregated counts per action over the 24h window - daily_created_14d: ordered [{date, count}] points so the sparkline renders chronologically without re-sorting client-side - action_values: echoed canonical action list (the FE constant + this echo + the emit_audit call sites form the 3-way drift guard) * _project_email_intent_audit() projection whitelists per- action fields: - created → ticket_id, tags (truncated) - logged_only → reason - dry_run → proposed_title - cap_exceeded → hourly_cap - failed → error (truncated to 200 chars defensively against runaway repr leaking stack-traceish content) * _EMAIL_INTENT_ACTION_VALUES — the BE projection allow-list (drift-guard pinned) * Unknown action values coerced to "unknown" (defensive against future writers adding new actions without updating the projection — test_unknown_action_coerced_defensively) Frontend ======== * web/src/pages/EmailIntentLogPage.tsx (new): - usePanelView("EmailIntentLogPage") - Summary chips: 24h counts per action - Sparkline: plain SVG <rect> bars (no chart-library dep per CC#2 discipline; same approach as CostTelemetryPage from PR #164). Empty days render as 1px ghost bars so operator sees day-grid position. - Filter chips: All / 5 actions; iterate EMAIL_INTENT_ACTION_VALUES (the canonical drift-guarded list) so adding a new action auto-adds a chip. - Per-row card: relative ts + subject + pattern chip + confidence + action badge + deep-link icon when ticket_id present (forward-compat /sea-tickets?focus=<ticket_id>) - Empty state: calm "no events / no filter matches" message with one-click "All" link, NOT a bare empty card list (spec §2c regression-guarded by test) * api.ts: api.getEmailIntentEventsRecent + EmailIntentEvent / EmailIntentEventsResponse / EmailIntentAction / EmailIntentDailyCount TS types + EMAIL_INTENT_ACTION_VALUES canonical constant (3-way drift guard's FE leg) * App.tsx: /email-intent-log route + sidebar nav entry placed right after /email (operator-flow: "Email inbox lens → Email Intent Log decisions") Drift-guard pin (marquee) ========================= test_action_values_drift_guard pins the 5 canonical action values across THREE sources: 1. BE projection allow-list: _EMAIL_INTENT_ACTION_VALUES in kora_cli/web_server.py 2. BE emitter: "action" string literals at the 5 _safe_audit call sites in kora_cli/intent/email_to_sea_ticket.py (regex-greps the source for `"action": "<value>"`) 3. FE constant: EMAIL_INTENT_ACTION_VALUES in web/src/lib/api.ts Drift in any one breaks the panel. Test fails CI on any mismatch. Tests (tests/kora_cli/test_email_intent_panel.py — 24 tests) ============================================================ Backend (13): * endpoint registered, empty zero-response, by_action_24h shape-stable across all known actions, per-action projection (created/logged_only/dry_run/cap_exceeded/ failed), unknown coerced to "unknown" defensively, daily sparkline bucket math (14 buckets ending today, chronological order, totals correct), 24h window cutoff, SECURITY (response doesn't echo arbitrary fields), subject + error truncated to bounded length Drift guard (2): * 3-way action-values drift guard (marquee) * SeamName Literal includes intent.email_to_sea_ticket FE source-pins (9): * api wrapper exists with correct URL * TS types declared (Event / Response / Action / DailyCount) * EMAIL_INTENT_ACTION_VALUES exported as a const * Page exists + usePanelView wired * Route + nav entry registered * Filter chips iterate the canonical action list * Deep-link uses /sea-tickets?focus=<id> * Sparkline uses plain SVG (no recharts/chart.js/d3/etc) * Empty state copy committed (regression guard) 24/24 pass. tsc -b clean. vite build clean. Screenshots =========== web/docs/email-intent-log-panel/populated.png — 4 events (2 created + STK-42/41 deep-links, 1 logged_only with reason, 1 failed with truncated error), summary chips showing 8/3/1 split, sparkline with 14 day-bars web/docs/email-intent-log-panel/empty_filtered.png — operator filtered to "Failed" but zero failures; calm green-toned empty state with one-click "All" link Sea_Ticket deep-link (forward-compat note) ========================================== Deep-link is /sea-tickets?focus=<ticket_id>. SeaTicketsPage doesn't currently consume the `focus` param — clicking lands operator on the Sea_Tickets panel where they can scan their assigned tickets (still useful). Follow-on bucket can teach SeaTicketsPage to scroll/highlight the focused ticket without requiring a re-write of this panel. Follow-on recommendation ======================== Once enough logged_only entries accumulate (the "promotion-loop training-data lens" — spec note), a Kora-side analyzer bucket would surface the un-acted-on emails for operator review (KR-EMAIL-LOGGED-ONLY-ANALYZER or similar). The audit shape this panel consumes is forward-compatible. Refs ==== * rafe-walker/kora-docs 17_cc_bucket_prompts/KR-FE-EMAIL-INTENT-LOG-PANEL_audit_stream_for_intent_seam.md * PR #155 — KR-AUDIT-PANEL-ENDPOINTS (per-seam endpoint pattern) * PR #163 — audit-emission infrastructure * PR #164 — CostTelemetryPage (plain-SVG charts pattern) * PR #176 — KR-INTENT-EMAIL-TO-SEA-TICKET (audit source) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
rafe-walker
added a commit
that referenced
this pull request
May 24, 2026
…eam in cockpit (#183) Symmetric to PR #180. Surfaces tool.email_to_operator_sent audit seam from #179. Per-seam endpoint /api/outbound-email/recent + page OutboundEmailLogPage.tsx. 3-source drift-guard pin for status enum. K-DG drift caught + privacy reality adopted: spec said 'subject CAN be shown' but #179 was MORE privacy-conscious than spec claimed — audit carries only subject_chars / body_chars (lengths), NOT subject string / body text / recipient. Stricter reality adopted. Pinned by test_privacy_response_never_contains_subject_or_body_text + test_page_does_not_render_subject_or_body_text. Shared-utility extraction flagged: 5 overlapping helpers/components with PR #180 (Sparkline / SummaryChips / FilterChips / formatters / BadgeTone type). Recommend follow-on KR-FE-AUDIT-PANEL-KIT-REFACTOR (3-consumer threshold — covered by the next bucket). 23/23 new tests + 47/47 combined with #180 + tsc + vite build clean.
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.
8 tasks
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
Build-list follow-on to PR #176 (KR-INTENT-EMAIL-TO-SEA-TICKET). Operator-facing lens onto the
intent.email_to_sea_ticketaudit stream — substrate-level "what did Kora decide to do with each inbound email" visible at a glance, with action chip filtering, Sea_Ticket deep-link from created rows, and a 14-day sparkline of daily-created counts.Screenshots
Populated state — 4 events showing all variants: 2 created with deep-links (STK-42 / STK-41), 1 logged-only with reason, 1 failed with truncated error. Sparkline at top shows 14-day daily-created counts.
Empty filtered state — operator filtered to "Failed" but zero failures in the window. Calm reassurance copy + one-click "All" link, NOT a bare empty card list (regression-guarded by
test_empty_state_copy_committed).Drift-guard pin confirmation
test_action_values_drift_guardpins the 5 canonical action values across three sources:_EMAIL_INTENT_ACTION_VALUESinkora_cli/web_server.py\"action\": \"<value>\"literals at the 5_safe_auditcall sites inkora_cli/intent/email_to_sea_ticket.pyEMAIL_INTENT_ACTION_VALUESinweb/src/lib/api.tsTest regex-greps each source and asserts equality. Drift in any one fails CI. The 5 canonical values:
created/logged_only/dry_run/cap_exceeded/failed.⚠ Spec deviation — added small BE endpoint
Bucket title says "FE-only PR — no backend changes (uses existing
/api/audit-events?seam=intent.email_to_sea_ticketfrom PR #163 era)." K-DG showed that endpoint does not exist — the established discipline (PR #155 KR-AUDIT-PANEL-ENDPOINTS) is one endpoint per seam (/api/webhooks/events/recent,/api/agent-activity/recent,/api/reasoning/recent, etc.), NOT a generic/api/audit-eventssurface.Spec §4 STOP-ASK clause explicitly anticipated this case + licensed a "small BE addition might be needed." Added the minimal per-seam endpoint
/api/email-intent/recent(~150 lines) following the established pattern, rather than escalating + creating a coord cycle on what's a routine substrate-shape adaptation.What's in here
Backend (
kora_cli/web_server.py):GET /api/email-intent/recent— readskora_audit_log.jsonlfiltered toseam=intent.email_to_sea_ticket, returns newest-first projection + 24h-window per-action counts + 14-day daily-created sparkline points_project_email_intent_audit()— per-action field whitelist (SECURITY: tested against arbitrary-field leak fromdetails)_EMAIL_INTENT_ACTION_VALUES— BE projection allow-list (drift-guarded)\"unknown\"defensivelyFrontend:
web/src/pages/EmailIntentLogPage.tsx(new) — summary chips, sparkline (plain SVG, no chart-library dep), filter chips iterating canonical action list, per-row cards, calm empty stateapi.getEmailIntentEventsRecent+ 4 new TS types +EMAIL_INTENT_ACTION_VALUEScanonical constant/email-intent-logroute + sidebar nav entry placed right after/email(operator-flow: Email inbox lens → Email Intent Log decisions)Sparkline: 14 daily bars (plain SVG
<rect>s); empty days render as 1px ghost bars so operator sees day-grid position. Same discipline asCostTelemetryPage's in-house charts (PR #164). Pinned against chart-library imports (recharts,chart.js,d3,@nivo,victory) bytest_sparkline_uses_plain_svg.Sea_Ticket deep-link:
/sea-tickets?focus=<ticket_id>. SeaTicketsPage doesn't yet consumefocus— operator lands on the Sea_Tickets panel where they can scan their assigned tickets. Forward-compatible param; follow-on bucket can teach SeaTicketsPage to scroll/highlight the focused ticket without touching this panel.SECURITY notes
details(e.g. operator PII), they don't leak. Pinned bytest_response_does_not_leak_arbitrary_audit_fieldswhich injects hostile fields + asserts they don't appear in the response.subjecttruncated to 200 chars;error(repr of exception) truncated to 200 chars defensively against runaway repr leaking stack-traceish content.\"unknown\"so the FE renders a known shape.Test plan
tests/kora_cli/test_email_intent_panel.py(13 backend + 2 drift guard + 9 FE source-pins)pnpm tsc -bcleanpnpm buildclean/email-intent-logagainst a daemon with at least one PR KR-INTENT-EMAIL-TO-SEA-TICKET — save ideas to Sea_Ticket from inbox #176 audit row → cards render with correct action chips + deep-link to STK-### → switch filter chips → counts narrow correctly → switch to a zero-count action → empty state renders.Follow-on recommendation
Once enough
logged_onlyentries accumulate (the spec-noted "promotion-loop training-data lens"), a Kora-side analyzer bucket would surface the un-acted-on emails for operator review (KR-EMAIL-LOGGED-ONLY-ANALYZER or similar). The audit shape this panel consumes is forward-compatible.Refs
rafe-walker/kora-docs→17_cc_bucket_prompts/KR-FE-EMAIL-INTENT-LOG-PANEL_audit_stream_for_intent_seam.md🤖 Generated with Claude Code