Skip to content

Engineering pass and UI redesign, merged and reviewed - #22

Merged
MrBeldum merged 55 commits into
mainfrom
integration/quality-design
Sep 30, 2026
Merged

MrBeldum merged 55 commits into
mainfrom
integration/quality-design

Conversation

@MrBeldum

@MrBeldum MrBeldum commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Lands #20 (engineering pass) and #21 (UI redesign) together, with the conflicts resolved and the fixes from a review of the combined change. See those two PRs for the full detail of each branch.

Merge

  • Both branches reworked idle redraws. The redesign's version is kept: no timer repaint, and the host poller tracks what changed. The engineering pass's change stays too: discovery redraws only when the machine list changes.
  • The engineering side dropped path::effective's unused Path argument, and the redesign's calls are updated to match.
  • Live::quality_label had no caller left and is removed.

Design follow-ups (from #21)

  • Every menu, dropdown and panel fits the window at 640×420. It opens on the side with room, is capped to that room, and scrolls inside. Tab scrolls the focused control into view. Tests check each one's rectangle against the window.
  • Notice actions line up with the notice's text.

Review of the combined change

  • Pairing's "isn't answering" message and the Machines row after setup appear without waiting for the mouse. The redesign had relied on the 3 s discovery repaint, which the engineering pass removed.
  • A pushed host update that the PC rolls back is logged on the PC, and the Mac warns once instead of re-sending 20 MB every 3 minutes.
  • The host poller no longer redraws every second once a wake packet has arrived. The Sharing page keeps that clock current itself.
  • Windows and Linux Settings say where their updates come from instead of showing a Check now button that did nothing there.
  • A stopped service reads "Not shared" on the Machines row, not "Starting…".
  • Key-expiry urgency comes from the day count, not from parsing the sentence.
  • A tagged host (the relay VPS) answers only people in its own tailnet, not other tagged machines. Tailscale's shared tagged-devices ID proves nothing. Checked against this tailnet: CurrentTailnet.Name is the Mac's login, so the VPS still answers the Mac.

Verification

  • cargo fmt --check, clippy -D warnings and cargo test --workspace pass on macOS. opus_packets_become_samples_and_the_device_pulls_them fails intermittently on this Mac when its Bluetooth headphones are the default output, and fails the same way on main.
  • 166 headless snapshots rendered and reviewed.
  • CI covers Linux, macOS and Windows.

🤖 Generated with Claude Code

MrBeldum and others added 30 commits September 30, 2026 01:01
The core config tests saved and deleted files in the user's real
~/Library/Application Support/dev.brolink.BroLink, and every test that
built a Service, a HostApp or a ClientApp read that install's host.toml
and client.toml (and would have rewritten them had a test changed a
setting). data_dir() now honours BROLINK_DATA_DIR, and the workspace's
.cargo/config.toml sets it to target/brolink-data for cargo test and
cargo run. A shipped binary never sees the variable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
a2f2ebe loosened the rule from "whois says the same user" to "whois
knows the peer at all", so the relay VPS (a tagged node, whose own user
is tagged-devices) would answer the Mac. That also admitted every machine
shared in from another tailnet: it could pair with Sunshine through
/v1/pin, sleep or restart the PC, read and write its clipboard, and POST
/v1/update with any executable whose SHA-256 it sent alongside.

The service now admits the account it is signed in as. A tagged node has
no account, so it admits members of its own tailnet: the login that names
a personal tailnet, or the users of an organisation's domain. Refusals are
cached like approvals, and logged as before 3.x.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The viewer tells a new copy on the host from an old one by the
clipboard's sequence number, and only Windows has one: elsewhere read()
reported 0 every time, so after the first poll no copy on a Mac or on the
Linux VPS desktop ever reached the viewer. The sequence number is now
derived from the text itself.

The host also opened and dropped an arboard::Clipboard for every read
and write. On X11 the selection belongs to that handle, and dropping the
last one discards the text unless a clipboard manager takes it within
100 ms, so text pasted in from the viewer could be gone before the
desktop pasted it. One handle now lives for the whole process.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
apply() only rolled back when Windows refused to create the new process
at all. A replacement that started and then crashed (panic = "abort", a
missing DLL) left the PC with the new file in place and no service: the
old one exits right after apply() returns, and nothing starts the new one
again until the next logon, which on an unattended PC is never.

