Closes the AfterTouch-side half of issue #234. After a factory reset
the speaker's /sources only lists the always-on local entries (AUX,
BLUETOOTH, AIRPLAY, NOTIFICATION, QPLAY, plus a SpotifyConnectUserName
placeholder); TUNEIN, LOCAL_INTERNET_RADIO, DEEZER, and linked
Spotify accounts are absent until the device receives the
<sourcesUpdated/> notification the reporter ran by hand. SyncDeviceData
now POSTs that notification as the final step, so users get the
visible-source-list recovery for free when they click Data Sync.
The other half — re-creating Marge.xml so playback resumes — is
already handled by the wizard's pair-account flow: it detects an
empty <margeAccountUUID/> in /info and prompts the user to pick a
known account or generate a new one. The wizard's pairing UI is
deliberately user-driven (the user picks the ID); the notification
nudge is purely automatic because there's no choice to make.
Implementation routes through the existing client surface rather
than reinventing it. setup.notifySpeakerSourcesUpdated delegates to
pkg/client.Client.NotifySourcesUpdated — the same path
handlers_mgmt.go already uses after music-service account changes
(handlers_mgmt.go:304, :637). The wire shape lives in one place
(pkg/models.NewSourcesUpdatedNotification). Fire-and-forget: a
notification failure logs but doesn't fail the sync.
Adjacent UX changes:
- docs/guides/TROUBLESHOOTING.md: new section "Presets flash then
revert to 'Select a preset' after a factory reset". Names the
symptom, the Marge.xml + reduced-/sources cause, and walks the
user through re-opening the Migration tab + Data Sync.
- pkg/service/handlers/web/js/script.js: devices list now renders
a "⚠ Not paired — re-pair" badge in the account-ID column for
speakers whose live /info reports an empty margeAccountUUID.
Clicking it opens the Migration tab pre-filled with that device,
surfacing the wizard's existing "Not paired (factory-reset or
never paired)" flow without making users discover it cold.
- pkg/service/testing/fakespeaker/testdata/info.xml: demo speaker
now reports margeAccountUUID=1234567 instead of the misleading
0000000 (which AfterTouch happens to accept as syntactically
valid but is not a documented sentinel anywhere — the convention
is empty for factory-reset, a real 7-digit number otherwise,
matching pkg/client/testdata/info_response_st{10,20}.xml).
Screenshots regenerated accordingly.
Test scaffolding:
- fakespeaker grows a POST /notification recorder that captures
body + Content-Type; tests assert on s.Notifications().
- TestIssue234_FactoryResetSpeakerSyncsReducedSources now drives
SyncDeviceData end-to-end (exercises the wiring) and asserts
the notification fires with the right deviceID and shape.
- TestFakeSpeakerNotificationRecorder pins the recorder contract
and the POST-only method gate.
Refs #234.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
make screenshots was producing artifacts: a ghost Spotify pill on
ui-devices, empty Plan-card URL inputs on ui-migration with cascading
"localhost" warnings, and "Checking configuration…" placeholder text
instead of "❌ Not configured" on ui-settings. Two root causes, fixed
together so the run is deterministic again.
1. Fakespeaker too thin for the post-wizard inspect pipeline. The new
migration wizard probes /supportedURLs and reads /networkInfo and
/sources alongside the existing /info, /presets, /recents. Those
routes now exist with sanitized fixtures (deviceID DEADBEEFCAFE,
loopback IPs, no real MACs or account IDs). The full group endpoint
set is also wired: /getGroup and /removeGroup return the empty
<group/> shape a real un-paired device emits; /addGroup and
/updateGroup echo the posted body with <status>GROUP_OK</status>
inserted before </group>, matching the success path documented in
issue #252. /supportedURLs lists everything the fake now serves so
any caller that probes capabilities first (e.g. marge_pairing.go)
sees a coherent picture. Tests cover the GET routes' XML roots, the
POST echo + GROUP_OK insertion contract, and /removeGroup's
GET-only contract (405 with Allow: GET on other methods).
2. run.sh seed hit a DNS cliff. The :443 preflight shipped in 3727ae6
resolves server_url on every /setup/settings call, and the
populatePlannedNetworkConfig step does it again. With the previous
seed of http://aftertouch.local:8000 each lookup burned ~5s on DNS
timeout, which compounded across the wizard calls and pushed
ui-migration past chromedp's 30s per-shot budget. Switched the seed
to http://aftertouch.localhost:8000 — RFC 6761 means *.localhost
resolves to loopback via the system resolver in milliseconds
(verified ~8ms on macOS / glibc / systemd-resolved) — so the brand-
friendly hostname survives in the captured PNGs without the
timeout. Manifest settle times bumped (ui-settings 300→2000ms,
ui-devices 500→2500ms, ui-sync 300→1000ms) to give fetchSettings +
fetchSpotifyStatus time to complete in headless Chrome.
While here, softened validateURL's loopback message to acknowledge the
on-device-install case (AfterTouch running on the speaker itself, where
loopback works) instead of unconditionally telling users they're
wrong. The validation still flags 127.0.0.1 / localhost since it's the
wrong answer 99% of the time, but the message now frames the
constraint rather than scolding.
docs/images/ui-*.png regenerated against the new pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refreshes docs/images/ui-{settings,devices,sync,migration}.png by
driving the web UI in chromedp against a synthetic speaker, so
documentation can be regenerated without real hardware and without
leaking personal data from the local network.
Three independent pieces:
- pkg/service/testing/fakespeaker — embeddable library serving the
HTTP and telnet surface the migration wizard probes (/info,
/presets, /recents and a getpdo CurrentSystemConfiguration reply
that places the device on the unmigrated happy path).
- cmd/dummy-speaker — thin CLI wrapping the library; self-registers
with a running service via POST /setup/devices.
- scripts/screenshots — chromedp runner driven by a JSON manifest;
decoupled from speaker/service setup so it can target any backend
URL. run.sh orchestrates a one-shot end-to-end capture and seeds
settings.json with a generic hostname plus discovery disabled to
keep real-network state out of the captures.
Captures are at DPR=2 for retina-sharp text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy 5 screenshots from _/screenshots/ into docs/images/ and wire them
into the migration guide (Settings, Devices, Sync, Migration tabs) and
the device initial setup guide (speaker AP mode Wi-Fi page). Replace the
images README wishlist with a table of what is actually present.
Also correct the AP mode IP address (192.0.2.1, verified on ST10) and
update the Settings step to match actual UI labels (Target Domain, DNS
Bind Address).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>