40 Commits

Author SHA1 Message Date
e2367d0b0e chore: Version auf 1.3.11 erhöht - Sprachausgabe-Funktion und verbesserte Feiertage-Anzeige 2025-08-01 13:10:31 +02:00
1eb55e32dc feat: Sprachausgabe-Funktion für barrierefreie Nutzung hinzugefügt
- Vorlesen-Buttons (🔊) bei allen Ergebnissen
- Web Speech API mit deutscher Sprachausgabe
- Vollständige Tastaturnavigation (Tab, Enter, Leertaste)
- ESC-Taste zum Stoppen der Wiedergabe
- Barrierefreiheit verbessert für Menschen mit Sehbehinderungen
- README aktualisiert mit Sprachausgabe-Dokumentation
2025-08-01 12:51:28 +02:00
9a45444db4 Version 1.3.10: Versionsanzeige im Footer hinzugefügt 2025-07-26 10:28:30 +02:00
386a3f688f Verbessere stats_dashboard.html: Schriftart angepasst und API-Counts entfernt 2025-07-26 10:25:11 +02:00
04dc301e5b CSS-Fixes: Scrollbars auf allen Geräten entfernt, responsive Box-Sizing und Breitenanpassung verbessert 2025-07-26 08:41:06 +02:00
aaf6dbdec0 API: Bundesland-Feiertage für REST API erweitert
- tage_werktage Endpunkt unterstützt jetzt bundesland Parameter
- Swagger-Dokumentation aktualisiert mit bundesland Parameter
- Alle 16 deutschen Bundesländer in der API-Dokumentation aufgelistet
2025-07-25 17:13:39 +02:00
e2a5c1a3fa Feat: Bundesland-Feiertage für Werktagsberechnung hinzugefügt
- Neue Funktion zur Abfrage bundeslandspezifischer Feiertage über feiertage-api.de
- Werktagsberechnung berücksichtigt jetzt optional Feiertage des gewählten Bundeslandes
- Frontend: Dropdown für Bundesland-Auswahl (nur aktiv wenn Werktage-Checkbox aktiviert)
- Anzeige der Anzahl Wochenendtage und Feiertage im Ergebnis
- REST API erweitert um bundesland-Parameter
- README.md aktualisiert mit Dokumentation der neuen Funktion
2025-07-25 17:10:57 +02:00
5009ec1085 Add lighthouse-score.pdf 2025-07-25 15:03:51 +02:00
feb179f7bc Update README 2025-07-25 14:51:15 +02:00
55a05ef2af Hilfe-Button mit Overlay hinzugefügt - Barrierefreier Tooltip und Modal-Dialog implementiert 2025-07-25 14:49:43 +02:00
97aa208bf2 Performance-Optimierungen: CSS inline eingebettet, Touch-Targets vergrößert, Layout-Shifts minimiert 2025-07-25 14:02:36 +02:00
ffa1af560c Code Statistik aktualisiert 2025-07-25 13:42:46 +02:00
5867e3eeb7 Fix 404 errors: Move favicon files to static directory and update all references 2025-07-25 13:19:12 +02:00
8f8c5f42ca Fix apple-mobile-web-app-status-bar-style 2025-07-25 13:13:59 +02:00
fec84005f4 UI: Submit-Buttons vergrößert und einheitlichen vertikalen Abstand hinzugefügt 2025-07-25 13:08:57 +02:00
f2391de8b4 UI: Checkbox-Alignment im Plus/Minus-Bereich verbessert 2025-07-25 12:54:28 +02:00
9361d11dff remove ) 2025-07-25 12:32:16 +02:00
c67df924c6 Update TOC 2025-07-25 12:31:48 +02:00
59145f9ea1 Add batch file 2025-07-25 12:30:05 +02:00
8c56cdeb55 Add cloc code stats 2025-07-25 12:25:06 +02:00
d24f93d039 Swagger/OpenAPI-Doku unter /api-docs, Link im Footer, keine Überschneidung mit API-Endpunkten 2025-07-25 12:14:22 +02:00
77a6b5c2c2 Change Codeberg link to README.md 2025-07-25 11:59:57 +02:00
25b0de1cce Text geändert 2025-07-25 11:28:55 +02:00
8d10c6f891 Refaktoriert: Logdatei-Auswertung in Hilfsfunktion ausgelagert, Redundanz in /stats und /api/stats beseitigt 2025-07-25 11:28:15 +02:00
e44e55af53 Entfernt: redundanter /monitor-Endpunkt, da /api/monitor identische Funktion bietet 2025-07-25 11:25:26 +02:00
34eeed8753 Verbesserte Kontrast- und Farbgestaltung der Accordion-Header für Barrierefreiheit und bessere Unterscheidbarkeit 2025-07-25 11:22:26 +02:00
de47c379b5 It´s FREE 2025-07-25 11:05:58 +02:00
9ef30029df Screenshot: crop and resize 2025-07-25 10:42:49 +02:00
def7c43050 TOC eingefügt 2025-07-25 10:40:22 +02:00
c832f372a6 Rearrange README 2025-07-25 10:37:28 +02:00
18bc24c0b1 Add screen shots 2025-07-25 10:02:29 +02:00
8a5816a547 Barrierefreiheit: Accordion, Formulare, aria-live, Kontraste, SEO und README für Accessibility optimiert 2025-07-25 09:38:36 +02:00
142ecb12cf Tests an API angepasst 2025-07-24 19:38:22 +02:00
474a2d485c REST API-Nutzung wird im Dashboard ausgewertet und dokumentiert 2025-07-24 19:37:58 +02:00
1351eae56e Accordion-Logik und Panel-Index korrigiert, kombinierte Plus/Minus-Funktion, Tests aktualisiert 2025-07-24 19:23:28 +02:00
18db1b2ded Footer: M. Busche mit mailto-Link versehen 2025-07-24 16:08:29 +02:00
0674849b6c Fix: Werktagsberechnung robust, Checkbox-Auswertung verbessert, Tests angepasst 2025-07-24 15:31:55 +02:00
b71bca3bb4 Remove PWA banner 2025-07-24 14:00:08 +02:00
1fe443d53d Update README 2025-07-24 13:47:06 +02:00
ab04fb52b9 Monitoring-Endpunkt hinzugefügt 2025-07-24 13:45:39 +02:00
19 changed files with 1921 additions and 496 deletions

299
README.md
View File

