Added event clearing and source tracking

- Added clear events endpoint that preserves latest gate status
- Added source tracking (api/mqtt) for gate triggers
- Added clear events button to frontend
- Updated MQTT handler to use mqtt source
This commit is contained in:
Josh Finlay 2025-01-08 10:41:26 +10:00
parent b372618cf7
commit 9ff21046d7
3 changed files with 76 additions and 15 deletions

View File

@ -251,7 +251,7 @@ app.add_middleware(
)
# Gate control
async def trigger_gate():
async def trigger_gate(source: str = "api"):
"""Trigger the gate relay"""
try:
settings = app.state.current_settings
@ -268,7 +268,7 @@ async def trigger_gate():
# Log event
timestamp = datetime.now().isoformat()
await add_event(timestamp, "gate triggered", "api")
await add_event(timestamp, "gate triggered", source)
# Wait for specified duration
await asyncio.sleep(trigger_duration)
@ -282,7 +282,7 @@ async def trigger_gate():
logger.error(f"Failed to trigger gate: {e}", exc_info=True)
# Log failure
timestamp = datetime.now().isoformat()
await add_event(timestamp, "gate trigger failed", "api", False)
await add_event(timestamp, "gate trigger failed", source, False)
return False
last_open_time = None
@ -437,6 +437,10 @@ async def cleanup_old_events():
"DELETE FROM events WHERE timestamp < ?",
(cutoff_date,)
)
await db.execute(
"DELETE FROM gate_status WHERE timestamp < ?",
(cutoff_date,)
)
await db.commit()
logger.info(f"Cleaned up {count} events older than {cutoff_date}")
@ -462,7 +466,7 @@ async def handle_mqtt_command(should_open: bool):
status_pin = settings.gpio.statusPin
if should_open != (GPIO.input(status_pin) == GPIO.HIGH):
await trigger_gate()
await trigger_gate("mqtt")
except Exception as e:
logger.error(f"Error handling MQTT command: {e}")
@ -471,17 +475,40 @@ async def handle_mqtt_command(should_open: bool):
async def trigger():
"""Trigger the gate"""
try:
success = await trigger_gate()
timestamp = datetime.now().isoformat()
# Get current status after trigger
settings = app.state.current_settings or Settings()
current_status = GPIO.input(settings.gpio.statusPin) == GPIO.HIGH
return {"success": success, "timestamp": timestamp, "isOpen": current_status}
result = await trigger_gate("api")
if result:
status = await get_status()
return {"success": True, "isOpen": status["isOpen"]}
return {"success": False}
except Exception as e:
logger.error("Error triggering gate", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to trigger gate")
logger.error(f"Failed to trigger gate: {e}", exc_info=True)
return {"success": False}
@app.post("/api/events/clear")
async def clear_events():
"""Clear all events from the database except the latest gate status"""
try:
async with DBConnection() as db:
# Delete all events
await db.execute("DELETE FROM events")
# Keep only the most recent gate status
await db.execute("""
DELETE FROM gate_status
WHERE timestamp NOT IN (
SELECT timestamp
FROM gate_status
ORDER BY timestamp DESC
LIMIT 1
)
""")
await db.commit()
logger.info("Event logs cleared (preserved latest gate status)")
return {"success": True}
except Exception as e:
logger.error(f"Failed to clear events: {e}", exc_info=True)
return {"success": False}
@app.get("/api/status")
async def get_status():
@ -491,6 +518,8 @@ async def get_status():
# HIGH (3.3V) means gate is open (receiving voltage)
# LOW (0V) means gate is closed (no voltage)
is_open = GPIO.input(settings.gpio.statusPin) == GPIO.HIGH
# Update state machine
gate_status.update(is_open)
return {"isOpen": gate_status.is_open, "lastChanged": gate_status.last_changed}

View File

@ -198,7 +198,29 @@ function App() {
{/* Recent Events */}
<div className="mt-6">
<h2 className="text-lg font-semibold mb-2">Recent Events</h2>
<div className="flex justify-between items-center mb-2">
<h2 className="text-lg font-semibold">Recent Events</h2>
<button
onClick={async () => {
try {
if (window.confirm('Are you sure you want to clear all events?')) {
await api.clearEvents();
// Reload events
const eventsData = await api.getEvents();
setEvents(eventsData.events);
setHasMoreEvents(eventsData.hasMore);
setTotalEvents(eventsData.total);
}
} catch (err) {
setError('Failed to clear events');
console.error(err);
}
}}
className="text-sm text-red-600 hover:text-red-800"
>
Clear Events
</button>
</div>
<div className="border rounded-lg divide-y divide-gray-200 max-h-96 overflow-y-auto">
{events.map((event, index) => (
<div key={index} className="px-3 py-2 hover:bg-gray-50 flex items-center justify-between text-sm">

View File

@ -49,3 +49,13 @@ export async function updateSettings(settings: Settings): Promise<{ success: boo
}
return response.json();
}
export async function clearEvents(): Promise<{ success: boolean }> {
const response = await fetch(`${API_BASE}/events/clear`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('Failed to clear events');
}
return response.json();
}