from flask import Flask, render_template, request, redirect, url_for, jsonify, make_response, session from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user from werkzeug.security import generate_password_hash, check_password_hash import os, json from datetime import datetime, timedelta from io import BytesIO from xhtml2pdf import pisa app = Flask(__name__) # --- Konfiguration --- app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:////app/data/raceplanner.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SECRET_KEY'] = 'renn-strategie-2026-final-v3' app.config['REMEMBER_COOKIE_DURATION'] = timedelta(days=31) app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=31) SCENARIO_DIR = '/app/data/scenarios/' db = SQLAlchemy(app) migrate = Migrate(app, db) login_manager = LoginManager(app) login_manager.login_view = 'login' if not os.path.exists(SCENARIO_DIR): os.makedirs(SCENARIO_DIR) # --- Models --- class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password = db.Column(db.String(200), nullable=False) theme = db.Column(db.String(20), default='light') # 'light' oder 'dark' class RaceConfig(db.Model): id = db.Column(db.Integer, primary_key=True) start_time = db.Column(db.DateTime, default=datetime.now) total_duration_h = db.Column(db.Integer, default=24) class Driver(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100)) car_number = db.Column(db.Integer) avg_lap_time = db.Column(db.Float) cons_per_lap = db.Column(db.Float) class Stint(db.Model): id = db.Column(db.Integer, primary_key=True) car_number = db.Column(db.Integer) driver_id = db.Column(db.Integer, db.ForeignKey('driver.id')) order = db.Column(db.Integer) change_tires = db.Column(db.Boolean, default=False) driver = db.relationship('Driver', backref='stints') @login_manager.user_loader def load_user(user_id): return db.session.get(User, int(user_id)) # --- Hilfsfunktionen --- def get_calculated_schedule(car_num): config = RaceConfig.query.first() stints = Stint.query.filter_by(car_number=car_num).order_by(Stint.order).all() schedule = [] current_time = config.start_time fuel_capacity = 120.0 # Liter (Beispielhaft fixiert) for i, stint in enumerate(stints): driver = stint.driver if not driver: continue # Vereinfachte Berechnung: Ein Stint ist voll, wenn der Tank leer ist laps_possible = int(fuel_capacity / driver.cons_per_lap) duration_sec = laps_possible * driver.avg_lap_time start_str = current_time.strftime('%H:%M') end_time = current_time + timedelta(seconds=duration_sec) schedule.append({ 'id': stint.id, 'number': i + 1, 'start': start_str, 'end': end_time.strftime('%H:%M'), 'date': current_time.strftime('%d.%m. '), 'driver_name': driver.name, 'driver_id': driver.id, 'laps': laps_possible, 'fuel': round(fuel_capacity, 1), 'change_tires': stint.change_tires, 'is_finish': (current_time + timedelta(seconds=duration_sec)) >= (config.start_time + timedelta(hours=config.total_duration_h)) }) current_time = end_time return schedule # --- Routen --- @app.route('/') @login_required def index(): drivers = Driver.query.all() config = RaceConfig.query.first() car1_sch = get_calculated_schedule(1) car2_sch = get_calculated_schedule(2) return render_template('index.html', car1=car1_sch, car2=car2_sch, drivers=drivers, config=config) @app.route('/update_theme', methods=['POST']) @login_required def update_theme(): data = request.get_json() new_theme = data.get('theme') if new_theme in ['light', 'dark']: current_user.theme = new_theme db.session.commit() return jsonify({'status': 'success'}) return jsonify({'status': 'error'}), 400 @app.route('/update_stint_driver', methods=['POST']) @login_required def update_stint_driver(): data = request.json s = db.session.get(Stint, data['stint_id']) s.driver_id = data['driver_id'] db.session.commit() return jsonify({'status': 'ok'}) @app.route('/reorder_stints', methods=['POST']) @login_required def reorder_stints(): order = request.json['order'] for idx, sid in enumerate(order): s = db.session.get(Stint, int(sid)) s.order = idx db.session.commit() return jsonify({'status': 'ok'}) @app.route('/export_pdf/') @login_required def export_pdf(car_num): config = RaceConfig.query.first() sch = get_calculated_schedule(car_num) html = render_template('pdf_template.html', schedule=sch, config=config, title=f"Strategie Fahrzeug #{car_num}") res = BytesIO() pisa.CreatePDF(BytesIO(html.encode("utf-8")), dest=res) response = make_response(res.getvalue()) response.headers['Content-Type'] = 'application/pdf' return response @app.route('/driver/') @login_required def driver_detail(id): driver = db.session.get(Driver, id) full_sch = get_calculated_schedule(driver.car_number) stints = [s for s in full_sch if s['driver_id'] == id] return render_template('driver_detail.html', driver=driver, stints=stints) @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': u = User.query.filter_by(username=request.form.get('username')).first() if u and check_password_hash(u.password, request.form.get('password')): session.permanent = True login_user(u, remember=True) return redirect(url_for('index')) return render_template('login.html') @app.route('/logout') def logout(): logout_user() return redirect(url_for('login')) if __name__ == '__main__': with app.app_context(): db.create_all() # Initialer User falls nicht vorhanden if not User.query.filter_by(username='mscaltenbach').first(): pw = generate_password_hash('admin123') db.session.add(User(username='mscaltenbach', password=pw, theme='light')) if not RaceConfig.query.first(): db.session.add(RaceConfig()) db.session.commit() app.run(host='0.0.0.0', port=5000, debug=True)