Files
EFTCOMPANION/trader-resets.js
T
2026-06-25 21:26:53 +00:00

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);
})();