Working member cards and assignment groups, some formatting

This commit is contained in:
2025-11-27 23:20:08 -05:00
parent 7cf882959b
commit 441ae31eb6
4 changed files with 451 additions and 237 deletions

View File

@@ -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
- players will be round-robin queued their targets from here

View File

@@ -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}<br>
Estimate: ${member.estimate}<br>
Status: <span class="status-text">${member.status || "Unknown"}</span>
`;
statsDiv.className = "stats";
statsDiv.innerHTML = `Lv: ${member.level} <br> Est: ${member.estimate} <br> Status: <span class="status-text">${member.status || "Unknown"}</span>`;
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");
});

View File

@@ -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; }

View File

@@ -1,42 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8" />
<title>War Dashboard</title>
<link rel="stylesheet" href="/static/styles.css">
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<div class="container">
<!-- Top bar: Title + Refresh Interval -->
<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">
<input type="number" id="refresh-interval" value="10" min="1" />
</div>
</div>
<!-- Faction row -->
<div class="faction-row">
<!-- Friendly Faction -->
<div class="faction-card">
<h2>Friendly Faction</h2>
<input type="number" id="friendly-id" placeholder="Faction ID">
<button id="friendly-populate-btn">Populate Friendly</button>
<button id="friendly-status-btn">Start Refresh</button>
<div id="friendly-container" class="members-container"></div>
<div class="main-row">
<!-- LEFT: faction lists stacked vertically -->
<div class="left-col">
<div class="faction-card small">
<h2>Friendly Faction</h2>
<div class="controls">
<input type="number" id="friendly-id" placeholder="Faction ID" />
<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>
<div class="faction-card small">
<h2>Enemy Faction</h2>
<div class="controls">
<input type="number" id="enemy-id" placeholder="Faction ID" />
<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>
</div>
<!-- Enemy Faction -->
<div class="faction-card">
<h2>Enemy Faction</h2>
<input type="number" id="enemy-id" placeholder="Faction ID">
<button id="enemy-populate-btn">Populate Enemy</button>
<button id="enemy-status-btn">Start Refresh</button>
<div id="enemy-container" class="members-container"></div>
</div>
</div>
</div>
<!-- RIGHT: 5 groups, each with friendly + enemy drop zones -->
<div class="right-col">
<div class="groups-grid">
<!-- Generate 5 groups -->
<div class="group" id="group-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">
<div class="zone-label">Friendly</div>
</div>
<div class="drop-zone enemy-zone" data-zone="enemy" data-group="1" id="group-1-enemy">
<div class="zone-label">Enemy</div>
</div>
</div>
</div>
<div class="group" id="group-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">
<div class="zone-label">Friendly</div>
</div>
<div class="drop-zone enemy-zone" data-zone="enemy" data-group="2" id="group-2-enemy">
<div class="zone-label">Enemy</div>
</div>
</div>
</div>
<div class="group" id="group-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">
<div class="zone-label">Friendly</div>
</div>
<div class="drop-zone enemy-zone" data-zone="enemy" data-group="3" id="group-3-enemy">
<div class="zone-label">Enemy</div>
</div>
</div>
</div>
<div class="group" id="group-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">
<div class="zone-label">Friendly</div>
</div>
<div class="drop-zone enemy-zone" data-zone="enemy" data-group="4" id="group-4-enemy">
<div class="zone-label">Enemy</div>
</div>
</div>
</div>
<div class="group" id="group-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">
<div class="zone-label">Friendly</div>
</div>
<div class="drop-zone enemy-zone" data-zone="enemy" data-group="5" id="group-5-enemy">
<div class="zone-label">Enemy</div>
</div>
</div>
</div>
</div> <!-- groups-grid -->
</div> <!-- right-col -->
</div> <!-- main-row -->
</div> <!-- container -->
<script src="/static/dashboard.js"></script>
</body>