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

feat(kora): KR-FE-OPS-QUALITY-PASS — empty-state + TZ + show-more#155

Open
rafe-walker wants to merge 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-OPS-QUALITY-PASS
Open

feat(kora): KR-FE-OPS-QUALITY-PASS — empty-state + TZ + show-more#155
rafe-walker wants to merge 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-FE-OPS-QUALITY-PASS

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Three banked observations from CC#2's PR #143 ship report, bundled per spec. ~150 LOC prod + ~80 LOC test estimate hit within margin (~230 net additions).

Improvement 1 — Empty-state convergence

AlertsPanel's positive-reinforcement empty state (green CheckCircle2 + "Daemon healthy") extended to panels where empty SEMANTICALLY MEANS healthy idle:

  • WebhookEventsPanel: empty events → "No webhook traffic. Public plane healthy on port 9118." + success-toned card. (Empty filter results keep neutral HelpCircle — not a healthy signal, just a no-match.)
  • AgentActivityPanel: empty calls → "No agent activity. /mcp endpoint healthy on port 9119."
  • ReasoningPanel: empty calls → "No reasoning activity. Kora is idle."

NOT converged per spec §1 (data-hasn't-arrived semantic; converging would mislead):

  • SlackDMPanel — empty pre-deploy points at slack_app_setup_runbook
  • EmailPanel — empty pre-deploy points at purelymail_setup_runbook
  • HeartbeatPanel — cache_warming has its own positive copy

Test pins preserve the slack/email runbook-pointer text so future refactors can't drop the data-arrives-from context.

Improvement 2 — Timestamp TZ rendering

panelHelpers.formatTimestamp now appends "(local)" suffix.

New companion: panelHelpers.timestampAbsoluteUtc(iso) returns the Z-suffixed UTC ISO for hover tooltips. Normalizes to the canonical "2026-05-23T17:48:42Z" shape that operator workflows grep for in logs / substrate.

Wired into the 5 panels that consume formatTimestamp from the shared helper. Pattern:

// Before
<span title={formatTimestamp(...)}>{formatRelative(...)}</span>
// After
<span title={timestampAbsoluteUtc(...)}>{formatRelative(...)}</span>

Plus title= added wherever formatTimestamp appears as visible text. Visible text shows localized + (local) disambiguator; hover reveals UTC for forensic correlation.

Improvement 3 — Show More affordance

New web/src/components/ShowMoreFooter.tsx:

  • SHOW_MORE_TIERS = [50, 100, 200] (tier ladder; backend cap matches kora_cli/web_server.py's max(1, min(limit, 200)))
  • Hidden when totalShown < currentLimit AND < cap
  • Below cap → "Showing N units · Show more" link button
  • At cap → terminus line: "Showing 200 units (backend cap; older entries via JSONL / substrate forensics)"

api.ts extended: getRecentSlackDM / getRecentAgentActivity / getRecentReasoning / getRecentWebhookEvents all accept an optional limit param.

The 4 timeline panels wire useState<number>(SHOW_MORE_DEFAULT_LIMIT) + thread limit into their useCallback deps so bumping re-fetches via the existing useEffect.

Test plan

  • tests/kora_cli/test_fe_ops_quality_pass.py15 source-pin tests:
    • TZ: formatTimestamp (local) suffix; timestampAbsoluteUtc exported; 5 panels import both
    • Empty-state: positive copy + success-tone card pin for webhook_events / agent_activity / reasoning; anti-regression pins for slack_dm / email runbook pointers
    • Show More: component exists; tier ladder = [50, 100, 200]; backend cap = 200; at-cap mentions JSONL + substrate; api.ts threads limit; 4 panels wire footer + setLimit + api(limit)
  • Full admin-panel + audit regression: 236/236 across the in-scope suites
  • pnpm tsc -b clean
  • pnpm build clean
  • Manual smoke: load each of the 4 timeline panels at full data → click "Show more" through 50→100→200 cap → verify terminus copy; hover timestamps to confirm UTC tooltip

Spec scope discipline

  • Single PR bundling all 3 per spec §0 + §4 (no thematic-commit split; changes are tightly interleaved via panelHelpers + panel files so a single commit reads more clearly)
  • TZ user preferences / infinite scroll / filter persistence / color-blind audit all explicitly non-scope per spec §3

Refs

  • rafe-walker/kora-docs17_cc_bucket_prompts/KR-FE-OPS-QUALITY-PASS_three_observations.md
  • PR #143 ship report — observations that surfaced this bucket
  • PR #151 — KR-FE-PANEL-HELPERS-DRY (formatTimestamp / formatRelative extraction; this bucket extends those helpers)
  • PR #134 — KR-ALERTS-PANEL (positive-reinforcement empty state pattern)

🤖 Generated with Claude Code

Three banked observations from CC#2's PR #143 ship report,
bundled per spec. ~150 LOC prod + ~80 LOC test estimate hit
within margin (~230 net additions including the new shared
component + test pin module).

Improvement 1 — Empty-state convergence
========================================

AlertsPanel's positive-reinforcement empty state (green
CheckCircle2 + "Daemon healthy") extended to the panels where
empty SEMANTICALLY MEANS healthy idle. Judgment call per panel
documented inline:

  * WebhookEventsPanel: empty events list → "No webhook traffic.
    Public plane healthy on port 9118." + success-toned card.
    BUT: empty filter results (operator chose a filter that
    matches nothing) keep neutral HelpCircle — that's not a
    healthy signal, just a no-match.
  * AgentActivityPanel: empty calls list → "No agent activity.
    /mcp endpoint healthy on port 9119."
  * ReasoningPanel: empty calls list → "No reasoning activity.
    Kora is idle."

NOT converged per spec §1 (data-hasn't-arrived semantic, not
healthy-idle):
  * SlackDMPanel — empty pre-deploy points at slack_app_setup_runbook
  * EmailPanel — empty pre-deploy points at purelymail_setup_runbook
  * HeartbeatPanel — cache_warming has its own positive copy

Test pins preserve the slack/email runbook-pointer text so
future refactors can't accidentally drop the data-arrives-from
context.

Improvement 2 — Timestamp TZ rendering
========================================

panelHelpers.formatTimestamp now appends "(local)" suffix to
the localized output so operators on multiple machines aren't
TZ-confused.

New companion: panelHelpers.timestampAbsoluteUtc(iso) returns
the Z-suffixed UTC ISO for hover tooltips. Normalizes to the
canonical "2026-05-23T17:48:42Z" shape that operator workflows
grep for in logs / substrate.

Wired into the 5 panels that consume formatTimestamp from the
shared helper (AgentActivityPanel, AlertsPanel, EmailPanel,
ReasoningPanel, SlackDMPanel). Pattern: existing
  <span title={formatTimestamp(...)}>{formatRelative(...)}</span>
becomes
  <span title={timestampAbsoluteUtc(...)}>{formatRelative(...)}</span>
plus a title= added wherever formatTimestamp appears as visible
text. Operator hover reveals UTC; visible text shows localized
+ "(local)" disambiguator.

Improvement 3 — Show More affordance
========================================

New ``web/src/components/ShowMoreFooter.tsx`` (1 component,
3 exports):
  * SHOW_MORE_TIERS = [50, 100, 200] (tier ladder)
  * SHOW_MORE_DEFAULT_LIMIT = 50
  * SHOW_MORE_BACKEND_CAP = 200 (matches backend's
    max(1, min(limit, 200)) in kora_cli/web_server.py)

Render semantics:
  * Hidden when totalShown < currentLimit AND < cap — the
    operator has everything; "Show more" would mislead.
  * Below cap → "Showing N <units> · Show more" link button.
  * At cap → terminus line: "Showing 200 <units> (backend
    cap; older entries via JSONL / substrate forensics)" —
    operator knows where to look for older data.

