4 Commits
Author SHA1 Message Date
Tobias GesellchenandClaude Opus 4.7 6e6e4838e6 fix(setup): fire <sourcesUpdated/> after data sync to recover post-factory-reset sources
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>
2026-05-15 16:25:07 +02:00
Tobias GesellchenandClaude Opus 4.7 0e10bfcb14 test(setup): wire issue #235 — Spotify Connect /now_playing reports IsPresetable=false
Two-part iteration. First, the fakespeaker grows a `/now_playing`
route with a default STANDBY fixture — issue #235 is the first one in
this series that needs to override /now_playing, and adding the route
on its own would be infrastructure noise; bundled here it has an
immediate consumer.

The regression test then locks in the device-side signal at the heart
of #235: when a SoundTouch is targeted by Spotify Connect (Spotify
app sends audio to the speaker), the speaker's /now_playing reports

  - source = SPOTIFY
  - sourceAccount = SpotifyConnectUserName (the marker)
  - ContentItem.location = /playback/container/<base64 spotify:...>
    — a perfectly resolvable URI
  - **ContentItem.isPresetable = false**

The contradiction (resolvable location + isPresetable=false) is the
reason the CLI's storeCurrentPreset at
cmd/soundtouch-cli/cmd_preset.go:41 refuses to act and emits "current
content cannot be preset" — exactly the reporter's symptom.

The test base64-decodes the location to surface the contradiction
explicitly: it should yield a `spotify:` URI. When AfterTouch grows a
fallback path (CLI --force, or service-side resolution to the
device's own Spotify integration via the SoundTouch Spotify source
provider), the assertion here stays sound — it tests what the device
emits, not what the CLI decides — but a sibling test should assert
the new fallback path produces a successful preset.

Fixture pattern matches the rest of the issue series:
testdata/issue235/ next to the test, fakespeaker driven via
FixtureOverrides, doc-comment naming what would have to change for
the assertion to flip.

Refs #235.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:13:24 +02:00
Tobias GesellchenandClaude Opus 4.7 abae685a85 fix(screenshots): widen fakespeaker coverage and stabilize the pipeline
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>
2026-05-15 12:56:43 +02:00
Tobias GesellchenandClaude Opus 4.7 bb71253690 feat(screenshots): add headless-Chrome capture pipeline with fake speaker
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>
2026-05-11 20:37:23 +02:00