dashboard fix/online off , and warehouse
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
from flask import Blueprint, render_template, request, jsonify
|
from flask import Blueprint, render_template, request, jsonify
|
||||||
from db import get_db
|
from db import get_db
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
dashboard = Blueprint('dashboard', __name__)
|
dashboard = Blueprint('dashboard', __name__)
|
||||||
|
|
||||||
@@ -55,6 +55,33 @@ def get_towns():
|
|||||||
return jsonify(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
|
# GET /dashboard/commands
|
||||||
# Returns command history (last 50) for the log panel.
|
# Returns command history (last 50) for the log panel.
|
||||||
@@ -93,7 +120,22 @@ def create_command():
|
|||||||
if cmd_type not in ('build', 'recruit'):
|
if cmd_type not in ('build', 'recruit'):
|
||||||
return jsonify({'error': 'type must be build or recruit'}), 400
|
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()
|
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 = conn.cursor()
|
||||||
c.execute('''
|
c.execute('''
|
||||||
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at)
|
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/<int:cmd_id>', methods=['DELETE'])
|
@dashboard.route('/dashboard/commands/<int:cmd_id>', methods=['DELETE'])
|
||||||
def cancel_command(cmd_id):
|
def cancel_command(cmd_id):
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
conn.execute('''
|
conn.execute('DELETE FROM commands WHERE id = ?', (cmd_id,))
|
||||||
DELETE FROM commands WHERE id = ?
|
|
||||||
''', (cmd_id,))
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True})
|
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})
|
||||||
|
|||||||
@@ -27,15 +27,20 @@
|
|||||||
color: #c8a44a;
|
color: #c8a44a;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
#connection-status {
|
.status-indicator {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.conn-badge {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #333;
|
background: #333;
|
||||||
}
|
}
|
||||||
#connection-status.online { background: #1a4a1a; color: #6fcf6f; }
|
.conn-badge.online { background: #1a4a1a; color: #6fcf6f; }
|
||||||
#connection-status.offline { background: #4a1a1a; color: #cf6f6f; }
|
.conn-badge.offline { background: #4a1a1a; color: #cf6f6f; }
|
||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -210,7 +215,10 @@
|
|||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1>⚔️ Grepolis Remote</h1>
|
<h1>⚔️ Grepolis Remote</h1>
|
||||||
<div id="connection-status">Connecting…</div>
|
<div class="status-indicator">
|
||||||
|
<div id="server-status" class="conn-badge">Server…</div>
|
||||||
|
<div id="client-status" class="conn-badge">Client…</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
@@ -330,6 +338,8 @@
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
let towns = [];
|
let towns = [];
|
||||||
let selectedTownId = null;
|
let selectedTownId = null;
|
||||||
|
let clientOnline = false; // tracks Tampermonkey script heartbeat
|
||||||
|
let wasClientOnline = null; // previous known state (null = unknown)
|
||||||
const POLL_INTERVAL = 4000;
|
const POLL_INTERVAL = 4000;
|
||||||
|
|
||||||
const BUILDING_NAMES_GR = {
|
const BUILDING_NAMES_GR = {
|
||||||
@@ -373,7 +383,7 @@ async function fetchTowns() {
|
|||||||
const res = await fetch('/dashboard/towns');
|
const res = await fetch('/dashboard/towns');
|
||||||
towns = await res.json();
|
towns = await res.json();
|
||||||
renderTowns();
|
renderTowns();
|
||||||
updateConnectionStatus(true);
|
updateServerStatus(true);
|
||||||
|
|
||||||
if (selectedTownId) {
|
if (selectedTownId) {
|
||||||
renderBuildQueuePreview();
|
renderBuildQueuePreview();
|
||||||
@@ -381,7 +391,28 @@ async function fetchTowns() {
|
|||||||
renderTownDetails();
|
renderTownDetails();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateConnectionStatus(online) {
|
function updateServerStatus(online) {
|
||||||
const el = document.getElementById('connection-status');
|
const el = document.getElementById('server-status');
|
||||||
if (online) {
|
if (online) {
|
||||||
el.textContent = '● Online';
|
el.textContent = '● Server';
|
||||||
el.className = 'online';
|
el.className = 'conn-badge online';
|
||||||
} else {
|
} else {
|
||||||
el.textContent = '● Offline';
|
el.textContent = '● Server offline';
|
||||||
el.className = '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;';
|
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 = `
|
document.getElementById('td-resources').innerHTML = `
|
||||||
<div style="font-size:0.75rem; color:#aaa; margin-bottom: 4px;">Χωρητικότητα: <strong style="color:#eee">${capFmt}</strong></div>
|
<div style="font-size:0.75rem; color:#aaa; margin-bottom: 4px;">Χωρητικότητα: <strong style="color:#eee">${capFmt}</strong></div>
|
||||||
@@ -554,6 +604,7 @@ function onCmdTypeChange() {
|
|||||||
// Send command
|
// Send command
|
||||||
// ================================================================
|
// ================================================================
|
||||||
async function sendCommand() {
|
async function sendCommand() {
|
||||||
|
if (!clientOnline) return alert('Το script είναι offline — δεν μπορείτε να στείλετε εντολές.');
|
||||||
const town = getSelectedTown();
|
const town = getSelectedTown();
|
||||||
if (!town) return alert('Select a town first.');
|
if (!town) return alert('Select a town first.');
|
||||||
|
|
||||||
@@ -583,6 +634,8 @@ async function sendCommand() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
fetchLog();
|
fetchLog();
|
||||||
|
} else if (data.error === 'client_offline') {
|
||||||
|
alert(data.message || 'Το script είναι offline.');
|
||||||
} else {
|
} else {
|
||||||
alert('Error: ' + JSON.stringify(data));
|
alert('Error: ' + JSON.stringify(data));
|
||||||
}
|
}
|
||||||
@@ -637,8 +690,10 @@ async function cancelCommand(id) {
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
fetchTowns();
|
fetchTowns();
|
||||||
fetchLog();
|
fetchLog();
|
||||||
|
fetchClientStatus();
|
||||||
setInterval(fetchTowns, POLL_INTERVAL);
|
setInterval(fetchTowns, POLL_INTERVAL);
|
||||||
setInterval(fetchLog, POLL_INTERVAL);
|
setInterval(fetchLog, POLL_INTERVAL);
|
||||||
|
setInterval(fetchClientStatus, POLL_INTERVAL);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user