From 1754cf80237e284c5a4a0aafa287bff4297ab0d4 Mon Sep 17 00:00:00 2001 From: jerick Date: Mon, 26 Jan 2026 15:20:55 -0500 Subject: [PATCH] Config page for adding tokens and settings --- .gitignore | 3 +- __pycache__/config.cpython-311.pyc | Bin 628 -> 2202 bytes config.py | 40 +++++--- main.py | 105 ++++++++++++++++++++ static/config.js | 108 ++++++++++++++++++++ static/dashboard.js | 20 ++-- static/styles.css | 153 ++++++++++++++++++++++++++++- templates/config.html | 99 +++++++++++++++++++ templates/dashboard.html | 36 ++++--- 9 files changed, 531 insertions(+), 33 deletions(-) create mode 100644 static/config.js create mode 100644 templates/config.html diff --git a/.gitignore b/.gitignore index 4f1cc74..df2328f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.venv temp.md -*data \ No newline at end of file +*data +tokens.md \ No newline at end of file diff --git a/__pycache__/config.cpython-311.pyc b/__pycache__/config.cpython-311.pyc index d8aaf66eb8ab038b55adc0068743211ff78be5b3..79536c3f290bb0f350465cfc33611a068a9a897e 100644 GIT binary patch literal 2202 zcmZ`)OKcNY6n*pYFZRSgOlT-{AXL;;b`vFOJ|rnlJYW<5VmkzzNLKKSlS%yR%z#~! zRIb!TijZg{q(rJJWfiFCf@Qla?7FihFRYPRRb6D~3Mxgc`kozQZ18(C=iPhf%)R&B zxij;aqIv)vxxaF}K>@(uNNEo8Sa|g(2EZ<00gEL;3X5VGlJO)S#aZHe5G8QH5eJa` zw0S3pk{r1Y{k=a}l2shwkPFYoh$_2sm8`0RtLn;CvziXBx+}Mb)pl?-UAa1z>frWt z<@U1r4z9K<*T5P(xVo-f(|dlYu3R&FqQh@5i-&;Ka)`=p#bh$DBCYx2$JMVdN{9qJ zUj^zwuocW7(OB-$`+4NnCbv zce!LHibdQ}zmF zuK`?h>DjrL=DtT0dka5JJ(}8fR?U68<2&PfuKn9(v$bNj))bIbXSU#9bmMCTE!&5r zqobn_nOnA_|CB&yfx5!ff|qKl)I0Lb2mu~t^=L&sT2hbJce1T%PU!mx=B6;gRlEYr z@z~^;27g>~3=+==bWnUgNIFJT&qoL(k6}n2uglXtv!4<#ROVTlc+rm`nN~oQbzy(d z7jp#Mv3VvU8zyde<_DJ~ZfG`l)4#^e$|i@$<6mN&u{oE+=VLrEw=;=e-PeYq={szQ z#(+#+y&9VHFNT?5!^h1?YAN8Kh(cwx}HzB=q% zUmW&@7Ykk|lnai$cfo$mHRin*u-{-Ku91bCses2e5#=WeYgwCL`ZAQ_=kiMn{-wp? zMcWX&AyWar2WI4hp>H@mhi744vdQHR!(^Gc`PPLgbfGuMI6@)!HDAohcpQ<#e4X7e zc^O|g7Iu3XxDDC-jxES|-99H1JhVZ!RWGYh?G!URMF>DR6?u~7=~5BsZ#q< z1=?bL)<_)&d+9Bl1=^3c?d6{SN>6`LRZ{{(-@|+N@9oW$KA$e@XDa%cqONNBc=y)M zt!HQw9 zNVVKP6Uvs!ie(adtwNutcTShi*q(@G<8Z||T;9V*-ZtQH7&X4_WDxaLH PoSZ5FbK?*$!g~AY8z%$<4HS(5#tDmcHWW1ZBbBL$EU%aQwE&fRV&>&<1Ax9q{ z|8Q5AcxMksKR;I=kRo0;w_s=g&=A)kuqnJQo zN1t0l9-bjU-LB5wAVG*2Uy!R~aImMlU%ZQ}k7MNI7i^mBD;Yim9mz1+oW(2m7l%!5 zeoARhs$G#NPz)59#Q{L#12ZEd;{$%h2A&(j5*HW*ZU~7t@ZS*CxWFLvfsH{>xq%l% lMKeNZ;RfCtqT)cg8-gMYd_Wm7pdO(cAW|5DiiClN0ssmXdyfDB diff --git a/config.py b/config.py index 1013ee2..5dbf683 100644 --- a/config.py +++ b/config.py @@ -1,20 +1,38 @@ +from pathlib import Path +import json + +def load_from_json(): + """Load config from JSON file if it exists""" + path = Path("data/config.json") + if not path.exists(): + return {} + + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return data.get("config", {}) + except Exception as e: + print(f"Error loading config from JSON: {e}") + return {} + +# Load from JSON or use defaults +_config = load_from_json() + # Torn API -TORN_API_KEY = "9VLK0Wte1BwXOheB" -ENEMY_FACTION_ID = 55325 -YOUR_FACTION_ID = 52935 -ALLOWED_CHANNEL_ID = 1442876328536707316 +TORN_API_KEY = _config.get("TORN_API_KEY", "YOUR_TORN_API_KEY_HERE") +ALLOWED_CHANNEL_ID = _config.get("ALLOWED_CHANNEL_ID", 0) # FFScouter API -FFSCOUTER_KEY = "XYmWPO9ZYkLqnv3v" +FFSCOUTER_KEY = _config.get("FFSCOUTER_KEY", "YOUR_FFSCOUTER_KEY_HERE") # Discord Bot -DISCORD_TOKEN = "MTQ0Mjg3NjU3NTUzMDg3NzAxMQ.GH7MGP.VdYH4QXmPL-9Zi9zhp-Ot6SmiCxWQOWU3U-1dk" +DISCORD_TOKEN = _config.get("DISCORD_TOKEN", "YOUR_DISCORD_BOT_TOKEN_HERE") # Intervals -POLL_INTERVAL = 30 -HIT_CHECK_INTERVAL = 60 -REASSIGN_DELAY = 120 +POLL_INTERVAL = _config.get("POLL_INTERVAL", 30) +HIT_CHECK_INTERVAL = _config.get("HIT_CHECK_INTERVAL", 60) +REASSIGN_DELAY = _config.get("REASSIGN_DELAY", 120) # Bot Assignment Settings -ASSIGNMENT_TIMEOUT = 60 # Seconds before reassigning a target -ASSIGNMENT_REMINDER = 30 # Seconds before sending reminder message +ASSIGNMENT_TIMEOUT = _config.get("ASSIGNMENT_TIMEOUT", 60) # Seconds before reassigning a target +ASSIGNMENT_REMINDER = _config.get("ASSIGNMENT_REMINDER", 45) # Seconds before sending reminder message diff --git a/main.py b/main.py index 97a25a3..9b52ee7 100644 --- a/main.py +++ b/main.py @@ -39,6 +39,10 @@ async def dashboard(request: Request): print(">>> DASHBOARD ROUTE LOADED") return templates.TemplateResponse("dashboard.html", {"request": request}) +@app.get("/config", response_class=HTMLResponse) +async def config_page(request: Request): + return templates.TemplateResponse("config.html", {"request": request}) + # ============================================================ # Pydantic models for JSON POST input # ============================================================ @@ -61,6 +65,10 @@ class DiscordMappingRequest(BaseModel): torn_id: int discord_id: int +class ConfigUpdateRequest(BaseModel): + key: str + value: Optional[str | int] + # ================================ # Helper: load JSON file into STATE # ================================ @@ -298,6 +306,103 @@ async def remove_discord_mapping(torn_id: int): else: raise HTTPException(status_code=404, detail="Mapping not found") +# ============================= +# Config endpoints +# ============================= +def reload_config_from_file(): + """Reload config values from JSON into module globals""" + path = Path("data/config.json") + if not path.exists(): + return + + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + + # Update config module globals + import config + for key, value in data.get("config", {}).items(): + if hasattr(config, key): + setattr(config, key, value) + except Exception as e: + print(f"Error reloading config from file: {e}") + +@app.get("/api/config") +async def get_config(): + """Get all config values (with sensitive values masked)""" + import config + + path = Path("data/config.json") + if not path.exists(): + # Return defaults from config.py (masked) + config_values = { + "TORN_API_KEY": config.TORN_API_KEY, + "FFSCOUTER_KEY": config.FFSCOUTER_KEY, + "DISCORD_TOKEN": config.DISCORD_TOKEN, + "ALLOWED_CHANNEL_ID": config.ALLOWED_CHANNEL_ID, + "POLL_INTERVAL": config.POLL_INTERVAL, + "HIT_CHECK_INTERVAL": config.HIT_CHECK_INTERVAL, + "REASSIGN_DELAY": config.REASSIGN_DELAY, + "ASSIGNMENT_TIMEOUT": config.ASSIGNMENT_TIMEOUT, + "ASSIGNMENT_REMINDER": config.ASSIGNMENT_REMINDER + } + else: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + config_values = data.get("config", {}) + + # Mask sensitive values + masked_config = config_values.copy() + sensitive = ["TORN_API_KEY", "FFSCOUTER_KEY", "DISCORD_TOKEN"] + for key in sensitive: + if key in masked_config and masked_config[key]: + val = str(masked_config[key]) + masked_config[key] = "****" + val[-4:] if len(val) > 4 else "****" + + return {"config": masked_config, "sensitive_fields": sensitive} + +@app.post("/api/config") +async def update_config(req: ConfigUpdateRequest): + """Update a single config value""" + path = Path("data/config.json") + + # Load existing or create from current config + if path.exists(): + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + else: + import config + data = { + "comment": "Application configuration settings", + "config": { + "TORN_API_KEY": config.TORN_API_KEY, + "FFSCOUTER_KEY": config.FFSCOUTER_KEY, + "DISCORD_TOKEN": config.DISCORD_TOKEN, + "ALLOWED_CHANNEL_ID": config.ALLOWED_CHANNEL_ID, + "POLL_INTERVAL": config.POLL_INTERVAL, + "HIT_CHECK_INTERVAL": config.HIT_CHECK_INTERVAL, + "REASSIGN_DELAY": config.REASSIGN_DELAY, + "ASSIGNMENT_TIMEOUT": config.ASSIGNMENT_TIMEOUT, + "ASSIGNMENT_REMINDER": config.ASSIGNMENT_REMINDER + } + } + + # Validate key exists + if req.key not in data["config"]: + raise HTTPException(status_code=400, detail="Invalid config key") + + # Update value + data["config"][req.key] = req.value + + # Save to file + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + # Reload config in memory + reload_config_from_file() + + return {"status": "ok", "key": req.key} + # ============================================================ # Reset Groups Endpoint diff --git a/static/config.js b/static/config.js new file mode 100644 index 0000000..89180e8 --- /dev/null +++ b/static/config.js @@ -0,0 +1,108 @@ +// Map config keys to input IDs +const CONFIG_FIELDS = { + "TORN_API_KEY": "torn-api-key", + "FFSCOUTER_KEY": "ffscouter-key", + "DISCORD_TOKEN": "discord-token", + "ALLOWED_CHANNEL_ID": "allowed-channel-id", + "POLL_INTERVAL": "poll-interval", + "HIT_CHECK_INTERVAL": "hit-check-interval", + "REASSIGN_DELAY": "reassign-delay", + "ASSIGNMENT_TIMEOUT": "assignment-timeout", + "ASSIGNMENT_REMINDER": "assignment-reminder" +}; + +let sensitiveFields = []; + +async function loadConfig() { + try { + const res = await fetch("/api/config", { cache: "no-store" }); + if (!res.ok) { + console.error("Failed to load config:", res.status); + return; + } + + const data = await res.json(); + sensitiveFields = data.sensitive_fields || []; + + // Populate form fields + for (const [key, inputId] of Object.entries(CONFIG_FIELDS)) { + const input = document.getElementById(inputId); + if (input && data.config[key] !== undefined) { + input.value = data.config[key]; + + // Add placeholder for masked sensitive fields + if (sensitiveFields.includes(key) && String(data.config[key]).startsWith("****")) { + input.placeholder = "Current: " + data.config[key]; + input.value = ""; // Clear the masked value + } + } + } + } catch (err) { + console.error("Error loading config:", err); + } +} + +async function saveConfigValue(key) { + const inputId = CONFIG_FIELDS[key]; + const input = document.getElementById(inputId); + + if (!input) { + console.error("Input not found:", inputId); + return; + } + + let value = input.value.trim(); + + // Don't save if sensitive field is empty (means user didn't change it) + if (sensitiveFields.includes(key) && value === "") { + alert("No changes to save"); + return; + } + + // Convert to number if needed + if (input.type === "number") { + value = parseInt(value); + if (isNaN(value)) { + alert("Invalid number"); + return; + } + } + + try { + const res = await fetch("/api/config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key, value }) + }); + + if (!res.ok) { + console.error("Save failed:", res.status); + alert("Failed to save config"); + return; + } + + const data = await res.json(); + console.log("Config saved:", data); + + // Reload to show masked value + await loadConfig(); + alert("Saved successfully"); + } catch (err) { + console.error("Error saving config:", err); + alert("Error saving config"); + } +} + +function wireUp() { + // Attach save handlers to all save buttons + const saveButtons = document.querySelectorAll(".config-save-btn"); + saveButtons.forEach(btn => { + const key = btn.dataset.key; + btn.addEventListener("click", () => saveConfigValue(key)); + }); +} + +document.addEventListener("DOMContentLoaded", async () => { + wireUp(); + await loadConfig(); +}); diff --git a/static/dashboard.js b/static/dashboard.js index c76c0cf..bf08a01 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -617,12 +617,14 @@ async function toggleFriendlyStatus() { if (friendlyStatusIntervalHandle) { clearInterval(friendlyStatusIntervalHandle); friendlyStatusIntervalHandle = null; - btn.textContent = "Start Refresh"; + btn.textContent = "Start"; + btn.dataset.running = "false"; + btn.style.backgroundColor = ""; return; } const id = toInt(document.getElementById("friendly-id").value); - const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10); + const interval = Math.max(1, toInt(document.getElementById("friendly-refresh-interval").value) || 10); await fetch("/api/start_friendly_status", { method: "POST", @@ -632,7 +634,9 @@ async function toggleFriendlyStatus() { friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000); refreshStatus("friendly"); - btn.textContent = "Stop Refresh"; + btn.textContent = "Stop"; + btn.dataset.running = "true"; + btn.style.backgroundColor = "#ff6b6b"; } async function toggleEnemyStatus() { @@ -640,12 +644,14 @@ async function toggleEnemyStatus() { if (enemyStatusIntervalHandle) { clearInterval(enemyStatusIntervalHandle); enemyStatusIntervalHandle = null; - btn.textContent = "Start Refresh"; + btn.textContent = "Start"; + btn.dataset.running = "false"; + btn.style.backgroundColor = ""; return; } const id = toInt(document.getElementById("enemy-id").value); - const interval = Math.max(1, toInt(document.getElementById("refresh-interval").value) || 10); + const interval = Math.max(1, toInt(document.getElementById("enemy-refresh-interval").value) || 10); await fetch("/api/start_enemy_status", { method: "POST", @@ -655,7 +661,9 @@ async function toggleEnemyStatus() { enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000); refreshStatus("enemy"); - btn.textContent = "Stop Refresh"; + btn.textContent = "Stop"; + btn.dataset.running = "true"; + btn.style.backgroundColor = "#ff6b6b"; } // --------------------------- diff --git a/static/styles.css b/static/styles.css index 33bf8a9..1c2f208 100644 --- a/static/styles.css +++ b/static/styles.css @@ -133,9 +133,71 @@ body { } .faction-card.small h2 { color: #66ccff; margin: 0; } -.faction-card .controls { display:flex; gap: 0.5rem; align-items:center; margin-bottom: 6px; } + +/* Controls row - wraps both populate and status controls */ +.faction-card .controls-row { + display: flex; + gap: 0.3rem; + align-items: center; + margin-bottom: 6px; + flex-wrap: nowrap; +} + +.faction-card .controls { + display: flex; + gap: 0.5rem; + align-items: center; +} .faction-card .controls input { padding: 0.5rem; border-radius:6px; border: none; } +/* Status controls section */ +.faction-card .status-controls { + display: flex; + gap: 0.3rem; + align-items: center; + background-color: #3a3a4d; + padding: 0.3rem 0.5rem; + border-radius: 6px; +} +.faction-card .status-controls label { + color: #ffcc66; + font-size: 0.75rem; + white-space: nowrap; +} +.faction-card .status-controls input { + width: 38px; + padding: 2px 3px; + border-radius: 4px; + border: none; + text-align: center; + font-size: 0.85rem; + appearance: textfield; + -moz-appearance: textfield; +} + +/* Hide spinner buttons in Chrome, Safari, Edge */ +.faction-card .status-controls input::-webkit-outer-spin-button, +.faction-card .status-controls input::-webkit-inner-spin-button { + -webkit-appearance: none; + appearance: none; + margin: 0; +} + +/* Status button */ +.status-btn { + padding: 0.5rem 1rem; + min-width: 60px; + background-color: #4CAF50; + color: white; + transition: background-color 0.3s; +} +.status-btn:hover { + opacity: 0.9; +} +.status-btn[data-running="true"] { + background-color: #ff6b6b; +} + /* member list in left column */ .member-list { max-height: 380px; @@ -237,3 +299,92 @@ button:hover { background-color: #3399ff; } /* 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; } + +/* ============================= + Config Page Styles + ============================= */ + +/* Navigation link */ +.nav-link { + color: #66ccff; + text-decoration: none; + font-weight: bold; + padding: 0.6rem 1rem; + border-radius: 8px; + transition: background-color 0.3s; +} + +.nav-link:hover { + background-color: rgba(102, 204, 255, 0.1); +} + +/* Config page layout */ +.config-container { + display: flex; + flex-direction: column; + gap: 1.5rem; + max-width: 800px; + margin: 0 auto; +} + +.config-section { + width: 100%; +} + +.config-group { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.config-group:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.config-group label { + color: #66ccff; + font-weight: bold; + font-size: 1rem; +} + +.config-description { + color: #99a7bf; + font-size: 0.85rem; + margin: 0; + font-style: italic; +} + +.config-input { + padding: 0.6rem; + border-radius: 6px; + border: 1px solid rgba(255, 255, 255, 0.1); + background-color: #1a1a26; + color: #f0f0f0; + font-size: 0.95rem; +} + +.config-input:focus { + outline: none; + border-color: #66ccff; +} + +.config-save-btn { + align-self: flex-start; + padding: 0.5rem 1.5rem; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 6px; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s; +} + +.config-save-btn:hover { + background-color: #45a049; +} diff --git a/templates/config.html b/templates/config.html new file mode 100644 index 0000000..bfb0434 --- /dev/null +++ b/templates/config.html @@ -0,0 +1,99 @@ + + + + + Configuration - War Dashboard + + + +
+ +
+

Configuration

+ +
+ + +
+ +
+

API Keys & Tokens

+
+ +

Your Torn API key for fetching faction member data

+ + +
+ +
+ +

FFScouter API key for enhanced stat estimates

+ + +
+ +
+ +

Discord bot token for sending DM notifications

+ + +
+
+ + +
+

Discord Settings

+
+ +

Discord channel ID where bot commands are allowed

+ + +
+
+ + +
+

Timing Settings

+
+ +

Seconds before reassigning a target to another player

+ + +
+ +
+ +

Seconds before sending a reminder message

+ + +
+ +
+ +

General polling interval for data refresh

+ + +
+ +
+ +

Interval for checking hit completion

+ + +
+ +
+ +

Delay before reassigning failed targets

+ + +
+
+
+
+ + + + diff --git a/templates/dashboard.html b/templates/dashboard.html index a2212bd..6cb65db 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -8,18 +8,14 @@
- +

War Dashboard

+ Settings - -
- - -
@@ -31,10 +27,16 @@

Friendly Faction

-
- - - +
+
+ + +
+
+ + + +

Enemy Faction

-
- - - +
+
+ + +
+
+ + + +