""" PipBoy — Fallout: Venice of Wasteland Dashboard unifié : Simulation | Outils | Lore enrichment | Paramètres (anciennement VAULTCOM) """ import os, json, subprocess, threading, time import psycopg2 import psycopg2.extras from flask import Flask, render_template_string, jsonify, request, redirect, url_for app = Flask(__name__) # --------------------------------------------------------------------------- # Stress test — état global (un seul test à la fois) # --------------------------------------------------------------------------- _stress_state = { "running": False, "started_at": None, "mode": None, "rounds": None, "pnj_count": None, "output": [], # lignes de stdout "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 DB_HOST = os.getenv("DB_HOST", "localhost") DB_PORT = int(os.getenv("DB_PORT", 5432)) DB_NAME = os.getenv("DB_NAME", "fallout") DB_USER = os.getenv("DB_USER", "fallout") DB_PASS = os.getenv("DB_PASS", "VeniceOfWasteland2026!") SIM_CONFIGS_DIR = os.getenv("SIM_CONFIGS_DIR", "/app/src/config") def get_conn(): return psycopg2.connect( host=DB_HOST, port=DB_PORT, dbname=DB_NAME, user=DB_USER, password=DB_PASS ) def query(sql, params=None): conn = get_conn() try: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute(sql, params) return [dict(r) for r in cur.fetchall()] finally: conn.close() def query_one(sql, params=None): rows = query(sql, params) return rows[0] if rows else None def execute(sql, params=None): conn = get_conn() try: with conn.cursor() as cur: cur.execute(sql, params) conn.commit() finally: conn.close() _REFERENCE_CONFIG_ID = 1 # sim_001.json = référence pour la liste des modes def load_sim_config(session_id: int) -> dict | None: """Retourne le cfg JSON ou None si introuvable (None est falsy ET distinguable de {}).""" path = os.path.join(SIM_CONFIGS_DIR, f"sim_{session_id:03d}.json") try: with open(path) as f: return json.load(f) except Exception as e: return None def load_all_modes() -> dict: """Charge les modes depuis sim_001.json (référence complète des 5 modes).""" cfg = load_sim_config(_REFERENCE_CONFIG_ID) or {} return cfg.get("modes", {}) def save_sim_config(session_id: int, cfg: dict): path = os.path.join(SIM_CONFIGS_DIR, f"sim_{session_id:03d}.json") with open(path, "w") as f: json.dump(cfg, f, indent=2, ensure_ascii=False) def get_all_sessions(): return query("SELECT * FROM sessions ORDER BY id") # --------------------------------------------------------------------------- # HTML Template # --------------------------------------------------------------------------- TEMPLATE = r""" PipBoy — Fallout: Venice of Wasteland
📟 PIPBOY
{% for s in all_sessions %} {% set is_active = s.status == 'active' %} S{{ s.id }} {% endfor %}
{% if flash_msg %}
{{ flash_msg }}
{% endif %}
{% if tab == 'sim' %} {% if session %}
Session #{{ session.id }} — {{ session.name }} Jour {{ session.current_day }} / Tick {{ session.current_tick }}h {{ session.mode | upper }} {% set st = session.status %} {{ st | upper }} Seed: {{ session.seed_global }} ↻ Actualiser
{% endif %}
PNJ Vivants
{{ alive_count }}
PNJ Morts
{{ stats.get('PNJ morts', 0) }}
Total PNJ
{{ total_count }}
Combats
{{ stats.get('Combats', 0) }}
Rencontres
{{ stats.get('Rencontres', 0) }}
Événements
{{ stats.get('Événements total', 0) }}

👥 PNJ ({{ alive_count }}/{{ total_count }})

{% for c in characters %} {% set pct = (c.hp / c.max_hp * 100) | int if c.max_hp else 0 %} {% endfor %}
TierNomPVFactionZoneCaps
{% if c.tier == 0 %}★ BOSS {% elif c.tier == 2 %}T2 {% elif c.tier == 3 %}T3 {% else %}T1{% endif %} {{ c.name }} {% if c.is_alive %} {{ c.hp }}/{{ c.max_hp }} {% else %}{% endif %} {% set f = c.faction_slug or 'sans' %} {{ f[:14] }} {{ (c.location_slug or '—')[:16] }} {{ c.caps }}¢

📡 Événements récents

{% for ev in events %} {% endfor %} {% if not events %}{% endif %}
J/TTypeDescription
{{ ev.day }}/{{ ev.tick }} {{ ev.event_type }} {{ ev.description[:90] }}{% if ev.description|length > 90 %}…{% endif %}
Aucun événement

🗺 État du monde

{% for ws in world_state %}
{{ ws.location_slug }} 🍖 {{ ws.food_level or 0 }}% 💧 {{ ws.water_level or 0 }}% 🛡 {{ ws.security_level or 0 }}% 👤 {{ ws.pop_count or 0 }}
{% endfor %} {% if not world_state %}

Pas de données monde

{% endif %}

⚔ Relations factions

