diff --git a/routes/dashboard.py b/routes/dashboard.py index e62b149..30d1be3 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, request, jsonify from db import get_db import json -from datetime import datetime +from datetime import datetime, timedelta dashboard = Blueprint('dashboard', __name__) @@ -55,6 +55,33 @@ def get_towns(): return jsonify(towns) +# ------------------------------------------------------------------ +# GET /dashboard/client-status +# Returns whether the Tampermonkey client is considered online. +# Online = at least one town_state row updated within the last 60 s. +# ------------------------------------------------------------------ +@dashboard.route('/dashboard/client-status', methods=['GET']) +def client_status(): + conn = get_db() + row = conn.execute(''' + SELECT MAX(updated_at) AS last_seen FROM town_state + ''').fetchone() + conn.close() + + last_seen = row['last_seen'] if row else None + if last_seen: + try: + dt = datetime.fromisoformat(last_seen) + age_s = (datetime.utcnow() - dt).total_seconds() + online = age_s <= 60 + except Exception: + online = False + else: + online = False + + return jsonify({'online': online, 'last_seen': last_seen}) + + # ------------------------------------------------------------------ # GET /dashboard/commands # Returns command history (last 50) for the log panel. @@ -93,7 +120,22 @@ def create_command(): if cmd_type not in ('build', 'recruit'): return jsonify({'error': 'type must be build or recruit'}), 400 + # Reject if the Tampermonkey client is offline (no state push in last 60 s) conn = get_db() + row = conn.execute('SELECT MAX(updated_at) AS last_seen FROM town_state').fetchone() + last_seen = row['last_seen'] if row else None + client_online = False + if last_seen: + try: + dt = datetime.fromisoformat(last_seen) + client_online = (datetime.utcnow() - dt).total_seconds() <= 60 + except Exception: + pass + + if not client_online: + conn.close() + return jsonify({'error': 'client_offline', 'message': 'Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'}), 503 + c = conn.cursor() c.execute(''' INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at) @@ -120,9 +162,29 @@ def create_command(): @dashboard.route('/dashboard/commands/', methods=['DELETE']) def cancel_command(cmd_id): conn = get_db() - conn.execute(''' - DELETE FROM commands WHERE id = ? - ''', (cmd_id,)) + conn.execute('DELETE FROM commands WHERE id = ?', (cmd_id,)) conn.commit() conn.close() return jsonify({'ok': True}) + + +# ------------------------------------------------------------------ +# POST /dashboard/commands/fail-stale +# Mark all pending/executing commands as failed (called by dashboard +# when it detects the client has gone offline). +# ------------------------------------------------------------------ +@dashboard.route('/dashboard/commands/fail-stale', methods=['POST']) +def fail_stale_commands(): + conn = get_db() + c = conn.cursor() + c.execute(''' + UPDATE commands + SET status = 'failed', + result_msg = 'Client went offline', + updated_at = ? + WHERE status IN ('pending', 'executing') + ''', (datetime.utcnow().isoformat(),)) + affected = c.rowcount + conn.commit() + conn.close() + return jsonify({'ok': True, 'failed': affected}) diff --git a/templates/dashboard.html b/templates/dashboard.html index 703c3db..7a74c07 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -27,15 +27,20 @@ color: #c8a44a; letter-spacing: 1px; } - #connection-status { + .status-indicator { margin-left: auto; + display: flex; + gap: 10px; + align-items: center; + } + .conn-badge { font-size: 0.8rem; padding: 4px 10px; border-radius: 12px; background: #333; } - #connection-status.online { background: #1a4a1a; color: #6fcf6f; } - #connection-status.offline { background: #4a1a1a; color: #cf6f6f; } + .conn-badge.online { background: #1a4a1a; color: #6fcf6f; } + .conn-badge.offline { background: #4a1a1a; color: #cf6f6f; } .layout { display: grid; @@ -210,7 +215,10 @@

