Files
grepo-remote/routes/dashboard.py

264 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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():
conn = get_db()
rows = conn.execute('''
SELECT player, player_id, MAX(updated_at) as last_seen, MAX(world_id) as world_id
FROM town_state
WHERE player IS NOT NULL
GROUP BY player, player_id
ORDER BY player ASC
''').fetchall()
# Pre-fetch all active captchas
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)
@dashboard.route('/player/<player_id>')
def player_dashboard(player_id):
return render_template('dashboard.html', player_id=player_id)
# ------------------------------------------------------------------
# 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),
'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():
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'):
return jsonify({'error': 'type must be build, recruit, or market_offer'}), 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/<id>
# Fully delete a command from the dashboard history.
# ------------------------------------------------------------------
@dashboard.route('/dashboard/commands/<int:cmd_id>', 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})