from flask import Blueprint, render_template, request, jsonify from flask_login import login_required, current_user from db import get_db import json from datetime import datetime, timedelta dashboard = Blueprint('dashboard', __name__) # ------------------------------------------------------------------ # GET / # Serve the dashboard HTML # ------------------------------------------------------------------ @dashboard.route('/') @login_required def index(): conn = get_db() # Get the clan the logged-in user belongs to clan_id = current_user.clan_id if not clan_id: # User has no clan yet — send them to options to create/join one conn.close() return render_template('index.html', players=[], no_clan=True) # Only fetch players that are members of this clan rows = conn.execute(''' SELECT ts.player, ts.player_id, MAX(ts.updated_at) as last_seen, MAX(ts.world_id) as world_id FROM town_state ts INNER JOIN clan_members cm ON cm.player_id = ts.player_id AND cm.clan_id = ? WHERE ts.player IS NOT NULL GROUP BY ts.player, ts.player_id ORDER BY ts.player ASC ''', (clan_id,)).fetchall() captcha_rows = conn.execute("SELECT key, value FROM kv_store WHERE key LIKE 'captcha_active_%'").fetchall() active_captchas = {r['key'].replace('captcha_active_', ''): True for r in captcha_rows if r['value'] == '1'} conn.close() players = [] now = datetime.utcnow() for r in rows: is_online = False if r['last_seen']: try: last_seen = datetime.fromisoformat(r['last_seen']) if (now - last_seen).total_seconds() <= 150: is_online = True except Exception: pass players.append({ 'player': r['player'], 'player_id': r['player_id'], 'world_id': r['world_id'] or 'Unknown', 'is_online': is_online, 'captcha_active': active_captchas.get(r['player_id'], False) }) return render_template('index.html', players=players, no_clan=False) @dashboard.route('/player/') @login_required def player_hub(player_id): return render_template('hub.html', player_id=player_id) @dashboard.route('/player//admin') @login_required def player_dashboard(player_id): return render_template('dashboard.html', player_id=player_id) @dashboard.route('/player//farm') @login_required def player_farm(player_id): return render_template('farm.html', player_id=player_id) # ------------------------------------------------------------------ # GET /dashboard/farm-settings — returns current farm config # POST /dashboard/farm-settings — updates farm config # ------------------------------------------------------------------ @dashboard.route('/dashboard/farm-settings', methods=['GET']) def get_farm_settings(): player_id = request.args.get('player_id') conn = get_db() row = conn.execute( 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,) ).fetchone() conn.close() if row: return jsonify({'enabled': bool(row['enabled']), 'loot_option': row['loot_option']}) return jsonify({'enabled': False, 'loot_option': 1}) @dashboard.route('/dashboard/farm-settings', methods=['POST']) def set_farm_settings(): data = request.get_json(silent=True) if not data or 'player_id' not in data: return jsonify({'error': 'missing player_id'}), 400 player_id = data['player_id'] enabled = 1 if data.get('enabled') else 0 loot_option = int(data.get('loot_option', 1)) conn = get_db() conn.execute(''' INSERT INTO farm_settings (player_id, enabled, loot_option, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(player_id) DO UPDATE SET enabled = excluded.enabled, loot_option = excluded.loot_option, updated_at = excluded.updated_at ''', (player_id, enabled, loot_option, datetime.utcnow().isoformat())) conn.commit() conn.close() return jsonify({'ok': True}) # ------------------------------------------------------------------ # GET /dashboard/farm-data # Returns ready-to-loot farm towns for a player across all towns # ------------------------------------------------------------------ @dashboard.route('/dashboard/farm-data', methods=['GET']) def get_farm_data(): player_id = request.args.get('player_id') conn = get_db() rows = conn.execute( 'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,) ).fetchall() # Also fetch when the bot last farmed lf_row = conn.execute( "SELECT value FROM kv_store WHERE key = ?", (f'last_farmed_{player_id}',) ).fetchone() last_farmed_at = lf_row['value'] if lf_row else None conn.close() now_ts = int(datetime.utcnow().timestamp()) farms_summary = [] for row in rows: d = json.loads(row['data']) farm_data = d.get('farms', []) ready = [f for f in farm_data if f.get('lootable_at', 0) <= now_ts and f.get('relation_status', 0) == 1] if farm_data: farms_summary.append({ 'town_id': row['town_id'], 'town_name': row['town_name'], 'total_farms': len(farm_data), 'ready_farms': len(ready), 'next_ready_at': min((f['lootable_at'] for f in farm_data if f.get('lootable_at', 0) > now_ts and f.get('relation_status', 0) == 1), default=None) }) return jsonify({'towns': farms_summary, 'last_farmed_at': last_farmed_at}) # ------------------------------------------------------------------ # GET /dashboard/market-data # Returns the latest market scan data for a player. # ------------------------------------------------------------------ @dashboard.route('/dashboard/market-data', methods=['GET']) def get_market_data(): player_id = request.args.get('player_id') conn = get_db() row = conn.execute( "SELECT value, updated_at FROM kv_store WHERE key = ?", (f'market_data_{player_id}', ) ).fetchone() conn.close() if row: return jsonify({'data': json.loads(row['value']), 'updated_at': row['updated_at']}) return jsonify({'data': None, 'updated_at': None}) # ------------------------------------------------------------------ # GET /dashboard/towns # Returns all known towns with their latest state snapshot. # ------------------------------------------------------------------ @dashboard.route('/dashboard/towns', methods=['GET']) def get_towns(): player_id = request.args.get('player_id') conn = get_db() rows = conn.execute(''' SELECT town_id, town_name, player, player_id, alliance_id, world_id, x, y, sea, data, updated_at FROM town_state WHERE player_id = ? ORDER BY town_name ASC ''', (player_id, )).fetchall() conn.close() towns = [] for row in rows: d = json.loads(row['data']) towns.append({ 'town_id': row['town_id'], 'town_name': row['town_name'], 'player': row['player'], 'player_id': row['player_id'], 'alliance_id': row['alliance_id'], 'world_id': row['world_id'], 'x': row['x'], 'y': row['y'], 'sea': row['sea'], 'updated_at': row['updated_at'], 'resources': { 'wood': d.get('wood', 0), 'stone': d.get('stone', 0), 'iron': d.get('iron', 0), 'storage': d.get('storage', 0), 'market_capacity': d.get('market_capacity', 0), 'population': d.get('population', 0), }, 'buildings': d.get('buildings', {}), 'units': d.get('units', {}), 'points': d.get('points', 0), 'god': d.get('god', None), 'build_queue': d.get('buildingOrder', []), 'build_data': d.get('buildData', {}), 'unit_data': d.get('unitData', {}), 'researches': d.get('researches', {}), 'has_premium': d.get('has_premium', False), 'bonuses': d.get('bonuses', {}), 'wonder_points': d.get('wonder_points', 0), 'total_points': d.get('total_points', 0), 'alliance_name': d.get('alliance_name', None) }) return jsonify(towns) # ------------------------------------------------------------------ # GET /dashboard/client-status # Returns whether the Tampermonkey client is considered online. # Online = at least one town_state row updated within the last 150 s. # ------------------------------------------------------------------ @dashboard.route('/dashboard/client-status', methods=['GET']) def client_status(): player_id = request.args.get('player_id') conn = get_db() row = conn.execute(''' SELECT MAX(updated_at) AS last_seen FROM town_state WHERE player_id = ? ''', (player_id, )).fetchone() conn.close() last_seen = row['last_seen'] if row else None if last_seen: try: dt = datetime.fromisoformat(last_seen) age_s = (datetime.utcnow() - dt).total_seconds() online = age_s <= 150 except Exception: online = False else: online = False return jsonify({'online': online, 'last_seen': last_seen}) # ------------------------------------------------------------------ # GET /dashboard/captcha-status # Returns whether a captcha is currently active in the game client. # ------------------------------------------------------------------ @dashboard.route('/dashboard/captcha-status', methods=['GET']) def captcha_status(): player_id = request.args.get('player_id') conn = get_db() row = conn.execute( "SELECT value FROM kv_store WHERE key = ?", (f'captcha_active_{player_id}', ) ).fetchone() conn.close() active = bool(row and row['value'] == '1') return jsonify({'captcha_active': active}) # ------------------------------------------------------------------ # GET /dashboard/commands # Returns command history (last 50) for the log panel. # ------------------------------------------------------------------ @dashboard.route('/dashboard/commands', methods=['GET']) def get_commands(): player_id = request.args.get('player_id') conn = get_db() rows = conn.execute(''' SELECT id, town_id, town_name, type, payload, status, result_msg, created_at, updated_at FROM commands WHERE player_id = ? ORDER BY id DESC LIMIT 50 ''', (player_id, )).fetchall() conn.close() return jsonify([dict(r) for r in rows]) # ------------------------------------------------------------------ # POST /dashboard/commands # Dashboard sends a new command. # Body: { town_id, town_name, type: 'build'|'recruit', payload: {...} } # ------------------------------------------------------------------ @dashboard.route('/dashboard/commands', methods=['POST']) def create_command(): data = request.get_json(silent=True) if not data: return jsonify({'error': 'no data'}), 400 required = ['town_id', 'type', 'payload', 'player_id'] for field in required: if field not in data: return jsonify({'error': f'missing field: {field}'}), 400 cmd_type = data['type'] if cmd_type not in ('build', 'recruit', 'market_offer', 'farm_loot', 'farm_upgrade', 'research'): return jsonify({'error': 'type must be build, recruit, market_offer, farm_loot, farm_upgrade, or research'}), 400 # Reject if the Tampermonkey client is offline (no state push in last 150 s) conn = get_db() row = conn.execute('SELECT MAX(updated_at) AS last_seen FROM town_state WHERE player_id = ?', (data['player_id'], )).fetchone() last_seen = row['last_seen'] if row else None client_online = False if last_seen: try: dt = datetime.fromisoformat(last_seen) client_online = (datetime.utcnow() - dt).total_seconds() <= 150 except Exception: pass if not client_online: conn.close() return jsonify({'error': 'client_offline', 'message': 'Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'}), 503 c = conn.cursor() c.execute(''' INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id) VALUES (?, ?, ?, ?, 'pending', ?, ?, ?) ''', ( str(data['town_id']), data.get('town_name', ''), cmd_type, json.dumps(data['payload']), datetime.utcnow().isoformat(), datetime.utcnow().isoformat(), str(data['player_id']) )) cmd_id = c.lastrowid conn.commit() conn.close() return jsonify({'ok': True, 'id': cmd_id}) # ------------------------------------------------------------------ # DELETE /dashboard/commands/ # Fully delete a command from the dashboard history. # ------------------------------------------------------------------ @dashboard.route('/dashboard/commands/', methods=['DELETE']) def cancel_command(cmd_id): conn = get_db() conn.execute('DELETE FROM commands WHERE id = ?', (cmd_id,)) conn.commit() conn.close() return jsonify({'ok': True}) # ------------------------------------------------------------------ # POST /dashboard/commands/fail-stale # Mark all pending/executing commands as failed (called by dashboard # when it detects the client has gone offline). # ------------------------------------------------------------------ @dashboard.route('/dashboard/commands/fail-stale', methods=['POST']) def fail_stale_commands(): player_id = request.args.get('player_id') conn = get_db() c = conn.cursor() c.execute(''' UPDATE commands SET status = 'failed', result_msg = 'Client went offline', updated_at = ? WHERE status IN ('pending', 'executing') AND player_id = ? ''', (datetime.utcnow().isoformat(), player_id)) affected = c.rowcount conn.commit() conn.close() return jsonify({'ok': True, 'failed': affected})