Compare commits

..

15 Commits

Author SHA1 Message Date
bab54fdf2d feat(ui): release v1.2.0 with improved installer UX, build tests, and docs update 2026-02-16 21:01:38 +01:00
efdb50083e fix(build): url-encode bookmarklet payload to prevent URI malformed error (v1.1.2) 2026-02-16 19:14:21 +01:00
d82dd31bed fix(core): resolve syntax errors in v1.1.0 preventing load (v1.1.1) 2026-02-16 19:00:43 +01:00
d6a2236a5b feat: release v1.1.0 (countdown, highlight tags, changelog) 2026-02-16 18:52:07 +01:00
71cb2e8475 fix: use htmlpreview.github.io for update link (v1.0.3) 2026-02-13 11:49:51 +01:00
18c4961402 build: update dist files for v1.0.2 2026-02-13 11:47:15 +01:00
Michael
25816b7fb5 Bump version from v1.0.1 to v1.0.2 - update test 2026-02-13 11:43:09 +01:00
d002e6a900 feat: add auto-update check (v1.0.1) 2026-02-13 11:40:45 +01:00
c58b54faf6 chore: bump version to v1.0.0 for initial release 2026-02-13 11:20:13 +01:00
bff7ce3874 fix: resolve version number conflict with Bessa global variable (v1.8.6) 2026-02-13 11:16:31 +01:00
6864838999 feat: show version number in app header (v1.8.6) 2026-02-13 11:13:34 +01:00
6f2a34ae0e feat: inject version number into installer and bookmarklet (v1.8.5) 2026-02-13 11:11:46 +01:00
7008b40987 fix: correct day header status logic and text color (v1.8.4) 2026-02-13 11:06:22 +01:00
3500790b5d feat: color-code day headers based on status (v1.8.3) 2026-02-13 10:54:29 +01:00
3d185140cc docs: revert to dark theme with teal accents (v1.8.2) 2026-02-13 09:36:35 +01:00
12 changed files with 1582 additions and 279 deletions

View File

@@ -1,15 +1,17 @@
# Kantine Wrapper Bookmarklet (v1.7.0) # Kantine Wrapper Bookmarklet (v1.2.0)
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.
## 🚀 Features ## 🚀 Features
* **Wochenübersicht:** Zeigt alle Tage der aktuellen Woche auf einen Blick. * **Wochenübersicht:** Zeigt alle Tage der aktuellen Woche auf einen Blick.
* **Bestell-Countdown:** ⏳ Roter Alarm 1h vor Bestellschluss.
* **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). * **Session Reuse:** Nutzt automatisch eine bestehende Login-Session (Loggt dich automatisch ein).
* **API Fallback:** Prüft die Verbindung und bietet bei Fehlern einen Direktlink zur Originalseite.
* **Menu Badges:** Zeigt Menü-Codes (M1, M2+) direkt im Header. * **Menu Badges:** Zeigt Menü-Codes (M1, M2+) direkt im Header.
* **Changelog:** Übersicht über neue Funktionen direkt im Installer.
## 📦 Installation ## 📦 Installation

View File

