Persistent Storage on reload, Start and Stop buttons

This commit is contained in:
2025-11-28 12:19:30 -05:00
parent 441ae31eb6
commit 4b0038a4b7
4 changed files with 129 additions and 61 deletions

Binary file not shown.

20
main.py
View File

@@ -100,6 +100,26 @@ async def get_enemy_members():
return json.load(f) 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)
# ============================================================ # ============================================================

View File

@@ -1,6 +1,6 @@
// dashboard.js // dashboard.js
// Full functionality: populate, start status loops, load members, drag & drop between lists and groups, // 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. // drop zones accept only friendly/enemy respectively, status color updates, localStorage persistence
// ---- In-memory maps ---- // ---- In-memory maps ----
const friendlyMembers = new Map(); // id -> member object 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 ---- // ---- Create structured card ----
function createMemberCard(member, kind) { function createMemberCard(member, kind) {
// member: { id, name, level, estimate, status }
// kind: "friendly" or "enemy" (for ARIA / dataset)
const card = document.createElement("div"); const card = document.createElement("div");
card.classList.add("member-card"); card.classList.add("member-card");
card.classList.add(kind);
card.setAttribute("draggable", "true"); card.setAttribute("draggable", "true");
card.dataset.id = member.id; card.dataset.id = member.id;
card.dataset.kind = kind; card.dataset.kind = kind;
// structured children
const nameDiv = document.createElement("div"); const nameDiv = document.createElement("div");
nameDiv.className = "name"; nameDiv.className = "name";
nameDiv.textContent = member.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"); const statsDiv = document.createElement("div");
statsDiv.className = "stats"; statsDiv.className = "stats";
@@ -36,21 +34,17 @@ function createMemberCard(member, kind) {
card.appendChild(nameDiv); card.appendChild(nameDiv);
card.appendChild(statsDiv); card.appendChild(statsDiv);
// store back-reference
member.domElement = card; member.domElement = card;
// drag events: encode type and id in dataTransfer
card.addEventListener("dragstart", (e) => { card.addEventListener("dragstart", (e) => {
const payload = JSON.stringify({ kind, id: member.id }); const payload = JSON.stringify({ kind, id: member.id });
e.dataTransfer.setData("text/plain", payload); e.dataTransfer.setData("text/plain", payload);
// visual
card.style.opacity = "0.5"; card.style.opacity = "0.5";
}); });
card.addEventListener("dragend", () => { card.addEventListener("dragend", () => {
card.style.opacity = "1"; card.style.opacity = "1";
}); });
// initial color
applyStatusClass(member); applyStatusClass(member);
return card; return card;
@@ -67,20 +61,16 @@ function applyStatusClass(member) {
else if (s === "hospitalized" || s === "hospital") span.classList.add("status-hospitalized"); 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) { async function loadMembers(faction) {
const url = faction === "friendly" ? "/api/friendly_members" : "/api/enemy_members"; const url = faction === "friendly" ? "/api/friendly_members" : "/api/enemy_members";
try { try {
const res = await fetch(url, { cache: "no-store" }); const res = await fetch(url, { cache: "no-store" });
if (!res.ok) { if (!res.ok) return console.error("Failed to fetch members:", res.status);
console.error("Failed to fetch members:", res.status);
return;
}
const arr = await res.json(); const arr = await res.json();
const container = faction === "friendly" ? friendlyContainer : enemyContainer; const container = faction === "friendly" ? friendlyContainer : enemyContainer;
const map = faction === "friendly" ? friendlyMembers : enemyMembers; const map = faction === "friendly" ? friendlyMembers : enemyMembers;
// clear
container.innerHTML = ""; container.innerHTML = "";
map.clear(); 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) { async function refreshStatus(faction) {
const url = faction === "friendly" ? "/api/friendly_status" : "/api/enemy_status"; const url = faction === "friendly" ? "/api/friendly_status" : "/api/enemy_status";
const map = faction === "friendly" ? friendlyMembers : enemyMembers; const map = faction === "friendly" ? friendlyMembers : enemyMembers;
try { try {
const res = await fetch(url, { cache: "no-store" }); const res = await fetch(url, { cache: "no-store" });
if (!res.ok) { if (!res.ok) return;
console.warn("Status fetch failed:", res.status); const statusData = await res.json();
return;
}
const statusData = await res.json(); // { id: { status: "..."}, ... }
Object.keys(statusData).forEach(k => { Object.keys(statusData).forEach(k => {
const id = parseInt(k); const id = parseInt(k);
const member = map.get(id); const member = map.get(id);
if (!member) return; // not loaded yet or moved elsewhere if (!member) return;
member.status = statusData[k].status; member.status = statusData[k].status;
const span = member.domElement.querySelector(".status-text"); const span = member.domElement.querySelector(".status-text");
span.textContent = member.status; 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() { async function populateFriendly() {
const id = toInt(document.getElementById("friendly-id").value); const id = toInt(document.getElementById("friendly-id").value);
if (!id) return alert("Enter friendly faction ID"); if (!id) return alert("Enter friendly faction ID");
@@ -143,11 +130,19 @@ async function populateEnemy() {
await loadMembers("enemy"); await loadMembers("enemy");
} }
// ---- Start status refresh loops on backend and JS-side polling ---- // ---- Start/Stop status refresh loops ----
let friendlyStatusIntervalHandle = null; let friendlyStatusIntervalHandle = null;
let enemyStatusIntervalHandle = 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 id = toInt(document.getElementById("friendly-id").value);
const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10); const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10);
if (!id) return alert("Enter friendly faction ID"); if (!id) return alert("Enter friendly faction ID");
@@ -158,14 +153,20 @@ async function startFriendlyStatus() {
body: JSON.stringify({ faction_id: id, interval }) body: JSON.stringify({ faction_id: id, interval })
}); });
// clear existing
if (friendlyStatusIntervalHandle) clearInterval(friendlyStatusIntervalHandle);
friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000); friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000);
// immediate btn.textContent = "Stop Refresh";
refreshStatus("friendly"); 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 id = toInt(document.getElementById("enemy-id").value);
const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10); const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10);
if (!id) return alert("Enter enemy faction ID"); if (!id) return alert("Enter enemy faction ID");
@@ -176,8 +177,8 @@ async function startEnemyStatus() {
body: JSON.stringify({ faction_id: id, interval }) body: JSON.stringify({ faction_id: id, interval })
}); });
if (enemyStatusIntervalHandle) clearInterval(enemyStatusIntervalHandle);
enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000); enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000);
btn.textContent = "Stop Refresh";
refreshStatus("enemy"); refreshStatus("enemy");
} }
@@ -188,7 +189,6 @@ function setupDropZones() {
dropZones.forEach(zone => { dropZones.forEach(zone => {
zone.addEventListener("dragover", (e) => { zone.addEventListener("dragover", (e) => {
e.preventDefault(); e.preventDefault();
// peek at payload to determine if valid
const raw = e.dataTransfer.types.includes("text/plain") ? e.dataTransfer.getData("text/plain") : null; const raw = e.dataTransfer.types.includes("text/plain") ? e.dataTransfer.getData("text/plain") : null;
let valid = false; let valid = false;
if (raw) { if (raw) {
@@ -219,25 +219,50 @@ function setupDropZones() {
const idn = parseInt(id); const idn = parseInt(id);
if (!kind || !idn) return; 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" : const zoneType = zone.classList.contains("friendly-zone") || zone.id.includes("friendly") ? "friendly" :
zone.classList.contains("enemy-zone") || zone.id.includes("enemy") ? "enemy" : null; zone.classList.contains("enemy-zone") || zone.id.includes("enemy") ? "enemy" : null;
if (zoneType !== kind) { if (zoneType !== kind) return;
// invalid drop
return;
}
// Get member object from maps
const map = kind === "friendly" ? friendlyMembers : enemyMembers; const map = kind === "friendly" ? friendlyMembers : enemyMembers;
const member = map.get(idn); const member = map.get(idn);
if (!member) return; if (!member) return;
// Remove from original parent if exists (so it moves)
const prev = member.domElement?.parentElement; const prev = member.domElement?.parentElement;
if (prev && prev !== zone) prev.removeChild(member.domElement); if (prev && prev !== zone) prev.removeChild(member.domElement);
// Append to target zone
zone.appendChild(member.domElement); 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() { function wireUp() {
document.getElementById("friendly-populate-btn").addEventListener("click", populateFriendly); document.getElementById("friendly-populate-btn").addEventListener("click", populateFriendly);
document.getElementById("enemy-populate-btn").addEventListener("click", populateEnemy); document.getElementById("enemy-populate-btn").addEventListener("click", populateEnemy);
document.getElementById("friendly-status-btn").addEventListener("click", startFriendlyStatus); document.getElementById("friendly-status-btn").addEventListener("click", toggleFriendlyStatus);
document.getElementById("enemy-status-btn").addEventListener("click", startEnemyStatus); document.getElementById("enemy-status-btn").addEventListener("click", toggleEnemyStatus);
setupDropZones(); setupDropZones();
attachCardClickLogging(); attachCardClickLogging();
@@ -269,7 +294,7 @@ function wireUp() {
// ---- On load: wire and attempt to load any existing JSON lists ---- // ---- On load: wire and attempt to load any existing JSON lists ----
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
wireUp(); wireUp();
// attempt to load existing data (if files exist)
await loadMembers("friendly"); await loadMembers("friendly");
await loadMembers("enemy"); await loadMembers("enemy");
loadAssignments(); // restore positions after load
}); });

View File

@@ -7,17 +7,27 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<!-- Top bar: Title + interval + Reset button -->
<div class="top-bar"> <div class="top-bar">
<h1>War Dashboard</h1> <h1>War Dashboard</h1>
<div class="top-controls">
<button id="reset-groups-btn" class="reset-btn">Reset Groups</button>
<div class="interval-box"> <div class="interval-box">
<label for="refresh-interval">Refresh Interval (seconds)</label> <label for="refresh-interval">Refresh Interval (seconds)</label>
<input type="number" id="refresh-interval" value="10" min="1" /> <input type="number" id="refresh-interval" value="10" min="1" />
</div> </div>
</div> </div>
</div>
<div class="main-row"> <div class="main-row">
<!-- LEFT: faction lists stacked vertically -->
<!-- LEFT COLUMN -->
<div class="left-col"> <div class="left-col">
<!-- FRIENDLY -->
<div class="faction-card small"> <div class="faction-card small">
<h2>Friendly Faction</h2> <h2>Friendly Faction</h2>
<div class="controls"> <div class="controls">
@@ -25,9 +35,13 @@
<button id="friendly-populate-btn">Populate</button> <button id="friendly-populate-btn">Populate</button>
<button id="friendly-status-btn">Start Refresh</button> <button id="friendly-status-btn">Start Refresh</button>
</div> </div>
<div id="friendly-container" class="member-list" aria-label="Friendly members"></div>
<div id="friendly-container"
class="member-list friendly-zone"
aria-label="Friendly members"></div>
</div> </div>
<!-- ENEMY -->
<div class="faction-card small"> <div class="faction-card small">
<h2>Enemy Faction</h2> <h2>Enemy Faction</h2>
<div class="controls"> <div class="controls">
@@ -35,15 +49,19 @@
<button id="enemy-populate-btn">Populate</button> <button id="enemy-populate-btn">Populate</button>
<button id="enemy-status-btn">Start Refresh</button> <button id="enemy-status-btn">Start Refresh</button>
</div> </div>
<div id="enemy-container" class="member-list" aria-label="Enemy members"></div>
<div id="enemy-container"
class="member-list enemy-zone"
aria-label="Enemy members"></div>
</div> </div>
</div> </div>
<!-- RIGHT: 5 groups, each with friendly + enemy drop zones --> <!-- RIGHT COLUMN: BATTLE GROUPS -->
<div class="right-col"> <div class="right-col">
<div class="groups-grid"> <div class="groups-grid">
<!-- Generate 5 groups -->
<div class="group" id="group-1"> <!-- Group 1 -->
<div class="group" id="group-1" data-id="1">
<div class="group-title">Group 1</div> <div class="group-title">Group 1</div>
<div class="group-zones"> <div class="group-zones">
<div class="drop-zone friendly-zone" data-zone="friendly" data-group="1" id="group-1-friendly"> <div class="drop-zone friendly-zone" data-zone="friendly" data-group="1" id="group-1-friendly">
@@ -55,7 +73,8 @@
</div> </div>
</div> </div>
<div class="group" id="group-2"> <!-- Group 2 -->
<div class="group" id="group-2" data-id="2">
<div class="group-title">Group 2</div> <div class="group-title">Group 2</div>
<div class="group-zones"> <div class="group-zones">
<div class="drop-zone friendly-zone" data-zone="friendly" data-group="2" id="group-2-friendly"> <div class="drop-zone friendly-zone" data-zone="friendly" data-group="2" id="group-2-friendly">
@@ -67,7 +86,8 @@
</div> </div>
</div> </div>
<div class="group" id="group-3"> <!-- Group 3 -->
<div class="group" id="group-3" data-id="3">
<div class="group-title">Group 3</div> <div class="group-title">Group 3</div>
<div class="group-zones"> <div class="group-zones">
<div class="drop-zone friendly-zone" data-zone="friendly" data-group="3" id="group-3-friendly"> <div class="drop-zone friendly-zone" data-zone="friendly" data-group="3" id="group-3-friendly">
@@ -79,7 +99,8 @@
</div> </div>
</div> </div>
<div class="group" id="group-4"> <!-- Group 4 -->
<div class="group" id="group-4" data-id="4">
<div class="group-title">Group 4</div> <div class="group-title">Group 4</div>
<div class="group-zones"> <div class="group-zones">
<div class="drop-zone friendly-zone" data-zone="friendly" data-group="4" id="group-4-friendly"> <div class="drop-zone friendly-zone" data-zone="friendly" data-group="4" id="group-4-friendly">
@@ -91,7 +112,8 @@
</div> </div>
</div> </div>
<div class="group" id="group-5"> <!-- Group 5 -->
<div class="group" id="group-5" data-id="5">
<div class="group-title">Group 5</div> <div class="group-title">Group 5</div>
<div class="group-zones"> <div class="group-zones">
<div class="drop-zone friendly-zone" data-zone="friendly" data-group="5" id="group-5-friendly"> <div class="drop-zone friendly-zone" data-zone="friendly" data-group="5" id="group-5-friendly">
@@ -102,10 +124,11 @@
</div> </div>
</div> </div>
</div> </div>
</div> <!-- groups-grid -->
</div> <!-- right-col --> </div>
</div> <!-- main-row --> </div>
</div> <!-- container --> </div>
</div>
<script src="/static/dashboard.js"></script> <script src="/static/dashboard.js"></script>
</body> </body>