api.ts extended: getRecentSlackDM / getRecentAgentActivity /
getRecentReasoning / getRecentWebhookEvents all accept an
optional limit param + thread it into the URL.

The 4 timeline panels wire the footer:
  * useState<number>(SHOW_MORE_DEFAULT_LIMIT)
  * limit threaded into the api call inside the loadX useCallback
    (added to deps so bumping limit re-fetches via the existing
    useEffect)
  * <ShowMoreFooter currentLimit={limit} totalShown={X.length}
    onShowMore={setLimit} unitLabel="..."> at timeline bottom

Tests (tests/kora_cli/test_fe_ops_quality_pass.py — 15 tests):

  Improvement 1:
    * webhook_events / agent_activity / reasoning use positive
      empty state (CheckCircle2 + success-toned card + copy pin)
    * slack_dm / email keep neutral with runbook pointer
      (anti-regression on the data-hasn't-arrived semantic)

  Improvement 2:
    * formatTimestamp appends "(local)" suffix
    * timestampAbsoluteUtc helper exported (uses toISOString)
    * 5 panels with formatTimestamp also import timestampAbsoluteUtc

  Improvement 3:
    * ShowMoreFooter component exists
    * Tier ladder = [50, 100, 200]
    * Backend cap = 200 matches backend's max-clamp
    * At-cap terminus mentions JSONL + substrate forensic paths
    * api.ts threads ?limit into 4 endpoints (signature pin)
    * 4 timeline panels wire footer + limit state + threaded api call

Verification:
  * Full admin-panel + audit suite: 236/236 (selective; full run
    matches previous 434 from #151 minus tests outside this
    bucket's scope).
  * tsc -b + vite build both clean.

Spec scope discipline:
  * Single PR bundling all 3 per spec §0 + §4 (no thematic-commit
    split; the changes are tightly interleaved via panelHelpers +
    panel files so a single commit reads more clearly).
  * TZ user preferences / infinite scroll / filter persistence /
    color-blind audit all explicitly non-scope per spec §3.

Refs:
  * rafe-walker/kora-docs 17_cc_bucket_prompts/KR-FE-OPS-QUALITY-PASS_three_observations.md
  * PR #143 ship report — observations that surfaced this bucket
  * PR #151 — KR-FE-PANEL-HELPERS-DRY (formatTimestamp / formatRelative
    extraction; this bucket extends those helpers)
  * PR #134 — KR-ALERTS-PANEL (positive-reinforcement empty state
    pattern that the other panels adopt)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rafe-walker pushed a commit that referenced this pull request May 24, 2026
