Commit Graph
157 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
Frank WandTobias Gesellchen 996faa0578 API uses "playback", not "playbook" 2026-05-15 13:45:54 +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 9cb8549c79 docs(troubleshooting): add filtered logread recipe + cross-link from Curl 7
Add the loopback-filtered command `logread -f | grep -v '127.0.0.1'` to
DEVICE-LOGGING.md's Pro-Tip section with a one-line rationale (strips
the speaker's in-device localhost chatter so cloud/AfterTouch attempts
are readable). Cross-link from the new Curl 7 entry in TROUBLESHOOTING
so users hitting that symptom find the SSH/logread how-to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:03:12 +02:00
Tobias GesellchenandClaude Opus 4.7 3727ae6f0f feat(service): pre-flight :443 reachability check with UI surfacing
Speakers connect to Bose hostnames over implicit HTTPS (:443) while
AfterTouch's listener defaults to :8443. Without iptables / setcap /
reverse-proxy in front, the speaker side sees Curl 7 / connection
refused and AfterTouch's HTTP log stays silent — a recurring source
of confusion (see #214, #269).

Add a server-side probe (Check443Reachability) that dials both
localhost:443 and the DNS-resolved LAN IP on :443. Run it once at
service startup with a 2s timeout and emit a [WARN] log with the
exact iptables/setcap commands keyed to the configured listener port.
Expose the result via GET /setup/settings (with a shorter inline
timeout) so the web UI renders a / line next to Target Domain
and a complementary browser-side fetch probe — the browser sits on
the LAN exactly where speakers do, and timing-to-error distinguishes
TCP refused from TLS handshake started even with an untrusted CA.

Both the startup WARN and the UI row are gated on dns_enabled,
since :443 only matters for the DNS migration path; SDK-override
migration uses the port from the configured URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:03:12 +02:00
Tobias GesellchenandClaude Opus 4.7 43578059dd docs(web): add soundtouch-web parity roadmap
Document the remaining feature gap between soundtouch-web and the
Stockholm app's local-control functionality (seek/scrub, queue view,
per-device settings) and the explicit non-goals (anything cloud-bound
that is either shut down or already handled by soundtouch-service).
Acts as both a contributor checklist and a public statement of what
the web UI will and won't try to cover.

Link the page under the Concepts section in SUMMARY.md so it shows up
in the published docs and satisfies the docs-consistency test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:15:53 +02:00
Tobias Gesellchen 36013cf005 docs(archive) add SoundTouch End-of-service Guidance
See https://www.bose.com/soundtouch-end-of-life
2026-05-14 22:15:53 +02:00
Tobias GesellchenandClaude Opus 4.7 a8d499cfe9 docs(telnet): document Docker fallback when telnet is not installed
Users on systems without a local telnet binary (modern macOS, Windows
without OptionalFeatures, minimal Linux distros) need a workable
recipe to reach the speaker's port-17000 shell. Add a one-line docker
run snippet that uses busybox-extras telnet inside an alpine
container, parameterised by the target speaker IP.

Placed at the top of the reference page so a reader who lands there
asking "how do I run telnet?" sees the fallback before the command
listings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:15:53 +02:00
Tobias GesellchenandClaude Opus 4.7 29a462da2b feat(setup): add CLI setup command group for end-to-end speaker provisioning
Add `soundtouch-cli setup` subcommand group covering the full reset →
re-provision → pair lifecycle as a scriptable alternative to the web UI:

  inspect, verify, plan, factory-reset, wait-ap, wifi-push, wait-online,
  ssh-check, install-ca, migrate, reboot, pair (bare | full state machine)

Supporting library code lives in pkg/service/setup: factory_reset.go,
wifi_provision.go, inspect.go, init_plan.go, setup_session.go.

Confirmed against ST10 firmware 27.0.6 that bare setMargeAccount over
WebSocket — no SETUP_START/SETUP_ENTER/SETUP_LEAVE bracket — is
sufficient to pair a factory-reset speaker; the firmware materializes
SystemConfigurationDB.xml and Sources.xml itself and the pairing
survives reboot. Result and field-by-field SystemConfigurationDB
comparison documented in docs/analysis/SETUP-WEBSOCKET-EXPERIMENT.md.
Captures the device's pre-reset DELETE-to-marge plus its LAN peer
notification flow in docs/analysis/FACTORY-RESET-PROTOCOL.md.

Perf: batch GetMigrationSummary's SSH probes into one Run() call via
ssh_probe.go / ssh_probe_apply.go — was ~8 sequential dials at
500-1000 ms each on FW 27 crypto, now one round-trip. Same data shape,
same MigrationSummary fields populated.

