feat: FastAPI server with WebSocket broadcast and static serving

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad Doloman
2026-06-22 18:58:49 +03:00
parent 3367eeb80b
commit 2e757fa645
2 changed files with 104 additions and 0 deletions

102
server.py Normal file
View File

@@ -0,0 +1,102 @@
import argparse
import asyncio
import json
import os
from pathlib import Path
from typing import Optional
import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from log_parser import LogParser, ServerState
from log_watcher import LogWatcher
FRONTEND_DIR = Path(__file__).parent / "frontend"
# ── App factory ───────────────────────────────────────────────────────────────
def create_app(log_path: str) -> FastAPI:
app = FastAPI()
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
clients: set[WebSocket] = set()
parser = LogParser()
last_state: dict = parser.state.to_dict()
@app.get("/")
async def index():
return FileResponse(FRONTEND_DIR / "index.html")
@app.websocket("/ws")
async def ws_endpoint(ws: WebSocket):
await ws.accept()
clients.add(ws)
await ws.send_text(json.dumps(last_state))
try:
while True:
await ws.receive_text()
except WebSocketDisconnect:
clients.discard(ws)
async def broadcast(state: ServerState):
nonlocal last_state
last_state = state.to_dict()
payload = json.dumps(last_state)
dead: set[WebSocket] = set()
for ws in clients:
try:
await ws.send_text(payload)
except Exception:
dead.add(ws)
clients -= dead
async def on_line(line: str):
result = parser.feed(line)
if result is not None:
await broadcast(result)
async def on_offline():
parser._state.state = "offline"
await broadcast(parser.state)
@app.on_event("startup")
async def startup():
watcher = LogWatcher(log_path, on_line, on_offline=on_offline)
watcher.start()
return app
# ── CLI entry point ───────────────────────────────────────────────────────────
def resolve_log_path(cli_log: Optional[str]) -> str:
path = cli_log or os.environ.get("LLAMAGOCHI_LOG")
if not path:
config_file = Path("config.json")
if config_file.exists():
data = json.loads(config_file.read_text())
path = data.get("log")
if not path:
raise SystemExit(
"Error: log file path required.\n"
" Use --log /path/to/llama-server.log\n"
" or set LLAMAGOCHI_LOG env var\n"
" or set \"log\" key in config.json"
)
return path
def main():
ap = argparse.ArgumentParser(description="Llamagochi — llama-server virtual pet")
ap.add_argument("--log", help="Path to llama-server log file")
ap.add_argument("--port", type=int, default=8080, help="HTTP port (default: 8080)")
args = ap.parse_args()
log_path = resolve_log_path(args.log)
app = create_app(log_path)
uvicorn.run(app, host="0.0.0.0", port=args.port)
if __name__ == "__main__":
main()