fix: sshfs polling, WebSocket broadcast, prompt progress bar, resizable stats panel

- log_watcher: add --reopen-log mode (close/reopen each poll) for sshfs/network
  filesystems; fix nonlocal clients bug causing UnboundLocalError on first broadcast;
  fix list(clients) copy to prevent RuntimeError on concurrent set mutation
- log_parser: capture progress=X.XX from prompt timing lines into Metrics.prompt_progress
- frontend: show progress bar during processing_prompt state with PROMPT N% label;
  widen stats panel to 300px; remove model name truncation; add drag handle on
  right border of stats panel to resize between 280px–840px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad Doloman
2026-06-22 21:02:34 +03:00
parent fddbd8f4d4
commit 311819b51c
6 changed files with 157 additions and 17 deletions

View File

@@ -17,7 +17,7 @@ FRONTEND_DIR = Path(__file__).parent / "frontend"
# ── App factory ───────────────────────────────────────────────────────────────
def create_app(log_path: str) -> FastAPI:
def create_app(log_path: str, reopen_log: bool = False) -> FastAPI:
app = FastAPI()
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
@@ -41,11 +41,11 @@ def create_app(log_path: str) -> FastAPI:
clients.discard(ws)
async def broadcast(state: ServerState):
nonlocal last_state
nonlocal last_state, clients
last_state = state.to_dict()
payload = json.dumps(last_state)
dead: set[WebSocket] = set()
for ws in clients:
for ws in list(clients):
try:
await ws.send_text(payload)
except Exception:
@@ -63,7 +63,7 @@ def create_app(log_path: str) -> FastAPI:
@app.on_event("startup")
async def startup():
watcher = LogWatcher(log_path, on_line, on_offline=on_offline)
watcher = LogWatcher(log_path, on_line, on_offline=on_offline, reopen=reopen_log)
watcher.start()
return app
@@ -71,6 +71,13 @@ def create_app(log_path: str) -> FastAPI:
# ── CLI entry point ───────────────────────────────────────────────────────────
def _config_bool(key: str) -> bool:
config_file = Path("config.json")
if config_file.exists():
return bool(json.loads(config_file.read_text()).get(key, False))
return False
def resolve_log_path(cli_log: Optional[str]) -> str:
path = cli_log or os.environ.get("LLAMAGOCHI_LOG")
if not path:
@@ -92,9 +99,12 @@ 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)")
ap.add_argument("--reopen-log", action="store_true",
help="Close and reopen log file each poll cycle (for sshfs/network filesystems)")
args = ap.parse_args()
log_path = resolve_log_path(args.log)
app = create_app(log_path)
reopen_log = args.reopen_log or _config_bool("reopen_log")
app = create_app(log_path, reopen_log=reopen_log)
uvicorn.run(app, host="0.0.0.0", port=args.port)