mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-24 14:47:23 +00:00
2361 lines
109 KiB
HTML
2361 lines
109 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>
|
|
<h1>
|
|
<img
|
|
src="/web/img/favicon-braille.svg"
|
|
alt="AfterTouch Logo"
|
|
class="logo"
|
|
/>AfterTouch
|
|
</h1>
|
|
<p class="subtitle">Bose SoundTouch Toolkit</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>
|
|
</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/guides/MIGRATION-SAFETY.html"
|
|
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/guides/SURVIVAL-GUIDE.html"
|
|
target="_blank"
|
|
>Cloud Shutdown Survival Guide</a
|
|
>
|
|
</li>
|
|
<li>
|
|
<a
|
|
href="https://gesellix.github.io/Bose-SoundTouch/guides/CLI-REFERENCE.html"
|
|
target="_blank"
|
|
>CLI Reference</a
|
|
>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Tab 1: Settings -->
|
|
<div id="tab-settings" class="tab-content">
|
|
<h2>System Settings</h2>
|
|
<div class="callout callout-info">
|
|
<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>.
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="target-domain">Target Domain:</label>
|
|
<input
|
|
type="text"
|
|
id="target-domain"
|
|
placeholder="http://192.168.x.x:8000"
|
|
style="width: 300px"
|
|
/>
|
|
<span class="help-text">(Standard services URL)</span>
|
|
<div
|
|
id="target-domain-resolved"
|
|
class="help-text mt-xs"
|
|
style="min-height: 1.2em"
|
|
></div>
|
|
<div
|
|
id="https-443-status"
|
|
class="help-text mt-xs"
|
|
style="min-height: 1.2em"
|
|
></div>
|
|
</div>
|
|
<div class="form-group">
|
|
<div class="config-header">Device Discovery</div>
|
|
<div class="mt-sm">
|
|
<label class="form-inline mb-sm">
|
|
<input type="checkbox" id="discovery-enabled" />
|
|
Enable Automated Discovery
|
|
</label>
|
|
<div class="ml-lg">
|
|
<label for="discovery-interval"
|
|
>Discovery Interval:</label
|
|
>
|
|
<input
|
|
type="text"
|
|
id="discovery-interval"
|
|
placeholder="5m"
|
|
style="width: 100px"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<div class="config-header">DNS Discovery</div>
|
|
<div class="mt-sm">
|
|
<label class="form-inline mb-sm">
|
|
<input type="checkbox" id="dns-enabled" /> Enable
|
|
DNS Discovery Server
|
|
</label>
|
|
<div class="ml-lg mb-sm">
|
|
<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 class="ml-lg">
|
|
<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>Spotify Integration:</strong>
|
|
<div style="margin-top: 8px; margin-left: 20px">
|
|
<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>
|
|
</div>
|
|
<div style="margin-bottom: 20px">
|
|
<strong>Amazon Music Integration:</strong>
|
|
<div style="margin-top: 8px; margin-left: 20px">
|
|
<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>
|
|
</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;
|
|
"
|
|
>
|
|
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 Persistent Remote Services
|
|
</button>
|
|
<button
|
|
id="remove-remote-btn"
|
|
style="
|
|
background-color: #f44336;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
"
|
|
>
|
|
Remove Persistent 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>
|
|
</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>
|