Compare commits

...
90 Commits
Author SHA1 Message Date
Tobias GesellchenandClaude Sonnet 4.6 172e14dc26 ci: pass COMMIT and DATE build args to Docker builds
ci.yml's Docker job was missing the build-args introduced alongside
the Dockerfile ARG/ldflags changes. COMMIT and DATE are now injected
into both soundtouch-service and soundtouch-web CI builds; VERSION
stays 'dev' (the Dockerfile default) since CI builds aren't tagged
releases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:30:15 +02:00
Tobias GesellchenandClaude Sonnet 4.6 6c993f47a8 fix(docker): inject version/commit/date via build args (closes #422)
The Docker build excluded .git via .dockerignore, so Go's debug.ReadBuildInfo()
found no vcs.revision / vcs.time settings and the binaries reported
version=dev, commit=unknown, date=unknown in the web UI.

Two fixes:

1. Dockerfile — declare ARG VERSION/COMMIT/DATE (default to dev/unknown/unknown
   so local docker build still works) and pass them to both go build commands
   via -X main.version/commit/date ldflags. Also add the -trimpath and -s -w
   flags that the Makefile's BUILDFLAGS already uses but the Dockerfile was
   missing.

2. release.yml — add a 'Set build date' step, then pass build-args with
   VERSION, COMMIT (full SHA), and DATE to both docker/build-push-action
   steps. The .git exclusion in .dockerignore stays correct; version info
   is now supplied explicitly instead of being read from VCS at build time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:30:15 +02:00
Tobias GesellchenandClaude Sonnet 4.6 58a5adde5e fix(service): include configured bind address in startup log
The 'listening on' message now shows both the configured address
(config.addr, e.g. ':8000') and the true effective address returned
by the listener (e.g. '0.0.0.0:8000'), making it immediately clear
which port was requested and which was actually bound:

  Go service listening on 0.0.0.0:8000 (configured: :8000, server URL: http://192.0.2.1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:20:20 +02:00
Tobias GesellchenandClaude Sonnet 4.6 417c0223dd feat(health): add server_url self-reachability check + log actual listen port
The most common misconfiguration on install-on-speaker setups is an
HTTP server URL that omits the port (e.g. http://192.0.2.1 instead of
http://192.0.2.1:8000). Port 80 is occupied by the Bose firmware's
PtsServer, so AfterTouch binds its default port 8000 — but the
margeURL pushed to speakers still resolves to port 80 and hits
PtsServer instead of AfterTouch. Marge calls are silently dropped,
sources are never registered, and TuneIn playback fails with error
1005 (UNKNOWN_SOURCE_ERROR). See issue #319.

Changes:
- pkg/service/health/checks_server_url.go — new health check
  (server_url_reachable) that probes GET {serverURL}/setup/version from
  inside the service; emits SeverityWarning with remediation steps when
  the endpoint is not reachable or returns non-200.
- pkg/service/handlers/server.go — register the new check in NewServer.
- cmd/soundtouch-service/main.go — replace http.ListenAndServe with an
  explicit net.Listen so the true effective port is logged before TLS
  starts. Both HTTP and HTTPS log lines now show the listener's actual
  bound address alongside the configured server URL:
    Go service listening on 0.0.0.0:8000 (server URL: http://192.0.2.1)
  Previously only the server URL was logged, creating the false
  impression that AfterTouch had bound that URL's implicit port.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:20:20 +02:00
Tobias GesellchenandClaude Sonnet 4.6 93bc04b334 fix(docs): fix font 404 on GitHub Pages (../../fonts/ path in custom.css)
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>
2026-05-26 23:19:04 +02:00
Tobias GesellchenandClaude Sonnet 4.6 aafc5ba3f9 fix(datastore): stop INTERNET_RADIO from being re-added on service restart
initializeDefaultSources() called GetDefaultSources(), which includes
the legacy INTERNET_RADIO stub (ID 10002). On every service start it
would re-add that entry to any device whose Sources.xml had it removed
— including devices where the stale_internet_radio health-check quick
fix was applied — silently undoing the clean-up.

getAccountSources() in marge.go had the same issue: it passed the full
default list into the /full cloud response, causing a phantom
"sources_xml_diff" Info finding after a clean-up.

Fix: export the existing private getInitialSources() as
GetInitialSources() (excludes INTERNET_RADIO) and use it in both call
sites instead of GetDefaultSources().

Existing devices that still have INTERNET_RADIO in their Sources.xml
are unaffected: the merge loop only appends entries that are missing,
so a present entry is preserved (the token is refreshed as before).

Update unit and integration test expectations accordingly: the no-device
fallback now returns 3 cloud sources (LOCAL_INTERNET_RADIO, TUNEIN,
RADIO_BROWSER) instead of 4 (dropping INTERNET_RADIO / ID 10002).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:51:35 +02:00
Tobias GesellchenandClaude Sonnet 4.6 6c4c420b29 docs: document soundtouch-web preset-saving UI (★ star and + button)
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>
2026-05-26 22:47:13 +02:00
Tobias GesellchenandClaude Sonnet 4.6 e117b472b1 feat(soundtouch-web): add save-as-preset from Now Playing and preset tiles
Two complementary ways to save what's currently playing to a preset slot
without leaving the web UI:

★ Star button (Now Playing card)
  A semi-transparent star appears in the top-right corner of the Now
  Playing card whenever a device is selected and something is playing.
  Clicking it opens a slot picker (1–6); selecting a slot calls
  POST /api/control/{id}/storepreset?id={slot}.  The star turns gold
  when the current ContentItem is already mapped to at least one preset,
  matching the preset list by Source + Location.  An outside-click
  closes the picker without saving.

+ button (preset tiles)
  While content is playing each of the six preset tiles shows a small +
  button on hover.  Clicking it saves directly to that slot — no picker
  needed.  The button cycles through +  →  ✓  →  (reset) states with
  a 1.5 s success flash and shows ✗ briefly on error.

Backend (handler.go):
  New "storepreset" case in handleControlAction dispatches to
  handleStorePreset, which validates the ?id= query param (1-6) and
  calls device.Client.StoreCurrentAsPreset(presetID).

Frontend (api.js):
  storePreset(deviceId, slotId) helper added.

CSS (app.css):
  .preset-slot-wrap wrapper + .preset-save-btn styles for the + button,
  source-specific --slot-color custom properties for border accents,
  .now-playing-fav-wrap / .now-playing-fav-btn / .now-playing-fav-overlay
  for the star button and its popover (right-aligned, z-index: 50).
  position: relative added to .now-playing so the star can be absolutely
  positioned without being clipped by .track-info overflow: hidden.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:47:13 +02:00
Tobias GesellchenandClaude Sonnet 4.6 ab5bd82fbc fix(client): copy Art.URL into ContainerArt when storing preset
StoreCurrentAsPreset only used ContentItem.ContainerArt for the stored
artwork URL. For Spotify (and some other streaming sources) the speaker
populates the top-level NowPlaying.Art.URL field instead, leaving
ContainerArt empty, which caused preset tiles to show as text-only.

When ContainerArt is empty and Art.URL is present with artImageStatus
IMAGE_PRESENT, copy the URL into a shallow-copy of the ContentItem
before storing it. Devices where ContainerArt is already set are
unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:47:13 +02:00
Tobias GesellchenandClaude Sonnet 4.6 d28c806903 fix(install): clear default Spotify redirect URI
The hardcoded default 'ueberboese-login://' scheme was a leftover from
an earlier Spotify callback flow that no longer applies. An empty default
is correct — the value is set by the user during installation if they want
Spotify support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:47:13 +02:00
Tobias GesellchenandJunie 09770f55e7 docs: use local Noto Sans font
Co-authored-by: Junie <junie@jetbrains.com>
2026-05-26 22:47:13 +02:00
Tobias GesellchenandJunie e09476da79 docs: enable search menu item in navbar
Co-authored-by: Junie <junie@jetbrains.com>
2026-05-26 22:47:13 +02:00
Tobias GesellchenandClaude Sonnet 4.6 7c71818027 fix(docs): resolve .md links to page RelPermalink in render hook
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>
2026-05-26 00:01:45 +02:00
Tobias GesellchenandClaude Sonnet 4.6 de239d8396 fix(migration): show warning instead of error when URLs migrated to different target
When isXMLMigrated and isTelnetMigrated both return false, the UI fell
through to the  "Original (Bose cloud)" catch-all even if the speaker's
on-device URLs clearly point to a non-Bose host. This happened when the
service's Settings Target Domain and the URL written to the speaker had
drifted — e.g. migrated with http://spotify:8000 but Settings URL is an
IP address, or vice versa.

Add isMigratedToOtherTarget() that checks parsed_current_config: if at
least one URL field is set and none contain a known Bose cloud hostname,
the speaker has been migrated, just not to the *current* Settings Target
Domain.

- urlConfigVerdict now returns ⚠️ "Migrated (URL mismatch)" in this case,
  showing the actual margeServerUrl and noting that the speaker must be
  able to reach the service there
- The top-level migration status badge shows ⚠️ orange instead of  red
- The apply plan path is unchanged: it will re-point the speaker to the
  current Settings Target Domain, which is one valid resolution path

Related to #408

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:19:45 +02:00
Tobias GesellchenandClaude Sonnet 4.6 7d9f3d6a39 docs: remove duplicate H1 headings from 91 pages (closes #414)
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>
2026-05-25 21:48:43 +02:00
Tobias GesellchenandClaude Sonnet 4.6 d93d9a3e26 docs+ui: surface SSH context for remote_services (closes #409)
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>
2026-05-25 21:46:57 +02:00
Tobias GesellchenandClaude Sonnet 4.6 bec52b87a5 fix(test): drop testing.Short() — env var alone gates the live test
testing.Short() would silently suppress the test even with
RADIOBROWSER_INTEGRATION=1 set, contradicting the skip message.
The env var opt-in is sufficient on its own.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:27:10 +02:00
Tobias GesellchenandClaude Sonnet 4.6 4799eab7e5 fix(test): skip TestRadioBrowserSearch_Real unless RADIOBROWSER_INTEGRATION=1
The test dials all.api.radio-browser.info directly. When the upstream
TLS certificate expires the test fails and blocks the build — the local
codebase has no control over third-party certificate health.

Guard with testing.Short() and an opt-in env var so CI stays green and
the live-network test can still be run explicitly when needed.

Closes #412

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:27:10 +02:00
Tobias GesellchenandClaude Sonnet 4.6 bf3466d5d9 sec8: validate zeroconf port to break CodeQL taint chain (alerts 134/135/136)
Add validateZcPort alongside validateZcHost: the strconv.Atoi→Itoa
round-trip produces a sanitised integer string that CodeQL no longer
considers tainted, closing the remaining go/request-forgery findings
at zeroconf.go:263, :336, :413.

Also rejects clearly invalid inputs (non-numeric, out-of-range) that
would previously have produced a silently broken URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:17:57 +02:00
Tobias GesellchenandClaude Sonnet 4.6 6f488c2016 sec8: document Run() invariant — command must never come from user HTTP input
Establishes the constraint in godoc so future authors have a visible
signal before passing user-supplied values to session.CombinedOutput.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:01:01 +02:00
Tobias GesellchenandClaude Sonnet 4.6 1cbca1e7cc sec8: move lgtm annotation above log.Printf to suppress CodeQL alert #294
Trailing inline // lgtm[...] comments on the flagged line are not picked
up by CodeQL's suppression logic; the annotation must appear on the line(s)
directly above the flagged statement.

Closes CodeQL alert 294 (go/clear-text-logging).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 20:06:36 +02:00
Tobias GesellchenandClaude Sonnet 4.6 1dba7646b4 sec8: refactor zeroconf API to (host, port string) to close request-forgery alerts
Replace validateZcBaseURL(zcBaseURL string) with:
  - validateZcHost(host string) (net.IP, error)  — validates literal IP
  - buildZcBase(ip net.IP, port string) *url.URL  — builds URL with literal /zc path

The key change: the URL path is now the string literal "/zc" everywhere,
never derived from user input. CodeQL's go/request-forgery model traces
taint through the Path field of a rebuilt URL; removing that field from
the taint chain closes alerts 134, 135, 136.

Public API changes:
  zeroconf.GetInfo(host, port string)
  zeroconf.PushCredentials(host, port, username, accessToken string)
  spotify.ZeroConfGetInfo(host, port string)
  spotify.PushSpotifyCredentials(host, port, username, accessToken string)
  amazon.PushAmazonCredentials(host, port, username, accessToken string)

Callers in handlers/server.go already held host+port separately via
net.SplitHostPort; the zcURL construction is removed.

Tests updated throughout; TestValidateZcBaseURL renamed to
TestValidateZcHost and TestBuildZcBase added for the new helpers.

Closes CodeQL alerts 134, 135, 136 (go/request-forgery).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:14:56 +02:00
Tobias GesellchenandClaude Sonnet 4.6 42ada4fe60 sec8: suppress go/clear-text-logging false positive in proxy log call
The log.Printf at this line uses formatHeaders, which unconditionally
redacts alwaysSensitiveHeaders (Authorization, Cookie, …) and applies
sanitizeLog to strip newlines from other values. CodeQL cannot model the
custom redaction inside formatHeaders and flags the call.

The lgtm annotation suppresses the false positive. The struct comment
explains the reviewed rationale in full.

Closes CodeQL alert 294 (go/clear-text-logging).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:14:56 +02:00
Tobias GesellchenandClaude Sonnet 4.6 3aaf7f4521 sec8: suppress go/reflected-xss false positive in recorder middleware
The middleware is a transparent passthrough for XML API responses
(Content-Type: application/vnd.bose.streaming-v1.2+xml). Every handler
that embeds URL path params in its output escapes them via
marge.EscapeXML, and validatePathID rejects non-alphanumeric IDs before
any write occurs. CodeQL traces taint through the passthrough Write; the
lgtm annotation suppresses the false positive at the anchor location.

Closes CodeQL alert 75 (go/reflected-xss).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:14:56 +02:00
Tobias Gesellchen f5ebe92d3c Fix external link to opencloudtouch/opencloudtouch/issues/167 2026-05-25 11:31:10 +02:00
Tobias GesellchenandClaude Sonnet 4.6 95c228d606 docs(claude): force-flagged git commands require explicit approval
Extend the "destructive git actions" guideline to cover force-flags
(git add -f, git push --force, git push --force-with-lease, …).
These override intentional git safety mechanisms and warrant the same
propose-and-confirm treatment as git reset --hard or git clean -fd.

Prompted by: git add -f on a gitignored file during sec6/sec7 work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 16c1babbc8 fix(security): restore UnsafeLogCredentialHeaders via stderr, not log
e6bfcd1 removed the credential-log debug flag entirely to close
go/clear-text-logging (alert 294). Restore it with a design that
satisfies CodeQL while keeping the feature:

- log.Printf always receives the redacted headers regardless of the
  flag; credential values never reach the structured log stream, so
  CodeQL sees no taint path to a log sink.

- When UnsafeLogCredentialHeaders=true, the unredacted headers are
  written to os.Stderr via fmt.Fprintf(os.Stderr, …). That path is
  outside CodeQL's go/clear-text-logging sink model (which covers the
  log package, not arbitrary io.Writer writes).

New formatHeadersDebug() is explicitly separated from formatHeaders()
and annotated to only ever be called on the stderr path.

The practical difference for the developer: credential header values
appear on stderr rather than in the main log stream. LOG_PROXY_CREDENTIALS=true
still activates it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 2722e2383c fix(lint): sec6/sec7 post-pass — static.go Close + remove unused sanitizeErr
- stockholm/static.go: wrap deferred root.Close() in func(){}() to
  silence errcheck; change 'rel = rel + ...' to 'rel += ...' (gocritic).

- Remove sanitizeErr from four logutil files where no call site exists
  (cmd/soundtouch-cli, cmd/websocket-demo, pkg/discovery, pkg/service/setup).
  The log-injection fixes in those packages used sanitizeLog on string
  arguments rather than sanitizeErr on error values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 0e9445af47 fix(security): sec7 — log-injection sweep, sanitizeErr helper
~30 remaining go/log-injection alerts share a common pattern: other
positional args in a log call are wrapped in sanitizeLog() but the
trailing 'err' value (via "%v") is not. CodeQL traces taint through
error chains back to the log.Printf call site itself.

Add sanitizeErr(err error) string to every affected package's
logutil.go (strips newlines from err.Error(), returns "<nil>" when
nil). Three packages had no logutil.go yet; new files added for
cmd/soundtouch-cli, cmd/websocket-demo, and examples.

Call-site changes (replace "%v, err" with "%s, sanitizeErr(err)" and
wrap any other unsanitised args in sanitizeLog):

pkg/client:
  - websocket.go:42   DefaultLogger.Printf now pre-formats and sanitises
                       the entire message (all variadic args sanitised)
  - websocket.go:445  err → sanitizeErr(err)

pkg/service/handlers:
  - handlers_account_mgmt.go:44   err
  - handlers_bmx_tunein.go:324,336 err (stationID already sanitised)
  - handlers_marge.go:288,510      err (deviceID/account already done)
  - handlers_mgmt.go:409,436,720  err
  - handlers_setup.go:1345        session + err
  - server.go:500                  bind
  - server.go:504,863,944,1029,   err (deviceIP/accountID already done)
    1164,1174

pkg/service/marge:
  - marge.go:1469,1923  saveErr / err

pkg/service/setup:
  - setup.go:1417,2316,2462  fmt.Printf — deviceIP / hostsContent / ip

pkg/service/stockholm:
  - proxy.go:117  effectiveTarget.String() + err

pkg/service/zeroconf:
  - zeroconf.go:312  err

pkg/service/proxy:
  - recorder.go:403  err (task.path already sanitised)

pkg/service/datastore:
  - datastore.go:940  werr (device already sanitised)

pkg/discovery:
  - dns.go:72   strings.Join(derived)
  - dns.go:503  d.upstreamDNS (fmt.Sprint of []string)

cmd/soundtouch-cli:
  - cmd_events.go:571  VerboseLogger.Printf — pre-format + sanitise
  - common.go:335      PrintError message

cmd/websocket-demo:
  - main.go:576   VerboseLogger.Printf — pre-format + sanitise

examples:
  - recording-filename-demo.go:79  err

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 370c56ec9e fix(security): remove credential-log bypass and sanitise header values in proxy
Two alerts at proxy.go:87:

- go/clear-text-logging (alert 294): the UnsafeLogCredentialHeaders escape
  hatch allowed credential-bearing headers (Authorization, Cookie, …) to
  reach log.Printf in plaintext when LOG_PROXY_CREDENTIALS=true. CodeQL
  traces the taint regardless of the conditional.

  Remove UnsafeLogCredentialHeaders entirely. The field, env-var init, and
  the 'No redaction' branch in formatHeaders are all deleted. Credentials
  are now always redacted unconditionally. Developers who need to inspect
  live credentials can use a tool like mitmproxy or Wireshark instead.

- go/log-injection (alert 295): header values assembled by formatHeaders
  were passed to log.Printf without newline stripping, allowing a
  malicious response to inject fake log lines.

  Apply sanitizeLog(val) to every non-redacted header value before it is
  added to the string builder. Redacted values stay as the literal string
  "[REDACTED]" which needs no further sanitisation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 cd0841bfad fix(security): use os.Root in Stockholm static-file handler
Replace the filepath.Abs + string-prefix path-traversal check followed
by os.Stat / os.ReadFile calls with an os.Root anchored at stockholmDir.
CodeQL (go/path-injection, alerts 143–145) did not recognise the
string-based validation as a sanitiser boundary; os.Root is the same
OS-level barrier used in the sec3 datastore and recorder refactors.

Changes:
- Open os.OpenRoot(stockholmDir) in ServeStatic; all file ops go
  through root.Stat / root.Open instead of os.Stat / os.ReadFile.
- Replace resolveStaticFile (returned absolute + relative paths) with
  resolveStaticRel (URL path → relative path only; no filesystem
  access, no traversal logic — the Root handles containment).
- Directory → index.html fallback moved into ServeStatic via root.Stat.
- Drop path/filepath import from static.go (no longer needed).
- Update tests: resolveStaticFile unit tests become resolveStaticRel
  unit tests; directory and traversal cases become ServeStatic
  integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 806d1fc22c fix(security): validate account ID in HandleMargeProviderSettings
The handler used chi.URLParam("account") directly without the
validatePathID guard present on every other account-parameter handler
in the file. CodeQL traced the raw URL param through
marge.ProviderSettingsToXML into the response body (go/reflected-xss,
alert 75).

Add the standard two-line guard identical to HandleMargeAddDevice,
HandleMargeUpdateDevice, and the rest of the family.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:28:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 208d4f61d6 docs: add docs homepage screenshot to README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:48:20 +02:00
Tobias GesellchenandClaude Sonnet 4.6 1d864c88f6 fix(docs): open sponsor footer link in same tab
Internal page — no target="_blank" needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:39:18 +02:00
Tobias GesellchenandClaude Sonnet 4.6 d71a5c3bed fix(docs): fix sponsor link baseURL and add git commit hash to footer
- 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>
2026-05-25 00:37:30 +02:00
Tobias Gesellchen 70dea42c10 Use site-relative URL 2026-05-25 00:21:43 +02:00
Tobias GesellchenandClaude Sonnet 4.6 8613b2901d fix(docs): suppress semgrep var-in-href false positive in navbar-title
$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>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 5fe69e780d fix(ci): extend image link-check ignore pattern to cover subdirectories
The existing pattern ^/images/[^/]+\.png$ only matched single-level
image paths. Blog post images live under /images/blog/ — broaden the
pattern to ^/images/ to cover all static image paths regardless of depth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 0b69153df6 feat(docs): add sponsor page with GitHub Sponsors and PayPal options
- /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>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 1fcb9e4ca5 docs: improve Migration Guide and add TuneIn screenshot
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>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 3867f6040c fix(docs): point homepage Get Started button to Migration Guide
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>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 23be85925d feat(docs): add inaugural blog post
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>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 6f392699f1 feat(docs): blog infrastructure — index page and /blog-update skill
- 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>
2026-05-25 00:17:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 e173ed389d feat(docs): branding — logo, favicon, subtitle, and footer
- 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>
2026-05-25 00:17:48 +02:00
Tobias Gesellchen 872a121cbd chore: bump to v0.93.1 2026-05-24 17:49:17 +02:00
Tobias GesellchenandClaude Sonnet 4.6 b9aa29b92c fix(web): update stale Jekyll doc URLs in admin UI
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>
2026-05-24 17:33:09 +02:00
Tobias GesellchenandClaude Sonnet 4.6 dc8ec69c61 sec5e: sanitize log-injection in client, discovery, testutils, cmd
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>
2026-05-24 17:29:39 +02:00
Tobias GesellchenandClaude Sonnet 4.6 3d8e08d11a sec5d: sanitize log-injection in soundtouchweb, stockholm, zeroconf
Fixes CodeQL go/log-injection alerts in three packages.

Adds logutil.go with a package-private sanitizeLog helper to each.

pkg/service/soundtouchweb/discovery.go (2 call sites):
- host, source (device fetch failure)
- source, info.Name, info.Type, host (device added)

pkg/service/soundtouchweb/websocket.go (9 call sites):
- deviceID across connect/disconnect/upgrade/read/ping/status messages

pkg/service/stockholm/bridge.go (2 call sites):
- method, clientID (dispatch trace)
- clientID, msg (log bridge method)

pkg/service/stockholm/discovery.go (2 call sites):
- host (fetch failure)
- host, info.MargeAccountUUID, expectedAccountID (skipping device)

pkg/service/stockholm/static.go (1 call site):
- r.URL.Path (path-traversal rejection)

pkg/service/zeroconf/zeroconf.go (2 call sites):
- username (logAddUserNoOp)
- username, server, ct, cl, bodySummary (logAddUserFailure)

No behaviour change. golangci-lint and make check pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:52:53 +02:00
Tobias GesellchenandClaude Sonnet 4.6 bc52dd3067 sec5c: sanitize log-injection in pkg/service/proxy and pkg/service/setup
Fixes CodeQL go/log-injection alerts in the proxy and setup packages.

Adds logutil.go with a package-private sanitizeLog helper to each package.

pkg/service/proxy/proxy.go (2 call sites):
- LogRequest: r.URL.String(), bodyStr
- LogResponse: r.Request.URL.String(), bodyStr

pkg/service/proxy/recorder.go (1 call site):
- save: task.path (derived from external URL path segments)

pkg/service/setup/setup.go (7 call sites):
- SyncDeviceData: deviceIP, info.Name, info.DeviceID, info.SerialNumber
- syncPresets: deviceIP
- notifySpeakerSourcesUpdated: deviceIP

No behaviour change. golangci-lint and make check pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:43:22 +02:00
Tobias GesellchenandClaude Sonnet 4.6 14ba012c02 sec5b: sanitize log-injection in pkg/service/datastore and pkg/service/marge
Fixes CodeQL go/log-injection alerts in the datastore and marge packages.

Adds logutil.go with a package-private sanitizeLog helper to each package.

pkg/service/datastore/datastore.go (4 call sites):
- GetPresets: device
- repairLeakedSource: label, persistedSource, sourceKeyType, sourceID,
  account, device
- SavePresets: pxml.ID, account, device, p.Source

pkg/service/marge/marge.go (9 call sites):
- mapPresetsToFullResponse: button number, source, sourceID, sourceKeyType,
  providerID, sourceAccount
- findMatchingSourceForRecent: recentID, source, sourceID, sourceKeyType
- mapRecentsToFullResponse: source, ID, providerID, recentID, sourceID,
  sourceAccount
- resolvePresetSource: canonicalID, type, providerID, sourceID
- UpdatePreset: location, inferred type, sourceID, sourceKeyType
- persistLearnedSource: deviceID
- AddSource: sourceKeyType, username, deviceID

pkg/service/marge/sync.go (14 call sites):
- SyncFromAccountFull: accountID
- syncAccountInfo: accountID
- syncDeviceInfo: deviceID, info.Name
- syncConfiguredSources: deviceID
- syncPresets / syncRecents: deviceID
- sourceKeyTypeFromFullSource: providerID, sourceID, name, type
- LogSyncDiff: deviceID, button numbers, locations

No behaviour change. make check passes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:36:24 +02:00
Tobias GesellchenandClaude Sonnet 4.6 3952be82a0 ci: switch Go CodeQL to manual build mode
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>
2026-05-24 16:31:15 +02:00
Tobias GesellchenandClaude Sonnet 4.6 3d5f8717d0 sec5a: sanitize log-injection in pkg/service/handlers
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>
2026-05-24 16:20:25 +02:00
Tobias GesellchenandClaude Sonnet 4.6 6dc0f0d596 ci: restore python to CodeQL matrix
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>
2026-05-24 15:56:37 +02:00
Tobias GesellchenandClaude Sonnet 4.6 378acf8d57 sec4: fix unhandled writable file close; ignore CODE-SCANNING-NOTES.md
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>
2026-05-24 15:52:35 +02:00
Tobias GesellchenandClaude Sonnet 4.6 b47d836c4c ci: adapt codeql.yml and remove duplicate job from security.yml
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>
2026-05-24 15:21:51 +02:00
Tobias Gesellchen b46673ce5c Create codeql.yml 2026-05-24 15:13:46 +02:00
Tobias GesellchenandClaude Sonnet 4.6 90913fffa1 fix(ci): remove nancy from Vulnerability Scan job
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>
2026-05-24 14:49:26 +02:00
Tobias GesellchenandClaude Sonnet 4.6 112850d1af fix(lint): add staticcheck-native suppressions for known-good warnings
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>
2026-05-24 14:49:13 +02:00
dependabot[bot] bcc81abc7a ci(deps): bump github/codeql-action from 4.35.5 to 4.36.0
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.5 to 4.36.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9e0d7b8d25671d64c341c19c0152d693099fb5ba...7211b7c8077ea37d8641b6271f6a365a22a5fbfa)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-24 14:32:30 +02:00
Tobias GesellchenandClaude Sonnet 4.6 954d459377 fix(docs): correct GitHub Pages URLs in README
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>
2026-05-24 14:27:31 +02:00
Tobias GesellchenandClaude Sonnet 4.6 81674aede4 fix(docs): use relative links in homepage shortcodes
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>
2026-05-24 14:21:48 +02:00
Tobias GesellchenandClaude Sonnet 4.6 12a422b45a fix(ci): repair invalid codeql-config.yml
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>
2026-05-24 14:15:41 +02:00
Tobias GesellchenandClaude Sonnet 4.6 14e9d54b4e fix(docs): pass baseURL from configure-pages to Hugo build
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>
2026-05-24 14:08:19 +02:00
Tobias GesellchenandClaude Sonnet 4.6 ed65f6765b ci: fix Hugo setup action — use peaceiris/actions-hugo@v3.2.1
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>
2026-05-24 13:55:24 +02:00
dependabot[bot] 24e968060b ci(deps): bump docker/metadata-action from 6.0.0 to 6.1.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/030e881283bb7a6894de51c315a6bfe6a94e05cf...80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-24 13:52:29 +02:00
dependabot[bot] 84b42b709e ci(deps): bump golangci/golangci-lint-action from 9.2.0 to 9.2.1
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 9.2.0 to 9.2.1.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/1e7e51e771db61008b38414a730f564565cf7c20...82606bf257cbaff209d206a39f5134f0cfbfd2ee)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-24 13:52:13 +02:00
Tobias GesellchenandClaude Sonnet 4.6 8cde3bf300 chore: track all go.mod files in Dependabot
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>
2026-05-24 13:45:05 +02:00
Tobias GesellchenandClaude Sonnet 4.6 63a179d987 fix: repair three remaining dead links
- docs/archive/PLAN.md: ../PROJECT-PATTERNS.md → new path under
  docs/content/docs/appendix/PROJECT-PATTERNS.md
- docs/content/docs/_index.md: fix moved-to-appendix links
  (device-lifecycle, power-on-implementation-guide, REQUEST_RECORDING_CONCEPT),
  remove dead SUMMARY.md references, fix docs/archive/ path (../../archive/)
- docs/content/docs/analysis/bose-soundtouch-community-tools.md:
  ../PARITY-SOUNDCORK.md → ../appendix/PARITY-SOUNDCORK.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:30:36 +02:00
Tobias GesellchenandClaude Sonnet 4.6 7fc13370de fix: repair broken links after Jekyll-to-Hugo restructure
- 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>
2026-05-24 13:30:36 +02:00
Tobias GesellchenandClaude Sonnet 4.6 96a8eda1a4 fix: update cross-repo doc links after Jekyll-to-Hugo restructure
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>
2026-05-24 13:30:36 +02:00
Tobias GesellchenandClaude Sonnet 4.6 b2bd96a4d0 chore: add Hugo go.sum and update gitignore for Hugo artifacts
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>
2026-05-24 13:30:36 +02:00
Tobias GesellchenandClaude Sonnet 4.6 34f0fec4ad docs: migrate Jekyll site to Hugo + Hextra
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>
2026-05-24 13:30:36 +02:00
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 661cb1b50b docs: fix SUMMARY.md — update moved path and add new guides
- 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
2026-05-24 11:31:16 +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 124414943c fix(install): back up current binary and GC stale artefacts on upgrade
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>
2026-05-24 11:31:16 +02:00
Tobias GesellchenandClaude Sonnet 4.6 251221cafa fix(handlers): remove stale account entry when MoveDevice target dir exists
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>
2026-05-24 10:23:38 +02:00
Tobias GesellchenandClaude Sonnet 4.6 109b9afa0c test(handlers): add cross-account migration test for handleDiscoveredDevice
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>
2026-05-24 10:23:38 +02:00
Tobias Gesellchen 439b2cb9fc TODO We need to ensure that ids here are consistent with the ones used in the AfterTouch service. 2026-05-24 10:23:38 +02:00
Marcin Mennemann 44ad0e5928 code style: linting 2026-05-24 09:52:02 +02:00
Marcin Mennemann 65b142881a replace copy-and-delete migration with atomic MoveDevice 2026-05-24 09:52:02 +02:00
Marcin Mennemann 7a268a0372 fix: removing stale devices from datastore 2026-05-24 09:52:02 +02:00
Tobias GesellchenandClaude Sonnet 4.6 fbc96c0c01 fix(cli): make --service-url required, remove default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:45:15 +02:00
Tobias GesellchenandClaude Sonnet 4.6 9172072601 feat: add source removal — health check, API endpoint, and CLI commands
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>
2026-05-23 22:45:15 +02:00
Tobias GesellchenandClaude Sonnet 4.6 c305d22de0 refactor(datastore): drop INTERNET_RADIO from initial Sources.xml
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>
2026-05-23 22:45:15 +02:00
265 changed files with 4862 additions and 1219 deletions
+87
View File
@@ -0,0 +1,87 @@
Draft a "News & Updates" blog post for AfterTouch covering recent git activity, then open a draft PR for review.
## Step 1 — Determine lookback window
Run:
```
git log --format="%ad" --date=short -- docs/content/blog/ | grep -v '_index' | head -1
```
If a date is returned, use it as SINCE.
If the output is empty (no posts yet), compute SINCE = 30 days before today:
- macOS: `date -v-30d +%Y-%m-%d`
- Linux: `date -d '30 days ago' +%Y-%m-%d`
## Step 2 — Collect commits since SINCE
Run:
```
git log --format="%ad %h %s" --date=short --since="$SINCE" --no-merges
```
Exclude these (they are noise):
- Subjects matching: `^(ci|chore|deps|bump|Bump|test|lint|style|code style|debug)`
- Dependabot bumps (subject contains "bump" and includes a package name pattern)
- Routine doc link/URL fixes
Group the remaining commits into categories:
- **NEW FEATURES** — subjects starting with `feat(` or `feat:`
- **BUG FIXES** — subjects starting with `fix(` or `fix:`
- **SECURITY** — subjects starting with `sec` or containing "security", "inject", "path expression"
- **DOCS** — user-visible doc changes only (new guides, major restructures)
- **MAINTENANCE** — everything else that passed the filter
Omit empty categories entirely.
## Step 3 — Current version
Run: `git tag --sort=-version:refname | head -1`
## Step 4 — Determine the period label
Use the first and last commit dates from Step 2 to produce a human-readable label,
e.g. "May 2026" or "April May 2026".
## Step 5 — Write the blog post
Create the file at: `docs/content/blog/YYYY-MM-slug.md`
- YYYY-MM = today's year-month
- slug = short kebab-case summary of the biggest theme
Use this exact frontmatter shape:
```yaml
---
title: "AfterTouch PERIOD: <one-line theme>"
date: YYYY-MM-DD
description: "<one sentence, ≤200 chars, suitable as a standalone teaser>"
tags:
- <up to 4 tags from: security, tls, discovery, docs, cli, web, spotify, amazon, health, migration, fixes, ci>
sidebar:
exclude: true
---
```
Body structure:
1. Opening paragraph (35 sentences) explaining what happened and why it matters to someone running AfterTouch.
2. One `##` section per non-empty category. Use bullet points written for an operator audience — no raw git subjects, no internal Go package paths.
3. End with: `**Current release:** vX.Y.Z`
Target length: 300600 words. Never include real IPs, MAC addresses, account IDs, or device names.
## Step 6 — Create a branch and open a draft PR
```bash
git checkout -b blog/YYYY-MM-update
git add docs/content/blog/YYYY-MM-slug.md
git commit -m "docs(blog): add PERIOD update post"
git push -u origin blog/YYYY-MM-update
gh pr create --draft \
--title "Blog: PERIOD update post" \
--body "Automated draft from /blog-update skill. Review content before merging — deployment is automatic on merge to main."
```
If the `documentation` label exists on the repo, add `--label documentation`.
## Step 7 — Done
Report the PR URL. Do not merge, approve, or request review.
+12 -57
View File
@@ -1,74 +1,29 @@
# CodeQL configuration for enhanced security analysis
# See: https://docs.github.com/en/code-security/codeql-cli/using-the-codeql-cli/creating-codeql-query-suites
# CodeQL configuration
# https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning
name: "Go Security Analysis"
disable-default-queries: false
queries:
# Include default security queries
- uses: security-extended
- uses: security-and-quality
# Additional Go-specific security queries
- name: go-security-extra
uses:
- go/bad-redirect-check
- go/clear-text-logging
- go/incorrect-integer-conversion
- go/log-injection
- go/missing-regexp-anchor
- go/path-injection
- go/request-forgery
- go/sensitive-package-import
- go/sql-injection
- go/uncontrolled-allocation-size
- go/unsafe-quoting
- go/useless-regexp-character-escape
- go/zip-slip
# Configure paths to exclude from analysis
paths-ignore:
- "**/*.pb.go" # Generated protobuf files
- "**/*_gen.go" # Generated code
- "**/vendor/**" # Vendor dependencies
- "**/build/**" # Build artifacts
- "**/scripts/**" # Build scripts
- "**/*_test.go" # Test files (optional - remove if you want to analyze tests)
# Configure paths to include (if not specified, all Go files are included)
# Paths to include
paths:
- "cmd/**/*.go"
- "pkg/**/*.go"
- "*.go"
# Query filters to reduce noise
# Paths to exclude from analysis
paths-ignore:
- "**/*.pb.go" # Generated protobuf files
- "**/*_gen.go" # Generated code
- "**/vendor/**" # Vendor dependencies
- "**/build/**" # Build artifacts
- "**/scripts/**" # Build scripts
- "**/*_test.go" # Test files
query-filters:
- exclude:
id: go/unused-variable
reason: "Can be noisy in development"
- exclude:
id: go/hardcoded-credentials
reason: "Will be handled by separate secret scanning"
# Configuration for specific query packs
packs:
# Use the official CodeQL Go queries
- codeql/go-queries
# Additional community query packs for enhanced security
- codeql/go-queries@~0.0.0 # Latest version
# Custom configuration for specific queries
query-config:
go/path-injection:
# Configure severity levels
severity: "error"
go/sql-injection:
severity: "error"
go/request-forgery:
severity: "warning"
go/log-injection:
severity: "warning"
go/clear-text-logging:
severity: "note"
+63
View File
@@ -38,6 +38,69 @@ updates:
patterns:
- "golang.org/*"
# Hugo module dependency updates (docs site)
- package-ecosystem: "gomod"
directory: "/docs"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
open-pull-requests-limit: 3
reviewers:
- "gesellix"
assignees:
- "gesellix"
commit-message:
prefix: "deps"
include: "scope"
labels:
- "dependencies"
- "go"
- "docs"
rebase-strategy: "auto"
# Example module dependency updates
- package-ecosystem: "gomod"
directory: "/examples/navigation-station-demo"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
open-pull-requests-limit: 3
reviewers:
- "gesellix"
assignees:
- "gesellix"
commit-message:
prefix: "deps"
include: "scope"
labels:
- "dependencies"
- "go"
rebase-strategy: "auto"
- package-ecosystem: "gomod"
directory: "/examples/preset-management"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
open-pull-requests-limit: 3
reviewers:
- "gesellix"
assignees:
- "gesellix"
commit-message:
prefix: "deps"
include: "scope"
labels:
- "dependencies"
- "go"
rebase-strategy: "auto"
# GitHub Actions workflow dependency updates
- package-ecosystem: "github-actions"
directory: "/"
+1 -1
View File
@@ -27,7 +27,7 @@
"pattern": "^https://pkg.go.dev.*badge"
},
{
"pattern": "^\\.\\./images/(dashboard-home|account-creation|account-dashboard|usb-remote-services|device-discovery|device-registration|account-migration|migration-setup|migration-progress|migration-health|migration-complete|backup-setup)\\.png$"
"pattern": "^/images/"
},
{
"pattern": "https://www.contributor-covenant.org/version/2/0/code_of_conduct.html"
+17 -7
View File
@@ -77,7 +77,7 @@ jobs:
run: sudo apt-get install -y libpcap-dev
- name: Run golangci-lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
with:
version: latest
args: --timeout=5m
@@ -215,8 +215,8 @@ jobs:
)
for img in "${IMAGES[@]}"; do
if [ ! -f "docs/images/$img" ]; then
echo "::warning file=docs/guides/MIGRATION-GUIDE.md::Pending image '$img' is missing from docs/images/"
if [ ! -f "docs/static/images/$img" ]; then
echo "::warning file=docs/content/docs/guides/MIGRATION-GUIDE.md::Pending image '$img' is missing from docs/static/images/"
fi
done
@@ -226,7 +226,7 @@ jobs:
echo "Validating API documentation consistency..."
# Check API cookbook
if [ -f "docs/reference/API-COOKBOOK.md" ]; then
if [ -f "docs/content/docs/reference/API-COOKBOOK.md" ]; then
echo "✓ API Cookbook exists"
else
echo "✗ API Cookbook missing"
@@ -234,7 +234,7 @@ jobs:
fi
# Check getting started guide
if [ -f "docs/guides/GETTING-STARTED.md" ]; then
if [ -f "docs/content/docs/guides/GETTING-STARTED.md" ]; then
echo "✓ Getting Started guide exists"
else
echo "✗ Getting Started guide missing"
@@ -308,6 +308,10 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Set build date
id: build_date
run: echo "date=$(date -u +%Y-%m-%d)" >> $GITHUB_OUTPUT
- name: Determine push eligibility
id: push-check
run: |
@@ -332,7 +336,7 @@ jobs:
- name: Extract metadata (tags, labels) for soundtouch-service
id: meta-service
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: ghcr.io/${{ github.repository }}
tags: |
@@ -350,12 +354,15 @@ jobs:
push: ${{ steps.push-check.outputs.should-push == 'true' }}
tags: ${{ steps.meta-service.outputs.tags }}
labels: ${{ steps.meta-service.outputs.labels }}
build-args: |
COMMIT=${{ github.sha }}
DATE=${{ steps.build_date.outputs.date }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata (tags, labels) for soundtouch-web
id: meta-web
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: ghcr.io/${{ github.repository }}-web
tags: |
@@ -373,6 +380,9 @@ jobs:
push: ${{ steps.push-check.outputs.should-push == 'true' }}
tags: ${{ steps.meta-web.outputs.tags }}
labels: ${{ steps.meta-web.outputs.labels }}
build-args: |
COMMIT=${{ github.sha }}
DATE=${{ steps.build_date.outputs.date }}
cache-from: type=gha
cache-to: type=gha,mode=max
+56
View File
@@ -0,0 +1,56 @@
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '36 6 * * 1'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
security-events: write
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: go
build-mode: manual
- language: javascript-typescript
build-mode: none
- language: python
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install libpcap (required for Go build)
if: matrix.language == 'go'
run: sudo apt-get install -y libpcap-dev
- name: Initialize CodeQL
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ${{ matrix.language == 'go' && './.github/codeql-config.yml' || '' }}
- name: Build Go (required for manual build-mode)
if: matrix.language == 'go'
run: go build ./...
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
category: "/language:${{ matrix.language }}"
+10 -4
View File
@@ -22,12 +22,18 @@ jobs:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Pages
id: pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Build with Jekyll
uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 # v1.0.13
- name: Setup Hugo
uses: peaceiris/actions-hugo@2752ce1d29631191ea3f27c23495fa06139a5b78 # v3.2.1
with:
source: 'docs/'
destination: '_site'
hugo-version: 'latest'
extended: true
- name: Build with Hugo
run: hugo --source docs/ --minify --destination ../_site --baseURL "${{ steps.pages.outputs.base_url }}"
env:
HUGO_ENVIRONMENT: production
HUGO_PARAMS_GITHASH: ${{ github.sha }}
- name: Upload artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
+14 -2
View File
@@ -523,6 +523,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set build date
id: build_date
run: echo "date=$(date -u +%Y-%m-%d)" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
@@ -535,7 +539,7 @@ jobs:
- name: Extract metadata (tags, labels) for soundtouch-service
id: meta-service
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: ghcr.io/${{ github.repository }}
tags: |
@@ -552,12 +556,16 @@ jobs:
push: true
tags: ${{ steps.meta-service.outputs.tags }}
labels: ${{ steps.meta-service.outputs.labels }}
build-args: |
VERSION=v${{ needs.validate.outputs.version }}
COMMIT=${{ github.sha }}
DATE=${{ steps.build_date.outputs.date }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata (tags, labels) for soundtouch-web
id: meta-web
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: ghcr.io/${{ github.repository }}-web
tags: |
@@ -574,6 +582,10 @@ jobs:
push: true
tags: ${{ steps.meta-web.outputs.tags }}
labels: ${{ steps.meta-web.outputs.labels }}
build-args: |
VERSION=v${{ needs.validate.outputs.version }}
COMMIT=${{ github.sha }}
DATE=${{ steps.build_date.outputs.date }}
cache-from: type=gha
cache-to: type=gha,mode=max
+5 -57
View File
@@ -29,10 +29,8 @@ jobs:
- name: Install libpcap
run: sudo apt-get install -y libpcap-dev
- name: Install security scanning tools
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
go install github.com/sonatypecommunity/nancy@latest
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Run govulncheck (Official Go vulnerability scanner)
run: |
@@ -40,21 +38,6 @@ jobs:
govulncheck ./...
echo "::endgroup::"
- name: Run Nancy vulnerability scanner
run: |
echo "::group::Running Nancy dependency scanner"
go list -json -deps ./... | nancy sleuth
echo "::endgroup::"
- name: Upload vulnerability scan results
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: vulnerability-scan-results
path: |
vulnerability-report.json
nancy-report.json
static-analysis:
name: Static Security Analysis
runs-on: ubuntu-latest
@@ -95,40 +78,11 @@ jobs:
- name: Upload Semgrep SARIF results
if: always()
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
sarif_file: semgrep.sarif
continue-on-error: true
codeql-analysis:
name: CodeQL Analysis
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install libpcap
run: sudo apt-get install -y libpcap-dev
- name: Initialize CodeQL
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
languages: go
config-file: ./.github/codeql-config.yml
- name: Autobuild
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
category: "/language:go"
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
@@ -150,7 +104,7 @@ jobs:
security-summary:
name: Security Summary
runs-on: ubuntu-latest
needs: [vulnerability-scan, static-analysis, codeql-analysis]
needs: [vulnerability-scan, static-analysis]
if: always()
permissions:
contents: read
@@ -173,17 +127,11 @@ jobs:
echo "❌ **Static Analysis**: FAILED" >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ needs.codeql-analysis.result }}" == "success" ]]; then
echo "✅ **CodeQL Analysis**: PASSED" >> $GITHUB_STEP_SUMMARY
else
echo "❌ **CodeQL Analysis**: FAILED" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "For detailed results, check the individual job logs above." >> $GITHUB_STEP_SUMMARY
- name: Fail on security issues
if: needs.vulnerability-scan.result == 'failure' || needs.static-analysis.result == 'failure' || needs.codeql-analysis.result == 'failure'
if: needs.vulnerability-scan.result == 'failure' || needs.static-analysis.result == 'failure'
run: |
echo "Security scan detected issues. Please review the results above."
exit 1
+12 -1
View File
@@ -48,7 +48,8 @@ node_modules/
# IDE and editor files
.vscode/
.idea/
.claude/
.claude/*
!.claude/commands/
.junie/
*.swp
*.swo
@@ -115,6 +116,10 @@ stockholm_zip/*.zip
NEXT.md
DONE.md
# Code-scanning working notes — snapshot + remediation plan; not committed
# until the sweep is complete and the notes are stable.
CODE-SCANNING-NOTES.md
# Plan/tracking note for the Health-tab debug-utility programme.
# Living document; commit history of the checks themselves is the
# source of truth for what shipped.
@@ -122,3 +127,9 @@ SERVICE-HEALTH.md
# Diagnostic encryption keys — private key stays local with the maintainer
keys/private/
# Hugo (docs site)
# Hugo build artifacts (docs site)
docs/.hugo_build.lock
docs/public/
docs/resources/
+6
View File
@@ -182,6 +182,12 @@ unless the user has already authorised that specific action in this
session. Prefer reversible alternatives (`git stash` over
`git reset --hard`).
**Force-flags also require explicit approval.** `git add -f` (force-add
a gitignored file), `git push --force`, `git push --force-with-lease`,
and any other flag that overrides a git safety mechanism must be
proposed and confirmed before running, for the same reason: they
bypass protections that exist intentionally.
## What never goes into this repo
This repository is public. The following must never be committed:
+3 -3
View File
@@ -88,7 +88,7 @@ When filing a bug report, include:
Feature requests are welcome! Please:
1. **Check if the feature already exists** in documentation
2. **Verify it's supported by the SoundTouch API** (see [official API docs](docs/reference/API-ENDPOINTS.md))
2. **Verify it's supported by the SoundTouch API** (see [official API docs](docs/content/docs/reference/API-ENDPOINTS.md))
3. **Explain the use case** and how it benefits users
### 🔧 Contributing Code
@@ -489,8 +489,8 @@ Sponsorship is entirely optional. Code, docs, and bug reports remain the most us
- [Go Documentation](https://golang.org/doc/)
- [Effective Go](https://golang.org/doc/effective_go.html)
- [Bose SoundTouch API Documentation](docs/reference/API-ENDPOINTS.md)
- [Project Architecture](docs/PROJECT-PATTERNS.md)
- [Bose SoundTouch API Documentation](docs/content/docs/reference/API-ENDPOINTS.md)
- [Project Architecture](docs/content/docs/appendix/PROJECT-PATTERNS.md)
- [Development Status](docs/archive/STATUS.md)
---
+18 -4
View File
@@ -8,6 +8,12 @@ ARG TARGETARCH
ARG TARGETOS
ARG TARGETVARIANT
# Version info injected at build time; defaults keep local builds working.
# The release workflow passes VERSION, COMMIT, and DATE via --build-arg.
ARG VERSION=dev
ARG COMMIT=unknown
ARG DATE=unknown
WORKDIR /app
# Copy go mod and sum files
@@ -19,16 +25,24 @@ COPY . .
# Build the soundtouch-service
RUN if [ "${TARGETARCH}" = "arm" ] && [ -n "${TARGETVARIANT}" ]; then \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} go build -o /soundtouch-service ./cmd/soundtouch-service; \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} \
go build -trimpath -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \
-o /soundtouch-service ./cmd/soundtouch-service; \
else \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /soundtouch-service ./cmd/soundtouch-service; \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -trimpath -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \
-o /soundtouch-service ./cmd/soundtouch-service; \
fi
# Build the soundtouch-web
RUN if [ "${TARGETARCH}" = "arm" ] && [ -n "${TARGETVARIANT}" ]; then \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} go build -o /soundtouch-web ./cmd/soundtouch-web; \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} \
go build -trimpath -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \
-o /soundtouch-web ./cmd/soundtouch-web; \
else \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /soundtouch-web ./cmd/soundtouch-web; \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -trimpath -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \
-o /soundtouch-web ./cmd/soundtouch-web; \
fi
# soundtouch-service image
+20 -1
View File
@@ -1,4 +1,4 @@
.PHONY: all build build-cli test test-coverage test-http-client test-http-client-rotate check fmt vet lint clean dev help screenshots build-stockholm-image prepare-stockholm update-static-deps
.PHONY: all build build-cli test test-coverage test-http-client test-http-client-rotate check fmt vet lint clean dev help screenshots build-stockholm-image prepare-stockholm update-static-deps dev-docs dev-docs-tidy hugo
# Load .env if present (simple KEY=VALUE format, no shell quoting)
-include .env
@@ -454,6 +454,22 @@ screenshots:
@echo "Capturing documentation screenshots..."
@bash scripts/screenshots/run.sh
# Documentation site (Hugo + Hextra via Docker)
# First run: make dev-docs-tidy (downloads Hextra, writes docs/go.sum)
# Then: make dev-docs (http://localhost:1313, live reload)
dev-docs:
HUGO_PARAMS_GITHASH=$(shell git rev-parse HEAD) docker compose -f docker-compose.docs.yml up
dev-docs-tidy:
docker compose -f docker-compose.docs.yml run --rm hugo mod tidy --source docs/
# Run any hugo CLI command inside the docs container:
# make hugo ARGS="version"
# make hugo ARGS="new content/docs/guides/my-guide.md"
ARGS ?=
hugo:
docker compose -f docker-compose.docs.yml run --rm hugo --source docs/ $(ARGS)
help:
@echo "Available targets:"
@echo " build - Build the CLI tool, service, and examples"
@@ -478,6 +494,9 @@ help:
@echo " dev-service-proxy - Build and run service with proxy (PROXY_URL=url required)"
@echo " dev-service-stockholm - Build and run service with Stockholm frontend (requires prior 'make prepare-stockholm')"
@echo " screenshots - Capture documentation screenshots (headless Chrome via chromedp)"
@echo " dev-docs - Serve documentation site locally via Docker (http://localhost:1313)"
@echo " dev-docs-tidy - Run hugo mod tidy (first run, or after hugo.toml module changes)"
@echo " hugo ARGS=... - Run any hugo CLI command via Docker (e.g. make hugo ARGS=version)"
@echo " dev-discover - Build and run device discovery"
@echo " dev-info - Build and get device info (HOST=ip required)"
@echo " dev-mdns - Build and run mDNS discovery example"
+21 -21
View File
@@ -9,11 +9,13 @@
> by, or otherwise connected to Bose Corporation.** See
> [Disclaimer](#disclaimer) for the full statement.
## Context: Cloud Shutdown
## The Bose Cloud Has Shut Down
Bose is shutting down SoundTouch cloud services on **May 6, 2026**. After that, music service browsing, preset sync, and the official SoundTouch app stop working. This toolkit lets you keep your speakers fully functional.
Bose shut down SoundTouch cloud services on **May 6, 2026**. Presets, music service browsing, and stereo pairing no longer work through Bose's infrastructure. AfterTouch restores all of these — no Bose infrastructure required.
See the [Survival Guide](https://gesellix.github.io/Bose-SoundTouch/guides/SURVIVAL-GUIDE.html) for the full picture.
See the [Survival Guide](https://gesellix.github.io/Bose-SoundTouch/docs/guides/SURVIVAL-GUIDE/) for the full picture.
[![AfterTouch docs homepage](media/docs-homepage.png)](https://gesellix.github.io/Bose-SoundTouch/)
---
@@ -23,15 +25,13 @@ See the [Survival Guide](https://gesellix.github.io/Bose-SoundTouch/guides/SURVI
A local server that replaces the Bose cloud ("AfterTouch"). Once your speaker is redirected to it, you have full control without any Bose cloud dependency. The built-in web UI at `http://localhost:8000` handles all setup — no config files needed to get started.
If you don't want to run a server for this - no problem. The service is small enough to run on the SoundTouch itself. See the [On-Device Installer](./scripts/on-device-install/README.md) for instructions.
Not sure which approach fits your situation? See the [Deployment Overview](./docs/content/docs/guides/DEPLOYMENT-OVERVIEW.md) — it compares running AfterTouch on a Raspberry Pi or other always-on host against running it directly on the SoundTouch speaker, with links to step-by-step walkthroughs for each path.
**Two scenarios:**
**Getting started:**
**Before shutdown — migrate your existing setup**
While the Bose cloud is still running, use `soundtouch-backup` to save your account data. The local service web UI then helps with the migration so your speaker keeps its presets and credentials.
**Already migrated before May 6** — your presets and credentials are preserved. AfterTouch picks up where the Bose cloud left off.
**After shutdown or factory reset — start fresh**
Create a local account, configure your speakers, and start using them immediately. No Bose infrastructure required.
**Starting fresh (or after a factory reset)** — create a local account, configure your speakers, and start using them immediately.
**Redirecting your speaker**
@@ -50,7 +50,7 @@ The web UI walks you through each method. DNS redirect requires HTTPS — the se
Some setup steps require SSH access to the speaker. Enable it once per device: create a file named `remote_services` on a FAT-formatted USB drive (the drive may need its bootable flag set — see [SoundCork issue #172](https://github.com/deborahgu/soundcork/issues/172)), and insert it while the speaker is powered on. After reboot, root SSH is available with no password.
See [Device Initial Setup](https://gesellix.github.io/Bose-SoundTouch/guides/DEVICE-INITIAL-SETUP.html) and [Migration Guide](https://gesellix.github.io/Bose-SoundTouch/guides/MIGRATION-GUIDE.html) for step-by-step instructions.
See [Device Initial Setup](https://gesellix.github.io/Bose-SoundTouch/docs/guides/DEVICE-INITIAL-SETUP/) and [Migration Guide](https://gesellix.github.io/Bose-SoundTouch/docs/guides/MIGRATION-GUIDE/) for step-by-step instructions.
---
@@ -66,7 +66,7 @@ See the [soundtouch-backup README](cmd/soundtouch-backup/README.md) for usage.
Command-line control of any SoundTouch device: play/pause/volume, presets, source selection, multiroom zones, device discovery, and more. Works entirely over the local network — no cloud dependency. Well-suited for scripting and home automation.
See the [CLI Reference](https://gesellix.github.io/Bose-SoundTouch/guides/CLI-REFERENCE.html) for full usage.
See the [CLI Reference](https://gesellix.github.io/Bose-SoundTouch/docs/guides/CLI-REFERENCE/) for full usage.
---
@@ -86,21 +86,21 @@ See the [soundtouch-web README](cmd/soundtouch-web/README.md) for usage.
go get github.com/gesellix/bose-soundtouch
```
See the [API Reference](https://gesellix.github.io/Bose-SoundTouch/reference/API-ENDPOINTS.html) and [pkg.go.dev](https://pkg.go.dev/github.com/gesellix/bose-soundtouch) for documentation.
See the [API Reference](https://gesellix.github.io/Bose-SoundTouch/docs/reference/API-ENDPOINTS/) and [pkg.go.dev](https://pkg.go.dev/github.com/gesellix/bose-soundtouch) for documentation.
---
## Documentation
- [Getting Started](https://gesellix.github.io/Bose-SoundTouch/guides/GETTING-STARTED.html)
- [Survival Guide](https://gesellix.github.io/Bose-SoundTouch/guides/SURVIVAL-GUIDE.html)
- [Migration Guide](https://gesellix.github.io/Bose-SoundTouch/guides/MIGRATION-GUIDE.html)
- [Device Initial Setup](https://gesellix.github.io/Bose-SoundTouch/guides/DEVICE-INITIAL-SETUP.html)
- [Migration & Safety Guide](https://gesellix.github.io/Bose-SoundTouch/guides/MIGRATION-SAFETY.html)
- [CLI Reference](https://gesellix.github.io/Bose-SoundTouch/guides/CLI-REFERENCE.html)
- [SoundTouch Service Guide](https://gesellix.github.io/Bose-SoundTouch/guides/SOUNDTOUCH-SERVICE.html)
- [HTTPS & CA Setup](https://gesellix.github.io/Bose-SoundTouch/guides/HTTPS-SETUP.html)
- [API Reference](https://gesellix.github.io/Bose-SoundTouch/reference/API-ENDPOINTS.html)
- [Getting Started](https://gesellix.github.io/Bose-SoundTouch/docs/guides/GETTING-STARTED/)
- [Survival Guide](https://gesellix.github.io/Bose-SoundTouch/docs/guides/SURVIVAL-GUIDE/)
- [Migration Guide](https://gesellix.github.io/Bose-SoundTouch/docs/guides/MIGRATION-GUIDE/)
- [Device Initial Setup](https://gesellix.github.io/Bose-SoundTouch/docs/guides/DEVICE-INITIAL-SETUP/)
- [Migration & Safety Guide](https://gesellix.github.io/Bose-SoundTouch/docs/guides/MIGRATION-SAFETY/)
- [CLI Reference](https://gesellix.github.io/Bose-SoundTouch/docs/guides/CLI-REFERENCE/)
- [SoundTouch Service Guide](https://gesellix.github.io/Bose-SoundTouch/docs/guides/SOUNDTOUCH-SERVICE/)
- [HTTPS & CA Setup](https://gesellix.github.io/Bose-SoundTouch/docs/guides/HTTPS-SETUP/)
- [API Reference](https://gesellix.github.io/Bose-SoundTouch/docs/reference/API-ENDPOINTS/)
---
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+3 -3
View File
@@ -43,10 +43,10 @@ func main() {
log.Fatalf("start fake speaker: %v", err)
}
log.Printf("fake speaker HTTP listening on http://%s", s.HTTPAddr())
log.Printf("fake speaker HTTP listening on http://%s", sanitizeLog(s.HTTPAddr()))
if addr := s.TelnetAddr(); addr != "" {
log.Printf("fake speaker telnet listening on tcp://%s", addr)
log.Printf("fake speaker telnet listening on tcp://%s", sanitizeLog(addr))
}
if *register != "" {
@@ -58,7 +58,7 @@ func main() {
if err := registerWithService(*register, target); err != nil {
log.Printf("self-register failed: %v (continuing anyway)", err)
} else {
log.Printf("registered %s with service at %s", target, *register)
log.Printf("registered %s with service at %s", sanitizeLog(target), sanitizeLog(*register))
}
}
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+2 -2
View File
@@ -116,7 +116,7 @@ func main() {
defer close(entries)
if *verbose {
log.Printf("mDNS: Starting scan for service '%s' with timeout %v", *service, *timeout)
log.Printf("mDNS: Starting scan for service '%s' with timeout %v", sanitizeLog(*service), *timeout)
}
// Query for services
@@ -196,7 +196,7 @@ func parseServiceEntry(entry *mdns.ServiceEntry, verbose bool) *ServiceInfo {
if verbose {
log.Printf("mDNS: Received service entry: Name='%s', Host='%s', Port=%d, AddrV4=%v, AddrV6=%v",
entry.Name, entry.Host, entry.Port, entry.AddrV4, entry.AddrV6)
sanitizeLog(entry.Name), sanitizeLog(entry.Host), entry.Port, entry.AddrV4, entry.AddrV6)
}
service := &ServiceInfo{
+1 -1
View File
@@ -207,6 +207,6 @@ Running `cloud` and `local` separately produces two archives. To combine them, u
## See also
- [Cloud Shutdown Survival Guide](../../docs/guides/SURVIVAL-GUIDE.md) — full migration context
- [Cloud Shutdown Survival Guide](../../docs/content/docs/guides/SURVIVAL-GUIDE.md) — full migration context
- [`soundtouch-cli`](../soundtouch-cli/) — live device control
- [`soundtouch-service`](../soundtouch-service/) — local cloud replacement
+128
View File
@@ -0,0 +1,128 @@
package main
import (
"fmt"
"io"
"net/http"
"strings"
"github.com/urfave/cli/v2"
)
// cloudCommand assembles the `soundtouch-cli cloud …` command group.
// All subcommands talk to the AfterTouch service (not the speaker directly)
// and require --service-url.
func cloudCommand() *cli.Command {
return &cli.Command{
Name: "cloud",
Usage: "Manage AfterTouch service data (sources, accounts, devices)",
Subcommands: []*cli.Command{
cloudSourceCmd(),
},
}
}
func cloudSourceCmd() *cli.Command {
return &cli.Command{
Name: "source",
Usage: "Manage sources stored in AfterTouch",
Subcommands: []*cli.Command{
cloudSourceRemoveCmd(),
},
}
}
func cloudSourceRemoveCmd() *cli.Command {
return &cli.Command{
Name: "remove",
Usage: "Remove a source from AfterTouch's datastore for a specific device",
Flags: append(CloudCommonFlags,
&cli.StringFlag{
Name: "account",
Aliases: []string{"a"},
Usage: "Account ID",
Required: true,
},
&cli.StringFlag{
Name: "device",
Aliases: []string{"d"},
Usage: "Device ID",
Required: true,
},
&cli.StringFlag{
Name: "id",
Usage: "Source ID to remove (e.g. 10002)",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Source type to remove (e.g. INTERNET_RADIO). Resolved to a canonical ID; fails if multiple sources share the type.",
},
),
Action: cloudSourceRemove,
}
}
// canonicalSourceID maps well-known SourceKeyType values to their canonical IDs.
// Used to resolve --type to an ID without requiring a round-trip GET.
// TODO We need to ensure that ids here are consistent with the ones used in the AfterTouch service.
var canonicalSourceID = map[string]string{
"AUX": "10001",
"INTERNET_RADIO": "10002",
"LOCAL_INTERNET_RADIO": "10003",
"TUNEIN": "10004",
"RADIO_BROWSER": "10005",
}
func cloudSourceRemove(c *cli.Context) error {
serviceURL := strings.TrimRight(c.String("service-url"), "/")
account := c.String("account")
device := c.String("device")
sourceID := c.String("id")
sourceType := strings.ToUpper(c.String("type"))
if sourceID == "" && sourceType == "" {
return fmt.Errorf("one of --id or --type is required")
}
if sourceID != "" && sourceType != "" {
return fmt.Errorf("only one of --id or --type may be given")
}
if sourceType != "" {
id, ok := canonicalSourceID[sourceType]
if !ok {
return fmt.Errorf("unknown source type %q; use --id for non-canonical sources", sourceType)
}
sourceID = id
}
url := fmt.Sprintf("%s/setup/sources/%s/%s/%s", serviceURL, account, device, sourceID)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return fmt.Errorf("build request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode == http.StatusNoContent {
PrintSuccess(fmt.Sprintf("Removed source %s from device %s (account %s)", sourceID, device, account))
if sourceType != "" {
fmt.Printf(" Type: %s\n", sourceType)
}
return nil
}
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10))
return fmt.Errorf("service returned %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
+1 -1
View File
@@ -568,7 +568,7 @@ type VerboseLogger struct{}
func (v *VerboseLogger) Printf(format string, args ...interface{}) {
timestamp := time.Now().Format("15:04:05")
fmt.Printf("[%s] [WebSocket] %s\n", timestamp, fmt.Sprintf(format, args...))
fmt.Printf("[%s] [WebSocket] %s\n", timestamp, sanitizeLog(fmt.Sprintf(format, args...)))
}
type SilentLogger struct{}
+50
View File
@@ -3,6 +3,8 @@ package main
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"strings"
@@ -685,3 +687,51 @@ func boolToStatus(b bool) string {
return "❌ No"
}
// notifySourcesUpdated POSTs a sourcesUpdated notification directly to the
// speaker's :8090/notification endpoint. The speaker re-fetches its source
// list from AfterTouch immediately. Requires network access to the speaker.
func notifySourcesUpdated(c *cli.Context) error {
if err := RequireHost(c); err != nil {
return err
}
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
info, err := client.GetDeviceInfo()
if err != nil {
return fmt.Errorf("failed to get device info from %s: %w", clientConfig.Host, err)
}
body := fmt.Sprintf(`<updates deviceID="%s"><sourcesUpdated/></updates>`, info.DeviceID)
notifyURL := fmt.Sprintf("http://%s:8090/notification", clientConfig.Host)
req, err := http.NewRequest(http.MethodPost, notifyURL, strings.NewReader(body))
if err != nil {
return fmt.Errorf("build request: %w", err)
}
req.Header.Set("Content-Type", "application/xml")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("post to speaker: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode >= 300 {
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10))
return fmt.Errorf("speaker returned %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
}
PrintSuccess(fmt.Sprintf("Sent sourcesUpdated to %s (%s)", info.DeviceID, clientConfig.Host))
return nil
}
+11 -1
View File
@@ -19,6 +19,16 @@ import (
"github.com/urfave/cli/v2"
)
// CloudCommonFlags defines flags for commands that talk to the AfterTouch service.
var CloudCommonFlags = []cli.Flag{
&cli.StringFlag{
Name: "service-url",
Usage: "AfterTouch service URL",
Required: true,
EnvVars: []string{"AFTERTOUCH_URL"},
},
}
// CommonFlags defines flags that are shared across multiple commands
var CommonFlags = []cli.Flag{
&cli.StringFlag{
@@ -322,7 +332,7 @@ func PrintSuccess(message string) {
// PrintError prints a standard error message
func PrintError(message string) {
fmt.Printf("✗ %s\n", message)
fmt.Printf("✗ %s\n", sanitizeLog(message))
}
// PrintWarning prints a standard warning message
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+10
View File
@@ -1146,6 +1146,12 @@ func main() {
Action: introspectAllServices,
Before: RequireHost,
},
{
Name: "notify-updated",
Usage: "Tell the speaker to re-fetch its source list from AfterTouch",
Action: notifySourcesUpdated,
Before: RequireHost,
},
},
},
// Bass commands
@@ -2238,6 +2244,10 @@ func main() {
// Defined in cmd_setup.go to keep the top-level command list readable.
app.Commands = append(app.Commands, setupCommand())
// AfterTouch service management (sources, accounts, devices).
// Defined in cmd_cloud.go.
app.Commands = append(app.Commands, cloudCommand())
// Sort commands alphabetically (including subcommands and flags recursively)
sortCommands(app.Commands)
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+34 -23
View File
@@ -71,13 +71,13 @@ func initializeDefaultSources(ds *datastore.DataStore) {
for i := range allDevices {
dev := &allDevices[i]
if sources, errGet := ds.GetConfiguredSources(dev.AccountID, dev.DeviceID); errGet == nil {
log.Printf("Initializing default Sources.xml for existing device %s", dev.DeviceID)
log.Printf("Initializing default Sources.xml for existing device %s", sanitizeLog(dev.DeviceID))
// Find default sources and merge them if missing or outdated tokens.
// claimed tracks which stored sources have already been matched by a default,
// so two defaults with the same SourceKeyType but different SourceProviderIDs
// (e.g. INTERNET_RADIO/2 and INTERNET_RADIO/39) are treated as distinct entries.
defaults := ds.GetDefaultSources()
defaults := ds.GetInitialSources()
modified := false
claimed := make(map[int]bool)
@@ -103,13 +103,13 @@ func initializeDefaultSources(ds *datastore.DataStore) {
claimed[foundIdx] = true
if sources[foundIdx].Secret == "" && def.Secret != "" {
log.Printf("Initializing missing token for source %s on device %s", def.SourceKeyType, dev.DeviceID)
log.Printf("Initializing missing token for source %s on device %s", sanitizeLog(def.SourceKeyType), sanitizeLog(dev.DeviceID))
sources[foundIdx].Secret = def.Secret
sources[foundIdx].SecretType = def.SecretType
modified = true
}
} else {
log.Printf("Adding missing default source %s (providerID=%s) to device %s", def.SourceKeyType, def.SourceProviderID, dev.DeviceID)
log.Printf("Adding missing default source %s (providerID=%s) to device %s", sanitizeLog(def.SourceKeyType), sanitizeLog(def.SourceProviderID), sanitizeLog(dev.DeviceID))
sources = append(sources, def)
modified = true
}
@@ -117,7 +117,7 @@ func initializeDefaultSources(ds *datastore.DataStore) {
if modified {
if errSave := ds.SaveConfiguredSources(dev.AccountID, dev.DeviceID, sources); errSave != nil {
log.Printf("Failed to save updated sources for %s: %v", dev.DeviceID, errSave)
log.Printf("Failed to save updated sources for %s: %v", sanitizeLog(dev.DeviceID), errSave)
}
}
}
@@ -147,7 +147,7 @@ func initMusicServices(config serviceConfig, server *handlers.Server) {
clientIDPrefix = clientIDPrefix[:8]
}
log.Printf("Spotify service initialized (client ID: %s...)", clientIDPrefix)
log.Printf("Spotify service initialized (client ID: %s...)", sanitizeLog(clientIDPrefix))
}
if config.amazonClientID != "" {
@@ -172,7 +172,7 @@ func initMusicServices(config serviceConfig, server *handlers.Server) {
clientIDPrefix = clientIDPrefix[:8]
}
log.Printf("Amazon Music service initialized (client ID: %s...)", clientIDPrefix)
log.Printf("Amazon Music service initialized (client ID: %s...)", sanitizeLog(clientIDPrefix))
}
}
@@ -188,7 +188,7 @@ func logBufferCapacityFromEnv(defaultCap int) int {
v, err := strconv.Atoi(raw)
if err != nil {
log.Printf("[Logs] Invalid SOUNDTOUCH_LOG_BUFFER_LINES=%q, using default %d", raw, defaultCap)
log.Printf("[Logs] Invalid SOUNDTOUCH_LOG_BUFFER_LINES=%q, using default %d", sanitizeLog(raw), defaultCap)
return defaultCap
}
@@ -415,7 +415,7 @@ func main() {
persisted := applyPersistedSettings(ds, &config)
if persisted.ServerURL == "" {
log.Printf("Creating default settings.json in %s", config.dataDir)
log.Printf("Creating default settings.json in %s", sanitizeLog(config.dataDir))
persisted = createDefaultSettings(ds, config)
}
@@ -468,7 +468,7 @@ func main() {
server.SetShortcuts(persisted.Shortcuts)
for path, status := range persisted.Shortcuts {
log.Printf("Warning: configured shortcut: %s -> %d", path, status)
log.Printf("Warning: configured shortcut: %s -> %d", sanitizeLog(path), status)
}
recorder := proxy.NewRecorder(config.dataDir)
@@ -477,11 +477,11 @@ func main() {
patterns, err := proxy.LoadPatterns(patternsPath)
if err != nil {
log.Printf("Warning: Failed to load patterns from %s: %v", patternsPath, err)
log.Printf("Warning: Failed to load patterns from %s: %v", sanitizeLog(patternsPath), err)
}
if len(patterns) == 0 {
log.Printf("Creating default patterns at %s", patternsPath)
log.Printf("Creating default patterns at %s", sanitizeLog(patternsPath))
patterns = proxy.DefaultPatterns()
@@ -512,17 +512,26 @@ func main() {
} else {
stockholmHandler = sh
log.Printf("Stockholm frontend enabled from %s", config.stockholmDir)
log.Printf("Stockholm frontend enabled from %s", sanitizeLog(config.stockholmDir))
}
}
r := setupRouter(server, stockholmHandler)
log.Printf("Go service starting on %s", config.serverURL)
// Bind the listener before logging so we print the true
// effective port (handles :0 and catches "address already
// in use" before the TLS goroutine launches).
ln, err := net.Listen("tcp", config.addr)
if err != nil {
return fmt.Errorf("failed to listen on %s: %w", config.addr, err)
}
log.Printf("Go service listening on %s (configured: %s, server URL: %s)",
ln.Addr().String(), sanitizeLog(config.addr), sanitizeLog(config.serverURL))
// TLS cert generation can be slow on constrained hardware; run it in the
// background so the HTTP server is available immediately.
log.Printf("HTTPS setup running in background; %s will be available shortly", config.httpsServerURL)
log.Printf("HTTPS setup running in background; %s will be available shortly", sanitizeLog(config.httpsServerURL))
go func() {
tlsConfig, err := cm.GetServerTLSConfig(config.domains)
@@ -536,7 +545,7 @@ func main() {
runHTTPSPreflight(config.httpsServerURL, config.serverURL, config.dnsEnabled, server.ResolveServerURLIPForPreflight)
}()
return http.ListenAndServe(config.addr, r)
return http.Serve(ln, r)
},
Commands: []*cli.Command{
{
@@ -652,7 +661,7 @@ func loadConfig(c *cli.Context) serviceConfig {
discoveryInterval, err := time.ParseDuration(discoveryIntervalStr)
if err != nil {
log.Printf("Warning: Failed to parse discovery interval %s, using default 5m: %v", discoveryIntervalStr, err)
log.Printf("Warning: Failed to parse discovery interval %s, using default 5m: %v", sanitizeLog(discoveryIntervalStr), err)
discoveryInterval = 5 * time.Minute
}
@@ -1244,6 +1253,7 @@ func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler) *
r.Get("/dns-discoveries", server.HandleGetDNSDiscoveries)
r.Get("/dns-discoveries/download", server.HandleDownloadDNSDiscoveries)
r.Delete("/dns-discoveries", server.HandleClearDNSDiscoveries)
r.Delete("/sources/{account}/{device}/{sourceID}", server.HandleDeleteSource)
r.Get("/devices/{deviceId}/events", server.HandleGetDeviceEvents)
r.Get("/device-summary/{deviceId}", server.HandleDeviceSummary)
@@ -1293,7 +1303,7 @@ func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, h
return &tlsConfig.Certificates[0], nil
}
log.Printf("[TLS] ❌ No certificate available for %s", clientHello.ServerName)
log.Printf("[TLS] ❌ No certificate available for %s", sanitizeLog(clientHello.ServerName))
return nil, fmt.Errorf("no certificate available for %s", clientHello.ServerName)
}
@@ -1305,8 +1315,6 @@ func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, h
ErrorLog: log.Default(), // Ensure error logging is enabled
}
log.Printf("Go service starting HTTPS on %s", httpsServerURL)
go func() {
listener, err := net.Listen("tcp", httpsAddr)
if err != nil {
@@ -1314,6 +1322,9 @@ func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, h
return
}
log.Printf("Go service listening HTTPS on %s (server URL: %s)",
listener.Addr().String(), sanitizeLog(httpsServerURL))
tlsListener := tls.NewListener(listener, tlsConfig)
// Wrap listener to log connection attempts
@@ -1360,9 +1371,9 @@ func runHTTPSPreflight(httpsServerURL, serverURL string, dnsEnabled bool, resolv
case res.Skipped:
// Listener already on :443 — nothing to say.
case res.NotApplicable:
log.Printf("HTTPS pre-flight: :443 check skipped — %s", res.Reason)
log.Printf("HTTPS pre-flight: :443 check skipped — %s", sanitizeLog(res.Reason))
default:
log.Printf("HTTPS pre-flight: :443 reachable at localhost and %s ✓", res.LANHost)
log.Printf("HTTPS pre-flight: :443 reachable at localhost and %s ✓", sanitizeLog(res.LANHost))
}
return
@@ -1437,7 +1448,7 @@ func (c *loggingTLSConn) Read(b []byte) (n int, err error) {
if strings.Contains(err.Error(), "tls:") ||
strings.Contains(err.Error(), "handshake") ||
strings.Contains(err.Error(), "certificate") {
log.Printf("[TLS] ❌ Handshake failed from %s: %v", c.addr, err)
log.Printf("[TLS] ❌ Handshake failed from %s: %v", sanitizeLog(c.addr.String()), err)
}
}
}
+1
View File
@@ -9,6 +9,7 @@ DELETE /setup/devices/{deviceId} handlers.(
DELETE /setup/dns-discoveries handlers.(*Server).HandleClearDNSDiscoveries-fm
DELETE /setup/interactions/sessions handlers.(*Server).HandleCleanupSessions-fm
DELETE /setup/interactions/sessions/{session} handlers.(*Server).HandleDeleteSession-fm
DELETE /setup/sources/{account}/{device}/{sourceID} handlers.(*Server).HandleDeleteSource-fm
DELETE /streaming/account/{account}/device/{device} handlers.(*Server).HandleMargeRemoveDevice-fm
DELETE /streaming/account/{account}/device/{device}/preset/{presetNumber} handlers.(*Server).HandleMargeRemovePreset-fm
DELETE /streaming/account/{account}/group/{groupId} handlers.(*Server).HandleMargeDeleteGroup-fm
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+3 -3
View File
@@ -86,7 +86,7 @@ func main() {
}
if rawBind != "" && bindAddr != rawBind {
log.Printf("Resolved --bind %q to %s", rawBind, bindAddr)
log.Printf("Resolved --bind %q to %s", sanitizeLog(rawBind), sanitizeLog(bindAddr))
}
rawIface := c.String("interface")
@@ -94,7 +94,7 @@ func main() {
ifaceName := defaultDiscoveryInterface(rawIface, rawBind, bindAddr)
if rawIface == "" && ifaceName != "" {
log.Printf("Defaulting --interface to %q from --bind", ifaceName)
log.Printf("Defaulting --interface to %q from --bind", sanitizeLog(ifaceName))
}
addr := ":" + port
@@ -131,7 +131,7 @@ func main() {
r := chi.NewRouter()
webApp.Mount(r, discoveryService)
log.Printf("AfterTouch Web UI starting on http://%s", addr)
log.Printf("AfterTouch Web UI starting on http://%s", sanitizeLog(addr))
return http.ListenAndServe(addr, r)
},
+13
View File
@@ -0,0 +1,13 @@
package main
import "strings"
// sanitizeLog strips newline characters from s to prevent log-injection
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
// external APIs may contain attacker-controlled newlines.
func sanitizeLog(s string) string {
s = strings.ReplaceAll(s, "\n", `\n`)
s = strings.ReplaceAll(s, "\r", `\r`)
return s
}
+1 -1
View File
@@ -573,7 +573,7 @@ type VerboseLogger struct{}
func (v *VerboseLogger) Printf(format string, args ...interface{}) {
timestamp := time.Now().Format("15:04:05")
fmt.Printf("[%s] [WebSocket] %s\n", timestamp, fmt.Sprintf(format, args...))
fmt.Printf("[%s] [WebSocket] %s\n", timestamp, sanitizeLog(fmt.Sprintf(format, args...)))
}
// SilentLogger provides no-op WebSocket logging
+34
View File
@@ -0,0 +1,34 @@
# Local Hugo/Hextra documentation server.
#
# Usage:
# make dev-docs # start the live-reload server (http://localhost:1313)
# make dev-docs-tidy # run hugo mod tidy (required on first run, or after
# # changing hugo.toml module imports)
# make hugo ARGS="..." # run any other hugo CLI command, e.g.
# # make hugo ARGS="version"
# # make hugo ARGS="new content/blog/my-post.md"
#
# The hugomods/hugo:exts image bundles Hugo extended + Go so Hugo modules
# (Hextra) work without any extra tooling on the host.
services:
hugo:
image: hugomods/hugo:exts
# --source docs/ because docs/ is the Hugo root inside the repo.
# --baseURL / overrides the production subpath (/Bose-SoundTouch/) so
# absolute links work at http://localhost:1313/ during local development.
# The full repo is mounted so enableGitInfo can read git history.
command: server --source docs/ --baseURL / --bind 0.0.0.0 --buildDrafts --navigateToChanged
ports:
- "1313:1313"
volumes:
- .:/src
# Persist the Hugo module cache across runs so 'hugo mod tidy' only
# downloads Hextra once.
- hugo-mod-cache:/root/.cache/hugo_cache
working_dir: /src
environment:
- HUGO_PARAMS_GITHASH
volumes:
hugo-mod-cache:
-113
View File
@@ -1,113 +0,0 @@
# Table of Contents
* [Introduction](README.md)
## User Guides
* [Device-Local Install Journeys](DEVICE-LOCAL-INSTALL.md)
* [Cloud Shutdown Survival Guide](guides/SURVIVAL-GUIDE.md)
* [Self-Hosting AfterTouch](guides/SELF-HOSTING.md)
* [Connecting Music Services](guides/MUSIC-SERVICES.md)
* [Migration & Safety Guide](guides/MIGRATION-SAFETY.md)
* [CLI Reference](guides/CLI-REFERENCE.md)
* [Backup Tool](../cmd/soundtouch-backup/README.md)
* [Getting Started](guides/GETTING-STARTED.md)
* [SoundTouch Service](guides/SOUNDTOUCH-SERVICE.md)
* [Initial Device Setup](guides/DEVICE-INITIAL-SETUP.md)
* [Capture Device Pairing Traffic](guides/CAPTURE-DEVICE-PAIRING.md)
* [Capture Migration Traffic](guides/CAPTURE-MIGRATION-TRAFFIC.md)
* [Device Setup Flow](DEVICE-SETUP.md)
* [MAC Address Mapping](guides/MAC-ADDRESS-MAPPING.md)
* [HTTPS Setup](guides/HTTPS-SETUP.md)
* [Deployment](guides/DEPLOYMENT.md)
* [Raspberry Pi Guide](guides/RASPBERRY-PI.md)
* [Troubleshooting](guides/TROUBLESHOOTING.md)
* [IoT Implementation Guide](guides/IOT-IMPLEMENTATION-GUIDE.md)
* [Migration Guide](guides/MIGRATION-GUIDE.md)
* [MQTT Integration Design](guides/MQTT-INTEGRATION-DESIGN.md)
* [Useful Links](#useful-links)
### Useful Links
* [Cloud Shutdown Survival Guide](guides/SURVIVAL-GUIDE.md)
* [Raspberry Pi Installer](../scripts/raspberry-pi/README.md)
* [Updating the Service](../scripts/raspberry-pi/README.md#updating-to-a-new-version)
* [CLI Reference](guides/CLI-REFERENCE.md)
## Technical Reference
* [API Cookbook](reference/API-COOKBOOK.md)
* [API Endpoints](reference/API-ENDPOINTS.md)
* [Spotify Account Addition](reference/spotify-account-addition.md)
* [Cloud API Emulation](reference/CLOUD-API.md)
* [System Endpoints](reference/SYSTEM-ENDPOINTS.md)
* [Speaker Endpoint](reference/SPEAKER-ENDPOINT.md)
* [WebSocket Events](reference/WEBSOCKET-EVENTS.md)
* [Device Pairing Flow](reference/DEVICE-PAIRING-FLOW.md)
* [Discovery](reference/DISCOVERY.md)
* [Zone Management](reference/ZONE-MANAGEMENT.md)
* [Preset Management](reference/PRESET-MANAGEMENT.md)
* [Source Selection](reference/SOURCE-SELECTION.md)
* [Volume Controls](reference/VOLUME-CONTROLS.md)
* [RadioBrowser](reference/radio-browser.md)
* [Bass Controls](reference/BASS-CONTROLS.md)
* [Key Controls](reference/KEY-CONTROLS.md)
* [Feature Mapping](reference/FEATURE-MAPPING.md)
## Concepts
* [Request Recording](REQUEST_RECORDING_CONCEPT.md)
* [Spotify Overview](concepts/spotify-overview.md)
* [Spotify Priming Strategy](concepts/spotify-priming-strategy.md)
* [Spotify OAuth](concepts/spotify-oauth.md)
* [Amazon Music OAuth](concepts/amazon-music-oauth.md)
* [Encrypted Export](concepts/ENCRYPTED-EXPORT.md)
* [Diagnostic Export (Maintainer Setup)](DIAGNOSTIC-EXPORT.md)
* [soundtouch-web Roadmap](soundtouch-web-roadmap.md)
## Analysis & Research
* [API Coverage Analysis](analysis/API-COVERAGE.md)
* [Supported URLs](analysis/SUPPORTED-URLS.md)
* [Upstream URLs](analysis/UPSTREAM-URLS.md)
* [Anonymization Summary](analysis/ANONYMIZATION-SUMMARY.md)
* [Device Redirect Methods](analysis/DEVICE-REDIRECT-METHODS.md)
* [Telnet (Port 17000) Migration Method](analysis/TELNET-MIGRATION-METHOD.md)
* [Telnet Command Reference](analysis/TELNET-COMMAND-REFERENCE.md)
* [Setup WebSocket Experiment](analysis/SETUP-WEBSOCKET-EXPERIMENT.md)
* [Factory Reset Protocol](analysis/FACTORY-RESET-PROTOCOL.md)
* [Wiki API Comparison](analysis/WIKI-COMPARISON.md)
* [IoT Config Summary](analysis/IOT-CONFIG-SUMMARY.md)
* [IoT Configuration Analysis](analysis/IOT-CONFIGURATION-ANALYSIS.md)
* [Bose Lab Runbook](analysis/BOSE-LAB-RUNBOOK.md)
* [Missing Routes Spotify](analysis/MISSING-ROUTES-SPOTIFY.md)
* [Bose App ADB Emulator](analysis/BOSE-APP-ADB-Emulator.md)
* [Community Tools](analysis/bose-soundtouch-community-tools.md)
## Parity Analysis
* [Parity Improvements](PARITY-IMPROVEMENTS.md)
* [Parity SoundCork](PARITY-SOUNDCORK.md)
* [Parity OpenCloudTouch](PARITY-OPENCLOUDTOUCH.md)
## Appendix (Other Documents)
* [External Services Abstraction](EXTERNAL-SERVICES-ABSTRACTION.md)
* [API Navigation Reference](API-NAVIGATION-REFERENCE.md)
* [Claude Instructions](CLAUDE.md)
* [Content Selection Implementation](CONTENT-SELECTION-IMPLEMENTATION.md)
* [Device Customization Setup](DEVICE-CUSTOMIZATION-SETUP.md)
* [Device Logging](DEVICE-LOGGING.md)
* [Feature History](FEATURE_HISTORY.md)
* [Host/Port Parsing](HOST-PORT-PARSING.md)
* [Manual Network Discovery](MANUAL-NETWORK-DISCOVERY.md)
* [Navigation Guide](NAVIGATION-GUIDE.md)
* [Official API Verification](OFFICIAL-API-VERIFICATION.md)
* [Preset Quickstart](PRESET-QUICKSTART.md)
* [Project Patterns](PROJECT-PATTERNS.md)
* [Service Availability Implementation](SERVICE-AVAILABILITY-IMPLEMENTATION.md)
* [SoundTouch Service Announcement](SOUNDTOUCH-SERVICE-ANNOUNCEMENT.md)
* [Undocumented Community Features](UNDOCUMENTED-COMMUNITY-FEATURES.md)
* [Unimplemented Endpoints](UNIMPLEMENTED-ENDPOINTS.md)
* [Preset Store](preset-store.md)
* [SCMUDC Enrichment Implementation](SCMUDC-ENRICHMENT-IMPLEMENTATION.md)
* [Device Lifecycle and Power On Enhancement](device-lifecycle-and-power-on-enhancement.md)
* [Device Lifecycle Summary](device-lifecycle-summary.md)
* [Power On Implementation Guide](power-on-implementation-guide.md)
* [SCMUDC Events Analysis](scmudc-events-analysis.md)
* [Parity Improvements](PARITY-IMPROVEMENTS.md)
* [Parity SoundCork](PARITY-SOUNDCORK.md)
* [Stockholm Port Guide](stockholm-port-guide.md)
-11
View File
@@ -1,11 +0,0 @@
title: Bose SoundTouch Toolkit
description: Documentation for controlling and preserving Bose SoundTouch devices
remote_theme: pages-themes/minimal@v0.2.0
plugins:
- jekyll-remote-theme
- jekyll-relative-links
relative_links:
enabled: true
collections: true
include:
- SUMMARY.md
+1 -1
View File
@@ -784,4 +784,4 @@ docker compose up # Mock devices + web app
- [UPnP Device Architecture](http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf)
- [Go Embed Directive](https://pkg.go.dev/embed)
- [Gorilla WebSocket](https://github.com/gorilla/websocket)
- [PROJECT-PATTERNS.md](../PROJECT-PATTERNS.md) - Detailed pattern documentation
- [PROJECT-PATTERNS.md](../content/docs/appendix/PROJECT-PATTERNS.md) - Detailed pattern documentation
+46
View File
@@ -0,0 +1,46 @@
/* Font paths use ../../fonts/ (two levels up) so the URL resolves correctly
regardless of where the CSS is served from:
- 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/ */
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../../fonts/noto-sans-v42-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Noto Sans';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('../../fonts/noto-sans-v42-latin-italic.woff2') format('woff2');
}
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../../fonts/noto-sans-v42-latin-700.woff2') format('woff2');
}
@font-face {
font-family: 'Noto Sans';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('../../fonts/noto-sans-v42-latin-700italic.woff2') format('woff2');
}
:root {
--hx-default-font-family: "Noto Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
.content {
font-family: var(--hx-default-font-family);
}
+65
View File
@@ -0,0 +1,65 @@
---
title: AfterTouch
layout: hextra-home
---
{{< hextra/hero-badge >}}
<div class="hx-w-2 hx-h-2 hx-rounded-full hx-bg-primary-400"></div>
<span>Free, open source</span>
{{< icon name="arrow-circle-right" attributes="height=14" >}}
{{< /hextra/hero-badge >}}
<div class="hx-mt-6 hx-mb-6">
{{< hextra/hero-headline >}}
Keep Your Bose SoundTouch&nbsp;Speakers Alive
{{< /hextra/hero-headline >}}
</div>
<div class="hx-mb-12">
{{< hextra/hero-subtitle >}}
Bose shut down SoundTouch cloud services on May 6, 2026.&nbsp;<br class="sm:hx-block hx-hidden" />AfterTouch replaces the cloud — presets, music browsing, stereo pairing, all restored.
{{< /hextra/hero-subtitle >}}
</div>
<div class="hx-mb-6">
{{< hextra/hero-button text="Get Started" link="docs/guides/MIGRATION-GUIDE" >}}
{{< hextra/hero-button text="Survival Guide" link="docs/guides/SURVIVAL-GUIDE" style="outline" >}}
</div>
<div class="hx-mt-6">
{{< hextra/feature-grid >}}
{{< hextra/feature-card
title="Presets Restored"
subtitle="Preset buttons, long-press assignment, and recently-played sync — fully working."
icon="star"
>}}
{{< hextra/feature-card
title="Music Browsing"
subtitle="TuneIn, Internet Radio, RadioBrowser, and Spotify via soundtouch-web and soundtouch-cli."
icon="speakerphone"
>}}
{{< hextra/feature-card
title="Stereo Pairing"
subtitle="SoundTouch 10 stereo pairing via soundtouch-cli, no Bose cloud required."
icon="adjustments"
>}}
{{< hextra/feature-card
title="Three Deployment Options"
subtitle="Run on a Raspberry Pi, a VPS, or directly on the speaker itself."
icon="server"
link="docs/guides/DEPLOYMENT-OVERVIEW"
>}}
{{< hextra/feature-card
title="CLI Control"
subtitle="soundtouch-cli for scripting, home automation, and direct device control."
icon="terminal"
link="docs/guides/CLI-REFERENCE"
>}}
{{< hextra/feature-card
title="Open Source"
subtitle="MIT licensed. Not affiliated with Bose Corporation."
icon="shield-check"
link="https://github.com/gesellix/Bose-SoundTouch"
>}}
{{< /hextra/feature-grid >}}
</div>
+120
View File
@@ -0,0 +1,120 @@
---
title: "Welcome to AfterTouch: Your SoundTouch Speakers, Still Alive"
date: 2026-05-24
description: "Bose shut down SoundTouch cloud services in May 2026. AfterTouch replaces everything your speakers relied on — migration, radio, Spotify, presets, and more."
tags:
- migration
- web
- spotify
- cli
sidebar:
exclude: true
---
On May 6, 2026, Bose shut down the SoundTouch cloud services that millions of speakers
depended on for account sync, presets, internet radio, and streaming. Speakers kept
working locally, but remote features stopped and first-time setup became impossible.
AfterTouch was built to change that. It is a self-hosted replacement for the Bose
cloud infrastructure — a drop-in local service that your speakers talk to instead of
`streaming.bose.com`. This post covers what works today and how to get started.
## What works right now
### Migration and first-time setup
If your speaker was registered with Bose before the shutdown, AfterTouch can **migrate
your existing account and presets** in a single step — no reconfiguration on the
speaker side. If you are setting up a factory-reset or brand-new speaker, AfterTouch
handles that path too, guiding you through Wi-Fi pairing and account creation locally.
See the [Migration Guide](../docs/guides/MIGRATION-GUIDE.md) for step-by-step instructions.
### Internet radio — TuneIn and RadioBrowser
Both **TuneIn** and **RadioBrowser** are fully supported for browsing and playback.
Navigate categories and search for stations exactly as you did with the original Bose
app. TuneIn delivers the same station catalogue; RadioBrowser provides an open,
community-maintained alternative.
### Spotify
**Spotify** works via both OAuth (account linking) and Spotify Connect (the ZeroConf
"connect to device" flow from the Spotify app). Once linked, playback and device
selection behave the same as before.
### Presets
Your six preset buttons work. AfterTouch stores preset bindings locally and serves them
back to the speaker on request. You can also **save new presets** — via the API,
via `soundtouch-cli`, or through the soundtouch-web UI.
### ST-10 stereo pairing
**SoundTouch 10 stereo pairs** (and other ST pairing configurations) are supported
end-to-end: creation, management, and playback routing all go through AfterTouch.
### soundtouch-web — browser UI
**soundtouch-web** is an early-stage but functional browser UI bundled with AfterTouch.
It gives you:
- TuneIn and RadioBrowser browsing and playback
- Speaker management and device discovery
- Recent tracks panel
- Multi-room zone management
It runs as part of the AfterTouch service — no separate install needed.
![soundtouch-web UI showing Spotify playback, presets, sources, and zone management](/images/blog/soundtouch-web-ui.png)
### Automation with soundtouch-cli
The **`soundtouch-cli`** command-line tool covers every speaker control: play, pause,
volume, source selection, preset recall, group management, migration, and more.
It is well-suited for home-automation scripts, cron jobs, and shell one-liners.
## Three ways to install
AfterTouch runs on any machine your speakers can reach:
1. **On the speaker itself** — install directly on supported SoundTouch hardware via
the on-device installer. The speaker hosts its own replacement cloud, with no
additional hardware required.
2. **On a local network host** — run AfterTouch on any machine on your LAN. A
**Raspberry Pi Zero 2W** handles the load without breaking a sweat, making this
path remarkably low-cost and low-power.
3. **On a cloud or VPS host** — deploy to a remote server for access outside your
home network. AfterTouch handles TLS certificate generation and DNS configuration
for this scenario.
All three paths are documented in the [Deployment Overview](../docs/guides/DEPLOYMENT-OVERVIEW.md).
## Current release
**v0.93.1** — released May 24, 2026
## Community
AfterTouch would not be where it is without the people who opened issues, tested
pre-release builds, reported edge cases, and contributed code. A significant share of
the fixes and features shipped in the lead-up to the cloud shutdown were driven by
real-world feedback from the community — from migration quirks to stereo-pair
specifics to Spotify Connect timing issues. Thank you to everyone who helped.
If you run into something or have an idea, the
[GitHub issue tracker](https://github.com/gesellix/Bose-SoundTouch/issues) and
[Discussions](https://github.com/gesellix/Bose-SoundTouch/discussions) are the
right places to start.
## What's next
The soundtouch-web UI will gain richer preset management — browsing, editing, and
reordering presets directly from the browser. Longer term, merging
`soundtouch-service` and `soundtouch-web` into a single binary is on the table,
which would simplify deployment to a single process with no extra flags.
This blog will be updated monthly — or whenever something significant ships.
Subscribe to the [GitHub releases](https://github.com/gesellix/Bose-SoundTouch/releases)
for individual version notes.
+5
View File
@@ -0,0 +1,5 @@
---
title: News & Updates
---
Project updates, release notes, and development notes for AfterTouch — the local replacement for the Bose SoundTouch cloud.
+13 -9
View File
@@ -1,3 +1,9 @@
---
title: Introduction
sidebar:
open: true
---
# Bose SoundTouch Toolkit Documentation
Welcome to the documentation for the Bose SoundTouch Toolkit. This comprehensive toolkit helps you keep your Bose SoundTouch speakers functional even after the Bose Cloud shutdown in May 2026, with enhanced local management and monitoring capabilities.
@@ -10,7 +16,7 @@ Welcome to the documentation for the Bose SoundTouch Toolkit. This comprehensive
### For Existing Users
- **[Cloud Shutdown Survival Guide](guides/SURVIVAL-GUIDE.md)** - Prepare for the May 2026 shutdown
- **[Backup Tool](../cmd/soundtouch-backup/README.md)** - Back up your cloud account and speaker data before shutdown
- **[Backup Tool](https://github.com/gesellix/Bose-SoundTouch/blob/main/cmd/soundtouch-backup/README.md)** - Back up your cloud account and speaker data before shutdown
- **[SoundTouch Service Guide](guides/SOUNDTOUCH-SERVICE.md)** - Advanced service configuration
## 📋 Essential Documentation
@@ -41,7 +47,7 @@ The documentation is organized into three main categories:
### Advanced Features
- [MAC Address Mapping](guides/MAC-ADDRESS-MAPPING.md) - Device identification
- [CLI Reference](guides/CLI-REFERENCE.md) - Command-line tools
- [Backup Tool](../cmd/soundtouch-backup/README.md) - Cloud account and speaker data backup
- [Backup Tool](https://github.com/gesellix/Bose-SoundTouch/blob/main/cmd/soundtouch-backup/README.md) - Cloud account and speaker data backup
- [IoT Implementation Guide](guides/IOT-IMPLEMENTATION-GUIDE.md) - IoT integrations
- [MQTT Integration Design](guides/MQTT-INTEGRATION-DESIGN.md) - MQTT setup
@@ -61,20 +67,18 @@ The documentation is organized into three main categories:
- [IoT Config Summary](analysis/IOT-CONFIG-SUMMARY.md) - Configuration summaries
### Device Lifecycle & Network Independence
- **[Device Lifecycle and /power_on Enhancement](device-lifecycle-and-power-on-enhancement.md)** - Complete analysis of device registration and network independence improvements
- [/power_on Implementation Guide](power-on-implementation-guide.md) - Technical implementation details for enhanced device management
- **[Device Lifecycle and /power_on Enhancement](appendix/device-lifecycle-and-power-on-enhancement.md)** - Complete analysis of device registration and network independence improvements
- [/power_on Implementation Guide](appendix/power-on-implementation-guide.md) - Technical implementation details for enhanced device management
## 🏗 Concept Documentation
Current concept docs are listed under the **Concepts** section of [SUMMARY.md](SUMMARY.md#concepts). Highlights:
- [Spotify Overview](concepts/spotify-overview.md) — mental model, Spotify Connect vs OAuth-intercept, DNS rewrite gotcha
- [Spotify OAuth](concepts/spotify-oauth.md) — flows and management endpoints
- [Amazon Music OAuth](concepts/amazon-music-oauth.md) — companion to Spotify OAuth; same protocol shape, different scopes
- [Encrypted Export](concepts/ENCRYPTED-EXPORT.md) — `.age`-encrypted diagnostic bundles
- [Request Recording](REQUEST_RECORDING_CONCEPT.md) — how the proxy captures live device traffic for parity testing
- [Request Recording](appendix/REQUEST_RECORDING_CONCEPT.md) — how the proxy captures live device traffic for parity testing
Older planning artefacts ("Enhanced State Management System", "Upstream Service Simulation") live under [docs/archive/](archive/) — kept for the record, no longer current.
Older planning artefacts ("Enhanced State Management System", "Upstream Service Simulation") live under [docs/archive/](../../archive/) — kept for the record, no longer current.
## 💡 Quick Reference
@@ -90,4 +94,4 @@ Older planning artefacts ("Enhanced State Management System", "Upstream Service
- **Documentation**: Check troubleshooting guides first
- **Community**: Share experiences and help others
For a complete list of all documents, see the [Summary](SUMMARY.md).
For a complete list of all documents, browse the sections in the sidebar.
@@ -1,5 +1,6 @@
# Placeholder values for examples
---
title: "Placeholder values for examples"
---
This repo is public. Documentation, READMEs, example configs, and test
fixtures must never carry real LAN IPs, real device MACs, real Bose
account IDs, or personal device names from any maintainer or
@@ -1,5 +1,6 @@
# Bose SoundTouch API Coverage Analysis
---
title: "Bose SoundTouch API Coverage Analysis"
---
**Last Updated:** February 2026
**API Version:** Official Bose SoundTouch Web API v1.0
**Implementation Status:** 100% Official Coverage + Extended Features
@@ -1,5 +1,6 @@
# Bose SoundTouch Traffic Interception Runbook
---
title: "Bose SoundTouch Traffic Interception Runbook"
---
Intercept HTTPS/WebSocket traffic from the Bose SoundTouch Android app using an Android emulator, mitmproxy, and Frida. Tested on Apple Silicon (ARM64) Mac.
## Automated Setup
@@ -1,5 +1,6 @@
# Bose SoundTouch Traffic Analysis Runbook
---
title: "Bose SoundTouch Traffic Analysis Runbook"
---
> **Goal:** Set up a Raspberry Pi as a transparent access point to fully observe the traffic of the Bose SoundTouch app specifically the pairing flow with the Bose Cloud. This serves as a basis for later reverse engineering / simulation of the cloud endpoints.
---
@@ -1,5 +1,6 @@
# Device Redirect Methods & Custom Service Setup
---
title: "Device Redirect Methods & Custom Service Setup"
---
To enable offline operation or use custom services like **SoundCork** or **ÜberBöse API**, SoundTouch devices must be redirected from Bose's official cloud endpoints to a local or custom server. This document outlines the three known methods to achieve this, gathered from community reverse-engineering efforts in the **SoundCork** and **ÜberBöse API** projects.
> A fourth, **SSH-free** path — driving the device's diagnostic shell on TCP port 17000 — is being added as a peer to the XML and DNS methods. See **[TELNET-MIGRATION-METHOD.md](TELNET-MIGRATION-METHOD.md)** for the use cases, community findings, and feasibility analysis. The `/etc/hosts` method documented below is now deprecated and will not be exposed in the web UI.
@@ -1,5 +1,6 @@
# What a SoundTouch speaker does during factory reset
---
title: "What a SoundTouch speaker does during factory reset"
---
Observed live on ST10 firmware `27.0.6.46330.5043500` (build `epdbuild.trunk.hepdswbld04.2022-08-04`) on 2026-05-12, by running `soundtouch-cli setup factory-reset` and tailing the speaker's `logread` over SSH. The trace is preserved at `_/logs/factory-reset.txt` for reference.
## Sequence
@@ -1,5 +1,6 @@
# IoT Configuration Quick Reference
---
title: "IoT Configuration Quick Reference"
---
## Key Files and Locations
| File/Location | Purpose | Notes |
@@ -1,5 +1,6 @@
# IoT Configuration Analysis
---
title: "IoT Configuration Analysis"
---
## Overview
This document provides a detailed analysis of the AWS IoT configuration system used by Bose SoundTouch devices, based on firmware backup analysis from ST10 and ST20 models.
@@ -1,5 +1,6 @@
# Spotify Account Addition Implementation Status
---
title: "Spotify Account Addition Implementation Status"
---
To fully replace Bose cloud services for the Spotify account addition flow in the "Stockholm" SoundTouch application, the following routes have been implemented in the `soundtouch-service`:
## 1. OAuth Token Exchange (Bose Cloud)
@@ -1,5 +1,6 @@
# Experiment: Does bare `setMargeAccount` work outside the SETUP bracket?
---
title: "Experiment: Does bare `setMargeAccount` work outside the SETUP bracket?"
---
## Why we are doing this
Our captured pairing flow (`docs/reference/DEVICE-PAIRING-FLOW.md`) shows the official Bose app always sends `setMargeAccount` *inside* a `SETUP_START``SETUP_ENTER``SETUP_LEAVE` state-machine bracket over WebSocket. The question this experiment answers:
@@ -1,5 +1,6 @@
# SoundTouch supportedURLs Endpoint Analysis
---
title: "SoundTouch supportedURLs Endpoint Analysis"
---
This document provides a comprehensive analysis of the `/supportedURLs` endpoint response from real Bose SoundTouch devices and compares it with our current implementation.
## Discovery Summary
@@ -1,5 +1,6 @@
# Bose SoundTouch Telnet (Port 17000) Command Reference
---
title: "Bose SoundTouch Telnet (Port 17000) Command Reference"
---
A consolidated reference for the diagnostic shell that listens on TCP port
17000 across the SoundTouch line. Compiled from multiple community sources
to give a single map of what's been observed in the wild — useful both for
@@ -1,5 +1,6 @@
# Telnet (Port 17000) Migration Method — Analysis
---
title: "Telnet (Port 17000) Migration Method — Analysis"
---
This document captures the use cases, community findings, and feasibility analysis
for adding a **Telnet/port 17000** migration path to `soundtouch-service` as a
peer of the existing XML and DNS-based methods. The `/etc/hosts` method stays
@@ -8,7 +9,7 @@ deprecated and is intentionally kept off the visible UI options.
> **Sources** — community discussion synthesised from
> [gesellix/Bose-SoundTouch#221](https://github.com/gesellix/Bose-SoundTouch/issues/221),
> [gesellix/Bose-SoundTouch#236](https://github.com/gesellix/Bose-SoundTouch/issues/236),
> [scheilch/opencloudtouch#167](https://github.com/scheilch/opencloudtouch/issues/167),
> [scheilch/opencloudtouch#167](https://github.com/opencloudtouch/opencloudtouch/issues/167),
> [deborahgu/soundcork#228](https://github.com/deborahgu/soundcork/issues/228),
> [deborahgu/soundcork#141](https://github.com/deborahgu/soundcork/issues/141),
> the post-EOS walkthrough PDF in `docs/`,
@@ -1,5 +1,6 @@
# Upstream URLs & Domains Analysis
---
title: "Upstream URLs & Domains Analysis"
---
This document provides a comprehensive overview of the upstream Bose cloud services and domains that SoundTouch devices communicate with. These details were gathered from firmware analysis of ST10/ST20 devices, binary string extraction, and community research from the **SoundCork** project (Issue #128).
## Core Service Domains
@@ -1,5 +1,6 @@
# SoundTouch API Comparison: Community Wiki vs Current Implementation
---
title: "SoundTouch API Comparison: Community Wiki vs Current Implementation"
---
**Date:** January 2026
**Source:** [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
**Our Implementation:** Bose-SoundTouch Go Library v1.0
+4
View File
@@ -0,0 +1,4 @@
---
title: "Analysis & Research"
weight: 4
---
@@ -1,5 +1,6 @@
# Bose SoundTouch — Community Tools for Post-EOL Preservation
---
title: "Bose SoundTouch — Community Tools for Post-EOL Preservation"
---
> **Context:** Bose announced the shutdown of SoundTouch cloud services, extended to **May 6, 2026**. On that date the official SoundTouch app will update to a local-only version. Bose has released the [SoundTouch Web API documentation](https://assets.bosecreative.com/m/496577402d128874/original/SoundTouch-Web-API.pdf) as open-source to enable community-driven development. This document surveys the active community projects, their feature coverage, and open development opportunities.
---
@@ -233,7 +234,7 @@ A design document exists (`docs/guides/MQTT-INTEGRATION-DESIGN.md`) but no code
## soundcork ↔ AfterTouch
soundcork and AfterTouch share the most functional overlap of any two projects in the ecosystem. For the implementation-level parity analysis and remaining tasks see [docs/PARITY-SOUNDCORK.md](../PARITY-SOUNDCORK.md).
soundcork and AfterTouch share the most functional overlap of any two projects in the ecosystem. For the implementation-level parity analysis and remaining tasks see [docs/PARITY-SOUNDCORK.md](../appendix/PARITY-SOUNDCORK.md).
### Architectural differences (not gaps)
@@ -1,5 +1,8 @@
# Navigation API Reference
---
title: "Navigation API Reference"
sidebar:
exclude: true
---
## Overview
This document provides a complete API reference for the Bose SoundTouch navigation and station management functionality. For usage examples and workflows, see [NAVIGATION-GUIDE.md](NAVIGATION-GUIDE.md).
@@ -1,13 +1,15 @@
# CLAUDE.md - Development Guidelines for Bose SoundTouch Project
---
title: "CLAUDE.md - Development Guidelines for Bose SoundTouch Project"
sidebar:
exclude: true
---
## Documentation Overview
This document contains important development guidelines for working on the Bose SoundTouch project. Please also read the following documentation:
- **[PLAN.md](archive/PLAN.md)** - Project planning and roadmap
- **[PLAN.md](../../../archive/PLAN.md)** - Project planning and roadmap
- **[PROJECT-PATTERNS.md](PROJECT-PATTERNS.md)** - Project structure and design patterns
- **[API-ENDPOINTS.md](reference/API-ENDPOINTS.md)** - API endpoints overview
- **[SoundTouch Web API.pdf](2025.12.18%20SoundTouch%20Web%20API.pdf)** - Official API documentation
- **[API-ENDPOINTS.md](../reference/API-ENDPOINTS.md)** - API endpoints overview
## Development Guidelines
@@ -1,5 +1,8 @@
# Content Selection Implementation Summary
---
title: "Content Selection Implementation Summary"
sidebar:
exclude: true
---
This document summarizes the implementation of advanced content selection features for the Bose SoundTouch Go client, including full support for the LOCAL_INTERNET_RADIO streamUrl format and LOCAL_MUSIC/STORED_MUSIC content selection.
## ✅ Implementation Status: COMPLETE
@@ -212,9 +215,9 @@ soundtouch-cli --host 192.0.2.100 source internet-radio \
- [SoundTouch WebServices API Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
- [LOCAL_INTERNET_RADIO - streamUrl format](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_internet_radio---streamurl-format)
- [LOCAL_MUSIC](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_music)
- [Content Selection Example](../examples/content-selection/README.md)
- [CLI Reference](guides/CLI-REFERENCE.md)
- [Content Selection Example (Direct)](../examples/content-selection/)
- [Content Selection Example](https://github.com/gesellix/Bose-SoundTouch/tree/main/examples/content-selection/README.md)
- [CLI Reference](../guides/CLI-REFERENCE.md)
- [Content Selection Example (Direct)](https://github.com/gesellix/Bose-SoundTouch/tree/main/examples/content-selection)
## ✅ Verification
@@ -1,5 +1,8 @@
# Device Customization Setup Guide
---
title: "Device Customization Setup Guide"
sidebar:
exclude: true
---
This guide documents the manual steps required to configure your Bose SoundTouch device for customization using the SoundCork approach.
Based on: https://github.com/deborahgu/soundcork
@@ -1,5 +1,8 @@
# Device Logging & Troubleshooting
---
title: "Device Logging & Troubleshooting"
sidebar:
exclude: true
---
Accessing logs from SoundTouch devices is critical for debugging custom service integrations and understanding internal device behavior. This document outlines the methods for collecting logs, as discovered by the **SoundCork** and **ÜberBöse API** communities.
## Log Types
@@ -80,7 +83,7 @@ If you have a managed switch or a router capable of port mirroring, you can use
### "IsItBose" Validation Failures
If the device fails to connect to your custom service despite correct configuration, it may be failing the internal `IsItBose` regex check.
- **Evidence**: Look for SSL handshake failures or "Unauthorized" errors in your service logs.
- **Solution**: See the [Binary Patching section in DEVICE-REDIRECT-METHODS.md](analysis/DEVICE-REDIRECT-METHODS.md#method-3-binary-patching).
- **Solution**: See the [Binary Patching section in DEVICE-REDIRECT-METHODS.md](../analysis/DEVICE-REDIRECT-METHODS.md#method-3-binary-patching).
### Disappearing Sources (TuneIn/Local Radio)
If `TUNEIN` or `LOCAL_INTERNET_RADIO` sources disappear after a reboot in an offline environment.
@@ -1,5 +1,8 @@
# Bose SoundTouch Device Setup Flow
---
title: "Bose SoundTouch Device Setup Flow"
sidebar:
exclude: true
---
This document details the multi-step process required to fully set up a Bose SoundTouch device, as derived from the Stockholm firmware (`setup/js/`) analysis.
A complete setup flow involves a sequence of local (WebSocket) and cloud (HTTP) actions that move the device from a factory-reset state to a fully registered, functional system.
@@ -1,5 +1,8 @@
# Encrypted Diagnostic Export
---
title: "Encrypted Diagnostic Export"
sidebar:
exclude: true
---
AfterTouch can produce an encrypted diagnostic report that users can download and
send to the project maintainer without exposing sensitive data to third parties.
The report is encrypted with an SSH public key using
@@ -1,5 +1,8 @@
# Technical Proposal: External Service Provider Abstraction
---
title: "Technical Proposal: External Service Provider Abstraction"
sidebar:
exclude: true
---
This document outlines a strategy to refactor the SoundTouch Service's content handling into a modular provider-based system.
## 1. Problem Statement
@@ -1,5 +1,8 @@
# Feature Development History
---
title: "Feature Development History"
sidebar:
exclude: true
---
This document tracks the detailed evolution of features and capabilities in the Bose SoundTouch API client library.
## Development Timeline
@@ -1,5 +1,8 @@
# Host:Port Parsing Feature
---
title: "Host:Port Parsing Feature"
sidebar:
exclude: true
---
This document describes the automatic host:port parsing functionality added to the SoundTouch CLI, which allows users to specify both host and port in a single `-host` flag.
## Overview
@@ -1,5 +1,8 @@
# Manual Network Discovery on macOS
---
title: "Manual Network Discovery on macOS"
sidebar:
exclude: true
---
This document provides comprehensive guidance for manually discovering network services and devices using built-in macOS tools and command-line utilities. This is particularly useful for troubleshooting network discovery issues or understanding what services are available on your local network.
## Overview
@@ -1,5 +1,8 @@
# Navigation and Station Management Guide
---
title: "Navigation and Station Management Guide"
sidebar:
exclude: true
---
## Overview
The Bose SoundTouch Go client provides comprehensive navigation and station management functionality that allows you to:
@@ -895,4 +898,4 @@ For additional help:
---
*This guide covers the complete navigation and station management functionality. For preset management, see [PRESET-MANAGEMENT.md](reference/PRESET-MANAGEMENT.md).*
*This guide covers the complete navigation and station management functionality. For preset management, see [PRESET-MANAGEMENT.md](../reference/PRESET-MANAGEMENT.md).*
@@ -1,5 +1,8 @@
# Official SoundTouch Web API Verification
---
title: "Official SoundTouch Web API Verification"
sidebar:
exclude: true
---
**Source**: Official Bose SoundTouch Web API v1.0 Documentation (January 7, 2026)
**Verification Date**: January 9, 2026
**Project Status**: Complete API coverage verification
@@ -1,3 +1,9 @@
---
title: "Parity Improvements"
sidebar:
exclude: true
---
### Overview of Recent Improvements and Next Steps
This document summarizes the improvements made to the **Marge service** to improve parity with the upstream Bose SoundTouch service, along with open issues and proposed next steps.
@@ -1,5 +1,8 @@
# Parity Analysis: Bose-SoundTouch (Go) vs. OpenCloudTouch (Python)
---
title: "Parity Analysis: Bose-SoundTouch (Go) vs. OpenCloudTouch (Python)"
sidebar:
exclude: true
---
This document provides a comparative analysis of the current Go implementation and the `scheilch/opencloudtouch` project, identifying functional gaps and potential improvements.
## 1. Core Architecture and Language
@@ -1,5 +1,8 @@
# Parity Analysis: Bose-SoundTouch (Go) vs. SoundCork (Python)
---
title: "Parity Analysis: Bose-SoundTouch (Go) vs. SoundCork (Python)"
sidebar:
exclude: true
---
This document provides a comparative analysis of the current Go implementation and the `deborahgu/soundcork` project, identifying functional gaps and potential improvements.
## 1. Core Architecture and Language
@@ -54,4 +57,4 @@ Group support and ZeroConf Spotify priming are now feature-complete in AfterTouc
- **BMX service extensibility**: the `bmx_services.json` registry makes it trivial to add or mock new streaming providers without code changes (step C above).
- **Group pairing logic**: master/slave relationship management for SoundTouch 10 stereo pairs goes beyond the CRUD AfterTouch implements.
For the broader ecosystem context (feature matrix across all community projects, AfterTouch open tasks, and cross-project observations) see [docs/analysis/bose-soundtouch-community-tools.md](analysis/bose-soundtouch-community-tools.md).
For the broader ecosystem context (feature matrix across all community projects, AfterTouch open tasks, and cross-project observations) see [docs/analysis/bose-soundtouch-community-tools.md](../analysis/bose-soundtouch-community-tools.md).
@@ -1,10 +1,33 @@
# Preset Management Quick Start Guide
---
title: "Preset Management Quick Start Guide"
sidebar:
exclude: true
---
**Save your favorite music, radio stations, and playlists as 1-6 presets for instant access.**
## Overview
SoundTouch devices support 6 preset slots that can store your favorite content for instant access. This guide shows you how to manage presets using both the CLI and Go library.
SoundTouch devices support 6 preset slots that can store your favorite content for instant access. This guide shows you how to manage presets using the soundtouch-web UI, the CLI, or the Go library.
## Via soundtouch-web (browser UI)
**soundtouch-web** (default port **8080**) is the easiest way to manage presets without the command line. Two save paths are available whenever content is playing:
### ★ Star button — save from Now Playing
1. Navigate to your speaker's detail page.
2. Play any station or track (via Radio Browser, TuneIn, or the speaker's own sources).
3. A semi-transparent **★** appears in the top-right corner of the **Now Playing** card.
4. Click the star — a slot picker **1 · 2 · 3 · 4 · 5 · 6** opens.
5. Click the target slot number. The star turns gold once the current content is saved to at least one slot.
### + button — save directly to a preset tile
While something is playing, hover over any of the six **Preset** tiles. A small **+** button appears in the tile's corner; clicking it saves the current content to that slot immediately (no picker needed).
Use the **+** when you already know which slot you want; use the **★** when you want to pick the slot after you've decided to save.
---
## Quick CLI Usage
@@ -332,11 +355,11 @@ soundtouch-cli --host 192.0.2.100 info
## Next Steps
- 📖 [Complete CLI Reference](guides/CLI-REFERENCE.md)
- 🔧 [Full Implementation Guide](reference/PRESET-MANAGEMENT.md)
- 📡 [WebSocket Events Documentation](reference/WEBSOCKET-EVENTS.md)
- 💻 [Preset Management Example](../examples/preset-management/)
- 📚 [API Endpoints Overview](reference/API-ENDPOINTS.md)
- 📖 [Complete CLI Reference](../guides/CLI-REFERENCE.md)
- 🔧 [Full Implementation Guide](../reference/PRESET-MANAGEMENT.md)
- 📡 [WebSocket Events Documentation](../reference/WEBSOCKET-EVENTS.md)
- 💻 [Preset Management Example](https://github.com/gesellix/Bose-SoundTouch/tree/main/examples/preset-management)
- 📚 [API Endpoints Overview](../reference/API-ENDPOINTS.md)
## Need Help?
@@ -1,4 +1,8 @@
# Project Structure Patterns: Bose SoundTouch API Client
---
title: "Project Structure Patterns: Bose SoundTouch API Client"
sidebar:
exclude: true
---
## Summary for Reuse in API Client Projects
This document describes the most important patterns for the Bose SoundTouch API client, especially for XML-based API clients with Web UI, CLI tool, and WASM support.
@@ -1,5 +1,8 @@
# Request Recording Concept
---
title: "Request Recording Concept"
sidebar:
exclude: true
---
## Problem Statement
The current request recording system has fundamental issues when dealing with request cloning, body consumption, and multiple response scenarios. Specifically:
@@ -1,5 +1,8 @@
# SCMUDC Enrichment Implementation Summary
---
title: "SCMUDC Enrichment Implementation Summary"
sidebar:
exclude: true
---
## Overview
This document summarizes the implementation of SCMUDC (Sound Control Management Usage Data Collection) event enrichment in the AfterTouch toolkit. The enhancement provides human-readable analysis of device telemetry data to improve usability and debugging capabilities.
@@ -1,5 +1,8 @@
# Service Availability Implementation Summary
---
title: "Service Availability Implementation Summary"
sidebar:
exclude: true
---
## Overview
This document summarizes the implementation of the `/serviceAvailability` endpoint support in the Bose SoundTouch Go client library. This feature enables applications to query which music services and input sources are available on a SoundTouch device, providing better user feedback about supported stations and sources.
@@ -1,5 +1,8 @@
# 🎉 Introducing SoundTouch Service: Local Cloud Service Emulation
---
title: "🎉 Introducing SoundTouch Service: Local Cloud Service Emulation"
sidebar:
exclude: true
---
**Date**: February 2026
**Version**: v2.0.0+
**Status**: Production Ready
@@ -144,10 +147,10 @@ LOG_PROXY_BODY=true soundtouch-service
## 📚 Documentation
- **[Complete Service Guide](guides/SOUNDTOUCH-SERVICE.md)**: Comprehensive setup and configuration
- **[API Reference](guides/SOUNDTOUCH-SERVICE.md#api-reference)**: Full endpoint documentation
- **[Migration Guide](guides/SOUNDTOUCH-SERVICE.md#device-migration)**: Step-by-step device migration
- **[Troubleshooting](guides/SOUNDTOUCH-SERVICE.md#troubleshooting)**: Common issues and solutions
- **[Complete Service Guide](../guides/SOUNDTOUCH-SERVICE.md)**: Comprehensive setup and configuration
- **[API Reference](../guides/SOUNDTOUCH-SERVICE.md#api-reference)**: Full endpoint documentation
- **[Migration Guide](../guides/SOUNDTOUCH-SERVICE.md#device-migration)**: Step-by-step device migration
- **[Troubleshooting](../guides/SOUNDTOUCH-SERVICE.md#troubleshooting)**: Common issues and solutions
## 🤝 Contributing
@@ -172,9 +175,9 @@ The collaborative spirit of reverse engineering and documentation in the SoundTo
## 🔗 Links
- **[Main Repository](https://github.com/gesellix/bose-soundtouch)**
- **[Service Documentation](guides/SOUNDTOUCH-SERVICE.md)**
- **[CLI Documentation](guides/CLI-REFERENCE.md)**
- **[Getting Started Guide](guides/GETTING-STARTED.md)**
- **[Service Documentation](../guides/SOUNDTOUCH-SERVICE.md)**
- **[CLI Documentation](../guides/CLI-REFERENCE.md)**
- **[Getting Started Guide](../guides/GETTING-STARTED.md)**
- **[SoundCork Project](https://github.com/deborahgu/soundcork)**
- **[ÜberBöse API](https://github.com/julius-d/ueberboese-api)**
@@ -1,4 +1,8 @@
# Undocumented Community Features & API Discoveries
---
title: "Undocumented Community Features & API Discoveries"
sidebar:
exclude: true
---
This document captures advanced API endpoints and device behaviors discovered by the SoundTouch community through reverse engineering projects like **SoundCork** and **ÜberBöse API**. These features are not documented in the official Bose SoundTouch Web API v1.0 but are crucial for full device emulation and offline operation.
## Cloud Emulation (Marge/BMX) Discoveries
While the local `/8090` API is well-documented, the cloud-side service emulation reveals deeper device integration points.
@@ -1,5 +1,8 @@
# Unimplemented SoundTouch API Endpoints
---
title: "Unimplemented SoundTouch API Endpoints"
sidebar:
exclude: true
---
**Last Updated:** January 2026
**Source:** [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
**Current Implementation:** 35 endpoints (including preset & navigation management discovered via SoundTouch Plus Wiki)
+9
View File
@@ -0,0 +1,9 @@
---
title: "Appendix"
weight: 99
sidebar:
open: false
---
Internal design documents, implementation notes, and historical records.
These are developer and maintainer references, not user guides.
@@ -1,5 +1,8 @@
# Device Lifecycle and /power_on Enhancement
---
title: "Device Lifecycle and /power_on Enhancement"
sidebar:
exclude: true
---
## Overview
This document provides a comprehensive analysis of the current SoundTouch device registration and lifecycle management implementation, and proposes enhancements using the `/power_on` endpoint to reduce dependency on local network connectivity.
@@ -1,5 +1,8 @@
# Device Lifecycle Analysis - Executive Summary
---
title: "Device Lifecycle Analysis - Executive Summary"
sidebar:
exclude: true
---
## Current State Assessment
The SoundTouch service currently relies heavily on local network connectivity for device discovery and management:
@@ -1,5 +1,8 @@
# /power_on Implementation Guide
---
title: "/power_on Implementation Guide"
sidebar:
exclude: true
---
## Overview
This guide provides detailed technical specifications for implementing `/power_on` endpoint enhancements to reduce network dependency and improve device lifecycle management in the SoundTouch service.
@@ -1,5 +1,8 @@
# SoundTouch `/storePreset` Implementation Guide
---
title: "SoundTouch `/storePreset` Implementation Guide"
sidebar:
exclude: true
---
## Overview
This document analyzes the feasibility and implementation approach for adding `/storePreset` functionality to the Bose SoundTouch API client, based on [GitHub Issue #14](https://github.com/gesellix/Bose-SoundTouch/issues/14) and endpoints discovered through the comprehensive [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API).
@@ -1,5 +1,8 @@
# SCMUDC Events Analysis
---
title: "SCMUDC Events Analysis"
sidebar:
exclude: true
---
## Overview
SCMUDC (Sound Control Management Usage Data Collection) events are telemetry data sent from SoundTouch devices to `events.api.bosecm.com` via `/v1/scmudc/{deviceId}` endpoints. These events track user interactions and device behaviors for analytics and monitoring.
@@ -1,10 +1,19 @@
# soundtouch-web: remaining features
---
title: "soundtouch-web: remaining features"
sidebar:
exclude: true
---
Four features complete the parity gap between soundtouch-web and the Stockholm
app's local-control functionality. Everything else in Stockholm (OAuth flows,
setup wizard, service account linking, onboarding, analytics) is cloud
infrastructure that is either shut down or already handled by soundtouch-service.
> **Shipped:** Saving the current content to a preset slot (slots 16) is
> already implemented — a ★ star button in the top-right corner of the Now
> Playing card opens a slot picker, and a **+** button on each preset tile
> saves to that slot directly. See [PRESET-QUICKSTART.md](PRESET-QUICKSTART.md)
> for usage details.
---
## 1. Seek / scrub
@@ -34,10 +43,16 @@ func (c *Client) Seek(positionSeconds int) error {
---
## 2. Favorites
## 2. Favorites (device-native, distinct from presets)
Mark or unmark the currently playing track as a favourite directly from the
Now Playing card.
> **Note:** This section is about the speaker's **built-in** `/favorites` API —
> a separate concept from the 6 preset slots. Preset-slot saving (★ star /
> **+** button) is already shipped; the native Favorites API is not yet
> surfaced in soundtouch-web.
Mark or unmark the currently playing track as a device favourite directly from
the Now Playing card. Unlike presets (maximum 6, numbered slots), the device
can hold a larger favourites list; support varies by source.
**Device API:**
- `GET /favorites` — returns `<favorites>` list
@@ -1,5 +1,8 @@
# Stockholm Backend — Port Guide for Bose-SoundTouch (Go)
---
title: "Stockholm Backend — Port Guide for Bose-SoundTouch (Go)"
sidebar:
exclude: true
---
This document describes everything needed to integrate the
[krahl/soundcork-stockholm-app](https://github.com/krahl/soundcork-stockholm-app)
functionality into the Go service. It is written as a reference; nothing here
@@ -1,4 +1,15 @@
# Device-Local Install: Four User Journeys
---
title: "Device-Local Install: Four User Journeys"
---
> **Looking for how to actually install AfterTouch?**
> See the [Deployment Overview](../guides/DEPLOYMENT-OVERVIEW.md) for user-friendly
> step-by-step guides for both deployment scenarios (external host and on-device).
>
> This document is an **architectural analysis** — user journeys, install patterns,
> technology tradeoffs, and future directions. It is aimed at contributors and
> project planning, not at end users.
---
A user-journey-shaped view of where AfterTouch sits today and where it could go. The same speaker, the same constraints, but four different audiences with non-overlapping needs:
+4
View File
@@ -0,0 +1,4 @@
---
title: "Architecture"
weight: 5
---
@@ -1,5 +1,6 @@
# Encrypting Sensitive Data Exports with SSH/age or GPG
---
title: "Encrypting Sensitive Data Exports with SSH/age or GPG"
---
## Problem
Allow users of our software to export potentially sensitive data, encrypt it locally, and send it to us. We decrypt on our side. Goal: no key exchange, minimal user friction.
+4
View File
@@ -0,0 +1,4 @@
---
title: "Concepts"
weight: 3
---
@@ -1,5 +1,6 @@
# Amazon Music OAuth Integration
---
title: "Amazon Music OAuth Integration"
---
This document describes the plan and specification for adding Amazon Music OAuth support to the SoundTouch service, enabling continued Amazon Music playback after the Bose cloud shutdown (May 2026).
The implementation mirrors the [Spotify OAuth integration](spotify-oauth.md) closely. Read that document first — this one calls out only the differences.
@@ -1,5 +1,6 @@
# Spotify OAuth Integration
---
title: "Spotify OAuth Integration"
---
> **New here?** Start with [spotify-overview.md](spotify-overview.md) for the
> mental model (Spotify Connect vs OAuth-intercept, DNS rewrite gotcha,
> end-to-end token lifecycle). This document zooms in on the OAuth flows and
@@ -1,5 +1,6 @@
# Spotify on SoundTouch — Overview
---
title: "Spotify on SoundTouch — Overview"
---
This is the entry point for understanding how Spotify works on a SoundTouch
speaker behind AfterTouch. Read this first; the deeper docs assume you already
have the mental model below.
@@ -1,5 +1,6 @@
# Spotify Priming Strategy
---
title: "Spotify Priming Strategy"
---
> **New here?** Start with [spotify-overview.md](spotify-overview.md) for the
> mental model. This document goes deep on the priming protocol, ZeroConf DH
> exchange, and deployment topologies.
@@ -1,5 +1,6 @@
# Migration Flow Diagrams
---
title: "Migration Flow Diagrams"
---
This document specifies the diagrams needed for the migration guide, with descriptions that can be used to create actual visual diagrams.
## 1. Overall Migration Process Flow

Some files were not shown because too many files have changed in this diff Show More