Bose shut down SoundTouch cloud services on 2026-05-06. Update the three
main user-facing docs to reflect that the shutdown has happened:
- README.md: rename section, rewrite opening paragraph, reframe the two
getting-started scenarios as 'already migrated' vs 'starting fresh'.
- SURVIVAL-GUIDE.md: past-tense title and opening; remove duplicate
Scenario B heading (copy-paste leftover from earlier edit); remove the
table of redirect methods and TLS note that belonged to the deleted
pre-shutdown Scenario B stub.
- MIGRATION-GUIDE.md: remove the 'cloud is still running' note from the
Sync step; fix the post-migration backup blurb to reference
soundtouch-backup rather than a non-existent Step 4 tar.gz.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DEVICE-LOCAL-INSTALL.md: old path at docs/ root → docs/architecture/
- Add Deployment Overview + three walkthrough pages under User Guides
- Add Architecture section for the planning doc
Data Sync requires AfterTouch to reach the speaker outbound, which
fails when AfterTouch is running in the cloud. Replace with the
correct three-step workaround from wimdeblauwe (discussion #295):
1. Manually create Sources.xml in the server's data volume with the
default source set (AUX, LOCAL_INTERNET_RADIO, TUNEIN, RADIO_BROWSER)
2. Send a sourcesUpdated notification to the speaker from a local machine
3. Power-cycle the speaker (CLI reboot is insufficient; firmware only
activates new source types at boot)
Add a clear note that Data Sync is not available from cloud deployments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'Each speaker needs its own install' is only true when the firmware
binds port 8000 to loopback (older devices, issue #196). Devices that
expose the port on the LAN can run one on-device AfterTouch and point
other LAN speakers at it — same as a Raspberry Pi. Qualify the cell
accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docs/guides/CLOUD-DEPLOY-WALKTHROUGH.md (new)
Step-by-step for deploying AfterTouch on a remote VPS:
Docker Compose + DISCOVERY_ENABLED=false, Coolify config from
wimdeblauwe's field report (discussion #295), CLI-driven speaker
migration (soundtouch-cli setup migrate/reboot from the local
machine), TuneIn source registration gotcha and fix, preset setup,
security warning about the unauthenticated Marge API, and the
'what breaks if the server goes offline' answer.
- docs/guides/DEPLOYMENT-OVERVIEW.md: expand from 2 to 3 options
(Local external host / Cloud VPS / On-device); update the
comparison table with the cloud-specific columns (HTTPS needed,
CLI migration, discovery disabled); link to the new walkthrough
and to discussion #295 as the community field report.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Problem: the existing docs gave no clear path for non-technical users.
- GETTING-STARTED.md is a Go library developer guide
- RASPBERRY-PI.md stops after the service is running (no migration or preset steps)
- DEVICE-LOCAL-INSTALL.md is an architectural analysis that confused installation intent
- No single page helped a user choose between external-host vs on-device
Changes:
- docs/DEVICE-LOCAL-INSTALL.md → docs/architecture/DEVICE-LOCAL-INSTALL.md
Move the planning/architecture doc out of the user-visible guides root;
add a redirect banner pointing to the user guides
- docs/guides/DEPLOYMENT-OVERVIEW.md (new)
Navigation landing page: comparison table (external host vs on-device),
links to user-friendly walkthrough + technical reference for each scenario
- docs/guides/EXTERNAL-HOST-WALKTHROUGH.md (new)
Step-by-step for Raspberry Pi / any always-on host: install, discover
speaker, run migration wizard, Health QuickFix, verify pairing, set
presets via UI or CLI — the post-install steps that RASPBERRY-PI.md
did not cover
- docs/guides/RASPBERRY-PI.md: cross-link to full walkthrough and overview
- README.md: replace the one-liner "see On-Device Installer" with a
pointer to the Deployment Overview so both paths are equally visible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- aftertouch init script: stop) now waits up to 15 s for SIGTERM
to take effect, then escalates to SIGKILL; prevents stale daemon
processes after '/etc/init.d/aftertouch stop' returns (weissigera's
workaround was manual 'killall aftertouch-service')
- install.sh: add --version / -v CLI flag so the version to install
can be passed as a command-line argument in addition to the VERSION
env var; document the trade-off of the hard-coded default in a
comment; update scripts/on-device-install/README.md with concrete
usage examples for env-override, CLI flag, and rollback tip
- docs/guides/ON-DEVICE-INSTALL-WALKTHROUGH.md: 10-step runbook
derived from weissigera's field-tested procedure (issue #329
comment #4521280831): SSH connection, storage cleanup, install via
install.sh, reboot, SSH tunnel, Health QuickFix, pairing
verification, soundtouch-cli download, custom-radio preset setup,
and final verification; troubleshooting table at the end
Closes#329 (remaining two tasks)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before overwriting the binary, read its version via --version and save
a copy as aftertouch-service.<version>.backup (falls back to a timestamp
if the flag is absent or the build is a dev build).
After the new binary is in place, delete every older *.backup, *.old,
and *.new artefact in INSTALL_DIR. /mnt/nv on SoundTouch SCM modules
has only tens of MB free; accumulating one ~12 MB backup per upgrade
quickly causes 'no space left on device' on the next download.
Only the backup created in this run (the <current-release>-1 binary) is
kept, giving a single one-step rollback point without wasting disk.
Relates to #329 (on-device install friction reported by weissigera).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When handleDiscoveredDevice calls MoveDevice and the target device
directory already exists (pre-existing duplicate state), os.Rename
fails with ENOTEMPTY/EEXIST leaving the stale source account entry
on disk. Because SaveDeviceInfo has just written fresh data under
accountID, it is safe to unconditionally remove the stale source
entry afterward — RemoveDevice returns nil when the path is already
gone (successful rename), so this is a no-op in the happy path and
a cleanup in the failure path.
Adds TestHandleDiscoveredDevice_CrossAccountMigration_TargetExists
which seeds a device under two real accounts (old sorts alphabetically
first so findExistingDeviceInfoByDeviceID picks it as storedAccount),
triggers discovery with the new account as MargeAccountUUID, and
asserts that after the cycle only the new account entry exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exercises the branch in handleDiscoveredDevice where a device's live
MargeAccountUUID differs from its stored account. The test:
- seeds a device + presets under 'default'
- mocks /info to report a different margeAccountUUID ('8637922')
- calls handleDiscoveredDevice
- asserts the device is now stored under the new account with the live name
- asserts the old 'default' entry is gone
- asserts presets survived the MoveDevice rename
- asserts ListAllDevices returns exactly one entry (no duplicates)
Closes the server-level gap noted during PR #348 review.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Health check (checks_stale_internet_radio.go): detects stub INTERNET_RADIO
sources (empty credentials) left on devices initialised before the stub was
removed from the default source list. Quick-fix removes by ID; skips any
INTERNET_RADIO source that has real credentials.
Datastore: DeleteSourceByID and DeleteSourceByType (uniqueness-guarded).
API: DELETE /setup/sources/{account}/{device}/{sourceID}
CLI — two new commands:
soundtouch-cli cloud source remove --service-url ... --account ... --device ... [--id 10002 | --type INTERNET_RADIO]
Talks to AfterTouch (service side). --type resolves to canonical ID
locally; fails for unknown types.
soundtouch-cli source notify-updated --host <speaker-ip>
Talks to the speaker directly. Fetches device ID from /info, then
POSTs sourcesUpdated to :8090/notification so the speaker re-fetches
its source list immediately.
CloudCommonFlags (--service-url / AFTERTOUCH_URL) mirrors CommonFlags
(--host) for AfterTouch-facing command groups.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add getInitialSources() that excludes the legacy INTERNET_RADIO (10002)
provider from newly-created device Sources.xml files. GetDefaultSources()
retains the entry for backward-compatible canonicalisation of existing
devices and cloud-level account responses.
Fix mergeDefaultSources() to rebuild the merged list in canonical ID
order (defaults first, using stored credentials when present, then
custom sources such as Spotify). This prevents INTERNET_RADIO from
landing at the end of the cloud /sources response when a device's
Sources.xml was created without it.
Drop the two verbose search-loop log lines from resolvePresetSource.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The guard was accidentally placed in HandlePlayRadioBrowser instead of
HandleDevicePlay in the initial fix commit, then removed from there by
the build-fix commit — leaving HandleDevicePlay with no guard at all.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous edit accidentally inserted the TUNEIN placeholder guard
into HandlePlayRadioBrowser, which uses a different req struct without
SourceAccount/Source fields, breaking the build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Speakers echo back the source name as SourceAccount when no real
credential is set (e.g. SourceAccount="TUNEIN" for a TUNEIN source).
HandleDevicePlay was forwarding this verbatim, causing the speaker to
try authenticating with the source name as a TuneIn account and
returning INVALID_SOURCE.
Clear SourceAccount when it equals Source; preserve it when it differs
(real credentials such as Spotify or STORED_MUSIC UUIDs).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TuneIn's profiles API caps initial results at ~10 per container (Stations,
Shows, etc.) and exposes a Pivots.More.Url cursor for the remainder. This
change wires that cursor through the stack so users can load additional
results without leaving the search view.
- tuneInSearchSection now extracts Pivots.More.Url as bmx_next when
itemToken is present; absent for containers already at their limit
- TuneInSearchNext fetches the cursor URL, which returns a flat Items[]
(not nested containers), and maps Station/Program/Topic items using
the existing play/profile builders
- New GET /v1/search/next and /api/tunein/search/next endpoints with
matching handlers in both service paths
- TuneInBrowser: flat items state replaced with per-section sections
state; each section shows a header label and a Load more button when
a cursor is available; browse/navigate mode is unaffected
Relates to #336.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When `discovery_enabled` is false the page no longer fires a discovery
scan on load. Both DOMContentLoaded handlers now await fetchSettings()
and gate triggerDiscovery() on the returned flag — default true keeps
existing behaviour for installations that never touched the setting.
Also renames the UI label from "Enable Automated Discovery" to
"Enable Periodic Discovery" to make clear the checkbox controls the
background timer, not the manual trigger button or IP-entry form.
Relates to #269
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
encoding/xml is case-sensitive, so Presets.xml files written by older
AfterTouch versions using <ContentItem> (capital C) had all source,
location, and type attributes silently dropped on read. Every preset for
such a device had empty fields, causing mapPresetsToFullResponse to skip
them all — the speaker received /full with zero presets and stored nothing.
Fix: normalise <ContentItem> → <contentItem> before unmarshaling in the
new readPresetsLocked helper. If normalisation was needed, GetPresets
rewrites the file in canonical form after releasing the read lock, so the
issue self-heals on first service start with no manual intervention.
Diagnosed via the i218 encrypted diagnostic export (device 304511B46CBC,
ST30 Master Bedroom): health check speaker_presets_count reported
"Speaker shows 0 preset slot(s); service Presets.xml has 6", and the
service log showed six [Marge] /full: skipping preset N — source ""
messages per /full call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#337's first commit added the OAuth-derivation to the DNS interceptor
but missed the served TLS certificate. With a serverURL of
`http://mac.fritz.box:8000` the cert SAN list covered `mac.fritz.box`
but not `macoauth.fritz.box`, so the speaker would resolve the OAuth
host correctly (via the new DNS hijack) and then immediately fail the
TLS handshake — Spotify / Amazon Music token refresh dies before
reaching AfterTouch.
getDomains now calls discovery.DeriveOAuthHostnames(serverURL) and
discovery.DeriveOAuthHostnames(httpsServerURL), feeding the derived
names into the SAN map alongside the existing entries. IP-based
serverURLs continue to produce no derivation (the OAuth construction
is unrecoverable for them — see the existing oauth_target_reachable
health check).
Tests in cmd/soundtouch-service/main_test.go lock in:
- Hostname serverURL → derived OAuth variant present in SAN list.
- IP serverURL → no malformed `192oauth.…` entry leaks in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The markdown-link-check CI step caught five stale links in
docs/README.md's Concept Documentation section pointing at files
the previous commit moved into docs/archive/. Replaced with a
pointer to SUMMARY.md's Concepts section + a short curated list
of the currently-relevant docs (Spotify Overview, Spotify OAuth,
Amazon Music OAuth, Encrypted Export, Request Recording). The
archived planning artefacts get a single line acknowledging
their existence under docs/archive/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The speaker firmware constructs the OAuth host by appending "oauth" to
the first label of the configured streaming hostname (aftertouch.lan
→ aftertouchoauth.lan, used by both Spotify and Amazon Music token
refresh). AfterTouch's DNS server previously only hijacked the
hardcoded list of Bose hostnames, so operators self-hosting at a
custom hostname had to add the OAuth alias themselves — and the
amazon-music-oauth.md / spotify-overview.md docs incorrectly
claimed the DNS server handled it automatically.
ofthesun9 (#337) caught this via the worst variant: IP-based
serverURL (192.168.0.30 → 192oauth.168.0.30), which is a malformed
hostname no DNS resolver can answer for. There is no clean DNS
workaround for the IP case — the operator must use a hostname.
Three changes:
- pkg/discovery/dns.go DeriveOAuthHostnames parses the configured
serverURL, derives <first-label>oauth.<rest> when the host is a
hostname (not IP), and adds it to the DNSDiscovery hijack list. IP
serverURLs deliberately yield no derivation — the malformed name
isn't worth handling and the new health check surfaces the trap.
- New checks_oauth_target health check fires a Warning when serverURL
is an IP literal, with a concrete example of the malformed name
(`192oauth.168.0.30`) and a ManualCommand pointing at the switch.
- amazon-music-oauth.md and spotify-overview.md rewritten: drop the
false "automatic" claim, document the three resolution paths
(AfterTouch DNS + speaker resolves via it / external LAN DNS /
per-speaker /etc/hosts), and explicitly flag IP-based --server-url
as incompatible with OAuth on either provider.
Tests cover the derivation matrix (hostname / IPv4 / IPv6 / single
label / empty / garbage URL), shouldIntercept's new behaviour
(derived host hit, base host not auto-hijacked, case-insensitive),
the health check's four states, and the malformed-host helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The TestDocsConsistency walk only iterated [".", "guides", "reference",
"analysis"] — concepts/ was silently invisible, which is why
amazon-music-oauth.md slipped into the tree without a SUMMARY entry.
Refactored to walk the entire docs/ tree, with a small dirsToSkip
allow-list (_includes, archive, diagrams, images) for asset trees.
New top-level narrative directories are picked up automatically;
only asset dirs need an explicit entry.
The wider walk surfaced six previously-hidden concepts/* files. Five
older planning artefacts ("Enhanced State Management System",
"Upstream Bose Service Simulation") moved into docs/archive/ where
the dirsToSkip already excludes them; concepts/README.md renamed to
upstream-service-simulation-overview.md since "README.md" inside
archive/ would be misleading. Spotify Overview and Amazon Music
OAuth are user-facing narrative docs and are now linked under
Concepts in SUMMARY.md.
Note: concepts/streborn-patterns.md is internal review notes (its
own opening line says so) and is currently unlinked from SUMMARY.md;
will be handled separately by the maintainer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Discovery cycles emit one line per UPnP M-SEARCH header, one per
parsed response, and one per enrichment step — by default. A typical
service-binary cycle prints ~50–80 lines for a 3-speaker LAN. Most
operators want a startup-and-summary view; the per-packet trace is
only useful for debugging.
- New SetVerbose/IsVerbose/logVerbose helpers in pkg/discovery (atomic
bool, zero-value off).
- Chatty log.Printf calls in upnp.go and mdns.go demoted to logVerbose:
per-header dumps, per-response dumps, per-device enrichment steps,
M-SEARCH details, read-deadline / cancel-context noise.
- Kept at default level: discovery start ("Starting SSDP discovery
for…"), end ("Discovery completed. Processed N responses, found N
unique devices" + per-device summary), warnings ("Configured
interface not found", "Failed to fetch device description", …), and
the new "Rejecting non-Bose device" classifier.
- cmd/soundtouch-cli/discover devices grew a --verbose / -v flag that
flips the package toggle on; the service binary leaves it at the
zero value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four small, independent improvements bundled into one cut:
1. Restrict device discovery to SoundTouch-family services (#269/#359).
- mDNS now queries all three SoundTouch service-type variants in
parallel (_soundtouch._tcp, _bose-soundtouch._tcp, _soundtouchstick._tcp)
and deduplicates results by host:port. mDNS has no native wildcard
for service types, so we fan out one query per variant.
- UPnP/SSDP M-SEARCH receives a manufacturer/modelName check after
fetching the device description: devices whose manufacturer doesn't
contain "bose" AND whose model doesn't contain "soundtouch" are
rejected. Closes the loop on NorbertBauer's diagnostic bundle that
showed a Dreambox dm920 and Onkyo HT-R695 living under the default
account because they answered our generic MediaRenderer:1 probe.
2. New health check: default-account-contains-non-Bose-devices (#269).
Walks devices keyed under data/accounts/default/devices/, flags any
whose ProductCode/Name doesn't look SoundTouch, and offers an Evict
QuickFix. Bose devices still in default (legitimate pre-pair) are
intentionally ignored — that's the consistency check's domain.
3. Clipboard fallback for Copy buttons (#355). The two health-tab Copy
buttons used navigator.clipboard.writeText, which requires a secure
context. Over plain HTTP at a LAN IP the browser blocks it silently
and the button shows "Copy failed". New copyTextToClipboard helper
tries the modern API first, falls back to document.execCommand("copy")
via an off-screen textarea.
4. Web UI static-asset cache-busting (#345). dekiesel needed Ctrl+F5 to
see the v0.89 Download button after upgrade. The root HTML now
carries a ?v=<hash> query string on /web/js/script.js and
/web/css/style.css references. Hash is sha256 over the embedded asset
bodies, truncated to 12 hex chars — stable per binary, changes when
the assets change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the findings list grows the diagnostic-export subsection got
pushed below the visible viewport. Moving it above the findings list
(but below the Refresh header and description) keeps the Download
button in reach regardless of how many checks fire.
Wrapped in a subtle gray box to visually distinguish it from the
checks themselves.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the Download button sat at the top-right with the Refresh
button, while the "What does the report contain?" details block lived
below the health-checks description paragraph — visually separated by
the description and an entire section's worth of layout.
Now the diagnostic export lives in its own subsection at the bottom of
the Health tab, with the button, a one-line tagline, the details
block, and the post-download status indicator all adjacent. The
header keeps just Refresh, which controls the health-checks view it
sits next to.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous text explained how the merge works but didn't give operators a
clear signal for when to act. New structure leads with:
- "When you need this": rarely; symptoms a user actually sees (presets
reset, BoseApp offline) instead of a syslog string most users won't
consult.
- "How to tell": open the Health tab, look for speaker_marge_url; if
clean, leave this empty.
- "Manual path": only after the user has decided they need it.
Adds a small, always-visible hint below the label that points to the
Health tab — most operators won't expand the ⓘ panel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Operators who deploy AfterTouch on an IP-only host (no DNS hostname) and
who get a speaker_marge_url health warning previously had to SSH in, edit
their systemd unit or docker-compose, add --tls-extra-host, and restart.
The fix is now reachable from the UI:
- datastore.Settings gains TLSExtraHosts []string. At startup
applyPersistedSettings merges CLI/env values (still authoritative)
with persisted ones, deduplicating while preserving order.
- /setup/settings (GET) exposes tls_extra_hosts (editable list) and
tls_san_hosts (the full effective SAN list, read-only).
- /setup/settings (POST) accepts tls_extra_hosts (*[]string so callers
can distinguish "field omitted" from "explicitly empty").
- Settings tab grows a "TLS extra hosts" textarea + an info panel
explaining the restart-required dance.
- speaker_marge_url emits a QuickFix labelled "Add <host> to TLS hosts"
alongside the existing CLI manual command. The fix re-probes the
device's /info, extracts the margeURL host, and appends it to the
persisted list — race-safe against stale findings.
- HTTPS-SETUP.md documents both paths.
Tests cover: merge dedup + ordering + whitespace, the new QuickFix
emission shape, and the margeURL host extraction across HTTPS/HTTP/bare
input forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The :443 reachability preflight was emitting a WARN on every deployment
where AfterTouch's configured --server-url is HTTP (not HTTPS), even
when speakers were migrated to that HTTP URL and never connect to :443.
Operators reproduced this on #218 (CTonyPeterson) and #344
(california444) — both saw the warning even though their setups had no
need for iptables port forwarding, and CTonyPeterson followed the
recommended iptables OUTPUT rule which then caught his host's own
outbound HTTPS traffic and broke `go install` and his browser.
Two changes:
- Probe443Result gains NotApplicable + Reason. Check443Reachability
returns the NotApplicable verdict when the parsed serverURL scheme is
http. The settings UI renders an ℹ️ info badge with the reason instead
of a red ✗.
- FormatPreflightGuidance grows a one-line caveat about the iptables
OUTPUT chain: it catches all outbound :443 on the host, including
browsers / go install / apt-get, which is rarely what the operator
wants.
HTTPS-SETUP.md gains the same caveat plus a section documenting the
new not-applicable verdict.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- setup remote-services subcommand: enables (default) or removes
(--remove) the remote_services SSH-enablement marker via SSH, targeting
persistent locations (/etc or /mnt/nv) before the volatile /tmp fallback
- setup plan now includes a "persist remote_services" step when the marker
is only in /tmp (would be lost on next reboot, breaking SSH mid-migration)
- setup plan state header shows a [⚠] line when remote_services is
enabled but not persistent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the single-sided requireAfterTouchDNSReachable with runDNSPreflight
that probes both the CLI machine and the speaker (via SSH nslookup) in
parallel, then renders a two-row table when results differ.
The speaker's perspective is authoritative: a CLI-only failure no longer
blocks the migration (the speaker may reach the DNS listener via a network
path the CLI host cannot). Migration is only aborted when the speaker itself
definitively cannot reach AfterTouch's DNS listener.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- isXMLMigrated and isResolvConfMigrated now guard against empty hostname
(Go's strings.Contains(s, "") is always true, causing any speaker to
appear migrated when --service-url has a malformed single-slash scheme)
- renderPlanSteps message no longer claims "and paired" when --include-pair=false
- validateServiceURL rejects malformed service URLs early with a hint
(e.g. "did you mean https://soundtouch.fritz.box?")
- Generated plan-step commands move --host before the subcommand name
(urfave/cli/v2 requires global flags before the first subcommand token)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a <details> block listing what the encrypted archive contains so
reporters know what they're sharing before clicking. After a successful
download, show two submission options with email preferred:
aftertouch-support@gesellix.net (mailto link with pre-filled subject and
filename) or a GitHub issue with the file renamed to <filename>.txt (GitHub
blocks .age uploads).
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>