Fixes /clockTime and /clockDisplay wire formats — firmware 27 rejects
the legacy flat XML ("Error parsing request"). ClockTimeRequest now
uses utcTime attribute; ClockDisplayRequest emits the nested
<clockConfig> envelope with timezoneInfo/timeFormat/brightnessLevel.

Removes cmd/example-init-speaker (superseded by setup pair).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:43:36 +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
Tobias GesellchenandClaude Opus 4.7 952200ee26 docs: align migration guide and analysis with simplified pre-flight
MIGRATION-GUIDE.md step 5 — replaces the "Telnet round-trip probe"
bullet with two honest variants: the new passive observer for
already-migrated speakers, and a skip-row explainer for not-yet-
migrated speakers pointing at the Apply + reboot cycle. The rollback
section drops the obsolete tangent about the probe step leaving
persisted URLs untouched (the probe no longer exists, and the wizard
already writes both layers).

TELNET-MIGRATION-METHOD.md — §9.4's pre-flight table swaps the
deprecated `POST /setup/telnet-probe` row for the new
`POST /setup/peer-probe` row plus a skip-explainer row for the
not-yet-migrated case. §9.5 gains a "REMOVED — see §9.8" header
pointer (the section is kept as historical record of what was
tried). §9.6's backend-additions table replaces the deleted
`probeRegistry` + `RunTelnetRoundTripProbe` + `/setup/telnet-probe`
row with the `peerObserver` + `RunPeerReachabilityProbe` +
`/setup/peer-probe` row that supersedes it.

NEXT.md is local-working-tree only (deliberately untracked) and
gains a  Resolved header pointing at §9.8; not part of this
commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:31:24 +02:00
Tobias GesellchenandClaude Opus 4.7 f0de4864b6 deprecate(service): mark active telnet round-trip probe for removal
The swUpdate daemon caches its target URL at boot and ignores live
`sys configuration` writes, so the active flip in
RunTelnetRoundTripProbe never reaches the running daemon — confirmed
empirically on a fully-migrated speaker (FW 27.0.6) where both the
runtime and persistence layers were flipped and the device still
dialed the previously-cached `/updates/soundtouch` URL plus
DNS-intercepted `/streaming/software/update/account/*`. The probe URL
was never observed.

