RESTORE: Funktionierender Stand aus TGZ inkl. gerettetem Szenario

This commit is contained in:
2026-01-30 16:43:29 +01:00
parent 93e34c498c
commit 2c567f751f
6 changed files with 316 additions and 191 deletions

View File

@@ -7,7 +7,6 @@ import os, json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
from xhtml2pdf import pisa from xhtml2pdf import pisa
from sqlalchemy import text
app = Flask(__name__) app = Flask(__name__)
@@ -31,122 +30,244 @@ if not os.path.exists(SCENARIO_DIR):
# --- Models --- # --- Models ---
class User(UserMixin, db.Model): class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False) username = db.Column(db.String(100), unique=True, nullable=False)
password = db.Column(db.String(200), 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): class Driver(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100)) name = db.Column(db.String(50), nullable=False)
car_number = db.Column(db.Integer) car_number = db.Column(db.Integer, nullable=False)
avg_lap_time = db.Column(db.Float) cons_per_lap = db.Column(db.Float, default=16.0)
cons_per_lap = db.Column(db.Float) 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): class Stint(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
car_number = db.Column(db.Integer) car_number = db.Column(db.Integer, nullable=False)
driver_id = db.Column(db.Integer, db.ForeignKey('driver.id')) driver_id = db.Column(db.Integer, db.ForeignKey('driver.id'))
order = db.Column(db.Integer) order_index = db.Column(db.Integer, default=0)
change_tires = db.Column(db.Boolean, default=False)
driver = db.relationship('Driver', backref='stints') 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 @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
return db.session.get(User, int(user_id)) return db.session.get(User, int(user_id))
# --- Hilfsfunktionen --- 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): def get_calculated_schedule(car_num):
config = RaceConfig.query.first() config = RaceConfig.query.first()
if not config: return [] stints = Stint.query.filter_by(car_number=car_num).order_by(Stint.order_index).all()
stints = Stint.query.filter_by(car_number=car_num).order_by(Stint.order).all()
schedule = [] schedule = []
current_time = config.start_time current_time = (config.start_time or datetime(2026, 1, 31, 12, 0)) + timedelta(minutes=config.interruption_mins)
fuel_capacity = 120.0 race_end = config.start_time + timedelta(hours=config.race_duration_hours)
current_tire_laps = 0
for i, stint in enumerate(stints):
driver = stint.driver for i, s in enumerate(stints):
if current_time >= race_end: break
driver = s.driver
if not driver: continue if not driver: continue
laps_possible = int(fuel_capacity / driver.cons_per_lap) laps_possible = int(config.tank_capacity / driver.cons_per_lap)
duration_sec = laps_possible * driver.avg_lap_time stint_dur = laps_possible * driver.avg_lap_time
end_time = current_time + timedelta(seconds=stint_dur)
start_str = current_time.strftime('%H:%M') is_finish = False
end_time = current_time + timedelta(seconds=duration_sec) 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({ schedule.append({
'id': stint.id, 'number': i + 1, 'stint_id': s.id, 'driver_id': driver.id, 'driver_name': driver.name,
'number': i + 1, 'start': current_time.strftime('%H:%M'), 'end': end_time.strftime('%H:%M'),
'start': start_str, 'date': current_time.strftime('%d.%m.'), 'laps': laps_to_do, 'fuel': fuel,
'end': end_time.strftime('%H:%M'), 'change_tires': change_tires, 'is_finish': is_finish
'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 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 return schedule
# --- Routen ---
@app.route('/') @app.route('/')
@login_required @login_required
def index(): def index():
drivers = Driver.query.all()
config = RaceConfig.query.first() config = RaceConfig.query.first()
car1_sch = get_calculated_schedule(1) sch1, sch2 = get_calculated_schedule(1), get_calculated_schedule(2)
car2_sch = get_calculated_schedule(2) stint_counts = {d.id: 0 for d in Driver.query.all()}
return render_template('index.html', car1=car1_sch, car2=car2_sch, drivers=drivers, config=config) 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_theme', methods=['POST']) @app.route('/update_config', methods=['POST'])
@login_required @login_required
def update_theme(): def update_config():
data = request.get_json() config = RaceConfig.query.first()
new_theme = data.get('theme') config.tank_capacity = float(request.form.get('tank_capacity'))
if new_theme in ['light', 'dark']: config.race_duration_hours = int(request.form.get('race_duration_hours'))
current_user.theme = new_theme 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/<int:id>', 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() db.session.commit()
return jsonify({'status': 'success'}) return redirect(url_for('index'))
return jsonify({'status': 'error'}), 400
@app.route('/delete_driver/<int:id>')
@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']) @app.route('/update_stint_driver', methods=['POST'])
@login_required @login_required
def update_stint_driver(): def update_stint_driver():
data = request.json data = request.json
s = db.session.get(Stint, data['stint_id']) stint = db.session.get(Stint, data.get('stint_id'))
s.driver_id = data['driver_id'] if stint:
db.session.commit() save_undo_state(stint.car_number)
stint.driver_id = data.get('driver_id')
db.session.commit()
return jsonify({'status': 'ok'}) return jsonify({'status': 'ok'})
@app.route('/reorder_stints', methods=['POST']) @app.route('/reorder_stints', methods=['POST'])
@login_required @login_required
def reorder_stints(): def reorder_stints():
order = request.json['order'] order = request.json.get('order', [])
for idx, sid in enumerate(order): if order:
s = db.session.get(Stint, int(sid)) first = db.session.get(Stint, order[0])
s.order = idx if first: save_undo_state(first.car_number)
db.session.commit() 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'}) return jsonify({'status': 'ok'})
@app.route('/export_pdf/<int:car_num>') @app.route('/undo/<int:car_num>')
@login_required @login_required
def export_pdf(car_num): 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/<int:car_num>')
@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() config = RaceConfig.query.first()
sch = get_calculated_schedule(car_num) data = {
html = render_template('pdf_template.html', schedule=sch, config=config, title=f"Strategie Fahrzeug #{car_num}") '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/<int:car_num>')
@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() res = BytesIO()
pisa.CreatePDF(BytesIO(html.encode("utf-8")), dest=res) pisa.CreatePDF(BytesIO(html.encode("utf-8")), dest=res)
response = make_response(res.getvalue()) response = make_response(res.getvalue())
response.headers['Content-Type'] = 'application/pdf' response.headers['Content-Type'] = 'application/pdf'
return response return response
@app.route('/export/driver/<int:driver_id>')
@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/<int:id>') @app.route('/driver/<int:id>')
@login_required @login_required
def driver_detail(id): def driver_detail(id):
@@ -158,7 +279,7 @@ def driver_detail(id):
@app.route('/login', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST'])
def login(): def login():
if request.method == 'POST': if request.method == 'POST':
u = User.query.filter_by(username=request.form.get('username')).first() u = User.query.filter_by(username='mscaltenbach').first()
if u and check_password_hash(u.password, request.form.get('password')): if u and check_password_hash(u.password, request.form.get('password')):
session.permanent = True session.permanent = True
login_user(u, remember=True) login_user(u, remember=True)
@@ -173,21 +294,8 @@ def logout():
if __name__ == '__main__': if __name__ == '__main__':
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
if not RaceConfig.query.first(): db.session.add(RaceConfig()); db.session.commit()
# Manueller Fix: Spalte 'theme' hinzufügen, falls sie fehlt (SQLite-spezifisch)
try:
db.session.execute(text("ALTER TABLE user ADD COLUMN theme VARCHAR(20) DEFAULT 'light'"))
db.session.commit()
except Exception:
# Fehler tritt auf, wenn Spalte bereits existiert das ist ok
db.session.rollback()
if not User.query.filter_by(username='mscaltenbach').first(): if not User.query.filter_by(username='mscaltenbach').first():
pw = generate_password_hash('admin123') db.session.add(User(username='mscaltenbach', password=generate_password_hash('SendIt123!', method='pbkdf2:sha256')))
db.session.add(User(username='mscaltenbach', password=pw, theme='light')) db.session.commit()
app.run(host='0.0.0.0', port=5000)
if not RaceConfig.query.first():
db.session.add(RaceConfig())
db.session.commit()
app.run(host='0.0.0.0', port=5000, debug=True)

View File

@@ -1,52 +1,47 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de" class="{% if current_user.theme == 'dark' %}dark{% endif %}"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ driver.name }} - Details</title> <title>{{ driver.name }} - Details</title>
<script>
tailwind.config = { darkMode: 'class' }
</script>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
</head> </head>
<body class="bg-slate-50 text-slate-900 dark:bg-[#0a0e17] dark:text-white p-8 transition-colors"> <body class="bg-[#0a0e17] text-white p-8">
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<div class="flex justify-between items-center mb-8"> <div class="flex justify-between items-center mb-8">
<h1 class="text-4xl font-black uppercase italic">{{ driver.name }} <span class="text-blue-500">#{{ driver.car_number }}</span></h1> <h1 class="text-4xl font-black uppercase italic">{{ driver.name }} <span class="text-blue-500">#{{ driver.car_number }}</span></h1>
<a href="/" class="bg-slate-200 dark:bg-slate-800 text-slate-800 dark:text-white px-4 py-2 rounded text-xs uppercase font-bold hover:bg-blue-500 hover:text-white transition-all">Zurück</a> <a href="/" class="bg-slate-800 px-4 py-2 rounded text-xs uppercase font-bold">Zurück</a>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"> <div class="grid grid-cols-3 gap-4 mb-8">
<div class="bg-white dark:bg-slate-900/50 p-4 rounded-xl border border-slate-200 dark:border-white/5 shadow-sm"> <div class="bg-slate-900/50 p-4 rounded-xl border border-white/5">
<div class="text-[10px] text-slate-500 uppercase font-bold">Verbrauch/Runde</div> <div class="text-[10px] text-slate-500 uppercase font-bold">Verbrauch/Runde</div>
<div class="text-2xl font-black text-green-600 dark:text-green-500">{{ driver.cons_per_lap }}L</div> <div class="text-2xl font-black text-green-500">{{ driver.cons_per_lap }}L</div>
</div> </div>
<div class="bg-white dark:bg-slate-900/50 p-4 rounded-xl border border-slate-200 dark:border-white/5 shadow-sm"> <div class="bg-slate-900/50 p-4 rounded-xl border border-white/5">
<div class="text-[10px] text-slate-500 uppercase font-bold">Ø Rundenzeit</div> <div class="text-[10px] text-slate-500 uppercase font-bold">Ø Rundenzeit</div>
<div class="text-2xl font-black text-blue-600 dark:text-blue-500">{{ driver.avg_lap_time }}s</div> <div class="text-2xl font-black text-blue-500">{{ driver.avg_lap_time }}s</div>
</div> </div>
<div class="bg-white dark:bg-slate-900/50 p-4 rounded-xl border border-slate-200 dark:border-white/5 shadow-sm"> <div class="bg-slate-900/50 p-4 rounded-xl border border-white/5">
<div class="text-[10px] text-slate-500 uppercase font-bold">Stints Gesamt</div> <div class="text-[10px] text-slate-500 uppercase font-bold">Stints Gesamt</div>
<div class="text-2xl font-black text-orange-600 dark:text-orange-500">{{ stints|length }}</div> <div class="text-2xl font-black text-orange-500">{{ stints|length }}</div>
</div> </div>
</div> </div>
<h2 class="text-xl font-bold mb-4 uppercase text-slate-400">Geplante Einsätze</h2> <h2 class="text-xl font-bold mb-4 uppercase text-slate-400">Geplante Einsätze</h2>
<div class="space-y-2"> <div class="space-y-2">
{% for s in stints %} {% for s in stints %}
<div class="bg-white dark:bg-slate-900/80 p-4 rounded-lg flex justify-between items-center border-l-4 border-blue-600 shadow-sm"> <div class="bg-slate-900/80 p-4 rounded-lg flex justify-between items-center border-l-4 border-blue-600">
<div> <div>
<div class="text-[10px] text-slate-500 font-mono">{{ s.date }}</div> <div class="text-[10px] text-slate-500 font-mono">{{ s.date }}</div>
<div class="text-lg font-black">{{ s.start }} - {{ s.end }}</div> <div class="text-lg font-black">{{ s.start }} - {{ s.end }}</div>
</div> </div>
<div class="text-right"> <div class="text-right">
<div class="text-xs font-bold text-slate-500 uppercase">Stint #{{ s.number }}</div> <div class="text-sm font-bold">{{ s.laps }} Runden</div>
<div class="text-sm font-black">{{ s.laps }} Runden</div> <div class="text-xs text-green-500">{{ s.fuel }}L Benzin</div>
</div> </div>
</div> </div>
{% else %}
<div class="text-center py-12 text-slate-500 italic">Keine Stints für diesen Fahrer geplant.</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</body> </body>
</html> </html>

0
app/templates/index.h Normal file
View File

0
app/templates/index.hmtl Normal file
View File

View File

@@ -1,81 +1,124 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de" class="{% if current_user.theme == 'dark' %}dark{% endif %}"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RACEPLANNER 2026</title> <title>RACEPLANNER 2026</title>
<script>
tailwind.config = {
darkMode: 'class',
}
</script>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
body { font-family: 'Inter', sans-serif; transition: background-color 0.3s, color 0.3s; } body { font-family: 'Inter', sans-serif; background-color: #0a0e17; color: #e2e8f0; }
.glass { background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(12px); border: 1px solid rgba(0,0,0,0.05); } .glass { background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.05); }
.dark .glass { background: rgba(15, 23, 42, 0.8); border: 1px solid rgba(255,255,255,0.05); }
.stint-card { transition: all 0.2s ease; border-left: 4px solid transparent; } .stint-card { transition: all 0.2s ease; border-left: 4px solid transparent; }
.stint-card:hover { transform: translateX(4px); } .stint-card:hover { transform: translateX(4px); background: rgba(30, 41, 59, 0.7); }
.dark .stint-card:hover { background: rgba(30, 41, 59, 0.7); }
.light .stint-card:hover { background: rgba(241, 245, 249, 1); }
.handle { cursor: grab; } .handle { cursor: grab; }
input, select { background: #0f172a; border: 1px solid #1e293b; color: white; padding: 0.5rem; border-radius: 0.375rem; }
</style> </style>
</head> </head>
<body class="bg-slate-50 text-slate-900 dark:bg-[#0a0e17] dark:text-e2e8f0 min-h-screen"> <body class="p-4 md:p-8">
<nav class="glass sticky top-0 z-50 px-6 py-4 flex justify-between items-center mb-8 border-b dark:border-white/5">
<div class="flex items-center gap-4">
<h1 class="text-2xl font-black italic tracking-tighter text-blue-600 dark:text-blue-500">RACEPLANNER <span class="text-slate-400">2026</span></h1>
</div>
<div class="flex items-center gap-4">
<!-- Theme Toggle -->
<button onclick="toggleTheme()" class="p-2 rounded-full bg-slate-200 dark:bg-slate-800 hover:ring-2 ring-blue-500 transition-all">
<span id="theme-icon-light" class="hidden dark:block">☀️</span>
<span id="theme-icon-dark" class="block dark:hidden">🌙</span>
</button>
<div class="text-right">
<div class="text-[10px] font-bold uppercase opacity-50">User</div>
<div class="text-sm font-bold">{{ current_user.username }}</div>
</div>
<a href="/logout" class="bg-red-500/10 text-red-500 px-4 py-2 rounded-lg text-xs font-bold uppercase hover:bg-red-500 hover:text-white transition-all">Logout</a>
</div>
</nav>
<div class="max-w-[1400px] mx-auto px-6 pb-20"> <div class="max-w-7xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div class="flex flex-col md:flex-row justify-between items-center mb-8 gap-4">
{% for car_num in [1, 2] %} <h1 class="text-3xl font-black italic tracking-tighter text-blue-500 uppercase">Raceplanner <span class="text-white">2026</span></h1>
{% set schedule = car1 if car_num == 1 else car2 %} <div class="flex gap-4 items-center glass p-4 rounded-xl">
<form action="/save_scenario" method="POST" class="flex gap-2">
<input type="text" name="scenario_name" placeholder="Scenario Name" class="text-xs w-32" required>
<button class="bg-green-600 hover:bg-green-500 text-[10px] font-bold px-3 py-2 rounded uppercase">Save</button>
</form>
<form action="/load_scenario" method="POST" class="flex gap-2 border-l border-slate-700 pl-4">
<select name="scenario_file" class="text-xs w-32">
{% for s in scenarios %}<option value="{{ s }}">{{ s }}</option>{% endfor %}
</select>
<button class="bg-blue-600 hover:bg-blue-500 text-[10px] font-bold px-3 py-2 rounded uppercase">Load</button>
</form>
<a href="/logout" class="text-xs text-slate-500 hover:text-white ml-2">Logout</a>
</div>
</div>
<form action="/update_config" method="POST" class="glass p-6 rounded-2xl mb-12 grid grid-cols-2 md:grid-cols-6 gap-6 items-end">
<div class="col-span-2">
<label class="block text-[10px] uppercase font-bold text-slate-500 mb-2">Race Start</label>
<input type="datetime-local" name="start_datetime" value="{{ config.start_time.strftime('%Y-%m-%dT%H:%M') }}" class="w-full font-mono text-sm">
</div>
<div><label class="block text-[10px] uppercase font-bold text-slate-500 mb-2">Dur(h)</label><input type="number" name="race_duration_hours" value="{{ config.race_duration_hours }}" class="w-full text-center"></div>
<div><label class="block text-[10px] uppercase font-bold text-slate-500 mb-2">Tank</label><input type="number" step="0.1" name="tank_capacity" value="{{ config.tank_capacity }}" class="w-full text-center"></div>
<div><label class="block text-[10px] uppercase font-bold text-slate-500 mb-2">Box(s)</label><input type="number" name="min_pit_stop_sec" value="{{ config.min_pit_stop_sec }}" class="w-full text-center"></div>
<div class="flex items-center gap-2 pb-3">
<input type="checkbox" name="always_change_tires" id="tires" {% if config.always_change_tires %}checked{% endif %}>
<label for="tires" class="text-[10px] uppercase font-bold text-slate-400">Force Tires</label>
<button type="submit" class="ml-4 bg-blue-600 hover:bg-blue-500 text-white font-black text-xs px-6 py-2 rounded uppercase shadow-lg shadow-blue-900/20">Apply</button>
</div>
</form>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
{% for car_num, drivers, schedule in [(1, car1_drivers, schedule1), (2, car2_drivers, schedule2)] %}
<div class="space-y-6"> <div class="space-y-6">
<div class="flex justify-between items-end border-b-2 border-blue-600 pb-2"> <div class="flex items-center justify-between border-b border-slate-800 pb-4">
<h2 class="text-4xl font-black italic">CAR <span class="text-blue-600">#{{ car_num }}</span></h2> <h2 class="text-2xl font-black italic uppercase"><span class="text-blue-500">#{{ car_num }}</span> Vehicle Dashboard</h2>
<a href="/export_pdf/{{ car_num }}" class="bg-blue-600 text-white px-4 py-2 rounded font-bold text-xs uppercase tracking-widest hover:bg-blue-700">PDF Export</a> <div class="flex gap-2">
<a href="/export/car/{{ car_num }}" target="_blank" class="bg-slate-800 hover:bg-slate-700 text-[10px] font-bold px-3 py-1.5 rounded uppercase">PDF Plan</a>
<a href="/generate_schedule/{{ car_num }}" class="bg-red-950 text-red-400 border border-red-900/50 text-[10px] font-bold px-3 py-1.5 rounded uppercase">Auto-Generate</a>
</div>
</div> </div>
<div id="stints-car-{{ car_num }}" class="space-y-3"> <div class="space-y-3">
{% for s in schedule %} {% for d in drivers %}
<div data-id="{{ s.id }}" class="stint-card glass p-4 rounded-xl flex items-center gap-4 border-l-4 {% if s.is_finish %}border-l-blue-500{% else %}border-l-slate-300 dark:border-l-slate-700{% endif %}"> <form action="/update_driver/{{ d.id }}" method="POST" class="glass p-4 rounded-xl flex items-center gap-4 stint-card">
<div class="handle text-slate-400 hover:text-blue-500"></div> <div class="w-8 h-8 rounded-full bg-orange-500/20 text-orange-500 flex items-center justify-center font-black text-xs">{{ stint_counts.get(d.id, 0) }}</div>
<div class="w-12 text-center"> <div class="flex-1"><input type="text" name="name" value="{{ d.name }}" class="bg-transparent border-none p-0 font-bold text-blue-400 w-full"></div>
<div class="text-[10px] font-bold text-slate-500">STINT</div> <div class="text-right"><label class="block text-[8px] uppercase text-slate-500">L/R</label><input type="number" step="0.1" name="cons" value="{{ d.cons_per_lap }}" class="w-16 bg-slate-900/50 text-[10px] text-center"></div>
<div class="text-xl font-black">#{{ s.number }}</div> <div class="text-right"><label class="block text-[8px] uppercase text-slate-500">Sec</label><input type="number" step="0.1" name="lap_time" value="{{ d.avg_lap_time }}" class="w-16 bg-slate-900/50 text-[10px] text-center"></div>
<div class="flex gap-2 ml-auto">
<button type="submit" class="bg-blue-600/20 text-blue-400 text-[10px] px-2 py-1 rounded font-black border border-blue-600/30">OK</button>
<a href="/export/driver/{{ d.id }}" target="_blank" class="bg-gray-800 px-2 py-1 rounded text-xs hover:bg-green-600">📄</a>
<a href="/driver/{{ d.id }}" class="bg-gray-800 px-2 py-1 rounded text-xs hover:bg-blue-600">👁️</a>
<a href="/delete_driver/{{ d.id }}" class="text-slate-600 hover:text-red-500"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12"/></svg></a>
</div> </div>
<div class="flex-1"> </form>
<div class="flex items-center gap-2 mb-1"> {% endfor %}
<span class="text-[10px] font-mono bg-slate-200 dark:bg-slate-800 px-2 py-0.5 rounded">{{ s.date }}</span> <form action="/add_driver" method="POST" class="flex gap-2 mt-4">
<span class="text-sm font-black">{{ s.start }} — {{ s.end }}</span> <input type="hidden" name="car_number" value="{{ car_num }}">
</div> <input type="text" name="name" placeholder="Driver Name" class="flex-1 text-sm h-10" required>
<select onchange="updateStintDriver({{ s.id }}, this.value)" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded px-2 py-1 text-sm font-bold focus:ring-2 ring-blue-500 outline-none"> <input type="number" step="0.1" name="cons" placeholder="L/R" class="w-20 text-sm h-10">
{% for d in drivers %} <input type="number" step="0.1" name="lap_time" placeholder="Sec" class="w-20 text-sm h-10">
<option value="{{ d.id }}" {% if d.id == s.driver_id %}selected{% endif %}>{{ d.name }}</option> <button class="bg-blue-600 text-white font-black text-[10px] px-6 py-2 rounded uppercase">Add</button>
{% endfor %} </form>
</div>
<div class="relative py-8">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
<div class="w-full border-t border-slate-800"></div>
</div>
<div class="relative flex justify-center">
<span class="bg-[#0a0e17] px-4 text-xs font-black text-slate-500 uppercase tracking-widest">Race Schedule #{{ car_num }}</span>
</div>
</div>
<div id="stints-car-{{ car_num }}" class="space-y-2">
{% for s in schedule %}
<div class="stint-item glass rounded-xl p-4 stint-card grid grid-cols-12 gap-2 items-center" data-id="{{ s.stint_id }}">
<div class="col-span-1 text-sm font-black text-blue-400">#{{ s.number }}</div>
<div class="col-span-1 handle text-slate-700 hover:text-slate-400">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M7 7h2v2H7V7zm0 4h2v2H7v-2zm4-4h2v2h-2V7zm0 4h2v2h-2v-2zM7 15h2v2H7v-2zm4 0h2v2h-2v-2z"/></svg>
</div>
<div class="col-span-2 font-mono">
<div class="text-[11px] font-bold text-slate-300 mb-0.5">{{ s.date }}</div>
<div class="text-sm font-black text-blue-500">{{ s.start }}</div>
<div class="text-[10px] text-slate-500">{{ s.end }}</div>
</div>
<div class="col-span-4">
<select onchange="updateStintDriver('{{ s.stint_id }}', this.value)" class="w-full bg-slate-900/80 border-slate-800 text-xs font-bold py-1">
{% for d in drivers %}<option value="{{ d.id }}" {% if d.id == s.driver_id %}selected{% endif %}>{{ d.name }}</option>{% endfor %}
</select> </select>
</div> </div>
<div class="text-right min-w-[80px]"> <div class="col-span-2 text-center">
<div class="text-[10px] font-bold text-green-600 dark:text-green-500">{{ s.fuel }}L</div> <div class="font-black">{{ s.laps }} R</div>
<div class="text-lg font-black">{{ s.laps }} <span class="text-[10px] text-slate-500 uppercase">Laps</span></div> <div class="text-[10px] text-green-500">{{ s.fuel }}L</div>
{% if s.is_finish %}<span class="bg-blue-600 text-white text-[8px] font-black px-2 py-1 rounded uppercase">Finish</span> </div>
{% elif s.change_tires %}<span class="bg-green-600/20 text-green-600 dark:text-green-500 text-[8px] font-black px-2 py-1 rounded border border-green-500/30 uppercase">Tires</span>{% endif %} <div class="col-span-2 text-right">
{% if s.is_finish %}<span class="bg-blue-600 text-[8px] font-black px-2 py-1 rounded text-white uppercase">Finish</span>
{% elif s.change_tires %}<span class="bg-green-600/20 text-green-500 text-[8px] font-black px-2 py-1 rounded border border-green-500/30 uppercase">Tires</span>{% endif %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@@ -86,23 +129,6 @@
</div> </div>
<script> <script>
function toggleTheme() {
const isDark = document.documentElement.classList.contains('dark');
const newTheme = isDark ? 'light' : 'dark';
if (newTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
fetch('/update_theme', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({theme: newTheme})
});
}
function updateStintDriver(stintId, driverId) { function updateStintDriver(stintId, driverId) {
fetch('/update_stint_driver', { fetch('/update_stint_driver', {
method: 'POST', method: 'POST',
@@ -110,7 +136,6 @@
body: JSON.stringify({stint_id: stintId, driver_id: driverId}) body: JSON.stringify({stint_id: stintId, driver_id: driverId})
}).then(() => window.location.reload()); }).then(() => window.location.reload());
} }
[1, 2].forEach(num => { [1, 2].forEach(num => {
const el = document.getElementById('stints-car-' + num); const el = document.getElementById('stints-car-' + num);
if(el) { if(el) {
@@ -129,4 +154,4 @@
}); });
</script> </script>
</body> </body>
</html> </html>

View File

@@ -5,24 +5,21 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RacePlanner Login</title> <title>RacePlanner Login</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: 'Inter', sans-serif; }
</style>
</head> </head>
<body class="bg-slate-100 flex items-center justify-center h-screen"> <body class="bg-gray-900 text-white flex items-center justify-center h-screen">
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-sm border border-slate-200"> <div class="bg-gray-800 p-8 rounded-2xl shadow-2xl w-full max-w-sm border border-gray-700">
<h1 class="text-3xl font-black text-blue-600 mb-6 text-center italic">RACEPLANNER</h1> <h1 class="text-3xl font-black text-blue-500 mb-6 text-center italic">RACEPLANNER</h1>
<form action="/login" method="POST" class="space-y-4"> <form action="/login" method="POST" class="space-y-4">
<div> <div>
<label class="block text-xs font-bold text-slate-400 uppercase mb-1">Benutzer</label> <label class="block text-xs font-bold text-gray-500 uppercase mb-1">Benutzer</label>
<input type="text" name="username" class="w-full bg-slate-50 border border-slate-200 rounded p-3 text-slate-900 focus:ring-2 ring-blue-500 outline-none" placeholder="z.B. mscaltenbach" required> <input type="text" name="username" class="w-full bg-gray-900 border-none rounded p-3" required>
</div> </div>
<div> <div>
<label class="block text-xs font-bold text-slate-400 uppercase mb-1">Passwort</label> <label class="block text-xs font-bold text-gray-500 uppercase mb-1">Passwort</label>
<input type="password" name="password" class="w-full bg-slate-50 border border-slate-200 rounded p-3 text-slate-900 focus:ring-2 ring-blue-500 outline-none" required> <input type="password" name="password" class="w-full bg-gray-900 border-none rounded p-3" required>
</div> </div>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-black py-3 rounded-lg uppercase tracking-widest transition-all">Anmelden</button> <button type="submit" class="w-full bg-blue-600 hover:bg-blue-500 font-bold py-3 rounded uppercase transition">Anmelden</button>
</form> </form>
</div> </div>
</body> </body>
</html> </html>