Persistent Storage on reload, Start and Stop buttons
This commit is contained in:
Binary file not shown.
20
main.py
20
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)
|
||||
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user