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/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); } diff --git a/static_ws/dereth.png b/static_ws/dereth.png deleted file mode 100644 index 4722941c..00000000 Binary files a/static_ws/dereth.png and /dev/null differ diff --git a/static_ws/favicon.ico b/static_ws/favicon.ico deleted file mode 100644 index 3be83f80..00000000 Binary files a/static_ws/favicon.ico and /dev/null differ diff --git a/static_ws/graphs.html b/static_ws/graphs.html deleted file mode 100644 index 644086dc..00000000 --- a/static_ws/graphs.html +++ /dev/null @@ -1,162 +0,0 @@ - - - - - Dereth Tracker – Analytics - - - - - - - -
-

Session Analytics

- -
-

Kills over Time

-
-
- -
-

Kills per Hour

-
-
-
- - - - diff --git a/static_ws/index.html b/static_ws/index.html deleted file mode 100644 index 8f1de30b..00000000 --- a/static_ws/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - Dereth Tracker - - - - - - - - -
-
- Dereth map - -
-
-
-
- - - - - - diff --git a/static_ws/script.js b/static_ws/script.js deleted file mode 100644 index 576e5418..00000000 --- a/static_ws/script.js +++ /dev/null @@ -1,383 +0,0 @@ -/* ---------- DOM references --------------------------------------- */ -const wrap = document.getElementById('mapContainer'); -const group = document.getElementById('mapGroup'); -const img = document.getElementById('map'); -const dots = document.getElementById('dots'); -const trailsContainer = document.getElementById('trails'); -const list = document.getElementById('playerList'); -const btnContainer = document.getElementById('sortButtons'); -const tooltip = document.getElementById('tooltip'); -// Chat UI elements -const chatOverlay = document.getElementById('chatOverlay'); -const chatTitle = document.getElementById('chatTitle'); -const chatClose = document.getElementById('chatClose'); -const chatMessages = document.getElementById('chatMessages'); -const chatForm = document.getElementById('chatForm'); -const chatInput = document.getElementById('chatInput'); -let chatSocket = null; -let currentChatName = null; - -/* ---------- constants ------------------------------------------- */ -const MAX_Z = 10; -const FOCUS_ZOOM = 3; // zoom level when you click a name -const POLL_MS = 2000; -const MAP_BOUNDS = { - west : -102.04, - east : 102.19, - north: 102.16, - south: -102.00 -}; - -/* ---------- sort configuration ---------------------------------- */ -const sortOptions = [ - { - value: "name", - label: "Name ↑", - comparator: (a, b) => a.character_name.localeCompare(b.character_name) - }, - { - value: "kph", - label: "KPH ↓", - comparator: (a, b) => b.kills_per_hour - a.kills_per_hour - }, - { - value: "kills", - label: "Kills ↓", - comparator: (a, b) => b.kills - a.kills - }, - { - value: "rares", - label: "Rares ↓", - comparator: (a, b) => (b.rares_found || 0) - (a.rares_found || 0) - } -]; - -let currentSort = sortOptions[0]; -let currentPlayers = []; - -/* ---------- generate segmented buttons -------------------------- */ -sortOptions.forEach(opt => { - const btn = document.createElement('div'); - btn.className = 'btn'; - btn.textContent = opt.label; - btn.dataset.value = opt.value; - if (opt.value === currentSort.value) btn.classList.add('active'); - - btn.addEventListener('click', () => { - btnContainer.querySelectorAll('.btn') - .forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - currentSort = opt; - renderList(); - }); - - btnContainer.appendChild(btn); -}); - -/* ---------- map & state variables ------------------------------- */ -let imgW = 0, imgH = 0; -let scale = 1, offX = 0, offY = 0, minScale = 1; -let dragging = false, sx = 0, sy = 0; -let selected = ""; -let pollID = null; - -/* ---------- utility functions ----------------------------------- */ -const hue = name => { - let h = 0; - for (let c of name) h = c.charCodeAt(0) + ((h << 5) - h); - return `hsl(${Math.abs(h) % 360},72%,50%)`; -}; - -const loc = (ns, ew) => - `${Math.abs(ns).toFixed(1)}${ns>=0?"N":"S"} ` - + `${Math.abs(ew).toFixed(1)}${ew>=0?"E":"W"}`; - -function worldToPx(ew, ns) { - const x = ((ew - MAP_BOUNDS.west) - / (MAP_BOUNDS.east - MAP_BOUNDS.west)) * imgW; - const y = ((MAP_BOUNDS.north - ns) - / (MAP_BOUNDS.north - MAP_BOUNDS.south)) * imgH; - return { x, y }; -} - -const applyTransform = () => - group.style.transform = `translate(${offX}px,${offY}px) scale(${scale})`; - -function clampPan() { - if (!imgW) return; - const r = wrap.getBoundingClientRect(); - const vw = r.width, vh = r.height; - const mw = imgW * scale, mh = imgH * scale; - - offX = mw <= vw ? (vw - mw) / 2 : Math.min(0, Math.max(vw - mw, offX)); - offY = mh <= vh ? (vh - mh) / 2 : Math.min(0, Math.max(vh - mh, offY)); -} - -function updateView() { - clampPan(); - applyTransform(); -} - -function fitToWindow() { - const r = wrap.getBoundingClientRect(); - scale = Math.min(r.width / imgW, r.height / imgH); - minScale = scale; - updateView(); -} - -/* ---------- tooltip handlers ------------------------------------ */ -function showTooltip(evt, p) { - tooltip.textContent = `${p.character_name} — Kills: ${p.kills} - Kills/h: ${p.kills_per_hour}`; - const r = wrap.getBoundingClientRect(); - tooltip.style.left = `${evt.clientX - r.left + 10}px`; - tooltip.style.top = `${evt.clientY - r.top + 10}px`; - tooltip.style.display = 'block'; -} -function hideTooltip() { - tooltip.style.display = 'none'; -} - -/* ---------- polling and initialization -------------------------- */ -async function pollLive() { - try { - const [liveRes, trailsRes] = await Promise.all([ - fetch('/live/'), - fetch('/trails/?seconds=600'), - ]); - const { players } = await liveRes.json(); - const { trails } = await trailsRes.json(); - currentPlayers = players; - renderTrails(trails); - renderList(); - } catch (e) { - console.error('Live or trails fetch failed:', e); - } -} - -function startPolling() { - if (pollID !== null) return; - pollLive(); - pollID = setInterval(pollLive, POLL_MS); -} -// -------------------- WebSocket live updates -------------------- -let wsLive = null; -function startWebSocket() { - const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${location.host}/ws/track`; - wsLive = new WebSocket(wsUrl); - wsLive.onopen = () => console.log('WS /ws/track connected'); - wsLive.onmessage = evt => { - try { - const snap = JSON.parse(evt.data); - currentPlayers = currentPlayers.filter(p => p.character_name !== snap.character_name) - .concat(snap); - renderList(); - } catch (e) { - console.error('Invalid WS message', e); - } - }; - wsLive.onclose = () => console.log('WS /ws/track closed'); - wsLive.onerror = e => console.error('WS error', e); -} - -img.onload = () => { - imgW = img.naturalWidth; - imgH = img.naturalHeight; - // size the SVG trails container to match the map dimensions - if (trailsContainer) { - trailsContainer.setAttribute('viewBox', `0 0 ${imgW} ${imgH}`); - trailsContainer.setAttribute('width', `${imgW}`); - trailsContainer.setAttribute('height', `${imgH}`); - } - fitToWindow(); - startPolling(); - // start live updates via WebSocket - startWebSocket(); -}; - -/* ---------- rendering sorted list & dots ------------------------ */ -function renderList() { - const sorted = [...currentPlayers].sort(currentSort.comparator); - render(sorted); -} - -function render(players) { - dots.innerHTML = ''; - list.innerHTML = ''; - - players.forEach(p => { - const { x, y } = worldToPx(p.ew, p.ns); - - // dot - const dot = document.createElement('div'); - dot.className = 'dot'; - dot.style.left = `${x}px`; - dot.style.top = `${y}px`; - dot.style.background = hue(p.character_name); - - - - - // custom tooltip - dot.addEventListener('mouseenter', e => showTooltip(e, p)); - dot.addEventListener('mousemove', e => showTooltip(e, p)); - dot.addEventListener('mouseleave', hideTooltip); - - // click to select/zoom - dot.addEventListener('click', () => selectPlayer(p, x, y)); - - if (p.character_name === selected) dot.classList.add('highlight'); - dots.appendChild(dot); - //sidebar - const li = document.createElement('li'); - const color = hue(p.character_name); - li.style.borderLeftColor = color; - li.className = 'player-item'; - li.innerHTML = ` - ${p.character_name} - ${loc(p.ns, p.ew)} - ${p.kills} - ${p.kills_per_hour} - ${p.rares_found} - ${p.vt_state} - `; - - // Add Chat button - const chatBtn = document.createElement('button'); - chatBtn.textContent = 'Chat'; - chatBtn.className = 'chat-btn'; - chatBtn.addEventListener('click', e => { e.stopPropagation(); openChat(p.character_name); }); - li.appendChild(chatBtn); - li.addEventListener('click', () => selectPlayer(p, x, y)); - if (p.character_name === selected) li.classList.add('selected'); - list.appendChild(li); - }); -} - -/* ---------- rendering trails ------------------------------- */ -function renderTrails(trailData) { - trailsContainer.innerHTML = ''; - const byChar = trailData.reduce((acc, pt) => { - (acc[pt.character_name] = acc[pt.character_name] || []).push(pt); - return acc; - }, {}); - for (const [name, pts] of Object.entries(byChar)) { - if (pts.length < 2) continue; - const points = pts.map(pt => { - const { x, y } = worldToPx(pt.ew, pt.ns); - return `${x},${y}`; - }).join(' '); - const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); - poly.setAttribute('points', points); - poly.setAttribute('stroke', hue(name)); - poly.setAttribute('fill', 'none'); - poly.setAttribute('class', 'trail-path'); - trailsContainer.appendChild(poly); - } -} -/* ---------- selection centering, focus zoom & blink ------------ */ -function selectPlayer(p, x, y) { - selected = p.character_name; - // set focus zoom - scale = Math.min(MAX_Z, Math.max(minScale, FOCUS_ZOOM)); - // center on the player - const r = wrap.getBoundingClientRect(); - offX = r.width / 2 - x * scale; - offY = r.height / 2 - y * scale; - updateView(); - renderList(); // keep sorted + highlight -} - -/* ---------- pan & zoom handlers -------------------------------- */ -wrap.addEventListener('wheel', e => { - e.preventDefault(); - if (!imgW) return; - - const r = wrap.getBoundingClientRect(); - const mx = (e.clientX - r.left - offX) / scale; - const my = (e.clientY - r.top - offY) / scale; - const factor = e.deltaY > 0 ? 0.9 : 1.1; - let ns = scale * factor; - ns = Math.max(minScale, Math.min(MAX_Z, ns)); - - offX -= mx * (ns - scale); - offY -= my * (ns - scale); - scale = ns; - updateView(); -}, { passive: false }); - -wrap.addEventListener('mousedown', e => { - dragging = true; sx = e.clientX; sy = e.clientY; - wrap.classList.add('dragging'); -}); -window.addEventListener('mousemove', e => { - if (!dragging) return; - offX += e.clientX - sx; offY += e.clientY - sy; - sx = e.clientX; sy = e.clientY; - updateView(); -}); -window.addEventListener('mouseup', () => { - dragging = false; wrap.classList.remove('dragging'); -}); - -wrap.addEventListener('touchstart', e => { - if (e.touches.length !== 1) return; - dragging = true; - sx = e.touches[0].clientX; sy = e.touches[0].clientY; -}); -wrap.addEventListener('touchmove', e => { - if (!dragging || e.touches.length !== 1) return; - const t = e.touches[0]; - offX += t.clientX - sx; offY += t.clientY - sy; - sx = t.clientX; sy = t.clientY; - updateView(); -}); -wrap.addEventListener('touchend', () => { - dragging = false; -}); -// Chat UI functions -function openChat(name) { - currentChatName = name; - chatTitle.textContent = `Chat: ${name}`; - chatMessages.innerHTML = ''; - chatOverlay.classList.remove('hidden'); - const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${location.host}/ws/command?character_name=${encodeURIComponent(name)}`; - chatSocket = new WebSocket(wsUrl); - chatSocket.onmessage = evt => { - try { - const msg = JSON.parse(evt.data); - appendChatMessage(msg); - } catch (e) { console.error('Invalid chat msg', e); } - }; - chatSocket.onopen = () => console.log(`Chat WS connected: ${wsUrl}`); - chatSocket.onclose = () => console.log('Chat WS closed'); - chatSocket.onerror = e => console.error('Chat WS error', e); -} - -function closeChat() { - if (chatSocket) { chatSocket.close(); chatSocket = null; } - chatOverlay.classList.add('hidden'); - currentChatName = null; -} - -function appendChatMessage(msg) { - const div = document.createElement('div'); - div.className = 'chat-message'; - const time = new Date(msg.timestamp).toLocaleTimeString(); - const who = msg.from === 'browser' ? 'You' : msg.from; - div.textContent = `[${time}] ${who}: ${msg.text}`; - chatMessages.appendChild(div); - chatMessages.scrollTop = chatMessages.scrollHeight; -} - -chatClose.addEventListener('click', closeChat); -chatForm.addEventListener('submit', e => { - e.preventDefault(); - if (!chatSocket || !currentChatName) return; - const text = chatInput.value.trim(); - if (!text) return; - chatSocket.send(JSON.stringify({ command: text })); - appendChatMessage({ from: 'browser', text, timestamp: new Date().toISOString() }); - chatInput.value = ''; -}); diff --git a/static_ws/style.css b/static_ws/style.css deleted file mode 100644 index 5deb8ea6..00000000 --- a/static_ws/style.css +++ /dev/null @@ -1,354 +0,0 @@ -:root { - --sidebar-width: 280px; - --bg-main: #111; - --bg-side: #1a1a1a; - --card: #222; - --card-hov:#333; - --text: #eee; - --accent: #88f; -} -.chat-form button { border: none; background: var(--accent); color: #111; padding: 8px 12px; cursor: pointer; } - -/* Chat overlay styling */ - .chat-overlay { - position: absolute; - top: 10px; - left: calc(var(--sidebar-width) + 10px); - width: 300px; - height: 400px; - background: rgba(20,20,20,0.95); - border: 1px solid #444; - border-radius: 4px; - display: flex; - flex-direction: column; - z-index: 1000; -} -.chat-overlay.hidden { - display: none; -} -.chat-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - background: #333; - color: #eee; - font-weight: bold; -} -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 8px; - color: #eee; - font-size: 0.85rem; -} -.chat-form { - display: flex; - border-top: 1px solid #444; -} -.chat-form input { - flex: 1; - border: none; - padding: 8px; - background: #222; - color: #eee; -} -.chat-form input:focus { - outline: none; -} -.chat-form button { - border: none; - background: var(--accent); - color: #111; - padding: 8px 12px; - cursor: pointer; -} -.chat-message { - margin-bottom: 6px; -} -.chat-message:nth-child(odd) { - background: rgba(255,255,255,0.05); - padding: 4px 6px; - border-radius: 3px; -} -.chat-message:nth-child(even) { - background: rgba(255,255,255,0.02); - padding: 4px 6px; - border-radius: 3px; -} - -html { - margin: 0; - height: 100%; - width: 100%; -} - -body { - margin: 0; - height: 100%; - display: flex; - overflow: hidden; - font-family: "Segoe UI", sans-serif; - background: var(--bg-main); - color: var(--text); -} - -/* ---------- sort buttons --------------------------------------- */ -.sort-buttons { - display: flex; - gap: 4px; - margin: 12px 16px 8px; -} -.sort-buttons .btn { - flex: 1; - padding: 6px 8px; - background: #222; - color: #eee; - border: 1px solid #555; - border-radius: 4px; - text-align: center; - cursor: pointer; - user-select: none; - font-size: 0.9rem; -} -.sort-buttons .btn.active { - background: var(--accent); - color: #111; - border-color: var(--accent); -} - -/* ---------- sidebar --------------------------------------------- */ -#sidebar { - width: var(--sidebar-width); - scrollbar-width: none; - background: var(--bg-side); - border-right: 2px solid #333; - box-sizing: border-box; - padding: 18px 16px; - overflow-y: auto; -} -#sidebar h2 { - margin: 8px 0 12px; - font-size: 1.25rem; - color: var(--accent); -} -#playerList { - list-style: none; - margin: 0; - padding: 0; -} -#playerList li { - margin: 4px 0; - padding: 6px 8px; - background: var(--card); - border-left: 4px solid #555; - cursor: pointer; -} -#playerList li:hover { - background: var(--card-hov); -} -#playerList li.selected { - background: #454545; -} - -/* ---------- map container --------------------------------------- */ -#mapContainer { - flex: 1; - min-width: 0; - min-height: 0; - position: relative; - overflow: hidden; - background: #000; - cursor: grab; -} -#mapContainer.dragging { - cursor: grabbing; -} -#mapGroup { - position: absolute; - top: 0; - left: 0; - transform-origin: 0 0; -} -#map { - display: block; - user-select: none; - pointer-events: none; -} - -/* ---------- dots ------------------------------------------------ */ -#dots { - position: absolute; - top: 0; - left: 0; - pointer-events: none; -} -.dot { - position: absolute; - width: 6px; - height: 6px; - border-radius: 50%; - border: 1px solid #000; - transform: translate(-50%, -50%); - - /* enable events on each dot */ - pointer-events: auto; - cursor: pointer; -} -.dot.highlight { - width: 10px; - height: 10px; - animation: blink 0.6s step-end infinite; -} -@keyframes blink { - 50% { opacity: 0; } -} - -/* ---------- tooltip --------------------------------------------- */ -.tooltip { - position: absolute; - display: none; - background: rgba(0, 0, 0, 0.8); - color: #fff; - padding: 4px 8px; - border-radius: 4px; - font-size: 0.8rem; - pointer-events: none; - white-space: nowrap; - z-index: 1000; -} -/* make each row a flex container */ -/* 2-column flex layout for each player row */ -/* make each row a flex container */ -/* make each row a vertical stack */ -/* make each player row into a 3×2 grid */ -#playerList li { - display: grid; - grid-template-columns: 1fr auto; - grid-template-rows: auto auto auto; - grid-template-areas: - "name loc" - "kills kph" - "rares meta"; - gap: 4px 8px; - margin: 6px 0; - padding: 8px 10px; - background: var(--card); - border-left: 4px solid transparent; - transition: background 0.15s; - font-size: 0.85rem; -} - -/* assign each span into its grid cell */ -.player-name { grid-area: name; font-weight: 600; color: var(--text); } -.player-loc { grid-area: loc; font-size: 0.75rem; color: #aaa; } - -.stat.kills { grid-area: kills; } -.stat.kph { grid-area: kph; } -.stat.rares { grid-area: rares; } -.stat.meta { grid-area: meta; } - -/* pill styling */ -#playerList li .stat { - background: rgba(255,255,255,0.1); - padding: 4px 8px; - border-radius: 12px; - display: inline-block; - font-size: 0.75rem; - white-space: nowrap; - color: var(--text); -} - -/* icons & suffixes */ -.stat.kills::before { content: "⚔️ "; } -.stat.kph::after { content: " KPH"; font-size:0.7em; color:#aaa; } -.stat.rares::after { content: " Rares"; font-size:0.7em; color:#aaa; } -.stat.meta { - background: var(--accent); - color: #111; -} - -/* hover & selected states */ -#playerList li:hover { background: var(--card-hov); } -#playerList li.selected { background: #454545; } -/* trails paths */ -#trails { - position: absolute; - top: 0; - left: 0; - pointer-events: none; -} -.trail-path { - fill: none; - stroke-width: 2px; - stroke-opacity: 0.7; - stroke-linecap: round; - stroke-linejoin: round; -} -/* Chat overlay styling */ -.chat-overlay { - position: absolute; - top: 10px; - right: 10px; - width: 300px; - height: 400px; - background: rgba(20,20,20,0.95); - border: 1px solid #444; - border-radius: 4px; - display: flex; - flex-direction: column; - z-index: 1000; -} -.chat-overlay.hidden { - display: none; -} -.chat-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - background: #333; - color: #eee; - font-weight: bold; -} -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 8px; - color: #eee; - font-size: 0.85rem; -} -.chat-form { - display: flex; - border-top: 1px solid #444; -} -.chat-form input { - flex: 1; - border: none; - padding: 8px; - background: #222; - color: #eee; -} -.chat-form input:focus { - outline: none; -} -.chat-form button { - border: none; - background: var(--accent); - color: #111; - padding: 8px 12px; - cursor: pointer; -} -.chat-message { - margin-bottom: 6px; -} -.chat-message:nth-child(odd) { - background: rgba(255,255,255,0.05); - padding: 4px 6px; - border-radius: 3px; -} -.chat-message:nth-child(even) { - background: rgba(255,255,255,0.02); - padding: 4px 6px; - border-radius: 3px; -}