v0.5.24
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.aopens the acknowledgment modal (reason, expires, by) on the selected finding and posts to/api/findings/{sig}/ack.uopens 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 fromFindingResponse.acknowledged_by. Refreshed via a secondGET /api/findings?include_acked=trueafter every successful submit.--api-key-file <path>flag onperf-sentinel query inspectmirroring the v0.5.22 CLI ack helper. Auth resolution priority:PERF_SENTINEL_DAEMON_API_KEYenv var first, file content fallback. No interactive password prompt in the TUI (raw mode incompatible with rpassword TTY input).include_acked=trueon the boot fetch:query inspectnow fetches/api/findings?include_acked=true&limit=1000so the TUI sees acknowledged findings and renders the indicator. Previously only active findings were displayed.- Sync-to-async bridge via
tokio::task::block_in_placewrappingcrate::tui::runinquery.rs::run_inspect_action, allowing the synchronous run loop to call the async daemon HTTP helpers viaHandle::current().block_on(...)without runtime panic. AckSubmitErrorenum incrate::ackmapping HTTP status codes to actionable variants (Unauthorized,Conflict,NotFound,StoreFull,Disabled,Validation,Http,Transport).Displayimpl never includes the API key. The modal rendersStoreFullwith a remediation hint (revoke expired acks or raise the daemon limits) and surfaces daemon-supplied 400 bodies viaValidationso the operator sees the actionable message.post_ack_via_daemonanddelete_ack_via_daemonhelpers: thin pub(crate) wrappers incrate::ackconsumed by the TUI submit path. Both percent-encode the signature segment before interpolating into the URL via the newpercent_encode_signature_segmenthelper (Cow<'_, str>return, zero-allocation common path).Deserializederives onFindingResponseandAckSourceinsentinel-core::daemon::query_api. The wire types are now round-trippable so the CLI can decode the daemon's per-finding ack annotation.acknowledged_byis#[serde(default)]on the deserialize side so older daemon responses without the field still parse cleanly.- Lifted helpers to
pub(crate)incrate::ack:parse_expires,read_api_key_file,resolve_api_key,http_call. Previously private tocmd_ack. The TUI submit path consumes them without duplication. FINDINGS_FETCH_LIMITre-exports daemon'sMAX_FINDINGS_LIMIT(1000) so the boot fetch and the post-submit refetch cannot drift from the server-side cap. BothMAX_FINDINGS_LIMITandMAX_ACKS_RESPONSEare nowpubwith#[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: rejectsc.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.mdcovering the TUI keybindings, the ack modal flow, the auth resolution, and the sync HTTP caveat. Updateddocs/ACK-WORKFLOW.mdwith a fourth row in the decision table (TUI ack action). French mirrors atdocs/FR/INSPECT-FR.mdanddocs/FR/ACK-WORKFLOW-FR.md.
Changed
- Helm chart
0.2.26->0.2.27,appVersion0.5.23->0.5.24, default daemon image tag points atghcr.io/robintra/perf-sentinel:0.5.24. Theartifacthub.io/imagesannotation is updated in lockstep. No chart template change. query inspectboot fetch changed from/api/findings?limit=10000to/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 theacknowledged_byfield 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: 0plusmargin: autorather than absolute positioning (was misaligned on tall viewports), theShow acknowledgedtoggle is initialized at boot rather than only on user change (the initial state is now honored), the auth modal surfaces anInvalid API keymessage on the second 401 when a stale key was already cached and clears it viasessionStorage.removeItem, theForget keybutton is hidden when no key is cached (was always visible underbody.ps-live).
Notes
aandukeys are no-op in batch mode (inspect --input). Acknowledgment requires a running daemon to persist; the keys silently drop the keystroke whendaemon_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 = trueis 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 rendersdaemon 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