feat(tauri): make unpack logs viewer usable for debugging

The original unpack logs window was a flat, monolithic scroll of every
unrar invocation glued together as one continuous textfield. That is
fine for a sanity check but hostile to actually finding a failed
extraction in a session with 30+ games: empty lines from unrar bloated
the view, there was no way to focus on a single game, no filtering, and
no way to narrow in on the entries that actually failed.

This rewrites the viewer to be a proper debugging surface while keeping
the backend untouched -- it still consumes the existing
`get_unpack_logs` command and `unpack-logs-updated` event.

User-visible changes:

* Empty / whitespace-only lines are stripped from stdout and stderr
  before rendering, so unrar's padding no longer drowns out real output.
* Two-pane layout: a sidebar lists every captured invocation (badge,
  archive basename, finish time); the right pane shows the selected
  entry's metadata, stdout and stderr.
* "Errors only" checkbox filters the sidebar to entries whose `success`
  flag is false (sidecar exit != 0 or one of the pre-spawn failure
  paths). This is the primary affordance for "find the unpack that
  broke".
* Regex input filters lines (not entries) -- both per-log when viewing
  one, and across the list: entries that contribute zero matching lines
  are hidden, and the remaining ones display a per-entry match counter
  next to the badge. Regex is case-insensitive; a bad pattern reddens
  the input and renders the parser error inline rather than silently
  dropping all matches.
* Prev / Next buttons plus arrow keys (and j/k) step through the
  filtered list one entry at a time, with the active row auto-scrolled
  into view. Selection is tracked by the entry's index in the full log
  ring so it survives filter toggles and live appends.

Code organization:

The component, its types, helpers (`basename`, `nonEmptyLines`,
`formatLogTime`, `isUnpackLogsView`) and its CSS are moved out of
`App.tsx` / `App.css` into a dedicated `UnpackLogsWindow.tsx` +
`UnpackLogsWindow.css` pair. The viewer has no shared state with the
main window and lives behind its own `?view=unpack-logs` route, so
keeping ~200 lines of debug-UI plumbing inside `App.tsx` was just
noise. `App.tsx` now imports `UnpackLogsWindow` and `isUnpackLogsView`
and otherwise looks the same as before.

Intentionally out of scope:

* No backend changes. The Rust side already records everything needed;
  this is purely a presentation improvement.
* No "view all logs concatenated" mode. The flat view was what we just
  replaced -- if it is ever wanted back, it can be added as a third
  pane mode.
* Regex is applied to displayed lines only, not to archive paths or
  meta. Filtering by archive name is easy enough via the basename in
  the sidebar; adding a second filter for it now would be premature.
* Logs are still process-local and capped at `MAX_UNPACK_LOGS` (100)
  in the Rust state -- unchanged from b35755f.

Test plan:

* `tsc --noEmit` and `vite build` are clean.
* Manual: trigger several successful and failed unpacks (rename one
  archive between handshake and extraction to force a canonicalize
  failure), open Unpack Logs, and verify:
    - empty lines are gone from stdout/stderr,
    - sidebar lists every entry with the right OK/FAIL badge,
    - "Errors only" hides the OK rows,
    - typing a regex narrows lines in the open entry, hides entries
      with no matches, and shows the per-entry match counts,
    - an invalid regex (e.g. `[`) reddens the field and shows the
      parser error rather than crashing,
    - arrow keys / j / k step through the filtered list and the
      active row scrolls into view,
    - new entries arriving via `unpack-logs-updated` while the window
      is open keep the current selection rather than jumping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 19:54:50 +02:00
parent b35755f4e6
commit ff35f0d95f
4 changed files with 483 additions and 154 deletions
@@ -349,80 +349,3 @@ h1.align-center {
left: 20px;
z-index: 1001;
}
.unpack-log-window {
min-height: 100vh;
box-sizing: border-box;
padding: 18px;
background: #000313;
color: #D5DBFE;
}
.unpack-log-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.unpack-log-header h1 {
margin: 0;
font-size: 22px;
}
.unpack-log-textfield {
height: calc(100vh - 84px);
box-sizing: border-box;
overflow: auto;
padding: 14px;
border: 1px solid #444;
border-radius: 6px;
background: #050813;
font-family: Consolas, "Courier New", monospace;
font-size: 13px;
line-height: 1.35;
white-space: pre-wrap;
}
.unpack-log-empty {
color: #8892b0;
}
.unpack-log-entry {
padding-bottom: 16px;
margin-bottom: 16px;
border-bottom: 1px solid #26304f;
}
.unpack-log-entry:last-child {
margin-bottom: 0;
border-bottom: 0;
}
.unpack-log-meta {
margin-bottom: 6px;
font-weight: bold;
}
.unpack-log-meta.success {
color: #8ee6a6;
}
.unpack-log-meta.error,
.unpack-log-stream.stderr {
color: #ff8a8a;
}
.unpack-log-path {
color: #aeb7df;
}
.unpack-log-stream {
margin: 8px 0 0;
font: inherit;
}
.unpack-log-stream.stdout {
color: #D5DBFE;
}