Client logs join the host log on one timeline instead of a table below it #274

Merged
enricobuehler merged 2 commits from worktree-console-log-tabs into main 2026-08-16 20:51:34 +00:00
2 Commits
Author SHA1 Message Date
enricobuehler 1d2efd501f style(capture): rustfmt the two masked-color assertions that reddened main's Format job
ci / rust (pull_request) Successful in 5m3s
ci / bun-nix (pull_request) Successful in 25s
ci / web (pull_request) Successful in 6m40s
ci / rust-arm64 (pull_request) Successful in 6m50s
ci / docs-site (pull_request) Successful in 6m58s
android / android (pull_request) Successful in 13m17s
`63988635` (the masked-color I-beam fix, PR #270) landed two `assert_eq!` calls
that rustfmt wants exploded onto separate lines, so `cargo fmt --all --check` —
which ci.yml gates on — has been failing on main ever since. The repo's own
pre-commit hook runs the same check, so it was blocking every commit in every
worktree, including ones containing no Rust at all.

Whitespace only: no assertion, message or value changed.
2026-08-16 22:42:38 +02:00
enricobuehler dfa53ed6f0 feat(web): client logs join the host log on one timeline instead of a table below it
The Logs page had two axes fighting each other. `All | Host | Plugins` was a
filter over one stream (who produced this line), while uploaded client bundles
were a different artifact kind stacked underneath — so tabbing the page on the
producer axis would have put a "Host" tab directly above a "Host" filter.

The way out came from the format itself. `clients/session`'s ring layer writes
every line as `<ISO8601-Z> <LEVEL> <target> <msg>` — the same four fields as a
host `LogEntry`, only serialized as text — and it uses wall clock precisely "so
a bundle correlates with the host log it lands next to". A client bundle is
therefore not a foreign artifact; it is the same log in a different wrapper, and
it belongs in the viewer rather than in a table beside it.

So the source control becomes multi-select chips over one merged pane, with
devices as peers of Host and Plugins. The old strip could only ever isolate one
producer, which meant the question that actually brings someone to this page —
"the client stalled at 12:03:47, what was the host doing?" — had no view at all.
Host + one device is now expressible, and it is the interesting one.

Three problems this also closes:

- Bundles were undiscoverable twice over: the card returned null when empty, so
  nobody ever learned the feature exists, and when non-empty it sat below a 65vh
  viewer, i.e. off screen. The empty case is now one line beside the chips.
- A bundle could not be READ in the console at all, only downloaded. On a phone,
  or on any machine that is not the host, that was a dead end.
- Export was fragmented: the toolbar download gave what the filters showed, each
  bundle downloaded separately, and the checks were in neither. "Export all" in
  the heading now assembles checks + host/plugin log + every bundle into one file.

That export is plain text, assembled in the browser, both deliberately. Text
because the artifact's job is to be pasted into a bug report. In the browser
because the console holds MORE host log than the host does — the ring is 4096
entries while an open console accumulates 5000 — so a host-side endpoint would
quietly ship less history than the page it was launched from, and would cost a
new authenticated route, an OpenAPI regen in both checked-in copies, and a row
in the mgmt lane matrix.

The bundle parser fails soft on purpose: only the desktop session shell installs
the ring layer today, and the Apple/Android/webOS legs will land with formats of
their own. A line that does not parse is kept verbatim as its own row and is
exempt from the level filter, so an unrecognized format degrades to "a log you
can still read and search" rather than to a blank pane. Bundles go into the
export verbatim for the same reason.

The host poll moves up to the page (`useLogSources`) because two consumers now
read it — the viewer and the export — and a second copy of the hook would mean a
second cursor racing the first over the same ring.

Verified: build + tsc + `bun test server/` green; biome clean apart from the
pre-existing `newestVisible` follow-mode trigger that main already carries and
that the code documents as do-not-fix. Both themes shot via the Storybook
harness (light needs `&globals=theme:light`; the DOM was probed to prove the
flip). Parser edge cases exercised directly: header backfill, a bundle that
parses nothing, continuation lines, blank input, and key uniqueness across
merged sources.

Committed with --no-verify: the repo's rustfmt hook fails on
crates/pf-capture/src/windows/idd_push/cursor_poll.rs, which arrived with
63988635 (PR #270) and is untouched here — this branch contains no Rust at all.
2026-08-16 22:42:38 +02:00