Compare commits

..

38 Commits

Author SHA1 Message Date
Kantine Wrapper
f71bcf1ac7 chore: update build artifacts for v1.5.0 2026-02-26 10:39:49 +01:00
Kantine Wrapper
b041e9f318 chore: bump version to 1.5.0 and rollup changelog 2026-02-26 10:39:49 +01:00
Kantine Wrapper
984a897f73 chore: update build artifacts for v1.4.31 2026-02-26 10:35:28 +01:00
Kantine Wrapper
ae54d97d96 fix: target localStorage cleanup to kantine_ prefix to prevent host session loss (v1.4.31) 2026-02-26 10:35:28 +01:00
Kantine Wrapper
6ed0831f5d chore: update build artifacts for v1.4.30 2026-02-26 10:18:50 +01:00
Kantine Wrapper
ba75544f68 fix: session loss and order alarm rendering across unauthenticated sessions (v1.4.30) 2026-02-26 10:18:50 +01:00
Kantine Wrapper
7fdf7f6f3e chore: update build artifacts for v1.4.29 2026-02-26 10:01:39 +01:00
Kantine Wrapper
614f498d11 fix: defer favicon injection with setTimeout for htmlpreview compat (v1.4.29) 2026-02-26 10:01:17 +01:00
Kantine Wrapper
5f30696315 chore: update build artifacts for v1.4.28 2026-02-26 09:57:46 +01:00
Kantine Wrapper
cb5aa28f94 feat: custom favicon from user design, resized to 32x32 (v1.4.28) 2026-02-26 09:57:11 +01:00
Kantine Wrapper
bc1a91b7d7 chore: update build artifacts for v1.4.27 2026-02-26 09:50:46 +01:00
Kantine Wrapper
7d5beedfbb feat: build-time favicon injection from favicon.png via placeholder (v1.4.27) 2026-02-26 09:50:17 +01:00
Kantine Wrapper
0651d517b2 chore: update build artifacts for v1.4.26 2026-02-26 09:15:15 +01:00
Kantine Wrapper
c5e236e095 feat: favicon switched to PNG file served via raw GitHub URL (v1.4.26) 2026-02-26 09:15:10 +01:00
Kantine Wrapper
a5bff19796 chore: update build artifacts for v1.4.25 2026-02-26 09:03:12 +01:00
Kantine Wrapper
284f3d9a32 fix: dynamic favicon injection + push main to GitHub (v1.4.25) 2026-02-26 09:03:07 +01:00
Kantine Wrapper
7ce82ce82e chore: update build artifacts for v1.4.24 2026-02-26 08:58:38 +01:00
Kantine Wrapper
ce12684193 fix: push main branch to GitHub in release script 2026-02-26 08:58:25 +01:00
Kantine Wrapper
6cee38e99f chore: update build artifacts for v1.4.24 2026-02-26 08:35:15 +01:00
Kantine Wrapper
88758427fd fix: favicon base64 encoding for Chrome/Windows compatibility (v1.4.24) 2026-02-26 08:35:09 +01:00
Kantine Wrapper
23ed867ac4 chore: update build artifacts for v1.4.23 2026-02-26 08:25:14 +01:00
Kantine Wrapper
f8b1334a9a fix: inject favicon into page when bookmarklet runs (v1.4.23) 2026-02-26 08:25:07 +01:00
Kantine Wrapper
6b1bd46210 chore: update build artifacts for v1.4.22 2026-02-26 08:19:40 +01:00
Kantine Wrapper
a429148324 docs: full documentation audit - README + REQUIREMENTS (v1.4.22) 2026-02-26 08:19:34 +01:00
Kantine Wrapper
5caaf7dcad chore: update build artifacts for v1.4.21 2026-02-25 15:00:04 +01:00
Kantine Wrapper
122c1078cf feat: dynamic glow on next-week button until first order (v1.4.21) 2026-02-25 14:59:59 +01:00
Kantine Wrapper
ff48befb8a chore: update build artifacts for v1.4.20 2026-02-25 14:57:50 +01:00
Kantine Wrapper
9391dfd8d7 fix: update next-week badge counter after order/cancel (v1.4.20) 2026-02-25 14:57:44 +01:00
Kantine Wrapper
467e48e1da chore: update build artifacts for v1.4.19 2026-02-24 21:20:34 +01:00
Kantine Wrapper
566410eea5 feat: custom favicon for bookmarklet (triangle + fork & knife) v1.4.19 2026-02-24 21:20:30 +01:00
Kantine Wrapper
37afc2957b chore: update build artifacts for v1.4.18 2026-02-24 20:50:20 +01:00
Kantine Wrapper
b1763135aa test(ui): massively expand DOM testing suite to cover all Modals and Actions (v1.4.18) 2026-02-24 20:50:19 +01:00
Kantine Wrapper
98020f0b8f chore: update build artifacts for v1.4.17 2026-02-24 20:40:55 +01:00
Kantine Wrapper
c2e3282131 fix(ui): restore highlight modal toggle event & add dom test suite (v1.4.17) 2026-02-24 20:40:54 +01:00
Kantine Wrapper
7a82cb06db chore: update build artifacts for v1.4.16 2026-02-24 15:47:13 +01:00
Kantine Wrapper
d89b080da5 feat: add clear local cache button to version menu (v1.4.16) 2026-02-24 15:47:13 +01:00
Kantine Wrapper
d80863a169 chore: update build artifacts for v1.4.15 2026-02-24 15:38:44 +01:00
Kantine Wrapper
ae79c58d30 fix(flags): properly remove expired flags from localstorage (v1.4.15) 2026-02-24 15:38:44 +01:00
18 changed files with 536 additions and 211 deletions

View File

@@ -48,6 +48,11 @@ trigger: always_on
- **Browser**: Allowed for documentation and safe browsing. No automated logins without permission.
- **Terminal**: No `rm -rf`. Run tests (`pytest` etc.) after logic changes.
## 7. Mandatory Testing Policy 🧪
**CRITICAL: No logic or UI fix is complete without a corresponding automated test.**
- If you fix a regression or implement a new UI feature, you **MUST** write or update a test in `tests/test_dom.js` or `test_logic.js`.
- Refactoring MUST include verifying that no click listeners drop out. This guarantees that features like modal toggles stay functional.
## 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?

View File

@@ -8,9 +8,14 @@ Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses
* **Bestell-Countdown:** ⏳ Roter Alarm 1h vor Bestellschluss.
* **Smart Highlights:** 🌟 Markiere deine Favoriten (z.B. "Schnitzel", "Vegetarisch").
* **Bestellstatus:** Farbige Indikatoren für bestellte Menüs.
* **Kostenkontrolle:** Summiert automatisch den Gesamtpreis der Woche.
* **Session Reuse:** Nutzt automatisch eine bestehende Login-Session (Loggt dich automatisch ein).
* **Menu Badges:** Zeigt Menü-Codes (M1, M2+) direkt im Header.
* **Kostenkontrolle:** 💰 Summiert automatisch den Gesamtpreis der Woche.
* **Bestellhistorie:** 📜 Gruppiert nach Monat & KW mit inkrementellem Delta-Cache.
* **Session Reuse:** 🔑 Nutzt automatisch eine bestehende Login-Session.
* **Menu Badges:** 🏷️ Zeigt Menü-Codes (M1, M2+) direkt im Header.
* **Menü-Flagging:** 🔔 Ausverkaufte Menüs beobachten und bei Verfügbarkeit benachrichtigt werden.
* **Version-Menü:** 📦 Versionsliste mit Installer-Links, Dev-Mode Toggle und Downgrade-Support.
* **Cache leeren:** 🗑️ Lokalen Cache mit einem Klick bereinigen (im Version-Menü).
* **Favicon:** 🍽️ Eigenes Icon für die Lesezeichenleiste.
* **Changelog:** Übersicht über neue Funktionen direkt im Installer.
## 📦 Installation
@@ -19,7 +24,7 @@ Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses
2. Ziehe den blauen Button **"Kantine Wrapper"** in deine Lesezeichen-Leiste.
3. Fertig!
## usage
## 🍽️ Nutzung
1. Navigiere zu [https://web.bessa.app/knapp-kantine](https://web.bessa.app/knapp-kantine).
2. Klicke auf das **"Kantine Wrapper"** Lesezeichen.
@@ -28,24 +33,38 @@ Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses
## 🛠️ Entwicklung
### Voraussetzungen
* Node.js (optional, nur für Build-Scripts)
* Node.js (für Build- und Test-Scripts)
* Python 3 (für Build-Tests)
* Bash (für `build-bookmarklet.sh`)
### Projektstruktur
#### Quelldateien
* `kantine.js`: Der Haupt-Quellcode des Bookmarklets (UI, API-Logik, Rendering).
* `style.css`: Das komplette Design (CSS mit Light/Dark Mode).
* `mock-data.js`: Mock-Fetch-Interceptor mit realistischen Dummy-Menüdaten für Standalone-Tests.
* `build-bookmarklet.sh`: Build-Skript erzeugt alle `dist/`-Artefakte.
* `test_build.py`: Automatische Build-Tests, laufen am Ende jedes Builds.
| Datei | Beschreibung |
|-------|-------------|
| `kantine.js` | Haupt-Quellcode des Bookmarklets (UI, API-Logik, Rendering). |
| `style.css` | Komplettes Design (CSS mit Light/Dark Mode). |
| `favicon.svg` | Favicon für die Installer-Seite (Dreieck + Gabel & Messer). |
| `mock-data.js` | Mock-Fetch-Interceptor mit realistischen Dummy-Menüdaten für Standalone-Tests. |
| `build-bookmarklet.sh` | Build-Skript erzeugt alle `dist/`-Artefakte und führt alle Tests aus. |
| `release.sh` | Release-Skript Commit, Tag, Push zu allen Remotes. |
| `version.txt` | Aktuelle Versionsnummer (SemVer). |
| `changelog.md` | Änderungshistorie aller Versionen. |
| `REQUIREMENTS.md` | System Requirements Specification (SRS). |
#### Tests
| Datei | Beschreibung |
|-------|-------------|
| `test_logic.js` | Logik-Unit-Tests (statische Analyse, Syntax-Check, Sandbox-Ausführung). |
| `tests/test_dom.js` | DOM-Interaktionstests via JSDOM (prüft Event-Listener-Bindung aller UI-Komponenten). |
| `test_build.py` | Build-Artefakt-Validierung (Existenz, Inhalt). |
#### `dist/` Build-Artefakte
| Datei | Beschreibung |
|-------|-------------|
| `bookmarklet.txt` | Die rohe Bookmarklet-URL (`javascript:...`). Enthält CSS + JS als selbstextrahierendes IIFE. Kann direkt als Lesezeichen-URL eingefügt werden. |
| `bookmarklet-payload.js` | Der entpackte Bookmarklet-Payload (JS). Erstellt `<style>` + `<script>` Elemente und injiziert sie in die Seite. Nützlich zum Debuggen. |
| `install.html` | Installer-Seite mit Drag & Drop Button, Anleitung, Feature-Liste und Changelog. Kann lokal oder gehostet geöffnet werden. |
| `install.html` | Installer-Seite mit Drag & Drop Button, Favicon, Anleitung, Feature-Liste und Changelog. Kann lokal oder gehostet geöffnet werden. |
| `kantine-standalone.html` | Eigenständige HTML-Datei mit eingebettetem CSS + JS + **Mock-Daten**. Lädt automatisch Dummy-Menüs für UI-Tests ohne API-Zugriff. |
### Build
@@ -55,5 +74,15 @@ Um Änderungen an `kantine.js` oder `style.css` wirksam zu machen, führe den Bu
./build-bookmarklet.sh
```
### Release
Erstellt einen Git-Tag, committet Build-Artefakte und pusht zu allen Remotes:
```bash
./release.sh
```
## ⚠️ Hinweis
Dieses Projekt enthält zum überwiegenden Teil **KI-generierten Code**. Der Code wurde mithilfe von KI-Assistenten erstellt, überprüft und iterativ verfeinert.
## 📝 Lizenz
Internes Tool.

View File

@@ -60,10 +60,10 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
| **Header UI & Navigation** | | | |
| FR-090 | Die Hauptnavigation (Wochen-Toggles) muss linksbündig neben dem App-Titel positioniert sein. | Niedrig | v1.5.0 |
| FR-091 | Ein dynamisches Alarm-Icon im Header muss den Überwachungsstatus geflaggter Menüs anzeigen (Gelb=Überwachung aktiv 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 | Solange Menüdaten für die Nächste Woche verfügbar sind, aber noch keine Bestellungen getätigt wurden, muss der entsprechende Navigation-Button animiert und farblich (Gelb) hervorgehoben werden. Nach der ersten Bestellung muss die Hervorhebung automatisch erlöschen. Zusätzlich muss beim erstmaligen Erscheinen der Daten ein einmaliger Toast-Hinweis angezeigt werden. | Mittel | v1.6.0 (Update v1.4.21) |
| **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 |
| FR-095 | Alle benutzerrelevanten Aktionen (Bestellung, Stornierung, Fehler) müssen durch nicht-blockierende Benachrichtigungen (Toasts) bestätigt werden. | Mittel | v1.0.1 |
| FR-096 | 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** | | | |
@@ -73,6 +73,8 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
| FR-113 | Es muss möglich sein, zu einer älteren Version zurückzukehren (Downgrade). | Niedrig | v1.3.0 |
| FR-114 | Ein Dev-Mode muss es ermöglichen, zwischen stabilen Releases und Entwicklungs-Tags umzuschalten. | Niedrig | v1.3.0 |
| FR-115 | Das Versionsmenü muss Links zur Erstellung von Feature-Requests und Bug-Reports auf GitHub enthalten. | Niedrig | v1.4.4 |
| FR-116 | Das Versionsmenü muss eine Funktion zum Leeren des lokalen Caches bereitstellen, um bei hartnäckigen Fehlern alle gespeicherten Daten bereinigen zu können. | Niedrig | v1.4.16 |
| FR-117 | Die Installer-Seite muss ein eingebettetes Favicon bereitstellen, das beim Drag & Drop in die Lesezeichenleiste als Icon für das Bookmarklet übernommen wird. | Niedrig | v1.4.19 |
## 3. Nicht-funktionale Anforderungen
@@ -85,7 +87,7 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
| **Benutzbarkeit** | NFR-005 | Die Oberfläche muss auf mobilen Geräten fehlerfrei nutzbar sein. | Viewports ab 320px Breite |
| **Benutzbarkeit** | NFR-006 | Alle interaktiven Elemente müssen Tooltips oder Hilfetexte bieten. | 100% Coverage |
| **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 |
| **Wartbarkeit** | NFR-008 | Die Build-Artefakte müssen durch automatisierte Tests validiert werden. | Build-Tests + Logik-Tests + DOM-Tests |
## 4. Technische Randbedingungen
* **Deployment**: Das System wird als Bookmarklet ausgeliefert, das auf der Bessa-Webseite ausgeführt wird.
@@ -93,4 +95,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).
* **Build**: Bash-basiertes Build-Script, das Bookmarklet-URL, Standalone-HTML und Installer-Seite generiert.
* **Versionierung**: SemVer, verwaltet über GitHub Releases/Tags.
* **Tests**: Python-basierte Build-Tests (`python3`) + Node.js-basierte Logik-Tests.
* **Tests**: Python-basierte Build-Tests (`python3`) + Node.js-basierte Logik-Tests + Node.js-basierte DOM-Interaktionstests (JSDOM).

View File

@@ -7,6 +7,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DIST_DIR="$SCRIPT_DIR/dist"
CSS_FILE="$SCRIPT_DIR/style.css"
JS_FILE="$SCRIPT_DIR/kantine.js"
FAVICON_FILE="$SCRIPT_DIR/favicon.png"
# === VERSION ===
if [ -f "$SCRIPT_DIR/version.txt" ]; then
@@ -23,11 +24,16 @@ echo "=== Kantine Bookmarklet Builder ($VERSION) ==="
# Check files exist
if [ ! -f "$CSS_FILE" ]; then echo "ERROR: $CSS_FILE not found"; exit 1; fi
if [ ! -f "$JS_FILE" ]; then echo "ERROR: $JS_FILE not found"; exit 1; fi
if [ ! -f "$FAVICON_FILE" ]; then echo "ERROR: $FAVICON_FILE not found"; exit 1; fi
# Generate favicon Base64 data URI from PNG
FAVICON_B64=$(base64 -w0 "$FAVICON_FILE")
FAVICON_URL="data:image/png;base64,${FAVICON_B64}"
CSS_CONTENT=$(cat "$CSS_FILE")
# Inject version into JS
JS_CONTENT=$(cat "$JS_FILE" | sed "s|{{VERSION}}|$VERSION|g")
# Inject version and favicon into JS
JS_CONTENT=$(cat "$JS_FILE" | sed "s|{{VERSION}}|$VERSION|g" | sed "s|{{FAVICON_DATA_URI}}|$FAVICON_URL|g")
# === 1. Build standalone HTML (for local testing/dev) ===
cat > "$DIST_DIR/kantine-standalone.html" << HTMLEOF
@@ -101,6 +107,7 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
<head>
<meta charset="UTF-8">
<title>Kantine Wrapper Installer ($VERSION)</title>
<link rel="icon" type="image/png" href="$FAVICON_URL">
<style>
body { font-family: 'Inter', sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
h1 { color: #029AA8; } /* Knapp Teal */
@@ -200,9 +207,9 @@ echo "document.getElementById('bookmarklet-link').href = " >> "$DIST_DIR/install
echo "$JS_CONTENT" | python3 -c "
import sys, json, urllib.parse
# 1. Read JS and Replace VERSION
# 1. Read JS and Replace VERSION + Favicon
js_template = sys.stdin.read()
js = js_template.replace('{{VERSION}}', '$VERSION')
js = js_template.replace('{{VERSION}}', '$VERSION').replace('{{FAVICON_DATA_URI}}', '$FAVICON_URL')
# 2. Prepare CSS for injection via createElement('style')
css = open('$CSS_FILE').read().replace('\n', ' ').replace(' ', ' ')
@@ -236,6 +243,16 @@ $CHANGELOG_HTML
EOF
cat >> "$DIST_DIR/install.html" << INSTALLEOF
// Dynamic favicon injection — setTimeout ensures it runs AFTER
// htmlpreview.github.io's document.write() processing completes
setTimeout(function() {
document.querySelectorAll('link[rel*="icon"]').forEach(function(el) { el.remove(); });
var fi = document.createElement('link');
fi.rel = 'icon';
fi.type = 'image/png';
fi.href = '$FAVICON_URL';
document.head.appendChild(fi);
}, 0);
document.getElementById('bookmarklet-link').textContent = 'Kantine $VERSION';
</script>
</body>
@@ -258,6 +275,14 @@ if [ $LOGIC_EXIT -ne 0 ]; then
exit 1
fi
echo "=== Running DOM Interaction Tests ==="
node "$SCRIPT_DIR/tests/test_dom.js"
DOM_EXIT=$?
if [ $DOM_EXIT -ne 0 ]; then
echo "❌ DOM UI tests FAILED! Regressions detected."
exit 1
fi
echo "=== Running Build Tests ==="
python3 "$SCRIPT_DIR/test_build.py"
TEST_EXIT=$?

View File

@@ -1,48 +1,16 @@
## v1.4.14
- 🐛 **Bugfix**: Alarmglocke versteckt sich jetzt zuverlässig auch auf Endgeräten mit CSS Konflikten
- 🚀 **Feature**: Sofortige API-Aktualisierung (Refresh) bei Aktivierung eines Menüalarms
-**Optimierung**: "Unbekannt" im letzten Refresh-Zeitpunkt wird abgefangen und zeigt initial "gerade eben"
## v1.5.0 (2026-02-26)
Das große "Quality of Life"-Update! Zusammenfassung aller Features und Fixes seit v1.4.0:
## v1.4.13 (2026-02-24)
- **Fix**: Die Farben der Glocke funktionieren nun verlässlich, da CSS-Variablen durch direkte Hex-Codes ersetzt wurden.
-**Bestellhistorie**: Übersichtliche Historie direkt in der App gruppiert nach Jahr/Monat, inklusive Summen, Stati (Offen/Abgeschlossen/Storniert) und Delta-Cache für rasantes Laden.
- **Smart Cache & Performance**: Massive Reduzierung von API-Calls und Ladezeiten durch intelligenten lokalen Cache. Das Bookmarklet startet nun praktisch verzögerungsfrei.
- 🔄 **GitHub Release Management**: In-App Versions-Menü mit Auto-Update Check (`🆕` Icon). Umschalten zwischen "Stable" und "Dev" Versionen sowie Downgrade-Möglichkeit direkt über die GitHub API.
- 🌟 **Smart Highlights & UX**: Speisen-Favoriten leuchten nun im Design-Violett und erhalten Feature-Badges. Der Bestell-Badge für nächste Woche filtert nun intelligent personalisierte Highlights voraus.
- 🔔 **Bestell-Warnung & Notifications**: Der System-Alarm berücksichtigt nun Sessions korrekt, zeigt dynamische Farbwechsel (gelb/grün/rot) und warnt verlässlich vor dem Bestellschluss (10:00 Uhr). Altlasten von Vortagen werden automatisch geputzt.
- 🎨 **Eigenes Favicon**: Das Bookmarklet und der Installer haben nun ein eigenes Icon (Dreieck mit Besteck), das beim Hineinziehen in die Lesezeichenleiste übernommen wird (dynamisch generiert als lokales PNG).
- 🧹 **Lokaler Cache-Clear**: Ein in das Versions-Menü eingebauter "Papierkorb", der ausschließlich fehlerhafte Kantinen-Caches putzt, ohne dabei versehentlich die aktive Bessa-Host-Session zu zerstören.
- 🔒 **Sitzungs-Persistenz**: Die Login-Session überdauert jetzt neue Tabs, Fenster und Version-Upgrades reibungslos durch den Wechsel auf `localStorage`.
- 🛡️ **Testing & Stabilität**: Vollautomatische DOM- und Logik-Testing-Suites in der Release-Pipeline integriert. Fehlerhafte UI-Buttons gehören der Vergangenheit an.
## 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)
- **Feature**: Das Versionsmenü enthält nun direkte Links zu GitHub, um Fehler zu melden oder neue Features vorzuschlagen.
## v1.4.3 (2026-02-24)
- **Fix**: Der Rahmen des "Heute Bestellt" Menüs ist nun konsequent violett (passend zum Glow-Effekt).
## v1.4.2 (2026-02-23)
- **Fix**: Das "Heute Bestellt" Menü leuchtet nun stimmig im Design-Violett statt Blau.
- **Fix**: Abfangen des GitHub API Rate Limit (403) im Versionsdialog mit einer freundlicheren Fehlermeldung, da der User-Agent im Browser nicht manuell gesetzt werden darf.
## v1.4.1 (2026-02-22)
- **UX Verbesserungen**: Bestellhistorie gruppiert nach Jahren und Monaten mittels einklappbarem Akkordeon. Monatssummen integriert und Stati farblich abgehoben (Offen, Abgeschlossen, Storniert).
## v1.4.0 (2026-02-22)
- **Feature**: Bestellhistorie per Knopfdruck abrufbar. Übersichtliche Darstellung, gruppiert nach Monaten und Kalenderwochen, inklusive Stornos. 📜✨

15
debug_test.js Executable file
View File

@@ -0,0 +1,15 @@
const fs = require('fs');
const jsCode = fs.readFileSync('kantine.js', 'utf8').replace('(function () {', '').replace(/}\)\(\);$/, '');
try {
const vm = require('vm');
new vm.Script(jsCode);
} catch (e) {
console.error(e.message);
const lines = jsCode.split('\n');
console.error("Around line", e.loc?.line);
if(e.loc?.line) {
console.log(lines[e.loc.line - 2]);
console.log(lines[e.loc.line - 1]);
console.log(lines[e.loc.line]);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

93
dist/install.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -1979,8 +1979,8 @@ body {
let currentWeekNumber = getISOWeek(new Date());
let currentYear = new Date().getFullYear();
let displayMode = 'this-week';
let authToken = sessionStorage.getItem('kantine_authToken');
let currentUser = sessionStorage.getItem('kantine_currentUser');
let authToken = localStorage.getItem('kantine_authToken');
let currentUser = localStorage.getItem('kantine_currentUser');
let orderMap = new Map();
let userFlags = new Set(JSON.parse(localStorage.getItem('kantine_flags') || '[]'));
let pollIntervalId = null;
@@ -2000,6 +2000,16 @@ body {
// Replace entire page content
document.title = 'Kantine Weekly Menu';
// Inject custom favicon (triangle + fork & knife PNG)
if (document.querySelectorAll) {
document.querySelectorAll('link[rel*="icon"]').forEach(el => el.remove());
}
const favicon = document.createElement('link');
favicon.rel = 'icon';
favicon.type = 'image/png';
favicon.href = 'data:image/png;base64,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';
document.head.appendChild(favicon);
// Inject Google Fonts if not already present
if (!document.querySelector('link[href*="fonts.googleapis.com/css2?family=Inter"]')) {
const fontLink = document.createElement('link');
@@ -2021,7 +2031,7 @@ body {
<div class="brand">
<span class="material-icons-round logo-icon">restaurant_menu</span>
<div class="header-left">
<h1>Kantinen Übersicht <small class="version-tag" style="font-size: 0.6em; opacity: 0.7; font-weight: 400; cursor: pointer;" title="Klick für Versionsmenü">v1.4.14</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.5.0</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div>
</div>
<div class="nav-group" style="margin-left: 1rem;">
@@ -2163,7 +2173,7 @@ body {
</div>
<div class="modal-body">
<div style="margin-bottom: 1rem;">
<strong>Aktuell:</strong> <span id="version-current">v1.4.14</span>
<strong>Aktuell:</strong> <span id="version-current">v1.5.0</span>
</div>
<div class="dev-toggle">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
@@ -2181,6 +2191,9 @@ body {
<a href="https://github.com/TauNeutrino/kantine-overview/discussions/categories/ideas" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Schlage ein neues Feature auf GitHub vor">
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
</a>
<button id="btn-clear-cache" style="background: none; border: none; padding: 0; color: var(--error-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; text-align: left; font-size: inherit; font-family: inherit;" title="Löscht alle lokalen Daten & erzwingt einen Neuladen">
<span class="material-icons-round" style="font-size: 1.2em;">delete_forever</span> Lokalen Cache leeren
</button>
</div>
</div>
</div>
@@ -2228,6 +2241,18 @@ body {
const historyModal = document.getElementById('history-modal');
const btnHistoryClose = document.getElementById('btn-history-close');
if (btnHighlights) {
btnHighlights.addEventListener('click', () => {
highlightsModal.classList.remove('hidden');
});
}
if (btnHighlightsClose) {
btnHighlightsClose.addEventListener('click', () => {
highlightsModal.classList.add('hidden');
});
}
btnHistory.addEventListener('click', () => {
if (!authToken) {
loginModal.classList.remove('hidden');
@@ -2265,6 +2290,21 @@ body {
});
}
const btnClearCache = document.getElementById('btn-clear-cache');
if (btnClearCache) {
btnClearCache.addEventListener('click', () => {
if (confirm('Möchtest du wirklich alle lokalen Daten (inkl. Login-Session, Cache und Einstellungen) löschen? Die Seite wird danach neu geladen.')) {
// Only clear our own keys so we don't destroy the host app's (Bessa's) session
Object.keys(localStorage).forEach(key => {
if (key.startsWith('kantine_')) {
localStorage.removeItem(key);
}
});
window.location.reload();
}
});
}
window.addEventListener('click', (e) => {
if (e.target === versionModal) versionModal.classList.add('hidden');
});
@@ -2373,8 +2413,8 @@ body {
if (response.ok) {
authToken = data.key;
currentUser = employeeId;
sessionStorage.setItem('kantine_authToken', data.key);
sessionStorage.setItem('kantine_currentUser', employeeId);
localStorage.setItem('kantine_authToken', data.key);
localStorage.setItem('kantine_currentUser', employeeId);
// Fetch user name
try {
@@ -2383,8 +2423,8 @@ body {
});
if (userResp.ok) {
const userData = await userResp.json();
if (userData.first_name) sessionStorage.setItem('kantine_firstName', userData.first_name);
if (userData.last_name) sessionStorage.setItem('kantine_lastName', userData.last_name);
if (userData.first_name) localStorage.setItem('kantine_firstName', userData.first_name);
if (userData.last_name) localStorage.setItem('kantine_lastName', userData.last_name);
}
} catch (err) {
console.error('Failed to fetch user info:', err);
@@ -2414,10 +2454,10 @@ body {
// Logout
btnLogout.addEventListener('click', () => {
sessionStorage.removeItem('kantine_authToken');
sessionStorage.removeItem('kantine_currentUser');
sessionStorage.removeItem('kantine_firstName');
sessionStorage.removeItem('kantine_lastName');
localStorage.removeItem('kantine_authToken');
localStorage.removeItem('kantine_currentUser');
localStorage.removeItem('kantine_firstName');
localStorage.removeItem('kantine_lastName');
authToken = null;
currentUser = null;
orderMap = new Map();
@@ -2438,13 +2478,13 @@ body {
if (parsed.auth && parsed.auth.token) {
console.log('Found existing Bessa session!');
authToken = parsed.auth.token;
sessionStorage.setItem('kantine_authToken', authToken);
localStorage.setItem('kantine_authToken', authToken);
if (parsed.auth.user) {
currentUser = parsed.auth.user.id || 'unknown';
sessionStorage.setItem('kantine_currentUser', currentUser);
if (parsed.auth.user.firstName) sessionStorage.setItem('kantine_firstName', parsed.auth.user.firstName);
if (parsed.auth.user.lastName) sessionStorage.setItem('kantine_lastName', parsed.auth.user.lastName);
localStorage.setItem('kantine_currentUser', currentUser);
if (parsed.auth.user.firstName) localStorage.setItem('kantine_firstName', parsed.auth.user.firstName);
if (parsed.auth.user.lastName) localStorage.setItem('kantine_lastName', parsed.auth.user.lastName);
}
}
}
@@ -2453,9 +2493,9 @@ body {
}
}
authToken = sessionStorage.getItem('kantine_authToken');
currentUser = sessionStorage.getItem('kantine_currentUser');
const firstName = sessionStorage.getItem('kantine_firstName');
authToken = localStorage.getItem('kantine_authToken');
currentUser = localStorage.getItem('kantine_currentUser');
const firstName = localStorage.getItem('kantine_firstName');
const btnLoginOpen = document.getElementById('btn-login-open');
const userInfo = document.getElementById('user-info');
const userIdDisplay = document.getElementById('user-id-display');
@@ -2505,6 +2545,7 @@ body {
}
console.log(`Fetched ${results.length} orders, mapped active ones.`);
renderVisibleWeeks();
updateNextWeekBadge();
}
} catch (error) {
console.error('Error fetching orders:', error);
@@ -3034,12 +3075,27 @@ body {
// FR-019: Auto-remove flags whose cutoff has passed
function cleanupExpiredFlags() {
const now = new Date();
const todayStr = now.toISOString().split('T')[0]; // Format: YYYY-MM-DD
let changed = false;
for (const flagId of [...userFlags]) {
const [date] = flagId.split('_');
const cutoff = new Date(date);
const [dateStr] = flagId.split('_'); // Format usually is YYYY-MM-DD
// If the flag's date string is entirely in the past (before today)
// or if it's today but past the 10:00 cutoff time
let isExpired = false;
if (dateStr < todayStr) {
isExpired = true;
} else if (dateStr === todayStr) {
const cutoff = new Date(dateStr);
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00
if (now >= cutoff) {
isExpired = true;
}
}
if (isExpired) {
userFlags.delete(flagId);
changed = true;
}
@@ -3555,13 +3611,18 @@ body {
badge.classList.add('has-highlights');
}
// FR-092: Highlight Next Week Button when new data arrives
// FR-092: Glow Next Week button while data exists but no orders placed
if (daysWithOrders === 0) {
btnNextWeek.classList.add('new-week-available');
// One-time toast notification when new data first arrives
const storageKey = `kantine_notified_nextweek_${nextYear}_${nextWeek}`;
if (!localStorage.getItem(storageKey)) {
localStorage.setItem(storageKey, 'true');
btnNextWeek.classList.add('new-week-available');
showToast('Neue Menüdaten für nächste Woche verfügbar!', 'info');
}
} else {
btnNextWeek.classList.remove('new-week-available');
}
} else if (badge) {
badge.remove();
@@ -3969,7 +4030,7 @@ body {
// Periodic update check (runs on init + every hour)
async function checkForUpdates() {
const currentVersion = 'v1.4.14';
const currentVersion = 'v1.5.0';
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
try {
@@ -4010,7 +4071,7 @@ body {
const modal = document.getElementById('version-modal');
const container = document.getElementById('version-list-container');
const devToggle = document.getElementById('dev-mode-toggle');
const currentVersion = 'v1.4.14';
const currentVersion = 'v1.5.0';
if (!modal) return;
modal.classList.remove('hidden');
@@ -4107,6 +4168,12 @@ body {
// === Order Countdown ===
function updateCountdown() {
// Only show order alarms for logged-in users
if (!authToken || !currentUser) {
removeCountdown();
return;
}
const now = new Date();
const currentDay = now.getDay();
// Skip weekends (0=Sun, 6=Sat)
@@ -4171,7 +4238,7 @@ body {
// Notification logic (One time)
const notifiedKey = `kantine_notified_${todayStr}`;
if (!sessionStorage.getItem(notifiedKey)) {
if (!localStorage.getItem(notifiedKey)) {
if (Notification.permission === 'granted') {
new Notification('Kantine: Bestellschluss naht!', {
body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!',
@@ -4180,7 +4247,7 @@ body {
} else if (Notification.permission === 'default') {
Notification.requestPermission();
}
sessionStorage.setItem(notifiedKey, 'true');
localStorage.setItem(notifiedKey, 'true');
}
} else {
countdownEl.classList.remove('urgent');

BIN
favicon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

29
favicon.svg Executable file
View File

@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<!-- Fork (left, with gap to triangle) -->
<g transform="translate(2, 10)">
<!-- Tines -->
<rect x="1" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
<rect x="4.6" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
<rect x="8.2" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
<!-- Connector -->
<rect x="1" y="14" width="9" height="3.5" rx="1.5" fill="#333"/>
<!-- Handle -->
<rect x="3.5" y="16.5" width="4" height="24" rx="2" fill="#333"/>
</g>
<!-- Triangle (center, equilateral aspect ratio ~1:0.866) -->
<!-- Equilateral: base=28, height=24.25 => keeps proper ratio -->
<polygon points="32,8 47,48 17,48" fill="none" stroke="#333" stroke-width="4" stroke-linejoin="round"/>
<!-- Knife (right, with gap to triangle) -->
<g transform="translate(50, 10)">
<!-- Blade (slight curve) -->
<path d="M3,0 C3,0 3,0 3,0 L3,17 L10,14 C10,6 7,0 3,0 Z" fill="#333"/>
<!-- Spine -->
<rect x="1.5" y="0" width="2" height="18" rx="1" fill="#333"/>
<!-- Bolster -->
<rect x="1.5" y="16.5" width="8.5" height="3.5" rx="1.2" fill="#333"/>
<!-- Handle -->
<rect x="3.5" y="19" width="4" height="22" rx="2" fill="#333"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
favicon_base.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

View File

@@ -29,8 +29,8 @@
let currentWeekNumber = getISOWeek(new Date());
let currentYear = new Date().getFullYear();
let displayMode = 'this-week';
let authToken = sessionStorage.getItem('kantine_authToken');
let currentUser = sessionStorage.getItem('kantine_currentUser');
let authToken = localStorage.getItem('kantine_authToken');
let currentUser = localStorage.getItem('kantine_currentUser');
let orderMap = new Map();
let userFlags = new Set(JSON.parse(localStorage.getItem('kantine_flags') || '[]'));
let pollIntervalId = null;
@@ -50,6 +50,16 @@
// Replace entire page content
document.title = 'Kantine Weekly Menu';
// Inject custom favicon (triangle + fork & knife PNG)
if (document.querySelectorAll) {
document.querySelectorAll('link[rel*="icon"]').forEach(el => el.remove());
}
const favicon = document.createElement('link');
favicon.rel = 'icon';
favicon.type = 'image/png';
favicon.href = '{{FAVICON_DATA_URI}}';
document.head.appendChild(favicon);
// Inject Google Fonts if not already present
if (!document.querySelector('link[href*="fonts.googleapis.com/css2?family=Inter"]')) {
const fontLink = document.createElement('link');
@@ -231,6 +241,9 @@
<a href="https://github.com/TauNeutrino/kantine-overview/discussions/categories/ideas" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Schlage ein neues Feature auf GitHub vor">
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
</a>
<button id="btn-clear-cache" style="background: none; border: none; padding: 0; color: var(--error-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; text-align: left; font-size: inherit; font-family: inherit;" title="Löscht alle lokalen Daten & erzwingt einen Neuladen">
<span class="material-icons-round" style="font-size: 1.2em;">delete_forever</span> Lokalen Cache leeren
</button>
</div>
</div>
</div>
@@ -278,6 +291,18 @@
const historyModal = document.getElementById('history-modal');
const btnHistoryClose = document.getElementById('btn-history-close');
if (btnHighlights) {
btnHighlights.addEventListener('click', () => {
highlightsModal.classList.remove('hidden');
});
}
if (btnHighlightsClose) {
btnHighlightsClose.addEventListener('click', () => {
highlightsModal.classList.add('hidden');
});
}
btnHistory.addEventListener('click', () => {
if (!authToken) {
loginModal.classList.remove('hidden');
@@ -315,6 +340,21 @@
});
}
const btnClearCache = document.getElementById('btn-clear-cache');
if (btnClearCache) {
btnClearCache.addEventListener('click', () => {
if (confirm('Möchtest du wirklich alle lokalen Daten (inkl. Login-Session, Cache und Einstellungen) löschen? Die Seite wird danach neu geladen.')) {
// Only clear our own keys so we don't destroy the host app's (Bessa's) session
Object.keys(localStorage).forEach(key => {
if (key.startsWith('kantine_')) {
localStorage.removeItem(key);
}
});
window.location.reload();
}
});
}
window.addEventListener('click', (e) => {
if (e.target === versionModal) versionModal.classList.add('hidden');
});
@@ -423,8 +463,8 @@
if (response.ok) {
authToken = data.key;
currentUser = employeeId;
sessionStorage.setItem('kantine_authToken', data.key);
sessionStorage.setItem('kantine_currentUser', employeeId);
localStorage.setItem('kantine_authToken', data.key);
localStorage.setItem('kantine_currentUser', employeeId);
// Fetch user name
try {
@@ -433,8 +473,8 @@
});
if (userResp.ok) {
const userData = await userResp.json();
if (userData.first_name) sessionStorage.setItem('kantine_firstName', userData.first_name);
if (userData.last_name) sessionStorage.setItem('kantine_lastName', userData.last_name);
if (userData.first_name) localStorage.setItem('kantine_firstName', userData.first_name);
if (userData.last_name) localStorage.setItem('kantine_lastName', userData.last_name);
}
} catch (err) {
console.error('Failed to fetch user info:', err);
@@ -464,10 +504,10 @@
// Logout
btnLogout.addEventListener('click', () => {
sessionStorage.removeItem('kantine_authToken');
sessionStorage.removeItem('kantine_currentUser');
sessionStorage.removeItem('kantine_firstName');
sessionStorage.removeItem('kantine_lastName');
localStorage.removeItem('kantine_authToken');
localStorage.removeItem('kantine_currentUser');
localStorage.removeItem('kantine_firstName');
localStorage.removeItem('kantine_lastName');
authToken = null;
currentUser = null;
orderMap = new Map();
@@ -488,13 +528,13 @@
if (parsed.auth && parsed.auth.token) {
console.log('Found existing Bessa session!');
authToken = parsed.auth.token;
sessionStorage.setItem('kantine_authToken', authToken);
localStorage.setItem('kantine_authToken', authToken);
if (parsed.auth.user) {
currentUser = parsed.auth.user.id || 'unknown';
sessionStorage.setItem('kantine_currentUser', currentUser);
if (parsed.auth.user.firstName) sessionStorage.setItem('kantine_firstName', parsed.auth.user.firstName);
if (parsed.auth.user.lastName) sessionStorage.setItem('kantine_lastName', parsed.auth.user.lastName);
localStorage.setItem('kantine_currentUser', currentUser);
if (parsed.auth.user.firstName) localStorage.setItem('kantine_firstName', parsed.auth.user.firstName);
if (parsed.auth.user.lastName) localStorage.setItem('kantine_lastName', parsed.auth.user.lastName);
}
}
}
@@ -503,9 +543,9 @@
}
}
authToken = sessionStorage.getItem('kantine_authToken');
currentUser = sessionStorage.getItem('kantine_currentUser');
const firstName = sessionStorage.getItem('kantine_firstName');
authToken = localStorage.getItem('kantine_authToken');
currentUser = localStorage.getItem('kantine_currentUser');
const firstName = localStorage.getItem('kantine_firstName');
const btnLoginOpen = document.getElementById('btn-login-open');
const userInfo = document.getElementById('user-info');
const userIdDisplay = document.getElementById('user-id-display');
@@ -555,6 +595,7 @@
}
console.log(`Fetched ${results.length} orders, mapped active ones.`);
renderVisibleWeeks();
updateNextWeekBadge();
}
} catch (error) {
console.error('Error fetching orders:', error);
@@ -1084,12 +1125,27 @@
// FR-019: Auto-remove flags whose cutoff has passed
function cleanupExpiredFlags() {
const now = new Date();
const todayStr = now.toISOString().split('T')[0]; // Format: YYYY-MM-DD
let changed = false;
for (const flagId of [...userFlags]) {
const [date] = flagId.split('_');
const cutoff = new Date(date);
const [dateStr] = flagId.split('_'); // Format usually is YYYY-MM-DD
// If the flag's date string is entirely in the past (before today)
// or if it's today but past the 10:00 cutoff time
let isExpired = false;
if (dateStr < todayStr) {
isExpired = true;
} else if (dateStr === todayStr) {
const cutoff = new Date(dateStr);
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00
if (now >= cutoff) {
isExpired = true;
}
}
if (isExpired) {
userFlags.delete(flagId);
changed = true;
}
@@ -1605,13 +1661,18 @@
badge.classList.add('has-highlights');
}
// FR-092: Highlight Next Week Button when new data arrives
// FR-092: Glow Next Week button while data exists but no orders placed
if (daysWithOrders === 0) {
btnNextWeek.classList.add('new-week-available');
// One-time toast notification when new data first arrives
const storageKey = `kantine_notified_nextweek_${nextYear}_${nextWeek}`;
if (!localStorage.getItem(storageKey)) {
localStorage.setItem(storageKey, 'true');
btnNextWeek.classList.add('new-week-available');
showToast('Neue Menüdaten für nächste Woche verfügbar!', 'info');
}
} else {
btnNextWeek.classList.remove('new-week-available');
}
} else if (badge) {
badge.remove();
@@ -2157,6 +2218,12 @@
// === Order Countdown ===
function updateCountdown() {
// Only show order alarms for logged-in users
if (!authToken || !currentUser) {
removeCountdown();
return;
}
const now = new Date();
const currentDay = now.getDay();
// Skip weekends (0=Sun, 6=Sat)
@@ -2221,7 +2288,7 @@
// Notification logic (One time)
const notifiedKey = `kantine_notified_${todayStr}`;
if (!sessionStorage.getItem(notifiedKey)) {
if (!localStorage.getItem(notifiedKey)) {
if (Notification.permission === 'granted') {
new Notification('Kantine: Bestellschluss naht!', {
body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!',
@@ -2230,7 +2297,7 @@
} else if (Notification.permission === 'default') {
Notification.requestPermission();
}
sessionStorage.setItem(notifiedKey, 'true');
localStorage.setItem(notifiedKey, 'true');
}
} else {
countdownEl.classList.remove('urgent');

View File

@@ -49,8 +49,9 @@ echo "=== Pushing to remotes ==="
git push origin HEAD
git push origin --force tag "$VERSION"
# If a remote named 'github' exists, push tags there too
# If a remote named 'github' exists, push branch and tags there too
if git remote | grep -q "^github$"; then
git push github HEAD
git push github --force tag "$VERSION"
fi

17
syntax_check.js Executable file
View File

@@ -0,0 +1,17 @@
const fs = require('fs');
const jsCode = fs.readFileSync('kantine.js', 'utf8')
.replace('(function () {', '')
.replace('})();', '')
.replace('if (window.__KANTINE_LOADED) return;', '');
const testCode = `
console.log("TEST");
`;
const code = jsCode + '\n' + testCode;
try {
const vm = require('vm');
new vm.Script(code);
} catch (e) {
if(e.stack) {
console.log("Syntax error at:", e.stack.split('\n').slice(0,3).join('\n'));
}
}

View File

@@ -1,7 +1,12 @@
const fs = require('fs');
fs.writeFileSync('trace.log', '');
function log(m) { fs.appendFileSync('trace.log', m + '\n'); }
log("Initializing JSDOM...");
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
log("Reading html...");
const html = `
<!DOCTYPE html>
<html>
@@ -15,13 +20,66 @@ const html = `
<button id="alarm-bell" class="icon-btn hidden">
<span id="alarm-bell-icon" style="color:var(--text-secondary);"></span>
</button>
<!-- Mocks for Highlights Feature -->
<button id="btn-highlights">Highlights</button>
<div id="highlights-modal" class="modal hidden">
<button id="btn-highlights-close">Close</button>
<input id="tag-input" type="text" />
<button id="btn-add-tag">Add</button>
<ul id="tags-list"></ul>
</div>
<!-- Mocks for Login Modal -->
<button id="btn-login-open">Login</button>
<div id="login-modal" class="modal hidden">
<button id="btn-login-close">Close</button>
<form id="login-form"></form>
<div id="login-error" class="hidden"></div>
</div>
<!-- Mocks for History Modal -->
<button id="btn-history">History</button>
<div id="history-modal" class="modal hidden">
<button id="btn-history-close">Close</button>
<div id="history-loading" class="hidden"></div>
<div id="history-content"></div>
</div>
<!-- Mocks for Version Modal -->
<span class="version-tag">v1.4.17</span>
<div id="version-modal" class="modal hidden">
<button id="btn-version-close">Close</button>
<button id="btn-clear-cache">Clear</button>
<span id="version-current"></span>
<div id="version-list-container"></div>
</div>
<!-- Mocks for Theme Toggle -->
<button id="theme-toggle"><span class="theme-icon">light_mode</span></button>
<!-- Mocks for Navigation Tabs -->
<button id="btn-this-week" class="active">This Week</button>
<button id="btn-next-week">Next Week</button>
<button id="btn-refresh">Refresh</button>
<button id="btn-logout">Logout</button>
<div class="order-history-header">Header</div>
<button id="btn-error-redirect">Error Redirect</button>
</body>
</html>
`;
const jsCode = fs.readFileSync('kantine.js', 'utf8').replace('(function () {', '').replace(/}\)\(\);$/, '');
log("Reading file jsCode...");
const jsCode = fs.readFileSync('kantine.js', 'utf8')
.replace('(function () {', '')
.replace('})();', '')
.replace('if (window.__KANTINE_LOADED) return;', '')
.replace('window.location.reload();', 'window.__RELOAD_CALLED = true;');
const dom = new JSDOM(html, { runScripts: "dangerously" });
log("Instantiating JSDOM...");
const dom = new JSDOM(html, { runScripts: "dangerously", url: "http://localhost/" });
log("JSDOM dom created...");
global.window = dom.window;
global.document = window.document;
global.localStorage = { getItem: () => '[]', setItem: () => { } };
@@ -32,18 +90,95 @@ global.saveFlags = () => {};
global.renderVisibleWeeks = () => { };
// Mock missing browser features if needed
global.Notification = { permission: 'default', requestPermission: () => { } };
global.window.matchMedia = () => ({ matches: false, addListener: () => { }, removeListener: () => { } });
global.fetch = () => Promise.resolve({ ok: true, json: () => Promise.resolve({ results: [] }) });
global.window.fetch = global.fetch;
try {
dom.window.eval(jsCode);
console.log("Initial Bell Classes:", window.document.getElementById('alarm-bell').className);
log("Before eval...");
const testCode = `
console.log("--- Testing Alarm Bell ---");
// Add flag
dom.window.eval("userFlags.add('2026-02-24_123'); updateAlarmBell();");
console.log("After Add:", window.document.getElementById('alarm-bell').className);
userFlags.add('2026-02-24_123'); updateAlarmBell();
if (document.getElementById('alarm-bell').className.includes('hidden')) throw new Error("Bell should be visible");
// Remove flag
dom.window.eval("userFlags.delete('2026-02-24_123'); updateAlarmBell();");
console.log("After Remove:", window.document.getElementById('alarm-bell').className);
} catch (e) {
console.error(e);
userFlags.delete('2026-02-24_123'); updateAlarmBell();
if (!document.getElementById('alarm-bell').className.includes('hidden')) throw new Error("Bell should be hidden");
console.log("✅ Alarm Bell Test Passed");
console.log("--- Testing Highlights Modal ---");
// First, verify initial state
const hlModal = document.getElementById('highlights-modal');
if (!hlModal.classList.contains('hidden')) throw new Error("Highlights modal should be hidden initially");
// Click to open
document.getElementById('btn-highlights').click();
if (hlModal.classList.contains('hidden')) throw new Error("Highlights modal did not open upon clicking btn-highlights!");
// Click to close
document.getElementById('btn-highlights-close').click();
if (!hlModal.classList.contains('hidden')) throw new Error("Highlights modal did not close upon clicking btn-highlights-close!");
console.log("✅ Highlights Modal Test Passed");
console.log("--- Testing Login Modal ---");
const loginModal = document.getElementById('login-modal');
document.getElementById('btn-login-open').click();
if (loginModal.classList.contains('hidden')) throw new Error("Login modal should open");
document.getElementById('btn-login-close').click();
if (!loginModal.classList.contains('hidden')) throw new Error("Login modal should close");
console.log("✅ Login Modal Test Passed");
console.log("--- Testing History Modal ---");
// We need authToken to be truthy to open history modal
authToken = "fake_token";
const historyModal = document.getElementById('history-modal');
document.getElementById('btn-history').click();
if (historyModal.classList.contains('hidden')) throw new Error("History modal should open");
document.getElementById('btn-history-close').click();
if (!historyModal.classList.contains('hidden')) throw new Error("History modal should close");
console.log("✅ History Modal Test Passed");
console.log("--- Testing Version Modal ---");
const versionModal = document.getElementById('version-modal');
document.querySelector('.version-tag').click();
if (versionModal.classList.contains('hidden')) throw new Error("Version modal should open");
document.getElementById('btn-version-close').click();
if (!versionModal.classList.contains('hidden')) throw new Error("Version modal should close");
console.log("✅ Version Modal Test Passed");
console.log("--- Testing Theme Toggle ---");
const themeBtn = document.getElementById('theme-toggle');
const initialTheme = document.documentElement.getAttribute('data-theme');
themeBtn.click();
const newTheme = document.documentElement.getAttribute('data-theme');
if (initialTheme === newTheme) throw new Error("Theme did not toggle");
console.log("✅ Theme Toggle Test Passed");
console.log("--- Testing Navigation Tabs ---");
const btnThis = document.getElementById('btn-this-week');
const btnNext = document.getElementById('btn-next-week');
btnNext.click();
if (!btnNext.classList.contains('active') || btnThis.classList.contains('active')) throw new Error("Next week tab not active");
btnThis.click();
if (!btnThis.classList.contains('active') || btnNext.classList.contains('active')) throw new Error("This week tab not active");
console.log("✅ Navigation Tabs Test Passed");
console.log("--- Testing Clear Cache Button ---");
// Mock confirm directly inside evaluated JSDOM context
window.confirm = () => true;
document.getElementById('btn-clear-cache').click();
if (!window.__RELOAD_CALLED) throw new Error("Clear cache did not reload the page");
console.log("✅ Clear Cache Button Test Passed");
window.__TEST_PASSED = true;
`;
dom.window.eval(jsCode + "\n" + testCode);
if (!dom.window.__TEST_PASSED) {
throw new Error("Tests failed to reach completion inside JSDOM.");
}
process.exit(0);

View File

@@ -1 +1 @@
v1.4.14
v1.5.0