collabSupport/services/renderers/dectStatusRenderer.js
Joseph McQueen 7c7d0a898c Add /dectstatus command with full base dump and reboot cards.
Pulls complete status.xml from each reachable DBS-210 via the relay,
renders the full CLI-style dump, and posts per-base adaptive cards for
reboot and factory-reset (confirm flow + audit logging). Wired into
registry, help, and attachmentAction dispatch.
2026-07-21 15:51:07 -04:00

198 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/services/renderers/dectStatusRenderer.js
//
// Full CLI-style DECT base dump for `/dectstatus`. Complements the
// compact follow-up from renderDectDiagnosticsMarkdown (phonestatus):
// that one is "exceptions only"; this one is the complete status.xml
// picture (device, firmware, reboot log, network, RTP, security,
// emergency numbers, health verdict).
//
// Input is the same collectAll() result array used by the compact
// renderer. Pure — no I/O, no env.
import { formatDisplayTime } from '../../utils/time.js';
/**
* @param {Array} results collectAll() output
* @param {object} opts
* @param {string} opts.storeNum
* @param {boolean} [opts.footer=true]
* @param {object} [opts.relay] optional hub.status() snapshot
* @param {Array} [opts.discoveryWarnings]
* @returns {string}
*/
export function renderDectStatusMarkdown(results, opts = {}) {
const {
storeNum,
footer = true,
relay = null,
discoveryWarnings = [],
} = opts;
const list = Array.isArray(results) ? results : [];
const lines = [`**DECT Status — Store ${storeNum}**`, ''];
if (relay) {
if (relay.connected) {
const agent = relay.agent?.hostname || relay.agent?.version || 'connected';
lines.push(`_Relay: online (${agent})_`);
} else {
lines.push('_Relay: **offline** — base collect will fail until dect-relay-agent reconnects_');
}
lines.push('');
}
if (list.length === 0) {
lines.push('_No reachable DECT basestations discovered for this store._');
if (discoveryWarnings.length > 0) {
lines.push('');
lines.push('**Discovery notes:**');
for (const w of discoveryWarnings) {
const who = w.mac || w.ip || 'base';
lines.push(`- ${who}: ${w.reason}`);
}
}
if (footer) {
lines.push('');
lines.push(`_Pulled at ${formatDisplayTime()}._`);
}
return lines.join('\n').trim();
}
for (let i = 0; i < list.length; i++) {
if (i > 0) lines.push('', '---', '');
lines.push(...renderOneBaseFull(list[i]));
}
if (discoveryWarnings.length > 0) {
lines.push('', '**Discovery notes (skipped bases):**');
for (const w of discoveryWarnings) {
const who = w.mac || w.ip || 'base';
lines.push(`- ${who}: ${w.reason}`);
}
}
if (footer) {
lines.push('');
lines.push(
`_Full base dump at ${formatDisplayTime()} via the DECT relay. ` +
`Use the action cards below for reboot / factory-reset (chat only)._`,
);
}
return lines.join('\n').trim();
}
function renderOneBaseFull(r) {
const label = r.base?.name || `Basestation ${r.base?.mac || '?'}`;
const ip = r.base?.ip || '?';
const mac = r.base?.mac || '?';
const lines = [];
if (!r.ok) {
lines.push(`⚠️ **${label}**`);
lines.push(`- IP: \`${ip}\` · MAC: \`${mac}\``);
lines.push(`- Collect failed: ${r.error?.message || 'unknown error'}`);
if (r.error?.hint) lines.push(`- _${r.error.hint}_`);
if (r.elapsedMs != null) lines.push(`- Elapsed: ${r.elapsedMs}ms`);
return lines;
}
const p = r.data || {};
const verdict = r.verdict || {};
const icon = verdict.healthy ? '✅' : '⚠️';
lines.push(`${icon} **${label}**`);
// ── Device ──
lines.push('', '**Device**');
row(lines, 'Model', p.device?.model);
row(lines, 'System type', p.device?.systemType);
row(lines, 'Unit', [p.device?.unitName, p.device?.unitIndex].filter(Boolean).join(' · ') || null);
row(lines, 'MAC', p.device?.macAddress || mac);
row(lines, 'IP', p.device?.ipAddress || ip);
row(lines, 'RFPI', p.device?.rfpiAddress);
row(lines, 'RF band', p.device?.rfBand);
row(lines, 'Multi-cell', p.multiCell?.role || p.multiCell?.raw);
row(lines, 'Base status', p.baseStatus);
row(lines, 'Conflict', p.conflictInfo);
// ── Firmware ──
lines.push('', '**Firmware**');
row(lines, 'Version', p.firmware?.version);
row(lines, 'Update server', p.firmware?.updateServer);
row(lines, 'Update path', p.firmware?.updatePath);
// ── Time ──
lines.push('', '**Time / uptime**');
row(lines, 'Local time', p.time?.currentLocalTime);
row(lines, 'Uptime', p.time?.operatingTime);
if (r.elapsedMs != null) row(lines, 'Collect latency', `${r.elapsedMs}ms`);
// ── Reboot log ──
lines.push('', '**Reboot log** (newest first)');
const log = Array.isArray(p.rebootLog) ? p.rebootLog : [];
if (log.length === 0) {
lines.push('- _(none)_');
} else {
for (const entry of log) {
if (entry.unrecognized) {
lines.push(`- ??? ${entry.raw || ''}`);
continue;
}
const tag = entry.reasonCode === 80 ? '⚡' : '•';
lines.push(
`- ${tag} #${entry.sequence} ${entry.at} **${entry.reasonName}** (${entry.reasonCode}) fw=${entry.firmwareAtBoot || '?'}`,
);
}
}
// ── Network ──
lines.push('', '**Network stats** (since boot)');
row(lines, 'Tx packets', p.network?.txPackets);
row(lines, 'Tx dropped', p.network?.txDropped);
row(lines, 'Tx errors', p.network?.txErrors);
row(lines, 'Rx packets', p.network?.rxPackets);
row(lines, 'Rx dropped', p.network?.rxDropped);
row(lines, 'Rx errors', p.network?.rxErrors);
row(lines, 'Rx broadcasts', p.network?.rxBroadcasts);
// ── RTP ──
lines.push('', '**RTP**');
row(lines, 'Total since boot', p.rtp?.total);
row(lines, 'Current active', p.rtp?.current);
row(lines, 'Current local', p.rtp?.currentLocal);
row(lines, 'Current relay', p.rtp?.currentRelay);
// ── Security ──
lines.push('', '**Security**');
row(
lines,
'Custom CA',
p.security?.customCa?.installed
? (p.security.customCa.info || 'installed')
: 'Not installed',
);
row(lines, '802.1X protocol', p.security?.dot1x?.protocol);
row(lines, '802.1X status', p.security?.dot1x?.transactionStatus);
// ── Emergency ──
const emerg = Array.isArray(p.emergencyNumbers) ? p.emergencyNumbers : [];
lines.push('', '**Emergency numbers**');
lines.push(emerg.length ? `- ${emerg.join(', ')}` : '- _(none configured)_');
// ── Verdict ──
lines.push('', '**Health verdict**');
lines.push(`- healthy: **${verdict.healthy ? 'YES' : 'NO'}**`);
for (const w of verdict.warnings || []) {
lines.push(`- ⚠️ ${w}`);
}
for (const i of verdict.info || []) {
lines.push(`- ${i}`);
}
return lines;
}
function row(lines, label, value) {
if (value == null || value === '') return;
lines.push(`- **${label}:** ${value}`);
}