@@ -8,19 +8,28 @@ 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"
# === VERSION ===
if [ -f "$SCRIPT_DIR/version.txt" ]; then
VERSION=$(cat "$SCRIPT_DIR/version.txt" | tr -d '\n')
else
echo "ERROR: version.txt not found"
exit 1
fi
mkdir -p "$DIST_DIR" mkdir -p "$DIST_DIR"
echo "=== Kantine Bookmarklet Builder ===" echo "=== Kantine Bookmarklet Builder ($VERSION) ==="
# Check files exist # Check files exist
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
CSS_CONTENT=$(cat "$CSS_FILE") CSS_CONTENT=$(cat "$CSS_FILE")
JS_CONTENT=$(cat "$JS_FILE") # Inject version into JS
JS_CONTENT=$(cat "$JS_FILE" | sed "s/{{VERSION}}/$VERSION/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
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
@@ -37,7 +46,7 @@ HTMLEOF
# Inject CSS # Inject CSS
cat "$CSS_FILE" >> "$DIST_DIR/kantine-standalone.html" cat "$CSS_FILE" >> "$DIST_DIR/kantine-standalone.html"
cat >> "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF' cat >> "$DIST_DIR/kantine-standalone.html" << HTMLEOF
</style> </style>
</head> </head>
<body> <body>
@@ -45,9 +54,9 @@ cat >> "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF'
HTMLEOF HTMLEOF
# Inject JS # Inject JS
cat "$JS_FILE" >> "$DIST_DIR/kantine-standalone.html" echo "$JS_CONTENT" >> "$DIST_DIR/kantine-standalone.html"
cat >> "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF' cat >> "$DIST_DIR/kantine-standalone.html" << HTMLEOF
</script> </script>
</body> </body>
</html> </html>
@@ -69,7 +78,7 @@ var s=document.createElement('style');
s.textContent='${CSS_ESCAPED}'; s.textContent='${CSS_ESCAPED}';
document.head.appendChild(s); document.head.appendChild(s);
var sc=document.createElement('script'); var sc=document.createElement('script');
sc.textContent=$(cat "$JS_FILE" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || cat "$JS_FILE" | sed 's/\\/\\\\/g' | sed "s/'/\\\\'/g" | sed 's/"/\\\\"/g' | tr '\n' ' ' | sed 's/^/"/' | sed 's/$/"/'); 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/$/"/');
document.head.appendChild(sc); document.head.appendChild(sc);
})(); })();
PAYLOADEOF PAYLOADEOF
@@ -81,140 +90,129 @@ echo "javascript:${BOOKMARKLET_RAW}" > "$DIST_DIR/bookmarklet.txt"
echo "✅ Bookmarklet URL: $DIST_DIR/bookmarklet.txt" echo "✅ Bookmarklet URL: $DIST_DIR/bookmarklet.txt"
# === 3. Create an easy-to-use HTML installer page === # === 3. Create an easy-to-use HTML installer page ===
cat > "$DIST_DIR/install.html" << 'INSTALLEOF' cat > "$DIST_DIR/install.html" << INSTALLEOF
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Kantine Wrapper Installer</title> <title>Kantine Wrapper Installer ($VERSION)</title>
<style> <style>
:root { body { font-family: 'Inter', sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
--knapp-blue: #029AA8; h1 { color: #029AA8; } /* Knapp Teal */
--knapp-teal: #006269; .instructions { background: #16213e; padding: 20px; border-radius: 12px; margin: 20px 0; }
--knapp-yellow: #FFED00; .instructions ol li { margin: 10px 0; }
--text-main: #333333; a.bookmarklet { display: inline-block; background: #029AA8; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 18px; cursor: grab; }
--bg-body: #f4f6f8; a.bookmarklet:hover { background: #006269; }
--bg-card: #ffffff; code { background: #0f3460; padding: 2px 6px; border-radius: 4px; }
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
max-width: 700px;
margin: 40px auto;
padding: 20px;
background: var(--bg-body);
color: var(--text-main);
line-height: 1.6;
}
h1 {
color: var(--knapp-teal);
display: flex;
align-items: center;
gap: 10px;
border-bottom: 2px solid var(--knapp-blue);
padding-bottom: 15px;
}
h2 { color: var(--knapp-blue); font-size: 1.25em; margin-top: 0; }
.instructions {
background: var(--bg-card);
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
margin: 20px 0;
}
.instructions ol { padding-left: 20px; }
.instructions ol li { margin-bottom: 10px; }
a { color: var(--knapp-blue); text-decoration: none; font-weight: 500; }
a:hover { text-decoration: underline; }
a.bookmarklet {
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--knapp-teal);
color: white;
padding: 15px 30px;
border-radius: 4px;
text-decoration: none;
font-weight: 600;
font-size: 18px;
cursor: grab;
box-shadow: 0 4px 6px rgba(0,98,105,0.2);
transition: all 0.2s;
border-bottom: 3px solid rgba(0,0,0,0.1);
}
a.bookmarklet:hover {
background: var(--knapp-blue);
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(2,154,168,0.25);
}
a.bookmarklet:active {
cursor: grabbing;
}
code {
background: #eef2f5;
padding: 2px 6px;
border-radius: 4px;
color: var(--knapp-teal);
font-family: monospace;
border: 1px solid #dae1e7;
}
ul { padding-left: 20px; color: #555; }
ul li { margin-bottom: 8px; }
.disclaimer {
margin-top: 30px;
padding: 15px;
background: #fffde7; /* Light yellow */
border-left: 4px solid var(--knapp-yellow);
border-radius: 4px;
font-size: 0.85em;
color: #444;
}
</style> </style>
</head> </head>
<body> <body>
<h1>🍽️ Kantine Wrapper</h1> <h1>🍽️ Kantine Wrapper <span style="font-size:0.5em; opacity:0.6; font-weight:400; vertical-align:middle; margin-left:10px;">$VERSION</span></h1>
<div class="instructions">
<h2>Installation</h2>
<ol>
<li>Ziehe den Button unten in deine <strong>Lesezeichen-Leiste</strong> (Drag & Drop)</li>
<li>Navigiere zu <a href="https://web.bessa.app/knapp-kantine" style="color:#e94560">web.bessa.app/knapp-kantine</a></li>
<li>Klicke auf das Lesezeichen <code>Kantine Wrapper</code></li>
</ol>
<h2>✨ Features</h2> <!-- 1. BUTTON (Top Priority) -->
<ul> <div class="card" style="text-align: center; border: 2px solid #029AA8;">
<li>📅 <strong>Wochenübersicht:</strong> Die ganze Woche auf einen Blick.</li> <p style="margin-bottom:15px; font-weight:bold;">👇 Diesen Button in die Lesezeichen-Leiste ziehen:</p>
<li>💰 <strong>Kostenkontrolle:</strong> Automatische Berechnung der Wochensumme.</li> <p><a class="bookmarklet" id="bookmarklet-link" href="#">⏳ Wird generiert...</a></p>
<li>🔑 <strong>Auto-Login:</strong> Nutzt deine bestehende Session.</li> </div>
<li>🏷️ <strong>Badges & Status:</strong> Menü-Codes (M1, M2) und Bestellstatus direkt sichtbar.</li>
<li>🛡️ <strong>Offline-Support:</strong> Speichert Menüdaten lokal.</li>
</ul>
<div class="disclaimer"> <!-- 2. INSTRUCTIONS -->
<div class="card">
<h2>So funktioniert's</h2>
<ol>
<li>Ziehe den Button oben in deine <strong>Lesezeichen-Leiste</strong> (Drag & Drop)</li>
<li>Navigiere zu <a href="https://web.bessa.app/knapp-kantine" style="color:#029AA8">web.bessa.app/knapp-kantine</a></li>
<li>Klicke auf das Lesezeichen <code>Kantine $VERSION</code></li>
</ol>
</div>
<!-- 3. FEATURES -->
<div class="card">
<h2>✨ Features</h2>
<ul>
<li>📅 <strong>Wochenübersicht:</strong> Die ganze Woche auf einen Blick.</li>
<li>⏳ <strong>Order Countdown:</strong> Roter Alarm 1h vor Bestellschluss.</li>
<li>🌟 <strong>Smart Highlights:</strong> Markiere deine Favoriten (z.B. "Schnitzel").</li>
<li>💰 <strong>Kostenkontrolle:</strong> Automatische Berechnung der Wochensumme.</li>
<li>🔑 <strong>Auto-Login:</strong> Nutzt deine bestehende Session.</li>
<li>🏷️ <strong>Badges & Status:</strong> Menü-Codes (M1, M2) und Bestellstatus direkt sichtbar.</li>
</ul>
<div style="margin-top: 30px; padding: 15px; background: rgba(233, 69, 96, 0.1); border: 1px solid rgba(233, 69, 96, 0.3); border-radius: 8px; font-size: 0.85em; color: #ddd;">
<strong>⚠️ Haftungsausschluss:</strong><br> <strong>⚠️ Haftungsausschluss:</strong><br>
Die Verwendung dieses Bookmarklets erfolgt auf eigene Verantwortung. Der Entwickler übernimmt keine Haftung für Schäden, Datenverlust oder ungewollte Bestellungen, die durch die Nutzung dieser Software entstehen. Die Verwendung dieses Bookmarklets erfolgt auf eigene Verantwortung. Der Entwickler übernimmt keine Haftung für Schäden, Datenverlust oder ungewollte Bestellungen, die durch die Nutzung dieser Software entstehen.
</div>
</div>
<!-- 4. CHANGELOG (Bottom) -->
<div class="card">
<h2>Changelog</h2>
<div class="changelog-container">
<!-- CHANGELOG_PLACEHOLDER -->
</div> </div>
</div> </div>
<p>👇 Diesen Button in die Lesezeichen-Leiste ziehen:</p>
<p><a class="bookmarklet" id="bookmarklet-link" href="#">⏳ Wird generiert...</a></p>
<script> <script>
INSTALLEOF INSTALLEOF
# Generate Changlog HTML from Markdown
CHANGELOG_HTML=""
if [ -f "$SCRIPT_DIR/changelog.md" ]; then
CHANGELOG_HTML=$(cat "$SCRIPT_DIR/changelog.md" | python3 -c "
import sys, re
md = sys.stdin.read()
# Convert headers to h3/h4
html = re.sub(r'^## (.*)', r'<h3>\1</h3>', md, flags=re.MULTILINE)
# Convert bullets to list items
html = re.sub(r'^- (.*)', r'<li>\1</li>', html, flags=re.MULTILINE)
# Wrap lists (simple heuristic)
html = html.replace('</h3>\n<li>', '</h3>\n<ul>\n<li>').replace('</li>\n<h', '</li>\n</ul>\n<h').replace('</li>\n\n', '</li>\n</ul>\n')
if '<li>' in html and '<ul>' not in html: html = '<ul>' + html + '</ul>'
print(html)
")
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"
cat "$JS_FILE" | python3 -c " echo "$JS_CONTENT" | python3 -c "
import sys, json import sys, json, urllib.parse
js = sys.stdin.read()
css = open('$CSS_FILE').read().replace('\\n', ' ').replace(' ', ' ')
bmk = '''javascript:(function(){if(window.__KANTINE_LOADED){alert(\"Already loaded\");return;}var s=document.createElement(\"style\");s.textContent=''' + json.dumps(css) + ''';document.head.appendChild(s);var sc=document.createElement(\"script\");sc.textContent=''' + json.dumps(js) + ''';document.head.appendChild(sc);})();'''
print(json.dumps(bmk) + ';')
" 2>/dev/null >> "$DIST_DIR/install.html" || echo "'javascript:alert(\"Build error\")'" >> "$DIST_DIR/install.html"
cat >> "$DIST_DIR/install.html" << 'INSTALLEOF' # 1. Read JS and Replace VERSION
document.getElementById('bookmarklet-link').textContent = '🍽️ Kantine Wrapper'; js_template = sys.stdin.read()
js = js_template.replace('{{VERSION}}', '$VERSION')
# 2. Prepare CSS for injection via createElement('style')
css = open('$CSS_FILE').read().replace('\n', ' ').replace(' ', ' ')
escaped_css = css.replace('\\\\', '\\\\\\\\').replace(\"'\", \"\\\\'\").replace('\"', '\\\\\"')
# 3. Update URL
update_url = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html'
js = js.replace('https://github.com/TauNeutrino/kantine-overview/raw/main/dist/install.html', update_url)
# 4. Create Bookmarklet Code with CSS injection
# Inject CSS via style element (same pattern as bookmarklet-payload.js)
css_injection = \"var s=document.createElement('style');s.textContent='\" + escaped_css + \"';document.head.appendChild(s);\"
bookmarklet_code = 'javascript:(function(){' + css_injection + js + '})();'
# 5. URL Encode
encoded_code = urllib.parse.quote(bookmarklet_code, safe=':/()!;=+,')
# Output as JSON string for the HTML script to assign to href
print(json.dumps(encoded_code) + ';')
" >> "$DIST_DIR/install.html"
# Inject Changelog into Installer HTML (Safe Python replace)
python3 -c "
import sys
html = open('$DIST_DIR/install.html').read()
changelog = sys.stdin.read()
html = html.replace('<!-- CHANGELOG_PLACEHOLDER -->', changelog)
open('$DIST_DIR/install.html', 'w').write(html)
" << EOF
$CHANGELOG_HTML
EOF
cat >> "$DIST_DIR/install.html" << INSTALLEOF
document.getElementById('bookmarklet-link').textContent = 'Kantine $VERSION';
</script> </script>
</body> </body>
</html> </html>
@@ -225,3 +223,14 @@ echo ""
echo "=== Build Complete ===" echo "=== Build Complete ==="
echo "Files in $DIST_DIR:" echo "Files in $DIST_DIR:"
ls -la "$DIST_DIR/" ls -la "$DIST_DIR/"
# === 4. Run build-time tests ===
echo ""
echo "=== Running Build Tests ==="
python3 "$SCRIPT_DIR/test_build.py"
TEST_EXIT=$?
if [ $TEST_EXIT -ne 0 ]; then
echo "❌ Build tests FAILED! See above for details."
exit 1
fi
echo "✅ All build tests passed."

150
build-bookmarklet_old.sh Executable file
View File

@@ -0,0 +1,150 @@
#!/bin/bash
# Build script for Kantine Bookmarklet
# Creates a self-contained bookmarklet URL and standalone HTML file
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DIST_DIR="$SCRIPT_DIR/dist"
CSS_FILE="$SCRIPT_DIR/style.css"
JS_FILE="$SCRIPT_DIR/kantine.js"
mkdir -p "$DIST_DIR"
echo "=== Kantine Bookmarklet Builder ==="
# Check files exist
if [ ! -f "$CSS_FILE" ]; then echo "ERROR: $CSS_FILE not found"; exit 1; fi
if [ ! -f "$JS_FILE" ]; then echo "ERROR: $JS_FILE not found"; exit 1; fi
CSS_CONTENT=$(cat "$CSS_FILE")
JS_CONTENT=$(cat "$JS_FILE")
# === 1. Build standalone HTML (for local testing/dev) ===
cat > "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF'
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kantine Weekly Menu (Standalone)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
<style>
HTMLEOF
# Inject CSS
cat "$CSS_FILE" >> "$DIST_DIR/kantine-standalone.html"
cat >> "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF'
</style>
</head>
<body>
<script>
HTMLEOF
# Inject JS
cat "$JS_FILE" >> "$DIST_DIR/kantine-standalone.html"
cat >> "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF'
</script>
</body>
</html>
HTMLEOF
echo "✅ Standalone HTML: $DIST_DIR/kantine-standalone.html"
# === 2. Build bookmarklet (JavaScript URL) ===
# The bookmarklet injects CSS + JS into the current page
# Escape CSS for embedding in JS string
CSS_ESCAPED=$(echo "$CSS_CONTENT" | sed "s/'/\\\\'/g" | tr '\n' ' ' | sed 's/ */ /g')
# Build bookmarklet payload
cat > "$DIST_DIR/bookmarklet-payload.js" << PAYLOADEOF
(function(){
if(window.__KANTINE_LOADED){alert('Kantine Wrapper already loaded!');return;}
var s=document.createElement('style');
s.textContent='${CSS_ESCAPED}';
document.head.appendChild(s);
var sc=document.createElement('script');
sc.textContent=$(cat "$JS_FILE" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || cat "$JS_FILE" | sed 's/\\/\\\\/g' | sed "s/'/\\\\'/g" | sed 's/"/\\\\"/g' | tr '\n' ' ' | sed 's/^/"/' | sed 's/$/"/');
document.head.appendChild(sc);
})();
PAYLOADEOF
# URL-encode for bookmark
BOOKMARKLET_RAW=$(cat "$DIST_DIR/bookmarklet-payload.js" | tr '\n' ' ' | sed 's/ */ /g')
echo "javascript:${BOOKMARKLET_RAW}" > "$DIST_DIR/bookmarklet.txt"
echo "✅ Bookmarklet URL: $DIST_DIR/bookmarklet.txt"
# === 3. Create an easy-to-use HTML installer page ===
cat > "$DIST_DIR/install.html" << 'INSTALLEOF'
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Kantine Wrapper Installer</title>
<style>
body { font-family: 'Inter', sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
h1 { color: #e94560; }
.instructions { background: #16213e; padding: 20px; border-radius: 12px; margin: 20px 0; }
.instructions ol li { margin: 10px 0; }
a.bookmarklet { display: inline-block; background: #e94560; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 18px; cursor: grab; }
a.bookmarklet:hover { background: #c73652; }
code { background: #0f3460; padding: 2px 6px; border-radius: 4px; }
</style>
</head>
<body>
<h1>🍽️ Kantine Wrapper</h1>
<div class="instructions">
<h2>Installation</h2>
<ol>
<li>Ziehe den Button unten in deine <strong>Lesezeichen-Leiste</strong> (Drag & Drop)</li>
<li>Navigiere zu <a href="https://web.bessa.app/knapp-kantine" style="color:#e94560">web.bessa.app/knapp-kantine</a></li>
<li>Klicke auf das Lesezeichen <code>Kantine Wrapper</code></li>
</ol>
<h2>✨ Features</h2>
<ul>
<li>📅 <strong>Wochenübersicht:</strong> Die ganze Woche auf einen Blick.</li>
<li>💰 <strong>Kostenkontrolle:</strong> Automatische Berechnung der Wochensumme.</li>
<li>🔑 <strong>Auto-Login:</strong> Nutzt deine bestehende Session.</li>
<li>🏷️ <strong>Badges & Status:</strong> Menü-Codes (M1, M2) und Bestellstatus direkt sichtbar.</li>
<li>🛡️ <strong>Offline-Support:</strong> Speichert Menüdaten lokal.</li>
</ul>
<div style="margin-top: 30px; padding: 15px; background: rgba(233, 69, 96, 0.1); border: 1px solid rgba(233, 69, 96, 0.3); border-radius: 8px; font-size: 0.85em; color: #ddd;">
<strong>⚠️ Haftungsausschluss:</strong><br>
Die Verwendung dieses Bookmarklets erfolgt auf eigene Verantwortung. Der Entwickler übernimmt keine Haftung für Schäden, Datenverlust oder ungewollte Bestellungen, die durch die Nutzung dieser Software entstehen.
</div>
</div>
<p>👇 Diesen Button in die Lesezeichen-Leiste ziehen:</p>
<p><a class="bookmarklet" id="bookmarklet-link" href="#">⏳ Wird generiert...</a></p>
<script>
INSTALLEOF
# Embed the bookmarklet URL inline
echo "document.getElementById('bookmarklet-link').href = " >> "$DIST_DIR/install.html"
cat "$JS_FILE" | python3 -c "
import sys, json
js = sys.stdin.read()
css = open('$CSS_FILE').read().replace('\\n', ' ').replace(' ', ' ')
bmk = '''javascript:(function(){if(window.__KANTINE_LOADED){alert(\"Already loaded\");return;}var s=document.createElement(\"style\");s.textContent=''' + json.dumps(css) + ''';document.head.appendChild(s);var sc=document.createElement(\"script\");sc.textContent=''' + json.dumps(js) + ''';document.head.appendChild(sc);})();'''
print(json.dumps(bmk) + ';')
" 2>/dev/null >> "$DIST_DIR/install.html" || echo "'javascript:alert(\"Build error\")'" >> "$DIST_DIR/install.html"
cat >> "$DIST_DIR/install.html" << 'INSTALLEOF'
document.getElementById('bookmarklet-link').textContent = '🍽️ Kantine Wrapper';
</script>
</body>
</html>
INSTALLEOF
echo "✅ Installer page: $DIST_DIR/install.html"
echo ""
echo "=== Build Complete ==="
echo "Files in $DIST_DIR:"
ls -la "$DIST_DIR/"

26
changelog.md Executable file
View File

@@ -0,0 +1,26 @@
## v1.2.0 (2026-02-16)
- **Feature**: Bessere UX im Installer (Button oben, Log unten, Features aktualisiert). 💅
- **Tech**: Build-Tests hinzugefügt. 🧪
- **Fix**: Encoding-Probleme final behoben (dank Python Buildlogic). 🐍
## v1.1.2 (2026-02-16)
- **Fix**: Encoding-Problem beim Bookmarklet behoben (URL Malformed Error). 🔗
## v1.1.1 (2026-02-16)
- **Fix**: Kritischer Fehler behoben, der das Laden des Wrappers verhinderte. 🐛
## v1.1.0 (2026-02-16)
- **Feature: Bestell-Countdown**: Zeigt 1 Stunde vor Bestellschluss einen roten Countdown an. ⏳
- **Feature: Smart Highlights**: Markiere deine Lieblingsspeisen (z.B. "Schnitzel", "Vegetarisch"), damit sie leuchten. 🌟
- **Feature: Changelog**: Diese Übersicht der Änderungen. 📜
- **Verbesserung**: Live-Check der Version beim Update.
## v1.0.3 (2026-02-13)
- **Fix**: Update-Link öffnet nun den Installer direkt als Webseite (via htmlpreview).
## v1.0.2 (2026-02-13)
- **Sync**: Version mit GitHub synchronisiert.
## v1.0.1 (2026-02-12)
- **UI**: Besseres Design für "Nächste Woche" (Badges).
- **Core**: Grundlegende Funktionen (Bestellen, Guthaben, Token-Store).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

187
dist/install.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -152,17 +152,6 @@ body {
justify-content: center; justify-content: center;
} }
.weekly-cost {
white-space: nowrap;
font-size: 0.9rem;
font-weight: 600;
color: var(--success-color);
background-color: var(--bg-body);
padding: 0.25rem 0.75rem;
border-radius: 20px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
border: 1px solid var(--border-color);
}
.header-week-title { .header-week-title {
font-size: 1.1rem; font-size: 1.1rem;
@@ -1116,6 +1105,7 @@ body {
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3); box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
} }
} }
/* Day Header Badges */ /* Day Header Badges */
.day-header-left { .day-header-left {
display: flex; display: flex;
@@ -1126,7 +1116,8 @@ body {
.menu-code-badge { .menu-code-badge {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
color: #8b5cf6; /* Violet 500 */ color: #8b5cf6;
/* Violet 500 */
background-color: rgba(139, 92, 246, 0.15); background-color: rgba(139, 92, 246, 0.15);
border: 1px solid rgba(139, 92, 246, 0.3); border: 1px solid rgba(139, 92, 246, 0.3);
padding: 2px 6px; padding: 2px 6px;
@@ -1136,11 +1127,237 @@ body {
} }
/* Detailed Badge Colors */ /* Detailed Badge Colors */
.nav-badge.badge-violet { background-color: #8b5cf6; } .nav-badge.badge-violet {
.nav-badge.badge-green { background-color: var(--success-color); } background-color: #8b5cf6;
.nav-badge.badge-red { background-color: var(--error-color); } }
.nav-badge.badge-blue { background-color: var(--accent-color); }
</style> .nav-badge.badge-green {
background-color: var(--success-color);
}
.nav-badge.badge-red {
background-color: var(--error-color);
}
.nav-badge.badge-blue {
background-color: var(--accent-color);
}
/* Day Header Status Colors (User Request) */
.card-header.header-violet {
background-color: rgba(139, 92, 246, 0.15);
border-bottom: 2px solid #8b5cf6;
}
.card-header.header-green {
background-color: rgba(16, 185, 129, 0.15);
border-bottom: 2px solid var(--success-color);
}
.card-header.header-red {
background-color: rgba(239, 68, 68, 0.15);
border-bottom: 2px solid var(--error-color);
}
.card-header.header-violet .day-name,
.card-header.header-green .day-name,
.card-header.header-red .day-name {
font-weight: 700;
color: var(--text-primary);
/* Ensure text remains standard color */
}
/* Update Icon */
.update-icon {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 8px;
background-color: rgba(16, 185, 129, 0.2);
/* Green tint */
color: var(--success-color);
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
text-decoration: none;
animation: pulse 2s infinite;
}
.update-icon:hover {
background-color: var(--success-color);
color: white;
transform: scale(1.1);
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
}
70% {
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
/* Order Countdown */
#order-countdown {
background: rgba(255, 255, 255, 0.1);
padding: 0.25rem 0.75rem;
border-radius: 99px;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
border: 1px solid var(--border-color);
}
#order-countdown span {
opacity: 0.7;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
#order-countdown.urgent {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.5);
color: #ef4444;
animation: pulse-red 2s infinite;
}
@keyframes pulse-red {
0% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
}
70% {
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
}
}
/* Smart Highlights */
.highlight-glow {
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
/* Blue glow */
border: 1px solid rgba(59, 130, 246, 0.8);
background: rgba(59, 130, 246, 0.05);
position: relative;
z-index: 1;
}
/* Nav Badge with Count */
.nav-badge.has-highlights {
background-color: var(--bg-card);
/* Neutral background */
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 2px 6px;
}
.nav-badge .highlight-count {
color: #3b82f6;
/* Blue 500 */
font-weight: 700;
margin-left: 4px;
}
/* Tag Management Modal */
#tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
min-height: 50px;
}
.tag-badge {
display: inline-flex;
align-items: center;
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
padding: 4px 10px;
border-radius: 99px;
font-size: 0.85rem;
font-weight: 500;
}
.tag-remove {
margin-left: 6px;
cursor: pointer;
opacity: 0.7;
font-size: 1.1em;
line-height: 1;
}
.tag-remove:hover {
opacity: 1;
color: #ef4444;
}
.input-group {
display: flex;
gap: 0.5rem;
}
.input-group input {
flex: 1;
padding: 0.75rem;
background: var(--bg-body);
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 8px;
}
/* Update Banner Enhanced */
.change-summary {
font-size: 0.8rem;
background: rgba(0, 0, 0, 0.1);
padding: 0.5rem;
border-radius: 4px;
margin: 0.5rem 0;
white-space: pre-wrap;
font-family: inherit;
line-height: 1.4;
max-height: 100px;
overflow-y: auto;
}
.update-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
/* Installer Changelog */
.changelog-container ul {
padding-left: 1.5rem;
margin: 0.5rem 0;
}
.changelog-container li {
margin-bottom: 0.4rem;
line-height: 1.5;
}
.changelog-container h3 {
margin-top: 1.5rem;
margin-bottom: 0.5rem;
font-size: 1.1em;
color: var(--accent-color);
} </style>
</head> </head>
<body> <body>
<script> <script>
@@ -1211,8 +1428,8 @@ body {
<div class="header-content"> <div class="header-content">
<div class="brand"> <div class="brand">
<span class="material-icons-round logo-icon">restaurant_menu</span> <span class="material-icons-round logo-icon">restaurant_menu</span>
<div class="brand-text"> <div class="header-left">
<h1>Kantinen Übersicht</h1> <h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">v1.2.0</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div> <div id="last-updated-subtitle" class="subtitle"></div>
</div> </div>
</div> </div>
@@ -1224,6 +1441,9 @@ body {
<button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren" title="Menüdaten neu laden"> <button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren" title="Menüdaten neu laden">
<span class="material-icons-round">refresh</span> <span class="material-icons-round">refresh</span>
</button> </button>
<button id="btn-highlights" class="icon-btn" aria-label="Persönliche Highlights verwalten" title="Persönliche Highlights verwalten">
<span class="material-icons-round">label</span>
</button>
<div class="nav-group"> <div class="nav-group">
<button id="btn-this-week" class="nav-btn active">Diese Woche</button> <button id="btn-this-week" class="nav-btn active">Diese Woche</button>
<button id="btn-next-week" class="nav-btn">Nächste Woche</button> <button id="btn-next-week" class="nav-btn">Nächste Woche</button>
@@ -1290,6 +1510,27 @@ body {
</div> </div>
</div> </div>
<div id="highlights-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h2>Meine Highlights</h2>
<button id="btn-highlights-close" class="icon-btn" aria-label="Close">
<span class="material-icons-round">close</span>
</button>
</div>
<div class="modal-body">
<p style="margin-bottom: 1rem; color: var(--text-secondary);">
Markiere Menüs automatisch, wenn sie diese Schlagwörter enthalten.
</p>
<div class="input-group">
<input type="text" id="tag-input" placeholder="z.B. Schnitzel, Vegetarisch...">
<button id="btn-add-tag" class="btn-primary">Hinzufügen</button>
</div>
<div id="tags-list"></div>
</div>
</div>
</div>
<main class="container"> <main class="container">
<div id="last-updated-banner" class="banner hidden"> <div id="last-updated-banner" class="banner hidden">
<span class="material-icons-round">update</span> <span class="material-icons-round">update</span>
@@ -1320,6 +1561,41 @@ body {
const loginForm = document.getElementById('login-form'); const loginForm = document.getElementById('login-form');
const loginModal = document.getElementById('login-modal'); const loginModal = document.getElementById('login-modal');
// Highlights Modal
const btnHighlights = document.getElementById('btn-highlights');
const highlightsModal = document.getElementById('highlights-modal');
const btnHighlightsClose = document.getElementById('btn-highlights-close');
const btnAddTag = document.getElementById('btn-add-tag');
const tagInput = document.getElementById('tag-input');
btnHighlights.addEventListener('click', () => {
highlightsModal.classList.remove('hidden');
renderTagsList();
tagInput.focus();
});
btnHighlightsClose.addEventListener('click', () => {
highlightsModal.classList.add('hidden');
});
window.addEventListener('click', (e) => {
if (e.target === highlightsModal) highlightsModal.classList.add('hidden');
});
btnAddTag.addEventListener('click', () => {
const tag = tagInput.value;
if (addHighlightTag(tag)) {
tagInput.value = '';
renderTagsList();
}
});
tagInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
btnAddTag.click();
}
});
// Theme // Theme
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -1738,18 +2014,65 @@ body {
} }
// Refresh menu data to update UI // Refresh menu data to update UI
loadMenuDataFromAPI(); loadMenuDataFromAPI();
break; // One refresh is enough
} }
} }
} catch (err) { } catch (err) {
console.error(`Poll error for ${flagId}:`, err); console.error(`Poll error for ${flagId}:`, err);
// Small delay between checks
await new Promise(r => setTimeout(r, 200));
} }
// Small delay between checks
await new Promise(r => setTimeout(r, 200));
} }
} }
// === Highlight Management ===
let highlightTags = JSON.parse(localStorage.getItem('kantine_highlightTags') || '[]');
function saveHighlightTags() {
localStorage.setItem('kantine_highlightTags', JSON.stringify(highlightTags));
renderVisibleWeeks(); // Refresh UI to apply changes
updateNextWeekBadge();
}
function addHighlightTag(tag) {
tag = tag.trim().toLowerCase();
if (tag && !highlightTags.includes(tag)) {
highlightTags.push(tag);
saveHighlightTags();
return true;
}
return false;
}
function removeHighlightTag(tag) {
highlightTags = highlightTags.filter(t => t !== tag);
saveHighlightTags();
}
function renderTagsList() {
const list = document.getElementById('tags-list');
list.innerHTML = '';
highlightTags.forEach(tag => {
const badge = document.createElement('span');
badge.className = 'tag-badge';
badge.innerHTML = `${tag} <span class="tag-remove" data-tag="${tag}">&times;</span>`;
list.appendChild(badge);
});
// Bind remove events
list.querySelectorAll('.tag-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
removeHighlightTag(e.target.dataset.tag);
renderTagsList();
});
});
}
function checkHighlight(text) {
if (!text) return false;
text = text.toLowerCase();
return highlightTags.some(tag => text.includes(tag));
}
// === Local Menu Cache (localStorage) === // === Local Menu Cache (localStorage) ===
const CACHE_KEY = 'kantine_menuCache'; const CACHE_KEY = 'kantine_menuCache';
const CACHE_TS_KEY = 'kantine_menuCacheTs'; const CACHE_TS_KEY = 'kantine_menuCacheTs';
@@ -2074,6 +2397,25 @@ body {
badge.classList.add('badge-blue'); // Default / partial state badge.classList.add('badge-blue'); // Default / partial state
} }
// Advanced Feature: Highlight Count
let highlightCount = 0;
if (nextWeekData && nextWeekData.days) {
nextWeekData.days.forEach(day => {
day.items.forEach(item => {
if (checkHighlight(item.name) || checkHighlight(item.description)) {
highlightCount++;
}
});
});
}
if (highlightCount > 0) {
// Append blue count
badge.innerHTML += `<span class="highlight-count" title="${highlightCount} Highlight Menüs">(${highlightCount})</span>`;
badge.title += `${highlightCount} Highlights gefunden`;
badge.classList.add('has-highlights');
}
} else if (badge) { } else if (badge) {
badge.remove(); badge.remove();
} }
@@ -2239,6 +2581,33 @@ body {
const badgesHtml = menuBadges.map(code => `<span class="menu-code-badge">${code}</span>`).join(''); const badgesHtml = menuBadges.map(code => `<span class="menu-code-badge">${code}</span>`).join('');
// Determine Day Status for Header Color
// Violet: Has Order
// Green: No Order but Orderable
// Red: No Order and Not Orderable (Locked/Sold Out)
let headerClass = '';
const hasAnyOrder = day.items && day.items.some(item => {
const articleId = item.articleId || parseInt(item.id.split('_')[1]);
const key = `${day.date}_${articleId}`;
return orderMap.has(key) && orderMap.get(key).length > 0;
});
const hasOrderable = day.items && day.items.some(item => {
// Use pre-calculated available flag from loadMenuDataFromAPI calculation
return item.available;
});
if (hasAnyOrder) {
headerClass = 'header-violet';
} else if (hasOrderable && !isPastCutoff) {
headerClass = 'header-green';
} else {
// Red if not orderable (or past cutoff)
headerClass = 'header-red';
}
if (headerClass) header.classList.add(headerClass);
header.innerHTML = ` header.innerHTML = `
<div class="day-header-left"> <div class="day-header-left">
<span class="day-name">${translateDay(day.weekday)}</span> <span class="day-name">${translateDay(day.weekday)}</span>
@@ -2391,6 +2760,158 @@ body {
return card; return card;
} }
// === Version Check ===
async function checkForUpdates() {
const CurrentVersion = 'v1.2.0';
const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';
const InstallerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';
console.log(`[Kantine] Checking for updates... (Current: ${CurrentVersion})`);
try {
const response = await fetch(VersionUrl, { cache: 'no-cache' });
if (!response.ok) return;
const remoteVersion = (await response.text()).trim();
if (remoteVersion && remoteVersion !== CurrentVersion) {
console.log(`[Kantine] New version available: ${remoteVersion}`);
// Fetch Changelog content
let changeSummary = '';
try {
const clResp = await fetch('https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/changelog.md');
if (clResp.ok) {
const clText = await clResp.text();
const match = clText.match(/## (v[^\n]+)\n((?:-[^\n]+\n)+)/);
if (match && match[1].includes(remoteVersion)) {
changeSummary = match[2].replace(/- /g, '• ').trim();
}
}
} catch (e) { console.warn('No changelog', e); }
// Create Banner
const updateBanner = document.createElement('div');
updateBanner.className = 'update-banner';
updateBanner.innerHTML = `
<div class="update-content">
<strong>Update verfügbar: ${remoteVersion}</strong>
${changeSummary ? `<pre class="change-summary">${changeSummary}</pre>` : ''}
<a href="${InstallerUrl}" target="_blank" class="update-link">
<span class="material-icons-round">system_update_alt</span>
Jetzt aktualisieren
</a>
</div>
<button class="icon-btn-small close-update">&times;</button>
`;
document.body.appendChild(updateBanner);
updateBanner.querySelector('.close-update').addEventListener('click', () => updateBanner.remove());
// Highlight Header Icon
const lastUpdatedIcon = document.querySelector('.material-icons-round.logo-icon');
if (lastUpdatedIcon) {
lastUpdatedIcon.style.color = 'var(--accent-color)';
lastUpdatedIcon.parentElement.title = `Update verfügbar: ${remoteVersion}`;
}
}
} catch (error) {
console.warn('[Kantine] Version check failed:', error);
}
}
// === Order Countdown ===
function updateCountdown() {
const now = new Date();
const currentDay = now.getDay();
// Skip weekends (0=Sun, 6=Sat)
if (currentDay === 0 || currentDay === 6) {
removeCountdown();
return;
}
const todayStr = now.toISOString().split('T')[0];
// 1. Check if we already ordered for today
let hasOrder = false;
// Optimization: Check orderMap for today's date
// Keys are "YYYY-MM-DD_ArticleID"
for (const key of orderMap.keys()) {
if (key.startsWith(todayStr)) {
hasOrder = true;
break;
}
}
if (hasOrder) {
removeCountdown();
return;
}
// 2. Calculate time to cutoff (10:00 AM)
const cutoff = new Date();
cutoff.setHours(10, 0, 0, 0);
const diff = cutoff - now;
// If passed cutoff or more than 3 hours away (e.g. 07:00), maybe don't show?
// User req: "heute noch keine bestellung... countdown erscheinen"
// Let's show it if within valid order window (e.g. 00:00 - 10:00)
if (diff <= 0) {
removeCountdown();
return;
}
// 3. Render Countdown
const diffHrs = Math.floor(diff / 3600000);
const diffMins = Math.floor((diff % 3600000) / 60000);
const headerCenter = document.querySelector('.header-center-wrapper');
if (!headerCenter) return;
let countdownEl = document.getElementById('order-countdown');
if (!countdownEl) {
countdownEl = document.createElement('div');
countdownEl.id = 'order-countdown';
// Insert before cost display or append
headerCenter.insertBefore(countdownEl, headerCenter.firstChild);
}
countdownEl.innerHTML = `<span>Bestellschluss:</span> <strong>${diffHrs}h ${diffMins}m</strong>`;
// Red Alert if < 1 hour
if (diff < 3600000) { // 1 hour
countdownEl.classList.add('urgent');
// Notification logic (One time)
const notifiedKey = `kantine_notified_${todayStr}`;
if (!sessionStorage.getItem(notifiedKey)) {
if (Notification.permission === 'granted') {
new Notification('Kantine: Bestellschluss naht!', {
body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!',
icon: '⏳'
});
} else if (Notification.permission === 'default') {
Notification.requestPermission();
}
sessionStorage.setItem(notifiedKey, 'true');
}
} else {
countdownEl.classList.remove('urgent');
}
}
function removeCountdown() {
const el = document.getElementById('order-countdown');
if (el) el.remove();
}
// Update countdown every minute
setInterval(updateCountdown, 60000);
// Also update on load
setTimeout(updateCountdown, 1000);
// === Helpers === // === Helpers ===
function getISOWeek(date) { function getISOWeek(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
@@ -2406,6 +2927,7 @@ body {
return date.getFullYear(); return date.getFullYear();
} }
function translateDay(englishDay) { function translateDay(englishDay) {
const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' }; const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' };
return map[englishDay] || englishDay; return map[englishDay] || englishDay;
@@ -2436,6 +2958,9 @@ body {
startPolling(); startPolling();
} }
// Check for updates
checkForUpdates();
console.log('Kantine Wrapper loaded ✅'); console.log('Kantine Wrapper loaded ✅');
})(); })();

