From 022cb0eb7517fdc50d97154615828c271d2f4fce Mon Sep 17 00:00:00 2001 From: jerick Date: Fri, 28 Nov 2025 12:55:31 -0500 Subject: [PATCH] Fix for duplicate member cards when pressing populate --- __pycache__/main.cpython-311.pyc | Bin 9164 -> 9187 bytes main.py | 2 +- static/dashboard.js | 360 ++++++++++++++++++------------- 3 files changed, 211 insertions(+), 151 deletions(-) diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc index 5062ef846400df7cb486798bd51ad4b15c29066c..4080aaf194cd74a6a79dac127e390ba2a7d4ee20 100644 GIT binary patch delta 839 zcmZ{iO=uHA6vy{%l4cWKwJ}Yb50l!Gn6+t4Yqiv`La8LCH5MDeMsV3?rzIus!em?Y zRK4jwO6?_8G$Va!hK`36w5gZtXhbrMZ@RC*dQj+kT}p?)Z{!=dL;vOk6TcCtmoX_#i? zVYK}WCw8)BBYjX|QxUXzx?e)NidimlD3asXbX~2Ri`4%|q_fzo+FyE_AiL-HqE)1q znHtmb8H^r6U=`^c;woYQ(Tl)R(HR$PJvIq5>}Y%~_HW#gOhcta({E71e#KuLyTbjv zCUxF8zLup?%kfn&Or5Q4Ti6M>L3Q4==n`8`e1Hk|`oJm-v2yav>M&*yBrFZ^S(;-+GkqoviSMDZx|?#1bOIdQ&<6M$e~u;EuhJdIFj54MBwMxI6b9)gN0S JyKwM<_#3v^!0-S7 delta 794 zcmZ{iOK1~O6o%(aGR*Xm(Z;4W&zjU!Cv9p%u$EdPrJ4{!8clpgq)t22I*=xKGs#Yj z8*!&_7jE1sxNucw(S;zm71ssFmAJ@46a_aH1@B3)_@D#x-E;r*&zW=P&WG%qtm~P} z)z7i(^|oRxZn`>R`7ql(;&iS88du>ja=JrzYB0~#Zp34xIm(>kwB?MtY8v%gNx#+9 z8>ZdA*4r-|nk^YMQztF8x}*s7oHR!tNPdXW=aP3&9%>ibg}dx-7wB851C#Vs-xMUD zeC`8poRwD`B*7LkgcwJGCzDhhP#|R`2jXya8WYD5*sbF1+1)6hy{Fs2WONOJcbcVg@mb$RKivJfgs0i#hg7)Xau0 zYWkYmteWHo%KJ9-MnhfK*{Iz-+5HBjM0Y}gaJSuFKYEK5AR3mui7*gRdSYzK_xFrD l()>nUYgY9%xo_PbdkPcoFB}j5;^Eb=sD9f`S=sPo{ulNDwr2nU diff --git a/main.py b/main.py index dc70c88..3ca79df 100644 --- a/main.py +++ b/main.py @@ -45,7 +45,7 @@ async def dashboard(request: Request): class FactionRequest(BaseModel): faction_id: int - interval: int + interval: int = 30 # ----------------------------- diff --git a/static/dashboard.js b/static/dashboard.js index 322ad32..8fa7347 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -1,30 +1,39 @@ -// dashboard.js -// 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 +// dashboard.js (Corrected Full Version) +// Prevents duplicates on populate +// Preserves group placements +// Updates or creates cards (never recreates all) +// Status refresh works correctly -// ---- In-memory maps ---- -const friendlyMembers = new Map(); // id -> member object +//----------------------------------------------------- +// Maps of members (id -> member object) +//----------------------------------------------------- +const friendlyMembers = new Map(); const enemyMembers = new Map(); -// ---- DOM containers ---- +// DOM containers const friendlyContainer = document.getElementById("friendly-container"); const enemyContainer = document.getElementById("enemy-container"); -// utility to safe parse ints -function toInt(v) { const n = Number(v); return Number.isNaN(n) ? null : n; } +function toInt(v) { + const n = Number(v); + return Number.isNaN(n) ? null : n; +} -// ---- Apply status CSS class to the .status-text inside a member card ---- +//----------------------------------------------------- +// Status CSS assignment +//----------------------------------------------------- function applyStatusClass(member) { const span = member.domElement?.querySelector(".status-text"); if (!span) return; + span.classList.remove("status-ok", "status-traveling", "status-hospitalized"); - const s = String(member.status || "").toLowerCase(); + + const s = (member.status || "").toLowerCase(); if (s === "okay") span.classList.add("status-ok"); else if (s === "traveling" || s === "abroad") span.classList.add("status-traveling"); else if (s === "hospitalized" || s === "hospital") span.classList.add("status-hospitalized"); } -// ---- Decide which CSS class a status should use (helper used when creating card) --- function statusClass(status) { if (!status) return ""; status = status.toLowerCase(); @@ -34,7 +43,9 @@ function statusClass(status) { return ""; } -// ---- Create structured card ---- +//----------------------------------------------------- +// Create a new card DOM element for a member +//----------------------------------------------------- function createMemberCard(member, kind) { const card = document.createElement("div"); card.classList.add("member-card"); @@ -45,129 +56,195 @@ function createMemberCard(member, kind) { 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 + if (kind === "friendly") nameDiv.style.color = "#8fd38f"; + if (kind === "enemy") nameDiv.style.color = "#ff6b6b"; const statsDiv = document.createElement("div"); statsDiv.className = "stats"; - // Ensure initial status text and class (maybe "Unknown" until refresh) - statsDiv.innerHTML = `Lv: ${member.level}
Est: ${member.estimate}
Status: ${member.status || "Unknown"}`; + statsDiv.innerHTML = ` + Lv: ${member.level}
+ Est: ${member.estimate}
+ Status: ${member.status || "Unknown"} + `; card.appendChild(nameDiv); card.appendChild(statsDiv); + // save reference member.domElement = card; - // drag payload is JSON (kind + id) + // dragging card.addEventListener("dragstart", (e) => { - const payload = JSON.stringify({ kind, id: member.id }); - e.dataTransfer.setData("text/plain", payload); + e.dataTransfer.setData("text/plain", JSON.stringify({ + kind, + id: member.id + })); card.style.opacity = "0.5"; }); card.addEventListener("dragend", () => { card.style.opacity = "1"; }); - // Apply initial status class (for cases where member.status was set by populate) - applyStatusClass(member); - return card; } -// ---- Load static members from backend ---- -async function loadMembers(faction) { - const url = faction === "friendly" ? "/api/friendly_members" : "/api/enemy_members"; +//----------------------------------------------------- +// Update an existing card instead of replacing it +//----------------------------------------------------- +function updateMemberCard(member) { + if (!member.domElement) return; + + const span = member.domElement.querySelector(".status-text"); + if (span) { + span.textContent = member.status || "Unknown"; + } + applyStatusClass(member); +} + +//----------------------------------------------------- +// Load members WITHOUT recreating card duplicates +//----------------------------------------------------- +async function loadMembers(kind) { + const url = kind === "friendly" ? "/api/friendly_members" : "/api/enemy_members"; + const container = kind === "friendly" ? friendlyContainer : enemyContainer; + const map = kind === "friendly" ? friendlyMembers : enemyMembers; + try { const res = await fetch(url, { cache: "no-store" }); if (!res.ok) { - console.error("Failed to fetch members:", res.status); + console.error("Error loading members", res.status); return; } - const arr = await res.json(); - const container = faction === "friendly" ? friendlyContainer : enemyContainer; - const map = faction === "friendly" ? friendlyMembers : enemyMembers; - container.innerHTML = ""; - map.clear(); + const list = await res.json(); - arr.forEach(m => { - if (!m.status) m.status = "Unknown"; - const card = createMemberCard(m, faction); - map.set(m.id, m); - container.appendChild(card); - }); + // Keep track of IDs received so we don't remove existing group placements + const receivedIds = new Set(list.map(m => m.id)); + + // Update existing members or create new ones + for (const m of list) { + let existing = map.get(m.id); + + if (existing) { + // update info (but keep DOM placement) + existing.name = m.name; + existing.level = m.level; + existing.estimate = m.estimate; + if (!existing.status) existing.status = "Unknown"; + updateMemberCard(existing); + } else { + // NEW member — create card and add to list area + const newMember = { + id: m.id, + name: m.name, + level: m.level, + estimate: m.estimate, + status: "Unknown", + domElement: null + }; + + const card = createMemberCard(newMember, kind); + map.set(m.id, newMember); + + // Only add to container if they are NOT already assigned to a group + if (!isMemberAssigned(newMember.id)) { + container.appendChild(card); + } + } + } + + // Remove members that no longer exist from map (rare) + for (const existingId of map.keys()) { + if (!receivedIds.has(existingId)) { + const member = map.get(existingId); + if (member?.domElement?.parentElement) { + member.domElement.parentElement.removeChild(member.domElement); + } + map.delete(existingId); + } + } - // after we've added elements, ensure drop listeners are attached setupDropZones(); } catch (err) { console.error("loadMembers error", err); } } -// ---- 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; +//----------------------------------------------------- +// Check if a member is placed in a group zone +//----------------------------------------------------- +function isMemberAssigned(id) { + return document.querySelector(`.member-card[data-id="${id}"]`)?.parentElement?.classList.contains("drop-zone") || false; +} + +//----------------------------------------------------- +// Status refresh +//----------------------------------------------------- +async function refreshStatus(kind) { + const url = kind === "friendly" ? "/api/friendly_status" : "/api/enemy_status"; + const map = kind === "friendly" ? friendlyMembers : enemyMembers; + try { const res = await fetch(url, { cache: "no-store" }); if (!res.ok) return; + const statusData = await res.json(); - Object.keys(statusData).forEach(k => { - const id = parseInt(k); + for (const idStr of Object.keys(statusData)) { + const id = parseInt(idStr); const member = map.get(id); - if (!member) return; - member.status = statusData[k].status; - const span = member.domElement.querySelector(".status-text"); - if (span) { - span.textContent = member.status; - applyStatusClass(member); - } - }); + if (!member) continue; + + member.status = statusData[idStr].status; + updateMemberCard(member); + } } catch (err) { console.error("refreshStatus error", err); } } -// ---- Populate endpoints ---- -// NOW: populate also immediately fetches the latest status snapshot and applies it. +//----------------------------------------------------- +// Populate endpoints (immediate build + status refresh) +//----------------------------------------------------- async function populateFriendly() { const id = toInt(document.getElementById("friendly-id").value); - if (!id) return alert("Enter friendly faction ID"); + if (!id) return alert("Enter Friendly Faction ID"); + + // Send faction_id to FastAPI await fetch("/api/populate_friendly", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ faction_id: id, interval: 0 }) + body: JSON.stringify({ faction_id: id }) }); - // Rebuild list and then immediately fetch status snapshot + // Load members but do NOT recreate duplicates await loadMembers("friendly"); - // try to pull status once so the status text shows up immediately + + // Immediately refresh status so cards show correct status await refreshStatus("friendly"); } async function populateEnemy() { const id = toInt(document.getElementById("enemy-id").value); - if (!id) return alert("Enter enemy faction ID"); + if (!id) return alert("Enter Enemy Faction ID"); + await fetch("/api/populate_enemy", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ faction_id: id, interval: 0 }) + body: JSON.stringify({ faction_id: id }) }); await loadMembers("enemy"); await refreshStatus("enemy"); } -// ---- Start/Stop status refresh loops ---- +//----------------------------------------------------- +// Status refresh toggling +//----------------------------------------------------- let friendlyStatusIntervalHandle = null; let enemyStatusIntervalHandle = null; async function toggleFriendlyStatus() { const btn = document.getElementById("friendly-status-btn"); - if (!btn) { - console.error("friendly-status-btn not found"); - return; - } if (friendlyStatusIntervalHandle) { clearInterval(friendlyStatusIntervalHandle); @@ -178,7 +255,6 @@ async function toggleFriendlyStatus() { 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"); await fetch("/api/start_friendly_status", { method: "POST", @@ -187,17 +263,12 @@ async function toggleFriendlyStatus() { }); friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000); - btn.textContent = "Stop Refresh"; - // do an immediate status fetch as well refreshStatus("friendly"); + btn.textContent = "Stop Refresh"; } async function toggleEnemyStatus() { const btn = document.getElementById("enemy-status-btn"); - if (!btn) { - console.error("enemy-status-btn not found"); - return; - } if (enemyStatusIntervalHandle) { clearInterval(enemyStatusIntervalHandle); @@ -208,7 +279,6 @@ async function toggleEnemyStatus() { 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"); await fetch("/api/start_enemy_status", { method: "POST", @@ -217,39 +287,31 @@ async function toggleEnemyStatus() { }); enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000); - btn.textContent = "Stop Refresh"; refreshStatus("enemy"); + btn.textContent = "Stop Refresh"; } -// ------------------- -// DRAG & DROP SYSTEM -// ------------------- +//----------------------------------------------------- +// Drag & Drop for all zones +//----------------------------------------------------- function setupDropZones() { - const dropZones = document.querySelectorAll(".drop-zone, .member-list"); + const zones = document.querySelectorAll(".drop-zone, .member-list"); - dropZones.forEach(zone => { - // attach listeners only once + zones.forEach(zone => { if (zone.dataset._drag_listeners_attached) return; zone.addEventListener("dragover", (e) => { e.preventDefault(); - // check payload validity - let raw = null; - try { - raw = e.dataTransfer.getData("text/plain"); - } catch (err) { - raw = null; - } + const raw = e.dataTransfer.getData("text/plain"); let valid = false; - if (raw) { - try { - const p = JSON.parse(raw); - const kind = p.kind; - 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 === zoneType) valid = true; - } catch {} - } + try { + const p = JSON.parse(raw); + const zoneType = zone.classList.contains("friendly-zone") ? "friendly" : + zone.classList.contains("enemy-zone") ? "enemy" : + null; + + if (!zoneType || p.kind === zoneType) valid = true; + } catch {} zone.classList.toggle("dragover-valid", valid); zone.classList.toggle("dragover-invalid", !valid); }); @@ -262,32 +324,26 @@ function setupDropZones() { e.preventDefault(); zone.classList.remove("dragover-valid", "dragover-invalid"); - let raw = null; - try { - raw = e.dataTransfer.getData("text/plain"); - } catch (err) { - raw = null; - } - if (!raw) return; let payload; - try { payload = JSON.parse(raw); } catch { return; } - const { kind, id } = payload; - const idn = parseInt(id); - if (!kind || !idn) return; + try { + payload = JSON.parse(e.dataTransfer.getData("text/plain")); + } catch { return; } - 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) return; + const zoneType = zone.classList.contains("friendly-zone") ? "friendly" : + zone.classList.contains("enemy-zone") ? "enemy" : + null; - const map = kind === "friendly" ? friendlyMembers : enemyMembers; - const member = map.get(idn); + if (zoneType && payload.kind !== zoneType) return; + + const map = payload.kind === "friendly" ? friendlyMembers : enemyMembers; + const member = map.get(payload.id); if (!member) return; - const prev = member.domElement?.parentElement; - if (prev && prev !== zone) prev.removeChild(member.domElement); + // Move the card + const prev = member.domElement.parentElement; + if (prev !== zone) prev.removeChild(member.domElement); zone.appendChild(member.domElement); - // persist assignment in localStorage saveAssignments(); }); @@ -295,72 +351,76 @@ function setupDropZones() { }); } -// ------------------- -// localStorage helpers for battle groups (the format used earlier) -// ------------------- +//----------------------------------------------------- +// Save and load group assignments +//----------------------------------------------------- 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))); + 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)); } function loadAssignmentsFromStorage() { 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); + const m = friendlyMembers.get(id); + if (m?.domElement) group.querySelector(".friendly-zone").appendChild(m.domElement); }); + data[gid].enemy.forEach(id => { - const member = enemyMembers.get(id); - if (member) group.querySelector(".enemy-zone").appendChild(member.domElement); + const m = enemyMembers.get(id); + if (m?.domElement) group.querySelector(".enemy-zone").appendChild(m.domElement); }); }); } -// ---- Allow clicking a card to log for debug (optional) ---- -function attachCardClickLogging() { - document.addEventListener("click", (e) => { - const el = e.target.closest(".member-card"); - if (!el) return; - const id = parseInt(el.dataset.id); - const kind = el.dataset.kind; - const map = kind === "friendly" ? friendlyMembers : enemyMembers; - const member = map.get(id); - if (member) console.log("Member clicked:", member); - }); -} - -// ---- Initialize UI wiring ---- +//----------------------------------------------------- +// Wire up buttons +//----------------------------------------------------- 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", toggleFriendlyStatus); document.getElementById("enemy-status-btn").addEventListener("click", toggleEnemyStatus); - document.getElementById("reset-groups-btn").addEventListener("click", () => { - // clear storage and put members back - localStorage.removeItem("battleAssignments"); - // move all back to lists - friendlyMembers.forEach(m => { if (m.domElement) friendlyContainer.appendChild(m.domElement); }); - enemyMembers.forEach(m => { if (m.domElement) enemyContainer.appendChild(m.domElement); }); - }); - setupDropZones(); - attachCardClickLogging(); + document.getElementById("reset-groups-btn").addEventListener("click", () => { + localStorage.removeItem("battleAssignments"); + + friendlyMembers.forEach(m => friendlyContainer.appendChild(m.domElement)); + enemyMembers.forEach(m => enemyContainer.appendChild(m.domElement)); + }); } -// ---- On load: wire and attempt to load any existing JSON lists ---- +//----------------------------------------------------- +// On Load +//----------------------------------------------------- document.addEventListener("DOMContentLoaded", async () => { wireUp(); + await loadMembers("friendly"); await loadMembers("enemy"); - loadAssignmentsFromStorage(); // restore positions after load + + loadAssignmentsFromStorage(); });