// SvxPortal — web front-end for SvxReflector // Copyright (C) 2026 SA7AUX (Björn) // SPDX-License-Identifier: AGPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it under // the terms of the GNU Affero General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) any // later version. See the LICENSE file for details. // changelog.js — shared "recent updates" viewer (dmr-web, svx-web). // Canonical copy: /lib/changelog.js; other repos carry a COPY. // Reads changelog.json (newest first, {date, repo, hash, type, summary}) and // renders a client-side paginated list. Needs lib/fmt.js (escHtml). // // const ch = changelogViewer({ body: 'chBody', pager: 'chPager', fetchAt: 'chFetchAt', // url: 'changelog.json', perPage: 6 }); // ch.load(); // fetch once, render; later calls just re-render (function () { 'use strict'; function changelogViewer(opts) { opts = opts || {}; const ids = { body: opts.body || 'chBody', pager: opts.pager || 'chPager', fetchAt: opts.fetchAt || 'chFetchAt' }; const url = opts.url || 'changelog.json'; const perPage = opts.perPage || 6; const $ = id => document.getElementById(id); let entries = null, page = 0, wired = false; function typeBadge(t) { const m = { feature: ['Added', 'bg-success'], fix: ['Fix', 'bg-danger'], change: ['Change', 'bg-info text-dark'] }; const x = m[String(t || '').toLowerCase()] || [t || '—', 'bg-secondary']; return `${escHtml(x[0])}`; } function render() { const body = $(ids.body), pager = $(ids.pager); if (!body || !entries) return; const n = entries.length, pages = Math.max(1, Math.ceil(n / perPage)); page = Math.min(Math.max(0, page), pages - 1); const start = page * perPage; const rows = entries.slice(start, start + perPage).map(e => `
${escHtml(e.date || '')} ${typeBadge(e.type)} ${escHtml(e.summary || '')} ${escHtml(e.repo || '')} ${escHtml(e.hash || '')}
`).join(''); body.innerHTML = rows || '
No entries.
'; if (!pager) return; if (pages <= 1) { pager.innerHTML = ''; return; } const btn = (label, p, dis, act) => ``; let h = btn('«', 0, page === 0) + btn('‹', page - 1, page === 0); const lo = Math.max(0, page - 2), hi = Math.min(pages - 1, page + 2); for (let p = lo; p <= hi; p++) h += btn(String(p + 1), p, false, p === page); h += btn('›', page + 1, page === pages - 1) + btn('»', pages - 1, page === pages - 1); h += ``; pager.innerHTML = h; } async function load() { if (!wired) { wired = true; const pager = $(ids.pager); if (pager) { pager.addEventListener('click', ev => { const b = ev.target.closest('[data-chpage]'); if (!b) return; page = parseInt(b.dataset.chpage, 10) || 0; render(); }); pager.addEventListener('change', ev => { if (ev.target && ev.target.id === 'chPageSel') { page = parseInt(ev.target.value, 10) || 0; render(); } }); } } if (entries) { render(); return; } try { const r = await fetch(url + '?_=' + Date.now(), { credentials: 'omit' }); const j = await r.json(); entries = Array.isArray(j) ? j : (j.entries || []); const fa = $(ids.fetchAt); if (fa && entries[0]) fa.textContent = 'latest ' + (entries[0].date || ''); render(); } catch (_) { const b = $(ids.body); if (b) b.innerHTML = '
Changelog unavailable.
'; } } return { load, render }; } window.changelogViewer = changelogViewer; })();