Compare commits
21 Commits
1be6e44d7f
...
v1.6.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86e2e51dc3 | ||
|
|
12fe759970 | ||
|
|
bb5dab64cd | ||
|
|
cbf03ea497 | ||
|
|
6b8ac5ca1d | ||
|
|
2964eba88b | ||
|
|
089e5375b4 | ||
|
|
e514f42dbe | ||
|
|
f7a9b061ea | ||
|
|
fe75347682 | ||
|
|
5c5255058c | ||
|
|
e16398ef74 | ||
|
|
6a57e2716f | ||
|
|
d383808f4f | ||
|
|
498b400033 | ||
|
|
884a17e7d3 | ||
|
|
ad660799ec | ||
|
|
a5fe50bbf0 | ||
|
|
a98faec51e | ||
|
|
212bf3b015 | ||
|
|
f29ecd4b79 |
@@ -59,10 +59,11 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
|
|||||||
| 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** | | | |
|
| **Header UI & Navigation** | | | |
|
||||||
| FR-090 | Die Hauptnavigation (Wochen-Toggles) muss linksbündig neben dem App-Titel positioniert sein. | Niedrig | v1.5.0 |
|
| 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-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.6.11 (Update v1.5.0) |
|
||||||
| FR-092 | Solange Menüdaten für die Nächste Woche verfügbar sind, aber noch keine Bestellungen getätigt wurden, muss der entsprechende Navigation-Button animiert und farblich (Gelb) hervorgehoben werden. Nach der ersten Bestellung muss die Hervorhebung automatisch erlöschen. Zusätzlich muss beim erstmaligen Erscheinen der Daten ein einmaliger Toast-Hinweis angezeigt werden. | Mittel | v1.6.0 (Update v1.4.21) |
|
| 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** | | | |
|
| **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 |
|
| 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 |
|
||||||
|
| FR-121 | Das System muss bei fehlenden Übersetzungen in zweisprachigen Menüs robust reagieren. Wenn ein Gang nur in einer Sprache vorliegt, muss dieser Teil für beide Sprachansichten herangezogen werden, um die Konsistenz der Ganganzahl zu gewährleisten. | Mittel | v1.6.10 |
|
||||||
| **Benutzer-Feedback** | | | |
|
| **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-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 |
|
| FR-096 | Bei einem Verbindungsfehler muss ein Fehlerdialog mit Fallback-Link zur Originalseite angezeigt werden. | Mittel | v1.0.1 |
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ if [ ! -f "$JS_FILE" ]; then echo "ERROR: $JS_FILE not found"; exit 1; fi
|
|||||||
# Generate favicon.png from favicon_base.png if base exists
|
# Generate favicon.png from favicon_base.png if base exists
|
||||||
FAVICON_BASE="$SCRIPT_DIR/favicon_base.png"
|
FAVICON_BASE="$SCRIPT_DIR/favicon_base.png"
|
||||||
if [ -f "$FAVICON_BASE" ]; then
|
if [ -f "$FAVICON_BASE" ]; then
|
||||||
echo "Generating 32x32 favicon.png from favicon_base.png..."
|
echo "Generating 40x40 favicon.png from favicon_base.png..."
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import sys
|
import sys
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
try:
|
try:
|
||||||
img = Image.open('$FAVICON_BASE')
|
img = Image.open('$FAVICON_BASE')
|
||||||
img_resized = img.resize((32, 32), Image.Resampling.LANCZOS)
|
img_resized = img.resize((40, 40), Image.Resampling.LANCZOS)
|
||||||
img_resized.save('$FAVICON_FILE')
|
img_resized.save('$FAVICON_FILE')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('Favicon generation error:', e)
|
print('Favicon generation error:', e)
|
||||||
@@ -99,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
|
||||||
@@ -161,7 +166,7 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
|
|||||||
|
|
||||||
<div style="text-align: center; margin-bottom: 30px;">
|
<div style="text-align: center; margin-bottom: 30px;">
|
||||||
<h1 style="margin-bottom: 5px; display: flex; align-items: center; justify-content: center; gap: 10px;">
|
<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;">
|
<img src="$FAVICON_URL" alt="Logo" style="width: 40px; height: 40px;">
|
||||||
Kantine Wrapper
|
Kantine Wrapper
|
||||||
<span style="font-size:0.5em; opacity:0.6; font-weight:400; margin-left:5px;">$VERSION</span>
|
<span style="font-size:0.5em; opacity:0.6; font-weight:400; margin-left:5px;">$VERSION</span>
|
||||||
</h1>
|
</h1>
|
||||||
@@ -239,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 + Favicon
|
# 1. Read JS and Replace VERSION + Favicon
|
||||||
js_template = sys.stdin.read()
|
js = sys.stdin.read()
|
||||||
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(' ', ' ')
|
||||||
@@ -303,26 +307,30 @@ 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 ==="
|
echo "=== Running DOM Interaction Tests ==="
|
||||||
node "$SCRIPT_DIR/tests/test_dom.js"
|
timeout 15s node "$SCRIPT_DIR/tests/test_dom.js"
|
||||||
DOM_EXIT=$?
|
DOM_EXIT=$?
|
||||||
if [ $DOM_EXIT -ne 0 ]; then
|
if [ $DOM_EXIT -ne 0 ]; then
|
||||||
echo "❌ DOM UI tests FAILED! Regressions detected."
|
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
|
exit 1
|
||||||
fi
|
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."
|
||||||
|
|||||||
27
changelog.md
27
changelog.md
@@ -1,3 +1,30 @@
|
|||||||
|
## v1.6.11 (2026-03-09)
|
||||||
|
- 🔄 **Refactor**: Trennung der Zeitstempel für die Hauptaktualisierung (Header) und die Benachrichtigungsprüfung (Bell-Icon). Das Polling aktualisiert nun nicht mehr fälschlicherweise die "Aktualisiert am"-Zeit im Header.
|
||||||
|
- 🏷️ **Metadata**: Version auf v1.6.11 angehoben.
|
||||||
|
|
||||||
|
## v1.6.10 (2026-03-09)
|
||||||
|
- **Feature**: Robuste Kurs-Erkennung in zweisprachigen Menüs ([FR-121](REQUIREMENTS.md#FR-121)).
|
||||||
|
- **Fix**: Verhindert das Verschieben von Gängen bei fehlenden englischen Übersetzungen.
|
||||||
|
- **Improved**: Heuristik-Split erkennt nun zuverlässiger den Übergang von Englisch zurück zu Deutsch (z.B. bei "Achtung"-Hinweisen)
|
||||||
|
|
||||||
|
## v1.6.9 (2026-03-09)
|
||||||
|
- 🐛 **Bugfix**: Fehlerhafte Zeitangabe beim Bell-Icon ("vor 291h") behoben. Der Tooltip wird nun minütlich aktualisiert und nach jeder Menü-Prüfung korrekt neu gesetzt.
|
||||||
|
- 🔄 **Refactor**: Zeitstempel-Management für die letzte Aktualisierung vereinheitlicht und im `localStorage` persistiert.
|
||||||
|
|
||||||
|
## 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)
|
## 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.).
|
- ✨ **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.
|
- 🧹 **Cleanup**: Sprach-Lexikon dedupliziert und alphabetisch sortiert für bessere Performance und Wartbarkeit.
|
||||||
|
|||||||
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
52
dist/install.html
vendored
52
dist/install.html
vendored
File diff suppressed because one or more lines are too long
179
dist/kantine-standalone.html
vendored
179
dist/kantine-standalone.html
vendored
File diff suppressed because one or more lines are too long
BIN
favicon.png
BIN
favicon.png
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.6 KiB |
29
favicon.svg
29
favicon.svg
@@ -1,29 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
||||||
<!-- Fork (left, with gap to triangle) -->
|
|
||||||
<g transform="translate(2, 10)">
|
|
||||||
<!-- Tines -->
|
|
||||||
<rect x="1" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
|
|
||||||
<rect x="4.6" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
|
|
||||||
<rect x="8.2" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
|
|
||||||
<!-- Connector -->
|
|
||||||
<rect x="1" y="14" width="9" height="3.5" rx="1.5" fill="#333"/>
|
|
||||||
<!-- Handle -->
|
|
||||||
<rect x="3.5" y="16.5" width="4" height="24" rx="2" fill="#333"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Triangle (center, equilateral aspect ratio ~1:0.866) -->
|
|
||||||
<!-- Equilateral: base=28, height=24.25 => keeps proper ratio -->
|
|
||||||
<polygon points="32,8 47,48 17,48" fill="none" stroke="#333" stroke-width="4" stroke-linejoin="round"/>
|
|
||||||
|
|
||||||
<!-- Knife (right, with gap to triangle) -->
|
|
||||||
<g transform="translate(50, 10)">
|
|
||||||
<!-- Blade (slight curve) -->
|
|
||||||
<path d="M3,0 C3,0 3,0 3,0 L3,17 L10,14 C10,6 7,0 3,0 Z" fill="#333"/>
|
|
||||||
<!-- Spine -->
|
|
||||||
<rect x="1.5" y="0" width="2" height="18" rx="1" fill="#333"/>
|
|
||||||
<!-- Bolster -->
|
|
||||||
<rect x="1.5" y="16.5" width="8.5" height="3.5" rx="1.2" fill="#333"/>
|
|
||||||
<!-- Handle -->
|
|
||||||
<rect x="3.5" y="19" width="4" height="22" rx="2" fill="#333"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
144
kantine.js
144
kantine.js
@@ -14,7 +14,7 @@
|
|||||||
// === Constants ===
|
// === Constants ===
|
||||||
const API_BASE = 'https://api.bessa.app/v1';
|
const API_BASE = 'https://api.bessa.app/v1';
|
||||||
const GUEST_TOKEN = 'c3418725e95a9f90e3645cbc846b4d67c7c66131';
|
const GUEST_TOKEN = 'c3418725e95a9f90e3645cbc846b4d67c7c66131';
|
||||||
const CLIENT_VERSION = '1.7.0_prod/2026-01-26';
|
const CLIENT_VERSION = 'v1.6.11';
|
||||||
const VENUE_ID = 591;
|
const VENUE_ID = 591;
|
||||||
const MENU_ID = 7;
|
const MENU_ID = 7;
|
||||||
const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<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: 2em; width: 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>
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="app-footer">
|
<footer class="app-footer">
|
||||||
<p>Jetzt Bessa Einfach! • Knapp-Kantine Wrapper • <span id="current-year">${new Date().getFullYear()}</span> by Kaufis-Kitchen</p>
|
<p>Jetzt Bessa Einfach! • Knapp-Kantine Wrapper • <span id="current-year">${new Date().getFullYear()}</span> by Kaufi 😃👍 mit Hilfe von KI 🤖</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -691,7 +691,7 @@
|
|||||||
const existingOrder = localCache[existingOrderIndex];
|
const existingOrder = localCache[existingOrderIndex];
|
||||||
// If order exists and wasn't updated since our cache, we've reached the point
|
// If order exists and wasn't updated since our cache, we've reached the point
|
||||||
// where everything older is already correctly cached.
|
// where everything older is already correctly cached.
|
||||||
// order.updated is an ISO string like "2025-02-18T10:30:15.123456Z"
|
// order.updated is an ISO string like "2026-03-09T18:30:15.123456Z"
|
||||||
if (existingOrder.updated === order.updated && existingOrder.order_state === order.order_state) {
|
if (existingOrder.updated === order.updated && existingOrder.order_state === order.order_state) {
|
||||||
deltaComplete = true;
|
deltaComplete = true;
|
||||||
break;
|
break;
|
||||||
@@ -1091,19 +1091,15 @@
|
|||||||
if (anyAvailable) break;
|
if (anyAvailable) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
let lastUpdatedStr = localStorage.getItem('kantine_last_checked');
|
||||||
let timeStr = 'gerade eben'; // Fallback instead of Unbekannt
|
let timeStr = 'gerade eben'; // Fallback instead of Unbekannt
|
||||||
if (!lastUpdatedStr) {
|
if (!lastUpdatedStr) {
|
||||||
lastUpdatedStr = new Date().toISOString();
|
lastUpdatedStr = new Date().toISOString();
|
||||||
localStorage.setItem('kantine_last_updated', lastUpdatedStr);
|
localStorage.setItem('kantine_last_checked', lastUpdatedStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastUpdated = new Date(lastUpdatedStr);
|
const lastUpdated = new Date(lastUpdatedStr);
|
||||||
const diffMs = Date.now() - lastUpdated.getTime();
|
timeStr = getRelativeTime(lastUpdated);
|
||||||
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}`;
|
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
|
||||||
|
|
||||||
@@ -1231,6 +1227,9 @@
|
|||||||
await new Promise(r => setTimeout(r, 200));
|
await new Promise(r => setTimeout(r, 200));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Update ONLY the polling status timestamp (Bell Tooltip)
|
||||||
|
localStorage.setItem('kantine_last_checked', new Date().toISOString());
|
||||||
|
updateAlarmBell();
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Highlight Management ===
|
// === Highlight Management ===
|
||||||
@@ -1567,6 +1566,8 @@
|
|||||||
const subtitle = document.getElementById('last-updated-subtitle');
|
const subtitle = document.getElementById('last-updated-subtitle');
|
||||||
if (!isoTimestamp) return;
|
if (!isoTimestamp) return;
|
||||||
lastUpdatedTimestamp = isoTimestamp;
|
lastUpdatedTimestamp = isoTimestamp;
|
||||||
|
localStorage.setItem('kantine_last_updated', isoTimestamp);
|
||||||
|
localStorage.setItem('kantine_last_checked', isoTimestamp); // Also update bell on full refresh
|
||||||
try {
|
try {
|
||||||
const date = new Date(isoTimestamp);
|
const date = new Date(isoTimestamp);
|
||||||
const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||||
@@ -1579,7 +1580,10 @@
|
|||||||
// Auto-refresh relative time every minute
|
// Auto-refresh relative time every minute
|
||||||
if (!lastUpdatedIntervalId) {
|
if (!lastUpdatedIntervalId) {
|
||||||
lastUpdatedIntervalId = setInterval(() => {
|
lastUpdatedIntervalId = setInterval(() => {
|
||||||
if (lastUpdatedTimestamp) updateLastUpdatedTime(lastUpdatedTimestamp);
|
if (lastUpdatedTimestamp) {
|
||||||
|
updateLastUpdatedTime(lastUpdatedTimestamp);
|
||||||
|
updateAlarmBell(); // Ensure bell icon title (tooltip) also refreshes
|
||||||
|
}
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2380,7 +2384,7 @@
|
|||||||
// === Language Filter (FR-100) ===
|
// === Language Filter (FR-100) ===
|
||||||
// DE stems for fallback language detection
|
// DE stems for fallback language detection
|
||||||
const DE_STEMS = [
|
const DE_STEMS = [
|
||||||
'apfel', 'aubergine', 'auflauf', 'beere', 'blumenkohl', 'bohne', 'braten', 'brokkoli', 'brot', 'brust',
|
'apfel', 'achtung', 'aubergine', 'auflauf', 'beere', 'blumenkohl', 'bohne', 'braten', 'brokkoli', 'brot', 'brust',
|
||||||
'brötchen', 'butter', 'chili', 'dessert', 'dip', 'eier', 'eintopf', 'eis', 'erbse', 'erdbeer',
|
'brötchen', 'butter', 'chili', 'dessert', 'dip', 'eier', 'eintopf', 'eis', 'erbse', 'erdbeer',
|
||||||
'essig', 'filet', 'fisch', 'fisole', 'fleckerl', 'fleisch', 'flügel', 'frucht', 'für', 'gebraten',
|
'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',
|
'gemüse', 'gewürz', 'gratin', 'grieß', 'gulasch', 'gurke', 'himbeer', 'honig', 'huhn', 'hähnchen',
|
||||||
@@ -2422,8 +2426,8 @@
|
|||||||
if (!text) return { de: '', en: '', raw: '' };
|
if (!text) return { de: '', en: '', raw: '' };
|
||||||
|
|
||||||
const raw = text;
|
const raw = text;
|
||||||
// Formatting: add • for new lines, using the forgiving regex
|
// Formatting: add • for new lines, avoiding dots before slashes
|
||||||
let formattedRaw = text.replace(/(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*(?=\S)/g, '($1)\n• ');
|
let formattedRaw = text.replace(/(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*(?=\S)(?!\s*\/)/g, '($1)\n• ');
|
||||||
if (!formattedRaw.startsWith('• ')) {
|
if (!formattedRaw.startsWith('• ')) {
|
||||||
formattedRaw = '• ' + formattedRaw;
|
formattedRaw = '• ' + formattedRaw;
|
||||||
}
|
}
|
||||||
@@ -2479,10 +2483,11 @@
|
|||||||
|
|
||||||
const score = (leftScore.en - leftScore.de) + (rightScore.de - rightScore.en) + capitalBonus;
|
const score = (leftScore.en - leftScore.de) + (rightScore.de - rightScore.en) + capitalBonus;
|
||||||
|
|
||||||
// Strict condition! The assumed German part must actually look German
|
// Mandatory check: The assumed English part must actually look reasonably like English (or at least more so than the right part)
|
||||||
|
const leftLooksEnglish = (leftScore.en > leftScore.de) || (leftScore.en > 0);
|
||||||
const rightLooksGerman = (rightScore.de + capitalBonus) > rightScore.en;
|
const rightLooksGerman = (rightScore.de + capitalBonus) > rightScore.en;
|
||||||
|
|
||||||
if (rightLooksGerman && score > maxScore) {
|
if (leftLooksEnglish && rightLooksGerman && score > maxScore) {
|
||||||
maxScore = score;
|
maxScore = score;
|
||||||
bestK = k;
|
bestK = k;
|
||||||
}
|
}
|
||||||
@@ -2497,63 +2502,84 @@
|
|||||||
return { enPart: fragment, nextDe: '' };
|
return { enPart: fragment, nextDe: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW LOGIC: We no longer split by slash if the slash is part of a missing-parenthesis allergen like /ACGL)
|
// Match courses: Any text followed by an allergen marker "(...)" but NOT if followed by a slash.
|
||||||
const parts = text.split(/\s*\/\s*(?![A-Z,]+\))/);
|
const allergenRegex = /(.*?)(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*(?!\s*[/])/g;
|
||||||
|
let match;
|
||||||
|
const rawCourses = [];
|
||||||
|
let lastScanIndex = 0;
|
||||||
|
|
||||||
// Sanity check: max 3 courses means max 3 slashes → max 4 parts
|
while ((match = allergenRegex.exec(text)) !== null) {
|
||||||
if (parts.length > 4) {
|
if (match.index > lastScanIndex) {
|
||||||
return { de: formattedRaw, en: '', raw: formattedRaw };
|
rawCourses.push(text.substring(lastScanIndex, match.index).trim());
|
||||||
|
}
|
||||||
|
rawCourses.push(match[0].trim());
|
||||||
|
lastScanIndex = allergenRegex.lastIndex;
|
||||||
|
}
|
||||||
|
if (lastScanIndex < text.length) {
|
||||||
|
rawCourses.push(text.substring(lastScanIndex).trim());
|
||||||
|
}
|
||||||
|
if (rawCourses.length === 0 && text.trim() !== '') {
|
||||||
|
rawCourses.push(text.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
const deParts = [];
|
const deParts = [];
|
||||||
const enParts = [];
|
const enParts = [];
|
||||||
|
|
||||||
// Part 0 is ALWAYS German (beginning of the menu item)
|
// 2. Process each course individually
|
||||||
deParts.push(parts[0].trim());
|
for (let course of rawCourses) {
|
||||||
|
let courseMatch = course.match(/(.*?)(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*$/);
|
||||||
|
let courseText = course;
|
||||||
|
let allergenTxt = "";
|
||||||
|
let allergenCode = "";
|
||||||
|
|
||||||
// Matches e.g., "(GLM)" OR "/GLM)" OR " GLM)" with trailing spaces
|
if (courseMatch) {
|
||||||
const allergenRegex = /(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*/;
|
courseText = courseMatch[1].trim();
|
||||||
|
allergenCode = courseMatch[2];
|
||||||
for (let i = 1; i < parts.length; i++) {
|
allergenTxt = ` (${allergenCode})`;
|
||||||
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) {
|
// A) Split by slash if present
|
||||||
deParts.push(nextDe);
|
const slashParts = courseText.split(/\s*\/\s*(?![A-Z,]+$)/);
|
||||||
|
|
||||||
|
if (slashParts.length >= 2) {
|
||||||
|
// Potential DE / EN pair
|
||||||
|
const deCandidate = slashParts[0].trim();
|
||||||
|
let enCandidate = slashParts.slice(1).join(' / ').trim();
|
||||||
|
|
||||||
|
// Check for nested German in English part (e.g. "Pumpkin cream Achtung...")
|
||||||
|
const nestedSplit = heuristicSplitEnDe(enCandidate);
|
||||||
|
if (nestedSplit.nextDe) {
|
||||||
|
// Transition back to German found!
|
||||||
|
deParts.push(deCandidate + allergenTxt);
|
||||||
|
enParts.push(nestedSplit.enPart + allergenTxt);
|
||||||
|
|
||||||
|
// Push the nested German part as a new standalone course (fallback to itself)
|
||||||
|
const nestedDe = nestedSplit.nextDe + allergenTxt;
|
||||||
|
deParts.push(nestedDe);
|
||||||
|
enParts.push(nestedDe);
|
||||||
|
} else {
|
||||||
|
// Happy path: standard DE / EN
|
||||||
|
// Avoid double allergens if they were on both sides already
|
||||||
|
const enFinal = enCandidate + allergenTxt;
|
||||||
|
const deFinal = deCandidate.includes(allergenTxt.trim()) ? deCandidate : (deCandidate + allergenTxt);
|
||||||
|
|
||||||
|
deParts.push(deFinal);
|
||||||
|
enParts.push(enFinal);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No allergen code found! Need to heuristically split "EN DE"
|
// B) No slash found: Either missing translation or "EN DE" mixed
|
||||||
const split = heuristicSplitEnDe(fragment);
|
const heuristicSplit = heuristicSplitEnDe(courseText);
|
||||||
enParts.push(split.enPart);
|
if (heuristicSplit.nextDe) {
|
||||||
if (split.nextDe) {
|
enParts.push(heuristicSplit.enPart + allergenTxt);
|
||||||
deParts.push(split.nextDe);
|
deParts.push(heuristicSplit.nextDe + allergenTxt);
|
||||||
|
} else {
|
||||||
|
// Fallback: Use same chunk for both
|
||||||
|
deParts.push(courseText + allergenTxt);
|
||||||
|
enParts.push(courseText + allergenTxt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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• ');
|
let deJoined = deParts.join('\n• ');
|
||||||
if (deParts.length > 0 && !deJoined.startsWith('• ')) deJoined = '• ' + deJoined;
|
if (deParts.length > 0 && !deJoined.startsWith('• ')) deJoined = '• ' + deJoined;
|
||||||
|
|
||||||
|
|||||||
25
style.css
25
style.css
@@ -875,42 +875,31 @@ body {
|
|||||||
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 #8b5cf6;
|
|
||||||
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 #8b5cf6;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def main():
|
|||||||
|
|
||||||
# Check for placeholder leftovers
|
# Check for placeholder leftovers
|
||||||
if not check_content(BOOKMARKLET_TXT,
|
if not check_content(BOOKMARKLET_TXT,
|
||||||
must_contain=["document.createElement('style')", "M1", "M2"],
|
must_contain=["document.createElement('style')"],
|
||||||
must_not_contain=["{{VERSION}}", "{{CSS_ESCAPED}}"]):
|
must_not_contain=["{{VERSION}}", "{{CSS_ESCAPED}}"]):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -90,7 +102,8 @@ const sandbox = {
|
|||||||
try {
|
try {
|
||||||
vm.createContext(sandbox);
|
vm.createContext(sandbox);
|
||||||
// Execute the code
|
// Execute the code
|
||||||
vm.runInContext(code, sandbox);
|
const instrumentedCode = code.replace(/\n\}\)\(\);/, ' window.splitLanguage = splitLanguage;\n})();');
|
||||||
|
vm.runInContext(instrumentedCode, sandbox);
|
||||||
|
|
||||||
|
|
||||||
// Regex Check: update icon appended to header
|
// Regex Check: update icon appended to header
|
||||||
@@ -121,6 +134,7 @@ try {
|
|||||||
}
|
}
|
||||||
console.log("✅ Static Analysis Passed: All GitHub Release Management functions found.");
|
console.log("✅ Static Analysis Passed: All GitHub Release Management functions found.");
|
||||||
|
|
||||||
|
|
||||||
// Check dynamic logic usage
|
// Check dynamic logic usage
|
||||||
// Note: Since we mock fetch to fail for menu data, the app might perform error handling.
|
// Note: Since we mock fetch to fail for menu data, the app might perform error handling.
|
||||||
// We just want to ensure it doesn't CRASH (exit code) and that our specific feature logic ran.
|
// We just want to ensure it doesn't CRASH (exit code) and that our specific feature logic ran.
|
||||||
@@ -133,6 +147,51 @@ try {
|
|||||||
console.log("⚠️ Dynamic Check Skipped (Active execution verification relies on async/timing).");
|
console.log("⚠️ Dynamic Check Skipped (Active execution verification relies on async/timing).");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Split Language Logic Test ---
|
||||||
|
console.log("--- Testing splitLanguage Logic ---");
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: "Kürbiscremesuppe / Pumpkin cream (A) Achtung Änderung Frisches Grillhendl mit Semmel (A) Kuchen / Cake (ACGHO)",
|
||||||
|
expectedDeCourses: 3,
|
||||||
|
expectedEnCourses: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Schweinsbraten (M) / Roast pork (M)",
|
||||||
|
expectedDeCourses: 1,
|
||||||
|
expectedEnCourses: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Tagessuppe (L) / Daily soup (L)",
|
||||||
|
expectedDeCourses: 1,
|
||||||
|
expectedEnCourses: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "Nur Deutsch (A)",
|
||||||
|
expectedDeCourses: 1,
|
||||||
|
expectedEnCourses: 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// We can extract splitLanguage or getLocalizedText if they are in global scope,
|
||||||
|
// but they are inside the IIFE. We can instead check if the parsed data has the same number of courses visually.
|
||||||
|
// We can evaluate a function in the sandbox to do the splitting
|
||||||
|
for (const tc of testCases) {
|
||||||
|
const result = sandbox.window.splitLanguage(tc.input);
|
||||||
|
|
||||||
|
const deGange = result.de.split('•').filter(x => x.trim()).length;
|
||||||
|
const enGange = result.en.split('•').filter(x => x.trim()).length;
|
||||||
|
|
||||||
|
if (deGange !== tc.expectedDeCourses || enGange !== tc.expectedEnCourses || deGange !== enGange) {
|
||||||
|
console.error(`❌ splitLanguage Test Failed for "${tc.input}"`);
|
||||||
|
console.error(` Expected EN/DE: ${tc.expectedEnCourses}/${tc.expectedDeCourses}`);
|
||||||
|
console.error(` Got EN/DE: ${enGange}/${deGange}`);
|
||||||
|
console.error(` DE: ${result.de}`);
|
||||||
|
console.error(` EN: ${result.en}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("✅ splitLanguage Test Passed: DE and EN course counts match and fallback works.");
|
||||||
|
|
||||||
console.log("✅ Syntax Check Passed: Code executed in sandbox.");
|
console.log("✅ Syntax Check Passed: Code executed in sandbox.");
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v1.6.4
|
v1.6.11
|
||||||
|
|||||||
Reference in New Issue
Block a user