Files
reflector-dashboard/lib/changelog.js
T

90 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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: <dmr-portal>/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 `<span class="badge ${x[1]}" style="font-size:9px;">${escHtml(x[0])}</span>`;
}
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 => `
<div class="d-flex align-items-start gap-2 py-1" style="border-bottom:1px solid rgba(128,128,128,.15);">
<span class="text-secondary mono" style="min-width:78px;">${escHtml(e.date || '')}</span>
${typeBadge(e.type)}
<span class="flex-grow-1">${escHtml(e.summary || '')}</span>
<span class="text-secondary mono" style="font-size:10px;" title="${escHtml(e.repo || '')} ${escHtml(e.hash || '')}">${escHtml(e.repo || '')} ${escHtml(e.hash || '')}</span>
</div>`).join('');
body.innerHTML = rows || '<div class="text-secondary">No entries.</div>';
if (!pager) return;
if (pages <= 1) { pager.innerHTML = ''; return; }
const btn = (label, p, dis, act) =>
`<button class="btn btn-sm ${act ? 'btn-primary' : 'btn-outline-secondary'}" style="padding:1px 8px;" ${dis ? 'disabled' : ''} data-chpage="${p}">${label}</button>`;
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 += `<select class="form-select form-select-sm" style="width:auto;padding:1px 6px;" id="chPageSel" title="Jump to page">` +
Array.from({ length: pages }, (_, p) => `<option value="${p}" ${p === page ? 'selected' : ''}>${p + 1}/${pages}</option>`).join('') + `</select>`;
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 = '<div class="text-secondary">Changelog unavailable.</div>';
}
}
return { load, render };
}
window.changelogViewer = changelogViewer;
})();