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 = ` +
+
🏗️
+

Η ουρά κατασκευών είναι κενή.

+

Χρησιμοποιήστε την φόρμα για να προσθέσετε κατασκευές.

+
`; + return; + } + + const rows = cmds.map((cmd, idx) => { + const p = typeof cmd.payload === 'string' ? JSON.parse(cmd.payload) : cmd.payload; + const nameGr = window.BUILDING_NAMES_GR?.[p.building_id] || p.building_id || '?'; + const icon = window.BUILDING_ICONS?.[p.building_id] || '🏗️'; + const isExec = cmd.status === 'executing'; + + const statusDot = isExec + ? `` + : ``; + + return ` +
+ + ${idx + 1} + ${statusDot} + ${icon} + ${nameGr} + +
`; + }).join(''); + + el.innerHTML = `
${rows}
`; +}; + +// ---- Drag-and-drop handlers ----------------------------------- +window._bqDragStart = function(e, idx) { + _dragSrcIdx = idx; + e.dataTransfer.effectAllowed = 'move'; + setTimeout(() => { + const rows = document.querySelectorAll('.bq-row'); + if (rows[idx]) rows[idx].style.opacity = '0.4'; + }, 0); +}; + +window._bqDragOver = function(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + // Highlight target row + document.querySelectorAll('.bq-row').forEach(r => r.classList.remove('bq-drag-over')); + const row = e.currentTarget; + if (row) row.classList.add('bq-drag-over'); +}; + +window._bqDrop = function(e, targetIdx, townId) { + e.preventDefault(); + e.stopPropagation(); + if (_dragSrcIdx === null || _dragSrcIdx === targetIdx) return; + + // Re-order the DOM + const list = document.getElementById('bq-list'); + const rows = Array.from(list.querySelectorAll('.bq-row')); + const movedRow = rows.splice(_dragSrcIdx, 1)[0]; + rows.splice(targetIdx, 0, movedRow); + + // Update numbering & opacity + rows.forEach((r, i) => { + r.style.opacity = '1'; + r.classList.remove('bq-drag-over'); + r.dataset.idx = i; + r.querySelector('.bq-pos').textContent = i + 1; + r.setAttribute('ondragstart', `window._bqDragStart(event,${i})`); + r.setAttribute('ondrop', `window._bqDrop(event,${i},'${townId}')`); + }); + list.innerHTML = ''; + rows.forEach(r => list.appendChild(r)); + + // Persist new order to server + const orderedIds = rows.map(r => parseInt(r.dataset.id)); + fetch('/dashboard/commands/reorder', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ player_id: window.PLAYER_ID, town_id: townId, order: orderedIds }) + }); + + _dragSrcIdx = null; +}; + +window._bqDragEnd = function(e) { + document.querySelectorAll('.bq-row').forEach(r => { + r.style.opacity = '1'; + r.classList.remove('bq-drag-over'); + }); + _dragSrcIdx = null; +}; + +window._bqCancel = async function(id) { + await fetch(`/dashboard/commands/${id}`, { method: 'DELETE' }); + // Refresh the queue for the currently selected town + const town = window.getSelectedTown(); + if (town) window.fetchBuildQueue(town.town_id); +}; + +// ================================================================ +// COMMAND LOG (full history, existing behaviour) +// ================================================================ window.renderLog = function(cmds) { + if (window._logPanelMode !== 'log') return; const el = document.getElementById('log-content'); if (!cmds.length) { - 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 ` #${cmd.id}
${timeStr} ${cmd.town_name || cmd.town_id} diff --git a/static/js/components/townViewer.js b/static/js/components/townViewer.js index 788e183..f16678d 100644 --- a/static/js/components/townViewer.js +++ b/static/js/components/townViewer.js @@ -99,6 +99,10 @@ window.selectTown = function(id) { window.renderBuildingDropdown(); window.renderUnitDropdown(); window.renderTownDetails(); + // Refresh build queue panel for the newly selected town + if (window._logPanelMode === 'queue') { + window.fetchBuildQueue(id); + } }; window.getSelectedTown = function() { diff --git a/templates/dashboard.html b/templates/dashboard.html index e4d89a1..d249f85 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -219,11 +219,15 @@ - +
-

Command Log

+
+

Ουρά Κατασκευών

+ + +
-

No commands sent yet.

+

← Επιλέξτε πόλη για να δείτε την ουρά.

@@ -254,14 +258,78 @@ + - - - - - - + + + + + + diff --git a/templates/farm.html b/templates/farm.html index f0ac4e0..1106920 100644 --- a/templates/farm.html +++ b/templates/farm.html @@ -243,7 +243,7 @@
-