Compare commits

...

3 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
9 changed files with 231 additions and 98 deletions

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,10 @@
## 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.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

23
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">
<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.6</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;">
@@ -2163,7 +2163,7 @@ body {
</div>
<div class="modal-body">
<div style="margin-bottom: 1rem;">
<strong>Aktuell:</strong> <span id="version-current">v1.4.6</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;">
@@ -2520,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;
}
// Check local storage cache
const localCache = localStorage.getItem('kantine_history_cache');
if (localCache) {
localCache = fullOrderHistoryCache;
} else {
const ls = localStorage.getItem('kantine_history_cache');
if (ls) {
try {
fullOrderHistoryCache = JSON.parse(localCache);
renderHistory(fullOrderHistoryCache);
return;
localCache = JSON.parse(ls);
fullOrderHistoryCache = localCache;
} 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;
// Start background delta sync
if (localCache.length === 0) {
historyContent.innerHTML = '';
historyLoading.classList.remove('hidden');
progressFill.style.width = '0%';
progressText.textContent = 'Lade Bestellhistorie...';
}
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}`);
@@ -2560,29 +2572,75 @@ body {
totalCount = data.count;
}
allOrders = allOrders.concat(data.results || []);
const results = data.results || [];
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);
}
// Update progress
if (!deltaComplete && requiresFullFetch) {
if (totalCount > 0) {
const pct = Math.round((allOrders.length / totalCount) * 100);
const pct = Math.round((fetchedOrders.length / totalCount) * 100);
progressFill.style.width = `${pct}%`;
progressText.textContent = `Lade Bestellung ${allOrders.length} von ${totalCount}...`;
progressText.textContent = `Lade Bestellung ${fetchedOrders.length} von ${totalCount}...`;
} else {
progressText.textContent = `Lade Bestellung ${allOrders.length}...`;
progressText.textContent = `Lade Bestellung ${fetchedOrders.length}...`;
}
} else if (!deltaComplete) {
progressText.textContent = `${fetchedOrders.length} neue/geänderte Bestellungen gefunden...`;
}
nextUrl = data.next;
nextUrl = deltaComplete ? null : data.next;
}
fullOrderHistoryCache = allOrders;
// 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(allOrders));
} catch (e) { console.warn('History cache write error', e); }
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);
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');
}
@@ -2784,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();
@@ -2814,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();
@@ -2872,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';
}
}
@@ -3834,7 +3892,7 @@ body {
// Periodic update check (runs on init + every hour)
async function checkForUpdates() {
const currentVersion = 'v1.4.6';
const currentVersion = 'v1.4.8';
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
try {
@@ -3875,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.6';
const currentVersion = 'v1.4.8';
if (!modal) return;
modal.classList.remove('hidden');

View File

@@ -570,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;
}
// Check local storage cache
const localCache = localStorage.getItem('kantine_history_cache');
if (localCache) {
localCache = fullOrderHistoryCache;
} else {
const ls = localStorage.getItem('kantine_history_cache');
if (ls) {
try {
fullOrderHistoryCache = JSON.parse(localCache);
renderHistory(fullOrderHistoryCache);
return;
localCache = JSON.parse(ls);
fullOrderHistoryCache = localCache;
} 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;
// Start background delta sync
if (localCache.length === 0) {
historyContent.innerHTML = '';
historyLoading.classList.remove('hidden');
progressFill.style.width = '0%';
progressText.textContent = 'Lade Bestellhistorie...';
}
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}`);
@@ -610,29 +622,75 @@
totalCount = data.count;
}
allOrders = allOrders.concat(data.results || []);
const results = data.results || [];
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);
}
// Update progress
if (!deltaComplete && requiresFullFetch) {
if (totalCount > 0) {
const pct = Math.round((allOrders.length / totalCount) * 100);
const pct = Math.round((fetchedOrders.length / totalCount) * 100);
progressFill.style.width = `${pct}%`;
progressText.textContent = `Lade Bestellung ${allOrders.length} von ${totalCount}...`;
progressText.textContent = `Lade Bestellung ${fetchedOrders.length} von ${totalCount}...`;
} else {
progressText.textContent = `Lade Bestellung ${allOrders.length}...`;
progressText.textContent = `Lade Bestellung ${fetchedOrders.length}...`;
}
} else if (!deltaComplete) {
progressText.textContent = `${fetchedOrders.length} neue/geänderte Bestellungen gefunden...`;
}
nextUrl = data.next;
nextUrl = deltaComplete ? null : data.next;
}
fullOrderHistoryCache = allOrders;
// 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(allOrders));
} catch (e) { console.warn('History cache write error', e); }
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);
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');
}
@@ -834,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();
@@ -864,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();
@@ -922,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';
}
}

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.6
v1.4.8