various fixes totest

This commit is contained in:
2026-05-03 13:50:37 +03:00
parent eb31072c87
commit 11f30f4c6a
7 changed files with 199 additions and 115 deletions

2
db.py
View File

@@ -182,6 +182,7 @@ def init_db():
'ALTER TABLE commands ADD COLUMN position INTEGER', 'ALTER TABLE commands ADD COLUMN position INTEGER',
'ALTER TABLE farm_settings ADD COLUMN bandit_camp_enabled INTEGER NOT NULL DEFAULT 0', 'ALTER TABLE farm_settings ADD COLUMN bandit_camp_enabled INTEGER NOT NULL DEFAULT 0',
"ALTER TABLE clan_members ADD COLUMN features TEXT NOT NULL DEFAULT 'farm,admin'", "ALTER TABLE clan_members ADD COLUMN features TEXT NOT NULL DEFAULT 'farm,admin'",
'ALTER TABLE clan_members ADD COLUMN world_id TEXT',
'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)', 'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)',
]: ]:
try: try:
@@ -224,6 +225,7 @@ def init_db():
clan_id INTEGER NOT NULL REFERENCES clans(id), clan_id INTEGER NOT NULL REFERENCES clans(id),
player_id TEXT NOT NULL, player_id TEXT NOT NULL,
player_name TEXT, player_name TEXT,
world_id TEXT,
features TEXT NOT NULL DEFAULT 'farm,admin', features TEXT NOT NULL DEFAULT 'farm,admin',
joined_at TEXT NOT NULL DEFAULT (datetime('now')), joined_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(clan_id, player_id) UNIQUE(clan_id, player_id)

View File

@@ -29,17 +29,17 @@ def _get_clan_from_request():
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Helper — auto-register a player_id under a clan on first push. # Helper — auto-register a player_id under a clan on first push.
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _auto_register_member(clan_id, player_id, player_name): def _auto_register_member(clan_id, player_id, player_name, world_id=''):
conn = get_db() conn = get_db()
conn.execute(''' conn.execute('''
INSERT OR IGNORE INTO clan_members (clan_id, player_id, player_name) INSERT OR IGNORE INTO clan_members (clan_id, player_id, player_name, world_id)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?)
''', (clan_id, str(player_id), player_name or '')) ''', (clan_id, str(player_id), player_name or '', world_id or ''))
# Update name in case it changed # Update name and world on every push (they can change)
conn.execute(''' conn.execute('''
UPDATE clan_members SET player_name = ? UPDATE clan_members SET player_name = ?, world_id = ?
WHERE clan_id = ? AND player_id = ? WHERE clan_id = ? AND player_id = ?
''', (player_name or '', clan_id, str(player_id))) ''', (player_name or '', world_id or '', clan_id, str(player_id)))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -63,7 +63,7 @@ def receive_state():
# Auto-register this player to the clan that matches the key (if any) # Auto-register this player to the clan that matches the key (if any)
clan = _get_clan_from_request() clan = _get_clan_from_request()
if clan: if clan:
_auto_register_member(clan['id'], player_id, player) _auto_register_member(clan['id'], player_id, player, world_id)
conn = get_db() conn = get_db()
c = conn.cursor() c = conn.cursor()

View File

