Skip to content
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 into
feature/phase2-upgradesfrom
feat/kora-KR-FE-EMAIL-INTENT-LOG-PANEL
May 24, 2026
Merged

feat(kora): KR-FE-EMAIL-INTENT-LOG-PANEL — intent audit stream in cockpit#180
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-EMAIL-INTENT-LOG-PANEL

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

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 — 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.

Email Intent Log — populated

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).

Email Intent Log — empty filtered

Drift-guard pin confirmation

test_action_values_drift_guard pins the 5 canonical action values across three sources:

# Source Location
1 BE projection allow-list _EMAIL_INTENT_ACTION_VALUES in kora_cli/web_server.py
2 BE emitter call sites \"action\": \"<value>\" literals at the 5 _safe_audit call sites in kora_cli/intent/email_to_sea_ticket.py
3 FE constant EMAIL_INTENT_ACTION_VALUES in web/src/lib/api.ts

Test 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_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 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 (~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 — reads kora_audit_log.jsonl filtered to seam=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 from details)
  • _EMAIL_INTENT_ACTION_VALUES — BE projection allow-list (drift-guarded)
  • Unknown action values coerced to \"unknown\" defensively

Frontend:

  • 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 state
  • api.getEmailIntentEventsRecent + 4 new TS types + EMAIL_INTENT_ACTION_VALUES canonical constant
  • /email-intent-log route + 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 as CostTelemetryPage's in-house charts (PR #164). Pinned against chart-library imports (recharts, chart.js, d3, @nivo, victory) by test_sparkline_uses_plain_svg.

Sea_Ticket deep-link: /sea-tickets?focus=<ticket_id>. SeaTicketsPage doesn't yet consume focus — 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

  • Projection whitelist: response includes ONLY the per-action fields the panel renders. If a future writer adds new fields to details (e.g. operator PII), they don't leak. Pinned by test_response_does_not_leak_arbitrary_audit_fields which injects hostile fields + asserts they don't appear in the response.
  • Bounded lengths: subject truncated to 200 chars; error (repr of exception) truncated to 200 chars defensively against runaway repr leaking stack-traceish content.
  • Unknown action coercion: defensive against attacker-controlled or unexpected action strings — the projection collapses unknown values to \"unknown\" so the FE renders a known shape.

Test plan

  • 24/24 tests pass in tests/kora_cli/test_email_intent_panel.py (13 backend + 2 drift guard + 9 FE source-pins)
  • pnpm tsc -b clean
  • pnpm build clean
  • Manual smoke: open /email-intent-log against 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_only entries 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

🤖 Generated with Claude Code

…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>
@rafe-walker rafe-walker merged commit 0fc52df into feature/phase2-upgrades May 24, 2026
@rafe-walker rafe-walker deleted the feat/kora-KR-FE-EMAIL-INTENT-LOG-PANEL branch May 24, 2026 04:34
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.
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.
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