100 Commits

Author SHA1 Message Date
1c9fc93314 Version 1.4.18: Taschenrechner Button mit British Racing Green Hintergrundfarbe 2025-08-10 13:51:23 +02:00
510f039a2b Version 1.4.17: Taschenrechner Button mit British Racing Green Hintergrundfarbe 2025-08-10 13:50:36 +02:00
1967cef7fc Version 1.4.17: Taschenrechner Button mit British Racing Green Hintergrundfarbe 2025-08-10 13:49:17 +02:00
0c0997c1bd Add bruno/Datecalc.json 2025-08-04 16:10:37 +02:00
118c7c1516 Remove char 2025-08-04 11:50:52 +02:00
773696d6be Remove char 2025-08-04 11:50:21 +02:00
f53a2661da Add line feed 2025-08-04 11:41:53 +02:00
53b40ca91d Update CLOC 2025-08-04 11:41:12 +02:00
43eb609b4a Add multilingual README documentation - Rename README.md to README_de.md - Create README_en.md with English translation - Create new README.md with overview and language links 2025-08-04 11:39:51 +02:00
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
b40bb666b8 Manuelle Änderungen in README.md 2025-08-01 16:47:18 +02:00
7dbe91b32e APP_VERSION 2025-08-01 16:41:36 +02:00
c7d95e5c4c feat: Implementiere mehrsprachige Unterstützung (i18n)
- Füge Flask-Babel für professionelle i18n-Implementierung hinzu
- Implementiere automatische Browser-Spracherkennung
- Erstelle datenschutzfreundliche Sprachauswahl ohne Cookies
- Verwende URL-Parameter und localStorage für Sprachauswahl
- Füge vollständige Übersetzungen für Deutsch und Englisch hinzu
- Implementiere responsive Dropdown-Sprachauswahl mit Landesflaggen
- Verbessere Barrierefreiheit mit ARIA-Attributen und Screenreader-Support
- Aktualisiere README mit i18n-Dokumentation
- Version 1.4.0
2025-08-01 16:40:46 +02:00
d88581a663 Entferne idea.txt aus dem Repository 2025-08-01 15:38:17 +02:00
00e961a6bd Entferne versehentliche less-Hilfedatei 2025-08-01 15:36:22 +02:00
d9ecdbb86e Update cloc 2025-08-01 15:28:58 +02:00
a3753e3f4e Remove dash 2025-08-01 15:27:09 +02:00
af5ff2c094 Lighthouse-Dateien in eigenen Ordner verschoben 2025-08-01 15:26:00 +02:00
200a46fdaa Add Lighthouse report JSON 2025-08-01 14:57:04 +02:00
524f44b6f0 Lighthouse-Badges auf 100% gesetzt (basierend auf aktuellem Audit) 2025-08-01 14:50:37 +02:00
3f3cb3ed01 Wikipedia link 2025-08-01 14:44:41 +02:00
74d9c18bd9 Rearrange text 2025-08-01 14:42:18 +02:00
1b91f8e54d Punkt. 2025-08-01 14:40:48 +02:00
a594d37cf1 Fix markdown linter errors 2025-08-01 14:37:47 +02:00
b95b05e54f Remove bold 2025-08-01 14:36:14 +02:00
2271934228 Add image descriptions 2025-08-01 14:35:29 +02:00
dc73e49da0 Update cloc 2025-08-01 14:34:05 +02:00
030e4adab9 Update screenshot 2025-08-01 14:33:02 +02:00
bd895e356f Verbesserte Test-Coverage auf 90% und Coverage-Badge hinzugefügt 2025-08-01 14:29:15 +02:00
046271343d Updated lighthouse-score.pdf 2025-08-01 14:10:48 +02:00
d0d8e0aeb1 v1.3.13: Weitere Verbesserung der Farbkontraste für Wochentag- und Kalenderwoche-Header 2025-08-01 13:56:34 +02:00
c19cb17623 v1.3.12: Verbesserte Farbkontraste für bessere Barrierefreiheit 2025-08-01 13:49:02 +02:00
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
33 changed files with 23189 additions and 734 deletions

164
README.md
View File

