github robintra/perf-sentinel v0.5.24

latest releases: chart-v0.2.63, v0.8.14, chart-v0.2.62...
one month ago

What's new in v0.5.24

v0.5.24 closes the three-axis UX marathon over the daemon ack API. After the CLI helper landed in v0.5.22 and the HTML dual-mode in v0.5.23, the TUI ratatui inspector now supports interactive ack and revoke actions directly from perf-sentinel query inspect. Operators auditing findings in a terminal session can act without context-switching to a browser tab or a separate shell. The three surfaces consume the same daemon endpoints introduced in v0.5.20 and the same Prometheus counters introduced in v0.5.21, no daemon-side wire change in v0.5.24.

The keystroke surface adds two single-letter shortcuts. Pressing a on the selected finding opens an acknowledgment modal centered on the screen with three input fields (reason, expires, by) and a [Submit] / [Cancel] button row. Tab cycles forward through the cycle Reason -> Expires -> By -> Submit -> Cancel, BackTab cycles backward, Enter on a text field advances to the next, Enter on Submit posts to POST /api/findings/{sig}/ack, Esc closes without submitting. Pressing u on an acknowledged finding opens a confirmation modal with the focus pre-set on Submit, so a single Enter confirms a DELETE /api/findings/{sig}/ack. Both keys are no-op in batch mode (inspect --input) since acknowledgment requires a running daemon to persist.

The Findings panel now renders an italic gray [acked by <user>] indicator after the severity label for findings that already have an active ack, sourced from the new acknowledged_by field on the daemon's FindingResponse wrapper. The boot fetch is GET /api/findings?include_acked=true&limit=1000 (was ?limit=10000 without include_acked). The previous client-side 10000 was always silently capped server-side at MAX_FINDINGS_LIMIT = 1000 (the JSON response truncates at the cap), so 1000 is honesty-only, no payload-size change. The daemon's MAX_FINDINGS_LIMIT is now pub so the CLI re-exports it as FINDINGS_FETCH_LIMIT in crate::ack and the two sides cannot drift on a future tweak.

Authentication mirrors the v0.5.22 CLI helper. The daemon URL flows through the existing validate_url (now pub(crate) since v0.5.23) so user input rejects userinfo, paths, query strings, and trailing slashes. The X-API-Key resolves in the same priority order: PERF_SENTINEL_DAEMON_API_KEY env var first, then --api-key-file <path> flag (new on query inspect in v0.5.24), file content trimmed of trailing newlines and rejected on embedded control characters, O_NOFOLLOW symlink refusal on Unix, group/world-readable permissions warning on interactive runs. There is no interactive password prompt in the TUI: raw mode plus alternate-screen entry are incompatible with rpassword's TTY interaction. When the daemon answers 401 Unauthorized and no env or file key is configured, the modal footer surfaces an actionable message in red: API key required: set PERF_SENTINEL_DAEMON_API_KEY or pass --api-key-file when launching query inspect. The operator quits, sets the key, relaunches.

Bridging the synchronous crossterm event::read() loop with the async hyper-util HTTP helpers in crate::ack happens through tokio::task::block_in_place around crate::tui::run in query.rs::run_inspect_action. Inside submit_ack_modal, tokio::runtime::Handle::current().block_on(post_ack_via_daemon(...)) reaches the async helpers without a nested Runtime::new() panic. The UI freezes for the duration of the round-trip (typically 100-300ms on localhost, bounded by the REQUEST_TIMEOUT = 10s shared with the CLI ack helper). An async event loop refactor is a candidate followup if user feedback signals friction.

Errors from the write path land in a new AckSubmitError enum with eight variants (Unauthorized, Conflict, NotFound, StoreFull, Disabled, Validation, Http, Transport). The Display impl never includes the API key, defensive against accidental leak from a future logging path. Daemon-supplied 400 bodies surface via Validation so the operator sees the actionable message rather than a generic HTTP 400 ... line. HTTP 507 (ack store full) maps to a dedicated StoreFull variant whose Display already names the remediation: revoke expired acks or raise the [daemon.ack] limits. The sanitization contract is documented at the enum head: consumers rendering the message to a terminal must pipe it through sentinel_core::text_safety::sanitize_for_terminal. The TUI modal footer does this on every render, the bidi/control-char filter on the modal input narrows what the Validation payload can contain, daemon-supplied bodies stay untrusted.

