feat: immediate api refresh on flag, fix timestamp fallback (v1.4.14)
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
## 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
18
dist/install.html
vendored
18
dist/install.html
vendored
File diff suppressed because one or more lines are too long
98
dist/kantine-standalone.html
vendored
98
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.14</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.14</span>
|
||||
</div>
|
||||
<div class="dev-toggle">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
|
||||
@@ -2889,6 +2889,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 +2957,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 +2983,20 @@ body {
|
||||
if (anyAvailable) break;
|
||||
}
|
||||
|
||||
const lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
||||
let timeStr = 'Unbekannt';
|
||||
if (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.`;
|
||||
else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`;
|
||||
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 < 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 +3010,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,6 +3025,10 @@ body {
|
||||
saveFlags();
|
||||
updateAlarmBell();
|
||||
renderVisibleWeeks();
|
||||
|
||||
if (flagAdded) {
|
||||
refreshFlaggedItems();
|
||||
}
|
||||
}
|
||||
|
||||
// FR-019: Auto-remove flags whose cutoff has passed
|
||||
@@ -3895,7 +3969,7 @@ body {
|
||||
|
||||
// Periodic update check (runs on init + every hour)
|
||||
async function checkForUpdates() {
|
||||
const currentVersion = 'v1.4.13';
|
||||
const currentVersion = 'v1.4.14';
|
||||
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
|
||||
|
||||
try {
|
||||
@@ -3936,7 +4010,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.14';
|
||||
|
||||
if (!modal) return;
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
90
kantine.js
90
kantine.js
@@ -939,6 +939,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 +1007,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 +1033,20 @@
|
||||
if (anyAvailable) break;
|
||||
}
|
||||
|
||||
const lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
||||
let timeStr = 'Unbekannt';
|
||||
if (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.`;
|
||||
else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`;
|
||||
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 < 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 +1060,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,6 +1075,10 @@
|
||||
saveFlags();
|
||||
updateAlarmBell();
|
||||
renderVisibleWeeks();
|
||||
|
||||
if (flagAdded) {
|
||||
refreshFlaggedItems();
|
||||
}
|
||||
}
|
||||
|
||||
// FR-019: Auto-remove flags whose cutoff has passed
|
||||
|
||||
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.14
|
||||
|
||||
Reference in New Issue
Block a user