mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
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>
1718 lines
82 KiB
HTML
1718 lines
82 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<title>AfterTouch</title>
|
|
<meta name="description" content="AfterTouch Admin UI — Configure your SoundTouch replacement, migrate speakers, and manage your local account." />
|
|
<link rel="icon" href="/web/img/favicon-braille.svg" type="image/svg+xml"/>
|
|
<link rel="stylesheet" href="/web/css/style.css"/>
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<a class="brand" href="/?chooser" title="AfterTouch home">
|
|
<img src="/web/img/favicon-braille.svg" alt=""/>
|
|
<span class="brand-text">
|
|
<span class="brand-name">AfterTouch</span>
|
|
<span class="brand-subtitle">Bose SoundTouch Toolkit</span>
|
|
</span>
|
|
</a>
|
|
<nav class="bar-links">
|
|
<a href="https://gesellix.github.io/Bose-SoundTouch/" target="_blank" rel="noopener" title="Documentation">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
|
|
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
|
</svg>
|
|
Docs
|
|
</a>
|
|
<a href="/app">Open the Player →</a>
|
|
</nav>
|
|
</header>
|
|
<p style="margin: 0 0 16px; font-size: 0.9em; color: #888">
|
|
Admin & Setup console: migration, settings, accounts, and diagnostics.
|
|
</p>
|
|
|
|
<div class="tabs">
|
|
<div class="tab-buttons">
|
|
<button class="tab-btn active" onclick="openTab(event, 'tab-overview')">
|
|
Overview
|
|
</button>
|
|
<button class="tab-btn" onclick="openTab(event, 'tab-settings')">
|
|
1. Settings
|
|
</button>
|
|
<button class="tab-btn" onclick="openTab(event, 'tab-devices')">
|
|
2. Devices
|
|
</button>
|
|
<button class="tab-btn" onclick="openTab(event, 'tab-sync')">
|
|
3. Data Sync
|
|
</button>
|
|
<button class="tab-btn" onclick="openTab(event, 'tab-migration')">
|
|
4. Migration
|
|
</button>
|
|
<button class="tab-btn" onclick="openTab(event, 'tab-interactions')">
|
|
5. Interactions & Events
|
|
</button>
|
|
<button class="tab-btn" onclick="openTab(event, 'tab-account')">
|
|
6. Local Account
|
|
</button>
|
|
<button class="tab-btn" onclick="openTab(event, 'tab-health')">
|
|
7. Health
|
|
</button>
|
|
<button class="tab-btn" onclick="openTab(event, 'tab-logs')">
|
|
8. Logs
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab 0: Overview -->
|
|
<div id="tab-overview" class="tab-content active">
|
|
<h2>Welcome to AfterTouch</h2>
|
|
<p>
|
|
This toolkit helps you keep your Bose SoundTouch speakers
|
|
functional even after the Bose Cloud shutdown in May 2026.
|
|
It emulates the necessary cloud services locally on your
|
|
network.
|
|
</p>
|
|
|
|
<h3>Migration Process at a Glance</h3>
|
|
<div class="info-box prerequisite-box">
|
|
<strong>🔌 Speaker shell access</strong><br/>
|
|
The wizard talks to the speaker over one of two transports.
|
|
The <strong>Migration</strong> tab probes both automatically
|
|
and uses whichever your device exposes — you don't have to
|
|
choose manually.
|
|
<ul style="margin-top: 5px; margin-bottom: 0; padding-left: 20px;">
|
|
<li>
|
|
<strong>SSH</strong> (richest option — required for
|
|
the XML migration, the <code>/etc/resolv.conf</code>
|
|
DNS hook, and installing the local CA). Enable it by
|
|
creating an empty <code>remote_services</code> file
|
|
on a USB stick, inserting it into the speaker's
|
|
<strong>SERVICE</strong> port, and rebooting. Verify
|
|
on the Migration tab — <em>SSH</em> in the state
|
|
card's <em>Transports</em> row should show ✅
|
|
Reachable. Manual check:
|
|
<code
|
|
>ssh -oHostKeyAlgorithms=+ssh-rsa
|
|
root@<SPEAKER-IP></code
|
|
>
|
|
(no password).
|
|
</li>
|
|
<li>
|
|
<strong>Telnet (Port 17000)</strong> — the SSH-less
|
|
fallback. Most SoundTouch firmware exposes a
|
|
diagnostic shell on TCP/17000 automatically, no
|
|
USB-stick setup required. Limited to HTTP migrations
|
|
(no CA install possible without SSH). The state card
|
|
surfaces this in the same <em>Transports</em> row.
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<ol class="guide-steps">
|
|
<li>
|
|
<strong>Settings:</strong> Review the
|
|
<strong>Settings</strong> tab. Ensure the "Target
|
|
Domain" and "Proxy Domain" use an IP address or domain
|
|
name that is
|
|
<strong>accessible from your speakers</strong> (usually
|
|
the IP of this server on your local network). You can
|
|
also edit the Target URL directly from the Migration tab
|
|
with a <em>Save as default</em> button.
|
|
</li>
|
|
<li>
|
|
<strong>Discovery:</strong> Go to the
|
|
<strong>Devices</strong> tab to find your speakers on
|
|
the network. Ensure your speakers are powered on and
|
|
connected to the same network.
|
|
</li>
|
|
<li>
|
|
<strong>Data Sync:</strong> In the
|
|
<strong>Data Sync</strong> tab, fetch your current
|
|
presets, recents, and sources. This step is critical to
|
|
ensure your local service has all your personalized data
|
|
before you disconnect from the Bose cloud.
|
|
</li>
|
|
<li>
|
|
<strong>Migration:</strong> In the
|
|
<strong>Migration</strong> tab the wizard offers a
|
|
one-click <strong>Apply Suggested Plan</strong> that
|
|
picks the right recipe for your speaker (XML over SSH
|
|
when SSH is available, telnet URL flip otherwise). For
|
|
mix-and-match across the three independent axes — URL
|
|
flip transport, DNS interception, CA install — expand
|
|
<em>Customize this migration</em>. A visible pre-flight
|
|
check runs before any backend operation touches the
|
|
speaker.
|
|
</li>
|
|
<li>
|
|
<strong>Verification:</strong> After migration and
|
|
reboot, your speaker will communicate with this toolkit
|
|
instead of Bose servers.
|
|
</li>
|
|
</ol>
|
|
|
|
<div class="info-box safety-box">
|
|
<strong>⚠️ Safety First:</strong> Before starting any
|
|
migration, please read our
|
|
<a href="https://gesellix.github.io/Bose-SoundTouch/docs/guides/MIGRATION-SAFETY/" target="_blank">Professional
|
|
Migration & Safety Guide</a>. The toolkit automatically creates backups, but understanding the process
|
|
is key to a smooth transition.
|
|
</div>
|
|
|
|
<h3>Useful Links</h3>
|
|
<ul>
|
|
<li><a href="https://gesellix.github.io/Bose-SoundTouch/docs/guides/SURVIVAL-GUIDE/" target="_blank">Cloud
|
|
Shutdown Survival Guide</a></li>
|
|
<li><a href="https://gesellix.github.io/Bose-SoundTouch/docs/guides/CLI-REFERENCE/" target="_blank">CLI
|
|
Reference</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Tab 1: Settings -->
|
|
<div id="tab-settings" class="tab-content">
|
|
<h2>System Settings</h2>
|
|
<p style="font-size: 0.9em; color: #555; margin-bottom: 20px">
|
|
<strong>Note:</strong> These URLs must be
|
|
<strong>accessible from your SoundTouch devices</strong>.
|
|
Use the IP address of this server on your local network
|
|
(e.g., <code>http://192.0.2.100:8000</code>) rather than
|
|
<code>localhost</code>.
|
|
</p>
|
|
<div style="margin-bottom: 20px">
|
|
<label for="target-domain">Target Domain:</label>
|
|
<input type="text" id="target-domain" placeholder="http://192.168.x.x:8000" style="width: 300px"/>
|
|
<span style="font-size: 0.8em; color: #666">(Standard services URL)</span>
|
|
<div id="target-domain-resolved" style="font-size: 0.85em; margin-top: 4px; min-height: 1.2em"></div>
|
|
<div id="https-443-status" style="font-size: 0.85em; margin-top: 4px; min-height: 1.2em"></div>
|
|
</div>
|
|
<div style="margin-bottom: 20px">
|
|
<label>HTTPS URL:</label>
|
|
<code id="https-url-effective">—</code>
|
|
<span id="https-url-effective-note" style="font-size: 0.8em; color: #666"></span>
|
|
<div style="margin-top: 6px">
|
|
<span class="info-toggle" onclick="toggleInfo('https-url-override-info')">Advanced: override HTTPS URL ⓘ</span>
|
|
<div id="https-url-override-info" class="info-details">
|
|
<p style="font-size: 0.85em; color: #666; margin: 4px 0">
|
|
AfterTouch also serves an HTTPS endpoint (used for the
|
|
DNS-based redirect, music-service login, and certificate
|
|
trust). By default it follows the Target Domain above
|
|
(same host, on the HTTPS port), so you normally don't
|
|
set anything here. Provide an override only when a
|
|
reverse proxy serves HTTPS on a different host or port.
|
|
Leave it empty to go back to deriving it automatically.
|
|
Takes effect after saving.
|
|
</p>
|
|
<input type="text" id="https-url-override" placeholder="https://host:8443 (empty = derive)" style="width: 300px"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-bottom: 20px">
|
|
<label for="default-landing">Landing page (<code>/</code>):</label>
|
|
<select id="default-landing" style="margin-left: 4px">
|
|
<option value="chooser">Chooser (pick Player or Admin)</option>
|
|
<option value="app">Go straight to the Player</option>
|
|
<option value="admin">Go straight to Admin & Setup</option>
|
|
</select>
|
|
<div style="font-size: 0.85em; color: #666; margin-top: 4px;">
|
|
What a browser sees at the root URL. The Player and Admin pages
|
|
stay reachable at <code>/app</code> and <code>/admin</code> either way.
|
|
</div>
|
|
</div>
|
|
<div style="margin-bottom: 20px">
|
|
<strong>TLS extra hosts:</strong>
|
|
<span class="info-toggle" onclick="toggleInfo('tls-extra-hosts-info')">ⓘ</span>
|
|
<div id="tls-extra-hosts-info" class="info-details">
|
|
<strong>When you need this:</strong> rarely. The TLS
|
|
certificate already covers AfterTouch's configured
|
|
server URL, HTTPS URL, and the host's own name. Add
|
|
entries here only when a speaker can't reach
|
|
AfterTouch over TLS — typical symptoms include
|
|
presets resetting on reboot, the BoseApp showing
|
|
the speaker as offline, or
|
|
<code>CURLE_SSL_CACERT (60)</code> in the speaker
|
|
syslog.<br/>
|
|
<strong>How to tell:</strong> open the
|
|
<strong>Health tab</strong> and look for
|
|
<code>speaker_marge_url</code> warnings. Each
|
|
warning names the host a speaker is pointing at;
|
|
clicking the <em>Add <host> to TLS hosts</em>
|
|
QuickFix fills this list for you. If that check is
|
|
clean, this list can stay empty.<br/>
|
|
<strong>Manual path:</strong> add one host per line
|
|
and save. The TLS certificate is regenerated at
|
|
startup from the merged list of
|
|
<code>--server-url</code> host,
|
|
<code>--https-server-url</code> host, the system
|
|
hostname, any <code>--tls-extra-host</code> /
|
|
<code>TLS_EXTRA_HOST</code> CLI/env values, and the
|
|
hosts persisted here. CLI/env wins over persisted
|
|
on overlap.<br/>
|
|
<strong>Applying changes requires a service
|
|
restart.</strong>
|
|
</div>
|
|
<div style="font-size: 0.85em; color: #666; margin-top: 4px;">
|
|
Usually empty. Add a host here only if the
|
|
<a href="#" onclick="openTab(null, 'tab-health'); return false;">Health tab</a>
|
|
flags a <code>speaker_marge_url</code> warning — or use
|
|
the one-click QuickFix on that warning to fill it for you.
|
|
</div>
|
|
<div style="margin-top: 5px">
|
|
<textarea
|
|
id="tls-extra-hosts"
|
|
placeholder="One host per line, e.g. 192.0.2.10 or aftertouch.lan"
|
|
style="width: 360px; height: 84px; font-family: monospace; font-size: 0.9em;"
|
|
></textarea>
|
|
</div>
|
|
<div id="tls-san-effective" style="font-size: 0.8em; color: #666; margin-top: 4px;"></div>
|
|
</div>
|
|
<div style="margin-bottom: 20px">
|
|
<strong>Device Discovery:</strong>
|
|
<div style="margin-top: 5px">
|
|
<label style="display: block; margin-bottom: 5px">
|
|
<input type="checkbox" id="discovery-enabled"/> Enable Periodic Discovery
|
|
</label>
|
|
<div style="margin-left: 20px">
|
|
<label for="discovery-interval">Discovery Interval:</label>
|
|
<input type="text" id="discovery-interval" placeholder="5m" style="width: 100px"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-bottom: 20px">
|
|
<strong>DNS Discovery:</strong>
|
|
<div style="margin-top: 5px">
|
|
<label style="display: block; margin-bottom: 5px">
|
|
<input type="checkbox" id="dns-enabled"/> Enable
|
|
DNS Discovery Server
|
|
</label>
|
|
<div style="margin-left: 20px; margin-bottom: 5px">
|
|
<label for="dns-upstream">Upstream DNS:</label>
|
|
<input
|
|
type="text"
|
|
id="dns-upstream"
|
|
placeholder="Default: system nameservers"
|
|
style="width: 200px"
|
|
/>
|
|
<span
|
|
class="info-toggle"
|
|
onclick="toggleInfo('dns-upstream-info')"
|
|
>ⓘ</span
|
|
>
|
|
<div id="dns-upstream-info" class="info-details">
|
|
Optional: comma-separated list of DNS servers
|
|
(e.g., <code>1.1.1.1, 8.8.8.8</code>).<br/>
|
|
If empty, AfterTouch defaults to the system
|
|
nameservers (e.g. from
|
|
<code>/etc/resolv.conf</code>).<br/>
|
|
<div
|
|
id="dns-current-upstream"
|
|
style="margin-top: 5px; font-weight: bold"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-left: 20px">
|
|
<label for="dns-bind">DNS Bind Address:</label>
|
|
<input
|
|
type="text"
|
|
id="dns-bind"
|
|
placeholder=":53"
|
|
style="width: 100px"
|
|
/>
|
|
<span
|
|
style="
|
|
font-size: 0.8em;
|
|
color: #666;
|
|
margin-left: 5px;
|
|
"
|
|
>(e.g., :53 or 0.0.0.0:53.
|
|
<strong>Port 53</strong> is required for actual
|
|
migration)</span
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-bottom: 20px">
|
|
<strong>Integrations:</strong>
|
|
<div style="margin-top: 8px">
|
|
<details class="integration-panel" id="panel-spotify">
|
|
<summary class="integration-summary">
|
|
<span class="integration-name">Spotify</span>
|
|
<span class="integration-badge" id="spotify-badge">…</span>
|
|
</summary>
|
|
<div class="integration-body">
|
|
<div style="margin-bottom: 8px">
|
|
<label for="spotify-client-id" style="display: inline-block; width: 130px">Client ID:</label>
|
|
<input type="text" id="spotify-client-id" placeholder="32-char hex string" style="width: 280px"/>
|
|
</div>
|
|
<div style="margin-bottom: 8px">
|
|
<label for="spotify-client-secret" style="display: inline-block; width: 130px">Client Secret:</label>
|
|
<input type="password" id="spotify-client-secret" placeholder="(leave blank to keep existing)" style="width: 280px"/>
|
|
</div>
|
|
<div style="margin-bottom: 8px">
|
|
<label for="spotify-redirect-uri" style="display: inline-block; width: 130px">Redirect URI:</label>
|
|
<input type="text" id="spotify-redirect-uri" placeholder="<your-service-url>/mgmt/spotify/callback" style="width: 280px"/>
|
|
</div>
|
|
<div id="spotify-config-status" style="margin-top: 5px; font-size: 0.9em">Checking configuration...</div>
|
|
</div>
|
|
</details>
|
|
|
|
<details class="integration-panel" id="panel-amazon">
|
|
<summary class="integration-summary">
|
|
<span class="integration-name">Amazon Music</span>
|
|
<span class="integration-badge" id="amazon-badge">…</span>
|
|
</summary>
|
|
<div class="integration-body">
|
|
<div style="margin-bottom: 8px">
|
|
<label for="amazon-client-id" style="display: inline-block; width: 130px">Client ID:</label>
|
|
<input type="text" id="amazon-client-id" placeholder="amzn1.application-oa2-client..." style="width: 280px"/>
|
|
</div>
|
|
<div style="margin-bottom: 8px">
|
|
<label for="amazon-client-secret" style="display: inline-block; width: 130px">Client Secret:</label>
|
|
<input type="password" id="amazon-client-secret" placeholder="(leave blank to keep existing)" style="width: 280px"/>
|
|
</div>
|
|
<div style="margin-bottom: 8px">
|
|
<label for="amazon-redirect-uri" style="display: inline-block; width: 130px">Redirect URI:</label>
|
|
<input type="text" id="amazon-redirect-uri" placeholder="<your-service-url>/mgmt/amazon/callback" style="width: 280px"/>
|
|
</div>
|
|
<div id="amazon-config-status" style="margin-top: 5px; font-size: 0.9em">Checking configuration...</div>
|
|
</div>
|
|
</details>
|
|
|
|
<details class="integration-panel" id="panel-tts">
|
|
<summary class="integration-summary">
|
|
<span class="integration-name">Google Cloud TTS</span>
|
|
<span class="integration-badge" id="tts-badge">…</span>
|
|
</summary>
|
|
<div class="integration-body">
|
|
<div style="font-size: 0.85em; color: #555; max-width: 600px; margin-bottom: 8px">
|
|
Text-to-speech for speaker announcements. The <em>translate</em> provider needs no
|
|
credentials; <em>google-cloud</em> needs an API key. Either way, an app key is required to
|
|
play on the speaker (see the <strong>Speaker endpoint</strong> docs).
|
|
</div>
|
|
<div style="margin-bottom: 8px">
|
|
<label for="tts-provider" style="display: inline-block; width: 130px">Provider:</label>
|
|
<select id="tts-provider" style="width: 285px">
|
|
<option value="translate">Google Translate (no credentials)</option>
|
|
<option value="google-cloud">Google Cloud TTS (API key)</option>
|
|
</select>
|
|
</div>
|
|
<div style="margin-bottom: 8px">
|
|
<label for="tts-google-api-key" style="display: inline-block; width: 130px">Google API Key:</label>
|
|
<input type="password" id="tts-google-api-key" placeholder="(leave blank to keep existing)" style="width: 280px"/>
|
|
</div>
|
|
<div style="margin-bottom: 8px">
|
|
<label for="tts-app-key" style="display: inline-block; width: 130px">Speaker app_key:</label>
|
|
<input type="password" id="tts-app-key" placeholder="(leave blank to keep existing)" style="width: 280px"/>
|
|
</div>
|
|
<div style="margin-bottom: 8px">
|
|
<label for="tts-language" style="display: inline-block; width: 130px">Default language:</label>
|
|
<input type="text" id="tts-language" placeholder="EN (translate) or en-US (google-cloud)" style="width: 280px"/>
|
|
</div>
|
|
<div style="margin-bottom: 8px">
|
|
<label for="tts-voice" style="display: inline-block; width: 130px">Default voice:</label>
|
|
<input type="text" id="tts-voice" placeholder="e.g. en-US-Neural2-C (google-cloud only)" style="width: 280px"/>
|
|
</div>
|
|
<div style="margin-bottom: 8px">
|
|
<label for="tts-volume" style="display: inline-block; width: 130px">Default volume:</label>
|
|
<input type="number" id="tts-volume" min="0" max="100" placeholder="0 = keep current" style="width: 100px"/>
|
|
</div>
|
|
<div id="tts-config-status" style="margin-top: 5px; font-size: 0.9em">Checking configuration...</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
<div style="margin-bottom: 20px">
|
|
<strong>Logging:</strong>
|
|
<div style="margin-top: 5px">
|
|
<label style="display: block; margin-bottom: 5px"><input type="checkbox" id="logging-redact"
|
|
onchange="updateLoggingSettings()"/> Redact
|
|
Sensitive Headers</label>
|
|
<label style="display: block; margin-bottom: 5px"><input type="checkbox" id="logging-log-body"
|
|
onchange="updateLoggingSettings()"/> Log
|
|
Bodies</label>
|
|
<label style="display: block; margin-bottom: 5px"><input type="checkbox" id="logging-record"
|
|
onchange="updateLoggingSettings()"/> Record
|
|
Interactions<span style="font-size: 0.85em; color: #666; margin-left: 5px;">(View in <strong>5. Interactions</strong> tab)</span></label>
|
|
<div style="margin-left: 20px; margin-top: 10px">
|
|
<label for="internal-paths">Internal Paths (skip recording for these patterns):</label><br/>
|
|
<textarea
|
|
id="internal-paths"
|
|
rows="2"
|
|
style="width: 100%; max-width: 600px; margin-top: 5px; font-family: monospace;"
|
|
placeholder="/setup/* /web/*"
|
|
></textarea>
|
|
<div
|
|
style="
|
|
font-size: 0.8em;
|
|
color: #666;
|
|
margin-top: 2px;
|
|
"
|
|
>
|
|
Requests matching these patterns will be
|
|
excluded from recording. Use one pattern per
|
|
line.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 20px">
|
|
<strong>Root CA Certificate:</strong>
|
|
<div style="margin-top: 8px; font-size: 0.9em; color: #555; max-width: 600px">
|
|
Import this certificate into your OS or browser trust store to
|
|
trust HTTPS connections to this AfterTouch server from other
|
|
clients (e.g. curl, Python scripts, browsers).
|
|
</div>
|
|
<div style="margin-top: 8px">
|
|
<a href="/setup/ca.crt" download="soundtouch-ca.crt">
|
|
<button type="button">Download CA Certificate</button>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 20px">
|
|
<button onclick="updateSettings()">Save Settings</button>
|
|
<span
|
|
id="settings-status"
|
|
style="margin-left: 10px; font-size: 0.9em"
|
|
></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 2: Devices -->
|
|
<div id="tab-devices" class="tab-content">
|
|
<div
|
|
style="
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
"
|
|
>
|
|
<h2>
|
|
Known Devices
|
|
<span
|
|
id="discovery-indicator"
|
|
style="
|
|
font-size: 0.5em;
|
|
vertical-align: middle;
|
|
display: none;
|
|
"
|
|
>🔍 Scanning...</span
|
|
>
|
|
</h2>
|
|
<div
|
|
id="spotify-status-header"
|
|
style="
|
|
background: #f0f0f0;
|
|
padding: 5px 15px;
|
|
border-radius: 20px;
|
|
font-size: 0.9em;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
"
|
|
>
|
|
Spotify:
|
|
<span
|
|
id="spotify-account-name"
|
|
style="font-weight: bold"
|
|
>Not Linked</span
|
|
>
|
|
<button
|
|
id="link-spotify-btn"
|
|
onclick="linkSpotify()"
|
|
style="
|
|
font-size: 0.8em;
|
|
padding: 2px 8px;
|
|
background: #1db954;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
"
|
|
>
|
|
Link Account
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="device-list">Loading devices...</div>
|
|
<div style="margin-top: 20px">
|
|
<button onclick="triggerDiscovery()">Scan Again</button>
|
|
<input
|
|
type="text"
|
|
id="add-manual-ip"
|
|
placeholder="Manual IP (e.g. 192.0.2.100)"
|
|
style="margin-left: 20px; padding: 4px"
|
|
/>
|
|
<button onclick="addManualDevice()">Add Device</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 3: Data Sync -->
|
|
<div id="tab-sync" class="tab-content">
|
|
<h2>Initial Data Sync</h2>
|
|
<p>
|
|
Before migrating, fetch your presets, recents, and
|
|
configured sources from the device to ensure they are
|
|
available locally.
|
|
</p>
|
|
<div class="device-selection">
|
|
<label for="sync-device-list">Device:</label>
|
|
<select id="sync-device-list">
|
|
<option value="">-- Select a device --</option>
|
|
</select>
|
|
<button id="sync-now-btn">Start Sync</button>
|
|
</div>
|
|
<div id="sync-status" class="status"></div>
|
|
<div id="sync-results" style="margin-top: 20px; display: none">
|
|
<h3>Sync Results</h3>
|
|
<div
|
|
id="sync-log"
|
|
style="
|
|
font-family: monospace;
|
|
background: #f4f4f4;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 4: Migration -->
|
|
<div id="tab-migration" class="tab-content">
|
|
<h2>Device Migration</h2>
|
|
<div class="device-selection">
|
|
<label for="migration-device-list">Device:</label>
|
|
<select
|
|
id="migration-device-list"
|
|
onchange="showSummary(this.value)"
|
|
>
|
|
<option value="">-- Select a device --</option>
|
|
</select>
|
|
<button
|
|
type="button"
|
|
id="migration-refresh-btn"
|
|
onclick="refreshSummary()"
|
|
title="Reload summary for the selected device"
|
|
aria-label="Reload summary"
|
|
style="margin-left: 6px; padding: 2px 8px; font-size: 1em; line-height: 1; cursor: pointer"
|
|
>↻</button>
|
|
</div>
|
|
|
|
<div id="status" class="status"></div>
|
|
|
|
<div
|
|
id="command-output-box"
|
|
class="summary-box"
|
|
style="display: none; background-color: #f0f0f0"
|
|
>
|
|
<h3>Command Output</h3>
|
|
<div
|
|
id="command-output"
|
|
style="
|
|
font-family: monospace;
|
|
white-space: pre-wrap;
|
|
font-size: 0.85em;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
padding: 10px;
|
|
border: 1px solid #ccc;
|
|
background: #fff;
|
|
"
|
|
></div>
|
|
</div>
|
|
|
|
<div
|
|
id="migration-summary"
|
|
class="summary-box"
|
|
style="display: none"
|
|
>
|
|
<h3>
|
|
Migration Summary for
|
|
<span id="summary-device-display"></span>
|
|
</h3>
|
|
<input type="hidden" id="summary-device-id"/>
|
|
<p>Migration Status: <span id="migration-status"></span></p>
|
|
|
|
<div
|
|
id="migration-state-card"
|
|
style="margin: 10px 0 16px 0; padding: 12px; border: 1px solid #ddd; background: #fafafa; border-radius: 4px"
|
|
>
|
|
<div style="margin-bottom: 12px">
|
|
<h4 style="margin: 0 0 6px 0; font-size: 0.95em">Transports</h4>
|
|
<div style="display: flex; gap: 24px; flex-wrap: wrap; padding-left: 4px">
|
|
<div>
|
|
<strong>SSH:</strong> <span id="state-ssh"></span>
|
|
</div>
|
|
<div>
|
|
<strong>Telnet (Port 17000):</strong> <span id="state-telnet"></span>
|
|
<span id="state-telnet-banner" style="font-size: 0.85em; color: #666; margin-left: 4px"></span>
|
|
</div>
|
|
</div>
|
|
<div
|
|
id="state-telnet-error"
|
|
style="display: none; margin-top: 6px; padding: 4px 8px; background: #fff3e0; border-left: 3px solid #ef6c00; font-size: 0.85em; color: #5d4037"
|
|
></div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 12px">
|
|
<h4 style="margin: 0 0 6px 0; font-size: 0.95em">Migration State</h4>
|
|
<table style="width: 100%; border-collapse: collapse">
|
|
<tbody>
|
|
<tr style="border-top: 1px solid #eee">
|
|
<td style="padding: 6px 8px; width: 170px; vertical-align: top; color: #555">URL Configuration</td>
|
|
<td id="state-url" style="padding: 6px 8px; vertical-align: top"></td>
|
|
</tr>
|
|
<tr style="border-top: 1px solid #eee">
|
|
<td style="padding: 6px 8px; width: 170px; vertical-align: top; color: #555">DNS Interception</td>
|
|
<td id="state-dns" style="padding: 6px 8px; vertical-align: top"></td>
|
|
</tr>
|
|
<tr style="border-top: 1px solid #eee">
|
|
<td style="padding: 6px 8px; width: 170px; vertical-align: top; color: #555">CA / TLS</td>
|
|
<td id="state-ca" style="padding: 6px 8px; vertical-align: top">
|
|
<span id="state-ca-line"></span>
|
|
<span style="margin-left: 12px; white-space: nowrap">
|
|
<button
|
|
id="trust-ca-btn"
|
|
type="button"
|
|
style="display: none; background-color: #607d8b; color: white; border: none; padding: 2px 8px; font-size: 0.85em"
|
|
>Trust CA Now</button>
|
|
<a
|
|
href="/setup/ca.crt"
|
|
download="soundtouch-ca.crt"
|
|
style="margin-left: 6px; font-size: 0.85em"
|
|
title="Download CA cert to import into other clients"
|
|
>Download CA cert</a>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 style="margin: 0 0 6px 0; font-size: 0.95em">Preconditions</h4>
|
|
<table style="width: 100%; border-collapse: collapse">
|
|
<tbody>
|
|
<tr style="border-top: 1px solid #eee">
|
|
<td style="padding: 4px 8px; width: 170px; color: #555" title="The remote_services file controls whether SSH is available after reboot">SSH (remote_services)</td>
|
|
<td id="state-remote-services-cell" style="padding: 4px 8px"></td>
|
|
</tr>
|
|
<tr style="border-top: 1px solid #eee">
|
|
<td style="padding: 4px 8px; color: #555">Account paired</td>
|
|
<td id="state-paired" style="padding: 4px 8px"></td>
|
|
</tr>
|
|
<tr style="border-top: 1px solid #eee">
|
|
<td style="padding: 4px 8px; color: #555">XML config backup</td>
|
|
<td id="state-backup" style="padding: 4px 8px"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pre-flight panel: appears when the user clicks Apply,
|
|
runs the configured checks live, then auto-proceeds on
|
|
success or surfaces failures with override buttons. -->
|
|
<div
|
|
id="apply-preflight-panel"
|
|
style="display: none; margin: 12px 0; padding: 12px; border: 1px solid #2196f3; background: #e3f2fd; border-radius: 4px"
|
|
>
|
|
<h4 style="margin: 0 0 8px 0">Pre-flight checks</h4>
|
|
<ul
|
|
id="apply-preflight-list"
|
|
style="list-style: none; padding-left: 0; margin: 0; font-family: monospace; font-size: 0.9em"
|
|
></ul>
|
|
<div id="apply-preflight-summary" style="margin-top: 8px; font-weight: bold"></div>
|
|
<div id="apply-preflight-actions" style="margin-top: 10px"></div>
|
|
</div>
|
|
|
|
<div
|
|
id="preflight-warnings"
|
|
style="display: none; margin: 10px 0; padding: 8px 12px; background-color: #fff8e1; border-left: 4px solid #ffb300; font-size: 0.9em"
|
|
>
|
|
<strong>Cross-check warnings:</strong>
|
|
<ul id="preflight-warnings-list" style="margin: 4px 0 0 1em; padding: 0"></ul>
|
|
</div>
|
|
|
|
<div
|
|
id="connection-test"
|
|
style="
|
|
margin: 15px 0;
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
background-color: #eefbff;
|
|
"
|
|
>
|
|
<strong>HTTPS Connection Test:</strong><br/>
|
|
<span style="font-size: 0.85em; color: #555"
|
|
>Verify the device can reach the server over
|
|
HTTPS.</span
|
|
>
|
|
<div style="margin-top: 10px">
|
|
URL: <code id="test-url"></code>
|
|
</div>
|
|
<div style="margin-top: 10px">
|
|
<button
|
|
id="test-connection-explicit-btn"
|
|
style="
|
|
background-color: #607d8b;
|
|
color: white;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
font-size: 0.9em;
|
|
"
|
|
>
|
|
Test with Explicit CA.crt
|
|
</button>
|
|
<button
|
|
id="test-connection-trusted-btn"
|
|
style="
|
|
background-color: #607d8b;
|
|
color: white;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
font-size: 0.9em;
|
|
"
|
|
>
|
|
Test with Shared Trust Store
|
|
</button>
|
|
</div>
|
|
<div
|
|
id="test-result"
|
|
style="
|
|
margin-top: 10px;
|
|
display: none;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
white-space: pre-wrap;
|
|
font-size: 0.85em;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
"
|
|
></div>
|
|
</div>
|
|
|
|
<div
|
|
id="dns-redirection-test"
|
|
style="
|
|
margin: 15px 0;
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
background-color: #e6ffed;
|
|
display: none;
|
|
"
|
|
>
|
|
<strong>Preliminary DNS Test:</strong><br/>
|
|
<span style="font-size: 0.85em; color: #555"
|
|
>Verify the device can resolve domains via the
|
|
AfterTouch DNS server.</span
|
|
>
|
|
<div style="margin-top: 10px">
|
|
Domain: <code>aftertouch.test</code>
|
|
</div>
|
|
<div style="margin-top: 10px">
|
|
<button
|
|
id="test-dns-btn"
|
|
style="
|
|
background-color: #28a745;
|
|
color: white;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
font-size: 0.9em;
|
|
"
|
|
>
|
|
Test DNS Redirection
|
|
</button>
|
|
</div>
|
|
<div
|
|
id="dns-test-result"
|
|
style="
|
|
margin-top: 10px;
|
|
display: none;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
white-space: pre-wrap;
|
|
font-size: 0.85em;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
"
|
|
></div>
|
|
</div>
|
|
|
|
<div
|
|
id="migration-plan-card"
|
|
style="margin: 16px 0; padding: 12px; border: 1px solid #ddd; background: #fafafa; border-radius: 4px"
|
|
>
|
|
<h3 style="margin-top: 0">Plan</h3>
|
|
|
|
<div style="margin-bottom: 14px">
|
|
<label for="plan-target-url" style="font-weight: bold">Target service URL:</label>
|
|
<div style="margin-top: 4px">
|
|
<input
|
|
type="text"
|
|
id="plan-target-url"
|
|
oninput="onPlanTargetURLChange()"
|
|
style="width: 320px; font-family: monospace"
|
|
placeholder="http://192.168.x.x:8000"
|
|
/>
|
|
<button
|
|
type="button"
|
|
id="plan-save-default-btn"
|
|
onclick="saveTargetURLAsDefault()"
|
|
style="margin-left: 6px"
|
|
>Save as default</button>
|
|
</div>
|
|
<div id="plan-target-saved" style="font-size: 0.85em; color: #666; margin-top: 4px"></div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 14px">
|
|
<h4 style="margin: 0 0 6px 0; font-size: 0.95em">Capabilities</h4>
|
|
<div style="font-size: 0.9em; line-height: 1.7">
|
|
<div>This speaker exposes: <span id="plan-detected"></span></div>
|
|
<div>AfterTouch can offer: <span id="plan-possible"></span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 14px">
|
|
<h4 style="margin: 0 0 6px 0; font-size: 0.95em">Service URLs</h4>
|
|
<p style="margin: 0 0 8px 0; font-size: 0.85em; color: #555">
|
|
Pre-filled from the target URL above. Edit any field for advanced setups (e.g. soundcork users
|
|
append <code>/marge</code> to <code>margeServerUrl</code>). These overrides apply to both XML
|
|
and Telnet migrations.
|
|
</p>
|
|
<table style="width: 100%; border-collapse: collapse">
|
|
<thead>
|
|
<tr>
|
|
<th style="text-align: left; padding: 4px 6px; font-size: 0.85em">Field</th>
|
|
<th style="text-align: left; padding: 4px 6px; font-size: 0.85em">Current on Device</th>
|
|
<th style="text-align: left; padding: 4px 6px; font-size: 0.85em">Target URL</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td style="padding: 4px 6px; font-family: monospace; font-size: 0.85em">margeServerUrl</td>
|
|
<td id="plan-current-marge" style="padding: 4px 6px; font-family: monospace; font-size: 0.8em; color: #555">—</td>
|
|
<td style="padding: 4px 6px">
|
|
<input
|
|
type="text"
|
|
id="plan-marge-url"
|
|
oninput="validatePlanURLs()"
|
|
style="width: 100%; font-family: monospace; font-size: 0.85em; box-sizing: border-box"
|
|
/>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 4px 6px; font-family: monospace; font-size: 0.85em">statsServerUrl</td>
|
|
<td id="plan-current-stats" style="padding: 4px 6px; font-family: monospace; font-size: 0.8em; color: #555">—</td>
|
|
<td style="padding: 4px 6px">
|
|
<input
|
|
type="text"
|
|
id="plan-stats-url"
|
|
oninput="validatePlanURLs()"
|
|
style="width: 100%; font-family: monospace; font-size: 0.85em; box-sizing: border-box"
|
|
/>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 4px 6px; font-family: monospace; font-size: 0.85em">swUpdateUrl</td>
|
|
<td id="plan-current-sw_update" style="padding: 4px 6px; font-family: monospace; font-size: 0.8em; color: #555">—</td>
|
|
<td style="padding: 4px 6px">
|
|
<input
|
|
type="text"
|
|
id="plan-sw_update-url"
|
|
oninput="validatePlanURLs()"
|
|
style="width: 100%; font-family: monospace; font-size: 0.85em; box-sizing: border-box"
|
|
/>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 4px 6px; font-family: monospace; font-size: 0.85em">bmxRegistryUrl</td>
|
|
<td id="plan-current-bmx" style="padding: 4px 6px; font-family: monospace; font-size: 0.8em; color: #555">—</td>
|
|
<td style="padding: 4px 6px">
|
|
<input
|
|
type="text"
|
|
id="plan-bmx-url"
|
|
oninput="validatePlanURLs()"
|
|
style="width: 100%; font-family: monospace; font-size: 0.85em; box-sizing: border-box"
|
|
/>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div style="margin-top: 6px; font-size: 0.85em">
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
id="plan-soundcork-mode"
|
|
onchange="toggleSoundcorkMode()"
|
|
/>
|
|
Soundcork mode (append <code>/marge</code> to <code>margeServerUrl</code>)
|
|
</label>
|
|
<button
|
|
type="button"
|
|
onclick="resetPlanURLsToDefaults()"
|
|
style="margin-left: 16px; font-size: 0.85em"
|
|
>Reset to defaults</button>
|
|
</div>
|
|
<div
|
|
id="plan-url-validation"
|
|
style="display: none; margin-top: 8px; padding: 6px 10px; background: #ffebee; border-left: 3px solid #c62828; font-size: 0.85em; color: #c62828"
|
|
></div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 14px">
|
|
<h4 style="margin: 0 0 6px 0; font-size: 0.95em">Account pairing</h4>
|
|
<p id="plan-pair-current" style="margin: 0 0 6px 0; font-size: 0.85em; color: #555">—</p>
|
|
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap">
|
|
<label for="plan-pair-id">Account ID:</label>
|
|
<input
|
|
type="text"
|
|
id="plan-pair-id"
|
|
maxlength="7"
|
|
pattern="[0-9]{7}"
|
|
placeholder="1234567"
|
|
style="font-family: monospace; width: 8em"
|
|
oninput="onPlanPairIDChange()"
|
|
/>
|
|
<button type="button" onclick="generatePlanAccountID()" style="font-size: 0.85em">Generate</button>
|
|
<select id="plan-pair-existing" onchange="onPlanPairPick()" style="font-size: 0.85em">
|
|
<option value="">— pick from datastore —</option>
|
|
</select>
|
|
</div>
|
|
<div id="plan-pair-status" style="margin-top: 4px; font-size: 0.85em; color: #666"></div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 style="margin: 0 0 6px 0; font-size: 0.95em">Suggested plan</h4>
|
|
<div
|
|
id="plan-suggestion"
|
|
style="border: 1px solid #c8e6c9; background: #f1f8e9; padding: 10px 12px; border-radius: 3px"
|
|
>
|
|
<div id="plan-suggestion-summary" style="font-weight: bold; margin-bottom: 4px"></div>
|
|
<ul id="plan-suggestion-steps" style="margin: 4px 0 8px 1.2em; padding: 0; font-size: 0.9em"></ul>
|
|
<button
|
|
type="button"
|
|
id="plan-preflight-btn"
|
|
onclick="preflightSuggestedPlan()"
|
|
title="Run the same pre-flight checks Apply runs, without proceeding to migrate"
|
|
style="font-size: 0.95em; margin-right: 6px"
|
|
>Pre-flight</button>
|
|
<button
|
|
type="button"
|
|
id="plan-apply-btn"
|
|
onclick="applySuggestedPlan()"
|
|
style="font-size: 0.95em"
|
|
>Apply Suggested Plan</button>
|
|
<span id="plan-apply-status" style="margin-left: 10px; font-size: 0.85em"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<details style="margin: 16px 0">
|
|
<summary style="cursor: pointer; font-weight: bold">Customize this migration</summary>
|
|
|
|
<div
|
|
id="customize-form"
|
|
style="margin: 15px 0; padding: 12px; border: 1px solid #ddd; background-color: #f9f9f9; border-radius: 4px"
|
|
>
|
|
<p style="margin: 0 0 12px 0; font-size: 0.9em; color: #555">
|
|
Pick any combination of the three axes — the wizard runs
|
|
the matching backend operations in order. Disabled options
|
|
require a transport this speaker doesn't expose.
|
|
</p>
|
|
|
|
<fieldset style="margin: 0 0 10px 0; padding: 8px 12px; border: 1px solid #ddd">
|
|
<legend style="padding: 0 6px; font-weight: bold">URL flip transport</legend>
|
|
<label style="display: block; margin: 2px 0">
|
|
<input type="radio" name="customize-url-flip" value="xml" checked
|
|
onchange="onCustomizeChange()"/>
|
|
XML over SSH
|
|
<span class="customize-hint" data-axis="xml" style="margin-left: 6px; font-size: 0.85em; color: #c62828"></span>
|
|
</label>
|
|
<label style="display: block; margin: 2px 0">
|
|
<input type="radio" name="customize-url-flip" value="telnet"
|
|
onchange="onCustomizeChange()"/>
|
|
Telnet (Port 17000)
|
|
<span class="customize-hint" data-axis="telnet" style="margin-left: 6px; font-size: 0.85em; color: #c62828"></span>
|
|
</label>
|
|
<label style="display: block; margin: 2px 0">
|
|
<input type="radio" name="customize-url-flip" value="none"
|
|
onchange="onCustomizeChange()"/>
|
|
Skip — leave URLs at the Bose cloud (DNS interception will redirect them instead)
|
|
</label>
|
|
</fieldset>
|
|
|
|
<fieldset style="margin: 0 0 10px 0; padding: 8px 12px; border: 1px solid #ddd">
|
|
<legend style="padding: 0 6px; font-weight: bold">DNS interception</legend>
|
|
<label style="display: block; margin: 2px 0">
|
|
<input type="radio" name="customize-dns" value="none" checked
|
|
onchange="onCustomizeChange()"/>
|
|
None
|
|
</label>
|
|
<label style="display: block; margin: 2px 0">
|
|
<input type="radio" name="customize-dns" value="resolv"
|
|
onchange="onCustomizeChange()"/>
|
|
<code>/etc/resolv.conf</code> hook (also installs the local CA — needed when URLs stay at <code>https://*.bose.com</code>)
|
|
<span class="customize-hint" data-axis="resolv" style="margin-left: 6px; font-size: 0.85em; color: #c62828"></span>
|
|
</label>
|
|
</fieldset>
|
|
|
|
<fieldset style="margin: 0 0 10px 0; padding: 8px 12px; border: 1px solid #ddd">
|
|
<legend style="padding: 0 6px; font-weight: bold">Local CA install</legend>
|
|
<label style="display: block; margin: 2px 0">
|
|
<input type="checkbox" id="customize-ca-install"
|
|
onchange="onCustomizeChange()"/>
|
|
Install local root CA on the device via SSH (only needed when targeting <code>https://</code>)
|
|
<span class="customize-hint" data-axis="ca" style="margin-left: 6px; font-size: 0.85em; color: #c62828"></span>
|
|
</label>
|
|
</fieldset>
|
|
|
|
<div
|
|
id="customize-validation"
|
|
style="display: none; margin: 8px 0; padding: 6px 10px; background: #ffebee; border-left: 3px solid #c62828; font-size: 0.85em; color: #c62828"
|
|
></div>
|
|
|
|
<div style="margin-top: 12px">
|
|
<button
|
|
type="button"
|
|
id="customize-preflight-btn"
|
|
onclick="preflightCustomPlan()"
|
|
title="Run the same pre-flight checks Apply runs, without proceeding to migrate"
|
|
style="padding: 8px 14px; font-size: 0.95em; margin-right: 6px"
|
|
>Pre-flight</button>
|
|
<button
|
|
type="button"
|
|
id="customize-apply-btn"
|
|
onclick="applyCustomPlan()"
|
|
style="background-color: #4caf50; color: white; border: none; padding: 8px 14px; font-size: 0.95em"
|
|
>Apply Custom Plan</button>
|
|
<span id="customize-apply-status" style="margin-left: 10px; font-size: 0.9em"></span>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div
|
|
id="telnet-method-pane"
|
|
style="display: none; margin-bottom: 20px; padding: 10px; border: 1px solid #ddd; background-color: #f0f7ff;"
|
|
>
|
|
<h4 style="margin-top: 0">Telnet Migration (Port 17000)</h4>
|
|
<p style="margin: 5px 0">
|
|
Drives the speaker's diagnostic shell over TCP/17000.
|
|
Requires no SSH access. Works on most ST 10/20/300 and
|
|
Wave III/IV firmware (27.0.6.x).
|
|
</p>
|
|
<p style="margin: 5px 0; font-size: 0.9em; color: #555">
|
|
Limitation: HTTP-only redirection — telnet has no way to
|
|
install a custom CA. If you need end-to-end TLS, use the
|
|
XML or DNS method instead.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- XML diff pair: Current Config | Planned Config. Shown
|
|
when URL flip = xml in the Customize form. -->
|
|
<div class="diff-container" id="xml-diff-row" style="display: none">
|
|
<div id="xml-diff-pane" class="diff-pane">
|
|
<span class="config-header"
|
|
>Current Config (on Speaker)</span
|
|
>
|
|
<pre id="current-config"></pre>
|
|
</div>
|
|
<div id="planned-xml-pane" class="diff-pane">
|
|
<span class="config-header"
|
|
>Planned Config (AfterTouch)</span
|
|
>
|
|
<pre id="planned-config"></pre>
|
|
<div
|
|
id="resolve-ip-error"
|
|
style="display: none; margin-top: 10px; padding: 10px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; color: #856404;"
|
|
>
|
|
⚠️ <strong>Hostname resolution warning:</strong>
|
|
<span id="resolve-ip-error-msg"></span>
|
|
<br/>
|
|
The planned IP shown above may be incorrect.
|
|
Migration methods that write IPs to the device
|
|
(hosts, resolv.conf) will refuse to proceed until
|
|
the hostname can be resolved from the device itself.
|
|
<a href="https://github.com/gesellix/bose-soundtouch/blob/main/docs/guides/TROUBLESHOOTING.md#hostname-resolution" target="_blank" style="color: #856404;">Learn more →</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Resolv diff pair: Current /etc/resolv.conf | Planned hook.
|
|
Shown when DNS = resolv in the Customize form. -->
|
|
<div class="diff-container" id="resolv-diff-row" style="display: none; margin-top: 12px">
|
|
<div id="current-resolv-pane" class="diff-pane">
|
|
<span class="config-header"
|
|
>Current /etc/resolv.conf</span
|
|
>
|
|
<pre id="current-resolv-content"></pre>
|
|
</div>
|
|
<div id="planned-resolv-pane" class="diff-pane">
|
|
<span class="config-header"
|
|
>Planned /etc/resolv.conf Hook</span
|
|
>
|
|
<pre id="planned-resolv"></pre>
|
|
<div
|
|
id="resolv-note"
|
|
style="
|
|
margin-top: 10px;
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
"
|
|
>
|
|
<strong>Note:</strong> This method injects a
|
|
persistent DNS priority hook into the DHCP logic
|
|
(<code>/etc/udhcpc.d/50default</code>). It
|
|
preserves your router's search domain and
|
|
secondary DNS servers. It also injects the Local
|
|
Root CA.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top: 15px">
|
|
<button
|
|
id="revert-migrate-btn"
|
|
style="
|
|
background-color: #ff9800;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
display: none;
|
|
"
|
|
>
|
|
Revert to Defaults
|
|
</button>
|
|
<button
|
|
id="reboot-speaker-btn"
|
|
style="
|
|
background-color: #607d8b;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
"
|
|
>
|
|
Reboot Speaker
|
|
</button>
|
|
<button
|
|
id="ensure-remote-btn"
|
|
style="
|
|
background-color: #2196f3;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
"
|
|
>
|
|
Enable SSH (Persist remote_services)
|
|
</button>
|
|
<button
|
|
id="remove-remote-btn"
|
|
title="Removes the remote_services file — SSH will be disabled after the next reboot"
|
|
style="
|
|
background-color: #f44336;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
"
|
|
>
|
|
Disable SSH (Remove remote_services)
|
|
</button>
|
|
<button
|
|
onclick="
|
|
document.getElementById(
|
|
'migration-summary',
|
|
).style.display = 'none'
|
|
"
|
|
style="padding: 10px 20px"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 5: Interactions & Events -->
|
|
<div id="tab-interactions" class="tab-content">
|
|
<h2>Recorded Interactions & Device Events</h2>
|
|
<p>
|
|
Analysis of traffic handled by this service (self), proxied
|
|
to Bose (upstream), and internal device events (telemetry).
|
|
</p>
|
|
|
|
<div id="interaction-stats-container" class="summary-box">
|
|
<div
|
|
style="
|
|
display: flex;
|
|
gap: 20px;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
"
|
|
>
|
|
<p style="margin: 0">
|
|
Total Requests:
|
|
<strong id="total-requests">0</strong>
|
|
</p>
|
|
<button onclick="fetchInteractionStats()">
|
|
Refresh Stats
|
|
</button>
|
|
<div style="margin-left: 10px">
|
|
<button onclick="showDeviceEvents()">
|
|
View App/Device Events
|
|
</button>
|
|
</div>
|
|
<div style="margin-left: auto; text-align: right">
|
|
<button
|
|
onclick="cleanupSessions()"
|
|
class="btn-danger"
|
|
>
|
|
Cleanup old sessions
|
|
</button>
|
|
<div
|
|
style="
|
|
font-size: 0.75em;
|
|
color: #666;
|
|
margin-top: 3px;
|
|
"
|
|
>
|
|
Keeps only the 10 most recent sessions
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="display: flex; gap: 20px">
|
|
<div
|
|
style="
|
|
flex: 1;
|
|
border-right: 1px solid #eee;
|
|
padding-right: 20px;
|
|
"
|
|
>
|
|
<h3>By Service</h3>
|
|
<ul id="stats-by-service" class="stats-list"></ul>
|
|
</div>
|
|
<div style="flex: 2">
|
|
<h3>Sessions</h3>
|
|
<div
|
|
id="stats-by-session-container"
|
|
style="
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
border: 1px solid #eee;
|
|
padding: 5px;
|
|
border-radius: 4px;
|
|
"
|
|
>
|
|
<ul
|
|
id="stats-by-session"
|
|
class="stats-list"
|
|
></ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
id="browse-recordings"
|
|
class="summary-box"
|
|
style="margin-top: 20px"
|
|
>
|
|
<div
|
|
style="
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
"
|
|
>
|
|
<h3 style="margin: 0">Browse Recordings</h3>
|
|
</div>
|
|
<div
|
|
style="
|
|
margin-bottom: 15px;
|
|
display: flex;
|
|
gap: 15px;
|
|
align-items: center;
|
|
background: #f9f9f9;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
"
|
|
>
|
|
<div>
|
|
<label for="filter-session">Session:</label>
|
|
<select
|
|
id="filter-session"
|
|
onchange="fetchInteractions()"
|
|
>
|
|
<option value="">All Sessions</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="filter-category">Category:</label>
|
|
<select
|
|
id="filter-category"
|
|
onchange="fetchInteractions()"
|
|
>
|
|
<option value="">All Categories</option>
|
|
<option value="self">Self (Emulated)</option>
|
|
<option value="upstream">
|
|
Upstream (Bose)
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="filter-since"
|
|
>Since (YYYY-MM-DD HH:mm:ss):</label
|
|
>
|
|
<input
|
|
type="text"
|
|
id="filter-since"
|
|
placeholder="e.g. 2026-02-15 15:00:00"
|
|
size="25"
|
|
onchange="fetchInteractions()"
|
|
/>
|
|
</div>
|
|
<button onclick="fetchInteractions()">
|
|
Apply Filters
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
id="interactions-list-container"
|
|
style="max-height: 400px; overflow-y: auto"
|
|
>
|
|
<table style="width: 100%; border-collapse: collapse">
|
|
<thead>
|
|
<tr
|
|
style="
|
|
text-align: left;
|
|
border-bottom: 2px solid #eee;
|
|
"
|
|
>
|
|
<th style="padding: 8px">#</th>
|
|
<th style="padding: 8px">Time</th>
|
|
<th style="padding: 8px">Method</th>
|
|
<th style="padding: 8px">Path</th>
|
|
<th style="padding: 8px">Status</th>
|
|
<th style="padding: 8px">Category</th>
|
|
<th style="padding: 8px">Event Details</th>
|
|
<th style="padding: 8px">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="interactions-list">
|
|
<tr>
|
|
<td
|
|
colspan="8"
|
|
style="
|
|
padding: 20px;
|
|
text-align: center;
|
|
color: #666;
|
|
"
|
|
>
|
|
No interactions found.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
id="dns-discoveries"
|
|
class="summary-box"
|
|
style="margin-top: 20px"
|
|
>
|
|
<div
|
|
style="
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
"
|
|
>
|
|
<h3 style="margin: 0">DNS Discoveries</h3>
|
|
<div style="display: flex; gap: 10px">
|
|
<button
|
|
onclick="downloadDNSDiscoveries()"
|
|
class="btn-info"
|
|
>
|
|
Download JSON
|
|
</button>
|
|
<button
|
|
onclick="clearDNSDiscoveries()"
|
|
class="btn-danger"
|
|
>
|
|
Clear DNS Logs
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p style="font-size: 0.85em; color: #666">
|
|
Hosts discovered via the AfterTouch DNS server. "Self"
|
|
means the domain was intercepted and redirected to this
|
|
service.
|
|
</p>
|
|
<div
|
|
id="dns-discoveries-list-container"
|
|
style="max-height: 400px; overflow-y: auto"
|
|
>
|
|
<table style="width: 100%; border-collapse: collapse">
|
|
<thead>
|
|
<tr
|
|
style="
|
|
text-align: left;
|
|
border-bottom: 2px solid #eee;
|
|
"
|
|
>
|
|
<th style="padding: 8px">Hostname</th>
|
|
<th style="padding: 8px">Last Seen</th>
|
|
<th
|
|
style="padding: 8px; text-align: center"
|
|
>
|
|
Queries
|
|
</th>
|
|
<th
|
|
style="padding: 8px; text-align: center"
|
|
>
|
|
Bose?
|
|
</th>
|
|
<th style="padding: 8px">Category</th>
|
|
<th style="padding: 8px">Last Client IP</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="dns-discoveries-list">
|
|
<tr>
|
|
<td
|
|
colspan="6"
|
|
style="
|
|
padding: 20px;
|
|
text-align: center;
|
|
color: #666;
|
|
"
|
|
>
|
|
No DNS discoveries found.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
id="interaction-viewer"
|
|
class="summary-box"
|
|
style="
|
|
margin-top: 20px;
|
|
display: none;
|
|
background: #2b2b2b;
|
|
color: #a9b7c6;
|
|
"
|
|
>
|
|
<div
|
|
style="
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
"
|
|
>
|
|
<h3 style="margin: 0; color: #fff">
|
|
Recording Viewer:
|
|
<span id="viewer-filename" style="font-weight: normal; font-size: 0.8em"></span>
|
|
</h3>
|
|
<button onclick="document.getElementById('interaction-viewer').style.display = 'none'"
|
|
style="background: #444; color: #fff; border: 1px solid #666;">
|
|
Close
|
|
</button>
|
|
</div>
|
|
<pre
|
|
id="interaction-content"
|
|
style="
|
|
white-space: pre-wrap;
|
|
font-family: 'Courier New', Courier, monospace;
|
|
font-size: 0.9em;
|
|
margin: 0;
|
|
padding: 10px;
|
|
overflow-x: auto;
|
|
max-height: 600px;
|
|
"
|
|
></pre>
|
|
</div>
|
|
|
|
<!-- Device Events Overlay -->
|
|
<div
|
|
id="device-events-overlay"
|
|
class="summary-box"
|
|
style="
|
|
margin-top: 20px;
|
|
display: none;
|
|
background: #fdfdfd;
|
|
border: 1px solid #ddd;
|
|
"
|
|
>
|
|
<div
|
|
style="
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
"
|
|
>
|
|
<h3 style="margin: 0">App & Device Events</h3>
|
|
<div>
|
|
<select id="event-device-selector" onchange="fetchDeviceEvents(this.value)">
|
|
<option value="">-- Select Device --</option>
|
|
</select>
|
|
<button onclick="document.getElementById('device-events-overlay').style.display = 'none'"
|
|
style="margin-left: 10px">Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div
|
|
id="events-list-container"
|
|
style="max-height: 400px; overflow-y: auto"
|
|
>
|
|
<table style="width: 100%; border-collapse: collapse">
|
|
<thead>
|
|
<tr
|
|
style="
|
|
text-align: left;
|
|
border-bottom: 2px solid #eee;
|
|
"
|
|
>
|
|
<th style="padding: 8px">Time</th>
|
|
<th style="padding: 8px">Type</th>
|
|
<th style="padding: 8px">Data</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="events-list">
|
|
<tr>
|
|
<td colspan="3" style="padding: 20px; text-align: center; color: #666;">Select a device to view
|
|
events.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 6: Local Account -->
|
|
<div id="tab-account" class="tab-content">
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<h2>Local Account Details</h2>
|
|
<div style="display: flex; gap: 10px; align-items: center;">
|
|
<label for="account-selector">Account:</label>
|
|
<select id="account-selector" onchange="fetchAccountDetails(this.value)">
|
|
<option value="default">Default</option>
|
|
</select>
|
|
<button onclick="fetchAccountDetails(document.getElementById('account-selector').value)">Refresh</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="account-info-container" class="summary-box">
|
|
<h3>Account Overview</h3>
|
|
<div id="account-metadata">Loading...</div>
|
|
</div>
|
|
|
|
<div id="spotify-registration-container" class="summary-box" style="margin-top: 20px;">
|
|
<h3>Spotify Integration</h3>
|
|
<p style="font-size: 0.9em; color: #555;">
|
|
Register a new Spotify source for this local account. This mimics the official SoundTouch app flow:
|
|
</p>
|
|
<ol style="font-size: 0.85em; color: #555; margin-bottom: 15px;">
|
|
<li>Exchange OAuth code for a Bose-mediated token.</li>
|
|
<li>Register the source in the local Marge cloud profile.</li>
|
|
</ol>
|
|
<button id="connect-spotify-account-btn" onclick="connectSpotifyToAccount()" style="background: #1db954; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer;">
|
|
Connect Spotify to this Account
|
|
</button>
|
|
<div id="spotify-reg-status" style="margin-top: 10px; font-size: 0.9em;"></div>
|
|
</div>
|
|
|
|
<div id="amazon-registration-container" class="summary-box" style="margin-top: 20px;">
|
|
<h3>Amazon Music Integration</h3>
|
|
<p style="font-size: 0.9em; color: #555;">
|
|
Register a new Amazon Music source for this local account. This mimics the official SoundTouch app flow:
|
|
</p>
|
|
<ol style="font-size: 0.85em; color: #555; margin-bottom: 15px;">
|
|
<li>Exchange OAuth code for an Amazon LWA token.</li>
|
|
<li>Register the source in the local Marge cloud profile.</li>
|
|
</ol>
|
|
<button id="connect-amazon-account-btn" onclick="connectAmazonToAccount()" style="background: #ff9900; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer;">
|
|
Connect Amazon Music to this Account
|
|
</button>
|
|
<div id="amazon-reg-status" style="margin-top: 10px; font-size: 0.9em;"></div>
|
|
</div>
|
|
|
|
<div id="account-devices-container">
|
|
<h3>Connected Devices</h3>
|
|
<div id="account-devices-list">Select an account to view devices.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 7: Health -->
|
|
<div id="tab-health" class="tab-content">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
|
|
<h2 style="margin: 0;">Service Health Checks</h2>
|
|
<button onclick="fetchHealth()">Refresh</button>
|
|
</div>
|
|
<p style="font-size: 0.9em; color: #555;">
|
|
Runs a set of checks against the local datastore and flags
|
|
findings that may need attention. Quick fixes are offered
|
|
for issues the service knows how to remediate.
|
|
</p>
|
|
|
|
<div style="margin: 12px 0 16px 0; padding: 10px 12px; background: #fafafa; border: 1px solid #eee; border-radius: 4px;">
|
|
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
|
|
<button onclick="downloadDiagnostic()" title="Download an encrypted diagnostic report to share with the project maintainer">Download diagnostic report</button>
|
|
<span style="font-size: 0.9em; color: #555;">Share a diagnostic snapshot with the project maintainer.</span>
|
|
</div>
|
|
<details style="font-size: 0.85em; color: #555; margin-top: 8px;">
|
|
<summary style="cursor: pointer;">What does the diagnostic report contain?</summary>
|
|
<ul style="margin: 6px 0 0 0; padding-left: 1.4em;">
|
|
<li>Health check results and current device state</li>
|
|
<li>Device XML files from the datastore (no passwords)</li>
|
|
<li>HTTP response samples from speaker endpoints</li>
|
|
<li>System files: CA bundle, DNS resolver config</li>
|
|
<li>Speaker CA bundle and kernel log (via SSH, if reachable)</li>
|
|
<li>Service log tail</li>
|
|
<li>Settings file with secrets redacted</li>
|
|
</ul>
|
|
<p style="margin: 6px 0 0 0;">The archive is encrypted with the project maintainer's public key — only they can open it.</p>
|
|
</details>
|
|
<div id="health-diagnostic-status" style="font-size: 0.85em; margin-top: 8px;"></div>
|
|
</div>
|
|
|
|
<div id="health-generated-at" style="font-size: 0.8em; color: #888; margin-bottom: 10px;"></div>
|
|
<div id="health-findings">Loading…</div>
|
|
</div>
|
|
|
|
<!-- Tab 8: Logs -->
|
|
<div id="tab-logs" class="tab-content">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
|
|
<h2 style="margin: 0;">Service Logs</h2>
|
|
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
|
<input type="text" id="logs-filter" placeholder="Filter (substring, case-insensitive)" style="padding: 4px 8px; min-width: 260px;"/>
|
|
<label style="font-size: 0.9em;">
|
|
<input type="checkbox" id="logs-follow" checked/> Follow tail
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<p style="font-size: 0.9em; color: #555;">
|
|
Live mirror of the service's stderr log. Stderr still
|
|
receives every line — this is a read-only view for
|
|
convenience.
|
|
</p>
|
|
<div id="logs-status" style="font-size: 0.8em; color: #888; margin-bottom: 6px;">Idle.</div>
|
|
<pre id="logs-view" style="background: #111; color: #ddd; padding: 10px; border-radius: 4px; max-height: 60vh; overflow-y: auto; font-size: 0.8em; line-height: 1.35; margin: 0; white-space: pre-wrap; word-break: break-all;"></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/web/js/script.js"></script>
|
|
<footer
|
|
style="margin-top: 50px; padding: 20px; border-top: 1px solid #eee; font-size: 0.8em; color: #888; text-align: center;">
|
|
<span id="version-info">AfterTouch</span>
|
|
</footer>
|
|
</body>
|
|
</html>
|