The on-device and Raspberry Pi installers hardcoded the release version, which
had to be bumped on every release. Default VERSION to empty and resolve the
newest tag by following GitHub's documented stable redirect
(https://github.com/<repo>/releases/latest -> .../releases/tag/vX.Y.Z), reading
the effective URL. This avoids the GitHub API rate limit and needs no jq.
An explicit version (positional arg / VERSION= / --version) still pins a
release. If the lookup fails (offline, rate-limited, or a curl without -w
support), each script falls back to a pinned FALLBACK_VERSION so installs still
work. The Pi self_update path runs after resolution, so it fetches the resolved
tag's installer.
Docs updated to state the default installs the latest release; the pinned-version
examples remain as illustrations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The installer docs and the on-device + Raspberry Pi installer scripts all
defaulted to and showed v0.107.0. Update every install example and the
VERSION defaults to the current release v0.111.3 across the on-device and
Pi guides and scripts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
soundtouch-web was renamed to soundtouch-player and the web UI merged into
soundtouch-service, so the player is now optional. The Raspberry Pi / host
installers had no matching uninstaller (removal was only documented as manual
commands), and users who installed the old soundtouch-web have a leftover
service with no scripted way to remove it.
Add three uninstallers under scripts/raspberry-pi/, each mirroring its
installer's conventions and tolerant of already-missing pieces:
- uninstall.sh — soundtouch-service; preserves the data directory by
default, --purge / PURGE_DATA=true to delete it.
- uninstall-player.sh — soundtouch-player (stateless).
- uninstall-web.sh — leftover soundtouch-web; points users at install-player.sh.
The shared soundtouch:soundtouch user/group is removed only once no other
soundtouch-{service,player,web} install remains on the host.
Docs: the README and guides still told users to fetch install-web.sh to install
the player. Switch those to install-player.sh, keep but improve the manual
removal commands (note the service datastore is preserved unless explicitly
deleted), document the new uninstallers, and add a "Migrating from soundtouch-web"
section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
soundtouch-web was a transitional alias of soundtouch-player. Stop
building and publishing it everywhere, and refresh the release notes
while at it:
- release.yml: remove the soundtouch-web binary, its individual and
combined checksums, and its release assets (EXPECTED_COUNT 35 -> 28);
drop the ghcr.io/...-web Docker image steps. Also slim the
workflow_dispatch release notes to an accurate AfterTouch header plus
GitHub's auto-generated changelog, with the bare tag as the title.
- Dockerfile: drop the soundtouch-web image stage.
- Makefile: remove WEB_NAME and the build-web target (and its use in
build/install).
- Delete scripts/raspberry-pi/install-web.sh (it fetched a release asset
that is no longer published) and point the docs at install-player.sh.
- Correct README, CLAUDE.md, and main.go wording that claimed the alias
was still published.
The runtime notice for a binary still run under the soundtouch-web name
is kept, so anyone who renamed the binary is nudged to soundtouch-player.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The web player is intrinsically LAN-resident: it reaches speakers directly
and only delegates cloud-only features (e.g. TTS) to a possibly-remote
AfterTouch service via --service-url. That is exactly what a cloud-hosted
soundtouch-service cannot do, so the standalone player binary stays useful
and is not being deprecated. Rename it to state its purpose, with a
transition window so existing downloads keep working.
- cmd/soundtouch-web -> cmd/soundtouch-player; CLI name is now
soundtouch-player. When the binary is invoked under its old name it prints
a one-line rename notice (filepath.Base(os.Args[0])).
- Build/release both names from the same source: Makefile (build-player +
build-web alias, dev-player* targets), Dockerfile (soundtouch-player image
+ transitional soundtouch-web image), release.yml and ci.yml (player +
web artifacts, checksums, Docker images; release notes announce the
rename). The soundtouch-web binary, image, and install script remain a
transitional alias to be dropped in a future release (which will break
stale fetch scripts and nudge users to the release notes).
- scripts/raspberry-pi/install-player.sh is canonical; install-web.sh keeps
working but warns.
- Sweep docs, code comments, user-facing strings, and assets
(soundtouch-web-ui.png, soundtouch-web-tunein.png, soundtouch-web-roadmap.md)
to soundtouch-player; README documents the rename and why the player
remains separate from the embedded /app.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
soundtouch-web's "Speak" feature proxies to the AfterTouch service's
/setup/tts/speak endpoint. Two issues blocked it end to end.
1. TLS: the proxy used http.DefaultClient, which trusts only system
roots, so the HTTPS call to a service using its own self-signed CA
failed with "x509: certificate signed by unknown authority". Add a
--service-ca flag (SERVICE_CA env) that loads the CA PEM, appends it
to the system pool, and uses a custom client for the TTS call.
2. Target: soundtouch-web sent device.Client.Host() (a full base URL
like http://ip:8090), but the service's SSRF guard exact-matches the
target against bare datastore IPs, returning "host ... is not a known
device". Prefer the device ID (the canonical key) and send a bare-IP
host fallback. Also normalize the incoming host in resolveTTSHost so a
URL/host:port form still resolves; it still only ever returns a
datastore IP, so the SSRF guarantee is unchanged.
Adds unit tests for the CA client builder, hostOnly, and resolveTTSHost
(including the preserved unknown-host/device rejections). Documents
--service-ca in the soundtouch-web README and TROUBLESHOOTING guide.
Wires SERVICE_URL and SERVICE_CA (empty defaults) into the Raspberry Pi
install-web.sh env file and documents them in the Pi guide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update v0.103.0 -> v0.104.0 across installer scripts, walkthrough docs,
and example go.mod files, and refresh the devices/migration/settings/sync
UI screenshots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Following user feedback (Lang, issue #432 thread) the docs guides now
contain all operational detail — installation, configuration, service
management, logs, updates, and removal — and the scripts READMEs become
thin pointers to the docs rather than the other way around.
RASPBERRY-PI.md: expanded to cover soundtouch-web alongside
soundtouch-service (install, config, port-conflict note, service
management, logs, update, removal, arch auto-detection, security).
scripts/raspberry-pi/README.md: trimmed to a quick-start with the two
one-liners plus a link to the docs guide.
EXTERNAL-HOST-WALKTHROUGH.md Step 7: replaces the vague "download from
Releases" note with the actual install-web.sh one-liner and a link to
RASPBERRY-PI.md#soundtouch-web; adds a non-Pi install option too.
ON-DEVICE-INSTALL-WALKTHROUGH.md: removed both back-references to
scripts/on-device-install/README.md; added self-contained sections for
Updating (with rollback tip), Service management, Logs, and Uninstalling
so the walkthrough is complete without leaving the docs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add scripts/raspberry-pi/install-web.sh: mirrors install.sh but for
the stateless soundtouch-web binary (no privileged ports, no data dir,
no HTTPS). Default port 8080; override via HTTP_PORT at install time.
- Add GET /health to soundtouch-web (handler + mount); returns
{"status":"ok","version":"…"} — used by the installer's health check
and by monitoring.
- Update scripts/raspberry-pi/README.md to document both installers side
by side (installation, config, service management, updates, removal).
- Bump default VERSION to v0.97.0 in all three installer scripts
(install.sh, install-web.sh, on-device-install/install.sh).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
- aftertouch init script: stop) now waits up to 15 s for SIGTERM
to take effect, then escalates to SIGKILL; prevents stale daemon
processes after '/etc/init.d/aftertouch stop' returns (weissigera's
workaround was manual 'killall aftertouch-service')
- install.sh: add --version / -v CLI flag so the version to install
can be passed as a command-line argument in addition to the VERSION
env var; document the trade-off of the hard-coded default in a
comment; update scripts/on-device-install/README.md with concrete
usage examples for env-override, CLI flag, and rollback tip
- docs/guides/ON-DEVICE-INSTALL-WALKTHROUGH.md: 10-step runbook
derived from weissigera's field-tested procedure (issue #329
comment #4521280831): SSH connection, storage cleanup, install via
install.sh, reboot, SSH tunnel, Health QuickFix, pairing
verification, soundtouch-cli download, custom-radio preset setup,
and final verification; troubleshooting table at the end
Closes#329 (remaining two tasks)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before overwriting the binary, read its version via --version and save
a copy as aftertouch-service.<version>.backup (falls back to a timestamp
if the flag is absent or the build is a dev build).
After the new binary is in place, delete every older *.backup, *.old,
and *.new artefact in INSTALL_DIR. /mnt/nv on SoundTouch SCM modules
has only tens of MB free; accumulating one ~12 MB backup per upgrade
quickly causes 'no space left on device' on the next download.
Only the backup created in this run (the <current-release>-1 binary) is
kept, giving a single one-step rollback point without wasting disk.
Relates to #329 (on-device install friction reported by weissigera).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "Download diagnostic report" button on the Health tab that
produces an age-encrypted .age file the user can attach to a GitHub
issue without exposing sensitive data.
Archive contents (tar.gz, then age-encrypted with the maintainer's
SSH ed25519 public key):
- diagnostic.json structured health/device summary (no secrets)
- datastore/…/*.xml raw on-disk XML verbatim for diff vs HTTP
- http/service/… live service HTTP responses per account/device
- http/speaker/… live speaker API responses (port 8090)
- ssh/speaker/… CA bundles + logread (last 20 min, 127.0.0.1
filtered) + dmesg fetched via SSH
- system/ca.pem service CA cert
- system/resolv.conf host DNS resolver config
- settings.json service settings (OAuth secrets redacted)
- env.txt filtered process environment
- logs/service.txt in-memory service log buffer
Supporting tooling:
- scripts/setup-diagnostic-key.sh one-time SSH key-pair generation
- scripts/decrypt-diagnostic.go go run helper for maintainer decryption
- keys/public/diagnostic.pub committed public key (matches github.com/gesellix.keys)
- docs/DIAGNOSTIC-EXPORT.md maintainer setup + user workflow guide
- docs/concepts/ENCRYPTED-EXPORT.md research notes and architecture rationale
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the ding renderer into pkg/service/ding so it can run both
at request time (from the new HandleDing handler) and offline
(from the existing scripts/gen-aftertouch-ding CLI, now a thin
wrapper around the same package).
- GET /media/aftertouch-ding.wav synthesises on first call,
caches the default-options bytes via sync.Once, and accepts
query-string overrides for every knob (pitch-{high,mid,low},
chirp-ms, gap-ms, attack-ms, release-ms, sample-rate, peak).
Invalid / out-of-range values silently fall back to defaults.
- Embedded WAV is gone from VCS — no 52 KB binary in the
repo, and tweaking the sound is now a query-param away rather
than a regenerate-and-commit cycle.
- Health-tab playback_test check is unchanged: the URL it
references (/media/aftertouch-ding.wav) keeps the same shape,
the handler just produces the bytes dynamically now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A 600 ms two-chirp sound derived from the braille S+T pair that
makes up the AfterTouch logo. Used as the test-playback target so
operators can confirm a freshly migrated speaker actually emits
audio without depending on TuneIn or any external service.
Mapping: dot rows → pitches (A5/E5/A4), dot columns → stereo
channels. S (dots 2,3,4) renders first, then T (dots 2,3,4,5) —
audibly "S plus one more voice".
Generator under scripts/gen-aftertouch-ding regenerates the file
on demand:
go run ./scripts/gen-aftertouch-ding \
-o pkg/service/handlers/static/media/aftertouch-ding.wav
22050 Hz stereo 16-bit PCM, ~52 KB. Picked up by the existing
static/media/* embed in handlers_media.go, so it's served at
GET /media/aftertouch-ding.wav once handlers can play it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Sets up Dependabot for JS dependency updates
- Adds GitHub workflow for automated static dependency updates
- Creates update script for Preact and other static JS libraries
- Updates Preact to latest version via new automation
convert_mitm_script.py was the last tracked file carrying a real Bose
account ID (9569497) and the maintainer's test-speaker MAC
(A81B6A536A98), hardcoded as the values to redact from MITM captures.
Replaced with mitmproxy `--set` options (`account_id`, `device_id`),
defaulting to empty strings (no-op) so the tracked source no longer
contains either real value. Callers configure their own at runtime:
mitmdump -s convert_mitm_script.py \
--set out_dir=_/mitm \
--set account_id=1234567 \
--set device_id=AABBCCDDEEFF
Added a module docstring documenting the flags so the usage isn't
folded only into the loader help text.
After this commit, the tree is clean for every personal-data pattern
the audit at _/RFC-5737-cleanup/assessment.md identified. The only
remaining 192.168.1.x references live in
docs/analysis/ANONYMIZATION-SUMMARY.md as intentional doc-context
discussion of why we moved off that range.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the docs-tier RFC-5737 rollout by sweeping the remaining
192.168.1.x references that lived outside .md / .txt / test files:
- .env.example — active PREFERRED_DEVICES default + examples
- .github/ISSUE_TEMPLATE/*.yml + workflows — issue template + CI examples
- cmd/websocket-demo/main.go, doc.go — top-level docs
- examples/*/main.go (7 files) — example program comments
- pkg/client/client.go — godoc examples
- pkg/models/doc.go — package godoc
- pkg/service/{amazon,spotify,zeroconf}/zeroconf.go — godoc comments
- pkg/service/handlers/web/index.html — placeholder text in the UI
- scripts/prepare-release.sh — example invocations
- scripts/spotify/spotify-prime-speaker.sh — usage comment
- tests/integration/http-client/http-client.env.json — fixture IPs
Same mapping as the docs commit (136d24a): 192.168.1.X → 192.0.2.X
preserving the last octet.
One semantic carve-out: the three zeroconf `zcBaseURL` godoc comments
in pkg/service/{amazon,spotify,zeroconf}/zeroconf.go switched to
192.168.10.10 instead of the doc range, because validateZcBaseURL
only accepts RFC-1918 / loopback / link-local. The comment must show
a value the validator actually accepts — see the matching test fix
in 92f66a2 for the same reason.
go build ./... clean. go test ./... clean except the pre-existing
TestDocsConsistency (untracked DEVICE-LOCAL-INSTALL.md, unrelated).
golangci-lint run ./... — 0 issues after a gofmt fix on
examples/zone-slave-operations/main.go.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 of the docs portion of the rfc-5737-cleanup. Replaces all
192.168.1.x example IPs in tracked .md / .txt files with the
equivalent last-octet under 192.0.2.x.
192.168.1.x is RFC-1918 private space and routes on real networks,
which leaves readers guessing whether a documented IP is a placeholder
or a documented LAN. 192.0.2.0/24 is reserved by RFC 5737 exclusively
for documentation — readers know on sight that they're examples.
58 files touched, 551 line pairs. Includes .github issue/PR templates,
all docs/ references, example READMEs, and one script doc. No code
changes, no test changes; test files still carry the 192.168.1.x
placeholder pending Phase 2 in _/RFC-5737-cleanup/assessment.md.
Also fixed a small fallout in docs/analysis/ANONYMIZATION-SUMMARY.md
where the explanatory sentence "a reader can't tell whether
192.168.1.10 is a placeholder or a documented LAN address" had
itself been swept by the regex (inverting the point); restored the
literal example and noted the sweep progress inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two patching gaps caused every Stockholm HTTP-proxy call from a
/stockholm/* page to hit /api/http-proxy (404) instead of the
basePath-prefixed /stockholm/api/http-proxy:
1. The proxy URL constant in browser_http_proxy.js is declared as
`var PROXY_PATH` (uppercase). Our patch script only knew about the
lowercase `var proxyPath` form used in app_comm.js, so it never
matched the upstream file.
2. Even if the constant had matched, browser_http_proxy.js's IIFE
evaluates the URL at script-load time — but the injected bootstrap
that defines window.__stockholmBase is placed just before </head>,
i.e. after the <script src=…> tags. The captured value would
always fall back to the unprefixed "/api/http-proxy".
3. The Makefile never passed browser_http_proxy.js to the patch script
at all.
Fix:
- Add an uppercase `PROXY_PATH` replacement entry in
patch-stockholm-bridge.py (keeps the lowercase one for
app_comm.js).
- Add a second replacement that rewrites the **use site** in
browser_http_proxy.js to inline `(window.__stockholmBase||"") +
"/api/http-proxy?url=" + ...`. Reading __stockholmBase at
call-time bypasses the load-order trap; the patched
`var PROXY_PATH = …` declaration above becomes dead code but
stays harmless.
- Pass `$(STOCKHOLM_DIR)/js/browser_http_proxy.js` to the patch
script in the prepare-stockholm target so it actually gets
rewritten.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements pkg/service/stockholm with bridge (appSend/runQueue), HTTP
proxy, static serving, config URL rewriting, native state persistence,
and device discovery. Mounts under a configurable base path (/stockholm
by default) with correct http.StripPrefix routing and apiBase-prefixed
bridge API routes matching the patched JS window.__stockholmBase calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lifts the back-and-forth in issue #250 into the README so the next
user doesn't repeat the same three traps Gustour hit:
1. The `ssh -L 8000:localhost:8000` command must run on the user's
own machine, NOT inside the speaker's SSH session. Gustour
pasted it at the speaker's `root@mojo:~#` prompt; the tunnel
ended up speaker → speaker (loopback) and did nothing.
2. SoundTouch firmware offers only ssh-rsa/ssh-dss host-key
algorithms; modern OpenSSH refuses them by default with
`Unable to negotiate with <ip> port 22: no matching host key
type found`. The README's *initial* ssh command already
uses `-oHostKeyAlgorithms=+ssh-rsa`, but the port-forward
example didn't — adding it.
3. If the tunnel is correct and the browser still gets
ERR_CONNECTION_RESET, the daemon isn't listening. The previous
README left the user stranded here. Adds the diagnostic ladder
(`netstat`, `ps`, `logread | grep aftertouch`) that matches
the syslog-tag pattern shipped in the prior commit, plus the
`/etc/init.d/aftertouch start` + `status` retry — the new
status case can now distinguish "PID alive, listener up" from
"PID alive, listener silently died".
No script changes; pure docs lift.
Refs #250.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles the install-time hygiene work for issues #268 and #250.
# Install location — #268
Stock SoundTouch rootfs has only a few MB free (~4 MB on the ST20
the reporter captured); the AfterTouch binary is ~12 MB. The previous
flow downloaded into tmpfs (/media/aftertouch) and then `mv`'d the
binary into /opt/aftertouch on rootfs — which fails with
"No space left on device" on any speaker with the standard layout.
install.sh now installs to /mnt/nv/aftertouch by default (the
persistent partition, ~30 MB free on the same captures) and points
/opt/aftertouch at it via a symlink so the init script's hardcoded
DAEMON path keeps working unchanged. Power users can override with
INSTALL_DIR=/some/other/path. The interactive prompt from the
community patch in #268's thread is dropped — STDIN is the curl
pipe under the documented `curl | sh` invocation, so a read prompt
would hang or read garbage.
uninstall.sh is updated to resolve the symlink and remove the
target before unlinking, so the 12 MB binary doesn't get orphaned
on /mnt/nv when users uninstall.
# Logging — #250
Issue #250 surfaced a "running but unreachable" state: the install
script reported AfterTouch as running, the init script's status
agreed, but `curl :8000` returned connection-refused. start-stop-
daemon's --background detaches stdout/stderr, so any panic the
daemon emitted before dying went to /dev/null with no diagnostic
trail.
The fix is to route the daemon's stdout/stderr through `logger -t
aftertouch` so output lands in BusyBox syslog — a bounded in-memory
ring buffer that never grows on disk (writing to a file in /mnt/nv
would have eaten the volume over months). Diagnostic flow is now:
logread | grep aftertouch | tail -20
logread -f | grep aftertouch # live tail
Matches the recipe already documented in TROUBLESHOOTING.md for the
speaker's own logs (Curl 7 section).
Tightening on top of the syslog change:
- The init script's `status` case now also curls localhost:8000
when the PID is alive — distinguishes "PID alive, listener up"
from "PID alive, listener silently died" (which is what fooled
everyone on #250). A bare PID-liveness check returned "running"
in both cases.
- install.sh's post-install verification now does its own 10s
curl probe after the init script returns; on failure it tails
the aftertouch syslog so the user sees the actual error rather
than the install script claiming success.
- `exec` is added inside the start-stop-daemon's shell wrapper so
--make-pidfile records the daemon's own PID (not the shell's),
which keeps `stop` semantics correct.
README updated to document the install location, INSTALL_DIR
override, and the syslog tag.
No automated tests — these are shell scripts the install pipeline
runs once on the device. All three scripts pass `bash -n` /
`sh -n` syntax checks. Real validation is end-user retest, gated on
the next release.
Refs #268, refs #250.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
on-device-install and raspberry-pi installers default to the new
v0.79.0 release binary. Also refreshes two stale comment examples in
the raspberry-pi install script (v0.17.0 → v0.78.0, v0.18.1 → v0.79.0)
so the in-file usage hints reflect the same era as the default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
make screenshots was producing artifacts: a ghost Spotify pill on
ui-devices, empty Plan-card URL inputs on ui-migration with cascading
"localhost" warnings, and "Checking configuration…" placeholder text
instead of "❌ Not configured" on ui-settings. Two root causes, fixed
together so the run is deterministic again.
1. Fakespeaker too thin for the post-wizard inspect pipeline. The new
migration wizard probes /supportedURLs and reads /networkInfo and
/sources alongside the existing /info, /presets, /recents. Those
routes now exist with sanitized fixtures (deviceID DEADBEEFCAFE,
loopback IPs, no real MACs or account IDs). The full group endpoint
set is also wired: /getGroup and /removeGroup return the empty
<group/> shape a real un-paired device emits; /addGroup and
/updateGroup echo the posted body with <status>GROUP_OK</status>
inserted before </group>, matching the success path documented in
issue #252. /supportedURLs lists everything the fake now serves so
any caller that probes capabilities first (e.g. marge_pairing.go)
sees a coherent picture. Tests cover the GET routes' XML roots, the
POST echo + GROUP_OK insertion contract, and /removeGroup's
GET-only contract (405 with Allow: GET on other methods).
2. run.sh seed hit a DNS cliff. The :443 preflight shipped in 3727ae6
resolves server_url on every /setup/settings call, and the
populatePlannedNetworkConfig step does it again. With the previous
seed of http://aftertouch.local:8000 each lookup burned ~5s on DNS
timeout, which compounded across the wizard calls and pushed
ui-migration past chromedp's 30s per-shot budget. Switched the seed
to http://aftertouch.localhost:8000 — RFC 6761 means *.localhost
resolves to loopback via the system resolver in milliseconds
(verified ~8ms on macOS / glibc / systemd-resolved) — so the brand-
friendly hostname survives in the captured PNGs without the
timeout. Manifest settle times bumped (ui-settings 300→2000ms,
ui-devices 500→2500ms, ui-sync 300→1000ms) to give fetchSettings +
fetchSpotifyStatus time to complete in headless Chrome.
While here, softened validateURL's loopback message to acknowledge the
on-device-install case (AfterTouch running on the speaker itself, where
loopback works) instead of unconditionally telling users they're
wrong. The validation still flags 127.0.0.1 / localhost since it's the
wrong answer 99% of the time, but the message now frames the
constraint rather than scolding.
docs/images/ui-*.png regenerated against the new pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refreshes docs/images/ui-{settings,devices,sync,migration}.png by
driving the web UI in chromedp against a synthetic speaker, so
documentation can be regenerated without real hardware and without
leaking personal data from the local network.
Three independent pieces:
- pkg/service/testing/fakespeaker — embeddable library serving the
HTTP and telnet surface the migration wizard probes (/info,
/presets, /recents and a getpdo CurrentSystemConfiguration reply
that places the device on the unmigrated happy path).
- cmd/dummy-speaker — thin CLI wrapping the library; self-registers
with a running service via POST /setup/devices.
- scripts/screenshots — chromedp runner driven by a JSON manifest;
decoupled from speaker/service setup so it can target any backend
URL. run.sh orchestrates a one-shot end-to-end capture and seeds
settings.json with a generic hostname plus discovery disabled to
keep real-network state out of the captures.
Captures are at DPR=2 for retina-sharp text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>