⚔️ Grepolis Remote

-
Connecting…
+
+
Server…
+
Client…
+
@@ -330,6 +338,8 @@ // ================================================================ let towns = []; let selectedTownId = null; +let clientOnline = false; // tracks Tampermonkey script heartbeat +let wasClientOnline = null; // previous known state (null = unknown) const POLL_INTERVAL = 4000; const BUILDING_NAMES_GR = { @@ -373,15 +383,36 @@ async function fetchTowns() { const res = await fetch('/dashboard/towns'); towns = await res.json(); renderTowns(); - updateConnectionStatus(true); - + updateServerStatus(true); + if (selectedTownId) { renderBuildQueuePreview(); renderBuildingDropdown(); renderTownDetails(); } } catch (e) { - updateConnectionStatus(false); + updateServerStatus(false); + } +} + +async function fetchClientStatus() { + try { + const res = await fetch('/dashboard/client-status'); + const data = await res.json(); + const isOnline = data.online === true; + + // Detect transition: online → offline + if (wasClientOnline === true && !isOnline) { + // Fail all queued commands immediately + await fetch('/dashboard/commands/fail-stale', { method: 'POST' }); + fetchLog(); + } + + clientOnline = isOnline; + wasClientOnline = isOnline; + updateClientStatus(isOnline); + } catch (e) { + updateClientStatus(false); } } @@ -393,14 +424,33 @@ async function fetchLog() { } catch (e) {} } -function updateConnectionStatus(online) { - const el = document.getElementById('connection-status'); +function updateServerStatus(online) { + const el = document.getElementById('server-status'); if (online) { - el.textContent = '● Online'; - el.className = 'online'; + el.textContent = '● Server'; + el.className = 'conn-badge online'; } else { - el.textContent = '● Offline'; - el.className = 'offline'; + el.textContent = '● Server offline'; + el.className = 'conn-badge offline'; + } +} + +function updateClientStatus(online) { + const el = document.getElementById('client-status'); + if (online) { + el.textContent = '● Script'; + el.className = 'conn-badge online'; + } else { + el.textContent = '● Script offline'; + el.className = 'conn-badge offline'; + } + // Enable/disable the Send button + const btn = document.querySelector('#command-form-wrap .btn-gold'); + if (btn) { + btn.disabled = !online; + btn.title = online ? '' : 'Script is offline — cannot send commands'; + btn.style.opacity = online ? '' : '0.4'; + btn.style.cursor = online ? '' : 'not-allowed'; } } @@ -471,7 +521,7 @@ function renderTownDetails() { return 'color: #fff;'; }; - const capFmt = t.resources.storage ? fmt(t.resources.storage) : '?'; + const capFmt = (t.resources.storage != null && t.resources.storage !== '') ? fmt(t.resources.storage) : '?'; document.getElementById('td-resources').innerHTML = `
Χωρητικότητα: ${capFmt}
@@ -554,6 +604,7 @@ function onCmdTypeChange() { // Send command // ================================================================ async function sendCommand() { + if (!clientOnline) return alert('Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'); const town = getSelectedTown(); if (!town) return alert('Select a town first.'); @@ -583,6 +634,8 @@ async function sendCommand() { const data = await res.json(); if (data.ok) { fetchLog(); + } else if (data.error === 'client_offline') { + alert(data.message || 'Το script είναι offline.'); } else { alert('Error: ' + JSON.stringify(data)); } @@ -637,8 +690,10 @@ async function cancelCommand(id) { // ================================================================ fetchTowns(); fetchLog(); -setInterval(fetchTowns, POLL_INTERVAL); -setInterval(fetchLog, POLL_INTERVAL); +fetchClientStatus(); +setInterval(fetchTowns, POLL_INTERVAL); +setInterval(fetchLog, POLL_INTERVAL); +setInterval(fetchClientStatus, POLL_INTERVAL);