Compare commits

14 Commits

Author SHA1 Message Date
dbe22cd175 Update article tile button styles in index.html to improve responsiveness and visual consistency 2026-03-22 19:01:31 +01:00
c94e313672 Refactor price display in index.html to use span elements for better structure and styling 2026-03-22 18:59:04 +01:00
83afc3c2f1 Update version number to 1.1.1 in app.py 2026-03-22 18:57:28 +01:00
c0abf76643 Emojis auf Kassen-Tiles in maximaler Größe
Made-with: Cursor
2026-03-22 18:54:54 +01:00
67489bb46b Add X-Forwarded-Host header to nginx-proxy configuration
- Updated nginx-proxy.example.conf to include proxy_set_header for X-Forwarded-Host, enhancing header forwarding capabilities.
2026-03-22 18:53:23 +01:00
73cb0c7777 ProxyFix und nginx-Beispiel für Reverse-Proxy
- Werkzeug ProxyFix für X-Forwarded-* Header
- nginx-proxy.example.conf mit korrekten proxy_set_header
- README: Hinweise zu 502-Beseitigung hinter nginx

Made-with: Cursor
2026-03-22 18:43:55 +01:00
c9ea1c924a docker-compose.yml hinzugefügt
Made-with: Cursor
2026-03-22 17:08:58 +01:00
c523085405 Update .gitignore to exclude all database files and modify kasse.db 2026-03-22 17:05:41 +01:00
bd4664f23b Preis-Eingabe auf europäisches Komma-Format normalisiert
- Neuer Artikel: Textfeld mit Placeholder 4,50 statt Dezimalpunkt
- Artikel bearbeiten: Preise mit Komma anzeigen und akzeptieren
- Backend: Komma vor float()-Konvertierung in Punkt umwandeln

Made-with: Cursor
2026-03-22 17:04:30 +01:00
5c8c4a947e Artikel-Verwaltung und responsive Tiles
- Admin: Artikel löschen (min. 1 erforderlich)
- Admin: Neue Artikel hinzufügen
- Admin: Update-Logik für variable Artikelanzahl
- Kasse: Dynamische items/Session pro Produkt
- Kasse: CSS Grid Layout - alle Artikel fit auf einen Bildschirm
- Kasse: Leerzustand wenn keine Artikel

Made-with: Cursor
2026-03-22 16:56:34 +01:00
ef36d63aa7 Fix: Kassen ohne Passwort können nun wieder geöffnet werden
- Backend erkennt passwortlose Instanzen (Hash von leerem String)
- Login-Formular: required entfernt, Hinweis für passwortlose Kassen
- Bei leerem Feld und passwortloser Kasse wird Zugang gewährt

Made-with: Cursor
2026-03-22 16:53:14 +01:00
c23f08fdd9 feat: Einbinden des Footers in Landing-, Admin- und Login-Ansicht 2026-02-24 17:08:40 +01:00
9f3951e6ef feat: Plausible Privacy Monitoring in Templates integriert 2026-02-24 17:02:10 +01:00
8d8dd115c5 gitignore: add kasse.db 2026-02-24 17:00:23 +01:00
10 changed files with 322 additions and 124 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.venv/
log/
__pycache__/
*.db

View File

