diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..6ccb9e1
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,2 @@
+
+
Llamagochi loading...
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..079aef9
--- /dev/null
+++ b/server.py
@@ -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()