Add filter-mode parity (Last N Lines / Time Range / All) to Chat Logs modal - #141
Conversation
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.
There was a problem hiding this comment.
PR Review: Filter-mode parity for Chat Logs modal
Strengths
- Clean DRY extraction:
LogFilterControls.jsxandlogFilterOptions.jscorrectly centralize all filter UI and constants, eliminating the ~80-line duplicated block that previously lived inViewLogsModal.jsx. Both modals now share a single source of truth. - Solid injection prevention in Ansible:
SINCEandLOGFILEare 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_modeenum check,ALLOWED_CHAT_LOG_SINCEfrozenset 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
useEffectcorrectly removesfilterMode/lineCount/timeRangefrom its dependency array so filter changes don't trigger live reloads — intentional and well-commented. getFilterDescriptionlifted to shared module: Eliminates the local inline version that was inViewLogsModal.jsxand 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, invalidfilename(e.g.../etc/passwd,chat.log.abc), invalidsincefor time mode, validsincefor non-time mode (should pass without error), and boundary values forlines.
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-44 — cat {{ 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.
Summary
Brings the Chat Logs modal to feature parity with the Instance Logs modal by adding the
Filter Bycontrol: Last N Lines / Time Range / All.LogFilterControls.jsx+logFilterOptions.js, now used by both modals (removes duplication; both modal files stay under the 300-line limit).chat.log,chat.log.N) still reloads immediately. Header subtitle shows the active filter.filter_modefor chat logs:lines→tail -nall→cattime→ host-computeddate -d "<since>"cutoff, compared withawkagainst each line's fixed-width[YYYY-MM-DD HH:MM:SS]prefix. Scoped to the selected archive file.docs/api_reference.mdupdated for the newchat-logsquery params.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):
since(fixed set of windows) and a strictchat.log(.N)regex forfilename.SINCE/LOGFILEpassed to the shell viaenvironment:env vars, so Jinja values are never expanded into the command text. Verified locally: a malicioussinceyields onlydate: invalid dateand executes nothing.Test plan
pnpm/vitest: all 53 frontend tests pass; ESLint clean; production build succeeds.awk/datetime filter unit-tested locally against a syntheticchat.log(entries before cutoff dropped, at/after kept, connection-event lines included).PWNEDfile created).ansible-playbook --syntax-checkpasses; Python compiles.