@@ -126,7 +126,7 @@ def options():
members = [] members = []
if clan: if clan:
rows = conn.execute( rows = conn.execute(
'''SELECT cm.id, cm.player_id, cm.player_name, cm.joined_at, cm.features, '''SELECT cm.id, cm.player_id, cm.player_name, cm.world_id, cm.joined_at, cm.features,
ts.updated_at ts.updated_at
FROM clan_members cm FROM clan_members cm
LEFT JOIN town_state ts ON ts.player_id = cm.player_id LEFT JOIN town_state ts ON ts.player_id = cm.player_id
@@ -147,14 +147,15 @@ def options():
except Exception: except Exception:
pass pass
members.append({ members.append({
'id': row['id'], 'id': row['id'],
'player_id': row['player_id'], 'player_id': row['player_id'],
'player_name': row['player_name'] or 'Άγνωστος', 'player_name': row['player_name'] or 'Άγνωστος',
'joined_at': row['joined_at'][:10] if row['joined_at'] else '', 'world_id': row['world_id'] or '',
'is_online': is_online, 'joined_at': row['joined_at'][:10] if row['joined_at'] else '',
'feat_farm': 'farm' in (row['features'] or 'farm,admin'), 'is_online': is_online,
'feat_admin': 'admin' in (row['features'] or 'farm,admin'), 'feat_farm': 'farm' in (row['features'] or 'farm,admin'),
'feat_atk_planner': 'attack_planner' in (row['features'] or ''), 'feat_admin': 'admin' in (row['features'] or 'farm,admin'),
'feat_atk_planner': 'attack_planner' in (row['features'] or ''),
'feat_atk_planner_admin': 'attack_planner_admin' in (row['features'] or ''), 'feat_atk_planner_admin': 'attack_planner_admin' in (row['features'] or ''),
}) })

View File

@@ -100,7 +100,19 @@ def player_tracker(player_id, world_id):
@dashboard.route('/player/<player_id>/<world_id>/attack-planner') @dashboard.route('/player/<player_id>/<world_id>/attack-planner')
@login_required @login_required
def player_attack_planner(player_id, world_id): def player_attack_planner(player_id, world_id):
return render_template('attack_planner.html', player_id=player_id, world_id=world_id) clan_key = ''
if current_user.clan_id:
conn = get_db()
clan = conn.execute(
'SELECT clan_key FROM clans WHERE id = ?', (current_user.clan_id,)
).fetchone()
conn.close()
if clan:
clan_key = clan['clan_key']
return render_template('attack_planner.html',
player_id=player_id,
world_id=world_id,
clan_key=clan_key)
# ------------------------------------------------------------------ # ------------------------------------------------------------------

View File

@@ -21,10 +21,11 @@
.card-title{font-size:1rem;font-weight:700;color:var(--gold);margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--border)} .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{display:block;font-size:.8rem;color:var(--muted);margin-bottom:.3rem;margin-top:.75rem}
label:first-of-type{margin-top:0} label:first-of-type{margin-top:0}
input[type=text],input[type=number],input[type=datetime-local]{ input[type=text],input[type=number],input[type=datetime-local],select{
width:100%;padding:9px 12px;background:#0f0f1a;border:1px solid var(--border); 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} border-radius:8px;color:var(--text);font-size:.875rem;font-family:inherit;transition:border-color .2s}
input:focus{outline:none;border-color:var(--gold)} input:focus,select:focus{outline:none;border-color:var(--gold)}
select option{background:#181824}
.btn{padding:9px 18px;border:none;border-radius:8px;font-family:inherit;font-weight:600; .btn{padding:9px 18px;border:none;border-radius:8px;font-family:inherit;font-weight:600;
font-size:.85rem;cursor:pointer;transition:all .2s} font-size:.85rem;cursor:pointer;transition:all .2s}
.btn-gold{background:var(--gold);color:#0f0f1a}.btn-gold:hover{background:#e0b85a} .btn-gold{background:var(--gold);color:#0f0f1a}.btn-gold:hover{background:#e0b85a}
@@ -56,9 +57,8 @@
padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.75rem} 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); .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} padding:10px 14px;border-radius:8px;font-size:.83rem;margin-top:.5rem}
#msg{display:none;margin-top:.75rem} .town-meta{font-size:.75rem;color:var(--muted);margin-top:4px}
.countdown{font-family:monospace;font-weight:700;color:var(--yellow)} .section-sep{height:1px;background:var(--border);margin:1rem 0}
.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)} .plan-row{cursor:pointer}.plan-row:hover td{background:rgba(200,164,74,.05)}
.detail-panel{display:none} .detail-panel{display:none}
.detail-panel.open{display:block} .detail-panel.open{display:block}
@@ -68,25 +68,32 @@
.unit-input input{padding:6px 8px;font-size:.82rem} .unit-input input{padding:6px 8px;font-size:.82rem}
.feasible-ok{color:var(--green);font-size:.78rem} .feasible-ok{color:var(--green);font-size:.78rem}
.feasible-err{color:var(--red);font-size:.78rem} .feasible-err{color:var(--red);font-size:.78rem}
#msg{display:none;margin-top:.75rem}
</style> </style>
</head> </head>
<body> <body>
<!-- Inject credentials for API calls from the web browser -->
<script>
window.__GRC_CLAN_KEY = "{{ clan_key }}";
window.PLAYER_ID = "{{ player_id }}";
window.WORLD_ID = "{{ world_id }}";
</script>
<div class="page-header"> <div class="page-header">
<a href="/player/{{ player_id }}/{{ world_id }}/hub" class="back-link">← Hub</a> <a href="/player/{{ player_id }}/{{ world_id }}" class="back-link">← Hub</a>
<h1>⚔️ Attack Planner — {{ world_id }}</h1> <h1>⚔️ Attack Planner — {{ world_id }}</h1>
</div> </div>
<div class="grid"> <div class="grid">
<!-- LEFT: Plans list --> <!-- LEFT: Plans list + detail -->
<div> <div>
<div class="card"> <div class="card">
<div class="card-title">📋 Ενεργά Πλάνα</div> <div class="card-title">📋 Πλάνα Επίθεσης — {{ world_id }}</div>
<div id="plans-list"><div class="empty">Φόρτωση...</div></div> <div id="plans-list"><div class="empty">Φόρτωση...</div></div>
</div> </div>
<!-- Plan detail panel (shown when a plan is clicked) -->
<div class="card detail-panel" id="detail-panel"> <div class="card detail-panel" id="detail-panel">
<div class="card-title" id="detail-title">Λεπτομέρειες Πλάνου</div> <div class="card-title" id="detail-title">Λεπτομέρειες Πλάνου</div>
<div id="detail-body"></div> <div id="detail-body"></div>
@@ -95,27 +102,29 @@
<!-- RIGHT: Create plan + add participant --> <!-- RIGHT: Create plan + add participant -->
<div> <div>
<!-- Create plan -->
<div class="card"> <div class="card">
<div class="card-title"> Νέο Πλάνο Επίθεσης</div> <div class="card-title"> Νέο Πλάνο</div>
<label>Όνομα Πλάνου</label> <label>Όνομα Πλάνου</label>
<input type="text" id="plan-name" placeholder="π.χ. Επίθεση στον Leonidas"> <input type="text" id="plan-name" placeholder="π.χ. Επίθεση στον Leonidas">
<label>Κόσμος (World)</label>
<input type="text" id="plan-world" value="{{ world_id }}" readonly style="opacity:.6;cursor:not-allowed">
<label>Όνομα Στόχου</label> <label>Όνομα Στόχου</label>
<input type="text" id="target-name" placeholder="π.χ. Sparta Colony"> <input type="text" id="target-name" placeholder="π.χ. Sparta Colony">
<label>Συντεταγμένες Στόχου (X)</label> <label>Συντεταγμένες Στόχου X</label>
<input type="number" id="target-x" placeholder="π.χ. 394" min="0" max="999"> <input type="number" id="target-x" placeholder="π.χ. 503" min="0" max="999">
<label>Συντεταγμένες Στόχου (Y)</label> <label>Συντεταγμένες Στόχου Y</label>
<input type="number" id="target-y" placeholder="π.χ. 512" min="0" max="999"> <input type="number" id="target-y" placeholder="π.χ. 474" min="0" max="999">
<label>Ώρα Άφιξης (τοπική ώρα)</label> <label>Ώρα Άφιξης (τοπική ώρα)</label>
<input type="datetime-local" id="arrival-time"> <input type="datetime-local" id="arrival-time">
<div class="info-box"> <div class="warn-box mt">
Η ώρα άφιξης πρέπει να είναι τουλάχιστον 2 λεπτά στο μέλλον. Όλες οι ώρες αποθηκεύονται σε UTC. Η ώρα άφιξης πρέπει να είναι τουλάχιστον 2 λεπτά στο μέλλον.
</div> </div>
<div class="mt"> <div class="mt">
@@ -124,30 +133,21 @@
<div id="msg"></div> <div id="msg"></div>
</div> </div>
<!-- Add participant (shown after plan selected) --> <!-- Add participant — only shown after a plan is selected -->
<div class="card" id="add-participant-card" style="display:none"> <div class="card" id="add-participant-card" style="display:none">
<div class="card-title">👤 Προσθήκη Συμμετέχοντα</div> <div class="card-title">👤 Προσθήκη Πόλης στο Πλάνο</div>
<p style="font-size:.82rem;color:var(--muted);margin-bottom:.75rem"> <p style="font-size:.82rem;color:var(--muted);margin-bottom:.75rem">
Πλάνο: <strong id="selected-plan-name" style="color:var(--gold)"></strong> Πλάνο: <strong id="selected-plan-name" style="color:var(--gold)"></strong>
</p> </p>
<label>Origin Town ID</label> <label>Επιλογή Πόλης ({{ world_id }})</label>
<input type="text" id="p-town-id" placeholder="Από town_state"> <select id="p-town-select" onchange="onTownSelected()">
<option value="">— Επίλεξε πόλη —</option>
<label>Όνομα Πόλης</label> </select>
<input type="text" id="p-town-name" placeholder="Προαιρετικό"> <div class="town-meta" id="p-town-meta"></div>
<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="section-sep"></div>
<div class="card-title" style="border:none;padding:0;margin-bottom:.5rem">🗡 Μονάδες</div> <div style="font-size:.85rem;font-weight:700;color:var(--gold);margin-bottom:.5rem">🗡 Μονάδες</div>
<div class="unit-grid" id="unit-inputs"></div> <div class="unit-grid" id="unit-inputs"></div>
<div class="mt"> <div class="mt">
@@ -161,14 +161,18 @@
<script> <script>
(function() { (function() {
const PLAYER_ID = '{{ player_id }}'; const PLAYER_ID = window.PLAYER_ID;
const WORLD_ID = '{{ world_id }}'; const WORLD_ID = window.WORLD_ID;
const CLAN_KEY = window.__GRC_CLAN_KEY;
let selectedPlanId = null; let selectedPlanId = null;
// ---- Unit list for inputs (common Grepolis land units) ---- // ---- Town data loaded from DB ----
let townData = []; // array of town objects with x, y, sea, town_id, town_name
// ---- Unit list ----
const UNITS = [ const UNITS = [
'swordsman','slinger','archer','hoplite','horseman', 'swordsman','slinger','archer','hoplite','horseman',
'chariot','catapult','godsent', 'chariot','catapult',
'bireme','attack_ship','demolition_ship','transport_ship','colonize_ship' 'bireme','attack_ship','demolition_ship','transport_ship','colonize_ship'
]; ];
@@ -177,10 +181,51 @@
UNITS.forEach(u => { UNITS.forEach(u => {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'unit-input'; div.className = 'unit-input';
div.innerHTML = `<label>${u}</label><input type="number" id="unit_${u}" min="0" value="0" placeholder="0">`; div.innerHTML = `<label>${u}</label><input type="number" id="unit_${u}" min="0" value="0">`;
grid.appendChild(div); grid.appendChild(div);
}); });
// ---- Load player's towns for this world ----
async function loadTowns() {
try {
const res = await fetch(`/dashboard/towns?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`);
const towns = await res.json();
townData = towns;
const sel = document.getElementById('p-town-select');
// Clear old options (keep first placeholder)
while (sel.options.length > 1) sel.remove(1);
if (!towns.length) {
sel.options[0].text = '— Δεν βρέθηκαν πόλεις (script offline?) —';
return;
}
towns.forEach(t => {
const opt = document.createElement('option');
opt.value = t.town_id;
opt.textContent = `${t.town_name} (${t.x}, ${t.y})`;
sel.appendChild(opt);
});
} catch(e) {
console.error('Failed to load towns:', e);
}
}
// ---- When a town is selected from dropdown ----
window.onTownSelected = function() {
const sel = document.getElementById('p-town-select');
const tid = sel.value;
const meta = document.getElementById('p-town-meta');
if (!tid) { meta.textContent = ''; return; }
const town = townData.find(t => String(t.town_id) === String(tid));
if (town) {
meta.innerHTML =
`🗺 X: <strong>${town.x}</strong> &nbsp; Y: <strong>${town.y}</strong> &nbsp; Sea: <strong>${town.sea}</strong> &nbsp; World: <strong>${town.world_id}</strong>`;
}
};
// ---- Helpers ---- // ---- Helpers ----
function showMsg(el, text, isError) { function showMsg(el, text, isError) {
el.style.display = 'block'; el.style.display = 'block';
@@ -197,37 +242,39 @@
return new Date(unix * 1000).toLocaleString('el-GR'); return new Date(unix * 1000).toLocaleString('el-GR');
} }
function countdown(unix) { function apiHeaders() {
if (!unix) return ''; return { 'Content-Type': 'application/json', 'X-Clan-Key': CLAN_KEY };
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 ---- // ---- Load plans list ----
async function loadPlans() { async function loadPlans() {
const res = await fetch(`/api/${WORLD_ID}/attack_plans`); const res = await fetch(`/api/${WORLD_ID}/attack_plans`);
const data = await res.json(); const data = await res.json();
const el = document.getElementById('plans-list'); const el = document.getElementById('plans-list');
if (!data.length) { if (!Array.isArray(data) || !data.length) {
el.innerHTML = '<div class="empty">Δεν υπάρχουν πλάνα ακόμη.</div>'; el.innerHTML = '<div class="empty">Δεν υπάρχουν πλάνα ακόμη.</div>';
return; return;
} }
let html = `<table> let html = `<table>
<thead><tr><th>Πλάνο</th><th>Στόχος</th><th>Άφιξη</th><th>Κατάσταση</th><th>Συμμ.</th><th></th></tr></thead> <thead><tr>
<tbody>`; <th>Πλάνο</th><th>Στόχος</th><th>Άφιξη</th><th>Status</th><th>Συμμ.</th><th></th>
</tr></thead><tbody>`;
for (const p of data) { for (const p of data) {
html += `<tr class="plan-row" onclick="selectPlan(${p.id},'${p.plan_name}')"> html += `<tr class="plan-row" onclick="selectPlan(${p.id},'${p.plan_name}')">
<td><strong>${p.plan_name}</strong></td> <td><strong>${p.plan_name}</strong></td>
<td>${p.target_town_name || ''}</td> <td>${p.target_town_name||''} ${p.target_x?`(${p.target_x},${p.target_y})`:''}
</td>
<td style="font-size:.78rem">${formatTs(p.target_arrival_time)}</td> <td style="font-size:.78rem">${formatTs(p.target_arrival_time)}</td>
<td>${statusBadge(p.status)}</td> <td>${statusBadge(p.status)}</td>
<td>${p.participant_count}</td> <td>${p.participant_count}</td>
<td> <td>
${p.status==='draft' ? `<button class="btn btn-green btn-sm" onclick="event.stopPropagation();armPlan(${p.id})">ARM</button>` : ''} ${p.status==='draft'
<button class="btn btn-red btn-sm" onclick="event.stopPropagation();cancelPlan(${p.id})" style="margin-left:4px">✕</button> ? `<button class="btn btn-green btn-sm" onclick="event.stopPropagation();armPlan(${p.id})">ARM</button>`
: ''}
<button class="btn btn-red btn-sm" style="margin-left:4px"
onclick="event.stopPropagation();cancelPlan(${p.id})">✕</button>
</td> </td>
</tr>`; </tr>`;
} }
@@ -235,7 +282,7 @@
el.innerHTML = html; el.innerHTML = html;
} }
// ---- Select plan → show detail + add participant panel ---- // ---- Select plan → show detail + participant panel ----
window.selectPlan = async function(planId, planName) { window.selectPlan = async function(planId, planName) {
selectedPlanId = planId; selectedPlanId = planId;
document.getElementById('selected-plan-name').textContent = planName; document.getElementById('selected-plan-name').textContent = planName;
@@ -249,7 +296,7 @@
const panel = document.getElementById('detail-panel'); const panel = document.getElementById('detail-panel');
const body = document.getElementById('detail-body'); const body = document.getElementById('detail-body');
document.getElementById('detail-title').textContent = document.getElementById('detail-title').textContent =
`📌 ${plan.plan_name}${plan.target_town_name || 'Άγνωστος Στόχος'}`; `📌 ${plan.plan_name}${plan.target_town_name||'Άγνωστος Στόχος'} (${plan.target_x||'?'}, ${plan.target_y||'?'})`;
if (!plan.participants || !plan.participants.length) { if (!plan.participants || !plan.participants.length) {
body.innerHTML = '<div class="empty">Χωρίς συμμετέχοντες ακόμη.</div>'; body.innerHTML = '<div class="empty">Χωρίς συμμετέχοντες ακόμη.</div>';
@@ -260,20 +307,20 @@
let html = `<table> let html = `<table>
<thead><tr> <thead><tr>
<th>Πόλη</th><th>Τύπος</th><th>Πλοία</th> <th>Πόλη</th><th>Τύπος</th><th>Πλοία</th>
<th>Αποστολή</th><th>Επιστροφή</th><th>Κατάσταση</th><th>Feasible</th><th></th> <th>Αποστολή</th><th>Επιστροφή</th><th>Status</th><th>OK</th><th></th>
</tr></thead><tbody>`; </tr></thead><tbody>`;
for (const p of plan.participants) { for (const p of plan.participants) {
const feasHtml = p.is_feasible const f = p.is_feasible
? '<span class="feasible-ok">✅</span>' ? '<span class="feasible-ok">✅</span>'
: `<span class="feasible-err" title="${p.error_msg}">❌</span>`; : `<span class="feasible-err" title="${p.error_msg||''}">❌</span>`;
html += `<tr> html += `<tr>
<td><strong>${p.origin_town_name||p.origin_town_id}</strong></td> <td><strong>${p.origin_town_name||p.origin_town_id}</strong></td>
<td>${p.attack_type||''}</td> <td style="font-size:.75rem">${p.attack_type||''}</td>
<td>${p.transport_needed ? p.transport_count : ''}</td> <td>${p.transport_needed ? p.transport_count : ''}</td>
<td style="font-size:.78rem">${formatTs(p.send_time)}</td> <td style="font-size:.75rem">${formatTs(p.send_time)}</td>
<td style="font-size:.78rem">${formatTs(p.return_time)}</td> <td style="font-size:.75rem">${formatTs(p.return_time)}</td>
<td>${statusBadge(p.status)}</td> <td>${statusBadge(p.status)}</td>
<td>${feasHtml}</td> <td>${f}</td>
<td> <td>
<button class="btn btn-red btn-sm" <button class="btn btn-red btn-sm"
onclick="removeParticipant(${planId},'${p.origin_town_id}')">✕</button> onclick="removeParticipant(${planId},'${p.origin_town_id}')">✕</button>
@@ -282,11 +329,10 @@
} }
html += '</tbody></table>'; html += '</tbody></table>';
// Return time summary const latest = plan.participants.reduce((m, p) => Math.max(m, p.return_time||0), 0);
const latest = plan.participants.reduce((mx, p) => Math.max(mx, p.return_time||0), 0);
if (latest) { if (latest) {
html += `<div class="info-box" style="margin-top:.75rem"> html += `<div class="info-box" style="margin-top:.75rem">
🏠 Τελευταία επιστροφή στρατού: <strong>${formatTs(latest)}</strong> 🏠 Τελευταία επιστροφή: <strong>${formatTs(latest)}</strong>
</div>`; </div>`;
} }
@@ -296,24 +342,24 @@
// ---- Create plan ---- // ---- Create plan ----
window.createPlan = async function() { window.createPlan = async function() {
const msg = document.getElementById('msg'); const msg = document.getElementById('msg');
const name = document.getElementById('plan-name').value.trim(); const name = document.getElementById('plan-name').value.trim();
const tName = document.getElementById('target-name').value.trim(); const tName= document.getElementById('target-name').value.trim();
const tx = parseFloat(document.getElementById('target-x').value); const tx = parseFloat(document.getElementById('target-x').value) || null;
const ty = parseFloat(document.getElementById('target-y').value); const ty = parseFloat(document.getElementById('target-y').value) || null;
const dtLocal = document.getElementById('arrival-time').value; const dtLocal = document.getElementById('arrival-time').value;
if (!dtLocal) { showMsg(msg,'Επίλεξε ώρα άφιξης',true); return; } if (!dtLocal) { showMsg(msg, 'Επίλεξε ώρα άφιξης', true); return; }
const arrivalUnix = Math.floor(new Date(dtLocal).getTime() / 1000); const arrivalUnix = Math.floor(new Date(dtLocal).getTime() / 1000);
const res = await fetch(`/api/${WORLD_ID}/attack_plans`, { const res = await fetch(`/api/${WORLD_ID}/attack_plans`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', headers: apiHeaders(),
'X-Clan-Key': window.__GRC_CLAN_KEY || '' },
body: JSON.stringify({ body: JSON.stringify({
player_id: PLAYER_ID, plan_name: name||'Επίθεση', player_id: PLAYER_ID,
target_town_name: tName, target_x: tx||null, target_y: ty||null, plan_name: name || 'Επίθεση',
target_town_name: tName,
target_x: tx, target_y: ty,
target_arrival_time: arrivalUnix target_arrival_time: arrivalUnix
}) })
}); });
@@ -326,41 +372,58 @@
} }
}; };
// ---- Add participant ---- // ---- Add participant (uses selected town from dropdown) ----
window.addParticipant = async function() { window.addParticipant = async function() {
if (!selectedPlanId) return; if (!selectedPlanId) return;
const result = document.getElementById('participant-result'); const result = document.getElementById('participant-result');
const sel = document.getElementById('p-town-select');
const tid = sel.value;
if (!tid) {
result.innerHTML = '<div class="error-box">❌ Επίλεξε πόλη πρώτα</div>';
return;
}
const town = townData.find(t => String(t.town_id) === String(tid));
if (!town) {
result.innerHTML = '<div class="error-box">❌ Πόλη δεν βρέθηκε</div>';
return;
}
const units = {}; const units = {};
UNITS.forEach(u => { UNITS.forEach(u => {
const v = parseInt(document.getElementById(`unit_${u}`).value) || 0; const v = parseInt(document.getElementById(`unit_${u}`)?.value) || 0;
if (v > 0) units[u] = v; if (v > 0) units[u] = v;
}); });
const body = { const body = {
requester_player_id: PLAYER_ID, requester_player_id: PLAYER_ID,
player_id: document.getElementById('p-town-id').value.split('_')[0] || PLAYER_ID, player_id: PLAYER_ID,
origin_town_id: document.getElementById('p-town-id').value.trim(), origin_town_id: town.town_id,
origin_town_name:document.getElementById('p-town-name').value.trim(), origin_town_name: town.town_name,
origin_x: parseFloat(document.getElementById('p-x').value)||null, origin_x: town.x,
origin_y: parseFloat(document.getElementById('p-y').value)||null, origin_y: town.y,
origin_sea: parseInt(document.getElementById('p-sea').value)||null, origin_sea: town.sea,
units units
}; };
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${selectedPlanId}/participants`, { const res = await fetch(`/api/${WORLD_ID}/attack_plans/${selectedPlanId}/participants`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', headers: apiHeaders(),
'X-Clan-Key': window.__GRC_CLAN_KEY || '' },
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
const data = await res.json(); const data = await res.json();
if (data.ok || data.is_feasible !== undefined) { if (data.is_feasible !== undefined) {
let html = data.is_feasible if (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>` result.innerHTML = `<div class="info-box">
: `<div class="error-box">❌ ${data.error_msg}</div>`; ✅ Feasible — Χρόνος: ${Math.floor(data.travel_time_secs/60)}m
result.innerHTML = html; &nbsp;|&nbsp; Πλοία: ${data.transport_count||0}
&nbsp;|&nbsp; Αποστολή: ${new Date(data.send_time*1000).toLocaleString('el-GR')}
</div>`;
} else {
result.innerHTML = `<div class="error-box">❌ ${data.error_msg}</div>`;
}
loadPlanDetail(selectedPlanId); loadPlanDetail(selectedPlanId);
} else { } else {
result.innerHTML = `<div class="error-box">❌ ${data.error||'Unknown error'}</div>`; result.innerHTML = `<div class="error-box">❌ ${data.error||'Unknown error'}</div>`;
@@ -372,8 +435,7 @@
if (!confirm('Αφαίρεση συμμετέχοντα;')) return; if (!confirm('Αφαίρεση συμμετέχοντα;')) return;
await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/participants/${townId}`, { await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/participants/${townId}`, {
method: 'DELETE', method: 'DELETE',
headers: { 'Content-Type': 'application/json', headers: apiHeaders(),
'X-Clan-Key': window.__GRC_CLAN_KEY || '' },
body: JSON.stringify({ requester_player_id: PLAYER_ID }) body: JSON.stringify({ requester_player_id: PLAYER_ID })
}); });
loadPlanDetail(planId); loadPlanDetail(planId);
@@ -384,8 +446,7 @@
window.armPlan = async function(planId) { window.armPlan = async function(planId) {
const res = await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/arm`, { const res = await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/arm`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', headers: apiHeaders(),
'X-Clan-Key': window.__GRC_CLAN_KEY || '' },
body: JSON.stringify({ player_id: PLAYER_ID }) body: JSON.stringify({ player_id: PLAYER_ID })
}); });
const data = await res.json(); const data = await res.json();
@@ -399,8 +460,7 @@
if (!confirm('Ακύρωση πλάνου;')) return; if (!confirm('Ακύρωση πλάνου;')) return;
await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/cancel`, { await fetch(`/api/${WORLD_ID}/attack_plans/${planId}/cancel`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', headers: apiHeaders(),
'X-Clan-Key': window.__GRC_CLAN_KEY || '' },
body: JSON.stringify({ player_id: PLAYER_ID }) body: JSON.stringify({ player_id: PLAYER_ID })
}); });
loadPlans(); loadPlans();
@@ -410,8 +470,9 @@
} }
}; };
// ---- Auto-refresh every 15s ---- // ---- Init ----
loadPlans(); loadPlans();
loadTowns();
setInterval(loadPlans, 15000); setInterval(loadPlans, 15000);
setInterval(() => { if (selectedPlanId) loadPlanDetail(selectedPlanId); }, 15000); setInterval(() => { if (selectedPlanId) loadPlanDetail(selectedPlanId); }, 15000);

View File

@@ -257,6 +257,7 @@
<thead> <thead>
<tr> <tr>
<th>Παίκτης</th> <th>Παίκτης</th>
<th>Κόσμος</th>
<th>Κατάσταση</th> <th>Κατάσταση</th>
<th>Δυνατότητες</th> <th>Δυνατότητες</th>
<th>Προστέθηκε</th> <th>Προστέθηκε</th>
@@ -270,6 +271,13 @@
<div class="player-name">{{ m.player_name }}</div> <div class="player-name">{{ m.player_name }}</div>
<div class="player-id">ID: {{ m.player_id }}</div> <div class="player-id">ID: {{ m.player_id }}</div>
</td> </td>
<td>
{% if m.world_id and m.world_id != '' %}
<span style="font-size:0.82rem;font-family:monospace;color:#c8a44a;">{{ m.world_id }}</span>
{% else %}
<span style="font-size:0.78rem;color:#555;"></span>
{% endif %}
</td>
<td> <td>
{% if m.is_online %} {% if m.is_online %}
<span class="status-online">● Online</span> <span class="status-online">● Online</span>

View File

@@ -215,7 +215,7 @@
<body> <body>
<div class="page-header"> <div class="page-header">
<a href="/player/{{ player_id }}/{{ world_id }}/hub" class="back-link">← Πίσω στο Hub</a> <a href="/player/{{ player_id }}/{{ world_id }}" class="back-link">← Πίσω στο Hub</a>
<h1>🛡️ Live Tracker — {{ world_id }}</h1> <h1>🛡️ Live Tracker — {{ world_id }}</h1>
<div id="conn-status" class="wait"> <div id="conn-status" class="wait">
<span class="status-dot"></span> <span class="status-dot"></span>