Files
grepo-remote/templates/dashboard.html

700 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grepolis Remote</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #e0e0e0;
min-height: 100vh;
}
header {
background: #16213e;
border-bottom: 2px solid #c8a44a;
padding: 12px 24px;
display: flex;
align-items: center;
gap: 16px;
}
header h1 {
font-size: 1.3rem;
color: #c8a44a;
letter-spacing: 1px;
}
.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;
}
.conn-badge.online { background: #1a4a1a; color: #6fcf6f; }
.conn-badge.offline { background: #4a1a1a; color: #cf6f6f; }
.layout {
display: grid;
grid-template-columns: 320px 1fr;
grid-template-rows: auto 1fr;
gap: 0;
height: calc(100vh - 57px);
}
/* ---- Town list (left panel) ---- */
#town-panel {
grid-row: 1 / 3;
background: #16213e;
border-right: 1px solid #2a2a4a;
overflow-y: auto;
padding: 12px;
}
#town-panel h2 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 1px;
color: #888;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid #2a2a4a;
}
.town-card {
background: #0f3460;
border: 1px solid #2a2a6a;
border-radius: 6px;
padding: 10px;
margin-bottom: 8px;
cursor: pointer;
transition: border-color 0.15s;
}
.town-card:hover { border-color: #c8a44a; }
.town-card.selected { border-color: #c8a44a; background: #1a4070; }
.town-card .town-name { font-weight: 600; font-size: 0.95rem; color: #c8a44a; }
.town-card .town-meta { font-size: 0.72rem; color: #888; margin-top: 2px; }
.town-card .town-res {
display: flex; gap: 8px;
margin-top: 6px; font-size: 0.78rem;
}
.res-item { display: flex; align-items: center; gap: 3px; }
.res-icon { width: 12px; height: 12px; border-radius: 2px; }
.res-wood { background: #8B6914; }
.res-stone { background: #888; }
.res-iron { background: #5588aa; }
.res-pop { background: #88aa55; }
.town-stale { opacity: 0.5; }
/* ---- Command panel (top right) ---- */
#command-panel {
padding: 16px 20px;
background: #1a1a2e;
border-bottom: 1px solid #2a2a4a;
}
#command-panel h2 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 1px;
color: #888;
margin-bottom: 12px;
}
.command-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: flex-end;
}
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-group label { font-size: 0.72rem; color: #aaa; text-transform: uppercase; letter-spacing: 0.5px; }
select, input[type=number] {
background: #0f3460;
border: 1px solid #2a2a6a;
color: #e0e0e0;
padding: 7px 10px;
border-radius: 4px;
font-size: 0.85rem;
min-width: 140px;
}
select:focus, input:focus { outline: none; border-color: #c8a44a; }
.btn {
padding: 8px 18px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.5px;
transition: opacity 0.15s;
}
.btn:hover { opacity: 0.85; }
.btn-gold { background: #c8a44a; color: #1a1a2e; }
.btn-danger { background: #8b2222; color: #fff; }
.btn-sm { padding: 5px 10px; font-size: 0.75rem; }
#no-town-selected {
color: #666;
font-size: 0.85rem;
padding: 8px 0;
}
/* Build queue preview */
#build-queue-preview {
margin-top: 10px;
font-size: 0.78rem;
color: #aaa;
}
#build-queue-preview span {
background: #0f3460;
border: 1px solid #2a4a6a;
border-radius: 3px;
padding: 2px 7px;
margin-right: 4px;
display: inline-block;
margin-bottom: 4px;
}
/* ---- Command log (bottom right) ---- */
#log-panel {
padding: 16px 20px;
overflow-y: auto;
background: #1a1a2e;
}
#log-panel h2 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 1px;
color: #888;
margin-bottom: 10px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
}
th {
text-align: left;
color: #888;
font-weight: 500;
padding: 4px 8px;
border-bottom: 1px solid #2a2a4a;
white-space: nowrap;
}
td {
padding: 5px 8px;
border-bottom: 1px solid #1e1e3a;
vertical-align: middle;
}
tr:hover td { background: #1e1e40; }
.status-badge {
padding: 2px 8px;
border-radius: 10px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
white-space: nowrap;
}
.status-pending { background: #3a3a00; color: #cccc00; }
.status-executing { background: #003a4a; color: #00ccee; }
.status-done { background: #003a00; color: #00cc66; }
.status-failed { background: #4a0000; color: #ee4444; }
.status-cancelled { background: #2a2a2a; color: #888; }
#empty-log { color: #444; font-size: 0.85rem; padding: 12px 0; }
</style>
</head>
<body>
<header>
<h1>⚔️ Grepolis Remote</h1>
<div class="status-indicator">
<div id="server-status" class="conn-badge">Server…</div>
<div id="client-status" class="conn-badge">Client…</div>
</div>
</header>
<div class="layout">
<!-- Left: Town list -->
<div id="town-panel">
<h2>Towns</h2>
<div id="town-list"><p style="color:#444;font-size:0.8rem;">Waiting for data from home PC…</p></div>
</div>
<!-- Top right: Command panel -->
<div id="command-panel">
<h2>Send Command</h2>
<div id="no-town-selected">← Select a town first</div>
<!-- New dynamic town details panel -->
<div id="town-details-panel" style="display:none; margin-bottom: 20px; padding: 12px; background: #0f3460; border-radius: 6px; border: 1px solid #2a4a6a;">
<h3 id="td-name" style="color: #c8a44a; margin-bottom: 8px; font-size: 1rem; border-bottom: 1px solid #2a4a6a; padding-bottom: 4px;">Town Name</h3>
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 130px;">
<strong style="font-size: 0.75rem; color: #888; text-transform: uppercase;">Ποροι</strong>
<div id="td-resources" style="font-size: 0.85rem; margin-top: 4px; line-height: 1.5;"></div>
</div>
<div style="flex: 1; min-width: 130px;">
<strong style="font-size: 0.75rem; color: #888; text-transform: uppercase;">Γενικα</strong>
<div id="td-general" style="font-size: 0.85rem; margin-top: 4px; line-height: 1.5;"></div>
</div>
<div style="flex: 1; min-width: 130px;">
<strong style="font-size: 0.75rem; color: #888; text-transform: uppercase;">Στρατος</strong>
<div id="td-units" style="font-size: 0.85rem; margin-top: 4px; line-height: 1.5; color: #a4c84a;"></div>
</div>
</div>
</div>
<div id="command-form-wrap" style="display:none">
<div class="command-form">
<div class="form-group">
<label>Command Type</label>
<select id="cmd-type" onchange="onCmdTypeChange()">
<option value="build">Build / Upgrade</option>
<option value="recruit">Recruit Troops</option>
</select>
</div>
<!-- Build options -->
<div class="form-group" id="build-options">
<label>Building</label>
<select id="building-select">
<option disabled>Επιλέξτε πόλη...</option>
</select>
</div>
<!-- Recruit options -->
<div class="form-group" id="recruit-options" style="display:none">
<label>Unit</label>
<select id="unit-select">
<optgroup label="Ξηρά">
<option value="sword">Ξιφομάχος</option>
<option value="slinger">Σφενδονήτης</option>
<option value="archer">Τοξότης</option>
<option value="hoplite">Οπλίτης</option>
<option value="rider">Ιππέας</option>
<option value="chariot">Άρμα</option>
<option value="catapult">Καταπέλτης</option>
</optgroup>
<optgroup label="Ναυτικές">
<option value="big_transporter">Μεταφορικό Πλοίο</option>
<option value="small_transporter">Γρήγορο Μεταφορικό Πλοίο</option>
<option value="bireme">Διήρης</option>
<option value="attack_ship">Πλοίο Φάρος</option>
<option value="trireme">Τριήρης</option>
<option value="colonize_ship">Αποικιακό Πλοίο</option>
</optgroup>
<optgroup label="Μυθικές">
<option value="medusa">Μέδουσα</option>
<option value="zyklop">Κύκλωπας</option>
<option value="harpy">Άρπυια</option>
<option value="pegasus">Πήγασος</option>
<option value="minotaur">Μινώταυρος</option>
<option value="manticore">Μαντιχώρας</option>
<option value="cerberus">Κέρβερος</option>
<option value="hydra">Ύδρα</option>
<option value="sea_monster">Τέρας της Θάλασσας</option>
</optgroup>
</select>
</div>
<div class="form-group" id="amount-group" style="display:none">
<label>Amount</label>
<input type="number" id="recruit-amount" value="1" min="1" max="9999" style="width:80px;">
</div>
<button class="btn btn-gold" onclick="sendCommand()">Send ⚡</button>
</div>
<div id="build-queue-preview"></div>
</div>
</div>
<!-- Bottom right: Command log -->
<div id="log-panel">
<h2>Command Log</h2>
<div id="log-content">
<p id="empty-log">No commands sent yet.</p>
</div>
</div>
</div>
<script>
// ================================================================
// State
// ================================================================
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 = {
main: "Σύγκλητος",
storage: "Αποθήκη",
farm: "Φάρμα",
academy: "Ακαδημία",
temple: "Ναός",
barracks: "Στρατώνας",
docks: "Λιμάνι",
market: "Αγορά",
hide: "Σπηλιά",
lumber: "Ξυλουργείο",
stoner: "Λατομείο",
ironer: "Ορυχείο Αργύρου",
wall: "Τείχος"
};
const UNIT_NAMES_GR = {
sword: "Ξιφομάχος", slinger: "Σφενδονήτης", archer: "Τοξότης", hoplite: "Οπλίτης",
rider: "Ιππέας", chariot: "Άρμα", catapult: "Καταπέλτης",
big_transporter: "Μεταφορικό", small_transporter: "Γρήγ. Μεταφορικό", bireme: "Διήρης",
attack_ship: "Πλοίο Φάρος", trireme: "Τριήρης", colonize_ship: "Αποικιακό",
medusa: "Μέδουσα", zyklop: "Κύκλωπας", harpy: "Άρπυια", pegasus: "Πήγασος",
minotaur: "Μινώταυρος", manticore: "Μαντιχώρας", cerberus: "Κέρβερος",
hydra: "Ύδρα", sea_monster: "Τέρας Θάλασσας", militia: "Εθνοφρουρά"
};
const RES_ICONS = {
wood: '<span class="res-icon res-wood" style="display:inline-block; margin-right:4px;"></span>',
stone: '<span class="res-icon res-stone" style="display:inline-block; margin-right:4px;"></span>',
iron: '<span class="res-icon res-iron" style="display:inline-block; margin-right:4px;"></span>',
pop: '<span class="res-icon res-pop" style="display:inline-block; margin-right:4px;"></span>'
};
// ================================================================
// Polling
// ================================================================
async function fetchTowns() {
try {
const res = await fetch('/dashboard/towns');
towns = await res.json();
renderTowns();
updateServerStatus(true);
if (selectedTownId) {
renderBuildQueuePreview();
renderBuildingDropdown();
renderTownDetails();
}
} catch (e) {
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);
}
}
async function fetchLog() {
try {
const res = await fetch('/dashboard/commands');
const cmds = await res.json();
renderLog(cmds);
} catch (e) {}
}
function updateServerStatus(online) {
const el = document.getElementById('server-status');
if (online) {
el.textContent = '● Server';
el.className = 'conn-badge online';
} else {
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';
}
}
// ================================================================
// Render towns
// ================================================================
function renderTowns() {
const container = document.getElementById('town-list');
if (!towns.length) {
container.innerHTML = '<p style="color:#444;font-size:0.8rem;">No towns received yet.</p>';
return;
}
const now = Date.now();
container.innerHTML = towns.map(t => {
const updatedMs = new Date(t.updated_at + 'Z').getTime();
const ageMin = Math.round((now - updatedMs) / 60000);
const stale = ageMin > 3;
const selected = t.town_id === selectedTownId ? 'selected' : '';
return `
<div class="town-card ${selected} ${stale ? 'town-stale' : ''}"
onclick="selectTown('${t.town_id}')">
<div class="town-name">${t.town_name}</div>
<div class="town-meta">${t.points} pts · ${t.god || 'No god'} · ${ageMin}m ago</div>
<div class="town-res">
<div class="res-item"><div class="res-icon res-wood"></div>${fmt(t.resources.wood)}</div>
<div class="res-item"><div class="res-icon res-stone"></div>${fmt(t.resources.stone)}</div>
<div class="res-item"><div class="res-icon res-iron"></div>${fmt(t.resources.iron)}</div>
<div class="res-item"><div class="res-icon res-pop"></div>${fmt(t.resources.population)}</div>
</div>
</div>`;
}).join('');
}
function fmt(n) {
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
return n;
}
// ================================================================
// Town selection
// ================================================================
function selectTown(id) {
selectedTownId = id;
renderTowns();
document.getElementById('no-town-selected').style.display = 'none';
document.getElementById('command-form-wrap').style.display = 'block';
document.getElementById('town-details-panel').style.display = 'block';
renderBuildQueuePreview();
renderBuildingDropdown();
renderTownDetails();
}
function renderTownDetails() {
const t = getSelectedTown();
if(!t) return;
document.getElementById('td-name').textContent = t.town_name;
const cap = t.resources.storage || 1;
const getCol = (amt) => {
const pct = amt / cap;
if (pct >= 0.95) return 'color: #ff4a4a;'; // Red
if (pct >= 0.85) return 'color: #ffa500;'; // Orange
return 'color: #fff;';
};
const capFmt = (t.resources.storage != null && t.resources.storage !== '') ? fmt(t.resources.storage) : '?';
document.getElementById('td-resources').innerHTML = `
<div style="font-size:0.75rem; color:#aaa; margin-bottom: 4px;">Χωρητικότητα: <strong style="color:#eee">${capFmt}</strong></div>
<div>${RES_ICONS.wood} Ξύλο: <strong style="${getCol(t.resources.wood)}">${fmt(t.resources.wood)}</strong></div>
<div>${RES_ICONS.stone} Πέτρα: <strong style="${getCol(t.resources.stone)}">${fmt(t.resources.stone)}</strong></div>
<div>${RES_ICONS.iron} Ασήμι: <strong style="${getCol(t.resources.iron)}">${fmt(t.resources.iron)}</strong></div>
<div style="margin-top: 6px;">${RES_ICONS.pop} Πληθυσμός: <strong style="color:#fff">${t.resources.population}</strong></div>
`;
const godName = t.god ? t.god.charAt(0).toUpperCase() + t.god.slice(1) : 'Κανένας';
document.getElementById('td-general').innerHTML = `
<div>Πόντοι: <strong>${t.points}</strong></div>
<div>Θεός: <strong>${godName}</strong></div>
<div>Παίκτης: <strong style="color:#aaa">${t.player}</strong></div>
`;
const unitsObj = t.units || {};
let unitsHtml = '';
for(const [unitKey, count] of Object.entries(unitsObj)) {
if(count > 0 && unitKey !== 'militia') {
const name = UNIT_NAMES_GR[unitKey] || unitKey;
unitsHtml += `<div>${name}: <strong style="color:#fff">${count}</strong></div>`;
}
}
if(unitsHtml === '') unitsHtml = '<div style="color:#666">Κανένα στράτευμα</div>';
document.getElementById('td-units').innerHTML = unitsHtml;
}
function renderBuildingDropdown() {
const town = getSelectedTown();
if (!town) return;
const bSelect = document.getElementById('building-select');
const bLevels = town.buildings || {};
const currentVal = bSelect.value;
bSelect.innerHTML = '';
for (const [key, nameGr] of Object.entries(BUILDING_NAMES_GR)) {
const level = bLevels[key] !== undefined ? bLevels[key] : "?";
const option = document.createElement('option');
option.value = key;
option.textContent = `${nameGr} [Επίπεδο ${level}]`;
bSelect.appendChild(option);
}
if (currentVal && Array.from(bSelect.options).some(o => o.value === currentVal)) {
bSelect.value = currentVal;
}
}
function getSelectedTown() {
return towns.find(t => t.town_id === selectedTownId);
}
function renderBuildQueuePreview() {
const town = getSelectedTown();
const el = document.getElementById('build-queue-preview');
if (!town || !town.build_queue || !town.build_queue.length) {
el.innerHTML = '<span style="color:#444">Build queue: empty</span>';
return;
}
const items = town.build_queue.map(o =>
`<span>${o.building_type || o.name || JSON.stringify(o)}</span>`
).join('');
el.innerHTML = `<div style="margin-top:6px;color:#888;font-size:0.72rem;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;">Current queue</div>${items}`;
}
// ================================================================
// Form logic
// ================================================================
function onCmdTypeChange() {
const type = document.getElementById('cmd-type').value;
document.getElementById('build-options').style.display = type === 'build' ? '' : 'none';
document.getElementById('recruit-options').style.display = type === 'recruit' ? '' : 'none';
document.getElementById('amount-group').style.display = type === 'recruit' ? '' : 'none';
}
// ================================================================
// Send command
// ================================================================
async function sendCommand() {
if (!clientOnline) return alert('Το script είναι offline — δεν μπορείτε να στείλετε εντολές.');
const town = getSelectedTown();
if (!town) return alert('Select a town first.');
const type = document.getElementById('cmd-type').value;
let payload = {};
if (type === 'build') {
payload = { building_id: document.getElementById('building-select').value };
} else {
payload = {
unit_id: document.getElementById('unit-select').value,
amount: parseInt(document.getElementById('recruit-amount').value) || 1,
};
}
try {
const res = await fetch('/dashboard/commands', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
town_id: town.town_id,
town_name: town.town_name,
type,
payload
})
});
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));
}
} catch (e) {
alert('Failed to send command: ' + e);
}
}
// ================================================================
// Command log
// ================================================================
function renderLog(cmds) {
const el = document.getElementById('log-content');
if (!cmds.length) {
el.innerHTML = '<p id="empty-log">No commands sent yet.</p>';
return;
}
const rows = cmds.map(cmd => {
const p = typeof cmd.payload === 'string' ? JSON.parse(cmd.payload) : cmd.payload;
const desc = cmd.type === 'build'
? `Build: ${p.building_id}`
: `Recruit: ${p.amount}x ${p.unit_id}`;
const statusClass = `status-${cmd.status}`;
const cancelBtn = `<button class="btn btn-danger btn-sm" onclick="cancelCommand(${cmd.id})">✕</button>`;
return `<tr>
<td style="color:#888;font-size:0.7rem">#${cmd.id}</td>
<td>${cmd.town_name || cmd.town_id}</td>
<td>${desc}</td>
<td><span class="status-badge ${statusClass}">${cmd.status}</span></td>
<td style="color:#666;font-size:0.72rem">${cmd.result_msg || ''}</td>
<td>${cancelBtn}</td>
</tr>`;
}).join('');
el.innerHTML = `<table>
<thead><tr>
<th>#</th><th>Town</th><th>Command</th><th>Status</th><th>Result</th><th></th>
</tr></thead>
<tbody>${rows}</tbody>
</table>`;
}
async function cancelCommand(id) {
await fetch(`/dashboard/commands/${id}`, { method: 'DELETE' });
fetchLog();
}
// ================================================================
// Boot
// ================================================================
fetchTowns();
fetchLog();
fetchClientStatus();
setInterval(fetchTowns, POLL_INTERVAL);
setInterval(fetchLog, POLL_INTERVAL);
setInterval(fetchClientStatus, POLL_INTERVAL);
</script>
</body>
</html>