31 Commits

Author SHA1 Message Date
53d5309d65 Add sitemap.xml and robots.txt for SEO optimization 2025-08-04 11:29:06 +02:00
a131fc8077 Vorlesen-Buttons kontrastreicher gestaltet: schwarze Symbole auf hellem Hintergrund 2025-08-03 14:07:50 +02:00
deec62fec0 Update Screenshot 2025-08-03 13:54:19 +02:00
9e5906943d Screenshot zur Demo verlinkt 2025-08-03 13:40:52 +02:00
cabe628875 Bump version to 1.4.15 2025-08-03 13:32:43 +02:00
35ecba348b Fix stats route UnboundLocalError and bump version to 1.4.14 2025-08-03 12:59:41 +02:00
31b1c12dcb Code cleanup and dependency updates
- Remove unused imports (abort, g, ngettext) from app.py
- Remove unused variables (werktage, datumsrechnung, werktagsrechnung, wochen_monate)
- Update Flask from 3.0.0 to 3.1.1
- Update requests from 2.31.0 to 2.32.4
- Update pytest from 7.4.3 to 8.4.1
- Update numpy from 1.26.4 to 2.3.2 (safe migration based on NumPy 2.0 guide)
- Add pytest to requirements.txt (was missing)
2025-08-03 12:56:09 +02:00
95ed606796 Update README (manually) 2025-08-03 12:39:26 +02:00
52eac7530a Fix calculator button font (manually) 2025-08-03 12:28:46 +02:00
2f6138b1d6 Bump version to 1.4.13 2025-08-03 12:06:24 +02:00
f5a39e80b4 Taschenrechner-Features hinzugefügt und README aktualisiert 2025-08-03 12:05:02 +02:00
f9f73e24c9 Fix Back-Forward-Cache Lighthouse error by adding proper Cache-Control headers 2025-08-03 10:18:00 +02:00
d697928241 Bump version to 1.4.10 2025-08-03 10:04:09 +02:00
f998f7fff8 Fix JSON syntax error in swagger.json - remove invalid .v{ prefix 2025-08-03 10:01:37 +02:00
1a5aa003a2 v1.4.9: Verbesserte Sprachausgabe mit englischer Unterstützung und Service Worker Fix 2025-08-02 19:54:52 +02:00
eecc2b8b73 Sprachausgabe/englisch gefixt 2025-08-02 19:52:31 +02:00
0b13a408cd Update meta keywords 2025-08-02 19:08:48 +02:00
c4a65bba48 Version 2025-08-02 18:39:30 +02:00
e4b37d9261 Fehler in der API behoben 2025-08-02 18:37:17 +02:00
45cc02b4b0 Fehler in API behoben 2025-08-02 18:30:16 +02:00
05766d9a97 Version auf 1.4.7 erhöht - Dashboard mit Toggle-Funktionalität 2025-08-02 14:28:22 +02:00
e5fbc14a34 Dashboard erweitert: Toggle zwischen Wochen- und 24-Stunden-Verlauf für alle Charts 2025-08-02 14:25:07 +02:00
9e025bd4c7 Überarbeite Help Modal: Floating Schließen-Button und mehrsprachige Unterstützung
- Schließen-Button ist jetzt 'floating' mit position: fixed
- Button hat Hintergrund, Rahmen und Schatten für bessere Sichtbarkeit
- Alle Texte im Help Modal verwenden jetzt Übersetzungsfunktionen
- Vollständige mehrsprachige Unterstützung (Deutsch/Englisch)
- Bessere mobile Darstellung ohne Überschneidungen
2025-08-02 14:17:46 +02:00
f4ffd14624 Update CLOC 2025-08-02 11:38:28 +02:00
4740288c45 Desktop-Layout: Sprachauswahl und Hilfe-Button etwas nach oben verschoben für bessere Balance 2025-08-02 11:31:30 +02:00
512898b34b Fix mobile layout: Verbesserte Lösung für Sprachauswahl-Überlappung mit mehr Abstand 2025-08-02 11:25:22 +02:00
872d0f9e23 Fix: Button 'Berechnen' wird jetzt korrekt als 'Calculate' in englischer Version übersetzt 2025-08-02 11:18:19 +02:00
28fda213ba Fix mobile layout: Sprachauswahl überlappt nicht mehr mit Überschrift und korrigiere URL-Parameter für Sprachwechsel 2025-08-02 11:12:37 +02:00
bdf4e134e4 Version 2025-08-02 08:50:26 +02:00
601f993ccb Scrollbar-Optimierungen und Cookie-Bereinigung 2025-08-02 08:49:46 +02:00
8fdf764a7b Version 1.4.1: Scrollbar-Optimierungen und Cookie-Bereinigung 2025-08-02 08:41:34 +02:00
15 changed files with 2972 additions and 1663 deletions

View File

@@ -42,7 +42,7 @@ Diese moderne Python-Webanwendung (Flask) ermöglicht verschiedene Datumsberechn
Datumsrechner Live: [https://date.elpatron.me](https://date.elpatron.me)
![App Screenshot](./assets/image-20250725095959116.png)
[![App Screenshot](./assets/image-20250725095959116.png)](https://date.elpatron.me)
**[Lighthouse](https://en.wikipedia.org/wiki/Lighthouse_(software))-Performance-Score:**
@@ -60,6 +60,7 @@ Die Webanwendung erreicht hervorragende Performance-Werte in allen Kategorien (P
- Datum plus/minus X Wochen/Monate
- Kalenderwoche zu Datum
- Start-/Enddatum einer Kalenderwoche eines Jahres
- Integrierter Taschenrechner mit History und Sprachausgabe
- Mehrsprachige Unterstützung (Deutsch/Englisch) mit automatischer Browser-Spracherkennung
- Sprachausgabe für alle Ergebnisse (barrierefrei)
- Statistik-Dashboard mit Passwortschutz unter `/stats`
@@ -69,6 +70,7 @@ Die Webanwendung erreicht hervorragende Performance-Werte in allen Kategorien (P
Die Werktagsberechnung kann optional bundeslandspezifische Feiertage berücksichtigen. Dazu wird die kostenlose API von [feiertage-api.de](https://feiertage-api.de) verwendet.
**Verfügbare Bundesländer:**
- Baden-Württemberg (BW)
- Bayern (BY)
- Berlin (BE)
@@ -93,24 +95,29 @@ Die Feiertage werden automatisch für den gewählten Zeitraum abgerufen und bei
Die Anwendung unterstützt Deutsch und Englisch mit folgenden Features:
### Automatische Spracherkennung:
- *Browser-Sprache*: Automatische Erkennung der Browser-Einstellung
- *URL-Parameter*: Sprachauswahl über `?lang=de` oder `?lang=en`
- *localStorage*: Persistente Sprachauswahl im Browser
- *Fallback*: Deutsch als Standardsprache
### *Datenschutzfreundliche Implementierung:*
- *Keine Cookies*: Sprachauswahl ohne Cookies
- *URL-Parameter*: Transparente Sprachauswahl in der URL
- *localStorage*: Lokale Speicherung im Browser
- *Teilbare URLs*: URLs mit Sprachauswahl können geteilt werden
### *Barrierefreiheit:*
- *Screenreader*: Vollständige Unterstützung
- *Tastatur-Navigation*: Vollständig bedienbar
- *ARIA-Attribute*: Korrekte Beschriftungen
- *Semantische HTML*: Korrekte Struktur
- *Taschenrechner*: Vollständig barrierefrei mit Tastatur-Bedienung und Sprachausgabe
### *Technische Details:*
- *Flask-Babel*: Professionelle i18n-Implementierung
- *Gettext*: Standard für Übersetzungen
- *Responsive Design*: Angepasst für alle Geräte
@@ -200,8 +207,11 @@ docker-compose up --build
## REST API
Alle Datumsfunktionen stehen auch als REST-API zur Verfügung. Die API akzeptiert und liefert JSON.
**Basis-URL:** `http://localhost:5000/api/`
**Swagger Dokumentation:** [https://date.elpatron.me/api-docs](https://date.elpatron.me/api-docs)
**Hinweis:** Die Nutzung der REST API wird im Statistik-Dashboard ausgewertet und als Diagramm angezeigt.
### Endpunkte und Beispiele
@@ -334,6 +344,7 @@ curl -X POST http://localhost:5000/api/plusminus \
```
**Hinweis:**
- `"einheit"`: `"tage"`, `"wochen"` oder `"monate"`
- `"richtung"`: `"add"` (plus) oder `"sub"` (minus)
- `"werktage"`: `true` für Werktage, sonst `false` (nur bei `"tage"` unterstützt)
@@ -395,6 +406,7 @@ Elpatrons Datumsrechner ist als PWA installierbar (z.B. auf Android/iOS-Homescre
- Manifest und Service Worker sind integriert
- App-Icon und Theme-Color für Homescreen
- Installation über Browser-Menü ("Zum Startbildschirm hinzufügen")
- Taschenrechner funktioniert vollständig clientseitig (offline verfügbar)
## Monitoring & Healthcheck
@@ -407,9 +419,7 @@ Die App bietet einen Monitoring-Endpunkt unter `/monitor`, der Statusinformation
Beispiel-Aufruf:
```
GET https://date.elpatron.me/monitor
```
`GET https://date.elpatron.me/monitor`
Antwort:
@@ -448,7 +458,7 @@ Finde mal eine Datumsrechner- Webapp, die nicht völlig Werbe- und Tracking vers
### Vibe Coding
Dieses Projekt wurde zu nahezu 100% mit Unterstützung künsticher Intelligenz (*[Vibe Coding](https://de.wikipedia.org/wiki/Vibe_Coding)*) erstellt. Das Grundgerüst war nach ca. 45 Minuten fertig gestellt, insgesamt hat die Entwicklung des Projekts ca. 4 Stunden Zeit beansprucht.
Dieses Projekt wurde zu nahezu 100% mit Unterstützung künsticher Intelligenz (*[Vibe Coding](https://de.wikipedia.org/wiki/Vibe_Coding)*) erstellt. Das Grundgerüst war nach ca. 45 Minuten fertig gestellt, insgesamt hat die Entwicklung des Projekts ca. 12 Stunden Zeit beansprucht.
### Statistik-Erfassung, Logging
@@ -465,6 +475,7 @@ Es werden keine IP-Adressen oder sonstigen persönlichen Daten gespeichert, ledi
- *Farbkontraste:* Hohe Kontraste für Texte, Buttons und Ergebnisboxen, geprüft nach WCAG-Richtlinien.
- *Status- und Fehlermeldungen:* Ergebnisse und Fehler werden mit `aria-live` für Screenreader zugänglich gemacht.
- *Sprachausgabe:* Alle Ergebnisse können über 🔊-Buttons vorgelesen werden (Web Speech API, deutsche Sprache).
- *Taschenrechner:* Vollständig barrierefrei mit Tastatur-Bedienung, Sprachausgabe und History-Funktion.
- *Mobile Optimierung:* Zusätzliche Meta-Tags für bessere Bedienbarkeit auf mobilen Geräten und Unterstützung von Screenreadern.
- *SEO:* Das Thema Barrierefreiheit ist in den Meta-Tags für Suchmaschinen sichtbar.
@@ -472,22 +483,23 @@ Damit ist die App für Menschen mit unterschiedlichen Einschränkungen (z.B. Seh
### Code Statistik
cloc|github.com/AlDanial/cloc v 2.06 T=0.08 s (269.8 files/s, 57268.4 lines/s)
cloc|github.com/AlDanial/cloc v 2.06 T=0.23 s (109.5 files/s, 30735.0 lines/s)
--- | ---
Language|files|blank|comment|code
:-------|-------:|-------:|-------:|-------:
HTML|8|36|6|1998
Python|2|53|57|614
JavaScript|2|95|87|571
Markdown|2|139|0|360
HTML|8|159|8|2805
Python|2|66|74|739
JavaScript|2|95|88|580
PO File|2|260|266|544
Markdown|3|184|0|498
JSON|3|0|0|243
CSS|1|186|3|188
SVG|2|0|0|14
Dockerfile|1|5|6|8
DOS Batch|1|0|0|1
--------|--------|--------|--------|--------
SUM:|22|514|159|3997
SUM:|25|955|445|5620
## Lizenz
@@ -496,4 +508,4 @@ Dieses Projekt steht unter der [MIT-Lizenz](LICENSE).
---
(c) 2025 [Markus Busche](https://digitalcourage.social/@elpatron)
**Version 1.4.0** - Mehrsprachige Unterstützung hinzugefügt
**Version 1.4.12** - Integrierter Taschenrechner mit History und Sprachausgabe hinzugefügt

142
app.py
View File

@@ -1,5 +1,5 @@
from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify, g
from flask_babel import Babel, gettext, ngettext, get_locale
from flask import Flask, render_template, request, redirect, url_for, session, jsonify, make_response
from flask_babel import Babel, gettext, get_locale
from datetime import datetime, timedelta
import numpy as np
from dateutil.relativedelta import relativedelta
@@ -20,7 +20,23 @@ app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
babel = Babel()
# Version der App
APP_VERSION = "1.4.0"
APP_VERSION = "1.4.15"
def add_cache_headers(response):
"""Fügt Cache-Control-Header hinzu, die den Back-Forward-Cache ermöglichen"""
# Cache-Control für statische Inhalte und API-Endpunkte
if request.path.startswith('/static/') or request.path.startswith('/api/'):
response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=3600'
else:
# Für HTML-Seiten: kurze Cache-Zeit, aber Back-Forward-Cache erlauben
response.headers['Cache-Control'] = 'public, max-age=60, s-maxage=300'
# Wichtig: Keine Vary-Header für User-Agent oder andere dynamische Werte
# Dies verhindert den Back-Forward-Cache
if 'Vary' in response.headers:
del response.headers['Vary']
return response
# HTML-Template wird jetzt aus templates/index.html geladen
@@ -102,7 +118,7 @@ def index():
with open(log_path, 'a', encoding='utf-8') as f:
from datetime import datetime as dt
f.write(f"{dt.now().isoformat()} PAGEVIEW\n")
tage = werktage = wochentag = datumsrechnung = werktagsrechnung = kw_berechnen = kw_datum = wochen_monate = None
tage = wochentag = kw_berechnen = kw_datum = None
feiertage_anzahl = wochenendtage_anzahl = None
active_idx = 0
plusminus_result = None
@@ -229,34 +245,72 @@ def index():
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}"
except Exception:
plusminus_result = gettext('Ungültige Eingabe')
return render_template('index.html', tage=tage, werktage=werktage, wochentag=wochentag, plusminus_result=plusminus_result, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx
response = make_response(render_template('index.html', tage=tage, wochentag=wochentag, plusminus_result=plusminus_result, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx
, feiertage_anzahl=feiertage_anzahl, wochenendtage_anzahl=wochenendtage_anzahl, app_version=APP_VERSION, get_locale=get_locale
)
))
return add_cache_headers(response)
def parse_log_stats(log_path):
pageviews = 0
func_counts = {}
func_counts_hourly = {}
impressions_per_day = {}
impressions_per_hour = {}
api_counts = {}
api_counts_hourly = {}
if os.path.exists(log_path):
with open(log_path, encoding='utf-8') as f:
for line in f:
if 'PAGEVIEW' in line:
pageviews += 1
try:
date = line[:10]
# Parse timestamp (format: YYYY-MM-DDTHH:MM:SS)
timestamp = line[:19] # First 19 chars for YYYY-MM-DDTHH:MM:SS
date = timestamp[:10] # YYYY-MM-DD
hour = timestamp[11:13] # HH
if len(date) == 10 and date[4] == '-' and date[7] == '-':
impressions_per_day[date] = impressions_per_day.get(date, 0) + 1
if len(hour) == 2 and hour.isdigit():
hour_key = f"{date} {hour}:00"
impressions_per_hour[hour_key] = impressions_per_hour.get(hour_key, 0) + 1
except Exception:
pass
elif 'FUNC:' in line:
func = line.split('FUNC:')[1].strip()
func_counts[func] = func_counts.get(func, 0) + 1
# Stündliche Funktionsaufrufe
try:
timestamp = line[:19]
date = timestamp[:10]
hour = timestamp[11:13]
if len(hour) == 2 and hour.isdigit():
hour_key = f"{date} {hour}:00"
if hour_key not in func_counts_hourly:
func_counts_hourly[hour_key] = {}
func_counts_hourly[hour_key][func] = func_counts_hourly[hour_key].get(func, 0) + 1
except Exception:
pass
elif 'FUNC_API:' in line:
api = line.split('FUNC_API:')[1].strip()
api_counts[api] = api_counts.get(api, 0) + 1
return pageviews, func_counts, impressions_per_day, api_counts
# Stündliche API-Aufrufe
try:
timestamp = line[:19]
date = timestamp[:10]
hour = timestamp[11:13]
if len(hour) == 2 and hour.isdigit():
hour_key = f"{date} {hour}:00"
if hour_key not in api_counts_hourly:
api_counts_hourly[hour_key] = {}
api_counts_hourly[hour_key][api] = api_counts_hourly[hour_key].get(api, 0) + 1
except Exception:
pass
return pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly
@app.route('/stats', methods=['GET', 'POST'])
def stats():
@@ -267,11 +321,17 @@ def stats():
session['stats_auth'] = True
return redirect(url_for('stats'))
else:
return render_template('stats_login.html', error='Falsches Passwort!')
return render_template('stats_login.html', error=None)
response = make_response(render_template('stats_login.html', error='Falsches Passwort!'))
return add_cache_headers(response)
else:
response = make_response(render_template('stats_login.html', error=None))
return add_cache_headers(response)
# Wenn authentifiziert, zeige Dashboard
log_path = os.path.join('log', 'pageviews.log')
pageviews, func_counts, impressions_per_day, api_counts = parse_log_stats(log_path)
return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, impressions_per_day=impressions_per_day, api_counts=api_counts)
pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly = parse_log_stats(log_path)
response = make_response(render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, func_counts_hourly=func_counts_hourly, impressions_per_day=impressions_per_day, impressions_per_hour=impressions_per_hour, api_counts=api_counts, api_counts_hourly=api_counts_hourly))
return add_cache_headers(response)
# --- REST API ---
def log_api_usage(api_name):
@@ -305,7 +365,8 @@ def api_tage_werktage():
tage = int(np.busday_count(d1.date(), (d2 + timedelta(days=1)).date(), holidays=holidays))
else:
tage = abs((d2 - d1).days)
return jsonify({'result': tage})
response = jsonify({'result': tage})
return add_cache_headers(response)
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@@ -318,7 +379,8 @@ def api_wochentag():
d = datetime.strptime(datum, '%Y-%m-%d')
wochentage = get_wochentage()
wochentag = wochentage[d.weekday()]
return jsonify({'result': wochentag})
response = jsonify({'result': wochentag})
return add_cache_headers(response)
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@@ -335,7 +397,8 @@ def api_kw_berechnen():
kw_berechnen = f"Week {kw} ({d.year})"
else:
kw_berechnen = f"KW {kw} ({d.year})"
return jsonify({'result': kw_berechnen, 'kw': kw, 'jahr': d.year})
response = jsonify({'result': kw_berechnen, 'kw': kw, 'jahr': d.year})
return add_cache_headers(response)
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@@ -355,7 +418,8 @@ def api_kw_datum():
kw_datum = f"{start.strftime('%m/%d/%Y')} to {end.strftime('%m/%d/%Y')}"
else:
kw_datum = f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}"
return jsonify({'result': kw_datum, 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')})
response = jsonify({'result': kw_datum, 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')})
return add_cache_headers(response)
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@@ -378,34 +442,26 @@ def api_plusminus():
if is_werktage:
result = np.busday_offset(d.date(), anzahl_int, roll='forward')
result_dt = datetime.strptime(str(result), '%Y-%m-%d')
if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} workdays: {result_dt.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Werktage: {result_dt.strftime('%d.%m.%Y')}"
response = jsonify({'result': result_dt.strftime('%Y-%m-%d')})
return add_cache_headers(response)
else:
result = d + timedelta(days=anzahl_int)
if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} days: {result.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Tage: {result.strftime('%d.%m.%Y')}"
response = jsonify({'result': result.strftime('%Y-%m-%d')})
return add_cache_headers(response)
elif einheit == 'wochen':
if is_werktage:
return jsonify({'error': 'Nicht unterstützt: Werktage + Wochen.'}), 400
else:
result = d + timedelta(weeks=anzahl_int)
if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} weeks: {result.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Wochen: {result.strftime('%d.%m.%Y')}"
response = jsonify({'result': result.strftime('%Y-%m-%d')})
return add_cache_headers(response)
elif einheit == 'monate':
if is_werktage:
return jsonify({'error': 'Nicht unterstützt: Werktage + Monate.'}), 400
else:
result = d + relativedelta(months=anzahl_int)
if locale == 'en':
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} months: {result.strftime('%m/%d/%Y')}"
else:
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}"
response = jsonify({'result': result.strftime('%Y-%m-%d')})
return add_cache_headers(response)
else:
return jsonify({'error': 'Ungültige Einheit'}), 400
except Exception as e:
@@ -414,8 +470,14 @@ def api_plusminus():
@app.route('/api/stats', methods=['GET'])
def api_stats():
log_path = os.path.join('log', 'pageviews.log')
pageviews, func_counts, impressions_per_day, api_counts = parse_log_stats(log_path)
return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, impressions_per_day=impressions_per_day, api_counts=api_counts)
pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly = parse_log_stats(log_path)
response = jsonify({
"pageviews": pageviews,
"func_counts": func_counts,
"impressions_per_day": impressions_per_day,
"api_counts": api_counts
})
return add_cache_headers(response)
@app.route('/api/monitor', methods=['GET'])
def api_monitor():
@@ -427,17 +489,25 @@ def api_monitor():
if 'PAGEVIEW' in line:
pageviews += 1
uptime = int(time.time() - app_start_time)
return jsonify({
response = jsonify({
"status": "ok",
"message": "App running",
"time": datetime.now().isoformat(),
"uptime_seconds": uptime,
"pageviews_last_7_days": pageviews
})
return add_cache_headers(response)
@app.route('/api-docs')
def api_docs():
return render_template('swagger.html')
response = make_response(render_template('swagger.html'))
return add_cache_headers(response)
@app.route('/sitemap.xml')
def sitemap():
"""Serviert die Sitemap für Suchmaschinen"""
from flask import send_file
return send_file('sitemap.xml', mimetype='application/xml')
if __name__ == '__main__':

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -1,5 +0,0 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 0 session eyJsYW5ndWFnZSI6ImVuIn0.aIzL2Q.DZtPH-UmM3muNC8RZypEbL29jCg

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
Flask==3.0.0
numpy==1.26.4
Flask==3.1.1
numpy==2.3.2
python-dateutil==2.9.0.post0
requests==2.31.0
requests==2.32.4
Flask-Babel==4.0.0
pytest==8.4.1

View File

@@ -1,2 +1,14 @@
User-agent: *
Allow: /
# Sitemap
Sitemap: https://date.elpatron.me/sitemap.xml
# Disallow private areas
Disallow: /stats
Disallow: /log/
Disallow: /htmlcov/
# Allow API endpoints for documentation
Allow: /api-docs
Allow: /static/swagger.json

83
sitemap.xml Normal file
View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- Hauptseite -->
<url>
<loc>https://date.elpatron.me/</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<!-- API-Dokumentation -->
<url>
<loc>https://date.elpatron.me/api-docs</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<!-- Statische Ressourcen -->
<url>
<loc>https://date.elpatron.me/static/favicon.ico</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>yearly</changefreq>
<priority>0.1</priority>
</url>
<url>
<loc>https://date.elpatron.me/static/favicon.png</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>yearly</changefreq>
<priority>0.1</priority>
</url>
<url>
<loc>https://date.elpatron.me/static/favicon.svg</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>yearly</changefreq>
<priority>0.1</priority>
</url>
<url>
<loc>https://date.elpatron.me/static/logo.svg</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>yearly</changefreq>
<priority>0.1</priority>
</url>
<url>
<loc>https://date.elpatron.me/static/manifest.json</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://date.elpatron.me/static/service-worker.js</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://date.elpatron.me/static/swagger.json</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<!-- Sprachversionen der Hauptseite -->
<url>
<loc>https://date.elpatron.me/?lang=de</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://date.elpatron.me/?lang=en</loc>
<lastmod>2025-08-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
</urlset>

View File

@@ -1,15 +1,25 @@
const CACHE_NAME = 'datumsrechner-cache-v1';
const urlsToCache = [
'/',
'/static/style.css',
'/static/favicon.ico',
'/static/favicon.png',
'/static/favicon.svg',
'/static/logo.svg',
'/static/manifest.json',
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
.then(cache => {
// Füge nur existierende Dateien zum Cache hinzu
return Promise.allSettled(
urlsToCache.map(url =>
cache.add(url).catch(err => {
console.log('Failed to cache:', url, err);
})
)
);
})
);
});
self.addEventListener('fetch', event => {

View File

@@ -1,4 +1,4 @@
.v{
{
"openapi": "3.0.3",
"info": {
"title": "Elpatrons Datumsrechner API",

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,32 @@
.stats-label { color: #64748b; }
.stats-value { font-size: 1.5em; font-weight: bold; }
.chart-container { margin: 2em 0; }
.toggle-container {
display: flex;
justify-content: center;
margin-bottom: 1.5em;
gap: 0.5em;
}
.toggle-btn {
padding: 0.5em 1em;
border: 1px solid #d1d5db;
background: #f9fafb;
color: #6b7280;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.toggle-btn.active {
background: #2563eb;
color: white;
border-color: #2563eb;
}
.toggle-btn:hover {
background: #e5e7eb;
}
.toggle-btn.active:hover {
background: #1d4ed8;
}
</style>
</head>
<body>
@@ -28,6 +54,12 @@
<div class="stats-label">Gesamt-Pageviews (7 Tage):</div>
<div class="stats-value">{{ pageviews }}</div>
</div>
<div class="toggle-container">
<button class="toggle-btn active" data-period="week">Wochenverlauf</button>
<button class="toggle-btn" data-period="day">24-Stunden-Verlauf</button>
</div>
<div class="chart-container">
<canvas id="imprChart" width="400" height="180"></canvas>
</div>
@@ -43,75 +75,196 @@
</div>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
// Impressions pro Tag
// eslint-disable-next-line
const imprData = {{ impressions_per_day|tojson }};
const imprLabels = Object.keys(imprData);
const imprCounts = Object.values(imprData);
new Chart(document.getElementById('imprChart').getContext('2d'), {
type: 'line',
data: {
labels: imprLabels,
datasets: [{
label: 'Impressions/Tag',
data: imprCounts,
borderColor: '#059669',
backgroundColor: 'rgba(5,150,105,0.1)',
tension: 0.2,
fill: true
}]
},
options: {
plugins: { legend: { display: true } },
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } }
}
}
// Daten für verschiedene Zeiträume
const weekData = {{ impressions_per_day|tojson }};
const dayData = {{ impressions_per_hour|tojson }};
const weekFuncData = {{ func_counts|tojson }};
const dayFuncData = {{ func_counts_hourly|tojson }};
const weekApiData = {{ api_counts|tojson }};
const dayApiData = {{ api_counts_hourly|tojson }};
let currentPeriod = 'week';
let currentImprChart = null;
let currentFuncChart = null;
let currentApiChart = null;
// Toggle-Buttons Event Listener
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', function() {
// Aktiven Button aktualisieren
document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
// Zeitraum wechseln
currentPeriod = this.dataset.period;
updateAllCharts();
});
});
// Funktionsaufrufe
// eslint-disable-next-line
const funcCounts = {{ func_counts|tojson }};
const labels = Object.keys(funcCounts);
const data = Object.values(funcCounts);
new Chart(document.getElementById('funcChart').getContext('2d'), {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Funktionsaufrufe',
data: data,
backgroundColor: '#2563eb',
}]
},
options: {
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } }
}
function updateImpressionsChart() {
const ctx = document.getElementById('imprChart').getContext('2d');
// Bestehenden Chart zerstören
if (currentImprChart) {
currentImprChart.destroy();
}
});
// API-Nutzung
// eslint-disable-next-line
const apiCounts = {{ api_counts|tojson }};
if (Object.keys(apiCounts).length > 0 && document.getElementById('apiChart')) {
new Chart(document.getElementById('apiChart').getContext('2d'), {
type: 'bar',
let data, labels, counts;
if (currentPeriod === 'week') {
data = weekData;
labels = Object.keys(data);
counts = Object.values(data);
} else {
data = dayData;
labels = Object.keys(data);
counts = Object.values(data);
}
currentImprChart = new Chart(ctx, {
type: 'line',
data: {
labels: Object.keys(apiCounts),
labels: labels,
datasets: [{
label: 'API-Aufrufe nach Endpunkt',
data: Object.values(apiCounts),
backgroundColor: '#f59e42',
label: currentPeriod === 'week' ? 'Impressions/Tag' : 'Impressions/Stunde',
data: counts,
borderColor: '#059669',
backgroundColor: 'rgba(5,150,105,0.1)',
tension: 0.2,
fill: true
}]
},
options: {
plugins: { legend: { display: false } },
plugins: {
legend: { display: true },
title: {
display: true,
text: currentPeriod === 'week' ? 'Wochenverlauf' : '24-Stunden-Verlauf'
}
},
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } }
}
}
});
}
function updateFunctionChart() {
const ctx = document.getElementById('funcChart').getContext('2d');
// Bestehenden Chart zerstören
if (currentFuncChart) {
currentFuncChart.destroy();
}
let data, labels, counts;
if (currentPeriod === 'week') {
data = weekFuncData;
labels = Object.keys(data);
counts = Object.values(data);
} else {
// Für stündliche Daten: Summe aller Stunden für jede Funktion
const aggregatedData = {};
Object.values(dayFuncData).forEach(hourData => {
Object.keys(hourData).forEach(func => {
aggregatedData[func] = (aggregatedData[func] || 0) + hourData[func];
});
});
data = aggregatedData;
labels = Object.keys(data);
counts = Object.values(data);
}
currentFuncChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Funktionsaufrufe',
data: counts,
backgroundColor: '#2563eb',
}]
},
options: {
plugins: {
legend: { display: false },
title: {
display: true,
text: currentPeriod === 'week' ? 'Funktionsaufrufe (Woche)' : 'Funktionsaufrufe (24h)'
}
},
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } }
}
}
});
}
function updateApiChart() {
const apiChartElement = document.getElementById('apiChart');
if (!apiChartElement) return;
const ctx = apiChartElement.getContext('2d');
// Bestehenden Chart zerstören
if (currentApiChart) {
currentApiChart.destroy();
}
let data, labels, counts;
if (currentPeriod === 'week') {
data = weekApiData;
} else {
// Für stündliche Daten: Summe aller Stunden für jede API
const aggregatedData = {};
Object.values(dayApiData).forEach(hourData => {
Object.keys(hourData).forEach(api => {
aggregatedData[api] = (aggregatedData[api] || 0) + hourData[api];
});
});
data = aggregatedData;
}
if (Object.keys(data).length === 0) return;
labels = Object.keys(data);
counts = Object.values(data);
currentApiChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'API-Aufrufe nach Endpunkt',
data: counts,
backgroundColor: '#f59e42',
}]
},
options: {
plugins: {
legend: { display: false },
title: {
display: true,
text: currentPeriod === 'week' ? 'API-Nutzung (Woche)' : 'API-Nutzung (24h)'
}
},
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } }
}
}
});
}
function updateAllCharts() {
updateImpressionsChart();
updateFunctionChart();
updateApiChart();
}
// Initial Charts erstellen
updateAllCharts();
});
</script>
</body>