View File

@@ -65,8 +65,8 @@
<div class="header-content"> <div class="header-content">
<div class="brand"> <div class="brand">
<span class="material-icons-round logo-icon">restaurant_menu</span> <span class="material-icons-round logo-icon">restaurant_menu</span>
<div class="brand-text"> <div class="header-left">
<h1>Kantinen Übersicht</h1> <h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">{{VERSION}}</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div> <div id="last-updated-subtitle" class="subtitle"></div>
</div> </div>
</div> </div>
@@ -78,6 +78,9 @@
<button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren" title="Menüdaten neu laden"> <button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren" title="Menüdaten neu laden">
<span class="material-icons-round">refresh</span> <span class="material-icons-round">refresh</span>
</button> </button>
<button id="btn-highlights" class="icon-btn" aria-label="Persönliche Highlights verwalten" title="Persönliche Highlights verwalten">
<span class="material-icons-round">label</span>
</button>
<div class="nav-group"> <div class="nav-group">
<button id="btn-this-week" class="nav-btn active">Diese Woche</button> <button id="btn-this-week" class="nav-btn active">Diese Woche</button>
<button id="btn-next-week" class="nav-btn">Nächste Woche</button> <button id="btn-next-week" class="nav-btn">Nächste Woche</button>
@@ -144,6 +147,27 @@
</div> </div>
</div> </div>
<div id="highlights-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h2>Meine Highlights</h2>
<button id="btn-highlights-close" class="icon-btn" aria-label="Close">
<span class="material-icons-round">close</span>
</button>
</div>
<div class="modal-body">
<p style="margin-bottom: 1rem; color: var(--text-secondary);">
Markiere Menüs automatisch, wenn sie diese Schlagwörter enthalten.
</p>
<div class="input-group">
<input type="text" id="tag-input" placeholder="z.B. Schnitzel, Vegetarisch...">
<button id="btn-add-tag" class="btn-primary">Hinzufügen</button>
</div>
<div id="tags-list"></div>
</div>
</div>
</div>
<main class="container"> <main class="container">
<div id="last-updated-banner" class="banner hidden"> <div id="last-updated-banner" class="banner hidden">
<span class="material-icons-round">update</span> <span class="material-icons-round">update</span>
@@ -174,6 +198,41 @@
const loginForm = document.getElementById('login-form'); const loginForm = document.getElementById('login-form');
const loginModal = document.getElementById('login-modal'); const loginModal = document.getElementById('login-modal');
// Highlights Modal
const btnHighlights = document.getElementById('btn-highlights');
const highlightsModal = document.getElementById('highlights-modal');
const btnHighlightsClose = document.getElementById('btn-highlights-close');
const btnAddTag = document.getElementById('btn-add-tag');
const tagInput = document.getElementById('tag-input');
btnHighlights.addEventListener('click', () => {
highlightsModal.classList.remove('hidden');
renderTagsList();
tagInput.focus();
});
btnHighlightsClose.addEventListener('click', () => {
highlightsModal.classList.add('hidden');
});
window.addEventListener('click', (e) => {
if (e.target === highlightsModal) highlightsModal.classList.add('hidden');
});
btnAddTag.addEventListener('click', () => {
const tag = tagInput.value;
if (addHighlightTag(tag)) {
tagInput.value = '';
renderTagsList();
}
});
tagInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
btnAddTag.click();
}
});
// Theme // Theme
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -592,18 +651,65 @@
} }
// Refresh menu data to update UI // Refresh menu data to update UI
loadMenuDataFromAPI(); loadMenuDataFromAPI();
break; // One refresh is enough
} }
} }
} catch (err) { } catch (err) {
console.error(`Poll error for ${flagId}:`, err); console.error(`Poll error for ${flagId}:`, err);
// Small delay between checks
await new Promise(r => setTimeout(r, 200));
} }
// Small delay between checks
await new Promise(r => setTimeout(r, 200));
} }
} }
// === Highlight Management ===
let highlightTags = JSON.parse(localStorage.getItem('kantine_highlightTags') || '[]');
function saveHighlightTags() {
localStorage.setItem('kantine_highlightTags', JSON.stringify(highlightTags));
renderVisibleWeeks(); // Refresh UI to apply changes
updateNextWeekBadge();
}
function addHighlightTag(tag) {
tag = tag.trim().toLowerCase();
if (tag && !highlightTags.includes(tag)) {
highlightTags.push(tag);
saveHighlightTags();
return true;
}
return false;
}
function removeHighlightTag(tag) {
highlightTags = highlightTags.filter(t => t !== tag);
saveHighlightTags();
}
function renderTagsList() {
const list = document.getElementById('tags-list');
list.innerHTML = '';
highlightTags.forEach(tag => {
const badge = document.createElement('span');
badge.className = 'tag-badge';
badge.innerHTML = `${tag} <span class="tag-remove" data-tag="${tag}">&times;</span>`;
list.appendChild(badge);
});
// Bind remove events
list.querySelectorAll('.tag-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
removeHighlightTag(e.target.dataset.tag);
renderTagsList();
});
});
}
function checkHighlight(text) {
if (!text) return false;
text = text.toLowerCase();
return highlightTags.some(tag => text.includes(tag));
}
// === Local Menu Cache (localStorage) === // === Local Menu Cache (localStorage) ===
const CACHE_KEY = 'kantine_menuCache'; const CACHE_KEY = 'kantine_menuCache';
const CACHE_TS_KEY = 'kantine_menuCacheTs'; const CACHE_TS_KEY = 'kantine_menuCacheTs';
@@ -928,6 +1034,25 @@
badge.classList.add('badge-blue'); // Default / partial state badge.classList.add('badge-blue'); // Default / partial state
} }
// Advanced Feature: Highlight Count
let highlightCount = 0;
if (nextWeekData && nextWeekData.days) {
nextWeekData.days.forEach(day => {
day.items.forEach(item => {
if (checkHighlight(item.name) || checkHighlight(item.description)) {
highlightCount++;
}
});
});
}
if (highlightCount > 0) {
// Append blue count
badge.innerHTML += `<span class="highlight-count" title="${highlightCount} Highlight Menüs">(${highlightCount})</span>`;
badge.title += `${highlightCount} Highlights gefunden`;
badge.classList.add('has-highlights');
}
} else if (badge) { } else if (badge) {
badge.remove(); badge.remove();
} }
@@ -1093,6 +1218,33 @@
const badgesHtml = menuBadges.map(code => `<span class="menu-code-badge">${code}</span>`).join(''); const badgesHtml = menuBadges.map(code => `<span class="menu-code-badge">${code}</span>`).join('');
// Determine Day Status for Header Color
// Violet: Has Order
// Green: No Order but Orderable
// Red: No Order and Not Orderable (Locked/Sold Out)
let headerClass = '';
const hasAnyOrder = day.items && day.items.some(item => {
const articleId = item.articleId || parseInt(item.id.split('_')[1]);
const key = `${day.date}_${articleId}`;
return orderMap.has(key) && orderMap.get(key).length > 0;
});
const hasOrderable = day.items && day.items.some(item => {
// Use pre-calculated available flag from loadMenuDataFromAPI calculation
return item.available;
});
if (hasAnyOrder) {
headerClass = 'header-violet';
} else if (hasOrderable && !isPastCutoff) {
headerClass = 'header-green';
} else {
// Red if not orderable (or past cutoff)
headerClass = 'header-red';
}
if (headerClass) header.classList.add(headerClass);
header.innerHTML = ` header.innerHTML = `
<div class="day-header-left"> <div class="day-header-left">
<span class="day-name">${translateDay(day.weekday)}</span> <span class="day-name">${translateDay(day.weekday)}</span>
@@ -1245,6 +1397,158 @@
return card; return card;
} }
// === Version Check ===
async function checkForUpdates() {
const CurrentVersion = '{{VERSION}}';
const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';
const InstallerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';
console.log(`[Kantine] Checking for updates... (Current: ${CurrentVersion})`);
try {
const response = await fetch(VersionUrl, { cache: 'no-cache' });
if (!response.ok) return;
const remoteVersion = (await response.text()).trim();
if (remoteVersion && remoteVersion !== CurrentVersion) {
console.log(`[Kantine] New version available: ${remoteVersion}`);
// Fetch Changelog content
let changeSummary = '';
try {
const clResp = await fetch('https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/changelog.md');
if (clResp.ok) {
const clText = await clResp.text();
const match = clText.match(/## (v[^\n]+)\n((?:-[^\n]+\n)+)/);
if (match && match[1].includes(remoteVersion)) {
changeSummary = match[2].replace(/- /g, '• ').trim();
}
}
} catch (e) { console.warn('No changelog', e); }
// Create Banner
const updateBanner = document.createElement('div');
updateBanner.className = 'update-banner';
updateBanner.innerHTML = `
<div class="update-content">
<strong>Update verfügbar: ${remoteVersion}</strong>
${changeSummary ? `<pre class="change-summary">${changeSummary}</pre>` : ''}
<a href="${InstallerUrl}" target="_blank" class="update-link">
<span class="material-icons-round">system_update_alt</span>
Jetzt aktualisieren
</a>
</div>
<button class="icon-btn-small close-update">&times;</button>
`;
document.body.appendChild(updateBanner);
updateBanner.querySelector('.close-update').addEventListener('click', () => updateBanner.remove());
// Highlight Header Icon
const lastUpdatedIcon = document.querySelector('.material-icons-round.logo-icon');
if (lastUpdatedIcon) {
lastUpdatedIcon.style.color = 'var(--accent-color)';
lastUpdatedIcon.parentElement.title = `Update verfügbar: ${remoteVersion}`;
}
}
} catch (error) {
console.warn('[Kantine] Version check failed:', error);
}
}
// === Order Countdown ===
function updateCountdown() {
const now = new Date();
const currentDay = now.getDay();
// Skip weekends (0=Sun, 6=Sat)
if (currentDay === 0 || currentDay === 6) {
removeCountdown();
return;
}
const todayStr = now.toISOString().split('T')[0];
// 1. Check if we already ordered for today
let hasOrder = false;
// Optimization: Check orderMap for today's date
// Keys are "YYYY-MM-DD_ArticleID"
for (const key of orderMap.keys()) {
if (key.startsWith(todayStr)) {
hasOrder = true;
break;
}
}
if (hasOrder) {
removeCountdown();
return;
}
// 2. Calculate time to cutoff (10:00 AM)
const cutoff = new Date();
cutoff.setHours(10, 0, 0, 0);
const diff = cutoff - now;
// If passed cutoff or more than 3 hours away (e.g. 07:00), maybe don't show?
// User req: "heute noch keine bestellung... countdown erscheinen"
// Let's show it if within valid order window (e.g. 00:00 - 10:00)
if (diff <= 0) {
removeCountdown();
return;
}
// 3. Render Countdown
const diffHrs = Math.floor(diff / 3600000);
const diffMins = Math.floor((diff % 3600000) / 60000);
const headerCenter = document.querySelector('.header-center-wrapper');
if (!headerCenter) return;
let countdownEl = document.getElementById('order-countdown');
if (!countdownEl) {
countdownEl = document.createElement('div');
countdownEl.id = 'order-countdown';
// Insert before cost display or append
headerCenter.insertBefore(countdownEl, headerCenter.firstChild);
}
countdownEl.innerHTML = `<span>Bestellschluss:</span> <strong>${diffHrs}h ${diffMins}m</strong>`;
// Red Alert if < 1 hour
if (diff < 3600000) { // 1 hour
countdownEl.classList.add('urgent');
// Notification logic (One time)
const notifiedKey = `kantine_notified_${todayStr}`;
if (!sessionStorage.getItem(notifiedKey)) {
if (Notification.permission === 'granted') {
new Notification('Kantine: Bestellschluss naht!', {
body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!',
icon: '⏳'
});
} else if (Notification.permission === 'default') {
Notification.requestPermission();
}
sessionStorage.setItem(notifiedKey, 'true');
}
} else {
countdownEl.classList.remove('urgent');
}
}
function removeCountdown() {
const el = document.getElementById('order-countdown');
if (el) el.remove();
}
// Update countdown every minute
setInterval(updateCountdown, 60000);
// Also update on load
setTimeout(updateCountdown, 1000);
// === Helpers === // === Helpers ===
function getISOWeek(date) { function getISOWeek(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
@@ -1260,6 +1564,7 @@
return date.getFullYear(); return date.getFullYear();
} }
function translateDay(englishDay) { function translateDay(englishDay) {
const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' }; const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' };
return map[englishDay] || englishDay; return map[englishDay] || englishDay;
@@ -1290,6 +1595,9 @@
startPolling(); startPolling();
} }
// Check for updates
checkForUpdates();
console.log('Kantine Wrapper loaded ✅'); console.log('Kantine Wrapper loaded ✅');
})(); })();