The new copy waits for the old one's port, so a healthy one is still
running a moment after launch. apply() now waits two seconds and, if the
child has already exited, moves it aside and restores the old executable
while the old service keeps serving.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
http::request, request_with and the GitHub fetch all connected to the
first address a name resolved to and gave up if it failed. GitHub's
download hosts resolve to several addresses, and a network with an IPv6
address but no IPv6 route lists an unusable one first. A shared connect()
now walks the list. request() also goes through request_with() instead of
duplicating it, and takes its Host header from the connected peer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
build.rs wrote "MIT license" into brolink-host.exe's LegalCopyright
string. BroLink is GPL-3.0-or-later, as is the moonlight-common-c it
links, and that string is what Explorer, Task Manager and installers
show.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Service::status() asked machine_name() for the name on every request,
and off Windows that runs `scutil --get ComputerName` or `hostname`. The
window polls /v1/status every second and every other machine's lobby
every three, so the service spawned a process one to two times a second
for a name that almost never changes. It is now read at start and then
once a minute with the other slow probes. status() went from 3.4 ms to
0.65 µs a call on this Mac (200 calls, test build).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
decode() hands VideoToolbox a pointer to a Decoded on its own stack as
the frame refcon, and the output callback writes the picture through it.
That is only sound while every callback for the frame runs before
decode() returns. Synchronous decode does call it before returning, but
a decoder is allowed to hold a frame back and emit it during a later
call, which would write through a stack frame that no longer exists.
decode() now calls VTDecompressionSessionWaitForAsynchronousFrames after
a successful submit, as FFmpeg's VideoToolbox hwaccel does.

Also drops what was never used: VideoToolbox::full_range (always false),
the width and height every decoder constructor ignored, and OpenH264's
intermediate chroma buffer, which cost a copy of the UV plane per frame
on Windows and Linux. nal_units and interleave_uv are private to the
platform that uses them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
While Sunshine reports no audio device, the service calls
take_over_engine() on every sixth tick, and install_helpers() rewrote
take-over-engine.ps1, .vbs and .cmd and ran `schtasks /Create /F` each
time. The files are now written only when they differ from what this
build carries, and the logon task is registered once per process.

brand.rs also read VerQueryValue's UTF-16 strings and translation pair
by turning a pointer into a Vec<u8> into a &[u16], which assumes an
alignment a byte buffer does not promise. They are read unaligned now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
connect() retried server_info() eight times, 700 ms apart and up to 6 s
each, without looking at the cancel flag, so a Cancel while the PC was
not answering waited out the whole retry. retry() now checks the attempt
between tries.

The Live stream's codec label said HEVC whenever the PC offered it and
the setting allowed it, but Windows and Linux mask HEVC out of the
formats they ask for, so they streamed H.264 under an HEVC label. The
choice now also asks whether this machine decodes HEVC.

Removes what never had an effect: stream::Settings::remote (every stream
is flagged local, see the packet-size note in session.rs), with its
BROLINK_TEST_REMOTE hook, lan_like_stream() and
tailscale::overlay_or_lan(), and fixes two doc comments that sat on the
wrong items.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Session's own mouse, key, text and scroll methods duplicated Input's;
  every caller goes through Session::input().
- tailscale::Status::windows_peers, superseded by machine_peers since
  every OS can share.
- http::serve (serve_with_peer_check is the one the service uses) and
  nvhttp::Client::pair (the app pairs through pair_cancellable) are now
  test-only helpers.
- ffi declarations for constants and moonlight functions BroLink never
  uses (STREAM_CFG_REMOTE/AUTO, COLOR_RANGE_FULL, the AVC reference
  invalidation flag, CONN_STATUS_OKAY, LiGetPendingVideoFrames,
  LiRequestIdrFrame), and the module-wide allow(dead_code) that hid them.
- audio::Player's decoded counter and its packets()/decoded() accessors,
  which only the tests read; the test checks the queue instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
path::effective took the measured path and ignored it: since 3.3 Auto
asks for the same quality whatever the route. The parameter made every
caller build a Path it did not need, and the test looped over paths that
could not change the answer. Also removes a doc line left over from a
deleted function in stream.rs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Wake-on-LAN step printed `Step "Enabling Wake-on-LAN on '<name>'"`.
The name was escaped for a single-quoted literal, but this string is
double-quoted: a `"` in the adapter's name ended it, a syntax error that
fails the entire elevated script, and `$x` or `$(...)` in the name was
expanded or run. Both names are now assigned once as single-quoted
literals and used through variables.

