fix: enhance the UI of status bar
parent
526bc8e8fa
commit
f61af015ad
|
|
@ -10,6 +10,7 @@ from flask import Flask, jsonify, request, Response
|
|||
from flask_socketio import SocketIO, join_room
|
||||
from flask_cors import CORS
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import desc
|
||||
|
||||
# Import your custom core modules and the new models
|
||||
from core.mqtt_client import MqttClient
|
||||
|
|
@ -67,6 +68,10 @@ STATION_TIMEOUT_SECONDS = 10
|
|||
|
||||
# --- MQTT Message Handling ---
|
||||
def on_message_handler(station_id, topic, payload):
|
||||
|
||||
message_type = topic.split('/')[-1]
|
||||
|
||||
if message_type in ['PERIODIC']:
|
||||
last_message_timestamps[station_id] = time.time()
|
||||
|
||||
print(f"Main handler received message for station {station_id} on topic {topic}")
|
||||
|
|
@ -263,6 +268,30 @@ def get_stations():
|
|||
return jsonify({"error": f"Database query failed: {e}"}), 500
|
||||
|
||||
|
||||
@app.route('/api/logs/recent/<string:station_id>', methods=['GET'])
|
||||
def get_recent_logs(station_id):
|
||||
"""
|
||||
Fetches the 50 most recent logs for a given station from the database.
|
||||
"""
|
||||
try:
|
||||
# Query the MqttLog table, filter by station_id, order by timestamp descending, and take the first 50
|
||||
logs = MqttLog.query.filter_by(station_id=station_id).order_by(desc(MqttLog.timestamp)).limit(50).all()
|
||||
|
||||
# We reverse the list so the oldest are first, for correct display order
|
||||
logs.reverse()
|
||||
|
||||
log_list = [{
|
||||
"topic": log.topic,
|
||||
"payload": log.payload,
|
||||
"timestamp": log.timestamp.isoformat()
|
||||
} for log in logs]
|
||||
|
||||
return jsonify(log_list)
|
||||
except Exception as e:
|
||||
print(f"Error fetching recent logs: {e}")
|
||||
return jsonify({"message": "Could not fetch recent logs."}), 500
|
||||
|
||||
|
||||
# --- CSV Export route (UPDATED) ---
|
||||
def _format_periodic_row(payload, num_slots=9):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<script src="https://unpkg.com/lucide@latest" defer></script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||
<script src="./js/dashboard.js"></script>
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
|
|
@ -188,7 +188,7 @@
|
|||
|
||||
<span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
<span>Product ID:</span>
|
||||
<span id="product-id" class="font-semibold">—</span>
|
||||
<span id="product-id-display" class="font-semibold">—</span>
|
||||
</span>
|
||||
|
||||
<span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
|
|
@ -196,11 +196,6 @@
|
|||
<span id="device-id" class="font-semibold">—</span>
|
||||
</span>
|
||||
|
||||
<!-- <span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
<span>Device ID:</span>
|
||||
<span id="device-id">—</span>
|
||||
</span> -->
|
||||
|
||||
<span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
<svg class="w-2.5 h-2.5 opacity-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 3"></path>
|
||||
|
|
@ -212,13 +207,11 @@
|
|||
<span class="h-2 w-2 rounded-full bg-amber-400"></span> Connecting...
|
||||
</span>
|
||||
|
||||
<button id="station-reset-btn" class="btn btn-danger !p-2" title="Station Reset">
|
||||
<button id="station-reset-btn" class="btn btn-danger !p-2" title="Station Reset">
|
||||
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- <span id="backup-power-chip" class="cham_chip cham_chip-slate" title="Station power source" style="display: none;">On Backup</span> -->
|
||||
</button>
|
||||
|
||||
<div class="hidden sm:block w-px h-5 bg-white/10"></div>
|
||||
|
||||
|
|
@ -252,6 +245,9 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<!-- <span id="backup-power-chip" class="cham_chip cham_chip-slate" title="Station power source" style="display: none;">On Backup</span> -->
|
||||
|
||||
|
||||
<main class="relative z-10 flex-1 w-full px-3 py-3 overflow-y-auto lg:overflow-hidden">
|
||||
<div class="page mx-auto flex flex-col lg:h-full lg:flex-row gap-3">
|
||||
<section id="chambersGrid" class="flex-1 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 lg:grid-rows-3 gap-3"></section>
|
||||
|
|
@ -365,5 +361,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./js/common-header.js"></script>
|
||||
<script src="./js/dashboard.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -7,6 +7,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const stationNameEl = document.getElementById('station-name');
|
||||
const stationLocationEl = document.getElementById('station-location');
|
||||
const deviceIdEl = document.getElementById('device-id');
|
||||
const productIdEl = document.getElementById('product-id');
|
||||
const lastUpdateEl = document.getElementById('last-update-status');
|
||||
const connChip = document.getElementById('connection-status-chip');
|
||||
const requestLogArea = document.getElementById('request-log-area');
|
||||
|
|
@ -70,6 +71,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
throw new Error('No station selected. Please go back to the selection page.');
|
||||
}
|
||||
deviceIdEl.textContent = selectedStation.id;
|
||||
productIdEl.textContent = selectedStation.product_id;
|
||||
} catch (e) {
|
||||
document.body.innerHTML = `<div class="text-center p-8 text-rose-400">${e.message}<a href="./station_selection.html" class="underline ml-2">Go Back</a></div>`;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const stationNameEl = document.getElementById('station-name');
|
||||
const stationLocationEl = document.getElementById('station-location');
|
||||
const deviceIdEl = document.getElementById('device-id');
|
||||
const productIdEl = document.getElementById('product-id');
|
||||
const lastUpdateEl = document.getElementById('last-update-status');
|
||||
const stationDiagCodeEl = document.getElementById('station-diag-code');
|
||||
const backupPowerChip = document.getElementById('backup-power-chip');
|
||||
|
|
@ -459,7 +460,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
stationNameEl.textContent = selectedStation.name || 'Unknown Station';
|
||||
stationLocationEl.textContent = selectedStation.location || 'No location';
|
||||
|
||||
// This populates the span with id="device-id" with the Station's ID
|
||||
deviceIdEl.textContent = selectedStation.id;
|
||||
|
||||
// This populates the span with id="product-id" with the Product ID
|
||||
// productIdEl.textContent = selectedStation.product_id;
|
||||
} catch (e) {
|
||||
document.body.innerHTML = `<div class="text-center p-8 text-rose-400">${e.message} <a href="./station_selection.html" class="underline">Go Back</a></div>`;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,162 @@
|
|||
// document.addEventListener('DOMContentLoaded', () => {
|
||||
// // --- CONFIGURATION ---
|
||||
// const SOCKET_URL = "http://192.168.1.12:5000";
|
||||
// const API_BASE = "http://192.168.1.12:5000/api";
|
||||
|
||||
// // --- DOM ELEMENT REFERENCES ---
|
||||
// const stationNameEl = document.getElementById('station-name');
|
||||
// const stationLocationEl = document.getElementById('station-location');
|
||||
// const deviceIdEl = document.getElementById('device-id');
|
||||
// const lastUpdateEl = document.getElementById('last-update-status');
|
||||
// const connChip = document.getElementById('connection-status-chip');
|
||||
// const requestLogArea = document.getElementById('request-log-area');
|
||||
// const eventLogArea = document.getElementById('event-log-area');
|
||||
// const clearReqBtn = document.getElementById('clear-req');
|
||||
// const clearEvtBtn = document.getElementById('clear-evt');
|
||||
// const clearAllBtn = document.getElementById('clear-all');
|
||||
// const refreshBtn = document.getElementById('refreshBtn');
|
||||
// const downloadBtn = document.getElementById('downloadBtn');
|
||||
// const logoutBtn = document.getElementById('logout-btn');
|
||||
// const resetBtn = document.getElementById('station-reset-btn');
|
||||
|
||||
// // --- STATE ---
|
||||
// let selectedStation = null;
|
||||
// let socket;
|
||||
// let statusPollingInterval;
|
||||
|
||||
// // --- HELPER FUNCTIONS ---
|
||||
// // **MODIFIED** to accept the 'topic'
|
||||
// const prependLog = (textarea, data, topic) => {
|
||||
// if (!textarea) return;
|
||||
// const timestamp = new Date().toLocaleTimeString();
|
||||
// const formattedJson = JSON.stringify(data, null, 2);
|
||||
// // **MODIFIED** to include the topic string
|
||||
// const newLog = `[${timestamp}] - Topic: ${topic}\n${formattedJson}\n\n---------------------------------\n\n`;
|
||||
// textarea.value = newLog + textarea.value;
|
||||
// };
|
||||
|
||||
// // --- NEW: LOG PERSISTENCE HELPERS ---
|
||||
// const saveLogs = () => {
|
||||
// if (!selectedStation) return;
|
||||
// sessionStorage.setItem(`request_logs_${selectedStation.id}`, requestLogArea.value);
|
||||
// sessionStorage.setItem(`event_logs_${selectedStation.id}`, eventLogArea.value);
|
||||
// };
|
||||
|
||||
// const loadLogs = () => {
|
||||
// if (!selectedStation) return;
|
||||
// requestLogArea.value = sessionStorage.getItem(`request_logs_${selectedStation.id}`) || '';
|
||||
// eventLogArea.value = sessionStorage.getItem(`event_logs_${selectedStation.id}`) || '';
|
||||
// };
|
||||
|
||||
// const sendCommand = (command, data = null) => {
|
||||
// if (!selectedStation || !socket || !socket.connected) {
|
||||
// console.error(`Cannot send command '${command}', not connected.`);
|
||||
// return;
|
||||
// }
|
||||
// const payload = { station_id: selectedStation.id, command: command, data: data };
|
||||
// socket.emit('rpc_request', payload);
|
||||
// };
|
||||
|
||||
// const checkStationStatus = async () => {
|
||||
// // ... (This function is unchanged)
|
||||
// if (!selectedStation) return;
|
||||
// try {
|
||||
// const response = await fetch(`${API_BASE}/stations`);
|
||||
// if (!response.ok) return;
|
||||
// const stations = await response.json();
|
||||
// const thisStation = stations.find(s => s.id === selectedStation.id);
|
||||
// if (thisStation) {
|
||||
// stationNameEl.textContent = thisStation.name;
|
||||
// stationLocationEl.textContent = thisStation.location;
|
||||
// if (thisStation.status === 'Online') {
|
||||
// connChip.innerHTML = `<span class="h-2 w-2 rounded-full bg-emerald-400 animate-pulseDot"></span> Online`;
|
||||
// connChip.className = 'cham_chip cham_chip-emerald';
|
||||
// } else {
|
||||
// connChip.innerHTML = `<span class="h-2 w-2 rounded-full bg-rose-500"></span> Offline`;
|
||||
// connChip.className = 'cham_chip cham_chip-rose';
|
||||
// }
|
||||
// }
|
||||
// } catch (error) { console.error("Failed to fetch station status:", error); }
|
||||
// };
|
||||
|
||||
// // --- INITIALIZATION ---
|
||||
// try {
|
||||
// selectedStation = JSON.parse(localStorage.getItem('selected_station'));
|
||||
// if (!selectedStation || !selectedStation.id) {
|
||||
// throw new Error('No station selected. Please go back to the selection page.');
|
||||
// }
|
||||
// deviceIdEl.textContent = selectedStation.id;
|
||||
// } catch (e) {
|
||||
// document.body.innerHTML = `<div class="text-center p-8 text-rose-400">${e.message}<a href="./station_selection.html" class="underline ml-2">Go Back</a></div>`;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // --- SOCKET.IO CONNECTION ---
|
||||
// socket = io(SOCKET_URL);
|
||||
// socket.on('connect', () => {
|
||||
// console.log("Connected to WebSocket for logs.");
|
||||
// socket.emit('join_station_room', { station_id: selectedStation.id });
|
||||
// });
|
||||
|
||||
// socket.on('dashboard_update', (message) => {
|
||||
// const { stationId, topic, data } = message;
|
||||
// if (stationId !== selectedStation.id) return;
|
||||
|
||||
// lastUpdateEl.textContent = 'Last Recv ' + new Date().toLocaleTimeString();
|
||||
|
||||
// // **MODIFIED** to pass the 'topic' to prependLog
|
||||
// if (topic.endsWith('EVENTS')) {
|
||||
// prependLog(eventLogArea, data, topic);
|
||||
// } else if (topic.endsWith('REQUEST')) {
|
||||
// prependLog(requestLogArea, data, topic);
|
||||
// }
|
||||
|
||||
// // **NEW**: Save the logs after every update
|
||||
// saveLogs();
|
||||
// });
|
||||
|
||||
// // --- BUTTON EVENT LISTENERS ---
|
||||
// // **MODIFIED** to clear sessionStorage
|
||||
// if(clearReqBtn) clearReqBtn.addEventListener('click', () => {
|
||||
// requestLogArea.value = '';
|
||||
// sessionStorage.removeItem(`request_logs_${selectedStation.id}`);
|
||||
// });
|
||||
// if(clearEvtBtn) clearEvtBtn.addEventListener('click', () => {
|
||||
// eventLogArea.value = '';
|
||||
// sessionStorage.removeItem(`event_logs_${selectedStation.id}`);
|
||||
// });
|
||||
// if(clearAllBtn) clearAllBtn.addEventListener('click', () => {
|
||||
// requestLogArea.value = '';
|
||||
// eventLogArea.value = '';
|
||||
// sessionStorage.removeItem(`request_logs_${selectedStation.id}`);
|
||||
// sessionStorage.removeItem(`event_logs_${selectedStation.id}`);
|
||||
// });
|
||||
|
||||
// // (The rest of your button listeners are unchanged)
|
||||
// if(logoutBtn) logoutBtn.addEventListener('click', () => {
|
||||
// localStorage.clear();
|
||||
// window.location.href = 'index.html';
|
||||
// });
|
||||
// if(refreshBtn) refreshBtn.addEventListener('click', () => location.reload());
|
||||
// if(resetBtn) resetBtn.addEventListener('click', () => {
|
||||
// if (confirm('Are you sure you want to reset the station?')) {
|
||||
// sendCommand('STATION_RESET');
|
||||
// }
|
||||
// });
|
||||
|
||||
// // --- STARTUP ---
|
||||
// checkStationStatus();
|
||||
// statusPollingInterval = setInterval(checkStationStatus, 10000);
|
||||
// loadLogs(); // **NEW**: Load saved logs from this session on startup
|
||||
// if (typeof lucide !== 'undefined') {
|
||||
// lucide.createIcons();
|
||||
// }
|
||||
// });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- CONFIGURATION ---
|
||||
const SOCKET_URL = "http://192.168.1.12:5000";
|
||||
|
|
@ -7,30 +166,33 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const stationNameEl = document.getElementById('station-name');
|
||||
const stationLocationEl = document.getElementById('station-location');
|
||||
const deviceIdEl = document.getElementById('device-id');
|
||||
const lastUpdateEl = document.getElementById('last-update-status');
|
||||
const connChip = document.getElementById('connection-status-chip');
|
||||
const productIdEl = document.getElementById('product-id');
|
||||
const requestLogArea = document.getElementById('request-log-area');
|
||||
const eventLogArea = document.getElementById('event-log-area');
|
||||
const clearReqBtn = document.getElementById('clear-req');
|
||||
const clearEvtBtn = document.getElementById('clear-evt');
|
||||
const clearAllBtn = document.getElementById('clear-all');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const resetBtn = document.getElementById('station-reset-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
|
||||
// --- STATE ---
|
||||
let selectedStation = null;
|
||||
let socket;
|
||||
let statusPollingInterval;
|
||||
|
||||
// --- HELPER FUNCTIONS ---
|
||||
const prependLog = (textarea, data) => {
|
||||
|
||||
const appendLog = (textarea, data, topic, timestampStr) => {
|
||||
if (!textarea) return;
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const timestamp = new Date(timestampStr).toLocaleTimeString();
|
||||
const formattedJson = JSON.stringify(data, null, 2);
|
||||
const newLog = `[${timestamp}]\n${formattedJson}\n\n---------------------------------\n\n`;
|
||||
textarea.value = newLog + textarea.value;
|
||||
const newLog = `[${timestamp}] - Topic: ${topic}\n${formattedJson}\n\n---------------------------------\n\n`;
|
||||
|
||||
// Append the new log to the end of the existing text
|
||||
textarea.value += newLog;
|
||||
|
||||
// Auto-scroll to the bottom to always show the newest log
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
};
|
||||
|
||||
const sendCommand = (command, data = null) => {
|
||||
|
|
@ -63,22 +225,50 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
} catch (error) { console.error("Failed to fetch station status:", error); }
|
||||
};
|
||||
|
||||
// --- NEW: Fetch recent logs from the database ---
|
||||
const fetchAndRenderLogs = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/logs/recent/${selectedStation.id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch recent logs');
|
||||
}
|
||||
const logs = await response.json();
|
||||
|
||||
// Clear text areas before populating
|
||||
requestLogArea.value = '';
|
||||
eventLogArea.value = '';
|
||||
|
||||
logs.forEach(log => {
|
||||
if (log.topic.endsWith('EVENTS')) {
|
||||
appendLog(eventLogArea, log.payload, log.topic, log.timestamp);
|
||||
} else if (log.topic.endsWith('REQUEST')) {
|
||||
appendLog(requestLogArea, log.payload, log.topic, log.timestamp);
|
||||
}
|
||||
});
|
||||
console.log(`Successfully fetched and rendered ${logs.length} recent logs.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// --- INITIALIZATION ---
|
||||
try {
|
||||
selectedStation = JSON.parse(localStorage.getItem('selected_station'));
|
||||
if (!selectedStation || !selectedStation.id) {
|
||||
throw new Error('No station selected. Please go back to the selection page.');
|
||||
throw new Error('No station selected.');
|
||||
}
|
||||
deviceIdEl.textContent = selectedStation.id;
|
||||
productIdEl.textContent = selectedStation.product_id;
|
||||
} catch (e) {
|
||||
document.body.innerHTML = `<div class="text-center p-8 text-rose-400">${e.message}<a href="./station_selection.html" class="underline ml-2">Go Back</a></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// --- SOCKET.IO CONNECTION ---
|
||||
// --- SOCKET.IO FOR LIVE UPDATES ---
|
||||
socket = io(SOCKET_URL);
|
||||
socket.on('connect', () => {
|
||||
console.log("Connected to WebSocket for logs.");
|
||||
console.log("Connected to WebSocket for live logs.");
|
||||
socket.emit('join_station_room', { station_id: selectedStation.id });
|
||||
});
|
||||
|
||||
|
|
@ -86,12 +276,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const { stationId, topic, data } = message;
|
||||
if (stationId !== selectedStation.id) return;
|
||||
|
||||
lastUpdateEl.textContent = 'Last Recv ' + new Date().toLocaleTimeString();
|
||||
|
||||
// Just prepend live messages as they arrive
|
||||
const now = new Date().toISOString();
|
||||
if (topic.endsWith('EVENTS')) {
|
||||
prependLog(eventLogArea, data);
|
||||
appendLog(eventLogArea, data, topic, now);
|
||||
} else if (topic.endsWith('REQUEST')) {
|
||||
prependLog(requestLogArea, data);
|
||||
appendLog(requestLogArea, data, topic, now);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -102,12 +292,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
requestLogArea.value = '';
|
||||
eventLogArea.value = '';
|
||||
});
|
||||
|
||||
// (The rest of your button listeners are unchanged)
|
||||
if(logoutBtn) logoutBtn.addEventListener('click', () => {
|
||||
localStorage.clear();
|
||||
window.location.href = 'index.html';
|
||||
});
|
||||
if(refreshBtn) refreshBtn.addEventListener('click', () => location.reload());
|
||||
// if(downloadBtn) downloadBtn.addEventListener('click', () => alert("Download functionality can be added here.")); // Placeholder for download modal
|
||||
if(resetBtn) resetBtn.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to reset the station?')) {
|
||||
sendCommand('STATION_RESET');
|
||||
|
|
@ -115,8 +306,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
|
||||
// --- STARTUP ---
|
||||
fetchAndRenderLogs(); // Fetch historical logs on page load
|
||||
checkStationStatus();
|
||||
statusPollingInterval = setInterval(checkStationStatus, 10000);
|
||||
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<script src="js/common-header.js"></script>
|
||||
<script src="./js/common-header.js"></script>
|
||||
<script src="./js/logs.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue