Compare commits

...

100 Commits

Author SHA1 Message Date
Kantine Wrapper 212bf3b015 chore: update build artifacts for v1.6.4 2026-03-06 08:38:24 +01:00
Kantine Wrapper f29ecd4b79 style: remove past ordered borders and swap header icon 2026-03-06 08:38:17 +01:00
Kantine Wrapper 1be6e44d7f chore: update build artifacts for v1.6.4 2026-03-05 16:38:09 +01:00
Kantine Wrapper 49b0ab17ac chore: refine language heuristics and dictionary 2026-03-05 16:37:55 +01:00
Kantine Wrapper 55e738a554 chore: update build artifacts for v1.6.3 2026-03-05 12:47:56 +01:00
Kantine Wrapper 45adfa9d5d fix: Correct menu item text extraction from name to description and introduce a language splitting utility. 2026-03-05 12:47:51 +01:00
Kantine Wrapper f5f6dddba3 chore: update build artifacts for v1.6.3 2026-03-05 12:43:49 +01:00
Kantine Wrapper b06f6c3551 chore: inject temp menu logger 2026-03-05 12:43:25 +01:00
Kantine Wrapper b66030dce5 fix: prevent heuristic split on final English-only fragment 2026-03-05 11:56:55 +01:00
Kantine Wrapper 8e7ec468d4 chore: update build artifacts for v1.6.3 2026-03-05 11:46:57 +01:00
Kantine Wrapper 8ce3ae4c92 feat: Update footer slogan, optimize footer height and container padding, and bump version to v1.6.3. 2026-03-05 11:46:51 +01:00
Kantine Wrapper 6a70a5a5e8 feat: sticky headers (v1.6.2) 2026-03-05 11:34:19 +01:00
Kantine Wrapper edec109552 chore: update build artifacts for v1.6.1 2026-03-04 14:46:17 +01:00
Kantine Wrapper a7aea2ece3 chore: version bump 2026-03-04 14:46:11 +01:00
Kantine Wrapper 49dc1cc135 chore: update build artifacts for v1.6.0 2026-03-04 14:45:25 +01:00
Kantine Wrapper 90f1c0ed04 feat: Add descriptive German tooltips to various UI elements for improved usability 2026-03-04 14:45:13 +01:00
Kantine Wrapper 42978c6e7e chore: update build artifacts for v1.6.0 2026-03-04 13:29:25 +01:00
Kantine Wrapper 6ad3498bcc cleanup test file 2026-03-04 13:27:13 +01:00
Kantine Wrapper b44ecb2ccf v1.6.0: Language Filter 2026-03-04 13:26:46 +01:00
Kantine Wrapper 9e161e2907 chore: update build artifacts for v1.6.0 2026-03-04 13:11:43 +01:00
Kantine Wrapper 8b15760463 feat: Introduce language filter with DE/EN/ALL toggle for menu descriptions and update to version 1.6.0. 2026-03-04 13:11:34 +01:00
Kantine Wrapper 4aa67c9cbe chore: update build artifacts for v1.5.1 2026-03-04 11:42:04 +01:00
Kantine Wrapper 12c55ef883 feat: Add a collapsing video banner to the installation page. 2026-03-04 11:41:58 +01:00
Kantine Wrapper 1e9dd9a3b5 chore: update build artifacts for v1.5.1 2026-03-04 11:39:19 +01:00
Kantine Wrapper db8b2c5629 fix: Correct Friday order payload preorder and time, and update version to v1.5.1. 2026-03-04 11:39:09 +01:00
Kantine Wrapper 67533875bd chore: update build artifacts for v1.5.1 2026-03-04 11:33:37 +01:00
Kantine Wrapper 99809dafb7 fix: correct preorder flag and date format for Friday orders (v1.5.1) 2026-03-04 11:33:37 +01:00
Kantine Wrapper 4cf3e4adc2 chore: update build artifacts for v1.5.0 2026-02-26 17:37:34 +01:00
Kantine Wrapper 4fe7950697 style: update favicon_base.png 2026-02-26 17:37:34 +01:00
Kantine Wrapper e162a16550 build: autogenerate favicon.png and update installer assets for v1.5.0 2026-02-26 17:36:58 +01:00
Kantine Wrapper b7c3aac921 chore: update build artifacts for v1.5.0 2026-02-26 17:13:51 +01:00
Kantine Wrapper a902732d4b style: replace logo unicode char with dynamic favicon image in installer 2026-02-26 17:13:51 +01:00
Kantine Wrapper eaab21151a chore: update build artifacts for v1.5.0 2026-02-26 17:10:33 +01:00
Kantine Wrapper 5af1f86700 feat: Update favicon and add theming support to the bookmarklet. 2026-02-26 17:10:27 +01:00
Kantine Wrapper 90b503ddb7 chore: update build artifacts for v1.5.0 2026-02-26 12:43:13 +01:00
Kantine Wrapper 10ffbd8c68 build: automatically generate favicon.png from favicon_base.png if present 2026-02-26 12:43:03 +01:00
Kantine Wrapper 0294db7976 chore: update build artifacts for v1.5.0 2026-02-26 12:39:20 +01:00
Kantine Wrapper 5a2c23524d chore: update build artifacts for v1.5.0 2026-02-26 12:35:04 +01:00
Kantine Wrapper d4a9d39d67 feat: implement history modal with comprehensive styling, general UI improvements, and favicon update. 2026-02-26 12:34:50 +01:00
Kantine Wrapper 3c8d946a1e style: update favicon to new design for v1.5.0 release 2026-02-26 11:20:49 +01:00
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
Kantine Wrapper a0ef6e631e chore: update build artifacts for v1.4.14 2026-02-24 15:31:48 +01:00
Kantine Wrapper d895a5fb7c feat: immediate api refresh on flag, fix timestamp fallback (v1.4.14) 2026-02-24 15:31:48 +01:00
Kantine Wrapper fd765a74c0 chore: update build artifacts for v1.4.13 2026-02-24 13:24:50 +01:00
Kantine Wrapper 1f184fab8b fix: replace css variables with direct hex colors for alarm bell 2026-02-24 13:24:09 +01:00
Kantine Wrapper b6c7c66027 chore: update build artifacts for v1.4.12 2026-02-24 13:20:03 +01:00
Kantine Wrapper 33bb87d7f4 fix: enforce hidden state and default color of alarm bell 2026-02-24 13:19:38 +01:00
Kantine Wrapper d4a8a47ccd chore: update build artifacts for v1.4.11 2026-02-24 13:11:09 +01:00
Kantine Wrapper 8e8c93410b feat: background refresh for version menu 2026-02-24 13:11:02 +01:00
Kantine Wrapper cca59bcace chore: update build artifacts for v1.4.10 2026-02-24 13:05:53 +01:00
Kantine Wrapper 432bbcb6f2 build: separate release logic into release.sh 2026-02-24 13:05:46 +01:00
Kantine Wrapper accdccf897 docs: sync REQUIREMENTS.md with latest features 2026-02-24 13:00:16 +01:00
Kantine Wrapper 7fc8c6f1e0 dist files for v1.4.10 built 2026-02-24 12:59:19 +01:00
Kantine Wrapper 0eb14a1869 dist files for v1.4.9 built 2026-02-24 12:56:53 +01:00
Kantine Wrapper c841954c5d dist files for v1.4.8 built 2026-02-24 12:44:07 +01:00
Kantine Wrapper 320c4066f3 dist files for v1.4.7 built 2026-02-24 12:43:35 +01:00
Kantine Wrapper cda74e65db dist files for v1.4.7 built 2026-02-24 12:32:44 +01:00
Kantine Wrapper d1a19b043d dist files for v1.4.6 built 2026-02-24 11:11:15 +01:00
Kantine Wrapper 8c4de96432 dist files for v1.4.5 built 2026-02-24 10:52:53 +01:00
Kantine Wrapper ce7d8a3de5 dist files for v1.4.4 built 2026-02-24 10:46:06 +01:00
Kantine Wrapper 0309f488bd dist files for v1.4.4 built 2026-02-24 10:46:00 +01:00
Kantine Wrapper d82762430f dist files for v1.4.3 built 2026-02-24 10:26:59 +01:00
Kantine Wrapper 54e5ada03d dist files for v1.4.3 built 2026-02-24 08:37:52 +01:00
21 changed files with 2110 additions and 321 deletions
+6
View File
@@ -43,11 +43,17 @@ trigger: always_on
- **Visuals**: Generate screenshots/mockups for UI changes. - **Visuals**: Generate screenshots/mockups for UI changes.
- **Evidence**: Log outputs for verification. - **Evidence**: Log outputs for verification.
3. **Design**: Optimize code for AI readability (context efficiency). 3. **Design**: Optimize code for AI readability (context efficiency).
4. **Retry on Failure**: When an operation does not finish or does not work as expected, do not try endlessly to fix this. Try a few times and ask the user if no progress can be made.
## 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. 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 📋 ## 7. Requirements-Konsistenz 📋
Alle umgesetzten Anforderungen müssen mit `REQUIREMENTS.md` übereinstimmen. Alle umgesetzten Anforderungen müssen mit `REQUIREMENTS.md` übereinstimmen.
1. **Vor der Umsetzung prüfen**: Passt die neue Anforderung zu den bestehenden Requirements? 1. **Vor der Umsetzung prüfen**: Passt die neue Anforderung zu den bestehenden Requirements?
+40 -11
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. * **Bestell-Countdown:** ⏳ Roter Alarm 1h vor Bestellschluss.
* **Smart Highlights:** 🌟 Markiere deine Favoriten (z.B. "Schnitzel", "Vegetarisch"). * **Smart Highlights:** 🌟 Markiere deine Favoriten (z.B. "Schnitzel", "Vegetarisch").
* **Bestellstatus:** Farbige Indikatoren für bestellte Menüs. * **Bestellstatus:** Farbige Indikatoren für bestellte Menüs.
* **Kostenkontrolle:** Summiert automatisch den Gesamtpreis der Woche. * **Kostenkontrolle:** 💰 Summiert automatisch den Gesamtpreis der Woche.
* **Session Reuse:** Nutzt automatisch eine bestehende Login-Session (Loggt dich automatisch ein). * **Bestellhistorie:** 📜 Gruppiert nach Monat & KW mit inkrementellem Delta-Cache.
* **Menu Badges:** Zeigt Menü-Codes (M1, M2+) direkt im Header. * **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. * **Changelog:** Übersicht über neue Funktionen direkt im Installer.
## 📦 Installation ## 📦 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. 2. Ziehe den blauen Button **"Kantine Wrapper"** in deine Lesezeichen-Leiste.
3. Fertig! 3. Fertig!
## usage ## 🍽️ Nutzung
1. Navigiere zu [https://web.bessa.app/knapp-kantine](https://web.bessa.app/knapp-kantine). 1. Navigiere zu [https://web.bessa.app/knapp-kantine](https://web.bessa.app/knapp-kantine).
2. Klicke auf das **"Kantine Wrapper"** Lesezeichen. 2. Klicke auf das **"Kantine Wrapper"** Lesezeichen.
@@ -28,24 +33,38 @@ Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses
## 🛠️ Entwicklung ## 🛠️ Entwicklung
### Voraussetzungen ### 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`) * Bash (für `build-bookmarklet.sh`)
### Projektstruktur ### Projektstruktur
#### Quelldateien #### Quelldateien
* `kantine.js`: Der Haupt-Quellcode des Bookmarklets (UI, API-Logik, Rendering). | Datei | Beschreibung |
* `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. | `kantine.js` | Haupt-Quellcode des Bookmarklets (UI, API-Logik, Rendering). |
* `build-bookmarklet.sh`: Build-Skript erzeugt alle `dist/`-Artefakte. | `style.css` | Komplettes Design (CSS mit Light/Dark Mode). |
* `test_build.py`: Automatische Build-Tests, laufen am Ende jedes Builds. | `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 #### `dist/` Build-Artefakte
| Datei | Beschreibung | | 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.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. | | `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. | | `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 ### Build
@@ -55,5 +74,15 @@ Um Änderungen an `kantine.js` oder `style.css` wirksam zu machen, führe den Bu
./build-bookmarklet.sh ./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 ## 📝 Lizenz
Internes Tool. Internes Tool.
+14 -6
View File
@@ -40,7 +40,7 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
| FR-033 | Es muss möglich sein, dasselbe Menü mehrfach zu bestellen. Bei Mehrfachbestellungen muss die Anzahl angezeigt werden. | Niedrig | v1.0.1 | | FR-033 | Es muss möglich sein, dasselbe Menü mehrfach zu bestellen. Bei Mehrfachbestellungen muss die Anzahl angezeigt werden. | Niedrig | v1.0.1 |
| **Kostentransparenz & Bestellhistorie** | | | | | **Kostentransparenz & Bestellhistorie** | | | |
| FR-040 | Das System muss die Gesamtkosten aller Bestellungen einer Woche automatisch berechnen und anzeigen. | Mittel | v1.1.0 | | FR-040 | Das System muss die Gesamtkosten aller Bestellungen einer Woche automatisch berechnen und anzeigen. | Mittel | v1.1.0 |
| FR-041 | Das System muss dem Benutzer eine paginierte oder vollständige Bestellhistorie (gruppiert nach Monat und KW) mit Fortschrittsanzeige auf Abruf in einem Modal bereitstellen. | Mittel | v1.4.0 | | FR-041 | Das System muss dem Benutzer eine Bestellhistorie (gruppiert nach Monat und KW) mit Fortschrittsanzeige auf Abruf in einem Modal bereitstellen. Die Historie muss über ein lokales Delta-Caching verfügen, um Ladezeiten zu minimieren. | Mittel | v1.4.0 (Update v1.4.7) |
| **Bestell-Countdown** | | | | | **Bestell-Countdown** | | | |
| FR-050 | Das System muss vor Bestellschluss einen visuell hervorgehobenen Countdown anzeigen. | Mittel | v1.1.0 | | FR-050 | Das System muss vor Bestellschluss einen visuell hervorgehobenen Countdown anzeigen. | Mittel | v1.1.0 |
| **Menü-Flagging & Benachrichtigungen** | | | | | **Menü-Flagging & Benachrichtigungen** | | | |
@@ -54,13 +54,18 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
| FR-071 | Die Hervorhebung muss anhand von Menüname und Beschreibung erfolgen (Substring-Matching, case-insensitive). | Niedrig | 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-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 | | 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-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-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 | | FR-082 | Das System muss beim erstmaligen Laden die Betriebssystem-Präferenz für das Farbschema berücksichtigen. | Niedrig | v1.0.1 |
| **Header UI & Navigation** | | | |
| 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 | 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) |
| **Sprachfilter** | | | |
| FR-120 | Das System muss zweisprachige Menübeschreibungen (Deutsch/Englisch) erkennen und dem Benutzer erlauben, via UI-Toggle zwischen DE, EN und ALL (beide Sprachen) zu wechseln. Die Sprachpräferenz muss persistent gespeichert werden. Allergen-Codes müssen in allen Modi angezeigt werden. | Mittel | v1.6.0 |
| **Benutzer-Feedback** | | | | | **Benutzer-Feedback** | | | |
| FR-090 | Alle benutzerrelevanten Aktionen (Bestellung, Stornierung, Fehler) müssen durch nicht-blockierende Benachrichtigungen (Toasts) bestätigt werden. | Mittel | v1.0.1 | | FR-095 | 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-096 | Bei einem Verbindungsfehler muss ein Fehlerdialog mit Fallback-Link zur Originalseite angezeigt werden. | Mittel | v1.0.1 |
| **Nächste-Woche-Badge** | | | | | **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 | | 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** | | | | | **Update-Management** | | | |
@@ -69,6 +74,9 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
| FR-112 | Benutzer müssen eine Versionsliste mit Installationslinks einsehen können (Versionsmenü). | Niedrig | v1.3.0 | | 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-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-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 ## 3. Nicht-funktionale Anforderungen
@@ -81,7 +89,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-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-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 | | **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 ## 4. Technische Randbedingungen
* **Deployment**: Das System wird als Bookmarklet ausgeliefert, das auf der Bessa-Webseite ausgeführt wird. * **Deployment**: Das System wird als Bookmarklet ausgeliefert, das auf der Bessa-Webseite ausgeführt wird.
@@ -89,4 +97,4 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
* **Datenhaltung**: Clientseitig via `localStorage` (Menü-Cache, Flags, Highlights, Theme) und `sessionStorage` (Auth-Token). * **Datenhaltung**: Clientseitig via `localStorage` (Menü-Cache, Flags, Highlights, Theme) und `sessionStorage` (Auth-Token).
* **Build**: Bash-basiertes Build-Script, das Bookmarklet-URL, Standalone-HTML und Installer-Seite generiert. * **Build**: Bash-basiertes Build-Script, das Bookmarklet-URL, Standalone-HTML und Installer-Seite generiert.
* **Versionierung**: SemVer, verwaltet über GitHub Releases/Tags. * **Versionierung**: SemVer, verwaltet über GitHub Releases/Tags.
* **Tests**: Python-basierte Build-Tests + Node.js-basierte Logik-Tests. * **Tests**: Python-basierte Build-Tests (`python3`) + Node.js-basierte Logik-Tests + Node.js-basierte DOM-Interaktionstests (JSDOM).
Binary file not shown.
+74 -29
View File
@@ -7,6 +7,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DIST_DIR="$SCRIPT_DIR/dist" DIST_DIR="$SCRIPT_DIR/dist"
CSS_FILE="$SCRIPT_DIR/style.css" CSS_FILE="$SCRIPT_DIR/style.css"
JS_FILE="$SCRIPT_DIR/kantine.js" JS_FILE="$SCRIPT_DIR/kantine.js"
FAVICON_FILE="$SCRIPT_DIR/favicon.png"
# === VERSION === # === VERSION ===
if [ -f "$SCRIPT_DIR/version.txt" ]; then if [ -f "$SCRIPT_DIR/version.txt" ]; then
@@ -24,10 +25,33 @@ echo "=== Kantine Bookmarklet Builder ($VERSION) ==="
if [ ! -f "$CSS_FILE" ]; then echo "ERROR: $CSS_FILE not found"; exit 1; fi 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 "$JS_FILE" ]; then echo "ERROR: $JS_FILE not found"; exit 1; fi
# Generate favicon.png from favicon_base.png if base exists
FAVICON_BASE="$SCRIPT_DIR/favicon_base.png"
if [ -f "$FAVICON_BASE" ]; then
echo "Generating 32x32 favicon.png from favicon_base.png..."
python3 -c "
import sys
from PIL import Image
try:
img = Image.open('$FAVICON_BASE')
img_resized = img.resize((32, 32), Image.Resampling.LANCZOS)
img_resized.save('$FAVICON_FILE')
except Exception as e:
print('Favicon generation error:', e)
sys.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") CSS_CONTENT=$(cat "$CSS_FILE")
# Inject version into JS # Inject version and favicon into JS
JS_CONTENT=$(cat "$JS_FILE" | sed "s|{{VERSION}}|$VERSION|g") 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) === # === 1. Build standalone HTML (for local testing/dev) ===
cat > "$DIST_DIR/kantine-standalone.html" << HTMLEOF cat > "$DIST_DIR/kantine-standalone.html" << HTMLEOF
@@ -101,6 +125,7 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Kantine Wrapper Installer ($VERSION)</title> <title>Kantine Wrapper Installer ($VERSION)</title>
<link rel="icon" type="image/png" href="$FAVICON_URL">
<style> <style>
body { font-family: 'Inter', sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; } body { font-family: 'Inter', sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
h1 { color: #029AA8; } /* Knapp Teal */ h1 { color: #029AA8; } /* Knapp Teal */
@@ -121,8 +146,25 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
</style> </style>
</head> </head>
<body> <body>
<!-- Banner Video: plays once, collapses after ending -->
<div id="banner-video-wrap" style="width: 100%; max-width: 600px; margin: 0 auto 20px auto; border-radius: 12px; overflow: hidden; pointer-events: none; user-select: none; max-height: 400px; opacity: 1; transition: max-height 0.8s ease-in-out, opacity 0.6s ease-in-out, margin 0.8s ease-in-out;">
<video id="banner-video" autoplay muted playsinline disablepictureinpicture style="width: 100%; display: block;" src="https://github.com/TauNeutrino/kantine-overview/raw/main/dist/Arrow_and_fork_fly_away_bd43310bea.mp4"></video>
</div>
<script>
document.getElementById('banner-video').addEventListener('ended', function() {
var w = document.getElementById('banner-video-wrap');
w.style.maxHeight = '0';
w.style.opacity = '0';
w.style.marginBottom = '0';
});
</script>
<div style="text-align: center; margin-bottom: 30px;"> <div style="text-align: center; margin-bottom: 30px;">
<h1 style="margin-bottom: 5px;">🍽️ Kantine Wrapper <span style="font-size:0.5em; opacity:0.6; font-weight:400; vertical-align:middle; margin-left:10px;">$VERSION</span></h1> <h1 style="margin-bottom: 5px; display: flex; align-items: center; justify-content: center; gap: 10px;">
<img src="$FAVICON_URL" alt="Logo" style="width: 38px; height: 38px;">
Kantine Wrapper
<span style="font-size:0.5em; opacity:0.6; font-weight:400; margin-left:5px;">$VERSION</span>
</h1>
<p style="font-size: 1.2rem; color: #a0aec0; margin-top: 0; font-style: italic;">"Mahlzeit! Jetzt bessa einfach."</p> <p style="font-size: 1.2rem; color: #a0aec0; margin-top: 0; font-style: italic;">"Mahlzeit! Jetzt bessa einfach."</p>
</div> </div>
@@ -171,7 +213,7 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
</div> </div>
<div style="text-align: center; margin-top: 40px; color: #5c6b7f; font-size: 0.8rem;"> <div style="text-align: center; margin-top: 40px; color: #5c6b7f; font-size: 0.8rem;">
<p>Powered by <strong>Kaufi-Kitchen</strong> 👨‍🍳</p> <p>Powered by <strong>Kaufis-Kitchen</strong> 👨‍🍳</p>
</div> </div>
@@ -200,9 +242,9 @@ echo "document.getElementById('bookmarklet-link').href = " >> "$DIST_DIR/install
echo "$JS_CONTENT" | python3 -c " echo "$JS_CONTENT" | python3 -c "
import sys, json, urllib.parse import sys, json, urllib.parse
# 1. Read JS and Replace VERSION # 1. Read JS and Replace VERSION + Favicon
js_template = sys.stdin.read() 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') # 2. Prepare CSS for injection via createElement('style')
css = open('$CSS_FILE').read().replace('\n', ' ').replace(' ', ' ') css = open('$CSS_FILE').read().replace('\n', ' ').replace(' ', ' ')
@@ -236,6 +278,16 @@ $CHANGELOG_HTML
EOF EOF
cat >> "$DIST_DIR/install.html" << INSTALLEOF 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'; document.getElementById('bookmarklet-link').textContent = 'Kantine $VERSION';
</script> </script>
</body> </body>
@@ -251,39 +303,32 @@ ls -la "$DIST_DIR/"
# === 4. Run build-time tests === # === 4. Run build-time tests ===
echo "" echo ""
echo "=== Running Logic Tests ===" echo "=== Running Logic Tests ==="
node "$SCRIPT_DIR/test_logic.js" timeout 15s node "$SCRIPT_DIR/test_logic.js"
LOGIC_EXIT=$? LOGIC_EXIT=$?
if [ $LOGIC_EXIT -ne 0 ]; then if [ $LOGIC_EXIT -ne 0 ]; then
echo "❌ Logic tests FAILED! See above for details." echo "❌ Logic tests FAILED or TIMED OUT (Exit: $LOGIC_EXIT)! See above for details."
exit 1 exit 1
fi fi
echo "=== Running DOM Interaction Tests ==="
timeout 15s node "$SCRIPT_DIR/tests/test_dom.js"
DOM_EXIT=$?
if [ $DOM_EXIT -ne 0 ]; then
echo "❌ DOM UI tests FAILED or TIMED OUT (Exit: $DOM_EXIT)! Regressions detected."
# Ensure playwright processes are killed if they leak
pkill -f playwright || true
pkill -f "node.*test_dom" || true
exit 1
fi
echo "=== Running Build Tests ===" echo "=== Running Build Tests ==="
python3 "$SCRIPT_DIR/test_build.py" timeout 15s python3 "$SCRIPT_DIR/test_build.py"
TEST_EXIT=$? TEST_EXIT=$?
if [ $TEST_EXIT -ne 0 ]; then if [ $TEST_EXIT -ne 0 ]; then
echo "❌ Build tests FAILED! See above for details." echo "❌ Build tests FAILED or TIMED OUT (Exit: $TEST_EXIT)! See above for details."
exit 1 exit 1
fi fi
echo "✅ All build tests passed." echo "✅ All build tests passed."
# === 5. Commit, tag, and push ===
echo ""
echo "=== Committing & Pushing ==="
git add -A
git commit -m "dist files for $VERSION built" --allow-empty
echo ""
echo "=== Tagging $VERSION ==="
if git rev-parse "$VERSION" >/dev/null 2>&1; then
git tag -f "$VERSION"
echo "🔄 Tag $VERSION moved to current commit."
else
git tag "$VERSION"
echo "✅ Created tag: $VERSION"
fi
git push
git push origin --force tag "$VERSION"
git push github --force tag "$VERSION"
echo "✅ Pushed commit + tag $VERSION"
+33 -5
View File
@@ -1,9 +1,37 @@
## v1.4.2 (2026-02-23) ## v1.6.4 (2026-03-05)
- **Fix**: Das "Heute Bestellt" Menü leuchtet nun stimmig im Design-Violett statt Blau. - **Feature**: Sprach-Lexikon (DE/EN) massiv erweitert um österreichische Begriffe (Nockerl, Fleckerl, Topfen, Mohn, Most etc.) und gängige Tippfehler aus dem Bessa-System (trukey, coffe, oveb etc.).
- **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. - 🧹 **Cleanup**: Sprach-Lexikon dedupliziert und alphabetisch sortiert für bessere Performance und Wartbarkeit.
- 🐛 **Bugfix**: Trennung von zweisprachigen Menüs (`splitLanguage`) verbessert: Erfasst nun auch Schrägstriche ohne Leerzeichen (z.B. `Suppe/Soup`).
- 🐛 **Bugfix**: Fehlerhafte Badge-Anzeige korrigiert (Variable `count` vs `orderCount`).
## v1.6.3 (2026-03-05)
-**Chore**: Slogan im Footer aktualisiert ("Jetzt Bessa Einfach! • Knapp-Kantine Wrapper • 2026 by Kaufis-Kitchen") und Footer-Höhe für mehr Platzierung optimiert.
## v1.6.2 (2026-03-05)
-**Feature**: Wochentags-Header (Montag, Dienstag etc.) scrollen nun als "Sticky Header" mit und bleiben am oberen Bildschirmrand haften.
- Das Layout clippt scrollende Speisen ordentlich darunter weg.
- Vollständiges Viewport-Scrolling: Das Layout nutzt nun die ganze Höhe aus (`100dvh`), wodurch Scrollbalken sauber am Rand positioniert sind.
- 🐛 **Bugfix**: Probleme mit Bessa's default `overflow` Verhalten behoben, das `position: sticky` auf iOS/WebKit-Browsern blockierte.
## v1.6.0 (2026-03-04)
-**Feature**: Sprachfilter für zweisprachige Menübeschreibungen. Neuer DE/EN/ALL Toggle im Header ermöglicht das Umschalten zwischen Deutsch, Englisch und dem vollen Originaltext. Allergen-Codes werden in allen Modi angezeigt. Einstellung wird persistent gespeichert.
## v1.5.1 (2026-03-04)
- 🐛 **Bugfix**: Freitagsbestellungen schlugen fehl ("Onlinebestellung sind nicht verfügbar"). Ursache: Der Order-Payload verwendete `preorder: false` und eine falsche Uhrzeit (`T10:00:00.000Z` statt `T10:30:00Z`). Beides wurde anhand der originalen Bessa-API korrigiert.
## v1.5.0 (2026-02-26)
Das große "Quality of Life"-Update! Zusammenfassung aller Features und Fixes seit v1.4.0:
-**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.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) ## v1.4.0 (2026-02-22)
- **Feature**: Bestellhistorie per Knopfdruck abrufbar. Übersichtliche Darstellung, gruppiert nach Monaten und Kalenderwochen, inklusive Stornos. 📜✨ - **Feature**: Bestellhistorie per Knopfdruck abrufbar. Übersichtliche Darstellung, gruppiert nach Monaten und Kalenderwochen, inklusive Stornos. 📜✨
Executable
+15
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]);
}
}
BIN
View File
Binary file not shown.
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+70 -11
View File
File diff suppressed because one or more lines are too long
+787 -127
View File
File diff suppressed because it is too large Load Diff
Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+625 -79
View File
@@ -29,11 +29,12 @@
let currentWeekNumber = getISOWeek(new Date()); let currentWeekNumber = getISOWeek(new Date());
let currentYear = new Date().getFullYear(); let currentYear = new Date().getFullYear();
let displayMode = 'this-week'; let displayMode = 'this-week';
let authToken = sessionStorage.getItem('kantine_authToken'); let authToken = localStorage.getItem('kantine_authToken');
let currentUser = sessionStorage.getItem('kantine_currentUser'); let currentUser = localStorage.getItem('kantine_currentUser');
let orderMap = new Map(); let orderMap = new Map();
let userFlags = new Set(JSON.parse(localStorage.getItem('kantine_flags') || '[]')); let userFlags = new Set(JSON.parse(localStorage.getItem('kantine_flags') || '[]'));
let pollIntervalId = null; let pollIntervalId = null;
let langMode = localStorage.getItem('kantine_lang') || 'de';
// === API Helpers === // === API Helpers ===
function apiHeaders(token) { function apiHeaders(token) {
@@ -50,6 +51,16 @@
// Replace entire page content // Replace entire page content
document.title = 'Kantine Weekly Menu'; 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 // Inject Google Fonts if not already present
if (!document.querySelector('link[href*="fonts.googleapis.com/css2?family=Inter"]')) { if (!document.querySelector('link[href*="fonts.googleapis.com/css2?family=Inter"]')) {
const fontLink = document.createElement('link'); const fontLink = document.createElement('link');
@@ -69,13 +80,25 @@
<header class="app-header"> <header class="app-header">
<div class="header-content"> <div class="header-content">
<div class="brand"> <div class="brand">
<span class="material-icons-round logo-icon">restaurant_menu</span> <img src="{{FAVICON_DATA_URI}}" alt="Logo" class="logo-img" style="height: 1.2em; width: 1.2em; object-fit: contain;">
<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ü">{{VERSION}}</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ü">{{VERSION}}</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div> <div id="last-updated-subtitle" class="subtitle"></div>
</div> </div>
<div class="nav-group" style="margin-left: 1rem;">
<button id="btn-this-week" class="nav-btn active" title="Menü dieser Woche anzeigen">Diese Woche</button>
<button id="btn-next-week" class="nav-btn" title="Menü nächster Woche anzeigen">Nächste Woche</button>
</div>
<button id="alarm-bell" class="icon-btn hidden" aria-label="Benachrichtigungen" title="Keine beobachteten Menüs" style="margin-left: -0.5rem;">
<span class="material-icons-round" id="alarm-bell-icon" style="color:var(--text-secondary); transition: color 0.3s;">notifications</span>
</button>
</div> </div>
<div class="header-center-wrapper"> <div class="header-center-wrapper">
<div id="lang-toggle" class="lang-toggle" title="Sprache der Menübeschreibung">
<button class="lang-btn${langMode === 'de' ? ' active' : ''}" data-lang="de">DE</button>
<button class="lang-btn${langMode === 'en' ? ' active' : ''}" data-lang="en">EN</button>
<button class="lang-btn${langMode === 'all' ? ' active' : ''}" data-lang="all">ALL</button>
</div>
<div id="header-week-info" class="header-week-info"></div> <div id="header-week-info" class="header-week-info"></div>
<div id="weekly-cost-display" class="weekly-cost hidden"></div> <div id="weekly-cost-display" class="weekly-cost hidden"></div>
</div> </div>
@@ -89,21 +112,17 @@
<button id="btn-highlights" class="icon-btn" aria-label="Persönliche Highlights verwalten" title="Persönliche Highlights verwalten"> <button id="btn-highlights" class="icon-btn" aria-label="Persönliche Highlights verwalten" title="Persönliche Highlights verwalten">
<span class="material-icons-round">label</span> <span class="material-icons-round">label</span>
</button> </button>
<div class="nav-group"> <button id="theme-toggle" class="icon-btn" aria-label="Toggle Theme" title="Erscheinungsbild (Hell/Dunkel) wechseln">
<button id="btn-this-week" class="nav-btn active">Diese Woche</button>
<button id="btn-next-week" class="nav-btn">Nächste Woche</button>
</div>
<button id="theme-toggle" class="icon-btn" aria-label="Toggle Theme">
<span class="material-icons-round theme-icon">light_mode</span> <span class="material-icons-round theme-icon">light_mode</span>
</button> </button>
<button id="btn-login-open" class="user-badge-btn icon-btn-small"> <button id="btn-login-open" class="user-badge-btn icon-btn-small" title="Mit Bessa.app Account anmelden">
<span class="material-icons-round">login</span> <span class="material-icons-round">login</span>
<span>Anmelden</span> <span>Anmelden</span>
</button> </button>
<div id="user-info" class="user-badge hidden"> <div id="user-info" class="user-badge hidden">
<span class="material-icons-round">person</span> <span class="material-icons-round">person</span>
<span id="user-id-display"></span> <span id="user-id-display"></span>
<button id="btn-logout" class="icon-btn-small" aria-label="Logout"> <button id="btn-logout" class="icon-btn-small" aria-label="Logout" title="Von Bessa.app abmelden">
<span class="material-icons-round">logout</span> <span class="material-icons-round">logout</span>
</button> </button>
</div> </div>
@@ -115,7 +134,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>Login</h2> <h2>Login</h2>
<button id="btn-login-close" class="icon-btn" aria-label="Close"> <button id="btn-login-close" class="icon-btn" aria-label="Close" title="Schließen">
<span class="material-icons-round">close</span> <span class="material-icons-round">close</span>
</button> </button>
</div> </div>
@@ -159,7 +178,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>Meine Highlights</h2> <h2>Meine Highlights</h2>
<button id="btn-highlights-close" class="icon-btn" aria-label="Close"> <button id="btn-highlights-close" class="icon-btn" aria-label="Close" title="Schließen">
<span class="material-icons-round">close</span> <span class="material-icons-round">close</span>
</button> </button>
</div> </div>
@@ -168,8 +187,8 @@
Markiere Menüs automatisch, wenn sie diese Schlagwörter enthalten. Markiere Menüs automatisch, wenn sie diese Schlagwörter enthalten.
</p> </p>
<div class="input-group"> <div class="input-group">
<input type="text" id="tag-input" placeholder="z.B. Schnitzel, Vegetarisch..."> <input type="text" id="tag-input" placeholder="z.B. Schnitzel, Vegetarisch..." title="Neues Schlagwort zum Hervorheben eingeben">
<button id="btn-add-tag" class="btn-primary">Hinzufügen</button> <button id="btn-add-tag" class="btn-primary" title="Schlagwort zur Liste hinzufügen">Hinzufügen</button>
</div> </div>
<div id="tags-list"></div> <div id="tags-list"></div>
</div> </div>
@@ -180,7 +199,7 @@
<div class="modal-content history-modal-content"> <div class="modal-content history-modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>Bestellhistorie</h2> <h2>Bestellhistorie</h2>
<button id="btn-history-close" class="icon-btn" aria-label="Close"> <button id="btn-history-close" class="icon-btn" aria-label="Close" title="Schließen">
<span class="material-icons-round">close</span> <span class="material-icons-round">close</span>
</button> </button>
</div> </div>
@@ -204,7 +223,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>📦 Versionen</h2> <h2>📦 Versionen</h2>
<button id="btn-version-close" class="icon-btn" aria-label="Close"> <button id="btn-version-close" class="icon-btn" aria-label="Close" title="Schließen">
<span class="material-icons-round">close</span> <span class="material-icons-round">close</span>
</button> </button>
</div> </div>
@@ -218,9 +237,20 @@
<span>Dev-Mode (alle Tags anzeigen)</span> <span>Dev-Mode (alle Tags anzeigen)</span>
</label> </label>
</div> </div>
<div id="version-list-container" style="margin-top:1rem;"> <div id="version-list-container" style="margin-top:1rem; max-height: 250px; overflow-y: auto;">
<p style="color:var(--text-secondary);">Lade Versionen...</p> <p style="color:var(--text-secondary);">Lade Versionen...</p>
</div> </div>
<div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border-color); display: flex; flex-direction: column; gap: 0.75rem; font-size: 0.9em;">
<a href="https://github.com/TauNeutrino/kantine-overview/issues" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Melde einen Fehler auf GitHub">
<span class="material-icons-round" style="font-size: 1.2em;">bug_report</span> Fehler melden
</a>
<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>
</div> </div>
</div> </div>
@@ -238,7 +268,7 @@
</main> </main>
<footer class="app-footer"> <footer class="app-footer">
<p>Bessa Knapp-Kantine Wrapper &bull; <span id="current-year">${new Date().getFullYear()}</span></p> <p>Jetzt Bessa Einfach! &bull; Knapp-Kantine Wrapper &bull; <span id="current-year">${new Date().getFullYear()}</span> by Kaufis-Kitchen</p>
</footer> </footer>
</div>`; </div>`;
} }
@@ -267,6 +297,29 @@
const historyModal = document.getElementById('history-modal'); const historyModal = document.getElementById('history-modal');
const btnHistoryClose = document.getElementById('btn-history-close'); const btnHistoryClose = document.getElementById('btn-history-close');
// Language Toggle
document.querySelectorAll('.lang-btn').forEach(btn => {
btn.addEventListener('click', () => {
langMode = btn.dataset.lang;
localStorage.setItem('kantine_lang', langMode);
document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderVisibleWeeks();
});
});
if (btnHighlights) {
btnHighlights.addEventListener('click', () => {
highlightsModal.classList.remove('hidden');
});
}
if (btnHighlightsClose) {
btnHighlightsClose.addEventListener('click', () => {
highlightsModal.classList.add('hidden');
});
}
btnHistory.addEventListener('click', () => { btnHistory.addEventListener('click', () => {
if (!authToken) { if (!authToken) {
loginModal.classList.remove('hidden'); loginModal.classList.remove('hidden');
@@ -304,6 +357,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) => { window.addEventListener('click', (e) => {
if (e.target === versionModal) versionModal.classList.add('hidden'); if (e.target === versionModal) versionModal.classList.add('hidden');
}); });
@@ -354,6 +422,7 @@
}); });
btnNextWeek.addEventListener('click', () => { btnNextWeek.addEventListener('click', () => {
btnNextWeek.classList.remove('new-week-available');
if (displayMode !== 'next-week') { if (displayMode !== 'next-week') {
displayMode = 'next-week'; displayMode = 'next-week';
btnNextWeek.classList.add('active'); btnNextWeek.classList.add('active');
@@ -411,8 +480,8 @@
if (response.ok) { if (response.ok) {
authToken = data.key; authToken = data.key;
currentUser = employeeId; currentUser = employeeId;
sessionStorage.setItem('kantine_authToken', data.key); localStorage.setItem('kantine_authToken', data.key);
sessionStorage.setItem('kantine_currentUser', employeeId); localStorage.setItem('kantine_currentUser', employeeId);
// Fetch user name // Fetch user name
try { try {
@@ -421,8 +490,8 @@
}); });
if (userResp.ok) { if (userResp.ok) {
const userData = await userResp.json(); const userData = await userResp.json();
if (userData.first_name) sessionStorage.setItem('kantine_firstName', userData.first_name); if (userData.first_name) localStorage.setItem('kantine_firstName', userData.first_name);
if (userData.last_name) sessionStorage.setItem('kantine_lastName', userData.last_name); if (userData.last_name) localStorage.setItem('kantine_lastName', userData.last_name);
} }
} catch (err) { } catch (err) {
console.error('Failed to fetch user info:', err); console.error('Failed to fetch user info:', err);
@@ -452,10 +521,10 @@
// Logout // Logout
btnLogout.addEventListener('click', () => { btnLogout.addEventListener('click', () => {
sessionStorage.removeItem('kantine_authToken'); localStorage.removeItem('kantine_authToken');
sessionStorage.removeItem('kantine_currentUser'); localStorage.removeItem('kantine_currentUser');
sessionStorage.removeItem('kantine_firstName'); localStorage.removeItem('kantine_firstName');
sessionStorage.removeItem('kantine_lastName'); localStorage.removeItem('kantine_lastName');
authToken = null; authToken = null;
currentUser = null; currentUser = null;
orderMap = new Map(); orderMap = new Map();
@@ -476,13 +545,13 @@
if (parsed.auth && parsed.auth.token) { if (parsed.auth && parsed.auth.token) {
console.log('Found existing Bessa session!'); console.log('Found existing Bessa session!');
authToken = parsed.auth.token; authToken = parsed.auth.token;
sessionStorage.setItem('kantine_authToken', authToken); localStorage.setItem('kantine_authToken', authToken);
if (parsed.auth.user) { if (parsed.auth.user) {
currentUser = parsed.auth.user.id || 'unknown'; currentUser = parsed.auth.user.id || 'unknown';
sessionStorage.setItem('kantine_currentUser', currentUser); localStorage.setItem('kantine_currentUser', currentUser);
if (parsed.auth.user.firstName) sessionStorage.setItem('kantine_firstName', parsed.auth.user.firstName); if (parsed.auth.user.firstName) localStorage.setItem('kantine_firstName', parsed.auth.user.firstName);
if (parsed.auth.user.lastName) sessionStorage.setItem('kantine_lastName', parsed.auth.user.lastName); if (parsed.auth.user.lastName) localStorage.setItem('kantine_lastName', parsed.auth.user.lastName);
} }
} }
} }
@@ -491,9 +560,9 @@
} }
} }
authToken = sessionStorage.getItem('kantine_authToken'); authToken = localStorage.getItem('kantine_authToken');
currentUser = sessionStorage.getItem('kantine_currentUser'); currentUser = localStorage.getItem('kantine_currentUser');
const firstName = sessionStorage.getItem('kantine_firstName'); const firstName = localStorage.getItem('kantine_firstName');
const btnLoginOpen = document.getElementById('btn-login-open'); const btnLoginOpen = document.getElementById('btn-login-open');
const userInfo = document.getElementById('user-info'); const userInfo = document.getElementById('user-info');
const userIdDisplay = document.getElementById('user-id-display'); const userIdDisplay = document.getElementById('user-id-display');
@@ -543,6 +612,7 @@
} }
console.log(`Fetched ${results.length} orders, mapped active ones.`); console.log(`Fetched ${results.length} orders, mapped active ones.`);
renderVisibleWeeks(); renderVisibleWeeks();
updateNextWeekBadge();
} }
} catch (error) { } catch (error) {
console.error('Error fetching orders:', error); console.error('Error fetching orders:', error);
@@ -558,24 +628,49 @@
const progressFill = document.getElementById('history-progress-fill'); const progressFill = document.getElementById('history-progress-fill');
const progressText = document.getElementById('history-progress-text'); const progressText = document.getElementById('history-progress-text');
// Check local storage cache (we still use memory cache if available)
let localCache = [];
if (fullOrderHistoryCache) { if (fullOrderHistoryCache) {
renderHistory(fullOrderHistoryCache); localCache = fullOrderHistoryCache;
return; } else {
const ls = localStorage.getItem('kantine_history_cache');
if (ls) {
try {
localCache = JSON.parse(ls);
fullOrderHistoryCache = localCache;
} catch (e) {
console.warn('History cache parse error', e);
}
}
}
// Show cached version immediately if we have one
if (localCache.length > 0) {
renderHistory(localCache);
} }
if (!authToken) return; if (!authToken) return;
historyContent.innerHTML = ''; // Start background delta sync
historyLoading.classList.remove('hidden'); if (localCache.length === 0) {
progressFill.style.width = '0%'; historyContent.innerHTML = '';
progressText.textContent = 'Lade Bestellhistorie...'; historyLoading.classList.remove('hidden');
}
let nextUrl = `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=50`; progressFill.style.width = '0%';
let allOrders = []; progressText.textContent = localCache.length > 0 ? 'Suche nach neuen Bestellungen...' : 'Lade Bestellhistorie...';
if (localCache.length > 0) historyLoading.classList.remove('hidden');
let nextUrl = localCache.length > 0
? `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=5`
: `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=50`;
let fetchedOrders = [];
let totalCount = 0; let totalCount = 0;
let requiresFullFetch = localCache.length === 0;
let deltaComplete = false;
try { try {
while (nextUrl) { while (nextUrl && !deltaComplete) {
const response = await fetch(nextUrl, { headers: apiHeaders(authToken) }); const response = await fetch(nextUrl, { headers: apiHeaders(authToken) });
if (!response.ok) throw new Error(`Fetch failed: ${response.status}`); if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
@@ -585,26 +680,75 @@
totalCount = data.count; totalCount = data.count;
} }
allOrders = allOrders.concat(data.results || []); const results = data.results || [];
// Update progress for (const order of results) {
if (totalCount > 0) { // Check if we hit an order that is already in our cache AND has the exact same state/update time
const pct = Math.round((allOrders.length / totalCount) * 100); // Bessa returns 'updated' timestamp, we can use it to determine if anything changed
progressFill.style.width = `${pct}%`; const existingOrderIndex = localCache.findIndex(cached => cached.id === order.id);
progressText.textContent = `Lade Bestellung ${allOrders.length} von ${totalCount}...`;
} else { if (!requiresFullFetch && existingOrderIndex !== -1) {
progressText.textContent = `Lade Bestellung ${allOrders.length}...`; const existingOrder = localCache[existingOrderIndex];
// If order exists and wasn't updated since our cache, we've reached the point
// where everything older is already correctly cached.
// order.updated is an ISO string like "2025-02-18T10:30:15.123456Z"
if (existingOrder.updated === order.updated && existingOrder.order_state === order.order_state) {
deltaComplete = true;
break;
}
}
fetchedOrders.push(order);
} }
nextUrl = data.next; // Update progress
if (!deltaComplete && requiresFullFetch) {
if (totalCount > 0) {
const pct = Math.round((fetchedOrders.length / totalCount) * 100);
progressFill.style.width = `${pct}%`;
progressText.textContent = `Lade Bestellung ${fetchedOrders.length} von ${totalCount}...`;
} else {
progressText.textContent = `Lade Bestellung ${fetchedOrders.length}...`;
}
} else if (!deltaComplete) {
progressText.textContent = `${fetchedOrders.length} neue/geänderte Bestellungen gefunden...`;
}
nextUrl = deltaComplete ? null : data.next;
} }
fullOrderHistoryCache = allOrders; // Merge fetched orders with cache
renderHistory(fullOrderHistoryCache); if (fetchedOrders.length > 0) {
// We have new/updated orders. We need to merge them into the cache.
// 1. Create a map of the existing cache for quick ID lookup
const cacheMap = new Map(localCache.map(o => [o.id, o]));
// 2. Update/Insert the newly fetched orders
for (const order of fetchedOrders) {
cacheMap.set(order.id, order); // Overwrites existing, or adds new
}
// 3. Convert back to array and sort by created date (descending)
const mergedOrders = Array.from(cacheMap.values());
mergedOrders.sort((a, b) => new Date(b.created) - new Date(a.created));
fullOrderHistoryCache = mergedOrders;
try {
localStorage.setItem('kantine_history_cache', JSON.stringify(mergedOrders));
} catch (e) {
console.warn('History cache write error', e);
}
// Render the updated history
renderHistory(fullOrderHistoryCache);
}
} catch (error) { } catch (error) {
console.error('Error fetching full history:', error); console.error('Error in history sync:', error);
historyContent.innerHTML = `<p style="color:var(--error-color);text-align:center;">Fehler beim Laden der Historie.</p>`; if (localCache.length === 0) {
historyContent.innerHTML = `<p style="color:var(--error-color);text-align:center;">Fehler beim Laden der Historie.</p>`;
} else {
showToast('Hintergrund-Synchronisation fehlgeschlagen', 'error');
}
} finally { } finally {
historyLoading.classList.add('hidden'); historyLoading.classList.add('hidden');
} }
@@ -673,7 +817,7 @@
const monthGroup = yearGroup.months[mKey]; const monthGroup = yearGroup.months[mKey];
html += `<div class="history-month-group"> html += `<div class="history-month-group">
<div class="history-month-header" tabindex="0" role="button" aria-expanded="false"> <div class="history-month-header" tabindex="0" role="button" aria-expanded="false" title="Klicken, um die Bestellungen für diesen Monat ein-/auszublenden">
<div style="display:flex; flex-direction:column; gap:4px;"> <div style="display:flex; flex-direction:column; gap:4px;">
<span>${monthGroup.name}</span> <span>${monthGroup.name}</span>
<div class="history-month-summary"> <div class="history-month-summary">
@@ -784,7 +928,7 @@
venue: VENUE_ID, venue: VENUE_ID,
states: [], states: [],
order_state: 1, order_state: 1,
date: `${date}T10:00:00.000Z`, date: `${date}T10:30:00Z`,
payment_method: 'payroll', payment_method: 'payroll',
customer: { customer: {
first_name: userData.first_name, first_name: userData.first_name,
@@ -792,7 +936,7 @@
email: userData.email, email: userData.email,
newsletter: false newsletter: false
}, },
preorder: false, preorder: true,
delivery_fee: 0, delivery_fee: 0,
cash_box_table_name: null, cash_box_table_name: null,
take_away: false take_away: false
@@ -806,6 +950,7 @@
if (response.ok || response.status === 201) { if (response.ok || response.status === 201) {
showToast(`Bestellt: ${name}`, 'success'); showToast(`Bestellt: ${name}`, 'success');
fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
await fetchOrders(); await fetchOrders();
} else { } else {
const data = await response.json(); const data = await response.json();
@@ -835,6 +980,7 @@
if (response.ok) { if (response.ok) {
showToast(`Storniert: ${name}`, 'success'); showToast(`Storniert: ${name}`, 'success');
fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
await fetchOrders(); await fetchOrders();
} else { } else {
const data = await response.json(); const data = await response.json();
@@ -851,31 +997,172 @@
localStorage.setItem('kantine_flags', JSON.stringify([...userFlags])); localStorage.setItem('kantine_flags', JSON.stringify([...userFlags]));
} }
async function refreshFlaggedItems() {
if (userFlags.size === 0) return;
const token = authToken || GUEST_TOKEN;
const datesToFetch = new Set();
for (const flagId of userFlags) {
const [dateStr] = flagId.split('_');
datesToFetch.add(dateStr);
}
let updated = false;
for (const dateStr of datesToFetch) {
try {
const resp = await fetch(`${API_BASE}/venues/${VENUE_ID}/menu/${MENU_ID}/${dateStr}/`, {
headers: apiHeaders(token)
});
if (!resp.ok) continue;
const data = await resp.json();
const menuGroups = data.results || [];
let dayItems = [];
for (const group of menuGroups) {
if (group.items && Array.isArray(group.items)) {
dayItems = dayItems.concat(group.items);
}
}
// Update allWeeks in memory
for (let week of allWeeks) {
if (!week.days) continue;
let dayObj = week.days.find(d => d.date === dateStr);
if (dayObj) {
dayObj.items = dayItems.map(item => {
const isUnlimited = item.amount_tracking === false;
const hasStock = parseInt(item.available_amount) > 0;
return {
id: `${dateStr}_${item.id}`,
articleId: item.id,
name: item.name || 'Unknown',
description: item.description || '',
price: parseFloat(item.price) || 0,
available: isUnlimited || hasStock,
availableAmount: parseInt(item.available_amount) || 0,
amountTracking: item.amount_tracking !== false
};
});
updated = true;
}
}
} catch (e) {
console.error('Error refreshing flag date', dateStr, e);
}
}
if (updated) {
saveMenuCache();
updateLastUpdatedTime(new Date().toISOString());
updateAlarmBell();
renderVisibleWeeks();
}
}
function updateAlarmBell() {
const bellBtn = document.getElementById('alarm-bell');
const bellIcon = document.getElementById('alarm-bell-icon');
if (!bellBtn || !bellIcon) return;
if (userFlags.size === 0) {
bellBtn.classList.add('hidden');
bellBtn.style.display = 'none';
bellIcon.style.color = 'var(--text-secondary)';
bellIcon.style.textShadow = 'none';
return;
}
bellBtn.classList.remove('hidden');
bellBtn.style.display = 'inline-flex';
// Check if any flagged item is available
let anyAvailable = false;
for (const wk of allWeeks) {
if (!wk.days) continue;
for (const d of wk.days) {
if (!d.items) continue;
for (const item of d.items) {
if (item.available && userFlags.has(item.id)) {
anyAvailable = true;
break;
}
}
if (anyAvailable) break;
}
if (anyAvailable) break;
}
let lastUpdatedStr = localStorage.getItem('kantine_last_updated');
let timeStr = 'gerade eben'; // Fallback instead of Unbekannt
if (!lastUpdatedStr) {
lastUpdatedStr = new Date().toISOString();
localStorage.setItem('kantine_last_updated', lastUpdatedStr);
}
const lastUpdated = new Date(lastUpdatedStr);
const diffMs = Date.now() - lastUpdated.getTime();
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) timeStr = 'gerade eben';
else if (diffMins < 60) timeStr = `vor ${diffMins} Min.`;
else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`;
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
if (anyAvailable) {
bellIcon.style.color = '#10b981'; // green / success
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
} else {
bellIcon.style.color = '#f59e0b'; // yellow / warning
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
}
}
function toggleFlag(date, articleId, name, cutoff) { function toggleFlag(date, articleId, name, cutoff) {
const id = `${date}_${articleId}`; const id = `${date}_${articleId}`;
let flagAdded = false;
if (userFlags.has(id)) { if (userFlags.has(id)) {
userFlags.delete(id); userFlags.delete(id);
showToast(`Flag entfernt für ${name}`, 'success'); showToast(`Flag entfernt für ${name}`, 'success');
} else { } else {
userFlags.add(id); userFlags.add(id);
flagAdded = true;
showToast(`Benachrichtigung aktiviert für ${name}`, 'success'); showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
if (Notification.permission === 'default') { if (Notification.permission === 'default') {
Notification.requestPermission(); Notification.requestPermission();
} }
} }
saveFlags(); saveFlags();
updateAlarmBell();
renderVisibleWeeks(); renderVisibleWeeks();
if (flagAdded) {
refreshFlaggedItems();
}
} }
// FR-019: Auto-remove flags whose cutoff has passed // FR-019: Auto-remove flags whose cutoff has passed
function cleanupExpiredFlags() { function cleanupExpiredFlags() {
const now = new Date(); const now = new Date();
const todayStr = now.toISOString().split('T')[0]; // Format: YYYY-MM-DD
let changed = false; let changed = false;
for (const flagId of [...userFlags]) { for (const flagId of [...userFlags]) {
const [date] = flagId.split('_'); const [dateStr] = flagId.split('_'); // Format usually is YYYY-MM-DD
const cutoff = new Date(date);
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00 // If the flag's date string is entirely in the past (before today)
if (now >= cutoff) { // 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); userFlags.delete(flagId);
changed = true; changed = true;
} }
@@ -976,7 +1263,7 @@
highlightTags.forEach(tag => { highlightTags.forEach(tag => {
const badge = document.createElement('span'); const badge = document.createElement('span');
badge.className = 'tag-badge'; badge.className = 'tag-badge';
badge.innerHTML = `${tag} <span class="tag-remove" data-tag="${tag}">&times;</span>`; badge.innerHTML = `${tag} <span class="tag-remove" data-tag="${tag}" title="Schlagwort entfernen">&times;</span>`;
list.appendChild(badge); list.appendChild(badge);
}); });
@@ -1020,7 +1307,27 @@
console.log(`[Cache] Parsed ${allWeeks.length} weeks:`, allWeeks.map(w => `KW${w.weekNumber}/${w.year} (${(w.days || []).length} days)`)); console.log(`[Cache] Parsed ${allWeeks.length} weeks:`, allWeeks.map(w => `KW${w.weekNumber}/${w.year} (${(w.days || []).length} days)`));
renderVisibleWeeks(); renderVisibleWeeks();
updateNextWeekBadge(); updateNextWeekBadge();
updateAlarmBell();
if (cachedTs) updateLastUpdatedTime(cachedTs); if (cachedTs) updateLastUpdatedTime(cachedTs);
// --- TEMP DEBUG LOGGER ---
try {
const uniqueMenus = new Set();
allWeeks.forEach(w => {
(w.days || []).forEach(d => {
(d.items || []).forEach(item => {
let text = (item.description || '').replace(/\s+/g, ' ').trim();
if (text && text.includes(' / ')) {
uniqueMenus.add(text);
}
});
});
});
const res = Array.from(uniqueMenus).join('\n\n');
console.log("=== GEFUNDENE MENÜ-TEXTE (" + uniqueMenus.size + ") ===");
console.log(res);
} catch (e) { }
console.log('Loaded menu from cache'); console.log('Loaded menu from cache');
return true; return true;
} }
@@ -1232,6 +1539,7 @@
updateAuthUI(); // This will trigger fetchOrders if logged in updateAuthUI(); // This will trigger fetchOrders if logged in
renderVisibleWeeks(); renderVisibleWeeks();
updateNextWeekBadge(); updateNextWeekBadge();
updateAlarmBell();
progressMessage.textContent = 'Fertig!'; progressMessage.textContent = 'Fertig!';
setTimeout(() => progressModal.classList.add('hidden'), 500); setTimeout(() => progressModal.classList.add('hidden'), 500);
@@ -1389,6 +1697,19 @@
badge.classList.add('has-highlights'); badge.classList.add('has-highlights');
} }
// 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');
showToast('Neue Menüdaten für nächste Woche verfügbar!', 'info');
}
} else {
btnNextWeek.classList.remove('new-week-available');
}
} else if (badge) { } else if (badge) {
badge.remove(); badge.remove();
} }
@@ -1703,7 +2024,7 @@
<div class="badges">${statusBadge}</div> <div class="badges">${statusBadge}</div>
</div> </div>
${tagsHtml} ${tagsHtml}
<p class="item-desc">${escapeHtml(item.description)}</p>`; <p class="item-desc">${escapeHtml(getLocalizedText(item.description))}</p>`;
// Event: Order // Event: Order
const orderBtn = itemEl.querySelector('.btn-order'); const orderBtn = itemEl.querySelector('.btn-order');
@@ -1854,19 +2175,8 @@
const dm = devToggle.checked; const dm = devToggle.checked;
container.innerHTML = '<p style="color:var(--text-secondary);">Lade Versionen...</p>'; container.innerHTML = '<p style="color:var(--text-secondary);">Lade Versionen...</p>';
try { function renderVersionsList(versions) {
let versions; if (!versions || !versions.length) {
const cached = JSON.parse(localStorage.getItem('kantine_version_cache') || 'null');
if (!forceRefresh && cached && cached.devMode === dm && (Date.now() - cached.timestamp < 3600000)) {
versions = cached.versions;
} else {
versions = await fetchVersions(dm);
localStorage.setItem('kantine_version_cache', JSON.stringify({
timestamp: Date.now(), devMode: dm, versions
}));
}
if (!versions.length) {
container.innerHTML = '<p style="color:var(--text-secondary);">Keine Versionen gefunden.</p>'; container.innerHTML = '<p style="color:var(--text-secondary);">Keine Versionen gefunden.</p>';
return; return;
} }
@@ -1898,6 +2208,34 @@
`; `;
list.appendChild(li); list.appendChild(li);
}); });
}
try {
// 1. Show cached versions immediately if available
const cachedRaw = localStorage.getItem('kantine_version_cache');
let cached = null;
if (cachedRaw) {
try { cached = JSON.parse(cachedRaw); } catch (e) { }
}
if (cached && cached.devMode === dm && cached.versions) {
renderVersionsList(cached.versions);
}
// 2. Fetch fresh versions in background (or foreground if no cache)
const liveVersions = await fetchVersions(dm);
// Compare with cache to see if we need to re-render
const liveVersionsStr = JSON.stringify(liveVersions);
const cachedVersionsStr = cached ? JSON.stringify(cached.versions) : '';
if (liveVersionsStr !== cachedVersionsStr) {
localStorage.setItem('kantine_version_cache', JSON.stringify({
timestamp: Date.now(), devMode: dm, versions: liveVersions
}));
renderVersionsList(liveVersions);
}
} catch (e) { } catch (e) {
container.innerHTML = `<p style="color:#e94560;">Fehler: ${e.message}</p>`; container.innerHTML = `<p style="color:#e94560;">Fehler: ${e.message}</p>`;
} }
@@ -1916,6 +2254,12 @@
// === Order Countdown === // === Order Countdown ===
function updateCountdown() { function updateCountdown() {
// Only show order alarms for logged-in users
if (!authToken || !currentUser) {
removeCountdown();
return;
}
const now = new Date(); const now = new Date();
const currentDay = now.getDay(); const currentDay = now.getDay();
// Skip weekends (0=Sun, 6=Sat) // Skip weekends (0=Sun, 6=Sat)
@@ -1980,7 +2324,7 @@
// Notification logic (One time) // Notification logic (One time)
const notifiedKey = `kantine_notified_${todayStr}`; const notifiedKey = `kantine_notified_${todayStr}`;
if (!sessionStorage.getItem(notifiedKey)) { if (!localStorage.getItem(notifiedKey)) {
if (Notification.permission === 'granted') { if (Notification.permission === 'granted') {
new Notification('Kantine: Bestellschluss naht!', { new Notification('Kantine: Bestellschluss naht!', {
body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!', body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!',
@@ -1989,7 +2333,7 @@
} else if (Notification.permission === 'default') { } else if (Notification.permission === 'default') {
Notification.requestPermission(); Notification.requestPermission();
} }
sessionStorage.setItem(notifiedKey, 'true'); localStorage.setItem(notifiedKey, 'true');
} }
} else { } else {
countdownEl.classList.remove('urgent'); countdownEl.classList.remove('urgent');
@@ -2033,6 +2377,208 @@
return div.innerHTML; return div.innerHTML;
} }
// === Language Filter (FR-100) ===
// DE stems for fallback language detection
const DE_STEMS = [
'apfel', 'aubergine', 'auflauf', 'beere', 'blumenkohl', 'bohne', 'braten', 'brokkoli', 'brot', 'brust',
'brötchen', 'butter', 'chili', 'dessert', 'dip', 'eier', 'eintopf', 'eis', 'erbse', 'erdbeer',
'essig', 'filet', 'fisch', 'fisole', 'fleckerl', 'fleisch', 'flügel', 'frucht', 'für', 'gebraten',
'gemüse', 'gewürz', 'gratin', 'grieß', 'gulasch', 'gurke', 'himbeer', 'honig', 'huhn', 'hähnchen',
'jambalaya', 'joghurt', 'karotte', 'kartoffel', 'keule', 'kirsch', 'knacker', 'knoblauch', 'knödel', 'kompott',
'kraut', 'kräuter', 'kuchen', 'käse', 'kürbis', 'lauch', 'mandel', 'milch', 'mild', 'mit',
'mohn', 'most', 'möhre', 'natur', 'nockerl', 'nudel', 'nuss', 'nuß', 'obst', 'oder',
'olive', 'paprika', 'pfanne', 'pfannkuchen', 'pfeffer', 'pikant', 'pilz', 'plunder', 'püree', 'ragout',
'rahm', 'reis', 'rind', 'sahne', 'salami', 'salat', 'salz', 'sauer', 'scharf', 'schinken',
'schnitte', 'schnitzel', 'schoko', 'schupf', 'schwein', 'sellerie', 'senf', 'sosse', 'soße', 'spargel',
'spätzle', 'speck', 'spieß', 'spinat', 'steak', 'suppe', 'süß', 'tofu', 'tomate', 'topfen',
'torte', 'trüffel', 'und', 'vanille', 'vogerl', 'vom', 'wien', 'wurst', 'zucchini', 'zum',
'zur', 'zwiebel', 'öl'
];
const EN_STEMS = [
'almond', 'and', 'apple', 'asparagus', 'bacon', 'baked', 'ball', 'bean', 'beef', 'berry',
'bread', 'breast', 'broccoli', 'bun', 'butter', 'cabbage', 'cake', 'caper', 'carrot', 'casserole',
'cauliflower', 'celery', 'cheese', 'cherry', 'chicken', 'chili', 'choco', 'chocolate', 'cider', 'cilantro',
'coffee', 'compote', 'cream', 'cucumber', 'curd', 'danish', 'dessert', 'dip', 'dumpling', 'egg',
'eggplant', 'filet', 'fish', 'for', 'fried', 'from', 'fruit', 'garlic', 'goulash', 'gratin',
'ham', 'herb', 'honey', 'hot', 'ice', 'jambalaya', 'leek', 'leg', 'mash', 'meat',
'mexican', 'mild', 'milk', 'mint', 'mushroom', 'mustard', 'noodle', 'nut', 'oat', 'oil',
'olive', 'onion', 'or', 'oven', 'pan', 'pancake', 'pea', 'pepper', 'plain', 'plate',
'poppy', 'pork', 'potato', 'pumpkin', 'radish', 'ragout', 'raspberry', 'rice', 'roast', 'roll',
'salad', 'salami', 'salt', 'sauce', 'sausage', 'shrimp', 'skewer', 'slice', 'soup', 'sour',
'spice', 'spicy', 'spinach', 'steak', 'stew', 'strawberr', 'strawberry', 'strudel', 'sweet', 'tart',
'thyme', 'to', 'tofu', 'tomat', 'tomato', 'truffle', 'trukey', 'turkey', 'vanilla', 'vegan',
'vegetable', 'vinegar', 'wedge', 'wing', 'with', 'wok', 'yogurt', 'zucchini'
];
/**
* Splits bilingual menu text into DE and EN parts.
* Pattern per course: [DE] / [EN](ALLERGENS)
* Max 3 courses per menu item (sanity check).
* @param {string} text - The bilingual description text
* @returns {{ de: string, en: string, raw: string }}
*/
function splitLanguage(text) {
if (!text) return { de: '', en: '', raw: '' };
const raw = text;
// Formatting: add • for new lines, using the forgiving regex
let formattedRaw = text.replace(/(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*(?=\S)/g, '($1)\n• ');
if (!formattedRaw.startsWith('• ')) {
formattedRaw = '• ' + formattedRaw;
}
// Utility to compute DE/EN score for a subset of words
function scoreBlock(wordArray) {
let de = 0, en = 0;
wordArray.forEach(word => {
const w = word.toLowerCase().replace(/[^a-zäöüß]/g, '');
if (w) {
let bestDeMatch = 0;
let bestEnMatch = 0;
// Full match is better than partial string match
if (DE_STEMS.includes(w)) bestDeMatch = w.length;
else DE_STEMS.forEach(s => { if (w.includes(s) && s.length > bestDeMatch) bestDeMatch = s.length; });
if (EN_STEMS.includes(w)) bestEnMatch = w.length;
else EN_STEMS.forEach(s => { if (w.includes(s) && s.length > bestEnMatch) bestEnMatch = s.length; });
if (bestDeMatch > 0) de += (bestDeMatch / w.length);
if (bestEnMatch > 0) en += (bestEnMatch / w.length);
// Capitalized noun heuristic matches German text styles typically
if (/^[A-ZÄÖÜ]/.test(word)) {
de += 0.5;
}
}
});
return { de, en };
}
// Heuristic sliding window to split a fragment containing "EN DE"
function heuristicSplitEnDe(fragment) {
const words = fragment.trim().split(/\s+/);
if (words.length < 2) return { enPart: fragment, nextDe: '' };
let bestK = -1;
let maxScore = -9999;
for (let k = 1; k < words.length; k++) {
const left = words.slice(0, k);
const right = words.slice(k);
const leftScore = scoreBlock(left);
const rightScore = scoreBlock(right);
const rightFirstWord = right[0];
let capitalBonus = 0;
// Nouns are capitalized in German
if (/^[A-ZÄÖÜ]/.test(rightFirstWord)) {
capitalBonus = 1.0;
}
const score = (leftScore.en - leftScore.de) + (rightScore.de - rightScore.en) + capitalBonus;
// Strict condition! The assumed German part must actually look German
const rightLooksGerman = (rightScore.de + capitalBonus) > rightScore.en;
if (rightLooksGerman && score > maxScore) {
maxScore = score;
bestK = k;
}
}
if (bestK !== -1) {
return {
enPart: words.slice(0, bestK).join(' '),
nextDe: words.slice(bestK).join(' ')
};
}
return { enPart: fragment, nextDe: '' };
}
// NEW LOGIC: We no longer split by slash if the slash is part of a missing-parenthesis allergen like /ACGL)
const parts = text.split(/\s*\/\s*(?![A-Z,]+\))/);
// Sanity check: max 3 courses means max 3 slashes → max 4 parts
if (parts.length > 4) {
return { de: formattedRaw, en: '', raw: formattedRaw };
}
const deParts = [];
const enParts = [];
// Part 0 is ALWAYS German (beginning of the menu item)
deParts.push(parts[0].trim());
// Matches e.g., "(GLM)" OR "/GLM)" OR " GLM)" with trailing spaces
const allergenRegex = /(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*/;
for (let i = 1; i < parts.length; i++) {
const fragment = parts[i].trim();
const match = fragment.match(allergenRegex);
if (match) {
const allergenEnd = match.index + match[0].length;
const enPart = fragment.substring(0, match.index).trim();
const allergenCode = match[1];
const nextDe = fragment.substring(allergenEnd).trim();
enParts.push(enPart + '(' + allergenCode + ')');
if (deParts.length > 0) {
deParts[deParts.length - 1] = deParts[deParts.length - 1] + '(' + allergenCode + ')';
}
if (nextDe) {
deParts.push(nextDe);
}
} else {
// No allergen code found! Need to heuristically split "EN DE"
const split = heuristicSplitEnDe(fragment);
enParts.push(split.enPart);
if (split.nextDe) {
deParts.push(split.nextDe);
}
}
}
// FIX FOR SINGLE-LANGUAGE COURSES OR MISSING EN
if (parts.length === 1 && enParts.length === 0) {
enParts.push(deParts[0]);
}
// Mirror untranslated DE courses to EN (e.g. Dessert)
if (deParts.length > enParts.length) {
for (let i = enParts.length; i < deParts.length; i++) {
enParts.push(deParts[i]);
}
}
let deJoined = deParts.join('\n• ');
if (deParts.length > 0 && !deJoined.startsWith('• ')) deJoined = '• ' + deJoined;
let enJoined = enParts.join('\n• ');
if (enParts.length > 0 && !enJoined.startsWith('• ')) enJoined = '• ' + enJoined;
return {
de: deJoined,
en: enJoined,
raw: formattedRaw
};
}
/**
* Returns text filtered by the current language mode.
* @param {string} text - The bilingual text
* @returns {string}
*/
function getLocalizedText(text) {
if (langMode === 'all') return text || '';
const split = splitLanguage(text);
if (langMode === 'en') return split.en || split.raw;
return split.de || split.raw; // 'de' is default
}
// === Bootstrap === // === Bootstrap ===
injectUI(); injectUI();
bindEvents(); bindEvents();
Executable
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# release.sh - Deploys a new version of the Kantine Wrapper
# Ensure we're in the script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
cd "$SCRIPT_DIR"
# Ensure tests have run and artifacts exist
if [ ! -d "$SCRIPT_DIR/dist" ]; then
echo "❌ Error: dist folder missing. Please run build-bookmarklet.sh first"
exit 1
fi
# Get current version
VERSION=$(cat "version.txt" | tr -d '\n\r ')
# Validate that version is set
if [ -z "$VERSION" ]; then
echo "❌ Error: Could not determine version from version.txt"
exit 1
fi
echo "=== Kantine Bookmarklet Releaser ($VERSION) ==="
# Check for uncommitted changes (excluding dist/)
if ! git diff-index --quiet HEAD -- ":(exclude)dist"; then
echo "⚠️ Warning: You have uncommitted changes in the working directory."
echo "Please commit your code changes before running the release script."
exit 1
fi
echo "=== Committing build artifacts ==="
git add "dist/"
git commit -m "chore: update build artifacts for $VERSION" --allow-empty
echo ""
echo "=== Tagging $VERSION ==="
if git rev-parse "$VERSION" >/dev/null 2>&1; then
git tag -f "$VERSION"
echo "🔄 Tag $VERSION moved to current commit."
else
git tag "$VERSION"
echo "✅ Created tag: $VERSION"
fi
echo ""
echo "=== Pushing to remotes ==="
# Determine remote targets: Assume 'origin' for primary, optionally 'github'
git push origin HEAD
git push origin --force tag "$VERSION"
# If a remote named 'github' exists, push branch and tags there too
if git remote | grep -q "^github$"; then
git push github HEAD
git push github --force tag "$VERSION"
fi
echo "🎉 Successfully released version $VERSION!"
exit 0
+158 -44
View File
@@ -56,12 +56,22 @@ body {
} }
/* Fix scrolling bug: Reset html/body styles from host page */ /* Fix scrolling bug: Reset html/body styles from host page */
html, /* IMPORTANT: html must NOT have overflow set, or it creates a scroll container that breaks position: sticky */
html {
height: auto !important;
min-height: 100% !important;
overflow: visible !important;
position: static !important;
margin: 0 !important;
padding: 0 !important;
}
body { body {
height: auto !important; height: auto !important;
min-height: 100% !important; min-height: 100% !important;
overflow-y: auto !important; overflow-x: clip !important;
overflow-x: hidden !important; /* clip prevents horizontal overflow without breaking sticky */
overflow-y: visible !important;
position: static !important; position: static !important;
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
@@ -69,8 +79,7 @@ body {
/* Header */ /* Header */
.app-header { .app-header {
position: sticky; flex-shrink: 0;
top: 0;
z-index: 100; z-index: 100;
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
background-color: var(--header-bg); background-color: var(--header-bg);
@@ -153,6 +162,38 @@ body {
color: var(--text-secondary); color: var(--text-secondary);
} }
/* Language Toggle (FR-100) */
.lang-toggle {
display: inline-flex;
gap: 0;
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--border-color);
background: var(--bg-card);
}
.lang-btn {
padding: 3px 10px;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.03em;
background: transparent;
color: var(--text-secondary);
border: none;
cursor: pointer;
transition: all 0.2s;
}
.lang-btn:hover {
color: var(--text-primary);
background: rgba(100, 116, 139, 0.1);
}
.lang-btn.active {
background: var(--accent-color);
color: white;
}
.nav-group { .nav-group {
display: flex; display: flex;
background-color: var(--bg-card); background-color: var(--bg-card);
@@ -186,6 +227,32 @@ body {
color: white; color: white;
} }
/* Notification state for Next Week */
.nav-btn.new-week-available {
animation: goldPulse 2s infinite;
border-color: #f59e0b;
color: var(--accent-color);
}
.nav-btn.new-week-available.active {
color: white;
}
@keyframes goldPulse {
0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(245, 158, 11, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
}
}
/* Badge for nav buttons (day count indicator) */ /* Badge for nav buttons (day count indicator) */
.nav-badge { .nav-badge {
background-color: var(--error-color); background-color: var(--error-color);
@@ -354,13 +421,21 @@ body {
font-size: 18px; font-size: 18px;
} }
/* Container */ /* Container - flex column, full width so child scrollbar is at edge */
.container { .container {
flex: 1;
width: 100%; width: 100%;
/* Full width */ overflow: hidden;
margin: 2rem auto; padding: 0 0 0 0;
padding: 0 2rem; /* Only top padding, no horizontal so child fills width */
min-height: 80vh; display: flex;
flex-direction: column;
}
/* Add horizontal padding to direct children of container to maintain layout */
.container>*:not(.menu-grid) {
padding-left: 2rem;
padding-right: 2rem;
} }
/* Banner */ /* Banner */
@@ -709,14 +784,17 @@ body {
display: none !important; display: none !important;
} }
/* Menu Grid */ /* Menu Grid Container */
.menu-grid { .menu-grid {
display: grid; display: flex;
gap: 2rem; flex-direction: column;
flex: 1;
overflow: hidden;
gap: 1rem;
} }
.week-section { .week-section {
margin-bottom: 3rem; margin-bottom: 2rem;
} }
.week-header { .week-header {
@@ -738,10 +816,25 @@ body {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
/* Full-viewport layout: header + scrollable content + footer */
#kantine-wrapper {
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
/* Dynamic viewport height for mobile browsers */
overflow: hidden;
}
.days-grid { .days-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem; gap: 0.75rem;
flex: 1;
overflow-y: auto;
/* This is the scroll container at the window edge */
align-content: start;
padding: 0 2rem 2rem 2rem;
} }
/* Card */ /* Card */
@@ -750,21 +843,33 @@ body {
border-radius: 12px; border-radius: 12px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
overflow: hidden; overflow: clip;
transition: transform 0.2s ease, box-shadow 0.2s ease; /* Clips scrolling content behind sticky header */
transition: box-shadow 0.2s ease;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
/* Past Day Styling - Target specific elements so ordered items can remain visible */ /* Past Day Styling - Target specific elements so ordered items can remain visible AND preserve sticky context */
.menu-card.past-day .card-header, /* We MUST apply filter/opacity to children, not the parent .menu-card, or else position: sticky breaks */
/* Header keeps fully opaque background to hide scrolling items, only grayscales */
.menu-card.past-day .card-header {
filter: grayscale(0.8);
transition: filter 0.3s;
}
/* Items become semi-transparent */
.menu-card.past-day .menu-item:not(.ordered) { .menu-card.past-day .menu-item:not(.ordered) {
opacity: 0.6; opacity: 0.6;
filter: grayscale(0.8); filter: grayscale(0.8);
transition: opacity 0.3s, filter 0.3s; transition: opacity 0.3s, filter 0.3s;
} }
.menu-card.past-day:hover .card-header, .menu-card.past-day:hover .card-header {
filter: grayscale(0.4);
}
.menu-card.past-day:hover .menu-item:not(.ordered) { .menu-card.past-day:hover .menu-item:not(.ordered) {
opacity: 0.8; opacity: 0.8;
filter: grayscale(0.4); filter: grayscale(0.4);
@@ -775,7 +880,6 @@ body {
/* No opacity/filter here - fully visible */ /* No opacity/filter here - fully visible */
background: var(--bg-card); background: var(--bg-card);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid var(--accent-color);
border-radius: 8px; border-radius: 8px;
padding: 1rem; padding: 1rem;
margin: 0 -1rem 1.5rem -1rem; margin: 0 -1rem 1.5rem -1rem;
@@ -784,34 +888,33 @@ body {
} }
.menu-item.today-ordered { .menu-item.today-ordered {
border: 2px solid var(--accent-color); border: 2px solid #8b5cf6;
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4); box-shadow: 0 0 30px rgba(139, 92, 246, 0.6);
border-radius: 8px; border-radius: 8px;
padding: 1rem; padding: 1rem;
margin: 0 -1rem 1.5rem -1rem; margin: 0 -1rem 1.5rem -1rem;
background: var(--bg-card); background: var(--bg-card);
position: relative; position: relative;
z-index: 5; z-index: 5;
animation: pulse-glow 3s infinite; animation: pulse-glow-strong 3s infinite;
} }
@keyframes pulse-glow { @keyframes pulse-glow-strong {
0% { 0% {
box-shadow: 0 0 15px rgba(139, 92, 246, 0.3); box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
} }
50% { 50% {
box-shadow: 0 0 25px rgba(139, 92, 246, 0.6); box-shadow: 0 0 40px rgba(139, 92, 246, 0.8);
} }
100% { 100% {
box-shadow: 0 0 15px rgba(139, 92, 246, 0.3); box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
} }
} }
.menu-card:hover { .menu-card:hover {
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
} }
@@ -821,7 +924,23 @@ body {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
background-color: rgba(100, 116, 139, 0.05); background-color: var(--bg-card);
/* Removed border-radius: 12px 12px 0 0;
.menu-card's overflow: clip will round the corners initially.
When sticky at the top, it will be square and perfectly hide scrolling content! */
/* Sticky within .container scroll area */
position: sticky;
top: 0;
z-index: 90;
}
.card-body {
padding: 1.25rem;
display: grid;
grid-template-rows: auto;
align-content: start;
} }
.day-name { .day-name {
@@ -834,13 +953,6 @@ body {
color: var(--text-secondary); color: var(--text-secondary);
} }
.card-body {
padding: 1.25rem;
display: grid;
grid-template-rows: auto;
/* Each menu item gets its own row */
align-content: start;
}
.empty-state { .empty-state {
color: var(--text-secondary); color: var(--text-secondary);
@@ -887,6 +999,7 @@ body {
color: var(--text-secondary); color: var(--text-secondary);
line-height: 1.6; line-height: 1.6;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
white-space: pre-wrap;
} }
.badges { .badges {
@@ -968,12 +1081,12 @@ body {
/* Footer */ /* Footer */
.app-footer { .app-footer {
flex-shrink: 0;
text-align: center; text-align: center;
padding: 2rem; padding: 0.4rem 2rem;
color: var(--text-secondary); color: var(--text-secondary);
font-size: 0.875rem; font-size: 0.8rem;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
margin-top: auto;
} }
/* === Order / Cancel Buttons (inline in status row) === */ /* === Order / Cancel Buttons (inline in status row) === */
@@ -1304,17 +1417,20 @@ body {
/* Day Header Status Colors (User Request) */ /* Day Header Status Colors (User Request) */
.card-header.header-violet { .card-header.header-violet {
background-color: rgba(139, 92, 246, 0.15); background-color: var(--bg-card);
background-image: linear-gradient(rgba(139, 92, 246, 0.15), rgba(139, 92, 246, 0.15));
border-bottom: 2px solid #8b5cf6; border-bottom: 2px solid #8b5cf6;
} }
.card-header.header-green { .card-header.header-green {
background-color: rgba(16, 185, 129, 0.15); background-color: var(--bg-card);
background-image: linear-gradient(rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.15));
border-bottom: 2px solid var(--success-color); border-bottom: 2px solid var(--success-color);
} }
.card-header.header-red { .card-header.header-red {
background-color: rgba(239, 68, 68, 0.15); background-color: var(--bg-card);
background-image: linear-gradient(rgba(239, 68, 68, 0.15), rgba(239, 68, 68, 0.15));
border-bottom: 2px solid var(--error-color); border-bottom: 2px solid var(--error-color);
} }
@@ -1595,8 +1711,6 @@ body {
.version-list { .version-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
max-height: 350px;
overflow-y: auto;
margin: 0; margin: 0;
} }
+17
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'));
}
}
+19 -5
View File
@@ -41,20 +41,32 @@ const sandbox = {
fetch: async (url) => { fetch: async (url) => {
// Mock Version Check // Mock Version Check
if (url.includes('version.txt')) { if (url.includes('version.txt')) {
return { ok: true, text: async () => 'v9.9.9' }; // Simulate new version return { ok: true, text: async () => 'v9.9.9', json: async () => ({}) };
} }
// Mock Changelog // Mock Changelog
if (url.includes('changelog.md')) { if (url.includes('changelog.md')) {
return { ok: true, text: async () => '## v9.9.9\n- Feature: Cool Stuff' }; return { ok: true, text: async () => '## v9.9.9\n- Feature: Cool Stuff', json: async () => ({}) };
} }
return { ok: false }; // Fail others to prevent huge cascades // Mock GitHub Tags API
if (url.includes('api.github.com/') || url.includes('/tags')) {
return { ok: true, json: async () => [{ name: 'v9.9.9' }] };
}
// Mock Menu API
if (url.includes('/food-menu/menu/')) {
return { ok: true, json: async () => ({ dates: [], menu: {} }) };
}
// Mock Orders API
if (url.includes('/user/orders')) {
return { ok: true, json: async () => ({ results: [], count: 0 }) };
}
return { ok: false, status: 404, text: async () => '', json: async () => ({}) };
}, },
document: { document: {
body: createMockElement('body'), body: createMockElement('body'),
head: createMockElement('head'), head: createMockElement('head'),
createElement: (tag) => createMockElement(tag), createElement: (tag) => createMockElement(tag),
querySelector: (sel) => { querySelector: (sel) => {
if (sel === '.material-icons-round.logo-icon') { if (sel === '.logo-img' || sel === '.material-icons-round.logo-icon') {
const el = createMockElement('last-updated-icon-mock'); const el = createMockElement('last-updated-icon-mock');
// Mock legacy prop for specific test check if needed, // Mock legacy prop for specific test check if needed,
// but our generic mock handles replaceWith hook // but our generic mock handles replaceWith hook
@@ -62,6 +74,7 @@ const sandbox = {
} }
return createMockElement('query-result'); return createMockElement('query-result');
}, },
querySelectorAll: () => [createMockElement()],
getElementById: (id) => createMockElement(id), getElementById: (id) => createMockElement(id),
documentElement: { documentElement: {
setAttribute: () => { }, setAttribute: () => { },
@@ -108,7 +121,8 @@ try {
[/function\s+isNewer/, 'isNewer function'], [/function\s+isNewer/, 'isNewer function'],
[/function\s+openVersionMenu/, 'openVersionMenu function'], [/function\s+openVersionMenu/, 'openVersionMenu function'],
[/kantine_dev_mode/, 'dev-mode localStorage key'], [/kantine_dev_mode/, 'dev-mode localStorage key'],
[/function\s+isCacheFresh/, 'isCacheFresh function'] [/function\s+isCacheFresh/, 'isCacheFresh function'],
[/limit=5/, 'Delta fetch limit parameter']
]; ];
for (const [regex, label] of checks) { for (const [regex, label] of checks) {
+189
View File
@@ -0,0 +1,189 @@
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>
<head>
<style>
.hidden { display: none !important; }
.icon-btn { display: inline-flex; }
</style>
</head>
<body>
<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>
<!-- Mocks for Language Toggle -->
<button class="lang-btn" data-lang="de">DE</button>
<button class="lang-btn" data-lang="en">EN</button>
<button class="lang-btn" data-lang="all">ALL</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>
`;
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;');
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: () => { } };
global.sessionStorage = { getItem: () => null };
global.showToast = () => { };
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;
log("Before eval...");
const testCode = `
console.log("--- Testing Alarm Bell ---");
// Add flag
userFlags.add('2026-02-24_123'); updateAlarmBell();
if (document.getElementById('alarm-bell').className.includes('hidden')) throw new Error("Bell should be visible");
// Remove flag
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);
+1 -1
View File
@@ -1 +1 @@
v1.4.2 v1.6.4