commit 634d3d865af27f5e16c153d6613cf8baa99325ca Author: root Date: Fri Jan 23 21:42:48 2026 +0100 Initialer Import: Raceplaner mit Flask & Nginx Proxy diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..96817d2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +# Python & OS +__pycache__/ +*.py[cod] +.env +.DS_Store + +# Datenbanken (WICHTIG!) +data/*.db +data/*.sqlite + +# Zertifikate (WICHTIG!) +# Wir laden die Ordnerstruktur hoch, aber nicht die Schlüssel selbst +certs/*.pem +certs/*.key +certs/*.crt + +# IDE & Venv +.vscode/ +venv/ +pyenv/ +.python-version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96817d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Python & OS +__pycache__/ +*.py[cod] +.env +.DS_Store + +# Datenbanken (WICHTIG!) +data/*.db +data/*.sqlite + +# Zertifikate (WICHTIG!) +# Wir laden die Ordnerstruktur hoch, aber nicht die Schlüssel selbst +certs/*.pem +certs/*.key +certs/*.crt + +# IDE & Venv +.vscode/ +venv/ +pyenv/ +.python-version diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bc8be35 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y \ + build-essential \ + libcairo2-dev \ + pkg-config \ + python3-dev \ + fonts-dejavu \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 5000 +CMD ["python", "main.py"] diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..3c1f218 --- /dev/null +++ b/app/main.py @@ -0,0 +1,301 @@ +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(100), unique=True, nullable=False) + password = db.Column(db.String(200), nullable=False) + +class Driver(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50), nullable=False) + car_number = db.Column(db.Integer, nullable=False) + cons_per_lap = db.Column(db.Float, default=16.0) + avg_lap_time = db.Column(db.Float, default=540.0) + tire_life_laps = db.Column(db.Integer, default=50) + order_index = db.Column(db.Integer, default=0) + +class Stint(db.Model): + id = db.Column(db.Integer, primary_key=True) + car_number = db.Column(db.Integer, nullable=False) + driver_id = db.Column(db.Integer, db.ForeignKey('driver.id')) + order_index = db.Column(db.Integer, default=0) + driver = db.relationship('Driver', backref='stints') + +class RaceConfig(db.Model): + id = db.Column(db.Integer, primary_key=True) + tank_capacity = db.Column(db.Float, default=100.0) + race_duration_hours = db.Column(db.Integer, default=24) + start_time = db.Column(db.DateTime, default=datetime(2026, 1, 31, 12, 0)) + interruption_mins = db.Column(db.Integer, default=0) + always_change_tires = db.Column(db.Boolean, default=False) + min_pit_stop_sec = db.Column(db.Integer, default=180) + +@login_manager.user_loader +def load_user(user_id): + return db.session.get(User, int(user_id)) + +def save_undo_state(car_num): + stints = Stint.query.filter_by(car_number=car_num).order_by(Stint.order_index).all() + session[f'undo_{car_num}'] = [{'driver_id': s.driver_id, 'order_index': s.order_index} for s in stints] + +def get_calculated_schedule(car_num): + config = RaceConfig.query.first() + stints = Stint.query.filter_by(car_number=car_num).order_by(Stint.order_index).all() + schedule = [] + current_time = (config.start_time or datetime(2026, 1, 31, 12, 0)) + timedelta(minutes=config.interruption_mins) + race_end = config.start_time + timedelta(hours=config.race_duration_hours) + current_tire_laps = 0 + + for i, s in enumerate(stints): + if current_time >= race_end: break + driver = s.driver + if not driver: continue + + laps_possible = int(config.tank_capacity / driver.cons_per_lap) + stint_dur = laps_possible * driver.avg_lap_time + end_time = current_time + timedelta(seconds=stint_dur) + + is_finish = False + laps_to_do = laps_possible + if end_time >= race_end: + is_finish = True + rem_sec = (race_end - current_time).total_seconds() + laps_to_do = int(rem_sec / driver.avg_lap_time) + end_time = race_end + + change_tires = config.always_change_tires or (current_tire_laps + laps_to_do) > driver.tire_life_laps + fuel = round(laps_to_do * driver.cons_per_lap, 1) + + schedule.append({ + 'number': i + 1, 'stint_id': s.id, 'driver_id': driver.id, 'driver_name': driver.name, + 'start': current_time.strftime('%H:%M'), 'end': end_time.strftime('%H:%M'), + 'date': current_time.strftime('%d.%m.'), 'laps': laps_to_do, 'fuel': fuel, + 'change_tires': change_tires, 'is_finish': is_finish + }) + if is_finish: break + current_tire_laps = laps_to_do if change_tires else current_tire_laps + laps_to_do + current_time = end_time + timedelta(seconds=config.min_pit_stop_sec) + return schedule + +@app.route('/') +@login_required +def index(): + config = RaceConfig.query.first() + sch1, sch2 = get_calculated_schedule(1), get_calculated_schedule(2) + stint_counts = {d.id: 0 for d in Driver.query.all()} + for s in sch1 + sch2: + stint_counts[s['driver_id']] = stint_counts.get(s['driver_id'], 0) + 1 + + c1_drivers = Driver.query.filter_by(car_number=1).order_by(Driver.order_index).all() + c2_drivers = Driver.query.filter_by(car_number=2).order_by(Driver.order_index).all() + scenarios = [f for f in os.listdir(SCENARIO_DIR) if f.endswith('.json')] + return render_template('index.html', config=config, schedule1=sch1, schedule2=sch2, + car1_drivers=c1_drivers, car2_drivers=c2_drivers, + stint_counts=stint_counts, scenarios=scenarios) + +@app.route('/update_config', methods=['POST']) +@login_required +def update_config(): + config = RaceConfig.query.first() + config.tank_capacity = float(request.form.get('tank_capacity')) + config.race_duration_hours = int(request.form.get('race_duration_hours')) + config.min_pit_stop_sec = int(request.form.get('min_pit_stop_sec')) + config.always_change_tires = 'always_change_tires' in request.form + dt_str = request.form.get('start_datetime') + if dt_str: config.start_time = datetime.strptime(dt_str, '%Y-%m-%dT%H:%M') + db.session.commit() + return redirect(url_for('index')) + +@app.route('/add_driver', methods=['POST']) +@login_required +def add_driver(): + car_num = int(request.form.get('car_number')) + new_d = Driver(name=request.form.get('name'), car_number=car_num, + cons_per_lap=float(request.form.get('cons') or 16.0), + avg_lap_time=float(request.form.get('lap_time') or 540.0)) + db.session.add(new_d); db.session.commit() + return redirect(url_for('index')) + +@app.route('/update_driver/', methods=['POST']) +@login_required +def update_driver(id): + d = db.session.get(Driver, id) + if d: + d.name = request.form.get('name') + d.cons_per_lap = float(request.form.get('cons')) + d.avg_lap_time = float(request.form.get('lap_time')) + db.session.commit() + return redirect(url_for('index')) + +@app.route('/delete_driver/') +@login_required +def delete_driver(id): + d = db.session.get(Driver, id) + if d: + Stint.query.filter_by(driver_id=id).delete() + db.session.delete(d); db.session.commit() + return redirect(url_for('index')) + +@app.route('/update_stint_driver', methods=['POST']) +@login_required +def update_stint_driver(): + data = request.json + stint = db.session.get(Stint, data.get('stint_id')) + if stint: + save_undo_state(stint.car_number) + stint.driver_id = data.get('driver_id') + db.session.commit() + return jsonify({'status': 'ok'}) + +@app.route('/reorder_stints', methods=['POST']) +@login_required +def reorder_stints(): + order = request.json.get('order', []) + if order: + first = db.session.get(Stint, order[0]) + if first: save_undo_state(first.car_number) + for idx, sid in enumerate(order): + s = db.session.get(Stint, sid) + if s: s.order_index = idx + db.session.commit() + return jsonify({'status': 'ok'}) + +@app.route('/undo/') +@login_required +def undo(car_num): + state = session.get(f'undo_{car_num}') + if state: + Stint.query.filter_by(car_number=car_num).delete() + for s_data in state: + db.session.add(Stint(car_number=car_num, driver_id=s_data['driver_id'], order_index=s_data['order_index'])) + db.session.commit() + session.pop(f'undo_{car_num}') + return redirect(url_for('index')) + +@app.route('/generate_schedule/') +@login_required +def generate_schedule(car_num): + drivers = Driver.query.filter_by(car_number=car_num).order_by(Driver.order_index).all() + if not drivers: return redirect(url_for('index')) + save_undo_state(car_num) + Stint.query.filter_by(car_number=car_num).delete() + for i in range(40): + db.session.add(Stint(car_number=car_num, driver_id=drivers[i % len(drivers)].id, order_index=i)) + db.session.commit() + return redirect(url_for('index')) + +@app.route('/save_scenario', methods=['POST']) +@login_required +def save_scenario(): + name = request.form.get('scenario_name') + config = RaceConfig.query.first() + data = { + 'config': {'tank_capacity': config.tank_capacity, 'race_duration_hours': config.race_duration_hours, 'start_time': config.start_time.isoformat(), 'min_pit_stop_sec': config.min_pit_stop_sec, 'always_change_tires': config.always_change_tires}, + 'drivers': [{'id': d.id, 'name': d.name, 'car_number': d.car_number, 'cons_per_lap': d.cons_per_lap, 'avg_lap_time': d.avg_lap_time} for d in Driver.query.all()], + 'stints': [{'car_number': s.car_number, 'driver_id': s.driver_id, 'order_index': s.order_index} for s in Stint.query.all()] + } + with open(os.path.join(SCENARIO_DIR, f"{name}.json"), 'w') as f: json.dump(data, f) + return redirect(url_for('index')) + +@app.route('/load_scenario', methods=['POST']) +@login_required +def load_scenario(): + path = os.path.join(SCENARIO_DIR, request.form.get('scenario_file')) + with open(path, 'r') as f: data = json.load(f) + Stint.query.delete(); Driver.query.delete(); RaceConfig.query.delete() + c = data['config'] + db.session.add(RaceConfig(tank_capacity=c['tank_capacity'], race_duration_hours=c['race_duration_hours'], start_time=datetime.fromisoformat(c['start_time']), min_pit_stop_sec=c.get('min_pit_stop_sec', 180), always_change_tires=c['always_change_tires'])) + id_map = {} + for d in data['drivers']: + new_d = Driver(name=d['name'], car_number=d['car_number'], cons_per_lap=d['cons_per_lap'], avg_lap_time=d['avg_lap_time']) + db.session.add(new_d); db.session.flush(); id_map[d['id']] = new_d.id + for s in data['stints']: + db.session.add(Stint(car_number=s['car_number'], driver_id=id_map.get(s['driver_id']), order_index=s['order_index'])) + db.session.commit() + return redirect(url_for('index')) + +@app.route('/export/car/') +@login_required +def export_car_pdf(car_num): + config = RaceConfig.query.first() + html = render_template('pdf_template.html', schedule=get_calculated_schedule(car_num), config=config, title=f"Auto #{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('/export/driver/') +@login_required +def export_driver_pdf(driver_id): + driver = db.session.get(Driver, driver_id) + full_sch = get_calculated_schedule(driver.car_number) + stints = [s for s in full_sch if s['driver_id'] == driver_id] + config = RaceConfig.query.first() + html = render_template('pdf_template.html', schedule=stints, config=config, title=f"Fahrer-Plan: {driver.name}") + res = BytesIO() + pisa.CreatePDF(BytesIO(html.encode("utf-8")), dest=res) + response = make_response(res.getvalue()) + response.headers['Content-Type'] = 'application/pdf' + return response + +# HIER IST DIE FEHLENDE ROUTE +@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')) + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + if not RaceConfig.query.first(): db.session.add(RaceConfig()); db.session.commit() + if not User.query.filter_by(username='mscaltenbach').first(): + db.session.add(User(username='mscaltenbach', password=generate_password_hash('SendIt123!', method='pbkdf2:sha256'))) + db.session.commit() + app.run(host='0.0.0.0', port=5000) diff --git a/app/migrations/README b/app/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/app/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/app/migrations/alembic.ini b/app/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/app/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/app/migrations/env.py b/app/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/app/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/app/migrations/script.py.mako b/app/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/app/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/app/migrations/versions/0489144eea7b_auto_migration.py b/app/migrations/versions/0489144eea7b_auto_migration.py new file mode 100644 index 0000000..22e27f4 --- /dev/null +++ b/app/migrations/versions/0489144eea7b_auto_migration.py @@ -0,0 +1,32 @@ +"""Auto-Migration + +Revision ID: 0489144eea7b +Revises: +Create Date: 2026-01-21 19:09:43.187967 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0489144eea7b' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('driver', schema=None) as batch_op: + batch_op.add_column(sa.Column('order_index', sa.Integer(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('driver', schema=None) as batch_op: + batch_op.drop_column('order_index') + + # ### end Alembic commands ### diff --git a/app/migrations/versions/a2d7fac86703_auto_migration.py b/app/migrations/versions/a2d7fac86703_auto_migration.py new file mode 100644 index 0000000..4087bcd --- /dev/null +++ b/app/migrations/versions/a2d7fac86703_auto_migration.py @@ -0,0 +1,35 @@ +"""Auto-Migration + +Revision ID: a2d7fac86703 +Revises: 0489144eea7b +Create Date: 2026-01-21 19:12:54.952493 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a2d7fac86703' +down_revision = '0489144eea7b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('stint', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('car_number', sa.Integer(), nullable=False), + sa.Column('driver_id', sa.Integer(), nullable=True), + sa.Column('order_index', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['driver_id'], ['driver.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('stint') + # ### end Alembic commands ### diff --git a/app/migrations/versions/e8ff60abd420_auto_migration.py b/app/migrations/versions/e8ff60abd420_auto_migration.py new file mode 100644 index 0000000..c2461e2 --- /dev/null +++ b/app/migrations/versions/e8ff60abd420_auto_migration.py @@ -0,0 +1,34 @@ +"""Auto-Migration + +Revision ID: e8ff60abd420 +Revises: a2d7fac86703 +Create Date: 2026-01-21 19:24:58.766275 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e8ff60abd420' +down_revision = 'a2d7fac86703' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('race_config', schema=None) as batch_op: + batch_op.add_column(sa.Column('always_change_tires', sa.Boolean(), nullable=True)) + batch_op.add_column(sa.Column('min_pit_stop_sec', sa.Integer(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('race_config', schema=None) as batch_op: + batch_op.drop_column('min_pit_stop_sec') + batch_op.drop_column('always_change_tires') + + # ### end Alembic commands ### diff --git a/app/templates/driver_detail.html b/app/templates/driver_detail.html new file mode 100644 index 0000000..964a81e --- /dev/null +++ b/app/templates/driver_detail.html @@ -0,0 +1,47 @@ + + + + + {{ driver.name }} - Details + + + +
+
+

