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); }