Skip to content

feat: clock + server autodiscovery + AWTRIX device-control tab - #1

Merged
tarakanof merged 11 commits into
mainfrom
feat/discovery-and-device-control
Jun 10, 2026
Merged

tarakanof merged 11 commits into
mainfrom
feat/discovery-and-device-control

Conversation

@tarakanof

Copy link
Copy Markdown
Owner

Summary

Adds LAN autodiscovery in both directions plus an AWTRIX3-style Device-control tab, in one cohesive change:

  • A — Server discovers the clock. mDNS browse of _http._tcp + a /api/stats fingerprint (non-empty uid) replaces the hardcoded awtrix.http_base_url. Effective clock URL resolves as writable-store override > reachable config.json baseline > mDNS auto-pick (auto-pick is in-memory only; the read-only config.json is never written). New bearer endpoints GET /v1/device/discover, GET/PUT /v1/device/config; /admin/doctor notes the source.
  • B — App discovers the server. The server advertises _ember._tcp (gated by EMBER_MDNS_ADVERTISE, default on); the macOS app browses via NWBrowser and the Connection tab gains a "Discovered servers" picker. Manual entry remains the fallback.
  • C — Device tab. A new Tahoe sidebar tab mirrors the official AWTRIX3 app (General / Native Apps / Time & Date / Actions) with debounced auto-apply, native colour pickers, a battery+firmware header, and a confirmed Reboot. It routes through the server: GET/PUT /v1/device/settings, GET /v1/device/stats, POST /v1/device/{reboot,notify/dismiss} — the server whitelists + range-validates every AWTRIX key before forwarding, so the server stays the only writer to the device.

New dependency: github.com/brutella/dnssd (pure-Go DNS-SD, browse + advertise; distroless build stays CGO-free).

⚠️ Requires host networking

mDNS multicast doesn't cross the default Docker bridge. Run the Unraid container with --network host (or macvlan) for either discovery direction to work. The Device tab + clock control still work with a configured/discovered URL regardless.

Testing

  • Go: go test ./... -race green; go vet clean; gofmt clean. New table tests cover the fingerprint/filter, config-resolution precedence (store/config/discovered/none), settings whitelist + range/type/colour/enum/string validation, the proxy GET-filter and PUT-validate-before-forward, device-5xx→502 mapping, and the advertise gate/port parsing.
  • Swift: swift test (72 tests) green; EmberKit + app target build clean under Swift 6 strict concurrency. Tests cover the DeviceSettings AWTRIX-key mapping, partial-PUT encoding, and lenient decode of unexpected device types.

Reviewed

Reviewed by Codex and an independent review agent before opening; their findings were applied:

  • /admin/reload now re-applies the menu-persisted clock URL (was reverting to the file baseline until restart).
  • BrowseAWTRIX probes concurrently under a bounded context so the fingerprint phase honours the discovery budget regardless of host count.
  • macOS resolution NWConnections are cancelled on .waiting/terminal states and on stop() (no lifetime leak).
  • deviceSource() reports discovered (not config) when boot discovery re-finds an unreachable baseline URL.

Not covered (needs on-device pass)

mDNS browse/advertise and NWBrowser resolution can't be unit-tested deterministically. On a host-networked container, verify: clock discovery from a fresh config, server discovery from the app, a brightness/volume/native-app round-trip, Reboot + Dismiss, and that Pomodoro still transiently owns ATRANS/BLOCKN.

tarakanof added 11 commits June 10, 2026 02:16
Pure-Go DNS-SD (browse + advertise); keeps the distroless build CGO-free.
Backs server-side clock discovery and _ember._tcp advertising.
Probes resolved _http._tcp hosts at /api/stats and keeps those returning
AWTRIX-shaped stats (non-empty uid). The HTTP fingerprint/filter is unit-tested
via httptest; the dnssd browse wrapper is thin and verified on-device. Reachable
lets the server health-check a configured clock URL before auto-discovery.
…t wiring

Effective clock URL = store override > reachable config.json baseline > mDNS
auto-pick (in-memory only; the read-only config.json is never written). The
menu-chosen URL persists in the writable store (deviceSource reports the origin).
initDeviceDiscovery runs once at boot before the coordinator publishes; the
awtrix_reachable doctor check now notes the source.
…t+validate)

Thin passthrough of AWTRIX /api/settings behind bearer auth: GET filters the
device's settings to the managed keys; PUT validates each value against a
per-key whitelist (range/type/colour/enum) before forwarding, so the proxy
can't poke arbitrary firmware settings. stats/reboot/notify-dismiss proxy the
matching device endpoints.
Announces the Ember server over mDNS so the macOS app can discover it; TXT
carries version + health path. Default on; non-fatal and a no-op on bridge
networking. Gate + port parsing are unit-tested; the responder is verified
on-device.
Typed wrappers over /v1/device/*. DeviceSettings maps the AWTRIX keys with
all-optional fields (partial PUT omits nils) and a lenient per-field decode so
an unexpected device JSON type for one field can't break the whole tab load.
NWBrowser finds Ember servers; a short-lived NWConnection resolves each to a
concrete host:port for an http URL. Degrades silently when local-network access
is denied or nothing is found.
New Tahoe sidebar Device tab mirrors the AWTRIX3 app (General / Native Apps /
Time & Date / Actions) with debounced auto-apply, native colour pickers, a
battery+firmware header, and a confirmed Reboot. The Connection tab lists
Bonjour-discovered Ember servers (tap to fill the URL). Info.plist gains the
local-network usage string + _ember._tcp Bonjour service. AppEnvironment wires
DeviceService + ServerDiscovery and rebuilds device on reconnect.
…R_MDNS_ADVERTISE)

Document the mDNS discovery model, the host-networking requirement, the clock-URL
resolution precedence, and the new /v1/device/* endpoints across AGENTS, RUNBOOK,
and ARCHITECTURE.
… label

- /admin/reload now re-applies the menu-persisted clock URL (was dropped back to
  the file-config baseline until restart). [codex P2]
- BrowseAWTRIX probes concurrently under a fresh bounded context so the /api/stats
  fingerprint phase honours the discovery time budget regardless of host count. [codex P3]
- deviceSource() reports 'discovered' (not 'config') when boot discovery picked a
  URL that happens to equal an unreachable config baseline.
- settings PUT decodes via the shared decodeJSON helper; go mod tidy drops the
  stale dnssd // indirect tag; add device-5xx→502 and colour-array reject tests.
… stop()

Resolution connections were only cancelled on .ready/.failed; a .waiting
connection (advertised host with a filtered/refused port) would retry forever and
leak. Track in-flight connections, fail fast on .waiting, and cancel all on stop().
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.

1 participant