diff --git a/GrepolisRemoteControl.user.js b/GrepolisRemoteControl.user.js
new file mode 100644
index 0000000..f60ec63
--- /dev/null
+++ b/GrepolisRemoteControl.user.js
@@ -0,0 +1,269 @@
+// ==UserScript==
+// @name Grepolis Remote Control
+// @namespace http://tampermonkey.net/
+// @version 1.0
+// @description Polls grepo.haunter-pets.top for remote commands and executes them in-game
+// @author Dimitrios
+// @match https://*.grepolis.com/game/*
+// @grant unsafeWindow
+// ==/UserScript==
+
+(function () {
+ 'use strict';
+
+ const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
+ const BASE_URL = 'https://grepo.haunter-pets.top';
+ const POLL_INTERVAL_MS = 5000; // poll for commands
+ const STATE_INTERVAL_MS = 30000; // push town state
+
+ // ----------------------------------------------------------------
+ // Toolbar indicator button
+ // ----------------------------------------------------------------
+ const btnHtml = `
+
+ `;
+
+ let paused = false;
+
+ function togglePause() {
+ paused = !paused;
+ const label = document.getElementById('grc_label');
+ const btn = document.getElementById('grc_btn');
+ if (paused) {
+ label.textContent = 'Paused';
+ btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(-50deg) saturate(1000%) contrast(0.8)';
+ } else {
+ label.textContent = 'Remote';
+ btn.style.filter = 'brightness(294%) sepia(100%) hue-rotate(200deg) saturate(1000%) contrast(0.8)';
+ }
+ log(`Remote is now ${paused ? 'PAUSED' : 'ACTIVE'}`);
+ }
+
+ setTimeout(() => {
+ if (!document.getElementById('grc_btn')) {
+ uw.$('.tb_activities, .toolbar_activities').find('.middle').append(btnHtml);
+ }
+ }, 4000);
+
+ uw.$(document).on('click', '#grc_btn', togglePause);
+
+ // ----------------------------------------------------------------
+ // Helpers
+ // ----------------------------------------------------------------
+ function log(msg) {
+ console.log(`[GRC] ${msg}`);
+ }
+
+ function sleep(ms) {
+ return new Promise(r => setTimeout(r, ms));
+ }
+
+ // ----------------------------------------------------------------
+ // Push town state to relay
+ // ----------------------------------------------------------------
+ function gatherState() {
+ const towns = uw.ITowns?.towns || {};
+ const player = uw.Game?.player_name || '';
+ const world = uw.Game?.world_id || '';
+
+ const townList = Object.values(towns).map(town => {
+ const res = town.resources();
+ const buildings = town.buildings()?.attributes ?? {};
+
+ const unitsObj = {};
+ try {
+ const units = town.units();
+ if (units) {
+ Object.keys(units).forEach(k => {
+ unitsObj[k] = typeof units[k] === 'number'
+ ? units[k]
+ : (units[k]?.getAmount?.() ?? 0);
+ });
+ }
+ } catch (e) {}
+
+ let buildQueue = [];
+ try {
+ const bo = town.buildingOrders?.();
+ if (bo?.models) buildQueue = bo.models.map(m => m.attributes);
+ } catch (e) {}
+
+ return {
+ town_id: town.id,
+ town_name: town.name,
+ wood: res.wood,
+ stone: res.stone,
+ iron: res.iron,
+ population: res.population,
+ points: town.getPoints?.() ?? 0,
+ god: town.god?.() ?? null,
+ buildings,
+ units: unitsObj,
+ buildingOrder: buildQueue,
+ };
+ });
+
+ return { player, world_id: world, towns: townList };
+ }
+
+ function pushState() {
+ if (paused) return;
+ try {
+ const payload = gatherState();
+ fetch(`${BASE_URL}/api/state`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ })
+ .then(() => log(`State pushed — ${payload.towns.length} towns`))
+ .catch(e => log(`State push failed: ${e}`));
+ } catch (e) {
+ log(`gatherState error: ${e}`);
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // Report command result back to relay
+ // ----------------------------------------------------------------
+ function reportResult(cmdId, success, message) {
+ fetch(`${BASE_URL}/api/commands/${cmdId}/result`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ status: success ? 'done' : 'failed', message })
+ }).catch(e => log(`reportResult failed: ${e}`));
+ }
+
+ // ----------------------------------------------------------------
+ // Execute: Build
+ // ----------------------------------------------------------------
+ async function executeBuild(cmd) {
+ const { town_id, payload } = cmd;
+ const { building_id } = payload;
+
+ const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
+ if (!town) {
+ return { ok: false, msg: `Town ${town_id} not found in ITowns` };
+ }
+
+ // Check build queue
+ const queueLen = town.buildingOrders?.()?.length ?? 0;
+ const hasCurator = uw.GameDataPremium?.isAdvisorActivated?.('curator');
+ const maxQueue = hasCurator ? 7 : 2;
+ if (queueLen >= maxQueue) {
+ return { ok: false, msg: `Build queue full (${queueLen}/${maxQueue})` };
+ }
+
+ // Check resources
+ try {
+ const buildData = uw.MM.getModels?.()?.BuildingBuildData?.[town_id]
+ ?.attributes?.building_data?.[building_id];
+ if (buildData) {
+ const res = town.resources();
+ const { resources_for, population_for } = buildData;
+ if (town.getAvailablePopulation?.() < population_for) {
+ return { ok: false, msg: `Not enough population for ${building_id}` };
+ }
+ if (res.wood < resources_for.wood || res.stone < resources_for.stone || res.iron < resources_for.iron) {
+ return { ok: false, msg: `Not enough resources for ${building_id}` };
+ }
+ }
+ } catch (e) {
+ log(`Resource check skipped: ${e}`);
+ }
+
+ // Fire the build request
+ uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
+ model_url: 'BuildingOrder',
+ action_name: 'buildUp',
+ arguments: { building_id },
+ town_id
+ });
+
+ await sleep(500);
+ return { ok: true, msg: `buildUp ${building_id} queued` };
+ }
+
+ // ----------------------------------------------------------------
+ // Execute: Recruit
+ // ----------------------------------------------------------------
+ async function executeRecruit(cmd) {
+ const { town_id, payload } = cmd;
+ const { unit_id, amount } = payload;
+
+ const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
+ if (!town) {
+ return { ok: false, msg: `Town ${town_id} not found` };
+ }
+
+ // Determine endpoint based on unit type
+ const navalUnits = [
+ 'big_transporter', 'small_transporter', 'bireme',
+ 'attack_ship', 'trireme', 'colonize_ship', 'sea_monster'
+ ];
+ const endpoint = navalUnits.includes(unit_id) ? 'building_docks' : 'building_barracks';
+
+ uw.gpAjax.ajaxPost(endpoint, 'build', {
+ unit_id,
+ amount: parseInt(amount) || 1,
+ town_id
+ });
+
+ await sleep(500);
+ return { ok: true, msg: `Recruit ${amount}x ${unit_id} submitted` };
+ }
+
+ // ----------------------------------------------------------------
+ // Poll for and execute a pending command
+ // ----------------------------------------------------------------
+ async function pollAndExecute() {
+ if (paused) return;
+
+ let cmdData;
+ try {
+ const res = await fetch(`${BASE_URL}/api/commands/pending`);
+ cmdData = await res.json();
+ } catch (e) {
+ log(`Poll failed: ${e}`);
+ return;
+ }
+
+ const cmd = cmdData.command;
+ if (!cmd) return; // nothing pending
+
+ log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`);
+
+ let result;
+ try {
+ if (cmd.type === 'build') {
+ result = await executeBuild(cmd);
+ } else if (cmd.type === 'recruit') {
+ result = await executeRecruit(cmd);
+ } else {
+ result = { ok: false, msg: `Unknown command type: ${cmd.type}` };
+ }
+ } catch (e) {
+ result = { ok: false, msg: `Exception: ${e.message}` };
+ }
+
+ log(`Command #${cmd.id} result: ${result.ok ? '✅' : '❌'} ${result.msg}`);
+ reportResult(cmd.id, result.ok, result.msg);
+ }
+
+ // ----------------------------------------------------------------
+ // Boot
+ // ----------------------------------------------------------------
+ window.addEventListener('load', () => {
+ log('Grepolis Remote Control loaded');
+
+ // Push state immediately, then on interval
+ setTimeout(pushState, 5000);
+ setInterval(pushState, STATE_INTERVAL_MS);
+
+ // Poll for commands
+ setInterval(pollAndExecute, POLL_INTERVAL_MS);
+ });
+
+})();
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..7046ea9
--- /dev/null
+++ b/app.py
@@ -0,0 +1,16 @@
+from flask import Flask
+from flask_cors import CORS
+from db import init_db
+from routes.api import api
+from routes.dashboard import dashboard
+
+app = Flask(__name__)
+CORS(app) # Allow cross-origin requests from the Tampermonkey script
+
+app.register_blueprint(api)
+app.register_blueprint(dashboard)
+
+if __name__ == '__main__':
+ init_db()
+ print("✅ Grepolis Remote — DB initialised")
+ app.run(host='0.0.0.0', port=5050, debug=True)
diff --git a/db.py b/db.py
new file mode 100644
index 0000000..b82f1c9
--- /dev/null
+++ b/db.py
@@ -0,0 +1,45 @@
+import sqlite3
+import os
+
+DB_PATH = os.path.join(os.path.dirname(__file__), 'grepo.db')
+
+
+def get_db():
+ conn = sqlite3.connect(DB_PATH)
+ conn.row_factory = sqlite3.Row
+ return conn
+
+
+def init_db():
+ conn = get_db()
+ c = conn.cursor()
+
+ # Commands queue — sent from dashboard, consumed by Tampermonkey
+ c.execute('''
+ CREATE TABLE IF NOT EXISTS commands (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ town_id TEXT NOT NULL,
+ town_name TEXT,
+ type TEXT NOT NULL, -- 'build' | 'recruit'
+ payload TEXT NOT NULL, -- JSON string
+ status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed
+ result_msg TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
+ )
+ ''')
+
+ # Town state — pushed by Tampermonkey every poll cycle
+ c.execute('''
+ CREATE TABLE IF NOT EXISTS town_state (
+ town_id TEXT PRIMARY KEY,
+ town_name TEXT,
+ player TEXT,
+ world_id TEXT,
+ data TEXT NOT NULL, -- full JSON snapshot
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
+ )
+ ''')
+
+ conn.commit()
+ conn.close()
diff --git a/grepo.db b/grepo.db
new file mode 100644
index 0000000..38c9f30
Binary files /dev/null and b/grepo.db differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..f0861ea
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+flask
+flask-cors