diff --git a/db.py b/db.py index 28d8bd0..4577d7f 100644 --- a/db.py +++ b/db.py @@ -25,6 +25,7 @@ def init_db(): payload TEXT NOT NULL, -- JSON string status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed result_msg TEXT, + position INTEGER, -- manual sort order for build queue (lower = first) created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) @@ -75,6 +76,7 @@ def init_db(): 'ALTER TABLE town_state ADD COLUMN y REAL', 'ALTER TABLE town_state ADD COLUMN sea INTEGER', 'ALTER TABLE commands ADD COLUMN player_id TEXT', + 'ALTER TABLE commands ADD COLUMN position INTEGER', 'ALTER TABLE farm_settings ADD COLUMN bandit_camp_enabled INTEGER NOT NULL DEFAULT 0', "ALTER TABLE clan_members ADD COLUMN features TEXT NOT NULL DEFAULT 'farm,admin'", 'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)', @@ -84,6 +86,12 @@ def init_db(): except Exception: pass # column already exists + # Back-fill position for existing rows that have NULL position + try: + c.execute('UPDATE commands SET position = id WHERE position IS NULL') + except Exception: + pass + # Users — website admin accounts c.execute(''' CREATE TABLE IF NOT EXISTS users ( diff --git a/routes/api.py b/routes/api.py index 5774002..2e1b59c 100644 --- a/routes/api.py +++ b/routes/api.py @@ -152,7 +152,7 @@ def _fetch_pending_builds_all_towns(c, player_id): SELECT * FROM commands WHERE status = 'pending' AND type = 'build' AND player_id = ? AND town_id = ? - ORDER BY updated_at ASC, id ASC + ORDER BY position ASC, id ASC LIMIT 1 ''', (player_id, town_id)).fetchone() if not row: diff --git a/routes/dashboard.py b/routes/dashboard.py index 3eaa565..e22babc 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -269,6 +269,54 @@ def captcha_status(): 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. @@ -326,14 +374,26 @@ def create_command(): 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, created_at, updated_at, player_id) - VALUES (?, ?, ?, ?, 'pending', ?, ?, ?) + 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']) diff --git a/static/js/api.js b/static/js/api.js index a4a7259..795a8dd 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -19,6 +19,10 @@ window.fetchTowns = async function() { window.renderBuildingDropdown(); window.renderUnitDropdown(); window.renderTownDetails(); + // Refresh the build queue panel if in queue mode + if (window._logPanelMode === 'queue') { + window.fetchBuildQueue(window.selectedTownId); + } } } catch (e) { window.updateServerStatus(false); @@ -51,7 +55,9 @@ window.fetchLog = async function() { const res = await fetch('/dashboard/commands?player_id=' + window.PLAYER_ID); const cmds = await res.json(); window.cmds = cmds; // Save globally so viewer can see reserved resources - window.renderLog(cmds); + if (window._logPanelMode === 'log') { + window.renderLog(cmds); + } if (window.selectedTownId) window.renderTownDetails(); } catch (e) {} }; @@ -192,7 +198,12 @@ window.sendCommand = async function() { }); const data = await res.json(); if (data.ok) { - window.fetchLog(); + // Refresh whichever panel is active + if (type === 'build' && window._logPanelMode === 'queue') { + window.fetchBuildQueue(town.town_id); + } else { + window.fetchLog(); + } } else if (data.error === 'client_offline') { alert(data.message || 'Το script είναι offline.'); } else { diff --git a/static/js/app.js b/static/js/app.js index c75e31d..3178f9d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -4,11 +4,15 @@ window.addEventListener('DOMContentLoaded', () => { window.fetchTowns(); - window.fetchLog(); + window.fetchLog(); // pre-loads cmds globally even in queue mode window.fetchClientStatus(); window.fetchCaptchaStatus(); setInterval(window.fetchTowns, window.POLL_INTERVAL); - setInterval(window.fetchLog, window.POLL_INTERVAL); + // In log mode: fetchLog refreshes the panel. In queue mode: refreshLogPanel polls the queue. + setInterval(() => { + window.fetchLog(); // always keep cmds cache fresh for resource display + window.refreshLogPanel(); // refresh whichever panel is visible + }, window.POLL_INTERVAL); setInterval(window.fetchClientStatus, window.POLL_INTERVAL); setInterval(window.fetchCaptchaStatus, 5000); // check every 5s }); diff --git a/static/js/components/commandLog.js b/static/js/components/commandLog.js index 7af55e9..20befc4 100644 --- a/static/js/components/commandLog.js +++ b/static/js/components/commandLog.js @@ -1,11 +1,171 @@ // ================================================================ -// Command Log Component +// Command Log & Build Queue Component // ================================================================ +// -- Panel state: 'queue' | 'log' ---------------------------- +window._logPanelMode = 'queue'; + +// ---- Toggle buttons ------------------------------------------- +window.switchToQueueMode = function() { + window._logPanelMode = 'queue'; + document.getElementById('tab-queue').classList.add('tab-active'); + document.getElementById('tab-log').classList.remove('tab-active'); + window.refreshLogPanel(); +}; + +window.switchToLogMode = function() { + window._logPanelMode = 'log'; + document.getElementById('tab-log').classList.add('tab-active'); + document.getElementById('tab-queue').classList.remove('tab-active'); + window.fetchLog(); +}; + +// ---- Main dispatcher ------------------------------------------ +window.refreshLogPanel = function() { + if (window._logPanelMode === 'queue') { + const town = window.getSelectedTown(); + if (town) { + window.fetchBuildQueue(town.town_id); + } else { + document.getElementById('log-content').innerHTML = + '
← Επιλέξτε πόλη για να δείτε την ουρά.
'; + } + } +}; + +// ================================================================ +// BUILD QUEUE (per-town, draggable) +// ================================================================ +window.fetchBuildQueue = async function(townId) { + if (window._logPanelMode !== 'queue') return; + try { + const res = await fetch(`/dashboard/commands/queue?player_id=${window.PLAYER_ID}&town_id=${encodeURIComponent(townId)}`); + const cmds = await res.json(); + window.renderBuildQueue(cmds, townId); + } catch(e) {} +}; + +// Drag state +let _dragSrcIdx = null; + +window.renderBuildQueue = function(cmds, townId) { + const el = document.getElementById('log-content'); + + if (!cmds || cmds.length === 0) { + el.innerHTML = ` +Η ουρά κατασκευών είναι κενή.
+Χρησιμοποιήστε την φόρμα για να προσθέσετε κατασκευές.
+No commands sent yet.
'; + el.innerHTML = 'No commands sent yet.
'; return; } @@ -20,12 +180,13 @@ window.renderLog = function(cmds) { desc = `Recruit: ${p.amount}x ${nameGr}`; } else if (cmd.type === 'market_offer') { desc = `Market: ${p.offer} ${p.offer_type} ➞ ${p.demand} ${p.demand_type}`; + } else { + desc = cmd.type; } const statusClass = `status-${cmd.status}`; const cancelBtn = ``; - const timeStr = new Date(cmd.created_at + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); - + return `No commands sent yet.
+← Επιλέξτε πόλη για να δείτε την ουρά.