Compare commits

...

10 Commits

Author SHA1 Message Date
Kantine Wrapper
4c55e34bc1 dist files for v1.3.2 built 2026-02-19 20:45:18 +01:00
Kantine Wrapper
08ee2a2d0f dist files for v1.3.1 built 2026-02-17 21:33:09 +01:00
Kantine Wrapper
314728f6d0 dist files for v1.3.1 built 2026-02-17 21:31:25 +01:00
Kantine Wrapper
ea4e0d151f dist files for v1.3.1 built 2026-02-17 21:25:55 +01:00
Kantine Wrapper
b928b90728 dist files for v1.3.1 built 2026-02-17 21:17:40 +01:00
Kantine Wrapper
9b1f0e2fd3 v1.3.1: Smart Cache vereinfacht (KW-Check + 1h Alter), Build-Script auto-commit+push 2026-02-17 21:17:34 +01:00
Kantine Wrapper
05bc06660c docs: Add warning about force-pushing when moving an existing git tag. 2026-02-17 21:04:11 +01:00
Kantine Wrapper
20957c3582 chore: corrected README 2026-02-17 20:49:28 +01:00
fe86e68aca v1.3.1: Smart Cache Strategy + REQUIREMENTS.md überarbeitet
- Feature: isCacheFresh() – initialer API-Refresh nur wenn Cache >1h alt oder <5 Arbeitstage abgedeckt (FR-024)
- REQUIREMENTS.md komplett überarbeitet: lösungsneutral, alle Features dokumentiert, Versions-Spalte
- Build-Script: Git-Tag wird bei Rebuild auf aktuellen Commit verschoben (git tag -f)
- Neue Regel 7: Requirements-Konsistenz im Implementation Plan
- test_logic.js: statischer Check für isCacheFresh
2026-02-17 20:38:53 +01:00
4dbd6c930f feat: Expand and refine the system's purpose, scope, and functional/non-functional requirements, adding new categories like smart highlights and update management. 2026-02-17 18:06:57 +01:00
12 changed files with 250 additions and 68 deletions

View File

@@ -48,3 +48,10 @@ trigger: always_on
## 6. Workspace Scopes ## 6. Workspace Scopes
- **Browser**: Allowed for documentation and safe browsing. No automated logins without permission. - **Browser**: Allowed for documentation and safe browsing. No automated logins without permission.
- **Terminal**: No `rm -rf`. Run tests (`pytest` etc.) after logic changes. - **Terminal**: No `rm -rf`. Run tests (`pytest` etc.) after logic changes.
## 7. Requirements-Konsistenz 📋
Alle umgesetzten Anforderungen müssen mit `REQUIREMENTS.md` übereinstimmen.
1. **Vor der Umsetzung prüfen**: Passt die neue Anforderung zu den bestehenden Requirements?
2. **Fehlende Requirements**: Wenn eine umzusetzende Anforderung in `REQUIREMENTS.md` fehlt, muss dies im **Implementation Plan** explizit dokumentiert und freigegeben werden.
3. **Widersprüche**: Wenn eine neue Anforderung im Widerspruch zu bestehenden Requirements steht, muss dies im **Implementation Plan** als ⚠️ Warning hervorgehoben werden.
4. **Nach Freigabe**: Bei Genehmigung des Implementation Plans muss `REQUIREMENTS.md` entsprechend erweitert oder korrigiert werden (neue ID, Version, Beschreibung). Obsolet gewordene Requirements werden nicht aus `REQUIREMENTS.md` gelöscht sondern nur als durchgestrichen markiert und ein Kommentar mit dem Grund hinzugefügt.

View File

@@ -1,4 +1,4 @@
# Kantine Wrapper Bookmarklet (v1.2.0) # Kantine Wrapper Bookmarklet
Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses Skript erweitert die Standardansicht um eine **Wochenübersicht**, Kostenkontrolle und verbesserte Usability. Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses Skript erweitert die Standardansicht um eine **Wochenübersicht**, Kostenkontrolle und verbesserte Usability.

View File

