agoara update

This commit is contained in:
2026-05-06 23:41:52 +03:00
parent 7e98f1292e
commit b824144a6a
9 changed files with 1283 additions and 6 deletions

672
templates/agora.html Normal file
View File

@@ -0,0 +1,672 @@
<!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; }
.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];
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 btnLabel = cel.status === 'cooldown' ? `${cel.reason}`
: cel.status === 'unavailable' ? '✖ Μη διαθέσιμο'
: `▶ Εκκίνηση ${label}`;
return `
<div class="cel-card ${cel.status}">
<div class="cel-card-title">${label}</div>
${costsHtml}
${reasonHtml}
<button class="cel-btn primary"
${cel.available ? '' : 'disabled'}
onclick="openModal('${town.town_id}','${town.town_name}','${type}')">
${cel.available ? `▶ Εκκίνηση` : (cel.status === 'cooldown' ? `⏰ Σε αναμονή` : `✖ Μη διαθέσιμο`)}
</button>
</div>`;
}
function fmt(n) {
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
return n;
}
// ── 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;
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: pendingCmd.townId,
town_name: pendingCmd.townName,
celebration_type: pendingCmd.celType
})
});
const d = await r.json();
closeModal();
if (r.ok && d.ok) {
showToast(`✅ Εντολή στάλθηκε — ${TYPE_LABELS[pendingCmd.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">${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>

View File

@@ -100,6 +100,11 @@
.hub-card.tracker::before { background: radial-gradient(circle at top left, rgba(111,207,207,0.08), transparent 70%); }
.hub-card.tracker:hover { border-color: #6fcfcf; box-shadow: 0 12px 40px rgba(111,207,207,0.15); }
/* Αγορά — purple/gold */
.hub-card.agora { border-color: #2d1f3f; }
.hub-card.agora::before { background: radial-gradient(circle at top left, rgba(180,130,220,0.08), transparent 70%); }
.hub-card.agora:hover { border-color: #b482dc; box-shadow: 0 12px 40px rgba(180,130,220,0.18); }
.card-icon {
font-size: 2.8rem;
margin-bottom: 1rem;
@@ -113,6 +118,7 @@
.hub-card.admin .card-title { color: #c8a44a; }
.hub-card.farm .card-title { color: #4acc64; }
.hub-card.tracker .card-title { color: #6fcfcf; }
.hub-card.agora .card-title { color: #b482dc; }
.card-desc {
font-size: 0.875rem;
@@ -173,6 +179,12 @@
<div class="card-desc">Παρακολούθηση κινήσεων στρατού σε πραγματικό χρόνο. Εισερχόμενες επιθέσεις και ενισχύσεις.</div>
</a>
<a href="/player/{{ player_id }}/{{ world_id }}/agora" class="hub-card agora">
<span class="card-icon">🎭</span>
<div class="card-title">Αγορά</div>
<div class="card-desc">Εκκίνηση Γιορτής πόλης και Παρέλασης θριάμβου ανά πόλη. Έλεγχος πόρων, cooldown και ιστορικό εκτελέσεων.</div>
</a>
</div>
<a href="/" class="back-link">← Επιστροφή στην επιλογή παίκτη</a>