diff --git a/README.md b/README.md
index 87f5a3d..1b48dd1 100644
--- a/README.md
+++ b/README.md
@@ -10,8 +10,5 @@ Features:
ToDo:
-- move interval button to a neutral spot
-- sections to list faction memebers
- - needs to be movable objects
- Assignment pools depending on stats
- - players will be round-robin queued their targets from here
\ No newline at end of file
+ - players will be round-robin queued their targets from here
diff --git a/static/dashboard.js b/static/dashboard.js
index b1e5011..3fa7858 100644
--- a/static/dashboard.js
+++ b/static/dashboard.js
@@ -1,134 +1,156 @@
// 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.
-const friendlyMembers = new Map();
+// ---- In-memory maps ----
+const friendlyMembers = new Map(); // id -> member object
const enemyMembers = new Map();
+// ---- DOM containers ----
const friendlyContainer = document.getElementById("friendly-container");
const enemyContainer = document.getElementById("enemy-container");
-function createMemberCard(member) {
+// utility to safe parse ints
+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;
- // Clear innerHTML, create structured divs
+ // structured children
const nameDiv = document.createElement("div");
- nameDiv.classList.add("name");
+ nameDiv.className = "name";
nameDiv.textContent = member.name;
const statsDiv = document.createElement("div");
- statsDiv.classList.add("stats");
- statsDiv.innerHTML = `
- Level: ${member.level}
- Estimate: ${member.estimate}
- Status: ${member.status || "Unknown"}
- `;
+ statsDiv.className = "stats";
+ statsDiv.innerHTML = `Lv: ${member.level}
Est: ${member.estimate}
Status: ${member.status || "Unknown"}`;
card.appendChild(nameDiv);
card.appendChild(statsDiv);
- // Store reference to DOM element
+ // store back-reference
member.domElement = card;
- // Make card draggable
- card.draggable = true;
- card.addEventListener("dragstart", e => {
- e.dataTransfer.setData("text/plain", member.id);
+ // 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";
});
- // Set initial status color
- updateStatusColor(member);
+ // initial color
+ applyStatusClass(member);
return card;
}
-function updateStatusColor(member) {
- const statusSpan = member.domElement.querySelector(".status-text");
- statusSpan.classList.remove("status-ok", "status-traveling", "status-hospitalized");
-
- if (!member.status) return;
-
- const s = member.status.toLowerCase();
- if (s === "okay") statusSpan.classList.add("status-ok");
- else if (s === "traveling" || s === "abroad") statusSpan.classList.add("status-traveling");
- else if (s === "hospitalized") statusSpan.classList.add("status-hospitalized");
+// ---- Apply status CSS class to the .status-text inside a member card ----
+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();
+ 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");
}
+// ---- Load static members from backend into the list containers ----
async function loadMembers(faction) {
const url = faction === "friendly" ? "/api/friendly_members" : "/api/enemy_members";
- const response = await fetch(url);
- const members = await response.json();
-
- const container = faction === "friendly" ? friendlyContainer : enemyContainer;
- const map = faction === "friendly" ? friendlyMembers : enemyMembers;
-
- container.innerHTML = "";
- map.clear();
-
- members.forEach(m => {
- if (!m.status) m.status = "Unknown";
- const card = createMemberCard(m);
- map.set(m.id, m);
- container.appendChild(card);
- });
-
- refreshStatus(faction);
-}
-
-async function refreshStatus(faction) {
- const url = faction === "friendly" ? "/api/friendly_status" : "/api/enemy_status";
- const map = faction === "friendly" ? friendlyMembers : enemyMembers;
-
try {
- const response = await fetch(url);
- const statusData = await response.json();
+ const res = await fetch(url, { cache: "no-store" });
+ if (!res.ok) {
+ console.error("Failed to fetch members:", res.status);
+ return;
+ }
+ const arr = await res.json();
+ const container = faction === "friendly" ? friendlyContainer : enemyContainer;
+ const map = faction === "friendly" ? friendlyMembers : enemyMembers;
- Object.keys(statusData).forEach(id => {
- const member = map.get(parseInt(id));
- if (!member) return;
- member.status = statusData[id].status;
+ // clear
+ container.innerHTML = "";
+ map.clear();
- // Update DOM
- const statusSpan = member.domElement.querySelector(".status-text");
- statusSpan.textContent = member.status;
-
- // Apply correct color class
- updateStatusColor(member);
+ arr.forEach(m => {
+ if (!m.status) m.status = "Unknown";
+ const card = createMemberCard(m, faction);
+ map.set(m.id, m);
+ container.appendChild(card);
});
} catch (err) {
- console.error("Failed to refresh status:", err);
+ console.error("loadMembers error", err);
}
}
-async function populateFriendly() {
- const id = parseInt(document.getElementById("friendly-id").value);
- if (!id) return alert("Enter a valid faction ID!");
+// ---- Refresh status map and update DOM (does not touch static info or position) ----
+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: "..."}, ... }
+ Object.keys(statusData).forEach(k => {
+ const id = parseInt(k);
+ const member = map.get(id);
+ if (!member) return; // not loaded yet or moved elsewhere
+ member.status = statusData[k].status;
+ const span = member.domElement.querySelector(".status-text");
+ span.textContent = member.status;
+ applyStatusClass(member);
+ });
+ } catch (err) {
+ console.error("refreshStatus error", err);
+ }
+}
+// ---- Populate endpoints: call backend to fetch static info (FFScouter) once ----
+async function populateFriendly() {
+ const id = toInt(document.getElementById("friendly-id").value);
+ if (!id) return alert("Enter friendly faction ID");
await fetch("/api/populate_friendly", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ faction_id: id, interval: 0 })
});
-
- loadMembers("friendly");
+ await loadMembers("friendly");
}
async function populateEnemy() {
- const id = parseInt(document.getElementById("enemy-id").value);
- if (!id) return alert("Enter a valid faction ID!");
-
+ const id = toInt(document.getElementById("enemy-id").value);
+ 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 })
});
-
- loadMembers("enemy");
+ await loadMembers("enemy");
}
+// ---- Start status refresh loops on backend and JS-side polling ----
+let friendlyStatusIntervalHandle = null;
+let enemyStatusIntervalHandle = null;
+
async function startFriendlyStatus() {
- const id = parseInt(document.getElementById("friendly-id").value);
- const interval = parseInt(document.getElementById("refresh-interval").value) || 10;
+ 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",
@@ -136,12 +158,17 @@ async function startFriendlyStatus() {
body: JSON.stringify({ faction_id: id, interval })
});
- setInterval(() => refreshStatus("friendly"), interval * 1000);
+ // clear existing
+ if (friendlyStatusIntervalHandle) clearInterval(friendlyStatusIntervalHandle);
+ friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000);
+ // immediate
+ refreshStatus("friendly");
}
async function startEnemyStatus() {
- const id = parseInt(document.getElementById("enemy-id").value);
- const interval = parseInt(document.getElementById("refresh-interval").value) || 10;
+ 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",
@@ -149,10 +176,100 @@ async function startEnemyStatus() {
body: JSON.stringify({ faction_id: id, interval })
});
- setInterval(() => refreshStatus("enemy"), interval * 1000);
+ if (enemyStatusIntervalHandle) clearInterval(enemyStatusIntervalHandle);
+ enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000);
+ refreshStatus("enemy");
}
-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);
+// ---- Drag & drop handling for drop-zones ----
+function setupDropZones() {
+ const dropZones = document.querySelectorAll(".drop-zone, .member-list");
+
+ 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) {
+ 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 {}
+ }
+ zone.classList.toggle("dragover-valid", valid);
+ zone.classList.toggle("dragover-invalid", !valid);
+ });
+
+ zone.addEventListener("dragleave", () => {
+ zone.classList.remove("dragover-valid", "dragover-invalid");
+ });
+
+ zone.addEventListener("drop", (e) => {
+ e.preventDefault();
+ zone.classList.remove("dragover-valid", "dragover-invalid");
+ const raw = e.dataTransfer.getData("text/plain");
+ if (!raw) return;
+ let payload;
+ try { payload = JSON.parse(raw); } catch { return; }
+ const { kind, id } = payload;
+ 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;
+ }
+
+ // 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);
+ });
+ });
+}
+
+// ---- 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 ----
+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);
+
+ setupDropZones();
+ attachCardClickLogging();
+}
+
+// ---- 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");
+});
diff --git a/static/styles.css b/static/styles.css
index 6c299d7..6237fc8 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -1,3 +1,4 @@
+/* --- base --- */
body {
font-family: Arial, sans-serif;
background-color: #1e1e2f;
@@ -8,165 +9,195 @@ body {
}
.container {
- width: 95%;
- max-width: 1300px;
- margin: 2rem auto;
+ width: 96%;
+ max-width: 1400px;
+ margin: 1.5rem auto;
}
+/* top bar */
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 2rem;
+ margin-bottom: 1rem;
}
.interval-box {
background-color: #3a3a4d;
- padding: 0.75rem 1rem;
- border-radius: 10px;
+ padding: 0.5rem 0.8rem;
+ border-radius: 8px;
display: flex;
- flex-direction: column;
- gap: 0.4rem;
+ gap: 0.5rem;
+ align-items: center;
}
+.interval-box label { color: #ffcc66; font-size: 0.9rem; margin-right: 8px; }
+.interval-box input { width: 80px; padding: 6px; border-radius: 6px; border: none; }
-.interval-box label {
- font-size: 0.9rem;
- color: #ffcc66;
-}
-
-.interval-box input {
- padding: 0.5rem;
- border-radius: 6px;
- border: none;
-}
-
-.faction-row {
+/* main split */
+.main-row {
display: flex;
- flex-direction: row !important;
- justify-content: space-between;
- gap: 2rem;
+ gap: 1.5rem;
}
-.faction-card {
- flex: 1;
- background-color: #2c2c3e;
- padding: 1.5rem;
- border-radius: 12px;
- box-shadow: 0 0 20px rgba(0,0,0,0.5);
+/* left column: stacked friendly / enemy */
+.left-col {
+ width: 48%;
display: flex;
flex-direction: column;
gap: 1rem;
- width: 100%;
- max-width: 600px;
}
-.faction-card h2 {
- color: #66ccff;
+/* right column: groups grid */
+.right-col {
+ width: 52%;
}
-input[type="number"] {
+.groups-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+/* group card */
+.group {
+ background: #232331;
+ border-radius: 10px;
+ padding: 0.6rem;
+ min-height: 140px;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ border: 1px solid rgba(255,255,255,0.03);
+}
+
+.group-title {
+ font-weight: bold;
+ color: #ffcc66;
+ margin-bottom: 4px;
+}
+
+/* zones layout inside group */
+.group-zones {
+ display: flex;
+ gap: 0.5rem;
+ height: 100%;
+}
+
+/* drop zones */
+.drop-zone {
+ flex: 1;
+ background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02));
+ border-radius: 8px;
+ border: 2px dashed rgba(255,255,255,0.03);
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ align-items: stretch;
+ overflow-y: auto;
+ min-width: 0;
+}
+
+/* subtle label */
+.zone-label {
+ font-size: 0.9rem;
+ color: #99a7bf;
+ margin-bottom: 4px;
+}
+
+/* highlight when a valid draggable is over */
+.drop-zone.dragover-valid {
+ background: rgba(51,153,255,0.06);
+ border-color: rgba(102,204,255,0.4);
+}
+
+/* invalid dragover */
+.drop-zone.dragover-invalid {
+ background: rgba(255,77,77,0.06);
+ border-color: rgba(255,77,77,0.4);
+}
+
+/* friendly/enemy specific coloring for zone headers */
+.friendly-zone .zone-label { color: #8fd38f; }
+.enemy-zone .zone-label { color: #ff9b9b; }
+
+/* Faction card containers on left */
+.faction-card.small {
+ background-color: #2c2c3e;
+ padding: 1rem;
+ border-radius: 10px;
+ box-shadow: 0 0 18px rgba(0,0,0,0.45);
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.faction-card.small h2 { color: #66ccff; margin: 0; }
+.faction-card .controls { display:flex; gap: 0.5rem; align-items:center; margin-bottom: 6px; }
+.faction-card .controls input { padding: 0.5rem; border-radius:6px; border: none; }
+
+/* member list in left column */
+.member-list {
+ max-height: 380px;
+ overflow-y: auto;
+ background: #1a1a26;
+ padding: 0.6rem;
+ border-radius: 8px;
+ border: 1px solid rgba(255,255,255,0.02);
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+/* member card (used both in lists and zones) */
+.member-card {
+ background-color: #3a3a4d;
+ color: #f0f0f0;
padding: 0.7rem;
- border-radius: 6px;
- border: none;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 0.8rem;
+ box-shadow: 0 0 10px rgba(0,0,0,0.45);
+ cursor: grab;
}
+/* name and stat blocks */
+.member-card .name { min-width: 110px; color: #66ccff; font-weight: bold; }
+.member-card .stats { color: #f0f0f0; font-size: 0.9rem; line-height: 1.2; }
+
+/* Friendly name color */
+.member-card.friendly .name {
+ color: #4cff4c; /* Green */
+}
+
+/* Enemy name color */
+.member-card.enemy .name {
+ color: #ff4c4c; /* Red */
+}
+
+
+/* small status span; color is applied by classes */
+.status-text { font-weight: 700; padding-left: 6px; }
+
+/* status color classes */
+.status-ok { color: #28a745; text-shadow: 0 0 2px rgba(40,167,69,0.25); }
+.status-traveling { color: #3399ff; text-shadow: 0 0 2px rgba(51,153,255,0.25); }
+.status-hospitalized { color: #ff4d4d; text-shadow: 0 0 2px rgba(255,77,77,0.25); }
+
+/* buttons */
button {
- padding: 0.7rem 1rem;
+ padding: 0.5rem 0.7rem;
border-radius: 6px;
border: none;
background-color: #66ccff;
color: #1e1e2f;
font-weight: bold;
cursor: pointer;
- transition: background-color 0.2s;
}
+button:hover { background-color: #3399ff; }
-button:hover {
- background-color: #3399ff;
-}
-
-.member-list {
- margin-top: 1rem;
- max-height: 350px;
- overflow-y: auto;
- background: #1a1a26;
- padding: 0.8rem;
- border-radius: 10px;
-}
-
-.member-card {
- background-color: #3a3a4d;
- padding: 1rem;
- margin: 0.5rem 0;
- border-radius: 10px;
- display: flex;
- flex-direction: row; /* horizontal layout */
- align-items: center;
- justify-content: flex-start;
- gap: 2rem;
- box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
- cursor: grab;
- min-width: 200px;
-}
-
-.member-card:active {
- cursor: grabbing;
-}
-
-/* Name section */
-.member-card .name {
- font-weight: bold;
- color: #66ccff;
- min-width: 120px; /* ensures spacing */
-}
-
-/* Stats section */
-.member-card .stats {
- font-size: 0.9rem;
- line-height: 1.3;
- color: #f0f0f0;
-}
-
-.member-card strong {
- font-size: 1rem;
- min-width: 100px;
-}
-
-.member-card span {
- font-size: 0.9rem;
-}
-
-.member-card:hover {
- background-color: #4a4a60;
-}
-
-#friendly-container,
-#enemy-container {
- max-height: 400px;
- overflow-y: auto;
- padding: 0.5rem;
- border: 1px solid #444;
- border-radius: 10px;
- background-color: #2c2c3e;
-}
-
-#friendly-container::-webkit-scrollbar,
-#enemy-container::-webkit-scrollbar {
- width: 8px;
-}
-
-#friendly-container::-webkit-scrollbar-thumb,
-#enemy-container::-webkit-scrollbar-thumb {
- background-color: #66ccff;
- border-radius: 4px;
-}
-
-#friendly-container::-webkit-scrollbar-track,
-#enemy-container::-webkit-scrollbar-track {
- background-color: #2c2c3e;
- border-radius: 4px;
-}
-
-.status-ok { color: #28a745; font-weight: bold; }
-.status-traveling { color: #3399ff; font-weight: bold; }
-.status-hospitalized { color: #ff4d4d; font-weight: bold; }
+/* scrollbar niceties for drop zones and lists */
+.member-list::-webkit-scrollbar, .drop-zone::-webkit-scrollbar { width: 8px; height: 8px; }
+.member-list::-webkit-scrollbar-thumb, .drop-zone::-webkit-scrollbar-thumb { background: #66ccff; border-radius: 4px; }
diff --git a/templates/dashboard.html b/templates/dashboard.html
index b3009b1..ef372f3 100644
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -1,42 +1,111 @@