Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Checklist
go build ./...,go vet ./..., andgo test ./...passgofmt -l .is cleanGPL-3.0-or-later)