from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify from datetime import datetime, timedelta import numpy as np from dateutil.relativedelta import relativedelta import os import time app_start_time = time.time() app = Flask(__name__) app.secret_key = os.environ.get('SECRET_KEY', 'dev-key') # HTML-Template wird jetzt aus templates/index.html geladen WOCHENTAGE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] @app.route('/', methods=['GET', 'POST']) def index(): # Rudimentäres Logging für Page Impressions log_dir = 'log' os.makedirs(log_dir, exist_ok=True) log_path = os.path.join(log_dir, 'pageviews.log') # Logfile bereinigen: nur Zeilen der letzten 7 Tage behalten if os.path.exists(log_path): now = datetime.now() lines_new = [] with open(log_path, encoding='utf-8') as f: for line in f: try: date_str = line[:10] date = datetime.strptime(date_str, '%Y-%m-%d') if (now - date).days <= 7: lines_new.append(line) except Exception: # Zeile ohne Datum behalten (zur Sicherheit) lines_new.append(line) with open(log_path, 'w', encoding='utf-8') as f: f.writelines(lines_new) with open(log_path, 'a', encoding='utf-8') as f: from datetime import datetime as dt f.write(f"{dt.now().isoformat()} PAGEVIEW\n") tage = werktage = wochentag = datumsrechnung = werktagsrechnung = kw_berechnen = kw_datum = wochen_monate = None active_idx = 0 plusminus_result = None if request.method == 'POST': action = request.form.get('action') # Funktions-Logging with open(log_path, 'a', encoding='utf-8') as f: from datetime import datetime as dt f.write(f"{dt.now().isoformat()} FUNC: {action}\n") if action == 'tage_werktage': active_idx = 0 start = request.form.get('start1') end = request.form.get('end1') is_werktage = request.form.get('werktage') in ('on', 'true', '1', True) try: d1 = datetime.strptime(start, '%Y-%m-%d') d2 = datetime.strptime(end, '%Y-%m-%d') if is_werktage: if d1 > d2: d1, d2 = d2, d1 tage = np.busday_count(d1.date(), (d2 + timedelta(days=1)).date()) else: tage = abs((d2 - d1).days) except Exception: tage = 'Ungültige Eingabe' elif action == 'wochentag': active_idx = 1 datum = request.form.get('datum3') try: d = datetime.strptime(datum, '%Y-%m-%d') wochentag = WOCHENTAGE[d.weekday()] except Exception: wochentag = 'Ungültige Eingabe' elif action == 'kw_berechnen': active_idx = 2 datum = request.form.get('datum6') try: d = datetime.strptime(datum, '%Y-%m-%d') kw = d.isocalendar().week kw_berechnen = f"KW {kw} ({d.year})" except Exception: kw_berechnen = 'Ungültige Eingabe' elif action == 'kw_datum': active_idx = 3 jahr = request.form.get('jahr7') kw = request.form.get('kw7') try: jahr = int(jahr) kw = int(kw) # Montag der KW start = datetime.fromisocalendar(jahr, kw, 1) end = datetime.fromisocalendar(jahr, kw, 7) kw_datum = f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}" except Exception: kw_datum = 'Ungültige Eingabe' elif action == 'plusminus': active_idx = 4 datum = request.form.get('datum_pm') anzahl = request.form.get('anzahl_pm') einheit = request.form.get('einheit_pm') richtung = request.form.get('richtung_pm') is_werktage = request.form.get('werktage_pm') in ('on', 'true', '1', True) try: d = datetime.strptime(datum, '%Y-%m-%d') anzahl_int = int(anzahl) if richtung == 'sub': anzahl_int = -anzahl_int if einheit == 'tage': if is_werktage: # Werktage: numpy busday_offset result = np.busday_offset(d.date(), anzahl_int, roll='forward') result_dt = datetime.strptime(str(result), '%Y-%m-%d') plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Werktage: {result_dt.strftime('%d.%m.%Y')}" else: result = d + timedelta(days=anzahl_int) plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Tage: {result.strftime('%d.%m.%Y')}" elif einheit == 'wochen': if is_werktage: plusminus_result = 'Nicht unterstützt: Werktage + Wochen.' else: result = d + timedelta(weeks=anzahl_int) plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Wochen: {result.strftime('%d.%m.%Y')}" elif einheit == 'monate': if is_werktage: plusminus_result = 'Nicht unterstützt: Werktage + Monate.' else: result = d + relativedelta(months=anzahl_int) plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}" except Exception: plusminus_result = 'Ungültige Eingabe' return render_template('index.html', tage=tage, werktage=werktage, wochentag=wochentag, plusminus_result=plusminus_result, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx) def parse_log_stats(log_path): pageviews = 0 func_counts = {} impressions_per_day = {} api_counts = {} if os.path.exists(log_path): with open(log_path, encoding='utf-8') as f: for line in f: if 'PAGEVIEW' in line: pageviews += 1 try: date = line[:10] if len(date) == 10 and date[4] == '-' and date[7] == '-': impressions_per_day[date] = impressions_per_day.get(date, 0) + 1 except Exception: pass elif 'FUNC:' in line: func = line.split('FUNC:')[1].strip() func_counts[func] = func_counts.get(func, 0) + 1 elif 'FUNC_API:' in line: api = line.split('FUNC_API:')[1].strip() api_counts[api] = api_counts.get(api, 0) + 1 return pageviews, func_counts, impressions_per_day, api_counts @app.route('/stats', methods=['GET', 'POST']) def stats(): stats_password = os.environ.get('STATS_PASSWORD', 'changeme') if 'stats_auth' not in session: if request.method == 'POST': if request.form.get('password') == stats_password: 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) log_path = os.path.join('log', 'pageviews.log') pageviews, func_counts, impressions_per_day, api_counts = parse_log_stats(log_path) return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, impressions_per_day=impressions_per_day, api_counts=api_counts) # --- REST API --- def log_api_usage(api_name): log_dir = 'log' os.makedirs(log_dir, exist_ok=True) log_path = os.path.join(log_dir, 'pageviews.log') from datetime import datetime as dt with open(log_path, 'a', encoding='utf-8') as f: f.write(f"{dt.now().isoformat()} FUNC_API: {api_name}\n") @app.route('/api/tage_werktage', methods=['POST']) def api_tage_werktage(): log_api_usage('tage_werktage') data = request.get_json() start = data.get('start') end = data.get('end') is_werktage = data.get('werktage', False) try: d1 = datetime.strptime(start, '%Y-%m-%d') d2 = datetime.strptime(end, '%Y-%m-%d') if is_werktage: if d1 > d2: d1, d2 = d2, d1 tage = int(np.busday_count(d1.date(), (d2 + timedelta(days=1)).date())) else: tage = abs((d2 - d1).days) return jsonify({'result': tage}) except Exception as e: return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400 @app.route('/api/wochentag', methods=['POST']) def api_wochentag(): log_api_usage('wochentag') data = request.get_json() datum = data.get('datum') try: d = datetime.strptime(datum, '%Y-%m-%d') wochentag = WOCHENTAGE[d.weekday()] return jsonify({'result': wochentag}) except Exception as e: return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400 @app.route('/api/kw_berechnen', methods=['POST']) def api_kw_berechnen(): log_api_usage('kw_berechnen') data = request.get_json() datum = data.get('datum') try: d = datetime.strptime(datum, '%Y-%m-%d') kw = d.isocalendar().week return jsonify({'result': f"KW {kw} ({d.year})", 'kw': kw, 'jahr': d.year}) except Exception as e: return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400 @app.route('/api/kw_datum', methods=['POST']) def api_kw_datum(): log_api_usage('kw_datum') data = request.get_json() jahr = data.get('jahr') kw = data.get('kw') try: jahr = int(jahr) kw = int(kw) start = datetime.fromisocalendar(jahr, kw, 1) end = datetime.fromisocalendar(jahr, kw, 7) return jsonify({'result': f"{start.strftime('%d.%m.%Y')} bis {end.strftime('%d.%m.%Y')}", 'start': start.strftime('%Y-%m-%d'), 'end': end.strftime('%Y-%m-%d')}) except Exception as e: return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400 @app.route('/api/plusminus', methods=['POST']) def api_plusminus(): log_api_usage('plusminus') data = request.get_json() datum = data.get('datum') anzahl = data.get('anzahl') einheit = data.get('einheit') richtung = data.get('richtung', 'add') is_werktage = data.get('werktage', False) try: d = datetime.strptime(datum, '%Y-%m-%d') anzahl_int = int(anzahl) if richtung == 'sub': anzahl_int = -anzahl_int if einheit == 'tage': if is_werktage: result = np.busday_offset(d.date(), anzahl_int, roll='forward') result_dt = datetime.strptime(str(result), '%Y-%m-%d') return jsonify({'result': result_dt.strftime('%Y-%m-%d')}) else: result = d + timedelta(days=anzahl_int) return jsonify({'result': result.strftime('%Y-%m-%d')}) elif einheit == 'wochen': if is_werktage: return jsonify({'error': 'Nicht unterstützt: Werktage + Wochen.'}), 400 else: result = d + timedelta(weeks=anzahl_int) return jsonify({'result': result.strftime('%Y-%m-%d')}) elif einheit == 'monate': if is_werktage: return jsonify({'error': 'Nicht unterstützt: Werktage + Monate.'}), 400 else: result = d + relativedelta(months=anzahl_int) return jsonify({'result': result.strftime('%Y-%m-%d')}) else: return jsonify({'error': 'Ungültige Einheit'}), 400 except Exception as e: return jsonify({'error': 'Ungültige Eingabe', 'details': str(e)}), 400 @app.route('/api/stats', methods=['GET']) def api_stats(): log_path = os.path.join('log', 'pageviews.log') pageviews, func_counts, impressions_per_day, api_counts = parse_log_stats(log_path) return render_template('stats_dashboard.html', pageviews=pageviews, func_counts=func_counts, impressions_per_day=impressions_per_day, api_counts=api_counts) @app.route('/api/monitor', methods=['GET']) def api_monitor(): log_path = os.path.join('log', 'pageviews.log') pageviews = 0 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 uptime = int(time.time() - app_start_time) return jsonify({ "status": "ok", "message": "App running", "time": datetime.now().isoformat(), "uptime_seconds": uptime, "pageviews_last_7_days": pageviews }) @app.route('/api-docs') def api_docs(): return render_template('swagger.html') if __name__ == '__main__': app.run(debug=True, host="0.0.0.0")