Skip to content

Add filter-mode parity (Last N Lines / Time Range / All) to Chat Logs modal - #141

Merged
dngrtech merged 3 commits into
mainfrom
feat/chat-logs-filter-parity
Jul 8, 2026
Merged

Add filter-mode parity (Last N Lines / Time Range / All) to Chat Logs modal#141
dngrtech merged 3 commits into
mainfrom
feat/chat-logs-filter-parity

Conversation

@dngrtech

@dngrtech dngrtech commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Brings the Chat Logs modal to feature parity with the Instance Logs modal by adding the Filter By control: Last N Lines / Time Range / All.

  • Shared UI: extracts the filter bar into LogFilterControls.jsx + logFilterOptions.js, now used by both modals (removes duplication; both modal files stay under the 300-line limit).
  • Behavior parity: filter changes (mode / lines / time) apply via the Apply button instead of refetching on every click. Selecting a different archive file (chat.log, chat.log.N) still reloads immediately. Header subtitle shows the active filter.
  • Full-stack filter_mode for chat logs:
    • linestail -n
    • allcat
    • time → host-computed date -d "<since>" cutoff, compared with awk against each line's fixed-width [YYYY-MM-DD HH:MM:SS] prefix. Scoped to the selected archive file.
  • Docs: docs/api_reference.md updated for the new chat-logs query params.
  • Version: bumped to 1.13.12 (VERSION, version.json).

Security

The time-range task runs a shell command as root on the QLDS host. Hardened against command injection (flagged by automated review):

  • Server-side allowlist for since (fixed set of windows) and a strict chat.log(.N) regex for filename.
  • SINCE / LOGFILE passed to the shell via environment: env vars, so Jinja values are never expanded into the command text. Verified locally: a malicious since yields only date: invalid date and executes nothing.

Test plan

  • pnpm/vitest: all 53 frontend tests pass; ESLint clean; production build succeeds.
  • awk/date time filter unit-tested locally against a synthetic chat.log (entries before cutoff dropped, at/after kept, connection-event lines included).
  • Injection neutralization verified locally (no PWNED file created).
  • ansible-playbook --syntax-check passes; Python compiles.
  • Live click-through against a real instance (needs the production host to fetch remote chat logs).

rage added 2 commits July 8, 2026 08:57
Give the Chat Logs modal the same Filter By controls as the Instance
Logs modal: Last N Lines, Time Range, and All. Filters now apply via the
Apply button (not on every click); selecting a different archive file
still reloads immediately.

- Extract the shared filter bar into LogFilterControls.jsx +
  logFilterOptions.js and use it in both modals (removes duplication,
  keeps both files under the 300-line limit).
- Chat-logs backend + fetch_chat_logs.yml gain filter_mode/since:
  lines -> tail, all -> cat, time -> host-computed `date -d` cutoff with
  awk comparison on each line's [YYYY-MM-DD HH:MM:SS] prefix (scoped to
  the selected archive file).
- Harden the time-range task against command injection: validate
  `since` against a fixed allowlist and `filename` against a strict
  regex server-side, and pass SINCE/LOGFILE to the shell via env vars so
  Jinja values are never expanded as shell syntax.
- Update api.js and docs/api_reference.md.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: Filter-mode parity for Chat Logs modal

Strengths

  • Clean DRY extraction: LogFilterControls.jsx and logFilterOptions.js correctly centralize all filter UI and constants, eliminating the ~80-line duplicated block that previously lived in ViewLogsModal.jsx. Both modals now share a single source of truth.
  • Solid injection prevention in Ansible: SINCE and LOGFILE are passed as environment variables (not shell-interpolated), so the awk/date command cannot be exploited even if upstream validation were bypassed (ansible/playbooks/fetch_chat_logs.yml:56-58).
  • Strong input validation in the route: The filter_mode enum check, ALLOWED_CHAT_LOG_SINCE frozenset allowlist, and the filename regex (re.fullmatch(r'chat\.log(\.\d+)?', filename)) together close the obvious injection surfaces (ui/routes/instance_routes.py:616-629).
  • Apply-on-demand UX is consistent: The useEffect correctly removes filterMode/lineCount/timeRange from its dependency array so filter changes don't trigger live reloads — intentional and well-commented.
  • getFilterDescription lifted to shared module: Eliminates the local inline version that was in ViewLogsModal.jsx and keeps the header text consistent across both modals.

