- 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>
Three links in pkg/service/handlers/web/index.html still pointed to
the old Jekyll URL structure (/guides/FOO.html). The docs site moved
to Hugo+Hextra; correct URLs now include /docs/ and drop the .html
extension in favour of a trailing slash.
MIGRATION-SAFETY.html → docs/guides/MIGRATION-SAFETY/
SURVIVAL-GUIDE.html → docs/guides/SURVIVAL-GUIDE/
CLI-REFERENCE.html → docs/guides/CLI-REFERENCE/
The GitHub blob links in script.js and the hostname-resolution warning
in index.html point to source Markdown files and remain valid.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes CodeQL go/log-injection alerts in the final batch of packages.
New logutil.go helpers: pkg/client, pkg/testutils/amazon,
pkg/testutils/spotify, cmd/soundtouch-service, cmd/soundtouch-web,
cmd/dummy-speaker, cmd/mdns-scanner.
pkg/discovery/logger.go: added sanitizeLog and a nil-safe
remoteAddrString helper to the existing file (alongside logVerbose).
Call sites wrapped across 11 files — device IDs, source types,
hostnames, IPs, interface names, URLs, service names, HTTP method/form
values, WebSocket URLs and payloads, TLS SNI names, remote addresses.
No behaviour change. golangci-lint and make check pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
autobuild is a black box — if it fails for any reason (CGO/libpcap
timing, module cache, etc.) no SARIF gets uploaded and GitHub reports
'1 configuration not found: /language:go' on the PR.
Switching to build-mode: manual with an explicit 'go build ./...'
step placed after CodeQL init (so the build is traced) gives us a
deterministic, visible build step. libpcap-dev is still installed
before init so the CGO dependency is satisfied.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes CodeQL go/log-injection alerts in the handlers package.
Adds pkg/service/handlers/logutil.go with a package-private
sanitizeLog helper that strips \n and \r from strings before they
reach log call sites. Values from speakers, HTTP requests, and
external APIs (device IDs, account IDs, IP addresses, speaker names,
OAuth user IDs/emails, station IDs, URL paths, user-agent strings)
may contain attacker-controlled newlines.
Wraps all external-data string arguments across 12 files:
handlers_account_mgmt.go, handlers_alexa.go, handlers_bmx_orion.go,
handlers_bmx_siriusxm.go, handlers_bmx_tunein.go, handlers_catchall.go,
handlers_export.go, handlers_marge.go, handlers_mgmt.go,
handlers_oauth.go, origin_middleware.go, server.go.
No behaviour change — purely a logging concern. make check passes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two Python scripts are tracked in the repo (scripts/convert_mitm_script.py,
scripts/patch-stockholm-bridge.py). The original GitHub-generated codeql.yml
included language:python; our adapted version dropped it unintentionally.
Restores parity with what GitHub auto-detected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes CodeQL alerts 280 and 281 (go/unhandled-writable-file-close).
scripts/extract-ws/main.go: change bare 'defer f.Close()' to
'defer func() { _ = f.Close() }()' — function returns void, silent
discard is the correct pattern (matches existing '_, _ = w.Write()'
usage elsewhere).
pkg/service/certmanager/certmanager.go: sequence encode + close for
both the cert file and the key file, checking both errors. This also
fixes resource leaks on the pem.Encode error path (file was previously
left open when encode failed). Matches the established pattern in
handlers_export.go (tw.Close / gz.Close).
.gitignore: exclude CODE-SCANNING-NOTES.md (local working notes;
will be added to VCS once the scanning sweep is complete and the
notes are stable).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>