admin order line

This commit is contained in:
2026-05-01 01:13:18 +03:00
parent f250fbd5b6
commit 76ad37c1db
9 changed files with 337 additions and 22 deletions

8
db.py
View File

@@ -25,6 +25,7 @@ def init_db():
payload TEXT NOT NULL, -- JSON string payload TEXT NOT NULL, -- JSON string
status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed
result_msg TEXT, result_msg TEXT,
position INTEGER, -- manual sort order for build queue (lower = first)
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_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 y REAL',
'ALTER TABLE town_state ADD COLUMN sea INTEGER', 'ALTER TABLE town_state ADD COLUMN sea INTEGER',
'ALTER TABLE commands ADD COLUMN player_id TEXT', '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 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 clan_members ADD COLUMN features TEXT NOT NULL DEFAULT 'farm,admin'",
'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)', 'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)',
@@ -84,6 +86,12 @@ def init_db():
except Exception: except Exception:
pass # column already exists 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 # Users — website admin accounts
c.execute(''' c.execute('''
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (

View File

@@ -152,7 +152,7 @@ def _fetch_pending_builds_all_towns(c, player_id):
SELECT * FROM commands SELECT * FROM commands
WHERE status = 'pending' AND type = 'build' WHERE status = 'pending' AND type = 'build'
AND player_id = ? AND town_id = ? AND player_id = ? AND town_id = ?
ORDER BY updated_at ASC, id ASC ORDER BY position ASC, id ASC
LIMIT 1 LIMIT 1
''', (player_id, town_id)).fetchone() ''', (player_id, town_id)).fetchone()
if not row: if not row:

View File

@@ -269,6 +269,54 @@ def captcha_status():
return jsonify({'captcha_active': active}) 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 # GET /dashboard/commands
# Returns command history (last 50) for the log panel. # 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 return jsonify({'error': 'client_offline', 'message': 'Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'}), 503
c = conn.cursor() 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(''' c.execute('''
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id) INSERT INTO commands (town_id, town_name, type, payload, status, position, created_at, updated_at, player_id)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?) VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
''', ( ''', (
str(data['town_id']), str(data['town_id']),
data.get('town_name', ''), data.get('town_name', ''),
cmd_type, cmd_type,
json.dumps(data['payload']), json.dumps(data['payload']),
position,
datetime.utcnow().isoformat(), datetime.utcnow().isoformat(),
datetime.utcnow().isoformat(), datetime.utcnow().isoformat(),
str(data['player_id']) str(data['player_id'])

View File

@@ -19,6 +19,10 @@ window.fetchTowns = async function() {
window.renderBuildingDropdown(); window.renderBuildingDropdown();
window.renderUnitDropdown(); window.renderUnitDropdown();
window.renderTownDetails(); window.renderTownDetails();
// Refresh the build queue panel if in queue mode
if (window._logPanelMode === 'queue') {
window.fetchBuildQueue(window.selectedTownId);
}
} }
} catch (e) { } catch (e) {
window.updateServerStatus(false); window.updateServerStatus(false);
@@ -51,7 +55,9 @@ window.fetchLog = async function() {
const res = await fetch('/dashboard/commands?player_id=' + window.PLAYER_ID); const res = await fetch('/dashboard/commands?player_id=' + window.PLAYER_ID);
const cmds = await res.json(); const cmds = await res.json();
window.cmds = cmds; // Save globally so viewer can see reserved resources 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(); if (window.selectedTownId) window.renderTownDetails();
} catch (e) {} } catch (e) {}
}; };
@@ -192,7 +198,12 @@ window.sendCommand = async function() {
}); });
const data = await res.json(); const data = await res.json();
if (data.ok) { 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') { } else if (data.error === 'client_offline') {
alert(data.message || 'Το script είναι offline.'); alert(data.message || 'Το script είναι offline.');
} else { } else {

View File

@@ -4,11 +4,15 @@
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
window.fetchTowns(); window.fetchTowns();
window.fetchLog(); window.fetchLog(); // pre-loads cmds globally even in queue mode
window.fetchClientStatus(); window.fetchClientStatus();
window.fetchCaptchaStatus(); window.fetchCaptchaStatus();
setInterval(window.fetchTowns, window.POLL_INTERVAL); 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.fetchClientStatus, window.POLL_INTERVAL);
setInterval(window.fetchCaptchaStatus, 5000); // check every 5s setInterval(window.fetchCaptchaStatus, 5000); // check every 5s
}); });

View File

