diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..033df5fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv +__pycache__ diff --git a/generate_data.py b/generate_data.py new file mode 100644 index 00000000..fdbbfd1a --- /dev/null +++ b/generate_data.py @@ -0,0 +1,45 @@ +import httpx +from datetime import datetime, timedelta, timezone +from time import sleep +from main import TelemetrySnapshot + + +def main() -> None: + wait = 10 + online_time = 24 * 3600 # start at 1 day + ew = 0 + ns = 0 + while True: + snapshot = TelemetrySnapshot( + character_name="Test name", + char_tag="test_tag", + session_id="test_session_id", + timestamp=datetime.now(tz=timezone.utc), + ew=ew, + ns=ns, + z=0, + kills=0, + kills_per_hour="kph_str", + onlinetime=str(timedelta(seconds=online_time)), + deaths=0, + rares_found=0, + prismatic_taper_count=0, + vt_state="test state", + ) + resp = httpx.post( + "http://localhost:8000/position/", + data=snapshot.model_dump_json(), + headers={ + "Content-Type": "application/json", + "X-PLUGIN-SECRET": "your_shared_secret", + }, + ) + print(resp) + sleep(wait) + ew += 0.1 + ns += 0.1 + online_time += wait + + +if __name__ == "__main__": + main() diff --git a/main.py b/main.py index 0892dcd8..fdb48928 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ import json import sqlite3 from typing import Dict -from fastapi import FastAPI, Header, HTTPException, Query +from fastapi import FastAPI, Header, HTTPException, Query, WebSocket, WebSocketDisconnect from fastapi.responses import JSONResponse from fastapi.routing import APIRoute from fastapi.staticfiles import StaticFiles @@ -11,6 +11,8 @@ from pydantic import BaseModel from typing import Optional from db import init_db, save_snapshot, DB_FILE +import asyncio +from starlette.concurrency import run_in_threadpool # ------------------------------------------------------------------ app = FastAPI() @@ -194,6 +196,42 @@ def get_trails( ] return JSONResponse(content={"trails": trails}) +# -------------------- WebSocket endpoints ----------------------- +browser_conns: set[WebSocket] = set() + +async def _broadcast_to_browser_clients(snapshot: dict): + for ws in list(browser_conns): + try: + await ws.send_json(snapshot) + except WebSocketDisconnect: + browser_conns.remove(ws) + +@app.websocket("/ws/position") +async def ws_receive_snapshots(websocket: WebSocket, secret: str = Query(...)): + await websocket.accept() + if secret != SHARED_SECRET: + await websocket.close(code=1008) + return + try: + while True: + data = await websocket.receive_json() + snap = TelemetrySnapshot.parse_obj(data) + live_snapshots[snap.character_name] = snap.dict() + await run_in_threadpool(save_snapshot, snap.dict()) + await _broadcast_to_browser_clients(snap.dict()) + except WebSocketDisconnect: + pass + +@app.websocket("/ws/live") +async def ws_live_updates(websocket: WebSocket): + await websocket.accept() + browser_conns.add(websocket) + try: + while True: + await asyncio.sleep(3600) + except WebSocketDisconnect: + browser_conns.remove(websocket) + # -------------------- static frontend --------------------------- app.mount("/", StaticFiles(directory="static", html=True), name="static") diff --git a/static/script.js b/static/script.js index 0b6b9aff..bfe94b72 100644 --- a/static/script.js +++ b/static/script.js @@ -209,6 +209,8 @@ function render(players) { ${p.kills_per_hour} ${p.rares_found} ${p.vt_state} + ${p.onlinetime} + ${p.deaths} `; li.addEventListener('click', () => selectPlayer(p, x, y)); diff --git a/static/style.css b/static/style.css index 031a41be..b6a7286d 100644 --- a/static/style.css +++ b/static/style.css @@ -156,11 +156,12 @@ body { #playerList li { display: grid; grid-template-columns: 1fr auto; - grid-template-rows: auto auto auto; + grid-template-rows: auto auto auto auto; grid-template-areas: "name loc" "kills kph" - "rares meta"; + "rares meta" + "onlinetime deaths"; gap: 4px 8px; margin: 6px 0; padding: 8px 10px; @@ -178,6 +179,8 @@ body { .stat.kph { grid-area: kph; } .stat.rares { grid-area: rares; } .stat.meta { grid-area: meta; } +.stat.onlinetime { grid-area: onlinetime; } +.stat.deaths { grid-area: deaths; } /* pill styling */ #playerList li .stat { @@ -198,6 +201,8 @@ body { background: var(--accent); color: #111; } +.stat.onlinetime::before { content: "🕑 "} +.stat.deaths::before { content: "💀 "} /* hover & selected states */ #playerList li:hover { background: var(--card-hov); }