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)
SENT
${sentMB} MB
RECEIVED
${recvMB} MB
TOTAL
${totalMB} MB
`; } 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}
Usage
${usageKB} KB
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 };