250
style.css
View File

@@ -141,17 +141,6 @@ body {
justify-content: center; justify-content: center;
} }
.weekly-cost {
white-space: nowrap;
font-size: 0.9rem;
font-weight: 600;
color: var(--success-color);
background-color: var(--bg-body);
padding: 0.25rem 0.75rem;
border-radius: 20px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
border: 1px solid var(--border-color);
}
.header-week-title { .header-week-title {
font-size: 1.1rem; font-size: 1.1rem;
@@ -1105,6 +1094,7 @@ body {
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3); box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
} }
} }
/* Day Header Badges */ /* Day Header Badges */
.day-header-left { .day-header-left {
display: flex; display: flex;
@@ -1115,7 +1105,8 @@ body {
.menu-code-badge { .menu-code-badge {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
color: #8b5cf6; /* Violet 500 */ color: #8b5cf6;
/* Violet 500 */
background-color: rgba(139, 92, 246, 0.15); background-color: rgba(139, 92, 246, 0.15);
border: 1px solid rgba(139, 92, 246, 0.3); border: 1px solid rgba(139, 92, 246, 0.3);
padding: 2px 6px; padding: 2px 6px;
@@ -1125,7 +1116,234 @@ body {
} }
/* Detailed Badge Colors */ /* Detailed Badge Colors */
.nav-badge.badge-violet { background-color: #8b5cf6; } .nav-badge.badge-violet {
.nav-badge.badge-green { background-color: var(--success-color); } background-color: #8b5cf6;
.nav-badge.badge-red { background-color: var(--error-color); } }
.nav-badge.badge-blue { background-color: var(--accent-color); }
.nav-badge.badge-green {
background-color: var(--success-color);
}
.nav-badge.badge-red {
background-color: var(--error-color);
}
.nav-badge.badge-blue {
background-color: var(--accent-color);
}
/* Day Header Status Colors (User Request) */
.card-header.header-violet {
background-color: rgba(139, 92, 246, 0.15);
border-bottom: 2px solid #8b5cf6;
}
.card-header.header-green {
background-color: rgba(16, 185, 129, 0.15);
border-bottom: 2px solid var(--success-color);
}
.card-header.header-red {
background-color: rgba(239, 68, 68, 0.15);
border-bottom: 2px solid var(--error-color);
}
.card-header.header-violet .day-name,
.card-header.header-green .day-name,
.card-header.header-red .day-name {
font-weight: 700;
color: var(--text-primary);
/* Ensure text remains standard color */
}
/* Update Icon */
.update-icon {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 8px;
background-color: rgba(16, 185, 129, 0.2);
/* Green tint */
color: var(--success-color);
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
text-decoration: none;
animation: pulse 2s infinite;
}
.update-icon:hover {
background-color: var(--success-color);
color: white;
transform: scale(1.1);
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
}
70% {
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
/* Order Countdown */
#order-countdown {
background: rgba(255, 255, 255, 0.1);
padding: 0.25rem 0.75rem;
border-radius: 99px;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
border: 1px solid var(--border-color);
}
#order-countdown span {
opacity: 0.7;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
#order-countdown.urgent {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.5);
color: #ef4444;
animation: pulse-red 2s infinite;
}
@keyframes pulse-red {
0% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
}
70% {
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
}
}
/* Smart Highlights */
.highlight-glow {
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
/* Blue glow */
border: 1px solid rgba(59, 130, 246, 0.8);
background: rgba(59, 130, 246, 0.05);
position: relative;
z-index: 1;
}
/* Nav Badge with Count */
.nav-badge.has-highlights {
background-color: var(--bg-card);
/* Neutral background */
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 2px 6px;
}
.nav-badge .highlight-count {
color: #3b82f6;
/* Blue 500 */
font-weight: 700;
margin-left: 4px;
}
/* Tag Management Modal */
#tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
min-height: 50px;
}
.tag-badge {
display: inline-flex;
align-items: center;
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
padding: 4px 10px;
border-radius: 99px;
font-size: 0.85rem;
font-weight: 500;
}
.tag-remove {
margin-left: 6px;
cursor: pointer;
opacity: 0.7;
font-size: 1.1em;
line-height: 1;
}
.tag-remove:hover {
opacity: 1;
color: #ef4444;
}
.input-group {
display: flex;
gap: 0.5rem;
}
.input-group input {
flex: 1;
padding: 0.75rem;
background: var(--bg-body);
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 8px;
}
/* Update Banner Enhanced */
.change-summary {
font-size: 0.8rem;
background: rgba(0, 0, 0, 0.1);
padding: 0.5rem;
border-radius: 4px;
margin: 0.5rem 0;
white-space: pre-wrap;
font-family: inherit;
line-height: 1.4;
max-height: 100px;
overflow-y: auto;
}
.update-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
/* Installer Changelog */
.changelog-container ul {
padding-left: 1.5rem;
margin: 0.5rem 0;
}
.changelog-container li {
margin-bottom: 0.4rem;
line-height: 1.5;
}
.changelog-container h3 {
margin-top: 1.5rem;
margin-bottom: 0.5rem;
font-size: 1.1em;
color: var(--accent-color);
}

89
test_build.py Executable file
View File

@@ -0,0 +1,89 @@
import os
import sys
DIST_DIR = os.path.join(os.path.dirname(__file__), 'dist')
INSTALL_HTML = os.path.join(DIST_DIR, 'install.html')
BOOKMARKLET_TXT = os.path.join(DIST_DIR, 'bookmarklet.txt')
STANDALONE_HTML = os.path.join(DIST_DIR, 'kantine-standalone.html')
def check_file_exists(path, description):
if not os.path.exists(path):
print(f"❌ MISSING: {description} ({path})")
return False
# Check if not empty
if os.path.getsize(path) == 0:
print(f"❌ EMPTY: {description} ({path})")
return False
print(f"✅ FOUND: {description}")
return True
def check_content(path, must_contain=[], must_not_contain=[]):
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
success = True
for item in must_contain:
if item not in content:
print(f"❌ MISSING CONTENT: '{item}' in {os.path.basename(path)}")
success = False
for item in must_not_contain:
if item in content:
print(f"❌ FORBIDDEN CONTENT: '{item}' in {os.path.basename(path)}")
success = False
if success:
print(f"✅ CONTENT VERIFIED: {os.path.basename(path)}")
return success
def main():
print("=== Running Build Tests ===")
# 1. Existence Check
if not all([
check_file_exists(INSTALL_HTML, "Installer HTML"),
check_file_exists(BOOKMARKLET_TXT, "Bookmarklet Text"),
check_file_exists(STANDALONE_HTML, "Standalone HTML")
]):
sys.exit(1)
# 2. Bookmarklet Logic Check
# Must have the CSS injection fix from the external AI
# Must have correct versioning
# Must be properly URL encoded (checking for common issues)
# Read bookmarklet code (decoded mostly by being in txt? No, txt is usually the raw URL)
with open(BOOKMARKLET_TXT, 'r') as f:
bm_code = f.read().strip()
if not bm_code.startswith("javascript:"):
print("❌ Bookmarklet does not start with 'javascript:'")
sys.exit(1)
# Check for placeholder leftovers
if not check_content(BOOKMARKLET_TXT,
must_contain=["document.createElement('style')", "M1", "M2"],
must_not_contain=["{{VERSION}}", "{{CSS_ESCAPED}}"]):
sys.exit(1)
# Check for CSS injection specific logic
if "document.head.appendChild(s)" not in bm_code and "appendChild(s)" not in bm_code: # URL encoded might mask this, strictly checking decoded would be better but simple check first
# Actually bm_code is URL encoded. We should decode it to verify logic.
import urllib.parse
decoded = urllib.parse.unquote(bm_code)
if "document.createElement('style')" not in decoded:
print("❌ CSS Injection logic missing in bookmarklet")
sys.exit(1)
print("✅ CSS Injection logic confirmed")
# 3. Installer Check
if not check_content(INSTALL_HTML,
must_contain=["Kantine Wrapper", "So funktioniert's", "changelog-container"],
must_not_contain=["CHANGELOG_HTML_PLACEHOLDER"]): # If we used that
sys.exit(1)
print("🎉 ALL TESTS PASSED")
sys.exit(0)
if __name__ == "__main__":
main()

1
version.txt Executable file
View File

@@ -0,0 +1 @@
v1.2.0