refacto: dashboard Flask Blueprints + templates Jinja2, engine run/lore_enricher mis a jour

This commit is contained in:
2026-06-17 14:10:01 +00:00
parent ae6356ded4
commit ea8e24e238
24 changed files with 2457 additions and 1850 deletions
View File
+95
View File
@@ -0,0 +1,95 @@
from flask import Blueprint, render_template, jsonify, request, redirect, url_for
from ..db import query, query_one, execute, get_all_sessions
from ..chroma import upsert_lore_enriched, CHROMA_COL_OUT
bp = Blueprint("lore", __name__)
def _get_proposals(faction: str = "", status: str = "pending") -> list:
sql = "SELECT * FROM lore_proposals WHERE 1=1"
params = []
if faction:
sql += " AND faction_slug = %s"
params.append(faction)
if status:
sql += " AND status = %s"
params.append(status)
sql += " ORDER BY id DESC LIMIT 100"
return query(sql, params or None)
def _get_stats() -> dict:
rows = query("SELECT status, COUNT(*) as n FROM lore_proposals GROUP BY status")
m = {r["status"]: r["n"] for r in rows}
return {"pending": m.get("pending", 0), "accepted": m.get("accepted", 0),
"rejected": m.get("rejected", 0), "modified": m.get("modified", 0)}
def _get_factions() -> list:
rows = query("SELECT DISTINCT faction_slug FROM lore_proposals ORDER BY faction_slug")
return [r["faction_slug"] for r in rows]
@bp.route("/lore")
def lore():
all_sessions = get_all_sessions()
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
session = query_one("SELECT * FROM sessions WHERE id = %s", (sid,))
filt_faction = request.args.get("faction", "")
filt_status = request.args.get("status", "pending")
proposals = _get_proposals(filt_faction, filt_status)
flash_msg = request.args.get("msg")
flash_ok = request.args.get("ok", "1") == "1"
# Threads de discussion
threads_raw = query("SELECT * FROM lore_proposal_threads ORDER BY created_at ASC") if proposals else []
proposal_threads = {}
for row in (threads_raw or []):
pid = row["proposal_id"]
proposal_threads.setdefault(pid, []).append(row)
return render_template("lore.html",
tab="lore", current_sid=sid, all_sessions=all_sessions, session=session,
proposals=proposals, lore_stats=_get_stats(),
all_factions=_get_factions(), filt_faction=filt_faction, filt_status=filt_status,
proposal_threads=proposal_threads, flash_msg=flash_msg, flash_ok=flash_ok,
)
@bp.route("/lore/action", methods=["POST"])
def lore_action():
prop_id = int(request.form.get("id", 0))
action = request.form.get("action", "")
modified_text = request.form.get("modified_text", "").strip()
filt_faction = request.form.get("filt_faction", "")
filt_status = request.form.get("filt_status", "pending")
proposal = query_one("SELECT * FROM lore_proposals WHERE id = %s", (prop_id,))
if not proposal:
return redirect(url_for("lore.lore", msg="Proposition introuvable", ok=0,
faction=filt_faction, status=filt_status))
try:
if action == "accept":
execute("UPDATE lore_proposals SET status='accepted', reviewed_at=NOW() WHERE id=%s", (prop_id,))
upsert_lore_enriched({**proposal, "modified_text": None})
msg = f"Proposition #{prop_id} acceptée et indexée dans {CHROMA_COL_OUT}."
elif action == "reject":
execute("UPDATE lore_proposals SET status='rejected', reviewed_at=NOW() WHERE id=%s", (prop_id,))
msg = f"Proposition #{prop_id} rejetée."
elif action == "modify":
if not modified_text:
modified_text = proposal["proposed_text"]
execute("""UPDATE lore_proposals SET status='modified', modified_text=%s, reviewed_at=NOW()
WHERE id=%s""", (modified_text, prop_id))
upsert_lore_enriched({**proposal, "modified_text": modified_text})
msg = f"Proposition #{prop_id} modifiée et indexée dans {CHROMA_COL_OUT}."
else:
msg = "Action inconnue."
ok = 1
except Exception as e:
msg = f"Erreur: {e}"
ok = 0
return redirect(url_for("lore.lore", msg=msg, ok=ok,
faction=filt_faction, status=filt_status))
+83
View File
@@ -0,0 +1,83 @@
from flask import Blueprint, render_template, request, redirect, url_for
from ..db import query_one, execute, get_all_sessions
from ..config import SIM_CONFIGS_DIR, load_sim_config, save_sim_config, load_all_modes
bp = Blueprint("params", __name__)
@bp.route("/params")
def params():
all_sessions = get_all_sessions()
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
session = query_one("SELECT * FROM sessions WHERE id = %s", (sid,))
cfg = load_sim_config(sid)
modes = load_all_modes()
flash_msg = request.args.get("msg")
flash_ok = request.args.get("ok", "1") == "1"
return render_template("params.html",
tab="params", current_sid=sid, all_sessions=all_sessions,
session=session, cfg=cfg, modes=modes, sim_configs_dir=SIM_CONFIGS_DIR,
flash_msg=flash_msg, flash_ok=flash_ok,
)
@bp.route("/params/save", methods=["POST"])
def params_save():
sid = int(request.args.get("sid", 1))
cfg = load_sim_config(sid)
if cfg is None:
return redirect(url_for("params.params", sid=sid,
msg=f"Fichier sim_{sid:03d}.json introuvable dans {SIM_CONFIGS_DIR}", ok=0))
f = request.form
cfg["name"] = f.get("name", cfg.get("name", ""))
cfg["mode"] = f.get("mode", cfg.get("mode", "pacifiste"))
cfg.setdefault("tick", {})["tick_sleep_sec"] = int(f.get("tick_sleep_sec", 60))
cfg.setdefault("survival", {}).update({
"drain_speed": float(f.get("survival_drain_speed", 1.0)),
"fatigue_hp_per_2pts": int(f.get("survival_fatigue_hp", 2)),
"enabled": "survival_enabled" in f,
})
cfg.setdefault("encounter", {}).update({
"global_rate_multiplier": float(f.get("encounter_rate", 1.0)),
"enabled": "encounter_enabled" in f,
})
cfg.setdefault("economy", {})["caps_global_multiplier"] = float(f.get("caps_multiplier", 1.0))
try:
save_sim_config(sid, cfg)
execute("UPDATE sessions SET mode=%s WHERE id=%s", (cfg["mode"], sid))
msg = f"Config sim_{sid:03d}.json sauvegardée. Mode DB mis à jour → {cfg['mode']}."
ok = 1
except Exception as e:
msg = f"Erreur: {e}"
ok = 0
return redirect(url_for("params.params", sid=sid, msg=msg, ok=ok))
@bp.route("/params/setstatus", methods=["POST"])
def params_setstatus():
sid = int(request.args.get("sid", 1))
status = request.form.get("status", "paused")
try:
execute("UPDATE sessions SET status=%s WHERE id=%s", (status, sid))
msg = f"Session {sid} → status '{status}'."
ok = 1
except Exception as e:
msg = f"Erreur: {e}"
ok = 0
return redirect(url_for("params.params", sid=sid, msg=msg, ok=ok))
@bp.route("/params/resetday", methods=["POST"])
def params_resetday():
sid = int(request.args.get("sid", 1))
day = int(request.form.get("day", 1))
tick = int(request.form.get("tick", 0))
try:
execute("UPDATE sessions SET current_day=%s, current_tick=%s WHERE id=%s", (day, tick, sid))
msg = f"Session {sid} remise à J{day} T{tick}h."
ok = 1
except Exception as e:
msg = f"Erreur: {e}"
ok = 0
return redirect(url_for("params.params", sid=sid, msg=msg, ok=ok))
+261
View File
@@ -0,0 +1,261 @@
import os
import subprocess
import threading
from flask import Blueprint, render_template, jsonify, request, send_file
from ..db import query, query_one, execute, get_all_sessions
from ..config import SIM_CONFIGS_DIR
bp = Blueprint("sim", __name__)
_sim_procs = {
"truth": {"proc": None, "output": [], "running": False, "returncode": None, "error": None, "sid": None},
"enriched": {"proc": None, "output": [], "running": False, "returncode": None, "error": None, "sid": None},
}
_sim_lock = threading.Lock()
def _run_sim_bg(mode, sid, tick_speed_s):
env = {**os.environ,
"SESSION_ID": str(sid),
"TICK_SLEEP_SEC": str(tick_speed_s),
"PYTHONUNBUFFERED": "1"}
if mode == "enriched":
env["USE_ENRICHED"] = "1"
try:
row = query_one(
"INSERT INTO sim_runs (mode,session_id,tick_speed_s,status,started_at) "
"VALUES (%s,%s,%s,'running',NOW()) RETURNING id",
(mode, sid, tick_speed_s))
run_id = row["id"] if row else None
except Exception:
run_id = None
with _sim_lock:
_sim_procs[mode].update({"output": [], "running": True, "returncode": None, "error": None, "sid": sid})
try:
proc = subprocess.Popen(
["python3", "-u", "/app/src/engine/run.py"],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, env=env)
with _sim_lock:
_sim_procs[mode]["proc"] = proc
for line in proc.stdout:
with _sim_lock:
_sim_procs[mode]["output"].append(line.rstrip())
proc.wait()
with _sim_lock:
_sim_procs[mode]["returncode"] = proc.returncode
_sim_procs[mode]["running"] = False
if run_id:
try:
execute("UPDATE sim_runs SET status='stopped',stopped_at=NOW() WHERE id=%s", (run_id,))
except Exception:
pass
except Exception as e:
with _sim_lock:
_sim_procs[mode]["error"] = str(e)
_sim_procs[mode]["running"] = False
def _get_sim_data(sid: int) -> dict:
session = query_one("SELECT * FROM sessions WHERE id = %s", (sid,))
if not session:
session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1")
characters = query("""
SELECT c.*, COALESCE(c.tier, 1) as tier
FROM characters c
WHERE c.session_id = %s
ORDER BY COALESCE(c.tier,1), c.is_alive DESC, c.name
""", (sid,))
alive_count = sum(1 for c in characters if c["is_alive"])
events = query("""
SELECT e.*, c.name as actor_name FROM events e
LEFT JOIN characters c ON c.id = e.actor_id
WHERE e.session_id = %s
AND e.event_type NOT IN ('encounter_lambda','encounter_group','encounter_actif','encounter_boss','cycle_tick')
ORDER BY e.day DESC, e.tick DESC, e.id DESC LIMIT 50
""", (sid,))
encounters = query("""
SELECT e.*, c.name as actor_name FROM events e
LEFT JOIN characters c ON c.id = e.actor_id
WHERE e.session_id = %s AND e.event_type LIKE 'encounter%%'
ORDER BY e.day DESC, e.tick DESC, e.id DESC LIMIT 30
""", (sid,))
world_state = query("""
SELECT DISTINCT ON (location_slug) *
FROM world_state WHERE session_id = %s
ORDER BY location_slug, day DESC
""", (sid,))
faction_relations = query("""
SELECT * FROM faction_relations WHERE session_id = %s
ORDER BY relation_score ASC
""", (sid,))
tier_labels = {0: "BOSS", 1: "ACTIFS SIM", 2: "PNJ+", 3: "PASSAGE"}
tier_raw = query("""
SELECT COALESCE(tier,1) as tier,
COUNT(*) FILTER (WHERE is_alive) as alive,
COUNT(*) FILTER (WHERE NOT is_alive) as dead
FROM characters WHERE session_id = %s
GROUP BY COALESCE(tier,1) ORDER BY COALESCE(tier,1)
""", (sid,))
tier_stats = [{"tier": r["tier"], "label": tier_labels.get(r["tier"], "?"),
"alive": r["alive"], "dead": r["dead"]} for r in tier_raw]
zone_stats = query("""
SELECT location_slug, COUNT(*) as cnt FROM characters
WHERE session_id = %s AND is_alive = TRUE
GROUP BY location_slug ORDER BY cnt DESC LIMIT 5
""", (sid,))
ev_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s", (sid,))
cb_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='combat'", (sid,))
dt_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='death'", (sid,))
enc_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type LIKE 'encounter%%'", (sid,))
stats = {
"Événements total": ev_total["n"] if ev_total else 0,
"Combats": cb_total["n"] if cb_total else 0,
"Morts": dt_total["n"] if dt_total else 0,
"Rencontres": enc_total["n"] if enc_total else 0,
"PNJ vivants": alive_count,
"PNJ morts": len(characters) - alive_count,
}
boss_inventory = query("""
SELECT c.name as char_name, i.item_name, i.item_type, i.quantity, i.is_equipped
FROM inventory i
JOIN characters c ON c.id = i.character_id
WHERE c.session_id = %s AND COALESCE(c.tier,1) = 0
ORDER BY c.name, i.is_equipped DESC
""", (sid,))
return dict(
session=session,
characters=characters,
alive_count=alive_count,
total_count=len(characters),
events=events,
encounters=encounters,
world_state=world_state,
faction_relations=faction_relations,
tier_stats=tier_stats,
zone_stats=zone_stats,
stats=stats,
boss_inventory=boss_inventory,
)
@bp.route("/")
def index():
all_sessions = get_all_sessions()
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
data = _get_sim_data(sid)
return render_template("sim.html", tab="sim", current_sid=sid,
all_sessions=all_sessions, flash_msg=None, flash_ok=True, **data)
@bp.route("/api/sim/<mode>/run", methods=["POST"])
def sim_run(mode):
if mode not in ("truth", "enriched"):
return jsonify({"error": "mode invalide"}), 400
with _sim_lock:
if _sim_procs[mode].get("running"):
return jsonify({"error": f"Simulation {mode} déjà en cours"}), 400
data = request.get_json(silent=True) or {}
sid = int(data.get("sid", 1))
tick_speed_s = int(data.get("tick_speed_s", 5))
t = threading.Thread(target=_run_sim_bg, args=(mode, sid, tick_speed_s), daemon=True)
t.start()
return jsonify({"ok": True, "sid": sid, "mode": mode})
@bp.route("/api/sim/<mode>/status")
def sim_status(mode):
if mode not in ("truth", "enriched"):
return jsonify({"error": "mode invalide"}), 400
with _sim_lock:
state = {k: v for k, v in _sim_procs[mode].items() if k != "proc"}
return jsonify(state)
@bp.route("/api/sim/<mode>/stop", methods=["POST"])
def sim_stop(mode):
if mode not in ("truth", "enriched"):
return jsonify({"error": "mode invalide"}), 400
with _sim_lock:
p = _sim_procs[mode].get("proc")
if p:
try:
p.terminate()
except Exception:
pass
return jsonify({"ok": True})
@bp.route("/api/sim/<mode>/log")
def sim_log(mode):
if mode not in ("truth", "enriched"):
return jsonify({"error": "mode invalide"}), 400
import io
import json as _json
import datetime as _dt
run = query_one("SELECT * FROM sim_runs WHERE mode=%s ORDER BY id DESC LIMIT 1", (mode,))
if not run:
return jsonify({"error": "Aucun run trouvé — lancez d'abord une simulation"}), 404
sid = run["session_id"]
session = query_one("SELECT * FROM sessions WHERE id=%s", (sid,))
events = query("SELECT * FROM events WHERE session_id=%s ORDER BY id DESC LIMIT 500", (sid,))
with _sim_lock:
output_lines = list(_sim_procs[mode].get("output", []))
log_data = {
"meta": {
"sim_mode": mode,
"session_id": sid,
"session_name": session.get("name", "") if session else "",
"world": "Venice of Wasteland, Louisiane post-apo",
"tick_speed_s": run["tick_speed_s"],
"started_at": run["started_at"].isoformat() if run.get("started_at") else None,
"stopped_at": run["stopped_at"].isoformat() if run.get("stopped_at") else None,
"status": run["status"],
"exported_at": _dt.datetime.now().isoformat(),
},
"console_output": output_lines,
"recent_events": [dict(e) for e in events],
}
buf = io.BytesIO(_json.dumps(log_data, ensure_ascii=False, indent=2, default=str).encode("utf-8"))
return send_file(buf, mimetype="application/json", as_attachment=True,
download_name=f"sim_{mode}_{sid}.json")
@bp.route("/api/session/set-mode", methods=["POST"])
def session_set_mode():
data = request.get_json(silent=True) or {}
sid = int(data.get("sid", 1))
mode = data.get("mode", "").strip()
valid = {"pacifiste", "politique", "guerre_commerciale", "guerre", "survie_extreme"}
if mode not in valid:
return jsonify({"error": f"Mode invalide: {mode}"}), 400
try:
execute("UPDATE sessions SET mode=%s WHERE id=%s", (mode, sid))
return jsonify({"ok": True, "sid": sid, "mode": mode})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/api/state")
def api_state():
all_sessions = get_all_sessions()
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
session = query_one("SELECT * FROM sessions WHERE id=%s", (sid,))
alive = query("""
SELECT id, name, hp, max_hp, tier, faction_slug, location_slug
FROM characters WHERE session_id=%s AND is_alive=TRUE
""", (sid,))
return jsonify({"session": session, "alive_pnj": alive})
+138
View File
@@ -0,0 +1,138 @@
import os
import json
import urllib.request
from flask import Blueprint, render_template, jsonify, request, redirect, url_for
from ..db import query, query_one, execute, get_all_sessions
from ..chroma import upsert_sim_enriched, upsert_sim_reject, CHROMA_SIM_COL
bp = Blueprint("sim_proposals", __name__)
def _get_proposals(ptype: str = "", status: str = "pending") -> list:
sql = "SELECT * FROM sim_proposals WHERE 1=1"
params = []
if ptype:
sql += " AND proposal_type = %s"
params.append(ptype)
if status:
sql += " AND status = %s"
params.append(status)
sql += " ORDER BY id DESC LIMIT 100"
return query(sql, params or None)
def _get_stats() -> dict:
rows = query("SELECT status, COUNT(*) as n FROM sim_proposals GROUP BY status")
m = {r["status"]: r["n"] for r in rows}
return {"pending": m.get("pending", 0), "accepted": m.get("accepted", 0),
"rejected": m.get("rejected", 0)}
def _get_types() -> list:
rows = query("SELECT DISTINCT proposal_type FROM sim_proposals ORDER BY proposal_type")
return [r["proposal_type"] for r in rows]
@bp.route("/sim-proposals")
def sim_proposals_view():
all_sessions = get_all_sessions()
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
filt_type = request.args.get("type", "")
filt_status = request.args.get("status", "pending")
proposals = _get_proposals(filt_type, filt_status)
flash_msg = request.args.get("msg")
flash_ok = request.args.get("ok", "1") == "1"
threads_raw = query("SELECT * FROM sim_proposal_threads ORDER BY created_at ASC") if proposals else []
proposal_threads = {}
for row in (threads_raw or []):
pid = row["proposal_id"]
proposal_threads.setdefault(pid, []).append(row)
return render_template("sim_proposals.html",
tab="sim_proposals", current_sid=sid, all_sessions=all_sessions,
proposals=proposals, sim_stats=_get_stats(), sim_types=_get_types(),
filt_type=filt_type, filt_status=filt_status,
proposal_threads=proposal_threads, flash_msg=flash_msg, flash_ok=flash_ok,
)
@bp.route("/sim-proposals/action", methods=["POST"])
def sim_proposals_action():
prop_id = int(request.form.get("id", 0))
action = request.form.get("action", "")
filt_type = request.form.get("filt_type", "")
filt_status = request.form.get("filt_status", "pending")
proposal = query_one("SELECT * FROM sim_proposals WHERE id = %s", (prop_id,))
if not proposal:
return redirect(url_for("sim_proposals.sim_proposals_view", msg="Proposition introuvable", ok=0))
try:
if action == "accept":
execute("UPDATE sim_proposals SET status='accepted', reviewed_at=NOW() WHERE id=%s", (prop_id,))
upsert_sim_enriched(dict(proposal))
msg = f"Proposition #{prop_id} acceptée → indexée dans {CHROMA_SIM_COL}."
elif action == "reject":
reason = request.form.get("reject_reason", "").strip()
execute("UPDATE sim_proposals SET status='rejected', reviewed_at=NOW(), reject_reason=%s WHERE id=%s",
(reason or None, prop_id))
if reason:
try:
upsert_sim_reject(dict(proposal), reason)
except Exception:
pass
msg = f"Proposition #{prop_id} rejetée." + (" Motif indexé." if reason else "")
else:
msg = "Action inconnue."
ok = 1
except Exception as e:
msg = f"Erreur: {e}"
ok = 0
return redirect(url_for("sim_proposals.sim_proposals_view", msg=msg, ok=ok,
type=filt_type, status=filt_status))
@bp.route("/sim-proposals/<int:prop_id>/discuss", methods=["POST"])
def sim_proposals_discuss(prop_id):
proposal = query_one("SELECT * FROM sim_proposals WHERE id=%s", (prop_id,))
if not proposal:
return jsonify({"error": "Proposition introuvable"}), 404
data = request.get_json(silent=True) or {}
user_msg = data.get("message", "").strip()
temperature = float(data.get("temperature", 0.75))
num_predict = int(data.get("num_predict", 512))
if not user_msg:
return jsonify({"error": "Message vide"}), 400
execute("INSERT INTO sim_proposal_threads (proposal_id,role,content,temperature,num_predict) VALUES (%s,'user',%s,%s,%s)",
(prop_id, user_msg, temperature, num_predict))
context = [
"Tu es un assistant de game design pour un JDR Fallout post-apocalyptique (Louisiane, 'Venice of Wasteland').",
f"Proposition de règle : [{proposal.get('proposal_type','')}] {proposal.get('title','')}",
f"Description : {proposal.get('description','')}",
f"Rationale : {proposal.get('rationale','')}",
"",
f"Le maître de jeu demande : {user_msg}",
"",
"Réponds de façon concise et pratique. Si tu proposes une version modifiée, structure-la clairement.",
]
ollama_url = os.getenv("OLLAMA_URL", "http://10.8.0.2:11434")
model = os.getenv("MODEL_MJ", "qwen2.5:14b")
try:
payload = json.dumps({"model": model, "prompt": "\n".join(context), "stream": False,
"options": {"temperature": temperature, "num_predict": num_predict}}).encode()
req = urllib.request.Request(f"{ollama_url}/api/generate",
data=payload, headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=120) as resp:
result = json.loads(resp.read())
llm_response = result.get("response", "").strip()
except Exception as e:
return jsonify({"error": f"Erreur LLM : {e}"}), 500
execute("INSERT INTO sim_proposal_threads (proposal_id,role,content,temperature,num_predict) VALUES (%s,'llm',%s,%s,%s)",
(prop_id, llm_response, temperature, num_predict))
return jsonify({"ok": True, "response": llm_response})
+150
View File
@@ -0,0 +1,150 @@
import os
import json
import time
import threading
import subprocess
from flask import Blueprint, render_template, jsonify, request
from ..db import query_one, get_all_sessions
from ..config import SIM_CONFIGS_DIR
from .. import process_mgr as pm
bp = Blueprint("tools", __name__)
# ── Stress test state ─────────────────────────────────────────────────────────
_stress_state = {
"running": False, "started_at": None, "mode": None,
"rounds": None, "pnj_count": None,
"output": [], "returncode": None, "error": None,
}
_stress_lock = threading.Lock()
def _run_stress_bg(mode: str, rounds: int, pnj_count: int):
global _stress_state
ollama_url = os.getenv("OLLAMA_URL", "http://host.docker.internal:11434")
model_mj = os.getenv("MODEL_MJ", "qwen2.5:14b")
model_pnj = os.getenv("MODEL_PNJ", "qwen2.5:7b")
results_path = os.path.join(SIM_CONFIGS_DIR, "crash_results.json")
cmd = [
"python3", "/app/tools/llm_crash_test.py",
"--mode", mode, "--rounds", str(rounds), "--pnj-count", str(pnj_count),
]
env = {**os.environ,
"OLLAMA_URL": ollama_url, "MODEL_MJ": model_mj, "MODEL_PNJ": model_pnj,
"CRASH_RESULTS_PATH": results_path, "PYTHONUNBUFFERED": "1"}
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, env=env)
for line in proc.stdout:
with _stress_lock:
_stress_state["output"].append(line.rstrip())
proc.wait()
with _stress_lock:
_stress_state["returncode"] = proc.returncode
_stress_state["running"] = False
except Exception as e:
with _stress_lock:
_stress_state["error"] = str(e)
_stress_state["running"] = False
# ── Routes ────────────────────────────────────────────────────────────────────
@bp.route("/tools")
def tools():
all_sessions = get_all_sessions()
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
session = query_one("SELECT * FROM sessions WHERE id = %s", (sid,))
return render_template("tools.html", tab="tools", current_sid=sid,
all_sessions=all_sessions, session=session,
flash_msg=None, flash_ok=True)
@bp.route("/api/stress-test/run", methods=["POST"])
def api_stress_run():
global _stress_state
with _stress_lock:
if _stress_state["running"]:
return jsonify({"error": "Un test est déjà en cours.", "running": True})
mode = request.json.get("mode", "simultane")
rounds = int(request.json.get("rounds", 5))
pnj_count = int(request.json.get("pnj_count", 2))
_stress_state = {
"running": True, "started_at": time.strftime("%H:%M:%S"),
"mode": mode, "rounds": rounds, "pnj_count": pnj_count,
"output": [], "returncode": None, "error": None,
}
t = threading.Thread(target=_run_stress_bg, args=(mode, rounds, pnj_count), daemon=True)
t.start()
return jsonify({"ok": True, "message": f"Test '{mode}' {rounds} rounds lancé."})
@bp.route("/api/stress-test/status")
def api_stress_status():
with _stress_lock:
return jsonify({
"running": _stress_state["running"],
"started_at": _stress_state["started_at"],
"mode": _stress_state["mode"],
"rounds": _stress_state["rounds"],
"output": _stress_state["output"][-60:],
"returncode": _stress_state["returncode"],
"error": _stress_state["error"],
})
@bp.route("/api/stress-test/results")
def api_stress_results():
import datetime
path = os.path.join(SIM_CONFIGS_DIR, "crash_results.json")
if not os.path.exists(path):
return jsonify({"error": "Aucun résultat. Lancez d'abord le test."})
try:
mtime = os.path.getmtime(path)
file_date = datetime.datetime.fromtimestamp(mtime).strftime("%d/%m/%Y %H:%M:%S")
with open(path) as f:
data = json.load(f)
data["file_date"] = file_date
return jsonify(data)
except Exception as e:
return jsonify({"error": f"Erreur lecture : {e}"})
@bp.route("/api/proc/<name>/run", methods=["POST"])
def proc_run(name):
if name == "mix":
if pm.proc_is_running("mix"):
return jsonify({"error": "Mix déjà en cours"}), 400
data = request.get_json(silent=True) or {}
count = int(data.get("count", 10))
pm.proc_mix_start(count)
return jsonify({"ok": True, "label": f"Mode MIX — {count if count else ''}× chaque"})
if name not in pm._PROC_DEFS:
return jsonify({"error": f"Processus inconnu: {name}"}), 400
if pm.proc_is_running(name):
return jsonify({"error": "Déjà en cours"}), 400
defn = pm._PROC_DEFS[name]
data = request.get_json(silent=True) or {}
count = int(data.get("count", 1))
cmd = list(defn["cmd"])
if name == "sim_enricher" and data.get("type"):
try: cmd[cmd.index("all")] = data["type"]
except ValueError: pass
if name == "lore_enricher" and data.get("faction"):
try: cmd[cmd.index("all")] = data["faction"]
except ValueError: pass
pm.proc_start(name, cmd, defn.get("env_extra", {}), count)
return jsonify({"ok": True, "label": defn["label"]})
@bp.route("/api/proc/<name>/status")
def proc_status(name):
return jsonify(pm.proc_get_state(name))
@bp.route("/api/proc/<name>/stop", methods=["POST"])
def proc_stop_route(name):
pm.proc_stop(name)
return jsonify({"ok": True})