Compare commits
6 Commits
v1.4.13
...
7a82cb06db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a82cb06db | ||
|
|
d89b080da5 | ||
|
|
d80863a169 | ||
|
|
ae79c58d30 | ||
|
|
a0ef6e631e | ||
|
|
d895a5fb7c |
11
changelog.md
11
changelog.md
@@ -1,3 +1,14 @@
|
||||
## v1.4.16
|
||||
- ⚡ **Feature**: Ein Button "Lokalen Cache leeren" wurde zum Versionen-Menü hinzugefügt, um bei hartnäckigen lokalen Fehlern alle Caches und Sessions bereinigen zu können, ohne die Entwicklertools (F12) des Browsers bemühen zu müssen.
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
2
dist/bookmarklet-payload.js
vendored
2
dist/bookmarklet-payload.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bookmarklet.txt
vendored
2
dist/bookmarklet.txt
vendored
File diff suppressed because one or more lines are too long
26
dist/install.html
vendored
26
dist/install.html
vendored
File diff suppressed because one or more lines are too long
125
dist/kantine-standalone.html
vendored
125
dist/kantine-standalone.html
vendored
@@ -2021,7 +2021,7 @@ body {
|
||||
<div class="brand">
|
||||
<span class="material-icons-round logo-icon">restaurant_menu</span>
|
||||
<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.13</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.16</small></h1>
|
||||
<div id="last-updated-subtitle" class="subtitle"></div>
|
||||
</div>
|
||||
<div class="nav-group" style="margin-left: 1rem;">
|
||||
@@ -2163,7 +2163,7 @@ body {
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<strong>Aktuell:</strong> <span id="version-current">v1.4.13</span>
|
||||
<strong>Aktuell:</strong> <span id="version-current">v1.4.16</span>
|
||||
</div>
|
||||
<div class="dev-toggle">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
|
||||
@@ -2181,6 +2181,9 @@ body {
|
||||
<a href="https://github.com/TauNeutrino/kantine-overview/discussions/categories/ideas" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Schlage ein neues Feature auf GitHub vor">
|
||||
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
|
||||
</a>
|
||||
<button id="btn-clear-cache" style="background: none; border: none; padding: 0; color: var(--error-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; text-align: left; font-size: inherit; font-family: inherit;" title="Löscht alle lokalen Daten & erzwingt einen Neuladen">
|
||||
<span class="material-icons-round" style="font-size: 1.2em;">delete_forever</span> Lokalen Cache leeren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2265,6 +2268,17 @@ body {
|
||||
});
|
||||
}
|
||||
|
||||
const btnClearCache = document.getElementById('btn-clear-cache');
|
||||
if (btnClearCache) {
|
||||
btnClearCache.addEventListener('click', () => {
|
||||
if (confirm('Möchtest du wirklich alle lokalen Daten (inkl. Login-Session, Cache und Einstellungen) löschen? Die Seite wird danach neu geladen.')) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target === versionModal) versionModal.classList.add('hidden');
|
||||
});
|
||||
@@ -2889,6 +2903,67 @@ body {
|
||||
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() {
|
||||
const bellBtn = document.getElementById('alarm-bell');
|
||||
const bellIcon = document.getElementById('alarm-bell-icon');
|
||||
@@ -2896,12 +2971,14 @@ body {
|
||||
|
||||
if (userFlags.size === 0) {
|
||||
bellBtn.classList.add('hidden');
|
||||
bellBtn.style.display = 'none';
|
||||
bellIcon.style.color = 'var(--text-secondary)';
|
||||
bellIcon.style.textShadow = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
bellBtn.classList.remove('hidden');
|
||||
bellBtn.style.display = 'inline-flex';
|
||||
|
||||
// Check if any flagged item is available
|
||||
let anyAvailable = false;
|
||||
@@ -2920,15 +2997,20 @@ body {
|
||||
if (anyAvailable) break;
|
||||
}
|
||||
|
||||
const lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
||||
let timeStr = 'Unbekannt';
|
||||
if (lastUpdatedStr) {
|
||||
let lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
||||
let timeStr = 'gerade eben'; // Fallback instead of Unbekannt
|
||||
if (!lastUpdatedStr) {
|
||||
lastUpdatedStr = new Date().toISOString();
|
||||
localStorage.setItem('kantine_last_updated', lastUpdatedStr);
|
||||
}
|
||||
|
||||
const lastUpdated = new Date(lastUpdatedStr);
|
||||
const diffMs = Date.now() - lastUpdated.getTime();
|
||||
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.`;
|
||||
}
|
||||
|
||||
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
|
||||
|
||||
if (anyAvailable) {
|
||||
@@ -2942,11 +3024,13 @@ body {
|
||||
|
||||
function toggleFlag(date, articleId, name, cutoff) {
|
||||
const id = `${date}_${articleId}`;
|
||||
let flagAdded = false;
|
||||
if (userFlags.has(id)) {
|
||||
userFlags.delete(id);
|
||||
showToast(`Flag entfernt für ${name}`, 'success');
|
||||
} else {
|
||||
userFlags.add(id);
|
||||
flagAdded = true;
|
||||
showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
|
||||
if (Notification.permission === 'default') {
|
||||
Notification.requestPermission();
|
||||
@@ -2955,17 +3039,36 @@ body {
|
||||
saveFlags();
|
||||
updateAlarmBell();
|
||||
renderVisibleWeeks();
|
||||
|
||||
if (flagAdded) {
|
||||
refreshFlaggedItems();
|
||||
}
|
||||
}
|
||||
|
||||
// FR-019: Auto-remove flags whose cutoff has passed
|
||||
function cleanupExpiredFlags() {
|
||||
const now = new Date();
|
||||
const todayStr = now.toISOString().split('T')[0]; // Format: YYYY-MM-DD
|
||||
let changed = false;
|
||||
|
||||
for (const flagId of [...userFlags]) {
|
||||
const [date] = flagId.split('_');
|
||||
const cutoff = new Date(date);
|
||||
const [dateStr] = flagId.split('_'); // Format usually is YYYY-MM-DD
|
||||
|
||||
// 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
|
||||
if (now >= cutoff) {
|
||||
isExpired = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isExpired) {
|
||||
userFlags.delete(flagId);
|
||||
changed = true;
|
||||
}
|
||||
@@ -3895,7 +3998,7 @@ body {
|
||||
|
||||
// Periodic update check (runs on init + every hour)
|
||||
async function checkForUpdates() {
|
||||
const currentVersion = 'v1.4.13';
|
||||
const currentVersion = 'v1.4.16';
|
||||
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
|
||||
|
||||
try {
|
||||
@@ -3936,7 +4039,7 @@ body {
|
||||
const modal = document.getElementById('version-modal');
|
||||
const container = document.getElementById('version-list-container');
|
||||
const devToggle = document.getElementById('dev-mode-toggle');
|
||||
const currentVersion = 'v1.4.13';
|
||||
const currentVersion = 'v1.4.16';
|
||||
|
||||
if (!modal) return;
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
117
kantine.js
117
kantine.js
@@ -231,6 +231,9 @@
|
||||
<a href="https://github.com/TauNeutrino/kantine-overview/discussions/categories/ideas" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Schlage ein neues Feature auf GitHub vor">
|
||||
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
|
||||
</a>
|
||||
<button id="btn-clear-cache" style="background: none; border: none; padding: 0; color: var(--error-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; text-align: left; font-size: inherit; font-family: inherit;" title="Löscht alle lokalen Daten & erzwingt einen Neuladen">
|
||||
<span class="material-icons-round" style="font-size: 1.2em;">delete_forever</span> Lokalen Cache leeren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -315,6 +318,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
const btnClearCache = document.getElementById('btn-clear-cache');
|
||||
if (btnClearCache) {
|
||||
btnClearCache.addEventListener('click', () => {
|
||||
if (confirm('Möchtest du wirklich alle lokalen Daten (inkl. Login-Session, Cache und Einstellungen) löschen? Die Seite wird danach neu geladen.')) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target === versionModal) versionModal.classList.add('hidden');
|
||||
});
|
||||
@@ -939,6 +953,67 @@
|
||||
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() {
|
||||
const bellBtn = document.getElementById('alarm-bell');
|
||||
const bellIcon = document.getElementById('alarm-bell-icon');
|
||||
@@ -946,12 +1021,14 @@
|
||||
|
||||
if (userFlags.size === 0) {
|
||||
bellBtn.classList.add('hidden');
|
||||
bellBtn.style.display = 'none';
|
||||
bellIcon.style.color = 'var(--text-secondary)';
|
||||
bellIcon.style.textShadow = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
bellBtn.classList.remove('hidden');
|
||||
bellBtn.style.display = 'inline-flex';
|
||||
|
||||
// Check if any flagged item is available
|
||||
let anyAvailable = false;
|
||||
@@ -970,15 +1047,20 @@
|
||||
if (anyAvailable) break;
|
||||
}
|
||||
|
||||
const lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
||||
let timeStr = 'Unbekannt';
|
||||
if (lastUpdatedStr) {
|
||||
let lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
||||
let timeStr = 'gerade eben'; // Fallback instead of Unbekannt
|
||||
if (!lastUpdatedStr) {
|
||||
lastUpdatedStr = new Date().toISOString();
|
||||
localStorage.setItem('kantine_last_updated', lastUpdatedStr);
|
||||
}
|
||||
|
||||
const lastUpdated = new Date(lastUpdatedStr);
|
||||
const diffMs = Date.now() - lastUpdated.getTime();
|
||||
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.`;
|
||||
}
|
||||
|
||||
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
|
||||
|
||||
if (anyAvailable) {
|
||||
@@ -992,11 +1074,13 @@
|
||||
|
||||
function toggleFlag(date, articleId, name, cutoff) {
|
||||
const id = `${date}_${articleId}`;
|
||||
let flagAdded = false;
|
||||
if (userFlags.has(id)) {
|
||||
userFlags.delete(id);
|
||||
showToast(`Flag entfernt für ${name}`, 'success');
|
||||
} else {
|
||||
userFlags.add(id);
|
||||
flagAdded = true;
|
||||
showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
|
||||
if (Notification.permission === 'default') {
|
||||
Notification.requestPermission();
|
||||
@@ -1005,17 +1089,36 @@
|
||||
saveFlags();
|
||||
updateAlarmBell();
|
||||
renderVisibleWeeks();
|
||||
|
||||
if (flagAdded) {
|
||||
refreshFlaggedItems();
|
||||
}
|
||||
}
|
||||
|
||||
// FR-019: Auto-remove flags whose cutoff has passed
|
||||
function cleanupExpiredFlags() {
|
||||
const now = new Date();
|
||||
const todayStr = now.toISOString().split('T')[0]; // Format: YYYY-MM-DD
|
||||
let changed = false;
|
||||
|
||||
for (const flagId of [...userFlags]) {
|
||||
const [date] = flagId.split('_');
|
||||
const cutoff = new Date(date);
|
||||
const [dateStr] = flagId.split('_'); // Format usually is YYYY-MM-DD
|
||||
|
||||
// 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
|
||||
if (now >= cutoff) {
|
||||
isExpired = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isExpired) {
|
||||
userFlags.delete(flagId);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
49
tests/test_dom.js
Executable file
49
tests/test_dom.js
Executable 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);
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
v1.4.13
|
||||
v1.4.16
|
||||
|
||||
Reference in New Issue
Block a user