feat: opportunistic ip tracking scans on wake and shutdown - #2
Draft
mwpastore wants to merge 6 commits into
Draft
Conversation
mwpastore
force-pushed
the
claude/upsnap-opportunistic-ip-scans
branch
2 times, most recently
from
August 10, 2026 16:43
a599a77 to
4dd91ff
Compare
Set IgnoreUnchangedFields on every device record whose flow only mutates status: the ping, wake, and shutdown crons, the wake/sleep/reboot/shutdown handlers and their group variants, and the boot-time state reset. Their full-record saves could clobber fields written concurrently by other flows, such as ip addresses updated by the tracking scans. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WakeDevice and PingDevice now take a getIp function that supplies the address to ping on each attempt; nil means the record's ip. DeviceIPFunc composes one from app and device: when ip tracking is enabled globally and for the device, each call re-reads the record from the database, so a wait loop picks up address changes written by concurrent tracking scans instead of pinging a stale ip until timeout. Wake call sites pass the tracking getter; the crons and shutdown waits pass nil, preserving their behavior. The gating check is exported as DeviceTrackingEnabled for use by other tracking call sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TrackDeviceAfterWake schedules a tracking scan of the device's subnet 15 seconds after a wake attempt starts, giving the device time to boot and renew its dhcp lease. Combined with the getIp refresh during the wake wait, a wake now recovers a moved ip within seconds instead of pinging the stale address until timeout. Applies to the wake handler, group wake, reboot, and the wake cron, and only when ip tracking is enabled globally and for the device. Concurrent scans of the same subnet now coalesce: TrackOneSubnet wraps the nmap run in a singleflight group keyed on the subnet, so a group wake or a scan racing the periodic sweep joins the in-flight run and shares its result instead of spawning another scan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shutdown handler, group shutdown, reboot, and the shutdown cron now call TrackDevice first: a synchronous scan of the device's subnet (joining any in-flight scan) followed by a re-read of the record, so the online check and the shutdown command use the device's current address instead of a stale one. The shutdown cron also no longer bails when the status column says offline: by that point it has just pinged the device successfully via its configured check, which supersedes bookkeeping that may be stale — exactly the state a device is left in after an unnoticed ip change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The frontend now passes async=true for shutdown, reboot, and sleep, so those requests return as soon as the action starts instead of blocking for the pre-action scan plus the action itself — which could outlive reverse proxy timeouts even before ip tracking existed. The device card already shows a pending countdown, and the outcome arrives as a realtime status update. If the countdown expires while the device still shows pending, the card re-syncs the record once, covering scan-extended actions and missed realtime events. Wake stays synchronous because its response drives link_open. The four action handlers now return the current record without acting when the device is already pending, so a double click (or a request racing a cron) can't start a second action on the same device. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sleep sends an http request to the device's ip, so it needs the same pre-action scan as the shutdown flows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mwpastore
force-pushed
the
claude/upsnap-opportunistic-ip-scans
branch
from
August 10, 2026 17:17
4dd91ff to
12773fd
Compare
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.
Summary
Stacked on the seriousm4x#1763 branch (
claude/upsnap-magic-packet-routing-dc13bais the base here, so the diff shows only the six new commits). Where the base PR makes ip tracking periodic, this makes it opportunistic — scans fire at the two moments a stale ip actually hurts:wake_timeout(default 120s), so a device that renewed its DHCP lease during boot woke fine physically but was reported as "not online" — and stayed misreported until the next periodic sweep. Now a tracking scan of the device's subnet fires 15s after the wake attempt starts, and the wait loop re-reads the ip from the db before every ping attempt, so a moved ip is picked up seconds after the device answers ARP and the wake succeeds within its timeout.{{ DEVICE_IP }}. Scheduled actions are the primary beneficiary: they have no UI gating, and a stale ip previously made the shutdown cron's online check silently no-op. Manual actions are only offered while the device shows online, and a stale ip usually flips it offline first — the wake path above then heals it — but "online" can lie: the old ip may have been re-leased to another host that answers pings, and a customping_cmdruns verbatim and ignores the ip field entirely, so status accuracy implies nothing about ip accuracy. The pre-action scan re-locates the mac before the action targets an address, soshutdown_cmdand sleep can't hit the wrong host.Opportunistic scans are gated the same way on both paths: the global
track_ip_intervalmust be set and the device'strack_iptoggle on.Commits
status(ping/wake/shutdown crons, all six handlers, boot-time reset) now saves withIgnoreUnchangedFields, so their full-record saves can never clobber an ip written concurrently by a tracking scan. (The ports status save is deliberately untouched.) Save baselines are refreshed (PostScan) after each intermediate pending write, so a later revert to the load-time status is never dropped as an unchanged field — pinned by a cronjobs regression test.WakeDevice/PingDevicetake agetIp func() string(nil = the record's ip);networking.DeviceIPFunc(app, device)composes the db-reading getter, and the gate is exported asnetworking.DeviceTrackingEnabled.iptracking.TrackDeviceAfterWakeschedules the +15s scan at all four wake initiation points.TrackOneSubnetnow wraps the nmap run in a singleflight group keyed on the subnet, so a group wake coalesces into one scan and opportunistic scans join an in-flight sweep instead of doubling it (golang.org/x/syncpromoted to a direct dependency). Nmap invocations are additionally serialized process-wide, since raising and restoringCAP_NET_RAWmutates process state.iptracking.TrackDevice(synchronous scan + re-read, returns the given record on gate-off or failure) at the top of the shutdown/reboot/group-shutdown flows and in the shutdown cron before its online check.async=truefor those three actions (a capability the API already had), so requests return as soon as the action starts instead of blocking for the scan plus the action — which could outlive reverse-proxy timeouts even before this PR. The card's pending countdown plus realtime updates deliver the outcome, and the countdown re-syncs the record once if it expires while still pending. The action handlers also no-op with the current record when the device is already pending, so a double click can't start a second action. Wake stays synchronous because its response driveslink_open.Behavior changes to note
status != "online". By that point it has just successfully pinged the device via its configured check (default ICMP orping_cmd); a live ping supersedes a status column that is stale in exactly the scenario this PR targets (ip moved → ping cron marked the device offline).pendingis still respected: checked on entry and re-checked after the potentially slow pre-action scan, so a wake initiated mid-scan is never raced by the shutdown.ping_cmd), and it would flap for ICMP-blocking devices between each scan and the next ping tick.Testing
go buildfor linux, windows, and darwin;go vet;gofmt— clean.-race: theiptrackingtests (gating on both toggles, the delayed post-wake scan, singleflight join proving one nmap run for two concurrent callers,TrackDevice's refresh/gate-off/unscannable paths, guard/skip/orchestration coverage), newnetworkingtests forDeviceTrackingEnabled,DeviceIPFunc, andWakeDevice's getIp consultation, and acronjobsregression test pinning that a failed scheduled wake persists its status revert. All hermetic — stubbed scanner, loopback subnets, real migrated schema viatests.NewTestApp.TestCheckPort/Invalid_IPfailure is environment-dependent and unrelated (fails on the base branch too).Not included
track_ip/track_ip_interval. Frontend changes are confined to the threeasync=truecall sites and the countdown re-sync.🤖 Generated with Claude Code