';
- // Battery (from dedicated get_bat or from core stats)
- if (bat && typeof bat === 'object' && bat.voltage) {
- html += `| ${tHtml('device.stats.battery')} | ${bat.voltage}V |
`;
- } else if (stats.core && stats.core.battery_mv) {
- html += `| ${tHtml('device.stats.battery')} | ${(stats.core.battery_mv / 1000).toFixed(2)}V |
`;
+ // Battery comes from the dedicated get_bat call, or from core stats when
+ // that returned nothing.
+ const volts = (bat && bat.voltage) ? Number(bat.voltage).toFixed(2)
+ : (core.battery_mv != null ? (core.battery_mv / 1000).toFixed(2) : null);
+
+ // Share of uptime the radio spent transmitting or receiving.
+ const util = (radio.tx_air_secs != null && radio.rx_air_secs != null && core.uptime_secs)
+ ? (((radio.tx_air_secs + radio.rx_air_secs) / core.uptime_secs) * 100).toFixed(2) + '%'
+ : '—';
+
+ // "flood" and "direct" are protocol terms and stay English in every
+ // language (see docs/translations.md). The split drops to its own line
+ // below sm, where keeping it inline wraps the row label as well.
+ const split = (flood, direct) =>
+ `(flood ${formatInt(flood)} · direct ${formatInt(direct)})`;
+
+ let html = '';
+
+ if (Object.keys(core).length || volts !== null) {
+ html += deviceStatsSection(t('rptmgmt.status.system'), [
+ [t('device.stats.battery'), volts !== null ? `${volts}V` : '—'],
+ [t('device.stats.uptime'), formatStatsDuration(core.uptime_secs)],
+ [t('device.stats.queue'), formatInt(core.queue_len)],
+ [t('device.stats.errors'), formatInt(core.errors)],
+ ]);
}
- // Core stats
- if (stats.core) {
- const c = stats.core;
- if (c.uptime !== undefined) {
- const d = Math.floor(c.uptime / 86400);
- const h = Math.floor((c.uptime % 86400) / 3600);
- const m = Math.floor((c.uptime % 3600) / 60);
- html += `| ${tHtml('device.stats.uptime')} | ${d}d ${h}h ${m}m |
`;
- }
- if (c.queue_length !== undefined)
- html += `| ${tHtml('device.stats.queue')} | ${c.queue_length} |
`;
- if (c.errors !== undefined)
- html += `| ${tHtml('device.stats.errors')} | ${c.errors} |
`;
+ if (Object.keys(radio).length) {
+ html += deviceStatsSection(t('rptmgmt.status.radio'), [
+ [t('rptmgmt.status.last_rssi'), radio.last_rssi != null ? `${radio.last_rssi} dBm` : '—'],
+ [t('rptmgmt.status.last_snr'), radio.last_snr != null ? `${radio.last_snr} dB` : '—'],
+ [t('rptmgmt.status.noise'), radio.noise_floor != null ? `${radio.noise_floor} dBm` : '—'],
+ [t('device.stats.tx_air'), formatStatsDuration(radio.tx_air_secs)],
+ [t('device.stats.rx_air'), formatStatsDuration(radio.rx_air_secs)],
+ [t('rptmgmt.status.utilization'), util],
+ ]);
}
- // Radio stats
- if (stats.radio) {
- const r = stats.radio;
- if (r.tx_air_time !== undefined)
- html += `| ${tHtml('device.stats.tx_air')} | ${r.tx_air_time.toFixed(1)} min |
`;
- if (r.rx_air_time !== undefined)
- html += `| ${tHtml('device.stats.rx_air')} | ${r.rx_air_time.toFixed(1)} min |
`;
+ if (Object.keys(pkt).length) {
+ const rows = [
+ [t('device.stats.packets_tx'), `${formatInt(pkt.sent)} ${split(pkt.flood_tx, pkt.direct_tx)}`],
+ [t('device.stats.packets_rx'), `${formatInt(pkt.recv)} ${split(pkt.flood_rx, pkt.direct_rx)}`],
+ ];
+ if (pkt.recv_errors != null) rows.push([t('rptmgmt.status.rx_errors'), formatInt(pkt.recv_errors)]);
+ html += deviceStatsSection(t('rptmgmt.status.packets'), rows);
}
- // Packet stats
- if (stats.packets) {
- const p = stats.packets;
- if (p.sent !== undefined)
- html += `| ${tHtml('device.stats.packets_tx')} | ${p.sent.toLocaleString()} |
`;
- if (p.received !== undefined)
- html += `| ${tHtml('device.stats.packets_rx')} | ${p.received.toLocaleString()} |
`;
+ if (Object.keys(db).length) {
+ html += deviceStatsSection(t('device.stats.database'), [
+ [t('device.stats.contacts_db'), formatInt(db.contacts)],
+ [t('device.stats.channel_msgs'), formatInt(db.channel_messages)],
+ [t('device.stats.direct_msgs'), formatInt(db.direct_messages)],
+ [t('device.stats.db_size'), db.db_size_bytes != null
+ ? `${(db.db_size_bytes / (1024 * 1024)).toFixed(1)} MB` : '—'],
+ ]);
}
- // DB stats (included in same response)
- if (data.db_stats) {
- const db = data.db_stats;
- if (db.contacts !== undefined)
- html += `| ${tHtml('device.stats.contacts_db')} | ${db.contacts} |
`;
- if (db.channel_messages !== undefined)
- html += `| ${tHtml('device.stats.channel_msgs')} | ${db.channel_messages.toLocaleString()} |
`;
- if (db.direct_messages !== undefined)
- html += `| ${tHtml('device.stats.direct_msgs')} | ${db.direct_messages.toLocaleString()} |
`;
- if (db.db_size_bytes !== undefined) {
- const sizeMB = (db.db_size_bytes / (1024 * 1024)).toFixed(1);
- html += `| ${tHtml('device.stats.db_size')} | ${sizeMB} MB |
`;
- }
- }
-
- html += '