Marks DEPRECATED:
  - pkg/service/setup/telnet_probe.go: ProbeRegistrar,
    TelnetProbeResult, generateProbeToken, RunTelnetRoundTripProbe.
  - pkg/service/handlers/handlers_telnet_probe.go: HandleTelnetProbe,
    HandleProbeInbound, telnetProbeTimeout, telnetProbeResponse.
  - pkg/service/handlers/probe_registry.go: probeRegistry.
  - Server.probes field.
  - /probe/{token}[/*] and /setup/telnet-probe/{deviceId} routes.

Adds §9.8 to docs/analysis/TELNET-MIGRATION-METHOD.md documenting the
daemon-cache finding, the diagnostic that confirmed it, the passive
observer replacement, the pre-flight branch on migration state, and
the canonical telnet flow (Apply config → reboot → passive
validation). All code symbols remain in place this commit; the
follow-up commit performs the hard delete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:31:24 +02:00
Tobias Gesellchen 41a0f32296 chore 2026-05-11 00:39:28 +02:00
Tobias GesellchenandClaude Opus 4.7 ae9b02a42b docs(service): API reference for the *_url option family + telnet-probe
The /setup/migrate/{deviceIP} reference table covered only the legacy
self/proxied/original mode selectors, with a one-line "Custom service
URL" mention of target_url. The wizard has been writing literal
per-field URLs via marge_url / stats_url / sw_update_url / bmx_url
for weeks; external API callers had nothing to read.

Expanded the table into three blocks with precedence rules:

  1. Top-level params — method, target_url, proxy_url with the
     four migration mechanisms (xml / telnet / resolv, hosts marked
     deprecated).
  2. Per-field implementation mode — the legacy self/proxied/original
     family, kept for API back-compat with a note that the UI no
     longer sets them.
  3. Per-field literal URL overrides — marge_url / stats_url /
     sw_update_url / bmx_url with a "literal wins over mode" rule
     and the soundcork-suffix-propagates-to-envswitch note.

Three example curl invocations (canonical XML, soundcork telnet,
resolv with HTTPS) replace the old proxy=original-only snippet up
top.

Also added stub reference entries for POST /setup/telnet-probe and
the internal GET /probe/{token}[/*] catch-all — the SSH-less
reachability check the wizard runs automatically in its pre-flight
panel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:37:11 +02:00
Tobias GesellchenandClaude Opus 4.7 56c3e4f641 docs(guide): user-facing migration guide reflects the wizard
The guide still described the pre-wizard UI: "SSH status, CA trust
status, and connection test results before letting you apply the
redirect" and two methods (XML / DNS). The migration tab now opens
with the state card + Plan card + Customize three-axis form + visible
pre-flight panel, and a third transport (Telnet:17000) lets users
without SSH access migrate too.

Updates:

  - Step 3 retitled "Enable shell access on each speaker" with two
    sub-sections: SSH (the richest option, required for XML / DNS /
    CA install) and Telnet:17000 (the SSH-less fallback, no setup
    required, HTTP-only).
  - Step 5 rewritten to walk through the actual UI:
      * the state card's three rows (Transports, Migration State,
        Preconditions) with the action affordances inline
      * the Plan card — target URL with Save as default, per-field
        Service URLs editor with validation and soundcork-mode,
        account pairing, and Apply Suggested Plan
      * the visible pre-flight checks panel with its three or four
        checks per method and the Proceed Anyway / Cancel branch
      * Customize this migration with three independent axes
  - Step 6 mentions the auto-expand of Customize on Apply success
    and the per-transport reboot picking.
  - Rollback section adds the telnet-only "reboot reverts the
    runtime layer if envswitch isn't written" property, plus the
    rename to "Revert to Defaults" matching the button label.

The image reference (ui-migration.png) stays pointing at the
existing screenshot; a fresh capture is needed once the wizard is
final but the surrounding prose is now accurate either way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:37:11 +02:00
Tobias GesellchenandClaude Opus 4.7 441632b642 docs(analysis): post-implementation addendum (§9) for the telnet method
The feasibility analysis (§§1–8) was written before any of the wizard
shipped, and §7 forecast the surface area roughly. The migration tab
grew considerably during implementation — three-axis state model,
Plan card with per-field URL editor and validation, Customize
three-axis form, visible pre-flight panel, account pairing folded
into the wizard, and the SSH-less round-trip probe — none of which
the original §7 captures faithfully.

Added §9 "What actually shipped (post-implementation addendum)" with:

  §9.1 Three-axis state model (per-axis migration booleans, IsPaired,
        the state-card layout)
  §9.2 Plan card per-field URL editor (single source of URL overrides
        for both XML and Telnet, live optimistic preview)
  §9.3 Customize three-axis form (URL flip / DNS / CA radios driving
        applyCustomPlan)
  §9.4 Pre-flight panel (visible check list, decision tree, override
        affordances)
  §9.5 Telnet round-trip probe (the SSH-less reachability check via
        swUpdateUrl flip + :8090/swUpdateCheck trigger + probe-token
        registry)
  §9.6 Backend additions worth knowing (applyURLOverrides, parser,
        option allow-list, telnet timeout bumps)
  §9.7 Future probe candidates (pushCustomerSupportInfoToMarge;
        running the round-trip probe on SSH-capable speakers too)

§§1–8 stay verbatim as the historical feasibility record, with a
forward-pointer at the head of §7 so readers know the as-shipped
state is documented further down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:37:11 +02:00
Tobias GesellchenandClaude Opus 4.7 f951fc92df feat(handlers): proxy-aware RemoteAddr via opt-in TrustForwardedHeaders
Wire up X-Real-IP / X-Forwarded-For / True-Client-IP support for
deployments fronted by a reverse proxy, while staying safe on flat-LAN
deployments where a malicious speaker could spoof those headers
directly.

Two new fields on `datastore.Settings`:

* TrustForwardedHeaders (bool, default false) — opt-in switch.
* TrustedProxyCIDRs ([]string, default `["127.0.0.0/8", "::1/128"]`)
  — only requests whose immediate TCP peer falls in one of these
  blocks may have their source IP rewritten from forwarded headers.
  Loopback default matches the documented same-host nginx layout in
  docs/guides/HTTPS-SETUP.md.

New middleware in `pkg/service/handlers/middleware_realip.go`:

* TrustedRealIP wraps `chi/middleware.RealIP` with a trusted-peer
  gate. When the immediate TCP peer is in the allowlist, chi's
  parsing handles the actual header → IP rewrite. When it isn't
  (e.g. a speaker sending forwarded headers itself), we ignore the
  headers and r.RemoteAddr stays as-is.
* ParseTrustedProxyCIDRs converts string CIDRs into *net.IPNet,
  applying the loopback default on empty input and erroring loudly
  on invalid entries.

Server.TrustedRealIPMiddleware() returns the middleware (or nil) by
reading the live settings; the router setup in
cmd/soundtouch-service/main.go installs it as the very first
middleware so SnapshotMiddleware and downstream handlers see the
correct r.RemoteAddr.

HandleMargePowerOn now prefers r.RemoteAddr over the body's
self-reported `<IPAddress>` for outbound credential push:

* The body field is treated as a hint only — a malicious LAN speaker
  could set it to any value; using it for outbound HTTP requests is
  the SSRF surface the previous zeroconf hardening was guarding
  against from the sink side. Fixing it at the source as well closes
  the gap entirely.
* When body IP and TCP source disagree, a log line names both and
  the device ID so the discrepancy is investigable.
* RemoteAddr is unparseable → fall back to the body so we don't
  silently drop the priming.

docs/guides/HTTPS-SETUP.md gains a follow-up note next to the existing
nginx snippet explaining the new flag, the loopback-only default, and
the explicit warning against enabling the flag on a flat-LAN
deployment without a real proxy.

Eleven test cases in middleware_realip_test.go lock in the gate
behaviour: trusted peers honoured for X-Real-IP / X-Forwarded-For /
no-headers / IPv6, untrusted peers' headers ignored, garbage values
rejected, ParseTrustedProxyCIDRs covers default / override / invalid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:40:15 +02:00
Tobias GesellchenandClaude Opus 4.7 dc1f811a81 docs(zeroconf): clearer literal-IP error and a Security Considerations note
Building on the strict literal-IP validator from the previous commit,
make the runtime error self-explanatory so anyone tripping on a
hostname URL can fix it in one shot:

* Errors now lead with the offending zeroconf URL and the rejected
  host, so wrapping by GetInfo / PushCredentials / pushSimplifiedToken
  doesn't bury the actual bad value.
* The "host must be a literal IP" error suggests two concrete one-liner
  resolutions (`getent hosts <name>` and `dig +short <name>`) so the
  user has a copy-paste fix.
* The "host is not on a local network" error names the accepted ranges
  (loopback / RFC1918 private / link-local v4+v6) so the user knows
  what they're allowed to pass.

docs/guides/SOUNDTOUCH-SERVICE.md gains a bullet under Security
Considerations explaining the constraint and the rationale (LAN-resident
SSRF surface), so the strict behaviour is documented rather than a
surprise.

The 17 TestValidateZcBaseURL cases still pass — only the message bodies
changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:30:55 +02:00
Tobias GesellchenandClaude Opus 4.7 d3b1593953 docs(analysis): add device compatibility matrix for telnet migration
New section §8 records what is currently known about which devices and
firmware our migrateViaTelnet flow handles end-to-end, derived from the
six community sources catalogued in TELNET-COMMAND-REFERENCE.md plus our
issue threads.

* §8.1 — proven to work end-to-end (ST 10, 20, 300, Wave III, Wave IV on
  FW 27.0.6 with multi-reporter agreement).
* §8.2 — proven to need the PairAccount telnet fallback (ST Portable,
  BST20 Portable: /setMargeAccount missing or wedged on those firmware
  builds).
* §8.3 — likely to fail (SA-5 on FW 9.x with the older shell generation;
  newer ST Portable builds with shrunk command set). The preflight +
  abort-on-first-rejection design ensures these fail cleanly, leaving no
  half-configured state.
* §8.4 — unverified targets that are expected to work but lack concrete
  captures (ST 30, ST 520, Wave Music System I/II).
* §8.5 — flags the apparent contradiction between S5's enumerated
  "valid roots" on ST 10 / FW 27.0.6 (which omits envswitch) and #221's
  successful envswitch use on the same firmware. Most plausible reading:
  S5 is a non-exhaustive probe, not a negative claim; preflight catches
  any real absence.
* §8.6 — maps every failure mode to its observable outcome and the unit
  test that exercises it.
* §8.7 — TL;DR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:43:36 +02:00
Tobias GesellchenandClaude Opus 4.7 889470716b docs(analysis): add consolidated Telnet command reference
Synthesises every Bose SoundTouch port-17000 telnet command we have evidence
for, across six community sources: flarn2006's 2014 root-shell post,
Sam Hobbs's 2016 ST 10 setup-mode walkthrough, izndgroup's 2021 reissue,
sijeffrey's 2017 `bose` remote-control script, the 2026 r/bose telnet
probing thread (FW 27.0.6 ST 10), and our own #221 / #236 / soundcork#141
findings.

Groups the commands by family — `key` (front-panel button emulation, the
addition the Reddit thread brought in), `network` (WiFi profile management),
`sys` (verbs + the XML-tag-keyed `sys configuration` setter our migration
uses), `envswitch` (parallel persistence layer), `getpdo` (PDO read), `scm`,
`ws`, `swupdate`, and the historic shell-unlock commands. Each entry notes
firmware-era availability so implementations know whether to expect
"Command not found" on newer builds.

Records the four top-level command roots that S5 confirmed reachable on a
vanilla FW 27.x ST 10 (`key`, `net`, `sys`, `getpdo`), and flags that
`envswitch` works on other ST 20 / Wave models running the same firmware
family — a per-model variation the migration's preflight already handles.

Cross-linked from TELNET-MIGRATION-METHOD.md §2 and indexed in SUMMARY.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:43:36 +02:00
Tobias GesellchenandClaude Opus 4.7 fb47807f70 feat(telnet): add port-17000 migration method and account pairing
Adds an SSH-free third migration path that drives the SoundTouch device's
diagnostic shell on TCP port 17000, plus a hardened replacement for the
fragile /setMargeAccount HTTP pairing call.

* `pkg/telnet` — new reusable, dependency-free client (sibling of `pkg/ssh`)
  with deadline-driven Dial / Probe / SendCommand / Close. Mock-server tests
  cover happy path, command-not-found, mid-stream close, and the wedged-device
  read-timeout scenario.

* `setup.MigrationMethodTelnet` — runs `sys configuration` for all four URLs
  plus the parallel `envswitch boseurls set` persistence layer that otherwise
  wins on reboot, then verifies with `getpdo CurrentSystemConfiguration`.
  Aborts on the first non-OK response so configuration is never half-written.
  No SSH backup or rw pre-flight (the path is SSH-free by design).

* `setup.PairAccount` — probes :8090/supportedURLs first, time-bounds
  POST /setMargeAccount aggressively (5s connect / 12s total) to avoid the
  hangs reported in #236, and falls back to `envswitch accountid set <id>`
  over telnet when the HTTP endpoint is missing or wedged. Returns a
  PairAccountResult breadcrumb so the UI can show which path actually
  succeeded.

* `setup.Reboot(deviceIP, method)` — gains a RebootMethod selector;
  RebootMethodSSH stays the default (preserving prior behavior),
  RebootMethodTelnet sends `sys reboot` over a fresh telnet session and
  treats the inevitable socket-close as success.

* New endpoints on `/setup`:
  - GET  /account-id-suggestions/{deviceId} — returns the device's current
    margeAccountUUID (from :8090/info) plus known account IDs from the
    datastore, so the UI can offer reuse.
  - POST /pair-account/{deviceId}?account_id=NNNNNNN — invokes PairAccount;
    the existing reboot endpoint reads ?method=ssh|telnet from the query
    string.

* Helpers `IsValidAccountID` (exactly 7 digits) and `GenerateAccountID`
  (crypto/rand, retries on collision against a known-IDs list).

Documentation in docs/analysis/TELNET-MIGRATION-METHOD.md is updated to match
the implementation: bare-URL convention for `soundtouch-service`, no automatic
`sys reboot` (user-initiated via the existing button with a method selector),
and the realised package layout. The /etc/hosts method is intentionally not
exposed in the new flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:43:36 +02:00
Tobias GesellchenandClaude Opus 4.7 d9894be7db docs(analysis): add Telnet (port 17000) migration method analysis
Documents the SSH-free third migration path on top of the device's diagnostic
shell, synthesised from #221, #236, scheilch/opencloudtouch#167,
deborahgu/soundcork#228, and deborahgu/soundcork#141.

Captures the URL configuration command sequence, the dual persistence layers
(`sys configuration` + `envswitch boseurls set`), the `/setMargeAccount`
failure modes (404, hang, post-migration 502 on power_on) with their bounded
fallbacks, port-17000 preflight requirements, and account-ID sourcing rules
(reuse from `:8090/info`, pick from `DataStore.ListAccounts`, or 7-digit
manual/randomized entry). Cross-links the new doc from
DEVICE-REDIRECT-METHODS.md, marks the `/etc/hosts` method as deprecated, and
fixes the existing margeServerUrl example to use our service's bare-URL
convention with an explicit note for soundcork's `/marge` sub-path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:43:36 +02:00
Tobias GesellchenandClaude Sonnet 4.6 3891c08dd1 docs(migration): add Docker Compose quickstart with .env config guidance
Adds a "Docker Compose (recommended for home servers and VMs)" section
to Step 1, pointing users to the existing docker-compose.yml and
.env.example. Clarifies the purpose of docker-compose.ci.yml (CI tests
only) and docker-compose.override.yml (local modifications, not in VCS).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 21:36:34 +02:00
ca19bb32f7 feat(setup): harden hostname resolution before migration (#204)
- resolveIP now returns (string, error): error when result did not come
  from the device's own SSH ping (service-side fallback or total
failure)
- migrateViaResolvConf and parseTargetURLAndResolveIP abort on error,
  preventing a bad IP from being written to the device
- GetMigrationSummary captures the error in ResolveIPError and falls
back
  to the hostname for the preview display; XML migration is unaffected
- Web UI shows a warning box with the error and a docs link when
resolution
  is uncertain; migrate button stays enabled for the XML method
- Add hostname resolution troubleshooting section to TROUBLESHOOTING.md

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 20:00:24 +02:00
Tobias GesellchenandClaude Sonnet 4.6 fb0465bf5f docs: add UI screenshots to migration guide and device setup (#159)
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>
2026-05-02 22:58:29 +02:00
Tobias GesellchenandClaude Sonnet 4.6 624da2c2b8 docs: rewrite migration guide, fix broken images (#159)
Replace the placeholder MIGRATION-GUIDE.md (which had a "planned to be"
header, a nonexistent install.sh reference, and 9 broken screenshot links)
with a complete, image-free step-by-step walkthrough covering all 6 steps:
install, configure URL, enable SSH via USB stick, discover/sync, migrate
(XML or DNS/DHCP), and verify.

Add the Migration Guide to the README docs section and link to it from
the Survival Guide.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 22:58:29 +02:00
Tobias GesellchenandClaude Sonnet 4.6 6b90d2c994 docs: rewrite README and survival guide for post-shutdown user journey
Rewrite README.md to be concise and tool-focused (no code snippets),
clearly presenting all five tools and their use cases. Expand the
soundtouch-service section to cover both user scenarios and redirect
method trade-offs.

Rewrite SURVIVAL-GUIDE.md around the same two scenarios with step-by-step
instructions. Remove deprecated hosts-file method from all user-facing
docs; update MIGRATION-SAFETY.md, HTTPS-SETUP.md, and SOUNDTOUCH-SERVICE.md
to reflect only the two supported methods (XML redirect and DNS/DHCP).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 22:58:29 +02:00
e99c04888c feat: implement missing endpoints and serve static resources from downloads/media hosts (#199)
Endpoints:

- POST /streaming/music/musicprovider/{id}/trial/is_eligible (reuses
is_eligible handler)
- POST /bmx/tunein/v1/favorite/{stationID} with datastore persistence
(SaveTuneInFavorite)
- DELETE /bmx/tunein/v1/favorite/{stationID} (DeleteTuneInFavorite)
- POST /bmx/core02/svc-bmx-adapter-orion/prod/orion/token (anonymous
Orion token)
- GET /bmx-icons/* serving embedded static/media assets (media.bose.io)
- GET /ced/* serving embedded firmware index, release notes, and 10
app-help XMLs (downloads.bose.com)

Add media.bose.io and downloads.bose.com to DNS redirect lists (setup.go
both domain slices, dns.go shouldIntercept list, main.go getDomains
map). Document implemented endpoints in
tests/interactions_20260502_missing_external.md; mark rows 0246–0247 as
self/☑ in the interactions table.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 21:22:36 +02:00
Tobias GesellchenandClaude Sonnet 4.6 0b2e03820b docs: add CAPTURE-MIGRATION-TRAFFIC.md to SUMMARY.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 18:08:39 +02:00
Tobias GesellchenandClaude Sonnet 4.6 faacba5d91 docs(mitm): add .mitm to .http conversion script and document workflow
- Add scripts/convert_mitm_script.py (mitmproxy addon, converts flows to .http files)
- Gitignore scripts/android/mitm/ (converted output, derived from captures)
- Document conversion step in CAPTURE-DEVICE-PAIRING.md Phase 5
- Document conversion step in CAPTURE-MIGRATION-TRAFFIC.md Step 6.2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 18:08:39 +02:00
Tobias GesellchenandClaude Sonnet 4.6 aecd41bdfa docs(migration): add migration traffic capture runbook with session trace
- Add CAPTURE-MIGRATION-TRAFFIC.md with step-by-step migration runbook
- Include session trace from first interactive ST10 migration run
- Genericize example IP addresses in BOSE-APP-ADB-Emulator.md and CAPTURE-DEVICE-PAIRING.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 18:08:39 +02:00
Tobias GesellchenandClaude Sonnet 4.6 cf7f3431f6 feat(android): scripted MITM setup with emulator snapshot and Frida SSL unpinning
- Add scripts/android/ with setup-mitm-avd.sh (one-time) and start-mitm-session.sh (per-session)
- Move frida Dockerfile to scripts/android/; extract frida-server + SSL scripts via Docker
- Use native macOS mitmproxy app for capture (Docker NAT blocks emulator traffic)
- Add native-connect-hook.js to Frida launch — required for Bose app's native networking
- Document verified AP mode Wi-Fi provisioning endpoint (POST :8090/addWirelessProfile)
- Correct factory reset sequences for ST10/ST20 from official Bose guides
- Remove old scripts/setup-mitm-avd.sh and scripts/start-mitm-session.sh (moved to android/)
- Add session trace with lessons learned from first interactive capture run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 18:08:39 +02:00
86825c44af feat(backup): add soundtouch-backup tool for cloud and local speaker backup (#197)
Introduces a standalone `soundtouch-backup` CLI with three subcommands:
- `all`: authenticates with the Bose cloud, backs up account data, then
reads device IPs from devices.xml and backs up each reachable speaker
- `cloud`: fetches account profile, devices, sources, presets, and full
endpoint from streaming.bose.com
- `local`: backs up each speaker via HTTP API (12 endpoints) and
optionally via SSH (individual files + /opt/Bose/etc/ and
/mnt/nv/BoseApp-Persistence/1/ directories)

Also centralises pkg/service/ssh → pkg/ssh so both the service and the
backup tool share the same SSH client; adds ReadFile and ReadDir
methods, and handles the firmware quirk where cat exits 1 on empty
files.

Output is a single dated .tar.gz or .zip archive.

Example flow:

```shell
gesellix@Mac Bose-SoundTouch % go run ./cmd/soundtouch-backup all --output _/cloud-backup --email user@example.com
Password: 
Authenticating as user@example.com...
  ✓ Authenticated (account ID: 1234567)
  ✓ email address (107 bytes)
  ✓ devices (1492 bytes)
  ✓ sources (1111 bytes)
  ✓ presets (2585 bytes)
  ✓ full account (55037 bytes)
Found 2 device(s) in cloud account, attempting local backup...
  ✓ ST20: 12 files via HTTP
  ⚠ ST20: SSH skipped /etc/remote_services (Process exited with status 1)
  ⚠ ST20: SSH empty file /mnt/nv/remote_services
  ✓ ST20: 64 files via SSH
  ✓ ST10: 12 files via HTTP
  ⚠ ST10: SSH empty file /etc/remote_services
  ⚠ ST10: SSH skipped /mnt/nv/remote_services (Process exited with status 1)
  ✓ ST10: 48 files via SSH
Archive written: _/cloud-backup/soundtouch-backup-2026-05-02.tar.gz (141 files)
```

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 14:00:23 +02:00
Tobias GesellchenandClaude Sonnet 4.6 ee44526d25 docs(amazon): confirm amazon_music:access scope requires device client ID
Attempting to request amazon_music:access with a standard application
client ID (amzn1.application-oa2-client.*) returns HTTP 400
lwa-invalid-parameter-bad-scope from the LWA authorization endpoint.
The scope is gated to Amazon Music partner device client IDs.

Revert scope to "profile" (working state) and document the confirmed
blocker with the exact error. Path forward: Amazon Music partner
registration for a device client ID; one-line change to AmazonScopes
when available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:30:06 +02:00
Tobias GesellchenandClaude Sonnet 4.6 50c40be763 feat(amazon): fix bridge fallback, source display name, and document streaming blocker
- Amazon bridge: fall back to sync/legacy on any error from
  SetMusicServiceOAuthAccount (not only error 1029); timeouts from
  unresponsive speakers no longer silently skip the fallback chain
- Amazon bridge: reduce speaker client timeout from 30s to 5s for
  faster failure on local network calls
- marge: resolveSourceName now prefers SourceName/DisplayName over
  SourceKeyAccount, so Amazon (and Spotify) sources show the account
  holder's name instead of the raw account ID
- docs: update amazon-music-oauth.md with real-world test results;
  music-api.amazon.com returns 401 because standard LWA apps lack
  music::* partner scopes — infrastructure is complete but streaming
  is blocked pending Amazon partner access
- docs: add SELF-HOSTING.md and MUSIC-SERVICES.md user guides; link
  both in SUMMARY.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:30:06 +02:00
Tobias GesellchenandClaude Sonnet 4.6 2ab98f2b6f docs: update amazon-music-oauth.md with setup guide and implementation status
- Mark status as Implemented
- Add "Trying It Out" section: LWA app setup, service flags, OAuth flow,
  account verification, speaker priming, DNS requirement, site_id open question
- Fix stale endpoint table entry (no longer a stub)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:30:06 +02:00
Tobias Gesellchen 3e96e95a7d Update implementation plan/spec for Amazon Music OAuth integration 2026-04-29 20:30:06 +02:00
Tobias Gesellchen c8f280f9d4 Add implementation plan/spec for Amazon Music OAuth integration 2026-04-29 20:30:06 +02:00
Tobias Gesellchen 4f76c82f9b cleanup 2026-04-28 17:57:46 +02:00
Tobias Gesellchen c6fbc45be5 lint 2026-04-28 17:57:46 +02:00
Tobias GesellchenandClaude Sonnet 4.6 376c85a641 docs: update soundcork parity and community tools analysis
- Mark ZeroConf Spotify priming and 404 handler as addressed in both docs
- Remove stale "Remaining gaps" and "Already adopted" tracking tables from
  community-tools.md; detail now lives in PARITY-SOUNDCORK.md
- Update PARITY-SOUNDCORK.md summary to reflect Groups and ZeroConf as done;
  add cross-reference to community-tools.md
- Rename remaining "gesellix" project references to "AfterTouch" throughout
  community-tools.md (URLs and author attribution unchanged)
- Add soundcork-stockholm-app (entry 7) to community projects list
- Correct DNS priority entry: built-in DNS server requires no external tools

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 17:57:46 +02:00
968312aa39 Implement Spotify Connect ZeroConf DH blob encryption (#192)
Replace the simplified tokenType=accesstoken push with the full Spotify
Connect ZeroConf protocol: GET getInfo to fetch the speaker's 768-bit DH
public key, derive AES-128-CTR + HMAC-SHA1 keys from the shared secret,
and POST an encrypted LoginCredentials protobuf blob. Speakers that
receive a proper blob can self-refresh their Spotify session
independently, eliminating the need for periodic re-priming on token
expiry. Falls back to the raw token approach automatically when getInfo
fails, preserving compatibility with older firmware.

SHA1 is mandated by the Spotify Connect ZeroConf protocol spec for DH key derivation. This cannot be changed without breaking protocol compatibility.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 15:34:05 +02:00
Tobias GesellchenandGitHub 29c904b7e4 The official Bose SoundTouch USB update website is not available anymore (#187)
The previous link
https://downloads.bose.com/ced/soundtouch/soundtouch_usb/index.html
responds with status code 403 and redirects to
[`/index.html`](https://downloads.bose.com/index.html), which ultimately
lands at https://www.bose.com/support/international
2026-04-25 21:35:07 +02:00
Tobias Gesellchen 5e6885cfe8 Add missing RADIO_BROWSER default source 2026-04-20 19:18:39 +02:00
Tobias GesellchenandGitHub 747a9cec97 Add app analyzing/debugging docs and scripts (#174) 2026-04-19 22:27:54 +02:00
Tobias Gesellchen 82fd77c8e2 Add Bose SoundTouch Web API v1.1 docs 2026-04-08 19:35:27 +02:00
Tobias GesellchenandJunie 276d01fe42 feat(spotify): improve Spotify registration flow and speaker notification
- Implement full SoundTouch app flow for Spotify registration in the Web UI.
- Update `/mgmt/spotify/init` to pass `accountID` via OAuth `state`.
- Add "Connect Spotify" button to Local Account tab in Web UI with polling.
- Implement legacy and Marge-sync fallbacks for speaker notifications (Error 1029).
- Add support for parsing multi-error XML responses (`<errors>`) from speakers.
- Add `NotifySourcesUpdated` to client for triggering manual source synchronization.
- Improve test coverage for error parsing and Spotify initialization handlers.

Co-authored-by: Junie <junie@jetbrains.com>
2026-04-06 22:39:30 +02:00
Tobias Gesellchen 153d387aaf Fix a complete flow for Spotify registration, preset 2026-04-06 21:15:15 +02:00
Tobias Gesellchen e5b94158e6 Use modern docker compose command syntax 2026-04-06 15:05:44 +02:00
Tobias GesellchenandGitHub 21ce44fa2e Update the "bose-lab" runbook for app activity tracing (#140) 2026-04-03 23:50:28 +02:00