Skip to content

feat(tui): interactive filter query ('/') and threshold alerts#196

Merged
inureyes merged 4 commits into
mainfrom
feature/issue-186-tui-filter-alerts
Apr 20, 2026
Merged

feat(tui): interactive filter query ('/') and threshold alerts#196
inureyes merged 4 commits into
mainfrom
feature/issue-186-tui-filter-alerts

Conversation

@inureyes

@inureyes inureyes commented Apr 20, 2026

Copy link
Copy Markdown
Member

Summary

Adds two tightly-coupled TUI capabilities scoped to local and remote modes.

  • Filter DSL (/ key): temp>85, util<5 & host~=dgx,
    user==alice | power>400, and similar. Fields, numeric comparisons,
    string equality, size-bounded regex (~=), &/|, and parentheses.
    Unknown fields are parse errors; fields absent on the row fail closed.
    Non-matching rows are dimmed via DarkGrey (hidable via config).
  • Threshold alerts: [alerts] config with defaults
    (temp_warn_c=80, temp_crit_c=90, util_idle_pct=5,
    util_idle_warn_mins=15, power_crit_w=0, hysteresis_c=2). Per-
    device per-rule hysteresis state machines emit
    AlertTransition records, which become toast notifications, a 1 Hz
    border flash on the affected tile, and entries in a 50-slot ring
    buffer. Optional async webhook POST (2 s timeout, bounded mpsc queue,
    non-blocking: new alerts are dropped if the worker queue is saturated).
    A toggles the history panel.