View File

@@ -221,10 +221,11 @@ def test_api_plusminus(client):
def test_api_stats(client):
resp = client.get('/api/stats')
assert resp.status_code == 200
# Die Route gibt HTML zurück, nicht JSON
html = resp.data.decode('utf-8')
# Prüfe auf typische HTML-Elemente des Dashboards
assert 'Statistik-Dashboard' in html or 'Dashboard' in html
data = resp.get_json()
assert "pageviews" in data
assert "func_counts" in data
assert "impressions_per_day" in data
assert "api_counts" in data
def test_api_monitor(client):
resp = client.get('/api/monitor')

View File

@@ -481,3 +481,107 @@ msgstr "Select German"
#: templates/index.html:131
msgid "English auswählen"
msgstr "Select English"
#: templates/index.html:132
msgid "Taschenrechner"
msgstr "Calculator"
#: templates/index.html:133
msgid "Taschenrechner öffnen"
msgstr "Open calculator"
#: templates/index.html:134
msgid "Taschenrechner schließen"
msgstr "Close calculator"
#: templates/index.html:135
msgid "Verwenden Sie die Tab-Taste um durch die Tasten zu navigieren. Tastatur-Kurzbefehle: Zahlen 0-9, Punkt oder Komma für Dezimal, Plus (+) oder P für Addition, Minus (-) oder M für Subtraktion, Stern (*) oder X für Multiplikation, Schrägstrich (/) oder D für Division, Enter oder Leertaste für Gleich, C für Löschen, Backspace für letzte Ziffer löschen."
msgstr "Use the Tab key to navigate through the buttons. Keyboard shortcuts: Numbers 0-9, period or comma for decimal, Plus (+) or P for addition, Minus (-) or M for subtraction, Asterisk (*) or X for multiplication, Slash (/) or D for division, Enter or Space for equals, C for clear, Backspace for delete last digit."
#: templates/index.html:136
msgid "Taschenrechner Anzeige"
msgstr "Calculator display"
#: templates/index.html:137
msgid "Berechnungsverlauf"
msgstr "Calculation history"
#: templates/index.html:138
msgid "Löschen (Taste: C)"
msgstr "Clear (key: C)"
#: templates/index.html:139
msgid "Letzte Ziffer löschen (Taste: Backspace)"
msgstr "Delete last digit (key: Backspace)"
#: templates/index.html:140
msgid "Dividieren (Taste: / oder D)"
msgstr "Divide (key: / or D)"
#: templates/index.html:141
msgid "Multiplizieren (Taste: * oder X)"
msgstr "Multiply (key: * or X)"
#: templates/index.html:142
msgid "Sieben"
msgstr "Seven"
#: templates/index.html:143
msgid "Acht"
msgstr "Eight"
#: templates/index.html:144
msgid "Neun"
msgstr "Nine"
#: templates/index.html:145
msgid "Subtrahieren (Taste: - oder M)"
msgstr "Subtract (key: - or M)"
#: templates/index.html:146
msgid "Vier"
msgstr "Four"
#: templates/index.html:147
msgid "Fünf"
msgstr "Five"
#: templates/index.html:148
msgid "Sechs"
msgstr "Six"
#: templates/index.html:149
msgid "Addieren (Taste: + oder P)"
msgstr "Add (key: + or P)"
#: templates/index.html:150
msgid "Eins"
msgstr "One"
#: templates/index.html:151
msgid "Zwei"
msgstr "Two"
#: templates/index.html:152
msgid "Drei"
msgstr "Three"
#: templates/index.html:153
msgid "Gleich (Taste: Enter oder Leertaste)"
msgstr "Equals (key: Enter or Space)"
#: templates/index.html:154
msgid "Null"
msgstr "Zero"
#: templates/index.html:155
msgid "Komma (Taste: . oder ,)"
msgstr "Decimal (key: . or ,)"
#: templates/index.html:156
msgid "Fehler"
msgstr "Error"
#: templates/index.html:157
msgid "Berechnung"
msgstr "Calculation"