Also drops a second, undocumented BROLINK_DUMP_SETUP hook that did what
BROLINK_DUMP_SETUP_SCRIPT does, fixes two test helpers whose doc
comments were swapped, and a stale task reference in migrate.rs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Windows setup script is generated, so a quoting slip in Rust shows
up only on a real PC. The Windows job now dumps it for every plan shape
(fresh install, migration, each with and without dry run), with an exe
path and adapter names that need quoting, and runs it, install-host.ps1
and take-over-engine.ps1 through Windows PowerShell's own parser.

The macOS and Linux jobs and the release workflow listed four crates by
name and so never ran brolink-ui's tests; they use --workspace now. The
Linux job and the node Dockerfile stop installing libssl-dev and
libpulse-dev, which nothing links (there is no openssl or libpulse crate
in Cargo.lock).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
build.rs globbed every .c file under the vendored src/ and enet/, so it
also built four files nothing links: ConnectionTester.c and SimpleStun.c
(public helpers BroLink never calls), RecorderCallbacks.c (only for
LC_DEBUG_RECORD_MODE) and ENet's compress.c (moonlight never enables
compression). They are gone, and build.rs lists the sources it compiles,
so a re-vendor has to decide about each new file.

VERSION now records the upstream commits of moonlight-common-c and its
ENet and nanors submodules, everything trimmed, and the two local
patches with their reasons (the ENet control-channel timeout for relayed
streams, and the 150 Mbps bitrate latch), plus how to re-vendor.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
brolink-stream declared serde but serialises nothing; its only JSON
lives in brolink-core.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The thread that owns the cpal stream woke every 50 ms to look at a stop
flag for the whole of every session, and kept the device open up to
50 ms after the player was dropped. It now blocks on a channel whose
sender the player holds, so it wakes exactly once, at the end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The lobby asked egui for a new frame every 400 ms, and the unified app's
service poller asked for another after every one-second poll, whether or
not anything had changed. Workers already repaint when discovery, a
connection attempt or an update moves, so the lobby's own timer only has
to age notices: it is now one second, and the poller repaints only when
the status, error, paired clients or gamepad state it read differ from
before.

Measured on this Mac with the release viewer (brolink-client, lobby
open and idle, 60 s after a 12 s warm-up): 0.58 s of CPU at 400 ms,
0.39 s at 1 s, from about 1.0% to 0.65% of a core.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ROADMAP still listed clipboard sync under Later; it shipped in 3.1, and
3.1 to 4.0 had no section at all. README called the stream "BroLink's
stream protocol" (it is GameStream, through moonlight-common-c and
Sunshine), left out deploy/native, and now states the control API's
account rule including tagged servers. MACOS.md still described a
Windows-only machine list; WINDOWS.md's route table lacked /v1/display.
CONTRIBUTING explains BROLINK_DATA_DIR, where the vendored library is
described, and the stream_real variables for streaming from another
machine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
install-macos.sh and install-macos-release.sh replaced
/Applications/BroLink.app but left the launchd job running the old
binary: replacing a bundle does not stop a process, and the agent's
KeepAlive restarts only after a failure. The Mac's sharing service then
answered on the previous version until the next login, and every manual
install needed a `launchctl kickstart`. Both scripts now kickstart the
agent when launchd has it loaded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every decoded frame is checked for being entirely black (the stream's
black-picture diagnosis). The check walked the planes byte by byte with
a short-circuiting all(), which cannot be vectorised, so a frame that
really was black, the case the check exists for, cost the decoder thread
2.5 ms at 3024x1964: 15% of a core at 60 fps, on the latency path. Each
row is now reduced to its maximum (and minimum, for chroma), which the
compiler vectorises, still stopping at the first row with a visible
pixel.

Release build, 3024x1964 NV12, 200 runs: a black frame 2.47 ms -> 143 µs,
a frame with a picture near the top 8.1 µs -> 0.34 µs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The unified window renders the 64-pixel logo for the client's header,
again for the host panel's, and on Windows and Linux once more for the
window icon, all on the UI thread before the first frame. Each render is
a Lanczos resize of the 1024-pixel logo, about 6.6 ms in a release
build. Rendered sizes are now kept, so every render after the first of a
size costs a 16 KB copy (3.5 µs).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bundle-macos.sh fell back to the viewer-only brolink-client binary when
no brolink-host was built, which produced a BroLink.app that could not
share its machine. Nothing builds the bundle that way; it now asks for
the host build instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The window read as three apps glued together: violet pills and cards in
the viewer, a host panel the unified window never showed, and egui's own
menus in the stream toolbar. Every screen now takes its colours, type,
spacing, radii and elevation from crates/ui, in the product site's
black, white and hairlines, set in Geist and Geist Mono.

