Compare commits

..

8 Commits

Author SHA1 Message Date
Kantine Wrapper
d80863a169 chore: update build artifacts for v1.4.15 2026-02-24 15:38:44 +01:00
Kantine Wrapper
ae79c58d30 fix(flags): properly remove expired flags from localstorage (v1.4.15) 2026-02-24 15:38:44 +01:00
Kantine Wrapper
a0ef6e631e chore: update build artifacts for v1.4.14 2026-02-24 15:31:48 +01:00
Kantine Wrapper
d895a5fb7c feat: immediate api refresh on flag, fix timestamp fallback (v1.4.14) 2026-02-24 15:31:48 +01:00
Kantine Wrapper
fd765a74c0 chore: update build artifacts for v1.4.13 2026-02-24 13:24:50 +01:00
Kantine Wrapper
1f184fab8b fix: replace css variables with direct hex colors for alarm bell 2026-02-24 13:24:09 +01:00
Kantine Wrapper
b6c7c66027 chore: update build artifacts for v1.4.12 2026-02-24 13:20:03 +01:00
Kantine Wrapper
33bb87d7f4 fix: enforce hidden state and default color of alarm bell 2026-02-24 13:19:38 +01:00
8 changed files with 304 additions and 41 deletions

View File

@@ -1,3 +1,17 @@
## v1.4.15
- 🧹 **Bugfix**: In der Vergangenheit gesetzte Alarme/Flags wurden nicht zuverlässig gelöscht. Dies ist nun behoben, sodass verfallene Menüs nach 10:00 Uhr bzw. an vergangenen Tagen automatisch aus dem Tracker verschwinden.
## v1.4.14
- 🐛 **Bugfix**: Alarmglocke versteckt sich jetzt zuverlässig auch auf Endgeräten mit CSS Konflikten
- 🚀 **Feature**: Sofortige API-Aktualisierung (Refresh) bei Aktivierung eines Menüalarms
-**Optimierung**: "Unbekannt" im letzten Refresh-Zeitpunkt wird abgefangen und zeigt initial "gerade eben"
## v1.4.13 (2026-02-24)
- **Fix**: Die Farben der Glocke funktionieren nun verlässlich, da CSS-Variablen durch direkte Hex-Codes ersetzt wurden.
## v1.4.12 (2026-02-24)
- **Fix**: Das Glocken-Icon sollte nun endgültig versteckt bleiben, wenn keine Benachrichtigungen aktiv sind (CSS-Kollision mit `.hidden` behoben).
## v1.4.11 (2026-02-24) ## v1.4.11 (2026-02-24)
- **Feature**: Das Versionsmenü prüft nun im Hintergrund direkt beim Öffnen nach neuen Versionen und aktualisiert die Liste automatisch, selbst wenn eine veraltete Liste noch im Cache liegt. - **Feature**: Das Versionsmenü prüft nun im Hintergrund direkt beim Öffnen nach neuen Versionen und aktualisiert die Liste automatisch, selbst wenn eine veraltete Liste noch im Cache liegt.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

