Compare commits

..

7 Commits

Author SHA1 Message Date
Kantine Wrapper
c841954c5d dist files for v1.4.8 built 2026-02-24 12:44:07 +01:00
Kantine Wrapper
320c4066f3 dist files for v1.4.7 built 2026-02-24 12:43:35 +01:00
Kantine Wrapper
cda74e65db dist files for v1.4.7 built 2026-02-24 12:32:44 +01:00
Kantine Wrapper
d1a19b043d dist files for v1.4.6 built 2026-02-24 11:11:15 +01:00
Kantine Wrapper
8c4de96432 dist files for v1.4.5 built 2026-02-24 10:52:53 +01:00
Kantine Wrapper
ce7d8a3de5 dist files for v1.4.4 built 2026-02-24 10:46:06 +01:00
Kantine Wrapper
0309f488bd dist files for v1.4.4 built 2026-02-24 10:46:00 +01:00
12 changed files with 345 additions and 107 deletions

View File

@@ -60,6 +60,7 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
| **Header UI & Navigation** | | | |
| FR-090 | Die Hauptnavigation (Wochen-Toggles) muss linksbündig neben dem App-Titel positioniert sein. | Niedrig | v1.5.0 |
| FR-091 | Ein dynamisches Alarm-Icon im Header muss den Überwachungsstatus geflaggter Menüs anzeigen (Gelb=Überwachung aktiv, Grün=Menü verfügbar, Versteckt=keine Flags). Der Tooltip muss den Zeitpunkt der letzten Prüfung als relativen String (z.B. "vor 4 Min.") enthalten. | Mittel | v1.5.0 |
| FR-092 | Sobald über den Daten-Refresh erstmals Menüdaten für die Nächste Woche geladen werden, muss der entsprechende Navigation-Button animiert und farblich (Gelb) hervorgehoben werden. Zusätzlich muss einmalig ein Hinweis eingeblendet werden. Bei Klick auf den Button muss die Hervorhebung erlöschen. | Mittel | v1.6.0 |
| **Benutzer-Feedback** | | | |
| FR-090 | Alle benutzerrelevanten Aktionen (Bestellung, Stornierung, Fehler) müssen durch nicht-blockierende Benachrichtigungen (Toasts) bestätigt werden. | Mittel | v1.0.1 |
| FR-091 | Bei einem Verbindungsfehler muss ein Fehlerdialog mit Fallback-Link zur Originalseite angezeigt werden. | Mittel | v1.0.1 |
@@ -71,6 +72,7 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
| FR-112 | Benutzer müssen eine Versionsliste mit Installationslinks einsehen können (Versionsmenü). | Niedrig | v1.3.0 |
| FR-113 | Es muss möglich sein, zu einer älteren Version zurückzukehren (Downgrade). | Niedrig | v1.3.0 |
| FR-114 | Ein Dev-Mode muss es ermöglichen, zwischen stabilen Releases und Entwicklungs-Tags umzuschalten. | Niedrig | v1.3.0 |
| FR-115 | Das Versionsmenü muss Links zur Erstellung von Feature-Requests und Bug-Reports auf GitHub enthalten. | Niedrig | v1.4.4 |
## 3. Nicht-funktionale Anforderungen

Binary file not shown.

View File

@@ -171,7 +171,7 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
</div>
<div style="text-align: center; margin-top: 40px; color: #5c6b7f; font-size: 0.8rem;">
<p>Powered by <strong>Kaufi-Kitchen</strong> 👨‍🍳</p>
<p>Powered by <strong>Kaufis-Kitchen</strong> 👨‍🍳</p>
</div>

View File

