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, which is where this started, would have put a Host tab directly above a Host filter.
The finding that unlocked it
A client bundle is not a foreign artifact. clients/session/src/ring_layer.rs writes every line as <ISO8601-Z> <LEVEL> <target> <msg> — the same four fields as a host LogEntry, only serialized as text — and wallclock()'s own doc comment says it is wall time "so a bundle correlates with the host log it lands next to".
So parsing a bundle back into rows recovers structure the client already had. It belongs in the viewer, not in a table beside it.
What changed
The source control becomes multi-select chips over one merged, wall-clock-sorted pane, with devices as peers of Host and Plugins:
Sources [Host] [Plugins] [couch-deck 12:04]
17:06:43.000 INFO punktfunk_host::punktfunk1 session start mode=1920x1080@60
17:06:43.000 [couch-deck] punktfunk-session 0.4.2 (linux x86_64)
17:06:43.000 INFO [couch-deck] punktfunk_session::stream connected host=skynet
17:06:44.500 WARN punktfunk_host::inject gamepad driver not attached…
17:06:44.500 WARN [couch-deck] punktfunk_session::audio egress late=31% — link stalled
The old strip could only ever isolate one producer, so 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. Device rows are tagged [name]; host rows are not, because absence reads as "this host" and tagging every line would double the noise in the common case.
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 naming the client-side action.
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 health checks were in neither. Export all in the page heading now assembles checks + host/plugin log + every stored bundle into one file, deliberately kept apart from the toolbar's own download ("what I am looking at").
Two calls worth reviewing
The combined export is plain text, assembled in the browser — and the browser is the correct layer, not a shortcut. The console holds more host log than the host does: the ring is CAPACITY = 4096 (log_capture.rs) while an open console accumulates KEEP = 5_000 and keeps lines the ring has already evicted. A host-side endpoint would quietly ship less history than the page it was launched from, on top of costing a new authenticated route, an OpenAPI regen in both checked-in copies, and a row in the mgmt lane matrix. Text rather than a zip because the artifact's job is to be pasted into a bug report.
The bundle parser fails soft on purpose. Only clients/session installs the ring layer today — the Apple, Android and webOS legs are still open and will land with formats of their own. An unparsed line is kept verbatim as its own row and is exempt from the level filter, since a fail-soft row hiding behind any filter above DEBUG is the one outcome that defeats the point. Bundles go into the export verbatim for the same reason.
Structure
rows.ts is new and pure (parser + merge). useLogSources.ts lifts the host poll up to the page, because the export reads it too and a second copy of the hook would mean a second cursor racing the first over the same ring. Messages dropped: logs_source_all, client_logs_title, client_logs_subtitle.
The second commit
style(capture): reflows two assert_eq! calls in cursor_poll.rs that arrived with #270. cargo fmt --all --check — which ci.yml gates on — had been failing on main ever since, and the repo's own pre-commit hook runs the same check, so it was blocking every commit in every worktree including ones with no Rust at all. Whitespace only.
Verification
bun run build + bun run lint (tsc) + bun test server/ green, re-run after rebasing onto 455f83eb.
cargo fmt --all --check green (both workspace gates the hook enforces).
Biome clean apart from the pre-existing useExhaustiveDependencies on the follow-mode trigger, which main already carries and the code documents as do-not-fix.
Both themes shot through the Storybook harness. Light needs &globals=theme:light and the DOM was probed to prove the flip rather than trusting the Playwright context — that pass caught the device tag reading too light on white, now violet-600 dark:violet-400.
Parser edge cases exercised directly (18 checks): header backfill, a bundle that parses nothing, continuation lines, blank input, key uniqueness across merged sources. rows.ts is pure and test-ready, but the console has no browser-side test runner (bun test covers server/ only), so these ran as a one-off rather than landing as a suite.
Not verified on glass against a real host — no paired device uploaded a bundle in this pass; the merged view was exercised through fixtures shaped exactly like the ring layer's output, header line included.
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, which is where this started, would have put a **Host** tab directly above a **Host** filter.
## The finding that unlocked it
A client bundle is not a foreign artifact. `clients/session/src/ring_layer.rs` writes every line as `<ISO8601-Z> <LEVEL> <target> <msg>` — the same four fields as a host `LogEntry`, only serialized as text — and `wallclock()`'s own doc comment says it is wall time *"so a bundle correlates with the host log it lands next to"*.
So parsing a bundle back into rows **recovers structure the client already had**. It belongs in the viewer, not in a table beside it.
## What changed
The source control becomes multi-select **chips** over one merged, wall-clock-sorted pane, with devices as peers of Host and Plugins:
```
Sources [Host] [Plugins] [couch-deck 12:04]
17:06:43.000 INFO punktfunk_host::punktfunk1 session start mode=1920x1080@60
17:06:43.000 [couch-deck] punktfunk-session 0.4.2 (linux x86_64)
17:06:43.000 INFO [couch-deck] punktfunk_session::stream connected host=skynet
17:06:44.500 WARN punktfunk_host::inject gamepad driver not attached…
17:06:44.500 WARN [couch-deck] punktfunk_session::audio egress late=31% — link stalled
```
The old strip could only ever isolate *one* producer, so 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. Device rows are tagged `[name]`; host rows are not, because absence reads as "this host" and tagging every line would double the noise in the common case.
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 naming the client-side action.
- **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 health checks were in neither. **Export all** in the page heading now assembles checks + host/plugin log + every stored bundle into one file, deliberately kept apart from the toolbar's own download ("what I am looking at").
## Two calls worth reviewing
**The combined export is plain text, assembled in the browser** — and the browser is the correct layer, not a shortcut. The console holds *more* host log than the host does: the ring is `CAPACITY = 4096` (`log_capture.rs`) while an open console accumulates `KEEP = 5_000` and keeps lines the ring has already evicted. A host-side endpoint would quietly ship *less* history than the page it was launched from, on top of costing a new authenticated route, an OpenAPI regen in both checked-in copies, and a row in the mgmt lane matrix. Text rather than a zip because the artifact's job is to be pasted into a bug report.
**The bundle parser fails soft on purpose.** Only `clients/session` installs the ring layer today — the Apple, Android and webOS legs are still open and will land with formats of their own. An unparsed line is kept verbatim as its own row *and* is exempt from the level filter, since a fail-soft row hiding behind any filter above DEBUG is the one outcome that defeats the point. Bundles go into the export verbatim for the same reason.
## Structure
`rows.ts` is new and pure (parser + merge). `useLogSources.ts` lifts the host poll up to the page, because the export reads it too and a second copy of the hook would mean a second cursor racing the first over the same ring. Messages dropped: `logs_source_all`, `client_logs_title`, `client_logs_subtitle`.
## The second commit
`style(capture):` reflows two `assert_eq!` calls in `cursor_poll.rs` that arrived with #270. `cargo fmt --all --check` — which `ci.yml` gates on — had been failing on main ever since, and the repo's own pre-commit hook runs the same check, so it was blocking every commit in every worktree including ones with no Rust at all. Whitespace only.
## Verification
- `bun run build` + `bun run lint` (tsc) + `bun test server/` green, re-run after rebasing onto `455f83eb`.
- `cargo fmt --all --check` green (both workspace gates the hook enforces).
- Biome clean apart from the pre-existing `useExhaustiveDependencies` on the follow-mode trigger, which main already carries and the code documents as do-not-fix.
- **Both themes shot** through the Storybook harness. Light needs `&globals=theme:light` and the DOM was probed to prove the flip rather than trusting the Playwright context — that pass caught the device tag reading too light on white, now `violet-600 dark:violet-400`.
- Parser edge cases exercised directly (18 checks): header backfill, a bundle that parses nothing, continuation lines, blank input, key uniqueness across merged sources. `rows.ts` is pure and test-ready, but the console has no browser-side test runner (`bun test` covers `server/` only), so these ran as a one-off rather than landing as a suite.
Not verified on glass against a real host — no paired device uploaded a bundle in this pass; the merged view was exercised through fixtures shaped exactly like the ring layer's output, header line included.
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.
`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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The Logs page had two axes fighting each other.
All | Host | Pluginswas 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, which is where this started, would have put a Host tab directly above a Host filter.The finding that unlocked it
A client bundle is not a foreign artifact.
clients/session/src/ring_layer.rswrites every line as<ISO8601-Z> <LEVEL> <target> <msg>— the same four fields as a hostLogEntry, only serialized as text — andwallclock()'s own doc comment says it is wall time "so a bundle correlates with the host log it lands next to".So parsing a bundle back into rows recovers structure the client already had. It belongs in the viewer, not in a table beside it.
What changed
The source control becomes multi-select chips over one merged, wall-clock-sorted pane, with devices as peers of Host and Plugins:
The old strip could only ever isolate one producer, so 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. Device rows are tagged
[name]; host rows are not, because absence reads as "this host" and tagging every line would double the noise in the common case.Three problems this also closes:
nullwhen 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 naming the client-side action.Two calls worth reviewing
The combined export is plain text, assembled in the browser — and the browser is the correct layer, not a shortcut. The console holds more host log than the host does: the ring is
CAPACITY = 4096(log_capture.rs) while an open console accumulatesKEEP = 5_000and keeps lines the ring has already evicted. A host-side endpoint would quietly ship less history than the page it was launched from, on top of costing a new authenticated route, an OpenAPI regen in both checked-in copies, and a row in the mgmt lane matrix. Text rather than a zip because the artifact's job is to be pasted into a bug report.The bundle parser fails soft on purpose. Only
clients/sessioninstalls the ring layer today — the Apple, Android and webOS legs are still open and will land with formats of their own. An unparsed line is kept verbatim as its own row and is exempt from the level filter, since a fail-soft row hiding behind any filter above DEBUG is the one outcome that defeats the point. Bundles go into the export verbatim for the same reason.Structure
rows.tsis new and pure (parser + merge).useLogSources.tslifts the host poll up to the page, because the export reads it too and a second copy of the hook would mean a second cursor racing the first over the same ring. Messages dropped:logs_source_all,client_logs_title,client_logs_subtitle.The second commit
style(capture):reflows twoassert_eq!calls incursor_poll.rsthat arrived with #270.cargo fmt --all --check— whichci.ymlgates on — had been failing on main ever since, and the repo's own pre-commit hook runs the same check, so it was blocking every commit in every worktree including ones with no Rust at all. Whitespace only.Verification
bun run build+bun run lint(tsc) +bun test server/green, re-run after rebasing onto455f83eb.cargo fmt --all --checkgreen (both workspace gates the hook enforces).useExhaustiveDependencieson the follow-mode trigger, which main already carries and the code documents as do-not-fix.&globals=theme:lightand the DOM was probed to prove the flip rather than trusting the Playwright context — that pass caught the device tag reading too light on white, nowviolet-600 dark:violet-400.rows.tsis pure and test-ready, but the console has no browser-side test runner (bun testcoversserver/only), so these ran as a one-off rather than landing as a suite.Not verified on glass against a real host — no paired device uploaded a bundle in this pass; the merged view was exercised through fixtures shaped exactly like the ring layer's output, header line included.