@@ -2,55 +2,90 @@
## 1. Einleitung ## 1. Einleitung
### 1.1 Zweck des Systems ### 1.1 Zweck des Systems
Das System dient als Wrapper und Erweiterung für das Bessa Web-Shop-Portal der Knapp-Kantine (https://web.bessa.app/knapp-kantine). Es ermöglicht den Benutzern, Menüpläne einzusehen, Buchungen automatisiert vorzunehmen und Menüdaten persistent für den öffentlichen Zugriff bereitzustellen, ohne dass jeder Betrachter eigene Zugangsdaten benötigt. Das System dient als Erweiterung für das Bessa Web-Shop-Portal der Knapp-Kantine (https://web.bessa.app/knapp-kantine). Es verbessert die Benutzererfahrung durch eine übersichtliche Wochenansicht, vereinfachte Bestellvorgänge, Kostentransparenz und proaktive Benachrichtigungen.
### 1.2 High-Level-Scope ### 1.2 High-Level-Scope
Das System umfasst einen automatisierten Scraper, eine API zur Datenbereitstellung, einen persistenten Dateispeicher für erfasste Menüs und ein Frontend-Dashboard zur Visualisierung der Kantinen-Wochenpläne. Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, die Verwaltung von Bestellungen und Stornierungen, ein Benachrichtigungssystem für Verfügbarkeitsänderungen, personalisierte Menü-Highlights sowie ein automatisches Update-Management.
## 2. Funktionale Anforderungen ## 2. Funktionale Anforderungen
| ID | Anforderung (Satzschablone nach Chris Rupp) | Priorität | | ID | Anforderung (Satzschablone nach Chris Rupp) | Priorität | Seit |
|:---|:---|:---| |:---|:---|:---|:---|
| **Auth & Sessions** | | | | **Authentifizierung & Zugang** | | | |
| FR-001 | Das System muss dem Benutzer die Möglichkeit bieten, sich mit Mitarbeiternummer und Passwort am Bessa-Backend anzumelden. | Hoch | | FR-001 | Das System muss dem Benutzer die Möglichkeit bieten, sich mit Mitarbeiternummer und Passwort anzumelden. | Hoch | v1.0.1 |
| FR-002 | Sobald ein Benutzer erfolgreich angemeldet ist, muss das System eine Session-ID erzeugen und die resultierenden Cookies verschlüsselt für 2 Stunden vorhalten. | Hoch | | FR-002 | Das System muss eine bestehende Bessa-Session erkennen und automatisch wiederverwenden, um eine erneute Anmeldung zu vermeiden. | Hoch | v1.0.1 |
| FR-003 | Das System muss die Zugangsdaten (Mitarbeiternummer/Passwort) unmittelbar nach der Verwendung durch den Scraper verwerfen und darf diese nicht dauerhaft speichern. | Hoch | | FR-003 | Das System darf keine Zugangsdaten dauerhaft speichern. Die Authentifizierung muss sitzungsbasiert sein. | Hoch | v1.0.1 |
| **Scraper & Datenextraktion** | | | | FR-004 | Dem Benutzer muss angezeigt werden, ob und als wer er angemeldet ist (Vorname, Name oder ID). | Mittel | v1.0.1 |
| FR-004 | Das System muss in der Lage sein, den wöchentlichen Menüplan (Montag bis Freitag) automatisiert von der Bessa-Webseite zu extrahieren. | Hoch | | FR-005 | Nicht authentifizierte Benutzer müssen die Menüdaten einsehen können (eingeschränkter Lesezugriff). | Mittel | v1.0.1 |
| FR-005 | Wenn ein Tag bereits eine Buchung enthält, muss das System den Navigationspfad so anpassen, dass dennoch alle verfügbaren Menüoptionen (M1F, M2F, etc.) extrahiert werden können. | Mittel | | FR-006 | Das System muss eine explizite Logout-Funktion bereitstellen, die alle sitzungsbezogenen Daten entfernt. | Mittel | v1.0.1 |
| FR-006 | Das System muss für jedes extrahierte Gericht den Namen, die Beschreibung, den Preis und den Status (verfügbar/nicht verfügbar/ bestellt) erfassen. | Hoch | | **Menüanzeige** | | | |
| **Datenhaltung & Zugriff** | | | | FR-010 | Das System muss dem Benutzer alle verfügbaren Tagesmenüs einer Woche gleichzeitig in einer Übersicht darstellen. | Hoch | v1.0.1 |
| FR-007 | Das System muss erfolgreich gescrapte Menüpläne in einer persistenten JSON-Datei (`data/menus.json`) speichern. | Hoch | | FR-011 | Das System muss dem Benutzer die Navigation zwischen der aktuellen und der kommenden Woche ermöglichen. | Mittel | v1.0.1 |
| FR-008 | Das System muss unauthentifizierten Benutzern den Zugriff auf bereits im Speicher befindliche Menüdaten ermöglichen (Public Access). | Mittel | | FR-012 | Für jedes Gericht müssen Name, Beschreibung, Preis und Verfügbarkeitsstatus angezeigt werden. | Hoch | v1.0.1 |
| FR-009 | Falls keine Daten im persistenten Speicher vorhanden sind, muss das System einen nicht authentifizierten Benutzer die Möglichkeit bieten sich anzumelden, um den Speicher zu initialisieren. | Hoch | | FR-013 | Bereits bestellte Menüs müssen visuell von nicht bestellten unterscheidbar sein (farbliche Markierung, Badge). | Mittel | v1.0.1 |
| **Dashboard & UI** | | | | FR-014 | Die Tageskarten-Header müssen den Bestellstatus des Tages farblich signalisieren (bestellt / bestellbar / nicht bestellbar). | Niedrig | v1.0.1 |
| FR-010 | Das System muss dem Benutzer eine intuitive Wochenansicht des Menüplans im Browser darstellen. | Mittel | | FR-015 | Bestellte Menü-Codes (z.B. M1, M2) müssen als Badges im Tageskarten-Header angezeigt werden. | Niedrig | v1.0.1 |
| FR-011 | Wenn ein Scraper-Vorgang aktiv ist, muss das System den Status (Fortschritt, aktuelle Aktion) in Echtzeit visualisieren. | Niedrig | | FR-016 | Am heutigen Tag müssen bestellte Menüs an erster Stelle sortiert werden. | Niedrig | v1.0.1 |
| **Buchungsfunktion** | | | | FR-017 | Wenn keine Menüdaten für eine Woche vorliegen, muss ein aussagekräftiger Leertext angezeigt werden. | Niedrig | v1.0.1 |
| FR-012 | Wenn der Benutzer authentifiziert ist, soll das System eine Bestellung für ein Menü ermöglichen. | Mittel | | **Daten-Aktualisierung** | | | |
| FR-013 | Wenn der Benutzer authentifiziert ist, soll das System ein bereits bestelltes Menü zu stornieren. | Mittel | | FR-020 | Wenn Menüdaten geladen werden, muss der Fortschritt dem Benutzer in Echtzeit angezeigt werden (Fortschrittsbalken, Statustext). | Niedrig | v1.0.1 |
| **Menu Flagging & Notifications** | | | | FR-021 | Das System muss bereits geladene Menüdaten zwischenspeichern, um bei erneutem Aufruf sofort eine Übersicht anzeigen zu können. | Mittel | v1.0.1 |
| FR-014 | Das System muss authentifizierten Benutzern ermöglichen, nicht bestellbare Menüs (deren Cutoff noch nicht erreicht ist) zu markieren ("flaggen"). | Mittel | | FR-022 | Das System muss dem Benutzer die Möglichkeit bieten, die Menüdaten manuell neu zu laden. | Niedrig | v1.0.1 |
| FR-015 | Das System soll die Statusprüfung für geflaggte Menüs auf verbundene Clients verteilen (Distributed Polling), um die Last zu minimieren. Der Server orchestriert, welcher Client wann welche Menüs prüft. | Mittel | | FR-023 | Der Zeitpunkt der letzten Aktualisierung muss für den Benutzer sichtbar sein. | Niedrig | v1.0.1 |
| FR-016 | Bei Statusänderung auf "verfügbar" muss das System den Benutzer benachrichtigen (Systembenachrichtigung). | Mittel | | FR-024 | Das System darf beim Start keinen automatischen API-Refresh durchführen, wenn der Cache frisch (< 1 Stunde) und Daten für die aktuelle Kalenderwoche vorhanden sind. | Mittel | v1.3.1 |
| FR-017 | Geflaggte und ausverkaufte Menüs müssen im UI mit einem gelben Glow hervorgehoben werden. | Mittel | | **Bestellfunktion** | | | |
| FR-018 | Geflaggte und verfügbare Menüs müssen im UI mit einem grünen Glow hervorgehoben werden. | Mittel | | FR-030 | Authentifizierte Benutzer müssen ein verfügbares Menü direkt aus der Übersicht bestellen können. | Hoch | v1.0.1 |
| FR-019 | Wenn die Bestell-Cutoff-Zeit erreicht ist, muss das System das Flag automatisch entfernen. | Mittel | | FR-031 | Authentifizierte Benutzer müssen eine bestehende Bestellung direkt aus der Übersicht stornieren können. | Hoch | v1.0.1 |
| FR-032 | Nach Bestellschluss (Cutoff-Zeit) dürfen keine neuen Bestellungen oder Stornierungen für diesen Tag möglich sein. | Hoch | 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** | | | |
| FR-040 | Das System muss die Gesamtkosten aller Bestellungen einer Woche automatisch berechnen und anzeigen. | Mittel | v1.1.0 |
| **Bestell-Countdown** | | | |
| FR-050 | Das System muss vor Bestellschluss einen visuell hervorgehobenen Countdown anzeigen. | Mittel | v1.1.0 |
| **Menü-Flagging & Benachrichtigungen** | | | |
| FR-060 | Authentifizierte Benutzer müssen ausverkaufte Menüs zur Beobachtung markieren können ("flaggen"). | Mittel | v1.0.1 |
| FR-061 | Das System muss geflaggte Menüs periodisch auf Verfügbarkeitsänderungen prüfen. | Mittel | v1.0.1 |
| FR-062 | Bei Statusänderung eines geflaggten Menüs auf „verfügbar" muss der Benutzer benachrichtigt werden (In-App + Systembenachrichtigung). | Mittel | v1.0.1 |
| FR-063 | Geflaggte Menüs müssen im UI visuell hervorgehoben werden (gelber Glow bei ausverkauft, grüner Glow bei verfügbar). | Mittel | v1.0.1 |
| FR-064 | Wenn die Bestell-Cutoff-Zeit erreicht ist, muss das System ein Flag automatisch entfernen. | Mittel | v1.0.1 |
| **Personalisierung: Smart Highlights** | | | |
| FR-070 | Benutzer müssen Schlagwörter (Tags) definieren können, nach denen Menüs automatisch visuell hervorgehoben werden. | Mittel | v1.1.0 |
| FR-071 | Die Hervorhebung muss anhand von Menüname und Beschreibung erfolgen (Substring-Matching, case-insensitive). | Niedrig | v1.1.0 |
| FR-072 | Hervorgehobene Menüs müssen ein Tag-Badge mit dem matchenden Schlagwort anzeigen. | Niedrig | v1.2.4 |
| FR-073 | Die Nächste-Woche-Navigation muss die Anzahl der dort gefundenen Highlights als Badge anzeigen. | Niedrig | v1.2.5 |
| **Darstellung & Theming** | | | |
| FR-080 | Das System muss einen hellen und einen dunklen Darstellungsmodus (Light/Dark Theme) unterstützen. | Niedrig | v1.0.1 |
| FR-081 | Die Theme-Präferenz des Benutzers muss persistent gespeichert werden. | 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 |
| **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 |
| **Nächste-Woche-Badge** | | | |
| FR-100 | Die Navigation zur nächsten Woche muss ein Badge anzeigen, das den Überblick über den Bestellstatus der kommenden Woche visualisiert (bestellt / bestellbar / gesamt). | Niedrig | v1.0.1 |
| **Update-Management** | | | |
| FR-110 | Das System muss periodisch prüfen, ob eine neuere Version verfügbar ist. | Niedrig | v1.0.3 |
| FR-111 | Bei Verfügbarkeit einer neueren Version muss ein diskreter Indikator im Header angezeigt werden. | Niedrig | v1.0.3 |
| 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 |
## 3. Nicht-funktionale Anforderungen ## 3. Nicht-funktionale Anforderungen
| Kategorie (ISO 25010) | ID | Anforderung | Zielwert/Metrik | | Kategorie (ISO 25010) | ID | Anforderung | Zielwert/Metrik |
|:---|:---|:---|:---| |:---|:---|:---|:---|
| **Performance** | NFR-001 | Antwortzeit der API für gecachte Daten | < 200 ms (95. Perzentil) | | **Performance** | NFR-001 | Die Darstellung bereits gecachter Daten muss ohne spürbare Verzögerung erfolgen. | < 200 ms (UI-Rendering) |
| **Performance** | NFR-007 | Polling-Effizienz & Token-Nutzung | Kein System-Token verfügbar. Polling muss über authentifizierte User-Clients erfolgen. Der Server muss die Anfragen so verteilen, dass redundante Abfragen vermieden werden. | | **Performance** | NFR-002 | Das Polling für geflaggte Menüs darf die reguläre Nutzung nicht beeinträchtigen. | Intervall ≥ 5 Minuten |
| **Performance** | NFR-002 | Dauer eines vollständigen Scrape-Vorgangs (exkl. Navigation) | < 30 Sekunden pro Woche | | **Sicherheit** | NFR-003 | Es dürfen keine Zugangsdaten dauerhaft gespeichert werden. | 0 (keine persistente Speicherung von Passwörtern) |
| **Sicherheit** | NFR-003 | Speicherung von Zugangsdaten | 0 (keine dauerhafte Speicherung von Passwörtern) | | **Sicherheit** | NFR-004 | Auth-Tokens müssen sitzungsbasiert gespeichert werden und bei Schließen des Browsers verfallen. | sessionStorage |
| **Sicherheit** | NFR-004 | Session-Sicherheit | HttpOnly Cookies für die Kommunikation zwischen Frontend und Backend | | **Benutzbarkeit** | NFR-005 | Die Oberfläche muss auf mobilen Geräten fehlerfrei nutzbar sein. | Viewports ab 320px Breite |
| **Wartbarkeit** | NFR-005 | Testabdeckung der Scraper-Logik | Alle Kern-Selektoren müssen durch Debug-HTML-Dumps verifizierbar sein | | **Benutzbarkeit** | NFR-006 | Alle interaktiven Elemente müssen Tooltips oder Hilfetexte bieten. | 100% Coverage |
| **Benutzbarkeit** | NFR-006 | Mobile Responsiveness | Dashboard muss auf Viewports ab 320px Breite fehlerfrei nutzbar sein | | **Benutzbarkeit** | NFR-007 | Die Benutzeroberfläche muss vollständig in deutscher Sprache sein. | Vollständige Lokalisierung |
| **Wartbarkeit** | NFR-008 | Die Build-Artefakte müssen durch automatisierte Tests validiert werden. | Build-Tests + Logik-Tests |
## 4. Technische Randbedingungen ## 4. Technische Randbedingungen
* **Architektur**: Node.js Backend mit Express (API + Static Serving) und Vanilla JS Frontend. * **Deployment**: Das System wird als Bookmarklet ausgeliefert, das auf der Bessa-Webseite ausgeführt wird.
* **Engine**: Direkte API-Integration (Reverse Engineering der Bessa API) für maximale Performance und Zuverlässigkeit. * **Datenquelle**: Direkte Integration mit der Bessa REST-API (`api.bessa.app/v1`).
* **Datenspeicher**: Dateibasierter JSON-Store für persistente Daten + In-Memory Caching. * **Datenhaltung**: Clientseitig via `localStorage` (Menü-Cache, Flags, Highlights, Theme) und `sessionStorage` (Auth-Token).
* **Schnittstellen**: REST API (`/api/bookings`, `/api/status`, `/api/order`). * **Build**: Bash-basiertes Build-Script, das Bookmarklet-URL, Standalone-HTML und Installer-Seite generiert.
* **Runtime**: Node.js Umgebung, Docker-ready. * **Versionierung**: SemVer, verwaltet über GitHub Releases/Tags.
* **Tests**: Python-basierte Build-Tests + Node.js-basierte Logik-Tests.

View File

@@ -267,12 +267,23 @@ if [ $TEST_EXIT -ne 0 ]; then
fi fi
echo "✅ All build tests passed." echo "✅ All build tests passed."
# === 5. Auto-tag version === # === 5. Commit, tag, and push ===
echo ""
echo "=== Committing & Pushing ==="
git add -A
git commit -m "dist files for $VERSION built" --allow-empty
echo "" echo ""
echo "=== Tagging $VERSION ===" echo "=== Tagging $VERSION ==="
if git rev-parse "$VERSION" >/dev/null 2>&1; then if git rev-parse "$VERSION" >/dev/null 2>&1; then
echo " Tag $VERSION already exists, skipping." git tag -f "$VERSION"
echo "🔄 Tag $VERSION moved to current commit."
else else
git tag "$VERSION" git tag "$VERSION"
echo "✅ Created tag: $VERSION" echo "✅ Created tag: $VERSION"
fi 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,9 @@
## v1.3.2 (2026-02-19)
- **Fix**: Falsche Anzahl an Highlight-Menüs im "Nächste Woche"-Badge korrigiert (zählte alle Menüs statt nur Highlights). 🐛
## v1.3.1 (2026-02-17)
- **Feature**: Smart Cache API-Refresh beim Start wird übersprungen wenn Daten für die aktuelle KW vorhanden und Cache < 1h alt ist. ⚡
## v1.3.0 (2026-02-16) ## v1.3.0 (2026-02-16)
- **Feature**: GitHub Release Management 📦 - **Feature**: GitHub Release Management 📦
- Version-Menü: Klick auf Versionsnummer zeigt alle verfügbaren Versionen - Version-Menü: Klick auf Versionsnummer zeigt alle verfügbaren Versionen

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

20
dist/install.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -1807,7 +1807,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.3.0</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.3.2</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div> <div id="last-updated-subtitle" class="subtitle"></div>
</div> </div>
</div> </div>
@@ -1919,7 +1919,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.3.0</span> <strong>Aktuell:</strong> <span id="version-current">v1.3.2</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;">
@@ -2516,10 +2516,12 @@ body {
try { try {
const cached = localStorage.getItem(CACHE_KEY); const cached = localStorage.getItem(CACHE_KEY);
const cachedTs = localStorage.getItem(CACHE_TS_KEY); const cachedTs = localStorage.getItem(CACHE_TS_KEY);
console.log(`[Cache] localStorage: key=${!!cached} (${cached ? cached.length : 0} chars), ts=${cachedTs}`);
if (cached) { if (cached) {
allWeeks = JSON.parse(cached); allWeeks = JSON.parse(cached);
currentWeekNumber = getISOWeek(new Date()); currentWeekNumber = getISOWeek(new Date());
currentYear = new Date().getFullYear(); currentYear = new Date().getFullYear();
console.log(`[Cache] Parsed ${allWeeks.length} weeks:`, allWeeks.map(w => `KW${w.weekNumber}/${w.year} (${(w.days || []).length} days)`));
renderVisibleWeeks(); renderVisibleWeeks();
updateNextWeekBadge(); updateNextWeekBadge();
if (cachedTs) updateLastUpdatedTime(cachedTs); if (cachedTs) updateLastUpdatedTime(cachedTs);
@@ -2532,6 +2534,31 @@ body {
return false; return false;
} }
// FR-024: Check if cache is fresh enough to skip API refresh
function isCacheFresh() {
const cachedTs = localStorage.getItem(CACHE_TS_KEY);
if (!cachedTs) {
console.log('[Cache] No timestamp found');
return false;
}
// Condition 1: Cache < 1 hour old
const ageMs = Date.now() - new Date(cachedTs).getTime();
const ageMin = Math.round(ageMs / 60000);
if (ageMs > 60 * 60 * 1000) {
console.log(`[Cache] Stale: ${ageMin}min old (max 60)`);
return false;
}
// Condition 2: Data for current week exists
const thisWeek = getISOWeek(new Date());
const thisYear = getWeekYear(new Date());
const hasCurrentWeek = allWeeks.some(w => w.weekNumber === thisWeek && w.year === thisYear && w.days && w.days.length > 0);
console.log(`[Cache] Age: ${ageMin}min, looking for KW${thisWeek}/${thisYear}, found: ${hasCurrentWeek}`);
return hasCurrentWeek;
}
// === Menu Data Fetching (Direct from Bessa API) === // === Menu Data Fetching (Direct from Bessa API) ===
async function loadMenuDataFromAPI() { async function loadMenuDataFromAPI() {
const loading = document.getElementById('loading'); const loading = document.getElementById('loading');
@@ -2729,17 +2756,39 @@ body {
} }
// === Last Updated Display === // === Last Updated Display ===
let lastUpdatedTimestamp = null;
let lastUpdatedIntervalId = null;
function updateLastUpdatedTime(isoTimestamp) { function updateLastUpdatedTime(isoTimestamp) {
const subtitle = document.getElementById('last-updated-subtitle'); const subtitle = document.getElementById('last-updated-subtitle');
if (!isoTimestamp) return; if (!isoTimestamp) return;
lastUpdatedTimestamp = isoTimestamp;
try { try {
const date = new Date(isoTimestamp); const date = new Date(isoTimestamp);
const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
const dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
subtitle.textContent = `Aktualisiert: ${dateStr} ${timeStr}`; const ago = getRelativeTime(date);
subtitle.textContent = `Aktualisiert: ${dateStr} ${timeStr} (${ago})`;
} catch (e) { } catch (e) {
subtitle.textContent = ''; subtitle.textContent = '';
} }
// Auto-refresh relative time every minute
if (!lastUpdatedIntervalId) {
lastUpdatedIntervalId = setInterval(() => {
if (lastUpdatedTimestamp) updateLastUpdatedTime(lastUpdatedTimestamp);
}, 60 * 1000);
}
}
function getRelativeTime(date) {
const diffMs = Date.now() - date.getTime();
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return 'gerade eben';
if (diffMin === 1) return 'vor 1 min.';
if (diffMin < 60) return `vor ${diffMin} min.`;
const diffH = Math.floor(diffMin / 60);
if (diffH === 1) return 'vor 1 Std.';
return `vor ${diffH} Std.`;
} }
// === Toast Notification === // === Toast Notification ===
@@ -2828,7 +2877,9 @@ body {
if (nextWeekData && nextWeekData.days) { if (nextWeekData && nextWeekData.days) {
nextWeekData.days.forEach(day => { nextWeekData.days.forEach(day => {
day.items.forEach(item => { day.items.forEach(item => {
if (checkHighlight(item.name) || checkHighlight(item.description)) { const nameMatches = checkHighlight(item.name);
const descMatches = checkHighlight(item.description);
if (nameMatches.length > 0 || descMatches.length > 0) {
highlightCount++; highlightCount++;
} }
}); });
@@ -3243,7 +3294,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.3.0'; const currentVersion = 'v1.3.2';
const devMode = localStorage.getItem('kantine_dev_mode') === 'true'; const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
try { try {
@@ -3284,7 +3335,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.3.0'; const currentVersion = 'v1.3.2';
if (!modal) return; if (!modal) return;
modal.classList.remove('hidden'); modal.classList.remove('hidden');
@@ -3487,13 +3538,19 @@ body {
updateAuthUI(); updateAuthUI();
cleanupExpiredFlags(); cleanupExpiredFlags();
// Load cached data first for instant UI, then refresh from API // Load cached data first for instant UI, refresh only if stale (FR-024)
const hadCache = loadMenuCache(); const hadCache = loadMenuCache();
if (hadCache) { if (hadCache) {
// Hide loading spinner since cache is shown
document.getElementById('loading').classList.add('hidden'); document.getElementById('loading').classList.add('hidden');
if (!isCacheFresh()) {
console.log('Cache stale or incomplete refreshing from API');
loadMenuDataFromAPI();
} else {
console.log('Cache fresh & complete skipping API refresh');
}
} else {
loadMenuDataFromAPI();
} }
loadMenuDataFromAPI();
// Auto-start polling if already logged in // Auto-start polling if already logged in
if (authToken) { if (authToken) {

View File

@@ -780,10 +780,12 @@
try { try {
const cached = localStorage.getItem(CACHE_KEY); const cached = localStorage.getItem(CACHE_KEY);
const cachedTs = localStorage.getItem(CACHE_TS_KEY); const cachedTs = localStorage.getItem(CACHE_TS_KEY);
console.log(`[Cache] localStorage: key=${!!cached} (${cached ? cached.length : 0} chars), ts=${cachedTs}`);
if (cached) { if (cached) {
allWeeks = JSON.parse(cached); allWeeks = JSON.parse(cached);
currentWeekNumber = getISOWeek(new Date()); currentWeekNumber = getISOWeek(new Date());
currentYear = new Date().getFullYear(); currentYear = new Date().getFullYear();
console.log(`[Cache] Parsed ${allWeeks.length} weeks:`, allWeeks.map(w => `KW${w.weekNumber}/${w.year} (${(w.days || []).length} days)`));
renderVisibleWeeks(); renderVisibleWeeks();
updateNextWeekBadge(); updateNextWeekBadge();
if (cachedTs) updateLastUpdatedTime(cachedTs); if (cachedTs) updateLastUpdatedTime(cachedTs);
@@ -796,6 +798,31 @@
return false; return false;
} }
// FR-024: Check if cache is fresh enough to skip API refresh
function isCacheFresh() {
const cachedTs = localStorage.getItem(CACHE_TS_KEY);
if (!cachedTs) {
console.log('[Cache] No timestamp found');
return false;
}
// Condition 1: Cache < 1 hour old
const ageMs = Date.now() - new Date(cachedTs).getTime();
const ageMin = Math.round(ageMs / 60000);
if (ageMs > 60 * 60 * 1000) {
console.log(`[Cache] Stale: ${ageMin}min old (max 60)`);
return false;
}
// Condition 2: Data for current week exists
const thisWeek = getISOWeek(new Date());
const thisYear = getWeekYear(new Date());
const hasCurrentWeek = allWeeks.some(w => w.weekNumber === thisWeek && w.year === thisYear && w.days && w.days.length > 0);
console.log(`[Cache] Age: ${ageMin}min, looking for KW${thisWeek}/${thisYear}, found: ${hasCurrentWeek}`);
return hasCurrentWeek;
}
// === Menu Data Fetching (Direct from Bessa API) === // === Menu Data Fetching (Direct from Bessa API) ===
async function loadMenuDataFromAPI() { async function loadMenuDataFromAPI() {
const loading = document.getElementById('loading'); const loading = document.getElementById('loading');
@@ -993,17 +1020,39 @@
} }
// === Last Updated Display === // === Last Updated Display ===
let lastUpdatedTimestamp = null;
let lastUpdatedIntervalId = null;
function updateLastUpdatedTime(isoTimestamp) { function updateLastUpdatedTime(isoTimestamp) {
const subtitle = document.getElementById('last-updated-subtitle'); const subtitle = document.getElementById('last-updated-subtitle');
if (!isoTimestamp) return; if (!isoTimestamp) return;
lastUpdatedTimestamp = isoTimestamp;
try { try {
const date = new Date(isoTimestamp); const date = new Date(isoTimestamp);
const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
const dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
subtitle.textContent = `Aktualisiert: ${dateStr} ${timeStr}`; const ago = getRelativeTime(date);
subtitle.textContent = `Aktualisiert: ${dateStr} ${timeStr} (${ago})`;
} catch (e) { } catch (e) {
subtitle.textContent = ''; subtitle.textContent = '';
} }
// Auto-refresh relative time every minute
if (!lastUpdatedIntervalId) {
lastUpdatedIntervalId = setInterval(() => {
if (lastUpdatedTimestamp) updateLastUpdatedTime(lastUpdatedTimestamp);
}, 60 * 1000);
}
}
function getRelativeTime(date) {
const diffMs = Date.now() - date.getTime();
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return 'gerade eben';
if (diffMin === 1) return 'vor 1 min.';
if (diffMin < 60) return `vor ${diffMin} min.`;
const diffH = Math.floor(diffMin / 60);
if (diffH === 1) return 'vor 1 Std.';
return `vor ${diffH} Std.`;
} }
// === Toast Notification === // === Toast Notification ===
@@ -1092,7 +1141,9 @@
if (nextWeekData && nextWeekData.days) { if (nextWeekData && nextWeekData.days) {
nextWeekData.days.forEach(day => { nextWeekData.days.forEach(day => {
day.items.forEach(item => { day.items.forEach(item => {
if (checkHighlight(item.name) || checkHighlight(item.description)) { const nameMatches = checkHighlight(item.name);
const descMatches = checkHighlight(item.description);
if (nameMatches.length > 0 || descMatches.length > 0) {
highlightCount++; highlightCount++;
} }
}); });
@@ -1751,13 +1802,19 @@
updateAuthUI(); updateAuthUI();
cleanupExpiredFlags(); cleanupExpiredFlags();
// Load cached data first for instant UI, then refresh from API // Load cached data first for instant UI, refresh only if stale (FR-024)
const hadCache = loadMenuCache(); const hadCache = loadMenuCache();
if (hadCache) { if (hadCache) {
// Hide loading spinner since cache is shown
document.getElementById('loading').classList.add('hidden'); document.getElementById('loading').classList.add('hidden');
if (!isCacheFresh()) {
console.log('Cache stale or incomplete refreshing from API');
loadMenuDataFromAPI();
} else {
console.log('Cache fresh & complete skipping API refresh');
}
} else {
loadMenuDataFromAPI();
} }
loadMenuDataFromAPI();
// Auto-start polling if already logged in // Auto-start polling if already logged in
if (authToken) { if (authToken) {

View File

@@ -107,7 +107,8 @@ try {
[/function\s+fetchVersions/, 'fetchVersions function'], [/function\s+fetchVersions/, 'fetchVersions function'],
[/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']
]; ];
for (const [regex, label] of checks) { for (const [regex, label] of checks) {

View File

@@ -1 +1 @@
v1.3.0 v1.3.2