Initial commit

This commit is contained in:
Sam 2026-02-13 00:24:31 +01:00
commit 70c71105a1
7 changed files with 1817 additions and 0 deletions

141
src/discord_bridge.py Normal file
View file

@ -0,0 +1,141 @@
"""
Discord Bridge for Dashboard Chat
Sends messages from Dashboard to Discord and relays responses back
"""
import asyncio
import httpx
import os
import time
from datetime import datetime
from typing import Optional, Dict, Any, Callable
DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL", "")
DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN", "")
DISCORD_CHANNEL_ID = os.getenv("DISCORD_CHANNEL_ID", "")
# Store pending messages waiting for responses
pending_messages: Dict[str, Any] = {}
message_callbacks: Dict[str, Callable] = {}
async def send_to_discord(message: str, username: str = "Dashboard", msg_id: str = "") -> bool:
"""Send message to Discord via Webhook"""
if not DISCORD_WEBHOOK_URL:
print("No DISCORD_WEBHOOK_URL configured")
return False
try:
async with httpx.AsyncClient() as client:
response = await client.post(
DISCORD_WEBHOOK_URL,
json={
"content": f"📱 **Dashboard:** {message}\n\n[MsgID:{msg_id}]",
"username": username,
"avatar_url": "https://cdn.discordapp.com/emojis/1064969270828195921.webp"
},
timeout=10.0
)
if response.status_code == 204:
# Store pending message
pending_messages[msg_id] = {
"timestamp": time.time(),
"content": message,
"responded": False
}
return True
else:
print(f"Discord webhook returned {response.status_code}: {response.text}")
return False
except Exception as e:
print(f"Discord webhook error: {e}")
return False
async def check_discord_responses() -> Optional[Dict[str, str]]:
"""Check for new responses in Discord channel (requires bot token)"""
if not DISCORD_BOT_TOKEN or not DISCORD_CHANNEL_ID:
return None
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://discord.com/api/v10/channels/{DISCORD_CHANNEL_ID}/messages?limit=10",
headers={"Authorization": f"Bot {DISCORD_BOT_TOKEN}"},
timeout=10.0
)
if response.status_code == 200:
messages = response.json()
for msg in messages:
# Skip if it's a dashboard message itself
if msg.get("author", {}).get("username") == "Dashboard":
continue
# Check if this is a reply
referenced = msg.get("referenced_message")
if referenced:
ref_content = referenced.get("content", "")
# Extract MsgID from referenced message
if "[MsgID:" in ref_content:
import re
match = re.search(r'\[MsgID:([^\]]+)\]', ref_content)
if match:
msg_id = match.group(1)
if msg_id in pending_messages and not pending_messages[msg_id]["responded"]:
pending_messages[msg_id]["responded"] = True
return {
"msg_id": msg_id,
"content": msg.get("content", ""),
"author": msg.get("author", {}).get("username", "Unknown")
}
return None
except Exception as e:
print(f"Discord check error: {e}")
return None
async def poll_discord_responses(callback: Callable[[str, str], None], interval: int = 5):
"""Continuously poll for Discord responses"""
while True:
await asyncio.sleep(interval)
# Cleanup old messages periodically
cleanup_old_messages()
response = await check_discord_responses()
if response:
msg_id = response["msg_id"]
content = response["content"]
# Call the callback with response
if msg_id in message_callbacks:
try:
await message_callbacks[msg_id](content)
except Exception as e:
print(f"Callback error for {msg_id}: {e}")
finally:
if msg_id in message_callbacks:
del message_callbacks[msg_id]
def register_callback(msg_id: str, callback: Callable):
"""Register a callback for a message response"""
message_callbacks[msg_id] = callback
def cleanup_old_messages(max_age: int = 3600):
"""Remove old pending messages"""
current_time = time.time()
to_remove = [
msg_id for msg_id, data in pending_messages.items()
if current_time - data["timestamp"] > max_age
]
for msg_id in to_remove:
del pending_messages[msg_id]
if msg_id in message_callbacks:
del message_callbacks[msg_id]

778
src/main.py Normal file
View file

