Compare commits

..

16 Commits

Author SHA1 Message Date
Kantine Wrapper
fd765a74c0 chore: update build artifacts for v1.4.13 2026-02-24 13:24:50 +01:00
Kantine Wrapper
1f184fab8b fix: replace css variables with direct hex colors for alarm bell 2026-02-24 13:24:09 +01:00
Kantine Wrapper
b6c7c66027 chore: update build artifacts for v1.4.12 2026-02-24 13:20:03 +01:00
Kantine Wrapper
33bb87d7f4 fix: enforce hidden state and default color of alarm bell 2026-02-24 13:19:38 +01:00
Kantine Wrapper
d4a8a47ccd chore: update build artifacts for v1.4.11 2026-02-24 13:11:09 +01:00
Kantine Wrapper
8e8c93410b feat: background refresh for version menu 2026-02-24 13:11:02 +01:00
Kantine Wrapper
cca59bcace chore: update build artifacts for v1.4.10 2026-02-24 13:05:53 +01:00
Kantine Wrapper
432bbcb6f2 build: separate release logic into release.sh 2026-02-24 13:05:46 +01:00
Kantine Wrapper
accdccf897 docs: sync REQUIREMENTS.md with latest features 2026-02-24 13:00:16 +01:00
Kantine Wrapper
7fc8c6f1e0 dist files for v1.4.10 built 2026-02-24 12:59:19 +01:00
Kantine Wrapper
0eb14a1869 dist files for v1.4.9 built 2026-02-24 12:56:53 +01:00
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
12 changed files with 414 additions and 151 deletions

View File

