Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08ee2a2d0f | ||
|
|
314728f6d0 | ||
|
|
ea4e0d151f | ||
|
|
b928b90728 | ||
|
|
9b1f0e2fd3 | ||
|
|
05bc06660c | ||
|
|
20957c3582 | ||
| fe86e68aca | |||
| 4dbd6c930f |
@@ -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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
117
REQUIREMENTS.md
117
REQUIREMENTS.md
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
## 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
|
||||||
|
|||||||
2
dist/bookmarklet-payload.js
vendored
2
dist/bookmarklet-payload.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bookmarklet.txt
vendored
2
dist/bookmarklet.txt
vendored
File diff suppressed because one or more lines are too long
16
dist/install.html
vendored
16
dist/install.html
vendored
File diff suppressed because one or more lines are too long
71
dist/kantine-standalone.html
vendored
71
dist/kantine-standalone.html
vendored
@@ -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.1</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.1</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 ===
|
||||||
@@ -3243,7 +3292,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.1';
|
||||||
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
|
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -3284,7 +3333,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.1';
|
||||||
|
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
@@ -3487,13 +3536,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();
|
loadMenuDataFromAPI();
|
||||||
|
} else {
|
||||||
|
console.log('Cache fresh & complete – skipping API refresh');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadMenuDataFromAPI();
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-start polling if already logged in
|
// Auto-start polling if already logged in
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
|
|||||||
63
kantine.js
63
kantine.js
@@ -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 ===
|
||||||
@@ -1751,13 +1800,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();
|
loadMenuDataFromAPI();
|
||||||
|
} else {
|
||||||
|
console.log('Cache fresh & complete – skipping API refresh');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadMenuDataFromAPI();
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-start polling if already logged in
|
// Auto-start polling if already logged in
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v1.3.0
|
v1.3.1
|
||||||
|
|||||||
Reference in New Issue
Block a user