Files
Bose-SoundTouch/pkg/service/handlers/web/index.html
T
Tobias GesellchenandClaude Opus 4.7 102770e301 feat(web): account pairing folded into Plan card and Apply orchestrator
Pairing was previously its own post-telnet pop-up pane —
loadAccountIDSuggestions(deviceId) was called only after a successful
telnet migration, leaving the user to interact with a separate panel
and click a separate "Pair Account" button. XML migrations didn't
surface pairing at all.

The Plan card now has its own Account pairing section between Service
URLs and Suggested plan, with the same affordances (current state,
7-digit input, Generate button, datastore picker) but always
visible. The implicit intent — read by readPlanPairTarget — is:

  - empty input + currently paired      → no pairing step (current ID kept)
  - empty input + currently unpaired    → no pairing step (warning hint visible)
  - input matches summary.account_id    → no pairing step
  - input is exactly 7 digits, differs  → pair step queued at Apply
  - input is non-empty but malformed    → blocks Apply with a clear error

Both Apply orchestrators (applySuggestedPlan, applyCustomPlan) now
queue a `pairAccount(deviceId, accountId)` call when the intent says
to. It runs *after* the URL flip / DNS / CA steps so the user sees
the migration succeed before pairing — pairing is independent of
the migration target so order is purely UX. First-failure-aborts is
preserved: a pair-account error stops the rest of the sequence.

Removed:
  - #pair-account-pane HTML and all its descendants
  - loadAccountIDSuggestions / generateAccountID / pairAccount(deviceId)
    (the old pane-bound functions)
  - the "if method === telnet → loadAccountIDSuggestions" trigger in migrate()

Added:
  - renderPlanPairing(summary, deviceId) — populates the section on
    every showSummary
  - loadPlanAccountSuggestions(deviceId) — fetches /setup/account-id-
    suggestions; gracefully degrades on failure
  - onPlanPairIDChange / onPlanPairPick / generatePlanAccountID — UI
    handlers with implicit-intent status hints
  - readPlanPairTarget — orchestrator-facing intent extractor
  - pairAccount(deviceId, accountId) — POSTs and throws on failure
    (replaces the old pane-bound function with a step-friendly shape)
  - resetPlanCardForDeviceSwitch clears the pairing input on speaker
    change so the previous device's ID can't leak

Backend untouched — all the pairing endpoints (/setup/account-id-
suggestions, /setup/pair-account) and the setup.PairAccount + telnet-
fallback logic stay exactly as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:37:11 +02:00

1735 lines
78 KiB
HTML