- crates/ui is a real design system: surface levels, three text
  strengths, one primary (white), status tones, the logo violet only for
  focus and progress, a 4-point spacing scale, a type scale, three
  radii, two elevations. Buttons come in five kinds; menus, tabs,
  switches, sliders, notices, tags and keycaps are drawn here, report
  themselves to accesskit and show a focus ring. Contrast is tested.
- The unified window has tabs: Machines, Sharing and Settings. Sharing
  is the host's page (setup, status, paired devices, options, log),
  which the unified window used to leave unreachable.
- The machine list, connection progress, failures, empty and Tailscale
  states, Settings and the stream overlay are rebuilt on it, with
  shorter, honest copy that names the right kind of machine.
- The idle window no longer repaints on a timer, the Tailscale probe
  and the host's network calls are off the UI thread, and the host
  poller repaints only when something changed.
- The snapshot harness builds every screen headless with default
  settings (it used to read the real client.toml) and renders each
  state at 640x420, 1280x800 and 1920x1200, at 1x and 2x; unignored
  tests check every state fits with no overlapping controls.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…etup

- The 100.64.0.0/10 test lived in core's wake.rs and again in the host
  service, with the IPv6 variant only in the service. Both are now
  tailscale::is_tailnet and is_tailnet_ip.
- wake.rs and virtual_display.rs each carried the same powershell()
  runner; setup.rs, with the other PowerShell helpers, has the one.
- brolink-host and the development viewer each built their own window
  options, and only the viewer's explained the vsync-off choice they
  share. brolink_client::native_options() is used by both, and the host
  no longer depends on egui and egui-wgpu directly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
body, titled_card, toggle and steps had no callers after the redesign,
and card_frame, focus_ring and paint_icon are only used inside the kit,
so they are private now. A component that nothing draws is one more
thing to keep consistent for no gain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every install is a viewer now, but the connection errors and the path
explanations still said "this Mac", and told people to "open BroLink
Host and run setup", a window and a button that no longer exist. They
now say "this machine" and point at the Sharing tab, and each error
ends with what to do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since 4.0 the unified window runs on every OS, and with it
update::spawn. The updater assumes a Mac throughout: it swaps an app
bundle and pulls brolink-host.exe out of the Windows zip with
/usr/bin/unzip. On a Windows PC with another Windows PC in the tailnet it
would fetch the release and then fail to push, again every three
minutes, racing the Mac that does the same job. Off macOS spawn() now
only says where updates come from.

Also corrects the module docs that still called the repository private.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
egui focuses widgets in the order they are made, and a right-to-left
layout makes the rightmost first, so Tab went to a machine's menu before
its Connect button, and through the stream toolbar from Disconnect
backwards. Controls on the right are now laid out left to right and
pushed against the edge, measured on the previous pass (a changed width
discards the pass, so nothing is ever drawn misplaced).

A key that expires within 30 days now shows at the top of the machine
list, with the other things that need doing, rather than under it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MrBeldum and others added 25 commits September 30, 2026 01:38
The rebinding guard allowed loopback and Tailscale IPv4 in the Host
header while the peer check itself admits Tailscale IPv6 too, so a
client reaching the service over IPv6 was told browser access is
disabled. Both now use tailscale::is_tailnet_ip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The guides still sent people to "Set up this PC", the status pill, the
This PC card and a Settings card the unified window does not have, and
described the toolbar's old Mouse, Keys and quick-profile controls. They
now describe what is on screen: the Machines, Sharing and Settings
tabs, a machine's … menu, the toolbar and its More menu on narrow
windows, and the grouped Settings. CONTRIBUTING says what the snapshot
run renders and where new visual elements go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a stream's picture is black and the machine says its HDR desktop
is the cause, the fix was a quiet text button after "Restart stream",
which on its own cannot help, and the headline kept saying "Asking the
PC why…" after the PC had answered. The fix is now the primary button,
Restart stream is secondary, and the stale half of the headline goes
once the answer is in. The snapshot run renders this overlay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The handover server polls a non-blocking listener so it can notice a
stop. On macOS a socket accepted from a non-blocking listener inherits
O_NONBLOCK, so the timeouts set on it did nothing: a read before the
PC's request arrived failed at once, and writing the ~20 MB executable
failed with WouldBlock as soon as the send buffer filled. The PC then
got a truncated file, the script's SHA-256 check refused it, and the
install through the stream could not complete from a Mac. Accepted
connections are now switched back to blocking.

