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
Open
feat(kora): KR-FE-OPS-QUALITY-PASS — empty-state + TZ + show-more#155rafe-walker wants to merge 1 commit into
rafe-walker wants to merge 1 commit into
Conversation
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>
This was referenced May 24, 2026
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>
4 tasks
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).
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
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:HelpCircle— not a healthy signal, just a no-match.)NOT converged per spec §1 (data-hasn't-arrived semantic; converging would mislead):
slack_app_setup_runbookpurelymail_setup_runbookcache_warminghas its own positive copyTest 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.formatTimestampnow 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
formatTimestampfrom the shared helper. Pattern:Plus
title=added whereverformatTimestampappears 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 matcheskora_cli/web_server.py'smax(1, min(limit, 200)))totalShown < currentLimit AND < capapi.tsextended:getRecentSlackDM/getRecentAgentActivity/getRecentReasoning/getRecentWebhookEventsall accept an optionallimitparam.The 4 timeline panels wire
useState<number>(SHOW_MORE_DEFAULT_LIMIT)+ threadlimitinto theiruseCallbackdeps so bumping re-fetches via the existinguseEffect.Test plan
tests/kora_cli/test_fe_ops_quality_pass.py— 15 source-pin tests:formatTimestamp(local)suffix;timestampAbsoluteUtcexported; 5 panels import both[50, 100, 200]; backend cap = 200; at-cap mentions JSONL + substrate; api.ts threads limit; 4 panels wire footer + setLimit + api(limit)pnpm tsc -bcleanpnpm buildcleanSpec scope discipline
Refs
rafe-walker/kora-docs→17_cc_bucket_prompts/KR-FE-OPS-QUALITY-PASS_three_observations.mdformatTimestamp/formatRelativeextraction; this bucket extends those helpers)🤖 Generated with Claude Code