Stylized member cards with static fields
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
from discord.ext import commands
|
||||
from services.torn_api import update_enemy_faction, update_friendly_faction
|
||||
#from services.torn_api import update_enemy_faction, update_friendly_faction
|
||||
|
||||
|
||||
class HitCommands(commands.Cog):
|
||||
|
||||
117
main.py
117
main.py
@@ -6,6 +6,8 @@ from cogs.commands import HitCommands
|
||||
|
||||
import asyncio
|
||||
import uvicorn
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
@@ -13,47 +15,97 @@ from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services.torn_api import update_enemy_faction, update_friendly_faction
|
||||
from services.torn_api import populate_friendly, populate_enemy, start_friendly_status_loop, start_enemy_status_loop
|
||||
|
||||
|
||||
|
||||
# ============================================================
|
||||
# FastAPI Setup
|
||||
# ============================================================
|
||||
|
||||
# -----------------------------
|
||||
# FastAPI setup
|
||||
# -----------------------------
|
||||
app = FastAPI()
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
# -----------------------------
|
||||
# Dashboard page
|
||||
# -----------------------------
|
||||
|
||||
# ============================================================
|
||||
# Dashboard Webpage
|
||||
# ============================================================
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def dashboard(request: Request):
|
||||
print(">>> DASHBOARD ROUTE LOADED")
|
||||
return templates.TemplateResponse("dashboard.html", {"request": request})
|
||||
|
||||
# -----------------------------
|
||||
# Pydantic model for JSON payloads
|
||||
# -----------------------------
|
||||
|
||||
# ============================================================
|
||||
# Pydantic model for JSON POST input
|
||||
# ============================================================
|
||||
|
||||
class FactionRequest(BaseModel):
|
||||
faction_id: int
|
||||
interval: int
|
||||
|
||||
# -----------------------------
|
||||
# API Endpoints
|
||||
# -----------------------------
|
||||
@app.post("/api/update_enemy_faction")
|
||||
async def api_enemy(data: FactionRequest):
|
||||
await update_enemy_faction(data.faction_id, data.interval)
|
||||
return {"status": "enemy loop running", "id": data.faction_id, "interval": data.interval}
|
||||
|
||||
@app.post("/api/update_friendly_faction")
|
||||
async def api_friendly(data: FactionRequest):
|
||||
await update_friendly_faction(data.faction_id, data.interval)
|
||||
return {"status": "friendly loop running", "id": data.faction_id, "interval": data.interval}
|
||||
|
||||
# -----------------------------
|
||||
# Discord bot setup
|
||||
# Populate endpoints (populate JSON once)
|
||||
# -----------------------------
|
||||
@app.post("/api/populate_friendly")
|
||||
async def api_populate_friendly(data: FactionRequest):
|
||||
from services.torn_api import populate_friendly
|
||||
await populate_friendly(data.faction_id)
|
||||
return {"status": "friendly populated", "id": data.faction_id}
|
||||
|
||||
@app.post("/api/populate_enemy")
|
||||
async def api_populate_enemy(data: FactionRequest):
|
||||
from services.torn_api import populate_enemy
|
||||
await populate_enemy(data.faction_id)
|
||||
return {"status": "enemy populated", "id": data.faction_id}
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Start status refresh loops
|
||||
# -----------------------------
|
||||
@app.post("/api/start_friendly_status")
|
||||
async def api_start_friendly_status(data: FactionRequest):
|
||||
from services.torn_api import start_friendly_status_loop
|
||||
await start_friendly_status_loop(data.faction_id, data.interval)
|
||||
return {"status": "friendly status loop started", "id": data.faction_id, "interval": data.interval}
|
||||
|
||||
@app.post("/api/start_enemy_status")
|
||||
async def api_start_enemy_status(data: FactionRequest):
|
||||
from services.torn_api import start_enemy_status_loop
|
||||
await start_enemy_status_loop(data.faction_id, data.interval)
|
||||
return {"status": "enemy status loop started", "id": data.faction_id, "interval": data.interval}
|
||||
|
||||
|
||||
# =============================
|
||||
# Member JSON endpoints
|
||||
# =============================
|
||||
@app.get("/api/friendly_members")
|
||||
async def get_friendly_members():
|
||||
path = Path("data/friendly_faction.json")
|
||||
if not path.exists():
|
||||
return []
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
@app.get("/api/enemy_members")
|
||||
async def get_enemy_members():
|
||||
path = Path("data/enemy_faction.json")
|
||||
if not path.exists():
|
||||
return []
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Discord Bot Setup
|
||||
# ============================================================
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
|
||||
@@ -62,9 +114,9 @@ enemy_queue = []
|
||||
active_assignments = {}
|
||||
round_robin_index = 0
|
||||
|
||||
|
||||
class HitDispatchBot(commands.Bot):
|
||||
async def setup_hook(self):
|
||||
# Load cogs with injected state
|
||||
await self.add_cog(
|
||||
Assignments(
|
||||
self,
|
||||
@@ -72,9 +124,10 @@ class HitDispatchBot(commands.Bot):
|
||||
active_assignments=active_assignments,
|
||||
enrolled_attackers=enrolled_attackers,
|
||||
hit_check=HIT_CHECK_INTERVAL,
|
||||
reassign_delay=REASSIGN_DELAY
|
||||
reassign_delay=REASSIGN_DELAY,
|
||||
)
|
||||
)
|
||||
|
||||
await self.add_cog(
|
||||
HitCommands(
|
||||
self,
|
||||
@@ -86,25 +139,31 @@ class HitDispatchBot(commands.Bot):
|
||||
async def cog_check(self, ctx):
|
||||
return ctx.channel.id == ALLOWED_CHANNEL_ID
|
||||
|
||||
|
||||
bot = HitDispatchBot(command_prefix="!", intents=intents)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
print(f"Logged in as {bot.user.name}")
|
||||
|
||||
|
||||
TOKEN = "YOUR_DISCORD_TOKEN"
|
||||
|
||||
|
||||
async def start_bot():
|
||||
await bot.start(TOKEN)
|
||||
|
||||
# -----------------------------
|
||||
# Main entry
|
||||
# -----------------------------
|
||||
|
||||
# ============================================================
|
||||
# Main Entry Point
|
||||
# ============================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Start Discord bot in background
|
||||
loop.create_task(start_bot())
|
||||
|
||||
# Run FastAPI (this will keep the loop alive)
|
||||
# Run FastAPI app — keeps loop alive
|
||||
uvicorn.run(app, host="127.0.0.1", port=8000)
|
||||
|
||||
Binary file not shown.
@@ -3,31 +3,34 @@ import aiohttp
|
||||
import json
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from config import TORN_API_KEY, ENEMY_FACTION_ID, YOUR_FACTION_ID
|
||||
from config import TORN_API_KEY
|
||||
from .ffscouter import fetch_batch_stats
|
||||
|
||||
ENEMY_FILE = Path("data/enemy_faction.json")
|
||||
FRIENDLY_FILE = Path("data/friendly_faction.json")
|
||||
FRIENDLY_MEMBERS_FILE = Path("data/friendly_members.json")
|
||||
FRIENDLY_STATUS_FILE = Path("data/friendly_status.json")
|
||||
ENEMY_MEMBERS_FILE = Path("data/enemy_members.json")
|
||||
ENEMY_STATUS_FILE = Path("data/enemy_status.json")
|
||||
|
||||
# Track running tasks + current faction IDs
|
||||
enemy_task = None
|
||||
friendly_task = None
|
||||
# Tasks
|
||||
friendly_status_task = None
|
||||
enemy_status_task = None
|
||||
|
||||
current_enemy_id = None
|
||||
current_friendly_id = None
|
||||
# Locks
|
||||
friendly_lock = asyncio.Lock()
|
||||
enemy_lock = asyncio.Lock()
|
||||
|
||||
|
||||
async def fetch_and_save_faction(faction_id: int, file_path: Path) -> bool:
|
||||
"""
|
||||
Fetches faction members from Torn, fetches their estimated BS from FFScouter,
|
||||
and saves everything to a JSON file.
|
||||
"""
|
||||
# -----------------------------
|
||||
# Static population (once)
|
||||
# -----------------------------
|
||||
async def populate_faction(faction_id: int, members_file: Path, status_file: Path):
|
||||
"""Fetch members + FFScouter estimates once and save static info + initial status."""
|
||||
url = f"https://api.torn.com/v2/faction/{faction_id}?selections=members&key={TORN_API_KEY}"
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
print(f"Torn faction fetch error: {resp.status}")
|
||||
print(f"Error fetching faction {faction_id}: {resp.status}")
|
||||
return False
|
||||
data = await resp.json()
|
||||
|
||||
@@ -35,92 +38,95 @@ async def fetch_and_save_faction(faction_id: int, file_path: Path) -> bool:
|
||||
if not members_list:
|
||||
return False
|
||||
|
||||
# Build list of IDs (Torn uses 'id', not 'player_id')
|
||||
member_ids = [info.get("id") for info in members_list if "id" in info]
|
||||
member_ids = [m.get("id") for m in members_list if "id" in m]
|
||||
if not member_ids:
|
||||
return False
|
||||
|
||||
# Fetch batch FFScouter stats
|
||||
ff_data = await fetch_batch_stats(member_ids) # returns dict keyed by player_id
|
||||
|
||||
# Build final faction data
|
||||
faction_data = []
|
||||
for info in members_list:
|
||||
pid = info.get("id")
|
||||
if pid is None:
|
||||
continue
|
||||
# Fetch FFScouter data once
|
||||
ff_data = await fetch_batch_stats(member_ids)
|
||||
|
||||
# Build static member list
|
||||
members = []
|
||||
status_data = {}
|
||||
for m in members_list:
|
||||
pid = m["id"]
|
||||
est = ff_data.get(str(pid), {}).get("bs_estimate_human", "?")
|
||||
member = {
|
||||
"id": pid,
|
||||
"name": info.get("name", "Unknown"),
|
||||
"level": info.get("level", 0),
|
||||
"status": info.get("status", {}).get("state", "Unknown"),
|
||||
"estimate": est
|
||||
"name": m.get("name", "Unknown"),
|
||||
"level": m.get("level", 0),
|
||||
"estimate": est,
|
||||
}
|
||||
faction_data.append(member)
|
||||
members.append(member)
|
||||
# initial status
|
||||
status_data[pid] = {"status": m.get("status", {}).get("state", "Unknown")}
|
||||
|
||||
# Save to file
|
||||
file_path.parent.mkdir(exist_ok=True, parents=True) # ensure folder exists
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(faction_data, f, indent=2)
|
||||
# Save members
|
||||
members_file.parent.mkdir(exist_ok=True, parents=True)
|
||||
with open(members_file, "w", encoding="utf-8") as f:
|
||||
json.dump(members, f, indent=2)
|
||||
|
||||
# Save initial status
|
||||
with open(status_file, "w", encoding="utf-8") as f:
|
||||
json.dump(status_data, f, indent=2)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
#Loop for the constant update of members and the stop function
|
||||
|
||||
async def stop_task_if_running(task: asyncio.Task | None):
|
||||
"""Cancel an existing running task safely."""
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def faction_loop(faction_id: int, file_path: Path, interval: int):
|
||||
"""
|
||||
Runs fetch_and_save_faction() in a loop forever, waiting `interval`
|
||||
seconds between iterations.
|
||||
"""
|
||||
# -----------------------------
|
||||
# Status refresh loop
|
||||
# -----------------------------
|
||||
async def refresh_status_loop(faction_id: int, status_file: Path, lock: asyncio.Lock, interval: int):
|
||||
"""Refresh only status from Torn API periodically."""
|
||||
while True:
|
||||
try:
|
||||
await fetch_and_save_faction(faction_id, file_path)
|
||||
url = f"https://api.torn.com/v2/faction/{faction_id}?selections=members&key={TORN_API_KEY}"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
print(f"Status fetch error {resp.status}")
|
||||
await asyncio.sleep(interval)
|
||||
continue
|
||||
data = await resp.json()
|
||||
|
||||
members_list = data.get("members", [])
|
||||
status_data = {m["id"]: {"status": m.get("status", {}).get("state", "Unknown")} for m in members_list}
|
||||
|
||||
# Save status safely
|
||||
async with lock:
|
||||
with open(status_file, "w", encoding="utf-8") as f:
|
||||
json.dump(status_data, f, indent=2)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during faction loop for {faction_id}: {e}")
|
||||
print(f"Error in status loop for {faction_id}: {e}")
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Helper functions for endpoints
|
||||
# -----------------------------
|
||||
async def populate_friendly(faction_id: int):
|
||||
return await populate_faction(faction_id, FRIENDLY_MEMBERS_FILE, FRIENDLY_STATUS_FILE)
|
||||
|
||||
#Functions to call the loop, maybe add one to just call once?
|
||||
async def update_enemy_faction(new_faction_id: int, interval: int):
|
||||
global enemy_task, current_enemy_id
|
||||
|
||||
# If faction ID changes → stop old loop
|
||||
if new_faction_id != current_enemy_id:
|
||||
print(f"[ENEMY] Changing faction from {current_enemy_id} → {new_faction_id}")
|
||||
await stop_task_if_running(enemy_task)
|
||||
current_enemy_id = new_faction_id
|
||||
async def populate_enemy(faction_id: int):
|
||||
return await populate_faction(faction_id, ENEMY_MEMBERS_FILE, ENEMY_STATUS_FILE)
|
||||
|
||||
# Start new loop
|
||||
enemy_task = asyncio.create_task(
|
||||
faction_loop(new_faction_id, ENEMY_FILE, interval)
|
||||
|
||||
async def start_friendly_status_loop(faction_id: int, interval: int):
|
||||
global friendly_status_task
|
||||
if friendly_status_task and not friendly_status_task.done():
|
||||
friendly_status_task.cancel()
|
||||
friendly_status_task = asyncio.create_task(
|
||||
refresh_status_loop(faction_id, FRIENDLY_STATUS_FILE, friendly_lock, interval)
|
||||
)
|
||||
|
||||
|
||||
async def update_friendly_faction(new_faction_id: int, interval: int):
|
||||
global friendly_task, current_friendly_id
|
||||
|
||||
if new_faction_id != current_friendly_id:
|
||||
print(f"[FRIENDLY] Changing faction from {current_friendly_id} → {new_faction_id}")
|
||||
await stop_task_if_running(friendly_task)
|
||||
current_friendly_id = new_faction_id
|
||||
|
||||
friendly_task = asyncio.create_task(
|
||||
faction_loop(new_faction_id, FRIENDLY_FILE, interval)
|
||||
async def start_enemy_status_loop(faction_id: int, interval: int):
|
||||
global enemy_status_task
|
||||
if enemy_status_task and not enemy_status_task.done():
|
||||
enemy_status_task.cancel()
|
||||
enemy_status_task = asyncio.create_task(
|
||||
refresh_status_loop(faction_id, ENEMY_STATUS_FILE, enemy_lock, interval)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,36 +1,158 @@
|
||||
async function updateEnemy() {
|
||||
const factionId = parseInt(document.getElementById("enemyId").value);
|
||||
const interval = parseInt(document.getElementById("refreshInterval").value);
|
||||
// dashboard.js
|
||||
|
||||
if (!factionId || !interval) {
|
||||
alert("Please enter Enemy Faction ID and Refresh Interval!");
|
||||
return;
|
||||
}
|
||||
const friendlyMembers = new Map();
|
||||
const enemyMembers = new Map();
|
||||
|
||||
await fetch(`/api/update_enemy_faction`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ faction_id: factionId, interval: interval })
|
||||
const friendlyContainer = document.getElementById("friendly-container");
|
||||
const enemyContainer = document.getElementById("enemy-container");
|
||||
|
||||
function createMemberCard(member) {
|
||||
const card = document.createElement("div");
|
||||
card.classList.add("member-card");
|
||||
card.dataset.id = member.id;
|
||||
|
||||
// Clear innerHTML, create structured divs
|
||||
const nameDiv = document.createElement("div");
|
||||
nameDiv.classList.add("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>
|
||||
`;
|
||||
|
||||
card.appendChild(nameDiv);
|
||||
card.appendChild(statsDiv);
|
||||
|
||||
// Store reference to DOM element
|
||||
member.domElement = card;
|
||||
|
||||
// Make card draggable
|
||||
card.draggable = true;
|
||||
card.addEventListener("dragstart", e => {
|
||||
e.dataTransfer.setData("text/plain", member.id);
|
||||
});
|
||||
|
||||
// Set initial status color
|
||||
updateStatusColor(member);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
async function updateFriendly() {
|
||||
const factionId = parseInt(document.getElementById("friendlyId").value);
|
||||
function updateStatusColor(member) {
|
||||
const statusSpan = member.domElement.querySelector(".status-text");
|
||||
statusSpan.classList.remove("status-ok", "status-traveling", "status-hospitalized");
|
||||
|
||||
if (!factionId) {
|
||||
alert("Please enter Friendly Faction ID!");
|
||||
return;
|
||||
}
|
||||
if (!member.status) return;
|
||||
|
||||
const interval = parseInt(document.getElementById("refreshInterval").value);
|
||||
if (!interval) {
|
||||
alert("Please enter Refresh Interval!");
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch(`/api/update_friendly_faction`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ faction_id: factionId, interval: interval })
|
||||
});
|
||||
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");
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
Object.keys(statusData).forEach(id => {
|
||||
const member = map.get(parseInt(id));
|
||||
if (!member) return;
|
||||
member.status = statusData[id].status;
|
||||
|
||||
// Update DOM
|
||||
const statusSpan = member.domElement.querySelector(".status-text");
|
||||
statusSpan.textContent = member.status;
|
||||
|
||||
// Apply correct color class
|
||||
updateStatusColor(member);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to refresh status:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function populateFriendly() {
|
||||
const id = parseInt(document.getElementById("friendly-id").value);
|
||||
if (!id) return alert("Enter a valid faction ID!");
|
||||
|
||||
await fetch("/api/populate_friendly", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ faction_id: id, interval: 0 })
|
||||
});
|
||||
|
||||
loadMembers("friendly");
|
||||
}
|
||||
|
||||
async function populateEnemy() {
|
||||
const id = parseInt(document.getElementById("enemy-id").value);
|
||||
if (!id) return alert("Enter a valid faction ID!");
|
||||
|
||||
await fetch("/api/populate_enemy", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ faction_id: id, interval: 0 })
|
||||
});
|
||||
|
||||
loadMembers("enemy");
|
||||
}
|
||||
|
||||
async function startFriendlyStatus() {
|
||||
const id = parseInt(document.getElementById("friendly-id").value);
|
||||
const interval = parseInt(document.getElementById("refresh-interval").value) || 10;
|
||||
|
||||
await fetch("/api/start_friendly_status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ faction_id: id, interval })
|
||||
});
|
||||
|
||||
setInterval(() => refreshStatus("friendly"), interval * 1000);
|
||||
}
|
||||
|
||||
async function startEnemyStatus() {
|
||||
const id = parseInt(document.getElementById("enemy-id").value);
|
||||
const interval = parseInt(document.getElementById("refresh-interval").value) || 10;
|
||||
|
||||
await fetch("/api/start_enemy_status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ faction_id: id, interval })
|
||||
});
|
||||
|
||||
setInterval(() => refreshStatus("enemy"), interval * 1000);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -13,7 +13,6 @@ body {
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
/* Top Bar with Title + Interval Box */
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -41,7 +40,6 @@ body {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Horizontal Faction Row */
|
||||
.faction-row {
|
||||
display: flex;
|
||||
flex-direction: row !important;
|
||||
@@ -49,7 +47,6 @@ body {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* Each Faction Card */
|
||||
.faction-card {
|
||||
flex: 1;
|
||||
background-color: #2c2c3e;
|
||||
@@ -87,3 +84,89 @@ button {
|
||||
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; }
|
||||
|
||||
@@ -6,38 +6,38 @@
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- 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>
|
||||
|
||||
<div class="top-bar">
|
||||
<h1>War Dashboard</h1>
|
||||
|
||||
<div class="interval-box">
|
||||
<label for="refreshInterval">Refresh Interval (sec)</label>
|
||||
<input type="number" id="refreshInterval" placeholder="e.g., 30">
|
||||
<!-- 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>
|
||||
|
||||
<div class="faction-row">
|
||||
|
||||
<!-- Enemy -->
|
||||
<div class="faction-card">
|
||||
<h2>Enemy Faction</h2>
|
||||
<input type="number" id="enemyId" placeholder="Enemy Faction ID">
|
||||
<button onclick="updateEnemy()">Start Enemy Refresh</button>
|
||||
</div>
|
||||
|
||||
<!-- Friendly -->
|
||||
<div class="faction-card">
|
||||
<h2>Friendly Faction</h2>
|
||||
<input type="number" id="friendlyId" placeholder="Friendly Faction ID">
|
||||
<button onclick="updateFriendly()">Start Friendly Refresh</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/dashboard.js"></script>
|
||||
<script src="/static/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user