The new test writes 16 MB through a connection accepted this way; it
fails without the change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
setup::run starts `brolink-host --setup-elevated --local-app-data <dir>`
through `Start-Process -ArgumentList @(...)`. Start-Process joins that
array with spaces and quotes nothing (as its documentation says), so for
a Windows user whose profile folder contains a space the elevated
process got `--local-app-data C:\Users\Ada` plus a stray `Lovelace\...`
argument, which clap rejects, and setup failed. The folder now carries
its own double quotes, without a trailing backslash that would escape
the closing one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An iPhone or Android device on the tailnet showed "BroLink isn't
installed", which sends someone looking for an app that does not
exist. It now says phones and tablets can't share a desktop.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tailscale netcheck's preferred DERP region moves between runs on the
same network (this Mac's service.log: Paris, Chicago, Paris within an
hour), and every move logged a full network line. The window keeps the
last 80 lines, so the flips pushed out what mattered. The line is now
logged when UDP, the address families, the NAT's hardness or port
mapping change; the report itself still carries the current region.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The control service caches a whois answer for a minute and logged the
peer on every refresh. A machine polling /v1/status every few seconds
therefore wrote "hermes (…) asked" once a minute for as long as it was
up: 2,707 of 2,810 lines in this Mac's service.log, and the whole of the
80 lines the window shows. Answers are now remembered for an hour, and a
peer is logged when it is first seen, when its answer changes, or after
an hour without asking. The one-minute trust in an answer is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every problem found on 4.0.2, with its severity and what happened to
it, the tokens and components the screens are now built from, and what
is left or could not be verified without running the app.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After a successful setup the Sharing page kept a card titled "Sharing
is set up" with another Run setup again button and a note that "the
list above clears" when there was no list. It now shows one line that
other machines can connect. The power switch no longer mentions waking
on a Mac, which BroLink does not do, and Settings' update row is
labelled for what it shows.