@@ -66,9 +66,19 @@ touch kasse.db
docker run -p 9090:90 -v $(pwd)/kasse.db:/app/kasse.db -t erdbeerhannah:latest
```
### Nginx Reverse-Proxy
Bei 502 Bad Gateway hinter nginx:
1. **Proxy-Header**: Nginx muss `X-Forwarded-For`, `X-Forwarded-Proto`, `Host` setzen (die App nutzt `ProxyFix`).
2. **Upstream-Erreichbarkeit**: `proxy_pass` muss den Container erreichen:
- nginx auf gleichem Host wie Docker: `http://127.0.0.1:9090`
- nginx auf anderem Host: `http://<Docker-Host-IP>:9090`
3. **Beispielkonfiguration**: Siehe `nginx-proxy.example.conf`
## Privatsphäre
Die App *erdbeerhannah* protokolliert keinerlei Daten. Keine IP-Adressen, keine Klicks, keine Benutzer-Eingaben und auch sonst nichts. Es werden keine Cookies genutzt, es findet kein Tracking statt und es gibt keine Werbung.
Die App *erdbeerhannah* protokolliert keine personenbezogenen Daten. Keine IP-Adressen und keine persönlichen Benutzer-Eingaben. Es werden keine Cookies genutzt. Zur reinen Nutzungsstatistik (Seitenaufrufe) verwenden wir das datenschutzfreundliche, Cookie-lose [Plausible Analytics](https://plausible.io/). Es gibt keine Werbung.
## Lizenz

87
app.py
View File

@@ -1,13 +1,16 @@
import math
import os
import time
import logging
import sqlite3
import uuid
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.middleware.proxy_fix import ProxyFix
from flask import Flask, render_template, request, session, send_from_directory, g, redirect, url_for
from flask_bootstrap import Bootstrap
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
app.config['SECRET_KEY'] = 'j69ol5mcHLsEtLg4Y/+myd9wWD4pp56E'
# setup logging
@@ -21,7 +24,7 @@ app.logger.addHandler(handler)
Bootstrap(app)
version = "1.1.0/2026-02-24"
version = "1.1.1/2026-03-22"
def get_db_connection():
conn = sqlite3.connect('kasse.db')
@@ -110,7 +113,7 @@ def index(instance_id):
gesamtwert = session.get(f'{session_prefix}gesamtwert', 0.0)
change = session.get(f'{session_prefix}change', "0.00")
givenfloat = session.get(f'{session_prefix}given', 0.0)
items = {i: session.get(f'{session_prefix}item{i}', 0) for i in range(1, 7)}
items = {p: session.get(f'{session_prefix}item{p}', 0) for p in products}
background = "bg-white"
@@ -122,7 +125,7 @@ def index(instance_id):
gesamtwert = 0.0
change = "0.00"
givenfloat = 0.0
items = {i: 0 for i in range(1, 7)}
items = {p: 0 for p in products}
background = "bg-white"
elif action == "calculate_change":
given = request.form.get('given', "0", type=float)
@@ -138,15 +141,21 @@ def index(instance_id):
session[f'{session_prefix}gesamtwert'] = round(gesamtwert, 2)
session[f'{session_prefix}change'] = change
session[f'{session_prefix}given'] = round(givenfloat, 2)
for i in range(1, 7):
session[f'{session_prefix}item{i}'] = items[i]
for p in products:
session[f'{session_prefix}item{p}'] = items[p]
# Update our local variables after processing so they reflect what is rendered
gesamtwert = round(gesamtwert, 2)
n = len(products)
grid_cols = math.ceil(math.sqrt(n)) if n else 1
grid_rows = math.ceil(n / grid_cols) if n else 1
return render_template("index.html",
instance_id=instance_id,
products=products,
grid_cols=grid_cols,
grid_rows=grid_rows,
gesamtwert=f"{gesamtwert:.2f}",
change=change,
given=f"{givenfloat:.2f}" if givenfloat > 0 else "0",
@@ -165,9 +174,21 @@ def admin(instance_id):
auth_key = f'admin_auth_{instance_id}'
# Check if instance has no password (empty or None)
stored_password = instance['password']
has_no_password = (
stored_password is None or
check_password_hash(stored_password, '')
)
# Handle Login Submission
if request.method == "POST" and 'admin_password' in request.form:
if check_password_hash(instance['password'], request.form['admin_password']):
entered = request.form['admin_password']
if has_no_password and entered == '':
session[auth_key] = True
conn.close()
return redirect(url_for('admin', instance_id=instance_id))
elif not has_no_password and check_password_hash(stored_password, entered):
session[auth_key] = True
conn.close()
return redirect(url_for('admin', instance_id=instance_id))
@@ -191,18 +212,56 @@ def admin(instance_id):
session.pop(auth_key, None)
return redirect(url_for('landing'))
# Additional safety: Ensure that the post request isn't processed if not authenticated
# (Though we checked above, this is for the product update form submission)
for i in range(1, 7):
name = request.form.get(f'name_{i}')
price = request.form.get(f'price_{i}', type=float)
icon = request.form.get(f'icon_{i}')
color = request.form.get(f'color_{i}')
for key in request.form:
if key.startswith('delete_') and request.form[key]:
pos = int(key.split('_')[1])
conn.execute('DELETE FROM products WHERE instance_id = ? AND position = ?',
(instance_id, pos))
conn.commit()
conn.close()
return redirect(url_for('admin', instance_id=instance_id))
if action == "add_product":
name = request.form.get('add_name', '').strip()
price_str = request.form.get('add_price', '').replace(',', '.').strip()
try:
price = float(price_str) if price_str else None
except ValueError:
price = None
icon = request.form.get('add_icon', '🛒').strip() or '🛒'
color = request.form.get('add_color', 'btn-primary')
if name and price is not None:
max_pos = conn.execute(
'SELECT COALESCE(MAX(position), 0) FROM products WHERE instance_id = ?',
(instance_id,)
).fetchone()[0]
conn.execute(
'INSERT INTO products (instance_id, position, name, price, icon, color_class) VALUES (?, ?, ?, ?, ?, ?)',
(instance_id, max_pos + 1, name, price, icon, color)
)
conn.commit()
conn.close()
return redirect(url_for('admin', instance_id=instance_id))
# Update existing products
products_rows = conn.execute(
'SELECT position FROM products WHERE instance_id = ?', (instance_id,)
).fetchall()
for row in products_rows:
pos = row['position']
name = request.form.get(f'name_{pos}')
price_str = request.form.get(f'price_{pos}', '').replace(',', '.').strip()
try:
price = float(price_str) if price_str else None
except ValueError:
price = None
icon = request.form.get(f'icon_{pos}')
color = request.form.get(f'color_{pos}')
if name is not None and price is not None:
conn.execute('''
UPDATE products SET name = ?, price = ?, icon = ?, color_class = ?
WHERE instance_id = ? AND position = ?
''', (name, price, icon, color, instance_id, i))
''', (name, price, icon, color, instance_id, pos))
conn.commit()
conn.close()
return redirect(url_for('index', instance_id=instance_id))

10
docker-compose.yml Normal file
View File

@@ -0,0 +1,10 @@
# Vor dem ersten Start: touch kasse.db (bzw. echo. > kasse.db unter Windows)
# damit Docker keine Verzeichnis-Datei erstellt
services:
erdbeerkasse:
build: .
container_name: erdbeerkasse
ports:
- "9090:90"
volumes:
- ./kasse.db:/app/kasse.db

BIN
kasse.db

Binary file not shown.

29
nginx-proxy.example.conf Normal file
View File

@@ -0,0 +1,29 @@
# Beispiel nginx Reverse-Proxy Konfiguration für erdbeerhannah
# Pfad anpassen, z.B. /etc/nginx/sites-available/erdbeerhannah
#
# Upstream-URL je nach Deployment:
# - nginx auf gleichem Host wie Docker: http://127.0.0.1:9090
# - nginx auf anderem Host: http://10.7.0.5:9090 (Docker-Host-IP)
# - Beide im gleichen docker-compose: http://erdbeerkasse:90
server {
listen 443 ssl http2;
server_name erdbeerhannah.elpatron.me;
# SSL-Zertifikate
ssl_certificate /pfad/zu/cert.pem;
ssl_certificate_key /pfad/zu/key.pem;
location / {
proxy_pass http://127.0.0.1:9090;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 60s;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
}
}

View File

@@ -9,6 +9,7 @@
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#dc3545">
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
<script defer data-domain="erdbeerhannah.elpatron.me" src="https://plausible.elpatron.me/js/script.js"></script>
</head>
<body class="bg-light">
@@ -30,21 +31,26 @@
<th>Preis (€)</th>
<th>Icon/Emoji</th>
<th>Farbe (Bootstrap Klasse)</th>
<th></th>
</tr>
</thead>
<tbody>
{% for i in range(1, 7) %}
{% set prod = products[i] %}
{% if not products %}
<tr>
<td class="align-middle">{{ i }}</td>
<td><input type="text" class="form-control" name="name_{{ i }}" value="{{ prod['name'] }}"
<td colspan="6" class="text-muted text-center py-4">Keine Artikel. Füge unten einen neuen hinzu.</td>
</tr>
{% endif %}
{% for pos, prod in products|dictsort %}
<tr>
<td class="align-middle">{{ pos }}</td>
<td><input type="text" class="form-control" name="name_{{ pos }}" value="{{ prod['name'] }}"
required></td>
<td><input type="number" step="0.01" class="form-control" name="price_{{ i }}"
value="{{ prod['price'] }}" required></td>
<td><input type="text" class="form-control" name="icon_{{ i }}" value="{{ prod['icon'] }}"
<td><input type="text" inputmode="decimal" class="form-control" name="price_{{ pos }}"
value="{{ '{:.2f}'.format(prod['price']).replace('.', ',') }}" required></td>
<td><input type="text" class="form-control" name="icon_{{ pos }}" value="{{ prod['icon'] }}"
required></td>
<td>
<select class="form-control" name="color_{{ i }}">
<select class="form-control" name="color_{{ pos }}">
<option value="btn-primary" {% if prod['color_class']=='btn-primary' %}selected{% endif
%}>Blau (Primary)</option>
<option value="btn-secondary" {% if prod['color_class']=='btn-secondary' %}selected{%
@@ -61,6 +67,12 @@
Schwarz (Dark)</option>
</select>
</td>
<td class="align-middle">
{% if products|length > 1 %}
<button type="submit" name="delete_{{ pos }}" value="1" class="btn btn-outline-danger btn-sm"
onclick="return confirm('Artikel wirklich löschen?');">Löschen</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
@@ -76,6 +88,49 @@
<button type="submit" name="action" value="save" class="btn btn-success">Speichern & Zur Kasse</button>
</div>
</form>
<form method="post" class="bg-white p-4 shadow-sm rounded mt-4">
<input type="hidden" name="action" value="add_product">
<h5 class="mb-3">Neuer Artikel hinzufügen</h5>
<div class="form-row align-items-end">
<div class="form-group col-md-3">
<label for="add_name">Name</label>
<input type="text" class="form-control" name="add_name" id="add_name" required
placeholder="z.B. 500g Brombeeren">
</div>
<div class="form-group col-md-2">
<label for="add_price">Preis (€)</label>
<input type="text" inputmode="decimal" class="form-control" name="add_price" id="add_price" required
placeholder="4,50">
</div>
<div class="form-group col-md-1">
<label for="add_icon">Icon</label>
<input type="text" class="form-control" name="add_icon" id="add_icon" placeholder="🫐"
maxlength="4">
</div>
<div class="form-group col-md-2">
<label for="add_color">Farbe</label>
<select class="form-control" name="add_color" id="add_color">
<option value="btn-primary">Blau</option>
<option value="btn-secondary">Grau</option>
<option value="btn-success">Grün</option>
<option value="btn-danger">Rot</option>
<option value="btn-warning">Gelb</option>
<option value="btn-info">Hellblau</option>
<option value="btn-dark">Schwarz</option>
</select>
</div>
<div class="form-group col-md-2">
<button type="submit" class="btn btn-primary">Artikel hinzufügen</button>
</div>
</div>
</form>
<div class="mt-4 text-center text-muted pb-4">
<small>Made with ♥️, marmalade and zero knowledge in <a href="https://kiel-sailing-city.de/"
target="_blank">Kiel Strawberry City.</a><br>
Version: {{ version }}, Instanz: {{ instance_id[:8] }}...</small>
</div>
</div>
<script>
if ('serviceWorker' in navigator) {

View File

@@ -12,137 +12,162 @@
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#dc3545">
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
<script defer data-domain="erdbeerhannah.elpatron.me" src="https://plausible.elpatron.me/js/script.js"></script>
<style>
body,
html {
height: 100%;
margin: 0;
}
.table-container {
height: 100%;
.kasse-container {
height: 100vh;
display: flex;
flex-direction: column;
min-height: 0;
}
.kasse-header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
font-weight: bold;
font-size: 1.5rem;
}
.article-grid {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: repeat({{ grid_cols }}, 1fr);
grid-template-rows: repeat({{ grid_rows }}, 1fr);
gap: 4px;
padding: 4px;
}
.article-tile {
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
}
.article-tile .btn {
width: 100%;
height: 100%;
white-space: normal;
font-size: clamp(0.9rem, 4vw, 1.5rem);
padding: 0.5rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.table {
width: 100%;
height: 100%;
table-layout: fixed;
.article-tile .btn .article-icon {
font-size: min(12vw, 11vh);
line-height: 1.1;
margin-bottom: 0.25rem;
}
.table td {
height: calc(100vh / 6);
vertical-align: middle;
text-align: center;
}
.btn {
width: 100%;
height: 100%;
white-space: normal;
}
.bold-row {
.kasse-sum-row {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-around;
padding: 0.5rem;
font-weight: bold;
font-size: 250%;
font-size: 1.5rem;
}
.custom-btn-size {
font-size: 180%;
.kasse-reset {
flex-shrink: 0;
padding: 0.5rem;
}
.custom-btn-size-med {
font-size: 150%;
.kasse-footer {
flex-shrink: 0;
padding: 0.5rem;
text-align: center;
font-size: 0.9rem;
}
.input-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 0.25rem;
}
.input-container input {
margin-bottom: 10px;
width: 80%;
text-align: center;
font-size: 1.5rem;
font-size: 1.2rem;
width: 120px;
}
.input-container button {
width: 80%;
.empty-state {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
color: #6c757d;
}
</style>
</head>
<body>
<div class="container-fluid table-container">
<table class="table table-bordered">
<form method="post">
<tbody>
<tr class="bold-row">
<td colspan="2">erdbeerrechner 🍓💶</td>
<td><a href="{{ url_for('admin', instance_id=instance_id) }}"
class="btn btn-outline-secondary custom-btn-size-med">⚙️ Setup</a></td>
</tr>
<tr>
{% for i in range(1, 4) %}
{% set prod = products[i] %}
<td>
<button type="submit" name="position" value="{{ i }}" title="{{ prod['name'] }}"
class="btn btn-xl {{ prod['color_class'] }} custom-btn-size">
{{ prod['icon'] }} <br> {{ '{:,.2f}'.format(prod['price']).replace('.', ',') }}€ <br>
({{ items[i] }})
<form method="post" class="kasse-container container-fluid">
<div class="kasse-header">
<span>erdbeerrechner 🍓💶</span>
<a href="{{ url_for('admin', instance_id=instance_id) }}"
class="btn btn-outline-secondary">⚙️ Setup</a>
</div>
<div class="article-grid">
{% if not products %}
<div class="empty-state">
Keine Artikel. Bitte im <a href="{{ url_for('admin', instance_id=instance_id) }}">Admin</a> hinzufügen.
</div>
{% else %}
{% for pos, prod in products|dictsort %}
<div class="article-tile">
<button type="submit" name="position" value="{{ pos }}" title="{{ prod['name'] }}"
class="btn {{ prod['color_class'] }}">
<span class="article-icon">{{ prod['icon'] }}</span>
<span>{{ '{:,.2f}'.format(prod['price']).replace('.', ',') }}€</span>
<span>({{ items.get(pos, 0) }})</span>
</button>
</td>
</div>
{% endfor %}
</tr>
<tr>
{% for i in range(4, 7) %}
{% set prod = products[i] %}
<td>
<button type="submit" name="position" value="{{ i }}" title="{{ prod['name'] }}"
class="btn btn-xl {{ prod['color_class'] }} custom-btn-size">
{{ prod['icon'] }} <br> {{ '{:,.2f}'.format(prod['price']).replace('.', ',') }}€ <br>
({{ items[i] }})
</button>
</td>
{% endfor %}
</tr>
<tr>
<td title="Summe" class="bold-row">🫰 {{ gesamtwert.replace('.', ',') }}€</td>
<td>
{% endif %}
</div>
<div class="kasse-sum-row">
<span title="Summe">🫰 {{ gesamtwert.replace('.', ',') }}€</span>
<div class="input-container">
<input type="number" step="0.01" class="form-control" name="given"
placeholder="{{ given }}" value="{% if given != '0' %}{{ given }}{% endif %}">
<button type="submit" name="action" value="calculate_change"
title="Wechselgeld berechnen" class="btn btn-xl btn-primary custom-btn-size-med">🧾
Berechnen</button>
title="Wechselgeld berechnen" class="btn btn-primary btn-sm">🧾 Berechnen</button>
</div>
</td>
<td title="Wechselgeld" class="bold-row {{ background }}">🪙 {{ change.replace('.', ',') }}€
</td>
</tr>
<tr>
<td colspan="3">
<button type="submit" name="action" value="reset" id="reset"
class="btn btn-xl btn-dark custom-btn-size">Reset 🦭</button>
</td>
</tr>
<tr>
<td colspan="3">Made with ♥️, marmalade and zero knowledge in <a
href="https://kiel-sailing-city.de/" target="_blank">Kiel Strawberry City.</a><br>
<span title="Wechselgeld" class="{{ background }}">🪙 {{ change.replace('.', ',') }}€</span>
</div>
<div class="kasse-reset">
<button type="submit" name="action" value="reset" class="btn btn-dark btn-block">Reset 🦭</button>
</div>
<div class="kasse-footer">
Made with ♥️, marmalade and zero knowledge in <a href="https://kiel-sailing-city.de/"
target="_blank">Kiel Strawberry City.</a><br>
Version: {{ version }}, Instanz: {{ instance_id[:8] }}...
<button type="button" onclick="shareInstance()"
class="btn btn-sm btn-outline-primary ml-2">📤 URL Teilen</button>
</td>
</tr>
</tbody>
</form>
</table>
<button type="button" onclick="shareInstance()" class="btn btn-sm btn-outline-primary ml-2">📤 URL Teilen</button>
</div>
</form>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {

View File

@@ -25,6 +25,7 @@
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#dc3545">
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
<script defer data-domain="erdbeerhannah.elpatron.me" src="https://plausible.elpatron.me/js/script.js"></script>
</head>
<body>
@@ -45,8 +46,10 @@
<ul class="list-group shadow-sm" id="saved-instances-list">
</ul>
</div>
<div class="mt-5 text-muted">
<small>Version: {{ version }}</small>
<div class="mt-5 text-muted pb-4">
<small>Made with ♥️, marmalade and zero knowledge in <a href="https://kiel-sailing-city.de/"
target="_blank">Kiel Strawberry City.</a><br>
Version: {{ version }}</small>
</div>
</div>
<script>

View File

@@ -9,6 +9,7 @@
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#dc3545">
<link rel="apple-touch-icon" href="/static/icon-192x192.png">
<script defer data-domain="erdbeerhannah.elpatron.me" src="https://plausible.elpatron.me/js/script.js"></script>
</head>
<body class="bg-light">
@@ -21,14 +22,19 @@
<form method="post" class="bg-white p-4 shadow-sm rounded">
<div class="form-group">
<label for="admin_password">Passworteingabe erforderlich</label>
<input type="password" class="form-control" name="admin_password" id="admin_password" required
autofocus>
<label for="admin_password">Passwort (bei passwortloser Kasse leer lassen)</label>
<input type="password" class="form-control" name="admin_password" id="admin_password"
autofocus placeholder="Leer lassen wenn keine Kasse mit Passwort">
</div>
<button type="submit" class="btn btn-primary w-100">Einloggen</button>
<a href="{{ url_for('index', instance_id=instance_id) }}" class="btn btn-secondary w-100 mt-2">Zurück zur
Kasse</a>
</form>
<div class="mt-4 text-center text-muted pb-4">
<small>Made with ♥️, marmalade and zero knowledge in <a href="https://kiel-sailing-city.de/"
target="_blank">Kiel Strawberry City.</a><br>
Version: {{ version }}, Instanz: {{ instance_id[:8] }}...</small>
</div>
</div>
<script>
if ('serviceWorker' in navigator) {