Files
grepo-remote/routes/dashboard.py
2026-05-01 02:37:46 +03:00

566 lines
21 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 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/<player_id>')
@login_required
def player_hub(player_id):
return render_template('hub.html', player_id=player_id)
@dashboard.route('/player/<player_id>/admin')
@login_required
def player_dashboard(player_id):
return render_template('dashboard.html', player_id=player_id)
@dashboard.route('/player/<player_id>/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/queue
# Returns pending+executing BUILD commands for a specific town,
# ordered by their manual position (for the per-town build queue UI).
# ------------------------------------------------------------------
@dashboard.route('/dashboard/commands/queue', methods=['GET'])
def get_town_build_queue():
player_id = request.args.get('player_id')
town_id = request.args.get('town_id')
conn = get_db()
rows = conn.execute('''
SELECT id, town_id, town_name, type, payload, status, result_msg, position, created_at, updated_at
FROM commands
WHERE player_id = ? AND town_id = ? AND type = 'build'
AND status IN ('pending', 'executing')
ORDER BY position ASC, id ASC
''', (player_id, town_id)).fetchall()
conn.close()
return jsonify([dict(r) for r in rows])
# ------------------------------------------------------------------
# POST /dashboard/commands/reorder
# Accepts { player_id, town_id, order: [id1, id2, ...] }
# and updates position for each command in the list.
# ------------------------------------------------------------------
@dashboard.route('/dashboard/commands/reorder', methods=['POST'])
def reorder_commands():
data = request.get_json(silent=True) or {}
player_id = data.get('player_id')
town_id = data.get('town_id')
order = data.get('order', []) # list of command ids in desired order
if not player_id or not town_id or not order:
return jsonify({'error': 'missing player_id, town_id or order'}), 400
conn = get_db()
for idx, cmd_id in enumerate(order):
conn.execute('''
UPDATE commands
SET position = ?
WHERE id = ? AND player_id = ? AND town_id = ?
''', (idx + 1, cmd_id, player_id, str(town_id)))
conn.commit()
conn.close()
return jsonify({'ok': True})
# ------------------------------------------------------------------
# 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()
# Assign position = one more than the current max for this town's pending build queue
if cmd_type == 'build':
pos_row = c.execute(
"SELECT MAX(position) as max_pos FROM commands"
" WHERE player_id = ? AND town_id = ? AND type = 'build'"
" AND status IN ('pending', 'executing')",
(str(data['player_id']), str(data['town_id']))
).fetchone()
position = (pos_row['max_pos'] or 0) + 1
else:
position = None
c.execute('''
INSERT INTO commands (town_id, town_name, type, payload, status, position, created_at, updated_at, player_id)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
''', (
str(data['town_id']),
data.get('town_name', ''),
cmd_type,
json.dumps(data['payload']),
position,
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})
# ------------------------------------------------------------------
# GET /dashboard/bot-settings — fetch bootcamp + rural trade config
# POST /dashboard/bot-settings — save config
# ------------------------------------------------------------------
@dashboard.route('/dashboard/bot-settings', methods=['GET', 'POST'])
def bot_settings():
player_id = request.args.get('player_id') or (request.json or {}).get('player_id')
if not player_id:
return jsonify({'error': 'missing player_id'}), 400
conn = get_db()
c = conn.cursor()
if request.method == 'GET':
row = c.execute(
'SELECT * FROM bot_settings WHERE player_id = ?', (player_id,)
).fetchone()
conn.close()
if row:
return jsonify(dict(row))
return jsonify({
'player_id': player_id,
'bootcamp_enabled': 0,
'bootcamp_use_def': 0,
'rural_trade_enabled': 0,
'rural_trade_ratio': 3,
})
# POST — upsert
data = request.json or {}
c.execute('''
INSERT INTO bot_settings (player_id, bootcamp_enabled, bootcamp_use_def,
rural_trade_enabled, rural_trade_ratio, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(player_id) DO UPDATE SET
bootcamp_enabled = excluded.bootcamp_enabled,
bootcamp_use_def = excluded.bootcamp_use_def,
rural_trade_enabled = excluded.rural_trade_enabled,
rural_trade_ratio = excluded.rural_trade_ratio,
updated_at = excluded.updated_at
''', (
player_id,
int(bool(data.get('bootcamp_enabled', 0))),
int(bool(data.get('bootcamp_use_def', 0))),
int(bool(data.get('rural_trade_enabled', 0))),
int(data.get('rural_trade_ratio', 3)),
datetime.utcnow().isoformat()
))
conn.commit()
conn.close()
return jsonify({'ok': True})
# ------------------------------------------------------------------
# GET /dashboard/bot-logs?player_id=&feature= — last 50 log lines
# POST /dashboard/bot-logs — append + prune
# ------------------------------------------------------------------
@dashboard.route('/dashboard/bot-logs', methods=['GET', 'POST'])
def bot_logs():
player_id = request.args.get('player_id') or (request.json or {}).get('player_id')
if not player_id:
return jsonify({'error': 'missing player_id'}), 400
conn = get_db()
c = conn.cursor()
if request.method == 'GET':
feature = request.args.get('feature', '')
query = 'SELECT * FROM bot_logs WHERE player_id = ?'
params = [player_id]
if feature:
query += ' AND feature = ?'
params.append(feature)
query += ' ORDER BY id DESC LIMIT 50'
rows = c.execute(query, params).fetchall()
conn.close()
return jsonify([dict(r) for r in rows])
# POST — append entry and prune to last 50
data = request.json or {}
feature = data.get('feature', 'bootcamp')
message = data.get('message', '')
c.execute(
'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)',
(player_id, feature, message)
)
# Prune: keep only the latest 50 per player/feature
c.execute('''
DELETE FROM bot_logs
WHERE player_id = ? AND feature = ?
AND id NOT IN (
SELECT id FROM bot_logs
WHERE player_id = ? AND feature = ?
ORDER BY id DESC LIMIT 50
)
''', (player_id, feature, player_id, feature))
conn.commit()
conn.close()
return jsonify({'ok': True})
# ------------------------------------------------------------------
# POST /dashboard/bootcamp-attack-now
# Sets a one-shot flag consumed by the TM bot on the next poll.
# ------------------------------------------------------------------
@dashboard.route('/dashboard/bootcamp-attack-now', methods=['POST'])
def bootcamp_attack_now():
player_id = (request.json or {}).get('player_id')
if not player_id:
return jsonify({'error': 'missing player_id'}), 400
key = f'bootcamp_attack_now_{player_id}'
conn = get_db()
conn.execute('''
INSERT INTO kv_store (key, value, updated_at) VALUES (?, '1', ?)
ON CONFLICT(key) DO UPDATE SET value='1', updated_at=excluded.updated_at
''', (key, datetime.utcnow().isoformat()))
conn.commit()
conn.close()
return jsonify({'ok': True})