diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc index 93572d5..5062ef8 100644 Binary files a/__pycache__/main.cpython-311.pyc and b/__pycache__/main.cpython-311.pyc differ diff --git a/main.py b/main.py index 4de8cdf..dc70c88 100644 --- a/main.py +++ b/main.py @@ -100,6 +100,26 @@ async def get_enemy_members(): return json.load(f) +# ============================= +# Status JSON endpoints +# ============================= +@app.get("/api/friendly_status") +async def api_friendly_status(): + path = Path("data/friendly_status.json") + if not path.exists(): + return {} + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +@app.get("/api/enemy_status") +async def api_enemy_status(): + path = Path("data/enemy_status.json") + if not path.exists(): + return {} + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + # ============================================================ diff --git a/static/dashboard.js b/static/dashboard.js index 3fa7858..68bb8d8 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -1,6 +1,6 @@ // dashboard.js -// Full functionality: populate, start status loops, load members, drag & drop between lists and groups, -// drop zones accept only friendly/enemy respectively, status color updates. +// Full functionality: populate, start/stop status loops, load members, drag & drop between lists and groups, +// drop zones accept only friendly/enemy respectively, status color updates, localStorage persistence // ---- In-memory maps ---- const friendlyMembers = new Map(); // id -> member object @@ -15,19 +15,17 @@ function toInt(v) { const n = Number(v); return Number.isNaN(n) ? null : n; } // ---- Create structured card ---- function createMemberCard(member, kind) { - // member: { id, name, level, estimate, status } - // kind: "friendly" or "enemy" (for ARIA / dataset) const card = document.createElement("div"); card.classList.add("member-card"); - card.classList.add(kind); card.setAttribute("draggable", "true"); card.dataset.id = member.id; card.dataset.kind = kind; - // structured children const nameDiv = document.createElement("div"); nameDiv.className = "name"; nameDiv.textContent = member.name; + if (kind === "friendly") nameDiv.style.color = "#8fd38f"; // green + if (kind === "enemy") nameDiv.style.color = "#ff6b6b"; // red const statsDiv = document.createElement("div"); statsDiv.className = "stats"; @@ -36,21 +34,17 @@ function createMemberCard(member, kind) { card.appendChild(nameDiv); card.appendChild(statsDiv); - // store back-reference member.domElement = card; - // drag events: encode type and id in dataTransfer card.addEventListener("dragstart", (e) => { const payload = JSON.stringify({ kind, id: member.id }); e.dataTransfer.setData("text/plain", payload); - // visual card.style.opacity = "0.5"; }); card.addEventListener("dragend", () => { card.style.opacity = "1"; }); - // initial color applyStatusClass(member); return card; @@ -67,20 +61,16 @@ function applyStatusClass(member) { else if (s === "hospitalized" || s === "hospital") span.classList.add("status-hospitalized"); } -// ---- Load static members from backend into the list containers ---- +// ---- Load static members from backend ---- async function loadMembers(faction) { const url = faction === "friendly" ? "/api/friendly_members" : "/api/enemy_members"; try { const res = await fetch(url, { cache: "no-store" }); - if (!res.ok) { - console.error("Failed to fetch members:", res.status); - return; - } + if (!res.ok) return console.error("Failed to fetch members:", res.status); const arr = await res.json(); const container = faction === "friendly" ? friendlyContainer : enemyContainer; const map = faction === "friendly" ? friendlyMembers : enemyMembers; - // clear container.innerHTML = ""; map.clear(); @@ -95,21 +85,18 @@ async function loadMembers(faction) { } } -// ---- Refresh status map and update DOM (does not touch static info or position) ---- +// ---- Refresh status map and update DOM ---- async function refreshStatus(faction) { const url = faction === "friendly" ? "/api/friendly_status" : "/api/enemy_status"; const map = faction === "friendly" ? friendlyMembers : enemyMembers; try { const res = await fetch(url, { cache: "no-store" }); - if (!res.ok) { - console.warn("Status fetch failed:", res.status); - return; - } - const statusData = await res.json(); // { id: { status: "..."}, ... } + if (!res.ok) return; + const statusData = await res.json(); Object.keys(statusData).forEach(k => { const id = parseInt(k); const member = map.get(id); - if (!member) return; // not loaded yet or moved elsewhere + if (!member) return; member.status = statusData[k].status; const span = member.domElement.querySelector(".status-text"); span.textContent = member.status; @@ -120,7 +107,7 @@ async function refreshStatus(faction) { } } -// ---- Populate endpoints: call backend to fetch static info (FFScouter) once ---- +// ---- Populate endpoints ---- async function populateFriendly() { const id = toInt(document.getElementById("friendly-id").value); if (!id) return alert("Enter friendly faction ID"); @@ -143,11 +130,19 @@ async function populateEnemy() { await loadMembers("enemy"); } -// ---- Start status refresh loops on backend and JS-side polling ---- +// ---- Start/Stop status refresh loops ---- let friendlyStatusIntervalHandle = null; let enemyStatusIntervalHandle = null; -async function startFriendlyStatus() { +async function toggleFriendlyStatus() { + const btn = document.getElementById("friendly-status-btn"); + if (friendlyStatusIntervalHandle) { + clearInterval(friendlyStatusIntervalHandle); + friendlyStatusIntervalHandle = null; + btn.textContent = "Start Refresh"; + return; + } + const id = toInt(document.getElementById("friendly-id").value); const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10); if (!id) return alert("Enter friendly faction ID"); @@ -158,14 +153,20 @@ async function startFriendlyStatus() { body: JSON.stringify({ faction_id: id, interval }) }); - // clear existing - if (friendlyStatusIntervalHandle) clearInterval(friendlyStatusIntervalHandle); friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000); - // immediate + btn.textContent = "Stop Refresh"; refreshStatus("friendly"); } -async function startEnemyStatus() { +async function toggleEnemyStatus() { + const btn = document.getElementById("enemy-status-btn"); + if (enemyStatusIntervalHandle) { + clearInterval(enemyStatusIntervalHandle); + enemyStatusIntervalHandle = null; + btn.textContent = "Start Refresh"; + return; + } + const id = toInt(document.getElementById("enemy-id").value); const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10); if (!id) return alert("Enter enemy faction ID"); @@ -176,8 +177,8 @@ async function startEnemyStatus() { body: JSON.stringify({ faction_id: id, interval }) }); - if (enemyStatusIntervalHandle) clearInterval(enemyStatusIntervalHandle); enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000); + btn.textContent = "Stop Refresh"; refreshStatus("enemy"); } @@ -188,7 +189,6 @@ function setupDropZones() { dropZones.forEach(zone => { zone.addEventListener("dragover", (e) => { e.preventDefault(); - // peek at payload to determine if valid const raw = e.dataTransfer.types.includes("text/plain") ? e.dataTransfer.getData("text/plain") : null; let valid = false; if (raw) { @@ -219,25 +219,50 @@ function setupDropZones() { const idn = parseInt(id); if (!kind || !idn) return; - // Determine target zone type (friendly or enemy) for validation const zoneType = zone.classList.contains("friendly-zone") || zone.id.includes("friendly") ? "friendly" : zone.classList.contains("enemy-zone") || zone.id.includes("enemy") ? "enemy" : null; - if (zoneType !== kind) { - // invalid drop - return; - } + if (zoneType !== kind) return; - // Get member object from maps const map = kind === "friendly" ? friendlyMembers : enemyMembers; const member = map.get(idn); if (!member) return; - // Remove from original parent if exists (so it moves) const prev = member.domElement?.parentElement; if (prev && prev !== zone) prev.removeChild(member.domElement); - - // Append to target zone zone.appendChild(member.domElement); + + // persist assignment in localStorage + saveAssignments(); + }); + }); +} + +// ---- Persist card assignments to localStorage ---- +function saveAssignments() { + const groups = document.querySelectorAll(".group"); + const data = {}; + groups.forEach(g => { + const gid = g.dataset.id; + data[gid] = { friendly: [], enemy: [] }; + g.querySelectorAll(".friendly-zone .member-card").forEach(c => data[gid].friendly.push(parseInt(c.dataset.id))); + g.querySelectorAll(".enemy-zone .member-card").forEach(c => data[gid].enemy.push(parseInt(c.dataset.id))); + }); + localStorage.setItem("battleAssignments", JSON.stringify(data)); +} + +// ---- Restore card assignments from localStorage ---- +function loadAssignments() { + const data = JSON.parse(localStorage.getItem("battleAssignments") || "{}"); + Object.keys(data).forEach(gid => { + const group = document.querySelector(`.group[data-id="${gid}"]`); + if (!group) return; + data[gid].friendly.forEach(id => { + const member = friendlyMembers.get(id); + if (member) group.querySelector(".friendly-zone").appendChild(member.domElement); + }); + data[gid].enemy.forEach(id => { + const member = enemyMembers.get(id); + if (member) group.querySelector(".enemy-zone").appendChild(member.domElement); }); }); } @@ -259,8 +284,8 @@ function attachCardClickLogging() { function wireUp() { document.getElementById("friendly-populate-btn").addEventListener("click", populateFriendly); document.getElementById("enemy-populate-btn").addEventListener("click", populateEnemy); - document.getElementById("friendly-status-btn").addEventListener("click", startFriendlyStatus); - document.getElementById("enemy-status-btn").addEventListener("click", startEnemyStatus); + document.getElementById("friendly-status-btn").addEventListener("click", toggleFriendlyStatus); + document.getElementById("enemy-status-btn").addEventListener("click", toggleEnemyStatus); setupDropZones(); attachCardClickLogging(); @@ -269,7 +294,7 @@ function wireUp() { // ---- On load: wire and attempt to load any existing JSON lists ---- document.addEventListener("DOMContentLoaded", async () => { wireUp(); - // attempt to load existing data (if files exist) await loadMembers("friendly"); await loadMembers("enemy"); + loadAssignments(); // restore positions after load }); diff --git a/templates/dashboard.html b/templates/dashboard.html index ef372f3..ca53085 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -7,17 +7,27 @@
+ +

War Dashboard

-
- - + +
+ + +
+ + +
- + +
+ +

Friendly Faction

@@ -25,9 +35,13 @@
-
+ +
+

Enemy Faction

@@ -35,15 +49,19 @@
-
+ +
- +
- -
+ + +
Group 1
@@ -55,7 +73,8 @@
-
+ +
Group 2
@@ -67,7 +86,8 @@
-
+ +
Group 3
@@ -79,7 +99,8 @@
-
+ +
Group 4
@@ -91,7 +112,8 @@
-
+ +
Group 5
@@ -102,10 +124,11 @@
-
-
-
-
+ +
+
+
+