fix: Threading-Lock verhindert parallele Scans, Status-API, Button-Feedback

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Cursor 2026-02-25 16:50:01 +07:00
parent ba0893a337
commit 639e6a4fc3
2 changed files with 103 additions and 36 deletions

View file

@ -7,6 +7,10 @@ from datetime import datetime, timedelta
from db import init_db, get_conn, log
from ki import auswerten
# Verhindert dass zwei Läufe gleichzeitig laufen
_scan_lock = threading.Lock()
_lauf_aktiv = False
def get_nodes():
conn = get_conn()
@ -105,55 +109,88 @@ def speichere_preise(results, node_name, job):
def scraping_lauf(label="Standard", flex_tage_liste=None):
"""
Führt alle aktiven Jobs auf allen Nodes aus.
flex_tage_liste: Liste von 'tage'-Werten für Datums-Flexibilität (z.B. [27,28,30,32,33])
Übersprungen wenn ein anderer Lauf noch aktiv ist (Lock-Schutz).
"""
log(f"=== Scraping-Lauf [{label}] gestartet ===")
nodes = get_nodes()
jobs = get_aktive_jobs()
global _lauf_aktiv
if not nodes:
log("Keine aktiven Nodes konfiguriert", "WARN")
if not _scan_lock.acquire(blocking=False):
log(f"Lauf [{label}] übersprungen — vorheriger Lauf noch aktiv", "WARN")
return
tage_varianten = flex_tage_liste or [None] # None = Job-Default verwenden
_lauf_aktiv = True
start = datetime.now()
online = 0
for node in nodes:
if node_ping(node):
update_node_status(node["name"], "online")
online += 1
for job in jobs:
for tage_var in tage_varianten:
dispatch_job(node, job, tage_override=tage_var)
else:
log(f"Node {node['name']} nicht erreichbar", "WARN")
update_node_status(node["name"], "offline")
try:
log(f"=== Scraping-Lauf [{label}] gestartet ===")
nodes = get_nodes()
jobs = get_aktive_jobs()
log(f"Scraping [{label}] abgeschlossen — {online}/{len(nodes)} Nodes online")
auswerten()
log(f"=== Lauf [{label}] beendet ===")
if not nodes:
log("Keine aktiven Nodes konfiguriert", "WARN")
return
tage_varianten = flex_tage_liste or [None]
online = 0
fehler = 0
preise_gesamt = 0
for node in nodes:
if node_ping(node):
update_node_status(node["name"], "online")
online += 1
for job in jobs:
for tage_var in tage_varianten:
try:
ok = dispatch_job(node, job, tage_override=tage_var)
if not ok:
fehler += 1
except Exception as e:
log(f"Job-Fehler {node['name']}/{job['scanner']}: {e}", "ERROR")
fehler += 1
else:
log(f"Node {node['name']} nicht erreichbar", "WARN")
update_node_status(node["name"], "offline")
dauer = round((datetime.now() - start).total_seconds())
log(f"Scraping [{label}] fertig — {online}/{len(nodes)} Nodes | "
f"{fehler} Fehler | {dauer}s Laufzeit")
try:
auswerten()
except Exception as e:
log(f"KI-Fehler: {e}", "ERROR")
log(f"=== Lauf [{label}] beendet ===")
finally:
_lauf_aktiv = False
_scan_lock.release()
def standard_lauf():
"""Normaler 4×-täglich Lauf mit Standard-Datum."""
scraping_lauf(label=datetime.now().strftime("%H:%M"))
"""30-Minuten-Takt — in eigenem Thread damit der Scheduler nicht blockiert."""
threading.Thread(
target=scraping_lauf,
kwargs={"label": datetime.now().strftime("%H:%M")},
daemon=True
).start()
def flex_lauf():
"""
Di/Mi Nacht: ±3 Tage Datums-Flexibilität.
Sucht Abflug in 27-33 Tagen statt nur 30 Tagen.
"""
wochentag = datetime.now().weekday() # 0=Mo, 1=Di, 2=Mi
"""Di/Mi 23:30 — ±3 Tage Datumsfenster."""
wochentag = datetime.now().weekday()
if wochentag not in (1, 2):
log("Flex-Lauf: heute kein Di/Mi — übersprungen")
return
# 7 Varianten: 30±3 Tage
basis = 30
flex_varianten = list(range(basis - 3, basis + 4)) # [27,28,29,30,31,32,33]
flex_varianten = list(range(basis - 3, basis + 4))
log(f"=== Flex-Lauf Di/Mi ±3 Tage: {flex_varianten} ===")
scraping_lauf(label="Flex-Di/Mi", flex_tage_liste=flex_varianten)
threading.Thread(
target=scraping_lauf,
kwargs={"label": "Flex-Di/Mi", "flex_tage_liste": flex_varianten},
daemon=True
).start()
def run():

View file

@ -114,7 +114,7 @@ OVERVIEW_HTML = BASE_HTML.replace("{% block content %}{% endblock %}", """
<div style="margin-top:0.5rem;font-size:0.75rem;color:#475569" id="ki-datum"></div>
</div>
<div style="margin-top:1rem">
<button class="btn btn-green" onclick="manuellScrapen()"> Jetzt scrapen</button>
<button id="scan-btn" class="btn btn-green" onclick="manuellScrapen()"> Jetzt scrapen</button>
</div>
</div>
</div>
@ -289,13 +289,34 @@ async function ladeUebersicht() {
});
}
async function pruefeScanStatus() {
const d = await fetch('/api/scrape/status').then(r=>r.json());
const btn = document.getElementById('scan-btn');
if (d.running) {
btn.textContent = '⏳ Läuft...';
btn.disabled = true;
btn.style.background = '#92400e';
setTimeout(pruefeScanStatus, 5000);
} else {
btn.textContent = '▶ Jetzt scrapen';
btn.disabled = false;
btn.style.background = '';
}
}
async function manuellScrapen() {
if (!confirm('Scraping jetzt starten?')) return;
const r = await fetch('/api/scrape/now', {method:'POST'});
const d = await r.json();
alert(d.message);
if (r.status === 409) {
alert(d.message);
return;
}
pruefeScanStatus();
}
pruefeScanStatus();
setInterval(pruefeScanStatus, 10000);
ladeUebersicht();
</script>
""")
@ -491,12 +512,21 @@ def api_schedule():
@app.route("/api/scrape/now", methods=["POST"])
def api_scrape_now():
from scheduler import _lauf_aktiv
if _lauf_aktiv:
return jsonify({"message": "⚠ Lauf bereits aktiv — bitte warten", "running": True}), 409
threading.Thread(
target=scraping_lauf,
kwargs={"label": "Manuell"},
daemon=True
).start()
return jsonify({"message": "Scraping gestartet — läuft im Hintergrund"})
return jsonify({"message": "Scraping gestartet — läuft im Hintergrund", "running": False})
@app.route("/api/scrape/status")
def api_scrape_status():
from scheduler import _lauf_aktiv
return jsonify({"running": _lauf_aktiv})
# ─── Seiten ────────────────────────────────────────────────────────────────────