10 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
7 changed files with 155 additions and 38 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) 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:** **[Lighthouse](https://en.wikipedia.org/wiki/Lighthouse_(software))-Performance-Score:**
@@ -60,7 +60,7 @@ Die Webanwendung erreicht hervorragende Performance-Werte in allen Kategorien (P
- Datum plus/minus X Wochen/Monate - Datum plus/minus X Wochen/Monate
- Kalenderwoche zu Datum - Kalenderwoche zu Datum
- Start-/Enddatum einer Kalenderwoche eines Jahres - Start-/Enddatum einer Kalenderwoche eines Jahres
- **Integrierter Taschenrechner** mit History und Sprachausgabe - Integrierter Taschenrechner mit History und Sprachausgabe
- Mehrsprachige Unterstützung (Deutsch/Englisch) mit automatischer Browser-Spracherkennung - Mehrsprachige Unterstützung (Deutsch/Englisch) mit automatischer Browser-Spracherkennung
- Sprachausgabe für alle Ergebnisse (barrierefrei) - Sprachausgabe für alle Ergebnisse (barrierefrei)
- Statistik-Dashboard mit Passwortschutz unter `/stats` - Statistik-Dashboard mit Passwortschutz unter `/stats`
@@ -70,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. 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:** **Verfügbare Bundesländer:**
- Baden-Württemberg (BW) - Baden-Württemberg (BW)
- Bayern (BY) - Bayern (BY)
- Berlin (BE) - Berlin (BE)
@@ -94,18 +95,21 @@ Die Feiertage werden automatisch für den gewählten Zeitraum abgerufen und bei
Die Anwendung unterstützt Deutsch und Englisch mit folgenden Features: Die Anwendung unterstützt Deutsch und Englisch mit folgenden Features:
### Automatische Spracherkennung: ### Automatische Spracherkennung:
- *Browser-Sprache*: Automatische Erkennung der Browser-Einstellung - *Browser-Sprache*: Automatische Erkennung der Browser-Einstellung
- *URL-Parameter*: Sprachauswahl über `?lang=de` oder `?lang=en` - *URL-Parameter*: Sprachauswahl über `?lang=de` oder `?lang=en`
- *localStorage*: Persistente Sprachauswahl im Browser - *localStorage*: Persistente Sprachauswahl im Browser
- *Fallback*: Deutsch als Standardsprache - *Fallback*: Deutsch als Standardsprache
### *Datenschutzfreundliche Implementierung:* ### *Datenschutzfreundliche Implementierung:*
- *Keine Cookies*: Sprachauswahl ohne Cookies - *Keine Cookies*: Sprachauswahl ohne Cookies
- *URL-Parameter*: Transparente Sprachauswahl in der URL - *URL-Parameter*: Transparente Sprachauswahl in der URL
- *localStorage*: Lokale Speicherung im Browser - *localStorage*: Lokale Speicherung im Browser
- *Teilbare URLs*: URLs mit Sprachauswahl können geteilt werden - *Teilbare URLs*: URLs mit Sprachauswahl können geteilt werden
### *Barrierefreiheit:* ### *Barrierefreiheit:*
- *Screenreader*: Vollständige Unterstützung - *Screenreader*: Vollständige Unterstützung
- *Tastatur-Navigation*: Vollständig bedienbar - *Tastatur-Navigation*: Vollständig bedienbar
- *ARIA-Attribute*: Korrekte Beschriftungen - *ARIA-Attribute*: Korrekte Beschriftungen
@@ -113,6 +117,7 @@ Die Anwendung unterstützt Deutsch und Englisch mit folgenden Features:
- *Taschenrechner*: Vollständig barrierefrei mit Tastatur-Bedienung und Sprachausgabe - *Taschenrechner*: Vollständig barrierefrei mit Tastatur-Bedienung und Sprachausgabe
### *Technische Details:* ### *Technische Details:*
- *Flask-Babel*: Professionelle i18n-Implementierung - *Flask-Babel*: Professionelle i18n-Implementierung
- *Gettext*: Standard für Übersetzungen - *Gettext*: Standard für Übersetzungen
- *Responsive Design*: Angepasst für alle Geräte - *Responsive Design*: Angepasst für alle Geräte
@@ -202,8 +207,11 @@ docker-compose up --build
## REST API ## REST API
Alle Datumsfunktionen stehen auch als REST-API zur Verfügung. Die API akzeptiert und liefert JSON. Alle Datumsfunktionen stehen auch als REST-API zur Verfügung. Die API akzeptiert und liefert JSON.
**Basis-URL:** `http://localhost:5000/api/` **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. **Hinweis:** Die Nutzung der REST API wird im Statistik-Dashboard ausgewertet und als Diagramm angezeigt.
### Endpunkte und Beispiele ### Endpunkte und Beispiele
@@ -411,9 +419,7 @@ Die App bietet einen Monitoring-Endpunkt unter `/monitor`, der Statusinformation
Beispiel-Aufruf: Beispiel-Aufruf:
``` `GET https://date.elpatron.me/monitor`
GET https://date.elpatron.me/monitor
```
Antwort: Antwort:
@@ -477,23 +483,23 @@ Damit ist die App für Menschen mit unterschiedlichen Einschränkungen (z.B. Seh
### Code Statistik ### Code Statistik
cloc|github.com/AlDanial/cloc v 2.06 T=0.22 s (114.3 files/s, 32032.3 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 Language|files|blank|comment|code
:-------|-------:|-------:|-------:|-------: :-------|-------:|-------:|-------:|-------:
HTML|8|159|8|2800 HTML|8|159|8|2805
Python|2|66|74|739 Python|2|66|74|739
JavaScript|2|95|88|580 JavaScript|2|95|88|580
PO File|2|260|266|544 PO File|2|260|266|544
Markdown|3|177|0|497 Markdown|3|184|0|498
JSON|3|0|0|243 JSON|3|0|0|243
CSS|1|186|3|188 CSS|1|186|3|188
SVG|2|0|0|14 SVG|2|0|0|14
Dockerfile|1|5|6|8 Dockerfile|1|5|6|8
DOS Batch|1|0|0|1 DOS Batch|1|0|0|1
--------|--------|--------|--------|-------- --------|--------|--------|--------|--------
SUM:|25|948|445|5614 SUM:|25|955|445|5620
## Lizenz ## Lizenz

27
app.py
View File

@@ -1,5 +1,5 @@
from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify, g, make_response from flask import Flask, render_template, request, redirect, url_for, session, jsonify, make_response
from flask_babel import Babel, gettext, ngettext, get_locale from flask_babel import Babel, gettext, get_locale
from datetime import datetime, timedelta from datetime import datetime, timedelta
import numpy as np import numpy as np
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@@ -20,7 +20,7 @@ app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
babel = Babel() babel = Babel()
# Version der App # Version der App
APP_VERSION = "1.4.12" APP_VERSION = "1.4.15"
def add_cache_headers(response): def add_cache_headers(response):
"""Fügt Cache-Control-Header hinzu, die den Back-Forward-Cache ermöglichen""" """Fügt Cache-Control-Header hinzu, die den Back-Forward-Cache ermöglichen"""
@@ -118,7 +118,7 @@ def index():
with open(log_path, 'a', encoding='utf-8') as f: with open(log_path, 'a', encoding='utf-8') as f:
from datetime import datetime as dt from datetime import datetime as dt
f.write(f"{dt.now().isoformat()} PAGEVIEW\n") 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 feiertage_anzahl = wochenendtage_anzahl = None
active_idx = 0 active_idx = 0
plusminus_result = None plusminus_result = None
@@ -245,7 +245,7 @@ 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')}" 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: except Exception:
plusminus_result = gettext('Ungültige Eingabe') plusminus_result = gettext('Ungültige Eingabe')
response = make_response(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 , feiertage_anzahl=feiertage_anzahl, wochenendtage_anzahl=wochenendtage_anzahl, app_version=APP_VERSION, get_locale=get_locale
)) ))
return add_cache_headers(response) return add_cache_headers(response)
@@ -321,10 +321,13 @@ def stats():
session['stats_auth'] = True session['stats_auth'] = True
return redirect(url_for('stats')) return redirect(url_for('stats'))
else: else:
response = make_response(render_template('stats_login.html', error='Falsches Passwort!')) response = make_response(render_template('stats_login.html', error='Falsches Passwort!'))
return add_cache_headers(response) return add_cache_headers(response)
response = make_response(render_template('stats_login.html', error=None)) else:
return add_cache_headers(response) 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') log_path = os.path.join('log', 'pageviews.log')
pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly = parse_log_stats(log_path) 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)) 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))
@@ -500,6 +503,12 @@ def api_docs():
response = make_response(render_template('swagger.html')) response = make_response(render_template('swagger.html'))
return add_cache_headers(response) 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__': if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0") app.run(debug=True, host="0.0.0.0")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

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

View File

@@ -1,2 +1,14 @@
User-agent: * User-agent: *
Allow: / 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

@@ -107,9 +107,7 @@ body {
background: var(--primary); background: var(--primary);
color: white; color: white;
border: none; border: none;
padding: 0.8em 1.5em;
border-radius: 8px; border-radius: 8px;
font-size: 1em;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
font-weight: 500; font-weight: 500;
@@ -117,6 +115,9 @@ body {
max-width: 480px; max-width: 480px;
min-height: 44px; min-height: 44px;
min-width: 44px; min-width: 44px;
padding: 1em 1.2em;
font-size: 1.1em;
font-weight: bold;
} }
.calculator-btn:hover { .calculator-btn:hover {
@@ -205,14 +206,15 @@ body {
.calc-btn { .calc-btn {
padding: 0.7em; padding: 0.7em;
font-size: 1em; font-size: 1.1em !important;
font-family: 'Segoe UI', Arial, sans-serif !important;
border: 2px solid #374151; border: 2px solid #374151;
background: #f9fafb; background: #f9fafb;
color: #111827; color: #111827;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
font-weight: 600; font-weight: 600 !important;
min-width: 44px; min-width: 44px;
min-height: 44px; min-height: 44px;
display: flex; display: flex;
@@ -496,9 +498,9 @@ button:focus, .accordion-header:focus {
position: absolute; position: absolute;
top: 0.5em; top: 0.5em;
right: 0.5em; right: 0.5em;
background: rgba(37, 99, 235, 0.15); background: #ffffff;
color: var(--primary-dark); color: #000000;
border: 1px solid var(--border); border: 2px solid #000000;
border-radius: 4px; border-radius: 4px;
padding: 0.3em 0.6em; padding: 0.3em 0.6em;
font-size: 0.8em; font-size: 0.8em;
@@ -512,19 +514,20 @@ button:focus, .accordion-header:focus {
z-index: 5; z-index: 5;
} }
.read-aloud-btn:hover { .read-aloud-btn:hover {
background: rgba(37, 99, 235, 0.25); background: #f0f0f0;
border-color: var(--primary); border-color: #333333;
} }
.read-aloud-btn:focus { .read-aloud-btn:focus {
outline: 3px solid #facc15; outline: 3px solid #facc15;
outline-offset: 2px; outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b; box-shadow: 0 0 0 4px #1e293b;
background: rgba(37, 99, 235, 0.25); background: #f0f0f0;
border-color: var(--primary); border-color: #333333;
} }
.read-aloud-btn.playing { .read-aloud-btn.playing {
background: var(--primary); background: #000000;
color: white; color: #ffffff;
border-color: #000000;
} }
.accordion { .accordion {
border-radius: 12px; border-radius: 12px;
@@ -714,7 +717,10 @@ button:focus, .accordion-header:focus {
.calc-btn { .calc-btn {
padding: 0.6em; padding: 0.6em;
font-size: 1em; font-size: 1.1em;
padding: 1em 1.2em;
font-weight: 600;
/* font-family: 'Segoe UI', Arial, sans-serif; */
min-width: 44px; min-width: 44px;
min-height: 44px; min-height: 44px;
} }
@@ -1494,7 +1500,7 @@ footer br + a {
{% if request.form.get('werktage') %} {% if request.form.get('werktage') %}
{{ _('Anzahl der Werktage zwischen') }} <b>{{ format_date(request.form.get('start1', '')) }}</b> {{ _('und') }} <b>{{ format_date(request.form.get('end1', '')) }}:</b>{% if request.form.get('bundesland') %} {{ _('(Feiertage:') }} {{ request.form.get('bundesland') }}){% endif %}: {{ tage }} {{ _('Anzahl der Werktage zwischen') }} <b>{{ format_date(request.form.get('start1', '')) }}</b> {{ _('und') }} <b>{{ format_date(request.form.get('end1', '')) }}:</b>{% if request.form.get('bundesland') %} {{ _('(Feiertage:') }} {{ request.form.get('bundesland') }}){% endif %}: {{ tage }}
{% else %} {% else %}
{{ _('Anzahl der Tage zwischen') }} <b>{{ format_date(request.form.get('start1', '')) }}</b> {{ _('und') }} <b>{{ format_date(request.form.get('end1', '')) }}</b>: {{ tage }}. {{ _('Anzahl der Tage zwischen') }} <b>{{ format_date(request.form.get('start1', '')) }}</b> {{ _('und') }} <b>{{ format_date(request.form.get('end1', '')) }}</b>: {{ tage }}
{% endif %} {% endif %}
{% if wochenendtage_anzahl is not none or (feiertage_anzahl is not none and request.form.get('bundesland')) %} {% if wochenendtage_anzahl is not none or (feiertage_anzahl is not none and request.form.get('bundesland')) %}
<br> <br>