Lade Men\u00fcdaten...
\ndiff --git a/build-bookmarklet.sh b/build-bookmarklet.sh index ae95505..5e0ca9d 100755 --- a/build-bookmarklet.sh +++ b/build-bookmarklet.sh @@ -161,18 +161,38 @@ print(html) ") fi +# Embed the bookmarklet URL inline echo "document.getElementById('bookmarklet-link').href = " >> "$DIST_DIR/install.html" echo "$JS_CONTENT" | python3 -c " -import sys, json -js = sys.stdin.read() -css = open('$CSS_FILE').read().replace('\\n', ' ').replace(' ', ' ') +import sys, json, urllib.parse + +# 1. Read JS and Replace VERSION +js_template = sys.stdin.read() +# We do replacement here in Python to be safe +js = js_template.replace('{{VERSION}}', '$VERSION') + +# 2. Prepare CSS +css = open('$CSS_FILE').read().replace('\n', ' ').replace(' ', ' ') escaped_css = css.replace('\\\\', '\\\\\\\\').replace(\"'\", \"\\\\'\").replace('\"', '\\\\\"') -# Inject Update URL with htmlpreview +# 3. Inject CSS and 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) +js = js.replace('{{CSS_ESCAPED}}', escaped_css) -print(json.dumps('javascript:(function(){' + js.replace('{{CSS_ESCAPED}}', escaped_css) + '})();')) +# 4. Create Bookmarklet Code +# Wrap in IIFE +bookmarklet_code = 'javascript:(function(){' + js + '})();' + +# 5. URL Encode the body (keeping javascript: prefix) +# We accept that simple encoding is better. +# But browsers expect encoded URI for href. +# However, for bookmarklet usage, user drags the link. +# If we encode everything, it's safer. +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) diff --git a/changelog.md b/changelog.md index 5e249a1..6a3bc6e 100755 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,6 @@ +## 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. 🐛 diff --git a/dist/bookmarklet-payload.js b/dist/bookmarklet-payload.js index ab6366b..8efdb9b 100755 --- a/dist/bookmarklet-payload.js +++ b/dist/bookmarklet-payload.js @@ -4,6 +4,6 @@ var s=document.createElement('style'); s.textContent=':root { /* Premium Slate/Gray-Blue Palette - Light Mode */ --bg-body: #f1f5f9; /* Slate 100 */ --bg-card: #ffffff; --text-primary: #334155; /* Slate 700 */ --text-secondary: #64748b; --accent-color: #0f172a; /* Slate 900 (High contrast) */ --border-color: #cbd5e1; /* Slate 300 */ --banner-bg: #e2e8f0; --banner-text: #1e293b; --success-color: #059669; --error-color: #dc2626; --card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05); --header-bg: rgba(255, 255, 255, 0.9); --header-border: 1px solid rgba(203, 213, 225, 0.6); } [data-theme="dark"] { /* Premium Slate/Gray-Blue Palette - Dark Mode */ --bg-body: #1e293b; /* Deep Slate Gray (Requested) */ --bg-card: #334155; /* Slate 700 */ --text-primary: #f8fafc; /* Slate 50 */ --text-secondary: #cbd5e1; /* Slate 300 */ --accent-color: #60a5fa; /* Blue 400 */ --border-color: #475569; /* Slate 600 */ --banner-bg: #475569; --banner-text: #e2e8f0; --header-bg: rgba(30, 41, 59, 0.9); --header-border: 1px solid rgba(71, 85, 105, 0.6); --card-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.4); } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: \'Inter\', system-ui, -apple-system, sans-serif; background-color: var(--bg-body); color: var(--text-primary); transition: background-color 0.3s ease, color 0.3s ease; line-height: 1.5; -webkit-font-smoothing: antialiased; } /* Fix scrolling bug: Reset html/body styles from host page */ html, body { height: auto !important; min-height: 100% !important; overflow-y: auto !important; overflow-x: hidden !important; position: static !important; margin: 0 !important; padding: 0 !important; } /* Header */ .app-header { position: sticky; top: 0; z-index: 100; backdrop-filter: blur(12px); background-color: var(--header-bg); border-bottom: var(--header-border); padding: 1rem 0; } .header-content { width: 100%; /* Full width */ padding: 0 2rem; /* Comfortable padding */ display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; } .brand { display: flex; align-items: center; gap: 0.75rem; } .brand-text { display: flex; flex-direction: column; } .brand h1 { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.025em; margin-bottom: 0; } .subtitle { font-size: 0.85rem; color: var(--text-secondary); font-weight: 400; margin-left: 2px; } .logo-icon { font-size: 1.5rem; color: var(--accent-color); } /* Controls */ .controls { display: flex; align-items: center; gap: 1.5rem; justify-self: end; } /* Header Week Info (centered) */ .header-week-info { text-align: center; line-height: 1.3; } .header-center-wrapper { display: flex; flex-direction: row; align-items: center; gap: 1.5rem; 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 { font-size: 1.1rem; font-weight: 600; color: var(--text-primary); } .header-week-subtitle { font-size: 0.85rem; color: var(--text-secondary); } .nav-group { display: flex; background-color: var(--bg-card); border: 1px solid var(--border-color); padding: 0.25rem; border-radius: 8px; } .nav-btn { background: none; border: none; padding: 0.5rem 1rem; font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); cursor: pointer; border-radius: 6px; transition: all 0.2s; display: flex; align-items: center; gap: 0.5rem; } .nav-btn:hover { color: var(--text-primary); background-color: rgba(100, 116, 139, 0.1); } .nav-btn.active { background-color: var(--accent-color); color: white; } /* Badge for nav buttons (day count indicator) */ .nav-badge { background-color: var(--error-color); color: white; font-size: 0.75rem; font-weight: 600; padding: 0 6px; border-radius: 10px; min-width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; margin-left: 8px; gap: 3px; line-height: 1; } .nav-badge .orderable { color: #fff; font-weight: 800; } .nav-badge .separator { opacity: 0.6; font-weight: 400; } .nav-badge .total { opacity: 0.8; font-weight: 400; } .nav-btn.active .nav-badge { background: rgba(255, 255, 255, 0.3); } /* Primary style for Login Button to match header */ #btn-login-open { background-color: var(--accent-color); color: white; padding: 0.5rem 1.25rem; border-radius: 8px; font-weight: 600; letter-spacing: 0.025em; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } #btn-login-open:hover { background-color: #334155; /* Slightly lighter than slate-900 */ transform: translateY(-1px); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } /* User Badge Button (Login) */ .user-badge-btn { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 20px; font-size: 0.9rem; font-weight: 500; color: var(--text-primary); cursor: pointer; transition: all 0.2s; } .user-badge-btn:hover { background: rgba(100, 116, 139, 0.1); border-color: var(--accent-color); } .user-badge-btn .material-icons-round { font-size: 1.25rem; color: var(--accent-color); } .icon-btn { background: none; border: none; color: var(--text-primary); cursor: pointer; padding: 0.5rem; border-radius: 50%; transition: background-color 0.2s; display: flex; align-items: center; justify-content: center; } .icon-btn:hover { background-color: rgba(100, 116, 139, 0.1); } /* Refresh button animation */ #btn-refresh.refreshing .material-icons-round { animation: rotate 1s linear infinite; } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* Progress Modal */ .progress-container { margin-bottom: 1.5rem; } .progress-bar { width: 100%; height: 8px; background-color: var(--border-color); border-radius: 4px; overflow: hidden; margin-bottom: 0.75rem; } .progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent-color) 0%, #60a5fa 100%); width: 0%; transition: width 0.3s ease; border-radius: 4px; } .progress-percent { text-align: center; font-size: 1.5rem; font-weight: 700; color: var(--text-primary); margin-bottom: 0.5rem; } .progress-message { text-align: center; color: var(--text-secondary); font-size: 0.9rem; font-weight: 500; } .weekly-cost { background-color: rgba(59, 130, 246, 0.1); /* Blue tint */ color: var(--accent-color); padding: 0.4rem 0.8rem; border-radius: 8px; font-weight: 600; font-size: 0.9rem; display: flex; align-items: center; gap: 0.5rem; border: 1px solid rgba(59, 130, 246, 0.2); } .weekly-cost .material-icons-round { font-size: 18px; } /* Container */ .container { width: 100%; /* Full width */ margin: 2rem auto; padding: 0 2rem; min-height: 80vh; } /* Banner */ .banner { background-color: var(--banner-bg); color: var(--banner-text); padding: 0.75rem 1rem; border-radius: 8px; display: flex; align-items: center; gap: 0.5rem; margin-bottom: 2rem; font-size: 0.875rem; font-weight: 500; border: 1px solid var(--border-color); max-width: fit-content; } /* User Badge */ .user-badge { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: var(--bg-card); /* Changed from --surface */ border: 1px solid var(--border-color); /* Changed from --border */ border-radius: 20px; font-size: 0.9rem; font-weight: 500; } .icon-btn-small { background: none; border: none; padding: 4px; cursor: pointer; color: var(--text-secondary); /* Changed from --text-muted */ display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.2s; } .icon-btn-small:hover { color: var(--error-color); /* Changed from --danger */ background: rgba(239, 68, 68, 0.1); } /* Modal */ .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; transition: all 0.3s; } .modal.hidden { opacity: 0; pointer-events: none; } .modal-content { background: var(--bg-card); /* Changed from --surface */ width: 90%; max-width: 400px; border-radius: 16px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); overflow: hidden; animation: modalSlide 0.3s ease-out; } @keyframes modalSlide { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .modal-header { display: flex; align-items: center; justify-content: space-between; padding: 20px; border-bottom: 1px solid var(--border-color); /* Changed from --border */ } .modal-header h2 { margin: 0; font-size: 1.25rem; } #login-form { padding: 20px; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 0.9rem; } .form-group input { width: 100%; padding: 10px 12px; border: 1px solid var(--border-color); /* Changed from --border */ border-radius: 8px; background: var(--bg-body); /* Changed from --bg */ color: var(--text-primary); /* Changed from --text */ font-family: inherit; transition: border-color 0.2s; } .form-group input:focus { outline: none; border-color: var(--accent-color); /* Changed from --primary */ } .help-text { display: block; margin-top: 4px; color: var(--text-secondary); /* Changed from --text-muted */ font-size: 0.75rem; } .error-msg { margin-bottom: 16px; padding: 10px; background: rgba(239, 68, 68, 0.1); color: var(--error-color); /* Changed from --danger */ border-radius: 8px; font-size: 0.85rem; text-align: center; } .modal-actions { margin-top: 24px; } .btn-primary.wide { width: 100%; justify-content: center; } .hidden { display: none !important; } /* Menu Grid */ .menu-grid { display: grid; gap: 2rem; } .week-section { margin-bottom: 3rem; } .week-header { margin-bottom: 1.5rem; border-bottom: 1px solid var(--border-color); padding-bottom: 1rem; text-align: center; } .week-title { font-size: 1.75rem; font-weight: 700; color: var(--text-primary); } .week-range { color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem; } .days-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; } /* Card */ .menu-card { background-color: var(--bg-card); border-radius: 12px; border: 1px solid var(--border-color); box-shadow: var(--card-shadow); overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; display: flex; flex-direction: column; } /* Past Day Styling - Target specific elements so ordered items can remain visible */ .menu-card.past-day .card-header, .menu-card.past-day .menu-item:not(.ordered) { opacity: 0.6; filter: grayscale(0.8); transition: opacity 0.3s, filter 0.3s; } .menu-card.past-day:hover .card-header, .menu-card.past-day:hover .menu-item:not(.ordered) { opacity: 0.8; filter: grayscale(0.4); } /* Enhancements for ordered items */ .menu-card.past-day .menu-item.ordered { /* No opacity/filter here - fully visible */ background: var(--bg-card); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border: 1px solid var(--accent-color); border-radius: 8px; padding: 1rem; margin: 0 -1rem 1.5rem -1rem; position: relative; z-index: 10; } .menu-item.today-ordered { border: 2px solid var(--accent-color); box-shadow: 0 0 20px rgba(96, 165, 250, 0.4); border-radius: 8px; padding: 1rem; margin: 0 -1rem 1.5rem -1rem; background: var(--bg-card); position: relative; z-index: 5; animation: pulse-glow 3s infinite; } @keyframes pulse-glow { 0% { box-shadow: 0 0 15px rgba(96, 165, 250, 0.3); } 50% { box-shadow: 0 0 25px rgba(96, 165, 250, 0.6); } 100% { box-shadow: 0 0 15px rgba(96, 165, 250, 0.3); } } .menu-card:hover { transform: translateY(-2px); box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); } .card-header { padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: baseline; background-color: rgba(100, 116, 139, 0.05); } .day-name { font-size: 1.125rem; font-weight: 600; } .day-date { font-size: 0.875rem; color: var(--text-secondary); } .card-body { padding: 1.25rem; display: grid; grid-template-rows: auto; /* Each menu item gets its own row */ align-content: start; } .empty-state { color: var(--text-secondary); font-style: italic; text-align: center; padding: 1rem; } /* Menu Items */ .menu-item { margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--border-color); } .menu-item:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; } .item-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem; gap: 1rem; } .item-name { font-weight: 600; color: var(--text-primary); font-size: 1rem; } .item-price { font-weight: 700; color: var(--accent-color); white-space: nowrap; } .item-desc { font-size: 0.875rem; color: var(--text-secondary); line-height: 1.6; margin-bottom: 0.75rem; } .badges { display: flex; gap: 0.5rem; margin-left: auto; } .item-status-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; } .badge { display: inline-flex; align-items: center; justify-content: center; height: 24px; font-size: 0.75rem; padding: 0 10px; border-radius: 4px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; line-height: normal; white-space: nowrap; } .badge.available { background-color: rgba(16, 185, 129, 0.1); /* Emerald 500 / 10% */ color: var(--success-color); border: 1px solid rgba(16, 185, 129, 0.2); } .badge.sold-out { background-color: rgba(239, 68, 68, 0.1); /* Red 500 / 10% */ color: var(--error-color); border: 1px solid rgba(239, 68, 68, 0.2); } .badge.ordered { background-color: rgba(139, 92, 246, 0.1); /* Violet 500 / 10% */ color: #8b5cf6; border: 1px solid rgba(139, 92, 246, 0.2); gap: 4px; } .badge.ordered .material-icons-round { font-size: 1rem; } /* Loading */ .loading-state { text-align: center; padding: 4rem; color: var(--text-secondary); } .spinner { width: 40px; height: 40px; border: 3px solid var(--border-color); border-top-color: var(--accent-color); border-radius: 50%; margin: 0 auto 1rem; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* Footer */ .app-footer { text-align: center; padding: 2rem; color: var(--text-secondary); font-size: 0.875rem; border-top: 1px solid var(--border-color); margin-top: auto; } /* === Order / Cancel Buttons (inline in status row) === */ .btn-order { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border: none; border-radius: 6px; background: var(--success-color); color: white; font-size: 0.75rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; font-family: inherit; } .btn-order .material-icons-round { font-size: 16px; } .btn-order:hover:not(:disabled) { filter: brightness(1.15); transform: translateY(-1px); } .btn-order:disabled { opacity: 0.5; cursor: not-allowed; } .btn-order.loading { pointer-events: none; opacity: 0.6; } .btn-order-compact { padding: 2px 4px; gap: 0; } .btn-order-compact .material-icons-round { font-size: 16px; } .btn-cancel { display: inline-flex; align-items: center; justify-content: center; padding: 4px 6px; border: none; border-radius: 6px; background: var(--error-color); color: white; font-size: 0.75rem; cursor: pointer; transition: all 0.2s ease; font-family: inherit; } .btn-cancel .material-icons-round { font-size: 16px; } .btn-cancel:hover:not(:disabled) { filter: brightness(1.15); transform: translateY(-1px); } .btn-cancel:disabled { opacity: 0.5; cursor: not-allowed; } /* Past days: hide action buttons */ .past-day .item-actions { display: none; } /* Order count badge (for multi-orders) */ .order-count-badge { display: inline-flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.3); color: white; font-size: 0.65rem; font-weight: 700; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; margin-left: 4px; line-height: 1; } /* === Toast Notifications === */ #toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 10000; display: flex; flex-direction: column; gap: 8px; pointer-events: none; } .toast { display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-radius: 8px; font-size: 0.85rem; font-weight: 500; font-family: \'Inter\', sans-serif; color: white; backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); pointer-events: auto; transform: translateX(120%); opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; } .toast.show { transform: translateX(0); opacity: 1; } .toast .material-icons-round { font-size: 18px; } .toast-success { background: rgba(5, 150, 105, 0.95); } .toast-error { background: rgba(220, 38, 38, 0.95); } .toast-info { background: rgba(59, 130, 246, 0.95); } /* === Mobile Responsiveness === */ @media (max-width: 600px) { .header-content { flex-direction: column; gap: 1rem; padding: 0.75rem; } .week-nav { width: 100%; justify-content: center; } .nav-pills { width: 100%; justify-content: space-between; } .nav-btn { flex: 1; justify-content: center; padding: 0.5rem; font-size: 0.85rem; } .days-grid { grid-template-columns: 1fr; /* Force single column */ } .main-content { padding: 1rem; } .week-title { font-size: 1.5rem; } /* Adjust toast position for mobile */ .toast-container { bottom: 1rem; right: 1rem; left: 1rem; /* Center on mobile */ width: auto; } .menu-card { margin-bottom: 1rem; } } /* === Flagging & Notification Styles === */ .btn-flag { display: inline-flex; align-items: center; justify-content: center; background: transparent; border: 1px solid var(--text-secondary); color: var(--text-secondary); border-radius: 6px; padding: 4px; cursor: pointer; transition: all 0.2s; margin-right: 0.5rem; width: 28px; height: 28px; } .btn-flag:hover { background: rgba(234, 179, 8, 0.1); /* Yellow-500 / 10% */ color: #eab308; border-color: #eab308; } .btn-flag.active { background: rgba(234, 179, 8, 0.1); color: #eab308; border-color: #eab308; } .btn-flag .material-icons-round { font-size: 1.1rem; } /* Flagged & Sold Out (Yellow Glow) */ .menu-item.flagged-sold-out { border: 1px solid #eab308; box-shadow: 0 0 10px rgba(234, 179, 8, 0.2); border-radius: 8px; padding: 1rem; margin: 0 -1rem 1.5rem -1rem; background: var(--bg-card); position: relative; z-index: 5; animation: yellow-pulse 3s infinite; } @keyframes yellow-pulse { 0% { box-shadow: 0 0 8px rgba(234, 179, 8, 0.2); } 50% { box-shadow: 0 0 16px rgba(234, 179, 8, 0.5); } 100% { box-shadow: 0 0 8px rgba(234, 179, 8, 0.2); } } /* Flagged & Available (Green Glow) */ .menu-item.flagged-available { border: 2px solid var(--success-color); box-shadow: 0 0 15px rgba(16, 185, 129, 0.3); border-radius: 8px; padding: 1rem; margin: 0 -1rem 1.5rem -1rem; background: var(--bg-card); position: relative; z-index: 5; animation: green-pulse 3s infinite; } @keyframes green-pulse { 0% { box-shadow: 0 0 10px rgba(16, 185, 129, 0.3); } 50% { box-shadow: 0 0 20px rgba(16, 185, 129, 0.6); } 100% { box-shadow: 0 0 10px rgba(16, 185, 129, 0.3); } } /* Day Header Badges */ .day-header-left { display: flex; align-items: center; gap: 0.75rem; } .menu-code-badge { font-size: 0.75rem; font-weight: 700; color: #8b5cf6; /* Violet 500 */ background-color: rgba(139, 92, 246, 0.15); border: 1px solid rgba(139, 92, 246, 0.3); padding: 2px 6px; border-radius: 6px; line-height: normal; display: inline-block; } /* Detailed Badge Colors */ .nav-badge.badge-violet { background-color: #8b5cf6; } .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(--card-bg); /* 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-color); 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); } '; document.head.appendChild(s); var sc=document.createElement('script'); -sc.textContent="/**\n * Kantine Wrapper \u2013 Client-Only Bookmarklet\n * Replaces Bessa page content with enhanced weekly menu view.\n * All API calls go directly to api.bessa.app (same origin).\n * Data stored in localStorage (flags, theme, auth).\n */\n(function () {\n 'use strict';\n\n // Prevent double injection\n if (window.__KANTINE_LOADED) return;\n window.__KANTINE_LOADED = true;\n\n // === Constants ===\n const API_BASE = 'https://api.bessa.app/v1';\n const GUEST_TOKEN = 'c3418725e95a9f90e3645cbc846b4d67c7c66131';\n const CLIENT_VERSION = '1.7.0_prod/2026-01-26';\n const VENUE_ID = 591;\n const MENU_ID = 7;\n const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\n // === State ===\n let allWeeks = [];\n let currentWeekNumber = getISOWeek(new Date());\n let currentYear = new Date().getFullYear();\n let displayMode = 'this-week';\n let authToken = sessionStorage.getItem('kantine_authToken');\n let currentUser = sessionStorage.getItem('kantine_currentUser');\n let orderMap = new Map();\n let userFlags = new Set(JSON.parse(localStorage.getItem('kantine_flags') || '[]'));\n let pollIntervalId = null;\n\n // === API Helpers ===\n function apiHeaders(token) {\n return {\n 'Authorization': `Token ${token || GUEST_TOKEN}`,\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n 'X-Client-Version': CLIENT_VERSION\n };\n }\n\n // === Inject UI ===\n function injectUI() {\n // Replace entire page content\n document.title = 'Kantine Weekly Menu';\n\n // Inject Google Fonts if not already present\n if (!document.querySelector('link[href*=\"fonts.googleapis.com/css2?family=Inter\"]')) {\n const fontLink = document.createElement('link');\n fontLink.rel = 'stylesheet';\n fontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap';\n document.head.appendChild(fontLink);\n }\n if (!document.querySelector('link[href*=\"Material+Icons+Round\"]')) {\n const iconLink = document.createElement('link');\n iconLink.rel = 'stylesheet';\n iconLink.href = 'https://fonts.googleapis.com/icon?family=Material+Icons+Round';\n document.head.appendChild(iconLink);\n }\n\n document.body.innerHTML = `\n
Lade Men\u00fcdaten...
\nKeine Men\u00fcdaten f\u00fcr KW ${targetWeek} (${targetYear}) verf\u00fcgbar.
\n Versuchen Sie eine andere Woche oder schauen Sie sp\u00e4ter vorbei.\n${escapeHtml(item.description)}
`;\n\n // Event: Order\n const orderBtn = itemEl.querySelector('.btn-order');\n if (orderBtn) {\n orderBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n btn.disabled = true;\n btn.classList.add('loading');\n placeOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, parseFloat(btn.dataset.price), btn.dataset.desc || '')\n .finally(() => { btn.disabled = false; btn.classList.remove('loading'); });\n });\n }\n\n // Event: Cancel\n const cancelBtn = itemEl.querySelector('.btn-cancel');\n if (cancelBtn) {\n cancelBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n btn.disabled = true;\n cancelOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name)\n .finally(() => { btn.disabled = false; });\n });\n }\n\n // Event: Flag\n const flagBtn = itemEl.querySelector('.btn-flag');\n if (flagBtn) {\n flagBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n toggleFlag(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff);\n });\n }\n\n body.appendChild(itemEl);\n });\n\n card.appendChild(body);\n return card;\n}\n\n// === Version Check ===\nasync function checkForUpdates() {\n icon.className = 'update-icon';\n icon.href = url;\n icon.target = '_blank';\n icon.innerHTML = '\ud83c\udd95'; // User requested icon\n icon.title = `Neue Version verf\u00fcgbar (${newVersion}). Klick f\u00fcr download`;\n\n headerTitle.appendChild(icon);\n showToast(`Update verf\u00fcgbar: ${newVersion}`, 'info');\n}\n\n// === Order Countdown ===\nfunction updateCountdown() {\n const now = new Date();\n const currentDay = now.getDay();\n // Skip weekends (0=Sun, 6=Sat)\n if (currentDay === 0 || currentDay === 6) {\n removeCountdown();\n return;\n }\n\n const todayStr = now.toISOString().split('T')[0];\n\n // 1. Check if we already ordered for today\n let hasOrder = false;\n // Optimization: Check orderMap for today's date\n // Keys are \"YYYY-MM-DD_ArticleID\"\n for (const key of orderMap.keys()) {\n if (key.startsWith(todayStr)) {\n hasOrder = true;\n break;\n }\n }\n\n if (hasOrder) {\n removeCountdown();\n return;\n }\n\n // 2. Calculate time to cutoff (10:00 AM)\n const cutoff = new Date();\n cutoff.setHours(10, 0, 0, 0);\n\n const diff = cutoff - now;\n\n // If passed cutoff or more than 3 hours away (e.g. 07:00), maybe don't show?\n // User req: \"heute noch keine bestellung... countdown erscheinen\"\n // Let's show it if within valid order window (e.g. 00:00 - 10:00)\n\n if (diff <= 0) {\n removeCountdown();\n return;\n }\n\n // 3. Render Countdown\n const diffHrs = Math.floor(diff / 3600000);\n const diffMins = Math.floor((diff % 3600000) / 60000);\n\n const headerCenter = document.querySelector('.header-center-wrapper');\n if (!headerCenter) return;\n\n let countdownEl = document.getElementById('order-countdown');\n if (!countdownEl) {\n countdownEl = document.createElement('div');\n countdownEl.id = 'order-countdown';\n // Insert before cost display or append\n headerCenter.insertBefore(countdownEl, headerCenter.firstChild);\n }\n\n countdownEl.innerHTML = `Bestellschluss: ${diffHrs}h ${diffMins}m`;\n\n // Red Alert if < 1 hour\n if (diff < 3600000) { // 1 hour\n countdownEl.classList.add('urgent');\n\n // Notification logic (One time)\n const notifiedKey = `kantine_notified_${todayStr}`;\n if (!sessionStorage.getItem(notifiedKey)) {\n if (Notification.permission === 'granted') {\n new Notification('Kantine: Bestellschluss naht!', {\n body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!',\n icon: '\u23f3'\n });\n } else if (Notification.permission === 'default') {\n Notification.requestPermission();\n }\n sessionStorage.setItem(notifiedKey, 'true');\n }\n } else {\n countdownEl.classList.remove('urgent');\n }\n}\n\nfunction removeCountdown() {\n const el = document.getElementById('order-countdown');\n if (el) el.remove();\n}\n\n// Update countdown every minute\nsetInterval(updateCountdown, 60000);\n// Also update on load\nsetTimeout(updateCountdown, 1000);\n\n// === Helpers === \nfunction getISOWeek(date) {\n const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));\n const dayNum = d.getUTCDay() || 7;\n d.setUTCDate(d.getUTCDate() + 4 - dayNum);\n const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));\n return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);\n}\n\nfunction getWeekYear(d) {\n const date = new Date(d.getTime());\n date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);\n return date.getFullYear();\n}\n\n\nfunction translateDay(englishDay) {\n const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' };\n return map[englishDay] || englishDay;\n}\n\nfunction escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text || '';\n return div.innerHTML;\n}\n\n// === Bootstrap ===\ninjectUI();\nbindEvents();\nupdateAuthUI();\ncleanupExpiredFlags();\n\n// Load cached data first for instant UI, then refresh from API\nconst hadCache = loadMenuCache();\nif (hadCache) {\n // Hide loading spinner since cache is shown\n document.getElementById('loading').classList.add('hidden');\n}\nloadMenuDataFromAPI();\n\n// Auto-start polling if already logged in\nif (authToken) {\n startPolling();\n}\n\n// Check for updates\ncheckForUpdates();\n\nconsole.log('Kantine Wrapper loaded \u2705');\n}) ();\n\n// === Error Modal ===\nfunction showErrorModal(title, htmlContent, btnText, url) {\n const modalId = 'error-modal';\n let modal = document.getElementById(modalId);\n if (modal) modal.remove();\n\n modal = document.createElement('div');\n modal.id = modalId;\n modal.className = 'modal hidden';\n modal.innerHTML = `\n \n `;\n document.body.appendChild(modal);\n\n document.getElementById('btn-error-redirect').addEventListener('click', () => {\n window.location.href = url;\n });\n\n requestAnimationFrame(() => {\n modal.classList.remove('hidden');\n });\n}\n"; +sc.textContent="/**\n * Kantine Wrapper \u2013 Client-Only Bookmarklet\n * Replaces Bessa page content with enhanced weekly menu view.\n * All API calls go directly to api.bessa.app (same origin).\n * Data stored in localStorage (flags, theme, auth).\n */\n(function () {\n 'use strict';\n\n // Prevent double injection\n if (window.__KANTINE_LOADED) return;\n window.__KANTINE_LOADED = true;\n\n // === Constants ===\n const API_BASE = 'https://api.bessa.app/v1';\n const GUEST_TOKEN = 'c3418725e95a9f90e3645cbc846b4d67c7c66131';\n const CLIENT_VERSION = '1.7.0_prod/2026-01-26';\n const VENUE_ID = 591;\n const MENU_ID = 7;\n const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\n // === State ===\n let allWeeks = [];\n let currentWeekNumber = getISOWeek(new Date());\n let currentYear = new Date().getFullYear();\n let displayMode = 'this-week';\n let authToken = sessionStorage.getItem('kantine_authToken');\n let currentUser = sessionStorage.getItem('kantine_currentUser');\n let orderMap = new Map();\n let userFlags = new Set(JSON.parse(localStorage.getItem('kantine_flags') || '[]'));\n let pollIntervalId = null;\n\n // === API Helpers ===\n function apiHeaders(token) {\n return {\n 'Authorization': `Token ${token || GUEST_TOKEN}`,\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n 'X-Client-Version': CLIENT_VERSION\n };\n }\n\n // === Inject UI ===\n function injectUI() {\n // Replace entire page content\n document.title = 'Kantine Weekly Menu';\n\n // Inject Google Fonts if not already present\n if (!document.querySelector('link[href*=\"fonts.googleapis.com/css2?family=Inter\"]')) {\n const fontLink = document.createElement('link');\n fontLink.rel = 'stylesheet';\n fontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap';\n document.head.appendChild(fontLink);\n }\n if (!document.querySelector('link[href*=\"Material+Icons+Round\"]')) {\n const iconLink = document.createElement('link');\n iconLink.rel = 'stylesheet';\n iconLink.href = 'https://fonts.googleapis.com/icon?family=Material+Icons+Round';\n document.head.appendChild(iconLink);\n }\n\n document.body.innerHTML = `\nLade Men\u00fcdaten...
\nKeine Men\u00fcdaten f\u00fcr KW ${targetWeek} (${targetYear}) verf\u00fcgbar.
\n Versuchen Sie eine andere Woche oder schauen Sie sp\u00e4ter vorbei.\n${escapeHtml(item.description)}
`;\n\n // Event: Order\n const orderBtn = itemEl.querySelector('.btn-order');\n if (orderBtn) {\n orderBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n btn.disabled = true;\n btn.classList.add('loading');\n placeOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, parseFloat(btn.dataset.price), btn.dataset.desc || '')\n .finally(() => { btn.disabled = false; btn.classList.remove('loading'); });\n });\n }\n\n // Event: Cancel\n const cancelBtn = itemEl.querySelector('.btn-cancel');\n if (cancelBtn) {\n cancelBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n btn.disabled = true;\n cancelOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name)\n .finally(() => { btn.disabled = false; });\n });\n }\n\n // Event: Flag\n const flagBtn = itemEl.querySelector('.btn-flag');\n if (flagBtn) {\n flagBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n toggleFlag(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff);\n });\n }\n\n body.appendChild(itemEl);\n });\n\n card.appendChild(body);\n return card;\n }\n\n // === Version Check ===\n async function checkForUpdates() {\n const CurrentVersion = 'v1.1.2';\n const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';\n const InstallerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';\n\n console.log(`[Kantine] Checking for updates... (Current: ${CurrentVersion})`);\n\n try {\n const response = await fetch(VersionUrl, { cache: 'no-cache' });\n if (!response.ok) return;\n\n const remoteVersion = (await response.text()).trim();\n\n if (remoteVersion && remoteVersion !== CurrentVersion) {\n console.log(`[Kantine] New version available: ${remoteVersion}`);\n\n // Fetch Changelog content\n let changeSummary = '';\n try {\n const clResp = await fetch('https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/changelog.md');\n if (clResp.ok) {\n const clText = await clResp.text();\n const match = clText.match(/## (v[^\\n]+)\\n((?:-[^\\n]+\\n)+)/);\n if (match && match[1].includes(remoteVersion)) {\n changeSummary = match[2].replace(/- /g, '\u2022 ').trim();\n }\n }\n } catch (e) { console.warn('No changelog', e); }\n\n // Create Banner\n const updateBanner = document.createElement('div');\n updateBanner.className = 'update-banner';\n updateBanner.innerHTML = `\n${changeSummary}` : ''}\n \n \n Jetzt aktualisieren\n \n Lade Men\u00fcdaten...
\nKeine Men\u00fcdaten f\u00fcr KW ${targetWeek} (${targetYear}) verf\u00fcgbar.
\n Versuchen Sie eine andere Woche oder schauen Sie sp\u00e4ter vorbei.\n${escapeHtml(item.description)}
`;\n\n // Event: Order\n const orderBtn = itemEl.querySelector('.btn-order');\n if (orderBtn) {\n orderBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n btn.disabled = true;\n btn.classList.add('loading');\n placeOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, parseFloat(btn.dataset.price), btn.dataset.desc || '')\n .finally(() => { btn.disabled = false; btn.classList.remove('loading'); });\n });\n }\n\n // Event: Cancel\n const cancelBtn = itemEl.querySelector('.btn-cancel');\n if (cancelBtn) {\n cancelBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n btn.disabled = true;\n cancelOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name)\n .finally(() => { btn.disabled = false; });\n });\n }\n\n // Event: Flag\n const flagBtn = itemEl.querySelector('.btn-flag');\n if (flagBtn) {\n flagBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n toggleFlag(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff);\n });\n }\n\n body.appendChild(itemEl);\n });\n\n card.appendChild(body);\n return card;\n}\n\n// === Version Check ===\nasync function checkForUpdates() {\n icon.className = 'update-icon';\n icon.href = url;\n icon.target = '_blank';\n icon.innerHTML = '\ud83c\udd95'; // User requested icon\n icon.title = `Neue Version verf\u00fcgbar (${newVersion}). Klick f\u00fcr download`;\n\n headerTitle.appendChild(icon);\n showToast(`Update verf\u00fcgbar: ${newVersion}`, 'info');\n}\n\n// === Order Countdown ===\nfunction updateCountdown() {\n const now = new Date();\n const currentDay = now.getDay();\n // Skip weekends (0=Sun, 6=Sat)\n if (currentDay === 0 || currentDay === 6) {\n removeCountdown();\n return;\n }\n\n const todayStr = now.toISOString().split('T')[0];\n\n // 1. Check if we already ordered for today\n let hasOrder = false;\n // Optimization: Check orderMap for today's date\n // Keys are \"YYYY-MM-DD_ArticleID\"\n for (const key of orderMap.keys()) {\n if (key.startsWith(todayStr)) {\n hasOrder = true;\n break;\n }\n }\n\n if (hasOrder) {\n removeCountdown();\n return;\n }\n\n // 2. Calculate time to cutoff (10:00 AM)\n const cutoff = new Date();\n cutoff.setHours(10, 0, 0, 0);\n\n const diff = cutoff - now;\n\n // If passed cutoff or more than 3 hours away (e.g. 07:00), maybe don't show?\n // User req: \"heute noch keine bestellung... countdown erscheinen\"\n // Let's show it if within valid order window (e.g. 00:00 - 10:00)\n\n if (diff <= 0) {\n removeCountdown();\n return;\n }\n\n // 3. Render Countdown\n const diffHrs = Math.floor(diff / 3600000);\n const diffMins = Math.floor((diff % 3600000) / 60000);\n\n const headerCenter = document.querySelector('.header-center-wrapper');\n if (!headerCenter) return;\n\n let countdownEl = document.getElementById('order-countdown');\n if (!countdownEl) {\n countdownEl = document.createElement('div');\n countdownEl.id = 'order-countdown';\n // Insert before cost display or append\n headerCenter.insertBefore(countdownEl, headerCenter.firstChild);\n }\n\n countdownEl.innerHTML = `Bestellschluss: ${diffHrs}h ${diffMins}m`;\n\n // Red Alert if < 1 hour\n if (diff < 3600000) { // 1 hour\n countdownEl.classList.add('urgent');\n\n // Notification logic (One time)\n const notifiedKey = `kantine_notified_${todayStr}`;\n if (!sessionStorage.getItem(notifiedKey)) {\n if (Notification.permission === 'granted') {\n new Notification('Kantine: Bestellschluss naht!', {\n body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!',\n icon: '\u23f3'\n });\n } else if (Notification.permission === 'default') {\n Notification.requestPermission();\n }\n sessionStorage.setItem(notifiedKey, 'true');\n }\n } else {\n countdownEl.classList.remove('urgent');\n }\n}\n\nfunction removeCountdown() {\n const el = document.getElementById('order-countdown');\n if (el) el.remove();\n}\n\n// Update countdown every minute\nsetInterval(updateCountdown, 60000);\n// Also update on load\nsetTimeout(updateCountdown, 1000);\n\n// === Helpers === \nfunction getISOWeek(date) {\n const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));\n const dayNum = d.getUTCDay() || 7;\n d.setUTCDate(d.getUTCDate() + 4 - dayNum);\n const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));\n return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);\n}\n\nfunction getWeekYear(d) {\n const date = new Date(d.getTime());\n date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);\n return date.getFullYear();\n}\n\n\nfunction translateDay(englishDay) {\n const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' };\n return map[englishDay] || englishDay;\n}\n\nfunction escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text || '';\n return div.innerHTML;\n}\n\n// === Bootstrap ===\ninjectUI();\nbindEvents();\nupdateAuthUI();\ncleanupExpiredFlags();\n\n// Load cached data first for instant UI, then refresh from API\nconst hadCache = loadMenuCache();\nif (hadCache) {\n // Hide loading spinner since cache is shown\n document.getElementById('loading').classList.add('hidden');\n}\nloadMenuDataFromAPI();\n\n// Auto-start polling if already logged in\nif (authToken) {\n startPolling();\n}\n\n// Check for updates\ncheckForUpdates();\n\nconsole.log('Kantine Wrapper loaded \u2705');\n}) ();\n\n// === Error Modal ===\nfunction showErrorModal(title, htmlContent, btnText, url) {\n const modalId = 'error-modal';\n let modal = document.getElementById(modalId);\n if (modal) modal.remove();\n\n modal = document.createElement('div');\n modal.id = modalId;\n modal.className = 'modal hidden';\n modal.innerHTML = `\n \n `;\n document.body.appendChild(modal);\n\n document.getElementById('btn-error-redirect').addEventListener('click', () => {\n window.location.href = url;\n });\n\n requestAnimationFrame(() => {\n modal.classList.remove('hidden');\n });\n}\n"; document.head.appendChild(sc); })(); +javascript:(function(){ if(window.__KANTINE_LOADED){alert('Kantine Wrapper already loaded!');return;} var s=document.createElement('style'); s.textContent=':root { /* Premium Slate/Gray-Blue Palette - Light Mode */ --bg-body: #f1f5f9; /* Slate 100 */ --bg-card: #ffffff; --text-primary: #334155; /* Slate 700 */ --text-secondary: #64748b; --accent-color: #0f172a; /* Slate 900 (High contrast) */ --border-color: #cbd5e1; /* Slate 300 */ --banner-bg: #e2e8f0; --banner-text: #1e293b; --success-color: #059669; --error-color: #dc2626; --card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05); --header-bg: rgba(255, 255, 255, 0.9); --header-border: 1px solid rgba(203, 213, 225, 0.6); } [data-theme="dark"] { /* Premium Slate/Gray-Blue Palette - Dark Mode */ --bg-body: #1e293b; /* Deep Slate Gray (Requested) */ --bg-card: #334155; /* Slate 700 */ --text-primary: #f8fafc; /* Slate 50 */ --text-secondary: #cbd5e1; /* Slate 300 */ --accent-color: #60a5fa; /* Blue 400 */ --border-color: #475569; /* Slate 600 */ --banner-bg: #475569; --banner-text: #e2e8f0; --header-bg: rgba(30, 41, 59, 0.9); --header-border: 1px solid rgba(71, 85, 105, 0.6); --card-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.4); } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: \'Inter\', system-ui, -apple-system, sans-serif; background-color: var(--bg-body); color: var(--text-primary); transition: background-color 0.3s ease, color 0.3s ease; line-height: 1.5; -webkit-font-smoothing: antialiased; } /* Fix scrolling bug: Reset html/body styles from host page */ html, body { height: auto !important; min-height: 100% !important; overflow-y: auto !important; overflow-x: hidden !important; position: static !important; margin: 0 !important; padding: 0 !important; } /* Header */ .app-header { position: sticky; top: 0; z-index: 100; backdrop-filter: blur(12px); background-color: var(--header-bg); border-bottom: var(--header-border); padding: 1rem 0; } .header-content { width: 100%; /* Full width */ padding: 0 2rem; /* Comfortable padding */ display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; } .brand { display: flex; align-items: center; gap: 0.75rem; } .brand-text { display: flex; flex-direction: column; } .brand h1 { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.025em; margin-bottom: 0; } .subtitle { font-size: 0.85rem; color: var(--text-secondary); font-weight: 400; margin-left: 2px; } .logo-icon { font-size: 1.5rem; color: var(--accent-color); } /* Controls */ .controls { display: flex; align-items: center; gap: 1.5rem; justify-self: end; } /* Header Week Info (centered) */ .header-week-info { text-align: center; line-height: 1.3; } .header-center-wrapper { display: flex; flex-direction: row; align-items: center; gap: 1.5rem; 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 { font-size: 1.1rem; font-weight: 600; color: var(--text-primary); } .header-week-subtitle { font-size: 0.85rem; color: var(--text-secondary); } .nav-group { display: flex; background-color: var(--bg-card); border: 1px solid var(--border-color); padding: 0.25rem; border-radius: 8px; } .nav-btn { background: none; border: none; padding: 0.5rem 1rem; font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); cursor: pointer; border-radius: 6px; transition: all 0.2s; display: flex; align-items: center; gap: 0.5rem; } .nav-btn:hover { color: var(--text-primary); background-color: rgba(100, 116, 139, 0.1); } .nav-btn.active { background-color: var(--accent-color); color: white; } /* Badge for nav buttons (day count indicator) */ .nav-badge { background-color: var(--error-color); color: white; font-size: 0.75rem; font-weight: 600; padding: 0 6px; border-radius: 10px; min-width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; margin-left: 8px; gap: 3px; line-height: 1; } .nav-badge .orderable { color: #fff; font-weight: 800; } .nav-badge .separator { opacity: 0.6; font-weight: 400; } .nav-badge .total { opacity: 0.8; font-weight: 400; } .nav-btn.active .nav-badge { background: rgba(255, 255, 255, 0.3); } /* Primary style for Login Button to match header */ #btn-login-open { background-color: var(--accent-color); color: white; padding: 0.5rem 1.25rem; border-radius: 8px; font-weight: 600; letter-spacing: 0.025em; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } #btn-login-open:hover { background-color: #334155; /* Slightly lighter than slate-900 */ transform: translateY(-1px); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } /* User Badge Button (Login) */ .user-badge-btn { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 20px; font-size: 0.9rem; font-weight: 500; color: var(--text-primary); cursor: pointer; transition: all 0.2s; } .user-badge-btn:hover { background: rgba(100, 116, 139, 0.1); border-color: var(--accent-color); } .user-badge-btn .material-icons-round { font-size: 1.25rem; color: var(--accent-color); } .icon-btn { background: none; border: none; color: var(--text-primary); cursor: pointer; padding: 0.5rem; border-radius: 50%; transition: background-color 0.2s; display: flex; align-items: center; justify-content: center; } .icon-btn:hover { background-color: rgba(100, 116, 139, 0.1); } /* Refresh button animation */ #btn-refresh.refreshing .material-icons-round { animation: rotate 1s linear infinite; } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* Progress Modal */ .progress-container { margin-bottom: 1.5rem; } .progress-bar { width: 100%; height: 8px; background-color: var(--border-color); border-radius: 4px; overflow: hidden; margin-bottom: 0.75rem; } .progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent-color) 0%, #60a5fa 100%); width: 0%; transition: width 0.3s ease; border-radius: 4px; } .progress-percent { text-align: center; font-size: 1.5rem; font-weight: 700; color: var(--text-primary); margin-bottom: 0.5rem; } .progress-message { text-align: center; color: var(--text-secondary); font-size: 0.9rem; font-weight: 500; } .weekly-cost { background-color: rgba(59, 130, 246, 0.1); /* Blue tint */ color: var(--accent-color); padding: 0.4rem 0.8rem; border-radius: 8px; font-weight: 600; font-size: 0.9rem; display: flex; align-items: center; gap: 0.5rem; border: 1px solid rgba(59, 130, 246, 0.2); } .weekly-cost .material-icons-round { font-size: 18px; } /* Container */ .container { width: 100%; /* Full width */ margin: 2rem auto; padding: 0 2rem; min-height: 80vh; } /* Banner */ .banner { background-color: var(--banner-bg); color: var(--banner-text); padding: 0.75rem 1rem; border-radius: 8px; display: flex; align-items: center; gap: 0.5rem; margin-bottom: 2rem; font-size: 0.875rem; font-weight: 500; border: 1px solid var(--border-color); max-width: fit-content; } /* User Badge */ .user-badge { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: var(--bg-card); /* Changed from --surface */ border: 1px solid var(--border-color); /* Changed from --border */ border-radius: 20px; font-size: 0.9rem; font-weight: 500; } .icon-btn-small { background: none; border: none; padding: 4px; cursor: pointer; color: var(--text-secondary); /* Changed from --text-muted */ display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.2s; } .icon-btn-small:hover { color: var(--error-color); /* Changed from --danger */ background: rgba(239, 68, 68, 0.1); } /* Modal */ .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; transition: all 0.3s; } .modal.hidden { opacity: 0; pointer-events: none; } .modal-content { background: var(--bg-card); /* Changed from --surface */ width: 90%; max-width: 400px; border-radius: 16px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); overflow: hidden; animation: modalSlide 0.3s ease-out; } @keyframes modalSlide { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .modal-header { display: flex; align-items: center; justify-content: space-between; padding: 20px; border-bottom: 1px solid var(--border-color); /* Changed from --border */ } .modal-header h2 { margin: 0; font-size: 1.25rem; } #login-form { padding: 20px; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 0.9rem; } .form-group input { width: 100%; padding: 10px 12px; border: 1px solid var(--border-color); /* Changed from --border */ border-radius: 8px; background: var(--bg-body); /* Changed from --bg */ color: var(--text-primary); /* Changed from --text */ font-family: inherit; transition: border-color 0.2s; } .form-group input:focus { outline: none; border-color: var(--accent-color); /* Changed from --primary */ } .help-text { display: block; margin-top: 4px; color: var(--text-secondary); /* Changed from --text-muted */ font-size: 0.75rem; } .error-msg { margin-bottom: 16px; padding: 10px; background: rgba(239, 68, 68, 0.1); color: var(--error-color); /* Changed from --danger */ border-radius: 8px; font-size: 0.85rem; text-align: center; } .modal-actions { margin-top: 24px; } .btn-primary.wide { width: 100%; justify-content: center; } .hidden { display: none !important; } /* Menu Grid */ .menu-grid { display: grid; gap: 2rem; } .week-section { margin-bottom: 3rem; } .week-header { margin-bottom: 1.5rem; border-bottom: 1px solid var(--border-color); padding-bottom: 1rem; text-align: center; } .week-title { font-size: 1.75rem; font-weight: 700; color: var(--text-primary); } .week-range { color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem; } .days-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; } /* Card */ .menu-card { background-color: var(--bg-card); border-radius: 12px; border: 1px solid var(--border-color); box-shadow: var(--card-shadow); overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; display: flex; flex-direction: column; } /* Past Day Styling - Target specific elements so ordered items can remain visible */ .menu-card.past-day .card-header, .menu-card.past-day .menu-item:not(.ordered) { opacity: 0.6; filter: grayscale(0.8); transition: opacity 0.3s, filter 0.3s; } .menu-card.past-day:hover .card-header, .menu-card.past-day:hover .menu-item:not(.ordered) { opacity: 0.8; filter: grayscale(0.4); } /* Enhancements for ordered items */ .menu-card.past-day .menu-item.ordered { /* No opacity/filter here - fully visible */ background: var(--bg-card); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border: 1px solid var(--accent-color); border-radius: 8px; padding: 1rem; margin: 0 -1rem 1.5rem -1rem; position: relative; z-index: 10; } .menu-item.today-ordered { border: 2px solid var(--accent-color); box-shadow: 0 0 20px rgba(96, 165, 250, 0.4); border-radius: 8px; padding: 1rem; margin: 0 -1rem 1.5rem -1rem; background: var(--bg-card); position: relative; z-index: 5; animation: pulse-glow 3s infinite; } @keyframes pulse-glow { 0% { box-shadow: 0 0 15px rgba(96, 165, 250, 0.3); } 50% { box-shadow: 0 0 25px rgba(96, 165, 250, 0.6); } 100% { box-shadow: 0 0 15px rgba(96, 165, 250, 0.3); } } .menu-card:hover { transform: translateY(-2px); box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); } .card-header { padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: baseline; background-color: rgba(100, 116, 139, 0.05); } .day-name { font-size: 1.125rem; font-weight: 600; } .day-date { font-size: 0.875rem; color: var(--text-secondary); } .card-body { padding: 1.25rem; display: grid; grid-template-rows: auto; /* Each menu item gets its own row */ align-content: start; } .empty-state { color: var(--text-secondary); font-style: italic; text-align: center; padding: 1rem; } /* Menu Items */ .menu-item { margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--border-color); } .menu-item:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; } .item-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem; gap: 1rem; } .item-name { font-weight: 600; color: var(--text-primary); font-size: 1rem; } .item-price { font-weight: 700; color: var(--accent-color); white-space: nowrap; } .item-desc { font-size: 0.875rem; color: var(--text-secondary); line-height: 1.6; margin-bottom: 0.75rem; } .badges { display: flex; gap: 0.5rem; margin-left: auto; } .item-status-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; } .badge { display: inline-flex; align-items: center; justify-content: center; height: 24px; font-size: 0.75rem; padding: 0 10px; border-radius: 4px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; line-height: normal; white-space: nowrap; } .badge.available { background-color: rgba(16, 185, 129, 0.1); /* Emerald 500 / 10% */ color: var(--success-color); border: 1px solid rgba(16, 185, 129, 0.2); } .badge.sold-out { background-color: rgba(239, 68, 68, 0.1); /* Red 500 / 10% */ color: var(--error-color); border: 1px solid rgba(239, 68, 68, 0.2); } .badge.ordered { background-color: rgba(139, 92, 246, 0.1); /* Violet 500 / 10% */ color: #8b5cf6; border: 1px solid rgba(139, 92, 246, 0.2); gap: 4px; } .badge.ordered .material-icons-round { font-size: 1rem; } /* Loading */ .loading-state { text-align: center; padding: 4rem; color: var(--text-secondary); } .spinner { width: 40px; height: 40px; border: 3px solid var(--border-color); border-top-color: var(--accent-color); border-radius: 50%; margin: 0 auto 1rem; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* Footer */ .app-footer { text-align: center; padding: 2rem; color: var(--text-secondary); font-size: 0.875rem; border-top: 1px solid var(--border-color); margin-top: auto; } /* === Order / Cancel Buttons (inline in status row) === */ .btn-order { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border: none; border-radius: 6px; background: var(--success-color); color: white; font-size: 0.75rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; font-family: inherit; } .btn-order .material-icons-round { font-size: 16px; } .btn-order:hover:not(:disabled) { filter: brightness(1.15); transform: translateY(-1px); } .btn-order:disabled { opacity: 0.5; cursor: not-allowed; } .btn-order.loading { pointer-events: none; opacity: 0.6; } .btn-order-compact { padding: 2px 4px; gap: 0; } .btn-order-compact .material-icons-round { font-size: 16px; } .btn-cancel { display: inline-flex; align-items: center; justify-content: center; padding: 4px 6px; border: none; border-radius: 6px; background: var(--error-color); color: white; font-size: 0.75rem; cursor: pointer; transition: all 0.2s ease; font-family: inherit; } .btn-cancel .material-icons-round { font-size: 16px; } .btn-cancel:hover:not(:disabled) { filter: brightness(1.15); transform: translateY(-1px); } .btn-cancel:disabled { opacity: 0.5; cursor: not-allowed; } /* Past days: hide action buttons */ .past-day .item-actions { display: none; } /* Order count badge (for multi-orders) */ .order-count-badge { display: inline-flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.3); color: white; font-size: 0.65rem; font-weight: 700; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; margin-left: 4px; line-height: 1; } /* === Toast Notifications === */ #toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 10000; display: flex; flex-direction: column; gap: 8px; pointer-events: none; } .toast { display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-radius: 8px; font-size: 0.85rem; font-weight: 500; font-family: \'Inter\', sans-serif; color: white; backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); pointer-events: auto; transform: translateX(120%); opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; } .toast.show { transform: translateX(0); opacity: 1; } .toast .material-icons-round { font-size: 18px; } .toast-success { background: rgba(5, 150, 105, 0.95); } .toast-error { background: rgba(220, 38, 38, 0.95); } .toast-info { background: rgba(59, 130, 246, 0.95); } /* === Mobile Responsiveness === */ @media (max-width: 600px) { .header-content { flex-direction: column; gap: 1rem; padding: 0.75rem; } .week-nav { width: 100%; justify-content: center; } .nav-pills { width: 100%; justify-content: space-between; } .nav-btn { flex: 1; justify-content: center; padding: 0.5rem; font-size: 0.85rem; } .days-grid { grid-template-columns: 1fr; /* Force single column */ } .main-content { padding: 1rem; } .week-title { font-size: 1.5rem; } /* Adjust toast position for mobile */ .toast-container { bottom: 1rem; right: 1rem; left: 1rem; /* Center on mobile */ width: auto; } .menu-card { margin-bottom: 1rem; } } /* === Flagging & Notification Styles === */ .btn-flag { display: inline-flex; align-items: center; justify-content: center; background: transparent; border: 1px solid var(--text-secondary); color: var(--text-secondary); border-radius: 6px; padding: 4px; cursor: pointer; transition: all 0.2s; margin-right: 0.5rem; width: 28px; height: 28px; } .btn-flag:hover { background: rgba(234, 179, 8, 0.1); /* Yellow-500 / 10% */ color: #eab308; border-color: #eab308; } .btn-flag.active { background: rgba(234, 179, 8, 0.1); color: #eab308; border-color: #eab308; } .btn-flag .material-icons-round { font-size: 1.1rem; } /* Flagged & Sold Out (Yellow Glow) */ .menu-item.flagged-sold-out { border: 1px solid #eab308; box-shadow: 0 0 10px rgba(234, 179, 8, 0.2); border-radius: 8px; padding: 1rem; margin: 0 -1rem 1.5rem -1rem; background: var(--bg-card); position: relative; z-index: 5; animation: yellow-pulse 3s infinite; } @keyframes yellow-pulse { 0% { box-shadow: 0 0 8px rgba(234, 179, 8, 0.2); } 50% { box-shadow: 0 0 16px rgba(234, 179, 8, 0.5); } 100% { box-shadow: 0 0 8px rgba(234, 179, 8, 0.2); } } /* Flagged & Available (Green Glow) */ .menu-item.flagged-available { border: 2px solid var(--success-color); box-shadow: 0 0 15px rgba(16, 185, 129, 0.3); border-radius: 8px; padding: 1rem; margin: 0 -1rem 1.5rem -1rem; background: var(--bg-card); position: relative; z-index: 5; animation: green-pulse 3s infinite; } @keyframes green-pulse { 0% { box-shadow: 0 0 10px rgba(16, 185, 129, 0.3); } 50% { box-shadow: 0 0 20px rgba(16, 185, 129, 0.6); } 100% { box-shadow: 0 0 10px rgba(16, 185, 129, 0.3); } } /* Day Header Badges */ .day-header-left { display: flex; align-items: center; gap: 0.75rem; } .menu-code-badge { font-size: 0.75rem; font-weight: 700; color: #8b5cf6; /* Violet 500 */ background-color: rgba(139, 92, 246, 0.15); border: 1px solid rgba(139, 92, 246, 0.3); padding: 2px 6px; border-radius: 6px; line-height: normal; display: inline-block; } /* Detailed Badge Colors */ .nav-badge.badge-violet { background-color: #8b5cf6; } .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(--card-bg); /* 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-color); 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); } '; document.head.appendChild(s); var sc=document.createElement('script'); sc.textContent="/**\n * Kantine Wrapper \u2013 Client-Only Bookmarklet\n * Replaces Bessa page content with enhanced weekly menu view.\n * All API calls go directly to api.bessa.app (same origin).\n * Data stored in localStorage (flags, theme, auth).\n */\n(function () {\n 'use strict';\n\n // Prevent double injection\n if (window.__KANTINE_LOADED) return;\n window.__KANTINE_LOADED = true;\n\n // === Constants ===\n const API_BASE = 'https://api.bessa.app/v1';\n const GUEST_TOKEN = 'c3418725e95a9f90e3645cbc846b4d67c7c66131';\n const CLIENT_VERSION = '1.7.0_prod/2026-01-26';\n const VENUE_ID = 591;\n const MENU_ID = 7;\n const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\n // === State ===\n let allWeeks = [];\n let currentWeekNumber = getISOWeek(new Date());\n let currentYear = new Date().getFullYear();\n let displayMode = 'this-week';\n let authToken = sessionStorage.getItem('kantine_authToken');\n let currentUser = sessionStorage.getItem('kantine_currentUser');\n let orderMap = new Map();\n let userFlags = new Set(JSON.parse(localStorage.getItem('kantine_flags') || '[]'));\n let pollIntervalId = null;\n\n // === API Helpers ===\n function apiHeaders(token) {\n return {\n 'Authorization': `Token ${token || GUEST_TOKEN}`,\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n 'X-Client-Version': CLIENT_VERSION\n };\n }\n\n // === Inject UI ===\n function injectUI() {\n // Replace entire page content\n document.title = 'Kantine Weekly Menu';\n\n // Inject Google Fonts if not already present\n if (!document.querySelector('link[href*=\"fonts.googleapis.com/css2?family=Inter\"]')) {\n const fontLink = document.createElement('link');\n fontLink.rel = 'stylesheet';\n fontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap';\n document.head.appendChild(fontLink);\n }\n if (!document.querySelector('link[href*=\"Material+Icons+Round\"]')) {\n const iconLink = document.createElement('link');\n iconLink.rel = 'stylesheet';\n iconLink.href = 'https://fonts.googleapis.com/icon?family=Material+Icons+Round';\n document.head.appendChild(iconLink);\n }\n\n document.body.innerHTML = `\nLade Men\u00fcdaten...
\nKeine Men\u00fcdaten f\u00fcr KW ${targetWeek} (${targetYear}) verf\u00fcgbar.
\n Versuchen Sie eine andere Woche oder schauen Sie sp\u00e4ter vorbei.\n${escapeHtml(item.description)}
`;\n\n // Event: Order\n const orderBtn = itemEl.querySelector('.btn-order');\n if (orderBtn) {\n orderBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n btn.disabled = true;\n btn.classList.add('loading');\n placeOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, parseFloat(btn.dataset.price), btn.dataset.desc || '')\n .finally(() => { btn.disabled = false; btn.classList.remove('loading'); });\n });\n }\n\n // Event: Cancel\n const cancelBtn = itemEl.querySelector('.btn-cancel');\n if (cancelBtn) {\n cancelBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n btn.disabled = true;\n cancelOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name)\n .finally(() => { btn.disabled = false; });\n });\n }\n\n // Event: Flag\n const flagBtn = itemEl.querySelector('.btn-flag');\n if (flagBtn) {\n flagBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const btn = e.currentTarget;\n toggleFlag(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff);\n });\n }\n\n body.appendChild(itemEl);\n });\n\n card.appendChild(body);\n return card;\n }\n\n // === Version Check ===\n async function checkForUpdates() {\n const CurrentVersion = 'v1.1.2';\n const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';\n const InstallerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';\n\n console.log(`[Kantine] Checking for updates... (Current: ${CurrentVersion})`);\n\n try {\n const response = await fetch(VersionUrl, { cache: 'no-cache' });\n if (!response.ok) return;\n\n const remoteVersion = (await response.text()).trim();\n\n if (remoteVersion && remoteVersion !== CurrentVersion) {\n console.log(`[Kantine] New version available: ${remoteVersion}`);\n\n // Fetch Changelog content\n let changeSummary = '';\n try {\n const clResp = await fetch('https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/changelog.md');\n if (clResp.ok) {\n const clText = await clResp.text();\n const match = clText.match(/## (v[^\\n]+)\\n((?:-[^\\n]+\\n)+)/);\n if (match && match[1].includes(remoteVersion)) {\n changeSummary = match[2].replace(/- /g, '\u2022 ').trim();\n }\n }\n } catch (e) { console.warn('No changelog', e); }\n\n // Create Banner\n const updateBanner = document.createElement('div');\n updateBanner.className = 'update-banner';\n updateBanner.innerHTML = `\n${changeSummary}` : ''}\n \n \n Jetzt aktualisieren\n \n Kantine v1.1.1Kantine v1.1.2Keine Menüdaten für KW ${targetWeek} (${targetYear}) verfügbar.
Versuchen Sie eine andere Woche oder schauen Sie später vorbei.${escapeHtml(item.description)}
`; - // Event: Order - const orderBtn = itemEl.querySelector('.btn-order'); - if (orderBtn) { - orderBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const btn = e.currentTarget; - btn.disabled = true; - btn.classList.add('loading'); - placeOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, parseFloat(btn.dataset.price), btn.dataset.desc || '') - .finally(() => { btn.disabled = false; btn.classList.remove('loading'); }); - }); - } - - // Event: Cancel - const cancelBtn = itemEl.querySelector('.btn-cancel'); - if (cancelBtn) { - cancelBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const btn = e.currentTarget; - btn.disabled = true; - cancelOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name) - .finally(() => { btn.disabled = false; }); - }); - } - - // Event: Flag - const flagBtn = itemEl.querySelector('.btn-flag'); - if (flagBtn) { - flagBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const btn = e.currentTarget; - toggleFlag(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff); - }); - } - - body.appendChild(itemEl); - }); - - card.appendChild(body); - return card; -} - -// === Version Check === -async function checkForUpdates() { - icon.className = 'update-icon'; - icon.href = url; - icon.target = '_blank'; - icon.innerHTML = '🆕'; // User requested icon - icon.title = `Neue Version verfügbar (${newVersion}). Klick für download`; - - headerTitle.appendChild(icon); - showToast(`Update verfügbar: ${newVersion}`, 'info'); -} - -// === 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 = `Bestellschluss: ${diffHrs}h ${diffMins}m`; - - // 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: '⏳' + // Event: Order + const orderBtn = itemEl.querySelector('.btn-order'); + if (orderBtn) { + orderBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const btn = e.currentTarget; + btn.disabled = true; + btn.classList.add('loading'); + placeOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, parseFloat(btn.dataset.price), btn.dataset.desc || '') + .finally(() => { btn.disabled = false; btn.classList.remove('loading'); }); }); - } else if (Notification.permission === 'default') { - Notification.requestPermission(); } - sessionStorage.setItem(notifiedKey, 'true'); - } - } else { - countdownEl.classList.remove('urgent'); + + // Event: Cancel + const cancelBtn = itemEl.querySelector('.btn-cancel'); + if (cancelBtn) { + cancelBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const btn = e.currentTarget; + btn.disabled = true; + cancelOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name) + .finally(() => { btn.disabled = false; }); + }); + } + + // Event: Flag + const flagBtn = itemEl.querySelector('.btn-flag'); + if (flagBtn) { + flagBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const btn = e.currentTarget; + toggleFlag(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff); + }); + } + + body.appendChild(itemEl); + }); + + card.appendChild(body); + return card; } -} -function removeCountdown() { - const el = document.getElementById('order-countdown'); - if (el) el.remove(); -} + // === Version Check === + async function checkForUpdates() { + const CurrentVersion = 'v1.1.2'; + 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'; -// Update countdown every minute -setInterval(updateCountdown, 60000); -// Also update on load -setTimeout(updateCountdown, 1000); + console.log(`[Kantine] Checking for updates... (Current: ${CurrentVersion})`); -// === Helpers === -function getISOWeek(date) { - const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); - const dayNum = d.getUTCDay() || 7; - d.setUTCDate(d.getUTCDate() + 4 - dayNum); - const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); - return Math.ceil(((d - yearStart) / 86400000 + 1) / 7); -} + try { + const response = await fetch(VersionUrl, { cache: 'no-cache' }); + if (!response.ok) return; -function getWeekYear(d) { - const date = new Date(d.getTime()); - date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); - return date.getFullYear(); -} + 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 = ` +${changeSummary}` : ''}
+
+
+ Jetzt aktualisieren
+
+ Keine Menüdaten für KW ${targetWeek} (${targetYear}) verfügbar.
Versuchen Sie eine andere Woche oder schauen Sie später vorbei.${escapeHtml(item.description)}
`; - // Event: Order - const orderBtn = itemEl.querySelector('.btn-order'); - if (orderBtn) { - orderBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const btn = e.currentTarget; - btn.disabled = true; - btn.classList.add('loading'); - placeOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, parseFloat(btn.dataset.price), btn.dataset.desc || '') - .finally(() => { btn.disabled = false; btn.classList.remove('loading'); }); - }); - } - - // Event: Cancel - const cancelBtn = itemEl.querySelector('.btn-cancel'); - if (cancelBtn) { - cancelBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const btn = e.currentTarget; - btn.disabled = true; - cancelOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name) - .finally(() => { btn.disabled = false; }); - }); - } - - // Event: Flag - const flagBtn = itemEl.querySelector('.btn-flag'); - if (flagBtn) { - flagBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const btn = e.currentTarget; - toggleFlag(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff); - }); - } - - body.appendChild(itemEl); - }); - - card.appendChild(body); - return card; -} - -// === Version Check === -async function checkForUpdates() { - icon.className = 'update-icon'; - icon.href = url; - icon.target = '_blank'; - icon.innerHTML = '🆕'; // User requested icon - icon.title = `Neue Version verfügbar (${newVersion}). Klick für download`; - - headerTitle.appendChild(icon); - showToast(`Update verfügbar: ${newVersion}`, 'info'); -} - -// === 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 = `Bestellschluss: ${diffHrs}h ${diffMins}m`; - - // 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: '⏳' + // Event: Order + const orderBtn = itemEl.querySelector('.btn-order'); + if (orderBtn) { + orderBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const btn = e.currentTarget; + btn.disabled = true; + btn.classList.add('loading'); + placeOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, parseFloat(btn.dataset.price), btn.dataset.desc || '') + .finally(() => { btn.disabled = false; btn.classList.remove('loading'); }); }); - } else if (Notification.permission === 'default') { - Notification.requestPermission(); } - sessionStorage.setItem(notifiedKey, 'true'); - } - } else { - countdownEl.classList.remove('urgent'); + + // Event: Cancel + const cancelBtn = itemEl.querySelector('.btn-cancel'); + if (cancelBtn) { + cancelBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const btn = e.currentTarget; + btn.disabled = true; + cancelOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name) + .finally(() => { btn.disabled = false; }); + }); + } + + // Event: Flag + const flagBtn = itemEl.querySelector('.btn-flag'); + if (flagBtn) { + flagBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const btn = e.currentTarget; + toggleFlag(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff); + }); + } + + body.appendChild(itemEl); + }); + + card.appendChild(body); + return card; } -} -function removeCountdown() { - const el = document.getElementById('order-countdown'); - if (el) el.remove(); -} + // === 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'; -// Update countdown every minute -setInterval(updateCountdown, 60000); -// Also update on load -setTimeout(updateCountdown, 1000); + console.log(`[Kantine] Checking for updates... (Current: ${CurrentVersion})`); -// === Helpers === -function getISOWeek(date) { - const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); - const dayNum = d.getUTCDay() || 7; - d.setUTCDate(d.getUTCDate() + 4 - dayNum); - const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); - return Math.ceil(((d - yearStart) / 86400000 + 1) / 7); -} + try { + const response = await fetch(VersionUrl, { cache: 'no-cache' }); + if (!response.ok) return; -function getWeekYear(d) { - const date = new Date(d.getTime()); - date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); - return date.getFullYear(); -} + 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 = ` +${changeSummary}` : ''}
+
+
+ Jetzt aktualisieren
+
+