Compare commits
10 Commits
e5fbc14a34
...
v1.4.11
Author | SHA1 | Date | |
---|---|---|---|
f9f73e24c9 | |||
d697928241 | |||
f998f7fff8 | |||
1a5aa003a2 | |||
eecc2b8b73 | |||
0b13a408cd | |||
c4a65bba48 | |||
e4b37d9261 | |||
45cc02b4b0 | |||
05766d9a97 |
@@ -334,6 +334,7 @@ curl -X POST http://localhost:5000/api/plusminus \
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Hinweis:**
|
**Hinweis:**
|
||||||
|
|
||||||
- `"einheit"`: `"tage"`, `"wochen"` oder `"monate"`
|
- `"einheit"`: `"tage"`, `"wochen"` oder `"monate"`
|
||||||
- `"richtung"`: `"add"` (plus) oder `"sub"` (minus)
|
- `"richtung"`: `"add"` (plus) oder `"sub"` (minus)
|
||||||
- `"werktage"`: `true` für Werktage, sonst `false` (nur bei `"tage"` unterstützt)
|
- `"werktage"`: `true` für Werktage, sonst `false` (nur bei `"tage"` unterstützt)
|
||||||
|
86
app.py
86
app.py
@@ -1,4 +1,4 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify, g
|
from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify, g, make_response
|
||||||
from flask_babel import Babel, gettext, ngettext, get_locale
|
from flask_babel import Babel, gettext, ngettext, get_locale
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -20,7 +20,23 @@ app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
|
|||||||
babel = Babel()
|
babel = Babel()
|
||||||
|
|
||||||
# Version der App
|
# Version der App
|
||||||
APP_VERSION = "1.4.6"
|
APP_VERSION = "1.4.11"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@@ -229,9 +245,10 @@ def index():
|
|||||||
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}"
|
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}"
|
||||||
except Exception:
|
except Exception:
|
||||||
plusminus_result = gettext('Ungültige Eingabe')
|
plusminus_result = gettext('Ungültige Eingabe')
|
||||||
return render_template('index.html', tage=tage, werktage=werktage, wochentag=wochentag, plusminus_result=plusminus_result, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx
|
response = make_response(render_template('index.html', tage=tage, werktage=werktage, wochentag=wochentag, plusminus_result=plusminus_result, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx
|
||||||
, feiertage_anzahl=feiertage_anzahl, wochenendtage_anzahl=wochenendtage_anzahl, app_version=APP_VERSION, get_locale=get_locale
|
, feiertage_anzahl=feiertage_anzahl, wochenendtage_anzahl=wochenendtage_anzahl, app_version=APP_VERSION, get_locale=get_locale
|
||||||
)
|
))
|
||||||
|
return add_cache_headers(response)
|
||||||
|
|
||||||
|
|
||||||
def parse_log_stats(log_path):
|
def parse_log_stats(log_path):
|
||||||
@@ -304,11 +321,14 @@ 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)
|
||||||
|
response = make_response(render_template('stats_login.html', error=None))
|
||||||
|
return add_cache_headers(response)
|
||||||
log_path = os.path.join('log', 'pageviews.log')
|
log_path = os.path.join('log', 'pageviews.log')
|
||||||
pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly = parse_log_stats(log_path)
|
pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly = parse_log_stats(log_path)
|
||||||
return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, func_counts_hourly=func_counts_hourly, impressions_per_day=impressions_per_day, impressions_per_hour=impressions_per_hour, api_counts=api_counts, api_counts_hourly=api_counts_hourly)
|
response = make_response(render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, func_counts_hourly=func_counts_hourly, impressions_per_day=impressions_per_day, impressions_per_hour=impressions_per_hour, api_counts=api_counts, api_counts_hourly=api_counts_hourly))
|
||||||
|
return add_cache_headers(response)
|
||||||
|
|
||||||
# --- REST API ---
|
# --- REST API ---
|
||||||
def log_api_usage(api_name):
|
def log_api_usage(api_name):
|
||||||
@@ -342,7 +362,8 @@ def api_tage_werktage():
|
|||||||
tage = int(np.busday_count(d1.date(), (d2 + timedelta(days=1)).date(), holidays=holidays))
|
tage = int(np.busday_count(d1.date(), (d2 + timedelta(days=1)).date(), holidays=holidays))
|
||||||
else:
|
else:
|
||||||
tage = abs((d2 - d1).days)
|
tage = abs((d2 - d1).days)
|
||||||
return jsonify({'result': tage})
|
response = jsonify({'result': tage})
|
||||||
|
return add_cache_headers(response)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
||||||
|
|
||||||
@@ -355,7 +376,8 @@ def api_wochentag():
|
|||||||
d = datetime.strptime(datum, '%Y-%m-%d')
|
d = datetime.strptime(datum, '%Y-%m-%d')
|
||||||
wochentage = get_wochentage()
|
wochentage = get_wochentage()
|
||||||
wochentag = wochentage[d.weekday()]
|
wochentag = wochentage[d.weekday()]
|
||||||
return jsonify({'result': wochentag})
|
response = jsonify({'result': wochentag})
|
||||||
|
return add_cache_headers(response)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
||||||
|
|
||||||
@@ -372,7 +394,8 @@ def api_kw_berechnen():
|
|||||||
kw_berechnen = f"Week {kw} ({d.year})"
|
kw_berechnen = f"Week {kw} ({d.year})"
|
||||||
else:
|
else:
|
||||||
kw_berechnen = f"KW {kw} ({d.year})"
|
kw_berechnen = f"KW {kw} ({d.year})"
|
||||||
return jsonify({'result': kw_berechnen, 'kw': kw, 'jahr': d.year})
|
response = jsonify({'result': kw_berechnen, 'kw': kw, 'jahr': d.year})
|
||||||
|
return add_cache_headers(response)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
||||||
|
|
||||||
@@ -392,7 +415,8 @@ def api_kw_datum():
|
|||||||
kw_datum = f"{start.strftime('%m/%d/%Y')} to {end.strftime('%m/%d/%Y')}"
|
kw_datum = f"{start.strftime('%m/%d/%Y')} to {end.strftime('%m/%d/%Y')}"
|
||||||
else:
|
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')}"
|
||||||
return jsonify({'result': kw_datum, 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')})
|
response = jsonify({'result': kw_datum, 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')})
|
||||||
|
return add_cache_headers(response)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
||||||
|
|
||||||
@@ -415,34 +439,26 @@ def api_plusminus():
|
|||||||
if is_werktage:
|
if is_werktage:
|
||||||
result = np.busday_offset(d.date(), anzahl_int, roll='forward')
|
result = np.busday_offset(d.date(), anzahl_int, roll='forward')
|
||||||
result_dt = datetime.strptime(str(result), '%Y-%m-%d')
|
result_dt = datetime.strptime(str(result), '%Y-%m-%d')
|
||||||
if locale == 'en':
|
response = jsonify({'result': result_dt.strftime('%Y-%m-%d')})
|
||||||
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')}"
|
return add_cache_headers(response)
|
||||||
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:
|
else:
|
||||||
result = d + timedelta(days=anzahl_int)
|
result = d + timedelta(days=anzahl_int)
|
||||||
if locale == 'en':
|
response = jsonify({'result': result.strftime('%Y-%m-%d')})
|
||||||
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')}"
|
return add_cache_headers(response)
|
||||||
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':
|
elif einheit == 'wochen':
|
||||||
if is_werktage:
|
if is_werktage:
|
||||||
return jsonify({'error': 'Nicht unterstützt: Werktage + Wochen.'}), 400
|
return jsonify({'error': 'Nicht unterstützt: Werktage + Wochen.'}), 400
|
||||||
else:
|
else:
|
||||||
result = d + timedelta(weeks=anzahl_int)
|
result = d + timedelta(weeks=anzahl_int)
|
||||||
if locale == 'en':
|
response = jsonify({'result': result.strftime('%Y-%m-%d')})
|
||||||
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')}"
|
return add_cache_headers(response)
|
||||||
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':
|
elif einheit == 'monate':
|
||||||
if is_werktage:
|
if is_werktage:
|
||||||
return jsonify({'error': 'Nicht unterstützt: Werktage + Monate.'}), 400
|
return jsonify({'error': 'Nicht unterstützt: Werktage + Monate.'}), 400
|
||||||
else:
|
else:
|
||||||
result = d + relativedelta(months=anzahl_int)
|
result = d + relativedelta(months=anzahl_int)
|
||||||
if locale == 'en':
|
response = jsonify({'result': result.strftime('%Y-%m-%d')})
|
||||||
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')}"
|
return add_cache_headers(response)
|
||||||
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')}"
|
|
||||||
else:
|
else:
|
||||||
return jsonify({'error': 'Ungültige Einheit'}), 400
|
return jsonify({'error': 'Ungültige Einheit'}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -451,8 +467,14 @@ def api_plusminus():
|
|||||||
@app.route('/api/stats', methods=['GET'])
|
@app.route('/api/stats', methods=['GET'])
|
||||||
def api_stats():
|
def api_stats():
|
||||||
log_path = os.path.join('log', 'pageviews.log')
|
log_path = os.path.join('log', 'pageviews.log')
|
||||||
pageviews, func_counts, impressions_per_day, api_counts = parse_log_stats(log_path)
|
pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly = parse_log_stats(log_path)
|
||||||
return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, impressions_per_day=impressions_per_day, api_counts=api_counts)
|
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'])
|
@app.route('/api/monitor', methods=['GET'])
|
||||||
def api_monitor():
|
def api_monitor():
|
||||||
@@ -464,17 +486,19 @@ def api_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')
|
@app.route('/api-docs')
|
||||||
def api_docs():
|
def api_docs():
|
||||||
return render_template('swagger.html')
|
response = make_response(render_template('swagger.html'))
|
||||||
|
return add_cache_headers(response)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -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',
|
'/static/favicon.ico',
|
||||||
'/static/favicon.png',
|
'/static/favicon.png',
|
||||||
|
'/static/favicon.svg',
|
||||||
'/static/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 => {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
.v{
|
{
|
||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Elpatrons Datumsrechner API",
|
"title": "Elpatrons Datumsrechner API",
|
||||||
|
@@ -6,12 +6,12 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="de">
|
<html lang="{{ get_locale() }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{ _('Elpatrons Datumsrechner – Open Source Kalender- und Datumsberechnungen') }}</title>
|
<title>{{ _('Elpatrons Datumsrechner – Open Source Kalender- und Datumsberechnungen') }}</title>
|
||||||
<meta name="description" content="{{ _('Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen – barrierefrei, werbefrei, trackingfrei, kostenlos.') }}">
|
<meta name="description" content="{{ _('Elpatrons Datumsrechner: Open Source Web-App für Kalender- und Datumsberechnungen. Tage, Werktage, Wochen, Monate, Kalenderwochen, Wochentage und mehr berechnen – barrierefrei, werbefrei, trackingfrei, kostenlos.') }}">
|
||||||
<meta name="keywords" content="{{ _('Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, Python, Flask, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei') }}">
|
<meta name="keywords" content="{{ _('Datum, Kalender, Datumsrechner, Werktage, Tage zählen, Kalenderwoche, Wochentag, Open Source, barrierefreiheit, barrierefrei, kostenlos, werbefrei, trackingfrei, cookiefrei, progressive web app, pwa') }}">
|
||||||
<meta property="og:title" content="{{ _('Elpatrons Datumsrechner – Open Source Kalender- und Datumsberechnungen') }}">
|
<meta property="og:title" content="{{ _('Elpatrons Datumsrechner – Open Source Kalender- und Datumsberechnungen') }}">
|
||||||
<meta property="og:description" content="{{ _('Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos.') }}">
|
<meta property="og:description" content="{{ _('Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos.') }}">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
@@ -683,7 +683,8 @@ footer br + a {
|
|||||||
function readAloud(text, button) {
|
function readAloud(text, button) {
|
||||||
// Stoppe vorherige Wiedergabe
|
// Stoppe vorherige Wiedergabe
|
||||||
if (currentSpeech) {
|
if (currentSpeech) {
|
||||||
currentSpeech.cancel();
|
speechSynthesis.cancel();
|
||||||
|
currentSpeech = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entferne "playing" Klasse von allen Buttons
|
// Entferne "playing" Klasse von allen Buttons
|
||||||
@@ -692,12 +693,92 @@ footer br + a {
|
|||||||
btn.textContent = '🔊';
|
btn.textContent = '🔊';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bestimme die aktuelle Sprache
|
||||||
|
let currentLang = 'de-DE'; // Standard
|
||||||
|
|
||||||
|
// Methode 1: Prüfe URL-Parameter
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const langParam = urlParams.get('lang');
|
||||||
|
|
||||||
|
// Methode 2: Prüfe localStorage
|
||||||
|
const savedLang = localStorage.getItem('preferred_language');
|
||||||
|
|
||||||
|
// Methode 3: Prüfe das HTML lang-Attribut
|
||||||
|
const htmlLang = document.documentElement.lang;
|
||||||
|
|
||||||
|
// Debug-Ausgabe
|
||||||
|
console.log('URL lang param:', langParam);
|
||||||
|
console.log('Saved lang:', savedLang);
|
||||||
|
console.log('HTML lang:', htmlLang);
|
||||||
|
|
||||||
|
// Verbesserte Spracherkennung - prüfe alle Quellen
|
||||||
|
if (langParam === 'en' || savedLang === 'en' || htmlLang === 'en') {
|
||||||
|
// Prüfe, ob eine britische Stimme verfügbar ist
|
||||||
|
const voices = speechSynthesis.getVoices();
|
||||||
|
const hasBritishVoice = voices.some(voice => voice.lang === 'en-GB');
|
||||||
|
const hasAmericanVoice = voices.some(voice => voice.lang === 'en-US');
|
||||||
|
|
||||||
|
if (hasBritishVoice) {
|
||||||
|
currentLang = 'en-GB';
|
||||||
|
console.log('Setting language to British English:', currentLang);
|
||||||
|
} else if (hasAmericanVoice) {
|
||||||
|
currentLang = 'en-US';
|
||||||
|
console.log('Setting language to American English:', currentLang);
|
||||||
|
} else {
|
||||||
|
// Fallback auf en-US, auch wenn keine Stimme verfügbar ist
|
||||||
|
currentLang = 'en-US';
|
||||||
|
console.log('Setting language to English (no specific voice available):', currentLang);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Setting language to German:', currentLang);
|
||||||
|
}
|
||||||
|
|
||||||
// Erstelle neue Sprachausgabe
|
// Erstelle neue Sprachausgabe
|
||||||
currentSpeech = new SpeechSynthesisUtterance(text);
|
currentSpeech = new SpeechSynthesisUtterance(text);
|
||||||
currentSpeech.lang = 'de-DE';
|
currentSpeech.lang = currentLang;
|
||||||
currentSpeech.rate = 0.9;
|
currentSpeech.rate = 0.9;
|
||||||
currentSpeech.pitch = 1;
|
currentSpeech.pitch = 1;
|
||||||
|
|
||||||
|
// Versuche eine passende Stimme zu finden
|
||||||
|
const voices = speechSynthesis.getVoices();
|
||||||
|
console.log('Available voices:', voices.map(v => `${v.name} (${v.lang})`));
|
||||||
|
|
||||||
|
// Suche nach einer Stimme in der gewünschten Sprache
|
||||||
|
let preferredVoice = voices.find(voice =>
|
||||||
|
voice.lang === currentLang
|
||||||
|
);
|
||||||
|
|
||||||
|
// Falls keine exakte Übereinstimmung, suche nach ähnlicher Sprache
|
||||||
|
if (!preferredVoice) {
|
||||||
|
preferredVoice = voices.find(voice =>
|
||||||
|
voice.lang.startsWith(currentLang.split('-')[0])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls immer noch keine Stimme gefunden, suche nach englischen Stimmen für Englisch
|
||||||
|
if (!preferredVoice && (currentLang === 'en-US' || currentLang === 'en-GB')) {
|
||||||
|
// Bevorzuge britische Stimmen für en-GB
|
||||||
|
if (currentLang === 'en-GB') {
|
||||||
|
preferredVoice = voices.find(voice =>
|
||||||
|
voice.lang === 'en-GB' || voice.name.toLowerCase().includes('british')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls keine britische Stimme, suche nach amerikanischen oder allgemeinen englischen Stimmen
|
||||||
|
if (!preferredVoice) {
|
||||||
|
preferredVoice = voices.find(voice =>
|
||||||
|
voice.lang.includes('en') || voice.name.toLowerCase().includes('english')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferredVoice) {
|
||||||
|
currentSpeech.voice = preferredVoice;
|
||||||
|
console.log('Using voice:', preferredVoice.name, 'for language:', currentLang);
|
||||||
|
} else {
|
||||||
|
console.log('No specific voice found for language:', currentLang, '- using default');
|
||||||
|
}
|
||||||
|
|
||||||
// Button-Status aktualisieren
|
// Button-Status aktualisieren
|
||||||
button.classList.add('playing');
|
button.classList.add('playing');
|
||||||
button.textContent = '⏹️';
|
button.textContent = '⏹️';
|
||||||
@@ -720,6 +801,16 @@ footer br + a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readAloudFromElement(button) {
|
function readAloudFromElement(button) {
|
||||||
|
// Prüfe, ob bereits eine Wiedergabe läuft
|
||||||
|
if (currentSpeech && speechSynthesis.speaking) {
|
||||||
|
// Stoppe die aktuelle Wiedergabe
|
||||||
|
speechSynthesis.cancel();
|
||||||
|
currentSpeech = null;
|
||||||
|
button.classList.remove('playing');
|
||||||
|
button.textContent = '🔊';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Finde das Ergebnis-Element (das div mit class="result")
|
// Finde das Ergebnis-Element (das div mit class="result")
|
||||||
const resultElement = button.closest('.result');
|
const resultElement = button.closest('.result');
|
||||||
if (!resultElement) return;
|
if (!resultElement) return;
|
||||||
@@ -736,7 +827,7 @@ footer br + a {
|
|||||||
|
|
||||||
function stopReading() {
|
function stopReading() {
|
||||||
if (currentSpeech) {
|
if (currentSpeech) {
|
||||||
currentSpeech.cancel();
|
speechSynthesis.cancel();
|
||||||
currentSpeech = null;
|
currentSpeech = null;
|
||||||
}
|
}
|
||||||
document.querySelectorAll('.read-aloud-btn').forEach(btn => {
|
document.querySelectorAll('.read-aloud-btn').forEach(btn => {
|
||||||
@@ -746,6 +837,13 @@ footer br + a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Stelle sicher, dass die Stimmen geladen sind
|
||||||
|
if (speechSynthesis.onvoiceschanged !== undefined) {
|
||||||
|
speechSynthesis.onvoiceschanged = function() {
|
||||||
|
console.log('Voices loaded:', speechSynthesis.getVoices().length);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfe localStorage für gespeicherte Sprachauswahl
|
// Prüfe localStorage für gespeicherte Sprachauswahl
|
||||||
const savedLanguage = localStorage.getItem('preferred_language');
|
const savedLanguage = localStorage.getItem('preferred_language');
|
||||||
if (savedLanguage && !window.location.search.includes('lang=')) {
|
if (savedLanguage && !window.location.search.includes('lang=')) {
|
||||||
@@ -881,7 +979,7 @@ footer br + a {
|
|||||||
</form>
|
</form>
|
||||||
{% if tage is not none %}
|
{% if tage is not none %}
|
||||||
<div class="result" aria-live="polite">
|
<div class="result" aria-live="polite">
|
||||||
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="Ergebnis vorlesen" title="Ergebnis vorlesen" tabindex="0">🔊</button>
|
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0">🔊</button>
|
||||||
{% if request.form.get('werktage') %}
|
{% if request.form.get('werktage') %}
|
||||||
{{ _('Anzahl der Werktage zwischen') }} <b>{{ format_date(request.form.get('start1', '')) }}</b> {{ _('und') }} <b>{{ format_date(request.form.get('end1', '')) }}:</b>{% if request.form.get('bundesland') %} {{ _('(Feiertage:') }} {{ request.form.get('bundesland') }}){% endif %}: {{ tage }}
|
{{ _('Anzahl der Werktage zwischen') }} <b>{{ format_date(request.form.get('start1', '')) }}</b> {{ _('und') }} <b>{{ format_date(request.form.get('end1', '')) }}:</b>{% if request.form.get('bundesland') %} {{ _('(Feiertage:') }} {{ request.form.get('bundesland') }}){% endif %}: {{ tage }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -923,7 +1021,7 @@ footer br + a {
|
|||||||
</form>
|
</form>
|
||||||
{% if wochentag is not none %}
|
{% if wochentag is not none %}
|
||||||
<div class="result" aria-live="polite">
|
<div class="result" aria-live="polite">
|
||||||
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="Ergebnis vorlesen" title="Ergebnis vorlesen" tabindex="0">🔊</button>
|
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0">🔊</button>
|
||||||
{{ _('Wochentag von') }} <b>{{ format_date(request.form.get('datum3', '')) }}</b>: {{ wochentag }}
|
{{ _('Wochentag von') }} <b>{{ format_date(request.form.get('datum3', '')) }}</b>: {{ wochentag }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -949,7 +1047,7 @@ footer br + a {
|
|||||||
</form>
|
</form>
|
||||||
{% if kw_berechnen is not none %}
|
{% if kw_berechnen is not none %}
|
||||||
<div class="result" aria-live="polite">
|
<div class="result" aria-live="polite">
|
||||||
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="Ergebnis vorlesen" title="Ergebnis vorlesen" tabindex="0">🔊</button>
|
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0">🔊</button>
|
||||||
{{ _('Kalenderwoche von') }} <b>{{ format_date(request.form.get('datum6', '')) }}</b>: {{ kw_berechnen }}
|
{{ _('Kalenderwoche von') }} <b>{{ format_date(request.form.get('datum6', '')) }}</b>: {{ kw_berechnen }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -975,7 +1073,7 @@ footer br + a {
|
|||||||
</form>
|
</form>
|
||||||
{% if kw_datum is not none %}
|
{% if kw_datum is not none %}
|
||||||
<div class="result" aria-live="polite">
|
<div class="result" aria-live="polite">
|
||||||
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="Ergebnis vorlesen" title="Ergebnis vorlesen" tabindex="0">🔊</button>
|
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0">🔊</button>
|
||||||
{{ _('Start-/Enddatum der KW') }} <b>{{ request.form.get('kw7', '') }}</b> {{ _('im Jahr') }} <b>{{ request.form.get('jahr7', '') }}</b>: {{ kw_datum }}
|
{{ _('Start-/Enddatum der KW') }} <b>{{ request.form.get('kw7', '') }}</b> {{ _('im Jahr') }} <b>{{ request.form.get('jahr7', '') }}</b>: {{ kw_datum }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -1021,7 +1119,7 @@ footer br + a {
|
|||||||
</form>
|
</form>
|
||||||
{% if plusminus_result is not none %}
|
{% if plusminus_result is not none %}
|
||||||
<div class="result" aria-live="polite">
|
<div class="result" aria-live="polite">
|
||||||
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="Ergebnis vorlesen" title="Ergebnis vorlesen" tabindex="0">🔊</button>
|
<button type="button" class="read-aloud-btn" onclick="readAloudFromElement(this)" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); readAloudFromElement(this); }" aria-label="{{ _('Ergebnis vorlesen') }}" title="{{ _('Ergebnis vorlesen') }}" tabindex="0">🔊</button>
|
||||||
{{ plusminus_result }}
|
{{ plusminus_result }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -1081,9 +1179,9 @@ footer br + a {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer style="text-align:center; margin-top:2em; color:#475569; font-size:0.98em; padding-bottom:1.5em;">
|
<footer style="text-align:center; margin-top:2em; color:#475569; font-size:0.98em; padding-bottom:1.5em;">
|
||||||
Dies ist ein werbe- und trackingfreier <a href="https://codeberg.org/elpatron/datecalc/src/branch/main/README.md" target="_blank" style="color:#1e40af; text-decoration:underline;">Open Source Datumsrechner</a><br>
|
Dies ist ein werbe- und trackingfreier <a href="https://codeberg.org/elpatron/datecalc/src/branch/main/README.md" target="_blank" style="color:#1e40af; text-decoration:underline;">Open Source</a> Datumsrechner<br>
|
||||||
<a href="/api-docs" target="_blank" style="color:#1e40af; text-decoration:underline;">REST API Dokumentation (Swagger)</a><br>
|
<a href="/api-docs" target="_blank" style="color:#1e40af; text-decoration:underline;">REST API Dokumentation (Swagger)</a><br>
|
||||||
© 2025 <a href="mailto:elpatron@mailbox.org?subject=Datumsrechner" style="color:#1e40af; text-decoration:underline;">M. Busche</a>
|
© 2025 <a href="mailto:elpatron@mailbox.org?subject=Datumsrechner" style="color:#1e40af; text-decoration:underline;">Markus Busche</a>
|
||||||
<div style="margin-top:0.5em; font-size:0.85em; color:#64748b;">v{{ app_version }}</div>
|
<div style="margin-top:0.5em; font-size:0.85em; color:#64748b;">v{{ app_version }}</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script>
|
<script>
|
||||||
|
@@ -221,10 +221,11 @@ def test_api_plusminus(client):
|
|||||||
def test_api_stats(client):
|
def test_api_stats(client):
|
||||||
resp = client.get('/api/stats')
|
resp = client.get('/api/stats')
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
# Die Route gibt HTML zurück, nicht JSON
|
data = resp.get_json()
|
||||||
html = resp.data.decode('utf-8')
|
assert "pageviews" in data
|
||||||
# Prüfe auf typische HTML-Elemente des Dashboards
|
assert "func_counts" in data
|
||||||
assert 'Statistik-Dashboard' in html or 'Dashboard' in html
|
assert "impressions_per_day" in data
|
||||||
|
assert "api_counts" in data
|
||||||
|
|
||||||
def test_api_monitor(client):
|
def test_api_monitor(client):
|
||||||
resp = client.get('/api/monitor')
|
resp = client.get('/api/monitor')
|
||||||
|
Reference in New Issue
Block a user