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)
# =============================
# 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
// 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
});

View File

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