{{ driver.name }} #{{ driver.car_number }}

+ Zurück +
+ +
+
+
Verbrauch/Runde
+
{{ driver.cons_per_lap }}L
+
+
+
Ø Rundenzeit
+
{{ driver.avg_lap_time }}s
+
+
+
Stints Gesamt
+
{{ stints|length }}
+
+
+ +

Geplante Einsätze

+
+ {% for s in stints %} +
+
+
{{ s.date }}
+
{{ s.start }} - {{ s.end }}
+
+
+
{{ s.laps }} Runden
+
{{ s.fuel }}L Benzin
+
+
+ {% endfor %} +
+
+ + diff --git a/app/templates/index.h b/app/templates/index.h new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/index.hmtl b/app/templates/index.hmtl new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..adb1509 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,157 @@ + + + + + + RACEPLANNER 2026 + + + + + + +
+
+

Raceplanner 2026

+
+
+ + +
+
+ + +
+ Logout +
+
+ +
+
+ + +
+
+
+
+
+ + + +
+
+ +
+ {% for car_num, drivers, schedule in [(1, car1_drivers, schedule1), (2, car2_drivers, schedule2)] %} +
+
+

#{{ car_num }} Vehicle Dashboard

+ +
+ +
+ {% for d in drivers %} +
+
{{ stint_counts.get(d.id, 0) }}
+
+
+
+
+ + 📄 + 👁️ + +
+
+ {% endfor %} +
+ + + + + +
+
+ +
+ +
+ Race Schedule #{{ car_num }} +
+
+ +
+ {% for s in schedule %} +
+
#{{ s.number }}
+
+ +
+
+
{{ s.date }}
+
{{ s.start }}
+
{{ s.end }}
+
+
+ +
+
+
{{ s.laps }} R
+
{{ s.fuel }}L
+
+
+ {% if s.is_finish %}Finish + {% elif s.change_tires %}Tires{% endif %} +
+
+ {% endfor %} +
+
+ {% endfor %} +
+
+ + + + diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..6656720 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,25 @@ + + + + + + RacePlanner Login + + + +
+