{% for r in faction_relations %} {% set cls = 'rel-hostile' if r.relation_score < -20 else 'rel-allie' if r.relation_score > 20 else 'rel-neutral' %} {% endfor %} {% if not faction_relations %}{% endif %}
Faction AFaction BScoreÉtat
{{ r.faction_a[:14] }} {{ r.faction_b[:14] }} {{ r.relation_score }} {{ r.relation_label or ('HOSTILE' if r.relation_score < -20 else 'ALLIÉ' if r.relation_score > 20 else 'NEUTRE') }}
Pas de données

🎭 PNJ par tier

{% for row in tier_stats %} {% endfor %}
TierLabelVivantsMorts
{{ row.tier }} {{ row.label }} {{ row.alive }} {{ row.dead }}

🏙 PNJ par zone (top 5)

{% for row in zone_stats %} {% endfor %}
ZonePNJ
{{ row.location_slug }} {{ row.cnt }}

⚡ Rencontres récentes

{% for ev in encounters %} {% endfor %} {% if not encounters %}{% endif %}
J/TTypeActeurZoneDescription
{{ ev.day }}/{{ ev.tick }} {{ ev.event_type }} {{ ev.actor_name or '—' }} {{ (ev.location_slug or '—')[:16] }} {{ ev.description[:90] }}{% if ev.description|length > 90 %}…{% endif %}
Aucune rencontre
{% if boss_inventory %}

★ Équipement chefs de faction

{% for row in boss_inventory %} {% endfor %}
BossItemTypeQtéÉquipé
{{ row.char_name }} {{ row.item_name }} {{ row.item_type }} {{ row.quantity }} {{ '✓' if row.is_equipped else '' }}
{% endif %} {% elif tab == 'tools' %}

🚀 Lancer une simulation

{% for s in all_sessions %} {% endfor %}

    

📚 Lancer un enrichissement lore


    

⚡ Stress Test LLM

{% elif tab == 'lore' %}
📜 Enrichissement Lore — Venice of Wasteland
En attente : {{ lore_stats.pending }} Acceptées : {{ lore_stats.accepted }} Rejetées : {{ lore_stats.rejected }} Modifiées : {{ lore_stats.modified }}
{{ proposals|length }} proposition(s) affichée(s)
{% if not proposals %}
Aucune proposition lore.
Lancer : python3 lore_enricher.py --faction grand_krewe sur Ampère
{% endif %} {% for p in proposals %}
#{{ p.id }} {{ p.status | upper }} {{ p.change_type }} {{ p.faction_slug }} {% if p.field_path %}{{ p.field_path }}{% endif %} {{ p.created_at.strftime('%d/%m %H:%M') if p.created_at else '' }}

{{ p.rationale or '(pas de justification)' }}

Original
{{ p.original_text or '(ajout — pas de texte original)' }}
Proposé
{{ p.proposed_text }}
{% if p.status == 'pending' %}
{% elif p.status == 'modified' and p.modified_text %}
Texte modifié accepté
{{ p.modified_text }}
{% endif %}
{% endfor %} {% elif tab == 'params' %} {% if session %}
Session #{{ session.id }} — {{ session.name }} {{ session.mode | upper }} {% set st = session.status %} {{ st | upper }} Jour {{ session.current_day }} / Tick {{ session.current_tick }}h
{% endif %}

⚙ Config sim_{{ '%03d' % current_sid }}.json

{% if cfg is not none %}

Général

Survie

Rencontres

Économie

Prise en compte au prochain jour (J{{ (session.current_day or 0) + 1 }}). Pour effet immédiat : STOP → relancer via terminal.

{% else %}

Fichier sim_{{ '%03d' % current_sid }}.json introuvable dans {{ sim_configs_dir }}

Vérifier le mount Docker : /home/ubuntu/fallout-venice/src/config → /app/src/config

{% endif %}

🔧 Gestion session #{{ current_sid }}

Statut DB

STOP → la sim s'arrête dans les 5 ticks suivants. Puis RESET JOUR/TICK → relancer via terminal.

Reset jour/tick

Modes disponibles

{% for m_name, m_vals in modes.items() %} {% endfor %}
ModeDrainEncountersCaps
{{ m_name }}{% if m_name == cfg.get('mode','') %} ◀{% endif %} ×{{ m_vals.get('survival',{}).get('drain_speed','—') }} ×{{ m_vals.get('encounter',{}).get('global_rate_multiplier','—') }} ×{{ m_vals.get('economy',{}).get('caps_global_multiplier','—') }}

Commandes SSH (référence)

Copier-coller dans votre terminal SSH. Pour les commandes générées, utiliser l'onglet OUTILS.

