Commit Graph
67 Commits
Author SHA1 Message Date
Tobias GesellchenandClaude Sonnet 4.6 794a5b3a8f docs: update cloud-shutdown messaging to past tense
Bose shut down SoundTouch cloud services on 2026-05-06. Update the three
main user-facing docs to reflect that the shutdown has happened:

- README.md: rename section, rewrite opening paragraph, reframe the two
  getting-started scenarios as 'already migrated' vs 'starting fresh'.
- SURVIVAL-GUIDE.md: past-tense title and opening; remove duplicate
  Scenario B heading (copy-paste leftover from earlier edit); remove the
  table of redirect methods and TLS note that belonged to the deleted
  pre-shutdown Scenario B stub.
- MIGRATION-GUIDE.md: remove the 'cloud is still running' note from the
  Sync step; fix the post-migration backup blurb to reference
  soundtouch-backup rather than a non-existent Step 4 tar.gz.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:56:46 +02:00
Tobias Gesellchen 7c625d953c docs: rename 'local external host' to 'local network host' 2026-05-24 11:31:16 +02:00
Tobias GesellchenandClaude Sonnet 4.6 8097caa985 docs: fix TuneIn workaround in cloud walkthrough — data sync doesn't work from cloud
Data Sync requires AfterTouch to reach the speaker outbound, which
fails when AfterTouch is running in the cloud. Replace with the
correct three-step workaround from wimdeblauwe (discussion #295):

1. Manually create Sources.xml in the server's data volume with the
   default source set (AUX, LOCAL_INTERNET_RADIO, TUNEIN, RADIO_BROWSER)
2. Send a sourcesUpdated notification to the speaker from a local machine
3. Power-cycle the speaker (CLI reboot is insufficient; firmware only
   activates new source types at boot)

Add a clear note that Data Sync is not available from cloud deployments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:31:16 +02:00
Tobias GesellchenandClaude Sonnet 4.6 f1f2f260b9 docs: fix on-device multi-speaker claim in deployment overview
'Each speaker needs its own install' is only true when the firmware
binds port 8000 to loopback (older devices, issue #196). Devices that
expose the port on the LAN can run one on-device AfterTouch and point
other LAN speakers at it — same as a Raspberry Pi. Qualify the cell
accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:31:16 +02:00
Tobias GesellchenandClaude Sonnet 4.6 b05043e6ad docs: add cloud/VPS deployment as Option B in the overview
- docs/guides/CLOUD-DEPLOY-WALKTHROUGH.md (new)
  Step-by-step for deploying AfterTouch on a remote VPS:
  Docker Compose + DISCOVERY_ENABLED=false, Coolify config from
  wimdeblauwe's field report (discussion #295), CLI-driven speaker
  migration (soundtouch-cli setup migrate/reboot from the local
  machine), TuneIn source registration gotcha and fix, preset setup,
  security warning about the unauthenticated Marge API, and the
  'what breaks if the server goes offline' answer.

- docs/guides/DEPLOYMENT-OVERVIEW.md: expand from 2 to 3 options
  (Local external host / Cloud VPS / On-device); update the
  comparison table with the cloud-specific columns (HTTPS needed,
  CLI migration, discovery disabled); link to the new walkthrough
  and to discussion #295 as the community field report.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:31:16 +02:00
Tobias GesellchenandClaude Sonnet 4.6 56462d7145 docs: reorganize deployment docs — overview page, two walkthroughs, architecture dir
Problem: the existing docs gave no clear path for non-technical users.
- GETTING-STARTED.md is a Go library developer guide
- RASPBERRY-PI.md stops after the service is running (no migration or preset steps)
- DEVICE-LOCAL-INSTALL.md is an architectural analysis that confused installation intent
- No single page helped a user choose between external-host vs on-device

Changes:
- docs/DEVICE-LOCAL-INSTALL.md → docs/architecture/DEVICE-LOCAL-INSTALL.md
  Move the planning/architecture doc out of the user-visible guides root;
  add a redirect banner pointing to the user guides
- docs/guides/DEPLOYMENT-OVERVIEW.md (new)
  Navigation landing page: comparison table (external host vs on-device),
  links to user-friendly walkthrough + technical reference for each scenario
- docs/guides/EXTERNAL-HOST-WALKTHROUGH.md (new)
  Step-by-step for Raspberry Pi / any always-on host: install, discover
  speaker, run migration wizard, Health QuickFix, verify pairing, set
  presets via UI or CLI — the post-install steps that RASPBERRY-PI.md
  did not cover
- docs/guides/RASPBERRY-PI.md: cross-link to full walkthrough and overview
- README.md: replace the one-liner "see On-Device Installer" with a
  pointer to the Deployment Overview so both paths are equally visible

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:31:16 +02:00
Tobias GesellchenandClaude Sonnet 4.6 73d0d4b176 fix(install): reliable stop, VERSION flag, and on-device walkthrough
- aftertouch init script: stop) now waits up to 15 s for SIGTERM
  to take effect, then escalates to SIGKILL; prevents stale daemon
  processes after '/etc/init.d/aftertouch stop' returns (weissigera's
  workaround was manual 'killall aftertouch-service')

- install.sh: add --version / -v CLI flag so the version to install
  can be passed as a command-line argument in addition to the VERSION
  env var; document the trade-off of the hard-coded default in a
  comment; update scripts/on-device-install/README.md with concrete
  usage examples for env-override, CLI flag, and rollback tip

- docs/guides/ON-DEVICE-INSTALL-WALKTHROUGH.md: 10-step runbook
  derived from weissigera's field-tested procedure (issue #329
  comment #4521280831): SSH connection, storage cleanup, install via
  install.sh, reboot, SSH tunnel, Health QuickFix, pairing
  verification, soundtouch-cli download, custom-radio preset setup,
  and final verification; troubleshooting table at the end

Closes #329 (remaining two tasks)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:31:16 +02:00
Tobias GesellchenandClaude Sonnet 4.6 3a0b30bc33 feat(tls): persist TLSExtraHosts + Settings UI + speaker_marge_url QuickFix
Operators who deploy AfterTouch on an IP-only host (no DNS hostname) and
who get a speaker_marge_url health warning previously had to SSH in, edit
their systemd unit or docker-compose, add --tls-extra-host, and restart.
The fix is now reachable from the UI:

- datastore.Settings gains TLSExtraHosts []string. At startup
  applyPersistedSettings merges CLI/env values (still authoritative)
  with persisted ones, deduplicating while preserving order.
- /setup/settings (GET) exposes tls_extra_hosts (editable list) and
  tls_san_hosts (the full effective SAN list, read-only).
- /setup/settings (POST) accepts tls_extra_hosts (*[]string so callers
  can distinguish "field omitted" from "explicitly empty").
- Settings tab grows a "TLS extra hosts" textarea + an info panel
  explaining the restart-required dance.
- speaker_marge_url emits a QuickFix labelled "Add <host> to TLS hosts"
  alongside the existing CLI manual command. The fix re-probes the
  device's /info, extracts the margeURL host, and appends it to the
  persisted list — race-safe against stale findings.
- HTTPS-SETUP.md documents both paths.

Tests cover: merge dedup + ordering + whitespace, the new QuickFix
emission shape, and the margeURL host extraction across HTTPS/HTTP/bare
input forms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:32:44 +02:00
Tobias GesellchenandClaude Sonnet 4.6 377fa9ceda feat(preflight): skip :443 check in HTTP-only deployments + OUTPUT-chain caveat
The :443 reachability preflight was emitting a WARN on every deployment
where AfterTouch's configured --server-url is HTTP (not HTTPS), even
when speakers were migrated to that HTTP URL and never connect to :443.
Operators reproduced this on #218 (CTonyPeterson) and #344
(california444) — both saw the warning even though their setups had no
need for iptables port forwarding, and CTonyPeterson followed the
recommended iptables OUTPUT rule which then caught his host's own
outbound HTTPS traffic and broke `go install` and his browser.

Two changes:

- Probe443Result gains NotApplicable + Reason. Check443Reachability
  returns the NotApplicable verdict when the parsed serverURL scheme is
  http. The settings UI renders an ℹ️ info badge with the reason instead
  of a red ✗.
- FormatPreflightGuidance grows a one-line caveat about the iptables
  OUTPUT chain: it catches all outbound :443 on the host, including
  browsers / go install / apt-get, which is rarely what the operator
  wants.

HTTPS-SETUP.md gains the same caveat plus a section documenting the
new not-applicable verdict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:32:44 +02:00
Tobias Gesellchen b95bdae751 feat: Add RadioBrowser integration alongside TuneIn support
- Refactors BMX service to support multiple radio providers
- Adds RadioBrowser.com API integration with search and browse
- Splits TuneIn logic into separate module for better organization
- Adds new web UI components for radio station discovery
- Includes new SVG icons for RadioBrowser branding
2026-05-18 22:34:26 +02:00
Marcin MennemannandTobias Gesellchen f1821d5995 doc: remove mirroring and parity with Bose cloud 2026-05-18 20:45:18 +02:00
Tobias GesellchenandClaude Opus 4.7 50f1c5980a docs(mac-mapping): scrub dash-form of the real test-speaker MAC
The earlier MAC sweep in 04f9c31 only matched the colon form
(A8:1B:6A:53:6A:98). MAC-ADDRESS-MAPPING.md documents the
normalisation behaviour with separator variants, so it also carried
the dash form (A8-1B-6A-53-6A-98) — 2 hits both replaced with the
canonical AA-BB-CC-DD-EE-FF placeholder.

Surfaced by the post-cleanup re-scan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:05:13 +02:00
Tobias GesellchenandClaude Opus 4.7 1b21e0eaa8 docs: sweep example LAN IPs to RFC-5737 documentation range
Phase 4 of the docs portion of the rfc-5737-cleanup. Replaces all
192.168.1.x example IPs in tracked .md / .txt files with the
equivalent last-octet under 192.0.2.x.

192.168.1.x is RFC-1918 private space and routes on real networks,
which leaves readers guessing whether a documented IP is a placeholder
or a documented LAN. 192.0.2.0/24 is reserved by RFC 5737 exclusively
for documentation — readers know on sight that they're examples.

58 files touched, 551 line pairs. Includes .github issue/PR templates,
all docs/ references, example READMEs, and one script doc. No code
changes, no test changes; test files still carry the 192.168.1.x
placeholder pending Phase 2 in _/RFC-5737-cleanup/assessment.md.

Also fixed a small fallout in docs/analysis/ANONYMIZATION-SUMMARY.md
where the explanatory sentence "a reader can't tell whether
192.168.1.10 is a placeholder or a documented LAN address" had
itself been swept by the regex (inverting the point); restored the
literal example and noted the sweep progress inline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:05:13 +02:00
Tobias GesellchenandClaude Opus 4.7 29f3fc6f96 docs: replace real Bose account IDs in examples with placeholders
Two real Bose customer account IDs were embedded in documentation
examples: 3230304 (16 files repo-wide, 5 of them .md/.txt) and
9569497 (2 files, 1 .md). Account IDs look numeric and innocuous but
they're tied to a specific Bose customer — same exposure class as
MACs and home-LAN IPs.

Mapping:
  3230304  → 1000001
  9569497  → 1000002

6 .md files touched in this commit. Remaining occurrences live in
test files and one Python script (scripts/convert_mitm_script.py) —
those are out-of-scope for the docs sweep and will be handled in a
dedicated test-fixtures commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:05:13 +02:00
Tobias GesellchenandClaude Opus 4.7 fa51a6f610 docs: replace real MAC addresses in examples with placeholders
The maintainer's two test-speaker MACs (A81B6A536A98 / A81B6A849D99,
plus colon-separated forms) appeared throughout documentation, runbooks,
and example READMEs. Public repo — same hygiene argument as the LAN-IP
sweep in 787c4fa.

Mapping:
  A81B6A536A98          → AABBCCDDEEFF
  A81B6A849D99          → AABBCCDDEE01
  A8:1B:6A:53:6A:98     → AA:BB:CC:DD:EE:FF
  A8:1B:6A:84:9D:99     → AA:BB:CC:DD:EE:01

The placeholders use the IANA-reserved AA:BB:CC:DD:EE:FF address that's
clearly synthetic, matching the convention the earlier anonymisation
pass had already adopted. 13 .md files touched; no tests, no code.

ANONYMIZATION-SUMMARY.md left for a dedicated rewrite commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:05:13 +02:00
Tobias GesellchenandClaude Opus 4.7 51d196dd03 docs: replace personal LAN IPs and device names with placeholders
Public-repo hygiene: docs and READMEs carried the maintainer's home
LAN range (192.168.178.x) and personal speaker names ("Sound
Machinechen", "A Sound Machine"). Swapped to RFC-5737 documentation
IPs (192.0.2.x — reserved for examples, won't collide with anyone's
real network) and generic names ("Living Room SoundTouch",
"Kitchen SoundTouch").

12 files touched, all .md / .txt documentation. No code or tests
changed in this commit; subsequent commits will address the
docs/analysis/ANONYMIZATION-SUMMARY.md mapping log and the wider
real-MAC/real-account-ID footprint surfaced by the audit at
_/RFC-5737-cleanup/assessment.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:05:13 +02:00
Tobias GesellchenandClaude Opus 4.7 f8108b0dd9 refactor: rename /setup/proxy-settings → /setup/logging-settings
After the proxy/mirror removal there is no proxy left in the service,
but the parallel partial-update endpoint /setup/proxy-settings stuck
around with its legacy name. It serves a legitimate purpose distinct
from the bulk /setup/settings POST: the three checkboxes
(Redact / Log Bodies / Record) use onchange-triggered live save,
while /setup/settings drives a Save-button form for dozens of fields.
Folding the two endpoints together would either lose the live-toggle
UX or send half-edited draft form data on every toggle, so the
partial-update endpoint earns its keep — it just needed the right
name.

Renamed symbols (no behaviour change):

  Go handler funcs:
    HandleGetProxySettings      → HandleGetLoggingSettings
    HandleUpdateProxySettings   → HandleUpdateLoggingSettings
    GetProxySettings            → GetLoggingSettings

  Route:
    /setup/proxy-settings       → /setup/logging-settings

  JS:
    fetchProxySettings()        → fetchLoggingSettings()
    updateProxySettings()       → updateLoggingSettings()

  HTML element IDs (cosmetic, kept consistent):
    proxy-redact / proxy-log-body / proxy-record
                                → logging-redact / logging-log-body / logging-record

  HTML heading:
    "Proxy Logging:"            → "Logging:"

JSON payload shapes (request + response keys) are UNCHANGED: the
endpoint still emits / accepts {"redact", "log_body", "record"}.
Persisted Settings on disk are UNCHANGED. CLI flags are UNCHANGED.
Server struct fields redactLogs / logBodies / recordEnabled
(renamed earlier this session) are UNCHANGED.

testdata/router_routes.txt regenerated. go build clean. go test
./... clean except pre-existing TestDocsConsistency (untracked-file
issue, unrelated). golangci-lint 0 issues.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:53:33 +02:00
Tobias GesellchenandClaude Opus 4.7 ae5a1d5a4f docs(stockholm): mention dev-service-stockholm in the user guide
The "Enabling the Stockholm UI" section listed the binary/env-var/Docker
forms but not the new dev-service-stockholm make target — which is the
shortest path through the local roundtrip and the one most contributors
will want.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:05:39 +02:00
Tobias GesellchenandClaude Sonnet 4.6 6fb999435a feat(stockholm): add Go backend integration for Stockholm frontend
Implements pkg/service/stockholm with bridge (appSend/runQueue), HTTP
proxy, static serving, config URL rewriting, native state persistence,
and device discovery. Mounts under a configurable base path (/stockholm
by default) with correct http.StripPrefix routing and apiBase-prefixed
bridge API routes matching the patched JS window.__stockholmBase calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 15:05:39 +02:00
chrisandGitHub 6d462191d9 docs: add SoundTouch 30 factory reset sequence (#305)
## Description

Add missing factory reset sequence for SoundTouch 30 (non-Series III).
The current table only lists SoundTouch 30 Series III. The SoundTouch 30
uses a different sequence: power on, then hold Preset 1 + Volume − for
10 s. The display counts down from 10 to 1 and shows "Hold to restore
factory settings" before restarting.

## Type of Change

Please check the type of change your PR introduces:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
- [ ] Test improvements
- [ ] Build/CI improvements

## Related Issues

## Changes Made

### API Changes
- [ ] Added new endpoints
- [ ] Modified existing endpoints
- [ ] Added new CLI commands
- [ ] Modified existing CLI commands
- [ ] Added new configuration options

### Implementation Details
- Added missing table row for SoundTouch 30 (non-Series III) in the
factory reset sequences table. No new dependencies.

## Testing

### Automated Tests
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] All existing tests pass
- [ ] Test coverage maintained or improved

### Manual Testing
- [x] Tested with real SoundTouch device(s)
- [ ] Tested CLI changes manually
- [ ] Tested in different network environments

**Device(s) tested with:**
- Device model: SoundTouch 30
- Firmware: 27.0.6.46330
- Test results: Factory reset sequence verified on real device

### Test Commands

## Documentation

- [ ] Updated relevant documentation
- [ ] Added code comments for complex logic
- [ ] Updated CLI help text
- [ ] Added usage examples
- [ ] Updated API documentation

**Documentation files updated:**
- [ ] README.md
- [ ] docs/API-Endpoints-Overview.md
- [ ] docs/CLI-REFERENCE.md
- [ ] Code documentation (godoc)

docs/DEVICE-INITIAL-SETUP.md

## Backward Compatibility

- [x] This change is backward compatible
- [ ] This change includes breaking changes (requires major version
bump)
- [ ] This change requires configuration migration

**Breaking changes (if any):**

## Security Considerations

- [x] No security implications
- [ ] Security review required
- [ ] Added input validation
- [ ] Updated authentication/authorization

## Performance Impact

- [x] No performance impact
- [ ] Performance improvement
- [ ] Potential performance regression (justify why)

**Performance notes:**

## Code Quality

- [ ] Code follows project style guidelines
- [ ] No linting errors
- [ ] No security warnings
- [ ] Memory leaks checked (if applicable)

### Pre-submission Checklist

- [ ] `make check` passes (format, lint, vet)
- [ ] `make test` passes
- [ ] No TODO comments left in production code
- [ ] Error handling is comprehensive
- [ ] Logging is appropriate (not too verbose, not too quiet)

## Deployment Notes

## Screenshots (if applicable)

## Additional Notes

## Review Requests
2026-05-17 10:15:10 +02:00
Tobias GesellchenandClaude Opus 4.7 f3c974cbbd docs(troubleshooting): capture three recurring symptoms from issues #224 #235 #253
Add three new entries to docs/guides/TROUBLESHOOTING.md so the next
reporter who hits these symptoms finds the answer without needing the
issue thread.

- "Every cloud source shows status=UNAVAILABLE / can't stream anything"
  (Connection Issues). Three-step diagnostic checklist: :443
  reachability preflight, margeAccountUUID check, filtered
  `logread -f`. Distilled from the diagnostic ping on #224 plus
  Thatboioofy's resolution (missing margeAccountUUID was the cause).
  Sidebar clarifies that the firmware-internal placeholder sources
  (SpotifyConnectUserName, SpotifyAlexaUserName, UPnPUserName,
  StoredMusicUserName, QPlay{1,2}UserName, AirPlay2DefaultUserName)
  are speaker-synthesized and their UNAVAILABLE status is never an
  AfterTouch problem on its own.

- New section "Music Service & Preset Issues" with "Spotify preset
  fails with 'Current content cannot be saved as preset'". Explains
  the firmware-side isPresetable="false" gate on Connect-pushed
  playback (foob61451's NowPlaying capture in #235), why an
  OAuth-linked account flips it to true, and cross-links to
  MUSIC-SERVICES.md and the new spotify-overview.md.

- "TuneIn (or Internet Radio) missing from /sources after a factory
  reset". TuneIn is not a default source; the speaker only registers
  it after first play. Captured from the #253 side-thread with both
  app and `soundtouch-cli source content` recipes plus the
  no-SSH caveat for newer hardware (SA-5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 00:12:28 +02:00
b0d7e8aae2 feat(spotify): wire preset storage end-to-end via server-centric priming (#302)
storePreset on the speaker was failing with "AddPreset - failed due to
invalid SourceID" because the watchdog priming path only pushed ZeroConf
credentials and never registered a SPOTIFY ConfiguredSource in marge.

PrimeDeviceWithSpotify now:
- resolves the device's paired account via live :8090/info
(margeAccountUUID), falling back to ServiceDeviceInfo.AccountID — same
order as setup.populateDeviceInfo;
- writes a SPOTIFY ConfiguredSource under that account (providerID=15,
BoseSecret as credential), mirroring bridgeSpotifyToMarge;
- POSTs `<updates><sourcesUpdated/></updates>` so the speaker re-fetches
its on-device Sources.xml from marge.

Also introduce zeroconf.ErrAddUserNoOp for the narrow firmware quirk
(404 + empty body on ?action=addUser when activeUser already matches).
Recognised only on that exact pattern; real 4xx/5xx still surface loudly
with full response details. Same treatment applied to Amazon priming.

Docs:
- new docs/concepts/spotify-overview.md anchors the topic (mental model,
streamingoauth.bose.com DNS gotcha, token lifecycle, clientId notes,
troubleshooting table);
- spotify-oauth.md drops the removed install-primer endpoint and the
on-device boot-primer install sections, adds /mgmt/spotify/prime;
- spotify-priming-strategy.md and MUSIC-SERVICES.md link to the
overview.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:47:57 +02:00
Tobias GesellchenandClaude Opus 4.7 675288a329 docs(migration): add CLI-driven factory-reset alternative
The web-UI wizard is in-place migration: it preserves the speaker's
existing pairing and synced data. The CLI sequence is a different
shape — full factory-reset → wifi-push → pair against AfterTouch
from scratch — and it's the right tool when you want a clean,
scriptable, reproducible setup (automation, batched onboarding, or
just starting from a reset speaker).

Documents the full 6-step CLI flow (plan / factory-reset / wait-ap
/ wifi-push / wait-online / setup pair --mode=full), the verification
checks, and a side-by-side comparison so users can pick the right
path. Placed after "Repeat for each speaker" so the wizard remains
the recommended default for one-off migrations.

The flow assumes #195 and #269 are fixed in v0.80.2 — without the
AUX/sources filter, the CLI factory-reset path produces a speaker
where AUX won't dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:14:18 +02:00
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 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 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 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 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 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
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
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 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 e5b94158e6 Use modern docker compose command syntax 2026-04-06 15:05:44 +02:00
Tobias GesellchenandGitHub ad5344b309 Do not use /bmx for our custom endpoint (#115)
Follow-up for https://github.com/gesellix/Bose-SoundTouch/pull/114
2026-03-16 23:36:36 +01:00
Tobias GesellchenandGitHub 8d95e170f6 Add a custom-radio url stream source (#114)
Based on the descriptions at

- https://gist.github.com/rody64/98a59990ff60ea962cac72cbe93edf56
-
https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/discussions/37

Example usage:

```
go run ./cmd/soundtouch-cli --host 192.... source custom-radio --url https://stream.antenne.de/chillout/stream/aacp --service-url http://soundtouch.local:8000
Selecting custom radio stream from 192....:8090...
  URL: https://stream.antenne.de/chillout/stream/aacp
  Proxy: http://soundtouch.local:8000/bmx/custom/v1/playback/aHR0cHM6Ly9zdHJlYW0uYW50ZW5uZS5kZS9jaGlsbG91dC9zdHJlYW0vYWFjcA==
✓ Custom radio stream selected
```

Relates to https://github.com/gesellix/Bose-SoundTouch/issues/94
2026-03-16 23:17:50 +01:00
Tobias GesellchenandGitHub b8bbc52803 Refine migration guide (#100) 2026-03-07 12:56:25 +01:00
Tobias GesellchenandGitHub d296b59a9e Add/update docs. Some are only in preparation for future improvements and features (#99) 2026-03-06 21:50:41 +01:00
Tobias Gesellchen 5edab77209 feat: add comprehensive TLS certificate SAN support with wildcard domains
- Add RFC-compliant wildcard certificates (*.api.bose.io, *.api.bosecm.com) for automatic API coverage
- Include additional Bose production domains (worldwide.bose.com, music.api.bose.com, bose-prod.apigee.net)
- Implement TLS certificate request logging and wildcard domain matching logic
- Add detailed TLS handshake debugging with connection state tracking
- Wrap TLS listener with logging to capture certificate selection and handshake failures
- Update documentation with wildcard certificate coverage and debugging features
- Normalize test data to use consistent local IP addresses

This enables automatic coverage of all current and future Bose API subdomains
while providing comprehensive TLS debugging for DNS redirection troubleshooting.
2026-02-24 21:47:20 +01:00
Tobias Gesellchen 0b75a2f70d feat: implement robust MAC address to serial number mapping
Enhances device identification by adding MAC address normalization and comprehensive documentation.

- Add `MAC-ADDRESS-MAPPING.md` guide explaining device identification and troubleshooting.
- Implement `normalizeMAC` in `DataStore` to handle various MAC formats (case-insensitive, with/without separators).
- Export `EnrichDeviceInfo` in UPnP discovery to allow better integration and testing.
- Update `TROUBLESHOOTING.md` with a new section on device identification issues.
- Add comprehensive integration and diagnostic tests for MAC mapping, case sensitivity, and UPnP discovery.
- Update documentation structure (`README.md`, `SUMMARY.md`) to include the new mapping guide.
2026-02-24 21:45:40 +01:00
Tobias Gesellchen 9ee1c96477 feat(mirror): add background mirroring and parity analysis for Bose services
Implements the ability to mirror local requests to the official Bose
Cloud in the background, allowing for real-time comparison and parity
analysis between the emulated service and the original backend.

Core Changes:
- Implement `MirrorMiddleware` for asynchronous and synchronous mirroring.
- Add `Parity Logger` to detect discrepancies in status, headers, and body.
- Implement storage for parity mismatches in `data/parity_mismatches/`.
- Add `Internal Paths` configuration to exclude management traffic from logs.

Web UI & API:
- Add "Parity & Mirroring" tab to the Web UI for discrepancy analysis.
- Integrated "Internal Paths" configuration in Settings.
- Add "mirror" category filter to the Interactions UI.
- Implement endpoints for listing and clearing parity mismatches.

Infrastructure & Tools:
- Extend `setup.Manager` with `HTTPGet` override for reliable testing.
- Add CLI flags `--mirror-enabled`, `--mirror-endpoints`, and `--internal-paths`.
- Update `datastore.Settings` to persist mirroring and internal path configurations.

Tests:
- Add `pkg/service/handlers/mirror_test.go` for middleware verification.
- Update `TestProxySettingsAPI` and `TestRecordMiddleware` for new settings.
- Refactor `TestMigrationAndCA` to use mocked network calls (30x speedup).
2026-02-22 22:20:03 +01:00