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 from sqlalchemy import text app = Flask(__name__) # --- Konfiguration --- db_path = os.path.join('/app/data', 'raceplanner.db') app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' 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') 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() if not config: return [] stints = Stint.query.filter_by(car_number=car_num).order_by(Stint.order).all() schedule = [] current_time = config.start_time fuel_capacity = 120.0 for i, stint in enumerate(stints): driver = stint.driver if not driver: continue laps_possible = int(fuel_capacity / (driver.cons_per_lap or 1.0)) duration_sec = laps_possible * (driver.avg_lap_time or 120.0) 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': end_time >= (config.start_time + timedelta(hours=config.total_duration_h)) }) current_time = end_time return schedule # --- Szenario Routen (Speichern/Laden) --- @app.route('/save_scenario', methods=['POST']) @login_required def save_scenario(): name = request.json.get('name', 'unnamed_scenario') filename = f"{name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" data = { 'config': { 'start_time': RaceConfig.query.first().start_time.isoformat(), 'total_duration_h': RaceConfig.query.first().total_duration_h }, 'stints': [] } for s in Stint.query.all(): data['stints'].append({ 'car_number': s.car_number, 'driver_id': s.driver_id, 'order': s.order, 'change_tires': s.change_tires }) with open(os.path.join(SCENARIO_DIR, filename), 'w') as f: json.dump(data, f) return jsonify({'status': 'ok', 'filename': filename}) @app.route('/list_scenarios') @login_required def list_scenarios(): files = [f for f in os.listdir(SCENARIO_DIR) if f.endswith('.json')] return jsonify(sorted(files, reverse=True)) @app.route('/load_scenario', methods=['POST']) @login_required def load_scenario(): filename = request.json.get('filename') path = os.path.join(SCENARIO_DIR, filename) if not os.path.exists(path): return jsonify({'status': 'error', 'message': 'Datei nicht gefunden'}) with open(path, 'r') as f: data = json.load(f) Stint.query.delete() for s_data in data['stints']: db.session.add(Stint(**s_data)) db.session.commit() return jsonify({'status': 'ok'}) # --- Standard 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']) if s: 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)) if s: 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='mscaltenbach').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')) def init_db(): with app.app_context(): db.create_all() try: db.session.execute(text("ALTER TABLE user ADD COLUMN theme VARCHAR(20) DEFAULT 'light'")) db.session.commit() except Exception: db.session.rollback() target_username = 'mscaltenbach' target_password = 'SendIt123!' admin_user = User.query.filter_by(username=target_username).first() if not admin_user: hashed_pw = generate_password_hash(target_password) db.session.add(User(username=target_username, password=hashed_pw, theme='light')) db.session.commit() if not RaceConfig.query.first(): db.session.add(RaceConfig(start_time=datetime.now(), total_duration_h=24)) db.session.commit() if not Driver.query.first(): drivers = [ Driver(name="Caltenbach", car_number=1, avg_lap_time=128.5, cons_per_lap=3.8), Driver(name="Mueller", car_number=1, avg_lap_time=130.2, cons_per_lap=3.6), Driver(name="Schmidt", car_number=2, avg_lap_time=129.1, cons_per_lap=3.7), Driver(name="Weber", car_number=2, avg_lap_time=131.5, cons_per_lap=3.5) ] db.session.add_all(drivers) db.session.commit() d1, d2 = Driver.query.filter_by(car_number=1).first(), Driver.query.filter_by(car_number=2).first() if d1 and d2: for i in range(12): db.session.add(Stint(car_number=1, driver_id=d1.id, order=i)) db.session.add(Stint(car_number=2, driver_id=d2.id, order=i)) db.session.commit() if __name__ == '__main__': init_db() app.run(host='0.0.0.0', port=5000, debug=True)