refacto: dashboard Flask Blueprints + templates Jinja2, engine run/lore_enricher mis a jour
This commit is contained in:
@@ -8,3 +8,6 @@ __pycache__/
|
||||
Thumbs.db
|
||||
INFRA_PRIVATE.md
|
||||
.env
|
||||
*.bak
|
||||
*.bak_*
|
||||
docker-compose.yml.bak_pre_refacto
|
||||
|
||||
+11
-1840
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
import os
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
CHROMA_URL = os.getenv("CHROMA_URL", "http://localhost:8800")
|
||||
CHROMA_COL_OUT = os.getenv("CHROMA_COL_OUT", "fallout_lore_enriched")
|
||||
CHROMA_SIM_COL = os.getenv("CHROMA_SIM_COL", "fallout_sim_enriched")
|
||||
CHROMA_BASE = f"{CHROMA_URL}/api/v2/tenants/default_tenant/databases/default_database"
|
||||
|
||||
|
||||
def _chroma_post(path: str, body: dict) -> dict:
|
||||
data = 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 json.loads(r.read())
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _get_or_create_col(name: str) -> str | None:
|
||||
try:
|
||||
with urllib.request.urlopen(f"{CHROMA_BASE}/collections", timeout=5) as r:
|
||||
cols = json.loads(r.read())
|
||||
for c in cols:
|
||||
if c["name"] == name:
|
||||
return c["id"]
|
||||
except Exception:
|
||||
pass
|
||||
res = _chroma_post("/collections", {"name": name, "metadata": {"hnsw:space": "cosine"}})
|
||||
return res.get("id")
|
||||
|
||||
|
||||
def upsert_lore_enriched(proposal: dict):
|
||||
col_id = _get_or_create_col(CHROMA_COL_OUT)
|
||||
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": doc_id,
|
||||
}],
|
||||
})
|
||||
|
||||
|
||||
def upsert_sim_enriched(proposal: dict):
|
||||
col_id = _get_or_create_col(CHROMA_SIM_COL)
|
||||
if not col_id:
|
||||
return
|
||||
patch_str = json.dumps(proposal.get("json_patch") or {}, ensure_ascii=False)
|
||||
text = (
|
||||
f"{proposal.get('title','')}\n"
|
||||
f"{proposal.get('description','')}\n"
|
||||
f"Patch: {patch_str}\n"
|
||||
f"Rationale: {proposal.get('rationale','')}"
|
||||
)
|
||||
doc_id = f"sim_proposal_{proposal['id']}"
|
||||
_chroma_post(f"/collections/{col_id}/upsert", {
|
||||
"ids": [doc_id],
|
||||
"documents": [text],
|
||||
"metadatas": [{
|
||||
"proposal_type": proposal.get("proposal_type", ""),
|
||||
"title": proposal.get("title", ""),
|
||||
"category": "sim_enriched",
|
||||
"source": doc_id,
|
||||
}],
|
||||
})
|
||||
|
||||
|
||||
def upsert_sim_reject(proposal: dict, reason: str):
|
||||
col_id = _get_or_create_col(CHROMA_SIM_COL + "_rejects")
|
||||
if not col_id:
|
||||
return
|
||||
neg_text = (
|
||||
f"REJET — {proposal.get('proposal_type','')}: {proposal.get('title','')}\n"
|
||||
f"Motif: {reason}\n"
|
||||
f"Description: {proposal.get('description','')}"
|
||||
)
|
||||
_chroma_post(f"/collections/{col_id}/upsert", {
|
||||
"ids": [f"reject_{proposal['id']}"],
|
||||
"documents": [neg_text],
|
||||
"metadatas": [{
|
||||
"proposal_type": proposal.get("proposal_type", ""),
|
||||
"category": "negative_example",
|
||||
"source": f"reject_{proposal['id']}",
|
||||
}],
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
import json
|
||||
|
||||
SIM_CONFIGS_DIR = os.getenv("SIM_CONFIGS_DIR", "/app/src/config")
|
||||
|
||||
_REFERENCE_CONFIG_ID = 1
|
||||
|
||||
|
||||
def load_sim_config(session_id: int) -> dict | None:
|
||||
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:
|
||||
return None
|
||||
|
||||
|
||||
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 load_all_modes() -> dict:
|
||||
cfg = load_sim_config(_REFERENCE_CONFIG_ID) or {}
|
||||
return cfg.get("modes", {})
|
||||
@@ -0,0 +1,45 @@
|
||||
import os
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
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_PASSWORD", "VeniceOfWasteland2026!")
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def get_all_sessions():
|
||||
return query("SELECT * FROM sessions ORDER BY id")
|
||||
@@ -0,0 +1,152 @@
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
_procs = {}
|
||||
_procs_lock = threading.Lock()
|
||||
|
||||
_PROC_DEFS = {
|
||||
"sim_enricher": {
|
||||
"label": "Enrichissement règles sim (LLM → sim_proposals)",
|
||||
"cmd": ["python3", "-u", "/opt/coyote/pipelines/sim_enricher.py", "--type", "all"],
|
||||
"env_extra": {"CHROMA_URL": "http://chromadb:8000"},
|
||||
},
|
||||
"lore_enricher": {
|
||||
"label": "Enrichissement lore (LLM → lore_proposals)",
|
||||
"cmd": ["python3", "-u", "/app/src/engine/lore_enricher.py", "--faction", "all"],
|
||||
"env_extra": {"CHROMA_URL": "http://chromadb:8000"},
|
||||
},
|
||||
"embed_fallout": {
|
||||
"label": "Re-embed PDFs Fallout → fallout_lore",
|
||||
"cmd": ["python3", "-u", "/opt/coyote/pipelines/embed_fallout.py"],
|
||||
"env_extra": {"CHROMA_URL": "http://chromadb:8000"},
|
||||
},
|
||||
"embed_sim": {
|
||||
"label": "Re-embed règles sim → fallout_sim_rules",
|
||||
"cmd": ["python3", "-u", "/opt/coyote/pipelines/embed_sim.py"],
|
||||
"env_extra": {"CHROMA_URL": "http://chromadb:8000"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _run_proc_loop_bg(name, cmd, env_extra=None, count=1):
|
||||
env = {**os.environ, **(env_extra or {}), "PYTHONUNBUFFERED": "1"}
|
||||
with _procs_lock:
|
||||
_procs[name] = {"proc": None, "output": [], "running": True,
|
||||
"returncode": None, "error": None, "iteration": 0, "total": count}
|
||||
iteration = 0
|
||||
try:
|
||||
while count == 0 or iteration < count:
|
||||
iteration += 1
|
||||
sep = f"─── Itération {iteration}" + (f"/{count}" if count else "/∞") + " ───"
|
||||
with _procs_lock:
|
||||
_procs[name]["output"].append(sep)
|
||||
_procs[name]["iteration"] = iteration
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, env=env)
|
||||
with _procs_lock:
|
||||
_procs[name]["proc"] = proc
|
||||
for line in proc.stdout:
|
||||
with _procs_lock:
|
||||
_procs[name]["output"].append(line.rstrip())
|
||||
proc.wait()
|
||||
if proc.returncode is not None and proc.returncode < 0:
|
||||
break
|
||||
with _procs_lock:
|
||||
if not _procs[name]["running"]:
|
||||
break
|
||||
rc = proc.returncode
|
||||
if rc != 0:
|
||||
with _procs_lock:
|
||||
_procs[name]["returncode"] = rc
|
||||
break
|
||||
with _procs_lock:
|
||||
if _procs[name]["returncode"] is None:
|
||||
_procs[name]["returncode"] = 0
|
||||
_procs[name]["running"] = False
|
||||
except Exception as e:
|
||||
with _procs_lock:
|
||||
_procs[name]["error"] = str(e)
|
||||
_procs[name]["running"] = False
|
||||
|
||||
|
||||
def _run_mix_bg(count=10):
|
||||
name = "mix"
|
||||
sim_cmd = list(_PROC_DEFS["sim_enricher"]["cmd"])
|
||||
lore_cmd = list(_PROC_DEFS["lore_enricher"]["cmd"])
|
||||
env = {**os.environ, "CHROMA_URL": "http://chromadb:8000", "PYTHONUNBUFFERED": "1"}
|
||||
with _procs_lock:
|
||||
_procs[name] = {"proc": None, "output": [], "running": True,
|
||||
"returncode": None, "error": None, "iteration": 0, "total": count * 2}
|
||||
iteration = 0
|
||||
try:
|
||||
for script_name, cmd in [("sim_enricher", sim_cmd), ("lore_enricher", lore_cmd)]:
|
||||
i = 0
|
||||
while count == 0 or i < count:
|
||||
i += 1
|
||||
iteration += 1
|
||||
sep = f"─── MIX {script_name} {i}" + (f"/{count}" if count else "/∞") + " ───"
|
||||
with _procs_lock:
|
||||
if not _procs[name]["running"]:
|
||||
raise StopIteration
|
||||
_procs[name]["output"].append(sep)
|
||||
_procs[name]["iteration"] = iteration
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, env=env)
|
||||
with _procs_lock:
|
||||
_procs[name]["proc"] = proc
|
||||
for line in proc.stdout:
|
||||
with _procs_lock:
|
||||
_procs[name]["output"].append(line.rstrip())
|
||||
proc.wait()
|
||||
if proc.returncode is not None and proc.returncode < 0:
|
||||
raise StopIteration
|
||||
with _procs_lock:
|
||||
if not _procs[name]["running"]:
|
||||
raise StopIteration
|
||||
with _procs_lock:
|
||||
_procs[name]["returncode"] = 0
|
||||
_procs[name]["running"] = False
|
||||
except StopIteration:
|
||||
with _procs_lock:
|
||||
_procs[name]["returncode"] = 0
|
||||
_procs[name]["running"] = False
|
||||
except Exception as e:
|
||||
with _procs_lock:
|
||||
_procs[name]["error"] = str(e)
|
||||
_procs[name]["running"] = False
|
||||
|
||||
|
||||
def proc_start(name: str, cmd: list, env_extra: dict, count: int):
|
||||
t = threading.Thread(target=_run_proc_loop_bg,
|
||||
args=(name, cmd, env_extra, count), daemon=True)
|
||||
t.start()
|
||||
|
||||
|
||||
def proc_mix_start(count: int):
|
||||
t = threading.Thread(target=_run_mix_bg, args=(count,), daemon=True)
|
||||
t.start()
|
||||
|
||||
|
||||
def proc_get_state(name: str) -> dict:
|
||||
with _procs_lock:
|
||||
state = dict(_procs.get(name, {"running": False, "output": [], "returncode": None, "error": None}))
|
||||
state.pop("proc", None)
|
||||
return state
|
||||
|
||||
|
||||
def proc_is_running(name: str) -> bool:
|
||||
with _procs_lock:
|
||||
return _procs.get(name, {}).get("running", False)
|
||||
|
||||
|
||||
def proc_stop(name: str):
|
||||
with _procs_lock:
|
||||
p = _procs.get(name, {}).get("proc")
|
||||
if name in _procs:
|
||||
_procs[name]["running"] = False
|
||||
if p:
|
||||
try:
|
||||
p.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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})
|
||||
@@ -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})
|
||||
@@ -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})
|
||||
+1
-2
@@ -1,11 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Tunnel SSH vers Vigile (DB)
|
||||
cp /ssh/vigile.key /tmp/vigile.key
|
||||
chmod 600 /tmp/vigile.key
|
||||
ssh -fN -L 15432:localhost:5432 -i /tmp/vigile.key -o StrictHostKeyChecking=no -o ServerAliveInterval=30 ubuntu@79.72.30.231
|
||||
|
||||
echo '[start.sh] Tunnel SSH vers Vigile établi sur 127.0.0.1:15432'
|
||||
|
||||
exec python app.py
|
||||
exec python -m dashboard.app
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PipBoy — Fallout: Venice of Wasteland</title>
|
||||
<style>
|
||||
:root {
|
||||
--green: #4ade80;
|
||||
--red: #f87171;
|
||||
--amber: #fbbf24;
|
||||
--blue: #60a5fa;
|
||||
--bg: #0f1117;
|
||||
--panel: #161b27;
|
||||
--panel2: #1a1f2e;
|
||||
--border: #2a3040;
|
||||
--text: #e8e8e8;
|
||||
--muted: #8892a4;
|
||||
--dim: #4a5568;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: var(--bg); color: var(--text); font-family: system-ui, 'Segoe UI', sans-serif; font-size: 14px; }
|
||||
|
||||
.topbar { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: #0a0d14; border-bottom: 1px solid var(--border); }
|
||||
.topbar-title { font-size: 15px; font-weight: 600; color: var(--green); letter-spacing: 1px; margin-right: 8px; }
|
||||
.session-pills { display: flex; gap: 6px; }
|
||||
.session-pill { padding: 4px 12px; border-radius: 20px; border: 1px solid var(--border); color: var(--muted); text-decoration: none; font-size: 12px; font-weight: 500; display: inline-flex; align-items: center; gap: 5px; transition: all .15s; }
|
||||
.session-pill:hover { border-color: var(--dim); color: var(--text); }
|
||||
.session-pill.active { border-color: var(--green); color: var(--green); background: rgba(74,222,128,.08); }
|
||||
.session-pill.stopped { border-color: var(--red); color: var(--red); }
|
||||
.session-pill .dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
|
||||
.dot-green { background: var(--green); }
|
||||
.dot-red { background: var(--red); }
|
||||
.dot-amber { background: var(--amber); }
|
||||
.nav-sep { flex-grow: 1; }
|
||||
.nav-tabs { display: flex; gap: 2px; }
|
||||
.nav-tabs a { padding: 6px 14px; text-decoration: none; color: var(--muted); font-size: 13px; font-weight: 500; border-radius: 6px 6px 0 0; border: 1px solid transparent; border-bottom: none; transition: all .15s; }
|
||||
.nav-tabs a:hover:not(.active) { color: var(--text); background: var(--panel2); }
|
||||
.nav-tabs a.active { color: var(--amber); background: var(--panel); border-color: var(--border); border-bottom-color: var(--panel); }
|
||||
|
||||
.main { padding: 16px; }
|
||||
|
||||
.flash { padding: 8px 12px; margin: 0 16px 12px; border-radius: 6px; font-size: 13px; }
|
||||
.flash-ok { background: rgba(74,222,128,.1); border: 1px solid rgba(74,222,128,.3); color: var(--green); }
|
||||
.flash-err { background: rgba(248,113,113,.1); border: 1px solid rgba(248,113,113,.3); color: var(--red); }
|
||||
|
||||
.info-bar { display: flex; align-items: center; gap: 16px; padding: 10px 14px; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 16px; font-size: 13px; flex-wrap: wrap; }
|
||||
.info-bar .label { color: var(--muted); }
|
||||
.info-bar .val { color: var(--text); font-weight: 500; }
|
||||
.status-badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||||
.status-active { background: rgba(74,222,128,.12); color: var(--green); }
|
||||
.status-stopped { background: rgba(248,113,113,.12); color: var(--red); }
|
||||
.status-paused { background: rgba(251,191,36,.12); color: var(--amber); }
|
||||
.mode-tag { padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; background: rgba(96,165,250,.1); color: var(--blue); border: 1px solid rgba(96,165,250,.2); }
|
||||
|
||||
.metric-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; margin-bottom: 16px; }
|
||||
.metric-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; }
|
||||
.metric-card .metric-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }
|
||||
.metric-card .metric-value { font-size: 26px; font-weight: 700; line-height: 1; }
|
||||
.metric-green { color: var(--green); }
|
||||
.metric-red { color: var(--red); }
|
||||
.metric-amber { color: var(--amber); }
|
||||
.metric-blue { color: var(--blue); }
|
||||
.metric-muted { color: var(--muted); }
|
||||
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-top: 12px; }
|
||||
|
||||
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
|
||||
.panel h2 { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .8px; color: var(--amber); border-bottom: 1px solid var(--border); padding-bottom: 8px; margin-bottom: 10px; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { color: var(--muted); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; padding: 6px 10px; text-align: left; border-bottom: 1px solid var(--border); }
|
||||
td { padding: 6px 10px; font-size: 13px; border-bottom: 1px solid rgba(42,48,64,.5); vertical-align: middle; }
|
||||
tr:nth-child(even) td { background: #1a1a2e; }
|
||||
tr:hover td { background: rgba(74,222,128,.04) !important; }
|
||||
.dead td { opacity: .45; text-decoration: line-through; }
|
||||
|
||||
.tier0-text { color: #fb923c; }
|
||||
.tier1-text { color: var(--green); }
|
||||
.tier2-text { color: var(--blue); }
|
||||
.tier3-text { color: var(--muted); }
|
||||
|
||||
.hp-ok { color: var(--green); font-weight: 600; }
|
||||
.hp-low { color: var(--amber); font-weight: 600; }
|
||||
.hp-crit { color: var(--red); font-weight: 600; }
|
||||
|
||||
.faction-union { color: #60a5fa; }
|
||||
.faction-cda { color: #f87171; }
|
||||
.faction-ecumeurs { color: #c084fc; }
|
||||
.faction-syndicat { color: #facc15; }
|
||||
.faction-krewe { color: #34d399; }
|
||||
.faction-oak { color: #fb923c; }
|
||||
.faction-regie { color: #e2e8f0; }
|
||||
.faction-default { color: var(--muted); }
|
||||
|
||||
.ev-combat { color: var(--red); }
|
||||
.ev-encounter { color: var(--amber); }
|
||||
.ev-survival { color: #86efac; }
|
||||
.ev-death { color: #ff0000; font-weight: 700; }
|
||||
|
||||
.rel-hostile { color: var(--red); }
|
||||
.rel-neutral { color: var(--muted); }
|
||||
.rel-allie { color: var(--green); }
|
||||
|
||||
.scrollable { max-height: 300px; overflow-y: auto; }
|
||||
|
||||
.world-zone { display: flex; justify-content: space-between; align-items: center; padding: 5px 0; border-bottom: 1px solid rgba(42,48,64,.5); font-size: 13px; }
|
||||
.zone-name { color: var(--amber); min-width: 150px; }
|
||||
.zone-stats { display: flex; gap: 10px; font-size: 12px; color: var(--muted); }
|
||||
.zone-stats span { color: var(--text); }
|
||||
|
||||
.cfg-block { margin-bottom: 16px; }
|
||||
.cfg-block h3 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .8px; color: var(--amber); border-bottom: 1px solid var(--border); padding-bottom: 6px; margin-bottom: 10px; }
|
||||
.cfg-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||||
.cfg-row label { min-width: 230px; font-size: 13px; color: var(--muted); }
|
||||
.cfg-row input[type=number], .cfg-row input[type=text], .cfg-row select { background: #0a0d14; color: var(--text); border: 1px solid var(--border); border-radius: 5px; padding: 5px 10px; font-family: inherit; font-size: 13px; width: 140px; }
|
||||
.cfg-row input[type=checkbox] { accent-color: var(--green); width: auto; }
|
||||
.cfg-row select:focus, .cfg-row input:focus { outline: none; border-color: var(--amber); }
|
||||
|
||||
.btn { padding: 7px 18px; border: 1px solid var(--border); background: var(--panel2); color: var(--text); font-family: inherit; font-size: 13px; font-weight: 500; border-radius: 6px; cursor: pointer; transition: all .15s; }
|
||||
.btn:hover { border-color: var(--dim); background: #222736; }
|
||||
.btn-green { border-color: var(--green); color: var(--green); background: rgba(74,222,128,.08); }
|
||||
.btn-green:hover { background: rgba(74,222,128,.15); }
|
||||
.btn-red { border-color: var(--red); color: var(--red); background: rgba(248,113,113,.08); }
|
||||
.btn-red:hover { background: rgba(248,113,113,.15); }
|
||||
.btn-amber { border-color: var(--amber); color: var(--amber); background: rgba(251,191,36,.08); }
|
||||
.btn-amber:hover { background: rgba(251,191,36,.15); }
|
||||
|
||||
.code-block { position: relative; margin: 8px 0; }
|
||||
.code-block pre { background: #0a0d14; border: 1px solid var(--border); border-radius: 6px; padding: 10px 14px; font-family: 'Consolas', 'Courier New', monospace; font-size: 12px; color: #86efac; white-space: pre-wrap; word-break: break-all; margin: 0; padding-right: 80px; }
|
||||
.copy-btn { position: absolute; top: 6px; right: 8px; padding: 3px 10px; font-size: 11px; border: 1px solid var(--border); background: var(--panel2); color: var(--muted); border-radius: 4px; cursor: pointer; font-family: inherit; }
|
||||
.copy-btn:hover { color: var(--text); border-color: var(--dim); }
|
||||
|
||||
.lore-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 10px; }
|
||||
.lore-card h3 { font-size: 13px; color: var(--text); margin-bottom: 8px; font-weight: 500; }
|
||||
.lore-diff { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
|
||||
.diff-old { background: rgba(248,113,113,.06); border: 1px solid rgba(248,113,113,.2); border-radius:5px; padding: 8px; font-size: 12px; color: #fca5a5; white-space: pre-wrap; font-family: 'Consolas', monospace; }
|
||||
.diff-new { background: rgba(74,222,128,.06); border: 1px solid rgba(74,222,128,.2); border-radius:5px; padding: 8px; font-size: 12px; color: #86efac; white-space: pre-wrap; font-family: 'Consolas', monospace; }
|
||||
.diff-label { font-size: 11px; color: var(--muted); margin-bottom: 4px; text-transform: uppercase; letter-spacing: .5px; }
|
||||
.lore-meta { font-size: 12px; color: var(--muted); margin-bottom: 8px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
||||
.lore-actions { display: flex; gap: 8px; align-items: flex-start; flex-wrap: wrap; }
|
||||
.lore-actions textarea { background: #0a0d14; color: var(--text); border: 1px solid var(--border); border-radius: 5px; font-family: 'Consolas', monospace; font-size: 12px; padding: 6px 8px; width: 100%; min-height: 60px; resize: vertical; }
|
||||
.lore-filters { display: flex; gap: 10px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
|
||||
.lore-filters label { font-size: 13px; color: var(--muted); }
|
||||
.lore-filters select { background: #0a0d14; color: var(--text); border: 1px solid var(--border); border-radius: 5px; font-family: inherit; font-size: 13px; padding: 4px 8px; }
|
||||
.lore-stat-bar { display: flex; gap: 16px; margin-bottom: 12px; font-size: 13px; flex-wrap: wrap; }
|
||||
.lore-stat { color: var(--muted); }
|
||||
.lore-stat span { font-weight: 600; }
|
||||
.badge-pending { background: rgba(251,191,36,.12); color: var(--amber); padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }
|
||||
.badge-accepted { background: rgba(74,222,128,.12); color: var(--green); padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }
|
||||
.badge-rejected { background: rgba(248,113,113,.12); color: var(--red); padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }
|
||||
.badge-modified { background: rgba(96,165,250,.12); color: var(--blue); padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }
|
||||
.change-type-badge { font-size: 11px; color: var(--dim); border: 1px solid var(--border); padding: 1px 7px; border-radius: 4px; }
|
||||
|
||||
.tools-section { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 14px; }
|
||||
.tools-section h2 { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: .8px; color: var(--amber); border-bottom: 1px solid var(--border); padding-bottom: 8px; margin-bottom: 14px; }
|
||||
.tool-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
|
||||
.tool-row label { font-size: 13px; color: var(--muted); min-width: 100px; }
|
||||
.tool-row select, .tool-row input[type=number] { background: #0a0d14; color: var(--text); border: 1px solid var(--border); border-radius: 5px; padding: 5px 10px; font-family: inherit; font-size: 13px; }
|
||||
.radio-group { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.radio-group label { display: flex; align-items: center; gap: 5px; padding: 4px 12px; border: 1px solid var(--border); border-radius: 5px; cursor: pointer; font-size: 13px; color: var(--muted); min-width: auto; transition: all .15s; }
|
||||
.radio-group input[type=radio] { display: none; }
|
||||
.radio-group input[type=radio]:checked + span { color: var(--amber); }
|
||||
.radio-group label:has(input:checked) { border-color: var(--amber); background: rgba(251,191,36,.08); color: var(--amber); }
|
||||
.enrich-card { background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:12px 14px; }
|
||||
.mini-console { background:#020408;border:1px solid var(--border);padding:8px 10px;max-height:200px;overflow-y:auto;font-size:11px;color:#a8d5a2;white-space:pre-wrap;border-radius:4px;margin-top:8px; }
|
||||
.results-area { margin-top: 12px; }
|
||||
.results-meta { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
|
||||
.diag-good { color: var(--green); font-weight: 600; }
|
||||
.diag-bad { color: var(--red); font-weight: 600; }
|
||||
|
||||
.sim-tab-btn { background:none;border:none;border-bottom:2px solid transparent;color:var(--dim);padding:8px 18px;cursor:pointer;font-size:13px;font-family:inherit;transition:color .15s; }
|
||||
.sim-tab-btn:hover { color:var(--text); }
|
||||
.sim-tab-btn.active { color:var(--amber);border-bottom-color:var(--amber);font-weight:600; }
|
||||
|
||||
.refresh-note { font-size: 12px; color: var(--dim); margin-left: auto; }
|
||||
.refresh-note a { color: var(--muted); text-decoration: none; }
|
||||
.refresh-note a:hover { color: var(--text); }
|
||||
|
||||
.footer { text-align: center; font-size: 11px; color: var(--dim); padding: 16px; border-top: 1px solid var(--border); margin-top: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="topbar">
|
||||
<span class="topbar-title">📟 PIPBOY</span>
|
||||
<div class="session-pills">
|
||||
{% for s in all_sessions %}
|
||||
{% set is_active = s.status == 'active' %}
|
||||
<a href="/?sid={{ s.id }}"
|
||||
class="session-pill {{ 'active' if s.id == current_sid and tab == 'sim' else ('stopped' if not is_active else '') }}">
|
||||
<span class="dot {{ 'dot-green' if is_active else 'dot-red' }}"></span>
|
||||
S{{ s.id }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="nav-sep"></span>
|
||||
<div class="nav-tabs">
|
||||
<a href="/?sid={{ current_sid }}" class="{{ 'active' if tab=='sim' }}">📡 SIM</a>
|
||||
<a href="/tools?sid={{ current_sid }}" class="{{ 'active' if tab=='tools' }}">🔧 OUTILS</a>
|
||||
<a href="/lore" class="{{ 'active' if tab=='lore' }}">📜 LORE</a>
|
||||
<a href="/sim-proposals" class="{{ 'active' if tab=='sim_proposals' }}">🎲 RÈGLES SIM</a>
|
||||
<a href="/params?sid={{ current_sid }}" class="{{ 'active' if tab=='params' }}">⚙ PARAMS</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if flash_msg %}
|
||||
<div class="flash {{ 'flash-ok' if flash_ok else 'flash-err' }}">{{ flash_msg }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="main">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="footer">PipBoy v5.1 | SIM LIBRE • OUTILS • ENRICHISSEMENT • PARAMÈTRES</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,141 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="info-bar">
|
||||
<span style="font-weight:600">📜 Enrichissement Lore — Venice of Wasteland</span>
|
||||
<div class="lore-stat-bar" style="margin:0">
|
||||
<span class="lore-stat">En attente : <span style="color:var(--amber)">{{ lore_stats.pending }}</span></span>
|
||||
<span class="lore-stat">Acceptées : <span style="color:var(--green)">{{ lore_stats.accepted }}</span></span>
|
||||
<span class="lore-stat">Rejetées : <span style="color:var(--red)">{{ lore_stats.rejected }}</span></span>
|
||||
<span class="lore-stat">Modifiées : <span style="color:var(--blue)">{{ lore_stats.modified }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lore-filters">
|
||||
<label>Faction :</label>
|
||||
<select onchange="location.href='/lore?faction='+this.value+'&status='+document.getElementById('filt-status').value">
|
||||
<option value="" {{ 'selected' if not filt_faction else '' }}>— toutes —</option>
|
||||
{% for f in all_factions %}
|
||||
<option value="{{ f }}" {{ 'selected' if filt_faction==f else '' }}>{{ f }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>Statut :</label>
|
||||
<select id="filt-status" onchange="location.href='/lore?faction='+document.querySelector('[onchange]').value+'&status='+this.value">
|
||||
<option value="pending" {{ 'selected' if filt_status=='pending' else '' }}>pending</option>
|
||||
<option value="accepted" {{ 'selected' if filt_status=='accepted' else '' }}>accepted</option>
|
||||
<option value="rejected" {{ 'selected' if filt_status=='rejected' else '' }}>rejected</option>
|
||||
<option value="modified" {{ 'selected' if filt_status=='modified' else '' }}>modified</option>
|
||||
<option value="" {{ 'selected' if not filt_status else '' }}>— tous —</option>
|
||||
</select>
|
||||
<span style="font-size:12px;color:var(--dim)">{{ proposals|length }} proposition(s) affichée(s)</span>
|
||||
</div>
|
||||
|
||||
{% if not proposals %}
|
||||
<div class="panel" style="color:var(--dim);text-align:center;padding:24px">
|
||||
Aucune proposition lore.<br>
|
||||
<small style="color:var(--border)">Lancer : <code>python3 lore_enricher.py --faction grand_krewe</code> sur Ampère</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for p in proposals %}
|
||||
<div class="lore-card">
|
||||
<div class="lore-meta">
|
||||
<span>#{{ p.id }}</span>
|
||||
<span class="badge-{{ p.status }}">{{ p.status | upper }}</span>
|
||||
<span class="change-type-badge">{{ p.change_type }}</span>
|
||||
<span style="color:var(--amber)">{{ p.faction_slug }}</span>
|
||||
{% if p.field_path %}<span>{{ p.field_path }}</span>{% endif %}
|
||||
<span style="color:var(--dim)">{{ p.created_at.strftime('%d/%m %H:%M') if p.created_at else '' }}</span>
|
||||
</div>
|
||||
|
||||
<h3>{{ p.rationale or '(pas de justification)' }}</h3>
|
||||
|
||||
<div class="lore-diff">
|
||||
<div>
|
||||
<div class="diff-label">Original</div>
|
||||
<div class="diff-old">{{ p.original_text or '(ajout — pas de texte original)' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="diff-label">Proposé</div>
|
||||
<div class="diff-new">{{ p.proposed_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if p.status == 'pending' %}
|
||||
<div class="lore-actions">
|
||||
<form method="POST" action="/lore/action" style="display:inline">
|
||||
<input type="hidden" name="id" value="{{ p.id }}">
|
||||
<input type="hidden" name="action" value="accept">
|
||||
<input type="hidden" name="filt_faction" value="{{ filt_faction }}">
|
||||
<input type="hidden" name="filt_status" value="{{ filt_status }}">
|
||||
<button type="submit" class="btn btn-green" onclick="return confirm('Accepter cette proposition ?')">✓ Accepter</button>
|
||||
</form>
|
||||
<form method="POST" action="/lore/action" style="display:inline">
|
||||
<input type="hidden" name="id" value="{{ p.id }}">
|
||||
<input type="hidden" name="action" value="reject">
|
||||
<input type="hidden" name="filt_faction" value="{{ filt_faction }}">
|
||||
<input type="hidden" name="filt_status" value="{{ filt_status }}">
|
||||
<button type="submit" class="btn btn-red">✗ Rejeter</button>
|
||||
</form>
|
||||
<form method="POST" action="/lore/action" style="width:100%;margin-top:6px">
|
||||
<input type="hidden" name="id" value="{{ p.id }}">
|
||||
<input type="hidden" name="action" value="modify">
|
||||
<input type="hidden" name="filt_faction" value="{{ filt_faction }}">
|
||||
<input type="hidden" name="filt_status" value="{{ filt_status }}">
|
||||
<textarea name="modified_text" placeholder="Texte modifié…">{{ p.proposed_text }}</textarea>
|
||||
<button type="submit" class="btn" style="margin-top:6px">✎ Modifier & Accepter</button>
|
||||
</form>
|
||||
</div>
|
||||
{% elif p.status == 'modified' and p.modified_text %}
|
||||
<div style="margin-top:8px">
|
||||
<div class="diff-label">Texte modifié accepté</div>
|
||||
<div class="diff-new" style="border-color:rgba(96,165,250,.3)">{{ p.modified_text }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if p.status in ('modified', 'accepted') %}
|
||||
<button type="button" class="btn" onclick="toggleDiscuss({{ p.id }})" style="font-size:12px;margin-top:8px">💬 Discuter / Modifier</button>
|
||||
<div id="discuss-{{ p.id }}" style="display:none;margin-top:12px;border-top:1px solid var(--border);padding-top:10px">
|
||||
{% for msg in proposal_threads.get(p.id, []) %}
|
||||
<div style="margin-bottom:8px;padding:6px 10px;border-radius:4px;{% if msg.role=='user' %}background:#0d1117;border-left:2px solid var(--amber){% else %}background:#050d05;border-left:2px solid var(--green){% endif %}">
|
||||
<span style="font-size:10px;color:var(--dim);display:block;margin-bottom:3px">{{ '👤 Toi' if msg.role=='user' else '🤖 LLM' }} — {{ msg.created_at.strftime('%d/%m %H:%M') if msg.created_at else '' }}</span>
|
||||
<span style="font-size:12px;white-space:pre-wrap">{{ msg.content }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">
|
||||
<textarea id="discuss-msg-{{ p.id }}" placeholder="Ex: Garde l'idée mais réduis l'impact de 50%…" style="background:var(--bg);border:1px solid var(--border);color:var(--text);font-size:12px;padding:6px;border-radius:3px;height:54px;resize:vertical"></textarea>
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||||
<span style="font-size:11px;color:var(--dim)">temp: <input type="range" id="discuss-temp-{{ p.id }}" min="0" max="1" step="0.05" value="0.75" style="width:70px;vertical-align:middle" oninput="document.getElementById('discuss-temp-val-{{ p.id }}').textContent=this.value"> <span id="discuss-temp-val-{{ p.id }}">0.75</span></span>
|
||||
<span style="font-size:11px;color:var(--dim)">tokens: <input type="number" id="discuss-np-{{ p.id }}" value="512" min="128" max="2048" step="128" style="width:58px;background:var(--bg);border:1px solid var(--border);color:var(--text);font-size:11px;padding:2px 4px"></span>
|
||||
<button class="btn btn-green" onclick="submitDiscuss({{ p.id }})" style="font-size:12px">▶ Envoyer</button>
|
||||
<span id="discuss-spinner-{{ p.id }}" style="display:none;color:var(--amber);font-size:12px">⏳ Génération…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<script>
|
||||
function toggleDiscuss(id) {
|
||||
var el = document.getElementById('discuss-' + id);
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
function submitDiscuss(id) {
|
||||
var msg = document.getElementById('discuss-msg-' + id).value.trim();
|
||||
var temp = parseFloat(document.getElementById('discuss-temp-' + id).value);
|
||||
var np = parseInt(document.getElementById('discuss-np-' + id).value);
|
||||
if (!msg) { alert('Message vide'); return; }
|
||||
var spin = document.getElementById('discuss-spinner-' + id);
|
||||
spin.style.display = 'inline';
|
||||
fetch('/lore/' + id + '/discuss', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({message: msg, temperature: temp, num_predict: np})
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
spin.style.display = 'none';
|
||||
if (d.error) { alert(d.error); return; }
|
||||
location.reload();
|
||||
}).catch(function(e){ spin.style.display = 'none'; alert('Erreur réseau : ' + e); });
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,173 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% if session %}
|
||||
<div class="info-bar">
|
||||
<span><span class="label">Session</span> <strong class="val">#{{ session.id }} — {{ session.name }}</strong></span>
|
||||
<span class="mode-tag">{{ session.mode | upper }}</span>
|
||||
{% set st = session.status %}
|
||||
<span class="status-badge {{ 'status-active' if st=='active' else 'status-stopped' if st=='stopped' else 'status-paused' }}">
|
||||
<span class="dot {{ 'dot-green' if st=='active' else 'dot-red' if st=='stopped' else 'dot-amber' }}"></span>
|
||||
{{ st | upper }}
|
||||
</span>
|
||||
<span><span class="label">Jour</span> <strong class="val">{{ session.current_day }}</strong> / <span class="label">Tick</span> <strong class="val">{{ session.current_tick }}h</strong></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<!-- Config JSON -->
|
||||
<div class="panel">
|
||||
<h2>⚙ Config sim_{{ '%03d' % current_sid }}.json</h2>
|
||||
<form method="POST" action="/params/save?sid={{ current_sid }}">
|
||||
{% if cfg is not none %}
|
||||
|
||||
<div class="cfg-block">
|
||||
<h3>Général</h3>
|
||||
<div class="cfg-row">
|
||||
<label>Nom de la session</label>
|
||||
<input type="text" name="name" value="{{ cfg.get('name','') }}" style="width:220px">
|
||||
</div>
|
||||
<div class="cfg-row">
|
||||
<label>Mode actif</label>
|
||||
<select name="mode">
|
||||
{% for m in ['pacifiste','politique','guerre_commerciale','guerre','survie_extreme'] %}
|
||||
<option value="{{ m }}" {{ 'selected' if cfg.get('mode')==m else '' }}>{{ m }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="cfg-row">
|
||||
<label>Tick sleep (secondes)</label>
|
||||
<input type="number" name="tick_sleep_sec" value="{{ cfg.get('tick',{}).get('tick_sleep_sec',60) }}" step="1" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cfg-block">
|
||||
<h3>Survie</h3>
|
||||
<div class="cfg-row">
|
||||
<label>drain_speed (multiplicateur)</label>
|
||||
<input type="number" name="survival_drain_speed" value="{{ cfg.get('survival',{}).get('drain_speed',1.0) }}" step="0.1" min="0.1" max="5">
|
||||
</div>
|
||||
<div class="cfg-row">
|
||||
<label>fatigue_hp_per_2pts</label>
|
||||
<input type="number" name="survival_fatigue_hp" value="{{ cfg.get('survival',{}).get('fatigue_hp_per_2pts',2) }}" step="1" min="0">
|
||||
</div>
|
||||
<div class="cfg-row">
|
||||
<label>Survie activée</label>
|
||||
<input type="checkbox" name="survival_enabled" {{ 'checked' if cfg.get('survival',{}).get('enabled',True) else '' }}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cfg-block">
|
||||
<h3>Rencontres</h3>
|
||||
<div class="cfg-row">
|
||||
<label>global_rate_multiplier</label>
|
||||
<input type="number" name="encounter_rate" value="{{ cfg.get('encounter',{}).get('global_rate_multiplier',1.0) }}" step="0.1" min="0" max="5">
|
||||
</div>
|
||||
<div class="cfg-row">
|
||||
<label>Rencontres activées</label>
|
||||
<input type="checkbox" name="encounter_enabled" {{ 'checked' if cfg.get('encounter',{}).get('enabled',True) else '' }}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cfg-block">
|
||||
<h3>Économie</h3>
|
||||
<div class="cfg-row">
|
||||
<label>caps_global_multiplier</label>
|
||||
<input type="number" name="caps_multiplier" value="{{ cfg.get('economy',{}).get('caps_global_multiplier',1.0) }}" step="0.1" min="0.1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-green">💾 Sauvegarder config</button>
|
||||
<p style="margin-top:8px;font-size:12px;color:var(--muted)">
|
||||
Prise en compte au prochain jour (J{{ (session.current_day or 0) + 1 if session else '?' }}).
|
||||
Pour effet immédiat : STOP → relancer via terminal.
|
||||
</p>
|
||||
{% else %}
|
||||
<p style="color:var(--red);font-size:13px">
|
||||
Fichier <code>sim_{{ '%03d' % current_sid }}.json</code> introuvable dans <code>{{ sim_configs_dir }}</code>
|
||||
</p>
|
||||
<p style="color:var(--muted);font-size:12px;margin-top:6px">
|
||||
Vérifier le mount Docker : /home/ubuntu/fallout-venice/src/config → /app/src/config
|
||||
</p>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Actions session -->
|
||||
<div class="panel">
|
||||
<h2>🔧 Gestion session #{{ current_sid }}</h2>
|
||||
|
||||
<div class="cfg-block">
|
||||
<h3>Statut DB</h3>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<form method="POST" action="/params/setstatus?sid={{ current_sid }}" style="display:inline">
|
||||
<input type="hidden" name="status" value="active">
|
||||
<button type="submit" class="btn btn-green">▶ Activer</button>
|
||||
</form>
|
||||
<form method="POST" action="/params/setstatus?sid={{ current_sid }}" style="display:inline">
|
||||
<input type="hidden" name="status" value="paused">
|
||||
<button type="submit" class="btn">⏸ Pause</button>
|
||||
</form>
|
||||
<form method="POST" action="/params/setstatus?sid={{ current_sid }}" style="display:inline">
|
||||
<input type="hidden" name="status" value="stopped">
|
||||
<button type="submit" class="btn btn-red">■ Stop</button>
|
||||
</form>
|
||||
</div>
|
||||
<p style="margin-top:8px;font-size:12px;color:var(--muted)">
|
||||
STOP → la sim s'arrête dans les 5 ticks suivants.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="cfg-block">
|
||||
<h3>Reset jour/tick</h3>
|
||||
<form method="POST" action="/params/resetday?sid={{ current_sid }}">
|
||||
<div class="cfg-row">
|
||||
<label>Remettre au Jour</label>
|
||||
<input type="number" name="day" value="1" min="1">
|
||||
</div>
|
||||
<div class="cfg-row">
|
||||
<label>Tick</label>
|
||||
<input type="number" name="tick" value="0" min="0" max="23">
|
||||
</div>
|
||||
<button type="submit" class="btn">⏮ Reset position</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="cfg-block">
|
||||
<h3>Modes disponibles</h3>
|
||||
<table>
|
||||
<tr><th>Mode</th><th>Drain</th><th>Encounters</th><th>Caps</th></tr>
|
||||
{% for m_name, m_vals in modes.items() %}
|
||||
<tr>
|
||||
<td class="{{ 'tier1-text' if cfg and m_name == cfg.get('mode','') else '' }}">
|
||||
{{ m_name }}{% if cfg and m_name == cfg.get('mode','') %} ◀{% endif %}</td>
|
||||
<td style="color:var(--muted);font-size:12px">×{{ m_vals.get('survival',{}).get('drain_speed','—') }}</td>
|
||||
<td style="color:var(--muted);font-size:12px">×{{ m_vals.get('encounter',{}).get('global_rate_multiplier','—') }}</td>
|
||||
<td style="color:var(--muted);font-size:12px">×{{ m_vals.get('economy',{}).get('caps_global_multiplier','—') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="cfg-block">
|
||||
<h3>Commandes SSH (référence)</h3>
|
||||
<div class="code-block">
|
||||
<pre id="ssh_logs_cmd">tail -f ~/fallout_sim_s{{ current_sid }}.log</pre>
|
||||
<button class="copy-btn" onclick="copyText('ssh_logs_cmd')">Copier</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyText(elementId) {
|
||||
var el = document.getElementById(elementId);
|
||||
navigator.clipboard.writeText(el.textContent.trim()).then(function() {
|
||||
var btn = el.nextElementSibling;
|
||||
if (btn) { btn.textContent = '✓ Copié'; setTimeout(function(){ btn.textContent = 'Copier'; }, 2000); }
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,402 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% if session %}
|
||||
<div class="info-bar">
|
||||
<span><span class="label">Session</span> <strong class="val">#{{ session.id }} — {{ session.name }}</strong></span>
|
||||
<span><span class="label">Jour</span> <strong class="val">{{ session.current_day }}</strong> <span class="label">/ Tick</span> <strong class="val">{{ session.current_tick }}h</strong></span>
|
||||
<span class="mode-tag">{{ session.mode | upper }}</span>
|
||||
{% set st = session.status %}
|
||||
<span class="status-badge {{ 'status-active' if st=='active' else 'status-stopped' if st=='stopped' else 'status-paused' }}">
|
||||
<span class="dot {{ 'dot-green' if st=='active' else 'dot-red' if st=='stopped' else 'dot-amber' }}"></span>
|
||||
{{ st | upper }}
|
||||
</span>
|
||||
<span class="label">Seed: <span class="val">{{ session.seed_global }}</span></span>
|
||||
<span class="refresh-note">
|
||||
<a href="?sid={{ session.id }}">↻ Actualiser</a>
|
||||
<span id="countdown"></span>
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="metric-row">
|
||||
<div class="metric-card"><div class="metric-label">PNJ Vivants</div><div class="metric-value metric-green">{{ alive_count }}</div></div>
|
||||
<div class="metric-card"><div class="metric-label">PNJ Morts</div><div class="metric-value metric-red">{{ stats.get('PNJ morts', 0) }}</div></div>
|
||||
<div class="metric-card"><div class="metric-label">Total PNJ</div><div class="metric-value metric-muted">{{ total_count }}</div></div>
|
||||
<div class="metric-card"><div class="metric-label">Combats</div><div class="metric-value metric-amber">{{ stats.get('Combats', 0) }}</div></div>
|
||||
<div class="metric-card"><div class="metric-label">Rencontres</div><div class="metric-value metric-blue">{{ stats.get('Rencontres', 0) }}</div></div>
|
||||
<div class="metric-card"><div class="metric-label">Événements</div><div class="metric-value metric-muted">{{ stats.get('Événements total', 0) }}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="panel">
|
||||
<h2>👥 PNJ ({{ alive_count }}/{{ total_count }})</h2>
|
||||
<div class="scrollable">
|
||||
<table>
|
||||
<tr><th>Tier</th><th>Nom</th><th>PV</th><th>Faction</th><th>Zone</th><th>Caps</th></tr>
|
||||
{% for c in characters %}
|
||||
{% set pct = (c.hp / c.max_hp * 100) | int if c.max_hp else 0 %}
|
||||
<tr class="{{ 'dead' if not c.is_alive else '' }}">
|
||||
<td>
|
||||
{% if c.tier == 0 %}<span class="tier0-text">★ BOSS</span>
|
||||
{% elif c.tier == 2 %}<span class="tier2-text">T2</span>
|
||||
{% elif c.tier == 3 %}<span class="tier3-text">T3</span>
|
||||
{% else %}<span class="tier1-text">T1</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ c.name }}</td>
|
||||
<td>
|
||||
{% if c.is_alive %}
|
||||
<span class="{{ 'hp-crit' if pct < 25 else 'hp-low' if pct < 50 else 'hp-ok' }}">{{ c.hp }}/{{ c.max_hp }}</span>
|
||||
{% else %}<span class="tier3-text">†</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% set f = c.faction_slug or 'sans' %}
|
||||
<span class="faction-{{ 'union' if 'union' in f else 'cda' if 'cda' in f else 'ecumeurs' if 'ecumeur' in f else 'krewe' if 'krewe' in f else 'oak' if 'oak' in f else 'regie' if 'regie' in f else 'syndicat' if 'syndicat' in f else 'default' }}">{{ f[:14] }}</span>
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted)">{{ (c.location_slug or '—')[:16] }}</td>
|
||||
<td style="color:var(--amber)">{{ c.caps }}¢</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>📡 Événements récents</h2>
|
||||
<div class="scrollable">
|
||||
<table>
|
||||
<tr><th>J/T</th><th>Type</th><th>Description</th></tr>
|
||||
{% for ev in events %}
|
||||
<tr>
|
||||
<td style="color:var(--muted);white-space:nowrap">{{ ev.day }}/{{ ev.tick }}</td>
|
||||
<td class="ev-{{ ev.event_type }}" style="white-space:nowrap"><small>{{ ev.event_type }}</small></td>
|
||||
<td style="font-size:12px">{{ ev.description[:90] }}{% if ev.description|length > 90 %}…{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not events %}<tr><td colspan="3" style="color:var(--dim);text-align:center;padding:16px">Aucun événement</td></tr>{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid3">
|
||||
<div class="panel">
|
||||
<h2>🗺 État du monde</h2>
|
||||
<div class="scrollable">
|
||||
{% for ws in world_state %}
|
||||
<div class="world-zone">
|
||||
<span class="zone-name">{{ ws.location_slug }}</span>
|
||||
<span class="zone-stats">
|
||||
<span>🍖 <span>{{ ws.food_level or 0 }}%</span></span>
|
||||
<span>💧 <span>{{ ws.water_level or 0 }}%</span></span>
|
||||
<span>🛡 <span>{{ ws.security_level or 0 }}%</span></span>
|
||||
<span>👤 <span>{{ ws.pop_count or 0 }}</span></span>
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not world_state %}<p style="color:var(--dim);padding:12px 0">Pas de données monde</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>⚔ Relations factions</h2>
|
||||
<div class="scrollable">
|
||||
<table>
|
||||
<tr><th>Faction A</th><th>Faction B</th><th>Score</th><th>État</th></tr>
|
||||
{% 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' %}
|
||||
<tr>
|
||||
<td style="font-size:12px">{{ r.faction_a[:14] }}</td>
|
||||
<td style="font-size:12px">{{ r.faction_b[:14] }}</td>
|
||||
<td class="{{ cls }}">{{ r.relation_score }}</td>
|
||||
<td class="{{ cls }}" style="font-size:12px">{{ r.relation_label or ('HOSTILE' if r.relation_score < -20 else 'ALLIÉ' if r.relation_score > 20 else 'NEUTRE') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not faction_relations %}<tr><td colspan="4" style="color:var(--dim)">Pas de données</td></tr>{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>🎭 PNJ par tier</h2>
|
||||
<table style="margin-bottom:12px">
|
||||
<tr><th>Tier</th><th>Label</th><th>Vivants</th><th>Morts</th></tr>
|
||||
{% for row in tier_stats %}
|
||||
<tr>
|
||||
<td class="tier{{ row.tier }}-text">{{ row.tier }}</td>
|
||||
<td>{{ row.label }}</td>
|
||||
<td class="metric-green">{{ row.alive }}</td>
|
||||
<td style="color:var(--muted)">{{ row.dead }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<h2 style="margin-top:4px">🏙 PNJ par zone (top 5)</h2>
|
||||
<table>
|
||||
<tr><th>Zone</th><th>PNJ</th></tr>
|
||||
{% for row in zone_stats %}
|
||||
<tr>
|
||||
<td style="font-size:12px">{{ row.location_slug }}</td>
|
||||
<td class="metric-green">{{ row.cnt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" style="margin-top:12px">
|
||||
<h2>⚡ Rencontres récentes</h2>
|
||||
<div class="scrollable" style="max-height:180px">
|
||||
<table>
|
||||
<tr><th>J/T</th><th>Type</th><th>Acteur</th><th>Zone</th><th>Description</th></tr>
|
||||
{% for ev in encounters %}
|
||||
<tr>
|
||||
<td style="color:var(--muted);white-space:nowrap;font-size:12px">{{ ev.day }}/{{ ev.tick }}</td>
|
||||
<td class="ev-encounter" style="font-size:12px;white-space:nowrap">{{ ev.event_type }}</td>
|
||||
<td style="font-size:12px">{{ ev.actor_name or '—' }}</td>
|
||||
<td style="font-size:12px;color:var(--muted)">{{ (ev.location_slug or '—')[:16] }}</td>
|
||||
<td style="font-size:12px">{{ ev.description[:90] }}{% if ev.description|length > 90 %}…{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not encounters %}<tr><td colspan="5" style="color:var(--dim);text-align:center;padding:12px">Aucune rencontre</td></tr>{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if boss_inventory %}
|
||||
<div class="panel" style="margin-top:12px">
|
||||
<h2>★ Équipement chefs de faction</h2>
|
||||
<table>
|
||||
<tr><th>Boss</th><th>Item</th><th>Type</th><th>Qté</th><th>Équipé</th></tr>
|
||||
{% for row in boss_inventory %}
|
||||
<tr>
|
||||
<td class="tier0-text">{{ row.char_name }}</td>
|
||||
<td>{{ row.item_name }}</td>
|
||||
<td style="color:var(--muted);font-size:12px">{{ row.item_type }}</td>
|
||||
<td>{{ row.quantity }}</td>
|
||||
<td style="color:var(--green)">{{ '✓' if row.is_equipped else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Contrôles simulation -->
|
||||
<div style="margin-top:16px">
|
||||
<div style="display:flex;align-items:center;gap:0;border-bottom:1px solid var(--border);flex-wrap:wrap">
|
||||
<button class="sim-tab-btn active" id="stab-truth" onclick="showSimTab('truth')">📚 Vérité</button>
|
||||
<button class="sim-tab-btn" id="stab-enriched" onclick="showSimTab('enriched')">✨ Enrichie</button>
|
||||
<button class="sim-tab-btn" id="stab-mix" onclick="showSimTab('mix')">⚡ MIX</button>
|
||||
<div style="margin-left:auto;display:flex;align-items:center;gap:8px;padding:4px 6px;flex-wrap:wrap">
|
||||
<span style="font-size:11px;color:var(--dim)">Tick global :</span>
|
||||
<div class="radio-group" style="margin:0">
|
||||
<label><input type="radio" name="sim_tick" value="0"><span>INSTANT</span></label>
|
||||
<label><input type="radio" name="sim_tick" value="5" checked><span>5s</span></label>
|
||||
<label><input type="radio" name="sim_tick" value="30"><span>30s</span></label>
|
||||
<label><input type="radio" name="sim_tick" value="60"><span>60s</span></label>
|
||||
<label><input type="radio" name="sim_tick" value="3600"><span>1h</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="simtab-truth" class="sim-tab-content" style="display:block">
|
||||
<div class="panel" style="margin-top:12px;padding:16px">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<span class="dot dot-red" id="dot-sim-truth"></span>
|
||||
<span style="font-weight:600">📚 Sim Vérité</span>
|
||||
<span style="font-size:10px;color:var(--dim)">sources : fallout_lore + fallout_sim_rules uniquement</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:12px">
|
||||
<span style="font-size:12px;color:var(--dim)">Session : <strong style="color:var(--text)">#{{ current_sid }}</strong></span>
|
||||
<label style="font-size:12px;color:var(--dim)">Mode :
|
||||
<select id="truth-mode" onchange="setSessionMode(this.value)" style="background:var(--bg);border:1px solid var(--border);color:var(--text);font-size:12px;padding:2px 8px;margin-left:4px">
|
||||
{% for m in ['pacifiste','politique','guerre_commerciale','guerre','survie_extreme'] %}
|
||||
<option value="{{ m }}" {{ 'selected' if session and session.mode==m else '' }}>{{ m }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div id="sim-truth-status" style="font-size:12px;color:var(--dim);margin-bottom:10px">Arrêtée</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-green" id="btn-truth-start" onclick="simStart('truth')">▶ Démarrer</button>
|
||||
<button class="btn btn-red" id="btn-truth-stop" onclick="simStop('truth')">■ Arrêter</button>
|
||||
<button class="btn" onclick="simDownloadLog('truth')" style="font-size:11px">⬇ Log JSON</button>
|
||||
</div>
|
||||
<div id="console-sim-truth" class="mini-console" style="display:none;margin-top:12px;max-height:320px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="simtab-enriched" class="sim-tab-content" style="display:none">
|
||||
<div class="panel" style="margin-top:12px;padding:16px">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<span class="dot dot-red" id="dot-sim-enriched"></span>
|
||||
<span style="font-weight:600;color:var(--amber)">✨ Sim Enrichie</span>
|
||||
<span style="font-size:10px;color:var(--dim)">+ fallout_lore_enriched + fallout_sim_enriched</span>
|
||||
</div>
|
||||
<div id="sim-enriched-status" style="font-size:12px;color:var(--dim);margin-bottom:10px">Arrêtée</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-green" id="btn-enriched-start" onclick="simStart('enriched')">▶ Démarrer</button>
|
||||
<button class="btn btn-red" id="btn-enriched-stop" onclick="simStop('enriched')">■ Arrêter</button>
|
||||
<button class="btn" onclick="simDownloadLog('enriched')" style="font-size:11px">⬇ Log JSON</button>
|
||||
</div>
|
||||
<div id="console-sim-enriched" class="mini-console" style="display:none;margin-top:12px;max-height:320px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="simtab-mix" class="sim-tab-content" style="display:none">
|
||||
<div class="panel" style="margin-top:12px;padding:16px">
|
||||
<div style="margin-bottom:12px">
|
||||
<span style="font-weight:600;color:var(--green)">⚡ Mode MIX — Comparaison simultanée</span>
|
||||
<p style="font-size:12px;color:var(--dim);margin:6px 0 0">Lance les deux simulations en même temps.</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px">
|
||||
<button class="btn btn-green" id="btn-mix-start" onclick="simMixStart()">▶ Lancer les 2</button>
|
||||
<button class="btn btn-red" id="btn-mix-stop" onclick="simMixStop()">■ Arrêter les 2</button>
|
||||
<button class="btn" onclick="simDownloadLog('truth')" style="font-size:11px">⬇ Log Vérité</button>
|
||||
<button class="btn" onclick="simDownloadLog('enriched')" style="font-size:11px">⬇ Log Enrichie</button>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--dim);margin-bottom:6px;display:flex;align-items:center;gap:6px">
|
||||
<span class="dot dot-red" id="dot-mix-truth"></span>📚 Vérité
|
||||
</div>
|
||||
<div id="console-mix-truth" class="mini-console" style="max-height:260px"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:var(--amber);margin-bottom:6px;display:flex;align-items:center;gap:6px">
|
||||
<span class="dot dot-red" id="dot-mix-enriched"></span>✨ Enrichie
|
||||
</div>
|
||||
<div id="console-mix-enriched" class="mini-console" style="max-height:260px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var _simPolls = {truth: null, enriched: null};
|
||||
var _simLastLines = {truth: 0, enriched: 0};
|
||||
var _mixLastLines = {truth: 0, enriched: 0};
|
||||
|
||||
function showSimTab(tab) {
|
||||
['truth','enriched','mix'].forEach(function(t) {
|
||||
document.getElementById('simtab-' + t).style.display = (t === tab) ? 'block' : 'none';
|
||||
document.getElementById('stab-' + t).classList.toggle('active', t === tab);
|
||||
});
|
||||
}
|
||||
|
||||
function simStart(mode) {
|
||||
var tickEl = document.querySelector('input[name="sim_tick"]:checked');
|
||||
var tickSpeed = tickEl ? parseInt(tickEl.value) : 5;
|
||||
fetch('/api/sim/' + mode + '/run', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({sid: {{ current_sid }}, tick_speed_s: tickSpeed})
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.error) { alert(d.error); return; }
|
||||
_simLastLines[mode] = 0;
|
||||
var cons = document.getElementById('console-sim-' + mode);
|
||||
cons.textContent = '';
|
||||
cons.style.display = 'block';
|
||||
document.getElementById('dot-sim-' + mode).className = 'dot dot-green';
|
||||
document.getElementById('sim-' + mode + '-status').textContent = 'En cours — ' + (tickSpeed === 0 ? 'INSTANT' : tickSpeed + 's') + '/tick';
|
||||
document.getElementById('btn-' + mode + '-start').disabled = true;
|
||||
if (_simPolls[mode]) clearInterval(_simPolls[mode]);
|
||||
_simPolls[mode] = setInterval(function(){ simPoll(mode); }, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function simPoll(mode) {
|
||||
fetch('/api/sim/' + mode + '/status').then(function(r){ return r.json(); }).then(function(d){
|
||||
var cons = document.getElementById('console-sim-' + mode);
|
||||
var lines = d.output || [];
|
||||
if (lines.length > _simLastLines[mode]) {
|
||||
cons.textContent += lines.slice(_simLastLines[mode]).join('\n') + '\n';
|
||||
cons.scrollTop = cons.scrollHeight;
|
||||
_simLastLines[mode] = lines.length;
|
||||
}
|
||||
if (!d.running) {
|
||||
clearInterval(_simPolls[mode]);
|
||||
document.getElementById('dot-sim-' + mode).className = 'dot ' + (d.returncode === 0 ? 'dot-amber' : 'dot-red');
|
||||
document.getElementById('sim-' + mode + '-status').textContent = d.error || ('Arrêtée (code ' + d.returncode + ')');
|
||||
document.getElementById('btn-' + mode + '-start').disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function simStop(mode) {
|
||||
fetch('/api/sim/' + mode + '/stop', {method:'POST'}).then(function(){ simPoll(mode); });
|
||||
}
|
||||
|
||||
function simDownloadLog(mode) { window.location.href = '/api/sim/' + mode + '/log'; }
|
||||
|
||||
function simMixStart() {
|
||||
var tickEl = document.querySelector('input[name="sim_tick"]:checked');
|
||||
var tickSpeed = tickEl ? parseInt(tickEl.value) : 5;
|
||||
['truth','enriched'].forEach(function(mode) {
|
||||
fetch('/api/sim/' + mode + '/run', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({sid: {{ current_sid }}, tick_speed_s: tickSpeed})
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.error) { console.warn('[MIX] ' + mode + ':', d.error); return; }
|
||||
_mixLastLines[mode] = 0;
|
||||
document.getElementById('console-mix-' + mode).textContent = '';
|
||||
document.getElementById('dot-mix-' + mode).className = 'dot dot-green';
|
||||
document.getElementById('btn-mix-start').disabled = true;
|
||||
if (_simPolls[mode]) clearInterval(_simPolls[mode]);
|
||||
_simPolls[mode] = setInterval(function(){ simMixPoll(mode); }, 2000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function simMixPoll(mode) {
|
||||
fetch('/api/sim/' + mode + '/status').then(function(r){ return r.json(); }).then(function(d){
|
||||
var cons = document.getElementById('console-mix-' + mode);
|
||||
var lines = d.output || [];
|
||||
if (lines.length > _mixLastLines[mode]) {
|
||||
cons.textContent += lines.slice(_mixLastLines[mode]).join('\n') + '\n';
|
||||
cons.scrollTop = cons.scrollHeight;
|
||||
_mixLastLines[mode] = lines.length;
|
||||
}
|
||||
if (!d.running) {
|
||||
clearInterval(_simPolls[mode]);
|
||||
document.getElementById('dot-mix-' + mode).className = 'dot ' + (d.returncode === 0 ? 'dot-amber' : 'dot-red');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function simMixStop() { ['truth','enriched'].forEach(function(mode){ simStop(mode); }); }
|
||||
|
||||
function setSessionMode(mode) {
|
||||
fetch('/api/session/set-mode', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({sid: {{ current_sid }}, mode: mode})
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.error) alert('Erreur : ' + d.error);
|
||||
});
|
||||
}
|
||||
|
||||
(function(){
|
||||
['truth','enriched'].forEach(function(mode){
|
||||
fetch('/api/sim/' + mode + '/status').then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.running) {
|
||||
document.getElementById('dot-sim-' + mode).className = 'dot dot-green';
|
||||
document.getElementById('sim-' + mode + '-status').textContent = 'En cours';
|
||||
document.getElementById('btn-' + mode + '-start').disabled = true;
|
||||
document.getElementById('console-sim-' + mode).style.display = 'block';
|
||||
_simLastLines[mode] = (d.output||[]).length;
|
||||
_simPolls[mode] = setInterval(function(){ simPoll(mode); }, 2000);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
var t = 30;
|
||||
var el = document.getElementById('countdown');
|
||||
if (el) {
|
||||
setInterval(function() {
|
||||
t--;
|
||||
el.textContent = ' (' + t + 's)';
|
||||
if (t <= 0) { location.reload(); }
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,121 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="info-bar">
|
||||
<span style="font-weight:600">🎲 Règles Sim — Propositions LLM</span>
|
||||
<div class="lore-stat-bar" style="margin:0">
|
||||
<span class="lore-stat">En attente : <span style="color:var(--amber)">{{ sim_stats.pending }}</span></span>
|
||||
<span class="lore-stat">Acceptées : <span style="color:var(--green)">{{ sim_stats.accepted }}</span></span>
|
||||
<span class="lore-stat">Rejetées : <span style="color:var(--red)">{{ sim_stats.rejected }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lore-filters">
|
||||
<label>Type :</label>
|
||||
<select onchange="location.href='/sim-proposals?type='+this.value+'&status='+document.getElementById('sp-status').value">
|
||||
<option value="" {{ 'selected' if not filt_type else '' }}>— tous —</option>
|
||||
{% for t in sim_types %}
|
||||
<option value="{{ t }}" {{ 'selected' if filt_type==t else '' }}>{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>Statut :</label>
|
||||
<select id="sp-status" onchange="location.href='/sim-proposals?type='+document.querySelector('[onchange]').value+'&status='+this.value">
|
||||
<option value="pending" {{ 'selected' if filt_status=='pending' else '' }}>pending</option>
|
||||
<option value="accepted" {{ 'selected' if filt_status=='accepted' else '' }}>accepted</option>
|
||||
<option value="rejected" {{ 'selected' if filt_status=='rejected' else '' }}>rejected</option>
|
||||
<option value="" {{ 'selected' if not filt_status else '' }}>— tous —</option>
|
||||
</select>
|
||||
<span style="font-size:12px;color:var(--dim)">{{ proposals|length }} proposition(s)</span>
|
||||
</div>
|
||||
|
||||
{% if not proposals %}
|
||||
<div class="panel" style="color:var(--dim);text-align:center;padding:24px">
|
||||
Aucune proposition.<br>
|
||||
<small style="color:var(--border)">Lancer : <code>python3 sim_enricher.py --type all</code> sur Ampère</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for p in proposals %}
|
||||
<div class="lore-card">
|
||||
<div class="lore-meta">
|
||||
<span>#{{ p.id }}</span>
|
||||
<span class="badge-{{ p.status }}">{{ p.status | upper }}</span>
|
||||
<span class="change-type-badge">{{ p.proposal_type }}</span>
|
||||
<span style="color:var(--dim)">{{ p.created_at.strftime('%d/%m %H:%M') if p.created_at else '' }}</span>
|
||||
</div>
|
||||
|
||||
<h3>{{ p.title }}</h3>
|
||||
<p style="color:var(--text);margin:6px 0;font-size:13px">{{ p.description }}</p>
|
||||
|
||||
{% if p.rationale %}
|
||||
<p style="color:var(--dim);font-size:12px;font-style:italic">💡 {{ p.rationale }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if p.json_patch %}
|
||||
<div class="diff-new" style="font-size:11px;white-space:pre-wrap;max-height:200px;overflow:auto">{{ p.json_patch | tojson(indent=2) }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if p.status == 'pending' %}
|
||||
<div class="lore-actions" style="margin-top:10px">
|
||||
<form method="POST" action="/sim-proposals/action" style="display:inline">
|
||||
<input type="hidden" name="id" value="{{ p.id }}">
|
||||
<input type="hidden" name="action" value="accept">
|
||||
<input type="hidden" name="filt_type" value="{{ filt_type }}">
|
||||
<input type="hidden" name="filt_status" value="{{ filt_status }}">
|
||||
<button type="submit" class="btn btn-green" onclick="return confirm('Accepter et indexer dans fallout_sim_enriched ?')">✓ Accepter</button>
|
||||
</form>
|
||||
<form method="POST" action="/sim-proposals/action" style="display:inline-block;vertical-align:top">
|
||||
<input type="hidden" name="id" value="{{ p.id }}">
|
||||
<input type="hidden" name="action" value="reject">
|
||||
<input type="hidden" name="filt_type" value="{{ filt_type }}">
|
||||
<input type="hidden" name="filt_status" value="{{ filt_status }}">
|
||||
<textarea name="reject_reason" placeholder="Motif du rejet (optionnel — améliore le feedback futur)" style="display:block;width:260px;height:38px;margin-bottom:4px;background:var(--bg);border:1px solid var(--border);color:var(--text);font-size:11px;padding:4px 6px;border-radius:3px;resize:vertical"></textarea>
|
||||
<button type="submit" class="btn btn-red">✗ Rejeter</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="btn" onclick="toggleDiscuss({{ p.id }})" style="font-size:12px;margin-top:8px">💬 Discuter</button>
|
||||
<div id="discuss-{{ p.id }}" style="display:none;margin-top:12px;border-top:1px solid var(--border);padding-top:10px">
|
||||
{% for msg in proposal_threads.get(p.id, []) %}
|
||||
<div style="margin-bottom:8px;padding:6px 10px;border-radius:4px;{% if msg.role=='user' %}background:#0d1117;border-left:2px solid var(--amber){% else %}background:#050d05;border-left:2px solid var(--green){% endif %}">
|
||||
<span style="font-size:10px;color:var(--dim);display:block;margin-bottom:3px">{{ '👤 Toi' if msg.role=='user' else '🤖 LLM' }} — {{ msg.created_at.strftime('%d/%m %H:%M') if msg.created_at else '' }}</span>
|
||||
<span style="font-size:12px;white-space:pre-wrap">{{ msg.content }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">
|
||||
<textarea id="discuss-msg-{{ p.id }}" placeholder="Ex: Réduis l'impact de 50%…" style="background:var(--bg);border:1px solid var(--border);color:var(--text);font-size:12px;padding:6px;border-radius:3px;height:54px;resize:vertical"></textarea>
|
||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||||
<span style="font-size:11px;color:var(--dim)">temp: <input type="range" id="discuss-temp-{{ p.id }}" min="0" max="1" step="0.05" value="0.75" style="width:70px;vertical-align:middle" oninput="document.getElementById('discuss-temp-val-{{ p.id }}').textContent=this.value"> <span id="discuss-temp-val-{{ p.id }}">0.75</span></span>
|
||||
<span style="font-size:11px;color:var(--dim)">tokens: <input type="number" id="discuss-np-{{ p.id }}" value="512" min="128" max="2048" step="128" style="width:58px;background:var(--bg);border:1px solid var(--border);color:var(--text);font-size:11px;padding:2px 4px"></span>
|
||||
<button class="btn btn-green" onclick="submitDiscuss({{ p.id }})" style="font-size:12px">▶ Envoyer</button>
|
||||
<span id="discuss-spinner-{{ p.id }}" style="display:none;color:var(--amber);font-size:12px">⏳ Génération…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<script>
|
||||
function toggleDiscuss(id) {
|
||||
var el = document.getElementById('discuss-' + id);
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
function submitDiscuss(id) {
|
||||
var msg = document.getElementById('discuss-msg-' + id).value.trim();
|
||||
var temp = parseFloat(document.getElementById('discuss-temp-' + id).value);
|
||||
var np = parseInt(document.getElementById('discuss-np-' + id).value);
|
||||
if (!msg) { alert('Message vide'); return; }
|
||||
var spin = document.getElementById('discuss-spinner-' + id);
|
||||
spin.style.display = 'inline';
|
||||
fetch('/sim-proposals/' + id + '/discuss', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({message: msg, temperature: temp, num_predict: np})
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
spin.style.display = 'none';
|
||||
if (d.error) { alert(d.error); return; }
|
||||
location.reload();
|
||||
}).catch(function(e){ spin.style.display = 'none'; alert('Erreur réseau : ' + e); });
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,329 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
function copyText(elementId) {
|
||||
var el = document.getElementById(elementId);
|
||||
navigator.clipboard.writeText(el.textContent.trim()).then(function() {
|
||||
var btn = el.nextElementSibling;
|
||||
if (btn) { btn.textContent = '✓ Copié'; setTimeout(function(){ btn.textContent = 'Copier'; }, 2000); }
|
||||
});
|
||||
}
|
||||
|
||||
function genStressCmd() {
|
||||
var mode = document.querySelector('input[name="stress_mode"]:checked').value;
|
||||
var rounds = document.querySelector('input[name="stress_rounds"]:checked').value;
|
||||
var pnjRow = document.getElementById('stress_pnj_row');
|
||||
var pnj = '1';
|
||||
if (pnjRow.style.display !== 'none') {
|
||||
pnj = document.querySelector('input[name="stress_pnj"]:checked').value;
|
||||
}
|
||||
var cmd = 'cd /home/ubuntu/fallout-venice/tools && python3 llm_crash_test.py --mode ' + mode + ' --rounds ' + rounds;
|
||||
if (mode !== 'solo-mj' && mode !== 'solo-pnj') cmd += ' --pnj-count ' + pnj;
|
||||
document.getElementById('stress_cmd_text').textContent = cmd;
|
||||
document.getElementById('stress_cmd_out').style.display = 'block';
|
||||
}
|
||||
|
||||
function togglePnjRow() {
|
||||
var mode = document.querySelector('input[name="stress_mode"]:checked').value;
|
||||
document.getElementById('stress_pnj_row').style.display =
|
||||
(mode === 'solo-mj' || mode === 'solo-pnj') ? 'none' : 'flex';
|
||||
}
|
||||
|
||||
function loadStressResults() {
|
||||
var area = document.getElementById('stress_results');
|
||||
area.innerHTML = '<p style="color:var(--muted);padding:8px 0">Chargement…</p>';
|
||||
fetch('/api/stress-test/results')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.error) { area.innerHTML = '<p style="color:var(--red);padding:8px 0">' + data.error + '</p>'; return; }
|
||||
renderStressResults(area, data);
|
||||
});
|
||||
}
|
||||
|
||||
function renderStressResults(area, data) {
|
||||
var html = '<div class="results-meta">Fichier du ' + (data.file_date || '?') + '</div>';
|
||||
html += '<table><tr><th>Modèle</th><th>Appels</th><th>Erreurs</th><th>Moy tok/s</th><th>Min tok/s</th><th>Max tok/s</th><th>Moy durée</th></tr>';
|
||||
var models = data.models || {};
|
||||
for (var model in models) {
|
||||
var m = models[model];
|
||||
var avgTok = parseFloat(m.avg_tok_s || 0);
|
||||
var threshold = model.indexOf('14b') !== -1 ? 8 : 15;
|
||||
var cls = avgTok >= threshold ? 'diag-good' : 'diag-bad';
|
||||
html += '<tr><td class="' + cls + '">' + model + '</td><td>' + (m.calls||0) + '</td>';
|
||||
html += '<td style="color:' + ((m.errors||0) > 0 ? 'var(--red)' : 'var(--muted)') + '">' + (m.errors||0) + '</td>';
|
||||
html += '<td class="' + cls + '">' + avgTok.toFixed(1) + '</td>';
|
||||
html += '<td style="color:var(--muted)">' + parseFloat(m.min_tok_s||0).toFixed(1) + '</td>';
|
||||
html += '<td style="color:var(--muted)">' + parseFloat(m.max_tok_s||0).toFixed(1) + '</td>';
|
||||
html += '<td style="color:var(--muted)">' + parseFloat(m.avg_duration||0).toFixed(2) + 's</td></tr>';
|
||||
}
|
||||
html += '</table>';
|
||||
if (data.pipeline) {
|
||||
var p = data.pipeline;
|
||||
html += '<div style="margin-top:12px"><strong style="color:var(--amber)">Pipeline (décalé)</strong></div>';
|
||||
html += '<table><tr><th>Latence moy</th><th>Min</th><th>Max</th></tr>';
|
||||
html += '<tr><td>' + (p.avg_latency||'—') + '</td><td>' + (p.min_latency||'—') + '</td><td>' + (p.max_latency||'—') + '</td></tr></table>';
|
||||
}
|
||||
area.innerHTML = html;
|
||||
}
|
||||
|
||||
var _stressPollTimer = null;
|
||||
var _stressLastLine = 0;
|
||||
|
||||
function runStress() {
|
||||
var mode = document.querySelector('input[name="stress_mode"]:checked').value;
|
||||
var rounds = document.querySelector('input[name="stress_rounds"]:checked').value;
|
||||
var pnj = 2;
|
||||
if (mode !== 'solo-mj' && mode !== 'solo-pnj')
|
||||
pnj = parseInt(document.querySelector('input[name="stress_pnj"]:checked').value);
|
||||
fetch('/api/stress-test/run', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({mode:mode, rounds:parseInt(rounds), pnj_count:pnj})
|
||||
}).then(function(r){ return r.json(); }).then(function(d) {
|
||||
if (d.error) { alert(d.error); return; }
|
||||
_stressLastLine = 0;
|
||||
document.getElementById('stress_console').textContent = '';
|
||||
document.getElementById('stress_console_wrap').style.display = 'block';
|
||||
document.getElementById('stress_status_bar').style.display = 'block';
|
||||
document.getElementById('stress_status_text').textContent = 'Test ' + mode + ' en cours (' + rounds + ' rounds)…';
|
||||
document.getElementById('btn-run-stress').disabled = true;
|
||||
document.getElementById('btn-stop-stress').style.display = 'inline-block';
|
||||
document.getElementById('stress_results').innerHTML = '';
|
||||
_stressPollTimer = setInterval(function() {
|
||||
fetch('/api/stress-test/status').then(function(r){ return r.json(); }).then(function(d) {
|
||||
var cons = document.getElementById('stress_console');
|
||||
var lines = d.output || [];
|
||||
if (lines.length > _stressLastLine) {
|
||||
cons.textContent += lines.slice(_stressLastLine).join('\n') + '\n';
|
||||
cons.scrollTop = cons.scrollHeight;
|
||||
_stressLastLine = lines.length;
|
||||
}
|
||||
if (!d.running) {
|
||||
clearInterval(_stressPollTimer);
|
||||
document.getElementById('stress_status_text').textContent =
|
||||
d.returncode === 0 ? '✓ Terminé avec succès' : '✗ Erreur (code ' + d.returncode + ')';
|
||||
document.getElementById('stress_spinner').textContent = d.returncode === 0 ? '' : '⚠';
|
||||
document.getElementById('btn-run-stress').disabled = false;
|
||||
document.getElementById('btn-stop-stress').style.display = 'none';
|
||||
if (d.returncode === 0) loadStressResults();
|
||||
}
|
||||
});
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
var _procPolls = {};
|
||||
var _procLastLine = {};
|
||||
|
||||
function procRun(name, extra) {
|
||||
fetch('/api/proc/' + name + '/run', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify(extra)
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.error) { alert(d.error); return; }
|
||||
_procLastLine[name] = 0;
|
||||
document.getElementById('console-' + name).textContent = '';
|
||||
document.getElementById('console-' + name).style.display = 'block';
|
||||
document.getElementById('btn-run-' + name).disabled = true;
|
||||
document.getElementById('btn-stop-' + name).style.display = 'inline-block';
|
||||
document.getElementById('dot-' + name).className = 'dot dot-green';
|
||||
if (_procPolls[name]) clearInterval(_procPolls[name]);
|
||||
_procPolls[name] = setInterval(function(){ procPoll(name); }, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function procPoll(name) {
|
||||
fetch('/api/proc/' + name + '/status').then(function(r){ return r.json(); }).then(function(d){
|
||||
var cons = document.getElementById('console-' + name);
|
||||
var lines = d.output || [];
|
||||
if (lines.length > (_procLastLine[name]||0)) {
|
||||
cons.textContent += lines.slice(_procLastLine[name]||0).join('\n') + '\n';
|
||||
cons.scrollTop = cons.scrollHeight;
|
||||
_procLastLine[name] = lines.length;
|
||||
}
|
||||
var iterEl = document.getElementById('iter-' + name);
|
||||
if (iterEl && d.iteration) {
|
||||
iterEl.textContent = 'Itération ' + d.iteration + ' / ' + (d.total === 0 ? '∞' : d.total);
|
||||
iterEl.style.display = 'block';
|
||||
}
|
||||
if (!d.running) {
|
||||
clearInterval(_procPolls[name]);
|
||||
document.getElementById('btn-run-' + name).disabled = false;
|
||||
document.getElementById('btn-stop-' + name).style.display = 'none';
|
||||
document.getElementById('dot-' + name).className = 'dot ' + (d.returncode === 0 ? 'dot-green' : 'dot-red');
|
||||
if (iterEl) iterEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function procStop(name) {
|
||||
fetch('/api/proc/' + name + '/stop', {method:'POST'}).then(function(){ procPoll(name); });
|
||||
}
|
||||
|
||||
(function(){
|
||||
['sim_enricher','lore_enricher','embed_fallout','embed_sim','mix'].forEach(function(name){
|
||||
fetch('/api/proc/' + name + '/status').then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.running) {
|
||||
document.getElementById('btn-run-' + name).disabled = true;
|
||||
document.getElementById('btn-stop-' + name).style.display = 'inline-block';
|
||||
document.getElementById('dot-' + name).className = 'dot dot-green';
|
||||
document.getElementById('console-' + name).style.display = 'block';
|
||||
_procLastLine[name] = (d.output||[]).length;
|
||||
_procPolls[name] = setInterval(function(){ procPoll(name); }, 1500);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Stress Test -->
|
||||
<div class="tools-section">
|
||||
<h2>⚡ Stress Test LLM</h2>
|
||||
<div class="tool-row">
|
||||
<label>Mode</label>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="stress_mode" value="simultane" checked onchange="togglePnjRow()"><span>simultane</span></label>
|
||||
<label><input type="radio" name="stress_mode" value="decale" onchange="togglePnjRow()"><span>decale</span></label>
|
||||
<label><input type="radio" name="stress_mode" value="solo-mj" onchange="togglePnjRow()"><span>solo-mj</span></label>
|
||||
<label><input type="radio" name="stress_mode" value="solo-pnj" onchange="togglePnjRow()"><span>solo-pnj</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-row">
|
||||
<label>Rounds</label>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="stress_rounds" value="3" checked><span>3</span></label>
|
||||
<label><input type="radio" name="stress_rounds" value="5"><span>5</span></label>
|
||||
<label><input type="radio" name="stress_rounds" value="10"><span>10</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-row" id="stress_pnj_row">
|
||||
<label>PNJ count</label>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="stress_pnj" value="1" checked><span>1</span></label>
|
||||
<label><input type="radio" name="stress_pnj" value="2"><span>2</span></label>
|
||||
<label><input type="radio" name="stress_pnj" value="3"><span>3</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-row" style="gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-green" id="btn-run-stress" onclick="runStress()">▶ Exécuter maintenant</button>
|
||||
<button class="btn btn-amber" onclick="genStressCmd()">Voir commande SSH</button>
|
||||
<button class="btn" onclick="loadStressResults()">Charger derniers résultats</button>
|
||||
<button class="btn btn-red" id="btn-stop-stress" style="display:none" onclick="clearInterval(_stressPollTimer);document.getElementById('btn-run-stress').disabled=false;this.style.display='none'">■ Arrêter le suivi</button>
|
||||
</div>
|
||||
<div id="stress_status_bar" style="display:none;margin-top:8px;padding:6px 10px;background:#0d1117;border:1px solid var(--border);border-radius:4px;font-size:13px">
|
||||
<span id="stress_status_text" style="color:var(--amber)">En cours…</span>
|
||||
<span id="stress_spinner" style="margin-left:8px">⏳</span>
|
||||
</div>
|
||||
<div class="cmd-output" id="stress_cmd_out" style="display:none">
|
||||
<div class="code-block"><pre id="stress_cmd_text"></pre><button class="copy-btn" onclick="copyText('stress_cmd_text')">Copier</button></div>
|
||||
</div>
|
||||
<div id="stress_console_wrap" style="display:none;margin-top:10px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
||||
<span style="font-size:12px;font-weight:600;color:var(--dim)">SORTIE EN DIRECT</span>
|
||||
<button class="btn" style="padding:2px 8px;font-size:11px" onclick="document.getElementById('stress_console').innerHTML=''">Effacer</button>
|
||||
</div>
|
||||
<pre id="stress_console" style="background:#020408;border:1px solid var(--border);padding:10px;max-height:320px;overflow-y:auto;font-size:12px;color:#a8d5a2;white-space:pre-wrap;border-radius:4px"></pre>
|
||||
</div>
|
||||
<div class="results-area" id="stress_results"></div>
|
||||
</div>
|
||||
|
||||
<!-- Enrichissement LLM -->
|
||||
<div class="tools-section">
|
||||
<h2>🧠 Enrichissement LLM</h2>
|
||||
<div id="enrich-cards" style="display:flex;flex-direction:column;gap:12px">
|
||||
|
||||
<div class="enrich-card" id="card-sim_enricher">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span style="flex:1;font-weight:600;color:var(--amber)">🎲 Règles sim → sim_proposals</span>
|
||||
<select id="sim_enricher_type" style="width:148px">
|
||||
<option value="all">all</option>
|
||||
<option value="nouveau_mode">nouveau_mode</option>
|
||||
<option value="evenement_special">evenement_special</option>
|
||||
<option value="regle_maison">regle_maison</option>
|
||||
<option value="scenario">scenario</option>
|
||||
</select>
|
||||
<select id="count-sim_enricher" style="width:62px">
|
||||
<option value="1">1×</option><option value="5">5×</option>
|
||||
<option value="10" selected>10×</option><option value="50">50×</option>
|
||||
<option value="100">100×</option><option value="0">∞</option>
|
||||
</select>
|
||||
<button class="btn btn-green" id="btn-run-sim_enricher" onclick="procRun('sim_enricher',{type:document.getElementById('sim_enricher_type').value,count:parseInt(document.getElementById('count-sim_enricher').value)})">▶ Lancer</button>
|
||||
<button class="btn btn-red" id="btn-stop-sim_enricher" style="display:none" onclick="procStop('sim_enricher')">■ Stop</button>
|
||||
<span class="dot dot-red" id="dot-sim_enricher" style="margin-left:4px"></span>
|
||||
</div>
|
||||
<div id="iter-sim_enricher" style="font-size:11px;color:var(--dim);margin-top:4px;display:none"></div>
|
||||
<div id="console-sim_enricher" class="mini-console" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="enrich-card" id="card-lore_enricher">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span style="flex:1;font-weight:600;color:var(--amber)">📜 Lore → lore_proposals</span>
|
||||
<select id="lore_enricher_faction" style="width:148px">
|
||||
<option value="all">all</option>
|
||||
<option value="union">union</option><option value="cda">cda</option>
|
||||
<option value="ecumeurs">ecumeurs</option><option value="grand_krewe">grand_krewe</option>
|
||||
<option value="dynaste_oak">dynaste_oak</option><option value="regie">regie</option>
|
||||
<option value="syndicat_capitole">syndicat_capitole</option><option value="consortium">consortium</option>
|
||||
</select>
|
||||
<select id="count-lore_enricher" style="width:62px">
|
||||
<option value="1">1×</option><option value="5">5×</option>
|
||||
<option value="10" selected>10×</option><option value="50">50×</option>
|
||||
<option value="100">100×</option><option value="0">∞</option>
|
||||
</select>
|
||||
<button class="btn btn-green" id="btn-run-lore_enricher" onclick="procRun('lore_enricher',{faction:document.getElementById('lore_enricher_faction').value,count:parseInt(document.getElementById('count-lore_enricher').value)})">▶ Lancer</button>
|
||||
<button class="btn btn-red" id="btn-stop-lore_enricher" style="display:none" onclick="procStop('lore_enricher')">■ Stop</button>
|
||||
<span class="dot dot-red" id="dot-lore_enricher" style="margin-left:4px"></span>
|
||||
</div>
|
||||
<div id="iter-lore_enricher" style="font-size:11px;color:var(--dim);margin-top:4px;display:none"></div>
|
||||
<div id="console-lore_enricher" class="mini-console" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="enrich-card" id="card-mix" style="border-color:var(--green)">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span style="flex:1;font-weight:600;color:var(--green)">🔄 Mode MIX — sim + lore en alternance</span>
|
||||
<select id="count-mix" style="width:80px">
|
||||
<option value="5">5× chaque</option><option value="10" selected>10× chaque</option>
|
||||
<option value="50">50× chaque</option><option value="0">∞ chaque</option>
|
||||
</select>
|
||||
<button class="btn btn-green" id="btn-run-mix" onclick="procRun('mix',{count:parseInt(document.getElementById('count-mix').value)})">▶ Lancer</button>
|
||||
<button class="btn btn-red" id="btn-stop-mix" style="display:none" onclick="procStop('mix')">■ Stop</button>
|
||||
<span class="dot dot-red" id="dot-mix" style="margin-left:4px"></span>
|
||||
</div>
|
||||
<div id="iter-mix" style="font-size:11px;color:var(--dim);margin-top:4px;display:none"></div>
|
||||
<div id="console-mix" class="mini-console" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Re-embedding -->
|
||||
<div class="tools-section" style="opacity:0.82">
|
||||
<h2 style="color:var(--dim)">🔁 Re-embedding <small style="font-size:11px;font-weight:400;margin-left:8px">Opération rare — seulement si nouveaux PDFs ou configs modifiées</small></h2>
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
|
||||
<div class="enrich-card" id="card-embed_fallout" style="border-color:var(--border)">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span style="flex:1;font-weight:600;color:var(--dim)">PDFs Fallout → fallout_lore</span>
|
||||
<select id="count-embed_fallout" style="width:62px"><option value="1" selected>1×</option><option value="5">5×</option></select>
|
||||
<button class="btn btn-green" id="btn-run-embed_fallout" onclick="procRun('embed_fallout',{count:parseInt(document.getElementById('count-embed_fallout').value)})">▶ Lancer</button>
|
||||
<button class="btn btn-red" id="btn-stop-embed_fallout" style="display:none" onclick="procStop('embed_fallout')">■ Stop</button>
|
||||
<span class="dot dot-red" id="dot-embed_fallout" style="margin-left:4px"></span>
|
||||
</div>
|
||||
<div id="iter-embed_fallout" style="font-size:11px;color:var(--dim);margin-top:4px;display:none"></div>
|
||||
<div id="console-embed_fallout" class="mini-console" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="enrich-card" id="card-embed_sim" style="border-color:var(--border)">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span style="flex:1;font-weight:600;color:var(--dim)">Configs sim → fallout_sim_rules</span>
|
||||
<select id="count-embed_sim" style="width:62px"><option value="1" selected>1×</option><option value="5">5×</option></select>
|
||||
<button class="btn btn-green" id="btn-run-embed_sim" onclick="procRun('embed_sim',{count:parseInt(document.getElementById('count-embed_sim').value)})">▶ Lancer</button>
|
||||
<button class="btn btn-red" id="btn-stop-embed_sim" style="display:none" onclick="procStop('embed_sim')">■ Stop</button>
|
||||
<span class="dot dot-red" id="dot-embed_sim" style="margin-left:4px"></span>
|
||||
</div>
|
||||
<div id="iter-embed_sim" style="font-size:11px;color:var(--dim);margin-top:4px;display:none"></div>
|
||||
<div id="console-embed_sim" class="mini-console" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
+5
-5
@@ -5,12 +5,11 @@ services:
|
||||
restart: unless-stopped
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./dashboard/app.py:/app/app.py:ro
|
||||
- ./dashboard/start.sh:/app/start.sh:ro
|
||||
- /opt/coyote/fallout/src/dashboard:/app/dashboard:ro
|
||||
- /home/ubuntu/.ssh/vigile_backup:/ssh/vigile.key:ro
|
||||
- ./src/config:/app/src/config
|
||||
- ./tools:/app/tools:ro
|
||||
command: sh -c 'apk add --no-cache openssh-client && pip install flask psycopg2-binary --quiet && sh /app/start.sh'
|
||||
- /opt/coyote/fallout/src/config:/app/src/config
|
||||
- /opt/coyote/fallout/tools:/app/tools:ro
|
||||
command: sh -c 'apk add --no-cache openssh-client && pip install flask psycopg2-binary --quiet && sh /app/dashboard/start.sh'
|
||||
networks:
|
||||
- proxy-nw
|
||||
extra_hosts:
|
||||
@@ -28,6 +27,7 @@ services:
|
||||
- OLLAMA_URL=${OLLAMA_URL}
|
||||
- MODEL_MJ=${MODEL_MJ}
|
||||
- MODEL_PNJ=${MODEL_PNJ}
|
||||
- CHROMA_URL=http://chromadb:8000
|
||||
|
||||
networks:
|
||||
proxy-nw:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pymupdf
|
||||
@@ -77,9 +77,11 @@ def _get_collection_id(name: str) -> str | None:
|
||||
return c["id"]
|
||||
return None
|
||||
|
||||
def chroma_query(collection_id: str, query_text: str, n: int = 8,
|
||||
def chroma_query(collection_id, query_text: str, n: int = 8,
|
||||
where: dict | None = None) -> list[dict]:
|
||||
"""Recherche sémantique dans une collection Chroma."""
|
||||
if not collection_id:
|
||||
return []
|
||||
body = {
|
||||
"query_texts": [query_text],
|
||||
"n_results": n,
|
||||
@@ -98,7 +100,7 @@ def chroma_query(collection_id: str, query_text: str, n: int = 8,
|
||||
print(f" [chroma] Erreur query: {e}")
|
||||
return []
|
||||
|
||||
def chroma_get_by_category(collection_id: str, category: str) -> list[dict]:
|
||||
def chroma_get_by_category(collection_id, category: str) -> list[dict]:
|
||||
"""Récupère tous les chunks d'une catégorie."""
|
||||
body = {
|
||||
"where": {"category": {"$eq": category}},
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ def run(config_path: str | None = None):
|
||||
cfg.load(config_path, session_id=SESSION_ID)
|
||||
effective_mode = cfg.mode() or db_mode
|
||||
tick_sleep = float(os.getenv("TICK_SLEEP_SEC",
|
||||
cfg.get("tick", {}).get("tick_sleep_sec", tick_sleep)))
|
||||
cfg.get("tick", "tick_sleep_sec", tick_sleep)))
|
||||
_init_world_state(SESSION_ID, day)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
|
||||
Reference in New Issue
Block a user