Config page for adding tokens and settings
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
*.venv
|
*.venv
|
||||||
temp.md
|
temp.md
|
||||||
*data
|
*data
|
||||||
|
tokens.md
|
||||||
Binary file not shown.
40
config.py
40
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
|
||||||
TORN_API_KEY = "9VLK0Wte1BwXOheB"
|
TORN_API_KEY = _config.get("TORN_API_KEY", "YOUR_TORN_API_KEY_HERE")
|
||||||
ENEMY_FACTION_ID = 55325
|
ALLOWED_CHANNEL_ID = _config.get("ALLOWED_CHANNEL_ID", 0)
|
||||||
YOUR_FACTION_ID = 52935
|
|
||||||
ALLOWED_CHANNEL_ID = 1442876328536707316
|
|
||||||
|
|
||||||
# FFScouter API
|
# FFScouter API
|
||||||
FFSCOUTER_KEY = "XYmWPO9ZYkLqnv3v"
|
FFSCOUTER_KEY = _config.get("FFSCOUTER_KEY", "YOUR_FFSCOUTER_KEY_HERE")
|
||||||
|
|
||||||
# Discord Bot
|
# Discord Bot
|
||||||
DISCORD_TOKEN = "MTQ0Mjg3NjU3NTUzMDg3NzAxMQ.GH7MGP.VdYH4QXmPL-9Zi9zhp-Ot6SmiCxWQOWU3U-1dk"
|
DISCORD_TOKEN = _config.get("DISCORD_TOKEN", "YOUR_DISCORD_BOT_TOKEN_HERE")
|
||||||
|
|
||||||
# Intervals
|
# Intervals
|
||||||
POLL_INTERVAL = 30
|
POLL_INTERVAL = _config.get("POLL_INTERVAL", 30)
|
||||||
HIT_CHECK_INTERVAL = 60
|
HIT_CHECK_INTERVAL = _config.get("HIT_CHECK_INTERVAL", 60)
|
||||||
REASSIGN_DELAY = 120
|
REASSIGN_DELAY = _config.get("REASSIGN_DELAY", 120)
|
||||||
|
|
||||||
# Bot Assignment Settings
|
# Bot Assignment Settings
|
||||||
ASSIGNMENT_TIMEOUT = 60 # Seconds before reassigning a target
|
ASSIGNMENT_TIMEOUT = _config.get("ASSIGNMENT_TIMEOUT", 60) # Seconds before reassigning a target
|
||||||
ASSIGNMENT_REMINDER = 30 # Seconds before sending reminder message
|
ASSIGNMENT_REMINDER = _config.get("ASSIGNMENT_REMINDER", 45) # Seconds before sending reminder message
|
||||||
|
|||||||
105
main.py
105
main.py
@@ -39,6 +39,10 @@ async def dashboard(request: Request):
|
|||||||
print(">>> DASHBOARD ROUTE LOADED")
|
print(">>> DASHBOARD ROUTE LOADED")
|
||||||
return templates.TemplateResponse("dashboard.html", {"request": request})
|
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
|
# Pydantic models for JSON POST input
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -61,6 +65,10 @@ class DiscordMappingRequest(BaseModel):
|
|||||||
torn_id: int
|
torn_id: int
|
||||||
discord_id: int
|
discord_id: int
|
||||||
|
|
||||||
|
class ConfigUpdateRequest(BaseModel):
|
||||||
|
key: str
|
||||||
|
value: Optional[str | int]
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# Helper: load JSON file into STATE
|
# Helper: load JSON file into STATE
|
||||||
# ================================
|
# ================================
|
||||||
@@ -298,6 +306,103 @@ async def remove_discord_mapping(torn_id: int):
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=404, detail="Mapping not found")
|
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
|
# Reset Groups Endpoint
|
||||||
|
|||||||
108
static/config.js
Normal file
108
static/config.js
Normal file
@@ -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();
|
||||||
|
});
|
||||||
@@ -617,12 +617,14 @@ async function toggleFriendlyStatus() {
|
|||||||
if (friendlyStatusIntervalHandle) {
|
if (friendlyStatusIntervalHandle) {
|
||||||
clearInterval(friendlyStatusIntervalHandle);
|
clearInterval(friendlyStatusIntervalHandle);
|
||||||
friendlyStatusIntervalHandle = null;
|
friendlyStatusIntervalHandle = null;
|
||||||
btn.textContent = "Start Refresh";
|
btn.textContent = "Start";
|
||||||
|
btn.dataset.running = "false";
|
||||||
|
btn.style.backgroundColor = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = toInt(document.getElementById("friendly-id").value);
|
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", {
|
await fetch("/api/start_friendly_status", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -632,7 +634,9 @@ async function toggleFriendlyStatus() {
|
|||||||
|
|
||||||
friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000);
|
friendlyStatusIntervalHandle = setInterval(() => refreshStatus("friendly"), interval * 1000);
|
||||||
refreshStatus("friendly");
|
refreshStatus("friendly");
|
||||||
btn.textContent = "Stop Refresh";
|
btn.textContent = "Stop";
|
||||||
|
btn.dataset.running = "true";
|
||||||
|
btn.style.backgroundColor = "#ff6b6b";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleEnemyStatus() {
|
async function toggleEnemyStatus() {
|
||||||
@@ -640,12 +644,14 @@ async function toggleEnemyStatus() {
|
|||||||
if (enemyStatusIntervalHandle) {
|
if (enemyStatusIntervalHandle) {
|
||||||
clearInterval(enemyStatusIntervalHandle);
|
clearInterval(enemyStatusIntervalHandle);
|
||||||
enemyStatusIntervalHandle = null;
|
enemyStatusIntervalHandle = null;
|
||||||
btn.textContent = "Start Refresh";
|
btn.textContent = "Start";
|
||||||
|
btn.dataset.running = "false";
|
||||||
|
btn.style.backgroundColor = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = toInt(document.getElementById("enemy-id").value);
|
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", {
|
await fetch("/api/start_enemy_status", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -655,7 +661,9 @@ async function toggleEnemyStatus() {
|
|||||||
|
|
||||||
enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000);
|
enemyStatusIntervalHandle = setInterval(() => refreshStatus("enemy"), interval * 1000);
|
||||||
refreshStatus("enemy");
|
refreshStatus("enemy");
|
||||||
btn.textContent = "Stop Refresh";
|
btn.textContent = "Stop";
|
||||||
|
btn.dataset.running = "true";
|
||||||
|
btn.style.backgroundColor = "#ff6b6b";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|||||||
@@ -133,9 +133,71 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.faction-card.small h2 { color: #66ccff; margin: 0; }
|
.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; }
|
.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 in left column */
|
||||||
.member-list {
|
.member-list {
|
||||||
max-height: 380px;
|
max-height: 380px;
|
||||||
@@ -237,3 +299,92 @@ button:hover { background-color: #3399ff; }
|
|||||||
/* scrollbar niceties for drop zones and lists */
|
/* scrollbar niceties for drop zones and lists */
|
||||||
.member-list::-webkit-scrollbar, .drop-zone::-webkit-scrollbar { width: 8px; height: 8px; }
|
.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; }
|
.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;
|
||||||
|
}
|
||||||
|
|||||||
99
templates/config.html
Normal file
99
templates/config.html
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Configuration - War Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Navigation Bar -->
|
||||||
|
<div class="top-bar">
|
||||||
|
<h1>Configuration</h1>
|
||||||
|
<div class="top-controls">
|
||||||
|
<a href="/" class="nav-link">Back to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Config Sections -->
|
||||||
|
<div class="config-container">
|
||||||
|
<!-- API Keys Section -->
|
||||||
|
<div class="faction-card small config-section">
|
||||||
|
<h2>API Keys & Tokens</h2>
|
||||||
|
<div class="config-group">
|
||||||
|
<label for="torn-api-key">Torn API Key</label>
|
||||||
|
<p class="config-description">Your Torn API key for fetching faction member data</p>
|
||||||
|
<input type="password" id="torn-api-key" class="config-input" />
|
||||||
|
<button class="config-save-btn" data-key="TORN_API_KEY">Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label for="ffscouter-key">FFScouter API Key</label>
|
||||||
|
<p class="config-description">FFScouter API key for enhanced stat estimates</p>
|
||||||
|
<input type="password" id="ffscouter-key" class="config-input" />
|
||||||
|
<button class="config-save-btn" data-key="FFSCOUTER_KEY">Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label for="discord-token">Discord Bot Token</label>
|
||||||
|
<p class="config-description">Discord bot token for sending DM notifications</p>
|
||||||
|
<input type="password" id="discord-token" class="config-input" />
|
||||||
|
<button class="config-save-btn" data-key="DISCORD_TOKEN">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Discord Settings Section -->
|
||||||
|
<div class="faction-card small config-section">
|
||||||
|
<h2>Discord Settings</h2>
|
||||||
|
<div class="config-group">
|
||||||
|
<label for="allowed-channel-id">Allowed Channel ID</label>
|
||||||
|
<p class="config-description">Discord channel ID where bot commands are allowed</p>
|
||||||
|
<input type="number" id="allowed-channel-id" class="config-input" />
|
||||||
|
<button class="config-save-btn" data-key="ALLOWED_CHANNEL_ID">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timing Settings Section -->
|
||||||
|
<div class="faction-card small config-section">
|
||||||
|
<h2>Timing Settings</h2>
|
||||||
|
<div class="config-group">
|
||||||
|
<label for="assignment-timeout">Assignment Timeout (seconds)</label>
|
||||||
|
<p class="config-description">Seconds before reassigning a target to another player</p>
|
||||||
|
<input type="number" id="assignment-timeout" class="config-input" />
|
||||||
|
<button class="config-save-btn" data-key="ASSIGNMENT_TIMEOUT">Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label for="assignment-reminder">Assignment Reminder (seconds)</label>
|
||||||
|
<p class="config-description">Seconds before sending a reminder message</p>
|
||||||
|
<input type="number" id="assignment-reminder" class="config-input" />
|
||||||
|
<button class="config-save-btn" data-key="ASSIGNMENT_REMINDER">Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label for="poll-interval">Poll Interval (seconds)</label>
|
||||||
|
<p class="config-description">General polling interval for data refresh</p>
|
||||||
|
<input type="number" id="poll-interval" class="config-input" />
|
||||||
|
<button class="config-save-btn" data-key="POLL_INTERVAL">Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label for="hit-check-interval">Hit Check Interval (seconds)</label>
|
||||||
|
<p class="config-description">Interval for checking hit completion</p>
|
||||||
|
<input type="number" id="hit-check-interval" class="config-input" />
|
||||||
|
<button class="config-save-btn" data-key="HIT_CHECK_INTERVAL">Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label for="reassign-delay">Reassign Delay (seconds)</label>
|
||||||
|
<p class="config-description">Delay before reassigning failed targets</p>
|
||||||
|
<input type="number" id="reassign-delay" class="config-input" />
|
||||||
|
<button class="config-save-btn" data-key="REASSIGN_DELAY">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/config.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -8,18 +8,14 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<!-- Top bar: Title + interval + Reset button -->
|
<!-- Top bar: Title + Reset button -->
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<h1>War Dashboard</h1>
|
<h1>War Dashboard</h1>
|
||||||
|
|
||||||
<div class="top-controls">
|
<div class="top-controls">
|
||||||
|
<a href="/config" class="nav-link">Settings</a>
|
||||||
<button id="bot-control-btn" class="bot-btn" data-running="false">Start Bot</button>
|
<button id="bot-control-btn" class="bot-btn" data-running="false">Start Bot</button>
|
||||||
<button id="reset-groups-btn" class="reset-btn">Reset Groups</button>
|
<button id="reset-groups-btn" class="reset-btn">Reset Groups</button>
|
||||||
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,10 +27,16 @@
|
|||||||
<!-- FRIENDLY -->
|
<!-- FRIENDLY -->
|
||||||
<div class="faction-card small">
|
<div class="faction-card small">
|
||||||
<h2>Friendly Faction</h2>
|
<h2>Friendly Faction</h2>
|
||||||
<div class="controls">
|
<div class="controls-row">
|
||||||
<input type="number" id="friendly-id" placeholder="Faction ID" />
|
<div class="controls">
|
||||||
<button id="friendly-populate-btn">Populate</button>
|
<input type="number" id="friendly-id" placeholder="Faction ID" />
|
||||||
<button id="friendly-status-btn">Start Refresh</button>
|
<button id="friendly-populate-btn">Populate</button>
|
||||||
|
</div>
|
||||||
|
<div class="status-controls">
|
||||||
|
<label for="friendly-refresh-interval">Status Refresh (seconds):</label>
|
||||||
|
<input type="number" id="friendly-refresh-interval" value="10" min="1" />
|
||||||
|
<button id="friendly-status-btn" class="status-btn" data-running="false">Start</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="friendly-container"
|
<div id="friendly-container"
|
||||||
@@ -45,10 +47,16 @@
|
|||||||
<!-- ENEMY -->
|
<!-- ENEMY -->
|
||||||
<div class="faction-card small">
|
<div class="faction-card small">
|
||||||
<h2>Enemy Faction</h2>
|
<h2>Enemy Faction</h2>
|
||||||
<div class="controls">
|
<div class="controls-row">
|
||||||
<input type="number" id="enemy-id" placeholder="Faction ID" />
|
<div class="controls">
|
||||||
<button id="enemy-populate-btn">Populate</button>
|
<input type="number" id="enemy-id" placeholder="Faction ID" />
|
||||||
<button id="enemy-status-btn">Start Refresh</button>
|
<button id="enemy-populate-btn">Populate</button>
|
||||||
|
</div>
|
||||||
|
<div class="status-controls">
|
||||||
|
<label for="enemy-refresh-interval">Status Refresh (seconds):</label>
|
||||||
|
<input type="number" id="enemy-refresh-interval" value="10" min="1" />
|
||||||
|
<button id="enemy-status-btn" class="status-btn" data-running="false">Start</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="enemy-container"
|
<div id="enemy-container"
|
||||||
|
|||||||
Reference in New Issue
Block a user