// src/services/renderers/avStatusRenderer.js // // Extracted from commands/avStatus.js so the same output can drive // BOTH the chat reply (`bot.say('markdown', md)`) AND the Jira poller // comment (via utils/markdownToAdf). Byte-for-byte identical to what // the chat command used to emit for a given input. // // Options // storeNum (required) header string uses it // detailed default true — historically the chat handler always ran // detailed (the `Mode: detailed` string is hard-coded in // the header). We keep the option in the signature for // symmetry with the phone renderer and future compact-mode // work. // footer default true — appends `*Last checked: HH:MM*` italic // line. Poller passes false because a Jira comment already // carries an authoritative header timestamp. // // `appendMerakiClientLines` is preserved verbatim from the original // handler — same wired-vs-wireless branch, same double-arrow indent // convention, same fallback for `{client:...}` vs flat shapes. import { simpleTimeAgo } from '../../utils/time.js'; /** * Render an AV device-status markdown snapshot from a * `collectDeviceStatus` result. * * @param {object} data collectDeviceStatus() output * @param {object} opts * @param {string} opts.storeNum store id (header text) * @param {boolean} [opts.detailed=true] * @param {boolean} [opts.footer=true] * @returns {string} markdown, whitespace-trimmed and ready to send. */ export function renderAvStatusMarkdown(data, opts = {}) { const { storeNum, footer = true } = opts; // `detailed` is accepted for signature symmetry but the existing AV // renderer has always emitted detailed output — see file header note. // Explicit reference prevents accidental prop-name typos going silent. void opts.detailed; const mdmDevices = data.mdm?.data || []; const atlasData = data.atlas?.data || []; let reply = `**Device Status - Store ${storeNum}** (Mode: detailed)\n\n`; // Helper: append Meraki client info under the device name. Preserved // verbatim from commands/avStatus.js so wired-vs-wireless output stays // byte-identical. function appendMerakiClientLines(currentReply, mInput) { if (!mInput) return currentReply; const m = mInput.client || mInput; if (!m || Object.keys(m).length === 0) return currentReply; const clientSeenHours = m.lastSeen ? (Date.now() - new Date(m.lastSeen).getTime()) / (1000 * 60 * 60) : 999; const clientEmoji = clientSeenHours > 1 ? '⚠️' : '✅'; let clientLink = ''; const clientUrl = m.clientUrl || m.merakiClientUrl; if (clientUrl) { clientLink = ` [Meraki↗](${clientUrl})`; } else if (m.clientId && m.networkBaseUrl) { const base = m.networkBaseUrl.replace(/\/manage\/clients$/, ''); const merakiClientUrl = `${base}/manage/clients/${m.clientId}/overview`; clientLink = ` [Meraki↗](${merakiClientUrl})`; } const ip = m.ip ? `IP: ${m.ip}` : ''; const mac = m.mac ? `MAC: ${m.mac}` : ''; const last = m.lastSeen ? `• ${simpleTimeAgo(m.lastSeen)}` : ''; const conn = m.recentDeviceConnection || 'Wired'; const cstatus = m.clientStatus || m.status || '—'; const devName = m.deviceName || m.name || ''; const isWired = conn === 'Wired' || !!m.portNumber || !!m.switchport || !!m.port; if (isWired && devName) { currentReply += ` → ${clientEmoji} **${devName} (${conn} - ${cstatus})** • ${ip} • ${mac} ${last ? `• ${last}` : ''}${clientLink}\n`; const portNum = m.portNumber || m.switchport || m.port || '—'; const vlan = m.dataVlan || m.vlan || '—'; const voiceVlan = m.voiceVlan ? `Voice VLAN: ${m.voiceVlan}` : ''; const portType = m.type ? m.type : ''; const portName = m.portName ? m.portName : ''; const status = m.status || (m.enabled ? 'Enabled' : 'Disabled'); let portLine = ` → → Port: **${portNum}**`; if (portType) portLine += ` • Type: ${portType}`; if (portName) portLine += ` • Name: ${portName}`; portLine += ` • VLAN: ${vlan}`; if (voiceVlan) portLine += ` • ${voiceVlan}`; portLine += ` • ${status}\n`; currentReply += portLine; const policy = m.accessPolicy || '—'; const stickyCount = m.allowedMacs?.length || 0; const stickyText = stickyCount > 0 ? `Sticky MAC (${stickyCount})` : '—'; const poeText = m.poeEnabled === true ? '✅ POE On' : (m.poeEnabled === false ? 'POE Off' : '—'); const errors = m.errors?.length > 0 ? `⚠️ Errors: ${m.errors.join(', ')}` : ''; currentReply += ` → → Policy: ${policy} • ${stickyText} • ${poeText} ${errors ? `• ${errors}` : ''}\n`; } else { currentReply += ` → ${clientEmoji} **${devName || 'Wireless'} (${m.recentDeviceConnection || 'Wireless'} - ${cstatus})** IP: ${m.ip || '—'} • MAC: ${m.mac || '—'} ${m.lastSeen ? `• ${simpleTimeAgo(m.lastSeen)}` : ''}${clientLink}\n`; currentReply += ` → → SSID: ${m.ssid || '—'} • VLAN: ${m.vlan || '—'}\n`; } return currentReply; } if (mdmDevices.length === 0) { reply += 'No MDM devices found for this store.\n'; } else { mdmDevices.forEach(dev => { const mdmName = dev.friendlyName || dev.name || 'Unknown Device'; const mdmLastSeenRaw = dev.lastSeen || dev.LastSeen || dev.LastSystemSampleTime; const mdmLastSeen = mdmLastSeenRaw ? simpleTimeAgo(mdmLastSeenRaw) : '—'; const m = dev.meraki || {}; const mdmSeenHours = mdmLastSeenRaw ? (Date.now() - new Date(mdmLastSeenRaw).getTime()) / (1000 * 60 * 60) : 999; const mdmEmoji = mdmSeenHours > 24 ? '❌' : '✅'; reply += `${mdmEmoji} **${mdmName}** Last seen: ${mdmLastSeen}\n`; reply = appendMerakiClientLines(reply, m); if (dev.red) { const red = dev.red; const connectivity = red.Connectivity || '—'; const deployment = red.DeploymentStatusName || '—'; const stateTransition = red.StateTransitionStatus || '—'; const lastPing = red.LastPingTimeUTC ? simpleTimeAgo(red.LastPingTimeUTC) : '—'; const availability = red.AvailabilityStatus || '—'; const isLastPingOld = red.LastPingTimeUTC ? (Date.now() - new Date(red.LastPingTimeUTC).getTime()) / (1000 * 60 * 60) > 1 : true; const hasProblem = isLastPingOld || stateTransition !== 'Current' || availability !== 'Available' || deployment !== 'PROCESSED'; const redEmoji = hasProblem ? '⚠️' : '✅'; reply += ` → ${redEmoji} **RED ${connectivity}** • Availability: ${availability} • Deployment: ${deployment} • State: ${stateTransition} • Last Ping: ${lastPing}\n`; } if (dev.optisigns) { const o = dev.optisigns; const optiEmoji = o.isOld ? '⚠️' : '✅'; reply += ` → ${optiEmoji} **OptiSigns** ${o.content} • Last heartbeat: ${o.lastHeartBeat ? simpleTimeAgo(o.lastHeartBeat) : '—'}\n`; } reply += '\n'; }); } if (atlasData && atlasData.length > 0) { reply += `**Atlas Devices:**\n\n`; atlasData.forEach(dev => { const name = dev.name || dev.displayName || 'US002477AMP'; const status = (dev.status || dev.state?.status || 'unknown').toLowerCase(); const lastSeenTime = dev.last_seen_at || dev.lastSeen ? new Date(dev.last_seen_at || dev.lastSeen).getTime() : 0; const minutesAgo = (Date.now() - lastSeenTime) / (1000 * 60); const voltage = parseFloat(dev.state?.voltageMonitor) || 120; const voltageProblem = Math.abs(voltage - 120) > 8; const faultStatus = dev.state?.faultStatus || 0; const hasFault = faultStatus > 0; const ampsNotReady = dev.state ? Object.keys(dev.state).some(k => (k.startsWith('ampStatus_') || k.startsWith('ampModuleStatus_')) && !['Ready', 'Active', 'Standby'].includes(dev.state[k]) ) : false; const hasProblem = minutesAgo > 20 || status !== 'online' || voltageProblem || hasFault || ampsNotReady; const emoji = hasProblem ? '⚠️' : '✅'; const lastSeenStr = dev.last_seen_at || dev.lastSeen ? simpleTimeAgo(dev.last_seen_at || dev.lastSeen) : '—'; reply += `${emoji} **${name}** (${status}) • Last seen: ${lastSeenStr}\n`; const m = dev.meraki || {}; reply = appendMerakiClientLines(reply, m); const modelInfo = []; if (dev.model?.name) modelInfo.push(dev.model.name); if (dev.firmware?.version) modelInfo.push(`FW ${dev.firmware.version}`); if (dev.sn || dev.serial) modelInfo.push(`SN ${dev.sn || dev.serial}`); if (dev.state?.IpAddress) modelInfo.push(`IP ${dev.state.IpAddress}`); if (modelInfo.length > 0) { reply += ` → ${modelInfo.join(' • ')}\n`; } if (dev.state) { const s = dev.state; const cpuF = s.tempCpu ? Math.round((parseFloat(s.tempCpu) * 9 / 5) + 32) : '—'; const psuF = s.tempPsu ? Math.round((parseFloat(s.tempPsu) * 9 / 5) + 32) : '—'; const ioF = s.tempIo ? Math.round((parseFloat(s.tempIo) * 9 / 5) + 32) : '—'; reply += ` → CPU: ${cpuF}°F • PSU: ${psuF}°F • Io: ${ioF}°F • Voltage: ${voltage}V • Fan: ${s.fanSpeed ? Math.round(s.fanSpeed) : '—'}%\n`; } if (dev.state) { const s = dev.state; let ampInfo = []; for (let i = 1; i <= 8; i++) { const statusKey = `ampStatus_${i}`; const moduleKey = `ampModuleStatus_${i}`; const ampStatus = s[statusKey] || s[moduleKey]; if (ampStatus) { ampInfo.push(`Amp${i}: ${ampStatus}`); } } if (ampInfo.length > 0) { reply += ` → Amps: ${ampInfo.join(', ')}\n`; } } if (hasFault) { reply += ` → ⚠️ Fault Status: ${faultStatus}\n`; } if (dev.state?.lastLogEntry) { let logMessage = dev.state.lastLogEntry; if (logMessage.includes("System startup from power connection")) { logMessage = "System startup (power cycle)"; } const logTime = dev.state.lastLogEntry.includes("2025-Nov-04") ? simpleTimeAgo("2025-11-04T15:51:26Z") : "recent"; reply += ` → Last Log: ${logMessage} (${logTime})\n`; } reply += '\n'; }); } if (footer) { reply += `*Last checked: ${new Date().toLocaleTimeString()}*`; } return reply.trim(); }