Repository navigation
refactor(server): clock URL through the runtime-settings overlay - #176
Conversation
The clock URL was the last runtime setting outside the overlay. Five places
held pieces of its precedence: a hand-written load/apply pair, deviceSource
reverse-engineering the tier from the store, deviceBaseline and
deviceAutoPicked, and a fileURLChanged special case in /admin/reload.
AWTRIX.HTTPBaseURL meant the file value in one place and the live URL in
another.
Now the tiers live in one Config value: the file baseline (HTTPBaseURL, never
overwritten at runtime), the menu override (an overlay registration), and an
in-memory discovery swap. Config.clockURL() is the only precedence rule, and
every reader asks it: clock access, doctor, the device config/discover
bodies, clock health, and the callback hosts. A guard test keeps it that way.
The overlay gains an optional encode/decode pair, so the store keeps its raw
URL format, and putWith, so a PUT naming base_url clears a discovery swap in
the same critical section while reapply doesn't. Reload carries the runtime
tiers with carryClockURL instead of rewriting the file value.
A discovery swap now lands only if the URL it probed is still effective, so
it can't overwrite a PUT that raced it.
Behaviour change: PUT {} (or base_url null) is a 200 no-op instead of a 400.
An explicit "" is still a 400.
ARCHITECTURE, STYLE and the AGENTS endpoint list still called the clock URL the overlay's exception and PUT /v1/device/config a required body.
tarakanof
left a comment
There was a problem hiding this comment.
Review (read-only). Checked against origin/main: applyDeviceBaseURL, loadPersistedDeviceBaseURL, deviceSource, rediscoverClock, StartDeviceWatch, and the reload block.
Verdict: merge after the doc fix (1). The code preserves main's effective-URL behaviour.
Precedence: what main actually does
On main the effective URL is one live field, and the last writer wins. The store pin is laid on at boot (initDeviceDiscovery → loadPersistedDeviceBaseURL). rediscoverClock probes whatever is live, whatever its source, and if it fails 2 probes it swaps in cands[0], even over a menu pin. So yes: when a pinned clock changes IP, discovery on main overrides the pin, in memory, until a PUT or a reload that changes the file URL. The store row is never touched. In tier terms that is exactly discovered > store > config, where discovered is set only when the tier below failed its probes. The PR's clockURL() matches main.
Scenarios traced, all with the same effective URL as main:
- boot pin
- pin unreachable → swap
- PUT over a swap, including re-pinning the same URL
- reload with an unchanged file URL: swap and override kept (main kept the running URL, and the overlay reapply doesn't clear the swap)
- reload with a changed file URL: swap dropped, override wins (main did the same, plus a short window that this PR removes)
- invalid stored value ignored
- boot order:
settings.reapply()(main.go:79) runs beforeinitDeviceDiscovery(:110)
Findings
- should-fix, docs. The phrase "store override > reachable config.json baseline > mDNS auto-pick" is wrong on main, and still wrong after this PR. An unreachable store override is replaced by a discovery swap too. ARCHITECTURE.md now says "discovered > store > config … so this is the documented 'store override > reachable baseline > mDNS auto-pick'", which calls the two orders the same. They aren't: the old phrase says a pin can't be beaten. It is still in AGENTS.md:121 and in the main.go:105 comment. Suggested single wording: "Effective = menu override, else config.json baseline. If that fails its probes, an in-memory mDNS swap replaces it (the pin included) until a PUT naming base_url or a reload that changes the file URL."
- nit,
sourcedifference. Pin A dies → swap to B → B dies → discovery finds A again. Main reportssource:"store", because the store value equals cur. The PR reports"discovered". The effective URL is the same, and arguably the PR is more honest. The PR says "sourcevalues unchanged", so either mention this or ignore it. - nit, empty row. A
PUT {}with no override now persistsdevice_base_url="":persistalways runs, andencodeof a nil view is"". Harmless:decode("")→{}, and main's loader skips"", so a downgrade is safe. But it's a new empty row. - nit, guard test.
TestHTTPBaseURLOnlyReadAsBaselineallowlists whole files.admin.gois large and a future stray read there would pass. Consider an allowlist of function names (e.g.handleAdminReload), or accept it as is.
Verified OK
- encode/decode: keeps the raw-URL bytes both ways, so no migration and a downgrade stays safe.
- carryClockURL: runs inside
cfgMubeforevalidateConfig/diffConfig. The unexported fields are skipped bydiffStructFields, sochanged_fieldstracks the file URL only (the stated change). - CAS in
swapDiscoveredClock: compares the effective URL under the overlay lock. It loses only to a real change. Re-pinning the identical URL still gets swapped, which is the same as main. putWith: clears the swap only whenbase_urlis named. Reapply goes throughputand never clears it.PUT {}/null→ 200: fine.""and bad schemes are still 400 viavalidDeviceURL.- All former
HTTPBaseURLreaders: converted. The remaining ones are config load/sanitize, printconfig and the reload required-field check. - Parity goldens: untouched, and the branch is up to date with main.
go test ./... -race -count=3: green (all packages).
…rows
When discovery lands back on the pinned URL, main reported source store; the swap tier now clears in that case so it still does. A PUT {} with no override no longer writes an empty device_base_url row. The HTTPBaseURL guard now allowlists functions, not whole files.
"store override > reachable baseline > mDNS auto-pick" implied a pin can't be beaten; an unreachable pin is swapped in memory until a PUT naming base_url or a file-URL-changing reload.
|
Review addressed in 5c0704e and 7e8e370:
gofmt (1.26), vet and |
Closes #175
Spec summary: #175 (comment)
What changed
The device clock URL is now a settings-overlay registration, and a single function,
Config.clockURL(), owns its precedence.Structure
Before:
After:
Discovery sets a swap only after the effective URL fails its probes. So "discovered wins" is the documented "store override > reachable baseline > mDNS auto-pick" rule, expressed as state.
Files outside the issue's own
These hunks are minimal:
config.go: two unexported fields onAWTRIXConfig.app.go: removeddeviceBaseline/deviceAutoPicked.admin.go: reload block.boot_ping.go,device_buttons.go,device_capabilities.go,clock_health_http.goanddoctor.go.clock_parity_test.go: dropped theapp.deviceBaseline = srv.URLline. The goldens are unchanged, and the harness passes as-is.Behaviour changes
PUT /v1/device/config {}(or{"base_url":null}, or unrelated keys only): 200 with no change, instead of 400. This is the merge semantics every other…/configPUT already has.{"base_url":""}and bad schemes are still 400. There is no clear, the same as before./admin/reloadchanged_fieldslistsawtrix.http_base_urlexactly when the file value changed. Before, the running URL was compared.Unchanged:
base_url, andDeviceSettingsModel.useand Discover Clocks behave the same.device_base_urlis still the raw URL, via the overlay's newencode/decode, so there is no migration and a downgrade stays safe.sourcevalues.Tests
clock_url_test.go:{}leaves the swap;changed_fieldstracks the file URL;-race;HTTPBaseURLguard test (checked that it fails on a planted read).The existing device, rediscover, hardening, reload and parity tests moved to the new interface.
Checks run locally:
gofmt -l .with the go1.26 toolchain: clean.go vet ./...: clean.go test ./... -race: green; this branch contains fix: keep producer LaunchAgents registered and running (#142) #143.-race -count=15: green.