/** * Checks the Forgejo repo for newer releases and compares against the running * package.json version. Results are cached in memory (1 hour for success, * 5 minutes for errors) so the status page stays fast under load. */ const REPO_API_BASE = process.env.REPO_API_URL || 'https://dream.scheller.ltd/api/v1/repos/null/BillTracker'; const TTL_OK_MS = 60 * 60 * 1000; // 1 hour on success const TTL_ERROR_MS = 5 * 60 * 1000; // 5 min on error (avoid hammering) const FETCH_TIMEOUT_MS = 8_000; let _cache = { result: null, expiresAt: 0 }; let _pkg = null; function getCurrentVersion() { if (!_pkg) { try { _pkg = require('../package.json'); } catch { _pkg = { version: '0.0.0' }; } } return _pkg.version; } // Returns positive if a > b, negative if a < b, 0 if equal. function compareVersions(a, b) { const parse = v => String(v).replace(/^v/, '').split('.').map(Number); const pa = parse(a), pb = parse(b); for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const diff = (pa[i] || 0) - (pb[i] || 0); if (diff !== 0) return diff; } return 0; } /** * @param {boolean} force Skip the cache and always hit the API. * @returns {Promise} Update status object. */ async function checkForUpdates(force = false) { const now = Date.now(); if (!force && _cache.result && now < _cache.expiresAt) { return { ..._cache.result, cached: true }; } const currentVersion = getCurrentVersion(); const checkedAt = new Date().toISOString(); try { const res = await fetch(`${REPO_API_BASE}/releases/latest`, { headers: { 'Accept': 'application/json', 'User-Agent': `BillTracker/${currentVersion} UpdateCheck`, }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), }); // 404 = no releases published yet — treat as up-to-date if (res.status === 404) { const result = { current_version: currentVersion, latest_version: null, up_to_date: true, has_update: false, latest_release_url: null, published_at: null, last_checked_at: checkedAt, error: null, cached: false, }; _cache = { result, expiresAt: now + TTL_OK_MS }; return result; } if (!res.ok) throw new Error(`Forgejo API returned HTTP ${res.status}`); const release = await res.json(); const rawTag = release.tag_name || ''; const latestVersion = rawTag.replace(/^v/, '') || null; const hasUpdate = latestVersion ? compareVersions(currentVersion, latestVersion) < 0 : false; const result = { current_version: currentVersion, latest_version: latestVersion, up_to_date: !hasUpdate, has_update: hasUpdate, latest_release_url: release.html_url || null, published_at: release.published_at || null, last_checked_at: checkedAt, error: null, cached: false, }; _cache = { result, expiresAt: now + TTL_OK_MS }; return result; } catch (err) { const result = { current_version: currentVersion, latest_version: null, up_to_date: null, // unknown — network or API failure has_update: false, latest_release_url: null, published_at: null, last_checked_at: checkedAt, error: err.message || 'Update check failed', cached: false, }; _cache = { result, expiresAt: now + TTL_ERROR_MS }; return result; } } module.exports = { checkForUpdates };