@@ -1,3 +1,19 @@
## v1.4.8 (2026-02-24)
- **Fix**: Die Benachrichtigungs-Glocke wird nun korrekt in Gelb dargestellt, wenn beobachtete Menüs verfügbar sind.
- **Tools**: Fehler in Testskript behoben, der den CI/CD Build verlangsamt hat.
## v1.4.7 (2026-02-24)
- **Performance**: Die Bestellhistorie nutzt nun einen inkrementellen Delta-Cache anstatt immer alle Seiten von der API herunterzuladen, was die Ladezeiten für Vielbesteller enorm reduziert.
## v1.4.6 (2026-02-24)
- **Fix**: Die Umrandung für bereits bestellte Menüs der vergangenen Tage ist nun ebenfalls einheitlich violett statt blau.
## v1.4.5 (2026-02-24)
- **Fix**: Doppelten Scrollbalken in der Versionen-Liste entfernt.
## v1.4.4 (2026-02-24)
- **Feature**: Das Versionsmenü enthält nun direkte Links zu GitHub, um Fehler zu melden oder neue Features vorzuschlagen.
## v1.4.3 (2026-02-24)
- **Fix**: Der Rahmen des "Heute Bestellt" Menüs ist nun konsequent violett (passend zum Glow-Effekt).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

