229 lines
7.1 KiB
JavaScript
Executable File
229 lines
7.1 KiB
JavaScript
Executable File
|
|
// trader-resets.js (v2.2.0) — Local JSON version with ONE unified "data last updated" bubble
|
|
(() => {
|
|
'use strict';
|
|
|
|
// ===== Config =====
|
|
const REFRESH_MS = 5 * 60 * 1000; // auto-refresh every 5 minutes
|
|
const STORAGE_KEY = 'eft.traders.mode'; // 'regular' or 'pve'
|
|
const DEBUG_TS = false;
|
|
|
|
// Local JSON files (served from /var/www/html via cron)
|
|
const DATA_URL = {
|
|
regular: '/traders_pvp.json',
|
|
pve: '/traders_pve.json',
|
|
};
|
|
|
|
// ===== DOM refs =====
|
|
const els = {
|
|
grid: document.getElementById('traderGrid'),
|
|
refresh: document.getElementById('refreshBtn'),
|
|
modeToggle: document.getElementById('modeToggle'),
|
|
dataUpdated: document.getElementById('dataUpdated'),
|
|
};
|
|
|
|
if (!els.grid) {
|
|
console.warn('[trader-resets] Missing #traderGrid.');
|
|
return;
|
|
}
|
|
|
|
// ===== Utilities =====
|
|
// Normalize resetTime (seconds, ms, ISO) -> ms epoch or null
|
|
function toMsEpoch(resetTime) {
|
|
if (resetTime == null) return null;
|
|
if (typeof resetTime === 'number') return resetTime < 1e12 ? resetTime * 1000 : resetTime;
|
|
if (typeof resetTime === 'string') {
|
|
const parsed = Date.parse(resetTime);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function currentMode() {
|
|
return els.modeToggle && els.modeToggle.checked ? 'pve' : 'regular';
|
|
}
|
|
|
|
function fmtHMS(ms) {
|
|
if (!Number.isFinite(ms) || ms <= 0) return '00:00:00';
|
|
const sec = Math.floor(ms / 1000);
|
|
const h = String(Math.floor(sec / 3600)).padStart(2, '0');
|
|
const m = String(Math.floor((sec % 3600) / 60)).padStart(2, '0');
|
|
const s = String(sec % 60).padStart(2, '0');
|
|
return `${h}:${m}:${s}`;
|
|
}
|
|
|
|
async function getLocalTraders(mode) {
|
|
const url = DATA_URL[mode] || DATA_URL.regular;
|
|
const v = Math.floor(Date.now() / 60000); // per-minute cache-bust
|
|
const res = await fetch(`${url}?v=${v}`, { cache: 'no-store' });
|
|
if (!res.ok) throw new Error(`Failed to load ${url}: ${res.status} ${res.statusText}`);
|
|
return res.json(); // array of traders
|
|
}
|
|
|
|
// ===== Last-updated (from static file Last-Modified headers) =====
|
|
async function fetchLastModified(url) {
|
|
try {
|
|
const head = await fetch(url, { method: 'HEAD' });
|
|
const lm = head.headers.get('Last-Modified');
|
|
if (lm) return new Date(lm);
|
|
} catch {}
|
|
try {
|
|
const get = await fetch(url + '?stamp=' + Date.now(), { cache: 'no-store' });
|
|
const lm = get.headers.get('Last-Modified');
|
|
if (lm) return new Date(lm);
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
// Set the *single* bubble to the newest (max) of PvP/PvE file timestamps
|
|
async function refreshUnifiedUpdatedBubble() {
|
|
const [pvp, pve] = await Promise.all([
|
|
fetchLastModified(DATA_URL.regular),
|
|
fetchLastModified(DATA_URL.pve),
|
|
]);
|
|
|
|
const newest = (!pvp && !pve)
|
|
? null
|
|
: (pvp && pve ? (pvp > pve ? pvp : pve) : (pvp || pve));
|
|
|
|
if (els.dataUpdated) {
|
|
els.dataUpdated.textContent =
|
|
`Data last updated: ${newest ? newest.toLocaleString() : '—'}`;
|
|
els.dataUpdated.title = newest ? newest.toISOString() : '';
|
|
}
|
|
}
|
|
|
|
// ===== Rendering =====
|
|
function renderUnknownCard(trader) {
|
|
const card = document.createElement('article');
|
|
card.className = 'trader-card';
|
|
card.setAttribute('role', 'listitem');
|
|
|
|
const figure = document.createElement('figure');
|
|
figure.className = 'trader-figure';
|
|
|
|
const img = document.createElement('img');
|
|
img.className = 'trader-img';
|
|
img.alt = `${trader.name || 'Trader'}`;
|
|
img.loading = 'lazy';
|
|
img.src = trader.imageLink || 'https://tarkov.dev/images/traders/default.jpg';
|
|
figure.appendChild(img);
|
|
|
|
const name = document.createElement('div');
|
|
name.className = 'trader-name';
|
|
name.textContent = trader.name || 'Unknown trader';
|
|
|
|
const timer = document.createElement('div');
|
|
timer.className = 'trader-countdown';
|
|
timer.textContent = 'Unknown';
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'trader-meta';
|
|
meta.textContent = 'Next reset: —';
|
|
|
|
card.appendChild(figure);
|
|
card.appendChild(name);
|
|
card.appendChild(timer);
|
|
card.appendChild(meta);
|
|
els.grid.appendChild(card);
|
|
}
|
|
|
|
function render(traders) {
|
|
els.grid.innerHTML = '';
|
|
const now = Date.now();
|
|
|
|
traders.forEach(t => {
|
|
const nextReset = toMsEpoch(t.resetTime);
|
|
if (DEBUG_TS) {
|
|
console.debug('[trader-resets] raw resetTime:', t.name, t.resetTime, '→', nextReset && new Date(nextReset).toISOString());
|
|
}
|
|
if (!Number.isFinite(nextReset)) {
|
|
renderUnknownCard(t);
|
|
return;
|
|
}
|
|
|
|
const msLeft = nextReset - now;
|
|
|
|
const card = document.createElement('article');
|
|
card.className = 'trader-card';
|
|
card.setAttribute('role', 'listitem');
|
|
|
|
const figure = document.createElement('figure');
|
|
figure.className = 'trader-figure';
|
|
|
|
const img = document.createElement('img');
|
|
img.className = 'trader-img';
|
|
img.alt = t.name || 'Trader';
|
|
img.loading = 'lazy';
|
|
img.src = t.imageLink || 'https://tarkov.dev/images/traders/default.jpg';
|
|
figure.appendChild(img);
|
|
|
|
const name = document.createElement('div');
|
|
name.className = 'trader-name';
|
|
name.textContent = t.name || 'Unknown trader';
|
|
|
|
const timer = document.createElement('div');
|
|
timer.className = 'trader-countdown';
|
|
timer.textContent = fmtHMS(msLeft);
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'trader-meta';
|
|
meta.textContent = `Next reset: ${new Date(nextReset).toLocaleTimeString()}`;
|
|
|
|
card.appendChild(figure);
|
|
card.appendChild(name);
|
|
card.appendChild(timer);
|
|
card.appendChild(meta);
|
|
els.grid.appendChild(card);
|
|
|
|
// Live ticking
|
|
let tickId = setInterval(() => {
|
|
const left = nextReset - Date.now();
|
|
if (left <= 0) {
|
|
timer.textContent = '00:00:00';
|
|
setTimeout(() => { timer.textContent = 'Restocking…'; }, 500);
|
|
clearInterval(tickId);
|
|
return;
|
|
}
|
|
timer.textContent = fmtHMS(left);
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
// ===== Load from local JSON =====
|
|
async function load() {
|
|
try {
|
|
const mode = currentMode(); // 'regular' or 'pve'
|
|
const traders = await getLocalTraders(mode);
|
|
render(traders);
|
|
|
|
// Update the unified "data last updated" bubble
|
|
refreshUnifiedUpdatedBubble().catch(() => {});
|
|
} catch (err) {
|
|
console.error('[trader-resets] load failed:', err);
|
|
if (els.dataUpdated) {
|
|
els.dataUpdated.textContent = 'Data last updated: — (failed to read local JSON)';
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== Persistence: restore & save mode =====
|
|
if (els.modeToggle) {
|
|
const saved = localStorage.getItem(STORAGE_KEY);
|
|
if (saved === 'pve') els.modeToggle.checked = true;
|
|
if (saved === 'regular') els.modeToggle.checked = false;
|
|
|
|
els.modeToggle.addEventListener('change', () => {
|
|
localStorage.setItem(STORAGE_KEY, currentMode());
|
|
load(); // re-render immediately when mode flips
|
|
});
|
|
}
|
|
|
|
// ===== Events =====
|
|
els.refresh?.addEventListener('click', load);
|
|
|
|
// ===== Kickoff =====
|
|
load();
|
|
setInterval(load, REFRESH_MS);
|
|
})();
|