diff --git a/pkg/service/handlers/web/js/script.js b/pkg/service/handlers/web/js/script.js
index c3b7ea8..8b8abb9 100644
--- a/pkg/service/handlers/web/js/script.js
+++ b/pkg/service/handlers/web/js/script.js
@@ -1,71 +1,71 @@
async function fetchSpotifyStatus() {
try {
- const settingsResponse = await fetch('/setup/settings');
+ const settingsResponse = await fetch("/setup/settings");
const settings = await settingsResponse.json();
- const header = document.getElementById('spotify-status-header');
+ const header = document.getElementById("spotify-status-header");
if (!settings.spotify_configured) {
- if (header) header.style.display = 'none';
+ if (header) header.style.display = "none";
return;
}
- if (header) header.style.display = 'flex';
+ if (header) header.style.display = "flex";
- const response = await fetch('/mgmt/spotify/accounts');
+ const response = await fetch("/mgmt/spotify/accounts");
if (!response.ok) return;
const data = await response.json();
- const nameEl = document.getElementById('spotify-account-name');
- const linkBtn = document.getElementById('link-spotify-btn');
+ const nameEl = document.getElementById("spotify-account-name");
+ const linkBtn = document.getElementById("link-spotify-btn");
if (data.accounts && data.accounts.length > 0) {
- header.style.background = '#e6ffed';
- header.style.border = '1px solid #28a745';
- nameEl.innerText = data.accounts[0].display_name || data.accounts[0].user_id || 'Linked';
- if (linkBtn) linkBtn.style.display = 'none';
+ header.style.background = "#e6ffed";
+ header.style.border = "1px solid #28a745";
+ nameEl.innerText = data.accounts[0].display_name || data.accounts[0].user_id || "Linked";
+ if (linkBtn) linkBtn.style.display = "none";
// Show Prime Spotify buttons on all devices
- document.querySelectorAll('.btn-spotify').forEach(btn => {
- btn.style.display = 'inline-block';
+ document.querySelectorAll(".btn-spotify").forEach((btn) => {
+ btn.style.display = "inline-block";
});
} else {
- header.style.background = '#f0f0f0';
- header.style.border = '1px solid #ccc';
- nameEl.innerText = 'Not Linked';
- if (linkBtn) linkBtn.style.display = 'inline-block';
+ header.style.background = "#f0f0f0";
+ header.style.border = "1px solid #ccc";
+ nameEl.innerText = "Not Linked";
+ if (linkBtn) linkBtn.style.display = "inline-block";
- document.querySelectorAll('.btn-spotify').forEach(btn => {
- btn.style.display = 'none';
+ document.querySelectorAll(".btn-spotify").forEach((btn) => {
+ btn.style.display = "none";
});
}
} catch (error) {
- console.error('Failed to fetch Spotify status', error);
+ console.error("Failed to fetch Spotify status", error);
}
}
function toggleInfo(id) {
const el = document.getElementById(id);
if (el) {
- el.style.display = el.style.display === 'block' ? 'none' : 'block';
+ el.style.display = el.style.display === "block" ? "none" : "block";
}
}
async function linkSpotify() {
try {
- const response = await fetch('/mgmt/spotify/init', { method: 'POST' });
+ const response = await fetch("/mgmt/spotify/init", {method: "POST"});
if (!response.ok) {
const err = await response.text();
- alert('Failed to initialize Spotify link: ' + err);
+ alert("Failed to initialize Spotify link: " + err);
return;
}
const data = await response.json();
if (data.redirectUrl) {
// Open in a new tab
- const win = window.open(data.redirectUrl, '_blank');
+ const win = window.open(data.redirectUrl, "_blank");
if (win) {
win.focus();
// Start polling for status change
const pollInterval = setInterval(async () => {
- const statusResponse = await fetch('/mgmt/spotify/accounts');
+ const statusResponse = await fetch("/mgmt/spotify/accounts");
if (statusResponse.ok) {
const statusData = await statusResponse.json();
if (statusData.accounts && statusData.accounts.length > 0) {
@@ -77,43 +77,43 @@ async function linkSpotify() {
// Stop polling after 2 minutes
setTimeout(() => clearInterval(pollInterval), 120000);
} else {
- alert('Please allow popups to link your Spotify account.');
+ alert("Please allow popups to link your Spotify account.");
}
}
} catch (error) {
- alert('Error linking Spotify: ' + error.message);
+ alert("Error linking Spotify: " + error.message);
}
}
async function primeSpotify(deviceId) {
- const btn = document.getElementById('prime-spotify-' + deviceId);
+ const btn = document.getElementById("prime-spotify-" + deviceId);
const originalText = btn.innerText;
- btn.innerText = 'Priming...';
+ btn.innerText = "Priming...";
btn.disabled = true;
try {
const response = await fetch(`/mgmt/spotify/prime?deviceId=${encodeURIComponent(deviceId)}`, {
- method: 'POST'
- });
+ method: "POST",
+ },);
if (response.ok) {
- btn.innerText = '✅ Primed';
- btn.style.background = '#28a745';
+ btn.innerText = "✅ Primed";
+ btn.style.background = "#28a745";
setTimeout(() => {
btn.innerText = originalText;
- btn.style.background = '';
+ btn.style.background = "";
btn.disabled = false;
}, 3000);
} else {
const err = await response.text();
- alert('Failed to prime Spotify: ' + err);
- btn.innerText = '❌ Failed';
+ alert("Failed to prime Spotify: " + err);
+ btn.innerText = "❌ Failed";
setTimeout(() => {
btn.innerText = originalText;
btn.disabled = false;
}, 3000);
}
} catch (error) {
- alert('Error priming Spotify: ' + error.message);
+ alert("Error priming Spotify: " + error.message);
btn.innerText = originalText;
btn.disabled = false;
}
@@ -121,162 +121,165 @@ async function primeSpotify(deviceId) {
async function fetchSettings() {
try {
- const response = await fetch('/setup/settings');
+ const response = await fetch("/setup/settings");
const settings = await response.json();
if (settings.server_url) {
- document.getElementById('target-domain').value = settings.server_url;
+ document.getElementById("target-domain").value = settings.server_url;
}
if (settings.discovery_interval) {
- document.getElementById('discovery-interval').value = settings.discovery_interval;
+ document.getElementById("discovery-interval").value = settings.discovery_interval;
}
if (settings.discovery_enabled !== undefined) {
- document.getElementById('discovery-enabled').checked = settings.discovery_enabled;
+ document.getElementById("discovery-enabled").checked = settings.discovery_enabled;
}
if (settings.dns_enabled !== undefined) {
- document.getElementById('dns-enabled').checked = settings.dns_enabled;
+ document.getElementById("dns-enabled").checked = settings.dns_enabled;
}
if (settings.dns_upstream) {
- document.getElementById('dns-upstream').value = settings.dns_upstream;
+ document.getElementById("dns-upstream").value = settings.dns_upstream;
}
if (settings.dns_bind_addr) {
- document.getElementById('dns-bind').value = settings.dns_bind_addr;
+ document.getElementById("dns-bind").value = settings.dns_bind_addr;
}
- const dnsCurrentUpstream = document.getElementById('dns-current-upstream');
+ const dnsCurrentUpstream = document.getElementById("dns-current-upstream");
if (dnsCurrentUpstream && settings.dns_upstream) {
- dnsCurrentUpstream.innerText = 'Current upstreams: ' + settings.dns_upstream;
+ dnsCurrentUpstream.innerText = "Current upstreams: " + settings.dns_upstream;
} else if (dnsCurrentUpstream) {
- dnsCurrentUpstream.innerText = '';
+ dnsCurrentUpstream.innerText = "";
}
if (settings.mirror_enabled !== undefined) {
- document.getElementById('mirror-enabled').checked = settings.mirror_enabled;
+ document.getElementById("mirror-enabled").checked = settings.mirror_enabled;
}
if (settings.preferred_source !== undefined) {
- document.getElementById('preferred-source-upstream').checked = settings.preferred_source === 'upstream';
+ document.getElementById("preferred-source-upstream").checked = settings.preferred_source === "upstream";
}
if (settings.mirror_endpoints) {
- document.getElementById('mirror-endpoints').value = settings.mirror_endpoints.join('\n');
+ document.getElementById("mirror-endpoints").value = settings.mirror_endpoints.join("\n");
}
if (settings.internal_paths) {
- document.getElementById('internal-paths').value = settings.internal_paths.join('\n');
+ document.getElementById("internal-paths").value = settings.internal_paths.join("\n");
}
- const spotifyStatus = document.getElementById('spotify-config-status');
+ const spotifyStatus = document.getElementById("spotify-config-status");
if (spotifyStatus) {
if (settings.spotify_configured) {
spotifyStatus.innerHTML = '
(Client ID present)';
} else {
- spotifyStatus.innerHTML = '
';
}
}
fetchProxySettings();
fetchSpotifyStatus();
} catch (error) {
- console.error('Failed to fetch settings', error);
+ console.error("Failed to fetch settings", error);
}
}
async function fetchProxySettings() {
try {
- const response = await fetch('/setup/proxy-settings');
+ const response = await fetch("/setup/proxy-settings");
const settings = await response.json();
- document.getElementById('proxy-redact').checked = settings.redact;
- document.getElementById('proxy-log-body').checked = settings.log_body;
- document.getElementById('proxy-record').checked = settings.record;
+ document.getElementById("proxy-redact").checked = settings.redact;
+ document.getElementById("proxy-log-body").checked = settings.log_body;
+ document.getElementById("proxy-record").checked = settings.record;
} catch (error) {
- console.error('Failed to fetch proxy settings', error);
+ console.error("Failed to fetch proxy settings", error);
}
}
async function updateProxySettings() {
const settings = {
- redact: document.getElementById('proxy-redact').checked,
- log_body: document.getElementById('proxy-log-body').checked,
- record: document.getElementById('proxy-record').checked,
+ redact: document.getElementById("proxy-redact").checked,
+ log_body: document.getElementById("proxy-log-body").checked,
+ record: document.getElementById("proxy-record").checked,
};
try {
- await fetch('/setup/proxy-settings', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(settings)
+ await fetch("/setup/proxy-settings", {
+ method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(settings),
});
} catch (error) {
- console.error('Failed to update proxy settings', error);
+ console.error("Failed to update proxy settings", error);
}
}
async function updateSettings() {
const settings = {
- server_url: document.getElementById('target-domain').value,
- discovery_interval: document.getElementById('discovery-interval').value,
- discovery_enabled: document.getElementById('discovery-enabled').checked,
- dns_enabled: document.getElementById('dns-enabled').checked,
- dns_upstream: document.getElementById('dns-upstream').value,
- dns_bind_addr: document.getElementById('dns-bind').value,
- mirror_enabled: document.getElementById('mirror-enabled').checked,
- preferred_source: document.getElementById('preferred-source-upstream').checked ? 'upstream' : 'local',
- mirror_endpoints: document.getElementById('mirror-endpoints').value.split('\n').map(s => s.trim()).filter(s => s !== ''),
- internal_paths: document.getElementById('internal-paths').value.split('\n').map(s => s.trim()).filter(s => s !== ''),
+ server_url: document.getElementById("target-domain").value,
+ discovery_interval: document.getElementById("discovery-interval").value,
+ discovery_enabled: document.getElementById("discovery-enabled").checked,
+ dns_enabled: document.getElementById("dns-enabled").checked,
+ dns_upstream: document.getElementById("dns-upstream").value,
+ dns_bind_addr: document.getElementById("dns-bind").value,
+ mirror_enabled: document.getElementById("mirror-enabled").checked,
+ preferred_source: document.getElementById("preferred-source-upstream").checked ? "upstream" : "local",
+ mirror_endpoints: document
+ .getElementById("mirror-endpoints")
+ .value.split("\n")
+ .map((s) => s.trim())
+ .filter((s) => s !== ""),
+ internal_paths: document
+ .getElementById("internal-paths")
+ .value.split("\n")
+ .map((s) => s.trim())
+ .filter((s) => s !== ""),
};
- const status = document.getElementById('settings-status');
- status.innerText = 'Saving...';
- status.style.color = 'blue';
+ const status = document.getElementById("settings-status");
+ status.innerText = "Saving...";
+ status.style.color = "blue";
try {
- const response = await fetch('/setup/settings', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(settings)
+ const response = await fetch("/setup/settings", {
+ method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(settings),
});
if (response.ok) {
- status.innerText = '✅ Settings saved. Restart service to apply all changes (like certificate SANs).';
- status.style.color = 'green';
+ status.innerText = "✅ Settings saved. Restart service to apply all changes (like certificate SANs).";
+ status.style.color = "green";
setTimeout(() => fetchSettings(), 500); // Give backend a moment to settle
} else {
const err = await response.text();
- status.innerText = '❌ Failed: ' + err;
- status.style.color = 'red';
+ status.innerText = "❌ Failed: " + err;
+ status.style.color = "red";
}
} catch (error) {
- status.innerText = '❌ Error: ' + error.message;
- status.style.color = 'red';
+ status.innerText = "❌ Error: " + error.message;
+ status.style.color = "red";
}
}
async function fetchDevices() {
try {
- const response = await fetch('/setup/devices');
+ const response = await fetch("/setup/devices");
const devices = await response.json();
- const container = document.getElementById('device-list');
- const syncSelector = document.getElementById('sync-device-list');
- const migrationSelector = document.getElementById('migration-device-list');
+ const container = document.getElementById("device-list");
+ const syncSelector = document.getElementById("sync-device-list");
+ const migrationSelector = document.getElementById("migration-device-list");
if (devices.length === 0) {
- container.innerHTML = 'No devices known yet.';
+ container.innerHTML = "No devices known yet.";
} else {
- let html = '
";
container.innerHTML = html;
if (currentSyncVal) syncSelector.value = currentSyncVal;
@@ -312,22 +315,22 @@ async function fetchDevices() {
if (eventSelector && currentEventVal) eventSelector.value = currentEventVal;
// Asynchronously fetch live info for each device
- devices.forEach(d => updateDeviceInfo(d.device_id, d.ip_address));
+ devices.forEach((d) => updateDeviceInfo(d.device_id, d.ip_address));
fetchSpotifyStatus();
}
} catch (error) {
- document.getElementById('device-list').innerHTML = 'Error loading devices: ' + error;
+ document.getElementById("device-list").innerHTML = "Error loading devices: " + error;
}
}
function prepareSync(deviceId) {
- document.getElementById('sync-device-list').value = deviceId;
- openTab(null, 'tab-sync');
+ document.getElementById("sync-device-list").value = deviceId;
+ openTab(null, "tab-sync");
}
function prepareMigration(deviceId) {
- document.getElementById('migration-device-list').value = deviceId;
- openTab(null, 'tab-migration');
+ document.getElementById("migration-device-list").value = deviceId;
+ openTab(null, "tab-migration");
showSummary(deviceId);
}
@@ -347,13 +350,13 @@ function openTab(evt, tabId) {
content.className += " active";
}
- if (tabId === 'tab-interactions') {
+ if (tabId === "tab-interactions") {
fetchInteractionStats();
fetchInteractions();
fetchDNSDiscoveries();
}
- if (tabId === 'tab-parity') {
+ if (tabId === "tab-parity") {
fetchParityMismatches();
}
@@ -362,7 +365,7 @@ function openTab(evt, tabId) {
} else {
// Find the button that corresponds to the tabId and activate it
for (let i = 0; i < tablinks.length; i++) {
- const onclick = tablinks[i].getAttribute('onclick');
+ const onclick = tablinks[i].getAttribute("onclick");
if (onclick && onclick.includes(tabId)) {
tablinks[i].className += " active";
break;
@@ -375,32 +378,32 @@ function getDeviceDisplayName(deviceId) {
if (!deviceId) return "Unknown Device";
// 1. Try migration selector
- const migrationSelector = document.getElementById('migration-device-list');
+ const migrationSelector = document.getElementById("migration-device-list");
if (migrationSelector) {
for (let opt of migrationSelector.options) {
- if (opt.value === deviceId && opt.textContent !== '-- Select a device --') {
+ if (opt.value === deviceId && opt.textContent !== "-- Select a device --") {
return opt.textContent;
}
}
}
// 2. Try sync selector
- const syncSelector = document.getElementById('sync-device-list');
+ const syncSelector = document.getElementById("sync-device-list");
if (syncSelector) {
for (let opt of syncSelector.options) {
- if (opt.value === deviceId && opt.textContent !== '-- Select a device --') {
+ if (opt.value === deviceId && opt.textContent !== "-- Select a device --") {
return opt.textContent;
}
}
}
// 3. Try table lookup
- const rows = document.querySelectorAll('#device-list tr');
+ const rows = document.querySelectorAll("#device-list tr");
for (const row of rows) {
- const idCell = row.querySelector('.col-deviceid');
+ const idCell = row.querySelector(".col-deviceid");
if (idCell && idCell.innerText === deviceId) {
- const name = row.querySelector('.col-name').innerText;
- const ip = row.querySelector('.col-ip').innerText;
+ const name = row.querySelector(".col-name").innerText;
+ const ip = row.querySelector(".col-ip").innerText;
return `${name} (${ip})`;
}
}
@@ -409,104 +412,103 @@ function getDeviceDisplayName(deviceId) {
}
async function startSync() {
- const deviceId = document.getElementById('sync-device-list').value;
+ const deviceId = document.getElementById("sync-device-list").value;
if (!deviceId) {
- alert('Please select a device first');
+ alert("Please select a device first");
return;
}
- const status = document.getElementById('sync-status');
- const results = document.getElementById('sync-results');
- const log = document.getElementById('sync-log');
+ const status = document.getElementById("sync-status");
+ const results = document.getElementById("sync-results");
+ const log = document.getElementById("sync-log");
- status.style.display = 'block';
- status.style.backgroundColor = '#eef';
+ status.style.display = "block";
+ status.style.backgroundColor = "#eef";
const display = getDeviceDisplayName(deviceId);
- status.textContent = 'Syncing data from ' + display + '...';
- results.style.display = 'none';
- log.innerHTML = '';
+ status.textContent = "Syncing data from " + display + "...";
+ results.style.display = "none";
+ log.innerHTML = "";
try {
- const response = await fetch('/setup/sync/' + encodeURIComponent(deviceId), { method: 'POST' });
+ const response = await fetch("/setup/sync/" + encodeURIComponent(deviceId), {method: "POST"},);
if (response.ok) {
- status.style.backgroundColor = '#dfd';
- status.textContent = '✅ Sync completed successfully for ' + display + '!';
- results.style.display = 'block';
- log.innerHTML = 'Data fetched and saved to local datastore for ' + display + '.\nPresets: OK\nRecents: OK\nSources: OK';
+ status.style.backgroundColor = "#dfd";
+ status.textContent = "✅ Sync completed successfully for " + display + "!";
+ results.style.display = "block";
+ log.innerHTML = "Data fetched and saved to local datastore for " + display + ".\nPresets: OK\nRecents: OK\nSources: OK";
} else {
const err = await response.text();
throw new Error(err);
}
} catch (error) {
- status.style.backgroundColor = '#fdd';
- status.textContent = '❌ Sync failed for ' + display + ': ' + error.message;
+ status.style.backgroundColor = "#fdd";
+ status.textContent = "❌ Sync failed for " + display + ": " + error.message;
}
}
async function fetchVersion() {
try {
- const response = await fetch('/setup/version');
+ const response = await fetch("/setup/version");
const data = await response.json();
- const info = document.getElementById('version-info');
+ const info = document.getElementById("version-info");
if (info && data.version) {
info.innerText = `AfterTouch ${data.version} (${data.commit}) - ${data.date}`;
}
} catch (error) {
- console.error('Failed to fetch version info', error);
+ console.error("Failed to fetch version info", error);
}
}
async function fetchInteractionStats() {
- console.log('Fetching interaction stats...');
+ console.log("Fetching interaction stats...");
try {
- const response = await fetch('/setup/interaction-stats');
+ const response = await fetch("/setup/interaction-stats");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const stats = await response.json();
- console.log('Fetched interaction stats:', stats);
+ console.log("Fetched interaction stats:", stats);
- document.getElementById('total-requests').innerText = stats.total_requests || stats.TotalRequests || 0;
+ document.getElementById("total-requests").innerText = stats.total_requests || stats.TotalRequests || 0;
- const statsContainer = document.getElementById('interaction-stats-container');
+ const statsContainer = document.getElementById("interaction-stats-container",);
if (statsContainer) {
- statsContainer.style.display = 'block';
+ statsContainer.style.display = "block";
}
- const serviceList = document.getElementById('stats-by-service');
- serviceList.innerHTML = '';
+ const serviceList = document.getElementById("stats-by-service");
+ serviceList.innerHTML = "";
const byService = stats.by_service || stats.ByService;
if (byService) {
Object.entries(byService).forEach(([service, count]) => {
- const li = document.createElement('li');
+ const li = document.createElement("li");
li.innerHTML = `
${count || 0} requests`;
serviceList.appendChild(li);
});
}
- const sessionList = document.getElementById('stats-by-session');
- const sessionFilter = document.getElementById('filter-session');
+ const sessionList = document.getElementById("stats-by-session");
+ const sessionFilter = document.getElementById("filter-session");
const currentFilter = sessionFilter.value;
- sessionList.innerHTML = '';
+ sessionList.innerHTML = "";
sessionFilter.innerHTML = '
';
const bySession = stats.by_session || stats.BySession;
if (bySession) {
// Sort by session ID (timestamp) descending
- const sortedSessions = Object.entries(bySession)
- .sort((a, b) => {
- const sessionA = a[0] || "";
- const sessionB = b[0] || "";
- return sessionB.localeCompare(sessionA);
- });
+ const sortedSessions = Object.entries(bySession).sort((a, b) => {
+ const sessionA = a[0] || "";
+ const sessionB = b[0] || "";
+ return sessionB.localeCompare(sessionA);
+ });
sortedSessions.forEach(([session, count]) => {
// Session format is like 20260215-160705-99213
// Try to make it more readable: 2026-02-15 16:07:05 (PID 99213)
let sessionDisplay = session || "unknown";
- if (session && session.includes('-')) {
- const parts = session.split('-');
+ if (session && session.includes("-")) {
+ const parts = session.split("-");
if (parts.length >= 2) {
const date = parts[0]; // 20260215
const time = parts[1]; // 160705
@@ -519,7 +521,7 @@ async function fetchInteractionStats() {
}
}
- const li = document.createElement('li');
+ const li = document.createElement("li");
li.innerHTML = `
@@ -530,7 +532,7 @@ async function fetchInteractionStats() {
`;
sessionList.appendChild(li);
- const opt = document.createElement('option');
+ const opt = document.createElement("option");
opt.value = session || "";
opt.innerText = sessionDisplay;
sessionFilter.appendChild(opt);
@@ -539,7 +541,7 @@ async function fetchInteractionStats() {
sessionFilter.value = currentFilter;
}
} catch (error) {
- console.error('Failed to fetch interaction stats', error);
+ console.error("Failed to fetch interaction stats", error);
}
}
@@ -549,11 +551,11 @@ function downloadSession(sessionId) {
}
async function filterBySession(sessionId) {
- document.getElementById('filter-session').value = sessionId;
+ document.getElementById("filter-session").value = sessionId;
fetchInteractions();
- const browseContainer = document.getElementById('browse-recordings');
+ const browseContainer = document.getElementById("browse-recordings");
if (browseContainer) {
- browseContainer.scrollIntoView({ behavior: 'smooth' });
+ browseContainer.scrollIntoView({behavior: "smooth"});
}
}
@@ -565,11 +567,11 @@ async function deleteSession(sessionId) {
try {
const response = await fetch(`/setup/interactions/sessions/${sessionId}`, {
- method: 'DELETE'
+ method: "DELETE",
});
if (response.ok) {
// If the deleted session was selected in the filter, clear the filter
- const sessionFilter = document.getElementById('filter-session');
+ const sessionFilter = document.getElementById("filter-session");
if (sessionFilter.value === sessionId) {
sessionFilter.value = "";
fetchInteractions();
@@ -577,48 +579,48 @@ async function deleteSession(sessionId) {
fetchInteractionStats();
} else {
const err = await response.text();
- alert('Failed to delete session: ' + err);
+ alert("Failed to delete session: " + err);
}
} catch (error) {
- alert('Error deleting session: ' + error.message);
+ alert("Error deleting session: " + error.message);
}
}
async function cleanupSessions() {
- if (!confirm('Are you sure you want to cleanup old sessions? Only the 10 most recent ones will be kept.')) {
+ if (!confirm("Are you sure you want to cleanup old sessions? Only the 10 most recent ones will be kept.",)) {
return;
}
try {
- const response = await fetch('/setup/interactions/sessions?keep=10', {
- method: 'DELETE'
+ const response = await fetch("/setup/interactions/sessions?keep=10", {
+ method: "DELETE",
});
if (response.ok) {
// Refresh everything
- document.getElementById('filter-session').value = "";
+ document.getElementById("filter-session").value = "";
fetchInteractionStats();
fetchInteractions();
} else {
const err = await response.text();
- alert('Failed to cleanup sessions: ' + err);
+ alert("Failed to cleanup sessions: " + err);
}
} catch (error) {
- alert('Error cleaning up sessions: ' + error.message);
+ alert("Error cleaning up sessions: " + error.message);
}
}
async function fetchInteractions() {
- console.log('Fetching interactions...');
- const session = document.getElementById('filter-session').value;
- const category = document.getElementById('filter-category').value;
- const since = document.getElementById('filter-since').value;
+ console.log("Fetching interactions...");
+ const session = document.getElementById("filter-session").value;
+ const category = document.getElementById("filter-category").value;
+ const since = document.getElementById("filter-since").value;
- let url = '/setup/interactions';
+ let url = "/setup/interactions";
const params = [];
if (session) params.push(`session=${encodeURIComponent(session)}`);
if (category) params.push(`category=${encodeURIComponent(category)}`);
if (since) params.push(`since=${encodeURIComponent(since)}`);
- if (params.length > 0) url += '?' + params.join('&');
+ if (params.length > 0) url += "?" + params.join("&");
try {
const response = await fetch(url);
@@ -626,23 +628,23 @@ async function fetchInteractions() {
throw new Error(`HTTP error! status: ${response.status}`);
}
const interactions = await response.json();
- console.log('Fetched interactions:', interactions);
- const list = document.getElementById('interactions-list');
+ console.log("Fetched interactions:", interactions);
+ const list = document.getElementById("interactions-list");
if (!list) {
- console.error('Could not find interactions-list element');
+ console.error("Could not find interactions-list element");
return;
}
// Show the parent summary box if it was hidden
- const browseContainer = list.closest('.summary-box');
+ const browseContainer = list.closest(".summary-box");
if (browseContainer) {
- browseContainer.style.display = 'block';
+ browseContainer.style.display = "block";
}
- list.innerHTML = '';
+ list.innerHTML = "";
if (!interactions || interactions.length === 0) {
- list.innerHTML = '
No interactions found for current filters. ';
+ list.innerHTML = '
No interactions found for current filters. ';
return;
}
@@ -659,9 +661,9 @@ async function fetchInteractions() {
return counterA - counterB;
});
- interactions.forEach(i => {
- const tr = document.createElement('tr');
- tr.style.borderBottom = '1px solid #eee';
+ interactions.forEach((i) => {
+ const tr = document.createElement("tr");
+ tr.style.borderBottom = "1px solid #eee";
const counter = i.counter || i.Counter || 0;
const timestamp = i.timestamp || i.Timestamp || "";
@@ -671,73 +673,200 @@ async function fetchInteractions() {
const category = i.category || i.Category || "";
const session = i.session || i.Session || "";
const file = i.file || i.File || "";
+ const scmudcData = i.scmudc_data || i.SCMUDCData || null;
- let statusClass = '';
- if (status >= 200 && status < 300) statusClass = 'status-success';
- else if (status >= 400) statusClass = 'status-error';
+ let statusClass = "";
+ if (status >= 200 && status < 300) statusClass = "status-success"; else if (status >= 400) statusClass = "status-error";
+
+ // Create SCMUDC event details
+ let eventDetails = "";
+ if (scmudcData) {
+ const originIcon = getOriginIcon(scmudcData.origin);
+ const actionIcon = getActionIcon(scmudcData.action);
+ const truncatedCommand = scmudcData.command && scmudcData.command.length > 30 ? scmudcData.command.substring(0, 27) + "..." : scmudcData.command || "";
+
+ eventDetails = `
+ ${originIcon} ${actionIcon} ${truncatedCommand}
+ ${scmudcData.decoded_data ? '(...) " : ""}
+ `;
+ }
tr.innerHTML = `
${counter}
${timestamp}
${method}
${path}
-
${status || '???'}
+
${status || "???"}
${category}
+
${eventDetails}
View
`;
list.appendChild(tr);
});
} catch (error) {
- console.error('Failed to fetch interactions', error);
+ console.error("Failed to fetch interactions", error);
}
}
+// Helper functions for SCMUDC event display
+function getOriginIcon(origin) {
+ const icons = {
+ gabbo: "📱", // SoundTouch App
+ console: "🎛️", // Device Hardware
+ device: "🔄", // Internal System
+ };
+ return icons[origin] || "❓";
+}
+
+function getActionIcon(action) {
+ const icons = {
+ "play-pressed": "▶️",
+ "pause-pressed": "⏸️",
+ "power-pressed": "⚡",
+ "preset-pressed": "⭐",
+ "play-item": "🎵",
+ "skip-forward-pressed": "⏭️",
+ "stop-pressed": "⏹️",
+ };
+ return icons[action] || "🔘";
+}
+
+function showSCMUDCDetails(file) {
+ // Find the interaction data for this file
+ fetch("/setup/interactions")
+ .then((response) => response.json())
+ .then((interactions) => {
+ const interaction = interactions.find((i) => (i.file || i.File) === file);
+ if (interaction && interaction.scmudc_data) {
+ displaySCMUDCPopover(interaction.scmudc_data);
+ }
+ })
+ .catch((error) => {
+ console.error("Failed to fetch SCMUDC details:", error);
+ });
+}
+
+function displaySCMUDCPopover(scmudcData) {
+ const modal = document.createElement("div");
+ modal.style.cssText = `
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(0,0,0,0.5); z-index: 1000;
+ display: flex; align-items: center; justify-content: center;
+ `;
+
+ const content = document.createElement("div");
+ content.style.cssText = `
+ background: white; padding: 20px; border-radius: 8px;
+ max-width: 600px; max-height: 80%; overflow-y: auto;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+ `;
+
+ let html = `
+
SCMUDC Event Details
+
Origin: ${getOriginDescription(scmudcData.origin)} (${scmudcData.origin})
+
Action: ${scmudcData.action}
+
Summary: ${scmudcData.summary}
+ `;
+
+ if (scmudcData.decoded_data) {
+ html += `
+
Decoded Content:
+
Source: ${scmudcData.decoded_data.content_type}
+
Item: ${scmudcData.decoded_data.item_name}
+ `;
+
+ if (scmudcData.decoded_data.source_account) {
+ html += `
Account: ${scmudcData.decoded_data.source_account}
`;
+ }
+
+ if (scmudcData.decoded_data.artwork_url) {
+ html += `
Artwork: View
`;
+ }
+
+ if (scmudcData.decoded_data.is_presetable) {
+ html += `
Presetable: Yes
`;
+ }
+
+ if (scmudcData.decoded_data.xml_content) {
+ html += `
+
Full XML Content:
+
${escapeHtml(scmudcData.decoded_data.xml_content)}
+ `;
+ }
+ }
+
+ html += '
Close ';
+ content.innerHTML = html;
+ modal.appendChild(content);
+ document.body.appendChild(modal);
+
+ // Close on background click
+ modal.addEventListener("click", (e) => {
+ if (e.target === modal) modal.remove();
+ });
+}
+
+function getOriginDescription(origin) {
+ const descriptions = {
+ gabbo: "SoundTouch App", console: "Device Hardware", device: "Internal System",
+ };
+ return descriptions[origin] || origin;
+}
+
+function escapeHtml(text) {
+ const div = document.createElement("div");
+ div.textContent = text;
+ return div.innerHTML;
+}
+
async function viewInteraction(file) {
try {
- const response = await fetch(`/setup/interaction-content?file=${encodeURIComponent(file)}`);
+ const response = await fetch(`/setup/interaction-content?file=${encodeURIComponent(file)}`,);
const content = await response.text();
- document.getElementById('viewer-filename').innerText = file;
- document.getElementById('interaction-content').innerText = content;
- document.getElementById('interaction-viewer').style.display = 'block';
- document.getElementById('interaction-viewer').scrollIntoView({ behavior: 'smooth' });
+ document.getElementById("viewer-filename").innerText = file;
+ document.getElementById("interaction-content").innerText = content;
+ document.getElementById("interaction-viewer").style.display = "block";
+ document
+ .getElementById("interaction-viewer")
+ .scrollIntoView({behavior: "smooth"});
} catch (error) {
- alert('Failed to load interaction content: ' + error);
+ alert("Failed to load interaction content: " + error);
}
}
async function fetchDNSDiscoveries() {
- console.log('Fetching DNS discoveries...');
+ console.log("Fetching DNS discoveries...");
try {
- const response = await fetch('/setup/dns-discoveries');
+ const response = await fetch("/setup/dns-discoveries");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const discoveries = await response.json();
- console.log('Fetched DNS discoveries:', discoveries);
- const list = document.getElementById('dns-discoveries-list');
+ console.log("Fetched DNS discoveries:", discoveries);
+ const list = document.getElementById("dns-discoveries-list");
if (!list) {
- console.error('Could not find dns-discoveries-list element');
+ console.error("Could not find dns-discoveries-list element");
return;
}
- list.innerHTML = '';
+ list.innerHTML = "";
if (!discoveries || discoveries.length === 0) {
list.innerHTML = '
No DNS queries discovered yet. ';
return;
}
- discoveries.forEach(d => {
- const tr = document.createElement('tr');
- tr.style.borderBottom = '1px solid #eee';
+ discoveries.forEach((d) => {
+ const tr = document.createElement("tr");
+ tr.style.borderBottom = "1px solid #eee";
const hostname = d.hostname || "";
const lastSeen = d.last_seen || "";
const count = d.query_count || 0;
- const isBose = d.is_bose_service ? '✅' : '❌';
- const category = d.is_intercepted ? 'self' : 'upstream';
- const remoteAddr = d.remote_addr || 'unknown';
+ const isBose = d.is_bose_service ? "✅" : "❌";
+ const category = d.is_intercepted ? "self" : "upstream";
+ const remoteAddr = d.remote_addr || "unknown";
tr.innerHTML = `
${hostname}
@@ -750,42 +879,42 @@ async function fetchDNSDiscoveries() {
list.appendChild(tr);
});
} catch (error) {
- console.error('Failed to fetch DNS discoveries', error);
+ console.error("Failed to fetch DNS discoveries", error);
}
}
async function clearDNSDiscoveries() {
- if (!confirm('Are you sure you want to clear all DNS discovery logs?')) {
+ if (!confirm("Are you sure you want to clear all DNS discovery logs?")) {
return;
}
try {
- const response = await fetch('/setup/dns-discoveries', {
- method: 'DELETE'
+ const response = await fetch("/setup/dns-discoveries", {
+ method: "DELETE",
});
if (response.ok) {
fetchDNSDiscoveries();
} else {
const err = await response.text();
- alert('Failed to clear DNS discoveries: ' + err);
+ alert("Failed to clear DNS discoveries: " + err);
}
} catch (error) {
- alert('Error clearing DNS discoveries: ' + error.message);
+ alert("Error clearing DNS discoveries: " + error.message);
}
}
function downloadDNSDiscoveries() {
- window.location.href = '/setup/dns-discoveries/download';
+ window.location.href = "/setup/dns-discoveries/download";
}
async function showDeviceEvents() {
- const overlay = document.getElementById('device-events-overlay');
- overlay.style.display = 'block';
- overlay.scrollIntoView({ behavior: 'smooth' });
+ const overlay = document.getElementById("device-events-overlay");
+ overlay.style.display = "block";
+ overlay.scrollIntoView({behavior: "smooth"});
// Ensure device selector is populated (handled by fetchDevices)
// but if it's still empty, we can try to trigger a fetch
- const selector = document.getElementById('event-device-selector');
+ const selector = document.getElementById("event-device-selector");
if (selector.options.length <= 1) {
fetchDevices();
}
@@ -794,7 +923,7 @@ async function showDeviceEvents() {
async function fetchDeviceEvents(deviceId) {
if (!deviceId) return;
- const list = document.getElementById('events-list');
+ const list = document.getElementById("events-list");
list.innerHTML = '
Loading events... ';
try {
@@ -802,7 +931,7 @@ async function fetchDeviceEvents(deviceId) {
const data = await response.json();
const events = data.events;
- list.innerHTML = '';
+ list.innerHTML = "";
if (!events || events.length === 0) {
list.innerHTML = '
No events found for this device. ';
return;
@@ -811,9 +940,9 @@ async function fetchDeviceEvents(deviceId) {
// Sort events by time descending
events.sort((a, b) => (b.time || "").localeCompare(a.time || ""));
- events.forEach(e => {
- const tr = document.createElement('tr');
- tr.style.borderBottom = '1px solid #eee';
+ events.forEach((e) => {
+ const tr = document.createElement("tr");
+ tr.style.borderBottom = "1px solid #eee";
const time = e.time || "";
const type = e.type || "";
@@ -832,27 +961,27 @@ async function fetchDeviceEvents(deviceId) {
}
async function fetchParityMismatches() {
- const list = document.getElementById('parity-mismatches-list');
+ const list = document.getElementById("parity-mismatches-list");
list.innerHTML = '
Loading mismatches... ';
try {
- const response = await fetch('/setup/parity-mismatches');
+ const response = await fetch("/setup/parity-mismatches");
const mismatches = await response.json();
- list.innerHTML = '';
+ list.innerHTML = "";
if (!mismatches || mismatches.length === 0) {
list.innerHTML = '
No parity mismatches detected yet. ';
return;
}
- mismatches.forEach(m => {
- const tr = document.createElement('tr');
- tr.style.borderBottom = '1px solid #eee';
+ mismatches.forEach((m) => {
+ const tr = document.createElement("tr");
+ tr.style.borderBottom = "1px solid #eee";
const time = m.timestamp || "";
const method = m.method || "";
const path = m.path || "";
- const reasons = (m.reasons || []).join(', ');
+ const reasons = (m.reasons || []).join(", ");
tr.innerHTML = `
${time}
@@ -869,44 +998,46 @@ async function fetchParityMismatches() {
}
async function clearParityMismatches() {
- if (!confirm('Are you sure you want to clear all parity mismatch records?')) return;
+ if (!confirm("Are you sure you want to clear all parity mismatch records?")) return;
try {
- await fetch('/setup/parity-mismatches', { method: 'DELETE' });
+ await fetch("/setup/parity-mismatches", {method: "DELETE"});
fetchParityMismatches();
- document.getElementById('parity-diff-view').style.display = 'none';
+ document.getElementById("parity-diff-view").style.display = "none";
} catch (error) {
- alert('Failed to clear mismatches: ' + error.message);
+ alert("Failed to clear mismatches: " + error.message);
}
}
function viewParityMismatch(m) {
- document.getElementById('diff-path-display').innerText = m.method + ' ' + m.path;
- const reasonsList = document.getElementById('diff-reasons-list');
- reasonsList.innerHTML = '';
- (m.reasons || []).forEach(r => {
- const li = document.createElement('li');
+ document.getElementById("diff-path-display").innerText = m.method + " " + m.path;
+ const reasonsList = document.getElementById("diff-reasons-list");
+ reasonsList.innerHTML = "";
+ (m.reasons || []).forEach((r) => {
+ const li = document.createElement("li");
li.innerText = r;
reasonsList.appendChild(li);
});
- document.getElementById('diff-local-meta').innerText = `Status: ${m.local.status}`;
- document.getElementById('diff-upstream-meta').innerText = `Status: ${m.upstream.status}`;
+ document.getElementById("diff-local-meta").innerText = `Status: ${m.local.status}`;
+ document.getElementById("diff-upstream-meta").innerText = `Status: ${m.upstream.status}`;
- document.getElementById('diff-local-body').innerText = formatXML(m.local.body);
- document.getElementById('diff-upstream-body').innerText = formatXML(m.upstream.body);
+ document.getElementById("diff-local-body").innerText = formatXML(m.local.body,);
+ document.getElementById("diff-upstream-body").innerText = formatXML(m.upstream.body,);
- document.getElementById('parity-diff-view').style.display = 'block';
- document.getElementById('parity-diff-view').scrollIntoView({ behavior: 'smooth' });
+ document.getElementById("parity-diff-view").style.display = "block";
+ document
+ .getElementById("parity-diff-view")
+ .scrollIntoView({behavior: "smooth"});
}
function formatXML(xml) {
- if (!xml) return '';
+ if (!xml) return "";
try {
- let formatted = '';
+ let formatted = "";
let reg = /(>)(<)(\/*)/g;
- xml = xml.replace(reg, '$1\r\n$2$3');
+ xml = xml.replace(reg, "$1\r\n$2$3");
let pad = 0;
- xml.split('\r\n').forEach(function(node) {
+ xml.split("\r\n").forEach(function (node) {
let indent = 0;
if (node.match(/.+<\/\w[^>]*>$/)) {
indent = 0;
@@ -918,9 +1049,9 @@ function formatXML(xml) {
indent = 0;
}
- let padding = '';
- for (let i = 0; i < pad; i++) padding += ' ';
- formatted += padding + node + '\r\n';
+ let padding = "";
+ for (let i = 0; i < pad; i++) padding += " ";
+ formatted += padding + node + "\r\n";
pad += indent;
});
return formatted.trim();
@@ -929,41 +1060,38 @@ function formatXML(xml) {
}
}
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener("DOMContentLoaded", () => {
fetchSettings();
fetchDevices();
triggerDiscovery();
fetchVersion();
fetchParityMismatches();
- const syncBtn = document.getElementById('sync-now-btn');
+ const syncBtn = document.getElementById("sync-now-btn");
if (syncBtn) syncBtn.onclick = startSync;
});
-
async function addManualDevice() {
- const ip = document.getElementById('add-manual-ip').value.trim();
+ const ip = document.getElementById("add-manual-ip").value.trim();
if (!ip) {
- alert('Please enter an IP address');
+ alert("Please enter an IP address");
return;
}
try {
- const response = await fetch('/setup/devices', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ ip: ip })
+ const response = await fetch("/setup/devices", {
+ method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ip: ip}),
});
if (response.ok) {
- document.getElementById('add-manual-ip').value = '';
+ document.getElementById("add-manual-ip").value = "";
fetchDevices();
} else {
const err = await response.text();
- alert('Failed to add device: ' + err);
+ alert("Failed to add device: " + err);
}
} catch (error) {
- alert('Error adding device: ' + error.message);
+ alert("Error adding device: " + error.message);
}
}
@@ -974,695 +1102,693 @@ async function removeDevice(deviceId, name) {
try {
const response = await fetch(`/setup/devices/${deviceId}`, {
- method: 'DELETE'
+ method: "DELETE",
});
if (response.ok) {
fetchDevices();
} else {
const err = await response.text();
- alert('Failed to remove device: ' + err);
+ alert("Failed to remove device: " + err);
}
} catch (error) {
- alert('Error removing device: ' + error.message);
+ alert("Error removing device: " + error.message);
}
}
async function triggerDiscovery() {
- const indicator = document.getElementById('discovery-indicator');
- if (indicator) indicator.style.display = 'inline';
+ const indicator = document.getElementById("discovery-indicator");
+ if (indicator) indicator.style.display = "inline";
try {
- await fetch('/setup/discover', { method: 'POST' });
+ await fetch("/setup/discover", {method: "POST"});
pollDiscoveryStatus();
} catch (error) {
- console.error('Failed to trigger discovery', error);
- if (indicator) indicator.style.display = 'none';
+ console.error("Failed to trigger discovery", error);
+ if (indicator) indicator.style.display = "none";
}
}
async function pollDiscoveryStatus() {
- const indicator = document.getElementById('discovery-indicator');
+ const indicator = document.getElementById("discovery-indicator");
try {
- const response = await fetch('/setup/discovery-status');
+ const response = await fetch("/setup/discovery-status");
const data = await response.json();
if (data.discovering) {
setTimeout(pollDiscoveryStatus, 2000);
} else {
- if (indicator) indicator.style.display = 'none';
+ if (indicator) indicator.style.display = "none";
fetchDevices();
}
} catch (error) {
- console.error('Failed to check discovery status', error);
- if (indicator) indicator.style.display = 'none';
+ console.error("Failed to check discovery status", error);
+ if (indicator) indicator.style.display = "none";
}
}
async function updateDeviceInfo(deviceId, ip) {
try {
- const response = await fetch('/setup/info/' + encodeURIComponent(deviceId));
+ const response = await fetch("/setup/info/" + encodeURIComponent(deviceId));
if (!response.ok) return;
const info = await response.json();
- const rowId = 'device-row-' + deviceId;
+ const rowId = "device-row-" + deviceId;
const row = document.getElementById(rowId);
if (row) {
- const nameEl = row.querySelector('.col-name');
+ const nameEl = row.querySelector(".col-name");
if (nameEl && info.name) nameEl.innerText = info.name;
- const modelEl = row.querySelector('.col-model');
+ const modelEl = row.querySelector(".col-model");
if (modelEl && info.type) modelEl.innerText = info.type;
- const serialEl = row.querySelector('.col-serial');
+ const serialEl = row.querySelector(".col-serial");
if (serialEl && info.serialNumber) serialEl.innerText = info.serialNumber;
- const firmwareEl = row.querySelector('.col-firmware');
+ const firmwareEl = row.querySelector(".col-firmware");
if (firmwareEl && info.softwareVersion) firmwareEl.innerText = info.softwareVersion;
- const deviceIdEl = row.querySelector('.col-deviceid');
+ const deviceIdEl = row.querySelector(".col-deviceid");
if (deviceIdEl && info.deviceID) deviceIdEl.innerText = info.deviceID;
- const accountIdEl = row.querySelector('.col-accountid');
+ const accountIdEl = row.querySelector(".col-accountid");
if (accountIdEl && info.margeAccountUUID) accountIdEl.innerText = info.margeAccountUUID;
}
} catch (error) {
- console.warn('Failed to fetch live info for ' + ip, error);
+ console.warn("Failed to fetch live info for " + ip, error);
}
}
async function showSummary(deviceId) {
if (!deviceId) {
- document.getElementById('migration-summary').style.display = 'none';
+ document.getElementById("migration-summary").style.display = "none";
return;
}
- const targetUrl = document.getElementById('target-domain').value;
+ const targetUrl = document.getElementById("target-domain").value;
const opts = {
- marge: document.getElementById('opt-marge').value,
- stats: document.getElementById('opt-stats').value,
- sw_update: document.getElementById('opt-sw_update').value,
- bmx: document.getElementById('opt-bmx').value
+ marge: document.getElementById("opt-marge").value,
+ stats: document.getElementById("opt-stats").value,
+ sw_update: document.getElementById("opt-sw_update").value,
+ bmx: document.getElementById("opt-bmx").value,
};
- const statusDiv = document.getElementById('status');
- statusDiv.style.display = 'block';
- statusDiv.style.backgroundColor = '#ffffcc';
+ const statusDiv = document.getElementById("status");
+ statusDiv.style.display = "block";
+ statusDiv.style.backgroundColor = "#ffffcc";
const display = getDeviceDisplayName(deviceId);
- statusDiv.innerHTML = 'Fetching summary for ' + display + '...';
+ statusDiv.innerHTML = "Fetching summary for " + display + "...";
- const outputBox = document.getElementById('command-output-box');
- if (outputBox) outputBox.style.display = 'none';
+ const outputBox = document.getElementById("command-output-box");
+ if (outputBox) outputBox.style.display = "none";
- let query = '?target_url=' + encodeURIComponent(targetUrl);
+ let query = "?target_url=" + encodeURIComponent(targetUrl);
for (let k in opts) {
- query += '&' + k + '=' + encodeURIComponent(opts[k]);
+ query += "&" + k + "=" + encodeURIComponent(opts[k]);
}
try {
- const response = await fetch('/setup/summary/' + encodeURIComponent(deviceId) + query);
+ const response = await fetch("/setup/summary/" + encodeURIComponent(deviceId) + query,);
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
const summary = await response.json();
- statusDiv.style.display = 'none';
+ statusDiv.style.display = "none";
const ip = summary.ip_address || deviceId;
const finalDisplay = summary.device_name ? `${summary.device_name} (${ip})` : ip;
- document.getElementById('summary-device-display').innerText = finalDisplay;
+ document.getElementById("summary-device-display").innerText = finalDisplay;
// Keep deviceId hidden for subsequent calls
- document.getElementById('summary-device-id').value = deviceId;
+ document.getElementById("summary-device-id").value = deviceId;
// Update table row if it exists
- const rowId = 'device-row-' + deviceId;
+ const rowId = "device-row-" + deviceId;
const row = document.getElementById(rowId);
if (row) {
- const nameEl = row.querySelector('.col-name');
+ const nameEl = row.querySelector(".col-name");
if (nameEl && summary.device_name) nameEl.innerText = summary.device_name;
- const modelEl = row.querySelector('.col-model');
+ const modelEl = row.querySelector(".col-model");
if (modelEl && summary.device_model) modelEl.innerText = summary.device_model;
- const serialEl = row.querySelector('.col-serial');
+ const serialEl = row.querySelector(".col-serial");
if (serialEl && summary.device_serial) serialEl.innerText = summary.device_serial;
- const firmwareEl = row.querySelector('.col-firmware');
+ const firmwareEl = row.querySelector(".col-firmware");
if (firmwareEl && summary.firmware_version) firmwareEl.innerText = summary.firmware_version;
- const deviceIdEl = row.querySelector('.col-deviceid');
+ const deviceIdEl = row.querySelector(".col-deviceid");
if (deviceIdEl && summary.device_id) deviceIdEl.innerText = summary.device_id;
- const accountIdEl = row.querySelector('.col-accountid');
+ const accountIdEl = row.querySelector(".col-accountid");
if (accountIdEl && summary.account_id) accountIdEl.innerText = summary.account_id;
}
- document.getElementById('ssh-status').innerText = summary.ssh_success ? '✅ Success' : '❌ Failed';
- document.getElementById('ssh-status').style.color = summary.ssh_success ? 'green' : 'red';
+ document.getElementById("ssh-status").innerText = summary.ssh_success ? "✅ Success" : "❌ Failed";
+ document.getElementById("ssh-status").style.color = summary.ssh_success ? "green" : "red";
- const migrationStatus = document.getElementById('migration-status');
- migrationStatus.innerText = summary.is_migrated ? '✅ Migrated to AfterTouch' : '❌ Not Migrated';
- migrationStatus.style.color = summary.is_migrated ? 'green' : 'red';
- migrationStatus.style.fontWeight = 'bold';
+ const migrationStatus = document.getElementById("migration-status");
+ migrationStatus.innerText = summary.is_migrated ? "✅ Migrated to AfterTouch" : "❌ Not Migrated";
+ migrationStatus.style.color = summary.is_migrated ? "green" : "red";
+ migrationStatus.style.fontWeight = "bold";
- document.getElementById('original-config-status').style.display = summary.original_config ? 'block' : 'none';
- document.getElementById('no-original-config-status').style.display = summary.original_config ? 'none' : 'block';
- document.getElementById('original-config-content').innerText = summary.original_config || '';
- document.getElementById('original-config-pane').style.display = 'none';
+ document.getElementById("original-config-status").style.display = summary.original_config ? "block" : "none";
+ document.getElementById("no-original-config-status").style.display = summary.original_config ? "none" : "block";
+ document.getElementById("original-config-content").innerText = summary.original_config || "";
+ document.getElementById("original-config-pane").style.display = "none";
if (summary.parsed_current_config) {
- document.getElementById('service-options').style.display = 'block';
- document.getElementById('orig-marge').innerText = summary.parsed_current_config.margeServerUrl;
- document.getElementById('orig-stats').innerText = summary.parsed_current_config.statsServerUrl;
- document.getElementById('orig-sw_update').innerText = summary.parsed_current_config.swUpdateUrl;
- document.getElementById('orig-bmx').innerText = summary.parsed_current_config.bmxRegistryUrl;
+ document.getElementById("service-options").style.display = "block";
+ document.getElementById("orig-marge").innerText = summary.parsed_current_config.margeServerUrl;
+ document.getElementById("orig-stats").innerText = summary.parsed_current_config.statsServerUrl;
+ document.getElementById("orig-sw_update").innerText = summary.parsed_current_config.swUpdateUrl;
+ document.getElementById("orig-bmx").innerText = summary.parsed_current_config.bmxRegistryUrl;
} else {
- document.getElementById('service-options').style.display = 'none';
+ document.getElementById("service-options").style.display = "none";
}
- const remoteStatus = document.getElementById('remote-services-status');
- const remoteFound = document.getElementById('remote-services-found');
+ const remoteStatus = document.getElementById("remote-services-status");
+ const remoteFound = document.getElementById("remote-services-found");
if (summary.ssh_success) {
if (summary.remote_services_enabled) {
- remoteStatus.innerText = summary.remote_services_persistent ? '✅ Yes' : '⚠️ Yes (non-persistent)';
- remoteStatus.style.color = summary.remote_services_persistent ? 'green' : 'orange';
+ remoteStatus.innerText = summary.remote_services_persistent ? "✅ Yes" : "⚠️ Yes (non-persistent)";
+ remoteStatus.style.color = summary.remote_services_persistent ? "green" : "orange";
} else {
- remoteStatus.innerText = '❌ No';
- remoteStatus.style.color = 'red';
+ remoteStatus.innerText = "❌ No";
+ remoteStatus.style.color = "red";
}
- remoteFound.innerText = summary.remote_services_found && summary.remote_services_found.length > 0
- ? '(' + summary.remote_services_found.join(', ') + ')'
- : '';
+ remoteFound.innerText = summary.remote_services_found && summary.remote_services_found.length > 0 ? "(" + summary.remote_services_found.join(", ") + ")" : "";
- const caTrustStatus = document.getElementById('ca-trust-status');
- caTrustStatus.innerText = summary.ca_cert_trusted ? '✅ Yes' : '❌ No';
- caTrustStatus.style.color = summary.ca_cert_trusted ? 'green' : 'red';
- document.getElementById('trust-ca-btn').style.display = summary.ca_cert_trusted ? 'none' : 'inline-block';
- document.getElementById('trust-ca-btn').onclick = () => trustCA(deviceId, ip);
+ const caTrustStatus = document.getElementById("ca-trust-status");
+ caTrustStatus.innerText = summary.ca_cert_trusted ? "✅ Yes" : "❌ No";
+ caTrustStatus.style.color = summary.ca_cert_trusted ? "green" : "red";
+ document.getElementById("trust-ca-btn").style.display = summary.ca_cert_trusted ? "none" : "inline-block";
+ document.getElementById("trust-ca-btn").onclick = () => trustCA(deviceId, ip);
} else {
- remoteStatus.innerText = '❓ Unknown';
- remoteStatus.style.color = 'gray';
- remoteFound.innerText = '';
+ remoteStatus.innerText = "❓ Unknown";
+ remoteStatus.style.color = "gray";
+ remoteFound.innerText = "";
- const caTrustStatus = document.getElementById('ca-trust-status');
- caTrustStatus.innerText = '❓ Unknown';
- caTrustStatus.style.color = 'gray';
+ const caTrustStatus = document.getElementById("ca-trust-status");
+ caTrustStatus.innerText = "❓ Unknown";
+ caTrustStatus.style.color = "gray";
}
- const currentConfigElem = document.getElementById('current-config');
+ const currentConfigElem = document.getElementById("current-config");
currentConfigElem.innerText = summary.current_config;
- currentConfigElem.style.color = summary.ssh_success ? 'black' : 'red';
+ currentConfigElem.style.color = summary.ssh_success ? "black" : "red";
- document.getElementById('planned-config').innerText = summary.planned_config;
- document.getElementById('planned-hosts').innerText = summary.planned_hosts || '';
- document.getElementById('planned-resolv').innerText = summary.planned_resolv || '';
+ document.getElementById("planned-config").innerText = summary.planned_config;
+ document.getElementById("planned-hosts").innerText = summary.planned_hosts || "";
+ document.getElementById("planned-resolv").innerText = summary.planned_resolv || "";
- const currentResolvElem = document.getElementById('current-resolv-content');
+ const currentResolvElem = document.getElementById("current-resolv-content");
if (currentResolvElem) {
- currentResolvElem.innerText = summary.current_resolv_conf || 'Not available';
+ currentResolvElem.innerText = summary.current_resolv_conf || "Not available";
}
- const testUrlElem = document.getElementById('test-url');
- testUrlElem.innerText = summary.server_https_url || 'N/A';
- const testResultDiv = document.getElementById('test-result');
- testResultDiv.style.display = 'none';
- testResultDiv.innerText = '';
+ const testUrlElem = document.getElementById("test-url");
+ testUrlElem.innerText = summary.server_https_url || "N/A";
+ const testResultDiv = document.getElementById("test-result");
+ testResultDiv.style.display = "none";
+ testResultDiv.innerText = "";
- document.getElementById('test-connection-explicit-btn').onclick = () => testConnection(deviceId, true);
- document.getElementById('test-connection-trusted-btn').onclick = () => testConnection(deviceId, false);
- document.getElementById('test-hosts-btn').onclick = () => testHostsRedirection(deviceId);
- document.getElementById('test-dns-btn').onclick = () => testDNSRedirection(deviceId);
+ document.getElementById("test-connection-explicit-btn").onclick = () => testConnection(deviceId, true);
+ document.getElementById("test-connection-trusted-btn").onclick = () => testConnection(deviceId, false);
+ document.getElementById("test-hosts-btn").onclick = () => testHostsRedirection(deviceId);
+ document.getElementById("test-dns-btn").onclick = () => testDNSRedirection(deviceId);
toggleMigrationMethod();
- const migrateBtn = document.getElementById('confirm-migrate-btn');
+ const migrateBtn = document.getElementById("confirm-migrate-btn");
migrateBtn.onclick = () => migrate(deviceId, ip);
migrateBtn.disabled = !summary.ssh_success;
- const revertBtn = document.getElementById('revert-migrate-btn');
+ const revertBtn = document.getElementById("revert-migrate-btn");
revertBtn.onclick = () => revert(deviceId, ip);
revertBtn.disabled = !summary.ssh_success;
- revertBtn.style.display = summary.original_config ? 'inline-block' : 'none';
+ revertBtn.style.display = summary.original_config ? "inline-block" : "none";
- const rebootBtn = document.getElementById('reboot-speaker-btn');
+ const rebootBtn = document.getElementById("reboot-speaker-btn");
rebootBtn.onclick = () => reboot(deviceId, ip);
rebootBtn.disabled = !summary.ssh_success;
- rebootBtn.style.border = 'none'; // Reset border if it was set during migration
+ rebootBtn.style.border = "none"; // Reset border if it was set during migration
- const remoteBtn = document.getElementById('ensure-remote-btn');
+ const remoteBtn = document.getElementById("ensure-remote-btn");
remoteBtn.onclick = () => ensureRemoteServices(deviceId, ip);
remoteBtn.disabled = !summary.ssh_success;
- const removeRemoteBtn = document.getElementById('remove-remote-btn');
+ const removeRemoteBtn = document.getElementById("remove-remote-btn");
removeRemoteBtn.onclick = () => removeRemoteServices(deviceId, ip);
removeRemoteBtn.disabled = !summary.ssh_success || !summary.remote_services_enabled;
- const backupBtn = document.getElementById('backup-config-btn');
+ const backupBtn = document.getElementById("backup-config-btn");
backupBtn.onclick = () => backupConfig(deviceId, ip);
backupBtn.disabled = !summary.ssh_success || !!summary.original_config;
- document.getElementById('migration-summary').style.display = 'block';
- document.getElementById('migration-summary').scrollIntoView();
+ document.getElementById("migration-summary").style.display = "block";
+ document.getElementById("migration-summary").scrollIntoView();
} catch (error) {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Error fetching summary for ' + display + ': ' + error;
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Error fetching summary for " + display + ": " + error;
}
}
function refreshSummary() {
- const deviceId = document.getElementById('summary-device-id').value;
+ const deviceId = document.getElementById("summary-device-id").value;
if (deviceId) {
showSummary(deviceId);
}
}
function showCommandOutput(result) {
- const outputBox = document.getElementById('command-output-box');
- const outputText = document.getElementById('command-output');
+ const outputBox = document.getElementById("command-output-box");
+ const outputText = document.getElementById("command-output");
if (outputBox && outputText && result.output) {
- outputBox.style.display = 'block';
+ outputBox.style.display = "block";
outputText.innerText = result.output;
} else if (outputBox) {
- outputBox.style.display = 'none';
+ outputBox.style.display = "none";
}
}
async function revert(deviceId, ip) {
if (!deviceId) {
- alert('Please select a device.');
+ alert("Please select a device.");
return;
}
const display = getDeviceDisplayName(deviceId);
- if (!confirm('Are you sure you want to revert ' + display + ' to Bose cloud defaults?')) {
+ if (!confirm("Are you sure you want to revert " + display + " to Bose cloud defaults?",)) {
return;
}
- const summaryDiv = document.getElementById('migration-summary');
- summaryDiv.style.display = 'none';
+ const summaryDiv = document.getElementById("migration-summary");
+ summaryDiv.style.display = "none";
- const statusDiv = document.getElementById('status');
- statusDiv.style.display = 'block';
- statusDiv.style.backgroundColor = '#ffffcc';
- statusDiv.innerHTML = 'Reverting ' + display + ' to defaults...';
+ const statusDiv = document.getElementById("status");
+ statusDiv.style.display = "block";
+ statusDiv.style.backgroundColor = "#ffffcc";
+ statusDiv.innerHTML = "Reverting " + display + " to defaults...";
try {
- const response = await fetch('/setup/revert/' + encodeURIComponent(deviceId), { method: 'POST' });
+ const response = await fetch("/setup/revert/" + encodeURIComponent(deviceId), {method: "POST"},);
const result = await response.json();
showCommandOutput(result);
if (result.ok) {
- statusDiv.style.backgroundColor = '#ccffcc';
- statusDiv.innerHTML = 'Successfully started revert for ' + display + '.';
+ statusDiv.style.backgroundColor = "#ccffcc";
+ statusDiv.innerHTML = "Successfully started revert for " + display + ".";
} else {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Revert failed for ' + display + ': ' + (result.message || 'Unknown error');
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Revert failed for " + display + ": " + (result.message || "Unknown error");
}
} catch (error) {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Error reverting ' + display + ': ' + error;
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Error reverting " + display + ": " + error;
}
}
async function reboot(deviceId, ip) {
if (!deviceId) {
- alert('Please select a device.');
+ alert("Please select a device.");
return;
}
const display = getDeviceDisplayName(deviceId);
- if (!confirm('Are you sure you want to reboot the speaker at ' + display + '?')) {
+ if (!confirm("Are you sure you want to reboot the speaker at " + display + "?")) {
return;
}
- const statusDiv = document.getElementById('status');
- statusDiv.style.display = 'block';
- statusDiv.style.backgroundColor = '#ffffcc';
- statusDiv.innerHTML = 'Rebooting ' + display + '...';
+ const statusDiv = document.getElementById("status");
+ statusDiv.style.display = "block";
+ statusDiv.style.backgroundColor = "#ffffcc";
+ statusDiv.innerHTML = "Rebooting " + display + "...";
try {
- const response = await fetch('/setup/reboot/' + encodeURIComponent(deviceId), { method: 'POST' });
+ const response = await fetch("/setup/reboot/" + encodeURIComponent(deviceId), {method: "POST"},);
const result = await response.json();
showCommandOutput(result);
if (result.ok) {
- statusDiv.style.backgroundColor = '#ccffcc';
- statusDiv.innerHTML = 'Successfully started reboot for ' + display + '.';
+ statusDiv.style.backgroundColor = "#ccffcc";
+ statusDiv.innerHTML = "Successfully started reboot for " + display + ".";
} else {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Reboot failed for ' + display + ': ' + (result.message || 'Unknown error');
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Reboot failed for " + display + ": " + (result.message || "Unknown error");
}
} catch (error) {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Error rebooting ' + display + ': ' + error;
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Error rebooting " + display + ": " + error;
}
}
async function migrate(deviceId, ip) {
if (!deviceId) {
- alert('Please select a device.');
+ alert("Please select a device.");
return;
}
- const targetUrl = document.getElementById('target-domain').value;
- const method = document.getElementById('migration-method').value;
+ const targetUrl = document.getElementById("target-domain").value;
+ const method = document.getElementById("migration-method").value;
const opts = {
- marge: document.getElementById('opt-marge').value,
- stats: document.getElementById('opt-stats').value,
- sw_update: document.getElementById('opt-sw_update').value,
- bmx: document.getElementById('opt-bmx').value
+ marge: document.getElementById("opt-marge").value,
+ stats: document.getElementById("opt-stats").value,
+ sw_update: document.getElementById("opt-sw_update").value,
+ bmx: document.getElementById("opt-bmx").value,
};
- const summaryDiv = document.getElementById('migration-summary');
- summaryDiv.style.display = 'none';
+ const summaryDiv = document.getElementById("migration-summary");
+ summaryDiv.style.display = "none";
- const statusDiv = document.getElementById('status');
- statusDiv.style.display = 'block';
- statusDiv.style.backgroundColor = '#ffffcc';
+ const statusDiv = document.getElementById("status");
+ statusDiv.style.display = "block";
+ statusDiv.style.backgroundColor = "#ffffcc";
const display = getDeviceDisplayName(deviceId);
- statusDiv.innerHTML = 'Migrating ' + display + ' using ' + method + '...';
+ statusDiv.innerHTML = "Migrating " + display + " using " + method + "...";
- let query = '?method=' + encodeURIComponent(method) + '&target_url=' + encodeURIComponent(targetUrl);
+ let query = "?method=" + encodeURIComponent(method) + "&target_url=" + encodeURIComponent(targetUrl);
for (let k in opts) {
- query += '&' + k + '=' + encodeURIComponent(opts[k]);
+ query += "&" + k + "=" + encodeURIComponent(opts[k]);
}
try {
- const response = await fetch('/setup/migrate/' + encodeURIComponent(deviceId) + query, { method: 'POST' });
+ const response = await fetch("/setup/migrate/" + encodeURIComponent(deviceId) + query, {method: "POST"},);
const result = await response.json();
showCommandOutput(result);
if (result.ok) {
- statusDiv.style.backgroundColor = '#ccffcc';
- statusDiv.innerHTML = 'Successfully started migration for ' + display + '.
Please reboot the device to activate the changes. ';
+ statusDiv.style.backgroundColor = "#ccffcc";
+ statusDiv.innerHTML = "Successfully started migration for " + display + ".
Please reboot the device to activate the changes. ";
// Make reboot button available and prominent
- const rebootBtn = document.getElementById('reboot-speaker-btn');
- rebootBtn.style.display = 'inline-block';
+ const rebootBtn = document.getElementById("reboot-speaker-btn");
+ rebootBtn.style.display = "inline-block";
rebootBtn.disabled = false;
- rebootBtn.style.border = '2px solid #000';
+ rebootBtn.style.border = "2px solid #000";
// Re-show summary but with prominence on reboot
- summaryDiv.style.display = 'block';
+ summaryDiv.style.display = "block";
} else {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Migration failed for ' + display + ': ' + (result.message || 'Unknown error');
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Migration failed for " + display + ": " + (result.message || "Unknown error");
}
} catch (error) {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Error migrating ' + display + ': ' + error;
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Error migrating " + display + ": " + error;
}
}
async function trustCA(deviceId, ip) {
if (!deviceId) {
- alert('Please select a device.');
+ alert("Please select a device.");
return;
}
- const statusDiv = document.getElementById('status');
- statusDiv.style.display = 'block';
- statusDiv.style.backgroundColor = '#ffffcc';
+ const statusDiv = document.getElementById("status");
+ statusDiv.style.display = "block";
+ statusDiv.style.backgroundColor = "#ffffcc";
const display = getDeviceDisplayName(deviceId);
- statusDiv.innerHTML = 'Injecting Root CA into shared trust store on ' + display + '...';
+ statusDiv.innerHTML = "Injecting Root CA into shared trust store on " + display + "...";
try {
- const response = await fetch('/setup/trust-ca/' + encodeURIComponent(deviceId), { method: 'POST' });
+ const response = await fetch("/setup/trust-ca/" + encodeURIComponent(deviceId), {method: "POST"},);
const result = await response.json();
showCommandOutput(result);
if (result.ok) {
- statusDiv.style.backgroundColor = '#ccffcc';
- statusDiv.innerHTML = 'Successfully injected Root CA on ' + display + '.';
+ statusDiv.style.backgroundColor = "#ccffcc";
+ statusDiv.innerHTML = "Successfully injected Root CA on " + display + ".";
showSummary(deviceId); // Refresh to update status
} else {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Failed to trust CA on ' + display + ': ' + (result.message || 'Unknown error');
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Failed to trust CA on " + display + ": " + (result.message || "Unknown error");
}
} catch (error) {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Error trusting CA on ' + display + ': ' + error;
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Error trusting CA on " + display + ": " + error;
}
}
async function ensureRemoteServices(deviceId, ip) {
if (!deviceId) {
- alert('Please select a device.');
+ alert("Please select a device.");
return;
}
- const summaryDiv = document.getElementById('migration-summary');
- summaryDiv.style.display = 'none';
+ const summaryDiv = document.getElementById("migration-summary");
+ summaryDiv.style.display = "none";
- const statusDiv = document.getElementById('status');
- statusDiv.style.display = 'block';
- statusDiv.style.backgroundColor = '#ffffcc';
+ const statusDiv = document.getElementById("status");
+ statusDiv.style.display = "block";
+ statusDiv.style.backgroundColor = "#ffffcc";
const display = getDeviceDisplayName(deviceId);
- statusDiv.innerHTML = 'Ensuring remote services for ' + display + '...';
+ statusDiv.innerHTML = "Ensuring remote services for " + display + "...";
try {
- const response = await fetch('/setup/ensure-remote-services/' + encodeURIComponent(deviceId), { method: 'POST' });
+ const response = await fetch("/setup/ensure-remote-services/" + encodeURIComponent(deviceId), {method: "POST"},);
const result = await response.json();
showCommandOutput(result);
if (result.ok) {
- statusDiv.style.backgroundColor = '#ccffcc';
- statusDiv.innerHTML = 'Successfully ensured remote services for ' + display + '.';
+ statusDiv.style.backgroundColor = "#ccffcc";
+ statusDiv.innerHTML = "Successfully ensured remote services for " + display + ".";
} else {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Failed to ensure remote services for ' + display + ': ' + (result.message || 'Unknown error');
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Failed to ensure remote services for " + display + ": " + (result.message || "Unknown error");
}
} catch (error) {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Error ensuring remote services for ' + display + ': ' + error;
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Error ensuring remote services for " + display + ": " + error;
}
}
async function removeRemoteServices(deviceId, ip) {
if (!deviceId) {
- alert('Please select a device.');
+ alert("Please select a device.");
return;
}
const display = getDeviceDisplayName(deviceId);
- if (!confirm('Are you sure you want to remove remote services from ' + display + '?')) {
+ if (!confirm("Are you sure you want to remove remote services from " + display + "?",)) {
return;
}
- const summaryDiv = document.getElementById('migration-summary');
- summaryDiv.style.display = 'none';
+ const summaryDiv = document.getElementById("migration-summary");
+ summaryDiv.style.display = "none";
- const statusDiv = document.getElementById('status');
- statusDiv.style.display = 'block';
- statusDiv.style.backgroundColor = '#ffffcc';
- statusDiv.innerHTML = 'Removing remote services for ' + display + '...';
+ const statusDiv = document.getElementById("status");
+ statusDiv.style.display = "block";
+ statusDiv.style.backgroundColor = "#ffffcc";
+ statusDiv.innerHTML = "Removing remote services for " + display + "...";
try {
- const response = await fetch('/setup/remove-remote-services/' + encodeURIComponent(deviceId), { method: 'POST' });
+ const response = await fetch("/setup/remove-remote-services/" + encodeURIComponent(deviceId), {method: "POST"},);
const result = await response.json();
showCommandOutput(result);
if (result.ok) {
- statusDiv.style.backgroundColor = '#ccffcc';
- statusDiv.innerHTML = 'Successfully removed remote services from ' + display + '.';
+ statusDiv.style.backgroundColor = "#ccffcc";
+ statusDiv.innerHTML = "Successfully removed remote services from " + display + ".";
} else {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Failed to remove remote services for ' + display + ': ' + (result.message || 'Unknown error');
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Failed to remove remote services for " + display + ": " + (result.message || "Unknown error");
}
} catch (error) {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Error removing remote services for ' + display + ': ' + error;
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Error removing remote services for " + display + ": " + error;
}
}
async function backupConfig(deviceId, ip) {
if (!deviceId) {
- alert('Please select a device.');
+ alert("Please select a device.");
return;
}
- const statusDiv = document.getElementById('status');
- statusDiv.style.display = 'block';
- statusDiv.style.backgroundColor = '#ffffcc';
+ const statusDiv = document.getElementById("status");
+ statusDiv.style.display = "block";
+ statusDiv.style.backgroundColor = "#ffffcc";
const display = getDeviceDisplayName(deviceId);
- statusDiv.innerHTML = 'Creating backup for ' + display + '...';
+ statusDiv.innerHTML = "Creating backup for " + display + "...";
try {
- const response = await fetch('/setup/backup/' + encodeURIComponent(deviceId), { method: 'POST' });
+ const response = await fetch("/setup/backup/" + encodeURIComponent(deviceId), {method: "POST"},);
const result = await response.json();
showCommandOutput(result);
if (result.ok) {
- statusDiv.style.backgroundColor = '#ccffcc';
- statusDiv.innerHTML = 'Successfully created backup for ' + display + '.';
+ statusDiv.style.backgroundColor = "#ccffcc";
+ statusDiv.innerHTML = "Successfully created backup for " + display + ".";
showSummary(deviceId); // Refresh
} else {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Backup failed for ' + display + ': ' + (result.message || 'Unknown error');
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Backup failed for " + display + ": " + (result.message || "Unknown error");
}
} catch (error) {
- statusDiv.style.backgroundColor = '#ffcccc';
- statusDiv.innerHTML = 'Error creating backup for ' + display + ': ' + error;
+ statusDiv.style.backgroundColor = "#ffcccc";
+ statusDiv.innerHTML = "Error creating backup for " + display + ": " + error;
}
}
async function testConnection(deviceId, useExplicitCA) {
- const testUrl = document.getElementById('test-url').innerText;
- const testResultDiv = document.getElementById('test-result');
+ const testUrl = document.getElementById("test-url").innerText;
+ const testResultDiv = document.getElementById("test-result");
const display = getDeviceDisplayName(deviceId);
- testResultDiv.style.display = 'block';
- testResultDiv.style.backgroundColor = '#f0f0f0';
- testResultDiv.style.color = 'black';
- testResultDiv.innerText = 'Running connection test from ' + display + '...\n(This may take a few seconds)';
+ testResultDiv.style.display = "block";
+ testResultDiv.style.backgroundColor = "#f0f0f0";
+ testResultDiv.style.color = "black";
+ testResultDiv.innerText = "Running connection test from " + display + "...\n(This may take a few seconds)";
try {
const query = `?target_url=${encodeURIComponent(testUrl)}&use_explicit_ca=${useExplicitCA}`;
- const response = await fetch(`/setup/test-connection/${encodeURIComponent(deviceId)}${query}`, { method: 'POST' });
+ const response = await fetch(`/setup/test-connection/${encodeURIComponent(deviceId)}${query}`, {method: "POST"},);
const result = await response.json();
if (result.ok) {
- testResultDiv.style.backgroundColor = '#ccffcc';
- testResultDiv.innerText = '✅ ' + result.message + '\n\nOutput:\n' + result.output;
+ testResultDiv.style.backgroundColor = "#ccffcc";
+ testResultDiv.innerText = "✅ " + result.message + "\n\nOutput:\n" + result.output;
} else {
- testResultDiv.style.backgroundColor = '#ffcccc';
- testResultDiv.innerText = '❌ Connection failed: ' + result.message + '\n\nOutput:\n' + result.output;
+ testResultDiv.style.backgroundColor = "#ffcccc";
+ testResultDiv.innerText = "❌ Connection failed: " + result.message + "\n\nOutput:\n" + result.output;
}
} catch (error) {
- testResultDiv.style.backgroundColor = '#ffcccc';
- testResultDiv.innerText = '❌ Error triggering test: ' + error;
+ testResultDiv.style.backgroundColor = "#ffcccc";
+ testResultDiv.innerText = "❌ Error triggering test: " + error;
}
}
async function testHostsRedirection(deviceId) {
- const targetUrl = document.getElementById('target-domain').value;
- const testResultDiv = document.getElementById('hosts-test-result');
+ const targetUrl = document.getElementById("target-domain").value;
+ const testResultDiv = document.getElementById("hosts-test-result");
const display = getDeviceDisplayName(deviceId);
- testResultDiv.style.display = 'block';
- testResultDiv.style.backgroundColor = '#f0f0f0';
- testResultDiv.style.color = 'black';
- testResultDiv.innerText = 'Running hosts redirection test from ' + display + '...\n(This may take a few seconds)';
+ testResultDiv.style.display = "block";
+ testResultDiv.style.backgroundColor = "#f0f0f0";
+ testResultDiv.style.color = "black";
+ testResultDiv.innerText = "Running hosts redirection test from " + display + "...\n(This may take a few seconds)";
try {
const query = `?target_url=${encodeURIComponent(targetUrl)}`;
- const response = await fetch(`/setup/test-hosts/${encodeURIComponent(deviceId)}${query}`, { method: 'POST' });
+ const response = await fetch(`/setup/test-hosts/${encodeURIComponent(deviceId)}${query}`, {method: "POST"},);
const result = await response.json();
if (result.ok) {
- testResultDiv.style.backgroundColor = '#ccffcc';
- testResultDiv.innerText = '✅ ' + result.message + '\n\nOutput:\n' + result.output;
+ testResultDiv.style.backgroundColor = "#ccffcc";
+ testResultDiv.innerText = "✅ " + result.message + "\n\nOutput:\n" + result.output;
} else {
- testResultDiv.style.backgroundColor = '#ffcccc';
- testResultDiv.innerText = '❌ Test failed: ' + result.message + '\n\nOutput:\n' + result.output;
+ testResultDiv.style.backgroundColor = "#ffcccc";
+ testResultDiv.innerText = "❌ Test failed: " + result.message + "\n\nOutput:\n" + result.output;
}
} catch (error) {
- testResultDiv.style.backgroundColor = '#ffcccc';
- testResultDiv.innerText = '❌ Error triggering test: ' + error;
+ testResultDiv.style.backgroundColor = "#ffcccc";
+ testResultDiv.innerText = "❌ Error triggering test: " + error;
}
}
async function testDNSRedirection(deviceId) {
- const targetUrl = document.getElementById('target-domain').value;
- const testResultDiv = document.getElementById('dns-test-result');
+ const targetUrl = document.getElementById("target-domain").value;
+ const testResultDiv = document.getElementById("dns-test-result");
const display = getDeviceDisplayName(deviceId);
- testResultDiv.style.display = 'block';
- testResultDiv.style.backgroundColor = '#f0f0f0';
- testResultDiv.style.color = 'black';
- testResultDiv.innerText = 'Running DNS redirection test from ' + display + '...\n(This may take a few seconds)';
+ testResultDiv.style.display = "block";
+ testResultDiv.style.backgroundColor = "#f0f0f0";
+ testResultDiv.style.color = "black";
+ testResultDiv.innerText = "Running DNS redirection test from " + display + "...\n(This may take a few seconds)";
try {
const query = `?target_url=${encodeURIComponent(targetUrl)}`;
- const response = await fetch(`/setup/test-dns/${encodeURIComponent(deviceId)}${query}`, { method: 'POST' });
+ const response = await fetch(`/setup/test-dns/${encodeURIComponent(deviceId)}${query}`, {method: "POST"},);
const result = await response.json();
if (result.ok) {
- testResultDiv.style.backgroundColor = '#ccffcc';
- testResultDiv.innerText = '✅ ' + result.message + '\n\nOutput:\n' + result.output;
+ testResultDiv.style.backgroundColor = "#ccffcc";
+ testResultDiv.innerText = "✅ " + result.message + "\n\nOutput:\n" + result.output;
} else {
- testResultDiv.style.backgroundColor = '#ffcccc';
- testResultDiv.innerText = '❌ Test failed: ' + result.message + '\n\nOutput:\n' + result.output;
+ testResultDiv.style.backgroundColor = "#ffcccc";
+ testResultDiv.innerText = "❌ Test failed: " + result.message + "\n\nOutput:\n" + result.output;
}
} catch (error) {
- testResultDiv.style.backgroundColor = '#ffcccc';
- testResultDiv.innerText = '❌ Error triggering test: ' + error;
+ testResultDiv.style.backgroundColor = "#ffcccc";
+ testResultDiv.innerText = "❌ Error triggering test: " + error;
}
}
function toggleOriginalConfig() {
- const pane = document.getElementById('original-config-pane');
- pane.style.display = pane.style.display === 'none' ? 'block' : 'none';
+ const pane = document.getElementById("original-config-pane");
+ pane.style.display = pane.style.display === "none" ? "block" : "none";
}
async function toggleMigrationMethod() {
- const method = document.getElementById('migration-method').value;
- const xmlDiffPane = document.getElementById('xml-diff-pane');
- const plannedXmlPane = document.getElementById('planned-xml-pane');
- const plannedHostsPane = document.getElementById('planned-hosts-pane');
- const plannedResolvPane = document.getElementById('planned-resolv-pane');
- const currentResolvPane = document.getElementById('current-resolv-pane');
- const serviceOptions = document.getElementById('service-options');
- const hostsTestPane = document.getElementById('hosts-redirection-test');
- const dnsTestPane = document.getElementById('dns-redirection-test');
+ const method = document.getElementById("migration-method").value;
+ const xmlDiffPane = document.getElementById("xml-diff-pane");
+ const plannedXmlPane = document.getElementById("planned-xml-pane");
+ const plannedHostsPane = document.getElementById("planned-hosts-pane");
+ const plannedResolvPane = document.getElementById("planned-resolv-pane");
+ const currentResolvPane = document.getElementById("current-resolv-pane");
+ const serviceOptions = document.getElementById("service-options");
+ const hostsTestPane = document.getElementById("hosts-redirection-test");
+ const dnsTestPane = document.getElementById("dns-redirection-test");
- const dnsWarning = document.getElementById('dns-port-warning');
+ const dnsWarning = document.getElementById("dns-port-warning");
- if (method === 'hosts') {
- xmlDiffPane.style.display = 'none';
- plannedXmlPane.style.display = 'none';
- plannedHostsPane.style.display = 'block';
- plannedResolvPane.style.display = 'none';
- currentResolvPane.style.display = 'none';
- serviceOptions.style.display = 'none';
- hostsTestPane.style.display = 'block';
- dnsTestPane.style.display = 'none';
- if (dnsWarning) dnsWarning.style.display = 'none';
- } else if (method === 'resolv') {
- xmlDiffPane.style.display = 'none';
- plannedXmlPane.style.display = 'none';
- plannedHostsPane.style.display = 'none';
- plannedResolvPane.style.display = 'block';
- currentResolvPane.style.display = 'none';
- serviceOptions.style.display = 'none';
- hostsTestPane.style.display = 'none';
- dnsTestPane.style.display = 'block';
+ if (method === "hosts") {
+ xmlDiffPane.style.display = "none";
+ plannedXmlPane.style.display = "none";
+ plannedHostsPane.style.display = "block";
+ plannedResolvPane.style.display = "none";
+ currentResolvPane.style.display = "none";
+ serviceOptions.style.display = "none";
+ hostsTestPane.style.display = "block";
+ dnsTestPane.style.display = "none";
+ if (dnsWarning) dnsWarning.style.display = "none";
+ } else if (method === "resolv") {
+ xmlDiffPane.style.display = "none";
+ plannedXmlPane.style.display = "none";
+ plannedHostsPane.style.display = "none";
+ plannedResolvPane.style.display = "block";
+ currentResolvPane.style.display = "none";
+ serviceOptions.style.display = "none";
+ hostsTestPane.style.display = "none";
+ dnsTestPane.style.display = "block";
- const resolvNote = document.getElementById('resolv-note');
+ const resolvNote = document.getElementById("resolv-note");
if (resolvNote) {
- resolvNote.innerHTML = '
Note: This method injects a persistent DNS priority hook into the DHCP logic (
/etc/udhcpc.d/50default). It preserves your router\'s search domain and secondary DNS servers. It also injects the Local Root CA.';
+ resolvNote.innerHTML = "
Note: This method injects a persistent DNS priority hook into the DHCP logic (
/etc/udhcpc.d/50default). It preserves your router's search domain and secondary DNS servers. It also injects the Local Root CA.";
}
// Check DNS settings
try {
- const response = await fetch('/setup/settings');
+ const response = await fetch("/setup/settings");
const settings = await response.json();
- const dnsBind = settings.dns_bind_addr || '';
- const isPort53 = dnsBind.endsWith(':53') || dnsBind === '53';
+ const dnsBind = settings.dns_bind_addr || "";
+ const isPort53 = dnsBind.endsWith(":53") || dnsBind === "53";
const isEnabled = settings.dns_enabled;
const isRunning = settings.dns_running;
const actualBind = settings.dns_actual_bind;
if (dnsWarning) {
if (!isEnabled) {
- dnsWarning.innerText = '⚠️ DNS Discovery is DISABLED in Settings. Migration will fail.';
- dnsWarning.style.display = 'block';
+ dnsWarning.innerText = "⚠️ DNS Discovery is DISABLED in Settings. Migration will fail.";
+ dnsWarning.style.display = "block";
} else if (!isPort53) {
dnsWarning.innerText = `⚠️ DNS Discovery is bound to ${dnsBind}, but port 53 is required for migration.`;
- dnsWarning.style.display = 'block';
+ dnsWarning.style.display = "block";
} else if (!isRunning) {
dnsWarning.innerText = `⚠️ DNS Discovery server is NOT RUNNING on ${dnsBind} (check for port conflicts/permissions). Migration will fail.`;
- dnsWarning.style.display = 'block';
+ dnsWarning.style.display = "block";
} else {
- dnsWarning.style.display = 'none';
+ dnsWarning.style.display = "none";
}
}
} catch (e) {
- console.error('Failed to check DNS settings', e);
+ console.error("Failed to check DNS settings", e);
}
} else {
- xmlDiffPane.style.display = 'block';
- plannedXmlPane.style.display = 'block';
- plannedHostsPane.style.display = 'none';
- plannedResolvPane.style.display = 'none';
- currentResolvPane.style.display = 'none';
- hostsTestPane.style.display = 'none';
- dnsTestPane.style.display = 'none';
+ xmlDiffPane.style.display = "block";
+ plannedXmlPane.style.display = "block";
+ plannedHostsPane.style.display = "none";
+ plannedResolvPane.style.display = "none";
+ currentResolvPane.style.display = "none";
+ hostsTestPane.style.display = "none";
+ dnsTestPane.style.display = "none";
// Only show service options if we have a parsed config
- const currentConfig = document.getElementById('current-config').innerText;
- if (currentConfig && !currentConfig.startsWith('Error') && currentConfig !== 'loading...') {
- serviceOptions.style.display = 'block';
+ const currentConfig = document.getElementById("current-config").innerText;
+ if (currentConfig && !currentConfig.startsWith("Error") && currentConfig !== "loading...") {
+ serviceOptions.style.display = "block";
}
}
}
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener("DOMContentLoaded", () => {
fetchDevices();
fetchSettings();
triggerDiscovery();
diff --git a/pkg/service/proxy/recorder.go b/pkg/service/proxy/recorder.go
index 4e40edc..b557825 100644
--- a/pkg/service/proxy/recorder.go
+++ b/pkg/service/proxy/recorder.go
@@ -49,15 +49,16 @@ type InteractionStats struct {
// Interaction represents a single recorded HTTP interaction.
type Interaction struct {
- ID string `json:"id"`
- Session string `json:"session"`
- Category string `json:"category"`
- Method string `json:"method"`
- Path string `json:"path"`
- File string `json:"file"`
- Counter int `json:"counter"`
- Status int `json:"status"`
- Timestamp string `json:"timestamp"`
+ ID string `json:"id"`
+ Session string `json:"session"`
+ Category string `json:"category"`
+ Method string `json:"method"`
+ Path string `json:"path"`
+ File string `json:"file"`
+ Counter int `json:"counter"`
+ Status int `json:"status"`
+ Timestamp string `json:"timestamp"`
+ SCMUDCData *EnrichedSCMUDCEvent `json:"scmudc_data,omitempty"`
}
// NewRecorder creates a new HTTP interaction recorder.
@@ -181,11 +182,25 @@ func (r *Recorder) Record(category string, req *http.Request, res *http.Response
}
func (r *Recorder) save(task recordingTask) {
- var buf bytes.Buffer
- r.writeRequest(&buf, task.req, task.replacements)
+ var (
+ buf bytes.Buffer
+ enriched *EnrichedSCMUDCEvent
+ )
+
+ // Check if this is a SCMUDC request and enrich it
+
+ if strings.Contains(task.req.URL.Path, "/v1/scmudc/") && task.req.Body != nil {
+ bodyBytes, err := io.ReadAll(task.req.Body)
+ if err == nil {
+ task.req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
+ enriched = enrichSCMUDCRequest(bodyBytes)
+ }
+ }
+
+ r.writeRequestWithEnrichment(&buf, task.req, task.replacements, enriched)
if task.res != nil {
- r.writeResponse(&buf, task.res)
+ r.writeResponseWithEnrichment(&buf, task.res, enriched)
}
if err := os.WriteFile(task.path, buf.Bytes(), 0644); err != nil {
@@ -239,7 +254,7 @@ func (r *Recorder) getRecordingPath(dir, method string) string {
return filepath.Join(dir, filename)
}
-func (r *Recorder) writeRequest(buf *bytes.Buffer, req *http.Request, replacements map[string]string) {
+func (r *Recorder) writeRequestWithEnrichment(buf *bytes.Buffer, req *http.Request, replacements map[string]string, enriched *EnrichedSCMUDCEvent) {
displayURL := req.URL.String()
for orig, repl := range replacements {
displayURL = strings.ReplaceAll(displayURL, orig, "{{"+strings.Trim(repl, "{}")+"}}")
@@ -252,6 +267,14 @@ func (r *Recorder) writeRequest(buf *bytes.Buffer, req *http.Request, replacemen
fmt.Fprintf(buf, "// %s: %s\n", key, orig)
}
+ // Add SCMUDC enriched comments
+ if enriched != nil {
+ enrichedComments := generateSCMUDCComments(enriched)
+ for _, comment := range enrichedComments {
+ fmt.Fprintf(buf, "%s\n", comment)
+ }
+ }
+
fmt.Fprintf(buf, "%s %s\n", req.Method, displayURL)
fmt.Fprintf(buf, "Host: %s\n", req.Host)
@@ -283,10 +306,29 @@ func (r *Recorder) writeRequest(buf *bytes.Buffer, req *http.Request, replacemen
}
}
-func (r *Recorder) writeResponse(buf *bytes.Buffer, res *http.Response) {
+func (r *Recorder) writeResponseWithEnrichment(buf *bytes.Buffer, res *http.Response, enriched *EnrichedSCMUDCEvent) {
buf.WriteString("\n")
buf.WriteString("> {% \n")
fmt.Fprintf(buf, " // Response: %d %s\n", res.StatusCode, http.StatusText(res.StatusCode))
+
+ // Add SCMUDC enrichment summary in response
+ if enriched != nil {
+ buf.WriteString(" //\n")
+ buf.WriteString(" // SCMUDC Event Analysis:\n")
+ fmt.Fprintf(buf, " // - Origin: %s (%s)\n", getOriginDescription(enriched.Origin), enriched.Origin)
+ fmt.Fprintf(buf, " // - Action: %s\n", enriched.Action)
+ fmt.Fprintf(buf, " // - Summary: %s\n", enriched.Summary)
+
+ if enriched.DecodedData != nil {
+ fmt.Fprintf(buf, " // - Content: %s\n", enriched.DecodedData.ItemName)
+
+ if enriched.DecodedData.SourceAccount != "" {
+ fmt.Fprintf(buf, " // - Account: %s\n", enriched.DecodedData.SourceAccount)
+ }
+ }
+ }
+
+ buf.WriteString(" //\n")
buf.WriteString(" // Headers:\n")
for k, vv := range res.Header {
@@ -312,8 +354,8 @@ func (r *Recorder) writeResponse(buf *bytes.Buffer, res *http.Response) {
buf.WriteString("\n/*\n")
buf.Write(bodyBytes)
buf.WriteString("\n*/\n")
- } else {
- fmt.Fprintf(buf, "\n// [Binary response body: %d bytes]\n", len(bodyBytes))
+ } else if len(bodyBytes) > 0 {
+ fmt.Fprintf(buf, "\n[Binary response body: %d bytes]\n", len(bodyBytes))
}
}
}
@@ -503,7 +545,7 @@ func (r *Recorder) parseInteractionFile(rel, path string, parts []string) (Inter
requestPath = "/"
}
- return Interaction{
+ interaction := Interaction{
ID: filename,
Session: sessionID,
Category: category,
@@ -513,7 +555,122 @@ func (r *Recorder) parseInteractionFile(rel, path string, parts []string) (Inter
Counter: counter,
Status: r.peekStatus(path),
Timestamp: timestamp,
- }, true
+ }
+
+ // Extract SCMUDC enrichment data if this is a SCMUDC request
+ if strings.Contains(requestPath, "/v1/scmudc/") {
+ interaction.SCMUDCData = r.extractSCMUDCFromFile(path)
+ }
+
+ return interaction, true
+}
+
+// extractSCMUDCFromFile parses SCMUDC enrichment data from a .http file
+func (r *Recorder) extractSCMUDCFromFile(path string) *EnrichedSCMUDCEvent {
+ content, err := os.ReadFile(path)
+ if err != nil {
+ return nil
+ }
+
+ lines := strings.Split(string(content), "\n")
+
+ var (
+ enriched EnrichedSCMUDCEvent
+ foundSCMUDC bool
+ bodyStart int
+ )
+
+ // Look for SCMUDC enrichment comments
+
+ for i, line := range lines {
+ line = strings.TrimSpace(line)
+
+ switch {
+ case strings.HasPrefix(line, "// Origin: "):
+ parts := strings.Split(line, " (")
+ if len(parts) >= 2 {
+ enriched.Origin = strings.TrimSuffix(parts[1], ")")
+ foundSCMUDC = true
+ }
+ case strings.HasPrefix(line, "// Action: "):
+ enriched.Action = strings.TrimPrefix(line, "// Action: ")
+ case strings.HasPrefix(line, "// Command: "):
+ enriched.Command = strings.TrimPrefix(line, "// Command: ")
+ case strings.HasPrefix(line, "// Summary: "):
+ enriched.Summary = strings.TrimPrefix(line, "// Summary: ")
+ case strings.HasPrefix(line, "// - Source: "):
+ r.ensureDecodedData(&enriched)
+ enriched.DecodedData.ContentType = strings.TrimPrefix(line, "// - Source: ")
+ case strings.HasPrefix(line, "// - Item: "):
+ r.ensureDecodedData(&enriched)
+ enriched.DecodedData.ItemName = strings.TrimPrefix(line, "// - Item: ")
+ case strings.HasPrefix(line, "// - Account: "):
+ r.ensureDecodedData(&enriched)
+ enriched.DecodedData.SourceAccount = strings.TrimPrefix(line, "// - Account: ")
+ case strings.HasPrefix(line, "// - Artwork: "):
+ r.ensureDecodedData(&enriched)
+ enriched.DecodedData.ArtworkURL = strings.TrimPrefix(line, "// - Artwork: ")
+ case line == "// - Presetable: Yes":
+ r.ensureDecodedData(&enriched)
+ enriched.DecodedData.IsPresetable = true
+ case line == "{" && i > 0:
+ // Found start of JSON body
+ bodyStart = i
+ goto endLoop
+ }
+ }
+
+endLoop:
+ // If we didn't find enrichment comments but this is a SCMUDC request,
+ // try to parse the JSON body directly
+ if !foundSCMUDC && bodyStart > 0 {
+ if parsed := r.parseSCMUDBody(lines, bodyStart); parsed != nil {
+ return parsed
+ }
+ }
+
+ if !foundSCMUDC {
+ return nil
+ }
+
+ return &enriched
+}
+
+func (r *Recorder) ensureDecodedData(enriched *EnrichedSCMUDCEvent) {
+ if enriched.DecodedData == nil {
+ enriched.DecodedData = &DecodedContent{}
+ }
+}
+
+func (r *Recorder) parseSCMUDBody(lines []string, bodyStart int) *EnrichedSCMUDCEvent {
+ var bodyLines []string
+
+ inBody := false
+ braceCount := 0
+
+ for i := bodyStart; i < len(lines); i++ {
+ line := lines[i]
+ if strings.TrimSpace(line) == "{" && !inBody {
+ inBody = true
+
+ bodyLines = append(bodyLines, line)
+ braceCount = 1
+ } else if inBody {
+ bodyLines = append(bodyLines, line)
+
+ braceCount += strings.Count(line, "{") - strings.Count(line, "}")
+ if braceCount == 0 {
+ break
+ }
+ }
+ }
+
+ if len(bodyLines) > 0 {
+ bodyJSON := strings.Join(bodyLines, "\n")
+ return enrichSCMUDCRequest([]byte(bodyJSON))
+ }
+
+ return nil
}
func (r *Recorder) getFullTimestamp(sessionID, filename string) string {
diff --git a/pkg/service/proxy/scmudc.go b/pkg/service/proxy/scmudc.go
new file mode 100644
index 0000000..2c7938e
--- /dev/null
+++ b/pkg/service/proxy/scmudc.go
@@ -0,0 +1,257 @@
+package proxy
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "strings"
+)
+
+// SCMUDCRequest represents the structure of SCMUDC telemetry requests
+type SCMUDCRequest struct {
+ Envelope struct {
+ MonoTime int64 `json:"monoTime"`
+ PayloadProtocolVersion string `json:"payloadProtocolVersion"`
+ PayloadType string `json:"payloadType"`
+ ProtocolVersion string `json:"protocolVersion"`
+ Time string `json:"time"`
+ UniqueID string `json:"uniqueId"`
+ } `json:"envelope"`
+ Payload struct {
+ DeviceInfo struct {
+ BoseID string `json:"boseID"`
+ DeviceID string `json:"deviceID"`
+ DeviceType string `json:"deviceType"`
+ SerialNumber string `json:"serialNumber"`
+ SoftwareVersion string `json:"softwareVersion"`
+ SystemSerialNumber string `json:"systemSerialNumber"`
+ } `json:"deviceInfo"`
+ Events []SCMUDCEvent `json:"events"`
+ } `json:"payload"`
+}
+
+// SCMUDCEvent represents individual events within SCMUDC requests
+type SCMUDCEvent struct {
+ Type string `json:"type"`
+ Data struct {
+ ButtonID string `json:"buttonId,omitempty"`
+ Origin string `json:"origin"`
+ ContentItem string `json:"contentItem,omitempty"`
+ Preset string `json:"preset,omitempty"`
+ } `json:"data"`
+}
+
+// EnrichedSCMUDCEvent contains human-readable analysis of SCMUDC events
+type EnrichedSCMUDCEvent struct {
+ Origin string `json:"origin"`
+ Action string `json:"action"`
+ Command string `json:"command"`
+ Summary string `json:"summary"`
+ DecodedData *DecodedContent `json:"decoded_data,omitempty"`
+}
+
+// DecodedContent represents decoded ContentItem XML data
+type DecodedContent struct {
+ ContentType string `json:"content_type"`
+ ItemName string `json:"item_name"`
+ SourceAccount string `json:"source_account,omitempty"`
+ Location string `json:"location,omitempty"`
+ ArtworkURL string `json:"artwork_url,omitempty"`
+ IsPresetable bool `json:"is_presetable,omitempty"`
+ XMLContent string `json:"xml_content,omitempty"`
+}
+
+// ContentItem represents the XML structure within Base64-encoded content
+type ContentItem struct {
+ XMLName xml.Name `xml:"ContentItem"`
+ Source string `xml:"source,attr"`
+ Type string `xml:"type,attr"`
+ Location string `xml:"location,attr"`
+ SourceAccount string `xml:"sourceAccount,attr"`
+ IsPresetable string `xml:"isPresetable,attr"`
+ ItemName string `xml:"itemName"`
+ ContainerArt string `xml:"containerArt"`
+}
+
+// enrichSCMUDCRequest analyzes and enriches SCMUDC request data
+func enrichSCMUDCRequest(body []byte) *EnrichedSCMUDCEvent {
+ var scmudcReq SCMUDCRequest
+ if err := json.Unmarshal(body, &scmudcReq); err != nil {
+ return nil
+ }
+
+ // Process the first event (most requests contain single events)
+ if len(scmudcReq.Payload.Events) == 0 {
+ return nil
+ }
+
+ event := scmudcReq.Payload.Events[0]
+ enriched := &EnrichedSCMUDCEvent{
+ Origin: event.Data.Origin,
+ Action: event.Type,
+ }
+
+ switch event.Data.Origin {
+ case "gabbo":
+ enriched.Command = event.Data.ButtonID
+ enriched.Summary = fmt.Sprintf("App: %s", formatButton(event.Data.ButtonID))
+
+ case "console":
+ enriched.Command = event.Data.ButtonID
+ enriched.Summary = fmt.Sprintf("Device: %s", formatButton(event.Data.ButtonID))
+
+ case "device":
+ if event.Data.ContentItem != "" {
+ if decoded := decodeContentItem(event.Data.ContentItem); decoded != nil {
+ enriched.Command = decoded.ItemName
+ enriched.Summary = fmt.Sprintf("Device: %s", summarizeContent(decoded))
+ enriched.DecodedData = decoded
+ } else {
+ enriched.Command = "Content Item"
+ enriched.Summary = "Device: Content Action"
+ }
+ } else {
+ enriched.Command = "System Action"
+ enriched.Summary = "Device: Internal Action"
+ }
+ }
+
+ return enriched
+}
+
+// decodeContentItem decodes Base64-encoded XML content
+func decodeContentItem(base64Content string) *DecodedContent {
+ data, err := base64.StdEncoding.DecodeString(base64Content)
+ if err != nil {
+ return nil
+ }
+
+ var contentItem ContentItem
+ if err := xml.Unmarshal(data, &contentItem); err != nil {
+ return nil
+ }
+
+ decoded := &DecodedContent{
+ ContentType: contentItem.Source,
+ ItemName: contentItem.ItemName,
+ SourceAccount: contentItem.SourceAccount,
+ Location: contentItem.Location,
+ ArtworkURL: contentItem.ContainerArt,
+ XMLContent: string(data),
+ }
+
+ if contentItem.IsPresetable == "true" {
+ decoded.IsPresetable = true
+ }
+
+ return decoded
+}
+
+// formatButton converts button IDs to human-readable names
+func formatButton(buttonID string) string {
+ switch buttonID {
+ case "POWER":
+ return "Power Button"
+ case "PLAY":
+ return "Play Button"
+ case "PAUSE":
+ return "Pause Button"
+ case "STOP":
+ return "Stop Button"
+ case "NEXT_TRACK":
+ return "Next Track"
+ case "PREV_TRACK":
+ return "Previous Track"
+ case "PRESET_1", "PRESET_2", "PRESET_3", "PRESET_4", "PRESET_5", "PRESET_6":
+ return fmt.Sprintf("Preset %s", strings.TrimPrefix(buttonID, "PRESET_"))
+ default:
+ return buttonID
+ }
+}
+
+// summarizeContent creates a brief summary of content items
+func summarizeContent(decoded *DecodedContent) string {
+ switch decoded.ContentType {
+ case "SPOTIFY":
+ return fmt.Sprintf("Spotify: %s", decoded.ItemName)
+ case "PANDORA":
+ return fmt.Sprintf("Pandora: %s", decoded.ItemName)
+ case "INTERNET_RADIO":
+ return fmt.Sprintf("Radio: %s", decoded.ItemName)
+ case "STORED_MUSIC":
+ return fmt.Sprintf("Library: %s", decoded.ItemName)
+ default:
+ if decoded.ItemName != "" {
+ return fmt.Sprintf("%s: %s", decoded.ContentType, decoded.ItemName)
+ }
+
+ return fmt.Sprintf("%s Content", decoded.ContentType)
+ }
+}
+
+// getOriginDescription returns human-readable origin descriptions
+func getOriginDescription(origin string) string {
+ switch origin {
+ case "gabbo":
+ return "SoundTouch App"
+ case "console":
+ return "Device Hardware"
+ case "device":
+ return "Internal System"
+ default:
+ return origin
+ }
+}
+
+// generateSCMUDCComments creates enriched comments for .http files
+func generateSCMUDCComments(enriched *EnrichedSCMUDCEvent) []string {
+ if enriched == nil {
+ return nil
+ }
+
+ comments := []string{
+ fmt.Sprintf("// Origin: %s (%s)", getOriginDescription(enriched.Origin), enriched.Origin),
+ fmt.Sprintf("// Action: %s", enriched.Action),
+ fmt.Sprintf("// Command: %s", enriched.Command),
+ fmt.Sprintf("// Summary: %s", enriched.Summary),
+ }
+
+ if enriched.DecodedData != nil {
+ comments = append(comments,
+ "//",
+ "// Decoded Content:",
+ fmt.Sprintf("// - Source: %s", enriched.DecodedData.ContentType),
+ fmt.Sprintf("// - Item: %s", enriched.DecodedData.ItemName),
+ )
+
+ if enriched.DecodedData.SourceAccount != "" {
+ comments = append(comments, fmt.Sprintf("// - Account: %s", enriched.DecodedData.SourceAccount))
+ }
+
+ if enriched.DecodedData.ArtworkURL != "" {
+ comments = append(comments, fmt.Sprintf("// - Artwork: %s", enriched.DecodedData.ArtworkURL))
+ }
+
+ if enriched.DecodedData.IsPresetable {
+ comments = append(comments, "// - Presetable: Yes")
+ }
+
+ if enriched.DecodedData.XMLContent != "" {
+ comments = append(comments,
+ "//",
+ "// Full XML Content:",
+ )
+
+ // Add XML content as comments, line by line
+ lines := strings.Split(enriched.DecodedData.XMLContent, "\n")
+ for _, line := range lines {
+ if strings.TrimSpace(line) != "" {
+ comments = append(comments, fmt.Sprintf("// %s", strings.TrimSpace(line)))
+ }
+ }
+ }
+ }
+
+ return comments
+}