codeql.yml (GitHub's Advanced Setup template) adapted for this repo:
- Pin action SHAs (checkout v6.0.2, codeql-action v4.36.0)
- Drop python from the language matrix (no Python in this repo)
- Add conditional libpcap install for the Go matrix entry
(gopacket requires libpcap-dev; autobuild fails without it)
- Wire in .github/codeql-config.yml for Go (path filters, query
selection); other languages get an empty config-file value
- Remove boilerplate template comments and the unused manual-build step
- Fix runner expression (no swift, so the macos-latest conditional
is unnecessary; always ubuntu-latest)
security.yml:
- Remove codeql-analysis job (now handled by codeql.yml)
- Drop codeql-analysis from security-summary needs, summary echo,
and fail condition
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nancy was installed from github.com/sonatypecommunity/nancy
which is a non-existent package (correct org is
sonatype-nexus-community). nancy v2.0.0 also has replace-
directive issues that break go install.
govulncheck already covers Go CVE scanning via the official
Go vulnerability database, making nancy redundant here.
The nancy-report.json artifact referenced in the upload step
was never actually produced by the pipeline anyway.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The static-analysis CI job runs 'staticcheck ./...' directly.
Standalone staticcheck uses //lint:ignore directives, not the
//nolint comments that golangci-lint reads.
SA1008 (non-canonical header key) on three ETag lines:
handlers_etag_test.go:228, :270
mac_mapping_integration_test.go:226
ETag must stay non-canonical — Bose speakers reject 'Etag'.
Existing //nolint:canonicalheader / //nolint:staticcheck comments
remain for golangci-lint; //lint:ignore SA1008 is added for the
standalone staticcheck invocation.
U1000 (unused function) on writeBMXUnauthorized in handlers_bmx.go:
The auth gate is temporarily disabled; the helper is kept as a
restore point. //lint:ignore U1000 replaces //nolint:unused because
golangci-lint's staticcheck runner also honours //lint:ignore,
making //nolint:unused redundant (nolintlint would complain).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The links used /guides/ and /reference/ directly, missing the
/docs/ sub-path that Hugo places all content under. They also
had a .html suffix which Hugo's clean URL mode does not produce.
Fix: /Bose-SoundTouch/guides/FOO.html → /Bose-SoundTouch/docs/guides/FOO/
/Bose-SoundTouch/reference/FOO.html → /Bose-SoundTouch/docs/reference/FOO/
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>
The config failed with:
MismatchedInputException "Cannot deserialize value of type
java.lang.String from Array value"
Root causes removed:
- 'uses' in a queries entry must be a string, not an array.
The 'go-security-extra' block used uses: [list] which is invalid.
All the listed queries are already covered by security-extended
and security-and-quality, so the block is simply removed.
- 'reason' is not a valid key under query-filters entries.
Removed from both exclude blocks (one entry had no other
valid keys so the whole exclude was dropped too).
- 'query-config' is not a CodeQL config section at all. Removed.
- 'packs' duplicated codeql/go-queries with an invalid semver
range (@~0.0.0). Removed the section entirely; the queries
package is already loaded transitively by the suites above.
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>
The previous SHA 75d2a84... did not correspond to any real commit in
peaceiris/actions-hugo (there is no v3.0.0 release). Update to the
correct v3.2.1 SHA.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add entries for docs/, examples/navigation-station-demo/, and
examples/preset-management/ alongside the existing root entry.
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>
Files in cmd/ examples/ scripts/ referenced docs/guides/ and docs/reference/
which moved to docs/content/docs/guides/ and docs/content/docs/reference/.
A few links to loose files at the docs/ root were updated to their new
location under docs/content/docs/appendix/.
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
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>
'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>
- 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>
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>
- 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>
Before overwriting the binary, read its version via --version and save
a copy as aftertouch-service.<version>.backup (falls back to a timestamp
if the flag is absent or the build is a dev build).
After the new binary is in place, delete every older *.backup, *.old,
and *.new artefact in INSTALL_DIR. /mnt/nv on SoundTouch SCM modules
has only tens of MB free; accumulating one ~12 MB backup per upgrade
quickly causes 'no space left on device' on the next download.
Only the backup created in this run (the <current-release>-1 binary) is
kept, giving a single one-step rollback point without wasting disk.
Relates to #329 (on-device install friction reported by weissigera).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When handleDiscoveredDevice calls MoveDevice and the target device
directory already exists (pre-existing duplicate state), os.Rename
fails with ENOTEMPTY/EEXIST leaving the stale source account entry
on disk. Because SaveDeviceInfo has just written fresh data under
accountID, it is safe to unconditionally remove the stale source
entry afterward — RemoveDevice returns nil when the path is already
gone (successful rename), so this is a no-op in the happy path and
a cleanup in the failure path.
Adds TestHandleDiscoveredDevice_CrossAccountMigration_TargetExists
which seeds a device under two real accounts (old sorts alphabetically
first so findExistingDeviceInfoByDeviceID picks it as storedAccount),
triggers discovery with the new account as MargeAccountUUID, and
asserts that after the cycle only the new account entry exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exercises the branch in handleDiscoveredDevice where a device's live
MargeAccountUUID differs from its stored account. The test:
- seeds a device + presets under 'default'
- mocks /info to report a different margeAccountUUID ('8637922')
- calls handleDiscoveredDevice
- asserts the device is now stored under the new account with the live name
- asserts the old 'default' entry is gone
- asserts presets survived the MoveDevice rename
- asserts ListAllDevices returns exactly one entry (no duplicates)
Closes the server-level gap noted during PR #348 review.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Health check (checks_stale_internet_radio.go): detects stub INTERNET_RADIO
sources (empty credentials) left on devices initialised before the stub was
removed from the default source list. Quick-fix removes by ID; skips any
INTERNET_RADIO source that has real credentials.
Datastore: DeleteSourceByID and DeleteSourceByType (uniqueness-guarded).
API: DELETE /setup/sources/{account}/{device}/{sourceID}
CLI — two new commands:
soundtouch-cli cloud source remove --service-url ... --account ... --device ... [--id 10002 | --type INTERNET_RADIO]
Talks to AfterTouch (service side). --type resolves to canonical ID
locally; fails for unknown types.
soundtouch-cli source notify-updated --host <speaker-ip>
Talks to the speaker directly. Fetches device ID from /info, then
POSTs sourcesUpdated to :8090/notification so the speaker re-fetches
its source list immediately.
CloudCommonFlags (--service-url / AFTERTOUCH_URL) mirrors CommonFlags
(--host) for AfterTouch-facing command groups.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add getInitialSources() that excludes the legacy INTERNET_RADIO (10002)
provider from newly-created device Sources.xml files. GetDefaultSources()
retains the entry for backward-compatible canonicalisation of existing
devices and cloud-level account responses.
Fix mergeDefaultSources() to rebuild the merged list in canonical ID
order (defaults first, using stored credentials when present, then
custom sources such as Spotify). This prevents INTERNET_RADIO from
landing at the end of the cloud /sources response when a device's
Sources.xml was created without it.
Drop the two verbose search-loop log lines from resolvePresetSource.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The guard was accidentally placed in HandlePlayRadioBrowser instead of
HandleDevicePlay in the initial fix commit, then removed from there by
the build-fix commit — leaving HandleDevicePlay with no guard at all.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous edit accidentally inserted the TUNEIN placeholder guard
into HandlePlayRadioBrowser, which uses a different req struct without
SourceAccount/Source fields, breaking the build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Speakers echo back the source name as SourceAccount when no real
credential is set (e.g. SourceAccount="TUNEIN" for a TUNEIN source).
HandleDevicePlay was forwarding this verbatim, causing the speaker to
try authenticating with the source name as a TuneIn account and
returning INVALID_SOURCE.
Clear SourceAccount when it equals Source; preserve it when it differs
(real credentials such as Spotify or STORED_MUSIC UUIDs).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TuneIn's profiles API caps initial results at ~10 per container (Stations,
Shows, etc.) and exposes a Pivots.More.Url cursor for the remainder. This
change wires that cursor through the stack so users can load additional
results without leaving the search view.
- tuneInSearchSection now extracts Pivots.More.Url as bmx_next when
itemToken is present; absent for containers already at their limit
- TuneInSearchNext fetches the cursor URL, which returns a flat Items[]
(not nested containers), and maps Station/Program/Topic items using
the existing play/profile builders
- New GET /v1/search/next and /api/tunein/search/next endpoints with
matching handlers in both service paths
- TuneInBrowser: flat items state replaced with per-section sections
state; each section shows a header label and a Load more button when
a cursor is available; browse/navigate mode is unaffected
Relates to #336.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When `discovery_enabled` is false the page no longer fires a discovery
scan on load. Both DOMContentLoaded handlers now await fetchSettings()
and gate triggerDiscovery() on the returned flag — default true keeps
existing behaviour for installations that never touched the setting.
Also renames the UI label from "Enable Automated Discovery" to
"Enable Periodic Discovery" to make clear the checkbox controls the
background timer, not the manual trigger button or IP-entry form.
Relates to #269
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
encoding/xml is case-sensitive, so Presets.xml files written by older
AfterTouch versions using <ContentItem> (capital C) had all source,
location, and type attributes silently dropped on read. Every preset for
such a device had empty fields, causing mapPresetsToFullResponse to skip
them all — the speaker received /full with zero presets and stored nothing.
Fix: normalise <ContentItem> → <contentItem> before unmarshaling in the
new readPresetsLocked helper. If normalisation was needed, GetPresets
rewrites the file in canonical form after releasing the read lock, so the
issue self-heals on first service start with no manual intervention.
Diagnosed via the i218 encrypted diagnostic export (device 304511B46CBC,
ST30 Master Bedroom): health check speaker_presets_count reported
"Speaker shows 0 preset slot(s); service Presets.xml has 6", and the
service log showed six [Marge] /full: skipping preset N — source ""
messages per /full call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#337's first commit added the OAuth-derivation to the DNS interceptor
but missed the served TLS certificate. With a serverURL of
`http://mac.fritz.box:8000` the cert SAN list covered `mac.fritz.box`
but not `macoauth.fritz.box`, so the speaker would resolve the OAuth
host correctly (via the new DNS hijack) and then immediately fail the
TLS handshake — Spotify / Amazon Music token refresh dies before
reaching AfterTouch.
getDomains now calls discovery.DeriveOAuthHostnames(serverURL) and
discovery.DeriveOAuthHostnames(httpsServerURL), feeding the derived
names into the SAN map alongside the existing entries. IP-based
serverURLs continue to produce no derivation (the OAuth construction
is unrecoverable for them — see the existing oauth_target_reachable
health check).
Tests in cmd/soundtouch-service/main_test.go lock in:
- Hostname serverURL → derived OAuth variant present in SAN list.
- IP serverURL → no malformed `192oauth.…` entry leaks in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The markdown-link-check CI step caught five stale links in
docs/README.md's Concept Documentation section pointing at files
the previous commit moved into docs/archive/. Replaced with a
pointer to SUMMARY.md's Concepts section + a short curated list
of the currently-relevant docs (Spotify Overview, Spotify OAuth,
Amazon Music OAuth, Encrypted Export, Request Recording). The
archived planning artefacts get a single line acknowledging
their existence under docs/archive/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The speaker firmware constructs the OAuth host by appending "oauth" to
the first label of the configured streaming hostname (aftertouch.lan
→ aftertouchoauth.lan, used by both Spotify and Amazon Music token
refresh). AfterTouch's DNS server previously only hijacked the
hardcoded list of Bose hostnames, so operators self-hosting at a
custom hostname had to add the OAuth alias themselves — and the
amazon-music-oauth.md / spotify-overview.md docs incorrectly
claimed the DNS server handled it automatically.
ofthesun9 (#337) caught this via the worst variant: IP-based
serverURL (192.168.0.30 → 192oauth.168.0.30), which is a malformed
hostname no DNS resolver can answer for. There is no clean DNS
workaround for the IP case — the operator must use a hostname.
Three changes:
- pkg/discovery/dns.go DeriveOAuthHostnames parses the configured
serverURL, derives <first-label>oauth.<rest> when the host is a
hostname (not IP), and adds it to the DNSDiscovery hijack list. IP
serverURLs deliberately yield no derivation — the malformed name
isn't worth handling and the new health check surfaces the trap.
- New checks_oauth_target health check fires a Warning when serverURL
is an IP literal, with a concrete example of the malformed name
(`192oauth.168.0.30`) and a ManualCommand pointing at the switch.
- amazon-music-oauth.md and spotify-overview.md rewritten: drop the
false "automatic" claim, document the three resolution paths
(AfterTouch DNS + speaker resolves via it / external LAN DNS /
per-speaker /etc/hosts), and explicitly flag IP-based --server-url
as incompatible with OAuth on either provider.
Tests cover the derivation matrix (hostname / IPv4 / IPv6 / single
label / empty / garbage URL), shouldIntercept's new behaviour
(derived host hit, base host not auto-hijacked, case-insensitive),
the health check's four states, and the malformed-host helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The TestDocsConsistency walk only iterated [".", "guides", "reference",
"analysis"] — concepts/ was silently invisible, which is why
amazon-music-oauth.md slipped into the tree without a SUMMARY entry.
Refactored to walk the entire docs/ tree, with a small dirsToSkip
allow-list (_includes, archive, diagrams, images) for asset trees.
New top-level narrative directories are picked up automatically;
only asset dirs need an explicit entry.
The wider walk surfaced six previously-hidden concepts/* files. Five
older planning artefacts ("Enhanced State Management System",
"Upstream Bose Service Simulation") moved into docs/archive/ where
the dirsToSkip already excludes them; concepts/README.md renamed to
upstream-service-simulation-overview.md since "README.md" inside
archive/ would be misleading. Spotify Overview and Amazon Music
OAuth are user-facing narrative docs and are now linked under
Concepts in SUMMARY.md.
Note: concepts/streborn-patterns.md is internal review notes (its
own opening line says so) and is currently unlinked from SUMMARY.md;
will be handled separately by the maintainer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Discovery cycles emit one line per UPnP M-SEARCH header, one per
parsed response, and one per enrichment step — by default. A typical
service-binary cycle prints ~50–80 lines for a 3-speaker LAN. Most
operators want a startup-and-summary view; the per-packet trace is
only useful for debugging.
- New SetVerbose/IsVerbose/logVerbose helpers in pkg/discovery (atomic
bool, zero-value off).
- Chatty log.Printf calls in upnp.go and mdns.go demoted to logVerbose:
per-header dumps, per-response dumps, per-device enrichment steps,
M-SEARCH details, read-deadline / cancel-context noise.
- Kept at default level: discovery start ("Starting SSDP discovery
for…"), end ("Discovery completed. Processed N responses, found N
unique devices" + per-device summary), warnings ("Configured
interface not found", "Failed to fetch device description", …), and
the new "Rejecting non-Bose device" classifier.
- cmd/soundtouch-cli/discover devices grew a --verbose / -v flag that
flips the package toggle on; the service binary leaves it at the
zero value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four small, independent improvements bundled into one cut:
1. Restrict device discovery to SoundTouch-family services (#269/#359).
- mDNS now queries all three SoundTouch service-type variants in
parallel (_soundtouch._tcp, _bose-soundtouch._tcp, _soundtouchstick._tcp)
and deduplicates results by host:port. mDNS has no native wildcard
for service types, so we fan out one query per variant.
- UPnP/SSDP M-SEARCH receives a manufacturer/modelName check after
fetching the device description: devices whose manufacturer doesn't
contain "bose" AND whose model doesn't contain "soundtouch" are
rejected. Closes the loop on NorbertBauer's diagnostic bundle that
showed a Dreambox dm920 and Onkyo HT-R695 living under the default
account because they answered our generic MediaRenderer:1 probe.
2. New health check: default-account-contains-non-Bose-devices (#269).
Walks devices keyed under data/accounts/default/devices/, flags any
whose ProductCode/Name doesn't look SoundTouch, and offers an Evict
QuickFix. Bose devices still in default (legitimate pre-pair) are
intentionally ignored — that's the consistency check's domain.
3. Clipboard fallback for Copy buttons (#355). The two health-tab Copy
buttons used navigator.clipboard.writeText, which requires a secure
context. Over plain HTTP at a LAN IP the browser blocks it silently
and the button shows "Copy failed". New copyTextToClipboard helper
tries the modern API first, falls back to document.execCommand("copy")
via an off-screen textarea.
4. Web UI static-asset cache-busting (#345). dekiesel needed Ctrl+F5 to
see the v0.89 Download button after upgrade. The root HTML now
carries a ?v=<hash> query string on /web/js/script.js and
/web/css/style.css references. Hash is sha256 over the embedded asset
bodies, truncated to 12 hex chars — stable per binary, changes when
the assets change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>