import { simpleTimeAgo } from '../utils/simple-time-ago.js';
function showDeviceModal(identifier, deviceType, fullData = {}) {
let modal = document.getElementById('avDeviceModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'avDeviceModal';
modal.className = 'hidden fixed inset-0 bg-black/80 flex items-center justify-center z-[100]';
modal.innerHTML = `
`;
document.body.appendChild(modal);
modal.querySelector('#modalClose').addEventListener('click', () => modal.classList.add('hidden'));
}
const titleEl = modal.querySelector('#modalTitle');
const contentEl = modal.querySelector('#modalContent');
const isSwitch = deviceType === 'switch' || fullData?.isInfra || /^SW/i.test(String(identifier));
titleEl.innerHTML = isSwitch
? `Switch / Infra: ${identifier}`
: `Device: ${identifier}`;
contentEl.innerHTML = getModalHTML(identifier, deviceType, fullData);
modal.classList.remove('hidden');
}
function getModalHTML(identifier, deviceType, data) {
const isInfra = deviceType === 'switch' || data?.isInfra || /^SW/i.test(String(identifier));
if (isInfra) {
return renderInfraSwitchModal(identifier, data || {});
}
const isAtlasAmp = !!data.isAtlasAmp || data.source === 'atlas';
const isWireless = data.meraki?.connectionType === 'Wireless' || !!data.meraki?.client?.ssid;
let html = '';
// MDM Section
if (data.source === 'mdm' && data.mdmData) {
html += renderMDMSection(data.mdmData);
}
// OptiSigns
if (data.optisigns) {
html += renderOptiSignsTopBlock(data);
}
// RED
if (data.red) {
html += renderREDSection(data.red);
}
// Atlas AMP
if (isAtlasAmp) {
html += renderAMPTopBlock(data);
}
// ====================== MERAKI GROUPED SECTION ======================
let merakiContent = '';
// Always show the Meraki Client Information section (even if no client data)
merakiContent += renderMerakiClientSection(data); // ← pass full device, not just client
// Wired devices: Show Switchport Status + Config
// Support both top-level (some shapes) and under .client (rich/build path where attach puts ports on client)
const portStatus = data.meraki?.portStatus || data.meraki?.client?.switchportStatus;
const portConfig = data.meraki?.portConfig || data.meraki?.client?.switchportConfig;
if (portStatus || portConfig) {
if (portStatus) {
merakiContent += renderSwitchportStatusSection(portStatus, portConfig);
}
if (portConfig) {
merakiContent += renderSwitchportConfigSection(portConfig);
}
}
// Wireless devices: Show Wireless Details
// Support top-level (rich) or under client
else if (data.meraki?.connectionType === 'Wireless' ||
data.meraki?.client?.ssid ||
data.meraki?.wirelessDetails ||
data.meraki?.client?.wirelessDetails) {
merakiContent += renderWirelessClientSection(data);
}
// No Meraki data at all (common for some wired MSC players)
else {
// Optional: You can add a small "No switchport data" message here if desired
}
html += sectionCard('Meraki Information', merakiContent);
return `${html}
`;
}
// ====================== CONSISTENT SECTION HELPER ======================
function sectionCard(title, content) {
return `
${title}
${content}
`;
}
// ====================== RED SECTION ======================
function renderREDSection(red) {
if (!red) return '';
const parseUTCDate = (ts) => {
if (!ts) return null;
let str = String(ts).trim();
if (!/[Z+-]/.test(str)) str += 'Z';
const date = new Date(str);
return isNaN(date.getTime()) ? null : date;
};
const lastPing = parseUTCDate(red.LastPingTimeUTC) ? simpleTimeAgo(parseUTCDate(red.LastPingTimeUTC)) : '—';
const lastStartup = parseUTCDate(red.LastStartupDateUTC || red.LastStartupDate) ? simpleTimeAgo(parseUTCDate(red.LastStartupDateUTC || red.LastStartupDate)) : '—';
const isOnline = red.Connectivity === 'Online';
const statusBadge = `
${red.Connectivity || 'Unknown'}
`;
const content = `
RED Audio Player
${statusBadge}
DEVICE ID
${red.DeviceID || '—'}
NAME
${red.Name || '—'}
DEPLOYMENT
${red.DeploymentStatusName || '—'}
AVAILABILITY
${red.AvailabilityStatus || '—'}
LAST PING
${lastPing}
LAST STARTUP
${lastStartup}
VERSION
${red.CurrentNanopointVersionString || '—'}
STATE
${red.StateTransitionStatus || '—'}
`;
return sectionCard('', content); // Title handled inside
}
// ====================== OPTISIGNS SECTION ======================
function renderOptiSignsTopBlock(data) {
const opti = data.optisigns || {};
if (!opti) return '';
const playlistName = opti.currentPlaylistName || opti.currentPlaylistId || '—';
const assetName = opti.currentAssetName || '—';
const hasContent = !!(opti.currentPlaylistId || opti.currentAssetId);
const isRecent = opti.lastHeartBeat && (Date.now() - new Date(opti.lastHeartBeat).getTime()) < 24 * 60 * 60 * 1000;
const isActive = hasContent && isRecent;
const lastHeartbeat = opti.lastHeartBeat ? simpleTimeAgo(new Date(opti.lastHeartBeat)) : '—';
const statusBadge = `
${isActive ? 'Active' : 'Inactive'}
`;
const content = `
OptiSigns
${statusBadge}
"${playlistName}"
${assetName !== '—' ? `Asset: ${assetName}
` : ''}
Last Heartbeat:
${lastHeartbeat}
`;
return sectionCard('', content);
}
// ====================== AMP SECTION ======================
function renderAMPTopBlock(data) {
let atlas = {};
if (Array.isArray(data.atlasData) && data.atlasData.length > 0) atlas = data.atlasData[0];
else if (data.atlasData && typeof data.atlasData === 'object') atlas = data.atlasData;
else if (data.atlas) atlas = data.atlas;
const state = atlas.state || {};
const model = atlas.model || {};
const firmware = atlas.firmware || {};
// Align status extraction with /avstatus command for consistency:
// prefer top-level status (from raw Atlas data) then state.status
const ampStatus = atlas.status || state.status || 'Unknown';
const isOnline = (ampStatus).toLowerCase() === 'online';
const cpuTemp = state.tempCpu ? Number(state.tempCpu).toFixed(1) + '°F' : '—';
const psuTemp = state.tempPsu ? Number(state.tempPsu).toFixed(1) + '°F' : '—';
const ioTemp = state.tempIo ? Number(state.tempIo).toFixed(1) + '°F' : '—';
const fanSpeed = state.fanSpeed ? Math.round(state.fanSpeed) + '%' : '—';
let ampHtml = '';
for (let i = 1; i <= 8; i++) {
const status = state[`ampStatus_${i}`] || '—';
const temp = state[`tempAmp_${i}`] ? Number(state[`tempAmp_${i}`]).toFixed(1) + '°F' : '—';
ampHtml += `
AMP ${i}
${status}
${temp}
`;
}
const content = `
Atlas IED Power Amplifier
${atlas.name || 'US002477AMP'}
${ampStatus}
Model
${model.name || 'AZMP8'}
Serial
${atlas.sn || '—'}
Firmware
${firmware.version || '4.5.16'}
IP
${state.IpAddress || '—'}
CPU Usage
${state.cpuUsage || '—'}%
RAM Usage
${state.ramUsage ? state.ramUsage.toFixed(1) : '—'}%
Voltage
${state.voltageMonitor || '120'}V
Last Seen
${simpleTimeAgo(atlas.last_seen_at)}
CPU Temp
${cpuTemp}
PSU Temp
${psuTemp}
Io Temp
${ioTemp}
Fan Speed
${fanSpeed}
Last Log Entry
${state.lastLogEntry || '—'}
Amplifier Channels
${ampHtml}
`;
return sectionCard('Atlas IED Power Amplifier', content);
}
/**
* Renders the Meraki Client Information section
* Handles both cases gracefully: has Meraki data OR no Meraki client (common for wired MSC on local switches)
*/
function renderMerakiClientSection(device) {
const meraki = device.meraki || {};
const client = meraki.client || null;
const connectionType = meraki.connectionType || 'Unknown';
let isOnline = client?.status === 'Online' || false;
// Fallback using lastSeen recency if the Meraki client record lacks an explicit status (or for the most-recent picked record)
if (!isOnline && client?.lastSeen) {
const d = new Date(client.lastSeen);
if (!isNaN(d.getTime())) {
const ageMins = (Date.now() - d.getTime()) / 60000;
if (ageMins < 5) isOnline = true;
// else leave false (will show Offline badge)
}
}
const lastSeen = client?.lastSeen ? simpleTimeAgo(new Date(client.lastSeen)) : '—';
const cleanMac = client?.mac
? client.mac.toUpperCase().replace(/:/g, '')
: '—';
const ip = client?.ip || '—';
// Data usage in MB (3 decimal places)
let sentMB = 0, recvMB = 0, totalMB = 0;
if (client?.usage) {
sentMB = (client.usage.sent / 1048576).toFixed(3);
recvMB = (client.usage.recv / 1048576).toFixed(3);
totalMB = (client.usage.total / 1048576).toFixed(3);
}
const statusBadge = `
${isOnline ? 'Online' : 'Offline'}
`;
const merakiLink = client?.id && device.meraki?.clientUrl
? `
View in Meraki →
`
: '';
let content = '';
if (client) {
// Has Meraki client data
content = `
Meraki Client Information
${statusBadge}
${merakiLink}
CONNECTION TYPE
${connectionType}
MAC ADDRESS
${cleanMac}
IP ADDRESS
${ip}
LAST SEEN
${lastSeen}
DATA USAGE (SINCE LAST SEEN)
`;
} else {
// No Meraki client found (common for wired MSC on local switches)
content = `
Meraki Client Information
${merakiLink}
⚠️ No Meraki Client Data
This device is likely connected to a local or non-Meraki switch/infrastructure.
Connection Type: ${connectionType}
`;
}
return sectionCard('', content);
}
// ====================== MDM SECTION ======================
function renderMDMSection(mdmData) {
if (!mdmData) return '';
const summary = mdmData.mdmDataSummary || {};
const lastSeen = mdmData.LastSeen || summary.lastSeen;
// MDM Console Link
const deviceId = mdmData.Id?.Value || summary.id || '';
const mdmLink = deviceId
? `
View in MDM Console →
`
: '';
const content = `
MDM Device Information
${mdmLink}
Username
${mdmData.UserName || summary.username || '—'}
Serial Number
${mdmData.SerialNumber || summary.serialNumber || '—'}
MAC Address
${mdmData.MacAddress || summary.macAddress || '—'}
Model
${mdmData.Model || summary.model || 'Apple TV'}
OS Version
${mdmData.OperatingSystem || summary.osVersion || '—'}
Compliance
${mdmData.ComplianceStatus || summary.complianceStatus || '—'}
Enrollment
${mdmData.EnrollmentStatus || summary.enrollmentStatus || '—'}
Location Group
${mdmData.LocationGroupName || summary.locationGroupName || '—'}
Last Seen
${lastSeen ? simpleTimeAgo(lastSeen) : '—'}
`;
return sectionCard('', content); // Title moved inside for link alignment
}
// ====================== WIRELESS SECTION ======================
function renderWirelessClientSection(data) {
const client = data.meraki?.client || {};
const wirelessDetails = data.meraki?.wirelessDetails || {};
const wirelessSummary = data.meraki?.wirelessSummary || {};
const rssi = wirelessSummary.rssi || wirelessDetails.signalQuality?.rssi || '—';
const snr = wirelessSummary.snr || wirelessDetails.signalQuality?.snr || '—';
const avgLatency = wirelessSummary.avgLatencyMs || wirelessDetails.latency?.avgLatencyMs || '—';
const health = wirelessDetails.healthScores || {};
const connStats = wirelessDetails.connectionStats || {};
const failed = wirelessDetails.failedConnections?.length || 0;
const content = `
SSID
${wirelessSummary.ssid || client.ssid || '—'}
Access Point
${wirelessSummary.apName || '—'}
RSSI
${rssi} dBm
SNR
${snr} dB
Avg Latency
${avgLatency} ms
Health Scores
Performance
${health.performance?.latest ?? '—'}
Onboarding
${health.onboarding?.latest ?? '—'}
Connection Stats
${connStats.assoc ?? 0}
Assoc Fail
${connStats.auth ?? 0}
Auth Fail
${connStats.dhcp ?? 0}
DHCP Fail
${connStats.dns ?? 0}
DNS Fail
${connStats.success ?? 0}
Success
Failed Connections
${failed}
`;
return sectionCard('Wireless Details', content);
}
// ====================== SWITCHPORT STATUS ======================
function renderSwitchportStatusSection(portStatus, portConfig) {
const enabledText = portStatus?.enabled !== false ? 'Enabled' : 'Disabled';
const enabledColor = enabledText === 'Enabled' ? 'bg-green-600 text-white' : 'bg-red-600 text-white';
const statusText = portStatus?.status || 'Unknown';
const isConnected = statusText.toLowerCase() === 'connected' || statusText.toLowerCase() === 'online';
const statusColor = isConnected ? 'bg-green-600 text-white' : 'bg-red-600 text-white';
const speedDuplex = `${portStatus?.speed || '—'} / ${portStatus?.duplex || '—'}`;
const poeAllocated = portStatus?.poe?.isAllocated ? 'Yes' : 'No';
const usageKB = portStatus?.usageInKb?.total ? portStatus.usageInKb.total.toLocaleString() : '—';
const trafficKbps = portStatus?.trafficInKbps?.total ? portStatus.trafficInKbps.total.toFixed(1) : '—';
const content = `
${enabledText}
${statusText}
Port ID
${portStatus?.portId || '—'}
Is Uplink
${portStatus?.isUplink ? 'Yes' : 'No'}
Speed / Duplex
${speedDuplex}
PoE Allocated
${poeAllocated}
Errors
${portStatus?.errors || 0}
Warnings
${portStatus?.warnings || 0}
Traffic
${trafficKbps} Kbps
Power Usage
${portStatus?.powerUsageInWh || '—'} Wh
`;
return sectionCard('Switchport Status', content);
}
// ====================== SWITCHPORT CONFIG ======================
function renderSwitchportConfigSection(portConfig) {
const enabledText = portConfig?.enabled !== false ? 'Enabled' : 'Disabled';
const enabledColor = enabledText === 'Enabled' ? 'bg-green-600 text-white' : 'bg-red-600 text-white';
const poeText = portConfig?.poeEnabled ? 'PoE Enabled' : 'PoE Disabled';
const poeColor = portConfig?.poeEnabled ? 'bg-green-600 text-white' : 'bg-gray-700 text-gray-300';
const stickyCount = (portConfig?.stickyMacAllowList || []).length;
const content = `
${enabledText}
${poeText}
Port ID
${portConfig?.portId || '—'}
Name
${portConfig?.name || '—'}
Type
${portConfig?.type || 'Access'}
VLAN
${portConfig?.vlan || '—'}
Voice VLAN
${portConfig?.voiceVlan || 'None'}
Access Policy
${portConfig?.accessPolicyType || '—'}
Sticky MAC Limit
${portConfig?.stickyMacAllowListLimit || '—'}
Sticky MAC List
${stickyCount} entries
`;
return sectionCard('Switchport Configuration', content);
}
// ====================== INFRA / SWITCH MODAL (for topology infra nodes like SW02477R) ======================
function renderInfraSwitchModal(identifier, data) {
const tNode = data.tNode || {};
const dev = (tNode.device || data.meraki?.device || data || {});
const serial = dev.serial || identifier || '—';
const name = dev.name || 'Switch / Access Point';
const model = dev.model || dev.productType || '—';
const mStatus = (data.status || dev.status || 'Unknown').toString();
const isOnline = /online/i.test(mStatus);
const statusBadge = `
${mStatus || 'Unknown'}
`;
const connected = Array.isArray(data.connectedAVs) ? data.connectedAVs : [];
let connectedHtml = '';
if (connected.length) {
connectedHtml = `
Connected AV Devices (${connected.length})
${connected.map(av => {
const c = av.meraki?.client || av.meraki || {};
const ip = c.ip || av.ip || '—';
const ago = c.lastSeen ? (typeof simpleTimeAgo === 'function' ? simpleTimeAgo(new Date(c.lastSeen)) : c.lastSeen) : '—';
return `
${av.identifier || 'AV'}
${av.source || '—'} • ${ip} • Last seen ${ago}
${c.switchport || c.port ? `
Port ${c.switchport || c.port} ${c.vlan ? 'VLAN ' + c.vlan : ''}
` : ''}
`;
}).join('')}
Click the green AV leaf nodes in the diagram for their full MDM/RED/Opti/Atlas/Meraki modals.
`;
} else {
connectedHtml = `No AV devices currently attached in the enriched data.
`;
}
const merakiLink = data.deviceUrl
? `
View in Meraki →
`
: '';
const content = `
Meraki Infrastructure Device
${name}
${statusBadge}
${merakiLink}
SERIAL${serial}
MODEL${model}
TYPE${tNode.type || (model && model.toUpperCase().startsWith('MR') ? 'AP' : 'Switch')}
LAST REPORTED${dev.lastReportedAt ? (typeof simpleTimeAgo === "function" ? simpleTimeAgo(new Date(dev.lastReportedAt)) : dev.lastReportedAt) : (dev.lastSeen ? (typeof simpleTimeAgo === "function" ? simpleTimeAgo(new Date(dev.lastSeen)) : dev.lastSeen) : "—")}
${connectedHtml}
This is a switch/AP-level modal (no AV source data like RED/Opti/Atlas/MDM). Use the diagram AV leaves for player details. Port-level info for attached AVs is shown above when available from Meraki client attachment.
`;
return sectionCard('', content);
}
export { showDeviceModal };