tail -f ~/fallout_sim_s{{ current_sid }}.log
{% endif %}
""" # --------------------------------------------------------------------------- # Helpers data # --------------------------------------------------------------------------- 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, ) # --------------------------------------------------------------------------- # Routes # --------------------------------------------------------------------------- @app.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_string(TEMPLATE, tab="sim", current_sid=sid, all_sessions=all_sessions, flash_msg=None, flash_ok=True, modes={}, cfg={}, sim_configs_dir=SIM_CONFIGS_DIR, **data, ) @app.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_string(TEMPLATE, tab="tools", current_sid=sid, all_sessions=all_sessions, session=session, flash_msg=None, flash_ok=True, cfg=None, modes={}, sim_configs_dir=SIM_CONFIGS_DIR, proposals=[], lore_stats={"pending":0,"accepted":0,"rejected":0,"modified":0}, all_factions=[], filt_faction="", filt_status="", characters=[], alive_count=0, total_count=0, events=[], encounters=[], world_state=[], faction_relations=[], tier_stats=[], zone_stats=[], stats={}, boss_inventory=[], ) @app.route("/api/stress-test/results") def api_stress_test_results(): 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: import datetime 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}"}) @app.route("/api/stress-test/run", methods=["POST"]) def api_stress_test_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é."}) @app.route("/api/stress-test/status") def api_stress_test_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:], # 60 dernières lignes "returncode": _stress_state["returncode"], "error": _stress_state["error"], }) @app.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() # toujours les 5 modes depuis sim_001 (référence) flash_msg = request.args.get("msg") flash_ok = request.args.get("ok", "1") == "1" return render_template_string(TEMPLATE, 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, # unused in params tab but required by template proposals=[], lore_stats={"pending":0,"accepted":0,"rejected":0,"modified":0}, all_factions=[], filt_faction="", filt_status="", characters=[], alive_count=0, total_count=0, events=[], encounters=[], world_state=[], faction_relations=[], tier_stats=[], zone_stats=[], stats={}, boss_inventory=[], ) @app.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", 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", sid=sid, msg=msg, ok=ok)) @app.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}'. Le process run.py sortira au prochain tick si arrêté." ok = 1 except Exception as e: msg = f"Erreur: {e}" ok = 0 return redirect(url_for("params", sid=sid, msg=msg, ok=ok)) @app.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", sid=sid, msg=msg, ok=ok)) CHROMA_URL = os.getenv("CHROMA_URL", "http://localhost:8800") CHROMA_COL_OUT = os.getenv("CHROMA_COL_OUT", "fallout_lore_enriched") CHROMA_BASE = f"{CHROMA_URL}/api/v2/tenants/default_tenant/databases/default_database" def _chroma_post(path: str, body: dict): import urllib.request data = __import__("json").dumps(body).encode() req = urllib.request.Request(f"{CHROMA_BASE}{path}", data=data, method="POST", headers={"Content-Type": "application/json"}) try: with urllib.request.urlopen(req, timeout=10) as r: return __import__("json").loads(r.read()) except Exception: return {} def _chroma_get_enriched_col_id(): import urllib.request try: with urllib.request.urlopen(f"{CHROMA_BASE}/collections", timeout=5) as r: cols = __import__("json").loads(r.read()) for c in cols: if c["name"] == CHROMA_COL_OUT: return c["id"] except Exception: pass # Créer la collection si absente res = _chroma_post("/collections", {"name": CHROMA_COL_OUT, "metadata": {"hnsw:space": "cosine"}}) return res.get("id") def _upsert_to_chroma_enriched(proposal: dict): col_id = _chroma_get_enriched_col_id() if not col_id: return text = proposal.get("modified_text") or proposal.get("proposed_text", "") doc_id = f"lore_proposal_{proposal['id']}" _chroma_post(f"/collections/{col_id}/upsert", { "ids": [doc_id], "documents": [text], "metadatas": [{ "faction_slug": proposal.get("faction_slug", ""), "change_type": proposal.get("change_type", ""), "field_path": proposal.get("field_path") or "", "category": "lore_enriched", "source": f"lore_proposal_{proposal['id']}", }], }) def get_lore_proposals(faction: str = "", status: str = "pending") -> list[dict]: 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_lore_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_all_factions() -> list[str]: rows = query("SELECT DISTINCT faction_slug FROM lore_proposals ORDER BY faction_slug") return [r["faction_slug"] for r in rows] @app.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_lore_proposals(filt_faction, filt_status) flash_msg = request.args.get("msg") flash_ok = request.args.get("ok", "1") == "1" return render_template_string(TEMPLATE, tab="lore", current_sid=sid, all_sessions=all_sessions, session=session, proposals=proposals, lore_stats=get_lore_stats(), all_factions=get_all_factions(), filt_faction=filt_faction, filt_status=filt_status, flash_msg=flash_msg, flash_ok=flash_ok, cfg=None, modes={}, sim_configs_dir=SIM_CONFIGS_DIR, characters=[], alive_count=0, total_count=0, events=[], encounters=[], world_state=[], faction_relations=[], tier_stats=[], zone_stats=[], stats={}, boss_inventory=[], ) @app.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", 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_to_chroma_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_to_chroma_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", msg=msg, ok=ok, faction=filt_faction, status=filt_status)) @app.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}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)