Implementation notes

  • New modules: src/ui/filter_dsl/{mod,lexer,parser,eval}.rs,
    src/ui/alerts.rs, src/network/webhook.rs,
    src/ui/renderers/dim.rs.
  • AppState gains filter_query, filter_buffer,
    filter_input_mode, filter_recent, alerter, alert_history,
    and alert_panel_open — all cloned into RenderSnapshot so the
    render path stays lock-free.
  • event_handler intercepts every key while
    FilterInputMode::Editing so that q, d, u, f etc. become
    literal text until the operator commits (Enter) or aborts (ESC).
  • Filter dimming avoided rewriting every print_colored_text call by
    post-processing the rendered bytes: dim_ansi replaces every SGR
    foreground with \x1b[90m (DarkGrey) while preserving
    cursor-movement CSI sequences. This keeps existing renderers
    untouched.
  • data_collector runs Alerter::evaluate on the GPU snapshot after
    every successful collection, pushes transitions to the notification
    manager + ring buffer, optionally emits a bell (\x07), and
    fire-and-forget POSTs to the webhook channel.
  • CLI: --alert-temp and --alert-util-low-mins on both local and
    view. Compiled defaults apply when no config file is present.
  • Regex safety: RegexBuilder::size_limit(128 * 1024); over-sized
    patterns surface as parse error: invalid regex: ....

Testing

  • 62 parser tests covering success cases for every field, every
    operator, logical combinations, whitespace tolerance, precedence,
    and failure modes (unknown field, type mismatch, unterminated
    parens, oversized regex, invalid regex, trailing garbage).
  • 23 eval tests for numeric / string / regex paths, fail-closed
    semantics, GpuInfo, ProcessInfo, and CpuInfo impls.
  • Hysteresis boundary tests: ok->warn, warn->crit, no
    transition inside the hysteresis band, recovery to ok, zero
    thresholds disabling the rule, and crit->ok direct recovery when
    the warn rule is disabled.
  • Webhook integration test (tests/alert_webhook_test.rs): minimal
    tokio TCP server captures the body, asserts the serialized JSON
    matches the field shape from the issue.
  • Event handler tests: / enters edit mode; characters append;
    Enter commits a valid filter and adds to recent; invalid query
    keeps edit mode and surfaces an error; ESC aborts; q is literal
    text while editing; Ctrl-R recalls the newest entry; A toggles
    the alert panel; ESC closes it.
  • cargo build, cargo test --lib (678), cargo test --bin all-smi
    (737), cargo clippy --all-targets -- -D warnings, and
    cargo fmt --all -- --check all pass.

Closes #186

Add two tightly-coupled TUI capabilities for local and remote modes:

Filter DSL (`/` key)
- New `src/ui/filter_dsl` module with lexer, parser, and evaluator.
- Fields: temp, util, mem_pct, mem_used, mem_total, power, user, host,
  gpu_name, driver, index, uuid, pstate, numa, device_type.
- Numeric ops `>`, `>=`, `<`, `<=`, `==`, `!=`; string ops `==`, `!=`,
  and regex `~=` (size-limited to 128 KiB via RegexBuilder).
- Logical `&`, `|`, and parenthesised sub-expressions.
- Unknown fields raise a parse error; fields absent on the device cause
  the row to fail closed so mixed views stay readable.
- `DeviceRowView` trait implemented on `GpuInfo`, `ProcessInfo`, and
  `CpuInfo` so renderers can call `apply_filter(&state.filter_query,
  row)` uniformly.

Filter UX
- `/` opens a status-bar buffer with live `[matched X of Y]` preview.
- `Enter` commits; `ESC` clears or aborts; `Ctrl-R` recalls the last
  five queries.
- Invalid queries show an inline red `parse error: ... at col N`
  message without crashing or committing.
- Non-matching rows render at 40% opacity via a post-processing
  `dim_ansi` helper so no renderer signature had to change.

Threshold alerts
- New `src/ui/alerts.rs` with per-device per-rule state machines and
  hysteresis (`hysteresis_c`).
- Rules: temperature (`temp_warn_c` / `temp_crit_c`), sustained idle
  utilization (`util_idle_warn_mins`), and power (`power_crit_w`).
- Transitions produce toast notifications (5 s via
  `AppConfig::NOTIFICATION_DURATION_SECS`), 1 Hz border flash on the
  affected GPU tile, and entries in a 50-slot ring buffer.
- `A` toggles the alert history panel.
- Optional fire-and-forget webhook POST (`reqwest`, 2 s timeout, bounded
  `mpsc` queue with drop-oldest-on-full) via new
  `src/network/webhook.rs`.

Config + CLI
- `AlertConfig` in `src/common/config.rs` with defaults matching the
  issue's `[alerts]` sketch.
- CLI overrides `--alert-temp` and `--alert-util-low-mins` on both
  `local` and `view` subcommands; the TOML loader can layer on top
  when the companion config-file issue lands.

Tests
- 62 parser tests + 23 eval tests covering all field types, hysteresis
  boundaries (entering/leaving crit), regex safety, and the webhook
  body shape (inline + `tests/alert_webhook_test.rs`).
- Event-handler tests for the filter input state machine (`/` opens,
  `ESC` aborts, `Enter` commits, `Ctrl-R` recalls, `q` is literal text
  while editing).

Closes #186
@inureyes inureyes added type:enhancement New feature or request priority:high High priority issue status:review Under review labels Apr 20, 2026
- alerts: when temp_warn_c=0 and temp_crit_c>0, a device in Crit that
  cools below crit_off now transitions straight to Ok instead of passing
  through a spurious Warn level. warn_off would otherwise be negative
  (0 - hysteresis_c), making `temp <= warn_off` almost never true. Add
  regression test `warn_disabled_crit_enabled_recovers_straight_to_ok`.
- filter_dsl/eval: gate mem_total_field on total_memory > 0 for symmetry
  with mem_pct_field. Prevents mem_total==0 from matching zero-total
  devices (fail-closed). Add `mem_total_absent_when_zero_fails_closed`.
- webhook: update module and `enqueue` doc comments to reflect actual
  behavior (try_send drops the newest payload on overflow, not oldest).
  No behavior change; the UI-never-blocks invariant is preserved.
Addresses pr-security-checker findings on PR #196:

CRITICAL
- webhook: disable HTTP redirects (Policy::none) to prevent SSRF pivot
  through attacker-controlled 3xx responses from operator-configured hosts

HIGH
- webhook: redact userinfo from URL before logging to avoid credential
  leak when operators accidentally configure basic-auth URLs
- webhook: document that config reload must rebuild DataCollector
  (OnceLock binds URL at first init)

MEDIUM
- alerts: garbage-collect states HashMap for devices that disappear from
  the snapshot (decommission / UUID churn) to prevent slow leak
- data_collector: bell write moved to spawn_blocking to avoid stalling
  Tokio executor on slow terminals
- filter_dsl: add DFA size limit (1 MiB) on regex compilation
- event_handler: cap filter_buffer at 512 chars to block UI DoS via
  bracketed-paste of large blobs
- filter_dsl: add 16 KiB lexer input cap as defense-in-depth for
  non-interactive callers
- renderers/dim: preserve background color SGR codes so selected-row
  and alert-flash highlights survive the filter-dim pass
- Add truecolor RGB background preservation tests for dim_ansi (covers
  the `48;2;r;g;b` code path documented but not previously tested)
- Add regression test for filter buffer cap at FILTER_BUFFER_MAX (512)
  to guard the DoS-prevention truncation in the editing event handler
- Document webhook SSRF protection (redirects disabled) and filter query
  buffer limit (512 chars / 16 KiB lexer gate) in README Filtering &
  Alerts section
@inureyes

Copy link
Copy Markdown
Member Author

PR Finalization Complete

Summary

Tests: Added 3 new tests covering previously-untested code paths:

  • dim_ansi with truecolor RGB background (48;2;r;g;b) — the path was documented but had no test
  • dim_ansi with a foreground-before-truecolor-bg sequence — ensures fg is dimmed while bg survives
  • Filter buffer cap at FILTER_BUFFER_MAX (512 bytes) — regression guard for the DoS-prevention truncation in the event handler

Documentation: Updated README Filtering & Alerts section with explicit security notes:

  • Webhook SSRF protection: redirects disabled, behavior on 3xx described
  • Filter query buffer limit: 512-char UI cap and 16 KiB lexer gate explained

Help pane: /, Ctrl-R, and A keys already listed in src/ui/help.rs — no change needed.

Lint/Format: cargo fmt -- --check and cargo clippy -- -D warnings both pass clean.

CHANGELOG: No CHANGELOG file exists in this repo — skipped.

Final test counts (all passing)

  • lib: 695 passed
  • bin: 755 passed
  • integration: 1 passed (alert_webhook_test)

@inureyes inureyes added status:done Completed and removed status:review Under review labels Apr 20, 2026
@inureyes inureyes merged commit 8e3452a into main Apr 20, 2026
3 checks passed
@inureyes inureyes deleted the feature/issue-186-tui-filter-alerts branch April 20, 2026 12:52
@inureyes inureyes self-assigned this May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority:high High priority issue status:done Completed type:enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(tui): interactive filter query ('/') and threshold alerts for local/remote modes

1 participant