Files
grepo-remote/templates/agora.html
2026-05-07 20:22:28 +03:00

758 lines
24 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="el">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Αγορά — Grepolis Remote</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f0f1a;
--panel: #181824;
--border: #2a2a3e;
--purple: #b482dc;
--purple-dim: rgba(180,130,220,0.12);
--gold: #c8a44a;
--green: #4acc64;
--red: #e05555;
--yellow: #e0b847;
--text: #e0e0e0;
--muted: #666;
}
body {
background: var(--bg);
font-family: 'Inter', sans-serif;
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ── Top bar ── */
.topbar {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.9rem 1.5rem;
background: #13131f;
border-bottom: 1px solid var(--border);
}
.topbar-title {
font-size: 1.2rem;
font-weight: 800;
color: var(--purple);
flex: 1;
}
.topbar a {
color: var(--muted);
text-decoration: none;
font-size: 0.82rem;
transition: color .2s;
}
.topbar a:hover { color: var(--text); }
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--muted);
display: inline-block;
margin-right: 6px;
}
.status-dot.online { background: var(--green); box-shadow: 0 0 6px var(--green); }
/* ── Main layout ── */
.main {
display: grid;
grid-template-columns: 300px 1fr;
grid-template-rows: 1fr auto;
gap: 0;
flex: 1;
overflow: hidden;
height: calc(100vh - 52px);
}
/* ── Left panel ── */
.left-panel {
background: var(--panel);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
grid-row: 1 / 3;
}
.panel-header {
padding: 1rem 1.2rem 0.6rem;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--muted);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.search-box {
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.search-box input {
width: 100%;
background: #1e1e2e;
border: 1px solid var(--border);
border-radius: 8px;
padding: 6px 10px;
color: var(--text);
font-size: 0.82rem;
outline: none;
transition: border-color .2s;
}
.search-box input:focus { border-color: var(--purple); }
.town-list {
overflow-y: auto;
flex: 1;
}
.town-row {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.65rem 1.1rem;
border-bottom: 1px solid #1e1e2e;
cursor: pointer;
transition: background .15s;
position: relative;
}
.town-row:hover { background: #1e1e30; }
.town-row.active { background: var(--purple-dim); border-left: 3px solid var(--purple); }
.town-row .t-name { font-size: 0.84rem; font-weight: 600; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.town-row .t-dots { display: flex; gap: 4px; }
.t-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--muted);
}
.t-dot.available { background: var(--green); }
.t-dot.cooldown { background: var(--yellow); }
.t-dot.unavailable { background: #333; }
/* ── Right panel ── */
.right-panel {
display: flex;
flex-direction: column;
overflow: hidden;
}
.cards-area {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.2rem;
align-content: start;
}
.no-selection {
grid-column: 1 / -1;
text-align: center;
color: var(--muted);
font-size: 0.9rem;
padding: 4rem 0;
}
/* ── Celebration cards ── */
.cel-card {
background: #1e1e30;
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.4rem;
display: flex;
flex-direction: column;
gap: 0.9rem;
transition: border-color .2s;
}
.cel-card.available { border-color: rgba(180,130,220,0.35); }
.cel-card.cooldown { border-color: rgba(224,184,71,0.35); }
.cel-card-title {
font-size: 1rem;
font-weight: 700;
color: var(--purple);
display: flex;
align-items: center;
gap: 0.5rem;
}
.cel-card.cooldown .cel-card-title { color: var(--yellow); }
.cost-row {
display: flex;
gap: 0.8rem;
flex-wrap: wrap;
}
.cost-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.78rem;
font-weight: 600;
}
.cost-item.ok { color: var(--green); }
.cost-item.bad { color: var(--red); }
.cost-item.neutral { color: var(--text); }
.reason-box {
font-size: 0.78rem;
color: var(--yellow);
padding: 6px 10px;
background: rgba(224,184,71,0.08);
border-radius: 8px;
border: 1px solid rgba(224,184,71,0.2);
line-height: 1.4;
}
.reason-box.error {
color: var(--red);
background: rgba(224,85,85,0.08);
border-color: rgba(224,85,85,0.2);
}
.cel-btn {
padding: 9px 16px;
border-radius: 9px;
border: none;
cursor: pointer;
font-size: 0.84rem;
font-weight: 700;
transition: all .2s;
align-self: flex-start;
}
.cel-btn.primary {
background: var(--purple);
color: #fff;
}
.cel-btn.primary:hover { background: #c99ef0; transform: translateY(-1px); }
.cel-btn:disabled {
background: #2a2a3e;
color: var(--muted);
cursor: not-allowed;
transform: none;
}
/* ── Log panel ── */
.log-panel {
border-top: 1px solid var(--border);
background: #13131f;
flex-shrink: 0;
}
.log-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 1.2rem;
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
}
.log-header span { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
.log-body {
overflow-y: auto;
max-height: 170px;
padding: 0.5rem 1rem;
font-family: 'Courier New', monospace;
font-size: 0.77rem;
display: none;
}
.log-body.open { display: block; }
.log-entry {
display: flex;
gap: 0.8rem;
padding: 3px 0;
border-bottom: 1px solid #1a1a28;
align-items: center;
}
.log-time { color: var(--muted); min-width: 55px; }
.log-town { color: var(--purple); min-width: 140px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.log-type { color: var(--text); min-width: 150px; }
.log-status.success { color: var(--green); }
.log-status.failed { color: var(--red); }
.log-status.pending { color: var(--yellow); }
.log-msg { color: var(--muted); font-size: 0.72rem; flex: 1; }
.log-empty { color: var(--muted); padding: 0.6rem 0; font-size: 0.8rem; }
/* ── Confirm Modal ── */
.modal-overlay {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,0.7);
z-index: 100;
align-items: center;
justify-content: center;
}
.modal-overlay.open { display: flex; }
.modal-box {
background: #1e1e30;
border: 1px solid var(--purple);
border-radius: 16px;
padding: 2rem;
min-width: 340px;
max-width: 420px;
box-shadow: 0 20px 60px rgba(180,130,220,0.2);
}
.modal-title {
font-size: 1.1rem;
font-weight: 800;
color: var(--purple);
margin-bottom: 1rem;
}
.modal-row {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
padding: 5px 0;
border-bottom: 1px solid var(--border);
}
.modal-row span:first-child { color: var(--muted); }
.modal-row span:last-child { font-weight: 600; }
.modal-actions {
display: flex;
gap: 0.8rem;
margin-top: 1.4rem;
justify-content: flex-end;
}
.btn-cancel {
padding: 8px 18px;
border-radius: 8px;
background: #2a2a3e;
border: 1px solid var(--border);
color: var(--text);
cursor: pointer;
font-size: 0.84rem;
font-weight: 600;
}
.btn-cancel:hover { border-color: var(--purple); }
.btn-confirm {
padding: 8px 20px;
border-radius: 8px;
background: var(--purple);
border: none;
color: #fff;
cursor: pointer;
font-size: 0.84rem;
font-weight: 700;
transition: background .2s;
}
.btn-confirm:hover { background: #c99ef0; }
.btn-confirm:disabled { background: #444; color: var(--muted); cursor: not-allowed; }
/* ── Auto toggle ── */
.auto-toggle {
display: flex;
align-items: center;
gap: 0;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
width: fit-content;
font-size: 0.76rem;
font-weight: 700;
}
.auto-toggle button {
padding: 5px 12px;
border: none;
background: #1e1e30;
color: var(--muted);
cursor: pointer;
transition: all .2s;
}
.auto-toggle button.active-manual { background: #2a2a40; color: var(--text); }
.auto-toggle button.active-auto { background: rgba(180,130,220,0.25); color: var(--purple); }
.cel-card.auto-on { border-color: rgba(180,130,220,0.6); box-shadow: 0 0 12px rgba(180,130,220,0.08); }
.auto-badge {
font-size: 0.7rem;
background: rgba(180,130,220,0.15);
color: var(--purple);
border: 1px solid rgba(180,130,220,0.3);
border-radius: 6px;
padding: 2px 7px;
font-weight: 700;
}
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
padding: 10px 18px;
border-radius: 10px;
font-size: 0.84rem;
font-weight: 600;
z-index: 200;
opacity: 0;
transition: opacity .3s;
pointer-events: none;
}
.toast.show { opacity: 1; }
.toast.ok { background: rgba(74,204,100,0.15); border: 1px solid var(--green); color: var(--green); }
.toast.err { background: rgba(224,85,85,0.15); border: 1px solid var(--red); color: var(--red); }
</style>
</head>
<body>
<!-- Top Bar -->
<div class="topbar">
<div class="topbar-title">🎭 Αγορά</div>
<span id="online-indicator"><span class="status-dot" id="status-dot"></span><span id="status-text">Φόρτωση...</span></span>
<a href="/player/{{ player_id }}/{{ world_id }}">← Hub</a>
</div>
<!-- Main Layout -->
<div class="main">
<!-- Left: Town List -->
<div class="left-panel">
<div class="panel-header">Πόλεις (<span id="town-count">0</span>)</div>
<div class="search-box">
<input type="text" id="search" placeholder="Αναζήτηση πόλης...">
</div>
<div class="town-list" id="town-list">
<div style="padding:1.5rem;color:#555;font-size:0.82rem">Φόρτωση...</div>
</div>
</div>
<!-- Right: Cards + Log -->
<div class="right-panel">
<div class="cards-area" id="cards-area">
<div class="no-selection">⬅ Επέλεξε πόλη για να δεις τις διαθέσιμες εορτές</div>
</div>
<!-- Log Panel -->
<div class="log-panel">
<div class="log-header" onclick="toggleLog()">
<span>📜 Αρχείο Αγοράς</span>
<span id="log-toggle-icon"></span>
</div>
<div class="log-body open" id="log-body">
<div class="log-empty" id="log-empty">Δεν υπάρχουν εγγραφές ακόμα.</div>
</div>
</div>
</div>
</div>
<!-- Confirm Modal -->
<div class="modal-overlay" id="confirm-modal">
<div class="modal-box">
<div class="modal-title" id="modal-title">Επιβεβαίωση Εορτής</div>
<div id="modal-rows"></div>
<div class="modal-actions">
<button class="btn-cancel" onclick="closeModal()">Ακύρωση</button>
<button class="btn-confirm" id="btn-confirm" onclick="fireCommand()">⚡ Εκτέλεση</button>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
const PLAYER_ID = '{{ player_id }}';
const WORLD_ID = '{{ world_id }}';
const TYPE_LABELS = { party: 'Γιορτή πόλης 🎉', triumph: 'Παρέλαση θριάμβου ⚔️' };
const PARTY_COSTS = { wood: 15000, stone: 18000, iron: 15000 };
let agoraData = [];
let selectedId = null;
let pendingCmd = null; // { town, cel_type }
let logOpen = true;
// ── Fetch agora data ─────────────────────────────────────────────
async function fetchAgora() {
try {
const r = await fetch(`/dashboard/agora?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`);
const d = await r.json();
agoraData = d.towns || [];
document.getElementById('town-count').textContent = agoraData.length;
renderTownList();
if (selectedId) renderCards(agoraData.find(t => t.town_id === selectedId));
} catch (e) { /* silent */ }
}
// ── Render town list ─────────────────────────────────────────────
function renderTownList() {
const q = document.getElementById('search').value.toLowerCase();
const list = document.getElementById('town-list');
const filtered = agoraData.filter(t => t.town_name.toLowerCase().includes(q));
if (!filtered.length) {
list.innerHTML = '<div style="padding:1rem;color:#555;font-size:0.82rem">Δεν βρέθηκαν πόλεις.</div>';
return;
}
list.innerHTML = filtered.map(t => `
<div class="town-row ${t.town_id === selectedId ? 'active' : ''}"
onclick="selectTown('${t.town_id}')">
<span class="t-name">${t.town_name}</span>
<span class="t-dots">
<span class="t-dot ${t.party.status}" title="Γιορτή πόλης: ${t.party.status}"></span>
<span class="t-dot ${t.triumph.status}" title="Παρέλαση: ${t.triumph.status}"></span>
</span>
</div>
`).join('');
}
function selectTown(townId) {
selectedId = townId;
renderTownList();
renderCards(agoraData.find(t => t.town_id === townId));
}
// ── Render celebration cards ─────────────────────────────────────
function renderCards(town) {
const area = document.getElementById('cards-area');
if (!town) {
area.innerHTML = '<div class="no-selection">⬅ Επέλεξε πόλη για να δεις τις διαθέσιμες εορτές</div>';
return;
}
area.innerHTML = `
${celebCard(town, 'party')}
${celebCard(town, 'triumph')}
`;
}
function celebCard(town, type) {
const cel = town[type];
const label = TYPE_LABELS[type];
const autoKey = type === 'party' ? 'auto_party' : 'auto_triumph';
const autoOn = !!town[autoKey];
let costsHtml = '';
if (type === 'party') {
const r = town.resources;
costsHtml = `
<div class="cost-row">
<span class="cost-item ${r.wood >= PARTY_COSTS.wood ? 'ok' : 'bad'}">🪵 ${fmt(r.wood)} / ${fmt(PARTY_COSTS.wood)}</span>
<span class="cost-item ${r.stone >= PARTY_COSTS.stone ? 'ok' : 'bad'}">🪨 ${fmt(r.stone)} / ${fmt(PARTY_COSTS.stone)}</span>
<span class="cost-item ${r.iron >= PARTY_COSTS.iron ? 'ok' : 'bad'}">⚙️ ${fmt(r.iron)} / ${fmt(PARTY_COSTS.iron)}</span>
</div>`;
} else {
const needed = 300;
costsHtml = `
<div class="cost-row">
<span class="cost-item ${town.battle_points >= needed ? 'ok' : 'bad'}">⚔️ ${fmt(town.battle_points)} / ${needed} πόντοι</span>
</div>`;
}
const reasonHtml = cel.reason
? `<div class="reason-box ${cel.status === 'cooldown' ? '' : 'error'}">${cel.reason}</div>`
: '';
const autoToggle = `
<div class="auto-toggle" title="Αυτόματη εκτέλεση όταν ξεκλειδώσουν πόροι/cooldown">
<button id="btn-manual-${town.town_id}-${type}"
class="${!autoOn ? 'active-manual' : ''}"
onclick="saveAutoSetting('${town.town_id}','${type}',false)">
Χειροκίνητο
</button>
<button id="btn-auto-${town.town_id}-${type}"
class="${autoOn ? 'active-auto' : ''}"
onclick="saveAutoSetting('${town.town_id}','${type}',true)">
⚡ Αυτόματο
</button>
</div>`;
return `
<div class="cel-card ${cel.status}${autoOn ? ' auto-on' : ''}">
<div class="cel-card-title">
${label}
${autoOn ? '<span class="auto-badge">AUTO</span>' : ''}
</div>
${costsHtml}
${reasonHtml}
${autoToggle}
<button class="cel-btn primary"
${(cel.available && !autoOn) ? '' : 'disabled'}
onclick="openModal('${town.town_id}','${town.town_name}','${type}')">
${autoOn ? '⚡ Αυτόματο ενεργό' : cel.available ? '▶ Εκκίνηση' : (cel.status === 'cooldown' ? '⏰ Σε αναμονή' : '✖ Μη διαθέσιμο')}
</button>
</div>`;
}
function fmt(n) {
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
return n;
}
// ── Auto-setting toggle ───────────────────────────────────
async function saveAutoSetting(townId, type, enable) {
const town = agoraData.find(t => t.town_id === townId);
if (!town) return;
const payload = {
player_id: PLAYER_ID,
world_id: WORLD_ID,
town_id: townId,
auto_party: type === 'party' ? enable : !!town.auto_party,
auto_triumph: type === 'triumph' ? enable : !!town.auto_triumph
};
try {
const r = await fetch('/dashboard/culture-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const d = await r.json();
if (d.ok) {
// Optimistically update local state and re-render card
town[type === 'party' ? 'auto_party' : 'auto_triumph'] = enable;
renderCards(town);
showToast(enable ? `⚡ Αυτόματο ενεργό — ${TYPE_LABELS[type]}` : `✓ Χειροκίνητο — ${TYPE_LABELS[type]}`, 'ok');
}
} catch (e) {
showToast(`❌ Αποτυχία αποθήκευσης: ${e}`, 'err');
}
}
// ── Modal ────────────────────────────────────────────────────────
function openModal(townId, townName, celType) {
pendingCmd = { townId, townName, celType };
document.getElementById('modal-title').textContent = `Επιβεβαίωση — ${TYPE_LABELS[celType]}`;
let rows = `
<div class="modal-row"><span>Πόλη</span><span>${townName}</span></div>`;
if (celType === 'party') {
rows += `
<div class="modal-row"><span>Ξύλο</span><span>-15.000</span></div>
<div class="modal-row"><span>Πέτρα</span><span>-18.000</span></div>
<div class="modal-row"><span>Σίδερο</span><span>-15.000</span></div>`;
} else {
rows += `<div class="modal-row"><span>Πόντοι Μάχης</span><span>-300</span></div>`;
}
document.getElementById('modal-rows').innerHTML = rows;
document.getElementById('btn-confirm').disabled = false;
document.getElementById('confirm-modal').classList.add('open');
}
function closeModal() {
document.getElementById('confirm-modal').classList.remove('open');
pendingCmd = null;
}
async function fireCommand() {
if (!pendingCmd) return;
document.getElementById('btn-confirm').disabled = true;
// Capture before closeModal() nulls pendingCmd
const { townId, townName, celType } = pendingCmd;
try {
const r = await fetch('/dashboard/culture-command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
player_id: PLAYER_ID,
world_id: WORLD_ID,
town_id: townId,
town_name: townName,
celebration_type: celType
})
});
const d = await r.json();
closeModal();
if (r.ok && d.ok) {
showToast(`✅ Εντολή στάλθηκε — ${TYPE_LABELS[celType]}`, 'ok');
fetchLog();
setTimeout(fetchAgora, 3000);
} else {
showToast(`${d.message || d.error || 'Αποτυχία'}`, 'err');
}
} catch (e) {
showToast(`❌ Σφάλμα: ${e}`, 'err');
}
}
// ── Log ──────────────────────────────────────────────────────────
function toggleLog() {
logOpen = !logOpen;
document.getElementById('log-body').classList.toggle('open', logOpen);
document.getElementById('log-toggle-icon').textContent = logOpen ? '▲' : '▼';
}
async function fetchLog() {
try {
const r = await fetch(`/dashboard/culture-log?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`);
const rows = await r.json();
const body = document.getElementById('log-body');
const empty = document.getElementById('log-empty');
if (!rows.length) {
if (empty) empty.style.display = 'block';
return;
}
if (empty) empty.style.display = 'none';
const existing = new Set([...body.querySelectorAll('.log-entry')].map(el => el.dataset.id));
rows.forEach(row => {
if (existing.has(String(row.id))) return;
const el = document.createElement('div');
el.className = 'log-entry';
el.dataset.id = row.id;
const t = new Date(row.fired_at + 'Z');
const time = t.toLocaleTimeString('el-GR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const costStr = row.cost_battle_pts
? `-${row.cost_battle_pts} πόντοι`
: `-${fmt(row.cost_wood)} ξύλο / -${fmt(row.cost_stone)} πέτρα / -${fmt(row.cost_iron)} σίδερο`;
el.innerHTML = `
<span class="log-time">${time}</span>
<span class="log-town">${row.town_name}</span>
<span class="log-type">${TYPE_LABELS[row.celebration_type] || row.celebration_type}</span>
<span class="log-status ${row.status}">${row.status === 'success' ? '✅' : row.status === 'failed' ? '❌' : '⏳'}</span>
<span class="log-msg">
${row.source === 'auto' ? '<span class="auto-badge" style="margin-right:4px">AUTO</span>' : ''}
${costStr}${row.result_msg ? ' — ' + row.result_msg : ''}
</span>`;
body.insertBefore(el, body.firstChild);
});
} catch (e) { /* silent */ }
}
// ── Online status ────────────────────────────────────────────────
async function checkStatus() {
try {
const r = await fetch(`/dashboard/client-status?player_id=${PLAYER_ID}`);
const d = await r.json();
const dot = document.getElementById('status-dot');
const text = document.getElementById('status-text');
dot.className = 'status-dot' + (d.online ? ' online' : '');
text.textContent = d.online ? 'Online' : 'Offline';
} catch (e) { /* silent */ }
}
// ── Toast ────────────────────────────────────────────────────────
function showToast(msg, type) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = `toast ${type} show`;
setTimeout(() => { t.className = 'toast'; }, 3500);
}
// ── Search ───────────────────────────────────────────────────────
document.getElementById('search').addEventListener('input', renderTownList);
// ── Init ─────────────────────────────────────────────────────────
fetchAgora();
fetchLog();
checkStatus();
setInterval(fetchAgora, 30000);
setInterval(fetchLog, 15000);
setInterval(checkStatus, 30000);
</script>
</body>
</html>