fix: analytics page
parent
219fbd92b4
commit
eb88959660
338
backend/main.py
338
backend/main.py
|
|
@ -39,7 +39,7 @@ app = Flask(__name__)
|
|||
|
||||
# CORS(app, resources={r"/api/*": {"origins": "http://127.0.0.1:5500"}}, supports_credentials=True, expose_headers='Content-Disposition')
|
||||
|
||||
CORS(app, resources={r"/api/*": {"origins": ["http://192.168.1.12:5500","http://127.0.0.1:5500"]}}, supports_credentials=True, expose_headers='Content-Disposition')
|
||||
CORS(app, resources={r"/api/*": {"origins": ["http://10.10.1.183:5500","http://127.0.0.1:5500"]}}, supports_credentials=True, expose_headers='Content-Disposition')
|
||||
|
||||
# CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}}) , "http://127.0.0.1:5500"
|
||||
# This tells Flask: "For any route starting with /api/, allow requests
|
||||
|
|
@ -66,52 +66,8 @@ mqtt_clients = {}
|
|||
last_message_timestamps = {}
|
||||
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}")
|
||||
|
||||
decoded_data = None
|
||||
message_type = topic.split('/')[-1]
|
||||
|
||||
if message_type == 'PERIODIC':
|
||||
decoded_data = decoder.decode_periodic(payload)
|
||||
elif message_type == 'EVENTS':
|
||||
decoded_data = decoder.decode_event(payload)
|
||||
elif message_type == 'REQUEST':
|
||||
decoded_data = decoder.decode_rpc_request(payload)
|
||||
|
||||
if decoded_data:
|
||||
# print("DECODED DATA TO BE SENT:", decoded_data)
|
||||
try:
|
||||
with app.app_context():
|
||||
log_entry = MqttLog(
|
||||
station_id=station_id,
|
||||
topic=topic,
|
||||
topic_type=message_type,
|
||||
payload=decoded_data
|
||||
)
|
||||
db.session.add(log_entry)
|
||||
db.session.commit()
|
||||
print(f"Successfully wrote data for {station_id} to PostgreSQL.")
|
||||
except Exception as e:
|
||||
print(f"Error writing to PostgreSQL: {e}")
|
||||
|
||||
socketio.emit('dashboard_update', {
|
||||
'stationId': station_id,
|
||||
'topic': topic,
|
||||
'data': decoded_data
|
||||
}, room=station_id)
|
||||
|
||||
# If the message is an EVENT or PERIODIC data, it could affect analytics.
|
||||
if message_type in ['EVENTS', 'PERIODIC']:
|
||||
print(f"Analytics-related data received ({message_type}). Notifying clients to refresh.")
|
||||
socketio.emit('analytics_updated', room=station_id)
|
||||
|
||||
# --- (WebSocket and API routes remain the same) ---
|
||||
@socketio.on('connect')
|
||||
|
|
@ -314,11 +270,16 @@ def get_all_station_stats():
|
|||
@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.
|
||||
Fetches the 50 most recent non-periodic (EVENTS and REQUEST) 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()
|
||||
# --- THIS IS THE FIX ---
|
||||
# Query the MqttLog table, filtering for only EVENTS and REQUEST topic types.
|
||||
logs = MqttLog.query.filter(
|
||||
MqttLog.station_id == station_id,
|
||||
MqttLog.topic_type.in_(['EVENTS', 'REQUEST'])
|
||||
).order_by(desc(MqttLog.timestamp)).limit(50).all()
|
||||
|
||||
# We reverse the list so the oldest are first, for correct display order
|
||||
logs.reverse()
|
||||
|
|
@ -348,9 +309,157 @@ ABORT_REASON_MAP = {
|
|||
}
|
||||
|
||||
#--- Analytics Route ---
|
||||
# @app.route('/api/analytics', methods=['GET'])
|
||||
# def get_analytics_data():
|
||||
# # 1. Get and validate request parameters (same as before)
|
||||
# station_id = request.args.get('station_id')
|
||||
# start_date_str = request.args.get('start_date')
|
||||
# end_date_str = request.args.get('end_date')
|
||||
|
||||
# if not all([station_id, start_date_str, end_date_str]):
|
||||
# return jsonify({"message": "Missing required parameters."}), 400
|
||||
|
||||
# try:
|
||||
# start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
|
||||
# end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
||||
# start_datetime = datetime.combine(start_date, datetime.min.time())
|
||||
# end_datetime = datetime.combine(end_date, datetime.max.time())
|
||||
# except ValueError:
|
||||
# return jsonify({"message": "Invalid date format. Please use YYYY-MM-DD."}), 400
|
||||
|
||||
# # 2. Query for EVENT logs (for swap calculations)
|
||||
# try:
|
||||
# event_logs = MqttLog.query.filter(
|
||||
# MqttLog.station_id == station_id,
|
||||
# MqttLog.topic_type == 'EVENTS',
|
||||
# MqttLog.timestamp.between(start_datetime, end_datetime)
|
||||
# ).order_by(MqttLog.timestamp.asc()).all() # <-- ADD THIS SORTING
|
||||
# except Exception as e:
|
||||
# return jsonify({"message": f"Could not query event logs: {e}"}), 500
|
||||
|
||||
# # --- NEW: Query for PERIODIC logs (for uptime calculation) ---
|
||||
# try:
|
||||
# periodic_logs = MqttLog.query.filter(
|
||||
# MqttLog.station_id == station_id,
|
||||
# MqttLog.topic_type == 'PERIODIC',
|
||||
# MqttLog.timestamp.between(start_datetime, end_datetime)
|
||||
# ).order_by(MqttLog.timestamp.asc()).all()
|
||||
# except Exception as e:
|
||||
# return jsonify({"message": f"Could not query periodic logs: {e}"}), 500
|
||||
|
||||
# # --- 3. REVISED: Process logs to calculate KPIs and chart data ---
|
||||
# swap_starts = {} # Dictionary to store start times by sessionId
|
||||
# completed_swap_times = []
|
||||
|
||||
# total_swaps, completed_swaps, aborted_swaps = 0, 0, 0
|
||||
# daily_completed, daily_aborted, hourly_swaps, abort_reason_counts = {}, {}, [0] * 24, {}
|
||||
# slot_utilization_counts = {i: 0 for i in range(1, 10)}
|
||||
|
||||
# print("\n--- STARTING SWAP ANALYSIS ---") # Add this line
|
||||
# for log in event_logs:
|
||||
# event_type = log.payload.get('eventType')
|
||||
# session_id = log.payload.get('sessionId')
|
||||
# log_date = log.timestamp.date()
|
||||
# log_hour = log.timestamp.hour
|
||||
|
||||
# if event_type == 'EVENT_SWAP_START':
|
||||
# total_swaps += 1
|
||||
# hourly_swaps[log_hour] += 1
|
||||
# if session_id:
|
||||
# swap_starts[session_id] = log.timestamp # Store start time
|
||||
# print(f"Found START for session '{session_id}' at {log.timestamp}") # Add this line
|
||||
|
||||
# elif event_type == 'EVENT_SWAP_ENDED':
|
||||
# completed_swaps += 1
|
||||
# daily_completed[log_date] = daily_completed.get(log_date, 0) + 1
|
||||
# if session_id and session_id in swap_starts:
|
||||
# # Calculate duration if we have a matching start event
|
||||
# duration = (log.timestamp - swap_starts[session_id]).total_seconds()
|
||||
# completed_swap_times.append(duration)
|
||||
# print(f"Found MATCHING END for session '{session_id}'. Duration: {duration}s") # Add this line
|
||||
# del swap_starts[session_id] # Remove to prevent reuse
|
||||
# else:
|
||||
# print(f"Found END event but could not find matching START for session '{session_id}'") # Add this line
|
||||
|
||||
# elif event_type == 'EVENT_SWAP_ABORTED':
|
||||
# aborted_swaps += 1
|
||||
# daily_aborted[log_date] = daily_aborted.get(log_date, 0) + 1
|
||||
# reason = log.payload.get('eventData', {}).get('swapAbortReason', 'ABORT_UNKNOWN')
|
||||
# abort_reason_counts[reason] = abort_reason_counts.get(reason, 0) + 1
|
||||
|
||||
# elif event_type == 'EVENT_BATTERY_EXIT':
|
||||
# slot_id = log.payload.get('eventData', {}).get('slotId')
|
||||
# if slot_id and slot_id in slot_utilization_counts:
|
||||
# slot_utilization_counts[slot_id] += 1
|
||||
|
||||
# print(f"--- ANALYSIS COMPLETE ---") # Add this line
|
||||
# print(f"Calculated Durations: {completed_swap_times}") # Add this line
|
||||
|
||||
# # --- NEW: 4. Calculate Station Uptime ---
|
||||
# total_period_seconds = (end_datetime - start_datetime).total_seconds()
|
||||
# total_downtime_seconds = 0
|
||||
# MAX_ONLINE_GAP_SECONDS = 30 # Assume offline if no message for over 30 seconds
|
||||
|
||||
# if not periodic_logs:
|
||||
# total_downtime_seconds = total_period_seconds
|
||||
# else:
|
||||
# # Check gap from start time to first message
|
||||
# first_gap = (periodic_logs[0].timestamp - start_datetime).total_seconds()
|
||||
# if first_gap > MAX_ONLINE_GAP_SECONDS:
|
||||
# total_downtime_seconds += first_gap
|
||||
|
||||
# # Check gaps between consecutive messages
|
||||
# for i in range(1, len(periodic_logs)):
|
||||
# gap = (periodic_logs[i].timestamp - periodic_logs[i-1].timestamp).total_seconds()
|
||||
# if gap > MAX_ONLINE_GAP_SECONDS:
|
||||
# total_downtime_seconds += gap
|
||||
|
||||
# # Check gap from last message to end time
|
||||
# last_gap = (end_datetime - periodic_logs[-1].timestamp).total_seconds()
|
||||
# if last_gap > MAX_ONLINE_GAP_SECONDS:
|
||||
# total_downtime_seconds += last_gap
|
||||
|
||||
# station_uptime = 100 * (1 - (total_downtime_seconds / total_period_seconds))
|
||||
# station_uptime = max(0, min(100, station_uptime)) # Ensure value is between 0 and 100
|
||||
|
||||
# # 5. Prepare final data structures (KPI section is now updated)
|
||||
# avg_swap_time_seconds = sum(completed_swap_times) / len(completed_swap_times) if completed_swap_times else 0
|
||||
|
||||
# # avg_swap_time_seconds = sum(completed_swap_times) / len(completed_swap_times) if completed_swap_times else None
|
||||
|
||||
# kpi_data = {
|
||||
# "total_swaps": total_swaps, "completed_swaps": completed_swaps,
|
||||
# "aborted_swaps": aborted_swaps, "avg_swap_time_seconds": avg_swap_time_seconds,
|
||||
# "station_uptime": round(station_uptime, 2) # Add uptime to the KPI object
|
||||
# }
|
||||
|
||||
# # (The rest of the chart data preparation is unchanged)
|
||||
# date_labels, completed_data, aborted_data = [], [], []
|
||||
# current_date = start_date
|
||||
# while current_date <= end_date:
|
||||
# date_labels.append(current_date.strftime('%b %d'))
|
||||
# completed_data.append(daily_completed.get(current_date, 0))
|
||||
# aborted_data.append(daily_aborted.get(current_date, 0))
|
||||
# current_date += timedelta(days=1)
|
||||
|
||||
# swap_activity_data = {"labels": date_labels, "completed_data": completed_data, "aborted_data": aborted_data}
|
||||
# hourly_distribution_data = {"labels": [f"{h % 12 if h % 12 != 0 else 12} {'AM' if h < 12 else 'PM'}" for h in range(24)], "swap_data": hourly_swaps}
|
||||
# abort_reasons_data = {"labels": [ABORT_REASON_MAP.get(r, r) for r in abort_reason_counts.keys()], "reason_data": list(abort_reason_counts.values())}
|
||||
# slot_utilization_data = {"counts": [slot_utilization_counts[i] for i in range(1, 10)]} # Return counts as a simple list [_ , _, ...]
|
||||
|
||||
# # 6. Combine all data and return
|
||||
# return jsonify({
|
||||
# "kpis": kpi_data,
|
||||
# "swap_activity": swap_activity_data,
|
||||
# "hourly_distribution": hourly_distribution_data,
|
||||
# "abort_reasons": abort_reasons_data,
|
||||
# "slot_utilization": slot_utilization_data # <-- ADD THIS NEW KEY
|
||||
# })
|
||||
|
||||
|
||||
@app.route('/api/analytics', methods=['GET'])
|
||||
def get_analytics_data():
|
||||
# 1. Get and validate request parameters (same as before)
|
||||
# 1. Get and validate request parameters
|
||||
station_id = request.args.get('station_id')
|
||||
start_date_str = request.args.get('start_date')
|
||||
end_date_str = request.args.get('end_date')
|
||||
|
|
@ -366,103 +475,66 @@ def get_analytics_data():
|
|||
except ValueError:
|
||||
return jsonify({"message": "Invalid date format. Please use YYYY-MM-DD."}), 400
|
||||
|
||||
# 2. Query for EVENT logs (for swap calculations)
|
||||
# 2. Query for ALL relevant logs (EVENTS and REQUESTS) in one go
|
||||
try:
|
||||
event_logs = MqttLog.query.filter(
|
||||
logs = MqttLog.query.filter(
|
||||
MqttLog.station_id == station_id,
|
||||
MqttLog.topic_type == 'EVENTS',
|
||||
MqttLog.topic_type.in_(['EVENTS', 'REQUEST']),
|
||||
MqttLog.timestamp.between(start_datetime, end_datetime)
|
||||
).order_by(MqttLog.timestamp.asc()).all() # <-- ADD THIS SORTING
|
||||
except Exception as e:
|
||||
return jsonify({"message": f"Could not query event logs: {e}"}), 500
|
||||
).order_by(MqttLog.timestamp.asc()).all()
|
||||
|
||||
# --- NEW: Query for PERIODIC logs (for uptime calculation) ---
|
||||
try:
|
||||
periodic_logs = MqttLog.query.filter(
|
||||
MqttLog.station_id == station_id,
|
||||
MqttLog.topic_type == 'PERIODIC',
|
||||
MqttLog.timestamp.between(start_datetime, end_datetime)
|
||||
).order_by(MqttLog.timestamp.asc()).all()
|
||||
except Exception as e:
|
||||
return jsonify({"message": f"Could not query periodic logs: {e}"}), 500
|
||||
return jsonify({"message": f"Could not query logs: {e}"}), 500
|
||||
|
||||
# # 3. Process EVENT logs for swap KPIs and charts
|
||||
# total_swaps, completed_swaps, aborted_swaps = 0, 0, 0
|
||||
# completed_swap_times, daily_completed, daily_aborted, hourly_swaps, abort_reason_counts = [], {}, {}, [0] * 24, {}
|
||||
# slot_utilization_counts = {i: 0 for i in range(1, 10)} # For the heatmap
|
||||
|
||||
# for log in event_logs:
|
||||
# # (This processing logic is unchanged)
|
||||
# event_type = log.payload.get('eventType')
|
||||
# log_date = log.timestamp.date()
|
||||
# log_hour = log.timestamp.hour
|
||||
# if event_type == 'EVENT_SWAP_START':
|
||||
# total_swaps += 1
|
||||
# hourly_swaps[log_hour] += 1
|
||||
# elif event_type == 'EVENT_SWAP_ENDED':
|
||||
# completed_swaps += 1
|
||||
# daily_completed[log_date] = daily_completed.get(log_date, 0) + 1
|
||||
# swap_time = log.payload.get('eventData', {}).get('swapTime')
|
||||
# if swap_time is not None:
|
||||
# completed_swap_times.append(swap_time)
|
||||
# elif event_type == 'EVENT_SWAP_ABORTED':
|
||||
# aborted_swaps += 1
|
||||
# daily_aborted[log_date] = daily_aborted.get(log_date, 0) + 1
|
||||
# reason = log.payload.get('eventData', {}).get('swapAbortReason', 'ABORT_UNKNOWN')
|
||||
# abort_reason_counts[reason] = abort_reason_counts.get(reason, 0) + 1
|
||||
# elif event_type == 'EVENT_BATTERY_EXIT':
|
||||
# slot_id = log.payload.get('eventData', {}).get('slotId')
|
||||
# if slot_id and slot_id in slot_utilization_counts:
|
||||
# slot_utilization_counts[slot_id] += 1
|
||||
|
||||
# --- 3. REVISED: Process logs to calculate KPIs and chart data ---
|
||||
swap_starts = {} # Dictionary to store start times by sessionId
|
||||
# 3. Initialize data structures for processing
|
||||
swap_starts_map = {}
|
||||
completed_swap_times = []
|
||||
|
||||
total_swaps, completed_swaps, aborted_swaps = 0, 0, 0
|
||||
total_initiations = 0
|
||||
total_starts = 0
|
||||
completed_swaps = 0
|
||||
aborted_swaps = 0
|
||||
|
||||
daily_completed, daily_aborted, hourly_swaps, abort_reason_counts = {}, {}, [0] * 24, {}
|
||||
slot_utilization_counts = {i: 0 for i in range(1, 10)}
|
||||
|
||||
print("\n--- STARTING SWAP ANALYSIS ---") # Add this line
|
||||
for log in event_logs:
|
||||
# 4. Process the logs to calculate all KPIs and chart data
|
||||
for log in logs:
|
||||
event_type = log.payload.get('eventType')
|
||||
job_type = log.payload.get('jobType')
|
||||
session_id = log.payload.get('sessionId')
|
||||
log_date = log.timestamp.date()
|
||||
log_hour = log.timestamp.hour
|
||||
|
||||
if event_type == 'EVENT_SWAP_START':
|
||||
total_swaps += 1
|
||||
if job_type == 'JOBTYPE_SWAP_AUTH_SUCCESS':
|
||||
total_initiations += 1
|
||||
elif event_type == 'EVENT_SWAP_START':
|
||||
total_starts += 1
|
||||
hourly_swaps[log_hour] += 1
|
||||
if session_id:
|
||||
swap_starts[session_id] = log.timestamp # Store start time
|
||||
print(f"Found START for session '{session_id}' at {log.timestamp}") # Add this line
|
||||
|
||||
swap_starts_map[session_id] = log.timestamp
|
||||
elif event_type == 'EVENT_SWAP_ENDED':
|
||||
completed_swaps += 1
|
||||
daily_completed[log_date] = daily_completed.get(log_date, 0) + 1
|
||||
if session_id and session_id in swap_starts:
|
||||
# Calculate duration if we have a matching start event
|
||||
duration = (log.timestamp - swap_starts[session_id]).total_seconds()
|
||||
if session_id and session_id in swap_starts_map:
|
||||
duration = (log.timestamp - swap_starts_map[session_id]).total_seconds()
|
||||
completed_swap_times.append(duration)
|
||||
print(f"Found MATCHING END for session '{session_id}'. Duration: {duration}s") # Add this line
|
||||
del swap_starts[session_id] # Remove to prevent reuse
|
||||
else:
|
||||
print(f"Found END event but could not find matching START for session '{session_id}'") # Add this line
|
||||
|
||||
del swap_starts_map[session_id]
|
||||
elif event_type == 'EVENT_SWAP_ABORTED':
|
||||
aborted_swaps += 1
|
||||
daily_aborted[log_date] = daily_aborted.get(log_date, 0) + 1
|
||||
reason = log.payload.get('eventData', {}).get('swapAbortReason', 'ABORT_UNKNOWN')
|
||||
abort_reason_counts[reason] = abort_reason_counts.get(reason, 0) + 1
|
||||
|
||||
elif event_type == 'EVENT_BATTERY_EXIT':
|
||||
slot_id = log.payload.get('eventData', {}).get('slotId')
|
||||
if slot_id and slot_id in slot_utilization_counts:
|
||||
slot_utilization_counts[slot_id] += 1
|
||||
|
||||
print(f"--- ANALYSIS COMPLETE ---") # Add this line
|
||||
print(f"Calculated Durations: {completed_swap_times}") # Add this line
|
||||
|
||||
# --- NEW: 4. Calculate Station Uptime ---
|
||||
total_period_seconds = (end_datetime - start_datetime).total_seconds()
|
||||
total_downtime_seconds = 0
|
||||
|
|
@ -490,35 +562,18 @@ def get_analytics_data():
|
|||
station_uptime = 100 * (1 - (total_downtime_seconds / total_period_seconds))
|
||||
station_uptime = max(0, min(100, station_uptime)) # Ensure value is between 0 and 100
|
||||
|
||||
# if not periodic_logs:
|
||||
# total_downtime_seconds = total_period_seconds
|
||||
# else:
|
||||
# first_gap = (periodic_logs[0].timestamp - start_datetime).total_seconds()
|
||||
# if first_gap > MAX_ONLINE_GAP_SECONDS:
|
||||
# total_downtime_seconds += first_gap
|
||||
# for i in range(1, len(periodic_logs)):
|
||||
# gap = (periodic_logs[i].timestamp - periodic_logs[i-1].timestamp).total_seconds()
|
||||
# if gap > MAX_ONLINE_GAP_SECONDS:
|
||||
# total_downtime_seconds += gap
|
||||
# last_gap = (end_datetime - periodic_logs[-1].timestamp).total_seconds()
|
||||
# if last_gap > MAX_ONLINE_GAP_SECONDS:
|
||||
# total_downtime_seconds += last_gap
|
||||
# station_uptime = 100 * (1 - (total_downtime_seconds / total_period_seconds))
|
||||
# station_uptime = max(0, min(100, station_uptime))
|
||||
|
||||
|
||||
# 5. Prepare final data structures (KPI section is now updated)
|
||||
avg_swap_time_seconds = sum(completed_swap_times) / len(completed_swap_times) if completed_swap_times else 0
|
||||
|
||||
# avg_swap_time_seconds = sum(completed_swap_times) / len(completed_swap_times) if completed_swap_times else None
|
||||
# 6. Prepare final data structures
|
||||
avg_swap_time_seconds = sum(completed_swap_times) / len(completed_swap_times) if completed_swap_times else None
|
||||
|
||||
kpi_data = {
|
||||
"total_swaps": total_swaps, "completed_swaps": completed_swaps,
|
||||
"aborted_swaps": aborted_swaps, "avg_swap_time_seconds": avg_swap_time_seconds,
|
||||
"station_uptime": round(station_uptime, 2) # Add uptime to the KPI object
|
||||
"total_swaps_initiated": total_initiations,
|
||||
"total_swaps_started": total_starts,
|
||||
"completed_swaps": completed_swaps,
|
||||
"aborted_swaps": aborted_swaps,
|
||||
"avg_swap_time_seconds": avg_swap_time_seconds,
|
||||
"station_uptime": round(station_uptime, 2)
|
||||
}
|
||||
|
||||
# (The rest of the chart data preparation is unchanged)
|
||||
date_labels, completed_data, aborted_data = [], [], []
|
||||
current_date = start_date
|
||||
while current_date <= end_date:
|
||||
|
|
@ -532,13 +587,14 @@ def get_analytics_data():
|
|||
abort_reasons_data = {"labels": [ABORT_REASON_MAP.get(r, r) for r in abort_reason_counts.keys()], "reason_data": list(abort_reason_counts.values())}
|
||||
slot_utilization_data = {"counts": [slot_utilization_counts[i] for i in range(1, 10)]} # Return counts as a simple list [_ , _, ...]
|
||||
|
||||
# 6. Combine all data and return
|
||||
|
||||
# 7. Combine all data and return
|
||||
return jsonify({
|
||||
"kpis": kpi_data,
|
||||
"swap_activity": swap_activity_data,
|
||||
"hourly_distribution": hourly_distribution_data,
|
||||
"abort_reasons": abort_reasons_data,
|
||||
"slot_utilization": slot_utilization_data # <-- ADD THIS NEW KEY
|
||||
"slot_utilization": slot_utilization_data
|
||||
})
|
||||
|
||||
# --- CSV Export route (UPDATED) ---
|
||||
|
|
@ -564,6 +620,12 @@ def _format_periodic_row(payload, num_slots=9):
|
|||
for i in range(1, num_slots + 1):
|
||||
slot = slot_map.get(i)
|
||||
if slot:
|
||||
# Convert boolean values to readable text
|
||||
# door_status_text = "OPEN" if slot.get("doorStatus", 0) == 1 else "CLOSED"
|
||||
# door_lock_status_text = "UNLOCKED" if slot.get("doorLockStatus", 0) == 1 else "LOCKED"
|
||||
# battery_present_text = "YES" if slot.get("batteryPresent", 0) == 1 else "NO"
|
||||
# charger_present_text = "YES" if slot.get("chargerPresent", 0) == 1 else "NO"
|
||||
|
||||
row.extend([
|
||||
slot.get('batteryIdentification', ''),
|
||||
slot.get("batteryPresent", 0),
|
||||
|
|
@ -836,5 +898,5 @@ if __name__ == '__main__':
|
|||
mqtt_thread = threading.Thread(target=start_mqtt_clients, daemon=True)
|
||||
mqtt_thread.start()
|
||||
|
||||
print(f"Starting Flask-SocketIO server on http://192.168.1.12:5000")
|
||||
socketio.run(app, host='192.168.1.12', port=5000)
|
||||
print(f"Starting Flask-SocketIO server on http://10.10.1.183:5000")
|
||||
socketio.run(app, host='10.10.1.183', port=5000)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -25,6 +25,7 @@ enum jobType_e {
|
|||
JOBTYPE_REBOOT = 0x104;
|
||||
JOBTYPE_SWAP_DENY = 0x105;
|
||||
JOBTYPE_LANGUAGE_UPDATE = 0x106;
|
||||
JOBTYPE_SWAP_AUTH_SUCCESS = 0x107;
|
||||
}
|
||||
|
||||
enum jobResult_e {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -33,6 +33,7 @@ class jobType_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|||
JOBTYPE_REBOOT: _ClassVar[jobType_e]
|
||||
JOBTYPE_SWAP_DENY: _ClassVar[jobType_e]
|
||||
JOBTYPE_LANGUAGE_UPDATE: _ClassVar[jobType_e]
|
||||
JOBTYPE_SWAP_AUTH_SUCCESS: _ClassVar[jobType_e]
|
||||
|
||||
class jobResult_e(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
|
|
@ -92,6 +93,7 @@ JOBTYPE_TRANSACTION_ABORT: jobType_e
|
|||
JOBTYPE_REBOOT: jobType_e
|
||||
JOBTYPE_SWAP_DENY: jobType_e
|
||||
JOBTYPE_LANGUAGE_UPDATE: jobType_e
|
||||
JOBTYPE_SWAP_AUTH_SUCCESS: jobType_e
|
||||
JOB_RESULT_UNKNOWN: jobResult_e
|
||||
JOB_RESULT_SUCCESS: jobResult_e
|
||||
JOB_RESULT_REJECTED: jobResult_e
|
||||
|
|
|
|||
|
|
@ -184,7 +184,43 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-1 md:grid-cols-5 gap-3">
|
||||
<section class="grid grid-cols-2 lg:grid-cols-6 gap-3">
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Total Swaps Initiated</p>
|
||||
<p id="total-swaps-initiated" class="text-3xl font-extrabold text-sky-400">...</p>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Total Swaps Started</p>
|
||||
<p id="total-swaps-started" class="text-3xl font-extrabold text-blue-400">...</p>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Completed Swaps</p>
|
||||
<p class="text-3xl font-extrabold text-emerald-400">
|
||||
<span id="completed-swaps">...</span>
|
||||
<span id="success-rate" class="text-sm font-bold text-gray-400 ml-1">(...%)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Aborted Swaps</p>
|
||||
<p class="text-3xl font-extrabold text-rose-400">
|
||||
<span id="aborted-swaps">...</span>
|
||||
<span id="abort-rate" class="text-sm font-bold text-gray-400 ml-1">(...%)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Avg. Swap Time</p>
|
||||
<p id="avg-swap-time" class="text-3xl font-extrabold">
|
||||
<span id="avg-swap-time-value">...</span>
|
||||
<span class="text-lg font-bold text-gray-300">sec</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Station Uptime</p>
|
||||
<p id="station-uptime" class="text-3xl font-extrabold text-teal-400">... %</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- <section class="grid grid-cols-1 md:grid-cols-5 gap-3">
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Total Swaps Initiated</p>
|
||||
<p id="total-swaps" class="text-3xl font-extrabold text-sky-400">...</p>
|
||||
|
|
@ -214,7 +250,7 @@
|
|||
<p class="text-xs text-gray-400">Station Uptime</p>
|
||||
<p id="station-uptime" class="text-3xl font-extrabold text-teal-400">... %</p>
|
||||
</div>
|
||||
</section>
|
||||
</section> -->
|
||||
|
||||
<section class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// document.addEventListener('DOMContentLoaded', () => {
|
||||
// // --- CONFIGURATION ---
|
||||
// const SOCKET_URL = "http://192.168.1.12:5000";
|
||||
// const API_BASE = "http://192.168.1.12:5000/api";
|
||||
// const SOCKET_URL = "http://10.10.1.183:5000";
|
||||
// const API_BASE = "http://10.10.1.183:5000/api";
|
||||
|
||||
// // --- DOM ELEMENT REFERENCES ---
|
||||
// const stationNameEl = document.getElementById('station-name');
|
||||
|
|
@ -122,11 +122,12 @@
|
|||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- CONFIGURATION ---
|
||||
const API_BASE = "http://192.168.1.12:5000/api";
|
||||
const API_BASE = "http://10.10.1.183:5000/api";
|
||||
|
||||
// --- DOM ELEMENT REFERENCES ---
|
||||
// KPI Tiles
|
||||
const totalSwapsEl = document.getElementById('total-swaps');
|
||||
const totalInitiatedEl = document.getElementById('total-swaps-initiated');
|
||||
const totalStartedEl = document.getElementById('total-swaps-started');
|
||||
const completedSwapsEl = document.getElementById('completed-swaps');
|
||||
const successRateEl = document.getElementById('success-rate');
|
||||
const abortedSwapsEl = document.getElementById('aborted-swaps');
|
||||
|
|
@ -159,6 +160,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const resetBtn = document.getElementById('station-reset-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
|
||||
let socket;
|
||||
|
||||
// --- CONSTANTS ---
|
||||
|
||||
const chartDefaults = {
|
||||
|
|
@ -188,7 +191,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
*/
|
||||
const updateStatTiles = (data) => {
|
||||
if (!data) { // Used for loading state or on error
|
||||
totalSwapsEl.textContent = '...';
|
||||
totalInitiatedEl.textContent = '...';
|
||||
totalStartedEl.textContent = '...';
|
||||
completedSwapsEl.textContent = '...';
|
||||
successRateEl.textContent = '(...%)';
|
||||
abortedSwapsEl.textContent = '...';
|
||||
|
|
@ -198,28 +202,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const total = data.total_swaps ?? 0;
|
||||
// Assign new data from the backend
|
||||
totalInitiatedEl.textContent = data.total_swaps_initiated ?? 0;
|
||||
totalStartedEl.textContent = data.total_swaps_started ?? 0;
|
||||
|
||||
const completed = data.completed_swaps ?? 0;
|
||||
const aborted = data.aborted_swaps ?? 0;
|
||||
|
||||
totalSwapsEl.textContent = total;
|
||||
completedSwapsEl.textContent = completed;
|
||||
abortedSwapsEl.textContent = aborted;
|
||||
|
||||
const successRate = total > 0 ? ((completed / total) * 100).toFixed(1) : 0;
|
||||
// Use total_swaps_started for calculating success/abort rates
|
||||
const totalStarts = data.total_swaps_started ?? 0;
|
||||
const successRate = totalStarts > 0 ? ((completed / totalStarts) * 100).toFixed(1) : 0;
|
||||
successRateEl.textContent = `(${successRate}%)`;
|
||||
|
||||
const abortRate = total > 0 ? ((aborted / total) * 100).toFixed(1) : 0;
|
||||
const abortRate = totalStarts > 0 ? ((aborted / totalStarts) * 100).toFixed(1) : 0;
|
||||
abortRateEl.textContent = `(${abortRate}%)`;
|
||||
|
||||
const avgTimeInMinutes = data.avg_swap_time_seconds != null ? (data.avg_swap_time_seconds / 60).toFixed(1) : '—';
|
||||
|
||||
avgSwapTimeValueEl.textContent = avgTimeInMinutes;
|
||||
const avgTimeInSeconds = data.avg_swap_time_seconds != null ? Math.round(data.avg_swap_time_seconds) : '—';
|
||||
avgSwapTimeValueEl.textContent = avgTimeInSeconds;
|
||||
|
||||
stationUptimeEl.textContent = `${data.station_uptime ?? '...'} %`;
|
||||
|
||||
};
|
||||
|
||||
|
||||
// --- CHART.JS VISUALIZATION CODE ---
|
||||
|
||||
/**
|
||||
|
|
@ -470,6 +478,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
toDatePicker.setDate(todayStr, false);
|
||||
fetchAnalyticsData(todayStr, todayStr);
|
||||
|
||||
connectSocket();
|
||||
|
||||
// --- ADD THIS BLOCK FOR AUTO-REFRESH ---
|
||||
setInterval(() => {
|
||||
console.log("Auto-refreshing analytics data...");
|
||||
// Use .selectedDates[0] to get the date object
|
||||
const startDateObj = fromDatePicker.selectedDates[0];
|
||||
const endDateObj = toDatePicker.selectedDates[0];
|
||||
|
||||
if (startDateObj && endDateObj) {
|
||||
// Use the formatDate utility to get the string in the correct format
|
||||
const startDate = flatpickr.formatDate(startDateObj, "Y-m-d");
|
||||
const endDate = flatpickr.formatDate(endDateObj, "Y-m-d");
|
||||
fetchAnalyticsData(startDate, endDate);
|
||||
}
|
||||
}, 30000); // Refreshes every 30 seconds (30000 milliseconds)
|
||||
|
||||
// --- EVENT LISTENERS ---
|
||||
applyRangeBtn.addEventListener('click', () => {
|
||||
const startDate = fromDateInput.value;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('http://192.168.1.12:5000/api/login', {
|
||||
const response = await fetch('http://10.10.1.183:5000/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// frontend/js/common-header.js
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- CONFIGURATION ---
|
||||
const SOCKET_URL = "http://192.168.1.12:5000";
|
||||
const API_BASE = "http://192.168.1.12:5000/api";
|
||||
const SOCKET_URL = "http://10.10.1.183:5000";
|
||||
const API_BASE = "http://10.10.1.183:5000/api";
|
||||
|
||||
// --- STATE & SELECTED STATION ---
|
||||
let selectedStation = null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- CONFIGURATION ---
|
||||
const SOCKET_URL = "http://192.168.1.12:5000";
|
||||
const API_BASE = "http://192.168.1.12:5000/api"; // Added for API calls
|
||||
const SOCKET_URL = "http://10.10.1.183:5000";
|
||||
const API_BASE = "http://10.10.1.183:5000/api"; // Added for API calls
|
||||
|
||||
// --- DOM ELEMENT REFERENCES ---
|
||||
const grid = document.getElementById('chambersGrid');
|
||||
|
|
@ -38,6 +38,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
"Earth Leakage"
|
||||
];
|
||||
|
||||
const BATTERY_FAULT_MAP = {
|
||||
8: "UT", // Under Temperature
|
||||
4: "OV", // Over Voltage
|
||||
2: "OT", // Over Temperature
|
||||
1: "OC" // Over Current
|
||||
};
|
||||
|
||||
const CHARGER_FAULT_MAP = {
|
||||
1: "OV", // Over Voltage
|
||||
2: "UV", // Under Voltage
|
||||
4: "OT", // Over Temperature
|
||||
8: "CAN Failure"
|
||||
// Add other charger fault codes here
|
||||
};
|
||||
|
||||
// --- NEW: SWAP PROCESS ELEMENTS & LOGIC ---
|
||||
const swapIdleText = document.getElementById('swap-idle-text');
|
||||
const swapPairsList = document.getElementById('swap-pairs-list');
|
||||
|
|
@ -150,6 +165,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
logTextArea.value = newLog + logTextArea.value;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Decodes a fault bitmask into a human-readable string using a given map.
|
||||
* @param {number} faultCode The fault code number.
|
||||
* @param {object} faultMap The map to use for decoding (e.g., BATTERY_FAULT_MAP).
|
||||
* @returns {string} A comma-separated string of active faults, or "—" if none.
|
||||
*/
|
||||
const decodeFaults = (faultCode, faultMap) => {
|
||||
if (!faultCode || faultCode === 0) {
|
||||
return "—"; // No fault
|
||||
}
|
||||
|
||||
const activeFaults = [];
|
||||
for (const bit in faultMap) {
|
||||
if ((faultCode & bit) !== 0) {
|
||||
activeFaults.push(faultMap[bit]);
|
||||
}
|
||||
}
|
||||
|
||||
return activeFaults.length > 0 ? activeFaults.join(', ') : "—";
|
||||
};
|
||||
|
||||
|
||||
const updateChamberUI = (card, slot) => {
|
||||
if (!card || !slot) return;
|
||||
|
||||
|
|
@ -177,18 +215,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
card.querySelector('.soc').textContent = `${slot.soc || 0}%`;
|
||||
card.querySelector('.voltage').textContent = `${((slot.voltage || 0) / 1000).toFixed(1)} V`;
|
||||
card.querySelector('.bat-temp').textContent = `${((slot.batteryMaxTemp || 0) / 10).toFixed(1)} °C`;
|
||||
card.querySelector('.bat-fault').textContent = slot.batteryFaultCode || '—';
|
||||
// card.querySelector('.bat-fault').textContent = slot.batteryFaultCode || '—';
|
||||
|
||||
card.querySelector('.bat-fault').textContent = decodeFaults(slot.batteryFaultCode, BATTERY_FAULT_MAP);
|
||||
|
||||
// card.querySelector('.bat-fault').textContent = decodeBatteryFaults(slot.batteryFaultCode);
|
||||
|
||||
card.querySelector('.current').textContent = `${((slot.current || 0) / 1000).toFixed(1)} A`;
|
||||
card.querySelector('.slot-temp').textContent = `${((slot.slotTemperature || 0) / 10).toFixed(1)} °C`;
|
||||
card.querySelector('.chg-fault').textContent = slot.chargerFaultCode || '—';
|
||||
card.querySelector('.chg-temp').textContent = `${((slot.chargerMaxTemp || 0) / 10).toFixed(1)} °C`;
|
||||
// card.querySelector('.chg-fault').textContent = slot.chargerFaultCode || '—';
|
||||
card.querySelector('.chg-fault').textContent = decodeFaults(slot.chargerFaultCode, CHARGER_FAULT_MAP);
|
||||
|
||||
const batPill = card.querySelector('.battery-status-pill');
|
||||
batPill.innerHTML = `<span class="h-2 w-2 rounded-full bg-emerald-400 animate-pulseDot"></span> Present`;
|
||||
batPill.className = 'battery-status-pill chip chip-emerald';
|
||||
|
||||
const chgPill = card.querySelector('.charger-status-pill');
|
||||
if (slot.chargerMode === 1) {
|
||||
if (slot.chargingStatus === 1) {
|
||||
chgPill.innerHTML = `<span class="h-2 w-2 rounded-full bg-sky-400 animate-pulseDot"></span> Charging`;
|
||||
chgPill.className = 'charger-status-pill chip chip-sky';
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,166 +1,7 @@
|
|||
// 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";
|
||||
const API_BASE = "http://192.168.1.12:5000/api";
|
||||
const SOCKET_URL = "http://10.10.1.183:5000";
|
||||
const API_BASE = "http://10.10.1.183:5000/api";
|
||||
|
||||
// --- DOM ELEMENT REFERENCES ---
|
||||
const stationNameEl = document.getElementById('station-name');
|
||||
|
|
@ -169,72 +10,42 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const productIdEl = document.getElementById('product-id');
|
||||
const requestLogArea = document.getElementById('request-log-area');
|
||||
const eventLogArea = document.getElementById('event-log-area');
|
||||
const connChip = document.getElementById('connection-status-chip');
|
||||
const clearReqBtn = document.getElementById('clear-req');
|
||||
const clearEvtBtn = document.getElementById('clear-evt');
|
||||
const clearAllBtn = document.getElementById('clear-all');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const resetBtn = document.getElementById('station-reset-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
|
||||
// --- STATE ---
|
||||
let selectedStation = null;
|
||||
let socket;
|
||||
let statusPollingInterval;
|
||||
|
||||
// --- HELPER FUNCTIONS ---
|
||||
// --- HELPER FUNCTIONS --
|
||||
|
||||
const appendLog = (textarea, data, topic, timestampStr) => {
|
||||
if (!textarea) return;
|
||||
|
||||
const timestamp = new Date(timestampStr).toLocaleTimeString();
|
||||
const formattedJson = JSON.stringify(data, null, 2);
|
||||
const newLog = `[${timestamp}] - Topic: ${topic}\n${formattedJson}\n\n---------------------------------\n\n`;
|
||||
|
||||
// Append the new log to the end of the existing text
|
||||
// Clean up the topic for better display
|
||||
const topicParts = topic.split('/');
|
||||
const shortTopic = topicParts.slice(-2).join('/'); // Gets the last two parts, e.g., "RPC/REQUEST" or ".../EVENTS"
|
||||
|
||||
const newLog = `[${timestamp}] - Topic: ${shortTopic}\n${formattedJson}\n\n---------------------------------\n\n`;
|
||||
|
||||
textarea.value += newLog;
|
||||
|
||||
// Auto-scroll to the bottom to always show the newest log
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
};
|
||||
|
||||
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 () => {
|
||||
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); }
|
||||
};
|
||||
|
||||
// --- NEW: Fetch recent logs from the database ---
|
||||
const fetchAndRenderLogs = async () => {
|
||||
const fetchRecentLogs = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/logs/recent/${selectedStation.id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch recent logs');
|
||||
}
|
||||
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 = '';
|
||||
|
||||
|
|
@ -246,37 +57,72 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
});
|
||||
console.log(`Successfully fetched and rendered ${logs.length} recent logs.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// --- INITIALIZATION ---
|
||||
// --- INITIALIZATION AND EVENT HANDLERS ---
|
||||
function init() {
|
||||
console.log("1. Starting initialization...");
|
||||
|
||||
// Step 1: Load the station from localStorage.
|
||||
try {
|
||||
selectedStation = JSON.parse(localStorage.getItem('selected_station'));
|
||||
if (!selectedStation || !selectedStation.id) {
|
||||
throw new Error('No station selected.');
|
||||
const stationData = localStorage.getItem('selected_station');
|
||||
console.log("2. Fetched from localStorage:", stationData);
|
||||
|
||||
if (!stationData) {
|
||||
throw new Error('No station data found in localStorage.');
|
||||
}
|
||||
deviceIdEl.textContent = selectedStation.id;
|
||||
productIdEl.textContent = selectedStation.product_id;
|
||||
|
||||
selectedStation = JSON.parse(stationData);
|
||||
console.log("3. Parsed station data:", selectedStation);
|
||||
|
||||
if (!selectedStation || !selectedStation.id) {
|
||||
throw new Error('Parsed station data is invalid or missing an 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>`;
|
||||
console.error("ERROR during station loading:", e);
|
||||
// window.location.href = './station_selection.html'; // Temporarily disable redirect for debugging
|
||||
return;
|
||||
}
|
||||
|
||||
// --- SOCKET.IO FOR LIVE UPDATES ---
|
||||
// Step 2: Populate the header.
|
||||
console.log("4. Populating header...");
|
||||
stationNameEl.textContent = selectedStation.name;
|
||||
stationLocationEl.textContent = selectedStation.location;
|
||||
deviceIdEl.textContent = selectedStation.id;
|
||||
productIdEl.textContent = selectedStation.product_id;
|
||||
console.log("5. Header populated.");
|
||||
|
||||
// Step 3: Set up button event listeners.
|
||||
if(clearReqBtn) clearReqBtn.addEventListener('click', () => requestLogArea.value = '');
|
||||
if(clearEvtBtn) clearEvtBtn.addEventListener('click', () => eventLogArea.value = '');
|
||||
if(clearAllBtn) clearAllBtn.addEventListener('click', () => {
|
||||
requestLogArea.value = '';
|
||||
eventLogArea.value = '';
|
||||
});
|
||||
if(logoutBtn) logoutBtn.addEventListener('click', () => {
|
||||
localStorage.clear();
|
||||
window.location.href = 'index.html';
|
||||
});
|
||||
if(refreshBtn) refreshBtn.addEventListener('click', () => location.reload());
|
||||
console.log("6. Event listeners attached.");
|
||||
|
||||
// Step 4: Fetch initial data now that everything is set up.
|
||||
console.log("7. Calling fetchRecentLogs()...");
|
||||
fetchRecentLogs();
|
||||
|
||||
// Step 5: Start WebSocket connection for live updates.
|
||||
socket = io(SOCKET_URL);
|
||||
socket.on('connect', () => {
|
||||
console.log("Connected to WebSocket for live logs.");
|
||||
console.log("Logs Page: Connected to WebSocket.");
|
||||
socket.emit('join_station_room', { station_id: selectedStation.id });
|
||||
});
|
||||
|
||||
socket.on('dashboard_update', (message) => {
|
||||
const { stationId, topic, data } = message;
|
||||
if (stationId !== selectedStation.id) return;
|
||||
|
||||
// Just prepend live messages as they arrive
|
||||
const now = new Date().toISOString();
|
||||
if (topic.endsWith('EVENTS')) {
|
||||
appendLog(eventLogArea, data, topic, now);
|
||||
|
|
@ -284,32 +130,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
appendLog(requestLogArea, data, topic, now);
|
||||
}
|
||||
});
|
||||
|
||||
// --- BUTTON EVENT LISTENERS ---
|
||||
if(clearReqBtn) clearReqBtn.addEventListener('click', () => requestLogArea.value = '');
|
||||
if(clearEvtBtn) clearEvtBtn.addEventListener('click', () => eventLogArea.value = '');
|
||||
if(clearAllBtn) clearAllBtn.addEventListener('click', () => {
|
||||
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(resetBtn) resetBtn.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to reset the station?')) {
|
||||
sendCommand('STATION_RESET');
|
||||
}
|
||||
});
|
||||
|
||||
// --- STARTUP ---
|
||||
fetchAndRenderLogs(); // Fetch historical logs on page load
|
||||
checkStationStatus();
|
||||
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// --- START THE APPLICATION ---
|
||||
init();
|
||||
});
|
||||
|
|
@ -140,7 +140,7 @@
|
|||
// // --- DATA FETCHING & STATUS POLLING ---
|
||||
// const loadAndPollStations = async () => {
|
||||
// try {
|
||||
// const response = await fetch('http://192.168.1.12:5000/api/stations');
|
||||
// const response = await fetch('http://10.10.1.183:5000/api/stations');
|
||||
// if (!response.ok) throw new Error('Failed to fetch stations');
|
||||
// const stations = await response.json();
|
||||
|
||||
|
|
@ -178,7 +178,7 @@
|
|||
// }
|
||||
|
||||
// try {
|
||||
// const response = await fetch(`http://192.168.1.12:5000/api/stations/${stationId}`, {
|
||||
// const response = await fetch(`http://10.10.1.183:5000/api/stations/${stationId}`, {
|
||||
// method: 'DELETE',
|
||||
// });
|
||||
|
||||
|
|
@ -214,7 +214,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const stationCountEl = document.getElementById('station-count'); // Make sure you have an element with this ID in your HTML
|
||||
|
||||
// --- CONFIG & STATE ---
|
||||
const API_BASE = 'http://192.168.1.12:5000/api';
|
||||
const API_BASE = 'http://10.10.1.183:5000/api';
|
||||
let allStations = []; // Master list of stations from the API
|
||||
let pollingInterval = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://192.168.1.12:5000/api';
|
||||
const API_BASE = 'http://10.10.1.183:5000/api';
|
||||
|
||||
const grid = document.getElementById('stations-grid');
|
||||
const addStationCardTmpl = document.getElementById('add-station-card-template');
|
||||
|
|
|
|||
Loading…
Reference in New Issue