MJ: attack coordinator update / various fixes . captcha and back button and jitterloop 2 secs
This commit is contained in:
421
templates/attack_planner.html
Normal file
421
templates/attack_planner.html
Normal file
@@ -0,0 +1,421 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="el">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Attack Planner — Grepolis Remote</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#0f0f1a;--surface:#181824;--border:#2a2a40;--text:#e0e0e0;--muted:#666;
|
||||
--gold:#c8a44a;--red:#e05555;--green:#4acc64;--blue:#6fcfcf;--yellow:#f0c040;--orange:#e07830}
|
||||
body{min-height:100vh;background:var(--bg);font-family:'Inter',sans-serif;color:var(--text);padding:1.5rem 2rem}
|
||||
.page-header{display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem;flex-wrap:wrap}
|
||||
.page-header h1{font-size:1.7rem;font-weight:800;background:linear-gradient(135deg,#c8a44a,#f0c96e,#c8a44a);
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;flex:1}
|
||||
.back-link{color:var(--muted);text-decoration:none;font-size:.85rem;transition:color .2s}
|
||||
.back-link:hover{color:var(--text)}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start}
|
||||
@media(max-width:900px){.grid{grid-template-columns:1fr}}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:1.5rem;margin-bottom:1rem}
|
||||
.card-title{font-size:1rem;font-weight:700;color:var(--gold);margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--border)}
|
||||
label{display:block;font-size:.8rem;color:var(--muted);margin-bottom:.3rem;margin-top:.75rem}
|
||||
label:first-of-type{margin-top:0}
|
||||
input[type=text],input[type=number],input[type=datetime-local]{
|
||||
width:100%;padding:9px 12px;background:#0f0f1a;border:1px solid var(--border);
|
||||
border-radius:8px;color:var(--text);font-size:.875rem;font-family:inherit;transition:border-color .2s}
|
||||
input:focus{outline:none;border-color:var(--gold)}
|
||||
.btn{padding:9px 18px;border:none;border-radius:8px;font-family:inherit;font-weight:600;
|
||||
font-size:.85rem;cursor:pointer;transition:all .2s}
|
||||
.btn-gold{background:var(--gold);color:#0f0f1a}.btn-gold:hover{background:#e0b85a}
|
||||
.btn-red{background:rgba(224,85,85,.15);color:var(--red);border:1px solid rgba(224,85,85,.35)}
|
||||
.btn-red:hover{background:rgba(224,85,85,.25)}
|
||||
.btn-green{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.35)}
|
||||
.btn-green:hover{background:rgba(74,204,100,.25)}
|
||||
.btn-sm{padding:5px 12px;font-size:.78rem}
|
||||
.mt{margin-top:.75rem}
|
||||
table{width:100%;border-collapse:collapse;font-size:.83rem}
|
||||
th{padding:8px 12px;text-align:left;color:var(--muted);font-size:.72rem;text-transform:uppercase;
|
||||
letter-spacing:.05em;border-bottom:1px solid var(--border)}
|
||||
td{padding:9px 12px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle}
|
||||
tr:last-child td{border-bottom:none}
|
||||
tr:hover td{background:rgba(255,255,255,.02)}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:6px;font-size:.72rem;font-weight:700}
|
||||
.badge-draft{background:rgba(102,102,102,.2);color:#999;border:1px solid #444}
|
||||
.badge-active{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.35)}
|
||||
.badge-completed{background:rgba(111,207,207,.15);color:var(--blue);border:1px solid rgba(111,207,207,.35)}
|
||||
.badge-cancelled{background:rgba(224,85,85,.1);color:var(--red);border:1px solid rgba(224,85,85,.2)}
|
||||
.badge-pending{background:rgba(240,192,64,.12);color:var(--yellow);border:1px solid rgba(240,192,64,.3)}
|
||||
.badge-armed{background:rgba(224,120,48,.15);color:var(--orange);border:1px solid rgba(224,120,48,.35)}
|
||||
.badge-sent{background:rgba(74,204,100,.15);color:var(--green);border:1px solid rgba(74,204,100,.35)}
|
||||
.badge-missed{background:rgba(224,85,85,.15);color:var(--red);border:1px solid rgba(224,85,85,.35)}
|
||||
.empty{text-align:center;padding:2rem;color:var(--muted);font-size:.875rem}
|
||||
.error-box{background:rgba(224,85,85,.1);border:1px solid rgba(224,85,85,.3);color:var(--red);
|
||||
padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.75rem}
|
||||
.info-box{background:rgba(111,207,207,.08);border:1px solid rgba(111,207,207,.25);color:var(--blue);
|
||||
padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.75rem}
|
||||
.warn-box{background:rgba(240,192,64,.08);border:1px solid rgba(240,192,64,.25);color:var(--yellow);
|
||||
padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.5rem}
|
||||
#msg{display:none;margin-top:.75rem}
|
||||
.countdown{font-family:monospace;font-weight:700;color:var(--yellow)}
|
||||
.section-sep{height:1px;background:var(--border);margin:1.5rem 0}
|
||||
.plan-row{cursor:pointer}.plan-row:hover td{background:rgba(200,164,74,.05)}
|
||||
.detail-panel{display:none}
|
||||
.detail-panel.open{display:block}
|
||||
.unit-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:.5rem;margin-top:.5rem}
|
||||
.unit-input{display:flex;flex-direction:column;gap:3px}
|
||||
.unit-input label{font-size:.72rem;color:var(--muted);margin:0}
|
||||
.unit-input input{padding:6px 8px;font-size:.82rem}
|
||||
.feasible-ok{color:var(--green);font-size:.78rem}
|
||||
.feasible-err{color:var(--red);font-size:.78rem}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-header">
|
||||
<a href="/player/{{ player_id }}/{{ world_id }}/hub" class="back-link">← Hub</a>
|
||||
<h1>⚔️ Attack Planner — {{ world_id }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<!-- LEFT: Plans list -->
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-title">📋 Ενεργά Πλάνα</div>
|
||||
<div id="plans-list"><div class="empty">Φόρτωση...</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Plan detail panel (shown when a plan is clicked) -->
|
||||
<div class="card detail-panel" id="detail-panel">
|
||||
<div class="card-title" id="detail-title">Λεπτομέρειες Πλάνου</div>
|
||||
<div id="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: Create plan + add participant -->
|
||||
<div>
|
||||
<!-- Create plan -->
|
||||
<div class="card">
|
||||
<div class="card-title">➕ Νέο Πλάνο Επίθεσης</div>
|
||||
|
||||
<label>Όνομα Πλάνου</label>
|
||||
<input type="text" id="plan-name" placeholder="π.χ. Επίθεση στον Leonidas">
|
||||
|
||||
<label>Όνομα Στόχου</label>
|
||||
<input type="text" id="target-name" placeholder="π.χ. Sparta Colony">
|
||||
|
||||
<label>Συντεταγμένες Στόχου (X)</label>
|
||||
<input type="number" id="target-x" placeholder="π.χ. 394" min="0" max="999">
|
||||
|
||||
<label>Συντεταγμένες Στόχου (Y)</label>
|
||||
<input type="number" id="target-y" placeholder="π.χ. 512" min="0" max="999">
|
||||
|
||||
<label>Ώρα Άφιξης (τοπική ώρα)</label>
|
||||
<input type="datetime-local" id="arrival-time">
|
||||
|
||||
<div class="info-box">
|
||||
⏱ Η ώρα άφιξης πρέπει να είναι τουλάχιστον 2 λεπτά στο μέλλον. Όλες οι ώρες αποθηκεύονται σε UTC.
|
||||
</div>
|
||||
|
||||
<div class="mt">
|
||||
<button class="btn btn-gold" onclick="createPlan()">Δημιουργία Πλάνου →</button>
|
||||
</div>
|
||||
<div id="msg"></div>
|
||||
</div>
|
||||
|
||||
<!-- Add participant (shown after plan selected) -->
|
||||
<div class="card" id="add-participant-card" style="display:none">
|
||||
<div class="card-title">👤 Προσθήκη Συμμετέχοντα</div>
|
||||
<p style="font-size:.82rem;color:var(--muted);margin-bottom:.75rem">
|
||||
Πλάνο: <strong id="selected-plan-name" style="color:var(--gold)"></strong>
|
||||
</p>
|
||||
|
||||
<label>Origin Town ID</label>
|
||||
<input type="text" id="p-town-id" placeholder="Από town_state">
|
||||
|
||||
<label>Όνομα Πόλης</label>
|
||||
<input type="text" id="p-town-name" placeholder="Προαιρετικό">
|
||||
|
||||
<label>Συντεταγμένες Πόλης X</label>
|
||||
<input type="number" id="p-x" min="0" max="999">
|
||||
|
||||
<label>Συντεταγμένες Πόλης Y</label>
|
||||
<input type="number" id="p-y" min="0" max="999">
|
||||
|
||||
<label>Θαλάσσια Ζώνη (sea)</label>
|
||||
<input type="number" id="p-sea" placeholder="π.χ. 45">
|
||||
|
||||
<div class="section-sep"></div>
|
||||
<div class="card-title" style="border:none;padding:0;margin-bottom:.5rem">🗡 Μονάδες</div>
|
||||
<div class="unit-grid" id="unit-inputs"></div>
|
||||
|
||||
<div class="mt">
|
||||
<button class="btn btn-gold" onclick="addParticipant()">Υπολογισμός & Προσθήκη</button>
|
||||
</div>
|
||||
<div id="participant-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const PLAYER_ID = '{{ player_id }}';
|
||||
const WORLD_ID = '{{ world_id }}';
|
||||
let selectedPlanId = null;
|
||||
|
||||
// ---- Unit list for inputs (common Grepolis land units) ----
|
||||
const UNITS = [
|
||||
'swordsman','slinger','archer','hoplite','horseman',
|
||||
'chariot','catapult','godsent',
|
||||
'bireme','attack_ship','demolition_ship','transport_ship','colonize_ship'
|
||||
];
|
||||
|
||||
// Build unit input grid
|
||||
const grid = document.getElementById('unit-inputs');
|
||||
UNITS.forEach(u => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'unit-input';
|
||||
div.innerHTML = `<label>${u}</label><input type="number" id="unit_${u}" min="0" value="0" placeholder="0">`;
|
||||
grid.appendChild(div);
|
||||
});
|
||||
|
||||
// ---- Helpers ----
|
||||
function showMsg(el, text, isError) {
|
||||
el.style.display = 'block';
|
||||
el.className = isError ? 'error-box' : 'info-box';
|
||||
el.textContent = text;
|
||||
}
|
||||
|
||||
function statusBadge(s) {
|
||||
return `<span class="badge badge-${s}">${s}</span>`;
|
||||
}
|
||||
|
||||
function formatTs(unix) {
|
||||
if (!unix) return '–';
|
||||
return new Date(unix * 1000).toLocaleString('el-GR');
|
||||
}
|
||||
|
||||
function countdown(unix) {
|
||||
if (!unix) return '–';
|
||||
const s = Math.max(0, Math.floor(unix - Date.now()/1000));
|
||||
const h = Math.floor(s/3600), m = Math.floor((s%3600)/60), ss = s%60;
|
||||
return `<span class="countdown">${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(ss).padStart(2,'0')}</span>`;
|
||||
}
|
||||
|
||||
// ---- Load plans ----
|
||||
async function loadPlans() {
|
||||
const res = await fetch(`/api/${WORLD_ID}/attack_plans`);
|
||||
const data = await res.json();
|
||||
const el = document.getElementById('plans-list');
|
||||
|
||||
if (!data.length) {
|
||||
el.innerHTML = '<div class="empty">Δεν υπάρχουν πλάνα ακόμη.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<table>
|
||||
<thead><tr><th>Πλάνο</th><th>Στόχος</th><th>Άφιξη</th><th>Κατάσταση</th><th>Συμμ.</th><th></th></tr></thead>
|
||||
<tbody>`;
|
||||
for (const p of data) {
|
||||
html += `<tr class="plan-row" onclick="selectPlan(${p.id},'${p.plan_name}')">
|
||||
<td><strong>${p.plan_name}</strong></td>
|
||||
<td>${p.target_town_name || '–'}</td>
|
||||
<td style="font-size:.78rem">${formatTs(p.target_arrival_time)}</td>
|
||||
<td>${statusBadge(p.status)}</td>
|
||||
<td>${p.participant_count}</td>
|
||||
<td>
|
||||
${p.status==='draft' ? `<button class="btn btn-green btn-sm" onclick="event.stopPropagation();armPlan(${p.id})">ARM</button>` : ''}
|
||||
<button class="btn btn-red btn-sm" onclick="event.stopPropagation();cancelPlan(${p.id})" style="margin-left:4px">✕</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
// ---- Select plan → show detail + add participant panel ----
|
||||
window.selectPlan = async function(planId, planName) {
|
||||
selectedPlanId = planId;
|
||||
document.getElementById('selected-plan-name').textContent = planName;
|
||||
document.getElementById('add-participant-card').style.display = 'block';
|
||||
await loadPlanDetail(planId);
|
||||
};
|
||||
|
||||
async function loadPlanDetail(planId) {
|
||||
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${planId}`);
|
||||
const plan = await res.json();
|
||||
const panel = document.getElementById('detail-panel');
|
||||
const body = document.getElementById('detail-body');
|
||||
document.getElementById('detail-title').textContent =
|
||||
`📌 ${plan.plan_name} — ${plan.target_town_name || 'Άγνωστος Στόχος'}`;
|
||||
|
||||
if (!plan.participants || !plan.participants.length) {
|
||||
body.innerHTML = '<div class="empty">Χωρίς συμμετέχοντες ακόμη.</div>';
|
||||
panel.classList.add('open');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<table>
|
||||
<thead><tr>
|
||||
<th>Πόλη</th><th>Τύπος</th><th>Πλοία</th>
|
||||
<th>Αποστολή</th><th>Επιστροφή</th><th>Κατάσταση</th><th>Feasible</th><th></th>
|
||||
</tr></thead><tbody>`;
|
||||
for (const p of plan.participants) {
|
||||
const feasHtml = p.is_feasible
|
||||
? '<span class="feasible-ok">✅</span>'
|
||||
: `<span class="feasible-err" title="${p.error_msg}">❌</span>`;
|
||||
html += `<tr>
|
||||
<td><strong>${p.origin_town_name||p.origin_town_id}</strong></td>
|
||||
<td>${p.attack_type||'–'}</td>
|
||||
<td>${p.transport_needed ? p.transport_count : '–'}</td>
|
||||
<td style="font-size:.78rem">${formatTs(p.send_time)}</td>
|
||||
<td style="font-size:.78rem">${formatTs(p.return_time)}</td>
|
||||
<td>${statusBadge(p.status)}</td>
|
||||
<td>${feasHtml}</td>
|
||||
<td>
|
||||
<button class="btn btn-red btn-sm"
|
||||
onclick="removeParticipant(${planId},'${p.origin_town_id}')">✕</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
|
||||
// Return time summary
|
||||
const latest = plan.participants.reduce((mx, p) => Math.max(mx, p.return_time||0), 0);
|
||||
if (latest) {
|
||||
html += `<div class="info-box" style="margin-top:.75rem">
|
||||
🏠 Τελευταία επιστροφή στρατού: <strong>${formatTs(latest)}</strong>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
body.innerHTML = html;
|
||||
panel.classList.add('open');
|
||||
}
|
||||
|
||||
// ---- Create plan ----
|
||||
window.createPlan = async function() {
|
||||
const msg = document.getElementById('msg');
|
||||
const name = document.getElementById('plan-name').value.trim();
|
||||
const tName = document.getElementById('target-name').value.trim();
|
||||
const tx = parseFloat(document.getElementById('target-x').value);
|
||||
const ty = parseFloat(document.getElementById('target-y').value);
|
||||
const dtLocal = document.getElementById('arrival-time').value;
|
||||
|
||||
if (!dtLocal) { showMsg(msg,'Επίλεξε ώρα άφιξης',true); return; }
|
||||
|
||||
const arrivalUnix = Math.floor(new Date(dtLocal).getTime() / 1000);
|
||||
|
||||
const res = await fetch(`/api/${WORLD_ID}/attack_plans`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json',
|
||||
'X-Clan-Key': window.__GRC_CLAN_KEY || '' },
|
||||
body: JSON.stringify({
|
||||
player_id: PLAYER_ID, plan_name: name||'Επίθεση',
|
||||
target_town_name: tName, target_x: tx||null, target_y: ty||null,
|
||||
target_arrival_time: arrivalUnix
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg(msg, `✅ Πλάνο δημιουργήθηκε (ID: ${data.plan_id})`, false);
|
||||
loadPlans();
|
||||
} else {
|
||||
showMsg(msg, `❌ ${data.error}`, true);
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Add participant ----
|
||||
window.addParticipant = async function() {
|
||||
if (!selectedPlanId) return;
|
||||
const result = document.getElementById('participant-result');
|
||||
|
||||
const units = {};
|
||||
UNITS.forEach(u => {
|
||||
const v = parseInt(document.getElementById(`unit_${u}`).value) || 0;
|
||||
if (v > 0) units[u] = v;
|
||||
});
|
||||
|
||||
const body = {
|
||||
requester_player_id: PLAYER_ID,
|
||||
player_id: document.getElementById('p-town-id').value.split('_')[0] || PLAYER_ID,
|
||||
origin_town_id: document.getElementById('p-town-id').value.trim(),
|
||||
origin_town_name:document.getElementById('p-town-name').value.trim(),
|
||||
origin_x: parseFloat(document.getElementById('p-x').value)||null,
|
||||
origin_y: parseFloat(document.getElementById('p-y').value)||null,
|
||||
origin_sea: parseInt(document.getElementById('p-sea').value)||null,
|
||||
units
|
||||
};
|
||||
|
||||
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${selectedPlanId}/participants`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json',
|
||||
'X-Clan-Key': window.__GRC_CLAN_KEY || '' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok || data.is_feasible !== undefined) {
|
||||
let html = data.is_feasible
|
||||
? `<div class="info-box">✅ Feasible — Travel: ${Math.floor(data.travel_time_secs/60)}m, Ships: ${data.transport_count||0}, Send: ${new Date(data.send_time*1000).toLocaleString('el-GR')}</div>`
|
||||
: `<div class="error-box">❌ ${data.error_msg}</div>`;
|
||||
result.innerHTML = html;
|
||||
loadPlanDetail(selectedPlanId);
|
||||
} else {
|
||||
result.innerHTML = `<div class="error-box">❌ ${data.error||'Unknown error'}</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Remove participant ----
|
||||
window.removeParticipant = async function(planId, townId) {
|
||||
if (!confirm('Αφαίρεση συμμετέχοντα;')) return;
|
||||
await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/participants/${townId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json',
|
||||
'X-Clan-Key': window.__GRC_CLAN_KEY || '' },
|
||||
body: JSON.stringify({ requester_player_id: PLAYER_ID })
|
||||
});
|
||||
loadPlanDetail(planId);
|
||||
loadPlans();
|
||||
};
|
||||
|
||||
// ---- Arm plan ----
|
||||
window.armPlan = async function(planId) {
|
||||
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/arm`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json',
|
||||
'X-Clan-Key': window.__GRC_CLAN_KEY || '' },
|
||||
body: JSON.stringify({ player_id: PLAYER_ID })
|
||||
});
|
||||
const data = await res.json();
|
||||
alert(data.ok ? '✅ Πλάνο ενεργοποιήθηκε!' : `❌ ${data.error}`);
|
||||
loadPlans();
|
||||
if (selectedPlanId === planId) loadPlanDetail(planId);
|
||||
};
|
||||
|
||||
// ---- Cancel plan ----
|
||||
window.cancelPlan = async function(planId) {
|
||||
if (!confirm('Ακύρωση πλάνου;')) return;
|
||||
await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/cancel`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json',
|
||||
'X-Clan-Key': window.__GRC_CLAN_KEY || '' },
|
||||
body: JSON.stringify({ player_id: PLAYER_ID })
|
||||
});
|
||||
loadPlans();
|
||||
if (selectedPlanId === planId) {
|
||||
document.getElementById('detail-panel').classList.remove('open');
|
||||
document.getElementById('add-participant-card').style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Auto-refresh every 15s ----
|
||||
loadPlans();
|
||||
setInterval(loadPlans, 15000);
|
||||
setInterval(() => { if (selectedPlanId) loadPlanDetail(selectedPlanId); }, 15000);
|
||||
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,7 +9,7 @@
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1><a href="/" style="text-decoration: none; margin-right: 15px; cursor: pointer;" title="Back to Players">⬅️</a> ⚔️ Grepolis Remote</h1>
|
||||
<h1><a href="/player/{{ player_id }}/{{ world_id }}" style="text-decoration: none; margin-right: 15px; cursor: pointer;" title="Πίσω στο Hub">⬅️</a> ⚔️ Grepolis Remote</h1>
|
||||
<div class="status-indicator" style="display: flex; align-items: center; gap: 10px;">
|
||||
<button class="btn btn-gold btn-sm" id="live-btn" onclick="window.requestLiveSync()" title="Request immediate data update from game" style="padding: 4px 8px; font-size: 0.72rem; border-radius: 4px; border: 1px solid #c8a44a;">⚡ Live Sync</button>
|
||||
<div id="server-status" class="conn-badge">Server…</div>
|
||||
|
||||
@@ -100,6 +100,12 @@
|
||||
.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); }
|
||||
|
||||
/* Attack Planner — red/orange */
|
||||
.hub-card.attack { border-color: #301a1a; }
|
||||
.hub-card.attack::before { background: radial-gradient(circle at top left, rgba(224,85,85,0.08), transparent 70%); }
|
||||
.hub-card.attack:hover { border-color: #e05555; box-shadow: 0 12px 40px rgba(224,85,85,0.15); }
|
||||
.hub-card.attack .card-title { color: #e05555; }
|
||||
|
||||
.card-icon {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 1rem;
|
||||
@@ -173,6 +179,12 @@
|
||||
<div class="card-desc">Παρακολούθηση κινήσεων στρατού σε πραγματικό χρόνο. Εισερχόμενες επιθέσεις και ενισχύσεις.</div>
|
||||
</a>
|
||||
|
||||
<a href="/player/{{ player_id }}/{{ world_id }}/attack-planner" class="hub-card attack">
|
||||
<span class="card-icon">⚔️</span>
|
||||
<div class="card-title">Attack Planner</div>
|
||||
<div class="card-desc">Συντονισμένες επιθέσεις σε ακριβή χρόνο. Υπολογισμός χρόνου αποστολής, επιστροφής και πλοίων.</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<a href="/" class="back-link">← Επιστροφή στην επιλογή παίκτη</a>
|
||||
|
||||
@@ -287,14 +287,23 @@
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" name="admin" onchange="this.form.submit()" {{ 'checked' if m.feat_admin }}> 🏛 Admin
|
||||
</label>
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" name="attack_planner" onchange="this.form.submit()" {{ 'checked' if m.feat_atk_planner }}> ⚔️ Planner
|
||||
</label>
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" name="attack_planner_admin" onchange="this.form.submit()" {{ 'checked' if m.feat_atk_planner_admin }}> 🎯 Planner Admin
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="toggle-group" style="opacity: 0.8;">
|
||||
<span class="toggle-label" style="cursor:default; border-color: {{ '#3fb950' if m.feat_farm else '#30363d' }}; color: {{ '#3fb950' if m.feat_farm else '#8b949e' }};">🌾 Farm</span>
|
||||
<span class="toggle-label" style="cursor:default; border-color: {{ '#3fb950' if m.feat_admin else '#30363d' }}; color: {{ '#3fb950' if m.feat_admin else '#8b949e' }};">🏛 Admin</span>
|
||||
<span class="toggle-label" style="cursor:default; border-color: {{ '#3fb950' if m.feat_atk_planner else '#30363d' }}; color: {{ '#3fb950' if m.feat_atk_planner else '#8b949e' }};">⚔️ Planner</span>
|
||||
<span class="toggle-label" style="cursor:default; border-color: {{ '#3fb950' if m.feat_atk_planner_admin else '#30363d' }}; color: {{ '#3fb950' if m.feat_atk_planner_admin else '#8b949e' }};">🎯 Planner Admin</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td style="color:#8b949e; font-size:0.8rem;">{{ m.joined_at }}</td>
|
||||
<td style="text-align:right;">
|
||||
|
||||
Reference in New Issue
Block a user