…tion + revert

Build-list follow-on to PR #167 (phrasebook read-only viewer).
Adds the operator-edit story so the phrasebook can be modified
from the cockpit instead of YAML by hand. Also sets the UX
foundation that the eventual promotion-review panel will reuse
(KR-PROMOTE-PHRASEBOOK proposals are pending edits to this same
surface).

Backend (kora_cli/short_circuit/phrasebook_editor.py — new)
==========================================================

Module hosts everything the read-only viewer didn't need:

  * validate_entries(entries) → [EntryValidationError, ...]
    8 checks (in order; later skipped for an entry that failed
    earlier to avoid noise): payload is list, length cap (200
    entries), required-fields-non-empty, length caps on each
    field, regex compiles with re.IGNORECASE, catastrophic-
    backtracking guard (catches (a+)+, (.*)*, etc), snapshot
    placeholder paths resolve to known scalars, no duplicate
    (pattern, category) tuples.

  * SNAPSHOT_SCALAR_PATHS — static frozen-set of known snapshot
    v4 scalar paths the operator can reference in
    {snapshot.X.Y} placeholders. Drift-guarded by
    test_static_schema_matches_snapshot_collectors which greps
    each leaf key against the snapshot collector source. Only
    scalars (no dynamic-key dicts like listeners.X or
    cost_telemetry.X — those str() to Python repr and would
    render garbage in operator-facing DMs).

  * write_backup_for / rotate_backups / list_backups —
    timestamped backups under ${KORA_HOME}/phrasebook/backups/.
    KORA_PHRASEBOOK_BACKUP_COUNT env var tunes rotation
    (default 10; clamped to [1, 1000]). Filenames sort
    chronologically as plain strings (ISO-Z format).

  * write_phrasebook(entries) — atomic write via
    utils.atomic_replace (same pattern as snapshot writer).
    Deterministic field order for readable YAML diffs.

  * revert_phrasebook(filename=...) — revert to a specific
    backup OR (no filename) the most-recent OR (no backups)
    remove the override entirely. Path-traversal defense
    rejects any filename containing /, \, .., or not matching
    the slack_dm.*.yml shape.