@@ -40,7 +40,7 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
| FR-033 | Es muss möglich sein, dasselbe Menü mehrfach zu bestellen. Bei Mehrfachbestellungen muss die Anzahl angezeigt werden. | Niedrig | v1.0.1 | | FR-033 | Es muss möglich sein, dasselbe Menü mehrfach zu bestellen. Bei Mehrfachbestellungen muss die Anzahl angezeigt werden. | Niedrig | v1.0.1 |
| **Kostentransparenz & Bestellhistorie** | | | | | **Kostentransparenz & Bestellhistorie** | | | |
| FR-040 | Das System muss die Gesamtkosten aller Bestellungen einer Woche automatisch berechnen und anzeigen. | Mittel | v1.1.0 | | FR-040 | Das System muss die Gesamtkosten aller Bestellungen einer Woche automatisch berechnen und anzeigen. | Mittel | v1.1.0 |
| FR-041 | Das System muss dem Benutzer eine paginierte oder vollständige Bestellhistorie (gruppiert nach Monat und KW) mit Fortschrittsanzeige auf Abruf in einem Modal bereitstellen. | Mittel | v1.4.0 | | FR-041 | Das System muss dem Benutzer eine Bestellhistorie (gruppiert nach Monat und KW) mit Fortschrittsanzeige auf Abruf in einem Modal bereitstellen. Die Historie muss über ein lokales Delta-Caching verfügen, um Ladezeiten zu minimieren. | Mittel | v1.4.0 (Update v1.4.7) |
| **Bestell-Countdown** | | | | | **Bestell-Countdown** | | | |
| FR-050 | Das System muss vor Bestellschluss einen visuell hervorgehobenen Countdown anzeigen. | Mittel | v1.1.0 | | FR-050 | Das System muss vor Bestellschluss einen visuell hervorgehobenen Countdown anzeigen. | Mittel | v1.1.0 |
| **Menü-Flagging & Benachrichtigungen** | | | | | **Menü-Flagging & Benachrichtigungen** | | | |
@@ -59,7 +59,7 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
| FR-082 | Das System muss beim erstmaligen Laden die Betriebssystem-Präferenz für das Farbschema berücksichtigen. | Niedrig | v1.0.1 | | FR-082 | Das System muss beim erstmaligen Laden die Betriebssystem-Präferenz für das Farbschema berücksichtigen. | Niedrig | v1.0.1 |
| **Header UI & Navigation** | | | | | **Header UI & Navigation** | | | |
| FR-090 | Die Hauptnavigation (Wochen-Toggles) muss linksbündig neben dem App-Titel positioniert sein. | Niedrig | v1.5.0 | | 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-091 | Ein dynamisches Alarm-Icon im Header muss den Überwachungsstatus geflaggter Menüs anzeigen (Gelb=Überwachung aktiv aber kein Menü verfügbar, Grün=Mindestens ein 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 (Update v1.4.10) |
| 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 | | 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** | | | | | **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-090 | Alle benutzerrelevanten Aktionen (Bestellung, Stornierung, Fehler) müssen durch nicht-blockierende Benachrichtigungen (Toasts) bestätigt werden. | Mittel | v1.0.1 |
@@ -93,4 +93,4 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
* **Datenhaltung**: Clientseitig via `localStorage` (Menü-Cache, Flags, Highlights, Theme) und `sessionStorage` (Auth-Token). * **Datenhaltung**: Clientseitig via `localStorage` (Menü-Cache, Flags, Highlights, Theme) und `sessionStorage` (Auth-Token).
* **Build**: Bash-basiertes Build-Script, das Bookmarklet-URL, Standalone-HTML und Installer-Seite generiert. * **Build**: Bash-basiertes Build-Script, das Bookmarklet-URL, Standalone-HTML und Installer-Seite generiert.
* **Versionierung**: SemVer, verwaltet über GitHub Releases/Tags. * **Versionierung**: SemVer, verwaltet über GitHub Releases/Tags.
* **Tests**: Python-basierte Build-Tests + Node.js-basierte Logik-Tests. * **Tests**: Python-basierte Build-Tests (`python3`) + Node.js-basierte Logik-Tests.

View File

@@ -171,7 +171,7 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
</div> </div>
<div style="text-align: center; margin-top: 40px; color: #5c6b7f; font-size: 0.8rem;"> <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> </div>
@@ -267,23 +267,4 @@ if [ $TEST_EXIT -ne 0 ]; then
fi fi
echo "✅ All build tests passed." echo "✅ All build tests passed."
# === 5. Commit, tag, and push ===
echo ""
echo "=== Committing & Pushing ==="
git add -A
git commit -m "dist files for $VERSION built" --allow-empty
echo ""
echo "=== Tagging $VERSION ==="
if git rev-parse "$VERSION" >/dev/null 2>&1; then
git tag -f "$VERSION"
echo "🔄 Tag $VERSION moved to current commit."
else
git tag "$VERSION"
echo "✅ Created tag: $VERSION"
fi
git push
git push origin --force tag "$VERSION"
git push github --force tag "$VERSION"
echo "✅ Pushed commit + tag $VERSION"

View File

@@ -1,3 +1,31 @@
## 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)
- **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.
## v1.4.10 (2026-02-24)
- **Fix**: Die Farben der Benachrichtigungs-Glocke wurden korrigiert: Sie ist nun gelb, während man auf ein Menü wartet, und wird grün, sobald eines verfügbar ist.
## v1.4.9 (2026-02-24)
- **Fix**: Das Glocken-Icon für Benachrichtigungen wird nun direkt beim Start (wenn Daten aus dem lokalen Cache geladen werden) korrekt angezeigt.
## 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) ## v1.4.4 (2026-02-24)
- **Feature**: Das Versionsmenü enthält nun direkte Links zu GitHub, um Fehler zu melden oder neue Features vorzuschlagen. - **Feature**: Das Versionsmenü enthält nun direkte Links zu GitHub, um Fehler zu melden oder neue Features vorzuschlagen.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

51
dist/install.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -203,6 +203,7 @@ body {
border-color: #f59e0b; border-color: #f59e0b;
color: var(--accent-color); color: var(--accent-color);
} }
.nav-btn.new-week-available.active { .nav-btn.new-week-available.active {
color: white; color: white;
} }
@@ -211,9 +212,11 @@ body {
0% { 0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7);
} }
70% { 70% {
box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); box-shadow: 0 0 0 10px rgba(245, 158, 11, 0);
} }
100% { 100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
} }
@@ -809,7 +812,7 @@ body {
/* No opacity/filter here - fully visible */ /* No opacity/filter here - fully visible */
background: var(--bg-card); background: var(--bg-card);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid var(--accent-color); border: 1px solid #8b5cf6;
border-radius: 8px; border-radius: 8px;
padding: 1rem; padding: 1rem;
margin: 0 -1rem 1.5rem -1rem; margin: 0 -1rem 1.5rem -1rem;
@@ -1629,8 +1632,6 @@ body {
.version-list { .version-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
max-height: 350px;
overflow-y: auto;
margin: 0; margin: 0;
} }
@@ -2020,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.4</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.13</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;">
@@ -2162,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.4</span> <strong>Aktuell:</strong> <span id="version-current">v1.4.13</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;">
@@ -2519,37 +2520,49 @@ body {
const progressFill = document.getElementById('history-progress-fill'); const progressFill = document.getElementById('history-progress-fill');
const progressText = document.getElementById('history-progress-text'); 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) { if (fullOrderHistoryCache) {
renderHistory(fullOrderHistoryCache); localCache = fullOrderHistoryCache;
return; } else {
} const ls = localStorage.getItem('kantine_history_cache');
if (ls) {
// Check local storage cache
const localCache = localStorage.getItem('kantine_history_cache');
if (localCache) {
try { try {
fullOrderHistoryCache = JSON.parse(localCache); localCache = JSON.parse(ls);
renderHistory(fullOrderHistoryCache); fullOrderHistoryCache = localCache;
return;
} catch (e) { } catch (e) {
console.warn('History cache parse error', 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; if (!authToken) return;
// Start background delta sync
if (localCache.length === 0) {
historyContent.innerHTML = ''; historyContent.innerHTML = '';
historyLoading.classList.remove('hidden'); 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`; progressFill.style.width = '0%';
let allOrders = []; 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 totalCount = 0;
let requiresFullFetch = localCache.length === 0;
let deltaComplete = false;
try { try {
while (nextUrl) { while (nextUrl && !deltaComplete) {
const response = await fetch(nextUrl, { headers: apiHeaders(authToken) }); const response = await fetch(nextUrl, { headers: apiHeaders(authToken) });
if (!response.ok) throw new Error(`Fetch failed: ${response.status}`); if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
@@ -2559,29 +2572,75 @@ body {
totalCount = data.count; 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 // Update progress
if (!deltaComplete && requiresFullFetch) {
if (totalCount > 0) { if (totalCount > 0) {
const pct = Math.round((allOrders.length / totalCount) * 100); const pct = Math.round((fetchedOrders.length / totalCount) * 100);
progressFill.style.width = `${pct}%`; progressFill.style.width = `${pct}%`;
progressText.textContent = `Lade Bestellung ${allOrders.length} von ${totalCount}...`; progressText.textContent = `Lade Bestellung ${fetchedOrders.length} von ${totalCount}...`;
} else { } 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 { try {
localStorage.setItem('kantine_history_cache', JSON.stringify(allOrders)); localStorage.setItem('kantine_history_cache', JSON.stringify(mergedOrders));
} catch (e) { console.warn('History cache write error', e); } } catch (e) {
console.warn('History cache write error', e);
}
// Render the updated history
renderHistory(fullOrderHistoryCache); renderHistory(fullOrderHistoryCache);
}
} catch (error) { } 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>`; historyContent.innerHTML = `<p style="color:var(--error-color);text-align:center;">Fehler beim Laden der Historie.</p>`;
} else {
showToast('Hintergrund-Synchronisation fehlgeschlagen', 'error');
}
} finally { } finally {
historyLoading.classList.add('hidden'); historyLoading.classList.add('hidden');
} }
@@ -2783,7 +2842,7 @@ body {
if (response.ok || response.status === 201) { if (response.ok || response.status === 201) {
showToast(`Bestellt: ${name}`, 'success'); showToast(`Bestellt: ${name}`, 'success');
localStorage.removeItem('kantine_history_cache'); fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
await fetchOrders(); await fetchOrders();
} else { } else {
const data = await response.json(); const data = await response.json();
@@ -2813,7 +2872,7 @@ body {
if (response.ok) { if (response.ok) {
showToast(`Storniert: ${name}`, 'success'); showToast(`Storniert: ${name}`, 'success');
localStorage.removeItem('kantine_history_cache'); fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
await fetchOrders(); await fetchOrders();
} else { } else {
const data = await response.json(); const data = await response.json();
@@ -2837,6 +2896,8 @@ body {
if (userFlags.size === 0) { if (userFlags.size === 0) {
bellBtn.classList.add('hidden'); bellBtn.classList.add('hidden');
bellIcon.style.color = 'var(--text-secondary)';
bellIcon.style.textShadow = 'none';
return; return;
} }
@@ -2871,10 +2932,10 @@ body {
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)';
} }
} }
@@ -3049,6 +3110,7 @@ body {
console.log(`[Cache] Parsed ${allWeeks.length} weeks:`, allWeeks.map(w => `KW${w.weekNumber}/${w.year} (${(w.days || []).length} days)`)); console.log(`[Cache] Parsed ${allWeeks.length} weeks:`, allWeeks.map(w => `KW${w.weekNumber}/${w.year} (${(w.days || []).length} days)`));
renderVisibleWeeks(); renderVisibleWeeks();
updateNextWeekBadge(); updateNextWeekBadge();
updateAlarmBell();
if (cachedTs) updateLastUpdatedTime(cachedTs); if (cachedTs) updateLastUpdatedTime(cachedTs);
console.log('Loaded menu from cache'); console.log('Loaded menu from cache');
return true; return true;
@@ -3833,7 +3895,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.4'; const currentVersion = 'v1.4.13';
const devMode = localStorage.getItem('kantine_dev_mode') === 'true'; const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
try { try {
@@ -3874,7 +3936,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.4'; const currentVersion = 'v1.4.13';
if (!modal) return; if (!modal) return;
modal.classList.remove('hidden'); modal.classList.remove('hidden');
@@ -3892,19 +3954,8 @@ body {
const dm = devToggle.checked; const dm = devToggle.checked;
container.innerHTML = '<p style="color:var(--text-secondary);">Lade Versionen...</p>'; container.innerHTML = '<p style="color:var(--text-secondary);">Lade Versionen...</p>';
try { function renderVersionsList(versions) {
let versions; if (!versions || !versions.length) {
const cached = JSON.parse(localStorage.getItem('kantine_version_cache') || 'null');
if (!forceRefresh && cached && cached.devMode === dm && (Date.now() - cached.timestamp < 3600000)) {
versions = cached.versions;
} else {
versions = await fetchVersions(dm);
localStorage.setItem('kantine_version_cache', JSON.stringify({
timestamp: Date.now(), devMode: dm, versions
}));
}
if (!versions.length) {
container.innerHTML = '<p style="color:var(--text-secondary);">Keine Versionen gefunden.</p>'; container.innerHTML = '<p style="color:var(--text-secondary);">Keine Versionen gefunden.</p>';
return; return;
} }
@@ -3936,6 +3987,34 @@ body {
`; `;
list.appendChild(li); list.appendChild(li);
}); });
}
try {
// 1. Show cached versions immediately if available
const cachedRaw = localStorage.getItem('kantine_version_cache');
let cached = null;
if (cachedRaw) {
try { cached = JSON.parse(cachedRaw); } catch (e) { }
}
if (cached && cached.devMode === dm && cached.versions) {
renderVersionsList(cached.versions);
}
// 2. Fetch fresh versions in background (or foreground if no cache)
const liveVersions = await fetchVersions(dm);
// Compare with cache to see if we need to re-render
const liveVersionsStr = JSON.stringify(liveVersions);
const cachedVersionsStr = cached ? JSON.stringify(cached.versions) : '';
if (liveVersionsStr !== cachedVersionsStr) {
localStorage.setItem('kantine_version_cache', JSON.stringify({
timestamp: Date.now(), devMode: dm, versions: liveVersions
}));
renderVersionsList(liveVersions);
}
} catch (e) { } catch (e) {
container.innerHTML = `<p style="color:#e94560;">Fehler: ${e.message}</p>`; container.innerHTML = `<p style="color:#e94560;">Fehler: ${e.message}</p>`;
} }

View File

@@ -570,37 +570,49 @@
const progressFill = document.getElementById('history-progress-fill'); const progressFill = document.getElementById('history-progress-fill');
const progressText = document.getElementById('history-progress-text'); 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) { if (fullOrderHistoryCache) {
renderHistory(fullOrderHistoryCache); localCache = fullOrderHistoryCache;
return; } else {
} const ls = localStorage.getItem('kantine_history_cache');
if (ls) {
// Check local storage cache
const localCache = localStorage.getItem('kantine_history_cache');
if (localCache) {
try { try {
fullOrderHistoryCache = JSON.parse(localCache); localCache = JSON.parse(ls);
renderHistory(fullOrderHistoryCache); fullOrderHistoryCache = localCache;
return;
} catch (e) { } catch (e) {
console.warn('History cache parse error', 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; if (!authToken) return;
// Start background delta sync
if (localCache.length === 0) {
historyContent.innerHTML = ''; historyContent.innerHTML = '';
historyLoading.classList.remove('hidden'); 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`; progressFill.style.width = '0%';
let allOrders = []; 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 totalCount = 0;
let requiresFullFetch = localCache.length === 0;
let deltaComplete = false;
try { try {
while (nextUrl) { while (nextUrl && !deltaComplete) {
const response = await fetch(nextUrl, { headers: apiHeaders(authToken) }); const response = await fetch(nextUrl, { headers: apiHeaders(authToken) });
if (!response.ok) throw new Error(`Fetch failed: ${response.status}`); if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
@@ -610,29 +622,75 @@
totalCount = data.count; 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 // Update progress
if (!deltaComplete && requiresFullFetch) {
if (totalCount > 0) { if (totalCount > 0) {
const pct = Math.round((allOrders.length / totalCount) * 100); const pct = Math.round((fetchedOrders.length / totalCount) * 100);
progressFill.style.width = `${pct}%`; progressFill.style.width = `${pct}%`;
progressText.textContent = `Lade Bestellung ${allOrders.length} von ${totalCount}...`; progressText.textContent = `Lade Bestellung ${fetchedOrders.length} von ${totalCount}...`;
} else { } 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 { try {
localStorage.setItem('kantine_history_cache', JSON.stringify(allOrders)); localStorage.setItem('kantine_history_cache', JSON.stringify(mergedOrders));
} catch (e) { console.warn('History cache write error', e); } } catch (e) {
console.warn('History cache write error', e);
}
// Render the updated history
renderHistory(fullOrderHistoryCache); renderHistory(fullOrderHistoryCache);
}
} catch (error) { } 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>`; historyContent.innerHTML = `<p style="color:var(--error-color);text-align:center;">Fehler beim Laden der Historie.</p>`;
} else {
showToast('Hintergrund-Synchronisation fehlgeschlagen', 'error');
}
} finally { } finally {
historyLoading.classList.add('hidden'); historyLoading.classList.add('hidden');
} }
@@ -834,7 +892,7 @@
if (response.ok || response.status === 201) { if (response.ok || response.status === 201) {
showToast(`Bestellt: ${name}`, 'success'); showToast(`Bestellt: ${name}`, 'success');
localStorage.removeItem('kantine_history_cache'); fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
await fetchOrders(); await fetchOrders();
} else { } else {
const data = await response.json(); const data = await response.json();
@@ -864,7 +922,7 @@
if (response.ok) { if (response.ok) {
showToast(`Storniert: ${name}`, 'success'); showToast(`Storniert: ${name}`, 'success');
localStorage.removeItem('kantine_history_cache'); fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
await fetchOrders(); await fetchOrders();
} else { } else {
const data = await response.json(); const data = await response.json();
@@ -888,6 +946,8 @@
if (userFlags.size === 0) { if (userFlags.size === 0) {
bellBtn.classList.add('hidden'); bellBtn.classList.add('hidden');
bellIcon.style.color = 'var(--text-secondary)';
bellIcon.style.textShadow = 'none';
return; return;
} }
@@ -922,10 +982,10 @@
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)';
} }
} }
@@ -1100,6 +1160,7 @@
console.log(`[Cache] Parsed ${allWeeks.length} weeks:`, allWeeks.map(w => `KW${w.weekNumber}/${w.year} (${(w.days || []).length} days)`)); console.log(`[Cache] Parsed ${allWeeks.length} weeks:`, allWeeks.map(w => `KW${w.weekNumber}/${w.year} (${(w.days || []).length} days)`));
renderVisibleWeeks(); renderVisibleWeeks();
updateNextWeekBadge(); updateNextWeekBadge();
updateAlarmBell();
if (cachedTs) updateLastUpdatedTime(cachedTs); if (cachedTs) updateLastUpdatedTime(cachedTs);
console.log('Loaded menu from cache'); console.log('Loaded menu from cache');
return true; return true;
@@ -1943,19 +2004,8 @@
const dm = devToggle.checked; const dm = devToggle.checked;
container.innerHTML = '<p style="color:var(--text-secondary);">Lade Versionen...</p>'; container.innerHTML = '<p style="color:var(--text-secondary);">Lade Versionen...</p>';
try { function renderVersionsList(versions) {
let versions; if (!versions || !versions.length) {
const cached = JSON.parse(localStorage.getItem('kantine_version_cache') || 'null');
if (!forceRefresh && cached && cached.devMode === dm && (Date.now() - cached.timestamp < 3600000)) {
versions = cached.versions;
} else {
versions = await fetchVersions(dm);
localStorage.setItem('kantine_version_cache', JSON.stringify({
timestamp: Date.now(), devMode: dm, versions
}));
}
if (!versions.length) {
container.innerHTML = '<p style="color:var(--text-secondary);">Keine Versionen gefunden.</p>'; container.innerHTML = '<p style="color:var(--text-secondary);">Keine Versionen gefunden.</p>';
return; return;
} }
@@ -1987,6 +2037,34 @@
`; `;
list.appendChild(li); list.appendChild(li);
}); });
}
try {
// 1. Show cached versions immediately if available
const cachedRaw = localStorage.getItem('kantine_version_cache');
let cached = null;
if (cachedRaw) {
try { cached = JSON.parse(cachedRaw); } catch (e) { }
}
if (cached && cached.devMode === dm && cached.versions) {
renderVersionsList(cached.versions);
}
// 2. Fetch fresh versions in background (or foreground if no cache)
const liveVersions = await fetchVersions(dm);
// Compare with cache to see if we need to re-render
const liveVersionsStr = JSON.stringify(liveVersions);
const cachedVersionsStr = cached ? JSON.stringify(cached.versions) : '';
if (liveVersionsStr !== cachedVersionsStr) {
localStorage.setItem('kantine_version_cache', JSON.stringify({
timestamp: Date.now(), devMode: dm, versions: liveVersions
}));
renderVersionsList(liveVersions);
}
} catch (e) { } catch (e) {
container.innerHTML = `<p style="color:#e94560;">Fehler: ${e.message}</p>`; container.innerHTML = `<p style="color:#e94560;">Fehler: ${e.message}</p>`;
} }

58
release.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
# release.sh - Deploys a new version of the Kantine Wrapper
# Ensure we're in the script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
cd "$SCRIPT_DIR"
# Ensure tests have run and artifacts exist
if [ ! -d "$SCRIPT_DIR/dist" ]; then
echo "❌ Error: dist folder missing. Please run build-bookmarklet.sh first"
exit 1
fi
# Get current version
VERSION=$(cat "version.txt" | tr -d '\n\r ')
# Validate that version is set
if [ -z "$VERSION" ]; then
echo "❌ Error: Could not determine version from version.txt"
exit 1
fi
echo "=== Kantine Bookmarklet Releaser ($VERSION) ==="
# Check for uncommitted changes (excluding dist/)
if ! git diff-index --quiet HEAD -- ":(exclude)dist"; then
echo "⚠️ Warning: You have uncommitted changes in the working directory."
echo "Please commit your code changes before running the release script."
exit 1
fi
echo "=== Committing build artifacts ==="
git add "dist/"
git commit -m "chore: update build artifacts for $VERSION" --allow-empty
echo ""
echo "=== Tagging $VERSION ==="
if git rev-parse "$VERSION" >/dev/null 2>&1; then
git tag -f "$VERSION"
echo "🔄 Tag $VERSION moved to current commit."
else
git tag "$VERSION"
echo "✅ Created tag: $VERSION"
fi
echo ""
echo "=== Pushing to remotes ==="
# Determine remote targets: Assume 'origin' for primary, optionally 'github'
git push origin HEAD
git push origin --force tag "$VERSION"
# If a remote named 'github' exists, push tags there too
if git remote | grep -q "^github$"; then
git push github --force tag "$VERSION"
fi
echo "🎉 Successfully released version $VERSION!"
exit 0

View File

@@ -192,6 +192,7 @@ body {
border-color: #f59e0b; border-color: #f59e0b;
color: var(--accent-color); color: var(--accent-color);
} }
.nav-btn.new-week-available.active { .nav-btn.new-week-available.active {
color: white; color: white;
} }
@@ -200,9 +201,11 @@ body {
0% { 0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7);
} }
70% { 70% {
box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); box-shadow: 0 0 0 10px rgba(245, 158, 11, 0);
} }
100% { 100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
} }
@@ -798,7 +801,7 @@ body {
/* No opacity/filter here - fully visible */ /* No opacity/filter here - fully visible */
background: var(--bg-card); background: var(--bg-card);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid var(--accent-color); border: 1px solid #8b5cf6;
border-radius: 8px; border-radius: 8px;
padding: 1rem; padding: 1rem;
margin: 0 -1rem 1.5rem -1rem; margin: 0 -1rem 1.5rem -1rem;
@@ -1618,8 +1621,6 @@ body {
.version-list { .version-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
max-height: 350px;
overflow-y: auto;
margin: 0; margin: 0;
} }

View File

@@ -108,7 +108,8 @@ try {
[/function\s+isNewer/, 'isNewer function'], [/function\s+isNewer/, 'isNewer function'],
[/function\s+openVersionMenu/, 'openVersionMenu function'], [/function\s+openVersionMenu/, 'openVersionMenu function'],
[/kantine_dev_mode/, 'dev-mode localStorage key'], [/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) { for (const [regex, label] of checks) {

View File

@@ -1 +1 @@
v1.4.4 v1.4.13