From 74b51e74ca02436923fb359b5478dfd138a86884 Mon Sep 17 00:00:00 2001 From: haunter Date: Thu, 7 May 2026 20:22:28 +0300 Subject: [PATCH] fix que / add auto --- bot_modules/04d_execute_culture.js | 62 +++++---- bot_modules/05_main.js | 7 +- db.py | 38 ++++++ routes/api.py | 196 ++++++++++++++++++++++++++++- routes/dashboard.py | 115 ++++++++++++++--- templates/agora.html | 103 +++++++++++++-- 6 files changed, 467 insertions(+), 54 deletions(-) diff --git a/bot_modules/04d_execute_culture.js b/bot_modules/04d_execute_culture.js index 5c7b9c3..a2eb8c0 100644 --- a/bot_modules/04d_execute_culture.js +++ b/bot_modules/04d_execute_culture.js @@ -1,40 +1,52 @@ // ================================================================ // 04d_execute_culture.js — Execute Αγορά celebration commands // -// Handles commands of type 'culture' from the poll response. -// Supported celebration_type values: -// 'party' → Γιορτή πόλης (costs wood/stone/iron) -// 'triumph' → Παρέλαση θριάμβου (costs battle points) +// Handles commands of type 'culture' from the culture_queue. +// Completely separate from the commands table — builds/recruits +// are never blocked by a stuck culture command. // -// Uses the same fire-and-forget ajaxPost pattern as all other -// execute modules in this codebase (04a, 04b, 04c). +// Reports results to /api/culture/result/ (dedicated endpoint). // ================================================================ -async function executeCultureCommand(cmd) { - if (!cmd || !cmd.id || !cmd.payload) return; +async function reportCultureResult(queueId, status, msg) { + try { + await apiFetch(`${BASE_URL}/api/culture/result/${queueId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status, message: msg }) + }); + } catch (e) { + log(`[αγορά] ⚠️ Failed to report result for queue #${queueId}: ${e}`); + } +} - // town_id lives on the command root (like build/recruit), payload has celebration_type - const town_id = cmd.town_id || cmd.payload.town_id; +async function executeCultureCommand(cmd) { + if (!cmd || !cmd.id || !cmd.payload) return { ok: false, msg: 'No payload' }; + + // town_id lives on the command root; payload has celebration_type + const town_id = cmd.town_id || cmd.payload.town_id; const celebration_type = cmd.payload.celebration_type; + const source = cmd.payload.source || 'manual'; if (!celebration_type || !town_id) { - reportResult(cmd.id, 'failed', 'Invalid culture payload: missing celebration_type or town_id'); - return; + await reportCultureResult(cmd.id, 'failed', 'Invalid culture payload: missing celebration_type or town_id'); + return { ok: false, msg: 'Invalid payload' }; } if (!['party', 'triumph'].includes(celebration_type)) { - reportResult(cmd.id, 'failed', `Unknown celebration_type: ${celebration_type}`); - return; + await reportCultureResult(cmd.id, 'failed', `Unknown celebration_type: ${celebration_type}`); + return { ok: false, msg: 'Unknown type' }; } const label = celebration_type === 'party' ? 'Γιορτή πόλης' : 'Παρέλαση θριάμβου'; - log(`[αγορά] Εκτέλεση ${label} για πόλη ${town_id}`); + log(`[αγορά] Εκτέλεση ${label} για πόλη ${town_id} (${source})`); // Validate town exists in game memory const town = uw.ITowns?.towns?.[town_id]; if (!town) { - reportResult(cmd.id, 'failed', `Η πόλη ${town_id} δεν βρέθηκε στη μνήμη του παιχνιδιού.`); - return; + const msg = `Η πόλη ${town_id} δεν βρέθηκε στη μνήμη του παιχνιδιού.`; + await reportCultureResult(cmd.id, 'failed', msg); + return { ok: false, msg }; } // Double-check: is there already a celebration of this type running? @@ -47,9 +59,9 @@ async function executeCultureCommand(cmd) { if (String(a.town_id) === String(town_id) && a.celebration_type === celebration_type && (a.finished_at ?? 0) > nowTs) { - reportResult(cmd.id, 'failed', - `${label} ήδη ενεργή στην πόλη ${town_id}.`); - return; + const msg = `${label} ήδη ενεργή στην πόλη ${town_id}.`; + await reportCultureResult(cmd.id, 'failed', msg); + return { ok: false, msg }; } } } @@ -61,8 +73,8 @@ async function executeCultureCommand(cmd) { await sleep(reactionMs); if (paused) { - reportResult(cmd.id, 'failed', 'Aborted due to pause/captcha'); - return; + await reportCultureResult(cmd.id, 'failed', 'Aborted due to pause/captcha'); + return { ok: false, msg: 'Paused' }; } // Fire-and-forget — exact same 3-arg pattern used everywhere in this codebase. @@ -74,6 +86,8 @@ async function executeCultureCommand(cmd) { }); await sleep(500); - log(`[αγορά] ✅ ${label} εστάλη (πόλη ${town_id})`); - reportResult(cmd.id, 'done', `${label} εστάλη επιτυχώς.`); + const successMsg = `${label} εστάλη επιτυχώς (${source}).`; + log(`[αγορά] ✅ ${successMsg} (πόλη ${town_id})`); + await reportCultureResult(cmd.id, 'done', successMsg); + return { ok: true, msg: successMsg }; } diff --git a/bot_modules/05_main.js b/bot_modules/05_main.js index c100cf0..9cec4eb 100644 --- a/bot_modules/05_main.js +++ b/bot_modules/05_main.js @@ -97,7 +97,12 @@ async function pollAndExecute() { else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd); else if (cmd.type === 'research') result = await executeResearch(cmd); else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd); - else if (cmd.type === 'culture') result = await executeCultureCommand(cmd); + else if (cmd.type === 'culture') { + // executeCultureCommand reports to /api/culture/result directly + // — do NOT call the shared reportResult() afterwards + await executeCultureCommand(cmd); + return; + } else if (cmd.type === 'farm_loot') { // Guard: if auto-farm is mid-run, requeue rather than overlap if (farmLootRunning) { diff --git a/db.py b/db.py index a4058e8..e6ed4e3 100644 --- a/db.py +++ b/db.py @@ -126,6 +126,7 @@ def init_db(): cost_iron INTEGER NOT NULL DEFAULT 0, cost_battle_pts INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'pending', -- pending | success | failed + source TEXT NOT NULL DEFAULT 'manual', -- 'manual' | 'auto' result_msg TEXT, fired_at TEXT NOT NULL DEFAULT (datetime('now')), confirmed_at TEXT @@ -133,6 +134,43 @@ def init_db(): ''') c.execute('CREATE INDEX IF NOT EXISTS idx_culture_log_player_world ON culture_log(player_id, world_id)') + # Culture queue — dedicated queue for celebration commands (separate from commands table). + # Max 1 pending/executing per player+world+celebration_type enforced at app level. + # source: 'manual' (dashboard button) | 'auto' (server-side auto-fire from state push) + c.execute(''' + CREATE TABLE IF NOT EXISTS culture_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_id TEXT NOT NULL, + world_id TEXT NOT NULL, + town_id TEXT NOT NULL, + town_name TEXT NOT NULL DEFAULT '', + celebration_type TEXT NOT NULL, -- 'party' | 'triumph' + status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed + source TEXT NOT NULL DEFAULT 'manual', -- 'manual' | 'auto' + result_msg TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + executed_at TEXT + ) + ''') + c.execute('CREATE INDEX IF NOT EXISTS idx_culture_queue_player_world ON culture_queue(player_id, world_id, status)') + + # Culture settings — per-town auto-mode toggle (party / triumph). + # One row per player+world+town. Updated from Αγορά dashboard toggles. + c.execute(''' + CREATE TABLE IF NOT EXISTS culture_settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_id TEXT NOT NULL, + world_id TEXT NOT NULL, + town_id TEXT NOT NULL, + auto_party INTEGER NOT NULL DEFAULT 0, + auto_triumph INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(player_id, world_id, town_id) + ) + ''') + c.execute('CREATE INDEX IF NOT EXISTS idx_culture_settings_player_world ON culture_settings(player_id, world_id)') + + # Blueprints - assigns a blueprint to a specific town c.execute(''' CREATE TABLE IF NOT EXISTS town_blueprints ( diff --git a/routes/api.py b/routes/api.py index bbcb496..a520b9f 100644 --- a/routes/api.py +++ b/routes/api.py @@ -146,10 +146,103 @@ def receive_state(): ''', (str(player_id), world_id, town_id_cel, t_name, cel_type, finished_at, now_iso)) conn.commit() + # ── Auto-culture: check per-town settings and queue if eligible ── + # Runs on every state push — no extra game polling needed. + _auto_culture_check(c, str(player_id), world_id, towns, battle_points) + conn.commit() + conn.close() return jsonify({'ok': True, 'towns_updated': len(towns)}) +# Cost constants (must match dashboard.py PARTY_COST / TRIUMPH_COST) +_PARTY_COST = {'wood': 15000, 'stone': 18000, 'iron': 15000} +_TRIUMPH_BP = 300 + +def _auto_culture_check(c, player_id, world_id, towns, battle_points): + """Called after every state push. + For each town that has auto_party or auto_triumph enabled: + - skip if a pending/executing entry already exists in culture_queue + - skip if celebration cooldown hasn't expired yet + - skip if resources/battle-points are insufficient + - otherwise insert a new 'auto' entry into culture_queue + """ + if not player_id or not world_id: + return + + now_ts = int(datetime.utcnow().timestamp()) + now_iso = datetime.utcnow().isoformat() + bp_available = battle_points.get('available', 0) if isinstance(battle_points, dict) else 0 + + # Load auto settings for this player/world + auto_rows = c.execute( + 'SELECT town_id, auto_party, auto_triumph FROM culture_settings WHERE player_id = ? AND world_id = ?', + (player_id, world_id) + ).fetchall() + if not auto_rows: + return + + # Build a quick lookup of town data from the state payload + town_map = {str(t.get('town_id', '')): t for t in towns} + + for auto_row in auto_rows: + tid = str(auto_row['town_id']) + td = town_map.get(tid) + if not td: + continue # town not in this state push (shouldn't happen but guard anyway) + town_name = td.get('town_name', '') + + for cel_type, enabled in [('party', auto_row['auto_party']), ('triumph', auto_row['auto_triumph'])]: + if not enabled: + continue + + # 1. Already queued? + existing = c.execute( + "SELECT id FROM culture_queue WHERE player_id=? AND world_id=? AND celebration_type=? AND status IN ('pending','executing')", + (player_id, world_id, cel_type) + ).fetchone() + if existing: + continue + + # 2. Cooldown still active? + cel_cd = c.execute( + 'SELECT finished_at FROM celebrations WHERE player_id=? AND world_id=? AND town_id=? AND celebration_type=?', + (player_id, world_id, tid, cel_type) + ).fetchone() + if cel_cd and int(cel_cd['finished_at'] or 0) > now_ts: + continue # still cooling down + + # 3. Resources check + if cel_type == 'party': + if (td.get('wood', 0) < _PARTY_COST['wood'] or + td.get('stone', 0) < _PARTY_COST['stone'] or + td.get('iron', 0) < _PARTY_COST['iron']): + continue # not enough yet — will retry on next state push + else: # triumph + if bp_available < _TRIUMPH_BP: + continue + + # All clear — queue it + c.execute(''' + INSERT INTO culture_queue + (player_id, world_id, town_id, town_name, celebration_type, status, source, created_at) + VALUES (?, ?, ?, ?, ?, 'pending', 'auto', ?) + ''', (player_id, world_id, tid, town_name, cel_type, now_iso)) + # Also log it + cost_w = _PARTY_COST['wood'] if cel_type == 'party' else 0 + cost_s = _PARTY_COST['stone'] if cel_type == 'party' else 0 + cost_i = _PARTY_COST['iron'] if cel_type == 'party' else 0 + cost_b = _TRIUMPH_BP if cel_type == 'triumph' else 0 + c.execute(''' + INSERT INTO culture_log + (player_id, world_id, town_id, town_name, celebration_type, + cost_wood, cost_stone, cost_iron, cost_battle_pts, status, source, fired_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 'auto', ?) + ''', (player_id, world_id, tid, town_name, cel_type, + cost_w, cost_s, cost_i, cost_b, now_iso)) + + + # ------------------------------------------------------------------ @@ -184,7 +277,7 @@ def _fetch_pending_of_type(c, cmd_type, player_id, world_id): if not row: return None - c.execute(''' + c.execute('''\ UPDATE commands SET status = 'executing', updated_at = ? WHERE id = ? @@ -197,7 +290,58 @@ def _fetch_pending_of_type(c, cmd_type, player_id, world_id): } +def _fetch_pending_culture(c, player_id, world_id): + """Fetch one pending culture command from the dedicated culture_queue table. + Completely separate from the commands table — no interference with builds/recruits. + Also times out stuck 'executing' rows to 'failed' after 5 minutes. + """ + now = datetime.utcnow().isoformat() + five_min_ago = (datetime.utcnow() - timedelta(minutes=5)).isoformat() + + # Expire stuck executing entries (fail, don't requeue — auto will re-fire on next state push) + c.execute(''' + UPDATE culture_queue + SET status = 'failed', result_msg = 'Timeout (5 min)' + WHERE status = 'executing' AND executed_at < ? AND player_id = ? + ''', (five_min_ago, player_id)) + + # Fetch one pending row + if world_id: + row = c.execute(''' + SELECT * FROM culture_queue + WHERE status = 'pending' AND player_id = ? AND world_id = ? + ORDER BY created_at ASC + LIMIT 1 + ''', (player_id, world_id)).fetchone() + else: + row = c.execute(''' + SELECT * FROM culture_queue + WHERE status = 'pending' AND player_id = ? + ORDER BY created_at ASC + LIMIT 1 + ''', (player_id,)).fetchone() + + if not row: + return None + + c.execute(''' + UPDATE culture_queue SET status = 'executing', executed_at = ? WHERE id = ? + ''', (now, row['id'])) + + return { + 'id': row['id'], + 'town_id': row['town_id'], + 'type': 'culture', + 'payload': { + 'town_id': row['town_id'], + 'celebration_type': row['celebration_type'], + 'source': row['source'] + } + } + + def _fetch_pending_builds_all_towns(c, player_id, world_id): + """ Fetch ONE pending 'build' command per distinct town_id. This allows all towns to build in parallel within a single poll cycle. @@ -298,7 +442,7 @@ def get_pending_command(): farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id, world_id) farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id, world_id) research_cmd = _fetch_pending_of_type(c, 'research', player_id, world_id) - culture_cmd = _fetch_pending_of_type(c, 'culture', player_id, world_id) + culture_cmd = _fetch_pending_culture(c, player_id, world_id) # reads culture_queue sync_req = _check_and_reset_sync(c, player_id) # Determine player_key for world-specific settings if world_id is provided @@ -573,9 +717,53 @@ def api_bot_logs(): return jsonify({'ok': True}) - # ------------------------------------------------------------------ -# GET /api/bot +# POST /api/culture/result/ +# Bot reports success/failure of a culture command. +# Completely separate from /api/commands//result — zero +# interference with builds, recruits, or any other command type. +# ------------------------------------------------------------------ +@api.route('/api/culture/result/', methods=['POST']) +def culture_result(queue_id): + data = request.get_json(silent=True) or {} + status_in = data.get('status', 'done') # 'done' | 'failed' + msg = data.get('message', '') + now = datetime.utcnow().isoformat() + final_status = 'done' if status_in == 'done' else 'failed' + + conn = get_db() + row = conn.execute( + 'SELECT * FROM culture_queue WHERE id = ?', (queue_id,) + ).fetchone() + + if row: + conn.execute(''' + UPDATE culture_queue + SET status = ?, result_msg = ?, executed_at = ? + WHERE id = ? + ''', (final_status, msg, now, queue_id)) + + # Sync the most recent matching pending culture_log entry + log_row = conn.execute(''' + SELECT id FROM culture_log + WHERE player_id = ? AND world_id = ? AND town_id = ? + AND celebration_type = ? AND status = 'pending' + ORDER BY id DESC LIMIT 1 + ''', (row['player_id'], row['world_id'], row['town_id'], row['celebration_type'])).fetchone() + if log_row: + log_status = 'success' if final_status == 'done' else 'failed' + conn.execute(''' + UPDATE culture_log + SET status = ?, result_msg = ?, confirmed_at = ? + WHERE id = ? + ''', (log_status, msg, now, log_row['id'])) + + conn.commit() + conn.close() + return jsonify({'ok': True}) + + + # Serves the modular bot code concatenated into a single response # ------------------------------------------------------------------ @api.route('/api/bot', methods=['GET']) diff --git a/routes/dashboard.py b/routes/dashboard.py index 76a7431..e6d4677 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -731,6 +731,17 @@ def get_agora(): for r in cel_rows: cooldowns[(r['town_id'], r['celebration_type'])] = r['finished_at'] + # ── Auto settings ──────────────────────────────────────── + settings_rows = conn.execute(''' + SELECT town_id, auto_party, auto_triumph + FROM culture_settings + WHERE player_id = ? AND world_id = ? + ''', (player_id, world_id)).fetchall() + auto_settings = {} # town_id → {auto_party, auto_triumph} + for s in settings_rows: + auto_settings[(s['town_id'], 'auto_party')] = s['auto_party'] + auto_settings[(s['town_id'], 'auto_triumph')] = s['auto_triumph'] + conn.close() # ── Per-town eligibility ───────────────────────────────────────── @@ -801,6 +812,8 @@ def get_agora(): 'available': triumph_ok, 'cooldown_until': triumph_cd or None, }, + 'auto_party': bool(auto_settings.get((tid, 'auto_party'), 0)), + 'auto_triumph': bool(auto_settings.get((tid, 'auto_triumph'), 0)), }) return jsonify({'towns': towns_out, 'costs': {'party': PARTY_COST, 'triumph': TRIUMPH_COST}}) @@ -824,7 +837,7 @@ def _fmt_seconds(secs): # Body: { player_id, world_id, town_id, town_name, celebration_type } # • Validates client is online. # • Validates eligibility (resources / battle_points / cooldown). -# • Inserts a 'culture' command into the commands queue. +# • Inserts a 'culture' command into the culture_queue. # • Inserts a 'pending' row into culture_log. # ------------------------------------------------------------------ @dashboard.route('/dashboard/culture-command', methods=['POST']) @@ -902,32 +915,43 @@ def culture_command(): return jsonify({'error': 'insufficient_battle_points', 'message': f'Ανεπαρκείς πόντοι μάχης ({available_bp}/{TRIUMPH_COST}).'}), 409 - # ── Insert command into bot queue ──────────────────────────────── - now_iso = datetime.utcnow().isoformat() + # ── Insert into culture_queue ──────────────────────────────── c = conn.cursor() + + # One-at-a-time check: reject if this type already pending/executing + existing = c.execute(''' + SELECT id FROM culture_queue + WHERE player_id = ? AND world_id = ? AND town_id = ? AND celebration_type = ? + AND status IN ('pending', 'executing') + ''', (str(player_id), world_id, town_id, cel_type)).fetchone() + if existing: + conn.close() + label = 'Γιορτή πόλης' if cel_type == 'party' else 'Παρέλαση θριάμβου' + return jsonify({'error': 'already_queued', + 'message': f'{label} για την πόλη αυτή είναι ήδη σε αναμονή εκτέλεσης.'}), 409 + + now_iso = datetime.utcnow().isoformat() + c.execute(''' - INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id) - VALUES (?, ?, 'culture', ?, 'pending', ?, ?, ?) - ''', ( - town_id, town_name, - json.dumps({'celebration_type': cel_type, 'town_id': int(town_id)}), - now_iso, now_iso, str(player_id) - )) - cmd_id = c.lastrowid + INSERT INTO culture_queue + (player_id, world_id, town_id, town_name, celebration_type, status, source, created_at) + VALUES (?, ?, ?, ?, ?, 'pending', 'manual', ?) + ''', (str(player_id), world_id, town_id, town_name, cel_type, now_iso)) + queue_id = c.lastrowid # ── Append culture_log row (status=pending until bot confirms) ─── c.execute(''' INSERT INTO culture_log (player_id, world_id, town_id, town_name, celebration_type, - cost_wood, cost_stone, cost_iron, cost_battle_pts, status, fired_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?) + cost_wood, cost_stone, cost_iron, cost_battle_pts, status, source, fired_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 'manual', ?) ''', (str(player_id), world_id, town_id, town_name, cel_type, cost_wood, cost_stone, cost_iron, cost_bp, now_iso)) log_id = c.lastrowid conn.commit() conn.close() - return jsonify({'ok': True, 'cmd_id': cmd_id, 'log_id': log_id}) + return jsonify({'ok': True, 'queue_id': queue_id, 'log_id': log_id}) # ------------------------------------------------------------------ @@ -946,7 +970,7 @@ def get_culture_log(): rows = conn.execute(''' SELECT id, town_id, town_name, celebration_type, cost_wood, cost_stone, cost_iron, cost_battle_pts, - status, result_msg, fired_at, confirmed_at + status, result_msg, source, fired_at, confirmed_at FROM culture_log WHERE player_id = ? AND world_id = ? ORDER BY id DESC @@ -954,3 +978,64 @@ def get_culture_log(): ''', (player_id, world_id)).fetchall() conn.close() return jsonify([dict(r) for r in rows]) + + +# ------------------------------------------------------------------ +# GET /dashboard/culture-settings?player_id=&world_id=&town_id= +# POST /dashboard/culture-settings +# Body: { player_id, world_id, town_id, auto_party, auto_triumph } +# Save/retrieve per-town auto-celebration preferences. +# ------------------------------------------------------------------ +@dashboard.route('/dashboard/culture-settings', methods=['GET', 'POST']) +@login_required +def culture_settings(): + if request.method == 'GET': + player_id = request.args.get('player_id') + world_id = request.args.get('world_id', '') + town_id = request.args.get('town_id', '') + if not player_id: + return jsonify({'error': 'missing player_id'}), 400 + + conn = get_db() + if town_id: + row = conn.execute( + 'SELECT auto_party, auto_triumph FROM culture_settings WHERE player_id=? AND world_id=? AND town_id=?', + (player_id, world_id, town_id) + ).fetchone() + conn.close() + if row: + return jsonify({'auto_party': bool(row['auto_party']), 'auto_triumph': bool(row['auto_triumph'])}) + return jsonify({'auto_party': False, 'auto_triumph': False}) + else: + rows = conn.execute( + 'SELECT town_id, auto_party, auto_triumph FROM culture_settings WHERE player_id=? AND world_id=?', + (player_id, world_id) + ).fetchall() + conn.close() + return jsonify([dict(r) for r in rows]) + + # POST — save settings + data = request.get_json(silent=True) or {} + player_id = data.get('player_id') + world_id = data.get('world_id', '') + town_id = str(data.get('town_id', '')) + auto_party = 1 if data.get('auto_party') else 0 + auto_triumph = 1 if data.get('auto_triumph') else 0 + + if not player_id or not town_id: + return jsonify({'error': 'missing player_id or town_id'}), 400 + + now_iso = datetime.utcnow().isoformat() + conn = get_db() + conn.execute(''' + INSERT INTO culture_settings (player_id, world_id, town_id, auto_party, auto_triumph, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(player_id, world_id, town_id) DO UPDATE SET + auto_party = excluded.auto_party, + auto_triumph = excluded.auto_triumph, + updated_at = excluded.updated_at + ''', (str(player_id), world_id, town_id, auto_party, auto_triumph, now_iso)) + conn.commit() + conn.close() + return jsonify({'ok': True, 'auto_party': bool(auto_party), 'auto_triumph': bool(auto_triumph)}) + diff --git a/templates/agora.html b/templates/agora.html index a79fc4c..9467366 100644 --- a/templates/agora.html +++ b/templates/agora.html @@ -343,6 +343,39 @@ .btn-confirm:hover { background: #c99ef0; } .btn-confirm:disabled { background: #444; color: var(--muted); cursor: not-allowed; } + /* ── Auto toggle ── */ + .auto-toggle { + display: flex; + align-items: center; + gap: 0; + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; + width: fit-content; + font-size: 0.76rem; + font-weight: 700; + } + .auto-toggle button { + padding: 5px 12px; + border: none; + background: #1e1e30; + color: var(--muted); + cursor: pointer; + transition: all .2s; + } + .auto-toggle button.active-manual { background: #2a2a40; color: var(--text); } + .auto-toggle button.active-auto { background: rgba(180,130,220,0.25); color: var(--purple); } + .cel-card.auto-on { border-color: rgba(180,130,220,0.6); box-shadow: 0 0 12px rgba(180,130,220,0.08); } + .auto-badge { + font-size: 0.7rem; + background: rgba(180,130,220,0.15); + color: var(--purple); + border: 1px solid rgba(180,130,220,0.3); + border-radius: 6px; + padding: 2px 7px; + font-weight: 700; + } + .toast { position: fixed; bottom: 1.5rem; @@ -488,8 +521,10 @@ } function celebCard(town, type) { - const cel = town[type]; - const label = TYPE_LABELS[type]; + const cel = town[type]; + const label = TYPE_LABELS[type]; + const autoKey = type === 'party' ? 'auto_party' : 'auto_triumph'; + const autoOn = !!town[autoKey]; let costsHtml = ''; if (type === 'party') { @@ -512,19 +547,33 @@ ? `
${cel.reason}
` : ''; - const btnLabel = cel.status === 'cooldown' ? `⏰ ${cel.reason}` - : cel.status === 'unavailable' ? '✖ Μη διαθέσιμο' - : `▶ Εκκίνηση ${label}`; + const autoToggle = ` +
+ + +
`; return ` -
-
${label}
+
+
+ ${label} + ${autoOn ? 'AUTO' : ''} +
${costsHtml} ${reasonHtml} + ${autoToggle}
`; } @@ -534,6 +583,37 @@ return n; } + // ── Auto-setting toggle ─────────────────────────────────── + async function saveAutoSetting(townId, type, enable) { + const town = agoraData.find(t => t.town_id === townId); + if (!town) return; + + const payload = { + player_id: PLAYER_ID, + world_id: WORLD_ID, + town_id: townId, + auto_party: type === 'party' ? enable : !!town.auto_party, + auto_triumph: type === 'triumph' ? enable : !!town.auto_triumph + }; + + try { + const r = await fetch('/dashboard/culture-settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const d = await r.json(); + if (d.ok) { + // Optimistically update local state and re-render card + town[type === 'party' ? 'auto_party' : 'auto_triumph'] = enable; + renderCards(town); + showToast(enable ? `⚡ Αυτόματο ενεργό — ${TYPE_LABELS[type]}` : `✓ Χειροκίνητο — ${TYPE_LABELS[type]}`, 'ok'); + } + } catch (e) { + showToast(`❌ Αποτυχία αποθήκευσης: ${e}`, 'err'); + } + } + // ── Modal ──────────────────────────────────────────────────────── function openModal(townId, townName, celType) { pendingCmd = { townId, townName, celType }; @@ -633,7 +713,10 @@ ${row.town_name} ${TYPE_LABELS[row.celebration_type] || row.celebration_type} ${row.status === 'success' ? '✅' : row.status === 'failed' ? '❌' : '⏳'} - ${costStr}${row.result_msg ? ' — ' + row.result_msg : ''}`; + + ${row.source === 'auto' ? 'AUTO' : ''} + ${costStr}${row.result_msg ? ' — ' + row.result_msg : ''} + `; body.insertBefore(el, body.firstChild); }); } catch (e) { /* silent */ }