35
dist/install.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -197,6 +197,32 @@ body {
color: white;
}
/* Notification state for Next Week */
.nav-btn.new-week-available {
animation: goldPulse 2s infinite;
border-color: #f59e0b;
color: var(--accent-color);
}
.nav-btn.new-week-available.active {
color: white;
}
@keyframes goldPulse {
0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(245, 158, 11, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
}
}
/* Badge for nav buttons (day count indicator) */
.nav-badge {
background-color: var(--error-color);
@@ -786,7 +812,7 @@ body {
/* No opacity/filter here - fully visible */
background: var(--bg-card);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid var(--accent-color);
border: 1px solid #8b5cf6;
border-radius: 8px;
padding: 1rem;
margin: 0 -1rem 1.5rem -1rem;
@@ -1606,8 +1632,6 @@ body {
.version-list {
list-style: none;
padding: 0;
max-height: 350px;
overflow-y: auto;
margin: 0;
}
@@ -1997,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.3</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.8</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div>
</div>
<div class="nav-group" style="margin-left: 1rem;">
@@ -2139,7 +2163,7 @@ body {
</div>
<div class="modal-body">
<div style="margin-bottom: 1rem;">
<strong>Aktuell:</strong> <span id="version-current">v1.4.3</span>
<strong>Aktuell:</strong> <span id="version-current">v1.4.8</span>
</div>
<div class="dev-toggle">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
@@ -2147,9 +2171,17 @@ body {
<span>Dev-Mode (alle Tags anzeigen)</span>
</label>
</div>
<div id="version-list-container" style="margin-top:1rem;">
<div id="version-list-container" style="margin-top:1rem; max-height: 250px; overflow-y: auto;">
<p style="color:var(--text-secondary);">Lade Versionen...</p>
</div>
<div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border-color); display: flex; flex-direction: column; gap: 0.75rem; font-size: 0.9em;">
<a href="https://github.com/TauNeutrino/kantine-overview/issues" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Melde einen Fehler auf GitHub">
<span class="material-icons-round" style="font-size: 1.2em;">bug_report</span> Fehler melden
</a>
<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>
</div>
</div>
</div>
</div>
@@ -2283,6 +2315,7 @@ body {
});
btnNextWeek.addEventListener('click', () => {
btnNextWeek.classList.remove('new-week-available');
if (displayMode !== 'next-week') {
displayMode = 'next-week';
btnNextWeek.classList.add('active');
@@ -2487,37 +2520,49 @@ body {
const progressFill = document.getElementById('history-progress-fill');
const progressText = document.getElementById('history-progress-text');
// Check memory cache first
// Check local storage cache (we still use memory cache if available)
let localCache = [];
if (fullOrderHistoryCache) {
renderHistory(fullOrderHistoryCache);
return;
localCache = fullOrderHistoryCache;
} else {
const ls = localStorage.getItem('kantine_history_cache');
if (ls) {
try {
localCache = JSON.parse(ls);
fullOrderHistoryCache = localCache;
} catch (e) {
console.warn('History cache parse error', e);
}
}
}
// Check local storage cache
const localCache = localStorage.getItem('kantine_history_cache');
if (localCache) {
try {
fullOrderHistoryCache = JSON.parse(localCache);
renderHistory(fullOrderHistoryCache);
return;
} catch (e) {
console.warn('History cache parse error', e);
}
// Show cached version immediately if we have one
if (localCache.length > 0) {
renderHistory(localCache);
}
if (!authToken) return;
historyContent.innerHTML = '';
historyLoading.classList.remove('hidden');
progressFill.style.width = '0%';
progressText.textContent = 'Lade Bestellhistorie...';
// Start background delta sync
if (localCache.length === 0) {
historyContent.innerHTML = '';
historyLoading.classList.remove('hidden');
}
let nextUrl = `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=50`;
let allOrders = [];
progressFill.style.width = '0%';
progressText.textContent = localCache.length > 0 ? 'Suche nach neuen Bestellungen...' : 'Lade Bestellhistorie...';
if (localCache.length > 0) historyLoading.classList.remove('hidden');
let nextUrl = localCache.length > 0
? `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=5`
: `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=50`;
let fetchedOrders = [];
let totalCount = 0;
let requiresFullFetch = localCache.length === 0;
let deltaComplete = false;
try {
while (nextUrl) {
while (nextUrl && !deltaComplete) {
const response = await fetch(nextUrl, { headers: apiHeaders(authToken) });
if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
@@ -2527,29 +2572,75 @@ body {
totalCount = data.count;
}
allOrders = allOrders.concat(data.results || []);
const results = data.results || [];
// Update progress
if (totalCount > 0) {
const pct = Math.round((allOrders.length / totalCount) * 100);
progressFill.style.width = `${pct}%`;
progressText.textContent = `Lade Bestellung ${allOrders.length} von ${totalCount}...`;
} else {
progressText.textContent = `Lade Bestellung ${allOrders.length}...`;
for (const order of results) {
// Check if we hit an order that is already in our cache AND has the exact same state/update time
// Bessa returns 'updated' timestamp, we can use it to determine if anything changed
const existingOrderIndex = localCache.findIndex(cached => cached.id === order.id);
if (!requiresFullFetch && existingOrderIndex !== -1) {
const existingOrder = localCache[existingOrderIndex];
// If order exists and wasn't updated since our cache, we've reached the point
// where everything older is already correctly cached.
// order.updated is an ISO string like "2025-02-18T10:30:15.123456Z"
if (existingOrder.updated === order.updated && existingOrder.order_state === order.order_state) {
deltaComplete = true;
break;
}
}
fetchedOrders.push(order);
}
nextUrl = data.next;
// Update progress
if (!deltaComplete && requiresFullFetch) {
if (totalCount > 0) {
const pct = Math.round((fetchedOrders.length / totalCount) * 100);
progressFill.style.width = `${pct}%`;
progressText.textContent = `Lade Bestellung ${fetchedOrders.length} von ${totalCount}...`;
} else {
progressText.textContent = `Lade Bestellung ${fetchedOrders.length}...`;
}
} else if (!deltaComplete) {
progressText.textContent = `${fetchedOrders.length} neue/geänderte Bestellungen gefunden...`;
}
nextUrl = deltaComplete ? null : data.next;
}
fullOrderHistoryCache = allOrders;
try {
localStorage.setItem('kantine_history_cache', JSON.stringify(allOrders));
} catch (e) { console.warn('History cache write error', e); }
renderHistory(fullOrderHistoryCache);
// Merge fetched orders with cache
if (fetchedOrders.length > 0) {
// We have new/updated orders. We need to merge them into the cache.
// 1. Create a map of the existing cache for quick ID lookup
const cacheMap = new Map(localCache.map(o => [o.id, o]));
// 2. Update/Insert the newly fetched orders
for (const order of fetchedOrders) {
cacheMap.set(order.id, order); // Overwrites existing, or adds new
}
// 3. Convert back to array and sort by created date (descending)
const mergedOrders = Array.from(cacheMap.values());
mergedOrders.sort((a, b) => new Date(b.created) - new Date(a.created));
fullOrderHistoryCache = mergedOrders;
try {
localStorage.setItem('kantine_history_cache', JSON.stringify(mergedOrders));
} catch (e) {
console.warn('History cache write error', e);
}
// Render the updated history
renderHistory(fullOrderHistoryCache);
}
} catch (error) {
console.error('Error fetching full history:', error);
historyContent.innerHTML = `<p style="color:var(--error-color);text-align:center;">Fehler beim Laden der Historie.</p>`;
console.error('Error in history sync:', error);
if (localCache.length === 0) {
historyContent.innerHTML = `<p style="color:var(--error-color);text-align:center;">Fehler beim Laden der Historie.</p>`;
} else {
showToast('Hintergrund-Synchronisation fehlgeschlagen', 'error');
}
} finally {
historyLoading.classList.add('hidden');
}
@@ -2751,7 +2842,7 @@ body {
if (response.ok || response.status === 201) {
showToast(`Bestellt: ${name}`, 'success');
localStorage.removeItem('kantine_history_cache');
fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
await fetchOrders();
} else {
const data = await response.json();
@@ -2781,7 +2872,7 @@ body {
if (response.ok) {
showToast(`Storniert: ${name}`, 'success');
localStorage.removeItem('kantine_history_cache');
fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
await fetchOrders();
} else {
const data = await response.json();
@@ -2839,11 +2930,11 @@ body {
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
if (anyAvailable) {
bellIcon.style.color = 'var(--success-color)';
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
} else {
bellIcon.style.color = 'var(--warning-color)';
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
} else {
bellIcon.style.color = 'var(--text-secondary)';
bellIcon.style.textShadow = 'none';
}
}
@@ -3387,6 +3478,14 @@ body {
badge.classList.add('has-highlights');
}
// FR-092: Highlight Next Week Button when new data arrives
const storageKey = `kantine_notified_nextweek_${nextYear}_${nextWeek}`;
if (!localStorage.getItem(storageKey)) {
localStorage.setItem(storageKey, 'true');
btnNextWeek.classList.add('new-week-available');
showToast('Neue Menüdaten für nächste Woche verfügbar!', 'info');
}
} else if (badge) {
badge.remove();
}
@@ -3793,7 +3892,7 @@ body {
// Periodic update check (runs on init + every hour)
async function checkForUpdates() {
const currentVersion = 'v1.4.3';
const currentVersion = 'v1.4.8';
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
try {
@@ -3834,7 +3933,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.3';
const currentVersion = 'v1.4.8';
if (!modal) return;
modal.classList.remove('hidden');

View File

@@ -221,9 +221,17 @@
<span>Dev-Mode (alle Tags anzeigen)</span>
</label>
</div>
<div id="version-list-container" style="margin-top:1rem;">
<div id="version-list-container" style="margin-top:1rem; max-height: 250px; overflow-y: auto;">
<p style="color:var(--text-secondary);">Lade Versionen...</p>
</div>
<div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border-color); display: flex; flex-direction: column; gap: 0.75rem; font-size: 0.9em;">
<a href="https://github.com/TauNeutrino/kantine-overview/issues" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Melde einen Fehler auf GitHub">
<span class="material-icons-round" style="font-size: 1.2em;">bug_report</span> Fehler melden
</a>
<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>
</div>
</div>
</div>
</div>
@@ -357,6 +365,7 @@
});
btnNextWeek.addEventListener('click', () => {
btnNextWeek.classList.remove('new-week-available');
if (displayMode !== 'next-week') {
displayMode = 'next-week';
btnNextWeek.classList.add('active');
@@ -561,37 +570,49 @@
const progressFill = document.getElementById('history-progress-fill');
const progressText = document.getElementById('history-progress-text');
// Check memory cache first
// Check local storage cache (we still use memory cache if available)
let localCache = [];
if (fullOrderHistoryCache) {
renderHistory(fullOrderHistoryCache);
return;
localCache = fullOrderHistoryCache;
} else {
const ls = localStorage.getItem('kantine_history_cache');
if (ls) {
try {
localCache = JSON.parse(ls);
fullOrderHistoryCache = localCache;
} catch (e) {
console.warn('History cache parse error', e);
}
}
}
// Check local storage cache
const localCache = localStorage.getItem('kantine_history_cache');
if (localCache) {
try {
fullOrderHistoryCache = JSON.parse(localCache);
renderHistory(fullOrderHistoryCache);
return;
} catch (e) {
console.warn('History cache parse error', e);
}
// Show cached version immediately if we have one
if (localCache.length > 0) {
renderHistory(localCache);
}
if (!authToken) return;
historyContent.innerHTML = '';
historyLoading.classList.remove('hidden');
progressFill.style.width = '0%';
progressText.textContent = 'Lade Bestellhistorie...';
// Start background delta sync
if (localCache.length === 0) {
historyContent.innerHTML = '';
historyLoading.classList.remove('hidden');
}
let nextUrl = `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=50`;
let allOrders = [];
progressFill.style.width = '0%';
progressText.textContent = localCache.length > 0 ? 'Suche nach neuen Bestellungen...' : 'Lade Bestellhistorie...';
if (localCache.length > 0) historyLoading.classList.remove('hidden');
let nextUrl = localCache.length > 0
? `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=5`
: `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=50`;
let fetchedOrders = [];
let totalCount = 0;
let requiresFullFetch = localCache.length === 0;
let deltaComplete = false;
try {
while (nextUrl) {
while (nextUrl && !deltaComplete) {
const response = await fetch(nextUrl, { headers: apiHeaders(authToken) });
if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
@@ -601,29 +622,75 @@
totalCount = data.count;
}
allOrders = allOrders.concat(data.results || []);
const results = data.results || [];
// Update progress
if (totalCount > 0) {
const pct = Math.round((allOrders.length / totalCount) * 100);
progressFill.style.width = `${pct}%`;
progressText.textContent = `Lade Bestellung ${allOrders.length} von ${totalCount}...`;
} else {
progressText.textContent = `Lade Bestellung ${allOrders.length}...`;
for (const order of results) {
// Check if we hit an order that is already in our cache AND has the exact same state/update time
// Bessa returns 'updated' timestamp, we can use it to determine if anything changed
const existingOrderIndex = localCache.findIndex(cached => cached.id === order.id);
if (!requiresFullFetch && existingOrderIndex !== -1) {
const existingOrder = localCache[existingOrderIndex];
// If order exists and wasn't updated since our cache, we've reached the point
// where everything older is already correctly cached.
// order.updated is an ISO string like "2025-02-18T10:30:15.123456Z"
if (existingOrder.updated === order.updated && existingOrder.order_state === order.order_state) {
deltaComplete = true;
break;
}
}
fetchedOrders.push(order);
}
nextUrl = data.next;
// Update progress
if (!deltaComplete && requiresFullFetch) {
if (totalCount > 0) {
const pct = Math.round((fetchedOrders.length / totalCount) * 100);
progressFill.style.width = `${pct}%`;
progressText.textContent = `Lade Bestellung ${fetchedOrders.length} von ${totalCount}...`;
} else {
progressText.textContent = `Lade Bestellung ${fetchedOrders.length}...`;
}
} else if (!deltaComplete) {
progressText.textContent = `${fetchedOrders.length} neue/geänderte Bestellungen gefunden...`;
}
nextUrl = deltaComplete ? null : data.next;
}
fullOrderHistoryCache = allOrders;
try {
localStorage.setItem('kantine_history_cache', JSON.stringify(allOrders));
} catch (e) { console.warn('History cache write error', e); }
renderHistory(fullOrderHistoryCache);
// Merge fetched orders with cache
if (fetchedOrders.length > 0) {
// We have new/updated orders. We need to merge them into the cache.
// 1. Create a map of the existing cache for quick ID lookup
const cacheMap = new Map(localCache.map(o => [o.id, o]));
// 2. Update/Insert the newly fetched orders
for (const order of fetchedOrders) {
cacheMap.set(order.id, order); // Overwrites existing, or adds new
}
// 3. Convert back to array and sort by created date (descending)
const mergedOrders = Array.from(cacheMap.values());
mergedOrders.sort((a, b) => new Date(b.created) - new Date(a.created));
fullOrderHistoryCache = mergedOrders;
try {
localStorage.setItem('kantine_history_cache', JSON.stringify(mergedOrders));
} catch (e) {
console.warn('History cache write error', e);
}
// Render the updated history
renderHistory(fullOrderHistoryCache);
}
} catch (error) {
console.error('Error fetching full history:', error);
historyContent.innerHTML = `<p style="color:var(--error-color);text-align:center;">Fehler beim Laden der Historie.</p>`;
console.error('Error in history sync:', error);
if (localCache.length === 0) {
historyContent.innerHTML = `<p style="color:var(--error-color);text-align:center;">Fehler beim Laden der Historie.</p>`;
} else {
showToast('Hintergrund-Synchronisation fehlgeschlagen', 'error');
}
} finally {
historyLoading.classList.add('hidden');
}
@@ -825,7 +892,7 @@
if (response.ok || response.status === 201) {
showToast(`Bestellt: ${name}`, 'success');
localStorage.removeItem('kantine_history_cache');
fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
await fetchOrders();
} else {
const data = await response.json();
@@ -855,7 +922,7 @@
if (response.ok) {
showToast(`Storniert: ${name}`, 'success');
localStorage.removeItem('kantine_history_cache');
fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
await fetchOrders();
} else {
const data = await response.json();
@@ -913,11 +980,11 @@
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
if (anyAvailable) {
bellIcon.style.color = 'var(--success-color)';
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
} else {
bellIcon.style.color = 'var(--warning-color)';
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
} else {
bellIcon.style.color = 'var(--text-secondary)';
bellIcon.style.textShadow = 'none';
}
}
@@ -1461,6 +1528,14 @@
badge.classList.add('has-highlights');
}
// FR-092: Highlight Next Week Button when new data arrives
const storageKey = `kantine_notified_nextweek_${nextYear}_${nextWeek}`;
if (!localStorage.getItem(storageKey)) {
localStorage.setItem(storageKey, 'true');
btnNextWeek.classList.add('new-week-available');
showToast('Neue Menüdaten für nächste Woche verfügbar!', 'info');
}
} else if (badge) {
badge.remove();
}

View File

@@ -186,6 +186,32 @@ body {
color: white;
}
/* Notification state for Next Week */
.nav-btn.new-week-available {
animation: goldPulse 2s infinite;
border-color: #f59e0b;
color: var(--accent-color);
}
.nav-btn.new-week-available.active {
color: white;
}
@keyframes goldPulse {
0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(245, 158, 11, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
}
}
/* Badge for nav buttons (day count indicator) */
.nav-badge {
background-color: var(--error-color);
@@ -775,7 +801,7 @@ body {
/* No opacity/filter here - fully visible */
background: var(--bg-card);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid var(--accent-color);
border: 1px solid #8b5cf6;
border-radius: 8px;
padding: 1rem;
margin: 0 -1rem 1.5rem -1rem;
@@ -1595,8 +1621,6 @@ body {
.version-list {
list-style: none;
padding: 0;
max-height: 350px;
overflow-y: auto;
margin: 0;
}

View File

@@ -108,7 +108,8 @@ try {
[/function\s+isNewer/, 'isNewer function'],
[/function\s+openVersionMenu/, 'openVersionMenu function'],
[/kantine_dev_mode/, 'dev-mode localStorage key'],
[/function\s+isCacheFresh/, 'isCacheFresh function']
[/function\s+isCacheFresh/, 'isCacheFresh function'],
[/limit=5/, 'Delta fetch limit parameter']
];
for (const [regex, label] of checks) {

View File

@@ -1 +1 @@
v1.4.3
v1.4.8