Add SSE endpoint and dream notify — /api/events and /api/events/notify

This commit is contained in:
2026-04-27 02:20:50 +00:00
parent 9088b5643d
commit 9b312d936f
2 changed files with 60 additions and 0 deletions
+48
View File
@@ -26,6 +26,8 @@ from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import uvicorn import uvicorn
import asyncio
from fastapi.responses import StreamingResponse
load_dotenv(Path.home() / "aaronai" / ".env") load_dotenv(Path.home() / "aaronai" / ".env")
@@ -767,5 +769,51 @@ async def clear_all_conversations(auth: str = Depends(require_auth)):
return JSONResponse({"cleared": True}) return JSONResponse({"cleared": True})
# SSE client registry
sse_clients: list[asyncio.Queue] = []
async def sse_generator(queue: asyncio.Queue):
try:
yield 'data: {"type": "connected"}\n\n'
while True:
try:
event = await asyncio.wait_for(queue.get(), timeout=30.0)
import json as _json
yield 'data: ' + _json.dumps(event) + '\n\n'
except asyncio.TimeoutError:
yield 'data: {"type": "heartbeat"}\n\n'
except asyncio.CancelledError:
pass
finally:
if queue in sse_clients:
sse_clients.remove(queue)
@app.get("/api/events")
async def sse_endpoint(request: Request, auth: str = Depends(require_auth)):
queue: asyncio.Queue = asyncio.Queue()
sse_clients.append(queue)
return StreamingResponse(
sse_generator(queue),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive",
}
)
@app.post("/api/events/notify")
async def notify_clients(request: Request):
"""Internal endpoint — called by dream.py when a dream is delivered"""
# Only allow from localhost
client_host = request.client.host if request.client else ""
if client_host not in ("127.0.0.1", "::1", "localhost"):
raise HTTPException(status_code=403, detail="Internal only")
data = await request.json()
for queue in sse_clients:
await queue.put(data)
return JSONResponse({"notified": len(sse_clients)})
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)
+12
View File
@@ -289,6 +289,18 @@ def deliver(dream_text, mode, task=None):
print(f"Dream written to Nextcloud: Journal/Dreams/{filename}") print(f"Dream written to Nextcloud: Journal/Dreams/{filename}")
# Notify any open browser connections via SSE
try:
import requests as _req
_req.post("http://localhost:8000/api/events/notify", json={
"type": "dream",
"mode": mode,
"filename": filename,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
}, timeout=3)
except Exception as _e:
print(f"SSE notify failed (non-critical): {_e}")
state = load_dreamer_state() state = load_dreamer_state()
state["last_dream_timestamp"] = datetime.now().timestamp() state["last_dream_timestamp"] = datetime.now().timestamp()
state["last_dream_mode"] = mode state["last_dream_mode"] = mode