<!doctype html>
<html>
<head>
<meta charset="UTF-8"/>
<title>AfterTouch (SoundTouch Toolkit)</title>
<link rel="icon" href="/web/img/favicon-braille.svg" type="image/svg+xml"/>
<link rel="stylesheet" href="/web/css/style.css"/>
<script src="/web/js/diff.min.js"></script>
</head>
<body>
<h1><img src="/web/img/favicon-braille.svg" alt="AfterTouch Logo" class="logo"/>AfterTouch</h1>
<p style="margin-top: -10px; font-style: italic; color: #666">
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-parity')">
6. Parity & Mirroring
</button>
<button class="tab-btn" onclick="openTab(event, 'tab-account')">
7. 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>🔌 Prerequisite: Enable SSH</strong><br/>
Migration requires SSH access. To enable it:
<ol style="margin-top: 5px; margin-bottom: 5px">
<li>
Create an empty file named
<code>remote_services</code> on a USB stick.
</li>
<li>
Insert it into the speaker's
<strong>SERVICE</strong> port and reboot the
speaker.
</li>
</ol>
<strong>Verify connection:</strong>
<ul style="margin-top: 5px; margin-bottom: 0; padding-left: 20px;">
<li>
Use the <strong>Migration</strong> tab to select
your device and verify that
<em>SSH Connection</em> shows ✅ Success.
</li>
<li>
Or manually:
<code
>ssh -oHostKeyAlgorithms=+ssh-rsa
root@&lt;SPEAKER-IP&gt;</code
>
(no password).
</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).
</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, redirect your speaker to
this local service. We recommend the
<strong>XML Configuration</strong> method as it is
surgical and easily reversible.
</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>
<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.168.1.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>
<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 Automated 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>Endpoint Mirroring:</strong>
<div style="margin-top: 5px">
<label style="display: block; margin-bottom: 5px">
<input type="checkbox" id="mirror-enabled"/> Enable
Background Mirroring to Bose Cloud
</label>
<div style="margin-left: 20px; margin-bottom: 5px">
<label style="display: block; margin-bottom: 5px">
<input type="checkbox" id="preferred-source-upstream"/>
Prefer Upstream Response for Mirrored Endpoints
</label>
<label for="mirror-endpoints">Mirror Endpoints (one per line, supports * wildcards):</label><br/>
<textarea
id="mirror-endpoints"
rows="4"
style="width: 100%; max-width: 600px; margin-top: 5px; font-family: monospace;"
placeholder="/streaming/account/*/device/*/recent&#10;/accounts/*/devices/*/presets/*"
></textarea>
<label for="skip-mirror-endpoints" style="display: block; margin-top: 10px;">Skip Mirror Endpoints (local only, one per line):</label>
<textarea
id="skip-mirror-endpoints"
rows="2"
style="width: 100%; max-width: 600px; margin-top: 5px; font-family: monospace;"
placeholder="/oauth/device/*/music/musicprovider/*/token/cs3&#10;/oauth/device/*/music/musicprovider/*/token"
></textarea>
<div
class="info-box"
style="
margin-top: 5px;
font-size: 0.85em;
padding: 10px;
"
>
<strong>Note:</strong> Mirroring sends matching
requests (including full headers) to the
official Bose servers for parity comparison. If
<em>Redact Sensitive Data</em> is enabled in
Proxy Settings, credentials will be masked in
<strong>logs and recordings</strong>, but full
headers are always sent to Bose to ensure
service compatibility.
</div>
</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>Proxy Logging:</strong>
<div style="margin-top: 5px">
<label style="display: block; margin-bottom: 5px"><input type="checkbox" id="proxy-redact"
onchange="updateProxySettings()"/> Redact
Sensitive Headers</label>
<label style="display: block; margin-bottom: 5px"><input type="checkbox" id="proxy-log-body"
onchange="updateProxySettings()"/> Log
Bodies</label>
<label style="display: block; margin-bottom: 5px"><input type="checkbox" id="proxy-record"
onchange="updateProxySettings()"/> 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/*&#10;/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.168.1.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"
>&#x21bb;</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="hosts-redirection-test"
style="
margin: 15px 0;
padding: 10px;
border: 1px solid #ddd;
background-color: #fff4e6;
display: none;
"
>
<strong>Preliminary /etc/hosts Test:</strong><br/>
<span style="font-size: 0.85em; color: #555"
>Verify the device's /etc/hosts mechanism before
full migration.</span
>
<div style="margin-top: 10px">
Domain: <code>custom-test-api.bose.fake</code>
</div>
<div style="margin-top: 10px">
<button
id="test-hosts-btn"
style="
background-color: #ff9800;
color: white;
border: none;
padding: 5px 10px;
font-size: 0.9em;
"
>
Test Hosts Redirection
</button>
</div>
<div
id="hosts-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-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-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>
<p style="margin: 5px 0; font-size: 0.9em; color: #555">
After a successful migration a <em>Pair Account</em>
panel will appear below this one — use it to associate
the speaker with an account ID before presets and
streaming work.
</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>
<option value="mirror">Mirror (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: Parity & Mirroring -->
<div id="tab-parity" class="tab-content">
<h2>Parity Analysis</h2>
<p>
Detection of discrepancies between AfterTouch local
responses and official Bose Cloud responses for mirrored
endpoints.
</p>
<div class="summary-box">
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
"
>
<h3 style="margin: 0">Parity Mismatches</h3>
<div style="display: flex; gap: 10px">
<button onclick="fetchParityMismatches()">
Refresh Mismatches
</button>
<button
onclick="clearParityMismatches()"
class="btn-danger"
>
Clear All Records
</button>
</div>
</div>
<div
id="parity-list-container"
style="max-height: 500px; 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">Method</th>
<th style="padding: 8px">Path</th>
<th style="padding: 8px">Reasons</th>
<th style="padding: 8px">Action</th>
</tr>
</thead>
<tbody id="parity-mismatches-list">
<tr>
<td
colspan="5"
style="
padding: 20px;
text-align: center;
color: #666;
"
>
Loading mismatches...
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
id="parity-diff-view"
class="summary-box"
style="display: none; margin-top: 20px"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
"
>
<h3 style="margin: 0">
Mismatch Detail:
<span id="diff-path-display"></span>
</h3>
<button onclick="document.getElementById('parity-diff-view').style.display = 'none'">Close Detail
</button>
</div>
<div
style="
margin-bottom: 15px;
padding: 10px;
background: #fff4f4;
border: 1px solid #f5c6cb;
border-radius: 4px;
color: #721c24;
"
>
<strong>Detection Reasons:</strong>
<ul
id="diff-reasons-list"
style="margin: 5px 0 0 0; padding-left: 20px"
></ul>
</div>
<div id="diff-size-warning" style="display: none; margin-bottom: 15px; padding: 10px; background: #fff3cd; border: 1px solid #ffeeba; border-radius: 4px; color: #856404;">
⚠️ <strong>Large Payload:</strong> Rich diff highlighting is disabled to prevent a browser freeze. Showing raw comparison instead.
<button id="force-rich-diff-btn" style="margin-left: 10px; font-size: 0.8em; cursor: pointer;">Show Rich Diff Anyway</button>
</div>
<div class="diff-container" style="margin-top: 15px">
<div class="diff-pane">
<span
class="config-header"
style="background: #eefbff; color: #0056b3"
>Local Response (AfterTouch)</span
>
<div
id="diff-local-meta"
style="
font-size: 0.8em;
margin-bottom: 5px;
color: #666;
"
></div>
<pre
id="diff-local-body"
></pre>
</div>
<div class="diff-pane">
<span
class="config-header"
style="background: #fff4e6; color: #856404"
>Upstream Response (Bose)</span
>
<div
id="diff-upstream-meta"
style="
font-size: 0.8em;
margin-bottom: 5px;
color: #666;
"
></div>
<pre
id="diff-upstream-body"
></pre>
</div>
</div>
</div>
</div>
<!-- Tab 7: 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>