Backend (kora_cli/web_server.py — 3 new endpoints)
==================================================

  PUT  /api/phrasebook/slack_dm           — validate + back up +
                                            atomic-write + audit
  POST /api/phrasebook/slack_dm/revert    — revert to backup
  GET  /api/phrasebook/slack_dm/backups   — list backups

PUT contract:
  * Validation fails → 422 with structured per-entry errors;
    NO write; previous override preserved.
  * Validation passes → backup current (if exists) → atomic
    write → rotate backups → audit row → 200 with echoed
    entries + backup_filename + rotated_count.
  * Write itself fails → 500; backup preserved so operator
    can recover.

Audit seam (new): phrasebook.updated
====================================

Extended SeamName Literal in kora_cli/audit/jsonl_sink.py.
Each successful PUT (or revert) emits one entry with:
  * actor          — "operator" (cockpit-driven; future
                      "kora_proposal_approved" from the
                      promotion-loop bucket reuses this shape)
  * action         — "put" | "revert"
  * entry_count_before / entry_count_after
  * backup_filename (when applicable)
  * rotated_backup_count (when applicable)
  * reverted_to (when action=revert)

Drives the future KR-PROMOTION-REVIEW-PANEL via the existing
audit-panel infrastructure (KR-AUDIT-PANEL-ENDPOINTS PR #155).

Frontend
========

  * web/src/pages/PhrasebookEditor.tsx (new) — hosts the
    editor sub-components so PhrasebookPage stays readable:
    EntryEditorRow (4 inline editable fields + per-field
    validation errors), EditModeControls (Save / Cancel / Add),
    BackupsDialog (modal with newest-first list + per-row
    revert + confirm), ClientSidePreview (mirrors
    dm_phrasebook.match + render_reply in TS so operator can
    preview in-progress edits without saving).

  * api wrappers + types: putSlackDmPhrasebook /
    revertSlackDmPhrasebook / getSlackDmPhrasebookBackups +
    PhrasebookEntryWrite / PhrasebookPutResponse /
    PhrasebookValidationErrorEntry/Body /
    PhrasebookRevertResponse / PhrasebookBackupItem /
    PhrasebookBackupsResponse.

  * PhrasebookPage extended with edit-mode toggle. View-mode
    surface (read-only table + live tester) is unchanged for
    operators who just want to inspect. Edit-mode swaps to
    editor rows + client-side preview + Save/Cancel/Add.
    Backups button in view-mode opens the revert dialog.

  * 422 validation-error parsing: Save handler unmarshals
    fetchJSON's "STATUS: BODY" Error message; on 422 with
    error="validation_failed" the body's per-entry errors are
    routed to the editor for inline rendering.

Tests
=====

  Backend (tests/kora_cli/test_phrasebook_editor.py — 44 tests):

    Validation (14): valid entries, non-list, missing field,
      empty whitespace, all 4 length caps, entries count cap,
      invalid regex, catastrophic-backtracking parametrized
      across 4 pathological patterns, unknown vs known snapshot
      path, duplicate (pattern, category), schema drift guard
      against state_snapshot.py.
    Backups (5): no-override returns None, copies content +
      timestamps filename, rotation keeps N most recent, env
      override + clamping parametrized, list returns newest-
      first with entry_count.
    Write+revert (5): round-trip load, revert by name, revert
      most-recent, revert with no backups removes override,
      path-traversal rejection parametrized across 5 attacks.
    Endpoints (7): PUT valid + audit, PUT invalid + preserve,
      PUT no-existing-override, POST revert + audit, POST
      revert invalid filename → 400, POST revert missing →
      404, GET backups newest-first.
    Audit (1): SeamName Literal includes phrasebook.updated.
    Integration (1): round-trip PUT then revert restores seed.

  FE source-pins (tests/kora_cli/test_phrasebook_editor_fe_pins.py
  — 17 tests):

    api wrappers (3), TS types declared + shape pinned (2),
    editor file exists + exports (1), page wiring (3), edit
    button hidden in edit-mode (1), 422 body parsing branch
    present (1), ClientSidePreview mirrors backend regex flag
    + "unknown" sentinel + null-snapshot fall-through (3),
    SnapshotResponse type covers daemon_health (1).

  All 76 phrasebook tests pass (44 BE editor + 17 FE pins +
  15 existing PR #167 read-side tests).

  tsc -b clean. vite build clean.

Screenshots
===========

  web/docs/phrasebook-editor/edit_mode.png — edit-mode UI with
    client-side preview rendering a $0 reply
  web/docs/phrasebook-editor/validation.png — 422 response with
    4 inline per-field errors (bad regex, unknown snapshot path,
    catastrophic backtracking, missing description) + top-level
    duplicate error
  web/docs/phrasebook-editor/revert.png — backups modal with
    newest-first list, per-row Revert confirm flow, greyed-out
    corrupt backup

Design choices noted (no STOP-ASKs triggered)
=============================================

  * Snapshot field-path validation uses a STATIC allow-list
    (SNAPSHOT_SCALAR_PATHS), NOT a live snapshot walk. Spec §4
    flagged this as a possible STOP-ASK; the static-list
    approach is what the spec offered as the alternative
    ("use SnapshotResponse static schema") and avoids the
    dynamic-snapshot-during-warm-up problem where freshly-
    booted daemons would mark canonical paths as invalid.
    Drift guard test pins each scalar leaf against the
    snapshot collector source.

  * Audit seam SeamName extension follows the
    probe.wake_requested precedent — extend Literal with a
    new value + add the comment explaining the future
    actor extension. No consumer drift.

  * Operator-defined category values: free-form for v1
    (the runtime doesn't reserve any category names yet;
    promotion-loop bucket can add reserved-prefix
    discipline when its UX lands).

Refs
====

  * rafe-walker/kora-docs
    17_cc_bucket_prompts/KR-FE-PHRASEBOOK-EDITOR-AND-CRUD_write_path_with_validation.md
  * PR #160 — phrasebook + dm_phrasebook module (read side)
  * PR #167 — KR-FE-PHRASEBOOK-VIEWER (read-only viewer this extends)
  * PR #170 — snapshot v4 daemon_health (referenced in
    SNAPSHOT_SCALAR_PATHS allow-list)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rafe-walker added a commit that referenced this pull request May 24, 2026
…kpit (#180)

Cockpit panel surfacing intent.email_to_sea_ticket audit stream. 5-state action filter chips (created/logged_only/dry_run/cap_exceeded/failed). Sea_Ticket deep-link when ticket_id present. Sparkline of daily 'created' counts (plain SVG, no chart-library dep).

Spec deviation flagged inline: bucket said 'FE-only via /api/audit-events?seam=X' but K-DG showed that endpoint doesn't exist — established discipline (PR #155) is one endpoint per seam. Added /api/email-intent/recent (~150 lines) following agent-activity/reasoning/webhooks pattern instead of escalating + coord-cycle.

Drift-guard pin across THREE sources: BE projection allowlist _EMAIL_INTENT_ACTION_VALUES + 5 BE emit sites in intent/email_to_sea_ticket.py + FE constant EMAIL_INTENT_ACTION_VALUES. Test regex-greps each + asserts equality.

Security: per-action field projection whitelist (tested against arbitrary-field leak); subject + error truncated to 200 chars; unknown action values coerced defensively.

24/24 tests (13 BE + 2 drift guard + 9 FE source-pins). tsc + vite build clean.

Follow-on suggested: KR-EMAIL-LOGGED-ONLY-ANALYZER (surface un-acted-on emails as promotion-loop training-data lens once logged_only entries accumulate).
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