Compare commits

..

2 Commits

Author SHA1 Message Date
b22df1dc56 Wiederherstellung von Funktionalität 2026-01-23 23:24:42 +01:00
744a84663e Corrections of SQL Alchemy error 2026-01-23 23:13:03 +01:00
3 changed files with 252 additions and 126 deletions

View File

@@ -7,11 +7,13 @@ 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__)
# --- Konfiguration --- # --- Konfiguration ---
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:////app/data/raceplanner.db') 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['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'renn-strategie-2026-final-v3' app.config['SECRET_KEY'] = 'renn-strategie-2026-final-v3'
app.config['REMEMBER_COOKIE_DURATION'] = timedelta(days=31) app.config['REMEMBER_COOKIE_DURATION'] = timedelta(days=31)
@@ -32,7 +34,7 @@ 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(80), 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' theme = db.Column(db.String(20), default='light')
class RaceConfig(db.Model): class RaceConfig(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@@ -61,23 +63,19 @@ def load_user(user_id):
# --- Hilfsfunktionen --- # --- Hilfsfunktionen ---
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).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
fuel_capacity = 120.0 # Liter (Beispielhaft fixiert) fuel_capacity = 120.0
for i, stint in enumerate(stints): for i, stint in enumerate(stints):
driver = stint.driver driver = stint.driver
if not driver: continue if not driver: continue
laps_possible = int(fuel_capacity / (driver.cons_per_lap or 1.0))
# Vereinfachte Berechnung: Ein Stint ist voll, wenn der Tank leer ist duration_sec = laps_possible * (driver.avg_lap_time or 120.0)
laps_possible = int(fuel_capacity / driver.cons_per_lap)
duration_sec = laps_possible * driver.avg_lap_time
start_str = current_time.strftime('%H:%M') start_str = current_time.strftime('%H:%M')
end_time = current_time + timedelta(seconds=duration_sec) end_time = current_time + timedelta(seconds=duration_sec)
schedule.append({ schedule.append({
'id': stint.id, 'id': stint.id,
'number': i + 1, 'number': i + 1,
@@ -89,12 +87,62 @@ def get_calculated_schedule(car_num):
'laps': laps_possible, 'laps': laps_possible,
'fuel': round(fuel_capacity, 1), 'fuel': round(fuel_capacity, 1),
'change_tires': stint.change_tires, 'change_tires': stint.change_tires,
'is_finish': (current_time + timedelta(seconds=duration_sec)) >= (config.start_time + timedelta(hours=config.total_duration_h)) 'is_finish': end_time >= (config.start_time + timedelta(hours=config.total_duration_h))
}) })
current_time = end_time current_time = end_time
return schedule return schedule
# --- Routen --- # --- 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('/') @app.route('/')
@login_required @login_required
def index(): def index():
@@ -120,6 +168,7 @@ def update_theme():
def update_stint_driver(): def update_stint_driver():
data = request.json data = request.json
s = db.session.get(Stint, data['stint_id']) s = db.session.get(Stint, data['stint_id'])
if s:
s.driver_id = data['driver_id'] s.driver_id = data['driver_id']
db.session.commit() db.session.commit()
return jsonify({'status': 'ok'}) return jsonify({'status': 'ok'})
@@ -130,7 +179,7 @@ def reorder_stints():
order = request.json['order'] order = request.json['order']
for idx, sid in enumerate(order): for idx, sid in enumerate(order):
s = db.session.get(Stint, int(sid)) s = db.session.get(Stint, int(sid))
s.order = idx if s: s.order = idx
db.session.commit() db.session.commit()
return jsonify({'status': 'ok'}) return jsonify({'status': 'ok'})
@@ -157,7 +206,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)
@@ -169,14 +218,43 @@ def logout():
logout_user() logout_user()
return redirect(url_for('login')) return redirect(url_for('login'))
if __name__ == '__main__': def init_db():
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
# Initialer User falls nicht vorhanden try:
if not User.query.filter_by(username='mscaltenbach').first(): db.session.execute(text("ALTER TABLE user ADD COLUMN theme VARCHAR(20) DEFAULT 'light'"))
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() 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) app.run(host='0.0.0.0', port=5000, debug=True)

View File

