Bump to v2.3.0: replace polling with SSE stream, fix detector imports
- Add GET /stream SSE endpoint to aggregator.py; state is pushed instantly on every change instead of fetched every 2s - Replace setInterval polling in index.html with EventSource; onerror shows the ( ?.?) face and auto-reconnect is handled by the browser natively - Fix ModuleNotFoundError in detectors: inject project root into PYTHONPATH when launching subprocesses from kao.py - Update openapi.yaml, CLAUDE.md, README.md with /stream endpoint - Remove completed SSE item from TODO.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,12 +5,13 @@ A lightweight event broker that manages priority-based system status.
|
||||
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from flask import Flask, request, jsonify, send_from_directory
|
||||
from flask import Flask, request, jsonify, send_from_directory, Response, stream_with_context
|
||||
|
||||
app = Flask(__name__, static_folder=".")
|
||||
ROOT_DIR = Path(__file__).parent
|
||||
@@ -60,6 +61,23 @@ current_optimal_animation = OPTIMAL_EMOTES[0][1]
|
||||
# Notify counter for unique IDs
|
||||
_notify_counter = 0
|
||||
|
||||
# SSE subscribers: one queue per connected client
|
||||
_subscribers: set = set()
|
||||
_subscribers_lock = threading.Lock()
|
||||
|
||||
|
||||
def broadcast(state_json: str):
|
||||
"""Push state JSON to all connected SSE clients."""
|
||||
with _subscribers_lock:
|
||||
dead = []
|
||||
for q in _subscribers:
|
||||
try:
|
||||
q.put_nowait(state_json)
|
||||
except queue.Full:
|
||||
dead.append(q)
|
||||
for q in dead:
|
||||
_subscribers.discard(q)
|
||||
|
||||
# Sleep mode
|
||||
is_sleeping = False
|
||||
SLEEP_EMOTE = "( -_-)zzZ"
|
||||
@@ -156,13 +174,14 @@ def get_current_state():
|
||||
|
||||
|
||||
def write_status():
|
||||
"""Write current state to status.json."""
|
||||
"""Write current state to status.json and push to SSE subscribers."""
|
||||
state = get_current_state()
|
||||
try:
|
||||
with open(STATUS_FILE, "w") as f:
|
||||
json.dump(state, f, indent="\t")
|
||||
except OSError as e:
|
||||
print(f"[ERROR] Failed to write status file: {e}")
|
||||
broadcast(json.dumps(state))
|
||||
return state
|
||||
|
||||
|
||||
@@ -351,6 +370,38 @@ def list_events():
|
||||
return jsonify({"events": dict(active_events)}), 200
|
||||
|
||||
|
||||
@app.route("/stream")
|
||||
def stream():
|
||||
"""Server-Sent Events stream. Pushes state JSON on every change."""
|
||||
q = queue.Queue(maxsize=10)
|
||||
with _subscribers_lock:
|
||||
_subscribers.add(q)
|
||||
|
||||
def generate():
|
||||
try:
|
||||
# Send current state immediately on connect
|
||||
yield f"data: {json.dumps(get_current_state())}\n\n"
|
||||
while True:
|
||||
try:
|
||||
data = q.get(timeout=30)
|
||||
yield f"data: {data}\n\n"
|
||||
except queue.Empty:
|
||||
# Keepalive comment to prevent proxy timeouts
|
||||
yield ": keepalive\n\n"
|
||||
finally:
|
||||
with _subscribers_lock:
|
||||
_subscribers.discard(q)
|
||||
|
||||
return Response(
|
||||
stream_with_context(generate()),
|
||||
mimetype="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no", # Disable nginx buffering if proxied
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.route("/docs")
|
||||
def docs():
|
||||
"""Serve interactive API documentation via Swagger UI."""
|
||||
|
||||
Reference in New Issue
Block a user