@@ -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 =
'<p style="color:#555;font-size:0.85rem;padding:12px 0;">← Επιλέξτε πόλη για να δείτε την ουρά.</p>';
}
}
};
// ================================================================
// 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 = `
<div style="text-align:center;padding:2rem 1rem;color:#444;">
<div style="font-size:2rem;margin-bottom:0.5rem;">🏗️</div>
<p style="font-size:0.85rem;">Η ουρά κατασκευών είναι κενή.</p>
<p style="font-size:0.75rem;color:#333;margin-top:0.3rem;">Χρησιμοποιήστε την φόρμα για να προσθέσετε κατασκευές.</p>
</div>`;
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
? `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#4acc64;box-shadow:0 0 5px #4acc64;flex-shrink:0;" title="Εκτελείται"></span>`
: `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#555;flex-shrink:0;" title="Σε αναμονή"></span>`;
return `
<div class="bq-row" draggable="true"
data-idx="${idx}" data-id="${cmd.id}" data-town="${townId}"
ondragstart="window._bqDragStart(event,${idx})"
ondragover="window._bqDragOver(event)"
ondrop="window._bqDrop(event,${idx},'${townId}')"
ondragend="window._bqDragEnd(event)">
<span class="bq-handle" title="Σύρε για αναδιάταξη">⠿</span>
<span class="bq-pos">${idx + 1}</span>
${statusDot}
<span class="bq-icon">${icon}</span>
<span class="bq-name">${nameGr}</span>
<button class="bq-cancel-btn" onclick="window._bqCancel(${cmd.id})" title="Ακύρωση">✕</button>
</div>`;
}).join('');
el.innerHTML = `<div id="bq-list">${rows}</div>`;
};
// ---- 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) { window.renderLog = function(cmds) {
if (window._logPanelMode !== 'log') return;
const el = document.getElementById('log-content'); const el = document.getElementById('log-content');
if (!cmds.length) { if (!cmds.length) {
el.innerHTML = '<p id="empty-log">No commands sent yet.</p>'; el.innerHTML = '<p id="empty-log" style="color:#555;font-size:0.85rem;padding:12px 0;">No commands sent yet.</p>';
return; return;
} }
@@ -20,12 +180,13 @@ window.renderLog = function(cmds) {
desc = `Recruit: ${p.amount}x ${nameGr}`; desc = `Recruit: ${p.amount}x ${nameGr}`;
} else if (cmd.type === 'market_offer') { } else if (cmd.type === 'market_offer') {
desc = `Market: ${p.offer} ${p.offer_type}${p.demand} ${p.demand_type}`; desc = `Market: ${p.offer} ${p.offer_type}${p.demand} ${p.demand_type}`;
} else {
desc = cmd.type;
} }
const statusClass = `status-${cmd.status}`; const statusClass = `status-${cmd.status}`;
const cancelBtn = `<button class="btn btn-danger btn-sm" onclick="cancelCommand(${cmd.id})">✕</button>`; const cancelBtn = `<button class="btn btn-danger btn-sm" onclick="cancelCommand(${cmd.id})">✕</button>`;
const timeStr = new Date(cmd.created_at + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); const timeStr = new Date(cmd.created_at + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
return `<tr> return `<tr>
<td style="color:#888;font-size:0.75rem">#${cmd.id}<br><span style="font-size:0.65rem;color:#555;">${timeStr}</span></td> <td style="color:#888;font-size:0.75rem">#${cmd.id}<br><span style="font-size:0.65rem;color:#555;">${timeStr}</span></td>
<td>${cmd.town_name || cmd.town_id}</td> <td>${cmd.town_name || cmd.town_id}</td>

View File

@@ -99,6 +99,10 @@ window.selectTown = function(id) {
window.renderBuildingDropdown(); window.renderBuildingDropdown();
window.renderUnitDropdown(); window.renderUnitDropdown();
window.renderTownDetails(); window.renderTownDetails();
// Refresh build queue panel for the newly selected town
if (window._logPanelMode === 'queue') {
window.fetchBuildQueue(id);
}
}; };
window.getSelectedTown = function() { window.getSelectedTown = function() {

View File

@@ -219,11 +219,15 @@
</div> </div>
</div> </div>
<!-- Bottom right: Command log --> <!-- Bottom right: Build Queue / Command Log (tabbed) -->
<div id="log-panel"> <div id="log-panel">
<h2>Command Log</h2> <div style="display:flex; align-items:center; gap:8px; margin-bottom:12px; border-bottom:1px solid #2a3a5a; padding-bottom:10px;">
<h2 style="margin:0; flex:1;">Ουρά Κατασκευών</h2>
<button id="tab-queue" class="log-tab-btn tab-active" onclick="window.switchToQueueMode()">🏗️ Ουρά</button>
<button id="tab-log" class="log-tab-btn" onclick="window.switchToLogMode()">📋 Ιστορικό</button>
</div>
<div id="log-content"> <div id="log-content">
<p id="empty-log">No commands sent yet.</p> <p style="color:#555;font-size:0.85rem;padding:12px 0;">← Επιλέξτε πόλη για να δείτε την ουρά.</p>
</div> </div>
</div> </div>
@@ -254,14 +258,78 @@
</div> </div>
</div> </div>
<style>
/* Tab buttons for queue / log toggle */
.log-tab-btn {
background: transparent;
border: 1px solid #2a3a5a;
color: #666;
padding: 4px 10px;
border-radius: 6px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.log-tab-btn:hover { border-color: #4a7aaa; color: #aaa; }
.log-tab-btn.tab-active { border-color: #c8a44a; color: #c8a44a; background: rgba(200,164,74,0.1); }
/* Draggable build queue row */
.bq-row {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid #1a2a3a;
margin-bottom: 5px;
background: #0d1e30;
cursor: default;
transition: background 0.15s, border-color 0.15s;
user-select: none;
}
.bq-row:hover { background: #112038; border-color: #2a4a6a; }
.bq-row.bq-drag-over { border-color: #c8a44a; background: rgba(200,164,74,0.08); }
.bq-handle {
cursor: grab;
font-size: 1.1rem;
color: #3a5a7a;
line-height: 1;
flex-shrink: 0;
padding: 0 2px;
}
.bq-handle:hover { color: #c8a44a; }
.bq-pos {
width: 18px;
text-align: right;
font-size: 0.72rem;
color: #3a5a7a;
font-weight: 700;
flex-shrink: 0;
}
.bq-icon { font-size: 1.1rem; flex-shrink: 0; }
.bq-name { flex: 1; font-size: 0.88rem; color: #d0d0d0; }
.bq-cancel-btn {
background: transparent;
border: 1px solid #3a2a2a;
color: #884444;
border-radius: 4px;
padding: 2px 6px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.15s;
flex-shrink: 0;
}
.bq-cancel-btn:hover { background: rgba(200,80,80,0.15); border-color: #cc4444; color: #ff6666; }
</style>
<script> <script>
window.PLAYER_ID = "{{ player_id }}"; window.PLAYER_ID = "{{ player_id }}";
</script> </script>
<script src="/static/js/state.js?v=5"></script> <script src="/static/js/state.js?v=6"></script>
<script src="/static/js/components/townViewer.js?v=5"></script> <script src="/static/js/components/townViewer.js?v=6"></script>
<script src="/static/js/components/commandForm.js?v=5"></script> <script src="/static/js/components/commandForm.js?v=6"></script>
<script src="/static/js/components/commandLog.js?v=5"></script> <script src="/static/js/components/commandLog.js?v=6"></script>
<script src="/static/js/api.js?v=5"></script> <script src="/static/js/api.js?v=6"></script>
<script src="/static/js/app.js?v=5"></script> <script src="/static/js/app.js?v=6"></script>
</body> </body>
</html> </html>

View File

@@ -243,7 +243,7 @@
<div class="status-bar" id="status-bar"></div> <div class="status-bar" id="status-bar"></div>
<!-- Warehouse-full notice (hidden by default) --> <!-- Warehouse-full notice (hidden by default) -->
<div id="warehouse-full-banner" style="display:none; background: linear-gradient(90deg, #5a1a00, #8b2500); border: 1px solid #ff6600; border-radius: 8px; padding: 12px 18px; margin-bottom: 1rem; display: flex; align-items: center; gap: 12px; font-weight: 600;"> <div id="warehouse-full-banner" style="display:none; background: linear-gradient(90deg, #5a1a00, #8b2500); border: 1px solid #ff6600; border-radius: 8px; padding: 12px 18px; margin-bottom: 1rem; align-items: center; gap: 12px; font-weight: 600;">
<span style="font-size: 1.4rem;">📦</span> <span style="font-size: 1.4rem;">📦</span>
<span> <span>
<strong style="color:#ff9933;">Αποθήκη Γεμάτη!</strong> <strong style="color:#ff9933;">Αποθήκη Γεμάτη!</strong>
@@ -530,7 +530,6 @@
}); });
} }
// -- Warehouse full notice --
async function checkWarehouseStatus() { async function checkWarehouseStatus() {
try { try {
const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}`); const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}`);