The signature interpolated into /api/findings/{sig}/ack URLs goes through a new percent_encode_signature_segment helper that probes the input first and returns Cow::Borrowed zero-allocation for the common path (real signatures match [A-Za-z0-9_:.-]+ and pass through unchanged). Defense-in-depth against a future daemon shipping a less strict signature regex or a malicious daemon synthesizing exotic signatures in FindingResponse, the daemon already validates the shape server-side and returns 400 on malformed input.

Modal input filtering rejects C0/C1 control characters and the bidi block (U+202A..U+202E, U+2066..U+2069) at the push_char_into_focused_buffer boundary. Crossterm's KeyCode::Char(c) reaches the handler on character-bearing keys (Tab/Enter/Esc/Backspace/Delete are distinct variants), so direct keystrokes are largely safe; the vector closed here is bracketed paste of attacker-crafted content (a signature shared via chat, etc.) that could embed bidi overrides and skew the modal layout for the operator approving it. The daemon strips bidi server-side at ingest already (crates/sentinel-core/src/daemon/ack.rs::strip_bidi_and_invisible), this is the client-side defense-in-depth for the rendered input, not the persistence path.

A panic hook installed at tui::run boot via std::sync::Once restores the terminal (disable raw mode, leave alternate screen) before the standard hook prints the panic message, chained to the previous hook so the message is not lost. Without this, a panic inside run_loop (e.g. a future ratatui upgrade or a block_on edge case) leaves the operator with raw mode plus alternate screen still active, forcing a reset in their shell. The Once makes the install idempotent across run() re-entry and atomic across concurrent calls.

Helm chart 0.2.27 ships in lockstep, bumping appVersion to 0.5.24 and the default daemon image tag to ghcr.io/robintra/perf-sentinel:0.5.24. No chart-level template change beyond the image tag, the v0.5.24 surface is pure CLI runtime addition.