@@ -2,22 +2,79 @@
Diese moderne Python-Webanwendung (Flask) ermöglicht verschiedene Datumsberechnungen über eine übersichtliche Weboberfläche:
## Inhaltsverzeichnis
- [Demo](#demo)
- [Funktionen](#funktionen)
- [Installation (lokal)](#installation-lokal)
- [Starten der App](#starten-der-app)
- [Statistik-Dashboard & Passwortschutz](#statistik-dashboard-stats--passwortschutz)
- [Docker (empfohlen für Produktion)](#docker-empfohlen-für-produktion)
- [Log-Verzeichnis mounten](#log-verzeichnis-mounten-logs-auf-dem-host-speichern)
- [docker-compose Beispiel](#docker-compose-beispiel)
- [REST API](#rest-api)
- [Tage/Werktage zwischen zwei Daten](#1-tagewerktage-zwischen-zwei-daten)
- [Wochentag zu einem Datum](#2-wochentag-zu-einem-datum)
- [Kalenderwoche zu Datum](#3-kalenderwoche-zu-datum)
- [Start-/Enddatum einer Kalenderwoche](#4-start-enddatum-einer-kalenderwoche)
- [Datum plus/minus Tage, Wochen, Monate](#5-datum-plusminus-tage-wochen-monate)
- [Statistik](#6-statistik)
- [Monitoring & Healthcheck](#7-monitoring--healthcheck)
- [Progressive Web App (PWA)](#progressive-web-app-pwa)
- [Monitoring & Healthcheck](#monitoring--healthcheck)
- [Entwicklung & Hinweise](#entwicklung--hinweise)
- [Automatisierte Tests](#automatisierte-tests)
- [Hinweise](#hinweise)
- [Motivation](#motivation)
- [Vibe Coding](#vibe-coding)
- [Statistik-Erfassung, Logging](#statistik-erfassung-logging)
- [Barrierefreiheit (Accessibility)](#barrierefreiheit-accessibility)
- [Code Statistik](#code-statistik)
- [Lizenz](#lizenz)
## Demo
Datumsrechner Live: [https://date.elpatron.me](https://date.elpatron.me)
![image-20250725095959116](./assets/image-20250725095959116.png)
## Funktionen
- Anzahl der Tage zwischen zwei Daten
- Anzahl der Werktage zwischen zwei Daten
- Anzahl der Werktage zwischen zwei Daten (mit optionaler Berücksichtigung bundeslandspezifischer Feiertage)
- Anzeige des Wochentags eines Datums
- Datum plus/minus X Tage
- Datum plus/minus X Werktage
- Datum plus/minus X Wochen/Monate
- Kalenderwoche zu Datum
- Start-/Enddatum einer Kalenderwoche eines Jahres
- **Sprachausgabe** für alle Ergebnisse (barrierefrei)
- Statistik-Dashboard mit Passwortschutz unter `/stats`
## Bundesland-Feiertage
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)
- Brandenburg (BB)
- Bremen (HB)
- Hamburg (HH)
- Hessen (HE)
- Mecklenburg-Vorpommern (MV)
- Niedersachsen (NI)
- Nordrhein-Westfalen (NW)
- Rheinland-Pfalz (RP)
- Saarland (SL)
- Sachsen (SN)
- Sachsen-Anhalt (ST)
- Schleswig-Holstein (SH)
- Thüringen (TH)
Die Feiertage werden automatisch für den gewählten Zeitraum abgerufen und bei der Werktagsberechnung als arbeitsfreie Tage behandelt. Im Ergebnis werden zusätzlich die Anzahl der Wochenendtage und Feiertage angezeigt.
## Installation (lokal)
1. Python 3.8+ installieren
@@ -39,6 +96,8 @@ Die App ist dann unter http://localhost:5000 erreichbar.
Das Dashboard ist mit einem statischen Passwort geschützt, das über die Umgebungsvariable `STATS_PASSWORD` gesetzt wird.
![image-20250725100127004](./assets/image-20250725100127004.png)
Beispiel (PowerShell):
```powershell
@@ -97,6 +156,183 @@ Starte mit:
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/`
**Hinweis:** Die Nutzung der REST API wird im Statistik-Dashboard ausgewertet und als Diagramm angezeigt.
### Endpunkte und Beispiele
#### 1. Tage/Werktage zwischen zwei Daten
**POST** `/api/tage_werktage`
```json
{
"start": "2024-06-01",
"end": "2024-06-10",
"werktage": true,
"bundesland": "BY"
}
```
**Mit curl:**
```bash
curl -X POST http://localhost:5000/api/tage_werktage \
-H "Content-Type: application/json" \
-d '{"start": "2024-06-01", "end": "2024-06-10", "werktage": true, "bundesland": "BY"}'
```
**Antwort:**
```json
{ "result": 7 }
```
**Hinweis:** Der Parameter `bundesland` ist optional und wird nur bei `"werktage": true` berücksichtigt. Verfügbare Bundesland-Kürzel siehe oben.
#### 2. Wochentag zu einem Datum
**POST** `/api/wochentag`
```json
{ "datum": "2024-06-10" }
```
**Mit curl:**
```bash
curl -X POST http://localhost:5000/api/wochentag \
-H "Content-Type: application/json" \
-d '{"datum": "2024-06-10"}'
```
**Antwort:**
```json
{ "result": "Montag" }
```
#### 3. Kalenderwoche zu Datum
**POST** `/api/kw_berechnen`
```json
{ "datum": "2024-06-10" }
```
**Mit curl:**
```bash
curl -X POST http://localhost:5000/api/kw_berechnen \
-H "Content-Type: application/json" \
-d '{"datum": "2024-06-10"}'
```
**Antwort:**
```json
{ "result": "KW 24 (2024)", "kw": 24, "jahr": 2024 }
```
#### 4. Start-/Enddatum einer Kalenderwoche
**POST** `/api/kw_datum`
```json
{ "jahr": 2024, "kw": 24 }
```
**Mit curl:**
```bash
curl -X POST http://localhost:5000/api/kw_datum \
-H "Content-Type: application/json" \
-d '{"jahr": 2024, "kw": 24}'
```
**Antwort:**
```json
{
"result": "10.06.2024 bis 16.06.2024",
"start": "2024-06-10",
"end": "2024-06-16"
}
```
#### 5. Datum plus/minus Tage, Wochen, Monate
**POST** `/api/plusminus`
```json
{
"datum": "2024-06-10",
"anzahl": 5,
"einheit": "tage",
"richtung": "add",
"werktage": false
}
```
**Mit curl:**
```bash
curl -X POST http://localhost:5000/api/plusminus \
-H "Content-Type: application/json" \
-d '{"datum": "2024-06-10", "anzahl": 5, "einheit": "tage", "richtung": "add", "werktage": false}'
```
**Antwort:**
```json
{ "result": "2024-06-15" }
```
**Hinweis:**
- `"einheit"`: `"tage"`, `"wochen"` oder `"monate"`
- `"richtung"`: `"add"` (plus) oder `"sub"` (minus)
- `"werktage"`: `true` für Werktage, sonst `false` (nur bei `"tage"` unterstützt)
#### 6. Statistik
**GET** `/api/stats`
**Mit curl:**
```bash
curl http://localhost:5000/api/stats
```
**Antwort:**
```json
{
"pageviews": 42,
"func_counts": { "plusminus": 10, "tage_werktage": 5 },
"impressions_per_day": { "2024-06-10": 7 }
}
```
#### 7. Monitoring & Healthcheck
**GET** `/api/monitor`
**Mit curl:**
```bash
curl http://localhost:5000/api/monitor
```
**Antwort:**
```json
{
"status": "ok",
"message": "App running",
"time": "2025-07-24T13:37:00.123456",
"uptime_seconds": 12345,
"pageviews_last_7_days": 42
}
```
---
**Fehlerfälle** liefern immer einen HTTP-Statuscode 400 und ein JSON mit `"error"`-Feld, z.B.:
```json
{ "error": "Ungültige Eingabe", "details": "..." }
```
## Progressive Web App (PWA)
Elpatrons Datumsrechner ist als PWA installierbar (z.B. auf Android/iOS-Homescreen oder Desktop). Die App funktioniert offline für die Startseite und statische Ressourcen, die Datumsberechnung bleibt serverseitig.
@@ -105,6 +341,33 @@ Elpatrons Datumsrechner ist als PWA installierbar (z.B. auf Android/iOS-Homescre
- App-Icon und Theme-Color für Homescreen
- Installation über Browser-Menü ("Zum Startbildschirm hinzufügen")
## Monitoring & Healthcheck
Die App bietet einen Monitoring-Endpunkt unter `/monitor`, der Statusinformationen als JSON zurückgibt (z.B. für Uptime-Robot, Docker Healthcheck oder eigene Tools):
- Status (ok)
- Aktuelle Serverzeit
- Uptime (Sekunden seit Start)
- Pageviews der letzten 7 Tage
Beispiel-Aufruf:
```
GET https://date.elpatron.me/monitor
```
Antwort:
```json
{
"status": "ok",
"message": "App running",
"time": "2025-07-24T13:37:00.123456",
"uptime_seconds": 12345,
"pageviews_last_7_days": 42
}
```
## Entwicklung & Hinweise
- Die HTML-Templates liegen im Ordner `templates/` (Trennung von Logik und Darstellung)
@@ -136,6 +399,40 @@ Dieses Projekt wurde zu nahezu 100% mit Unterstützung künsticher Intelligenz (
Es werden keine IP-Adressen oder sonstigen persönlichen Daten gespeichert, lediglich die Zahl der Aufrufe der Funktionen in einer kumulierten Darstellung. Die Logdatei enthält nur Einträge der letzten sieben Tage.
### Barrierefreiheit (Accessibility)
*Elpatrons Datumsrechner* ist barrierefrei gestaltet und erfüllt zentrale Anforderungen an Accessibility (a11y):
- *Semantische HTML-Struktur:* Überschriften, Labels und Formularelemente sind korrekt ausgezeichnet und verknüpft.
- *ARIA-Attribute:* Accordion und Statusmeldungen sind mit ARIA-Attributen versehen, damit Screenreader die Struktur und Zustände erkennen.
- *Tastaturbedienbarkeit:* Alle interaktiven Elemente (Accordion, Buttons, Formulare) sind vollständig per Tastatur bedienbar (inkl. Fokus-Indikator und Pfeiltasten-Navigation im Accordion).
- *Fokus-Indikatoren:* Deutliche visuelle Hervorhebung des Fokus für alle Bedienelemente.
- *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).
- *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.
Damit ist die App für Menschen mit unterschiedlichen Einschränkungen (z.B. Sehbehinderung, motorische Einschränkungen) gut nutzbar und entspricht modernen Webstandards.
### Code Statistik
cloc|github.com/AlDanial/cloc v 2.06 T=0.06 s (263.5 files/s, 39193.4 lines/s)
--- | ---
Language|files|blank|comment|code
:-------|-------:|-------:|-------:|-------:
HTML|4|21|6|927
Python|2|34|26|482
Markdown|2|116|0|328
JSON|2|0|0|237
JavaScript|1|0|0|20
SVG|2|0|0|14
Dockerfile|1|5|6|8
DOS Batch|1|0|0|1
--------|--------|--------|--------|--------
SUM:|15|176|38|2017
## Lizenz
Dieses Projekt steht unter der [MIT-Lizenz](LICENSE).

316
app.py
View File

@@ -1,16 +1,35 @@
from flask import Flask, render_template, request, redirect, url_for, session, abort
from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify
from datetime import datetime, timedelta
import numpy as np
from dateutil.relativedelta import relativedelta
import os
import time
import requests
app_start_time = time.time()
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', 'dev-key')
# Version der App
APP_VERSION = "1.3.11"
# HTML-Template wird jetzt aus templates/index.html geladen
WOCHENTAGE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
def get_feiertage(year, bundesland):
"""Holt die Feiertage für ein Jahr und Bundesland von feiertage-api.de."""
url = f"https://feiertage-api.de/api/?jahr={year}&nur_land={bundesland}"
try:
resp = requests.get(url, timeout=5)
data = resp.json()
# Die API gibt ein Dict mit Feiertagsnamen als Key, jeweils mit 'datum' als Wert
return [v['datum'] for v in data.values() if 'datum' in v]
except Exception as e:
print(f"Fehler beim Abrufen der Feiertage: {e}")
return []
@app.route('/', methods=['GET', 'POST'])
def index():
# Rudimentäres Logging für Page Impressions
@@ -37,78 +56,55 @@ def index():
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
feiertage_anzahl = wochenendtage_anzahl = None
active_idx = 0
plusminus_result = None
if request.method == 'POST':
action = request.form.get('action')
# Funktions-Logging
with open(log_path, 'a', encoding='utf-8') as f:
from datetime import datetime as dt
f.write(f"{dt.now().isoformat()} FUNC: {action}\n")
if action == 'tage':
if action == 'tage_werktage':
active_idx = 0
start = request.form.get('start1')
end = request.form.get('end1')
try:
d1 = datetime.strptime(start, '%Y-%m-%d')
d2 = datetime.strptime(end, '%Y-%m-%d')
tage = abs((d2 - d1).days)
except Exception:
tage = 'Ungültige Eingabe'
elif action == 'werktage':
active_idx = 1
start = request.form.get('start2')
end = request.form.get('end2')
is_werktage = request.form.get('werktage') in ('on', 'true', '1', True)
bundesland = request.form.get('bundesland')
try:
d1 = datetime.strptime(start, '%Y-%m-%d')
d2 = datetime.strptime(end, '%Y-%m-%d')
if d1 > d2:
d1, d2 = d2, d1
werktage = np.busday_count(d1.date(), (d2 + timedelta(days=1)).date())
# Feiertage bestimmen
holidays = []
if bundesland:
years = set([d1.year, d2.year])
for y in years:
holidays.extend(get_feiertage(y, bundesland))
# Alle Tage im Bereich
all_days = [(d1 + timedelta(days=i)).date() for i in range((d2 - d1).days + 1)]
# Wochenendtage zählen
wochenendtage_anzahl = sum(1 for d in all_days if d.weekday() >= 5)
# Feiertage zählen (nur die, die im Bereich liegen und nicht auf Wochenende fallen)
feiertage_im_zeitraum = [f for f in holidays if d1.date() <= datetime.strptime(f, '%Y-%m-%d').date() <= d2.date()]
feiertage_anzahl = sum(1 for f in feiertage_im_zeitraum if datetime.strptime(f, '%Y-%m-%d').date().weekday() < 5)
if is_werktage:
tage = np.busday_count(d1.date(), (d2 + timedelta(days=1)).date(), holidays=holidays)
else:
tage = abs((d2 - d1).days)
except Exception:
werktage = 'Ungültige Eingabe'
tage = 'Ungültige Eingabe'
elif action == 'wochentag':
active_idx = 2
active_idx = 1
datum = request.form.get('datum3')
try:
d = datetime.strptime(datum, '%Y-%m-%d')
wochentag = WOCHENTAGE[d.weekday()]
except Exception:
wochentag = 'Ungültige Eingabe'
elif action == 'datumsrechnung':
active_idx = 3
datum = request.form.get('datum4')
tage_input = request.form.get('tage4')
richtung = request.form.get('richtung4')
try:
d = datetime.strptime(datum, '%Y-%m-%d')
tage_int = int(tage_input)
if richtung == 'add':
result = d + timedelta(days=tage_int)
else:
result = d - timedelta(days=tage_int)
datumsrechnung = result.strftime('%d.%m.%Y')
except Exception:
datumsrechnung = 'Ungültige Eingabe'
elif action == 'werktagsrechnung':
active_idx = 4
datum = request.form.get('datum5')
tage_input = request.form.get('tage5')
richtung = request.form.get('richtung5')
try:
d = datetime.strptime(datum, '%Y-%m-%d').date()
tage_int = int(tage_input)
if richtung == 'add':
result = np.busday_offset(d, tage_int, roll='forward')
else:
result = np.busday_offset(d, -tage_int, roll='backward')
werktagsrechnung = np.datetime_as_string(result, unit='D')
# Formatierung auf deutsch
dt = datetime.strptime(werktagsrechnung, '%Y-%m-%d')
werktagsrechnung = dt.strftime('%d.%m.%Y')
except Exception:
werktagsrechnung = 'Ungültige Eingabe'
elif action == 'kw_berechnen':
active_idx = 5
active_idx = 2
datum = request.form.get('datum6')
try:
d = datetime.strptime(datum, '%Y-%m-%d')
@@ -117,7 +113,7 @@ def index():
except Exception:
kw_berechnen = 'Ungültige Eingabe'
elif action == 'kw_datum':
active_idx = 6
active_idx = 3
jahr = request.form.get('jahr7')
kw = request.form.get('kw7')
try:
@@ -129,26 +125,70 @@ def index():
kw_datum = f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}"
except Exception:
kw_datum = 'Ungültige Eingabe'
elif action == 'wochen_monate':
datum = request.form.get('datum8')
anzahl = request.form.get('anzahl8')
einheit = request.form.get('einheit8')
richtung = request.form.get('richtung8')
elif action == 'plusminus':
active_idx = 4
datum = request.form.get('datum_pm')
anzahl = request.form.get('anzahl_pm')
einheit = request.form.get('einheit_pm')
richtung = request.form.get('richtung_pm')
is_werktage = request.form.get('werktage_pm') in ('on', 'true', '1', True)
try:
d = datetime.strptime(datum, '%Y-%m-%d')
anzahl_int = int(anzahl)
if richtung == 'sub':
anzahl_int = -anzahl_int
if einheit == 'wochen':
result = d + timedelta(weeks=anzahl_int)
else:
result = d + relativedelta(months=anzahl_int)
wochen_monate = result.strftime('%d.%m.%Y')
if einheit == 'tage':
if is_werktage:
# Werktage: numpy busday_offset
result = np.busday_offset(d.date(), anzahl_int, roll='forward')
result_dt = datetime.strptime(str(result), '%Y-%m-%d')
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')}"
else:
result = d + timedelta(days=anzahl_int)
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')}"
elif einheit == 'wochen':
if is_werktage:
plusminus_result = 'Nicht unterstützt: Werktage + Wochen.'
else:
result = d + timedelta(weeks=anzahl_int)
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')}"
elif einheit == 'monate':
if is_werktage:
plusminus_result = 'Nicht unterstützt: Werktage + Monate.'
else:
result = d + relativedelta(months=anzahl_int)
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:
wochen_monate = 'Ungültige Eingabe'
return render_template('index.html', tage=tage, werktage=werktage, wochentag=wochentag, datumsrechnung=datumsrechnung, werktagsrechnung=werktagsrechnung, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx, wochen_monate=wochen_monate)
plusminus_result = '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
, feiertage_anzahl=feiertage_anzahl, wochenendtage_anzahl=wochenendtage_anzahl, app_version=APP_VERSION
)
def parse_log_stats(log_path):
pageviews = 0
func_counts = {}
impressions_per_day = {}
api_counts = {}
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]
if len(date) == 10 and date[4] == '-' and date[7] == '-':
impressions_per_day[date] = impressions_per_day.get(date, 0) + 1
except Exception:
pass
elif 'FUNC:' in line:
func = line.split('FUNC:')[1].strip()
func_counts[func] = func_counts.get(func, 0) + 1
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
@app.route('/stats', methods=['GET', 'POST'])
def stats():
stats_password = os.environ.get('STATS_PASSWORD', 'changeme')
@@ -160,27 +200,151 @@ def stats():
else:
return render_template('stats_login.html', error='Falsches Passwort!')
return render_template('stats_login.html', error=None)
# Auswertung der Logdatei
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)
# --- REST API ---
def log_api_usage(api_name):
log_dir = 'log'
os.makedirs(log_dir, exist_ok=True)
log_path = os.path.join(log_dir, 'pageviews.log')
from datetime import datetime as dt
with open(log_path, 'a', encoding='utf-8') as f:
f.write(f"{dt.now().isoformat()} FUNC_API: {api_name}\n")
@app.route('/api/tage_werktage', methods=['POST'])
def api_tage_werktage():
log_api_usage('tage_werktage')
data = request.get_json()
start = data.get('start')
end = data.get('end')
is_werktage = data.get('werktage', False)
bundesland = data.get('bundesland')
try:
d1 = datetime.strptime(start, '%Y-%m-%d')
d2 = datetime.strptime(end, '%Y-%m-%d')
if is_werktage:
if d1 > d2:
d1, d2 = d2, d1
holidays = []
if bundesland:
# Feiertage für alle Jahre im Bereich holen
years = set([d1.year, d2.year])
for y in years:
holidays.extend(get_feiertage(y, bundesland))
tage = int(np.busday_count(d1.date(), (d2 + timedelta(days=1)).date(), holidays=holidays))
else:
tage = abs((d2 - d1).days)
return jsonify({'result': tage})
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@app.route('/api/wochentag', methods=['POST'])
def api_wochentag():
log_api_usage('wochentag')
data = request.get_json()
datum = data.get('datum')
try:
d = datetime.strptime(datum, '%Y-%m-%d')
wochentag = WOCHENTAGE[d.weekday()]
return jsonify({'result': wochentag})
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@app.route('/api/kw_berechnen', methods=['POST'])
def api_kw_berechnen():
log_api_usage('kw_berechnen')
data = request.get_json()
datum = data.get('datum')
try:
d = datetime.strptime(datum, '%Y-%m-%d')
kw = d.isocalendar().week
return jsonify({'result': f"KW {kw} ({d.year})", 'kw': kw, 'jahr': d.year})
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@app.route('/api/kw_datum', methods=['POST'])
def api_kw_datum():
log_api_usage('kw_datum')
data = request.get_json()
jahr = data.get('jahr')
kw = data.get('kw')
try:
jahr = int(jahr)
kw = int(kw)
start = datetime.fromisocalendar(jahr, kw, 1)
end = datetime.fromisocalendar(jahr, kw, 7)
return jsonify({'result': f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}", 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')})
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@app.route('/api/plusminus', methods=['POST'])
def api_plusminus():
log_api_usage('plusminus')
data = request.get_json()
datum = data.get('datum')
anzahl = data.get('anzahl')
einheit = data.get('einheit')
richtung = data.get('richtung', 'add')
is_werktage = data.get('werktage', False)
try:
d = datetime.strptime(datum, '%Y-%m-%d')
anzahl_int = int(anzahl)
if richtung == 'sub':
anzahl_int = -anzahl_int
if einheit == 'tage':
if is_werktage:
result = np.busday_offset(d.date(), anzahl_int, roll='forward')
result_dt = datetime.strptime(str(result), '%Y-%m-%d')
return jsonify({'result': result_dt.strftime('%Y-%m-%d')})
else:
result = d + timedelta(days=anzahl_int)
return jsonify({'result': result.strftime('%Y-%m-%d')})
elif einheit == 'wochen':
if is_werktage:
return jsonify({'error': 'Nicht unterstützt: Werktage + Wochen.'}), 400
else:
result = d + timedelta(weeks=anzahl_int)
return jsonify({'result': result.strftime('%Y-%m-%d')})
elif einheit == 'monate':
if is_werktage:
return jsonify({'error': 'Nicht unterstützt: Werktage + Monate.'}), 400
else:
result = d + relativedelta(months=anzahl_int)
return jsonify({'result': result.strftime('%Y-%m-%d')})
else:
return jsonify({'error': 'Ungültige Einheit'}), 400
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@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)
@app.route('/api/monitor', methods=['GET'])
def api_monitor():
log_path = os.path.join('log', 'pageviews.log')
pageviews = 0
func_counts = {}
impressions_per_day = {}
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
# Datum extrahieren (YYYY-MM-DD)
try:
date = line[:10]
if len(date) == 10 and date[4] == '-' and date[7] == '-':
impressions_per_day[date] = impressions_per_day.get(date, 0) + 1
except Exception:
pass
elif 'FUNC:' in line:
func = line.split('FUNC:')[1].strip()
func_counts[func] = func_counts.get(func, 0) + 1
return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, impressions_per_day=impressions_per_day)
uptime = int(time.time() - app_start_time)
return jsonify({
"status": "ok",
"message": "App running",
"time": datetime.now().isoformat(),
"uptime_seconds": uptime,
"pageviews_last_7_days": pageviews
})
@app.route('/api-docs')
def api_docs():
return render_template('swagger.html')
if __name__ == '__main__':

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

1
code-stats.cmd Normal file
View File

@@ -0,0 +1 @@
@cloc . .\templates\ .\static\ --exclude-dir=.venv,.git,log,__pycache__,.pytest_cache --exclude-ext=txt,bak --md

31
info.md Normal file
View File

@@ -0,0 +1,31 @@
# Was ist Elpatrons Datumsrechner?
Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:
- Anzahl der Tage zwischen zwei Daten
- Anzahl der Werktage zwischen zwei Daten
- Anzeige des Wochentags eines Datums
- Datum plus/minus X Tage
- Datum plus/minus X Werktage
- Datum plus/minus X Wochen/Monate
- Kalenderwoche zu Datum
- Start-/Enddatum einer Kalenderwoche eines Jahres
## Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?
Aus zwei Gründen:
- Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!
- Das hat mich so geärgert, dass ich meinen eigenen programmiert habe.
- Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt ([Vibe Coding}(https://www.wikiwand.com/en/articles/Vibe_coding).)
## Was du noch wissen solltest
- Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern.
- Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven.
- Den Quellcode dieser App habe ich auf [Codeberg](https://codeberg.org/elpatron/datecalc) veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben.
- Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt.
- Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht.
- Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)
Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche

BIN
lighthouse-score.pdf Normal file

Binary file not shown.

View File

@@ -2,3 +2,4 @@ Flask==3.0.3
numpy==1.26.4
python-dateutil==2.9.0.post0
pytest==8.2.2
requests

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 472 B

View File

@@ -7,8 +7,8 @@
"theme_color": "#2563eb",
"description": "Open Source Web-App für Kalender- und Datumsberechnungen.",
"icons": [
{ "src": "/favicon.png", "sizes": "32x32", "type": "image/png" },
{ "src": "/favicon.ico", "sizes": "48x48 64x64 128x128 256x256", "type": "image/x-icon" },
{ "src": "/static/favicon.png", "sizes": "32x32", "type": "image/png" },
{ "src": "/static/favicon.ico", "sizes": "48x48 64x64 128x128 256x256", "type": "image/x-icon" },
{ "src": "/static/logo.svg", "sizes": "any", "type": "image/svg+xml" }
]
}

View File

@@ -2,9 +2,9 @@ const CACHE_NAME = 'datumsrechner-cache-v1';
const urlsToCache = [
'/',
'/static/style.css',
'/favicon.ico',
'/favicon.png',
'/logo.svg',
'/static/favicon.ico',
'/static/favicon.png',
'/static/logo.svg',
];
self.addEventListener('install', event => {
event.waitUntil(

View File

@@ -1,168 +0,0 @@
:root {
--primary: #2563eb;
--primary-dark: #1e40af;
--background: #f8fafc;
--surface: #fff;
--border: #e5e7eb;
--text: #1e293b;
--shadow: 0 2px 8px rgba(30,41,59,0.07);
}
body {
background: var(--background);
color: var(--text);
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 0;
}
.container {
max-width: 480px;
margin: 3em auto;
background: var(--surface);
border-radius: 16px;
box-shadow: var(--shadow);
padding: 2.5em 2em 2em 2em;
border: 1px solid var(--border);
}
h1 {
text-align: center;
margin-bottom: 2em;
font-size: 2.1em;
letter-spacing: 0.01em;
}
form {
margin-bottom: 2.2em;
padding-bottom: 1.2em;
border-bottom: 1px solid var(--border);
}
form:last-of-type {
border-bottom: none;
margin-bottom: 0;
}
h2 {
font-size: 1.15em;
margin-bottom: 0.7em;
color: var(--primary-dark);
}
label {
display: block;
margin-bottom: 0.7em;
font-weight: 500;
}
.date-row {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.2em;
}
.date-calc-row {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.2em;
}
input[type="date"] {
padding: 0.45em 0.7em;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 1em;
background: #f1f5f9;
color: var(--text);
}
.today-btn {
padding: 0.35em 0.9em;
background: var(--primary-dark);
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.95em;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.today-btn:hover {
background: var(--primary);
}
button {
margin-top: 0.7em;
padding: 0.55em 1.3em;
background: var(--primary);
color: #fff;
border: none;
border-radius: 6px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
box-shadow: 0 1px 3px rgba(30,41,59,0.05);
transition: background 0.2s;
}
button:hover {
background: var(--primary-dark);
}
.result {
margin-top: 1em;
font-weight: bold;
background: #e0e7ff;
color: #1e293b;
border-radius: 6px;
padding: 0.7em 1em;
box-shadow: 0 1px 2px rgba(30,41,59,0.04);
}
.accordion {
border-radius: 12px;
overflow: hidden;
box-shadow: var(--shadow);
background: var(--surface);
margin-bottom: 2em;
}
.accordion-item + .accordion-item {
border-top: 1px solid var(--border);
}
.accordion-header {
background: var(--primary-dark);
color: #fff;
cursor: pointer;
padding: 1em 1.2em;
font-size: 1.1em;
font-weight: 600;
border: none;
outline: none;
width: 100%;
text-align: left;
transition: background 0.2s;
}
.accordion-header.active, .accordion-header:hover {
background: var(--primary);
}
.accordion-content {
display: none;
padding: 1.2em 1.2em 1em 1.2em;
background: var(--surface);
}
.accordion-content.active {
display: block;
}
.header-tage { background: #2563eb; }
.header-tage.active, .header-tage:hover { background: #1e40af; }
.header-werktage { background: #059669; }
.header-werktage.active, .header-werktage:hover { background: #047857; }
.header-wochentag { background: #f59e42; color: #fff; }
.header-wochentag.active, .header-wochentag:hover { background: #d97706; }
.header-plusminus-tage { background: #a21caf; }
.header-plusminus-tage.active, .header-plusminus-tage:hover { background: #701a75; }
.header-plusminus-werktage { background: #0ea5e9; }
.header-plusminus-werktage.active, .header-plusminus-werktage:hover { background: #0369a1; }
.header-plusminus-wochenmonate { background: #f43f5e; }
.header-plusminus-wochenmonate.active, .header-plusminus-wochenmonate:hover { background: #be123c; }
.header-kw { background: #a78bfa; color: #1e293b; }
.header-kw.active, .header-kw:hover { background: #7c3aed; }
.header-kw-datum { background: #facc15; color: #1e293b; }
.header-kw-datum.active, .header-kw-datum:hover { background: #ca8a04; }
@media (max-width: 600px) {
.container {
margin: 1em;
padding: 1.2em 0.7em 1em 0.7em;
}
h1 {
font-size: 1.3em;
}
}

228
static/swagger.json Normal file
View File

@@ -0,0 +1,228 @@
{
"openapi": "3.0.3",
"info": {
"title": "Elpatrons Datumsrechner API",
"version": "1.0.0",
"description": "REST-API für Datumsberechnungen."
},
"servers": [
{ "url": "/api" }
],
"paths": {
"/tage_werktage": {
"post": {
"summary": "Berechnet die Anzahl der Tage oder Werktage zwischen zwei Daten.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"start": { "type": "string", "format": "date" },
"end": { "type": "string", "format": "date" },
"werktage": { "type": "boolean", "default": false },
"bundesland": {
"type": "string",
"description": "Bundesland-Kürzel für Feiertagsberücksichtigung (nur bei werktage=true)",
"enum": ["BW", "BY", "BE", "BB", "HB", "HH", "HE", "MV", "NI", "NW", "RP", "SL", "SN", "ST", "SH", "TH"]
}
},
"required": ["start", "end"]
}
}
}
},
"responses": {
"200": {
"description": "Ergebnis der Berechnung.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": { "type": "integer" }
}
}
}
}
},
"400": { "description": "Ungültige Eingabe" }
}
}
},
"/wochentag": {
"post": {
"summary": "Gibt den Wochentag zu einem Datum zurück.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"datum": { "type": "string", "format": "date" }
},
"required": ["datum"]
}
}
}
},
"responses": {
"200": {
"description": "Wochentag als Text.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": { "type": "string" }
}
}
}
}
},
"400": { "description": "Ungültige Eingabe" }
}
}
},
"/kw_berechnen": {
"post": {
"summary": "Berechnet die Kalenderwoche zu einem Datum.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"datum": { "type": "string", "format": "date" }
},
"required": ["datum"]
}
}
}
},
"responses": {
"200": {
"description": "Kalenderwoche und Jahr.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": { "type": "string" },
"kw": { "type": "integer" },
"jahr": { "type": "integer" }
}
}
}
}
},
"400": { "description": "Ungültige Eingabe" }
}
}
},
"/kw_datum": {
"post": {
"summary": "Berechnet Start- und Enddatum einer Kalenderwoche.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"jahr": { "type": "integer" },
"kw": { "type": "integer" }
},
"required": ["jahr", "kw"]
}
}
}
},
"responses": {
"200": {
"description": "Start- und Enddatum der Kalenderwoche.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": { "type": "string" },
"start": { "type": "string", "format": "date" },
"end": { "type": "string", "format": "date" }
}
}
}
}
},
"400": { "description": "Ungültige Eingabe" }
}
}
},
"/plusminus": {
"post": {
"summary": "Berechnet ein Datum plus/minus X Tage, Wochen oder Monate.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"datum": { "type": "string", "format": "date" },
"anzahl": { "type": "integer" },
"einheit": { "type": "string", "enum": ["tage", "wochen", "monate"] },
"richtung": { "type": "string", "enum": ["add", "sub"], "default": "add" },
"werktage": { "type": "boolean", "default": false }
},
"required": ["datum", "anzahl", "einheit"]
}
}
}
},
"responses": {
"200": {
"description": "Berechnetes Datum.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": { "type": "string", "format": "date" }
}
}
}
}
},
"400": { "description": "Ungültige Eingabe" }
}
}
},
"/monitor": {
"get": {
"summary": "Status- und Monitoring-Informationen zur App.",
"responses": {
"200": {
"description": "Status-Objekt mit Uptime und Pageviews.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": { "type": "string" },
"message": { "type": "string" },
"time": { "type": "string", "format": "date-time" },
"uptime_seconds": { "type": "integer" },
"pageviews_last_7_days": { "type": "integer" }
}
}
}
}
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,13 @@
<link rel="stylesheet" href="/static/style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: #f8fafc;
color: #1e293b;
margin: 0;
padding: 0;
}
.dashboard-box { max-width: 600px; margin: 3em auto; background: #fff; border-radius: 12px; box-shadow: 0 2px 8px #cbd5e1; padding: 2em 2em 1.5em 2em; border: 1px solid #e5e7eb; }
.dashboard-box h2 { text-align: center; margin-bottom: 1.2em; }
.stats-row { display: flex; justify-content: space-between; margin-bottom: 2em; }
@@ -16,9 +23,9 @@
</head>
<body>
<div class="dashboard-box">
<h2>Statistik-Dashboard</h2>
<h1>Statistik-Dashboard</h1>
<div class="stats-row">
<div class="stats-label">Gesamt-Pageviews:</div>
<div class="stats-label">Gesamt-Pageviews (7 Tage):</div>
<div class="stats-value">{{ pageviews }}</div>
</div>
<div class="chart-container">
@@ -27,10 +34,17 @@
<div class="chart-container">
<canvas id="funcChart" width="400" height="220"></canvas>
</div>
{% if api_counts and api_counts|length > 0 %}
<div class="chart-container">
<canvas id="apiChart" width="400" height="220"></canvas>
</div>
{% endif %}
<a href="/" style="color:#2563eb;">Zurück zur App</a>
</div>
<script>
<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);
@@ -55,6 +69,7 @@
}
});
// Funktionsaufrufe
// eslint-disable-next-line
const funcCounts = {{ func_counts|tojson }};
const labels = Object.keys(funcCounts);
const data = Object.values(funcCounts);
@@ -75,6 +90,29 @@
}
}
});
// 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',
data: {
labels: Object.keys(apiCounts),
datasets: [{
label: 'API-Aufrufe nach Endpunkt',
data: Object.values(apiCounts),
backgroundColor: '#f59e42',
}]
},
options: {
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } }
}
}
});
}
});
</script>
</body>
</html>

