Compare commits
130 Commits
v1.3.0
...
089e5375b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
089e5375b4 | ||
|
|
e514f42dbe | ||
|
|
f7a9b061ea | ||
|
|
fe75347682 | ||
|
|
5c5255058c | ||
|
|
e16398ef74 | ||
|
|
6a57e2716f | ||
|
|
d383808f4f | ||
|
|
498b400033 | ||
|
|
884a17e7d3 | ||
|
|
ad660799ec | ||
|
|
a5fe50bbf0 | ||
|
|
a98faec51e | ||
|
|
212bf3b015 | ||
|
|
f29ecd4b79 | ||
|
|
1be6e44d7f | ||
|
|
49b0ab17ac | ||
|
|
55e738a554 | ||
|
|
45adfa9d5d | ||
|
|
f5f6dddba3 | ||
|
|
b06f6c3551 | ||
|
|
b66030dce5 | ||
|
|
8e7ec468d4 | ||
|
|
8ce3ae4c92 | ||
|
|
6a70a5a5e8 | ||
|
|
edec109552 | ||
|
|
a7aea2ece3 | ||
|
|
49dc1cc135 | ||
|
|
90f1c0ed04 | ||
|
|
42978c6e7e | ||
|
|
6ad3498bcc | ||
|
|
b44ecb2ccf | ||
|
|
9e161e2907 | ||
|
|
8b15760463 | ||
|
|
4aa67c9cbe | ||
|
|
12c55ef883 | ||
|
|
1e9dd9a3b5 | ||
|
|
db8b2c5629 | ||
|
|
67533875bd | ||
|
|
99809dafb7 | ||
|
|
4cf3e4adc2 | ||
|
|
4fe7950697 | ||
|
|
e162a16550 | ||
|
|
b7c3aac921 | ||
|
|
a902732d4b | ||
|
|
eaab21151a | ||
|
|
5af1f86700 | ||
|
|
90b503ddb7 | ||
|
|
10ffbd8c68 | ||
|
|
0294db7976 | ||
|
|
5a2c23524d | ||
|
|
d4a9d39d67 | ||
|
|
3c8d946a1e | ||
|
|
f71bcf1ac7 | ||
|
|
b041e9f318 | ||
|
|
984a897f73 | ||
|
|
ae54d97d96 | ||
|
|
6ed0831f5d | ||
|
|
ba75544f68 | ||
|
|
7fdf7f6f3e | ||
|
|
614f498d11 | ||
|
|
5f30696315 | ||
|
|
cb5aa28f94 | ||
|
|
bc1a91b7d7 | ||
|
|
7d5beedfbb | ||
|
|
0651d517b2 | ||
|
|
c5e236e095 | ||
|
|
a5bff19796 | ||
|
|
284f3d9a32 | ||
|
|
7ce82ce82e | ||
|
|
ce12684193 | ||
|
|
6cee38e99f | ||
|
|
88758427fd | ||
|
|
23ed867ac4 | ||
|
|
f8b1334a9a | ||
|
|
6b1bd46210 | ||
|
|
a429148324 | ||
|
|
5caaf7dcad | ||
|
|
122c1078cf | ||
|
|
ff48befb8a | ||
|
|
9391dfd8d7 | ||
|
|
467e48e1da | ||
|
|
566410eea5 | ||
|
|
37afc2957b | ||
|
|
b1763135aa | ||
|
|
98020f0b8f | ||
|
|
c2e3282131 | ||
|
|
7a82cb06db | ||
|
|
d89b080da5 | ||
|
|
d80863a169 | ||
|
|
ae79c58d30 | ||
|
|
a0ef6e631e | ||
|
|
d895a5fb7c | ||
|
|
fd765a74c0 | ||
|
|
1f184fab8b | ||
|
|
b6c7c66027 | ||
|
|
33bb87d7f4 | ||
|
|
d4a8a47ccd | ||
|
|
8e8c93410b | ||
|
|
cca59bcace | ||
|
|
432bbcb6f2 | ||
|
|
accdccf897 | ||
|
|
7fc8c6f1e0 | ||
|
|
0eb14a1869 | ||
|
|
c841954c5d | ||
|
|
320c4066f3 | ||
|
|
cda74e65db | ||
|
|
d1a19b043d | ||
|
|
8c4de96432 | ||
|
|
ce7d8a3de5 | ||
|
|
0309f488bd | ||
|
|
d82762430f | ||
|
|
54e5ada03d | ||
|
|
136fe7d355 | ||
|
|
f5b3635773 | ||
|
|
bff8669cd7 | ||
|
|
008462e304 | ||
|
|
9237e911d2 | ||
|
|
5bb0e01136 | ||
|
|
f19827ae91 | ||
|
|
4c55e34bc1 | ||
|
|
08ee2a2d0f | ||
|
|
314728f6d0 | ||
|
|
ea4e0d151f | ||
|
|
b928b90728 | ||
|
|
9b1f0e2fd3 | ||
|
|
05bc06660c | ||
|
|
20957c3582 | ||
| fe86e68aca | |||
| 4dbd6c930f |
@@ -26,16 +26,15 @@ trigger: always_on
|
|||||||
- **Interaction**: Be proactive, concise, and helpful. Focus on code value.
|
- **Interaction**: Be proactive, concise, and helpful. Focus on code value.
|
||||||
|
|
||||||
## 4. Development Standards
|
## 4. Development Standards
|
||||||
**Tech Stack:**
|
|
||||||
- **Container**: Docker-based application.
|
|
||||||
- **Config**: Configurable port.
|
|
||||||
|
|
||||||
**Coding Style:**
|
**Coding Style:**
|
||||||
- **Typing**: Strict typing where applicable.
|
- **Typing**: Strict typing where applicable.
|
||||||
- **Comments**: Concise, English.
|
- **Comments**: Concise, English.
|
||||||
- **Frontend/UX**:
|
- **Frontend/UX**:
|
||||||
- Priority on Usability.
|
- Priority on Usability.
|
||||||
- **MANDATORY**: Tooltips/Help texts for all interactions.
|
- **MANDATORY**: Tooltips/Help texts for all interactions.
|
||||||
|
- **Versioning**:
|
||||||
|
- version.txt has to be increased for any implemented features or fixed bugs)
|
||||||
|
- a change summary has to be documented in changelog.md
|
||||||
|
|
||||||
## 5. Agentic Workflow & Artifacts
|
## 5. Agentic Workflow & Artifacts
|
||||||
**Core Philosophy**: Plan first, act second.
|
**Core Philosophy**: Plan first, act second.
|
||||||
@@ -44,7 +43,20 @@ 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 📋
|
||||||
|
Alle umgesetzten Anforderungen müssen mit `REQUIREMENTS.md` übereinstimmen.
|
||||||
|
1. **Vor der Umsetzung prüfen**: Passt die neue Anforderung zu den bestehenden Requirements?
|
||||||
|
2. **Fehlende Requirements**: Wenn eine umzusetzende Anforderung in `REQUIREMENTS.md` fehlt, muss dies im **Implementation Plan** explizit dokumentiert und freigegeben werden.
|
||||||
|
3. **Widersprüche**: Wenn eine neue Anforderung im Widerspruch zu bestehenden Requirements steht, muss dies im **Implementation Plan** als ⚠️ Warning hervorgehoben werden.
|
||||||
|
4. **Nach Freigabe**: Bei Genehmigung des Implementation Plans muss `REQUIREMENTS.md` entsprechend erweitert oder korrigiert werden (neue ID, Version, Beschreibung). Obsolet gewordene Requirements werden nicht aus `REQUIREMENTS.md` gelöscht sondern nur als durchgestrichen markiert und ein Kommentar mit dem Grund hinzugefügt.
|
||||||
53
README.md
53
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Kantine Wrapper Bookmarklet (v1.2.0)
|
# Kantine Wrapper Bookmarklet
|
||||||
|
|
||||||
Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses Skript erweitert die Standardansicht um eine **Wochenübersicht**, Kostenkontrolle und verbesserte Usability.
|
Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses Skript erweitert die Standardansicht um eine **Wochenübersicht**, Kostenkontrolle und verbesserte Usability.
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|||||||
126
REQUIREMENTS.md
126
REQUIREMENTS.md
@@ -2,55 +2,99 @@
|
|||||||
|
|
||||||
## 1. Einleitung
|
## 1. Einleitung
|
||||||
### 1.1 Zweck des Systems
|
### 1.1 Zweck des Systems
|
||||||
Das System dient als Wrapper und Erweiterung für das Bessa Web-Shop-Portal der Knapp-Kantine (https://web.bessa.app/knapp-kantine). Es ermöglicht den Benutzern, Menüpläne einzusehen, Buchungen automatisiert vorzunehmen und Menüdaten persistent für den öffentlichen Zugriff bereitzustellen, ohne dass jeder Betrachter eigene Zugangsdaten benötigt.
|
Das System dient als Erweiterung für das Bessa Web-Shop-Portal der Knapp-Kantine (https://web.bessa.app/knapp-kantine). Es verbessert die Benutzererfahrung durch eine übersichtliche Wochenansicht, vereinfachte Bestellvorgänge, Kostentransparenz und proaktive Benachrichtigungen.
|
||||||
|
|
||||||
### 1.2 High-Level-Scope
|
### 1.2 High-Level-Scope
|
||||||
Das System umfasst einen automatisierten Scraper, eine API zur Datenbereitstellung, einen persistenten Dateispeicher für erfasste Menüs und ein Frontend-Dashboard zur Visualisierung der Kantinen-Wochenpläne.
|
Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, die Verwaltung von Bestellungen und Stornierungen, ein Benachrichtigungssystem für Verfügbarkeitsänderungen, personalisierte Menü-Highlights sowie ein automatisches Update-Management.
|
||||||
|
|
||||||
## 2. Funktionale Anforderungen
|
## 2. Funktionale Anforderungen
|
||||||
|
|
||||||
| ID | Anforderung (Satzschablone nach Chris Rupp) | Priorität |
|
| ID | Anforderung (Satzschablone nach Chris Rupp) | Priorität | Seit |
|
||||||
|:---|:---|:---|
|
|:---|:---|:---|:---|
|
||||||
| **Auth & Sessions** | | |
|
| **Authentifizierung & Zugang** | | | |
|
||||||
| FR-001 | Das System muss dem Benutzer die Möglichkeit bieten, sich mit Mitarbeiternummer und Passwort am Bessa-Backend anzumelden. | Hoch |
|
| FR-001 | Das System muss dem Benutzer die Möglichkeit bieten, sich mit Mitarbeiternummer und Passwort anzumelden. | Hoch | v1.0.1 |
|
||||||
| FR-002 | Sobald ein Benutzer erfolgreich angemeldet ist, muss das System eine Session-ID erzeugen und die resultierenden Cookies verschlüsselt für 2 Stunden vorhalten. | Hoch |
|
| FR-002 | Das System muss eine bestehende Bessa-Session erkennen und automatisch wiederverwenden, um eine erneute Anmeldung zu vermeiden. | Hoch | v1.0.1 |
|
||||||
| FR-003 | Das System muss die Zugangsdaten (Mitarbeiternummer/Passwort) unmittelbar nach der Verwendung durch den Scraper verwerfen und darf diese nicht dauerhaft speichern. | Hoch |
|
| FR-003 | Das System darf keine Zugangsdaten dauerhaft speichern. Die Authentifizierung muss sitzungsbasiert sein. | Hoch | v1.0.1 |
|
||||||
| **Scraper & Datenextraktion** | | |
|
| FR-004 | Dem Benutzer muss angezeigt werden, ob und als wer er angemeldet ist (Vorname, Name oder ID). | Mittel | v1.0.1 |
|
||||||
| FR-004 | Das System muss in der Lage sein, den wöchentlichen Menüplan (Montag bis Freitag) automatisiert von der Bessa-Webseite zu extrahieren. | Hoch |
|
| FR-005 | Nicht authentifizierte Benutzer müssen die Menüdaten einsehen können (eingeschränkter Lesezugriff). | Mittel | v1.0.1 |
|
||||||
| FR-005 | Wenn ein Tag bereits eine Buchung enthält, muss das System den Navigationspfad so anpassen, dass dennoch alle verfügbaren Menüoptionen (M1F, M2F, etc.) extrahiert werden können. | Mittel |
|
| FR-006 | Das System muss eine explizite Logout-Funktion bereitstellen, die alle sitzungsbezogenen Daten entfernt. | Mittel | v1.0.1 |
|
||||||
| FR-006 | Das System muss für jedes extrahierte Gericht den Namen, die Beschreibung, den Preis und den Status (verfügbar/nicht verfügbar/ bestellt) erfassen. | Hoch |
|
| **Menüanzeige** | | | |
|
||||||
| **Datenhaltung & Zugriff** | | |
|
| FR-010 | Das System muss dem Benutzer alle verfügbaren Tagesmenüs einer Woche gleichzeitig in einer Übersicht darstellen. | Hoch | v1.0.1 |
|
||||||
| FR-007 | Das System muss erfolgreich gescrapte Menüpläne in einer persistenten JSON-Datei (`data/menus.json`) speichern. | Hoch |
|
| FR-011 | Das System muss dem Benutzer die Navigation zwischen der aktuellen und der kommenden Woche ermöglichen. | Mittel | v1.0.1 |
|
||||||
| FR-008 | Das System muss unauthentifizierten Benutzern den Zugriff auf bereits im Speicher befindliche Menüdaten ermöglichen (Public Access). | Mittel |
|
| FR-012 | Für jedes Gericht müssen Name, Beschreibung, Preis und Verfügbarkeitsstatus angezeigt werden. | Hoch | v1.0.1 |
|
||||||
| FR-009 | Falls keine Daten im persistenten Speicher vorhanden sind, muss das System einen nicht authentifizierten Benutzer die Möglichkeit bieten sich anzumelden, um den Speicher zu initialisieren. | Hoch |
|
| FR-013 | Bereits bestellte Menüs müssen visuell von nicht bestellten unterscheidbar sein (farbliche Markierung, Badge). | Mittel | v1.0.1 |
|
||||||
| **Dashboard & UI** | | |
|
| FR-014 | Die Tageskarten-Header müssen den Bestellstatus des Tages farblich signalisieren (bestellt / bestellbar / nicht bestellbar). | Niedrig | v1.0.1 |
|
||||||
| FR-010 | Das System muss dem Benutzer eine intuitive Wochenansicht des Menüplans im Browser darstellen. | Mittel |
|
| FR-015 | Bestellte Menü-Codes (z.B. M1, M2) müssen als Badges im Tageskarten-Header angezeigt werden. | Niedrig | v1.0.1 |
|
||||||
| FR-011 | Wenn ein Scraper-Vorgang aktiv ist, muss das System den Status (Fortschritt, aktuelle Aktion) in Echtzeit visualisieren. | Niedrig |
|
| FR-016 | Am heutigen Tag müssen bestellte Menüs an erster Stelle sortiert werden. | Niedrig | v1.0.1 |
|
||||||
| **Buchungsfunktion** | | |
|
| FR-017 | Wenn keine Menüdaten für eine Woche vorliegen, muss ein aussagekräftiger Leertext angezeigt werden. | Niedrig | v1.0.1 |
|
||||||
| FR-012 | Wenn der Benutzer authentifiziert ist, soll das System eine Bestellung für ein Menü ermöglichen. | Mittel |
|
| **Daten-Aktualisierung** | | | |
|
||||||
| FR-013 | Wenn der Benutzer authentifiziert ist, soll das System ein bereits bestelltes Menü zu stornieren. | Mittel |
|
| FR-020 | Wenn Menüdaten geladen werden, muss der Fortschritt dem Benutzer in Echtzeit angezeigt werden (Fortschrittsbalken, Statustext). | Niedrig | v1.0.1 |
|
||||||
| **Menu Flagging & Notifications** | | |
|
| FR-021 | Das System muss bereits geladene Menüdaten zwischenspeichern, um bei erneutem Aufruf sofort eine Übersicht anzeigen zu können. | Mittel | v1.0.1 |
|
||||||
| FR-014 | Das System muss authentifizierten Benutzern ermöglichen, nicht bestellbare Menüs (deren Cutoff noch nicht erreicht ist) zu markieren ("flaggen"). | Mittel |
|
| FR-022 | Das System muss dem Benutzer die Möglichkeit bieten, die Menüdaten manuell neu zu laden. | Niedrig | v1.0.1 |
|
||||||
| FR-015 | Das System soll die Statusprüfung für geflaggte Menüs auf verbundene Clients verteilen (Distributed Polling), um die Last zu minimieren. Der Server orchestriert, welcher Client wann welche Menüs prüft. | Mittel |
|
| FR-023 | Der Zeitpunkt der letzten Aktualisierung muss für den Benutzer sichtbar sein. | Niedrig | v1.0.1 |
|
||||||
| FR-016 | Bei Statusänderung auf "verfügbar" muss das System den Benutzer benachrichtigen (Systembenachrichtigung). | Mittel |
|
| FR-024 | Das System darf beim Start keinen automatischen API-Refresh durchführen, wenn der Cache frisch (< 1 Stunde) und Daten für die aktuelle Kalenderwoche vorhanden sind. | Mittel | v1.3.1 |
|
||||||
| FR-017 | Geflaggte und ausverkaufte Menüs müssen im UI mit einem gelben Glow hervorgehoben werden. | Mittel |
|
| **Bestellfunktion** | | | |
|
||||||
| FR-018 | Geflaggte und verfügbare Menüs müssen im UI mit einem grünen Glow hervorgehoben werden. | Mittel |
|
| FR-030 | Authentifizierte Benutzer müssen ein verfügbares Menü direkt aus der Übersicht bestellen können. | Hoch | v1.0.1 |
|
||||||
| FR-019 | Wenn die Bestell-Cutoff-Zeit erreicht ist, muss das System das Flag automatisch entfernen. | Mittel |
|
| FR-031 | Authentifizierte Benutzer müssen eine bestehende Bestellung direkt aus der Übersicht stornieren können. | Hoch | v1.0.1 |
|
||||||
|
| FR-032 | Nach Bestellschluss (Cutoff-Zeit) dürfen keine neuen Bestellungen oder Stornierungen für diesen Tag möglich sein. | Hoch | v1.0.1 |
|
||||||
|
| FR-033 | Es muss möglich sein, dasselbe Menü mehrfach zu bestellen. Bei Mehrfachbestellungen muss die Anzahl angezeigt werden. | Niedrig | v1.0.1 |
|
||||||
|
| **Kostentransparenz & Bestellhistorie** | | | |
|
||||||
|
| 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 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** | | | |
|
||||||
|
| FR-050 | Das System muss vor Bestellschluss einen visuell hervorgehobenen Countdown anzeigen. | Mittel | v1.1.0 |
|
||||||
|
| **Menü-Flagging & Benachrichtigungen** | | | |
|
||||||
|
| FR-060 | Authentifizierte Benutzer müssen ausverkaufte Menüs zur Beobachtung markieren können ("flaggen"). | Mittel | v1.0.1 |
|
||||||
|
| FR-061 | Das System muss geflaggte Menüs periodisch auf Verfügbarkeitsänderungen prüfen. | Mittel | v1.0.1 |
|
||||||
|
| FR-062 | Bei Statusänderung eines geflaggten Menüs auf „verfügbar" muss der Benutzer benachrichtigt werden (In-App + Systembenachrichtigung). | Mittel | v1.0.1 |
|
||||||
|
| FR-063 | Geflaggte Menüs müssen im UI visuell hervorgehoben werden (gelber Glow bei ausverkauft, grüner Glow bei verfügbar). | Mittel | v1.0.1 |
|
||||||
|
| FR-064 | Wenn die Bestell-Cutoff-Zeit erreicht ist, muss das System ein Flag automatisch entfernen. | Mittel | v1.0.1 |
|
||||||
|
| **Personalisierung: Smart Highlights** | | | |
|
||||||
|
| FR-070 | Benutzer müssen Schlagwörter (Tags) definieren können, nach denen Menüs automatisch visuell hervorgehoben werden. | Mittel | v1.1.0 |
|
||||||
|
| FR-071 | Die Hervorhebung muss anhand von Menüname und Beschreibung erfolgen (Substring-Matching, case-insensitive). | Niedrig | v1.1.0 |
|
||||||
|
| FR-072 | Hervorgehobene Menüs müssen ein Tag-Badge mit dem matchenden Schlagwort anzeigen. | Niedrig | v1.2.4 |
|
||||||
|
| FR-073 | Die Nächste-Woche-Navigation muss die Anzahl der dort gefundenen Highlights als Badge anzeigen. | Niedrig | v1.2.5 |
|
||||||
|
| FR-080 | Das System muss einen hellen und einen dunklen Darstellungsmodus (Light/Dark Theme) unterstützen. | Niedrig | v1.0.1 |
|
||||||
|
| FR-081 | Die Theme-Präferenz des Benutzers muss persistent gespeichert werden. | Niedrig | v1.0.1 |
|
||||||
|
| FR-082 | Das System muss beim erstmaligen Laden die Betriebssystem-Präferenz für das Farbschema berücksichtigen. | Niedrig | v1.0.1 |
|
||||||
|
| **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** | | | |
|
||||||
|
| FR-095 | Alle benutzerrelevanten Aktionen (Bestellung, Stornierung, Fehler) müssen durch nicht-blockierende Benachrichtigungen (Toasts) bestätigt werden. | Mittel | v1.0.1 |
|
||||||
|
| FR-096 | Bei einem Verbindungsfehler muss ein Fehlerdialog mit Fallback-Link zur Originalseite angezeigt werden. | Mittel | v1.0.1 |
|
||||||
|
| **Nächste-Woche-Badge** | | | |
|
||||||
|
| FR-100 | Die Navigation zur nächsten Woche muss ein Badge anzeigen, das den Überblick über den Bestellstatus der kommenden Woche visualisiert (bestellt / bestellbar / gesamt). | Niedrig | v1.0.1 |
|
||||||
|
| **Update-Management** | | | |
|
||||||
|
| FR-110 | Das System muss periodisch prüfen, ob eine neuere Version verfügbar ist. | Niedrig | v1.0.3 |
|
||||||
|
| FR-111 | Bei Verfügbarkeit einer neueren Version muss ein diskreter Indikator im Header angezeigt werden. | Niedrig | v1.0.3 |
|
||||||
|
| FR-112 | Benutzer müssen eine Versionsliste mit Installationslinks einsehen können (Versionsmenü). | Niedrig | v1.3.0 |
|
||||||
|
| FR-113 | Es muss möglich sein, zu einer älteren Version zurückzukehren (Downgrade). | Niedrig | v1.3.0 |
|
||||||
|
| FR-114 | Ein Dev-Mode muss es ermöglichen, zwischen stabilen Releases und Entwicklungs-Tags umzuschalten. | Niedrig | v1.3.0 |
|
||||||
|
| 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
|
||||||
|
|
||||||
| Kategorie (ISO 25010) | ID | Anforderung | Zielwert/Metrik |
|
| Kategorie (ISO 25010) | ID | Anforderung | Zielwert/Metrik |
|
||||||
|:---|:---|:---|:---|
|
|:---|:---|:---|:---|
|
||||||
| **Performance** | NFR-001 | Antwortzeit der API für gecachte Daten | < 200 ms (95. Perzentil) |
|
| **Performance** | NFR-001 | Die Darstellung bereits gecachter Daten muss ohne spürbare Verzögerung erfolgen. | < 200 ms (UI-Rendering) |
|
||||||
| **Performance** | NFR-007 | Polling-Effizienz & Token-Nutzung | Kein System-Token verfügbar. Polling muss über authentifizierte User-Clients erfolgen. Der Server muss die Anfragen so verteilen, dass redundante Abfragen vermieden werden. |
|
| **Performance** | NFR-002 | Das Polling für geflaggte Menüs darf die reguläre Nutzung nicht beeinträchtigen. | Intervall ≥ 5 Minuten |
|
||||||
| **Performance** | NFR-002 | Dauer eines vollständigen Scrape-Vorgangs (exkl. Navigation) | < 30 Sekunden pro Woche |
|
| **Sicherheit** | NFR-003 | Es dürfen keine Zugangsdaten dauerhaft gespeichert werden. | 0 (keine persistente Speicherung von Passwörtern) |
|
||||||
| **Sicherheit** | NFR-003 | Speicherung von Zugangsdaten | 0 (keine dauerhafte Speicherung von Passwörtern) |
|
| **Sicherheit** | NFR-004 | Auth-Tokens müssen sitzungsbasiert gespeichert werden und bei Schließen des Browsers verfallen. | sessionStorage |
|
||||||
| **Sicherheit** | NFR-004 | Session-Sicherheit | HttpOnly Cookies für die Kommunikation zwischen Frontend und Backend |
|
| **Benutzbarkeit** | NFR-005 | Die Oberfläche muss auf mobilen Geräten fehlerfrei nutzbar sein. | Viewports ab 320px Breite |
|
||||||
| **Wartbarkeit** | NFR-005 | Testabdeckung der Scraper-Logik | Alle Kern-Selektoren müssen durch Debug-HTML-Dumps verifizierbar sein |
|
| **Benutzbarkeit** | NFR-006 | Alle interaktiven Elemente müssen Tooltips oder Hilfetexte bieten. | 100% Coverage |
|
||||||
| **Benutzbarkeit** | NFR-006 | Mobile Responsiveness | Dashboard muss auf Viewports ab 320px Breite fehlerfrei nutzbar sein |
|
| **Benutzbarkeit** | NFR-007 | Die Benutzeroberfläche muss vollständig in deutscher Sprache sein. | Vollständige Lokalisierung |
|
||||||
|
| **Wartbarkeit** | NFR-008 | Die Build-Artefakte müssen durch automatisierte Tests validiert werden. | Build-Tests + Logik-Tests + DOM-Tests |
|
||||||
|
|
||||||
## 4. Technische Randbedingungen
|
## 4. Technische Randbedingungen
|
||||||
* **Architektur**: Node.js Backend mit Express (API + Static Serving) und Vanilla JS Frontend.
|
* **Deployment**: Das System wird als Bookmarklet ausgeliefert, das auf der Bessa-Webseite ausgeführt wird.
|
||||||
* **Engine**: Direkte API-Integration (Reverse Engineering der Bessa API) für maximale Performance und Zuverlässigkeit.
|
* **Datenquelle**: Direkte Integration mit der Bessa REST-API (`api.bessa.app/v1`).
|
||||||
* **Datenspeicher**: Dateibasierter JSON-Store für persistente Daten + In-Memory Caching.
|
* **Datenhaltung**: Clientseitig via `localStorage` (Menü-Cache, Flags, Highlights, Theme) und `sessionStorage` (Auth-Token).
|
||||||
* **Schnittstellen**: REST API (`/api/bookings`, `/api/status`, `/api/order`).
|
* **Build**: Bash-basiertes Build-Script, das Bookmarklet-URL, Standalone-HTML und Installer-Seite generiert.
|
||||||
* **Runtime**: Node.js Umgebung, Docker-ready.
|
* **Versionierung**: SemVer, verwaltet über GitHub Releases/Tags.
|
||||||
|
* **Tests**: Python-basierte Build-Tests (`python3`) + Node.js-basierte Logik-Tests + Node.js-basierte DOM-Interaktionstests (JSDOM).
|
||||||
|
|||||||
BIN
__pycache__/test_build.cpython-312-pytest-9.0.2.pyc
Executable file
BIN
__pycache__/test_build.cpython-312-pytest-9.0.2.pyc
Executable file
Binary file not shown.
1
bessa_orders_debug.json
Executable file
1
bessa_orders_debug.json
Executable file
@@ -0,0 +1 @@
|
|||||||
|
{"next":null,"previous":null,"results":[]}
|
||||||
@@ -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 40x40 favicon.png from favicon_base.png..."
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
from PIL import Image
|
||||||
|
try:
|
||||||
|
img = Image.open('$FAVICON_BASE')
|
||||||
|
img_resized = img.resize((40, 40), 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
|
||||||
@@ -75,15 +99,20 @@ echo "✅ Standalone HTML: $DIST_DIR/kantine-standalone.html"
|
|||||||
# Escape CSS for embedding in JS string
|
# Escape CSS for embedding in JS string
|
||||||
CSS_ESCAPED=$(echo "$CSS_CONTENT" | sed "s/'/\\\\'/g" | tr '\n' ' ' | sed 's/ */ /g')
|
CSS_ESCAPED=$(echo "$CSS_CONTENT" | sed "s/'/\\\\'/g" | tr '\n' ' ' | sed 's/ */ /g')
|
||||||
|
|
||||||
# Build bookmarklet payload
|
# Create a minified version for the injected bookmarklet payloads
|
||||||
|
echo "Minifying JS with Terser..."
|
||||||
|
TEMP_JS=$(mktemp)
|
||||||
|
echo "$JS_CONTENT" > "$TEMP_JS"
|
||||||
|
JS_MINIFIED=$(npx -y terser "$TEMP_JS" --compress --mangle)
|
||||||
|
rm -f "$TEMP_JS"
|
||||||
|
|
||||||
cat > "$DIST_DIR/bookmarklet-payload.js" << PAYLOADEOF
|
cat > "$DIST_DIR/bookmarklet-payload.js" << PAYLOADEOF
|
||||||
(function(){
|
javascript:(function(){
|
||||||
if(window.__KANTINE_LOADED){alert('Kantine Wrapper already loaded!');return;}
|
if(window.__KANTINE_LOADED){alert('Kantine Wrapper already loaded!');return;}
|
||||||
var s=document.createElement('style');
|
var s=document.createElement('style');s.textContent='${CSS_ESCAPED}';document.head.appendChild(s);
|
||||||
s.textContent='${CSS_ESCAPED}';
|
// Inject JS logic
|
||||||
document.head.appendChild(s);
|
|
||||||
var sc=document.createElement('script');
|
var sc=document.createElement('script');
|
||||||
sc.textContent=$(echo "$JS_CONTENT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || echo "$JS_CONTENT" | sed 's/\\/\\\\/g' | sed "s/'/\\\\'/g" | sed 's/"/\\\\"/g' | tr '\n' ' ' | sed 's/^/"/' | sed 's/$/"/');
|
sc.textContent=$(echo "$JS_MINIFIED" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || echo "$JS_MINIFIED" | sed 's/\\/\\\\/g' | sed "s/'/\\\\'/g" | sed 's/"/\\\\"/g' | tr '\n' ' ' | sed 's/^/"/' | sed 's/$/"/');
|
||||||
document.head.appendChild(sc);
|
document.head.appendChild(sc);
|
||||||
})();
|
})();
|
||||||
PAYLOADEOF
|
PAYLOADEOF
|
||||||
@@ -101,6 +130,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 +151,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: 40px; height: 40px;">
|
||||||
|
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 +218,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>
|
||||||
|
|
||||||
|
|
||||||
@@ -197,12 +244,11 @@ fi
|
|||||||
|
|
||||||
# Embed the bookmarklet URL inline
|
# Embed the bookmarklet URL inline
|
||||||
echo "document.getElementById('bookmarklet-link').href = " >> "$DIST_DIR/install.html"
|
echo "document.getElementById('bookmarklet-link').href = " >> "$DIST_DIR/install.html"
|
||||||
echo "$JS_CONTENT" | python3 -c "
|
echo "$JS_MINIFIED" | 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 = sys.stdin.read()
|
||||||
js = js_template.replace('{{VERSION}}', '$VERSION')
|
|
||||||
|
|
||||||
# 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 +282,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,28 +307,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. Auto-tag version ===
|
|
||||||
echo ""
|
|
||||||
echo "=== Tagging $VERSION ==="
|
|
||||||
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
|
||||||
echo "ℹ️ Tag $VERSION already exists, skipping."
|
|
||||||
else
|
|
||||||
git tag "$VERSION"
|
|
||||||
echo "✅ Created tag: $VERSION"
|
|
||||||
fi
|
|
||||||
|
|||||||
58
changelog.md
58
changelog.md
@@ -1,3 +1,61 @@
|
|||||||
|
## v1.6.8 (2026-03-06)
|
||||||
|
- ⚡ **Performance**: Das JavaScript für das Kantinen-Bookmarklet wird nun beim Build-Prozess (via Terser) minimiert, was die Länge der injizierten URL spürbar reduziert.
|
||||||
|
|
||||||
|
## v1.6.7 (2026-03-06)
|
||||||
|
- 🎨 **Style**: Das neue Header-Logo (`favicon_base.png`) wird nun konsequent auf 40x40px generiert und gerendert.
|
||||||
|
|
||||||
|
## v1.6.6 (2026-03-06)
|
||||||
|
- 🎨 **Style**: Den Schatten und den hervorstehenden Karten-Effekt für bestellte Menüs an vergangenen Tagen komplett entfernt - verbleiben nun visuell flach und unaufdringlich wie nicht-bestellte Menüs.
|
||||||
|
|
||||||
|
## v1.6.5 (2026-03-06)
|
||||||
|
- ✨ **Feature**: Das `restaurant_menu` Icon im Header wurde durch das neue `favicon_base.png` Logo ersetzt, passend zur Textgröße skaliert.
|
||||||
|
- 🎨 **Style**: Violette Umrahmung (Bestellt-Markierung) an vergangenen Tagen entfernt, um den Fokus auf aktuelle und zukünftige Bestellungen zu lenken.
|
||||||
|
- 🎨 **Style**: Der Glow-Effekt für am heutigen Tag bestellte Menüs wurde intensiviert.
|
||||||
|
|
||||||
|
## v1.6.4 (2026-03-05)
|
||||||
|
- ✨ **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.).
|
||||||
|
- 🧹 **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.0 (2026-02-22)
|
||||||
|
- **Feature**: Bestellhistorie per Knopfdruck abrufbar. Übersichtliche Darstellung, gruppiert nach Monaten und Kalenderwochen, inklusive Stornos. 📜✨
|
||||||
|
|
||||||
|
## v1.3.2 (2026-02-19)
|
||||||
|
- **Fix**: Falsche Anzahl an Highlight-Menüs im "Nächste Woche"-Badge korrigiert (zählte alle Menüs statt nur Highlights). 🐛
|
||||||
|
|
||||||
|
## v1.3.1 (2026-02-17)
|
||||||
|
- **Feature**: Smart Cache – API-Refresh beim Start wird übersprungen wenn Daten für die aktuelle KW vorhanden und Cache < 1h alt ist. ⚡
|
||||||
|
|
||||||
## v1.3.0 (2026-02-16)
|
## v1.3.0 (2026-02-16)
|
||||||
- **Feature**: GitHub Release Management 📦
|
- **Feature**: GitHub Release Management 📦
|
||||||
- Version-Menü: Klick auf Versionsnummer zeigt alle verfügbaren Versionen
|
- Version-Menü: Klick auf Versionsnummer zeigt alle verfügbaren Versionen
|
||||||
|
|||||||
10
cors_server.py
Executable file
10
cors_server.py
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
|
||||||
|
class CORSRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def end_headers(self):
|
||||||
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
super().end_headers()
|
||||||
|
|
||||||
|
with socketserver.TCPServer(("127.0.0.1", 8080), CORSRequestHandler) as httpd:
|
||||||
|
httpd.serve_forever()
|
||||||
15
debug_test.js
Executable file
15
debug_test.js
Executable 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
dist/Arrow_and_fork_fly_away_bd43310bea.mp4
vendored
Executable file
BIN
dist/Arrow_and_fork_fly_away_bd43310bea.mp4
vendored
Executable file
Binary file not shown.
9
dist/bookmarklet-payload.js
vendored
9
dist/bookmarklet-payload.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bookmarklet.txt
vendored
2
dist/bookmarklet.txt
vendored
File diff suppressed because one or more lines are too long
106
dist/install.html
vendored
106
dist/install.html
vendored
File diff suppressed because one or more lines are too long
1382
dist/kantine-standalone.html
vendored
1382
dist/kantine-standalone.html
vendored
File diff suppressed because one or more lines are too long
BIN
favicon.png
Executable file
BIN
favicon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
favicon_base.png
Executable file
BIN
favicon_base.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
966
kantine.js
966
kantine.js
File diff suppressed because it is too large
Load Diff
25
mock-data.js
25
mock-data.js
@@ -128,8 +128,31 @@
|
|||||||
// Orders endpoint
|
// Orders endpoint
|
||||||
if (urlStr.includes('/user/orders/') && (!options || options.method === 'GET' || !options.method)) {
|
if (urlStr.includes('/user/orders/') && (!options || options.method === 'GET' || !options.method)) {
|
||||||
console.log('[MOCK] Returning mock orders');
|
console.log('[MOCK] Returning mock orders');
|
||||||
|
// Formatter for history mapping
|
||||||
|
const mappedOrders = mockOrders.map(o => ({
|
||||||
|
id: o.id,
|
||||||
|
date: `${o.date}T10:00:00Z`,
|
||||||
|
order_state: o.status === 'cancelled' ? 9 : 5,
|
||||||
|
total: o.price || '6.50',
|
||||||
|
items: [{
|
||||||
|
article: o.article,
|
||||||
|
name: o.article_name,
|
||||||
|
price: o.price || '6.50',
|
||||||
|
amount: 1
|
||||||
|
}]
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Handle lazy load / pagination if requesting full history
|
||||||
|
if (urlStr.includes('limit=50')) {
|
||||||
return Promise.resolve(new Response(JSON.stringify({
|
return Promise.resolve(new Response(JSON.stringify({
|
||||||
results: mockOrders
|
count: mappedOrders.length,
|
||||||
|
next: null,
|
||||||
|
results: mappedOrders
|
||||||
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(new Response(JSON.stringify({
|
||||||
|
results: mappedOrders
|
||||||
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
59
release.sh
Executable file
59
release.sh
Executable 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
|
||||||
383
style.css
383
style.css
@@ -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 */
|
||||||
@@ -437,7 +512,6 @@ body {
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
/* Changed from --surface */
|
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -446,6 +520,174 @@ body {
|
|||||||
animation: modalSlide 0.3s ease-out;
|
animation: modalSlide 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* History Modal specific */
|
||||||
|
.history-modal-content {
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 85vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-modal-content .modal-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0;
|
||||||
|
/* Padding is handled by inner elements */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* History Styles */
|
||||||
|
.history-year-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-year-header {
|
||||||
|
background: var(--bg-card);
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-group {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 20px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-body);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-header:hover {
|
||||||
|
background: var(--border-color);
|
||||||
|
/* Slight hover effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-content {
|
||||||
|
display: none;
|
||||||
|
/* Collapsed by default */
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-group.open .history-month-content {
|
||||||
|
display: block;
|
||||||
|
/* Expanded when open class is present */
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-group.open .history-month-header .material-icons-round {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-header .material-icons-round {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-week-group {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-bottom: 1px dashed var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-week-group:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-week-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-week-summary {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background: rgba(100, 116, 139, 0.1);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50px 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--bg-body);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-date {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-name {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-price {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-status {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-cancelled {
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-price-cancelled {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes modalSlide {
|
@keyframes modalSlide {
|
||||||
from {
|
from {
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
@@ -542,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 {
|
||||||
@@ -571,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 */
|
||||||
@@ -583,68 +843,68 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhancements for ordered items */
|
/* Past ordered items get no special frame or shadow, but remain visually distinct by staying fully opaque (via the :not(.ordered) selector above) */
|
||||||
.menu-card.past-day .menu-item.ordered {
|
|
||||||
/* No opacity/filter here - fully visible */
|
|
||||||
background: var(--bg-card);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
border: 1px solid var(--accent-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 0 -1rem 1.5rem -1rem;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item.today-ordered {
|
.menu-item.today-ordered {
|
||||||
border: 2px solid var(--accent-color);
|
border: 2px solid #8b5cf6;
|
||||||
box-shadow: 0 0 20px rgba(96, 165, 250, 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(96, 165, 250, 0.3);
|
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 25px rgba(96, 165, 250, 0.6);
|
box-shadow: 0 0 40px rgba(139, 92, 246, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 15px rgba(96, 165, 250, 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,7 +914,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 {
|
||||||
@@ -667,13 +943,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);
|
||||||
@@ -720,6 +989,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 {
|
||||||
@@ -801,12 +1071,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) === */
|
||||||
@@ -1137,17 +1407,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1428,8 +1701,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
syntax_check.js
Executable file
17
syntax_check.js
Executable 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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: () => { },
|
||||||
@@ -107,7 +120,9 @@ try {
|
|||||||
[/function\s+fetchVersions/, 'fetchVersions function'],
|
[/function\s+fetchVersions/, 'fetchVersions function'],
|
||||||
[/function\s+isNewer/, 'isNewer function'],
|
[/function\s+isNewer/, 'isNewer function'],
|
||||||
[/function\s+openVersionMenu/, 'openVersionMenu function'],
|
[/function\s+openVersionMenu/, 'openVersionMenu function'],
|
||||||
[/kantine_dev_mode/, 'dev-mode localStorage key']
|
[/kantine_dev_mode/, 'dev-mode localStorage key'],
|
||||||
|
[/function\s+isCacheFresh/, 'isCacheFresh function'],
|
||||||
|
[/limit=5/, 'Delta fetch limit parameter']
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [regex, label] of checks) {
|
for (const [regex, label] of checks) {
|
||||||
|
|||||||
189
tests/test_dom.js
Executable file
189
tests/test_dom.js
Executable 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 @@
|
|||||||
v1.3.0
|
v1.6.8
|
||||||
|
|||||||
Reference in New Issue
Block a user