Owner feature order (sneak, 2026-09-21, chat, verbatim): "we need another issue for adding a couple lines about starlink status (if starlink is detected as the non-vpn gateway)."
Definition of done:
When the non-VPN gateway interface's upstream is detected to be a Starlink dish (the standard dish address reachable on the local network, e.g. the dish's well-known endpoint), the UI shows a couple of extra lines with Starlink status (e.g. dish state/obstruction/quality as available from the dish's local status endpoint — implementer proposes the exact fields on this issue before building, kept to a couple of lines per the order).
When no Starlink is detected, nothing changes: no extra lines, no probing noise in the UI.
Detection and the status fetch are behind a mockable boundary; unit tests need no dish and no live network.
README updated with one short paragraph on the Starlink lines and how detection works.
Ordering: after #1 (standards) and #2 (platform/interface detection — this builds on its gateway detection).
model: claude-fable-5
Owner feature order (sneak, 2026-09-21, chat, verbatim): "we need another issue for adding a couple lines about starlink status (if starlink is detected as the non-vpn gateway)."
Definition of done:
- When the non-VPN gateway interface's upstream is detected to be a Starlink dish (the standard dish address reachable on the local network, e.g. the dish's well-known endpoint), the UI shows a couple of extra lines with Starlink status (e.g. dish state/obstruction/quality as available from the dish's local status endpoint — implementer proposes the exact fields on this issue before building, kept to a couple of lines per the order).
- When no Starlink is detected, nothing changes: no extra lines, no probing noise in the UI.
- Detection and the status fetch are behind a mockable boundary; unit tests need no dish and no live network.
- README updated with one short paragraph on the Starlink lines and how detection works.
Ordering: after https://git.eeqj.de/sneak/rtnetmon/issues/1 (standards) and https://git.eeqj.de/sneak/rtnetmon/issues/2 (platform/interface detection — this builds on its gateway detection).
model: claude-fable-5
Plan (implementer's brief). One unit, one PR based on next, after #2 lands (it introduces internal/netdetect, whose Select returns the panes; the non-VPN gateway is the physical default-route pane: pane B when there are two panes, the only pane when there is one).
Detection. A Starlink dish always answers at the fixed address 192.168.100.1 from behind its router, on port 9200 (the dish's gRPC status service). Starlink is "the non-VPN gateway" when a TCP connect to 192.168.100.1:9200, bound to the physical pane's interface the same way the TCP probes already bind, succeeds. Probe once at startup and then every minute; when it has never succeeded, nothing is drawn and nothing else is probed (no noise). Not a default-route/gateway-address check: the dish sits behind the Starlink router, so the default gateway is the router, not the dish.
Status fetch. The dish's get_status call on 192.168.100.1:9200. Proposed fields, kept to two lines under the physical pane, the implementer confirms the exact set on this issue before building:
line 1: Starlink: <state> uptime <d h m> obstruction <x.x%> alerts <n>
line 2: pop ping <ms> / drop <x.x%> down <Mbps> / up <Mbps>
Refreshed every 5 s, drawn in the pane's colors: red when state is not connected or alerts are present, otherwise default.
Mockable boundary. One small interface with two methods: Probe(ctx) bool and Status(ctx) (Status, error), where Status is a plain struct of the fields above. The real implementation is the only place that knows gRPC and the address; the loop and the rendering take the interface and are unit-tested with a fake. Rendering is a pure function from Status to the two strings.
Dependency decision, made by the implementer and stated in the PR body: the dish speaks gRPC without a published API contract. Two options: (a) vendor the generated Go bindings for get_status only under internal/starlink/pb from a community source, citing the source and the dish firmware they match; (b) a reflection-based dynamic gRPC client. Recommendation: (a), smaller and plain to read; check ~/.claude/GO_PACKAGE_DEFAULTS.md before adding google.golang.org/grpc, and record the decision there if it is new.
Not executable on this Linux host, to be disclosed: any call against a real dish. Tests use the fake only.
README. One short paragraph: the two lines, and that they appear only when the dish answers at 192.168.100.1:9200 over the non-VPN interface.
Model: fable-5-1
Plan (implementer's brief). One unit, one PR based on `next`, after https://git.eeqj.de/sneak/rtnetmon/issues/2 lands (it introduces `internal/netdetect`, whose `Select` returns the panes; the non-VPN gateway is the physical default-route pane: pane B when there are two panes, the only pane when there is one).
**Detection.** A Starlink dish always answers at the fixed address `192.168.100.1` from behind its router, on port `9200` (the dish's gRPC status service). Starlink is "the non-VPN gateway" when a TCP connect to `192.168.100.1:9200`, bound to the physical pane's interface the same way the TCP probes already bind, succeeds. Probe once at startup and then every minute; when it has never succeeded, nothing is drawn and nothing else is probed (no noise). Not a default-route/gateway-address check: the dish sits behind the Starlink router, so the default gateway is the router, not the dish.
**Status fetch.** The dish's `get_status` call on `192.168.100.1:9200`. Proposed fields, kept to two lines under the physical pane, the implementer confirms the exact set on this issue before building:
- line 1: `Starlink: <state> uptime <d h m> obstruction <x.x%> alerts <n>`
- line 2: ` pop ping <ms> / drop <x.x%> down <Mbps> / up <Mbps>`
Refreshed every 5 s, drawn in the pane's colors: red when state is not connected or alerts are present, otherwise default.
**Mockable boundary.** One small interface with two methods: `Probe(ctx) bool` and `Status(ctx) (Status, error)`, where `Status` is a plain struct of the fields above. The real implementation is the only place that knows gRPC and the address; the loop and the rendering take the interface and are unit-tested with a fake. Rendering is a pure function from `Status` to the two strings.
**Dependency decision, made by the implementer and stated in the PR body:** the dish speaks gRPC without a published API contract. Two options: (a) vendor the generated Go bindings for `get_status` only under `internal/starlink/pb` from a community source, citing the source and the dish firmware they match; (b) a reflection-based dynamic gRPC client. Recommendation: (a), smaller and plain to read; check `~/.claude/GO_PACKAGE_DEFAULTS.md` before adding `google.golang.org/grpc`, and record the decision there if it is new.
**Not executable on this Linux host, to be disclosed:** any call against a real dish. Tests use the fake only.
**README.** One short paragraph: the two lines, and that they appear only when the dish answers at `192.168.100.1:9200` over the non-VPN interface.
Model: fable-5-1
Confirming the two lines before building (per the plan). Exact fields, drawn under the physical (non-VPN gateway) pane:
Line 1: Starlink: <state> uptime <d h m> obstruction <x.x%> alerts <n>
Line 2: pop ping <ms> / drop <x.x%> down <Mbps> / up <Mbps>
Source fields from the dish get_status response:
state — dish state (CONNECTED / SEARCHING / BOOTING / UNKNOWN)
uptime — device uptime, shown as days/hours/minutes
obstruction — fraction obstructed, shown as a percentage
alerts — count of active dish alert flags
pop ping — pop-ping latency in ms
drop — pop-ping drop rate, shown as a percentage
down / up — downlink / uplink throughput, shown in Mbps
The lines turn red when the state is not CONNECTED or any alert is active; otherwise the pane's default color. When no dish answers, neither line is drawn.
Model: opus-4-8
Confirming the two lines before building (per the plan). Exact fields, drawn under the physical (non-VPN gateway) pane:
Line 1: `Starlink: <state> uptime <d h m> obstruction <x.x%> alerts <n>`
Line 2: ` pop ping <ms> / drop <x.x%> down <Mbps> / up <Mbps>`
Source fields from the dish `get_status` response:
- state — dish state (CONNECTED / SEARCHING / BOOTING / UNKNOWN)
- uptime — device uptime, shown as days/hours/minutes
- obstruction — fraction obstructed, shown as a percentage
- alerts — count of active dish alert flags
- pop ping — pop-ping latency in ms
- drop — pop-ping drop rate, shown as a percentage
- down / up — downlink / uplink throughput, shown in Mbps
The lines turn red when the state is not CONNECTED or any alert is active; otherwise the pane's default color. When no dish answers, neither line is drawn.
Model: opus-4-8
Two Starlink lines appear under the physical (non-VPN gateway) pane when a dish answers at 192.168.100.1:9200 over that interface: state, uptime, obstruction and alert count; then pop-ping latency and drop rate with downlink/uplink throughput. Red when the dish is not connected or an alert is active. It probes once a minute until a dish answers, then reads status every 5s; otherwise nothing is drawn and nothing is fetched. Detection and the status fetch sit behind a small interface and are tested with a fake — no dish, no network. Field layout is the one confirmed above.
The gRPC dependency decision and the disclosures (nothing run against a real dish; one gosec suppression) are in the PR body.
Model: opus-4-8
Built on https://git.eeqj.de/sneak/rtnetmon/pulls/7 (base `next`).
Two Starlink lines appear under the physical (non-VPN gateway) pane when a dish answers at `192.168.100.1:9200` over that interface: state, uptime, obstruction and alert count; then pop-ping latency and drop rate with downlink/uplink throughput. Red when the dish is not connected or an alert is active. It probes once a minute until a dish answers, then reads status every 5s; otherwise nothing is drawn and nothing is fetched. Detection and the status fetch sit behind a small interface and are tested with a fake — no dish, no network. Field layout is the one confirmed above.
The gRPC dependency decision and the disclosures (nothing run against a real dish; one gosec suppression) are in the PR body.
Model: opus-4-8
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Owner feature order (sneak, 2026-09-21, chat, verbatim): "we need another issue for adding a couple lines about starlink status (if starlink is detected as the non-vpn gateway)."
Definition of done:
Ordering: after #1 (standards) and #2 (platform/interface detection — this builds on its gateway detection).
model: claude-fable-5
Plan (implementer's brief). One unit, one PR based on
next, after #2 lands (it introducesinternal/netdetect, whoseSelectreturns the panes; the non-VPN gateway is the physical default-route pane: pane B when there are two panes, the only pane when there is one).Detection. A Starlink dish always answers at the fixed address
192.168.100.1from behind its router, on port9200(the dish's gRPC status service). Starlink is "the non-VPN gateway" when a TCP connect to192.168.100.1:9200, bound to the physical pane's interface the same way the TCP probes already bind, succeeds. Probe once at startup and then every minute; when it has never succeeded, nothing is drawn and nothing else is probed (no noise). Not a default-route/gateway-address check: the dish sits behind the Starlink router, so the default gateway is the router, not the dish.Status fetch. The dish's
get_statuscall on192.168.100.1:9200. Proposed fields, kept to two lines under the physical pane, the implementer confirms the exact set on this issue before building:Starlink: <state> uptime <d h m> obstruction <x.x%> alerts <n>pop ping <ms> / drop <x.x%> down <Mbps> / up <Mbps>Refreshed every 5 s, drawn in the pane's colors: red when state is not connected or alerts are present, otherwise default.
Mockable boundary. One small interface with two methods:
Probe(ctx) boolandStatus(ctx) (Status, error), whereStatusis a plain struct of the fields above. The real implementation is the only place that knows gRPC and the address; the loop and the rendering take the interface and are unit-tested with a fake. Rendering is a pure function fromStatusto the two strings.Dependency decision, made by the implementer and stated in the PR body: the dish speaks gRPC without a published API contract. Two options: (a) vendor the generated Go bindings for
get_statusonly underinternal/starlink/pbfrom a community source, citing the source and the dish firmware they match; (b) a reflection-based dynamic gRPC client. Recommendation: (a), smaller and plain to read; check~/.claude/GO_PACKAGE_DEFAULTS.mdbefore addinggoogle.golang.org/grpc, and record the decision there if it is new.Not executable on this Linux host, to be disclosed: any call against a real dish. Tests use the fake only.
README. One short paragraph: the two lines, and that they appear only when the dish answers at
192.168.100.1:9200over the non-VPN interface.Model: fable-5-1
Confirming the two lines before building (per the plan). Exact fields, drawn under the physical (non-VPN gateway) pane:
Line 1:
Starlink: <state> uptime <d h m> obstruction <x.x%> alerts <n>Line 2:
pop ping <ms> / drop <x.x%> down <Mbps> / up <Mbps>Source fields from the dish
get_statusresponse:The lines turn red when the state is not CONNECTED or any alert is active; otherwise the pane's default color. When no dish answers, neither line is drawn.
Model: opus-4-8
Built on #7 (base
next).Two Starlink lines appear under the physical (non-VPN gateway) pane when a dish answers at
192.168.100.1:9200over that interface: state, uptime, obstruction and alert count; then pop-ping latency and drop rate with downlink/uplink throughput. Red when the dish is not connected or an alert is active. It probes once a minute until a dish answers, then reads status every 5s; otherwise nothing is drawn and nothing is fetched. Detection and the status fetch sit behind a small interface and are tested with a fake — no dish, no network. Field layout is the one confirmed above.The gRPC dependency decision and the disclosures (nothing run against a real dish; one gosec suppression) are in the PR body.
Model: opus-4-8