Issues

Critical (Must Fix)

None.


Important (Should Fix)

1. No tests for the new validation logic
ui/routes/instance_routes.py:616–629 adds four distinct validation branches (filter_mode enum, filename regex, since allowlist, lines range bypass for 'all'). None of this is covered by automated tests. If the allowlist or regex is accidentally mutated, there is nothing to catch it.

  • Add route-level unit tests covering: valid modes, invalid filter_mode, invalid filename (e.g. ../etc/passwd, chat.log.abc), invalid since for time mode, valid since for non-time mode (should pass without error), and boundary values for lines.

2. Lines are validated for time mode but never used
ui/routes/instance_routes.py:632:

if filter_mode != 'all' and (lines < 10 or lines > 10000):

This rejects an out-of-range lines value even when filter_mode == 'time', where lines is ignored by the playbook. An innocent caller setting filter_mode=time&lines=0 (or not passing lines at all, which defaults to 500 and passes) would be confused by a 400 error unrelated to the actual operation. Restrict the check to filter_mode == 'lines' only, or document that lines is ignored for other modes.

3. leading prop is dead code
frontend-react/src/components/instances/LogFilterControls.jsx:125:

leading = null,

The prop is accepted and rendered but never passed by either consumer. The chat log modal's file selector Listbox is rendered in the modal header, not via leading. Either wire it up or remove it; dead API surface in a shared component creates confusion about what callers should do.


Minor (Nice to Have)

4. since is always sent in API params regardless of mode
frontend-react/src/services/api.js:558-559:

const params = new URLSearchParams({
  filter_mode: filterMode,
  since: since,
  ...

When filterMode is 'lines' or 'all', the since value is sent to the backend, which ignores it. This is harmless (backend validates it only for time mode) but adds noise to the request URL and makes logs harder to read. Only include since when filterMode === 'time'.

5. No server-side cap on response size for all mode
ansible/playbooks/fetch_chat_logs.yml:38-44cat {{ log_file }} has no size guard. A multi-GB log file would be streamed entirely through Ansible stdout, into Python memory, and returned over HTTP. The UI warning is good, but the server has no protection. The pre-existing instance-logs endpoint has the same issue, so this is not a regression — but extending the pattern to chat logs makes it worth flagging. A head -c 50M or similar hard cap would bound the blast radius.

6. filter_mode not validated at the Ansible layer
ansible/playbooks/fetch_chat_logs.yml — if log_mode receives a value other than 'lines', 'all', or 'time' (only possible by bypassing the Python route), all three tasks are skipped and the output variable falls through to the chat_output_lines.stdout | default('') branch, returning empty string with no error. This is a graceful silent failure rather than a security issue, but a failing assertion or assert task at the start of the play would make the misconfiguration visible.


Assessment

Ready to merge? Yes, with fixes recommended.

Reasoning: The security-sensitive path (injection prevention via env vars + input allowlists + filename regex) is handled correctly, and the refactoring is clean. The two "Important" issues (missing tests, misleading lines-validation-for-time-mode) are worth addressing before or shortly after merge, but neither is a data-loss risk or vulnerability.

Address PR #141 review:
- Remove the unused `leading` prop from LogFilterControls (neither modal
  passes it; the chat archive selector lives in the modal header).
- Add tests/test_chat_logs_validation.py covering the security-sensitive
  400 rejection paths (invalid filter_mode, path-traversal/malformed
  filename, invalid `since` in time mode) plus that `since` is ignored
  outside time mode and valid time requests reach the task logic.
@dngrtech
dngrtech merged commit 972637d into main Jul 8, 2026
@dngrtech
dngrtech deleted the feat/chat-logs-filter-parity branch July 8, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant