The device-API coverage docs had drifted from the code. Verified each claim
against pkg/client and corrected:
- UNIMPLEMENTED-ENDPOINTS.md: re-marked endpoints now implemented but still
listed as candidates — setMusicServiceAccount / removeMusicServiceAccount and
the stereo-pair group set (getGroup/addGroup/removeGroup/updateGroup); added a
reconciliation note and clarified this tracks the speaker :8090 API, not the
service router.
- SUPPORTED-URLS.md: fixed the "Not Yet Implemented" lists (music services,
presets, stations, navigate, speaker, requestToken/notification/playNotification
are all implemented), the contradictory storePreset double-listing, the native
group section, and the System Info over-claim (trackInfo non-functional,
bluetoothInfo not implemented).
- API-COVERAGE.md: fixed the exec-summary count (18/19 -> 20/21) to match its own
table and refreshed the date.
Also sanitised a real device ID (08DF1F0BA325 -> placeholder) found in
SUPPORTED-URLS.md, per the repo's no-real-MACs rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- architecture/_index.md: list the section's docs with links.
- reference/CLOUD-API.md: "See also" pointer (service cloud-emulation routes).
- reference/API-ENDPOINTS.md: note distinguishing the speaker device API from
the service route layout, with a link.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Architectural reference for the staged API refactoring that precedes the
soundtouch-web / soundtouch-service merge:
- Route classification by client audience and what pins each path (frozen
firmware contract vs externally-pinned OAuth callbacks vs our movable
admin/control surface), with service + web route tables.
- Actors model (speaker / app / cloud) and deployment topologies; speaker-direct
vs data-plane reachability.
- deployment-mode parameter (private/shared/public), trust tiers, auth posture
(opt-none -> opt-in -> opt-out?), and auth mechanisms (Marge as one auth
provider like EntraID; native/headless clients via RFC 8252 loopback or a
headless token; identity in logs).
- /app/* single role-gated app with code-splitting for on-device size.
- Versioning policy: no path versioning; semver with 0.x dual-routing and a 1.x
cutover that removes obsolete routes.
- Staged migration (add+alias, fold in web, deprecate the binary, observable
old-route warnings) with a "before 1.x" definition of done.
- Regression safety: contract tests from the frozen recordings, building on the
existing tests/integration/http-client suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
soundtouch-web's "Speak" feature proxies to the AfterTouch service's
/setup/tts/speak endpoint. Two issues blocked it end to end.
1. TLS: the proxy used http.DefaultClient, which trusts only system
roots, so the HTTPS call to a service using its own self-signed CA
failed with "x509: certificate signed by unknown authority". Add a
--service-ca flag (SERVICE_CA env) that loads the CA PEM, appends it
to the system pool, and uses a custom client for the TTS call.
2. Target: soundtouch-web sent device.Client.Host() (a full base URL
like http://ip:8090), but the service's SSRF guard exact-matches the
target against bare datastore IPs, returning "host ... is not a known
device". Prefer the device ID (the canonical key) and send a bare-IP
host fallback. Also normalize the incoming host in resolveTTSHost so a
URL/host:port form still resolves; it still only ever returns a
datastore IP, so the SSRF guarantee is unchanged.
Adds unit tests for the CA client builder, hostOnly, and resolveTTSHost
(including the preserved unknown-host/device rejections). Documents
--service-ca in the soundtouch-web README and TROUBLESHOOTING guide.
Wires SERVICE_URL and SERVICE_CA (empty defaults) into the Raspberry Pi
install-web.sh env file and documents them in the Pi guide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update v0.103.0 -> v0.104.0 across installer scripts, walkthrough docs,
and example go.mod files, and refresh the devices/migration/settings/sync
UI screenshots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeQL flagged "uncontrolled data used in network request": the
soundtouch-web TTS proxy built its outbound request URL from the
client-supplied serviceUrl, letting any LAN caller use the endpoint as an
SSRF proxy. The proxy target must be the operator-configured --service-url.
- handler: use only app.ServiceURL; drop the client-supplied serviceUrl
field and fallback.
- web TTS view: show the configured service URL read-only with an
explanation of why it can't be edited here (Play URL differs — its URL
is handed to the speaker, not fetched by soundtouch-web, so no SSRF).
- api.speak no longer sends serviceUrl.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
soundcork#104 confirms speakers validate the /speaker audio-notification
app_key against audionotification.api.bosecm.com (100 calls/day on real
Bose). Our /v1/auth shim accepts it, but a host-seeded migration only
worked if the speaker resolved that host to us. DNS interception already
covers it (bosecm.com substring), but the /etc/hosts migration domain
list did not — so the speaker method would fail on hosts-based setups.
Seed both audionotification.api.bosecm.com and the dev variant
(audionotificationdev.api.bosecm.com; firmware may use either) into the
migration /etc/hosts lists, and update the mock fixtures/docs accordingly.
/v1/auth is path-based, so it already answers regardless of which host the
speaker thinks it is calling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confirmed working on a real speaker (Bose_Lisa/27.0.6): the speaker GETs
/v1/auth at audionotification.api.bosecm.com (DNS-redirected to us) with
the app_key in an "Apikeyheader" header, and an empty 200 is sufficient.
- Make "speaker" the default playback method (ducks + resumes the current
playback, supports volume) for the speak endpoint, the CLI --method flag,
and the web UI button; "radio" remains opt-in.
- Remove the temporary full-request debug dump from /v1/auth now that the
contract is understood; document it in the handler comment instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the awkward top-level `tts speak --speaker-host` with a
`speaker tts-cloud` subcommand that sits alongside the existing
`speaker tts` and uses the global --host flag (--device still works as
an alternative). The two are now clearly related: `speaker tts` sends a
Google Translate URL straight to the speaker, while `speaker tts-cloud`
routes through the service for server-side synthesis (Cloud TTS) and
playback. --speaker-host is gone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The TTS speak/config endpoints were under /mgmt (Basic-Auth protected),
but the soundtouch-web proxy and CLI authenticated with their own
mgmt-password default (empty) while the service defaults to "change_me!",
so speaking from -web returned 401.
This was also inconsistent: the Google API key is configured via the
unauthenticated /setup/settings, and Play URL already proxies to /setup,
so gating only TTS playback behind mgmt auth made no sense. Move
/mgmt/tts/{speak,config} to /setup/tts/{speak,config} (LAN-trust, like
the rest of the setup surface), rename the handlers accordingly, and drop
the now-unused mgmt-credential plumbing from soundtouch-web and the CLI
tts command.
Verified: POST /setup/tts/speak now reaches the handler without auth
(502 only because the test speaker IP is unreachable; previously 401).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds text-to-speech that synthesizes higher-quality audio (Google Cloud
TTS) and plays it on a speaker via the /speaker endpoint. Because Cloud
TTS returns audio bytes (not a fetchable URL), the service caches the
clip and hosts it at GET /media/tts/{id}, mirroring the "ding" endpoint,
then points the speaker at that local URL.
The design is a pluggable Provider interface (pkg/service/tts) wrapping
two modes:
- translate: hands the speaker the (undocumented) Google Translate URL
directly (no credentials), reusing models.BuildTranslateTTSURL.
- google-cloud: REST API key auth (no SDK/gRPC), bytes cached locally.
Surfaces:
- service: POST /mgmt/tts/speak, GET /mgmt/tts/config, GET /media/tts/{id};
configured via TTS_PROVIDER / TTS_GOOGLE_API_KEY / TTS_LANGUAGE /
TTS_VOICE / TTS_APP_KEY / TTS_VOLUME.
- CLI: `soundtouch-cli tts speak` (calls the service with mgmt Basic Auth).
- web: a "TTS" source view (like Play URL / TuneIn), proxied to the
service via /api/device-speak/{id}.
The /speaker app_key requirement and model limitations still apply; see
docs/content/docs/reference/SPEAKER-ENDPOINT.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `find` family runs the search inside the CLI, querying the radio
provider's public API directly (no speaker cloud, no soundtouch-service).
Make it the canonical path and deprecate the speaker-based search family.
- Add `find-tunein` and `find-radiobrowser` siblings; refactor the find
actions onto a shared `runFind` helper (all support `--more`).
- Rename the unreleased `search-radiobrowser` to `find-radiobrowser`.
- Deprecate `search`, `search-tunein`, `search-pandora`, `search-spotify`:
they keep working but print a stderr deprecation notice (new
`PrintDeprecation` helper) pointing at the `find*` replacement. Pandora
and Spotify have no built-in equivalent yet (they need the speaker +
account), so their notices say so.
- Docs: lead with the `find` family as recommended; mark the speaker-based
search commands deprecated; drop the misleading "service-side" wording
in favour of "built-in / queries the provider directly".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add CLI-REFERENCE entries for the new service-side search commands
(`station find --provider tunein|radiobrowser [--more]` and
`station search-radiobrowser`), with a subsection explaining they run
the search in AfterTouch itself — working without the speaker's live
cloud and without a reachable --host. Also document the pre-existing
but undocumented `station list`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents the iptables block that SoundTouch firmware (since 2018)
applies to traffic from other subnets, which prevents AfterTouch from
being reachable when the speaker and server are on different VLANs.
Two fixes: targeted ACCEPT rule (from spookie85, discussion #354) and
the simpler DROP-line comment-out (from dekiesel). Also notes the ST20
Series I outbound-port restriction on non-standard ports (gmuth).
Outgoing link kept to our own discussion #354 for attribution; the
external third-party issue link is omitted as it may go stale.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Following user feedback (Lang, issue #432 thread) the docs guides now
contain all operational detail — installation, configuration, service
management, logs, updates, and removal — and the scripts READMEs become
thin pointers to the docs rather than the other way around.
RASPBERRY-PI.md: expanded to cover soundtouch-web alongside
soundtouch-service (install, config, port-conflict note, service
management, logs, update, removal, arch auto-detection, security).
scripts/raspberry-pi/README.md: trimmed to a quick-start with the two
one-liners plus a link to the docs guide.
EXTERNAL-HOST-WALKTHROUGH.md Step 7: replaces the vague "download from
Releases" note with the actual install-web.sh one-liner and a link to
RASPBERRY-PI.md#soundtouch-web; adds a non-Pi install option too.
ON-DEVICE-INSTALL-WALKTHROUGH.md: removed both back-references to
scripts/on-device-install/README.md; added self-contained sections for
Updating (with rollback tip), Service management, Logs, and Uninstalling
so the walkthrough is complete without leaving the docs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hextra's production build bundles assets/css/custom.css into
css/compiled/main.css. The original '../fonts/' relative path resolved
correctly from css/custom.css (dev) but landed at css/fonts/ in
production — one directory too deep.
Fix: use '../../fonts/' so the URL resolves correctly from every
output location browsers may encounter:
dev: /css/custom.css → ../../fonts/ → /fonts/
production: /css/compiled/main.css → ../../fonts/ → /fonts/
GH Pages: /Bose-SoundTouch/css/compiled/main.css
→ ../../fonts/ → /Bose-SoundTouch/fonts/
Browsers clamp traversal at the origin root, so going two levels up
from /css/custom.css still reaches /fonts/ — safe in dev, correct in
production.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EXTERNAL-HOST-WALKTHROUGH.md — Step 7 "Via soundtouch-web":
Replaced the single save path with two labelled options:
- ★ Star button: appears in the Now Playing card's top-right corner,
opens a slot picker (1–6), turns gold once mapped.
- + button: appears on each preset tile on hover, saves directly to
that slot without a picker.
Added a one-liner on when to use each.
PRESET-QUICKSTART.md:
New "Via soundtouch-web (browser UI)" section added above the CLI
section, covering both the ★ star and + paths with step-by-step
instructions.
soundtouch-web-roadmap.md:
- Added a "Shipped" callout noting that preset-slot saving is done.
- Retitled the Favorites section to "Favorites (device-native, distinct
from presets)" and added a note clarifying it refers to the speaker's
/favorites API (different from the 6 preset slots) which is still
pending.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stripping the .md extension alone is not enough under Hugo pretty URLs.
A page rendered at /guides/DEPLOYMENT-OVERVIEW/ treats a bare relative
href like 'CLOUD-DEPLOY-WALKTHROUGH' as relative to that directory,
producing /guides/DEPLOYMENT-OVERVIEW/CLOUD-DEPLOY-WALKTHROUGH (404).
Switch to site.GetPage to look up the target page by its content path
(resolved relative to the current file's directory) and write its
RelPermalink into the href. This gives an absolute path that is correct
in both the dev server and the GitHub Pages build (where --baseURL
injects the /Bose-SoundTouch/ prefix via RelPermalink automatically).
Also handles anchored links (OTHER.md#section) and falls back to
bare-stripped path when GetPage finds no match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The docs framework renders frontmatter title: as the page heading.
Every file that also had a matching # Heading as the first content
line displayed the title twice. Removed the redundant H1 and its
following blank line from all 91 affected files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration guide: expand the one-liner after SSH setup into a concrete
'To disable SSH' section covering both the USB-stick and persistent-file
cases, with the button name and CLI command.
Admin UI:
- Preconditions label: 'remote_services' → 'SSH (remote_services)'
with a tooltip explaining the connection
- Buttons: 'Enable/Remove Persistent Remote Services' →
'Enable SSH (Persist remote_services)' /
'Disable SSH (Remove remote_services)'
- Confirm dialog: mentions SSH and reboot requirement explicitly
- Verdict text: all three states now lead with 'SSH ...' so users
recognise what the check controls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use relURL (no leading slash) for the sponsor link so it respects
the /Bose-SoundTouch/ base path on GitHub Pages; absURL and relURL
both ignore the base path when the input starts with /
- Inject HUGO_PARAMS_GITHASH (github.sha) via the docs workflow and
forward it into the Hugo container via docker-compose.docs.yml +
make dev-docs, so the deployed footer shows a clickable short hash
linking to the exact commit
- Use site.Params.githash (global) instead of .Site.Params.githash
because Hextra calls custom/footer.html with a dict context, not a
page; .Site is nil in that scope
- Use substr not slice to trim the hash to 7 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
$logoLink is sourced from site config (never user input) and Hugo
auto-escapes template values. Pipe through safeURL to make the intent
explicit and satisfy the generic.html-templates.security.var-in-href rule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /sponsor landing page lists both options with feature cards
- Navbar heart icon and footer sponsor link both point to /sponsor
instead of directly to GitHub Sponsors, so PayPal is equally reachable
- No GitHub account required for PayPal path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration Guide step 1:
- Add 'Download pre-built binary' as the first option (no Go required)
- Add install-script option for Raspberry Pi / on-device deployments
- Move 'go install' to last (developer option)
- Add data/ directory callout: single directory to back up for a full restore
SoundTouch Service guide:
- Mention RadioBrowser alongside TuneIn in the BMX section
- Add soundtouch-web TuneIn search screenshot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous link aimed at a Go-developer getting-started page. Most
users are not Go developers — they want to migrate their speakers.
MIGRATION-GUIDE is the right first destination.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers what AfterTouch delivers today: migration (existing account and
factory-reset paths), marge+bmx replacement, TuneIn+RadioBrowser, Spotify,
presets, ST-10 stereo pairing, soundtouch-cli automation, soundtouch-web
browser UI, and the three installation options (on-device, local host /
Raspberry Pi Zero 2W, cloud/VPS).
Includes screenshot of the soundtouch-web UI (Spotify playback, presets,
sources, zone management).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- blog/_index.md: add introductory sentence to the News & Updates index
- .claude/commands/blog-update.md: project skill that drafts a monthly
update post from git history and opens a draft PR for review
- .gitignore: .claude/* + !.claude/commands/ so the skill is tracked
while session state (settings.local.json, worktrees/) stays ignored
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Enable navbar logo (favicon-braille.svg, 24×24)
- Override navbar-title partial to add 'Bose SoundTouch Toolkit' subtitle
- Add favicon.svg to static root (picked up by Hextra head automatically)
- Custom footer: sponsor link (left) + copyright (right) in a single row
- i18n/en.yaml: copyright text with link to github.com/gesellix
- hugo.toml: blog list sorted by date desc, tags enabled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The hextra/hero-button and hextra/feature-card shortcodes call
Hugo's relURL on any link starting with '/'. relURL prepends the
baseURL sub-path — but the deployed site was producing /docs/...
instead of /Bose-SoundTouch/docs/..., meaning relURL was seeing
a baseURL with no sub-path (likely just the domain).
Rather than depend on relURL working correctly at build time,
remove the leading slash from all four internal links. Bare paths
are emitted verbatim by the shortcode and are resolved by the
browser relative to the page's own URL (/Bose-SoundTouch/ on
GitHub Pages, / on local dev) — correct in both environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
actions/configure-pages v5+ exports HUGO_BASEURL automatically,
which overrides hugo.toml. By adding id: pages to the step and
passing --baseURL explicitly, we get the correct sub-path
(https://gesellix.github.io/Bose-SoundTouch/) on GitHub Pages
while local dev (docker-compose.docs.yml already passes --baseURL /)
continues to work unchanged.
Also change hugo.toml baseURL to '/' as the neutral local default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extend image ignorePattern in markdown-link-check.json to cover all
/images/*.png (covers ui-settings, ui-devices, ui-sync, ui-migration,
speaker-ap-wifi-setup that live under docs/static/images/ but are
referenced as absolute /images/ paths in Markdown)
- Fix appendix cross-section links: add ../ prefix to guides/, reference/,
and analysis/ paths in PRESET-QUICKSTART, SOUNDTOUCH-SERVICE-ANNOUNCEMENT,
CONTENT-SELECTION-IMPLEMENTATION, DEVICE-LOGGING, NAVIGATION-GUIDE,
PARITY-SOUNDCORK, and CLAUDE.md
- Convert ../examples/* relative links in appendix to GitHub URLs (the
examples/ dir is at repo root, not under docs/content/)
- Fix CLAUDE.md in appendix: archive/PLAN.md → ../../../archive/PLAN.md;
remove dead PDF link
- Fix TROUBLESHOOTING.md: ../DEVICE-LOGGING.md → ../appendix/DEVICE-LOGGING.md
- Fix CAPTURE-DEVICE-PAIRING.md: ../DEVICE-SETUP.md → ../appendix/DEVICE-SETUP.md
- Fix RASPBERRY-PI.md: remove accidental ../ prefix from GitHub URL
- Fix CONTRIBUTING.md: update docs/reference/ and docs/PROJECT-PATTERNS.md
to their new paths under docs/content/docs/
- Fix README.md: update deployment overview link to new path
- Fix BASS-CONTROLS.md and SOURCE-SELECTION.md: convert ../../pkg/models/
relative links to GitHub URLs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add docs/go.sum (Hextra v0.12.3 checksums) produced by hugo mod tidy.
Update docs/go.mod with the resolved module version.
Ignore docs/.hugo_build.lock, docs/public/, and docs/resources/ —
all are generated by Hugo locally and not needed in the repo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace docs/_config.yml + docs/SUMMARY.md with Hugo + Hextra theme.
Move all content into docs/content/, images into docs/static/images/.
Update docs_consistency_test.go to check Hugo front matter instead of
SUMMARY.md inclusion. Update CI workflow and screenshot script paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- DEVICE-LOCAL-INSTALL.md: old path at docs/ root → docs/architecture/
- Add Deployment Overview + three walkthrough pages under User Guides
- Add Architecture section for the planning doc