refacto: dashboard Flask Blueprints + templates Jinja2, engine run/lore_enricher mis a jour
This commit is contained in:
@@ -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})
|
||||
Reference in New Issue
Block a user