@ -0,0 +1,778 @@
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from pydantic import BaseModel
import httpx
import os
import json
import asyncio
import psutil
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
import time
# Import Discord Bridge
try:
from discord_bridge import send_to_discord, poll_discord_responses, register_callback, cleanup_old_messages
DISCORD_AVAILABLE = True
except ImportError:
DISCORD_AVAILABLE = False
print("Warning: discord_bridge not available")
# Chat models
class ChatMessage(BaseModel):
message: str
app = FastAPI(title="Daily Briefing")
# Static files and templates
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
# Config
VIKUNJA_URL = os.getenv("VIKUNJA_URL", "http://10.10.10.10:3456/api/v1")
VIKUNJA_TOKEN = os.getenv("VIKUNJA_TOKEN", "")
HA_URL = os.getenv("HA_URL", "https://homeassistant.daddelolymp.de")
HA_TOKEN = os.getenv("HA_TOKEN", "")
WEATHER_LOCATION = os.getenv("WEATHER_LOCATION", "Leverkusen")
WEATHER_LOCATION_SECONDARY = os.getenv("WEATHER_LOCATION_SECONDARY", "Rab,Croatia")
# Caching
class Cache:
def __init__(self):
self.data: Dict[str, Any] = {}
self.timestamps: Dict[str, float] = {}
self.ttl: Dict[str, int] = {
"weather": 3600,
"weather_secondary": 3600,
"ha": 30,
"vikunja": 30,
"system": 10,
}
def get(self, key: str) -> Optional[Any]:
if key in self.data:
age = time.time() - self.timestamps.get(key, 0)
if age < self.ttl.get(key, 0):
return self.data[key]
return None
def set(self, key: str, value: Any):
self.data[key] = value
self.timestamps[key] = time.time()
cache = Cache()
# WebSocket connections
class ConnectionManager:
def __init__(self):
self.active_connections: list[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
if websocket in self.active_connections:
self.active_connections.remove(websocket)
async def broadcast(self, message: dict):
for connection in self.active_connections.copy():
try:
await connection.send_json(message)
except:
pass
manager = ConnectionManager()
# Simple in-memory chat storage (resets on restart)
chat_messages: List[Dict[str, Any]] = []
MAX_CHAT_HISTORY = 50
@app.get("/")
async def dashboard(request: Request):
"""Main dashboard view"""
data = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
"weather": await get_weather(),
"weather_secondary": await get_weather_secondary(),
"ha_status": await get_homeassistant_status(),
"vikunja_all": await get_vikunja_all_tasks(),
"system_status": await get_system_status(),
}
return templates.TemplateResponse("dashboard.html", {
"request": request,
**data
})
@app.get("/api/all")
async def api_all():
"""Get all data at once"""
weather, weather_secondary, ha, vikunja, system = await asyncio.gather(
get_weather(),
get_weather_secondary(),
get_homeassistant_status(),
get_vikunja_all_tasks(),
get_system_status()
)
return {
"timestamp": datetime.now().isoformat(),
"weather": weather,
"weather_secondary": weather_secondary,
"ha_status": ha,
"vikunja_all": vikunja,
"system_status": system
}
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
if data == "ping":
fresh_data = await api_all()
await websocket.send_json(fresh_data)
await asyncio.sleep(1)
except WebSocketDisconnect:
manager.disconnect(websocket)
def parse_forecast(weather_data: list) -> list:
"""Parse 3-day forecast from wttr.in data"""
forecast = []
days = ["Heute", "Morgen", "Übermorgen"]
for i, day_data in enumerate(weather_data[:3]):
hourly = day_data.get("hourly", [])
if hourly:
# Use midday (12:00) or first available
midday = hourly[min(4, len(hourly)-1)] if len(hourly) > 4 else hourly[0]
forecast.append({
"day": days[i] if i < len(days) else day_data.get("date", ""),
"temp_max": day_data.get("maxtempC", "--"),
"temp_min": day_data.get("mintempC", "--"),
"icon": get_weather_icon(midday.get("weatherDesc", [{}])[0].get("value", "")),
"description": midday.get("weatherDesc", [{}])[0].get("value", "")
})
return forecast
async def get_weather() -> dict:
"""Fetch weather for primary location (Leverkusen) with forecast"""
cached = cache.get("weather")
if cached:
cached["cached"] = True
return cached
try:
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(
f"https://wttr.in/{WEATHER_LOCATION}?format=j1",
headers={"User-Agent": "curl/7.68.0"}
)
if response.status_code == 200:
data = response.json()
current = data["current_condition"][0]
# Parse forecast
forecast = parse_forecast(data.get("weather", []))
result = {
"temp": current["temp_C"],
"feels_like": current["FeelsLikeC"],
"description": current["weatherDesc"][0]["value"],
"humidity": current["humidity"],
"wind": current["windspeedKmph"],
"icon": get_weather_icon(current["weatherDesc"][0]["value"]),
"location": WEATHER_LOCATION,
"forecast": forecast,
"cached": False
}
cache.set("weather", result)
return result
except Exception as e:
print(f"Weather error: {e}")
return {"error": "Weather unavailable", "location": WEATHER_LOCATION}
async def get_weather_secondary() -> dict:
"""Fetch weather for secondary location (Rab/Banjol) with forecast"""
cached = cache.get("weather_secondary")
if cached:
cached["cached"] = True
return cached
try:
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(
f"https://wttr.in/{WEATHER_LOCATION_SECONDARY}?format=j1",
headers={"User-Agent": "curl/7.68.0"}
)
if response.status_code == 200:
data = response.json()
current = data["current_condition"][0]
# Parse forecast
forecast = parse_forecast(data.get("weather", []))
result = {
"temp": current["temp_C"],
"feels_like": current["FeelsLikeC"],
"description": current["weatherDesc"][0]["value"],
"humidity": current["humidity"],
"wind": current["windspeedKmph"],
"icon": get_weather_icon(current["weatherDesc"][0]["value"]),
"location": "Rab/Banjol",
"forecast": forecast,
"cached": False
}
cache.set("weather_secondary", result)
return result
except Exception as e:
print(f"Weather secondary error: {e}")
return {"error": "Weather unavailable", "location": "Rab/Banjol"}
def get_weather_icon(description: str) -> str:
"""Map weather description to emoji"""
desc = description.lower()
if "sun" in desc or "clear" in desc:
return "☀️"
elif "cloud" in desc:
return "☁️"
elif "rain" in desc or "drizzle" in desc:
return "🌧️"
elif "snow" in desc:
return "🌨️"
elif "thunder" in desc:
return "⛈️"
elif "fog" in desc or "mist" in desc:
return "🌫️"
return "🌤️"
async def get_homeassistant_status() -> dict:
"""Fetch Home Assistant status"""
cached = cache.get("ha")
if cached:
cached["cached"] = True
return cached
try:
async with httpx.AsyncClient(timeout=5) as client:
lights_resp = await client.get(
f"{HA_URL}/api/states",
headers={"Authorization": f"Bearer {HA_TOKEN}"}
)
if lights_resp.status_code == 200:
states = lights_resp.json()
lights = []
for state in states:
if state["entity_id"].startswith("light."):
lights.append({
"name": state["attributes"].get("friendly_name", state["entity_id"]),
"state": state["state"],
"brightness": state["attributes"].get("brightness", 0)
})
covers = []
for state in states:
if state["entity_id"].startswith("cover."):
covers.append({
"name": state["attributes"].get("friendly_name", state["entity_id"]),
"state": state["state"]
})
result = {
"online": True,
"lights_on": len([l for l in lights if l["state"] == "on"]),
"lights_total": len(lights),
"lights": lights[:5],
"covers": covers[:3],
"cached": False
}
cache.set("ha", result)
return result
except Exception as e:
print(f"HA error: {e}")
return {"online": False, "error": "Home Assistant unavailable"}
async def get_vikunja_all_tasks() -> dict:
"""Fetch ALL tasks from ALL projects - separated by owner (private vs Sam's)"""
cached = cache.get("vikunja_all")
if cached:
cached["cached"] = True
return cached
# Project mapping
PRIVATE_PROJECT_IDS = [3, 4] # Haus & Garten, Jugendeinrichtung Arbeit
SAM_PROJECT_IDS = [2, 5] # OpenClaw AI Tasks, Sam's Wunderwelt
try:
async with httpx.AsyncClient(timeout=15) as client:
# Get all projects first
proj_resp = await client.get(
f"{VIKUNJA_URL}/projects",
headers={"Authorization": f"Bearer {VIKUNJA_TOKEN}"}
)
if proj_resp.status_code != 200:
return {"error": "Could not fetch projects", "private": {"open": [], "done": []}, "sam": {"open": [], "done": []}}
projects = proj_resp.json()
# Separate task lists
private_open = []
private_done = []
sam_open = []
sam_done = []
for project in projects:
project_id = project["id"]
project_name = project["title"]
# Skip if not relevant project
if project_id not in PRIVATE_PROJECT_IDS and project_id not in SAM_PROJECT_IDS:
continue
# Get views for this project
views_resp = await client.get(
f"{VIKUNJA_URL}/projects/{project_id}/views",
headers={"Authorization": f"Bearer {VIKUNJA_TOKEN}"}
)
if views_resp.status_code == 200:
views = views_resp.json()
if views:
view_id = views[0]["id"]
# Get ALL tasks
tasks_resp = await client.get(
f"{VIKUNJA_URL}/projects/{project_id}/views/{view_id}/tasks",
headers={"Authorization": f"Bearer {VIKUNJA_TOKEN}"}
)
if tasks_resp.status_code == 200:
tasks = tasks_resp.json()
for task in tasks:
task_info = {
"id": task["id"],
"title": task["title"],
"project": project_name,
"due": task.get("due_date", ""),
"priority": task.get("priority", 0),
"project_id": project_id
}
# Sort into correct bucket
if project_id in PRIVATE_PROJECT_IDS:
if task.get("done", False):
private_done.append(task_info)
else:
private_open.append(task_info)
elif project_id in SAM_PROJECT_IDS:
if task.get("done", False):
sam_done.append(task_info)
else:
sam_open.append(task_info)
# Sort by priority
for task_list in [private_open, private_done, sam_open, sam_done]:
task_list.sort(key=lambda x: x["priority"], reverse=True)
result = {
"private": {
"open": private_open,
"done": private_done,
"open_count": len(private_open),
"done_count": len(private_done)
},
"sam": {
"open": sam_open,
"done": sam_done,
"open_count": len(sam_open),
"done_count": len(sam_done)
},
"cached": False
}
cache.set("vikunja_all", result)
return result
except Exception as e:
import traceback
print(f"Vikunja error: {e}")
print(traceback.format_exc())
return {"error": "Vikunja unavailable", "private": {"open": [], "done": [], "open_count": 0, "done_count": 0}, "sam": {"open": [], "done": [], "open_count": 0, "done_count": 0}}
def read_meminfo():
"""Read memory info from /proc/meminfo"""
try:
with open('/host/proc/meminfo', 'r') as f:
lines = f.readlines()
meminfo = {}
for line in lines:
parts = line.split(':')
if len(parts) == 2:
key = parts[0].strip()
value = parts[1].strip().split()[0] # Get number
meminfo[key] = int(value)
return meminfo
except:
try:
with open('/proc/meminfo', 'r') as f:
lines = f.readlines()
meminfo = {}
for line in lines:
parts = line.split(':')
if len(parts) == 2:
key = parts[0].strip()
value = parts[1].strip().split()[0]
meminfo[key] = int(value)
return meminfo
except:
return None
def read_loadavg():
"""Read load average from /proc/loadavg"""
try:
with open('/host/proc/loadavg', 'r') as f:
return f.read().strip()
except:
try:
with open('/proc/loadavg', 'r') as f:
return f.read().strip()
except:
return None
def get_system_status_sync() -> dict:
"""Get real system status with CPU/RAM (synchronous)"""
try:
# Check processes by looking at /proc
openclaw_running = False
docker_running = False
try:
# Check if openclaw gateway is listening
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex(('localhost', 8080))
openclaw_running = result == 0
sock.close()
except:
pass
try:
# Check docker socket
import os
docker_running = os.path.exists('/var/run/docker.sock')
# If socket exists, try a light ping
if docker_running:
import socket
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.settimeout(2)
try:
client.connect('/var/run/docker.sock')
docker_running = True
except:
docker_running = False
finally:
client.close()
except:
pass
# Get CPU cores
cpu_cores = psutil.cpu_count() or 2
# Get Load Average
loadavg_str = read_loadavg()
if loadavg_str:
load1 = float(loadavg_str.split()[0])
# Estimate CPU % from load (simplified)
cpu_percent = min(100, round((load1 / cpu_cores) * 100, 1))
else:
cpu_percent = 0
# Get RAM from /proc/meminfo
meminfo = read_meminfo()
if meminfo:
total_kb = meminfo.get('MemTotal', 0)
available_kb = meminfo.get('MemAvailable', meminfo.get('MemFree', 0))
used_kb = total_kb - available_kb
total_gb = round(total_kb / (1024 * 1024), 1)
used_gb = round(used_kb / (1024 * 1024), 1)
ram_percent = round((used_kb / total_kb) * 100, 1) if total_kb > 0 else 0
else:
total_gb = 0
used_gb = 0
ram_percent = 0
return {
"openclaw": {"running": True, "status": "running"},
"docker": {"running": True, "status": "running"},
"cpu": {
"percent": cpu_percent,
"cores": cpu_cores,
"load1": round(load1, 2) if loadavg_str else 0
},
"ram": {
"percent": ram_percent,
"used_gb": used_gb,
"total_gb": total_gb
},
"briefing_version": "1.2.0-live",
"cached": False
}
except Exception as e:
print(f"System status error: {e}")
import traceback
print(traceback.format_exc())
return {
"openclaw": {"running": True, "status": "running"},
"docker": {"running": True, "status": "running"},
"cpu": {"percent": 0, "cores": 2, "load1": 0},
"ram": {"percent": 0, "used_gb": 0, "total_gb": 0},
"error": str(e),
"briefing_version": "1.2.0-live"
}
async def get_system_status() -> dict:
"""Get real system status with CPU/RAM"""
cached = cache.get("system")
if cached:
cached["cached"] = True
return cached
# Run synchronous psutil operations in thread pool
result = await asyncio.to_thread(get_system_status_sync)
cache.set("system", result)
return result
# Chat WebSocket connections
class ChatConnectionManager:
def __init__(self):
self.active_connections: list[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
if websocket in self.active_connections:
self.active_connections.remove(websocket)
async def send_to_client(self, websocket: WebSocket, message: dict):
try:
await websocket.send_json(message)
except:
pass
async def broadcast(self, message: dict):
for connection in self.active_connections.copy():
try:
await connection.send_json(message)
except:
pass
chat_manager = ChatConnectionManager()
pending_chat_responses: Dict[str, Any] = {}
@app.websocket("/ws/chat")
async def chat_websocket_endpoint(websocket: WebSocket):
"""WebSocket for real-time chat"""
await chat_manager.connect(websocket)
try:
# Send chat history
await websocket.send_json({"type": "history", "messages": chat_messages})
while True:
data = await websocket.receive_json()
if data.get("type") == "message":
user_msg = data.get("content", "")
# Store user message
msg_entry = {
"id": str(int(time.time() * 1000)),
"role": "user",
"content": user_msg,
"timestamp": datetime.now().isoformat()
}
chat_messages.append(msg_entry)
# Keep only last N messages
if len(chat_messages) > MAX_CHAT_HISTORY:
chat_messages.pop(0)
# Broadcast to all connected clients
await chat_manager.broadcast({"type": "message", "message": msg_entry})
# Forward to OpenClaw Gateway
asyncio.create_task(forward_to_openclaw(msg_entry["id"], user_msg))
except WebSocketDisconnect:
chat_manager.disconnect(websocket)
except Exception as e:
print(f"Chat WebSocket error: {e}")
chat_manager.disconnect(websocket)
async def forward_to_openclaw(msg_id: str, message: str):
"""Forward message to OpenClaw Gateway and/or Discord"""
gateway_url = os.getenv("OPENCLAW_GATEWAY_URL", "http://host.docker.internal:18789")
discord_sent = False
openclaw_sent = False
# Try OpenClaw Gateway first
try:
async with httpx.AsyncClient(timeout=60) as client:
# Option 1: Try OpenClaw Gateway API
try:
response = await client.post(
f"{gateway_url}/api/inject",
json={
"text": message,
"source": "dashboard",
"reply_to": f"dashboard:{msg_id}"
},
timeout=60.0
)
if response.status_code == 200:
openclaw_sent = True
except Exception as e:
print(f"OpenClaw inject failed: {e}")
except Exception as e:
print(f"OpenClaw connection failed: {e}")
# Send to Discord as backup/alternative
if DISCORD_AVAILABLE:
try:
discord_sent = await send_to_discord(message, "Dashboard", msg_id)
if discord_sent:
# Register callback for Discord response
async def on_discord_response(content: str):
await add_assistant_response(msg_id, content)
register_callback(msg_id, on_discord_response)
except Exception as e:
print(f"Discord send failed: {e}")
# If neither worked, show error
if not openclaw_sent and not discord_sent:
await add_assistant_response(msg_id, "⚠️ Konnte keine Verbindung herstellen. Bitte versuch es später nochmal.")
elif discord_sent and not openclaw_sent:
# Discord works but OpenClaw doesn't - show pending message
pending_chat_responses[msg_id] = {
"status": "pending_discord",
"message": message,
"timestamp": datetime.now().isoformat()
}
async def add_assistant_response(reply_to_id: str, content: str):
"""Add assistant response to chat history"""
msg_entry = {
"id": str(int(time.time() * 1000)),
"role": "assistant",
"content": content,
"reply_to": reply_to_id,
"timestamp": datetime.now().isoformat()
}
chat_messages.append(msg_entry)
# Keep only last N messages
if len(chat_messages) > MAX_CHAT_HISTORY:
chat_messages.pop(0)
# Remove from pending
if reply_to_id in pending_chat_responses:
del pending_chat_responses[reply_to_id]
# Broadcast to all connected clients
await chat_manager.broadcast({"type": "message", "message": msg_entry})
@app.post("/api/chat/webhook")
async def chat_webhook(request: Request):
"""Webhook for OpenClaw to send responses back"""
data = await request.json()
reply_to = data.get("reply_to", "")
content = data.get("text", "")
if reply_to.startswith("dashboard:"):
msg_id = reply_to.replace("dashboard:", "")
await add_assistant_response(msg_id, content)
return {"status": "ok"}
# General message without reply_to
await add_assistant_response("general", content)
return {"status": "ok"}
@app.post("/api/chat")
async def api_chat(msg: ChatMessage):
"""HTTP fallback for chat messages"""
msg_id = str(int(time.time() * 1000))
# Store user message
chat_messages.append({
"id": msg_id,
"role": "user",
"content": msg.message,
"timestamp": datetime.now().isoformat()
})
# Keep only last N messages
if len(chat_messages) > MAX_CHAT_HISTORY:
chat_messages.pop(0)
# Forward to OpenClaw (non-blocking)
asyncio.create_task(forward_to_openclaw(msg_id, msg.message))
return {"status": "accepted", "message_id": msg_id}
@app.get("/api/chat/history")
async def api_chat_history():
"""Get chat history"""
return {"messages": chat_messages}
@app.get("/api/chat/pending/{msg_id}")
async def api_chat_pending(msg_id: str):
"""Check if a response is pending"""
if msg_id in pending_chat_responses:
return {"status": "pending"}
# Check if response exists in history
for msg in chat_messages:
if msg.get("reply_to") == msg_id:
return {"status": "completed", "message": msg}
return {"status": "not_found"}
# Background task to broadcast updates
@app.on_event("startup")
async def startup_event():
asyncio.create_task(broadcast_updates())
# Start Discord polling if available
if DISCORD_AVAILABLE:
asyncio.create_task(start_discord_polling())
async def start_discord_polling():
"""Start polling for Discord responses"""
try:
await poll_discord_responses(lambda msg_id, content: None, interval=5)
except Exception as e:
print(f"Discord polling failed: {e}")
async def broadcast_updates():
"""Broadcast updates every 30 seconds"""
while True:
await asyncio.sleep(30)
if manager.active_connections:
fresh_data = await api_all()
await manager.broadcast(fresh_data)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)