Added

  • TUI ack/revoke: two new keybindings in perf-sentinel query inspect. a opens the acknowledgment modal (reason, expires, by) on the selected finding and posts to /api/findings/{sig}/ack. u opens the revoke confirmation modal and DELETEs the existing acknowledgment.
  • [acked by <user>] indicator: italic gray badge appended to acknowledged findings in the Findings panel, sourced from FindingResponse.acknowledged_by. Refreshed via a second GET /api/findings?include_acked=true after every successful submit.
  • --api-key-file <path> flag on perf-sentinel query inspect mirroring the v0.5.22 CLI ack helper. Auth resolution priority: PERF_SENTINEL_DAEMON_API_KEY env var first, file content fallback. No interactive password prompt in the TUI (raw mode incompatible with rpassword TTY input).
  • include_acked=true on the boot fetch: query inspect now fetches /api/findings?include_acked=true&limit=1000 so the TUI sees acknowledged findings and renders the indicator. Previously only active findings were displayed.
  • Sync-to-async bridge via tokio::task::block_in_place wrapping crate::tui::run in query.rs::run_inspect_action, allowing the synchronous run loop to call the async daemon HTTP helpers via Handle::current().block_on(...) without runtime panic.
  • AckSubmitError enum in crate::ack mapping HTTP status codes to actionable variants (Unauthorized, Conflict, NotFound, StoreFull, Disabled, Validation, Http, Transport). Display impl never includes the API key. The modal renders StoreFull with a remediation hint (revoke expired acks or raise the daemon limits) and surfaces daemon-supplied 400 bodies via Validation so the operator sees the actionable message.
  • post_ack_via_daemon and delete_ack_via_daemon helpers: thin pub(crate) wrappers in crate::ack consumed by the TUI submit path. Both percent-encode the signature segment before interpolating into the URL via the new percent_encode_signature_segment helper (Cow<'_, str> return, zero-allocation common path).
  • Deserialize derives on FindingResponse and AckSource in sentinel-core::daemon::query_api. The wire types are now round-trippable so the CLI can decode the daemon's per-finding ack annotation. acknowledged_by is #[serde(default)] on the deserialize side so older daemon responses without the field still parse cleanly.
  • Lifted helpers to pub(crate) in crate::ack: parse_expires, read_api_key_file, resolve_api_key, http_call. Previously private to cmd_ack. The TUI submit path consumes them without duplication.
  • FINDINGS_FETCH_LIMIT re-exports daemon's MAX_FINDINGS_LIMIT (1000) so the boot fetch and the post-submit refetch cannot drift from the server-side cap. Both MAX_FINDINGS_LIMIT and MAX_ACKS_RESPONSE are now pub with #[doc(hidden)] on the daemon side: visible to the workspace CLI consumer, hidden from any future published API surface.
  • Bidi and control-character filter on modal input via is_modal_input_char_acceptable: rejects c.is_control() and the bidi block (U+202A..U+202E, U+2066..U+2069). Defense-in-depth against bracketed paste of attacker-crafted content; the daemon already strips bidi server-side on the persistence path.
  • Panic hook installed at TUI boot via std::sync::Once. Restores raw mode and the main screen before the standard hook prints the panic message, chained to the previous hook. Idempotent across re-entry, atomic across concurrent calls.
  • New documentation page docs/INSPECT.md covering the TUI keybindings, the ack modal flow, the auth resolution, and the sync HTTP caveat. Updated docs/ACK-WORKFLOW.md with a fourth row in the decision table (TUI ack action). French mirrors at docs/FR/INSPECT-FR.md and docs/FR/ACK-WORKFLOW-FR.md.

Changed

  • Helm chart 0.2.26 -> 0.2.27, appVersion 0.5.23 -> 0.5.24, default daemon image tag points at ghcr.io/robintra/perf-sentinel:0.5.24. The artifacthub.io/images annotation is updated in lockstep. No chart template change.
  • query inspect boot fetch changed from /api/findings?limit=10000 to /api/findings?include_acked=true&limit=1000. The daemon already capped at 1000 server-side, so this is honesty-only on the wire, the only behavioral change is the acknowledged_by field flowing through.
  • HTML report live mode receives a small batch of polish picked up during the v0.5.24 smoke pass on top of the v0.5.23 surface: modal centering uses inset: 0 plus margin: auto rather than absolute positioning (was misaligned on tall viewports), the Show acknowledged toggle is initialized at boot rather than only on user change (the initial state is now honored), the auth modal surfaces an Invalid API key message on the second 401 when a stale key was already cached and clears it via sessionStorage.removeItem, the Forget key button is hidden when no key is cached (was always visible under body.ps-live).

Notes

  • a and u keys are no-op in batch mode (inspect --input). Acknowledgment requires a running daemon to persist; the keys silently drop the keystroke when daemon_url.is_none(). A future axis could surface a status-bar hint, out of scope for v0.5.24.
  • HTTP requests in the modal are synchronous and freeze the UI for the duration of the round-trip (typically 100-300ms on localhost, bounded by REQUEST_TIMEOUT = 10s). Acceptable for a scope-minimal release. An async event loop refactor is a candidate followup if user feedback signals friction.
  • [daemon.ack] enabled = true is the default since v0.5.20. The TUI write path is gated behind it: the daemon answers 503 when the operator turned the store off, and the modal renders daemon ack store is disabled.
  • Marathon UX closed: this is the third and last surface above the daemon ack API: CLI helper (v0.5.22), HTML dual-mode (v0.5.23), TUI interactive (v0.5.24). All three consume the same daemon endpoints with no daemon-side wire change across the marathon.

Install

Pre-built static binaries are attached to this release for linux-amd64, linux-arm64, macos-arm64, and windows-amd64. Verify the SHA256 from SHA256SUMS.txt before extracting. Crate consumers can cargo install perf-sentinel --version 0.5.24 once the workflow finishes propagating.

Helm operators bump to chart 0.2.27 for the matching appVersion, no values.yaml change required.

Full Changelog: v0.5.23...v0.5.24

Don't miss a new perf-sentinel release

NewReleases is sending notifications on new releases.