The header drew the host's local time and then America/Los_Angeles
time, so on a Mac in that zone both clocks read the same and neither
was UTC. The first clock is now UTC and the second the host's local
time, which also drops the hardcoded zone and its load-failure path.
A VM with NAT networking makes vmnet install a default route scoped to
its bridge (bridge100) with no gateway. The detector counted it as a
second physical default route and refused to start:
multiple physical default-route interfaces found (bridge100, en0)
Scoped routes cannot simply be dropped: while a VPN holds the default
route, the physical interface keeps only a default route scoped to
itself. That route still names its gateway (flag G). The on-link one
does not, so a physical default route now requires a next-hop gateway.
parseNetstat records a gateway only when the G flag is set; a link#N
gateway column is the interface itself, not a next hop.
The workflow lines now say who merges where: clawbot squash-merges
reviewed issue PRs into next, only sneak merges next into main, and a
deploy is a squash commit from main onto prod, never automatic.
Status lists what is on next today, each item linked to its issue: the
repo standards, interface detection on Linux and macOS, the macOS VPN
detection fix, the Starlink status lines, the version stamp, and CI
running script/cibuild. Next Step is the owner's merge of the milestone
PR and a first run on a real Mac.
Model: opus-5-5
A utun tunnel counted as the VPN whenever it was up with a routable
IPv4 address, so idle Tailscale or a tunnel left behind by a
disconnected client became the VPN pane and its probes failed. A
tunnel is now the VPN only while it carries the IPv4 default route in
netstat: an unscoped default row, or both 0/1 and 128.0/1 rows on it.
A default row scoped to a tunnel (flag I) does not count; on the
physical interface it still does, since a VPN holding the default
leaves the physical default scoped. Tests feed netstat text in the
macOS layout through the parser into Select. Not run on a real Mac.
Model: opus-5-5
The CI job image ships with empty apt package lists, so the bootstrap's
`apt-get install -y golang` could not find the package and every CI run
stopped there. On the apt path the bootstrap now runs `apt-get update`
once, before the first install, and not at all when nothing needs
installing.
Model: opus-5-5
rtnetmon had no version. main.Version is now set at link time and logged
in the first startup line. `make build` and `make dev` set it from
`git describe --tags --always` unless VERSION is given. The Dockerfile
takes it from the VERSION build argument when one is given, otherwise
from `git describe --tags --always` of the .git the build context now
carries, and fails the build if the context carries .git and no version
comes out. .dockerignore follows the canonical copy: .git is sent,
.git/config, which can hold a credential, is not.
Model: opus-5-5
Detection is a TCP connect to the dish endpoint bound to the physical
interface; once a dish answers, get_status is read every 5s and two
status lines render under the physical pane, red on disconnect or
alert. No dish: nothing drawn, no RPC. Minimal vendored proto bindings
for get_status only. Independent review passed (PR 7 comment 100012).
Model: opus-4-8 (implementation and review); model: claude-fable-5
(merge)
Linux runs exactly as before when both bridge interfaces exist. When they do
not, the lone default-route interface is monitored, and a single interface
draws a single UI pane instead of an empty second one.
macOS is newly supported: a running VPN tunnel (utun) is monitored as the
primary pane alongside the physical default-route interface, or the physical
interface alone when no VPN is up.
Detection lives in internal/netdetect: interface/route data types, pure
selection logic keyed on OS name, and route parsers, all unit-tested on Linux
for both platforms. Only the real route query and the per-platform TCP dial
binding are build-tagged. NewMonitor now takes a list of interfaces.
Not verified on a real mac: live netstat parsing, IP_BOUND_IF dialing, Mullvad leak protection.
Model: opus-4-8 (implementation); fable-5-1 (landing commit)
Standard scaffold: script/ entrypoints with the Makefile as thin shims, a Dockerfile whose lint and test phases gate the build, a Gitea CI workflow running script/cibuild, REPO_POLICIES.md, TODO.md, .editorconfig, .dockerignore, LICENSE, wider .gitignore. .golangci.yml is byte-identical to the canonical copy in the prompts repo.
211 lint findings fixed in code: package globals became functions/fields and a cobra command constructor, magic numbers became named constants, ctx is threaded into the probes, monitor loops split. Tests moved to external _test packages with export_test.go. Behavior unchanged.
Readers will trip over: make lint/test/check now need a Docker daemon; the personal rsync copy/run targets are gone and make run runs locally.
Disclosure: four //nolint:gosec remain on the fixed-argv ping/curl calls and the operator-chosen log file, as the reference repo annotates the same class.
Disclosure: module path left as-is.
Model: opus-4-8 (implementation); fable-5-1 (merge)