diff --git a/routes/api.py b/routes/api.py new file mode 100644 index 0000000..4006f46 --- /dev/null +++ b/routes/api.py @@ -0,0 +1,104 @@ +from flask import Blueprint, request, jsonify +from db import get_db +import json +from datetime import datetime + +api = Blueprint('api', __name__) + + +# ------------------------------------------------------------------ +# POST /api/state +# Tampermonkey pushes a full town snapshot every poll cycle. +# ------------------------------------------------------------------ +@api.route('/api/state', methods=['POST']) +def receive_state(): + data = request.get_json(silent=True) + if not data: + return jsonify({'error': 'no data'}), 400 + + towns = data.get('towns', []) + player = data.get('player', '') + world_id = data.get('world_id', '') + + conn = get_db() + c = conn.cursor() + for town in towns: + c.execute(''' + INSERT INTO town_state (town_id, town_name, player, world_id, data, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(town_id) DO UPDATE SET + town_name = excluded.town_name, + player = excluded.player, + world_id = excluded.world_id, + data = excluded.data, + updated_at = excluded.updated_at + ''', ( + str(town['town_id']), + town.get('town_name', ''), + player, + world_id, + json.dumps(town), + datetime.utcnow().isoformat() + )) + conn.commit() + conn.close() + return jsonify({'ok': True, 'towns_updated': len(towns)}) + + +# ------------------------------------------------------------------ +# GET /api/commands/pending +# Tampermonkey polls this to get the next command to execute. +# Returns ONE command at a time, marks it as 'executing'. +# ------------------------------------------------------------------ +@api.route('/api/commands/pending', methods=['GET']) +def get_pending_command(): + conn = get_db() + c = conn.cursor() + row = c.execute(''' + SELECT * FROM commands + WHERE status = 'pending' + ORDER BY id ASC + LIMIT 1 + ''').fetchone() + + if not row: + conn.close() + return jsonify({'command': None}) + + c.execute(''' + UPDATE commands + SET status = 'executing', updated_at = ? + WHERE id = ? + ''', (datetime.utcnow().isoformat(), row['id'])) + conn.commit() + conn.close() + + return jsonify({ + 'command': { + 'id': row['id'], + 'town_id': row['town_id'], + 'type': row['type'], + 'payload': json.loads(row['payload']) + } + }) + + +# ------------------------------------------------------------------ +# POST /api/commands//result +# Tampermonkey reports back whether the command succeeded or failed. +# ------------------------------------------------------------------ +@api.route('/api/commands//result', methods=['POST']) +def command_result(cmd_id): + data = request.get_json(silent=True) or {} + status = data.get('status', 'done') # 'done' | 'failed' + msg = data.get('message', '') + + conn = get_db() + conn.execute(''' + UPDATE commands + SET status = ?, result_msg = ?, updated_at = ? + WHERE id = ? + ''', (status, msg, datetime.utcnow().isoformat(), cmd_id)) + conn.commit() + conn.close() + return jsonify({'ok': True}) diff --git a/routes/dashboard.py b/routes/dashboard.py new file mode 100644 index 0000000..65a7b45 --- /dev/null +++ b/routes/dashboard.py @@ -0,0 +1,127 @@ +from flask import Blueprint, render_template, request, jsonify +from db import get_db +import json +from datetime import datetime + +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, world_id, 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'], + 'world_id': row['world_id'], + 'updated_at': row['updated_at'], + 'resources': { + 'wood': d.get('wood', 0), + 'stone': d.get('stone', 0), + 'iron': d.get('iron', 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', []), + }) + return jsonify(towns) + + +# ------------------------------------------------------------------ +# 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 + + conn = get_db() + 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/ +# Cancel a pending command from the dashboard. +# ------------------------------------------------------------------ +@dashboard.route('/dashboard/commands/', methods=['DELETE']) +def cancel_command(cmd_id): + conn = get_db() + conn.execute(''' + UPDATE commands SET status = 'cancelled', updated_at = ? + WHERE id = ? AND status = 'pending' + ''', (datetime.utcnow().isoformat(), cmd_id)) + conn.commit() + conn.close() + return jsonify({'ok': True})