- Aggregator: Flask-based event broker with priority queue - Frontend: OLED-optimized UI with animations - Detectors: disk, cpu, memory, service, network - Unified entry point (sentry.py) with process management - Heartbeat TTL system for auto-clearing stale events Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
178 lines
5.0 KiB
Python
178 lines
5.0 KiB
Python
"""
|
|
Sentry-Emote Aggregator
|
|
A lightweight event broker that manages priority-based system status.
|
|
"""
|
|
|
|
import json
|
|
import threading
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from flask import Flask, request, jsonify, send_from_directory
|
|
|
|
app = Flask(__name__, static_folder=".")
|
|
ROOT_DIR = Path(__file__).parent
|
|
|
|
# Configuration
|
|
STATUS_FILE = Path(__file__).parent / "status.json"
|
|
DEFAULT_NOTIFY_TTL = 10 # Default TTL for Priority 3 (Notify) events
|
|
|
|
# Priority definitions
|
|
PRIORITY_CONFIG = {
|
|
1: {"name": "Critical", "emote": "( x_x)", "color": "#FF0000", "animation": "shaking"},
|
|
2: {"name": "Warning", "emote": "( o_o)", "color": "#FFFF00", "animation": "breathing"},
|
|
3: {"name": "Notify", "emote": "( 'o')", "color": "#0088FF", "animation": "popping"},
|
|
4: {"name": "Optimal", "emote": "( ^_^)", "color": "#00FF00", "animation": "breathing"},
|
|
}
|
|
|
|
# Thread-safe event storage
|
|
events_lock = threading.Lock()
|
|
active_events = {} # id -> {priority, message, timestamp, ttl}
|
|
|
|
|
|
def get_current_state():
|
|
"""Determine current state based on active events."""
|
|
with events_lock:
|
|
if not active_events:
|
|
priority = 4
|
|
events_list = []
|
|
else:
|
|
# Find highest priority (lowest number)
|
|
priority = min(e["priority"] for e in active_events.values())
|
|
events_list = [
|
|
{"id": eid, "priority": e["priority"], "message": e.get("message", "")}
|
|
for eid, e in active_events.items()
|
|
]
|
|
|
|
config = PRIORITY_CONFIG[priority]
|
|
return {
|
|
"current_state": config["name"].lower(),
|
|
"active_emote": config["emote"],
|
|
"color": config["color"],
|
|
"animation": config["animation"],
|
|
"message": config["name"] if priority == 4 else f"{config['name']} state active",
|
|
"active_events": sorted(events_list, key=lambda x: x["priority"]),
|
|
"last_updated": datetime.now().isoformat(timespec="seconds"),
|
|
}
|
|
|
|
|
|
def write_status():
|
|
"""Write current state to status.json."""
|
|
state = get_current_state()
|
|
with open(STATUS_FILE, "w") as f:
|
|
json.dump(state, f, indent="\t")
|
|
return state
|
|
|
|
|
|
def cleanup_expired_events():
|
|
"""Background thread to remove expired TTL events."""
|
|
while True:
|
|
time.sleep(1)
|
|
now = time.time()
|
|
expired = []
|
|
|
|
with events_lock:
|
|
for eid, event in active_events.items():
|
|
if event.get("ttl") and now > event["ttl"]:
|
|
expired.append(eid)
|
|
|
|
for eid in expired:
|
|
del active_events[eid]
|
|
|
|
if expired:
|
|
write_status()
|
|
|
|
|
|
@app.route("/event", methods=["POST"])
|
|
def post_event():
|
|
"""
|
|
Accept a new event.
|
|
Expected JSON: {"id": "event_id", "priority": 1-4, "message": "optional", "ttl": optional_seconds}
|
|
"""
|
|
data = request.get_json(force=True)
|
|
|
|
if not data or "id" not in data or "priority" not in data:
|
|
return jsonify({"error": "Missing required fields: id, priority"}), 400
|
|
|
|
event_id = str(data["id"])
|
|
priority = int(data["priority"])
|
|
|
|
if priority not in PRIORITY_CONFIG:
|
|
return jsonify({"error": f"Invalid priority: {priority}. Must be 1-4."}), 400
|
|
|
|
event = {
|
|
"priority": priority,
|
|
"message": data.get("message", ""),
|
|
"timestamp": time.time(),
|
|
}
|
|
|
|
# Apply TTL if provided, or use default for Priority 3 (Notify)
|
|
if "ttl" in data:
|
|
event["ttl"] = time.time() + int(data["ttl"])
|
|
elif priority == 3:
|
|
event["ttl"] = time.time() + DEFAULT_NOTIFY_TTL
|
|
|
|
with events_lock:
|
|
active_events[event_id] = event
|
|
|
|
state = write_status()
|
|
return jsonify({"status": "ok", "current_state": state}), 200
|
|
|
|
|
|
@app.route("/clear", methods=["POST"])
|
|
def clear_event():
|
|
"""
|
|
Clear an event by ID.
|
|
Expected JSON: {"id": "event_id"}
|
|
"""
|
|
data = request.get_json(force=True)
|
|
|
|
if not data or "id" not in data:
|
|
return jsonify({"error": "Missing required field: id"}), 400
|
|
|
|
event_id = str(data["id"])
|
|
|
|
with events_lock:
|
|
if event_id in active_events:
|
|
del active_events[event_id]
|
|
state = write_status()
|
|
return jsonify({"status": "cleared", "current_state": state}), 200
|
|
else:
|
|
return jsonify({"error": "Event not found"}), 404
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
"""Serve the frontend."""
|
|
return send_from_directory(ROOT_DIR, "index.html")
|
|
|
|
|
|
@app.route("/status", methods=["GET"])
|
|
def get_status():
|
|
"""Return current status as JSON."""
|
|
return jsonify(get_current_state()), 200
|
|
|
|
|
|
@app.route("/events", methods=["GET"])
|
|
def list_events():
|
|
"""List all active events."""
|
|
with events_lock:
|
|
return jsonify({"events": dict(active_events)}), 200
|
|
|
|
|
|
def main():
|
|
# Write initial optimal state
|
|
write_status()
|
|
print(f"Status file: {STATUS_FILE}")
|
|
|
|
# Start TTL cleanup thread
|
|
cleanup_thread = threading.Thread(target=cleanup_expired_events, daemon=True)
|
|
cleanup_thread.start()
|
|
|
|
# Run Flask
|
|
app.run(host="0.0.0.0", port=5000, threaded=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|