From 7cf882959b79973496bb75d6ddb42f2e5cddfaee Mon Sep 17 00:00:00 2001 From: jerick Date: Thu, 27 Nov 2025 02:44:52 -0500 Subject: [PATCH] Stylized member cards with static fields --- __pycache__/main.cpython-311.pyc | Bin 5360 -> 7895 bytes cogs/__pycache__/commands.cpython-311.pyc | Bin 3483 -> 3402 bytes cogs/commands.py | 2 +- main.py | 117 +++++++++--- services/__pycache__/torn_api.cpython-311.pyc | Bin 6636 -> 9191 bytes services/torn_api.py | 158 ++++++++-------- static/dashboard.js | 178 +++++++++++++++--- static/styles.css | 89 ++++++++- templates/dashboard.html | 56 +++--- 9 files changed, 435 insertions(+), 165 deletions(-) diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc index 21493d8879a03a696f13dd5a4944844c1e2f7466..93572d5218941e5a527f1a0ed911afcc38805c74 100644 GIT binary patch literal 7895 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 delta 2201 zcmb_dOKclO7@pZ(+ld`JapG6}NSwEwG>@iDA$=*OR0V~$QdqgKPDg(J8?>%2cdR@MkdVA5C_ZEDruh6VEV?+>fM`9LA41})y54;8{{*y26;NTFSAFGSTSMiQdFRaoRpv69u)7|4R8Y0bNe zK@NRe9;dOD1VUVv{09;7fmIu6uD1e7%c=~1(6<^!;Mc*i)OG<8KlQJ*u1cVXzST$@ zZO%$0u&mVO(J7&`hy<4r3DNN0m}(MYc8Qpyh=lj%Ng^}}@!DUC*KPYuA`;!#C-#z0 z4@L96cBU(RVnu#UZdvRnaoV35ur+f?O+ioRkUB_^I%H*@fXq9nG=IRhs@uabAUI4q zY5!VsFCyv)9ho1sy-3$GszYWhPP)~Dq-PnaKjz62j5Hu~DjuOl12J`c;oDm%6-#cdbW64U8*?mGg;>@bjY zgXz);$;=k#n9CNj3BeuV=K@_g#*2Z$77@Js4Unx9c+MXO-oXJb1y!8jGr@N|#5HB2 zu+kDc3WF)n8t<3I%}TV-|_|?!cr&aOe{6Xm9p>`0O^39@QDwQ&2EFdiRdYcVJJ;SI*?y$3SbDH9wBeD<>FH0X0y3GXqtqk)z%oaP|AZ+pJQI3Xeiww znhwUy2UG5E44kv>k0=sT)=DMj7o4L|tmuX*&rySkm&1B0Or=yT8`d4N>cn)tsT))` z%BG9ZtX9bz>@-hCgPxsoU=w^Q8t<}D-@=C81_uj1HZ9@`5Lh?A8$Hmw7wo?pju(og zlBXxxEwB;Sxg3!BkI~!wdhBLox{CJsYHt1_b{e~RDt;XMIE|+leMoYA68SJvb@y%H z{!QFplO%i{-yf+VG47_n%Sam7v8Xldc&0XRdK0IGr-tue7e1QhX;a}jxMNZCAy4aP zqtD!3Ke_qOi8NODKZ&6?#*izxq_3P_4_BR&8_vm1=j4+794pK1H3{RP8Ui8$d2E3b WH71rEH3j3CP;QeZ&N?(jRPMjVI`ZoP diff --git a/cogs/__pycache__/commands.cpython-311.pyc b/cogs/__pycache__/commands.cpython-311.pyc index 256618d3ebaaf79b5f2cd2cf50d63010b79c039e..f09a5395eec2c659096f9af969b5976f6f7040e5 100644 GIT binary patch delta 522 zcmY+AF-QY36o!+#Ywy}?P_Zhl7K?(V3c6M)E`p+i4h~|MQZ7*|y{qS7&nQ2_gfxjg&9F=}SETmM?E%(R zQREsR&`3)m32W}M5*vZWo@_W3Pc7SwXY~YEZO(b&a*5^~mSsg|?q|1hhLg9799-%$hkNh= z0Z+BdhQVyR%jj9V!=zCSOyJr@B;1zp*#BP5;DhLOs87v?zzN>qslOkWC9q>i}OH?z`^ow2P3hCKXSodE2l!on{rMV<|6K+nwVH(EM3^YiZM>SO$WQ_3}iogCJEzPZxLHCsSU5AV-VDkgKhjZot delta 581 zcmZut&r2IY6rRa$b~o8fLzJqCKL{0+Am~{rmC}Pkp-4R$gk{+oWhI-Dx4S{{R{9Uf z929yK6zQpW@KB1UBJ|`@=07OC^sE=>B_cWanE80`&HKLh=KX1Wn`GyVm5H$vF(1n# zRu9%pa7;Miv_lfwrWE}`r;u1}E3w-)B?Yodxb>BAThQGilF`mALcW_ju(rdC8>Hp@ z#jr)S7s(_EyF67}-b7uG=X*TNM6eC9=7K+DRr zr>80=qpc|0*KVE)2x6XMyJoWqOue_SY4ws8KhvceGDi^l8ly;aKqRs!;5DYl3=C1f zI6rAst(R_8(c5f+jU`CBumD@axB=~^c@&8>9pasMSEl89X^z$bx6miSjPZtwPRqZH zc<3)Izrqq6Si4WVfI6n85Sj>Y3|tVXqR$ZAKB(Wmj{8ZBKeb%#^Eizp@I|qo4J2!3 zA-&iPxR^=k2H{>j_y^9Vtq*c%pf!(z&-(Y?+eGrbBi_M+d7$Q|QvItm8h7O5?Mvss Rqn6m1T49cwWwSOaJpxOXfqwu1 diff --git a/cogs/commands.py b/cogs/commands.py index 7946083..1430427 100644 --- a/cogs/commands.py +++ b/cogs/commands.py @@ -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): diff --git a/main.py b/main.py index 32e34f9..4de8cdf 100644 --- a/main.py +++ b/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) diff --git a/services/__pycache__/torn_api.cpython-311.pyc b/services/__pycache__/torn_api.cpython-311.pyc index 37a6746c50c9f179cdc761251539fa6154033327..ea1cae790d371e33b43ca353f3f96c5aaec5fd59 100644 GIT binary patch literal 9191 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 N8~AKMk*`x6`ai!LFd_f| literal 6636 zcmd@YTWl29_0G=h?(F-$ewHr9U~jUqfzm<@1dJcWKum%S#EzZKvNN{V-ksgfj4?Rt zb}Q1zi7bU!k?b@?-H(JiB5M874@*^BNTe$Hn?;LQjcN%sid6cw2qGcAdhX1=F$q=s z*E@I4eayLM&OP_sbI!fLvsjD>(x1O>jr_@m(AT(8Ou9nl{=cBIj2OfaQ8c2K#0a6Z zH6t1*HBmA~jZg&sMn<(U-H0xxAJNC?5gPg^MjJK6j3Y(@X%I^>x~l|2*Wgvth)L$aImkdd#L~9K(N0@whf;=eX644E}k5WcZ{=Qcrth{grOjYL}9Lu35lT&mSbZV zgX5vF7>RT3Q$n1ZYg9gr^AVO~qSfDIV*xW5VPwO2B+3R8z?iJ#S#gHv!kVH0prqlw z1n>PT6hg-lfftTQq7Pw_#0s%65YFeSe9}rE&K7<%;B@S>{Ly>^ZKM-{Vsw5D5hl1cC3;FJ%Q#8n9|r^ z=8Z|?6F4zc>%+V$X-W}`jY)cQe9=+-H$q8M_1H+woHnPx7#UN|+|5bT6ZtbUmTK%L zVp>SHOn~%qQLvfx9_!Kh=DB>g=;eD(iMq(k6Wqd~l zRjj_g!Ek&=Wch$XF~l)GA#@%Fu;*F+qBt4hCVXPt7xEn&JUQUQ2DDv4obyeJVnXQb z=m;eu?IQfEJsgj9oZsD1V5VcP{*b^%S;bBZ2Met2m}V~?9Ju=>2xzVmb6DbCVbsU+ ze4Ov}^~w6efN;s#!4NIHE_m-B2C$4KP!f$b&4W8l66et$bV(u&{f{RGRcpX|n#E?2 zA|Y!c3_x;%75y4n)4o%tBHVafz~<5CdonV*3PaCF!mKcgr4z(rN?d$oJP?ftB8V#S!rU{ej{}(p zvCK6*+RM1Dz>(mYqCBRiDPY&E!|P@;o=}G2;kg#Y}VAjX%l>e6J-j!dmj5~nG{4`)`b!Y z@cFV%{j02vO*0W*rsB{mQ_M^(;kWVFDDZf<%j66nmGy#3U8ZRQm9=+C_O4uA zb6#IX=pLr~;5&&|{Fam#NTe$((l)0HO4)l!oedh&+>rn>&e;N z3p05gavmqZ%B+q8$T*H=9mi6~awbn1_|^#+`F>wU6Qpnr+x-B++Lt-Z_Xo& zwnYI8RL<&rck-Rd%i`PIBDX;1OxA_{IkWqY)${K3JJTO+TM2*AcBL(SBx7yOT3c^h zpS@{)Rtg-t?#ftuv)0}P@{ZG!1MZJ59bGw-9?rNsvhI$Yr(yY(rB_xa(m&65{8^7b z=WSe`T$)_DuYMT=ozs{{02Xw4D>8U)(@i(& zrj@=7y)8>`lavgT&|0&U`!>~hlWI)&thGo~V}{zBrS?kH-VK9gA+q91&q{`ljG-fI z=)m%MZf(~FZC*574y1|Itr8u`(19!+fJ&Z(uL_C+?qTEy7?k@qt0~L436>6-?F)N^ zL9iX4Hg`9ZH?%EBp4Z;+JD~o#r4j0%w>okCd1E(e_<|&%{)LXfb-hw=Hg@mOf8i$p ze))s?euDgpp!&7?uV@0GOo<_3y2|}xV|@wU`yT^P4@gPG_a{*r+K;E|1A(v?@G6~o zX+Sj|KB^?)Hw;lcK0t^BXUlC!#Q!g(sZzkzf_OMR4=XscNIfuMeC0fR-mJM-dMtw) zKE(RiM?eKUh0VJdTREDsZqHh`OY|l=ZviF+90mQ_+l;OlyR_t0PZuI@AT3m7E62_j zqhpB5s%~vc3j22$T{KD})QhQbU-Hw+8ykkG#K1Q^SRo%*#Y&|Wbw!GB*8$t%T4_sw z!lAPICs7EA6(`ZDlHymM3zJB+mg&KUf=yUHMOM=fkqm$nt&(h1`>@4Q_01IKSqSzO8>VQHY%E;W z^`GedRKxefC`AJchogesn@K=4%ohGo&_w=az-8bSeg&YQgfybPw;KF6^?ph3-=Hm$ z^~|k~LjWOP=hQMopUu)|CHm}!^+%s{W~}X5Yr8~ml5}ycf9m|J9hSulAkl?;YqlG5jQ@;jxbbeQ_1_rF4;6 zr81UMcS&pb0T`)ji>3INU>_(9{}Ozt(v-S_rj&>jmE{b^#P2Tw6qH4$C`;g`J|O7> zin5H}+TANzMKn>xYh_|L9PGt-6STvs^ zg(+pbJ)#f9;>=8xeSv=sK4J4Ci~$5oOAvREHKjZ`WJ)PduG~{Ep{kriN2Dj`9BPxQ z^Bs$0VMua5o3RA4mO#pwv%4=-l4n=OzB_B*ow7ifY;`K#Da##KorLTVflJ7h*X< 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) ) - diff --git a/static/dashboard.js b/static/dashboard.js index 48fb613..b1e5011 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -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}
+ Estimate: ${member.estimate}
+ Status: ${member.status || "Unknown"} + `; + + 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); diff --git a/static/styles.css b/static/styles.css index abd8908..6c299d7 100644 --- a/static/styles.css +++ b/static/styles.css @@ -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; } diff --git a/templates/dashboard.html b/templates/dashboard.html index 78f0f70..b3009b1 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -6,38 +6,38 @@ +
+ +
+

War Dashboard

+
+ + +
+
-
+ +
+ +
+

Friendly Faction

+ + + +
+
-
-

War Dashboard

- -
- - + +
+

Enemy Faction

+ + + +
+
-
- - -
-

Enemy Faction

- - -
- - -
-

Friendly Faction

- - -
- -
- -
- - +