from flask import Blueprint, render_template, request, jsonify from db import get_db import json from datetime import datetime, timedelta dashboard = Blueprint('dashboard', __name__) # ------------------------------------------------------------------ # GET / # Serve the dashboard HTML # ------------------------------------------------------------------ @dashboard.route('/') def index(): return render_template('dashboard.html') # ------------------------------------------------------------------ # GET /dashboard/towns # Returns all known towns with their latest state snapshot. # ------------------------------------------------------------------ @dashboard.route('/dashboard/towns', methods=['GET']) def get_towns(): 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 ORDER BY town_name ASC ''').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), '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), }) 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(): conn = get_db() row = conn.execute(''' SELECT MAX(updated_at) AS last_seen FROM town_state ''').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(): conn = get_db() row = conn.execute( "SELECT value FROM kv_store WHERE key = 'captcha_active'" ).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(): conn = get_db() rows = conn.execute(''' SELECT id, town_id, town_name, type, payload, status, result_msg, created_at, updated_at FROM commands ORDER BY id DESC LIMIT 50 ''').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'] 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'): return jsonify({'error': 'type must be build or recruit'}), 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').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) VALUES (?, ?, ?, ?, 'pending', ?, ?) ''', ( str(data['town_id']), data.get('town_name', ''), cmd_type, json.dumps(data['payload']), datetime.utcnow().isoformat(), datetime.utcnow().isoformat() )) 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(): 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') ''', (datetime.utcnow().isoformat(),)) affected = c.rowcount conn.commit() conn.close() return jsonify({'ok': True, 'failed': affected})