feat: GitHub Release Management v1.3.0 - Version menu, dev-mode, downgrade support

This commit is contained in:
2026-02-16 23:39:41 +01:00
parent 441198dd8d
commit ad4cfaf4ec
10 changed files with 687 additions and 81 deletions

View File

@@ -19,6 +19,11 @@
const MENU_ID = 7;
const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
// === GitHub Release Management ===
const GITHUB_REPO = 'TauNeutrino/kantine-overview';
const GITHUB_API = `https://api.github.com/repos/${GITHUB_REPO}`;
const INSTALLER_BASE = `https://htmlpreview.github.io/?https://github.com/${GITHUB_REPO}/blob`;
// === State ===
let allWeeks = [];
let currentWeekNumber = getISOWeek(new Date());
@@ -66,7 +71,7 @@
<div class="brand">
<span class="material-icons-round logo-icon">restaurant_menu</span>
<div class="header-left">
<h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">{{VERSION}}</small></h1>
<h1>Kantinen Übersicht <small class="version-tag" style="font-size: 0.6em; opacity: 0.7; font-weight: 400; cursor: pointer;" title="Klick für Versionsmenü">{{VERSION}}</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div>
</div>
</div>
@@ -168,6 +173,31 @@
</div>
</div>
<div id="version-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h2>📦 Versionen</h2>
<button id="btn-version-close" class="icon-btn" aria-label="Close">
<span class="material-icons-round">close</span>
</button>
</div>
<div class="modal-body">
<div style="margin-bottom: 1rem;">
<strong>Aktuell:</strong> <span id="version-current">{{VERSION}}</span>
</div>
<div class="dev-toggle">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" id="dev-mode-toggle">
<span>Dev-Mode (alle Tags anzeigen)</span>
</label>
</div>
<div id="version-list-container" style="margin-top:1rem;">
<p style="color:var(--text-secondary);">Lade Versionen...</p>
</div>
</div>
</div>
</div>
<main class="container">
<div id="last-updated-banner" class="banner hidden">
<span class="material-icons-round">update</span>
@@ -219,6 +249,29 @@
if (e.target === highlightsModal) highlightsModal.classList.add('hidden');
});
// Version Menu
const versionTag = document.querySelector('.version-tag');
const versionModal = document.getElementById('version-modal');
const btnVersionClose = document.getElementById('btn-version-close');
if (versionTag) {
versionTag.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
openVersionMenu();
});
}
if (btnVersionClose) {
btnVersionClose.addEventListener('click', () => {
versionModal.classList.add('hidden');
});
}
window.addEventListener('click', (e) => {
if (e.target === versionModal) versionModal.classList.add('hidden');
});
btnAddTag.addEventListener('click', () => {
const tag = tagInput.value;
if (addHighlightTag(tag)) {
@@ -1411,54 +1464,77 @@
return card;
}
// === Version Check (periodic, every hour) ===
// === GitHub Release Management ===
// Semver comparison: returns true if remote > local
function isNewer(remote, local) {
if (!remote || !local) return false;
const r = remote.replace(/^v/, '').split('.').map(Number);
const l = local.replace(/^v/, '').split('.').map(Number);
for (let i = 0; i < Math.max(r.length, l.length); i++) {
if ((r[i] || 0) > (l[i] || 0)) return true;
if ((r[i] || 0) < (l[i] || 0)) return false;
}
return false;
}
// GitHub API headers
function githubHeaders() {
return { 'Accept': 'application/vnd.github.v3+json' };
}
// Fetch versions from GitHub (releases or tags)
async function fetchVersions(devMode) {
const endpoint = devMode
? `${GITHUB_API}/tags?per_page=20`
: `${GITHUB_API}/releases?per_page=20`;
const resp = await fetch(endpoint, { headers: githubHeaders() });
if (!resp.ok) throw new Error(`GitHub API ${resp.status}`);
const data = await resp.json();
// Normalize to common format: { tag, name, url, body }
return data.map(item => {
const tag = devMode ? item.name : item.tag_name;
return {
tag,
name: devMode ? tag : (item.name || tag),
url: `${INSTALLER_BASE}/${tag}/dist/install.html`,
body: item.body || ''
};
});
}
// Periodic update check (runs on init + every hour)
async function checkForUpdates() {
console.log('[Kantine] Starting update check...');
const currentVersion = '{{VERSION}}';
const versionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';
const installerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
try {
console.log(`[Kantine] Fetching ${versionUrl}...`);
const resp = await fetch(versionUrl, { cache: 'no-cache' });
console.log(`[Kantine] Fetch status: ${resp.status}`);
const versions = await fetchVersions(devMode);
if (!versions.length) return;
if (!resp.ok) {
console.warn(`[Kantine] Version Check HTTP Error: ${resp.status}`);
return;
}
const remoteVersion = (await resp.text()).trim();
// Cache for version menu
localStorage.setItem('kantine_version_cache', JSON.stringify({
timestamp: Date.now(), devMode, versions
}));
console.log(`[Kantine] Version Check: Local [${currentVersion}] vs Remote [${remoteVersion}]`);
const latest = versions[0].tag;
console.log(`[Kantine] Version Check: Local [${currentVersion}] vs Latest [${latest}] (${devMode ? 'dev' : 'stable'})`);
// Check if remote is NEWER (simple semver check)
const isNewer = (remote, local) => {
if (!remote || !local) return false;
const r = remote.replace(/^v/, '').split('.').map(Number);
const l = local.replace(/^v/, '').split('.').map(Number);
for (let i = 0; i < Math.max(r.length, l.length); i++) {
if ((r[i] || 0) > (l[i] || 0)) return true;
if ((r[i] || 0) < (l[i] || 0)) return false;
}
return false;
};
if (!isNewer(latest, currentVersion)) return;
if (!isNewer(remoteVersion, currentVersion)) {
console.log('[Kantine] No update needed (Remote is not newer).');
return;
}
console.log(`[Kantine] Update verfügbar: ${remoteVersion}`);
console.log(`[Kantine] Update verfügbar: ${latest}`);
// Show 🆕 icon in header (only once)
const headerTitle = document.querySelector('.header-left h1');
if (headerTitle && !headerTitle.querySelector('.update-icon')) {
const icon = document.createElement('a');
icon.className = 'update-icon';
icon.href = installerUrl;
icon.href = versions[0].url;
icon.target = '_blank';
icon.innerHTML = '🆕';
icon.title = `Update verfügbar: ${remoteVersion} — Klick zum Installieren`;
icon.title = `Update: ${latest} — Klick zum Installieren`;
icon.style.cssText = 'margin-left:8px;font-size:1em;text-decoration:none;cursor:pointer;vertical-align:middle;';
headerTitle.appendChild(icon);
}
@@ -1467,6 +1543,89 @@
}
}
// Open Version Menu modal
function openVersionMenu() {
const modal = document.getElementById('version-modal');
const container = document.getElementById('version-list-container');
const devToggle = document.getElementById('dev-mode-toggle');
const currentVersion = '{{VERSION}}';
if (!modal) return;
modal.classList.remove('hidden');
// Set current version display
const cur = document.getElementById('version-current');
if (cur) cur.textContent = currentVersion;
// Init dev toggle
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
devToggle.checked = devMode;
// Load versions (from cache or fresh)
async function loadVersions(forceRefresh) {
const dm = devToggle.checked;
container.innerHTML = '<p style="color:var(--text-secondary);">Lade Versionen...</p>';
try {
let versions;
const cached = JSON.parse(localStorage.getItem('kantine_version_cache') || 'null');
if (!forceRefresh && cached && cached.devMode === dm && (Date.now() - cached.timestamp < 3600000)) {
versions = cached.versions;
} else {
versions = await fetchVersions(dm);
localStorage.setItem('kantine_version_cache', JSON.stringify({
timestamp: Date.now(), devMode: dm, versions
}));
}
if (!versions.length) {
container.innerHTML = '<p style="color:var(--text-secondary);">Keine Versionen gefunden.</p>';
return;
}
container.innerHTML = '<ul class="version-list"></ul>';
const list = container.querySelector('.version-list');
versions.forEach(v => {
const isCurrent = v.tag === currentVersion;
const isNew = isNewer(v.tag, currentVersion);
const li = document.createElement('li');
li.className = 'version-item' + (isCurrent ? ' current' : '');
let badge = '';
if (isCurrent) badge = '<span class="badge-current">✓ Installiert</span>';
else if (isNew) badge = '<span class="badge-new">⬆ Neu!</span>';
let action = '';
if (!isCurrent) {
action = `<a href="${v.url}" target="_blank" class="install-link" title="${v.tag} installieren">Installieren</a>`;
}
li.innerHTML = `
<div class="version-info">
<strong>${v.tag}</strong>
${badge}
</div>
${action}
`;
list.appendChild(li);
});
} catch (e) {
container.innerHTML = `<p style="color:#e94560;">Fehler: ${e.message}</p>`;
}
}
loadVersions(false);
// Dev toggle handler
devToggle.onchange = () => {
localStorage.setItem('kantine_dev_mode', devToggle.checked);
// Clear cache to force refresh when mode changes
localStorage.removeItem('kantine_version_cache');
loadVersions(true);
};
}
// === Order Countdown ===
function updateCountdown() {
const now = new Date();