@@ -1,169 +1,25 @@
# Elpatrons Datumsrechner # Elpatrons Date Calculator
Diese moderne Python-Webanwendung (Flask) ermöglicht verschiedene Datumsberechnungen über eine übersichtliche Weboberfläche: A modern Python web application (Flask) that enables various date calculations through a clear, accessible web interface. Features include calculating days between dates, working days with state-specific holidays, calendar weeks, date arithmetic, and more. The application supports German and English languages, includes a REST API, and is designed with accessibility in mind.
## Demo ## Documentation
Datumsrechner Live: [https://date.elpatron.me](https://date.elpatron.me) - **[English Documentation](README_en.md)** - Complete documentation in English
- **[Deutsche Dokumentation](README_de.md)** - Vollständige Dokumentation auf Deutsch
## Funktionen ## Quick Start
- 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
- Statistik-Dashboard mit Passwortschutz unter `/stats`
## Installation (lokal)
1. Python 3.8+ installieren
2. Abhängigkeiten installieren:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
```
## Starten der App
```bash
python app.py python app.py
``` ```
Die App ist dann unter http://localhost:5000 erreichbar. Visit [https://date.elpatron.me](https://date.elpatron.me) for the live demo.
## Statistik-Dashboard (/stats) & Passwortschutz ## License
Das Dashboard ist mit einem statischen Passwort geschützt, das über die Umgebungsvariable `STATS_PASSWORD` gesetzt wird. This project is licensed under the [MIT License](LICENSE).
Beispiel (PowerShell):
```powershell
$env:STATS_PASSWORD = "meinSicheresPasswort"
python app.py
```
Für Docker:
```powershell
$env:STATS_PASSWORD = "meinSicheresPasswort"
docker run -e STATS_PASSWORD=$env:STATS_PASSWORD -p 5000:5000 datumsrechner
```
## Docker (empfohlen für Produktion)
Die App läuft im Container mit dem WSGI-Server **Gunicorn**:
```bash
docker build -t datumsrechner .
docker run -p 5000:5000 datumsrechner
```
- Gunicorn startet automatisch (siehe Dockerfile)
- Empfohlen für produktiven Einsatz
### Log-Verzeichnis mounten (Logs auf dem Host speichern)
Um die Logdateien außerhalb des Containers zu speichern, kannst du das log-Verzeichnis mounten:
**PowerShell-Beispiel:**
```powershell
docker run -e STATS_PASSWORD=deinPasswort -p 5000:5000 -v ${PWD}/log:/app/log datumsrechner
```
### docker-compose Beispiel
Erstelle eine Datei `docker-compose.yml`:
```yaml
services:
datumsrechner:
build: .
ports:
- "5000:5000"
environment:
- STATS_PASSWORD=deinPasswort
volumes:
- ./log:/app/log
```
Starte mit:
```bash
docker-compose up --build
```
## 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.
- Manifest und Service Worker sind integriert
- 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)
- Das Projekt ist auf Codeberg gehostet: [https://codeberg.org/elpatron/datecalc](https://codeberg.org/elpatron/datecalc)
- Modernes, responsives Design mit Akkordeon und Icons
## Automatisierte Tests
Automatisierte Tests sind mit pytest möglich:
```powershell
pip install -r requirements.txt
pytest test_app.py
```
Die Tests prüfen u.a. die Erreichbarkeit der App, die wichtigsten Funktionen und den Schutz vor XSS-Angriffen.
## Hinweise
### Motivation
Finde mal eine Datumsrechner- Webapp, die nicht völlig Werbe- und Tracking verseucht ist! Da ich sowas häufiger mal brauche, hab ich mir eine eigene gemacht.
### 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.
### Statistik-Erfassung, Logging
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.
## Lizenz
Dieses Projekt steht unter der [MIT-Lizenz](LICENSE).
--- ---
(c) 2025 [Markus Busche](https://digitalcourage.social/@elpatron) (c) 2025 [Markus Busche](https://digitalcourage.social/@elpatron)

512
README_de.md Normal file
View File

@@ -0,0 +1,512 @@
# Elpatrons Datumsrechner
[![Test Coverage](https://img.shields.io/badge/test%20coverage-90%25-brightgreen)](https://github.com/elpatron/datecalc)
[![Lighthouse Performance](https://img.shields.io/badge/lighthouse%20performance-100%25-brightgreen)](https://date.elpatron.me)
[![Lighthouse Accessibility](https://img.shields.io/badge/lighthouse%20accessibility-100%25-brightgreen)](https://date.elpatron.me)
[![Lighthouse Best Practices](https://img.shields.io/badge/lighthouse%20best%20practices-100%25-brightgreen)](https://date.elpatron.me)
[![Lighthouse SEO](https://img.shields.io/badge/lighthouse%20seo-100%25-brightgreen)](https://date.elpatron.me)
Diese moderne Python-Webanwendung (Flask) ermöglicht verschiedene Datumsberechnungen über eine übersichtliche, barrierefreie 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)
[![App Screenshot](./assets/image-20250725095959116.png)](https://date.elpatron.me)
**[Lighthouse](https://en.wikipedia.org/wiki/Lighthouse_(software))-Performance-Score:**
Die Webanwendung erreicht hervorragende Performance-Werte in allen Kategorien (Performance, Accessibility, Best Practices, SEO).
[Lighthouse-Ergebnis (PDF)](./lighthouse/lighthouse-score.pdf)
## Funktionen
- Anzahl der Tage 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
- 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`
## 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.
## Mehrsprachige Unterstützung (i18n)
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
- *SEO-freundlich*: URLs sind indexierbar
## Installation (lokal)
1. Python 3.8+ installieren
2. Abhängigkeiten installieren:
```bash
pip install -r requirements.txt
```
## Starten der App
```bash
python app.py
```
Die App ist dann unter http://localhost:5000 erreichbar.
## Statistik-Dashboard (/stats) & Passwortschutz
Das Dashboard ist mit einem statischen Passwort geschützt, das über die Umgebungsvariable `STATS_PASSWORD` gesetzt wird.
![Statistics page](./assets/image-20250725100127004.png)
Beispiel (PowerShell):
```powershell
$env:STATS_PASSWORD = "meinSicheresPasswort"
python app.py
```
Für Docker:
```powershell
$env:STATS_PASSWORD = "meinSicheresPasswort"
docker run -e STATS_PASSWORD=$env:STATS_PASSWORD -p 5000:5000 datumsrechner
```
## Docker (empfohlen für Produktion)
Die App läuft im Container mit dem WSGI-Server **Gunicorn**:
```bash
docker build -t datumsrechner .
docker run -p 5000:5000 datumsrechner
```
- Gunicorn startet automatisch (siehe Dockerfile)
- Empfohlen für produktiven Einsatz
### Log-Verzeichnis mounten (Logs auf dem Host speichern)
Um die Logdateien außerhalb des Containers zu speichern, kannst du das log-Verzeichnis mounten:
**PowerShell-Beispiel:**
```powershell
docker run -e STATS_PASSWORD=deinPasswort -p 5000:5000 -v ${PWD}/log:/app/log datumsrechner
```
### docker-compose Beispiel
Erstelle eine Datei `docker-compose.yml`:
```yaml
services:
datumsrechner:
build: .
ports:
- "5000:5000"
environment:
- STATS_PASSWORD=deinPasswort
volumes:
- ./log:/app/log
```
Starte mit:
```bash
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
#### 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.
- 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
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)
- Das Projekt ist auf Codeberg gehostet: [https://codeberg.org/elpatron/datecalc](https://codeberg.org/elpatron/datecalc)
- Modernes, responsives Design mit Akkordeon und Icons
## Automatisierte Tests
Automatisierte Tests sind mit pytest möglich:
```powershell
pip install -r requirements.txt
pytest test_app.py
```
Die Tests prüfen u.a. die Erreichbarkeit der App, die wichtigsten Funktionen und den Schutz vor XSS-Angriffen.
## Hinweise
### Motivation
Finde mal eine Datumsrechner- Webapp, die nicht völlig Werbe- und Tracking verseucht ist! Da ich sowas häufiger mal brauche, hab ich mir eine eigene gemacht.
### 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. 12 Stunden Zeit beansprucht.
### Statistik-Erfassung, Logging
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).
- *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.
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.22 s (124.7 files/s, 34058.5 lines/s)
--- | ---
Language|files|blank|comment|code
:-------|-------:|-------:|-------:|-------:
HTML|8|159|8|2806
Markdown|5|340|0|876
Python|2|68|76|744
JavaScript|2|95|88|580
PO File|2|260|266|544
JSON|3|0|0|243
CSS|1|186|3|188
XML|1|10|4|69
SVG|2|0|0|14
Dockerfile|1|5|6|8
DOS Batch|1|0|0|1
--------|--------|--------|--------|--------
SUM:|28|1123|451|6073
## Lizenz
Dieses Projekt steht unter der [MIT-Lizenz](LICENSE).
---
(c) 2025 [Markus Busche](https://digitalcourage.social/@elpatron)
**Version 1.4.12** - Integrierter Taschenrechner mit History und Sprachausgabe hinzugefügt

512
README_en.md Normal file
View File

@@ -0,0 +1,512 @@
# Elpatrons Date Calculator
[![Test Coverage](https://img.shields.io/badge/test%20coverage-90%25-brightgreen)](https://github.com/elpatron/datecalc)
[![Lighthouse Performance](https://img.shields.io/badge/lighthouse%20performance-100%25-brightgreen)](https://date.elpatron.me)
[![Lighthouse Accessibility](https://img.shields.io/badge/lighthouse%20accessibility-100%25-brightgreen)](https://date.elpatron.me)
[![Lighthouse Best Practices](https://img.shields.io/badge/lighthouse%20best%20practices-100%25-brightgreen)](https://date.elpatron.me)
[![Lighthouse SEO](https://img.shields.io/badge/lighthouse%20seo-100%25-brightgreen)](https://date.elpatron.me)
This modern Python web application (Flask) enables various date calculations through a clear, accessible web interface.
## Table of Contents
- [Demo](#demo)
- [Features](#features)
- [Installation (local)](#installation-local)
- [Starting the App](#starting-the-app)
- [Statistics Dashboard & Password Protection](#statistics-dashboard--password-protection)
- [Docker (recommended for production)](#docker-recommended-for-production)
- [Mounting log directory](#mounting-log-directory-logs-stored-on-host)
- [docker-compose example](#docker-compose-example)
- [REST API](#rest-api)
- [Days/Working days between two dates](#1-daysworking-days-between-two-dates)
- [Weekday for a date](#2-weekday-for-a-date)
- [Calendar week for date](#3-calendar-week-for-date)
- [Start/End date of a calendar week](#4-startend-date-of-a-calendar-week)
- [Date plus/minus days, weeks, months](#5-date-plusminus-days-weeks-months)
- [Statistics](#6-statistics)
- [Monitoring & Healthcheck](#7-monitoring--healthcheck)
- [Progressive Web App (PWA)](#progressive-web-app-pwa)
- [Monitoring & Healthcheck](#monitoring--healthcheck)
- [Development & Notes](#development--notes)
- [Automated Tests](#automated-tests)
- [Notes](#notes)
- [Motivation](#motivation)
- [Vibe Coding](#vibe-coding)
- [Statistics collection, Logging](#statistics-collection-logging)
- [Accessibility](#accessibility)
- [Code Statistics](#code-statistics)
- [License](#license)
## Demo
Date Calculator Live: [https://date.elpatron.me](https://date.elpatron.me)
[![App Screenshot](./assets/image-20250725095959116.png)](https://date.elpatron.me)
**[Lighthouse](https://en.wikipedia.org/wiki/Lighthouse_(software)) Performance Score:**
The web application achieves excellent performance values in all categories (Performance, Accessibility, Best Practices, SEO).
[Lighthouse Result (PDF)](./lighthouse/lighthouse-score.pdf)
## Features
- Number of days between two dates
- Number of working days between two dates (with optional consideration of state-specific holidays)
- Display of the weekday of a date
- Date plus/minus X days
- Date plus/minus X working days
- Date plus/minus X weeks/months
- Calendar week for date
- Start/End date of a calendar week of a year
- Integrated calculator with history and speech output
- Multilingual support (German/English) with automatic browser language detection
- Speech output for all results (accessible)
- Statistics dashboard with password protection under `/stats`
## State Holidays
The working day calculation can optionally consider state-specific holidays. The free API from [feiertage-api.de](https://feiertage-api.de) is used for this purpose.
**Available States:**
- Baden-Württemberg (BW)
- Bavaria (BY)
- Berlin (BE)
- Brandenburg (BB)
- Bremen (HB)
- Hamburg (HH)
- Hesse (HE)
- Mecklenburg-Vorpommern (MV)
- Lower Saxony (NI)
- North Rhine-Westphalia (NW)
- Rhineland-Palatinate (RP)
- Saarland (SL)
- Saxony (SN)
- Saxony-Anhalt (ST)
- Schleswig-Holstein (SH)
- Thuringia (TH)
Holidays are automatically retrieved for the selected time period and treated as non-working days in the working day calculation. The result also shows the number of weekend days and holidays.
## Multilingual Support (i18n)
The application supports German and English with the following features:
### Automatic Language Detection:
- *Browser Language*: Automatic detection of browser settings
- *URL Parameter*: Language selection via `?lang=de` or `?lang=en`
- *localStorage*: Persistent language selection in browser
- *Fallback*: German as default language
### *Privacy-friendly Implementation:*
- *No Cookies*: Language selection without cookies
- *URL Parameters*: Transparent language selection in URL
- *localStorage*: Local storage in browser
- *Shareable URLs*: URLs with language selection can be shared
### *Accessibility:*
- *Screen Reader*: Full support
- *Keyboard Navigation*: Fully operable
- *ARIA Attributes*: Correct labels
- *Semantic HTML*: Correct structure
- *Calculator*: Fully accessible with keyboard operation and speech output
### *Technical Details:*
- *Flask-Babel*: Professional i18n implementation
- *Gettext*: Standard for translations
- *Responsive Design*: Adapted for all devices
- *SEO-friendly*: URLs are indexable
## Installation (local)
1. Install Python 3.8+
2. Install dependencies:
```bash
pip install -r requirements.txt
```
## Starting the App
```bash
python app.py
```
The app is then accessible at http://localhost:5000.
## Statistics Dashboard (/stats) & Password Protection
The dashboard is protected with a static password that is set via the environment variable `STATS_PASSWORD`.
![Statistics page](./assets/image-20250725100127004.png)
Example (PowerShell):
```powershell
$env:STATS_PASSWORD = "mySecurePassword"
python app.py
```
For Docker:
```powershell
$env:STATS_PASSWORD = "mySecurePassword"
docker run -e STATS_PASSWORD=$env:STATS_PASSWORD -p 5000:5000 datumsrechner
```
## Docker (recommended for production)
The app runs in a container with the **Gunicorn** WSGI server:
```bash
docker build -t datumsrechner .
docker run -p 5000:5000 datumsrechner
```
- Gunicorn starts automatically (see Dockerfile)
- Recommended for production use
### Mounting log directory (Logs stored on host)
To store log files outside the container, you can mount the log directory:
**PowerShell Example:**
```powershell
docker run -e STATS_PASSWORD=yourPassword -p 5000:5000 -v ${PWD}/log:/app/log datumsrechner
```
### docker-compose example
Create a `docker-compose.yml` file:
```yaml
services:
datumsrechner:
build: .
ports:
- "5000:5000"
environment:
- STATS_PASSWORD=yourPassword
volumes:
- ./log:/app/log
```
Start with:
```bash
docker-compose up --build
```
## REST API
All date functions are also available as a REST API. The API accepts and returns JSON.
**Base URL:** `http://localhost:5000/api/`
**Swagger Documentation:** [https://date.elpatron.me/api-docs](https://date.elpatron.me/api-docs)
**Note:** The use of the REST API is evaluated in the statistics dashboard and displayed as a chart.
### Endpoints and Examples
#### 1. Days/Working days between two dates
**POST** `/api/tage_werktage`
```json
{
"start": "2024-06-01",
"end": "2024-06-10",
"werktage": true,
"bundesland": "BY"
}
```
**With 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"}'
```
**Response:**
```json
{ "result": 7 }
```
**Note:** The `bundesland` parameter is optional and is only considered when `"werktage": true`. Available state abbreviations see above.
#### 2. Weekday for a date
**POST** `/api/wochentag`
```json
{ "datum": "2024-06-10" }
```
**With curl:**
```bash
curl -X POST http://localhost:5000/api/wochentag \
-H "Content-Type: application/json" \
-d '{"datum": "2024-06-10"}'
```
**Response:**
```json
{ "result": "Monday" }
```
#### 3. Calendar week for date
**POST** `/api/kw_berechnen`
```json
{ "datum": "2024-06-10" }
```
**With curl:**
```bash
curl -X POST http://localhost:5000/api/kw_berechnen \
-H "Content-Type: application/json" \
-d '{"datum": "2024-06-10"}'
```
**Response:**
```json
{ "result": "CW 24 (2024)", "kw": 24, "jahr": 2024 }
```
#### 4. Start/End date of a calendar week
**POST** `/api/kw_datum`
```json
{ "jahr": 2024, "kw": 24 }
```
**With curl:**
```bash
curl -X POST http://localhost:5000/api/kw_datum \
-H "Content-Type: application/json" \
-d '{"jahr": 2024, "kw": 24}'
```
**Response:**
```json
{
"result": "10.06.2024 to 16.06.2024",
"start": "2024-06-10",
"end": "2024-06-16"
}
```
#### 5. Date plus/minus days, weeks, months
**POST** `/api/plusminus`
```json
{
"datum": "2024-06-10",
"anzahl": 5,
"einheit": "tage",
"richtung": "add",
"werktage": false
}
```
**With 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}'
```
**Response:**
```json
{ "result": "2024-06-15" }
```
**Note:**
- `"einheit"`: `"tage"`, `"wochen"` or `"monate"`
- `"richtung"`: `"add"` (plus) or `"sub"` (minus)
- `"werktage"`: `true` for working days, otherwise `false` (only supported for `"tage"`)
#### 6. Statistics
**GET** `/api/stats`
**With curl:**
```bash
curl http://localhost:5000/api/stats
```
**Response:**
```json
{
"pageviews": 42,
"func_counts": { "plusminus": 10, "tage_werktage": 5 },
"impressions_per_day": { "2024-06-10": 7 }
}
```
#### 7. Monitoring & Healthcheck
**GET** `/api/monitor`
**With curl:**
```bash
curl http://localhost:5000/api/monitor
```
**Response:**
```json
{
"status": "ok",
"message": "App running",
"time": "2025-07-24T13:37:00.123456",
"uptime_seconds": 12345,
"pageviews_last_7_days": 42
}
```
---
**Error cases** always return an HTTP status code 400 and JSON with an `"error"` field, e.g.:
```json
{ "error": "Invalid input", "details": "..." }
```
## Progressive Web App (PWA)
Elpatron's Date Calculator is installable as a PWA (e.g., on Android/iOS home screen or desktop). The app works offline for the homepage and static resources, date calculation remains server-side.
- Manifest and Service Worker are integrated
- App icon and theme color for home screen
- Installation via browser menu ("Add to home screen")
- Calculator works completely client-side (available offline)
## Monitoring & Healthcheck
The app provides a monitoring endpoint at `/monitor` that returns status information as JSON (e.g., for Uptime Robot, Docker Healthcheck or own tools):
- Status (ok)
- Current server time
- Uptime (seconds since start)
- Pageviews of the last 7 days
Example call:
`GET https://date.elpatron.me/monitor`
Response:
```json
{
"status": "ok",
"message": "App running",
"time": "2025-07-24T13:37:00.123456",
"uptime_seconds": 12345,
"pageviews_last_7_days": 42
}
```
## Development & Notes
- HTML templates are in the `templates/` folder (separation of logic and presentation)
- The project is hosted on Codeberg: [https://codeberg.org/elpatron/datecalc](https://codeberg.org/elpatron/datecalc)
- Modern, responsive design with accordion and icons
## Automated Tests
Automated tests are possible with pytest:
```powershell
pip install -r requirements.txt
pytest test_app.py
```
The tests check, among other things, the accessibility of the app, the most important functions, and protection against XSS attacks.
## Notes
### Motivation
Try to find a date calculator web app that isn't completely riddled with ads and tracking! Since I need something like this more often, I made my own.
### Vibe Coding
This project was created almost 100% with the support of artificial intelligence (*[Vibe Coding](https://en.wikipedia.org/wiki/Vibe_Coding)*). The basic framework was completed after about 45 minutes, and the total development of the project took about 12 hours.
### Statistics collection, Logging
No IP addresses or other personal data are stored, only the number of function calls in a cumulative representation. The log file only contains entries from the last seven days.
### Accessibility
*Elpatron's Date Calculator* is designed to be accessible and meets central accessibility (a11y) requirements:
- *Semantic HTML Structure:* Headings, labels, and form elements are correctly marked and linked.
- *ARIA Attributes:* Accordion and status messages are equipped with ARIA attributes so that screen readers can recognize the structure and states.
- *Keyboard Operability:* All interactive elements (accordion, buttons, forms) are fully operable by keyboard (including focus indicator and arrow key navigation in accordion).
- *Focus Indicators:* Clear visual highlighting of focus for all control elements.
- *Color Contrasts:* High contrasts for texts, buttons, and result boxes, tested according to WCAG guidelines.
- *Status and Error Messages:* Results and errors are made accessible to screen readers with `aria-live`.
- *Speech Output:* All results can be read aloud via 🔊 buttons (Web Speech API, German language).
- *Calculator:* Fully accessible with keyboard operation, speech output, and history function.
- *Mobile Optimization:* Additional meta tags for better usability on mobile devices and support for screen readers.
- *SEO:* The accessibility topic is visible in meta tags for search engines.
This makes the app well usable for people with different disabilities (e.g., visual impairment, motor limitations) and meets modern web standards.
### Code Statistics
cloc|github.com/AlDanial/cloc v 2.06 T=0.22 s (124.7 files/s, 34058.5 lines/s)
--- | ---
Language|files|blank|comment|code
:-------|-------:|-------:|-------:|-------:
HTML|8|159|8|2806
Markdown|5|340|0|876
Python|2|68|76|744
JavaScript|2|95|88|580
PO File|2|260|266|544
JSON|3|0|0|243
CSS|1|186|3|188
XML|1|10|4|69
SVG|2|0|0|14
Dockerfile|1|5|6|8
DOS Batch|1|0|0|1
--------|--------|--------|--------|--------
SUM:|28|1123|451|6073
## License
This project is licensed under the [MIT License](LICENSE).
---
(c) 2025 [Markus Busche](https://digitalcourage.social/@elpatron)
**Version 1.4.12** - Integrated calculator with history and speech output added

481
app.py
View File

@@ -1,18 +1,97 @@
from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify 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 from datetime import datetime, timedelta
import numpy as np import numpy as np
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
import os import os
import time import time
import requests
app_start_time = time.time() app_start_time = time.time()
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', 'dev-key') app.secret_key = os.environ.get('SECRET_KEY', 'dev-key')
# Babel Konfiguration
app.config['BABEL_DEFAULT_LOCALE'] = 'de'
app.config['BABEL_SUPPORTED_LOCALES'] = ['de', 'en']
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
babel = Babel()
# Version der App
APP_VERSION = "1.4.18"
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 # HTML-Template wird jetzt aus templates/index.html geladen
WOCHENTAGE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] WOCHENTAGE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
WOCHENTAGE_EN = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
def get_locale():
# Prüfe URL-Parameter für Sprachauswahl (datenschutzfreundlich)
if request.args.get('lang') in ['de', 'en']:
return request.args.get('lang')
# Prüfe Session für Sprachauswahl (Fallback für ältere Browser)
if 'language' in session:
return session['language']
# Fallback auf Browser-Sprache
return request.accept_languages.best_match(['de', 'en'], default='de')
# Registriere die Locale-Funktion mit Babel
babel.init_app(app, locale_selector=get_locale)
def get_wochentage():
"""Gibt die Wochentage in der aktuellen Sprache zurück"""
locale = get_locale()
if locale == 'en':
return WOCHENTAGE_EN
return WOCHENTAGE
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('/set_language/<language>')
def set_language(language):
"""Setzt die Sprache über URL-Parameter (datenschutzfreundlich)"""
if language in ['de', 'en']:
# URL-Parameter verwenden statt Session
referrer = request.referrer or url_for('index')
if '?' in referrer:
# URL hat bereits Parameter
if 'lang=' in referrer:
# Ersetze bestehenden lang-Parameter
import re
referrer = re.sub(r'[?&]lang=[^&]*', '', referrer)
return redirect(f"{referrer}&lang={language}")
else:
# URL hat keine Parameter
return redirect(f"{referrer}?lang={language}")
return redirect(request.referrer or url_for('index'))
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
def index(): def index():
@@ -39,88 +118,70 @@ 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
active_idx = 0 active_idx = 0
plusminus_result = None
if request.method == 'POST': if request.method == 'POST':
action = request.form.get('action') action = request.form.get('action')
# Funktions-Logging # Funktions-Logging
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()} FUNC: {action}\n") f.write(f"{dt.now().isoformat()} FUNC: {action}\n")
if action == 'tage': if action == 'tage_werktage':
active_idx = 0 active_idx = 0
start = request.form.get('start1') start = request.form.get('start1')
end = request.form.get('end1') end = request.form.get('end1')
try: is_werktage = request.form.get('werktage') in ('on', 'true', '1', True)
d1 = datetime.strptime(start, '%Y-%m-%d') bundesland = request.form.get('bundesland')
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')
try: try:
d1 = datetime.strptime(start, '%Y-%m-%d') d1 = datetime.strptime(start, '%Y-%m-%d')
d2 = datetime.strptime(end, '%Y-%m-%d') d2 = datetime.strptime(end, '%Y-%m-%d')
if d1 > d2: if d1 > d2:
d1, d2 = d2, d1 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: except Exception:
werktage = 'Ungültige Eingabe' tage = gettext('Ungültige Eingabe')
elif action == 'wochentag': elif action == 'wochentag':
active_idx = 2 active_idx = 1
datum = request.form.get('datum3') datum = request.form.get('datum3')
try: try:
d = datetime.strptime(datum, '%Y-%m-%d') d = datetime.strptime(datum, '%Y-%m-%d')
wochentag = WOCHENTAGE[d.weekday()] wochentage = get_wochentage()
wochentag = wochentage[d.weekday()]
except Exception: except Exception:
wochentag = 'Ungültige Eingabe' wochentag = gettext('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': elif action == 'kw_berechnen':
active_idx = 5 active_idx = 2
datum = request.form.get('datum6') datum = request.form.get('datum6')
try: try:
d = datetime.strptime(datum, '%Y-%m-%d') d = datetime.strptime(datum, '%Y-%m-%d')
kw = d.isocalendar().week kw = d.isocalendar().week
locale = get_locale()
if locale == 'en':
kw_berechnen = f"Week {kw} ({d.year})"
else:
kw_berechnen = f"KW {kw} ({d.year})" kw_berechnen = f"KW {kw} ({d.year})"
except Exception: except Exception:
kw_berechnen = 'Ungültige Eingabe' kw_berechnen = gettext('Ungültige Eingabe')
elif action == 'kw_datum': elif action == 'kw_datum':
active_idx = 6 active_idx = 3
jahr = request.form.get('jahr7') jahr = request.form.get('jahr7')
kw = request.form.get('kw7') kw = request.form.get('kw7')
try: try:
@@ -129,29 +190,128 @@ def index():
# Montag der KW # Montag der KW
start = datetime.fromisocalendar(jahr, kw, 1) start = datetime.fromisocalendar(jahr, kw, 1)
end = datetime.fromisocalendar(jahr, kw, 7) end = datetime.fromisocalendar(jahr, kw, 7)
locale = get_locale()
if locale == 'en':
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')}" kw_datum = f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}"
except Exception: except Exception:
kw_datum = 'Ungültige Eingabe' kw_datum = gettext('Ungültige Eingabe')
elif action == 'wochen_monate': elif action == 'plusminus':
datum = request.form.get('datum8') active_idx = 4
anzahl = request.form.get('anzahl8') datum = request.form.get('datum_pm')
einheit = request.form.get('einheit8') anzahl = request.form.get('anzahl_pm')
richtung = request.form.get('richtung8') 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: try:
d = datetime.strptime(datum, '%Y-%m-%d') d = datetime.strptime(datum, '%Y-%m-%d')
anzahl_int = int(anzahl) anzahl_int = int(anzahl)
if richtung == 'sub': if richtung == 'sub':
anzahl_int = -anzahl_int anzahl_int = -anzahl_int
if einheit == 'wochen': locale = get_locale()
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')
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')}"
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')}"
elif einheit == 'wochen':
if is_werktage:
plusminus_result = gettext('Nicht unterstützt: Werktage + Wochen.')
else:
result = d + timedelta(weeks=anzahl_int) 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')}"
elif einheit == 'monate':
if is_werktage:
plusminus_result = gettext('Nicht unterstützt: Werktage + Monate.')
else: else:
result = d + relativedelta(months=anzahl_int) result = d + relativedelta(months=anzahl_int)
wochen_monate = result.strftime('%d.%m.%Y') 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')}"
except Exception: except Exception:
wochen_monate = 'Ungültige Eingabe' plusminus_result = gettext('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) 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:
# 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
# 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']) @app.route('/stats', methods=['GET', 'POST'])
def stats(): def stats():
stats_password = os.environ.get('STATS_PASSWORD', 'changeme') stats_password = os.environ.get('STATS_PASSWORD', 'changeme')
@@ -161,33 +321,166 @@ def stats():
session['stats_auth'] = True session['stats_auth'] = True
return redirect(url_for('stats')) return redirect(url_for('stats'))
else: else:
return render_template('stats_login.html', error='Falsches Passwort!') response = make_response(render_template('stats_login.html', error='Falsches Passwort!'))
return render_template('stats_login.html', error=None) return add_cache_headers(response)
# Auswertung der Logdatei 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') log_path = os.path.join('log', 'pageviews.log')
pageviews = 0 pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly = parse_log_stats(log_path)
func_counts = {} 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))
impressions_per_day = {} return add_cache_headers(response)
if os.path.exists(log_path):
with open(log_path, encoding='utf-8') as f: # --- REST API ---
for line in f: def log_api_usage(api_name):
if 'PAGEVIEW' in line: log_dir = 'log'
pageviews += 1 os.makedirs(log_dir, exist_ok=True)
# Datum extrahieren (YYYY-MM-DD) 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: try:
date = line[:10] d1 = datetime.strptime(start, '%Y-%m-%d')
if len(date) == 10 and date[4] == '-' and date[7] == '-': d2 = datetime.strptime(end, '%Y-%m-%d')
impressions_per_day[date] = impressions_per_day.get(date, 0) + 1 if is_werktage:
except Exception: if d1 > d2:
pass d1, d2 = d2, d1
elif 'FUNC:' in line: holidays = []
func = line.split('FUNC:')[1].strip() if bundesland:
func_counts[func] = func_counts.get(func, 0) + 1 # Feiertage für alle Jahre im Bereich holen
return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, impressions_per_day=impressions_per_day) 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)
response = jsonify({'result': tage})
return add_cache_headers(response)
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')
wochentage = get_wochentage()
wochentag = wochentage[d.weekday()]
response = jsonify({'result': wochentag})
return add_cache_headers(response)
except Exception as e:
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
@app.route('/monitor') @app.route('/api/kw_berechnen', methods=['POST'])
def monitor(): 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
locale = get_locale()
if locale == 'en':
kw_berechnen = f"Week {kw} ({d.year})"
else:
kw_berechnen = f"KW {kw} ({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
@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)
locale = get_locale()
if locale == 'en':
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')}"
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
@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
locale = get_locale()
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')
response = jsonify({'result': result_dt.strftime('%Y-%m-%d')})
return add_cache_headers(response)
else:
result = d + timedelta(days=anzahl_int)
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)
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)
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:
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, 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():
log_path = os.path.join('log', 'pageviews.log') log_path = os.path.join('log', 'pageviews.log')
pageviews = 0 pageviews = 0
if os.path.exists(log_path): if os.path.exists(log_path):
@@ -196,13 +489,25 @@ def monitor():
if 'PAGEVIEW' in line: if 'PAGEVIEW' in line:
pageviews += 1 pageviews += 1
uptime = int(time.time() - app_start_time) uptime = int(time.time() - app_start_time)
return jsonify({ response = jsonify({
"status": "ok", "status": "ok",
"message": "App running", "message": "App running",
"time": datetime.now().isoformat(), "time": datetime.now().isoformat(),
"uptime_seconds": uptime, "uptime_seconds": uptime,
"pageviews_last_7_days": pageviews "pageviews_last_7_days": pageviews
}) })
return add_cache_headers(response)
@app.route('/api-docs')
def api_docs():
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__': if __name__ == '__main__':

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

3
babel.cfg Normal file
View File

@@ -0,0 +1,3 @@
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

426
bruno/Datecalc.json Normal file
View File

@@ -0,0 +1,426 @@
{
"name": "Datecalc",
"version": "1",
"items": [
{
"type": "folder",
"name": "localhost",
"filename": "localhost",
"seq": 2,
"root": {
"request": {
"auth": {
"mode": "inherit"
}
},
"meta": {
"name": "localhost",
"seq": 2
}
},
"items": [
{
"type": "http",
"name": "wochentag",
"filename": "wochentag.bru",
"seq": 5,
"settings": {
"encodeUrl": false
},
"tags": [],
"request": {
"url": "http://localhost:5000/api/wochentag",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"datum\": \"2024-06-10\"\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "kw_berechnen",
"filename": "kw_berechnen.bru",
"seq": 1,
"settings": {
"encodeUrl": false
},
"tags": [],
"request": {
"url": "http://localhost:5000/api/kw_berechnen",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"datum\": \"2024-06-10\"\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "kw_datum",
"filename": "kw_datum.bru",
"seq": 3,
"settings": {
"encodeUrl": false
},
"tags": [],
"request": {
"url": "http://localhost:5000/api/kw_datum",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"jahr\": 2024,\n \"kw\": 24\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "plusminus",
"filename": "plusminus.bru",
"seq": 4,
"settings": {
"encodeUrl": false
},
"tags": [],
"request": {
"url": "http://localhost:5000/api/plusminus",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"datum\": \"2024-06-10\",\n \"anzahl\": 5,\n \"einheit\": \"tage\",\n \"richtung\": \"add\",\n \"werktage\": false\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "tage_werktage",
"filename": "tage_werktage.bru",
"seq": 2,
"settings": {
"encodeUrl": false
},
"tags": [],
"request": {
"url": "http://localhost:5000/api/tage_werktage",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"start\": \"2024-06-01\",\n \"end\": \"2024-06-10\",\n \"werktage\": true,\n \"bundesland\": \"BY\"\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
}
]
},
{
"type": "folder",
"name": "public",
"filename": "public",
"seq": 2,
"root": {
"request": {
"auth": {
"mode": "inherit"
}
},
"meta": {
"name": "public",
"seq": 2
}
},
"items": [
{
"type": "http",
"name": "kw_berechnen",
"filename": "kw_berechnen.bru",
"seq": 6,
"settings": {
"encodeUrl": false
},
"tags": [],
"request": {
"url": "https://date.elpatron.me/api/kw_berechnen",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"datum\": \"2024-06-10\"\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "tage_werktage",
"filename": "tage_werktage.bru",
"seq": 6,
"settings": {
"encodeUrl": false
},
"tags": [],
"request": {
"url": "https://date.elpatron.me/api/tage_werktage",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"start\": \"2024-06-01\",\n \"end\": \"2025-06-10\",\n \"werktage\": true,\n \"bundesland\": \"SH\"\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "kw_datum",
"filename": "kw_datum.bru",
"seq": 6,
"settings": {
"encodeUrl": false
},
"tags": [],
"request": {
"url": "https://date.elpatron.me/api/kw_datum",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"jahr\": 2024,\n \"kw\": 24\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "plusminus",
"filename": "plusminus.bru",
"seq": 6,
"settings": {
"encodeUrl": false
},
"tags": [],
"request": {
"url": "https://date.elpatron.me/api/plusminus",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"datum\": \"2024-06-10\",\n \"anzahl\": 5,\n \"einheit\": \"tage\",\n \"richtung\": \"add\",\n \"werktage\": false\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
},
{
"type": "http",
"name": "wochentag",
"filename": "wochentag.bru",
"seq": 6,
"settings": {
"encodeUrl": false
},
"tags": [],
"request": {
"url": "https://date.elpatron.me/api/wochentag",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"params": [],
"body": {
"mode": "json",
"json": "{\n \"datum\": \"2024-06-10\"\n}",
"formUrlEncoded": [],
"multipartForm": [],
"file": []
},
"script": {},
"vars": {},
"assertions": [],
"tests": "",
"docs": "",
"auth": {
"mode": "inherit"
}
}
}
]
}
],
"environments": [],
"brunoConfig": {
"version": "1",
"name": "Datecalc",
"type": "collection",
"ignore": [
"node_modules",
".git"
],
"size": 0.0014009475708007812,
"filesCount": 5
}
}

1
code-stats.cmd Normal file
View File

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

138
i18n_implementation.md Normal file
View File

@@ -0,0 +1,138 @@
# Internationalisierung (i18n) Implementierung
## Übersicht
Die Internationalisierung wurde erfolgreich in Elpatrons Datumsrechner implementiert. Das System unterstützt derzeit Deutsch (Standard) und Englisch.
## Implementierte Funktionen
### 1. Flask-Babel Integration
- **Flask-Babel 4.0.0** für Übersetzungsverwaltung
- **Babel 2.17.0** für Übersetzungskompilierung
- **Automatische Spracherkennung** basierend auf Browser-Einstellungen
- **Session-basierte Sprachauswahl** für Benutzer
### 2. Sprachauswahl
- **DE/EN Toggle** in der oberen linken Ecke
- **Visueller Indikator** für aktive Sprache
- **Persistente Sprachauswahl** über Session
### 3. Übersetzungsdateien
- **Deutsche Übersetzungen**: `translations/de/LC_MESSAGES/messages.po`
- **Englische Übersetzungen**: `translations/en/LC_MESSAGES/messages.po`
- **Kompilierte .mo Dateien** für optimale Performance
### 4. Übersetzte Inhalte
#### Meta-Tags
- Titel und Beschreibung
- Open Graph Tags
- Keywords
#### UI-Elemente
- Haupttitel und Untertitel
- Formular-Labels
- Button-Texte
- Hilfe-Texte
- Footer-Links
#### Dynamische Inhalte
- Wochentage (Deutsch/Englisch)
- Kalenderwochen-Format
- Datumsformate (DD.MM.YYYY vs MM/DD/YYYY)
- Fehlermeldungen
### 5. Technische Details
#### App-Konfiguration
```python
app.config['BABEL_DEFAULT_LOCALE'] = 'de'
app.config['BABEL_SUPPORTED_LOCALES'] = ['de', 'en']
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
```
#### Sprachauswahl-Funktion
```python
@app.route('/set_language/<language>')
def set_language(language):
if language in ['de', 'en']:
session['language'] = language
return redirect(request.referrer or url_for('index'))
```
#### Locale-Selector
```python
@babel.localeselector
def get_locale():
if 'language' in session:
return session['language']
return request.accept_languages.best_match(['de', 'en'], default='de')
```
## Verzeichnisstruktur
```
translations/
├── de/
│ └── LC_MESSAGES/
│ ├── messages.po
│ └── messages.mo
└── en/
└── LC_MESSAGES/
├── messages.po
└── messages.mo
```
## Verwendung
### Für Entwickler
1. **Neue Texte hinzufügen**:
```html
{{ _('Neuer Text') }}
```
2. **Übersetzungen extrahieren**:
```bash
pybabel extract -F babel.cfg -k _l -o messages.pot .
```
3. **Neue Übersetzungsdatei erstellen**:
```bash
pybabel init -i messages.pot -d translations -l en
```
4. **Übersetzungen kompilieren**:
```bash
pybabel compile -d translations
```
### Für Benutzer
- **Sprachauswahl**: Klick auf DE/EN in der oberen linken Ecke
- **Automatische Erkennung**: Browser-Sprache wird automatisch erkannt
- **Persistenz**: Sprachauswahl bleibt über Session erhalten
## Nächste Schritte
1. **Weitere Sprachen hinzufügen** (z.B. Französisch, Spanisch)
2. **Pluralisierung** für verschiedene Sprachen implementieren
3. **Dynamische Übersetzungen** für API-Responses
4. **RTL-Sprachen** unterstützen (Arabisch, Hebräisch)
## Dateien
- `app.py`: Hauptanwendung mit i18n-Integration
- `babel.cfg`: Babel-Konfiguration
- `requirements.txt`: Flask-Babel Abhängigkeit
- `templates/index.html`: Übersetzte UI-Templates
- `translations/`: Übersetzungsdateien
## Status
**Vollständig implementiert und funktionsfähig**
- Deutsche und englische Übersetzungen
- Sprachauswahl-UI
- Automatische Spracherkennung
- Session-basierte Persistenz
- Kompilierte Übersetzungen für Performance

View File

@@ -1,8 +0,0 @@
erstelle eine python web app, die verschiedene datumsberechnungen durchführt:
- Berechnung der Anzahl der Tage zwischen zwei Daten
- Berechnung der Anzahl der Werktage zwischen zwei Daten
- Anzeige des Wochentags eines Datums
Beachte:
- Virtual Environment unter ./.venv
- Wir entwickeln unter Windows

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,4 +1,6 @@
Flask==3.0.3 Flask==3.1.1
numpy==1.26.4 numpy==2.3.2
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pytest==8.2.2 requests==2.32.4
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

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

View File

@@ -1,15 +1,25 @@
const CACHE_NAME = 'datumsrechner-cache-v1'; const CACHE_NAME = 'datumsrechner-cache-v1';
const urlsToCache = [ const urlsToCache = [
'/', '/',
'/static/style.css', '/static/favicon.ico',
'/favicon.ico', '/static/favicon.png',
'/favicon.png', '/static/favicon.svg',
'/logo.svg', '/static/logo.svg',
'/static/manifest.json',
]; ];
self.addEventListener('install', event => { self.addEventListener('install', event => {
event.waitUntil( event.waitUntil(
caches.open(CACHE_NAME) 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 => { self.addEventListener('fetch', event => {

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,41 +6,128 @@
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style> <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 { 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; } .dashboard-box h2 { text-align: center; margin-bottom: 1.2em; }
.stats-row { display: flex; justify-content: space-between; margin-bottom: 2em; } .stats-row { display: flex; justify-content: space-between; margin-bottom: 2em; }
.stats-label { color: #64748b; } .stats-label { color: #64748b; }
.stats-value { font-size: 1.5em; font-weight: bold; } .stats-value { font-size: 1.5em; font-weight: bold; }
.chart-container { margin: 2em 0; } .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> </style>
</head> </head>
<body> <body>
<div class="dashboard-box"> <div class="dashboard-box">
<h2>Statistik-Dashboard</h2> <h1>Statistik-Dashboard</h1>
<div class="stats-row"> <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 class="stats-value">{{ pageviews }}</div>
</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"> <div class="chart-container">
<canvas id="imprChart" width="400" height="180"></canvas> <canvas id="imprChart" width="400" height="180"></canvas>
</div> </div>
<div class="chart-container"> <div class="chart-container">
<canvas id="funcChart" width="400" height="220"></canvas> <canvas id="funcChart" width="400" height="220"></canvas>
</div> </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> <a href="/" style="color:#2563eb;">Zurück zur App</a>
</div> </div>
<script> <script type="text/javascript">
// Impressions pro Tag document.addEventListener('DOMContentLoaded', function() {
const imprData = {{ impressions_per_day|tojson }}; // Daten für verschiedene Zeiträume
const imprLabels = Object.keys(imprData); const weekData = {{ impressions_per_day|tojson }};
const imprCounts = Object.values(imprData); const dayData = {{ impressions_per_hour|tojson }};
new Chart(document.getElementById('imprChart').getContext('2d'), { 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();
});
});
function updateImpressionsChart() {
const ctx = document.getElementById('imprChart').getContext('2d');
// Bestehenden Chart zerstören
if (currentImprChart) {
currentImprChart.destroy();
}
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', type: 'line',
data: { data: {
labels: imprLabels, labels: labels,
datasets: [{ datasets: [{
label: 'Impressions/Tag', label: currentPeriod === 'week' ? 'Impressions/Tag' : 'Impressions/Stunde',
data: imprCounts, data: counts,
borderColor: '#059669', borderColor: '#059669',
backgroundColor: 'rgba(5,150,105,0.1)', backgroundColor: 'rgba(5,150,105,0.1)',
tension: 0.2, tension: 0.2,
@@ -48,33 +135,137 @@
}] }]
}, },
options: { options: {
plugins: { legend: { display: true } }, plugins: {
legend: { display: true },
title: {
display: true,
text: currentPeriod === 'week' ? 'Wochenverlauf' : '24-Stunden-Verlauf'
}
},
scales: { scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } } y: { beginAtZero: true, ticks: { stepSize: 1 } }
} }
} }
}); });
// Funktionsaufrufe }
const funcCounts = {{ func_counts|tojson }};
const labels = Object.keys(funcCounts); function updateFunctionChart() {
const data = Object.values(funcCounts); const ctx = document.getElementById('funcChart').getContext('2d');
new Chart(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', type: 'bar',
data: { data: {
labels: labels, labels: labels,
datasets: [{ datasets: [{
label: 'Funktionsaufrufe', label: 'Funktionsaufrufe',
data: data, data: counts,
backgroundColor: '#2563eb', backgroundColor: '#2563eb',
}] }]
}, },
options: { options: {
plugins: { legend: { display: false } }, plugins: {
legend: { display: false },
title: {
display: true,
text: currentPeriod === 'week' ? 'Funktionsaufrufe (Woche)' : 'Funktionsaufrufe (24h)'
}
},
scales: { scales: {
y: { beginAtZero: true, ticks: { stepSize: 1 } } 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> </script>
</body> </body>
</html> </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

@@ -1,6 +1,7 @@
import os import os
import pytest import pytest
from app import app as flask_app from app import app as flask_app
from unittest.mock import patch, MagicMock
@pytest.fixture @pytest.fixture
def client(): def client():
@@ -13,30 +14,361 @@ def test_homepage(client):
assert resp.status_code == 200 assert resp.status_code == 200
assert b'Elpatrons Datumsrechner' in resp.data assert b'Elpatrons Datumsrechner' in resp.data
def test_tage_berechnung(client): def test_plusminus_tage(client):
resp = client.post('/', data={ resp = client.post('/', data={
'action': 'tage', 'action': 'plusminus',
'start1': '2024-01-01', 'datum_pm': '2024-01-10',
'end1': '2024-01-10' 'anzahl_pm': '5',
'einheit_pm': 'tage',
'richtung_pm': 'add'
}) })
assert resp.status_code == 200 assert resp.status_code == 200
assert b'Anzahl der Tage' in resp.data assert b'plus 5 Tage' in resp.data
assert b'9' 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): def test_xss_protection(client):
# Versuche ein Skript einzuschleusen # Versuche ein Skript einzuschleusen
xss = '<script>alert(1)</script>' xss = '<script>alert(1)</script>'
resp = client.post('/', data={ resp = client.post('/', data={
'action': 'tage', 'action': 'tage_werktage',
'start1': xss, 'start1': xss,
'end1': '2024-01-10' 'end1': '2024-01-10'
}) })
assert resp.status_code == 200 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'<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): def test_stats_login_required(client):
resp = client.get('/stats') resp = client.get('/stats')
assert resp.status_code == 200 assert resp.status_code == 200
assert b'Dashboard Login' in resp.data 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
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')
assert resp.status_code == 200
data = resp.get_json()
assert data['status'] == 'ok'
assert 'uptime_seconds' in data
# Neue Tests für bessere Coverage
def test_feiertage_api_error(client):
"""Test Fehlerbehandlung bei Feiertage-API"""
with patch('app.requests.get') as mock_get:
mock_get.side_effect = Exception("Network error")
resp = client.post('/', data={
'action': 'tage_werktage',
'start1': '2024-01-01',
'end1': '2024-01-10',
'bundesland': 'BY'
})
assert resp.status_code == 200
def test_logging_error_handling(client):
"""Test Logging-Fehlerbehandlung"""
# Test ohne Mock, da die App das Logging-Handling bereits hat
resp = client.get('/')
assert resp.status_code == 200
def test_invalid_date_handling(client):
"""Test ungültige Datumseingaben"""
# Ungültiges Datum bei tage_werktage
resp = client.post('/', data={
'action': 'tage_werktage',
'start1': 'invalid-date',
'end1': '2024-01-10'
})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_invalid_wochentag_input(client):
"""Test ungültige Eingaben bei Wochentag-Berechnung"""
resp = client.post('/', data={
'action': 'wochentag',
'datum3': 'invalid-date'
})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_invalid_kw_berechnen_input(client):
"""Test ungültige Eingaben bei KW-Berechnung"""
resp = client.post('/', data={
'action': 'kw_berechnen',
'datum6': 'invalid-date'
})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_invalid_kw_datum_input(client):
"""Test ungültige Eingaben bei KW-Datum"""
resp = client.post('/', data={
'action': 'kw_datum',
'jahr7': 'invalid',
'kw7': 'invalid'
})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_invalid_plusminus_input(client):
"""Test ungültige Eingaben bei Plusminus-Berechnung"""
resp = client.post('/', data={
'action': 'plusminus',
'datum_pm': 'invalid-date',
'anzahl_pm': 'invalid',
'einheit_pm': 'tage',
'richtung_pm': 'add'
})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Ungültige Eingabe' in html
def test_stats_login_success(client):
"""Test erfolgreiche Anmeldung im Stats-Bereich"""
with client.session_transaction() as sess:
sess['stats_auth'] = True
resp = client.get('/stats')
assert resp.status_code == 200
def test_stats_login_failure(client):
"""Test fehlgeschlagene Anmeldung im Stats-Bereich"""
resp = client.post('/stats', data={'password': 'wrong'})
assert resp.status_code == 200
html = resp.data.decode('utf-8')
assert 'Falsches Passwort' in html
def test_api_error_handling(client):
"""Test API-Fehlerbehandlung"""
# Test mit korrektem Content-Type
resp = client.post('/api/tage_werktage',
data='invalid json',
content_type='application/json')
assert resp.status_code == 400
def test_api_plusminus_werktage_unsupported(client):
"""Test nicht unterstützte Werktage + Wochen/Monate"""
# Werktage + Wochen
resp = client.post('/api/plusminus', json={
'datum': '2024-06-10', 'anzahl': 5, 'einheit': 'wochen', 'werktage': True
})
assert resp.status_code == 400
assert 'Nicht unterstützt' in resp.get_json()['error']
# Werktage + Monate
resp = client.post('/api/plusminus', json={
'datum': '2024-06-10', 'anzahl': 5, 'einheit': 'monate', 'werktage': True
})
assert resp.status_code == 400
assert 'Nicht unterstützt' in resp.get_json()['error']
def test_api_logging(client):
"""Test API-Logging"""
resp = client.post('/api/wochentag', json={'datum': '2024-06-10'})
assert resp.status_code == 200
# Prüfe ob Log-Datei existiert
log_path = os.path.join('log', 'pageviews.log')
assert os.path.exists(log_path)
def test_api_docs(client):
"""Test API-Dokumentation"""
resp = client.get('/api-docs')
assert resp.status_code == 200
def test_monitor_api_details(client):
"""Test detaillierte Monitor-API"""
resp = client.get('/api/monitor')
assert resp.status_code == 200
data = resp.get_json()
assert 'status' in data
assert 'message' in data
assert 'time' in data
assert 'uptime_seconds' in data
assert 'pageviews_last_7_days' in data

Binary file not shown.

View File

@@ -0,0 +1,483 @@
# German translations for Elpatrons Datumsrechner.
# Copyright (C) 2025 M. Busche
# This file is distributed under the same license as the Elpatrons Datumsrechner package.
msgid ""
msgstr ""
"Project-Id-Version: 1.3.13\n"
"Report-Msgid-Bugs-To: elpatron@mailbox.org\n"
"POT-Creation-Date: 2025-08-01 15:58+0100\n"
"PO-Revision-Date: 2025-08-01 15:58+0100\n"
"Last-Translator: M. Busche <elpatron@mailbox.org>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: templates/index.html:12
msgid "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
msgstr "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
#: templates/index.html:13
msgid "Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen barrierefrei, werbefrei, trackingfrei, kostenlos."
msgstr "Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen barrierefrei, werbefrei, trackingfrei, kostenlos."
#: templates/index.html:14
msgid "Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, Python, Flask, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei"
msgstr "Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, Python, Flask, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei"
#: templates/index.html:15
msgid "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
msgstr "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
#: templates/index.html:16
msgid "Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos."
msgstr "Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos."
#: templates/index.html:17
msgid "website"
msgstr "website"
#: templates/index.html:18
msgid "https://codeberg.org/elpatron/datecalc"
msgstr "https://codeberg.org/elpatron/datecalc"
#: templates/index.html:19
msgid "/static/logo.svg"
msgstr "/static/logo.svg"
#: templates/index.html:20
msgid "width=device-width, initial-scale=1"
msgstr "width=device-width, initial-scale=1"
#: templates/index.html:21
msgid "true"
msgstr "true"
#: templates/index.html:22
msgid "yes"
msgstr "yes"
#: templates/index.html:23
msgid "default"
msgstr "default"
#: templates/index.html:24
msgid "telephone=no"
msgstr "telephone=no"
#: templates/index.html:25
msgid "IE=edge"
msgstr "IE=edge"
#: templates/index.html:26
msgid "Elpatrons Datumsrechner"
msgstr "Elpatrons Datumsrechner"
#: templates/index.html:27
msgid "#2563eb"
msgstr "#2563eb"
#: templates/index.html:28
msgid "/static/favicon.ico"
msgstr "/static/favicon.ico"
#: templates/index.html:29
msgid "Hilfe anzeigen"
msgstr "Hilfe anzeigen"
#: templates/index.html:30
msgid "Hilfe anzeigen"
msgstr "Hilfe anzeigen"
#: templates/index.html:31
msgid "Öffnet ein Hilfefenster mit Informationen über den Datumsrechner"
msgstr "Öffnet ein Hilfefenster mit Informationen über den Datumsrechner"
#: templates/index.html:32
msgid "Elpatrons"
msgstr "Elpatrons"
#: templates/index.html:33
msgid "Datumsrechner"
msgstr "Datumsrechner"
#: templates/index.html:34
msgid "Eine <em>freie</em> Web-App: barriere<em>frei</em>, werbe<em>frei</em>, tracking<em>frei</em>, lizenz<em>frei</em> und kosten<em>frei</em>."
msgstr "Eine <em>freie</em> Web-App: barriere<em>frei</em>, werbe<em>frei</em>, tracking<em>frei</em>, lizenz<em>frei</em> und kosten<em>frei</em>."
#: templates/index.html:35
msgid "Anzahl der Tage/Werktage zwischen zwei Daten"
msgstr "Anzahl der Tage/Werktage zwischen zwei Daten"
#: templates/index.html:36
msgid "Startdatum:"
msgstr "Startdatum:"
#: templates/index.html:37
msgid "Heute"
msgstr "Heute"
#: templates/index.html:38
msgid "Enddatum:"
msgstr "Enddatum:"
#: templates/index.html:39
msgid "Optionen"
msgstr "Optionen"
#: templates/index.html:40
msgid "Nur Werktage"
msgstr "Nur Werktage"
#: templates/index.html:41
msgid "Feiertage berücksichtigen für:"
msgstr "Feiertage berücksichtigen für:"
#: templates/index.html:42
msgid "(kein Bundesland)"
msgstr "(kein Bundesland)"
#: templates/index.html:43
msgid "Baden-Württemberg"
msgstr "Baden-Württemberg"
#: templates/index.html:44
msgid "Bayern"
msgstr "Bayern"
#: templates/index.html:45
msgid "Berlin"
msgstr "Berlin"
#: templates/index.html:46
msgid "Brandenburg"
msgstr "Brandenburg"
#: templates/index.html:47
msgid "Bremen"
msgstr "Bremen"
#: templates/index.html:48
msgid "Hamburg"
msgstr "Hamburg"
#: templates/index.html:49
msgid "Hessen"
msgstr "Hessen"
#: templates/index.html:50
msgid "Mecklenburg-Vorpommern"
msgstr "Mecklenburg-Vorpommern"
#: templates/index.html:51
msgid "Niedersachsen"
msgstr "Niedersachsen"
#: templates/index.html:52
msgid "Nordrhein-Westfalen"
msgstr "Nordrhein-Westfalen"
#: templates/index.html:53
msgid "Rheinland-Pfalz"
msgstr "Rheinland-Pfalz"
#: templates/index.html:54
msgid "Saarland"
msgstr "Saarland"
#: templates/index.html:55
msgid "Sachsen"
msgstr "Sachsen"
#: templates/index.html:56
msgid "Sachsen-Anhalt"
msgstr "Sachsen-Anhalt"
#: templates/index.html:57
msgid "Schleswig-Holstein"
msgstr "Schleswig-Holstein"
#: templates/index.html:58
msgid "Thüringen"
msgstr "Thüringen"
#: templates/index.html:59
msgid "Berechnen"
msgstr "Berechnen"
#: templates/index.html:60
msgid "Ergebnis vorlesen"
msgstr "Ergebnis vorlesen"
#: templates/index.html:61
msgid "Ergebnis vorlesen"
msgstr "Ergebnis vorlesen"
#: templates/index.html:62
msgid "Anzahl der Werktage zwischen"
msgstr "Anzahl der Werktage zwischen"
#: templates/index.html:63
msgid "und"
msgstr "und"
#: templates/index.html:64
msgid "(Feiertage:"
msgstr "(Feiertage:"
#: templates/index.html:65
msgid "Anzahl der Tage zwischen"
msgstr "Anzahl der Tage zwischen"
#: templates/index.html:66
msgid "Davon sind"
msgstr "Davon sind"
#: templates/index.html:67
msgid "Tage Wochenendtage."
msgstr "Tage Wochenendtage."
#: templates/index.html:68
msgid "Feiertage (Mo-Fr,"
msgstr "Feiertage (Mo-Fr,"
#: templates/index.html:69
msgid "Wochentag eines Datums"
msgstr "Wochentag eines Datums"
#: templates/index.html:70
msgid "Datum:"
msgstr "Datum:"
#: templates/index.html:71
msgid "Anzeigen"
msgstr "Anzeigen"
#: templates/index.html:72
msgid "Wochentag von"
msgstr "Wochentag von"
#: templates/index.html:73
msgid "Kalenderwoche eines Datums"
msgstr "Kalenderwoche eines Datums"
#: templates/index.html:74
msgid "Kalenderwoche berechnen"
msgstr "Kalenderwoche berechnen"
#: templates/index.html:75
msgid "Kalenderwoche von"
msgstr "Kalenderwoche von"
#: templates/index.html:76
msgid "Start-/Enddatum zu Kalenderwoche"
msgstr "Start-/Enddatum zu Kalenderwoche"
#: templates/index.html:77
msgid "Jahr:"
msgstr "Jahr:"
#: templates/index.html:78
msgid "Kalenderwoche:"
msgstr "Kalenderwoche:"
#: templates/index.html:79
msgid "Start-/Enddatum berechnen"
msgstr "Start-/Enddatum berechnen"
#: templates/index.html:80
msgid "Start-/Enddatum der KW"
msgstr "Start-/Enddatum der KW"
#: templates/index.html:81
msgid "im Jahr"
msgstr "im Jahr"
#: templates/index.html:82
msgid "Datum plus/minus X Tage/Wochen/Monate"
msgstr "Datum plus/minus X Tage/Wochen/Monate"
#: templates/index.html:83
msgid "Anzahl:"
msgstr "Anzahl:"
#: templates/index.html:84
msgid "Rechenrichtung"
msgstr "Rechenrichtung"
#: templates/index.html:85
msgid "addieren"
msgstr "addieren"
#: templates/index.html:86
msgid "subtrahieren"
msgstr "subtrahieren"
#: templates/index.html:87
msgid "Einheit und Werktage"
msgstr "Einheit und Werktage"
#: templates/index.html:88
msgid "Einheit:"
msgstr "Einheit:"
#: templates/index.html:89
msgid "Tage"
msgstr "Tage"
#: templates/index.html:90
msgid "Wochen"
msgstr "Wochen"
#: templates/index.html:91
msgid "Monate"
msgstr "Monate"
#: templates/index.html:92
msgid "Nur Werktage"
msgstr "Nur Werktage"
#: templates/index.html:93
msgid "Hilfe schließen"
msgstr "Hilfe schließen"
#: templates/index.html:94
msgid "Was ist Elpatrons Datumsrechner?"
msgstr "Was ist Elpatrons Datumsrechner?"
#: templates/index.html:95
msgid "Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:"
msgstr "Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:"
#: templates/index.html:96
msgid "Anzahl der Tage zwischen zwei Daten"
msgstr "Anzahl der Tage zwischen zwei Daten"
#: templates/index.html:97
msgid "Anzahl der Werktage zwischen zwei Daten"
msgstr "Anzahl der Werktage zwischen zwei Daten"
#: templates/index.html:98
msgid "Anzeige des Wochentags eines Datums"
msgstr "Anzeige des Wochentags eines Datums"
#: templates/index.html:99
msgid "Datum plus/minus X Tage"
msgstr "Datum plus/minus X Tage"
#: templates/index.html:100
msgid "Datum plus/minus X Werktage"
msgstr "Datum plus/minus X Werktage"
#: templates/index.html:101
msgid "Datum plus/minus X Wochen/Monate"
msgstr "Datum plus/minus X Wochen/Monate"
#: templates/index.html:102
msgid "Kalenderwoche zu Datum"
msgstr "Kalenderwoche zu Datum"
#: templates/index.html:103
msgid "Start-/Enddatum einer Kalenderwoche eines Jahres"
msgstr "Start-/Enddatum einer Kalenderwoche eines Jahres"
#: templates/index.html:104
msgid "Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?"
msgstr "Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?"
#: templates/index.html:105
msgid "Aus zwei Gründen:"
msgstr "Aus zwei Gründen:"
#: templates/index.html:106
msgid "Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!"
msgstr "Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!"
#: templates/index.html:107
msgid "Das hat mich so geärgert, dass ich meinen eigenen programmiert habe."
msgstr "Das hat mich so geärgert, dass ich meinen eigenen programmiert habe."
#: templates/index.html:108
msgid "Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt (Vibe Coding)."
msgstr "Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt (Vibe Coding)."
#: templates/index.html:109
msgid "Was du noch wissen solltest"
msgstr "Was du noch wissen solltest"
#: templates/index.html:110
msgid "Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern."
msgstr "Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern."
#: templates/index.html:111
msgid "Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven."
msgstr "Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven."
#: templates/index.html:112
msgid "Den Quellcode dieser App habe ich auf"
msgstr "Den Quellcode dieser App habe ich auf"
#: templates/index.html:113
msgid "veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben."
msgstr "veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben."
#: templates/index.html:114
msgid "Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt."
msgstr "Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt."
#: templates/index.html:115
msgid "Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht."
msgstr "Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht."
#: templates/index.html:116
msgid "Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)"
msgstr "Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)"
#: templates/index.html:117
msgid "Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche"
msgstr "Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche"
#: templates/index.html:118
msgid "Hilfe-Informationen für den Datumsrechner mit Erklärungen zu allen Funktionen"
msgstr "Hilfe-Informationen für den Datumsrechner mit Erklärungen zu allen Funktionen"
#: templates/index.html:119
msgid "Dies ist ein werbe- und trackingfreier"
msgstr "Dies ist ein werbe- und trackingfreier"
#: templates/index.html:120
msgid "Open Source Datumsrechner"
msgstr "Open Source Datumsrechner"
#: templates/index.html:121
msgid "REST API Dokumentation (Swagger)"
msgstr "REST API Dokumentation (Swagger)"
#: templates/index.html:122
msgid "M. Busche"
msgstr "M. Busche"
#: app.py:123
msgid "Ungültige Eingabe"
msgstr "Ungültige Eingabe"
#: app.py:124
msgid "Nicht unterstützt: Werktage + Wochen."
msgstr "Nicht unterstützt: Werktage + Wochen."
#: app.py:125
msgid "Nicht unterstützt: Werktage + Monate."
msgstr "Nicht unterstützt: Werktage + Monate."
#: templates/index.html:126
msgid "Sprache auswählen"
msgstr "Sprache auswählen"
#: templates/index.html:127
msgid "Deutsch auswählen"
msgstr "Deutsch auswählen"
#: templates/index.html:131
msgid "English auswählen"
msgstr "English auswählen"

Binary file not shown.

View File

@@ -0,0 +1,587 @@
# English translations for Elpatrons Datumsrechner.
# Copyright (C) 2025 M. Busche
# This file is distributed under the same license as the Elpatrons Datumsrechner package.
msgid ""
msgstr ""
"Project-Id-Version: 1.3.13\n"
"Report-Msgid-Bugs-To: elpatron@mailbox.org\n"
"POT-Creation-Date: 2025-08-01 15:58+0100\n"
"PO-Revision-Date: 2025-08-01 15:58+0100\n"
"Last-Translator: M. Busche <elpatron@mailbox.org>\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: templates/index.html:12
msgid "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
msgstr "Elpatrons Date Calculator Open Source Calendar and Date Calculations"
#: templates/index.html:13
msgid "Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen barrierefrei, werbefrei, trackingfrei, kostenlos."
msgstr "Elpatrons Date Calculator: Open Source web app for calendar and date calculations. Calculate days, workdays, weeks, months, calendar weeks, weekdays and more accessible, ad-free, tracking-free, free of charge."
#: templates/index.html:14
msgid "Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, Python, Flask, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei"
msgstr "Date, Calendar, Date Calculator, Workdays, Count Days, Calendar Week, Weekday, Open Source, Python, Flask, accessibility, accessible, free, ad-free, tracking-free, cookie-free"
#: templates/index.html:15
msgid "Elpatrons Datumsrechner Open Source Kalender- und Datumsberechnungen"
msgstr "Elpatrons Date Calculator Open Source Calendar and Date Calculations"
#: templates/index.html:16
msgid "Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos."
msgstr "Open Source web app for calendar and date calculations. Ad-free, tracking-free, free of charge."
#: templates/index.html:17
msgid "website"
msgstr "website"
#: templates/index.html:18
msgid "https://codeberg.org/elpatron/datecalc"
msgstr "https://codeberg.org/elpatron/datecalc"
#: templates/index.html:19
msgid "/static/logo.svg"
msgstr "/static/logo.svg"
#: templates/index.html:20
msgid "width=device-width, initial-scale=1"
msgstr "width=device-width, initial-scale=1"
#: templates/index.html:21
msgid "true"
msgstr "true"
#: templates/index.html:22
msgid "yes"
msgstr "yes"
#: templates/index.html:23
msgid "default"
msgstr "default"
#: templates/index.html:24
msgid "telephone=no"
msgstr "telephone=no"
#: templates/index.html:25
msgid "IE=edge"
msgstr "IE=edge"
#: templates/index.html:26
msgid "Elpatrons Datumsrechner"
msgstr "Elpatrons Date Calculator"
#: templates/index.html:27
msgid "#2563eb"
msgstr "#2563eb"
#: templates/index.html:28
msgid "/static/favicon.ico"
msgstr "/static/favicon.ico"
#: templates/index.html:29
msgid "Hilfe anzeigen"
msgstr "Show Help"
#: templates/index.html:30
msgid "Hilfe anzeigen"
msgstr "Show Help"
#: templates/index.html:31
msgid "Öffnet ein Hilfefenster mit Informationen über den Datumsrechner"
msgstr "Opens a help window with information about the date calculator"
#: templates/index.html:32
msgid "Elpatrons"
msgstr "Elpatrons"
#: templates/index.html:33
msgid "Datumsrechner"
msgstr "Date Calculator"
#: templates/index.html:34
msgid "Eine <em>freie</em> Web-App: barriere<em>frei</em>, werbe<em>frei</em>, tracking<em>frei</em>, lizenz<em>frei</em> und kosten<em>frei</em>."
msgstr "A <em>free</em> web app: <em>accessible</em>, <em>ad-free</em>, <em>tracking-free</em>, <em>license-free</em> and <em>cost-free</em>."
#: templates/index.html:35
msgid "Anzahl der Tage/Werktage zwischen zwei Daten"
msgstr "Number of days/workdays between two dates"
#: templates/index.html:36
msgid "Startdatum:"
msgstr "Start date:"
#: templates/index.html:37
msgid "Heute"
msgstr "Today"
#: templates/index.html:38
msgid "Enddatum:"
msgstr "End date:"
#: templates/index.html:39
msgid "Optionen"
msgstr "Options"
#: templates/index.html:40
msgid "Nur Werktage"
msgstr "Workdays only"
#: templates/index.html:41
msgid "Feiertage berücksichtigen für:"
msgstr "Consider holidays for:"
#: templates/index.html:42
msgid "(kein Bundesland)"
msgstr "(no state)"
#: templates/index.html:43
msgid "Baden-Württemberg"
msgstr "Baden-Württemberg"
#: templates/index.html:44
msgid "Bayern"
msgstr "Bavaria"
#: templates/index.html:45
msgid "Berlin"
msgstr "Berlin"
#: templates/index.html:46
msgid "Brandenburg"
msgstr "Brandenburg"
#: templates/index.html:47
msgid "Bremen"
msgstr "Bremen"
#: templates/index.html:48
msgid "Hamburg"
msgstr "Hamburg"
#: templates/index.html:49
msgid "Hessen"
msgstr "Hesse"
#: templates/index.html:50
msgid "Mecklenburg-Vorpommern"
msgstr "Mecklenburg-Vorpommern"
#: templates/index.html:51
msgid "Niedersachsen"
msgstr "Lower Saxony"
#: templates/index.html:52
msgid "Nordrhein-Westfalen"
msgstr "North Rhine-Westphalia"
#: templates/index.html:53
msgid "Rheinland-Pfalz"
msgstr "Rhineland-Palatinate"
#: templates/index.html:54
msgid "Saarland"
msgstr "Saarland"
#: templates/index.html:55
msgid "Sachsen"
msgstr "Saxony"
#: templates/index.html:56
msgid "Sachsen-Anhalt"
msgstr "Saxony-Anhalt"
#: templates/index.html:57
msgid "Schleswig-Holstein"
msgstr "Schleswig-Holstein"
#: templates/index.html:58
msgid "Thüringen"
msgstr "Thuringia"
#: templates/index.html:59
msgid "Berechnen"
msgstr "Calculate"
#: templates/index.html:60
msgid "Ergebnis vorlesen"
msgstr "Read result aloud"
#: templates/index.html:61
msgid "Ergebnis vorlesen"
msgstr "Read result aloud"
#: templates/index.html:62
msgid "Anzahl der Werktage zwischen"
msgstr "Number of workdays between"
#: templates/index.html:63
msgid "und"
msgstr "and"
#: templates/index.html:64
msgid "(Feiertage:"
msgstr "(Holidays:"
#: templates/index.html:65
msgid "Anzahl der Tage zwischen"
msgstr "Number of days between"
#: templates/index.html:66
msgid "Davon sind"
msgstr "Of which"
#: templates/index.html:67
msgid "Tage Wochenendtage."
msgstr "days are weekend days."
#: templates/index.html:68
msgid "Feiertage (Mo-Fr,"
msgstr "holidays (Mon-Fri,"
#: templates/index.html:69
msgid "Wochentag eines Datums"
msgstr "Weekday of a date"
#: templates/index.html:70
msgid "Datum:"
msgstr "Date:"
#: templates/index.html:71
msgid "Anzeigen"
msgstr "Show"
#: templates/index.html:72
msgid "Wochentag von"
msgstr "Weekday of"
#: templates/index.html:73
msgid "Kalenderwoche eines Datums"
msgstr "Calendar week of a date"
#: templates/index.html:74
msgid "Kalenderwoche berechnen"
msgstr "Calculate calendar week"
#: templates/index.html:75
msgid "Kalenderwoche von"
msgstr "Calendar week of"
#: templates/index.html:76
msgid "Start-/Enddatum zu Kalenderwoche"
msgstr "Start/end date to calendar week"
#: templates/index.html:77
msgid "Jahr:"
msgstr "Year:"
#: templates/index.html:78
msgid "Kalenderwoche:"
msgstr "Calendar week:"
#: templates/index.html:79
msgid "Start-/Enddatum berechnen"
msgstr "Calculate start/end date"
#: templates/index.html:80
msgid "Start-/Enddatum der KW"
msgstr "Start/end date of week"
#: templates/index.html:81
msgid "im Jahr"
msgstr "in year"
#: templates/index.html:82
msgid "Datum plus/minus X Tage/Wochen/Monate"
msgstr "Date plus/minus X days/weeks/months"
#: templates/index.html:83
msgid "Anzahl:"
msgstr "Amount:"
#: templates/index.html:84
msgid "Rechenrichtung"
msgstr "Calculation direction"
#: templates/index.html:85
msgid "addieren"
msgstr "add"
#: templates/index.html:86
msgid "subtrahieren"
msgstr "subtract"
#: templates/index.html:87
msgid "Einheit und Werktage"
msgstr "Unit and workdays"
#: templates/index.html:88
msgid "Einheit:"
msgstr "Unit:"
#: templates/index.html:89
msgid "Tage"
msgstr "Days"
#: templates/index.html:90
msgid "Wochen"
msgstr "Weeks"
#: templates/index.html:91
msgid "Monate"
msgstr "Months"
#: templates/index.html:92
msgid "Nur Werktage"
msgstr "Workdays only"
#: templates/index.html:93
msgid "Hilfe schließen"
msgstr "Close help"
#: templates/index.html:94
msgid "Was ist Elpatrons Datumsrechner?"
msgstr "What is Elpatrons Date Calculator?"
#: templates/index.html:95
msgid "Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:"
msgstr "The date calculator can perform various date calculations:"
#: templates/index.html:96
msgid "Anzahl der Tage zwischen zwei Daten"
msgstr "Number of days between two dates"
#: templates/index.html:97
msgid "Anzahl der Werktage zwischen zwei Daten"
msgstr "Number of workdays between two dates"
#: templates/index.html:98
msgid "Anzeige des Wochentags eines Datums"
msgstr "Display of the weekday of a date"
#: templates/index.html:99
msgid "Datum plus/minus X Tage"
msgstr "Date plus/minus X days"
#: templates/index.html:100
msgid "Datum plus/minus X Werktage"
msgstr "Date plus/minus X workdays"
#: templates/index.html:101
msgid "Datum plus/minus X Wochen/Monate"
msgstr "Date plus/minus X weeks/months"
#: templates/index.html:102
msgid "Kalenderwoche zu Datum"
msgstr "Calendar week to date"
#: templates/index.html:103
msgid "Start-/Enddatum einer Kalenderwoche eines Jahres"
msgstr "Start/end date of a calendar week of a year"
#: templates/index.html:104
msgid "Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?"
msgstr "Online date calculators already exist in abundance, so why another one?"
#: templates/index.html:105
msgid "Aus zwei Gründen:"
msgstr "For two reasons:"
#: templates/index.html:106
msgid "Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!"
msgstr "Try to find a date calculator that isn't completely infested with ads, tracking and cookies!"
#: templates/index.html:107
msgid "Das hat mich so geärgert, dass ich meinen eigenen programmiert habe."
msgstr "This annoyed me so much that I programmed my own."
#: templates/index.html:108
msgid "Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt (Vibe Coding)."
msgstr "Actually not me myself. This app was developed largely by AI following my instructions (Vibe Coding)."
#: templates/index.html:109
msgid "Was du noch wissen solltest"
msgstr "What else you should know"
#: templates/index.html:110
msgid "Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern."
msgstr "I have tried to make the app as accessible as possible to make it easier for people with disabilities to use."
#: templates/index.html:111
msgid "Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven."
msgstr "This app doesn't spy on you, doesn't collect personal data and doesn't get on your nerves in any other way (hopefully!)."
#: templates/index.html:112
msgid "Den Quellcode dieser App habe ich auf"
msgstr "I have published the source code of this app on"
#: templates/index.html:113
msgid "veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben."
msgstr "you can view it, modify it or use it to run your own small date calculator."
#: templates/index.html:114
msgid "Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt."
msgstr "The app runs on my small home server and is currently not designed for large visitor numbers."
#: templates/index.html:115
msgid "Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht."
msgstr "I do not guarantee the functionality and calculation results. By the way, neither does the AI that programmed this."
#: templates/index.html:116
msgid "Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)"
msgstr "If you find a bug or want an additional feature, you can write me an email (see mailto link in the footer)"
#: templates/index.html:117
msgid "Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche"
msgstr "Have fun with Elpatrons Date Calculator! Yours, M. Busche"
#: templates/index.html:118
msgid "Hilfe-Informationen für den Datumsrechner mit Erklärungen zu allen Funktionen"
msgstr "Help information for the date calculator with explanations of all functions"
#: templates/index.html:119
msgid "Dies ist ein werbe- und trackingfreier"
msgstr "This is an ad- and tracking-free"
#: templates/index.html:120
msgid "Open Source Datumsrechner"
msgstr "Open Source Date Calculator"
#: templates/index.html:121
msgid "REST API Dokumentation (Swagger)"
msgstr "REST API Documentation (Swagger)"
#: templates/index.html:122
msgid "M. Busche"
msgstr "M. Busche"
#: app.py:123
msgid "Ungültige Eingabe"
msgstr "Invalid input"
#: app.py:124
msgid "Nicht unterstützt: Werktage + Wochen."
msgstr "Not supported: Workdays + weeks."
#: app.py:125
msgid "Nicht unterstützt: Werktage + Monate."
msgstr "Not supported: Workdays + months."
#: templates/index.html:126
msgid "Sprache auswählen"
msgstr "Select language"
#: templates/index.html:127
msgid "Deutsch auswählen"
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"