diff --git a/app/routes/api.py b/app/routes/api.py
index 3356c60..821a9f1 100644
--- a/app/routes/api.py
+++ b/app/routes/api.py
@@ -1061,6 +1061,32 @@ def get_device_info():
}), 500
+@api_bp.route('/device/stats', methods=['GET'])
+def get_device_stats():
+ """
+ Get device statistics (uptime, radio, packets).
+ """
+ try:
+ dm = current_app.config.get('DEVICE_MANAGER')
+ if not dm or not dm.is_connected:
+ return jsonify({'success': False, 'error': 'Device not connected'}), 503
+
+ stats = dm.get_device_stats()
+ bat = dm.get_battery()
+ db_stats = dm.db.get_stats() if dm.db else {}
+
+ return jsonify({
+ 'success': True,
+ 'stats': stats,
+ 'battery': bat,
+ 'db_stats': db_stats,
+ }), 200
+
+ except Exception as e:
+ logger.error(f"Error getting device stats: {e}")
+ return jsonify({'success': False, 'error': str(e)}), 500
+
+
# =============================================================================
# Special Commands
# =============================================================================
diff --git a/app/static/js/app.js b/app/static/js/app.js
index 41bb464..3b92b12 100644
--- a/app/static/js/app.js
+++ b/app/static/js/app.js
@@ -1338,6 +1338,102 @@ async function loadDeviceInfo() {
}
}
+/**
+ * Load device statistics (Stats tab in Device modal)
+ */
+async function loadDeviceStats() {
+ const container = document.getElementById('deviceStatsContent');
+ if (!container) return;
+
+ container.innerHTML = '
';
+
+ try {
+ const response = await fetch('/api/device/stats');
+ const data = await response.json();
+
+ if (!data.success) {
+ container.innerHTML = `${escapeHtml(data.error)}
`;
+ return;
+ }
+
+ const stats = data.stats || {};
+ const bat = data.battery || {};
+ let html = '';
+
+ // Battery
+ if (bat && typeof bat === 'object' && bat.voltage) {
+ html += `| Battery | ${bat.voltage}V |
`;
+ } else if (bat) {
+ html += `| Battery | ${bat} |
`;
+ }
+
+ // 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 += `| Uptime | ${d}d ${h}h ${m}m |
`;
+ }
+ if (c.queue_length !== undefined)
+ html += `| Queue | ${c.queue_length} |
`;
+ if (c.errors !== undefined)
+ html += `| Errors | ${c.errors} |
`;
+ }
+
+ // Radio stats
+ if (stats.radio) {
+ const r = stats.radio;
+ if (r.tx_air_time !== undefined)
+ html += `| TX Air Time | ${r.tx_air_time.toFixed(1)} min |
`;
+ if (r.rx_air_time !== undefined)
+ html += `| RX Air Time | ${r.rx_air_time.toFixed(1)} min |
`;
+ }
+
+ // Packet stats
+ if (stats.packets) {
+ const p = stats.packets;
+ if (p.sent !== undefined)
+ html += `| Packets TX | ${p.sent.toLocaleString()} |
`;
+ if (p.received !== undefined)
+ html += `| Packets RX | ${p.received.toLocaleString()} |
`;
+ }
+
+ // DB stats (included in same response)
+ if (data.db_stats) {
+ const db = data.db_stats;
+ if (db.contacts !== undefined)
+ html += `| Contacts (DB) | ${db.contacts} |
`;
+ if (db.channel_messages !== undefined)
+ html += `| Channel Msgs | ${db.channel_messages.toLocaleString()} |
`;
+ if (db.direct_messages !== undefined)
+ html += `| Direct Msgs | ${db.direct_messages.toLocaleString()} |
`;
+ if (db.db_size_bytes !== undefined) {
+ const sizeMB = (db.db_size_bytes / (1024 * 1024)).toFixed(1);
+ html += `| DB Size | ${sizeMB} MB |
`;
+ }
+ }
+
+ html += '
';
+
+ if (html === '') {
+ container.innerHTML = 'No statistics available
';
+ } else {
+ container.innerHTML = html;
+ }
+
+ } catch (error) {
+ console.error('Error loading device stats:', error);
+ container.innerHTML = 'Failed to load stats
';
+ }
+}
+
+// Load stats when Stats tab is clicked
+document.addEventListener('DOMContentLoaded', () => {
+ document.getElementById('statsTabBtn')?.addEventListener('shown.bs.tab', loadDeviceStats);
+});
+
/**
* Cleanup inactive contacts
*/
diff --git a/app/templates/base.html b/app/templates/base.html
index 199e714..224466c 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -284,13 +284,32 @@
-
-
-
Loading...
+
+
+
+
+
+
+ Click to load stats
+
+