@@ -1,50 +1,83 @@
<!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>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
:root {
--bg-body: {{ '#0a0e17' if current_user.theme == 'dark' else '#f8fafc' }};
--bg-card: {{ '#0f172a' if current_user.theme == 'dark' else '#ffffff' }};
--text-main: {{ '#e2e8f0' if current_user.theme == 'dark' else '#1e293b' }};
--border-color: {{ 'rgba(255,255,255,0.05)' if current_user.theme == 'dark' else '#e2e8f0' }};
}
body {
background-color: var(--bg-body);
color: var(--text-main);
font-family: 'Inter', sans-serif;
transition: background 0.3s ease;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
</style>
</head> </head>
<body class="bg-slate-50 text-slate-900 dark:bg-[#0a0e17] dark:text-white p-8 transition-colors"> <body class="p-4 md: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"> <!-- Header -->
<h1 class="text-4xl font-black uppercase italic">{{ driver.name }} <span class="text-blue-500">#{{ driver.car_number }}</span></h1> <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-12 gap-6">
<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>
</div>
<div class="grid grid-cols-1 md: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="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>
<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="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>
<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="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>
</div>
<h2 class="text-xl font-bold mb-4 uppercase text-slate-400">Geplante Einsätze</h2>
<div class="space-y-2">
{% 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> <div>
<div class="text-[10px] text-slate-500 font-mono">{{ s.date }}</div> <h1 class="text-5xl font-black italic uppercase tracking-tighter">
<div class="text-lg font-black">{{ s.start }} - {{ s.end }}</div> {{ driver.name }} <span class="text-blue-600">#{{ driver.car_number }}</span>
</h1>
<p class="text-slate-500 font-bold uppercase tracking-widest text-xs mt-2">Driver Profile & Schedule</p>
</div>
<a href="/" class="bg-blue-600 text-white px-8 py-4 rounded-2xl text-xs uppercase font-black hover:bg-blue-700 transition-all shadow-lg shadow-blue-600/20">Zurück zum Planner</a>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-12">
<div class="card p-6 rounded-3xl">
<div class="text-[10px] text-slate-500 uppercase font-bold mb-1 tracking-widest">Verbrauch / Runde</div>
<div class="text-4xl font-black text-green-500">{{ driver.cons_per_lap }}<span class="text-lg ml-1">L</span></div>
</div>
<div class="card p-6 rounded-3xl">
<div class="text-[10px] text-slate-500 uppercase font-bold mb-1 tracking-widest">Ø Rundenzeit</div>
<div class="text-4xl font-black text-blue-600">{{ driver.avg_lap_time }}<span class="text-lg ml-1">s</span></div>
</div>
<div class="card p-6 rounded-3xl">
<div class="text-[10px] text-slate-500 uppercase font-bold mb-1 tracking-widest">Anzahl Stints</div>
<div class="text-4xl font-black text-orange-500">{{ stints|length }}</div>
</div>
</div>
<!-- Schedule -->
<h2 class="text-2xl font-black mb-6 uppercase tracking-tight italic flex items-center gap-3">
<span class="w-8 h-1 bg-blue-600 rounded-full"></span>
Einsatzplanung
</h2>
<div class="space-y-4">
{% for s in stints %}
<div class="card p-6 rounded-3xl flex justify-between items-center border-l-8 border-blue-600 transition-transform hover:scale-[1.01]">
<div>
<div class="text-[10px] text-slate-500 font-bold uppercase tracking-widest mb-1">{{ s.date }}</div>
<div class="text-3xl font-black tracking-tight">{{ 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-black uppercase text-blue-600 mb-1">Stint #{{ s.number }}</div>
<div class="text-sm font-black">{{ s.laps }} Runden</div> <div class="text-xs font-bold text-slate-500">{{ s.laps }} Runden · {{ s.fuel }}L Start</div>
</div> </div>
</div> </div>
{% else %} {% else %}
<div class="text-center py-12 text-slate-500 italic">Keine Stints für diesen Fahrer geplant.</div> <div class="text-center py-20 card rounded-[2rem] opacity-50">
<p class="font-black uppercase tracking-widest text-slate-400">Keine Stints geplant</p>
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@@ -1,81 +1,82 @@
<!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; } :root {
.glass { background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(12px); border: 1px solid rgba(0,0,0,0.05); } --bg-body: {{ '#0a0e17' if current_user.theme == 'dark' else '#f8fafc' }};
.dark .glass { background: rgba(15, 23, 42, 0.8); border: 1px solid rgba(255,255,255,0.05); } --bg-card: {{ '#0f172a' if current_user.theme == 'dark' else '#ffffff' }};
.stint-card { transition: all 0.2s ease; border-left: 4px solid transparent; } --text-main: {{ '#e2e8f0' if current_user.theme == 'dark' else '#1e293b' }};
.stint-card:hover { transform: translateX(4px); } --text-muted: {{ '#64748b' if current_user.theme == 'dark' else '#94a3b8' }};
.dark .stint-card:hover { background: rgba(30, 41, 59, 0.7); } --border-color: {{ 'rgba(255,255,255,0.05)' if current_user.theme == 'dark' else '#e2e8f0' }};
.light .stint-card:hover { background: rgba(241, 245, 249, 1); } }
body { font-family: 'Inter', sans-serif; background-color: var(--bg-body); color: var(--text-main); transition: background 0.3s ease; }
.glass { background: var(--bg-card); border: 1px solid var(--border-color); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
.stint-card { transition: all 0.2s ease; border-left: 4px solid transparent; background: {{ 'rgba(30, 41, 59, 0.5)' if current_user.theme == 'dark' else '#f1f5f9' }}; }
.stint-card:hover { transform: translateX(4px); background: {{ 'rgba(30, 41, 59, 0.8)' if current_user.theme == 'dark' else '#e2e8f0' }}; }
.handle { cursor: grab; } .handle { cursor: grab; }
select { background: {{ '#1e293b' if current_user.theme == 'dark' else '#ffffff' }}; border: 1px solid var(--border-color); color: var(--text-main); outline: none; }
</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="max-w-7xl mx-auto">
<div class="flex items-center gap-4"> <!-- Header -->
<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> <header class="flex flex-col md:flex-row justify-between items-center mb-10 gap-4">
<div>
<h1 class="text-4xl font-black italic tracking-tighter text-blue-600">RACEPLANNER <span class="text-slate-400 not-italic font-light">2026</span></h1>
<p class="text-xs font-bold uppercase tracking-widest text-slate-500">Endurance Strategy Engine</p>
</div> </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="flex flex-wrap items-center gap-4 justify-center">
<!-- Scenario Actions -->
<button onclick="saveScenarioPrompt()" class="bg-emerald-600 hover:bg-emerald-700 text-white text-[10px] font-black px-4 py-2 rounded-full uppercase tracking-widest">Sichern</button>
<button onclick="loadScenarioList()" class="bg-slate-600 hover:bg-slate-700 text-white text-[10px] font-black px-4 py-2 rounded-full uppercase tracking-widest">Laden</button>
<div class="flex bg-slate-200 dark:bg-slate-800 p-1 rounded-lg">
<button onclick="setTheme('light')" class="px-3 py-1 rounded {{ 'bg-white shadow-sm' if current_user.theme == 'light' else '' }} text-xs font-bold uppercase">Light</button>
<button onclick="setTheme('dark')" class="px-3 py-1 rounded {{ 'bg-blue-600 text-white shadow-sm' if current_user.theme == 'dark' else '' }} text-xs font-bold uppercase">Dark</button>
</div>
<a href="/logout" class="text-xs font-bold uppercase px-4 py-2 border border-red-500/50 text-red-500 rounded hover:bg-red-500 transition-all">Logout</a>
</div>
</header>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
{% for car_num in [1, 2] %} {% for car_num in [1, 2] %}
{% set schedule = car1 if car_num == 1 else car2 %} <div class="glass rounded-3xl overflow-hidden p-6">
<div class="space-y-6"> <div class="flex justify-between items-center mb-6">
<div class="flex justify-between items-end border-b-2 border-blue-600 pb-2"> <h2 class="text-2xl font-black italic">CAR <span class="text-blue-600">#{{ car_num }}</span></h2>
<h2 class="text-4xl font-black italic">CAR <span class="text-blue-600">#{{ car_num }}</span></h2> <a href="/export_pdf/{{ car_num }}" class="bg-blue-600 hover:bg-blue-700 text-white text-[10px] font-black px-4 py-2 rounded-full uppercase tracking-widest">PDF Export</a>
<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> </div>
<div id="stints-car-{{ car_num }}" class="space-y-3"> <div id="stints-car-{{ car_num }}" class="space-y-3">
{% set schedule = car1 if car_num == 1 else car2 %}
{% for s in schedule %} {% for s in schedule %}
<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 %}"> <div class="stint-card p-4 rounded-xl flex items-center gap-4 group {{ 'border-blue-600' if s.is_finish else 'border-slate-400/20' }}" data-id="{{ s.id }}">
<div class="handle text-slate-400 hover:text-blue-500"></div> <div class="handle text-slate-500 opacity-20 group-hover:opacity-100">
<div class="w-12 text-center"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M7 10l5-5 5 5M7 14l5 5 5-5"/></svg>
<div class="text-[10px] font-bold text-slate-500">STINT</div> </div>
<div class="text-xl font-black">#{{ s.number }}</div> <div class="w-16">
<div class="text-[10px] font-bold text-slate-500 uppercase">{{ s.date }}</div>
<div class="text-lg font-black">{{ s.start }}</div>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-2 mb-1"> <select onchange="updateStintDriver({{ s.id }}, this.value)" class="text-sm font-bold p-1 rounded">
<span class="text-[10px] font-mono bg-slate-200 dark:bg-slate-800 px-2 py-0.5 rounded">{{ s.date }}</span>
<span class="text-sm font-black">{{ s.start }} — {{ s.end }}</span>
</div>
<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">
{% for d in drivers %} {% for d in drivers %}
<option value="{{ d.id }}" {% if d.id == s.driver_id %}selected{% endif %}>{{ d.name }}</option> <option value="{{ d.id }}" {{ 'selected' if d.id == s.driver_id else '' }}>{{ d.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<div class="text-[10px] font-bold text-slate-500 uppercase mt-1">
{{ s.laps }} Runden · <span class="text-green-500">{{ s.fuel }}L</span>
</div> </div>
<div class="text-right min-w-[80px]"> </div>
<div class="text-[10px] font-bold text-green-600 dark:text-green-500">{{ s.fuel }}L</div> <div class="text-right">
<div class="text-lg font-black">{{ s.laps }} <span class="text-[10px] text-slate-500 uppercase">Laps</span></div> <div class="text-lg font-black">{{ s.end }}</div>
{% if s.is_finish %}<span class="bg-blue-600 text-white text-[8px] font-black px-2 py-1 rounded uppercase">Finish</span> {% if s.is_finish %}<span class="bg-blue-600 text-white text-[8px] font-black px-2 py-1 rounded uppercase">Finish</span>{% endif %}
{% 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> </div>
</div> </div>
{% endfor %} {% endfor %}
@@ -85,32 +86,50 @@
</div> </div>
</div> </div>
<!-- Modals (Simple Replacement for alert/prompt) -->
<div id="scenario-modal" class="hidden fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div class="glass max-w-md w-full p-8 rounded-3xl">
<h3 id="modal-title" class="text-xl font-black mb-4 uppercase italic">Szenario</h3>
<div id="modal-content" class="space-y-4"></div>
<div class="flex justify-end gap-4 mt-6">
<button onclick="closeModal()" class="text-xs font-bold uppercase text-slate-500">Abbrechen</button>
</div>
</div>
</div>
<script> <script>
function toggleTheme() { function setTheme(theme) {
const isDark = document.documentElement.classList.contains('dark'); fetch('/update_theme', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({theme: theme})}).then(() => window.location.reload());
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', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({stint_id: stintId, driver_id: driverId})}).then(() => window.location.reload());
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({stint_id: stintId, driver_id: driverId})
}).then(() => window.location.reload());
} }
function saveScenarioPrompt() {
const name = prompt("Name für das Szenario:");
if(name) {
fetch('/save_scenario', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name: name})})
.then(r => r.json()).then(() => alert("Gespeichert!"));
}
}
function loadScenarioList() {
fetch('/list_scenarios').then(r => r.json()).then(files => {
const list = files.map(f => `<button onclick="loadScenario('${f}')" class="w-full text-left p-3 hover:bg-blue-600 rounded-lg text-sm font-bold border border-white/5 mb-2">${f}</button>`).join('');
document.getElementById('modal-title').innerText = "Szenario Laden";
document.getElementById('modal-content').innerHTML = list || "Keine Szenarien gefunden.";
document.getElementById('scenario-modal').classList.remove('hidden');
});
}
function loadScenario(file) {
fetch('/load_scenario', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({filename: file})})
.then(() => window.location.reload());
}
function closeModal() { document.getElementById('scenario-modal').classList.add('hidden'); }
[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) {
@@ -118,11 +137,7 @@
handle: '.handle', animation: 150, handle: '.handle', animation: 150,
onEnd: function() { onEnd: function() {
const order = Array.from(el.children).map(item => item.dataset.id); const order = Array.from(el.children).map(item => item.dataset.id);
fetch('/reorder_stints', { fetch('/reorder_stints', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({order: order})}).then(() => window.location.reload());
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({order: order})
}).then(() => window.location.reload());
} }
}); });
} }