Skip to content

[ai][mac][docs] Visualiser: set the Gerardus server address in Settings (Tailscale) - #832

Merged
ddol merged 1 commit into
mainfrom
claude/gerardus-address-setting
Oct 21, 2026
Merged

ddol merged 1 commit into
mainfrom
claude/gerardus-address-setting

Conversation

@ddol

@ddol ddol commented Oct 21, 2026

Copy link
Copy Markdown
Member

What

The visualiser's Gerardus server address can now be set in Settings (⌘,), so the app can reach Gerardus on another machine over Tailscale. Before, the address was the alignment.serviceAddress UserDefaults key with no UI. It was parsed by splitting on :, so it could not take an IPv6 literal or leave out the port, and a change needed a restart.

The server side is Gerardus's change to listen on 0.0.0.0:50061 by default (branch dd/feat/tailscale-bind-1021). Nothing here waits for it: an unset preference still means 127.0.0.1:50061.

Changes

  • GerardusAddress (gRPC/GerardusAddress.swift) parses and normalises the address in one function, GerardusAddress.parse:
    • It takes a host name or IP literal with an optional port, which defaults to 50061: 127.0.0.1, machine.tailnet.ts.net, 100.64.0.1:50061, [fd7a:115c:a1e0::1]:50061.
    • Whitespace is trimmed and names are lower-cased. IPv6 is accepted with or without brackets (brackets are needed to give a port). Ports run 1–65535.
    • URLs, paths, user@host, malformed IPv6 and dotted numbers that aren't IPv4 are refused.
  • Preference: kept as alignment.serviceAddress. Choosing the local server removes the key, so an unset preference behaves exactly as before, and a saved value that no longer parses falls back to the local server.
  • Settings window (UI/AlignmentSettingsView.swift, the app's first Settings scene):
    • the address field, with Connect and Use this Mac;
    • a status line naming the address in use and whether Gerardus answered there ("Gerardus 0.1.0 answered at …", or "No answer from …" with the gRPC reason).
  • Reconnect: AlignmentSession.useServiceAddress does the following:
  • Pane: the header names the server.
  • Dialling: GerardusClient takes the parsed address. An IPv6 literal is dialled directly, while names (MagicDNS included) and IPv4 go through the resolver.
  • Docs: docs/ui/visualiser/gerardus-alignment.md and the visualiser README.

macOS network permissions

What I found:

  • Sandbox: the app is sandboxed, and com.apple.security.network.client was already in VelocityVisualiser.entitlements, so outgoing connections to any host were allowed. No change needed.
  • Local Network privacy (macOS 15, TN3179): it covers networks on broadcast-capable interfaces (Wi-Fi and Ethernet), not VPNs.
    • Tailscale traffic runs over utun and needs no Local Network permission. Confirmed below, with no prompt.
    • A Gerardus on a LAN address does need it. I added NSLocalNetworkUsageDescription (as INFOPLIST_KEY_NSLocalNetworkUsageDescription in both configurations) so the system prompt explains why.
    • TN3179 notes the system may refuse the first attempt before the user answers. The 30 s re-probe covers that.

Tests

Added to VelocityVisualiserTests/AlignmentTests.swift:

  • GerardusAddressTests:
    • 11 accepted forms round-trip through description, and 18 refused forms give the expected error;
    • an IPv6 literal gets an IPv6 target and a name gets DNS;
    • the preference is saved, cleared when it equals the local server, and falls back when unreadable.
  • AlignmentAddressChangeTests:
    • the session starts from the saved address;
    • a change asks the new server and keeps it;
    • while connected, the loop follows the new address;
    • a late answer from the old server is ignored;
    • a change closes the pane.

make test-mac passes: 1,341 Swift Testing tests plus the XCTest suite, 0 failures. make build-mac (Release) builds clean, and the built Info.plist carries the usage string.

Verified in the running app

I ran the Release build against the running velocity.report and the Gerardus now listening on 0.0.0.0:50061. Each run passed the address as a launch argument (-alignment.serviceAddress …), which saves nothing, and I read the app's sockets after 9 s:

Address Result
this Mac's tailnet IPv4 (100.x) channel to 100.x:50061 ESTABLISHED, no Local Network prompt
this Mac's MagicDNS name resolved to the same 100.x, ESTABLISHED
192.0.2.1:50061 (unroutable) no channel; one connect attempt per 30 s, each gone within 15 s, none piling up over 70 s
127.0.0.1:1 (refused) no channel
http://nope/ (unreadable) fell back to 127.0.0.1:50061, ESTABLISHED

In every run the velocity.report stream (50051) stayed up, the app stayed alive, and no crash report was written.

Not verified: the Settings window itself, i.e. typing an address and reading the status line. This session can neither see nor click the app's windows. The same code path (useServiceAddress) is covered by the tests above.

Overlap

#831 (hint heading, open) also touches AlignmentSession.swift, AlignmentTests.swift and the hub doc, in other hunks. Whichever merges second may need a small merge.

🤖 Generated with Claude Code

The address was the UserDefaults key alignment.serviceAddress with no UI,
parsed as host:port by splitting on colons, so it could not take an IPv6
literal or omit the port, and changing it needed a restart. To reach
Gerardus over Tailscale it has to be editable.

- GerardusAddress.parse reads a host name or IP literal with an optional
  port (50061 by default): whitespace trimmed, names lower-cased, IPv6
  with or without brackets, ports 1-65535, URLs and paths refused. It is
  kept as alignment.serviceAddress; the local server (127.0.0.1:50061)
  removes the key, so an unset preference behaves as before, and a value
  that no longer parses falls back to it.
- Settings (Cmd-,) has the field, Connect and Use this Mac, and a status
  line naming the address in use and whether Gerardus answered there.
- AlignmentSession.useServiceAddress drops the old channel and anything
  placed against it, closes the pane, saves the address and asks the new
  server at once (and every 30 s while connected). A late answer from the
  old server is ignored. The pane header names the server.
- An IPv6 literal is dialled directly; names and IPv4 go through DNS.
- NSLocalNetworkUsageDescription: Local Network privacy covers Wi-Fi and
  Ethernet networks, not VPNs (TN3179), so Tailscale needs no permission
  but a LAN address does. The sandbox already has network.client.

Tests cover parsing (accepted and refused forms), persistence, the
reconnect, the stale answer and the pane closing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 10:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@ddol
ddol merged commit 7cf2118 into main Oct 21, 2026
11 of 12 checks passed
ddol added a commit that referenced this pull request Oct 22, 2026
…ses, pose scoring and Snap (Gerardus #8) (#833)

* [ai][mac] Alignment tests: two factories missed by the #831 and #832 merge

#831 added two AlignmentTests sessions with the old client factory,
`{ service }`, and #832 changed the factory to take the server address.
Each passed alone; merged, the test target on main does not compile,
and mac CI does not run on pushes to main. Both now take the address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [ai][mac][make] Alignment pane: automatic registration, hypotheses, pose scoring and Snap

Gerardus PR 8 (05559d9) made Register run real ICP and EvaluatePose
work. The pane now follows the operator loop of Gerardus's interface doc:

- Register searches 30 m around the pose drawn now at every heading
  (10-25 s, with Cancel). Each hypothesis shows as it converges; when the
  run ends they are ranked, the best is drawn, and the six gates are listed
  with their values. A run with none leaves the operator's pose.
- Picking a hypothesis draws it, and Accept records it by id:
  REGISTERED when every gate passed, else MANUALLY_PLACED. Moved off it,
  Accept records the pose as it stands (source MANUAL, or ICP if snapped).
- Once the pose rests for 0.4 s, EvaluatePose scores it with a residual per
  uploaded scan point; the scan is drawn over its background coloured by
  residual (green on the surface to red at 0.5 m, grey out of reach). A
  late score of an earlier pose is dropped. Snap is EvaluatePose(refine).
- The vendored proto moves to 05559d9: FitQuality gains facade_p75_m and
  facade_range_slope, which the pane shows.

Tests cover streaming, ranking and drawing the best, accepting by id or
by pose, failed runs, scoring on rest with residual colouring, stale
scores, Snap, cancelling, and the residual palette.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [ai][docs] Gerardus alignment pane: registration, scoring and residuals documented

The hub doc covers what now ships: Register with its hypotheses and
gates, what Accept records, scoring once the pose rests with the scan
coloured by residual, and Snap. The operator's path table gains the
hypothesis, nudge-and-score and Snap rows. The plan's pose-scoring item
is delivered; it stays Parked on tie points and the pose endpoint. The
backlog moves the item to Complete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants