Relates to #571.
## What
Adds a **Name / IP** sort toggle to the Player device list. The choice
is persisted in `localStorage` (`aftertouch_device_sort`), following the
same preference pattern as the service-URL field in `PlayURL.js`.
## Why
The device list was previously ordered only by IP: the service datastore
keys devices by IP address and Go marshals map keys lexicographically,
so the frontend received an already-IP-ordered object and rendered it
as-is. BirdyBA (#571) asked to be able to sort by name instead.
## Changes
- `DeviceList.js`: a `sortEntries()` helper plus a `useState`-backed
toggle seeded from `localStorage`. Name mode sorts by `device.info.name`
(falling back to the IP key when a device has no name yet); IP mode
sorts the IP key **numerically** (`.2` before `.10`), which also tidies
the old lexicographic ordering.
- `css/app.css`: additive `.device-sort` / `.sort-btn` styling, reusing
the existing accent / `.active` look. No existing rules touched.
No backend change: the device name and IP are already in the payload.
## Testing
- `node --check` on `DeviceList.js` passes.
- `make build-player` succeeds (the static tree is `//go:embed`ed into
the binary).
- Manual: open the Player, toggle Name / IP, confirm the order changes
and the choice survives a page reload.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
github/codeql-action/init, /analyze and /upload-sarif are separate
Dependabot dependencies but must run on the same version. Without a
group they update in independent PRs that merge at different times,
producing a version mismatch that fails CodeQL. Group them so all
sub-actions bump together in one PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dependabot bumped only github/codeql-action/analyze to v4.37.0 while
init and upload-sarif stayed on v4.36.2. CodeQL requires all of its
action steps on the same version; the mismatch failed every Analyze job
with 'Loaded a configuration file for version 4.36.2, but running
version 4.37.0'. Bump init and upload-sarif to the same v4.37.0 commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chi 5.3.1 recognizes the HTTP QUERY method, so chi.Walk now expands the
all-methods HandleFunc registrations for the SiriusXM live-adapter routes
to include QUERY. The routes are functionally unchanged; only the walk
output grew two lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- HTTPS-SETUP.md: the HTTPS endpoint is only needed for certain features
and its URL now derives from the Target Domain by default; note the
https-Target-Domain shortcut and the Settings override.
- SOUNDTOUCH-SERVICE.md: HTTPS_SERVER_URL is an override that derives from
SERVER_URL when empty, and is viewable/overridable in Settings.
- TROUBLESHOOTING.md: new entry for the "HTTPS endpoint TLS configuration"
health warning (wrong port / not reachable), how to fix via the Settings
HTTPS URL, and when it's an expected reverse-proxy case.
refs #355
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up polish to the HTTPS-URL settings work:
- Group Target Domain and the derived HTTPS URL under a single bold
"Service URLs" section header, matching the existing section-header
pattern (Landing page, TLS extra hosts, Device Discovery) rather than a
one-off fieldset box — consistent across the whole Settings tab.
- Tighten the spacing so the HTTPS URL sits with Target Domain (drop the
empty :443-status reserved line, reduce the intra-group gap) instead of
floating toward the next section.
- Make "Landing page" a bold header for the same consistency.
- Expand the HTTPS URL override hint: HTTPS is only needed for certain
features (DNS redirect, Spotify/Amazon login, cert trust); it derives
from the Target Domain; and if you don't need plain HTTP you can set the
Target Domain itself to an https:// URL, no override required.
refs #355
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-ups to the derive/show/override settings work:
- The "advanced override" affordance reused the .info-toggle style with a
text label, which is an 18px circular icon badge — the label rendered as
a broken blue circle. Use the icon-toggle pattern like TLS extra hosts:
a small ⓘ that reveals a details block containing the explanation and the
override input.
- Existing installs persist their old effective HTTPS URL in the (now
override) https_server_url field, so the UI showed "(override)" even when
the value equals what we would derive. On load, treat an override that
exactly matches the derived URL as "derive" (clear it), so default
installs show "(derived from Target Domain)"; genuinely custom values are
kept as overrides.
Verified live: an existing settings.json with https_server_url equal to the
derived value now reports an empty override, and the served admin HTML uses
the ⓘ toggle.
refs #355
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The diagnostic export wrote the raw persisted https_server_url, which is
now the override (empty when the URL is derived). Report the effective
HTTPS URL actually in use plus the override as a separate field, so a
diagnostic makes an advertised-URL/listener mismatch legible instead of
showing an empty field.
refs #355
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The HTTPS URL AfterTouch advertises (and points speakers at for the
DNS-redirect, OAuth, install-ca and cert-trust flows) was a separate,
internally-tracked value: sourced only from --https-server-url /
HTTPS_SERVER_URL / the settings file, defaulting to the machine hostname,
and never shown or editable in the web UI. So it could silently diverge
from the Target Domain (e.g. a different host, or a port-less value that
fell back to 443 while the listener was on 8443 — the root of #355), with
no way to see or fix it in the UI.
Make it derive + show + override:
- DeriveHTTPSURL resolves the effective HTTPS URL: an explicit override
wins; otherwise it follows the Target Domain (same host, https, on the
configured HTTPS port); an already-https Target Domain is honoured
verbatim (its port is not second-guessed); empty falls back to the
hostname default. So changing the Target Domain updates the HTTPS URL
automatically for the common single-host case.
- The persisted https_server_url is now the *override* (empty = derive).
Existing installs carry their old value here, so it is preserved as an
override — no silent change on upgrade; clearing it opts into derive.
- The server keeps httpsServerURL as the effective value, so all
consumers (cert SANs, migration, export, health) are unchanged; it is
recomputed whenever the Target Domain or override changes.
- Settings API returns https_server_url (effective) plus
https_server_url_override; the Settings page shows the effective URL
with a derived/override note and an "advanced" override field.
Verified live on a clean data dir: derive from an http Target Domain,
auto-follow when the Target Domain changes, explicit override, an https
Target Domain kept verbatim, and override persistence across restart.
Unit tests cover DeriveHTTPSURL including the already-https cases.
refs #355
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the previous commit. The cert-chain check dialed the
advertised HTTPS URL, whose port defaults to 443 when the URL omits it
(splitHTTPSHostPort). The advertised URL comes from
--https-server-url / HTTPS_SERVER_URL / the settings file and is not
editable in the web UI, so when it lost its port it silently pointed the
check (and speakers) at 443 while the real listener was on 8443 — the
exact "port 443" complaint in issue #355.
Thread the actual HTTPS listener port into the check (new
Server.SetHTTPSListenAddr, wired from config.httpsAddr). When the dial
fails and the advertised port differs from the listener port, emit a
mismatch-specific warning that names both ports and offers the corrected
HTTPS_SERVER_URL, while still deferring to reverse-proxy setups. A
reachable endpoint never reaches this branch.
Reproduced locally on a clean data dir: seeding a port-less
https_server_url with the listener on 8443 previously errored on
:443; it now warns with both ports and the fix. Regression tests added.
refs #355
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `service_cert_chain` check ("HTTPS endpoint TLS configuration") dials
the service's own configured HTTPS URL. When that dial fails before any
certificate is presented (connection refused, timeout, handshake reset),
it reported a hard red error.
But from inside the service we can't distinguish "the endpoint is down"
from "the advertised HTTPS URL simply isn't reachable from here" — and
the latter is a normal, healthy deployment: TLS terminated by a reverse
proxy in front of AfterTouch, or a Docker-published port / LAN-only
hostname that the container itself can't dial. In those setups the red
error is a false alarm (issue #355: reporter runs HTTP 8080 / HTTPS 8443
and noted "in my configuration that is expected").
Downgrade that specific case (no cert presented) to a warning, reword it
to name the expected reverse-proxy / unreachable-advertised-URL case, and
add an `openssl s_client` command to verify the endpoint from a client
that actually reaches the advertised URL. Cert-classification outcomes
(own-CA info, foreign-chain warning) are unchanged.
Reproduced locally on a clean data dir before/after: custom ports and
localhost/127.0.0.1 already returned INFO; only the unreachable-URL case
produced the error, which now returns a warning.
refs #355
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the two docs follow-ups from #493 (radio sources not mounting
after an in-place migration).
Troubleshooting: the "Radio sources never activate after an in-place
migration" entry now leads with the confirmed non-destructive fix,
re-running migration via the telnet method so all four service URLs
(incl. bmxRegistryUrl / statsServerUrl) land on the speaker's runtime:
soundtouch-cli --host <ip> setup migrate --method telnet --service-url http://<host>:8000
Factory reset is kept as the fallback for models without a reachable
telnet port. The cause text is updated to the diagnosed BMX-registry
explanation, and notes why pointing the service at http://bose:8000 with
a server-side /etc/hosts entry does not help.
Downloads: new top-level docs section (docs/content/docs/downloads/)
structured by tool (service / player / cli / backup) x OS/arch, using
the real release asset naming (soundtouch-<tool>-v<ver>-<os>-<arch>),
plus install-script, Docker, and go-install routes. Sibling section
weights bumped so Downloads leads the sidebar. README, the release-notes
template, and the key install guides now point here. Also fixes the
stale, never-produced .tar.gz/.zip filenames in SELF-HOSTING.md.
refs #493
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Close every post with the established footer convention (## Current
release + dated release line + subscribe note), and never restyle
already-published posts to fit a new convention.
- Prefer a narrative over a bare release-note aggregation.
- Forbid em dashes (with a grep check).
- Allow an explicit tag/date argument to override lookback detection.
- Stop the skill from pushing or opening the PR itself: commit to a
branch and hand the maintainer the push + PR commands (maintainer
always pushes over SSH).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tells the story since v0.93.1 (rescue to platform): local music + TTS,
robustness/security hardening, health diagnostics. Emphasises that the
roadmap is community-driven, shouts out Sander ten Brinke's
soundtouch-maui companion app, and sets out what v1.0.0 signals
(stability, clean-slate re-migration, beyond-Bose value: #495, #508, #188).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spell out that AfterTouch reads X-Forwarded-For only when the connecting
socket's source IP is in trusted_proxy_cidrs (the socket address, which a
header can't forge), reword the table rows in those terms, and note that a
proxy in a separate Docker container is usually seen as the Docker bridge
subnet rather than its published address.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverse-proxy client-IP resolution (trust_forwarded_headers /
trusted_proxy_cidrs) was documented under HTTPS-SETUP because proxies are
commonly used for TLS termination, but it's really a deployment concern.
Relocate it to CLOUD-DEPLOY-WALKTHROUGH as a "Client IP behind a proxy or
load balancer" section with a behavior table (no-proxy default, trusted-proxy
XFF resolution, and the untrusted-peer spoofing gate). HTTPS-SETUP keeps the
TLS-termination example and now cross-links to it; the deployment section
links back for the cert details.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Startup treated an empty server_url as "first run" and wrote a fresh
default settings.json via createDefaultSettings, which builds the struct
from CLI flags and does not merge the existing file. A hand-authored
settings.json that sets, say, trust_forwarded_headers but leaves
server_url to the --server-url flag has no server_url, so it was
silently clobbered on first start (losing the operator's keys).
Gate the default-seed (and the lost-volume "first run" notice) on the
ABSENCE of settings.json instead. An existing file is now always
respected; a genuinely empty data dir still gets defaults and the
notice. This also fixes a latent loop where a never-set server_url made
every start look like a first run.
Adds regression tests: settingsFileExists, plus first-run seed both
preserving a hand-authored file and writing defaults when absent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the HTTPS reverse-proxy guide and the trust_forwarded_headers /
trusted_proxy_cidrs settings comments to reflect that the client IP is now
resolved from X-Forwarded-For only (no longer X-Real-IP / True-Client-IP),
read via the request context rather than by rewriting r.RemoteAddr. The
nginx example now sets X-Forwarded-For.
(Release note staged locally at _/releases/v0_117_0.md, which is gitignored
like prior release notes.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chi v5.3.0 deprecates middleware.RealIP (IP-spoofing advisories), which
failed the Lint and Static Security Analysis CI jobs (SA1019). Replace the
RealIP wrapper with chi's middleware.ClientIP: ClientIPFromRemoteAddr is
always applied so middleware.GetClientIP is populated, and when
trust_forwarded_headers is set and the immediate peer is a trusted-proxy
CIDR, ClientIPFromXFF resolves the real client from X-Forwarded-For
(rightmost entry outside the trusted CIDRs). The immediate-peer trust gate
is preserved, so a non-trusted peer's XFF is ignored. CIDR strings are
validated with netip.ParsePrefix first to avoid ClientIPFromXFF's panic.
Behavior change: only X-Forwarded-For is honored now (RealIP also read
X-Real-IP / True-Client-IP). Docs and a release note follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route every HTTP read of the client IP through a single clientHost(r)
helper backed by chi's new middleware.GetClientIP, falling back to the
socket peer from r.RemoteAddr. AddDeviceToAccount now takes a bare client
host instead of a "host:port" RemoteAddr. Behavior is unchanged in this
commit (no ClientIP middleware is wired yet, so the fallback is always
taken); a follow-up wires middleware.ClientIP and removes the deprecated
middleware.RealIP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The check title asserted "Speaker /sources matches service Sources.xml",
but the row renders as a warning when they differ, so "matches" plus a
warning read as a contradiction (reported in #493). Reword to "should
match" so the title states the expectation; the per-finding messages and
severities already convey whether it holds and what the differences are.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-device health findings previously labelled the device by account
and device IDs only (e.g. "account 3230304 · device 08DF1F0BA325"),
which is hard to place at a glance. Add display-only Name and IP fields
to health.Target and fill them centrally via EnrichTargets after the
checks run, so individual checks don't each have to look up the device
record. Both the live health endpoint and the diagnostic export go
through the new Server.runHealthChecks helper, and the Health tab renders
the friendly name first, then account/device IDs, then IP.
Fixes match on Account+Device only, so the new fields don't affect
quick-fix dispatch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the setup where AfterTouch sits behind a local resolver
(AdGuard Home / Pi-hole / FRITZ!Box) and is addressed by a short
hostname like `bose` instead of a raw IP. Captures the symptom cluster
(INVALID_SOURCE, missing source types, URL-mismatch pre-flight) and the
fix: short-hostname DNS rewrites, TLS_EXTRA_HOST coverage, switching the
service URLs to the hostname, and re-migration. Based on a real
user-contributed setup; IPs sanitised to RFC 5737.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Acknowledge a small Discord for direct, real-time conversation when an
email exchange or an issue/discussion thread isn't enough. No public
invite link: Issues and Discussions stay the first stop, and the invite
is shared in-thread only when a conversation genuinely needs it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The help text still listed only tts/url/beep/notify. Add the UPnP
AVTransport option (no app key, no DNS; http:// only, replaces source).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a third way to push a clip to a speaker, surfaced by @dagrider in
#517: POST SetAVTransportURI + Play to the speaker's UPnP MediaRenderer
control endpoint (port 8091). Unlike /speaker play_info it needs no
app_key and no DNS interception, so it works on a plain LAN; the
trade-off is it switches the speaker to the UPNP source and replaces the
current playback (no duck-and-resume).
- pkg/client: SetAVTransportURI, AVTransportPlay, PlayURLViaUPnP (+ the
:8091 control-URL derivation and SOAP plumbing), with tests.
- cmd/soundtouch-cli: `speaker url-upnp --url <url>`.
- docs: document the UPnP/AVTransport option under POST /speaker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The on-device installer's curl fails with `curl: (60) ... certificate is
not yet valid` when the speaker's clock has fallen into the past (no NTP
since the cloud shutdown), since TLS then rejects the recently-issued
server cert. Document the symptom and the fix (set the date over SSH,
then re-run), and note the speaker_clock health check keeps it corrected
afterwards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>