The changelog's size figure is now measured: the macOS release binary
went from 12,167,440 to 11,689,120 bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When the Mac's default output changes during a stream, cpal 0.18's
CoreAudio backend reroutes the default-device stream itself and reports
ErrorKind::DeviceChanged through the error callback. BroLink logged that
as a warning (28 of them in this Mac's panel.log) and kept the old
device's name in Output::Playing, so the stats said "audio → WH-1000XM4"
while the sound played on AirPods. The callback now takes the new
default's name for a DeviceChanged and still warns about anything else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This Mac's panel.log has "host update attempt 1/3: connection closed"
followed by "host update for Hermes: this PC already runs BroLink Host
4.0.2": the first POST landed, Hermes answered and handed over to the
new executable, the reply was lost with the old process, and the retry
got 409 NotNewer, which the lobby showed as "Could not update Hermes".
After a dropped connection or on a retry, push_host now asks the host's
/v1/status (for up to 15 s, while it restarts) and treats a version at
or past the release as success.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A failed whois is not cached, so a peer polling every three seconds
logged the same failure twenty times a minute and emptied the window's
80-line log. The same failure is now logged once until another one
takes its place.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The black-picture report runs PowerShell with WMI queries and a screen
sample on the PC, then asks the engine's web API for its config and log
(curl, up to 8 s each). The Mac gave the whole thing 25 s and this Mac's
panel.log has three "display report: timed out", after which the notice
never says why the picture is black. The report now gets 60 s; it is
asked for once per stream, off the UI thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
stream_real now ends the app it launched (not one it resumed) before it
asserts, so a live run does not leave a Desktop session open on the PC.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since 4.0 a Windows or Linux machine is as likely as a Mac to be the one
asking, but the service still logged "the Mac sent the new BroLink
Host", "sleep requested from the Mac", "the Mac turned the HDR desktop
off" and "a Mac can only reach this PC through a relay". Those lines
are now OS-neutral, and a power, update or display change names the
tailnet address that asked for it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The discovery thread asked for a frame after every three-second scan,
and the netcheck and peer-relay probes asked for one after each of their
runs, whether or not anything had changed. The probes now only fill
their slots; the scan that picks their results up redraws the window
when the machines, their status, the relays or this machine's network
differ from the last scan. The time of the scan alone does not count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts: the idle-redraw work was done twice. The redesign's version
wins (no timer, the host poller tracks what changed); the engineering
pass's discovery change, which only redraws when the listing changes,
stays. path::effective lost its unused Path argument on the engineering
side, so the redesign's calls follow. Live::quality_label had no caller
left and is gone. The changelog keeps both lists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A pushed brolink-host.exe that dies on start is put back by the PC, but
only tracing heard about it, and the Mac, having had a 200, sent the same
20 MB again after every three-minute grace, forever. The PC now writes the
failure to the service log the window shows. The Mac remembers which PCs
took a release; one still on the old version after the grace has rolled
back, gets a warning that stays while it is true, and is not sent that
release again until BroLink restarts.

The updater also asks for a frame when it sets "updating" before
installing, and when a host notice fades, so neither waits for the mouse.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The redesign stopped redrawing on a timer and the engineering pass made
discovery redraw only on change, so anything that changed without asking
for a frame stayed on screen until the mouse moved:
- pairing's "BroLink on that machine isn't answering" (no spinner there);
- the Machines row after setup ends, which has no clock of its own.

Also:
- The host poller no longer redraws every second forever once a wake
  packet has arrived: its age counting up is not news. The Sharing page
  keeps that clock current itself, while it is shown, in s, min or h.
- Windows and Linux show where their updates come from instead of an
  updater toggle and a Check now button that did nothing there.
- A stopped service reads "Not shared" on the Machines row, not
  "Starting BroLink's background service…".
- Key-expiry urgency comes from the day count, not from reading the
  sentence back, so a machine named "Mac in office" is judged right.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A tagged host admitted any peer with its own user ID. Every tagged
machine is Tailscale's "tagged-devices" user, and nothing guarantees
that ID differs between tailnets, so a tagged machine shared in from
another tailnet could have matched. No BroLink flow has one tagged
machine call another, so the rule now fails closed: a tagged host
answers only a person's login from its own tailnet.

Checked against this tailnet: CurrentTailnet.Name is the Mac's login, so
the relay VPS still answers the Mac.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
At 640x420, the smallest window, the stream toolbar's More menu ran past
the bottom edge: "Command acts as Ctrl" and the PC's power items were cut
off, and Tab could move focus onto them without showing them. Stream
performance ran off the bottom as well, and both stream panels sat
partly under the toolbar.

Menus now choose where to open in crates/ui: below their control when
they fit, above it when there is more room there (a machine's menu near
the foot of the list), and capped at that side's room, scrolling inside
with the scroll bar showing when neither side has room for all of it.
Dropdown lists, Stream settings, Stream performance and the restart and
install questions are capped the same way, below the toolbar. A control
scrolls into view when Tab reaches it, and focus rings are no longer cut
off at a window's edge.

Tests open the More, Keys, Mouse, PC and machine menus, both panels, the
questions and a dropdown at 640x420 (the separate toolbar menus from the
narrowest width that shows them), Tab to the last item and check by rect
that it and the whole menu or panel are on screen. The snapshot run
renders each of those states at 1x and 2x.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The failed-connection card's "Dismiss" sat 12 points right of the text
above it. A quiet button's label is inset by the button's padding, and
with no fill or outline to show the box, the label is what the eye
lines up. Actions under a notice now go in an action row that pulls a
quiet button leading a line back by that padding; filled and outlined
buttons still line up by their edge.

Checking the other notices and cards turned up two more: a notice with
no actions (Tailscale isn't connected, Installing a BroLink update) kept
an empty row's height at its foot, and the setup card's "Get Tailscale"
link started at the card's edge instead of under the checklist item it
belongs to. The empty row now takes no room, and the link sits in the
item's text column.

A test checks the label of a quiet action and the edge of a filled one
against the notice's title.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@MrBeldum MrBeldum changed the title Engineering pass and UI redesign, merged Engineering pass and UI redesign, merged and reviewed Sep 30, 2026
@MrBeldum
MrBeldum marked this pull request as ready for review September 30, 2026 15:33
@MrBeldum
MrBeldum merged commit 9b57986 into main Sep 30, 2026
5 checks passed
@MrBeldum
MrBeldum deleted the integration/quality-design branch September 30, 2026 15:33
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