28
templates/swagger.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>API-Dokumentation Swagger UI</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css">
<style>
body { margin: 0; background: #f8fafc; }
#swagger-ui { max-width: 900px; margin: 2em auto; background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(30,41,59,0.07); }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
<script>
window.onload = function() {
SwaggerUIBundle({
url: '/static/swagger.json',
dom_id: '#swagger-ui',
presets: [SwaggerUIBundle.presets.apis],
layout: "BaseLayout",
docExpansion: 'list',
defaultModelsExpandDepth: 1
});
};
</script>
</body>
</html>

View File

@@ -13,30 +13,219 @@ def test_homepage(client):
assert resp.status_code == 200
assert b'Elpatrons Datumsrechner' in resp.data
def test_tage_berechnung(client):
def test_plusminus_tage(client):
resp = client.post('/', data={
'action': 'tage',
'start1': '2024-01-01',
'end1': '2024-01-10'
'action': 'plusminus',
'datum_pm': '2024-01-10',
'anzahl_pm': '5',
'einheit_pm': 'tage',
'richtung_pm': 'add'
})
assert resp.status_code == 200
assert b'Anzahl der Tage' in resp.data
assert b'9' in resp.data
assert b'plus 5 Tage' in resp.data
assert b'15.01.2024' in resp.data
# Subtraktion
resp = client.post('/', data={
'action': 'plusminus',
'datum_pm': '2024-01-10',
'anzahl_pm': '5',
'einheit_pm': 'tage',
'richtung_pm': 'sub'
})
assert b'minus 5 Tage' in resp.data
assert b'05.01.2024' in resp.data
def test_plusminus_werktage(client):
from numpy import busday_offset
from datetime import datetime
start = '2024-01-10'
anzahl = 5
# Addition
result = busday_offset(datetime.strptime(start, '%Y-%m-%d').date(), anzahl, roll='forward')
result_str = datetime.strptime(str(result), '%Y-%m-%d').strftime('%d.%m.%Y')
resp = client.post('/', data={
'action': 'plusminus',
'datum_pm': start,
'anzahl_pm': str(anzahl),
'einheit_pm': 'tage',
'richtung_pm': 'add',
'werktage_pm': 'on'
})
assert resp.status_code == 200
assert b'plus 5 Werktage' in resp.data
assert result_str.encode() in resp.data
# Subtraktion
result = busday_offset(datetime.strptime(start, '%Y-%m-%d').date(), -anzahl, roll='forward')
result_str = datetime.strptime(str(result), '%Y-%m-%d').strftime('%d.%m.%Y')
resp = client.post('/', data={
'action': 'plusminus',
'datum_pm': start,
'anzahl_pm': str(anzahl),
'einheit_pm': 'tage',
'richtung_pm': 'sub',
'werktage_pm': 'on'
})
assert b'minus 5 Werktage' in resp.data
assert result_str.encode() in resp.data
def test_plusminus_wochen_monate(client):
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
start = '2024-01-10'
# Wochen addieren
resp = client.post('/', data={
'action': 'plusminus',
'datum_pm': start,
'anzahl_pm': '2',
'einheit_pm': 'wochen',
'richtung_pm': 'add'
})
assert b'plus 2 Wochen' in resp.data
assert b'24.01.2024' in resp.data
# Wochen subtrahieren
resp = client.post('/', data={
'action': 'plusminus',
'datum_pm': start,
'anzahl_pm': '2',
'einheit_pm': 'wochen',
'richtung_pm': 'sub'
})
assert b'minus 2 Wochen' in resp.data
assert b'27.12.2023' in resp.data
# Monate addieren
resp = client.post('/', data={
'action': 'plusminus',
'datum_pm': start,
'anzahl_pm': '2',
'einheit_pm': 'monate',
'richtung_pm': 'add'
})
assert b'plus 2 Monate' in resp.data
assert b'10.03.2024' in resp.data
# Monate subtrahieren
resp = client.post('/', data={
'action': 'plusminus',
'datum_pm': start,
'anzahl_pm': '2',
'einheit_pm': 'monate',
'richtung_pm': 'sub'
})
assert b'minus 2 Monate' in resp.data
assert b'10.11.2023' in resp.data
def test_xss_protection(client):
# Versuche ein Skript einzuschleusen
xss = '<script>alert(1)</script>'
resp = client.post('/', data={
'action': 'tage',
'action': 'tage_werktage',
'start1': xss,
'end1': '2024-01-10'
})
assert resp.status_code == 200
# Das Skript darf nicht im HTML erscheinen (sollte escaped sein)
# Das Skript darf nicht im HTML erscheinen
assert b'<script>alert(1)</script>' not in resp.data
assert b'&lt;script&gt;alert(1)&lt;/script&gt;' in resp.data
# Es sollte eine Fehlermeldung erscheinen
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_stats_login_required(client):
resp = client.get('/stats')
assert resp.status_code == 200
assert b'Dashboard Login' in resp.data
def test_werktage_berechnung(client):
from numpy import busday_count
from datetime import date, timedelta
start = '2024-01-01'
end = '2024-03-01'
expected = busday_count(date.fromisoformat(start), date.fromisoformat(end) + timedelta(days=1))
resp = client.post('/', data={
'action': 'tage_werktage',
'start1': start,
'end1': end,
'werktage': 'on'
})
assert resp.status_code == 200
assert b'Anzahl der Werktage' in resp.data
assert f': {expected}'.encode() in resp.data
def test_api_tage_werktage(client):
# Erfolgsfall: Werktage
resp = client.post('/api/tage_werktage', json={
'start': '2024-06-01', 'end': '2024-06-10', 'werktage': True
})
assert resp.status_code == 200
data = resp.get_json()
assert 'result' in data
# Fehlerfall: Ungültiges Datum
resp = client.post('/api/tage_werktage', json={
'start': 'foo', 'end': 'bar', 'werktage': True
})
assert resp.status_code == 400
data = resp.get_json()
assert 'error' in data
def test_api_wochentag(client):
resp = client.post('/api/wochentag', json={'datum': '2024-06-10'})
assert resp.status_code == 200
data = resp.get_json()
assert data['result'] == 'Montag'
# Fehlerfall
resp = client.post('/api/wochentag', json={'datum': 'foo'})
assert resp.status_code == 400
assert 'error' in resp.get_json()
def test_api_kw_berechnen(client):
resp = client.post('/api/kw_berechnen', json={'datum': '2024-06-10'})
assert resp.status_code == 200
data = resp.get_json()
assert 'KW' in data['result']
# Fehlerfall
resp = client.post('/api/kw_berechnen', json={'datum': 'foo'})
assert resp.status_code == 400
assert 'error' in resp.get_json()
def test_api_kw_datum(client):
resp = client.post('/api/kw_datum', json={'jahr': 2024, 'kw': 24})
assert resp.status_code == 200
data = resp.get_json()
assert 'result' in data and 'start' in data and 'end' in data
# Fehlerfall
resp = client.post('/api/kw_datum', json={'jahr': 'foo', 'kw': 'bar'})
assert resp.status_code == 400
assert 'error' in resp.get_json()
def test_api_plusminus(client):
# Tage addieren
resp = client.post('/api/plusminus', json={
'datum': '2024-06-10', 'anzahl': 5, 'einheit': 'tage', 'richtung': 'add', 'werktage': False
})
assert resp.status_code == 200
data = resp.get_json()
assert data['result'] == '2024-06-15'
# Werktage subtrahieren
resp = client.post('/api/plusminus', json={
'datum': '2024-06-10', 'anzahl': 5, 'einheit': 'tage', 'richtung': 'sub', 'werktage': True
})
assert resp.status_code == 200
data = resp.get_json()
assert 'result' in data
# Fehlerfall: ungültige Einheit
resp = client.post('/api/plusminus', json={
'datum': '2024-06-10', 'anzahl': 5, 'einheit': 'foo'
})
assert resp.status_code == 400
assert 'error' in resp.get_json()
def test_api_stats(client):
resp = client.get('/api/stats')
assert resp.status_code == 200
data = resp.get_json()
assert 'pageviews' in data and 'func_counts' in data and 'impressions_per_day' in data
def test_api_monitor(client):
resp = client.get('/api/monitor')
assert resp.status_code == 200
data = resp.get_json()
assert data['status'] == 'ok'
assert 'uptime_seconds' in data