diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc
index 5062ef8..4080aaf 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 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();
});