Compare commits
9 Commits
05766d9a97
...
v1.4.11
Author | SHA1 | Date | |
---|---|---|---|
f9f73e24c9 | |||
d697928241 | |||
f998f7fff8 | |||
1a5aa003a2 | |||
eecc2b8b73 | |||
0b13a408cd | |||
c4a65bba48 | |||
e4b37d9261 | |||
45cc02b4b0 |
@@ -334,6 +334,7 @@ curl -X POST http://localhost:5000/api/plusminus \
|
||||
```
|
||||
|
||||
**Hinweis:**
|
||||
|
||||
- `"einheit"`: `"tage"`, `"wochen"` oder `"monate"`
|
||||
- `"richtung"`: `"add"` (plus) oder `"sub"` (minus)
|
||||
- `"werktage"`: `true` für Werktage, sonst `false` (nur bei `"tage"` unterstützt)
|
||||
|
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 datetime import datetime, timedelta
|
||||
import numpy as np
|
||||
@@ -20,7 +20,23 @@ app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
|
||||
babel = Babel()
|
||||
|
||||
# Version der App
|
||||
APP_VERSION = "1.4.7"
|
||||
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
|
||||
|
||||
@@ -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')}"
|
||||
except Exception:
|
||||
plusminus_result = gettext('Ungültige Eingabe')
|
||||
return render_template('index.html', tage=tage, werktage=werktage, wochentag=wochentag, plusminus_result=plusminus_result, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx
|
||||
response = make_response(render_template('index.html', tage=tage, 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
|
||||
)
|
||||
))
|
||||
return add_cache_headers(response)
|
||||
|
||||
|
||||
def parse_log_stats(log_path):
|
||||
@@ -304,11 +321,14 @@ def stats():
|
||||
session['stats_auth'] = True
|
||||
return redirect(url_for('stats'))
|
||||
else:
|
||||
return render_template('stats_login.html', error='Falsches Passwort!')
|
||||
return render_template('stats_login.html', error=None)
|
||||
response = make_response(render_template('stats_login.html', error='Falsches Passwort!'))
|
||||
return add_cache_headers(response)
|
||||
response = make_response(render_template('stats_login.html', error=None))
|
||||
return add_cache_headers(response)
|
||||
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)
|
||||
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 ---
|
||||
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))
|
||||
else:
|
||||
tage = abs((d2 - d1).days)
|
||||
return jsonify({'result': tage})
|
||||
response = jsonify({'result': tage})
|
||||
return add_cache_headers(response)
|
||||
except Exception as e:
|
||||
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
||||
|
||||
@@ -355,7 +376,8 @@ def api_wochentag():
|
||||
d = datetime.strptime(datum, '%Y-%m-%d')
|
||||
wochentage = get_wochentage()
|
||||
wochentag = wochentage[d.weekday()]
|
||||
return jsonify({'result': wochentag})
|
||||
response = jsonify({'result': wochentag})
|
||||
return add_cache_headers(response)
|
||||
except Exception as e:
|
||||
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
||||
|
||||
@@ -372,7 +394,8 @@ def api_kw_berechnen():
|
||||
kw_berechnen = f"Week {kw} ({d.year})"
|
||||
else:
|
||||
kw_berechnen = f"KW {kw} ({d.year})"
|
||||
return jsonify({'result': kw_berechnen, 'kw': kw, 'jahr': d.year})
|
||||
response = jsonify({'result': kw_berechnen, 'kw': kw, 'jahr': d.year})
|
||||
return add_cache_headers(response)
|
||||
except Exception as e:
|
||||
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
||||
|
||||
@@ -392,7 +415,8 @@ def api_kw_datum():
|
||||
kw_datum = f"{start.strftime('%m/%d/%Y')} to {end.strftime('%m/%d/%Y')}"
|
||||
else:
|
||||
kw_datum = f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}"
|
||||
return jsonify({'result': kw_datum, 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')})
|
||||
response = jsonify({'result': kw_datum, 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')})
|
||||
return add_cache_headers(response)
|
||||
except Exception as e:
|
||||
return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400
|
||||
|
||||
@@ -415,34 +439,26 @@ def api_plusminus():
|
||||
if is_werktage:
|
||||
result = np.busday_offset(d.date(), anzahl_int, roll='forward')
|
||||
result_dt = datetime.strptime(str(result), '%Y-%m-%d')
|
||||
if locale == 'en':
|
||||
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} workdays: {result_dt.strftime('%m/%d/%Y')}"
|
||||
else:
|
||||
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Werktage: {result_dt.strftime('%d.%m.%Y')}"
|
||||
response = jsonify({'result': result_dt.strftime('%Y-%m-%d')})
|
||||
return add_cache_headers(response)
|
||||
else:
|
||||
result = d + timedelta(days=anzahl_int)
|
||||
if locale == 'en':
|
||||
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} days: {result.strftime('%m/%d/%Y')}"
|
||||
else:
|
||||
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Tage: {result.strftime('%d.%m.%Y')}"
|
||||
response = jsonify({'result': result.strftime('%Y-%m-%d')})
|
||||
return add_cache_headers(response)
|
||||
elif einheit == 'wochen':
|
||||
if is_werktage:
|
||||
return jsonify({'error': 'Nicht unterstützt: Werktage + Wochen.'}), 400
|
||||
else:
|
||||
result = d + timedelta(weeks=anzahl_int)
|
||||
if locale == 'en':
|
||||
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} weeks: {result.strftime('%m/%d/%Y')}"
|
||||
else:
|
||||
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Wochen: {result.strftime('%d.%m.%Y')}"
|
||||
response = jsonify({'result': result.strftime('%Y-%m-%d')})
|
||||
return add_cache_headers(response)
|
||||
elif einheit == 'monate':
|
||||
if is_werktage:
|
||||
return jsonify({'error': 'Nicht unterstützt: Werktage + Monate.'}), 400
|
||||
else:
|
||||
result = d + relativedelta(months=anzahl_int)
|
||||
if locale == 'en':
|
||||
plusminus_result = f"Date {d.strftime('%m/%d/%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} months: {result.strftime('%m/%d/%Y')}"
|
||||
else:
|
||||
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}"
|
||||
response = jsonify({'result': result.strftime('%Y-%m-%d')})
|
||||
return add_cache_headers(response)
|
||||
else:
|
||||
return jsonify({'error': 'Ungültige Einheit'}), 400
|
||||
except Exception as e:
|
||||
@@ -451,8 +467,14 @@ def api_plusminus():
|
||||
@app.route('/api/stats', methods=['GET'])
|
||||
def api_stats():
|
||||
log_path = os.path.join('log', 'pageviews.log')
|
||||
pageviews, func_counts, impressions_per_day, api_counts = parse_log_stats(log_path)
|
||||
return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, impressions_per_day=impressions_per_day, api_counts=api_counts)
|
||||
pageviews, func_counts, func_counts_hourly, impressions_per_day, impressions_per_hour, api_counts, api_counts_hourly = parse_log_stats(log_path)
|
||||
response = jsonify({
|
||||
"pageviews": pageviews,
|
||||
"func_counts": func_counts,
|
||||
"impressions_per_day": impressions_per_day,
|
||||
"api_counts": api_counts
|
||||
})
|
||||
return add_cache_headers(response)
|
||||
|
||||
@app.route('/api/monitor', methods=['GET'])
|
||||
def api_monitor():
|
||||
@@ -464,17 +486,19 @@ def api_monitor():
|
||||
if 'PAGEVIEW' in line:
|
||||
pageviews += 1
|
||||
uptime = int(time.time() - app_start_time)
|
||||
return jsonify({
|
||||
response = jsonify({
|
||||
"status": "ok",
|
||||
"message": "App running",
|
||||
"time": datetime.now().isoformat(),
|
||||
"uptime_seconds": uptime,
|
||||
"pageviews_last_7_days": pageviews
|
||||
})
|
||||
return add_cache_headers(response)
|
||||
|
||||
@app.route('/api-docs')
|
||||
def api_docs():
|
||||
return render_template('swagger.html')
|
||||
response = make_response(render_template('swagger.html'))
|
||||
return add_cache_headers(response)
|
||||
|
||||
|
||||
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 urlsToCache = [
|
||||
'/',
|
||||
'/static/style.css',
|
||||
'/static/favicon.ico',
|
||||
'/static/favicon.png',
|
||||
'/static/favicon.svg',
|
||||
'/static/logo.svg',
|
||||
'/static/manifest.json',
|
||||
];
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => cache.addAll(urlsToCache))
|
||||
.then(cache => {
|
||||
// Füge nur existierende Dateien zum Cache hinzu
|
||||
return Promise.allSettled(
|
||||
urlsToCache.map(url =>
|
||||
cache.add(url).catch(err => {
|
||||
console.log('Failed to cache:', url, err);
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
self.addEventListener('fetch', event => {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
.v{
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "Elpatrons Datumsrechner API",
|
||||
|
@@ -6,12 +6,12 @@
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<html lang="{{ get_locale() }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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="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:description" content="{{ _('Open Source Web-App für Kalender- und Datumsberechnungen. Werbefrei, trackingfrei, kostenlos.') }}">
|
||||
<meta property="og:type" content="website">
|
||||
@@ -683,7 +683,8 @@ footer br + a {
|
||||
function readAloud(text, button) {
|
||||
// Stoppe vorherige Wiedergabe
|
||||
if (currentSpeech) {
|
||||
currentSpeech.cancel();
|
||||
speechSynthesis.cancel();
|
||||
currentSpeech = null;
|
||||
}
|
||||
|
||||
// Entferne "playing" Klasse von allen Buttons
|
||||
@@ -692,12 +693,92 @@ footer br + a {
|
||||
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
|
||||
currentSpeech = new SpeechSynthesisUtterance(text);
|
||||
currentSpeech.lang = 'de-DE';
|
||||
currentSpeech.lang = currentLang;
|
||||
currentSpeech.rate = 0.9;
|
||||
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.classList.add('playing');
|
||||
button.textContent = '⏹️';
|
||||
@@ -720,6 +801,16 @@ footer br + a {
|
||||
}
|
||||
|
||||
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")
|
||||
const resultElement = button.closest('.result');
|
||||
if (!resultElement) return;
|
||||
@@ -736,7 +827,7 @@ footer br + a {
|
||||
|
||||
function stopReading() {
|
||||
if (currentSpeech) {
|
||||
currentSpeech.cancel();
|
||||
speechSynthesis.cancel();
|
||||
currentSpeech = null;
|
||||
}
|
||||
document.querySelectorAll('.read-aloud-btn').forEach(btn => {
|
||||
@@ -746,6 +837,13 @@ footer br + a {
|
||||
}
|
||||
|
||||
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
|
||||
const savedLanguage = localStorage.getItem('preferred_language');
|
||||
if (savedLanguage && !window.location.search.includes('lang=')) {
|
||||
@@ -881,7 +979,7 @@ footer br + a {
|
||||
</form>
|
||||
{% if tage is not none %}
|
||||
<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') %}
|
||||
{{ _('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 %}
|
||||
@@ -923,7 +1021,7 @@ footer br + a {
|
||||
</form>
|
||||
{% if wochentag is not none %}
|
||||
<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 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -949,7 +1047,7 @@ footer br + a {
|
||||
</form>
|
||||
{% if kw_berechnen is not none %}
|
||||
<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 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -975,7 +1073,7 @@ footer br + a {
|
||||
</form>
|
||||
{% if kw_datum is not none %}
|
||||
<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 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1021,7 +1119,7 @@ footer br + a {
|
||||
</form>
|
||||
{% if plusminus_result is not none %}
|
||||
<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 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1081,9 +1179,9 @@ footer br + a {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
© 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>
|
||||
</footer>
|
||||
<script>
|
||||
|
@@ -221,10 +221,11 @@ def test_api_plusminus(client):
|
||||
def test_api_stats(client):
|
||||
resp = client.get('/api/stats')
|
||||
assert resp.status_code == 200
|
||||
# Die Route gibt HTML zurück, nicht JSON
|
||||
html = resp.data.decode('utf-8')
|
||||
# Prüfe auf typische HTML-Elemente des Dashboards
|
||||
assert 'Statistik-Dashboard' in html or 'Dashboard' in html
|
||||
data = resp.get_json()
|
||||
assert "pageviews" in data
|
||||
assert "func_counts" in data
|
||||
assert "impressions_per_day" in data
|
||||
assert "api_counts" in data
|
||||
|
||||
def test_api_monitor(client):
|
||||
resp = client.get('/api/monitor')
|
||||
|
Reference in New Issue
Block a user