RACEPLANNER

+
+
+ + +
+
+ + +
+ +
+
+ + diff --git a/app/templates/pdf_template.html b/app/templates/pdf_template.html new file mode 100644 index 0000000..2210da7 --- /dev/null +++ b/app/templates/pdf_template.html @@ -0,0 +1,55 @@ + + + + + + + +
+

{{ title }}

+
Rennstart: {{ config.start_time.strftime('%d.%m.%Y %H:%M') }}
+
+ + + + + + + + + + + + + {% for s in schedule %} + + + + + + + + + {% endfor %} + +
#ZeitraumFahrerRSpritAktion
{{ s.number }} + {{ s.date }}2026 + {{ s.start }} - {{ s.end }} + {{ s.driver_name }}{{ s.laps }}{{ s.fuel }}L + {% if s.is_finish %}ZIELANKUNFT + {% elif s.change_tires %}REIFEN & SPRIT + {% else %}NUR SPRIT{% endif %} +
+ + diff --git a/data/scenarios/24HNBR-Tom_V2_Test.json b/data/scenarios/24HNBR-Tom_V2_Test.json new file mode 100644 index 0000000..0f3307c --- /dev/null +++ b/data/scenarios/24HNBR-Tom_V2_Test.json @@ -0,0 +1 @@ +{"config": {"tank_capacity": 100.0, "race_duration_hours": 24, "start_time": "2026-01-31T12:00:00", "min_pit_stop_sec": 180, "always_change_tires": true}, "drivers": [{"id": 1, "name": "Robin B\u00f6hm", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 548.0}, {"id": 2, "name": "Eric Peng", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 540.0}, {"id": 3, "name": "Felix Mechler", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 543.0}, {"id": 4, "name": "Fabio Bertolini", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 543.0}, {"id": 5, "name": "Louis Arnold", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 544.0}, {"id": 6, "name": "Felix Peng", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 548.0}, {"id": 7, "name": "Andre", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 548.0}], "stints": [{"car_number": 1, "driver_id": 2, "order_index": 0}, {"car_number": 1, "driver_id": 1, "order_index": 1}, {"car_number": 1, "driver_id": 3, "order_index": 2}, {"car_number": 1, "driver_id": 1, "order_index": 3}, {"car_number": 1, "driver_id": 4, "order_index": 4}, {"car_number": 1, "driver_id": 5, "order_index": 5}, {"car_number": 1, "driver_id": 6, "order_index": 6}, {"car_number": 1, "driver_id": 1, "order_index": 7}, {"car_number": 1, "driver_id": 3, "order_index": 8}, {"car_number": 1, "driver_id": 2, "order_index": 9}, {"car_number": 1, "driver_id": 5, "order_index": 10}, {"car_number": 1, "driver_id": 4, "order_index": 11}, {"car_number": 1, "driver_id": 7, "order_index": 12}, {"car_number": 1, "driver_id": 6, "order_index": 13}, {"car_number": 1, "driver_id": 1, "order_index": 14}, {"car_number": 1, "driver_id": 4, "order_index": 15}, {"car_number": 1, "driver_id": 2, "order_index": 16}, {"car_number": 1, "driver_id": 5, "order_index": 17}, {"car_number": 1, "driver_id": 3, "order_index": 18}, {"car_number": 1, "driver_id": 7, "order_index": 19}, {"car_number": 1, "driver_id": 6, "order_index": 20}, {"car_number": 1, "driver_id": 1, "order_index": 21}, {"car_number": 1, "driver_id": 7, "order_index": 22}, {"car_number": 1, "driver_id": 5, "order_index": 23}, {"car_number": 1, "driver_id": 2, "order_index": 24}, {"car_number": 1, "driver_id": 2, "order_index": 25}, {"car_number": 1, "driver_id": 6, "order_index": 26}, {"car_number": 1, "driver_id": 7, "order_index": 27}, {"car_number": 1, "driver_id": 2, "order_index": 28}, {"car_number": 1, "driver_id": 1, "order_index": 29}, {"car_number": 1, "driver_id": 3, "order_index": 30}, {"car_number": 1, "driver_id": 4, "order_index": 31}, {"car_number": 1, "driver_id": 5, "order_index": 32}, {"car_number": 1, "driver_id": 6, "order_index": 33}, {"car_number": 1, "driver_id": 7, "order_index": 34}, {"car_number": 1, "driver_id": 2, "order_index": 35}, {"car_number": 1, "driver_id": 1, "order_index": 36}, {"car_number": 1, "driver_id": 3, "order_index": 37}, {"car_number": 1, "driver_id": 4, "order_index": 38}, {"car_number": 1, "driver_id": 5, "order_index": 39}, {"car_number": 1, "driver_id": 6, "order_index": 40}, {"car_number": 1, "driver_id": 7, "order_index": 41}, {"car_number": 1, "driver_id": 2, "order_index": 42}, {"car_number": 1, "driver_id": 1, "order_index": 43}, {"car_number": 1, "driver_id": 3, "order_index": 44}, {"car_number": 1, "driver_id": 4, "order_index": 45}, {"car_number": 1, "driver_id": 5, "order_index": 46}, {"car_number": 1, "driver_id": 6, "order_index": 47}, {"car_number": 1, "driver_id": 7, "order_index": 48}, {"car_number": 1, "driver_id": 2, "order_index": 49}, {"car_number": 1, "driver_id": 1, "order_index": 50}, {"car_number": 1, "driver_id": 3, "order_index": 51}, {"car_number": 1, "driver_id": 4, "order_index": 52}, {"car_number": 1, "driver_id": 5, "order_index": 53}, {"car_number": 1, "driver_id": 6, "order_index": 54}, {"car_number": 1, "driver_id": 7, "order_index": 55}, {"car_number": 1, "driver_id": 2, "order_index": 56}, {"car_number": 1, "driver_id": 1, "order_index": 57}, {"car_number": 1, "driver_id": 3, "order_index": 58}, {"car_number": 1, "driver_id": 4, "order_index": 59}, {"car_number": 1, "driver_id": 5, "order_index": 60}, {"car_number": 1, "driver_id": 6, "order_index": 61}, {"car_number": 1, "driver_id": 7, "order_index": 62}, {"car_number": 1, "driver_id": 2, "order_index": 63}, {"car_number": 1, "driver_id": 1, "order_index": 64}, {"car_number": 1, "driver_id": 3, "order_index": 65}, {"car_number": 1, "driver_id": 4, "order_index": 66}, {"car_number": 1, "driver_id": 5, "order_index": 67}, {"car_number": 1, "driver_id": 6, "order_index": 68}, {"car_number": 1, "driver_id": 7, "order_index": 69}, {"car_number": 1, "driver_id": 2, "order_index": 70}, {"car_number": 1, "driver_id": 1, "order_index": 71}, {"car_number": 1, "driver_id": 3, "order_index": 72}, {"car_number": 1, "driver_id": 4, "order_index": 73}, {"car_number": 1, "driver_id": 5, "order_index": 74}, {"car_number": 1, "driver_id": 6, "order_index": 75}, {"car_number": 1, "driver_id": 7, "order_index": 76}, {"car_number": 1, "driver_id": 2, "order_index": 77}, {"car_number": 1, "driver_id": 1, "order_index": 78}, {"car_number": 1, "driver_id": 3, "order_index": 79}]} \ No newline at end of file diff --git a/data/scenarios/24HNBR_Tom_Config_1_Test.json b/data/scenarios/24HNBR_Tom_Config_1_Test.json new file mode 100644 index 0000000..80b9ec6 --- /dev/null +++ b/data/scenarios/24HNBR_Tom_Config_1_Test.json @@ -0,0 +1 @@ +{"config": {"tank_capacity": 100.0, "race_duration_hours": 24, "start_time": "2026-01-31T12:00:00", "interruption_mins": 0, "always_change_tires": true, "min_pit_stop_sec": 180}, "drivers": [{"id": 1, "name": "Robin B\u00f6hm", "car_number": 1, "cons_per_lap": 15.0, "avg_lap_time": 548.0, "tire_life_laps": 6, "order_index": 1}, {"id": 2, "name": "Eric Peng", "car_number": 1, "cons_per_lap": 16.0, "avg_lap_time": 580.0, "tire_life_laps": 6, "order_index": 0}, {"id": 3, "name": "Felix Mechler", "car_number": 1, "cons_per_lap": 16.0, "avg_lap_time": 543.0, "tire_life_laps": 6, "order_index": 2}, {"id": 4, "name": "Fabio Bertolini", "car_number": 1, "cons_per_lap": 15.5, "avg_lap_time": 543.0, "tire_life_laps": 6, "order_index": 3}, {"id": 5, "name": "Louis Arnold", "car_number": 1, "cons_per_lap": 16.0, "avg_lap_time": 544.0, "tire_life_laps": 20, "order_index": 4}, {"id": 6, "name": "Felix Peng", "car_number": 1, "cons_per_lap": 15.5, "avg_lap_time": 548.0, "tire_life_laps": 6, "order_index": 5}, {"id": 7, "name": "Andre", "car_number": 1, "cons_per_lap": 15.0, "avg_lap_time": 548.0, "tire_life_laps": 6, "order_index": 6}], "stints": [{"car_number": 1, "driver_id": 2, "order_index": 0}, {"car_number": 1, "driver_id": 1, "order_index": 1}, {"car_number": 1, "driver_id": 3, "order_index": 2}, {"car_number": 1, "driver_id": 1, "order_index": 3}, {"car_number": 1, "driver_id": 4, "order_index": 4}, {"car_number": 1, "driver_id": 5, "order_index": 5}, {"car_number": 1, "driver_id": 6, "order_index": 6}, {"car_number": 1, "driver_id": 1, "order_index": 7}, {"car_number": 1, "driver_id": 3, "order_index": 8}, {"car_number": 1, "driver_id": 2, "order_index": 9}, {"car_number": 1, "driver_id": 5, "order_index": 10}, {"car_number": 1, "driver_id": 4, "order_index": 11}, {"car_number": 1, "driver_id": 7, "order_index": 12}, {"car_number": 1, "driver_id": 6, "order_index": 13}, {"car_number": 1, "driver_id": 1, "order_index": 14}, {"car_number": 1, "driver_id": 4, "order_index": 15}, {"car_number": 1, "driver_id": 2, "order_index": 16}, {"car_number": 1, "driver_id": 5, "order_index": 17}, {"car_number": 1, "driver_id": 3, "order_index": 18}, {"car_number": 1, "driver_id": 7, "order_index": 19}, {"car_number": 1, "driver_id": 6, "order_index": 20}, {"car_number": 1, "driver_id": 1, "order_index": 21}, {"car_number": 1, "driver_id": 7, "order_index": 22}, {"car_number": 1, "driver_id": 5, "order_index": 23}, {"car_number": 1, "driver_id": 2, "order_index": 24}, {"car_number": 1, "driver_id": 5, "order_index": 25}, {"car_number": 1, "driver_id": 6, "order_index": 26}, {"car_number": 1, "driver_id": 7, "order_index": 27}, {"car_number": 1, "driver_id": 2, "order_index": 28}, {"car_number": 1, "driver_id": 1, "order_index": 29}, {"car_number": 1, "driver_id": 3, "order_index": 30}, {"car_number": 1, "driver_id": 4, "order_index": 31}, {"car_number": 1, "driver_id": 5, "order_index": 32}, {"car_number": 1, "driver_id": 6, "order_index": 33}, {"car_number": 1, "driver_id": 7, "order_index": 34}, {"car_number": 1, "driver_id": 2, "order_index": 35}, {"car_number": 1, "driver_id": 1, "order_index": 36}, {"car_number": 1, "driver_id": 3, "order_index": 37}, {"car_number": 1, "driver_id": 4, "order_index": 38}, {"car_number": 1, "driver_id": 5, "order_index": 39}, {"car_number": 1, "driver_id": 6, "order_index": 40}, {"car_number": 1, "driver_id": 7, "order_index": 41}, {"car_number": 1, "driver_id": 2, "order_index": 42}, {"car_number": 1, "driver_id": 1, "order_index": 43}, {"car_number": 1, "driver_id": 3, "order_index": 44}, {"car_number": 1, "driver_id": 4, "order_index": 45}, {"car_number": 1, "driver_id": 5, "order_index": 46}, {"car_number": 1, "driver_id": 6, "order_index": 47}, {"car_number": 1, "driver_id": 7, "order_index": 48}, {"car_number": 1, "driver_id": 2, "order_index": 49}, {"car_number": 1, "driver_id": 1, "order_index": 50}, {"car_number": 1, "driver_id": 3, "order_index": 51}, {"car_number": 1, "driver_id": 4, "order_index": 52}, {"car_number": 1, "driver_id": 5, "order_index": 53}, {"car_number": 1, "driver_id": 6, "order_index": 54}, {"car_number": 1, "driver_id": 7, "order_index": 55}, {"car_number": 1, "driver_id": 2, "order_index": 56}, {"car_number": 1, "driver_id": 1, "order_index": 57}, {"car_number": 1, "driver_id": 3, "order_index": 58}, {"car_number": 1, "driver_id": 4, "order_index": 59}, {"car_number": 1, "driver_id": 5, "order_index": 60}, {"car_number": 1, "driver_id": 6, "order_index": 61}, {"car_number": 1, "driver_id": 7, "order_index": 62}, {"car_number": 1, "driver_id": 2, "order_index": 63}, {"car_number": 1, "driver_id": 1, "order_index": 64}, {"car_number": 1, "driver_id": 3, "order_index": 65}, {"car_number": 1, "driver_id": 4, "order_index": 66}, {"car_number": 1, "driver_id": 5, "order_index": 67}, {"car_number": 1, "driver_id": 6, "order_index": 68}, {"car_number": 1, "driver_id": 7, "order_index": 69}, {"car_number": 1, "driver_id": 2, "order_index": 70}, {"car_number": 1, "driver_id": 1, "order_index": 71}, {"car_number": 1, "driver_id": 3, "order_index": 72}, {"car_number": 1, "driver_id": 4, "order_index": 73}, {"car_number": 1, "driver_id": 5, "order_index": 74}, {"car_number": 1, "driver_id": 6, "order_index": 75}, {"car_number": 1, "driver_id": 7, "order_index": 76}, {"car_number": 1, "driver_id": 2, "order_index": 77}, {"car_number": 1, "driver_id": 1, "order_index": 78}, {"car_number": 1, "driver_id": 3, "order_index": 79}]} \ No newline at end of file diff --git a/data/scenarios/Temp .json b/data/scenarios/Temp .json new file mode 100644 index 0000000..0f3307c --- /dev/null +++ b/data/scenarios/Temp .json @@ -0,0 +1 @@ +{"config": {"tank_capacity": 100.0, "race_duration_hours": 24, "start_time": "2026-01-31T12:00:00", "min_pit_stop_sec": 180, "always_change_tires": true}, "drivers": [{"id": 1, "name": "Robin B\u00f6hm", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 548.0}, {"id": 2, "name": "Eric Peng", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 540.0}, {"id": 3, "name": "Felix Mechler", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 543.0}, {"id": 4, "name": "Fabio Bertolini", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 543.0}, {"id": 5, "name": "Louis Arnold", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 544.0}, {"id": 6, "name": "Felix Peng", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 548.0}, {"id": 7, "name": "Andre", "car_number": 1, "cons_per_lap": 15.2, "avg_lap_time": 548.0}], "stints": [{"car_number": 1, "driver_id": 2, "order_index": 0}, {"car_number": 1, "driver_id": 1, "order_index": 1}, {"car_number": 1, "driver_id": 3, "order_index": 2}, {"car_number": 1, "driver_id": 1, "order_index": 3}, {"car_number": 1, "driver_id": 4, "order_index": 4}, {"car_number": 1, "driver_id": 5, "order_index": 5}, {"car_number": 1, "driver_id": 6, "order_index": 6}, {"car_number": 1, "driver_id": 1, "order_index": 7}, {"car_number": 1, "driver_id": 3, "order_index": 8}, {"car_number": 1, "driver_id": 2, "order_index": 9}, {"car_number": 1, "driver_id": 5, "order_index": 10}, {"car_number": 1, "driver_id": 4, "order_index": 11}, {"car_number": 1, "driver_id": 7, "order_index": 12}, {"car_number": 1, "driver_id": 6, "order_index": 13}, {"car_number": 1, "driver_id": 1, "order_index": 14}, {"car_number": 1, "driver_id": 4, "order_index": 15}, {"car_number": 1, "driver_id": 2, "order_index": 16}, {"car_number": 1, "driver_id": 5, "order_index": 17}, {"car_number": 1, "driver_id": 3, "order_index": 18}, {"car_number": 1, "driver_id": 7, "order_index": 19}, {"car_number": 1, "driver_id": 6, "order_index": 20}, {"car_number": 1, "driver_id": 1, "order_index": 21}, {"car_number": 1, "driver_id": 7, "order_index": 22}, {"car_number": 1, "driver_id": 5, "order_index": 23}, {"car_number": 1, "driver_id": 2, "order_index": 24}, {"car_number": 1, "driver_id": 2, "order_index": 25}, {"car_number": 1, "driver_id": 6, "order_index": 26}, {"car_number": 1, "driver_id": 7, "order_index": 27}, {"car_number": 1, "driver_id": 2, "order_index": 28}, {"car_number": 1, "driver_id": 1, "order_index": 29}, {"car_number": 1, "driver_id": 3, "order_index": 30}, {"car_number": 1, "driver_id": 4, "order_index": 31}, {"car_number": 1, "driver_id": 5, "order_index": 32}, {"car_number": 1, "driver_id": 6, "order_index": 33}, {"car_number": 1, "driver_id": 7, "order_index": 34}, {"car_number": 1, "driver_id": 2, "order_index": 35}, {"car_number": 1, "driver_id": 1, "order_index": 36}, {"car_number": 1, "driver_id": 3, "order_index": 37}, {"car_number": 1, "driver_id": 4, "order_index": 38}, {"car_number": 1, "driver_id": 5, "order_index": 39}, {"car_number": 1, "driver_id": 6, "order_index": 40}, {"car_number": 1, "driver_id": 7, "order_index": 41}, {"car_number": 1, "driver_id": 2, "order_index": 42}, {"car_number": 1, "driver_id": 1, "order_index": 43}, {"car_number": 1, "driver_id": 3, "order_index": 44}, {"car_number": 1, "driver_id": 4, "order_index": 45}, {"car_number": 1, "driver_id": 5, "order_index": 46}, {"car_number": 1, "driver_id": 6, "order_index": 47}, {"car_number": 1, "driver_id": 7, "order_index": 48}, {"car_number": 1, "driver_id": 2, "order_index": 49}, {"car_number": 1, "driver_id": 1, "order_index": 50}, {"car_number": 1, "driver_id": 3, "order_index": 51}, {"car_number": 1, "driver_id": 4, "order_index": 52}, {"car_number": 1, "driver_id": 5, "order_index": 53}, {"car_number": 1, "driver_id": 6, "order_index": 54}, {"car_number": 1, "driver_id": 7, "order_index": 55}, {"car_number": 1, "driver_id": 2, "order_index": 56}, {"car_number": 1, "driver_id": 1, "order_index": 57}, {"car_number": 1, "driver_id": 3, "order_index": 58}, {"car_number": 1, "driver_id": 4, "order_index": 59}, {"car_number": 1, "driver_id": 5, "order_index": 60}, {"car_number": 1, "driver_id": 6, "order_index": 61}, {"car_number": 1, "driver_id": 7, "order_index": 62}, {"car_number": 1, "driver_id": 2, "order_index": 63}, {"car_number": 1, "driver_id": 1, "order_index": 64}, {"car_number": 1, "driver_id": 3, "order_index": 65}, {"car_number": 1, "driver_id": 4, "order_index": 66}, {"car_number": 1, "driver_id": 5, "order_index": 67}, {"car_number": 1, "driver_id": 6, "order_index": 68}, {"car_number": 1, "driver_id": 7, "order_index": 69}, {"car_number": 1, "driver_id": 2, "order_index": 70}, {"car_number": 1, "driver_id": 1, "order_index": 71}, {"car_number": 1, "driver_id": 3, "order_index": 72}, {"car_number": 1, "driver_id": 4, "order_index": 73}, {"car_number": 1, "driver_id": 5, "order_index": 74}, {"car_number": 1, "driver_id": 6, "order_index": 75}, {"car_number": 1, "driver_id": 7, "order_index": 76}, {"car_number": 1, "driver_id": 2, "order_index": 77}, {"car_number": 1, "driver_id": 1, "order_index": 78}, {"car_number": 1, "driver_id": 3, "order_index": 79}]} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8d88a7f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: '3.3' + +services: + flask_app: + build: . + container_name: raceplanner_backend + restart: always + volumes: + - ./app:/app + - ./data:/app/data + environment: + - SECRET_KEY=renn-strategie-2026-sicher + - DATABASE_URL=sqlite:////app/data/raceplanner.db + - PYTHONUNBUFFERED=1 + + nginx: + image: nginx:latest + container_name: raceplanner_proxy + restart: always + ports: + - "6060:443" # <--- Externer Zugriff über Port 6060 + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ./certs:/etc/nginx/certs + command: > + /bin/bash -c " + mkdir -p /etc/nginx/certs && + if [ ! -f /etc/nginx/certs/fullchain.pem ]; then + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/nginx/certs/privkey.pem \ + -out /etc/nginx/certs/fullchain.pem \ + -subj '/C=DE/ST=Berlin/L=Berlin/O=RaceTeam/CN=localhost'; + fi && + nginx -g 'daemon off;'" + depends_on: + - flask_app diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..669e019 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 80; + listen 443 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + + location / { + proxy_pass http://flask_app:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..81aec32 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +flask +flask-sqlalchemy +flask-migrate +flask-login +gunicorn +xhtml2pdf