Skip to content

device: TUI Logs tab + Players/Settings view fixes - #25

Merged
vynulldev merged 4 commits into
mainfrom
tui-logs
Jul 26, 2026
Merged

device: TUI Logs tab + Players/Settings view fixes#25
vynulldev merged 4 commits into
mainfrom
tui-logs

Conversation

@vynulldev

Copy link
Copy Markdown
Owner

What & why

Four commits: a new Logs tab, and three fixes found while testing it against the real DJM-A9.

Logs tab. The TUI owns the terminal, so logs go to a temp file the user has to find and tail in a second terminal. LogRing is an io.Writer keeping the last 2000 lines in a fixed ring; main tees the stdlib log output through it alongside the existing file writer, so the file remains the complete record and the ring only feeds the view. Partial writes buffer until their newline so split lines land whole, and writes never fail so the tee cannot break file logging. The tab follows the tail by default with a FOLLOWING indicator; scrolling up pauses it, scrolling back to the bottom re-attaches, G re-follows, g jumps to the oldest buffered line. Headless mode is untouched. LogRing is unit-tested for wrap-around, mid-line splits, and use as a stdlib log sink.

Settings tab scroll. The tab rendered all 25 rows unconditionally, so on any terminal shorter than the list the whole top of the screen (tab bar included) scrolled away, which read as the tab being broken; the cursor keys moved state the renderer never used. It now renders a cursor-following viewport like the Library tab, with a position indicator when clipped. Pre-existing, not a regression from the Logs work (confirmed by comparing against a main build at the same size).

Mixer strip race. The PeerTracker is created inside VirtualDevice.Start, which runs in a goroutine racing TUI construction, and main passed dev.Peers to NewTUI by value, so the mixer strip was a per-session coin flip: whichever goroutine won, the TUI either had the tracker or held nil for the whole run while /api/peers (which dereferences at request time) showed the mixer fine. NewTUI now takes a peersFn resolved at render time. peerTimeout also widens 5s to 10s: keep-alives arrive every 2s (measured on the DJM-A9), so the old window meant two consecutive lost broadcasts hid the device until the next packet.

0x30 status clobber. Newer DJMs split their broadcast state: a stripped 0x30 on the status port carries only name + device number, while 0x03 channel packets on the on-air port carry the channel state. The two listeners shared one mixerStatuses entry but only the 0x03 path merged; every 0x30 overwrote the entry wholesale, wiping ChannelStateKnown until the next 0x03 restored it, so mixer views flipped between the rich channel-state line and the bare "detected" fallback at packet cadence (TUI and /api/peers alike). A state-less 0x30 now merges over the learned fields; a rich legacy 0x29 still replaces the entry wholesale.

Verified live under tmux against the real DJM-A9: log lines stream in follow mode with pause/resume and oldest-jump behaving; the Settings tab scrolls with the header pinned; three consecutive TUI sessions all show the MIXER line immediately (previously luck-dependent). One honesty note: the 0x30 flip could not be reproduced live because the idle DJM-A9 emits no 0x30s at all (confirmed by counting packet types on the status port), so that fix rests on the code mechanism plus a 20/20-sample stability check of the merged path against the 0x03-only stream. Worth watching the mixer strip next time the full rig is up.

Hardware testing

  • Tested on: DJM-A9 (live network peer for the mixer-strip and keep-alive-cadence verification). No CDJ pass: nothing deck-facing changed — no protocol sends, load path, or analysis output; the changes are read-side (monitoring views, peer bookkeeping, log capture).

Checklist

  • go build ./..., go vet ./..., and go test ./... pass
  • gofmt -l . is clean
  • New source files carry an SPDX header (GPL-3.0-or-later)
  • Tested on real hardware (deck + firmware noted above), or this change doesn't affect deck behaviour
  • I agree my contribution is licensed under the project's GPLv3

The TUI owns the terminal, so logs go to a temp file the user has to
find and tail in a second terminal to see what the server is doing.
Add a Logs tab that shows the live stream in-place.

LogRing is an io.Writer keeping the last 2000 lines in a fixed ring;
main tees the stdlib log output through it alongside the existing file
writer (the file remains the complete record - the ring only feeds the
view). Partial writes buffer until their newline so split lines land
whole, and writes never fail so the tee cannot break file logging.

The tab follows the tail by default; scrolling up pauses it (with a
PAUSED indicator), G re-follows, g jumps to the oldest buffered line,
and scrolling back to the bottom re-attaches. Timestamps render
without the date column and lines clip to the terminal width.

Verified live under tmux: lines stream in follow mode, pause/resume
and oldest-jump behave, tab cycling wraps through the new tab, and
the ring picked up real peer traffic while browsing.
The Settings tab rendered all 25 rows unconditionally, so on any
terminal shorter than the list the whole top of the screen - tab bar
included - scrolled away, which read as the tab being broken. The
cursor state existed and the arrow keys moved it, but the renderer
never used it.

Render a viewport window that follows the cursor (same pattern as the
Library tab), with a position indicator in the header when the list is
clipped, a highlighted cursor row, and the cursor clamped to the row
count. g/G jump to the first/last row as elsewhere.
The mixer strip in the Players tab was a per-session coin flip: the
PeerTracker is created inside VirtualDevice.Start, which runs in a
goroutine racing TUI construction, and main passed dev.Peers to NewTUI
by value. Whichever goroutine won decided the whole run - the TUI
either had the tracker or held nil forever while /api/peers (which
dereferences at request time) showed the mixer fine. The db wiring a
few lines up even documents this exact trap and uses a closure; the
TUI call did not. NewTUI now takes a peersFn resolved at render time.

Also widen peerTimeout from 5s to 10s. Keep-alives arrive every 2s
(measured on a DJM-A9), so the 5s window meant two consecutive lost
broadcasts - easy on a busy link-local segment - dropped the device
from the players/mixer views until the next packet landed. 10s
tolerates four lost packets while still clearing departed devices
quickly.

Verified against the real DJM-A9: three consecutive TUI sessions all
show the MIXER line with channel state immediately; before the fix the
strip rendered only on lucky startups.
Newer DJMs split their broadcast state across two packet types: a
stripped 0x30 on the status port carrying only name + device number,
and 0x03 channel packets on the on-air port carrying the channel
state. The two listeners shared one mixerStatuses entry, but only the
0x03 path merged - the status path overwrote the entry wholesale, so
every 0x30 wiped ChannelStateKnown (and master fields) until the next
0x03 restored them. Mixer views flipped between the rich channel-state
line and the bare "detected" fallback at packet cadence, in the TUI
and /api/peers alike.

The status path now merges a state-less 0x30 over the learned fields
instead of clobbering; a rich legacy 0x29 (which carries everything
inline) still replaces the entry wholesale.

The DJM-A9 on the bench emits 0x30 only in certain states (none seen
while idle - confirmed by counting packet types on the status port),
so the flip could not be reproduced live today; the merged path was
verified stable against the 0x03-only stream, 20/20 samples rich.
@vynulldev
vynulldev merged commit e87bcb9 into main Jul 26, 2026
1 check passed
@vynulldev
vynulldev deleted the tui-logs branch July 26, 2026 19:24
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