Files
2026-06-25 21:26:53 +00:00

177 lines
6.4 KiB
HTML
Executable File

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>ESCAPE FROM TARKOV COMPANION — Goons Status</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Global site styles -->
<link rel="stylesheet" href="styles.css" />
<!-- Page-scoped styles for Goons page -->
<link rel="stylesheet" href="goons.css" />
</head>
<body>
<!-- ======= TOP BAR ======= -->
<header class="topbar">
<a href="index.html" class="logo">ESCAPE FROM TARKOV COMPANION</a>
<!-- Hamburger -->
<button class="nav-toggle" type="button" aria-controls="primary-nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="nav-toggle__bar" aria-hidden="true"></span>
<span class="nav-toggle__bar" aria-hidden="true"></span>
<span class="nav-toggle__bar" aria-hidden="true"></span>
</button>
<!-- Primary nav -->
<nav class="main-nav" id="primary-nav" aria-label="Primary navigation">
<ul>
<li><a href="index.html">Home</a></li>
<!-- This page -->
<li><a href="goons.html" aria-current="page">Goons</a></li>
<li><a href="maps.html">Maps</a></li>
<li><a href="quests.html">Quests</a></li>
<li><a href="ammo.html">Ammo</a></li>
<li><a href="traders.html">Traders</a></li>
<li><a href="pricewatch.html">Price Watch</a></li>
<li><a href="crafts.html">Craft Calculator</a></li>
<li><a href="https://tarkovgunsmith.com/ballistics_simulator" target="_blank" rel="noopener">Ballistics Simulator</a></li>
<li><a href="https://tarkov-market.com/" target="_blank" rel="noopener">Flea Market</a></li>
<li><a href="https://escapefromtarkov.fandom.com/wiki/Escape_from_Tarkov_Wiki" target="_blank" rel="noopener">Wiki</a></li>
</ul>
</nav>
</header>
<!-- ======= PAGE CONTENT ======= -->
<main class="content">
<h1 class="hero">Goons Locations</h1>
<p class="page-title">Locations are gathered from public trackers at <a href="https://www.goon-tracker.com" target="_blank" rel="noopener">PVP</a> and <a href="https://www.goon-tracker.com/pvetracker" target="_blank" rel="noopener">PVE</a> and refreshed every 5 minutes.</p>
<!-- Page meta row: stale badge + last updated -->
<div class="page-meta" style="margin-bottom:.5rem;">
<span id="gs-stale" class="chip" title="Data health badge" aria-live="polite">Checking freshness…</span>
<span class="spacer"></span>
<span id="gs-updated" class="badge" title="From local /goons-status.json">Last updated: —</span>
</div>
<!-- PvP + PvE cards -->
<section class="gs-section" aria-labelledby="gs-h2">
<h2 id="gs-h2">Live Snapshot</h2>
<div class="gs-grid">
<div class="gs-card" id="card-pvp">
<h3>Last Seen (PvP)</h3>
<div id="pvp-body" class="muted">Loading PvP last-seen…</div>
</div>
<div class="gs-card" id="card-pve">
<h3>Last Seen (PvE)</h3>
<div id="pve-body" class="muted">Loading PvE last-seen…</div>
</div>
</div>
<div class="muted" style="margin-top:.5rem;">
Sources: community tracker pages. Verify in-game; this is for convenience only.
</div>
</section>
</main>
<!-- HAMBURGER MENU SCRIPT -->
<script>
const btn = document.querySelector('.nav-toggle');
const nav = document.getElementById('primary-nav');
btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', !open);
nav.classList.toggle('is-open', !open);
});
</script>
<!-- PAGE LOGIC -->
<script>
(function() {
'use strict';
const UPDATED = document.getElementById('gs-updated');
const STALE = document.getElementById('gs-stale');
const PVP = document.getElementById('pvp-body');
const PVE = document.getElementById('pve-body');
// Consider data stale after N minutes
const STALE_MINUTES = 30;
function fmtDateLocal(iso) {
try {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '—';
return d.toLocaleString();
} catch { return '—'; }
}
function minutesDiffFromNow(iso) {
try {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return Infinity;
return (Date.now() - d.getTime()) / 60000;
} catch { return Infinity; }
}
function setStaleBadge(fetchedAtIso) {
const mins = minutesDiffFromNow(fetchedAtIso);
if (!isFinite(mins)) {
STALE.textContent = 'STALE (invalid time)';
STALE.classList.add('stale');
return;
}
if (mins > STALE_MINUTES) {
STALE.textContent = `STALE (${Math.round(mins)} min old)`;
STALE.classList.add('stale');
} else {
STALE.textContent = 'Fresh';
STALE.classList.remove('stale');
}
}
function mapPill(map) {
if (!map) return '<span class="muted">—</span>';
return `<span class="map-pill">${map}</span>`;
}
function renderBlock(el, node, label) {
if (!node || !node.map) {
el.innerHTML = `<div class="muted">No recent ${label} data.</div>`;
return;
}
el.innerHTML = `
<div>
<div class="pill-row">${mapPill(node.map)}</div>
${node.timeText ? `<div>Time: ${node.timeText}</div>` : ''}
${node.lastSeenText ? `<div class="muted" style="margin-top:.15rem;">Last seen: ${node.lastSeenText}</div>` : ''}
<div class="muted" style="margin-top:.35rem;">Source: ${node.source ? node.source.replace('https://','') : '—'}</div>
</div>
`;
}
async function init() {
try {
const res = await fetch('/goons-status.json', { cache: 'no-store' });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
// Last updated + stale badge
UPDATED.textContent = 'Last updated: ' + fmtDateLocal(data.fetchedAt || '');
setStaleBadge(data.fetchedAt || '');
// Cards
renderBlock(PVP, data.pvp, 'PvP');
renderBlock(PVE, data.pve, 'PvE');
} catch (e) {
console.error(e);
UPDATED.textContent = 'Last updated: —';
STALE.textContent = 'STALE (fetch failed)';
STALE.classList.add('stale');
PVP.textContent = 'Failed to load PvP last-seen.';
PVE.textContent = 'Failed to load PvE last-seen.';
}
}
init();
})();
</script>
</body>
</html>