Compare commits
10 Commits
v1.4.11
...
7a82cb06db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a82cb06db | ||
|
|
d89b080da5 | ||
|
|
d80863a169 | ||
|
|
ae79c58d30 | ||
|
|
a0ef6e631e | ||
|
|
d895a5fb7c | ||
|
|
fd765a74c0 | ||
|
|
1f184fab8b | ||
|
|
b6c7c66027 | ||
|
|
33bb87d7f4 |
17
changelog.md
17
changelog.md
@@ -1,3 +1,20 @@
|
|||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
|||||||
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
32
dist/install.html
vendored
32
dist/install.html
vendored
File diff suppressed because one or more lines are too long
131
dist/kantine-standalone.html
vendored
131
dist/kantine-standalone.html
vendored
@@ -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.16</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.16</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;">
|
||||||
@@ -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">
|
<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
|
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
|
||||||
</a>
|
</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>
|
</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) => {
|
window.addEventListener('click', (e) => {
|
||||||
if (e.target === versionModal) versionModal.classList.add('hidden');
|
if (e.target === versionModal) versionModal.classList.add('hidden');
|
||||||
});
|
});
|
||||||
@@ -2889,6 +2903,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 +2971,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 +2997,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 +3039,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 +3998,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.16';
|
||||||
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
|
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -3934,7 +4039,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.16';
|
||||||
|
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
|
|||||||
123
kantine.js
123
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">
|
<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
|
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
|
||||||
</a>
|
</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>
|
</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) => {
|
window.addEventListener('click', (e) => {
|
||||||
if (e.target === versionModal) versionModal.classList.add('hidden');
|
if (e.target === versionModal) versionModal.classList.add('hidden');
|
||||||
});
|
});
|
||||||
@@ -939,6 +953,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 +1021,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 +1047,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 +1089,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
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.11
|
v1.4.16
|
||||||
|
|||||||
Reference in New Issue
Block a user