28
dist/install.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -2021,7 +2021,7 @@ body {
<div class="brand"> <div class="brand">
<span class="material-icons-round logo-icon">restaurant_menu</span> <span class="material-icons-round logo-icon">restaurant_menu</span>
<div class="header-left"> <div class="header-left">
<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ü">v1.4.11</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ü">v1.4.15</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div> <div id="last-updated-subtitle" class="subtitle"></div>
</div> </div>
<div class="nav-group" style="margin-left: 1rem;"> <div class="nav-group" style="margin-left: 1rem;">
@@ -2163,7 +2163,7 @@ body {
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem;">
<strong>Aktuell:</strong> <span id="version-current">v1.4.11</span> <strong>Aktuell:</strong> <span id="version-current">v1.4.15</span>
</div> </div>
<div class="dev-toggle"> <div class="dev-toggle">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;"> <label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
@@ -2889,6 +2889,67 @@ body {
localStorage.setItem('kantine_flags', JSON.stringify([...userFlags])); localStorage.setItem('kantine_flags', JSON.stringify([...userFlags]));
} }
async function refreshFlaggedItems() {
if (userFlags.size === 0) return;
const token = authToken || GUEST_TOKEN;
const datesToFetch = new Set();
for (const flagId of userFlags) {
const [dateStr] = flagId.split('_');
datesToFetch.add(dateStr);
}
let updated = false;
for (const dateStr of datesToFetch) {
try {
const resp = await fetch(`${API_BASE}/venues/${VENUE_ID}/menu/${MENU_ID}/${dateStr}/`, {
headers: apiHeaders(token)
});
if (!resp.ok) continue;
const data = await resp.json();
const menuGroups = data.results || [];
let dayItems = [];
for (const group of menuGroups) {
if (group.items && Array.isArray(group.items)) {
dayItems = dayItems.concat(group.items);
}
}
// Update allWeeks in memory
for (let week of allWeeks) {
if (!week.days) continue;
let dayObj = week.days.find(d => d.date === dateStr);
if (dayObj) {
dayObj.items = dayItems.map(item => {
const isUnlimited = item.amount_tracking === false;
const hasStock = parseInt(item.available_amount) > 0;
return {
id: `${dateStr}_${item.id}`,
articleId: item.id,
name: item.name || 'Unknown',
description: item.description || '',
price: parseFloat(item.price) || 0,
available: isUnlimited || hasStock,
availableAmount: parseInt(item.available_amount) || 0,
amountTracking: item.amount_tracking !== false
};
});
updated = true;
}
}
} catch (e) {
console.error('Error refreshing flag date', dateStr, e);
}
}
if (updated) {
saveMenuCache();
updateLastUpdatedTime(new Date().toISOString());
updateAlarmBell();
renderVisibleWeeks();
}
}
function updateAlarmBell() { function updateAlarmBell() {
const bellBtn = document.getElementById('alarm-bell'); const bellBtn = document.getElementById('alarm-bell');
const bellIcon = document.getElementById('alarm-bell-icon'); const bellIcon = document.getElementById('alarm-bell-icon');
@@ -2896,10 +2957,14 @@ body {
if (userFlags.size === 0) { if (userFlags.size === 0) {
bellBtn.classList.add('hidden'); bellBtn.classList.add('hidden');
bellBtn.style.display = 'none';
bellIcon.style.color = 'var(--text-secondary)';
bellIcon.style.textShadow = 'none';
return; return;
} }
bellBtn.classList.remove('hidden'); bellBtn.classList.remove('hidden');
bellBtn.style.display = 'inline-flex';
// Check if any flagged item is available // Check if any flagged item is available
let anyAvailable = false; let anyAvailable = false;
@@ -2918,33 +2983,40 @@ body {
if (anyAvailable) break; if (anyAvailable) break;
} }
const lastUpdatedStr = localStorage.getItem('kantine_last_updated'); let lastUpdatedStr = localStorage.getItem('kantine_last_updated');
let timeStr = 'Unbekannt'; let timeStr = 'gerade eben'; // Fallback instead of Unbekannt
if (lastUpdatedStr) { if (!lastUpdatedStr) {
lastUpdatedStr = new Date().toISOString();
localStorage.setItem('kantine_last_updated', lastUpdatedStr);
}
const lastUpdated = new Date(lastUpdatedStr); const lastUpdated = new Date(lastUpdatedStr);
const diffMs = Date.now() - lastUpdated.getTime(); const diffMs = Date.now() - lastUpdated.getTime();
const diffMins = Math.floor(diffMs / 60000); const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 60) timeStr = `vor ${diffMins} Min.`; if (diffMins < 1) timeStr = 'gerade eben';
else if (diffMins < 60) timeStr = `vor ${diffMins} Min.`;
else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`; else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`;
}
bellBtn.title = `Zuletzt geprüft: ${timeStr}`; bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
if (anyAvailable) { if (anyAvailable) {
bellIcon.style.color = 'var(--success-color)'; bellIcon.style.color = '#10b981'; // green / success
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)'; bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
} else { } else {
bellIcon.style.color = 'var(--warning-color)'; bellIcon.style.color = '#f59e0b'; // yellow / warning
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)'; bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
} }
} }
function toggleFlag(date, articleId, name, cutoff) { function toggleFlag(date, articleId, name, cutoff) {
const id = `${date}_${articleId}`; const id = `${date}_${articleId}`;
let flagAdded = false;
if (userFlags.has(id)) { if (userFlags.has(id)) {
userFlags.delete(id); userFlags.delete(id);
showToast(`Flag entfernt für ${name}`, 'success'); showToast(`Flag entfernt für ${name}`, 'success');
} else { } else {
userFlags.add(id); userFlags.add(id);
flagAdded = true;
showToast(`Benachrichtigung aktiviert für ${name}`, 'success'); showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
if (Notification.permission === 'default') { if (Notification.permission === 'default') {
Notification.requestPermission(); Notification.requestPermission();
@@ -2953,17 +3025,36 @@ body {
saveFlags(); saveFlags();
updateAlarmBell(); updateAlarmBell();
renderVisibleWeeks(); renderVisibleWeeks();
if (flagAdded) {
refreshFlaggedItems();
}
} }
// FR-019: Auto-remove flags whose cutoff has passed // FR-019: Auto-remove flags whose cutoff has passed
function cleanupExpiredFlags() { function cleanupExpiredFlags() {
const now = new Date(); const now = new Date();
const todayStr = now.toISOString().split('T')[0]; // Format: YYYY-MM-DD
let changed = false; let changed = false;
for (const flagId of [...userFlags]) { for (const flagId of [...userFlags]) {
const [date] = flagId.split('_'); const [dateStr] = flagId.split('_'); // Format usually is YYYY-MM-DD
const cutoff = new Date(date);
// If the flag's date string is entirely in the past (before today)
// or if it's today but past the 10:00 cutoff time
let isExpired = false;
if (dateStr < todayStr) {
isExpired = true;
} else if (dateStr === todayStr) {
const cutoff = new Date(dateStr);
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00 cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00
if (now >= cutoff) { if (now >= cutoff) {
isExpired = true;
}
}
if (isExpired) {
userFlags.delete(flagId); userFlags.delete(flagId);
changed = true; changed = true;
} }
@@ -3893,7 +3984,7 @@ body {
// Periodic update check (runs on init + every hour) // Periodic update check (runs on init + every hour)
async function checkForUpdates() { async function checkForUpdates() {
const currentVersion = 'v1.4.11'; const currentVersion = 'v1.4.15';
const devMode = localStorage.getItem('kantine_dev_mode') === 'true'; const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
try { try {
@@ -3934,7 +4025,7 @@ body {
const modal = document.getElementById('version-modal'); const modal = document.getElementById('version-modal');
const container = document.getElementById('version-list-container'); const container = document.getElementById('version-list-container');
const devToggle = document.getElementById('dev-mode-toggle'); const devToggle = document.getElementById('dev-mode-toggle');
const currentVersion = 'v1.4.11'; const currentVersion = 'v1.4.15';
if (!modal) return; if (!modal) return;
modal.classList.remove('hidden'); modal.classList.remove('hidden');

View File

@@ -939,6 +939,67 @@
localStorage.setItem('kantine_flags', JSON.stringify([...userFlags])); localStorage.setItem('kantine_flags', JSON.stringify([...userFlags]));
} }
async function refreshFlaggedItems() {
if (userFlags.size === 0) return;
const token = authToken || GUEST_TOKEN;
const datesToFetch = new Set();
for (const flagId of userFlags) {
const [dateStr] = flagId.split('_');
datesToFetch.add(dateStr);
}
let updated = false;
for (const dateStr of datesToFetch) {
try {
const resp = await fetch(`${API_BASE}/venues/${VENUE_ID}/menu/${MENU_ID}/${dateStr}/`, {
headers: apiHeaders(token)
});
if (!resp.ok) continue;
const data = await resp.json();
const menuGroups = data.results || [];
let dayItems = [];
for (const group of menuGroups) {
if (group.items && Array.isArray(group.items)) {
dayItems = dayItems.concat(group.items);
}
}
// Update allWeeks in memory
for (let week of allWeeks) {
if (!week.days) continue;
let dayObj = week.days.find(d => d.date === dateStr);
if (dayObj) {
dayObj.items = dayItems.map(item => {
const isUnlimited = item.amount_tracking === false;
const hasStock = parseInt(item.available_amount) > 0;
return {
id: `${dateStr}_${item.id}`,
articleId: item.id,
name: item.name || 'Unknown',
description: item.description || '',
price: parseFloat(item.price) || 0,
available: isUnlimited || hasStock,
availableAmount: parseInt(item.available_amount) || 0,
amountTracking: item.amount_tracking !== false
};
});
updated = true;
}
}
} catch (e) {
console.error('Error refreshing flag date', dateStr, e);
}
}
if (updated) {
saveMenuCache();
updateLastUpdatedTime(new Date().toISOString());
updateAlarmBell();
renderVisibleWeeks();
}
}
function updateAlarmBell() { function updateAlarmBell() {
const bellBtn = document.getElementById('alarm-bell'); const bellBtn = document.getElementById('alarm-bell');
const bellIcon = document.getElementById('alarm-bell-icon'); const bellIcon = document.getElementById('alarm-bell-icon');
@@ -946,10 +1007,14 @@
if (userFlags.size === 0) { if (userFlags.size === 0) {
bellBtn.classList.add('hidden'); bellBtn.classList.add('hidden');
bellBtn.style.display = 'none';
bellIcon.style.color = 'var(--text-secondary)';
bellIcon.style.textShadow = 'none';
return; return;
} }
bellBtn.classList.remove('hidden'); bellBtn.classList.remove('hidden');
bellBtn.style.display = 'inline-flex';
// Check if any flagged item is available // Check if any flagged item is available
let anyAvailable = false; let anyAvailable = false;
@@ -968,33 +1033,40 @@
if (anyAvailable) break; if (anyAvailable) break;
} }
const lastUpdatedStr = localStorage.getItem('kantine_last_updated'); let lastUpdatedStr = localStorage.getItem('kantine_last_updated');
let timeStr = 'Unbekannt'; let timeStr = 'gerade eben'; // Fallback instead of Unbekannt
if (lastUpdatedStr) { if (!lastUpdatedStr) {
lastUpdatedStr = new Date().toISOString();
localStorage.setItem('kantine_last_updated', lastUpdatedStr);
}
const lastUpdated = new Date(lastUpdatedStr); const lastUpdated = new Date(lastUpdatedStr);
const diffMs = Date.now() - lastUpdated.getTime(); const diffMs = Date.now() - lastUpdated.getTime();
const diffMins = Math.floor(diffMs / 60000); const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 60) timeStr = `vor ${diffMins} Min.`; if (diffMins < 1) timeStr = 'gerade eben';
else if (diffMins < 60) timeStr = `vor ${diffMins} Min.`;
else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`; else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`;
}
bellBtn.title = `Zuletzt geprüft: ${timeStr}`; bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
if (anyAvailable) { if (anyAvailable) {
bellIcon.style.color = 'var(--success-color)'; bellIcon.style.color = '#10b981'; // green / success
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)'; bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
} else { } else {
bellIcon.style.color = 'var(--warning-color)'; bellIcon.style.color = '#f59e0b'; // yellow / warning
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)'; bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
} }
} }
function toggleFlag(date, articleId, name, cutoff) { function toggleFlag(date, articleId, name, cutoff) {
const id = `${date}_${articleId}`; const id = `${date}_${articleId}`;
let flagAdded = false;
if (userFlags.has(id)) { if (userFlags.has(id)) {
userFlags.delete(id); userFlags.delete(id);
showToast(`Flag entfernt für ${name}`, 'success'); showToast(`Flag entfernt für ${name}`, 'success');
} else { } else {
userFlags.add(id); userFlags.add(id);
flagAdded = true;
showToast(`Benachrichtigung aktiviert für ${name}`, 'success'); showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
if (Notification.permission === 'default') { if (Notification.permission === 'default') {
Notification.requestPermission(); Notification.requestPermission();
@@ -1003,17 +1075,36 @@
saveFlags(); saveFlags();
updateAlarmBell(); updateAlarmBell();
renderVisibleWeeks(); renderVisibleWeeks();
if (flagAdded) {
refreshFlaggedItems();
}
} }
// FR-019: Auto-remove flags whose cutoff has passed // FR-019: Auto-remove flags whose cutoff has passed
function cleanupExpiredFlags() { function cleanupExpiredFlags() {
const now = new Date(); const now = new Date();
const todayStr = now.toISOString().split('T')[0]; // Format: YYYY-MM-DD
let changed = false; let changed = false;
for (const flagId of [...userFlags]) { for (const flagId of [...userFlags]) {
const [date] = flagId.split('_'); const [dateStr] = flagId.split('_'); // Format usually is YYYY-MM-DD
const cutoff = new Date(date);
// If the flag's date string is entirely in the past (before today)
// or if it's today but past the 10:00 cutoff time
let isExpired = false;
if (dateStr < todayStr) {
isExpired = true;
} else if (dateStr === todayStr) {
const cutoff = new Date(dateStr);
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00 cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00
if (now >= cutoff) { if (now >= cutoff) {
isExpired = true;
}
}
if (isExpired) {
userFlags.delete(flagId); userFlags.delete(flagId);
changed = true; changed = true;
} }

49
tests/test_dom.js Executable file
View File

@@ -0,0 +1,49 @@
const fs = require('fs');
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
.hidden { display: none !important; }
.icon-btn { display: inline-flex; }
</style>
</head>
<body>
<button id="alarm-bell" class="icon-btn hidden">
<span id="alarm-bell-icon" style="color:var(--text-secondary);"></span>
</button>
</body>
</html>
`;
const jsCode = fs.readFileSync('kantine.js', 'utf8').replace('(function () {', '').replace(/}\)\(\);$/, '');
const dom = new JSDOM(html, { runScripts: "dangerously" });
global.window = dom.window;
global.document = window.document;
global.localStorage = { getItem: () => '[]', setItem: () => {} };
global.sessionStorage = { getItem: () => null };
global.showToast = () => {};
global.saveFlags = () => {};
global.renderVisibleWeeks = () => {};
// Mock missing browser features if needed
global.Notification = { permission: 'default', requestPermission: () => {} };
try {
dom.window.eval(jsCode);
console.log("Initial Bell Classes:", window.document.getElementById('alarm-bell').className);
// Add flag
dom.window.eval("userFlags.add('2026-02-24_123'); updateAlarmBell();");
console.log("After Add:", window.document.getElementById('alarm-bell').className);
// Remove flag
dom.window.eval("userFlags.delete('2026-02-24_123'); updateAlarmBell();");
console.log("After Remove:", window.document.getElementById('alarm-bell').className);
} catch (e) {
console.error(e);
}

View File

@@ -1 +1 @@
v1.4.11 v1.4.15