diff --git a/dashboard/app.py b/dashboard/app.py index 59f343a..c37147c 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -1,12 +1,13 @@ """ VAULTCOM — Fallout: Venice of Wasteland Dashboard simulation. Schéma DB actuel (migration_003). +Onglets: Simulation (par session) | Paramètres (config + restart) """ -import os, json +import os, json, subprocess import psycopg2 import psycopg2.extras -from flask import Flask, render_template_string, jsonify +from flask import Flask, render_template_string, jsonify, request, redirect, url_for app = Flask(__name__) @@ -16,6 +17,8 @@ DB_NAME = os.getenv("DB_NAME", "fallout") DB_USER = os.getenv("DB_USER", "fallout") DB_PASS = os.getenv("DB_PASS", "VeniceOfWasteland2026!") +SIM_CONFIGS_DIR = os.getenv("SIM_CONFIGS_DIR", "/app/src/config") + def get_conn(): return psycopg2.connect( host=DB_HOST, port=DB_PORT, dbname=DB_NAME, @@ -35,8 +38,33 @@ 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 load_sim_config(session_id: int) -> dict: + 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 {} + +def save_sim_config(session_id: int, cfg: dict): + path = os.path.join(SIM_CONFIGS_DIR, f"sim_{session_id:03d}.json") + with open(path, "w") as f: + json.dump(cfg, f, indent=2, ensure_ascii=False) + +def get_all_sessions(): + return query("SELECT * FROM sessions ORDER BY id") + # --------------------------------------------------------------------------- -# HTML template +# HTML Template # --------------------------------------------------------------------------- TEMPLATE = r""" @@ -44,33 +72,34 @@ TEMPLATE = r""" - VAULTCOM — Fallout: Venice of Wasteland

⚡ VAULTCOM — VENICE OF WASTELAND ⚡

+ + + +{% if flash_msg %} +
+ {{ flash_msg }} +
+{% endif %} + +{% if tab == 'sim' %} + + + + {% if session %}
SESSION #{{ session.id }} — {{ session.name }} - JOUR {{ session.current_day }} / TICK {{ session.current_tick }} - MODE: {{ session.mode | upper }} | STATUS: {{ session.status | upper }} + JOUR {{ session.current_day }} / TICK {{ session.current_tick }}h + {{ session.mode | upper }} + STATUS: {{ session.status | upper }} SEED: {{ session.seed_global }} - 🔄 Actualisé toutes les 30s + 🔄 Actualiser +
{% endif %}
- - +

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

@@ -156,8 +233,7 @@ TEMPLATE = r""" {% set f = c.faction_slug or 'sans' %} - {{ f[:12] }} - + {{ f[:12] }} {{ (c.location_slug or '—')[:14] }} {{ c.caps }}¢ @@ -167,9 +243,9 @@ TEMPLATE = r"""
- +
-

📡 ÉVÉNEMENTS RÉCENTS (J{{ session.current_day if session else '?' }})

+

📡 ÉVÉNEMENTS RÉCENTS

@@ -184,12 +260,10 @@ TEMPLATE = r"""
J/TTYPEDESCRIPTION
- -
+
- - +

🗺 ÉTAT DU MONDE

@@ -208,7 +282,7 @@ TEMPLATE = r"""
- +

⚔ RELATIONS FACTIONS

@@ -219,8 +293,7 @@ TEMPLATE = r""" {{ r.faction_a[:12] }} {{ r.faction_b[:12] }} - {{ r.relation_score }} - + {{ r.relation_score }} {{ r.relation_label or ('HOSTILE' if r.relation_score < -20 else 'ALLIÉ' if r.relation_score > 20 else 'NEUTRE') }} @@ -231,7 +304,7 @@ TEMPLATE = r"""
- +

📊 STATS SIMULATION

@@ -240,21 +313,17 @@ TEMPLATE = r""" {% endfor %}
{{ k }}{{ v }}
-

🎭 PNJ PAR TIER

{% for row in tier_stats %} - - - - + + {% endfor %}
TIERLABELVIVANTSMORTS
{{ row.tier }}{{ row.label }}{{ row.alive }}{{ row.dead }}{{ row.tier }}{{ row.label }}{{ row.alive }}{{ row.dead }}
-

🏙 PNJ PAR ZONE (TOP 5)

@@ -264,10 +333,9 @@ TEMPLATE = r""" {% endfor %}
+
- - - +

⚡ RENCONTRES RÉCENTES

@@ -287,7 +355,7 @@ TEMPLATE = r"""
- + {% if boss_inventory %}

★ ÉQUIPEMENT CHEFS DE FACTION

@@ -306,18 +374,189 @@ TEMPLATE = r"""
{% endif %} - + + +{% elif tab == 'params' %} + + + + +{% if session %} +
+ SESSION #{{ session.id }} — {{ session.name }} + {{ session.mode | upper }} + STATUS: {{ session.status | upper }} + JOUR {{ session.current_day }} / TICK {{ session.current_tick }}h +
+{% endif %} + +
+ + +
+

⚙ CONFIG SIM_{{ '%03d' % current_sid }}.JSON

+
+ {% if cfg %} + +
+

GÉNÉRAL

+
+ + +
+
+ + +
+
+ + +
+
+ +
+

SURVIE

+
+ + +
+
+ + +
+
+ + +
+
+ +
+

RENCONTRES

+
+ + +
+
+ + +
+
+ +
+

ÉCONOMIE

+
+ + +
+
+ + + {% else %} +

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

+ {% endif %} +
+
+ + +
+

🔧 GESTION SESSION #{{ current_sid }}

+ +
+

STATUT DB

+
+ + +
+   +
+ + +
+   +
+ + +
+

+ Note: ne tue pas le process Python. Mettre "paused" ou "stopped" fait sortir run.py au prochain tick. +

+
+ +
+

RESET JOUR/TICK

+
+
+ + +
+
+ + +
+ +
+
+ +
+

MODES DISPONIBLES

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

COMMANDES AMPÈRE

+

Copier-coller dans votre terminal SSH :

+
+

# Lancer/relancer S{{ current_sid }} :

+

cd /home/ubuntu/fallout-venice/src/engine

+

nohup env SESSION_ID={{ current_sid }} SIM_CONFIG=../config/sim_{{ '%03d' % current_sid }}.json TICK_SLEEP_SEC=60 PYTHONUNBUFFERED=1 python3 -u run.py > ~/fallout_sim_s{{ current_sid }}.log 2>&1 &

+
+

# Voir les logs :

+

tail -f ~/fallout_sim_s{{ current_sid }}.log

+
+
+
+ +
+{% endif %} + + """ # --------------------------------------------------------------------------- -# Routes +# Helpers data # --------------------------------------------------------------------------- -@app.route("/") -def index(): - session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1") - sid = session["id"] if session else 1 +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 @@ -325,40 +564,31 @@ def index(): 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"]) - total_count = len(characters) events = query(""" - SELECT e.*, c.name as actor_name - FROM events e + 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') - ORDER BY e.day DESC, e.tick DESC, e.id DESC - LIMIT 50 + 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 + 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 + 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 + 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 + SELECT * FROM faction_relations WHERE session_id = %s ORDER BY relation_score ASC """, (sid,)) @@ -368,33 +598,29 @@ def index(): 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) + 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 + 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 + GROUP BY location_slug ORDER BY cnt DESC LIMIT 5 """, (sid,)) - events_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s", (sid,)) - combat_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='combat'", (sid,)) - deaths_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,)) + 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": events_total["n"] if events_total else 0, - "Combats": combat_total["n"] if combat_total else 0, - "Morts": deaths_total["n"] if deaths_total else 0, + "É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": total_count - alive_count, + "PNJ morts": len(characters) - alive_count, } boss_inventory = query(""" @@ -405,11 +631,11 @@ def index(): ORDER BY c.name, i.is_equipped DESC """, (sid,)) - return render_template_string(TEMPLATE, + return dict( session=session, characters=characters, alive_count=alive_count, - total_count=total_count, + total_count=len(characters), events=events, encounters=encounters, world_state=world_state, @@ -420,11 +646,127 @@ def index(): boss_inventory=boss_inventory, ) +# --------------------------------------------------------------------------- +# Routes +# --------------------------------------------------------------------------- + +@app.route("/") +def index(): + all_sessions = get_all_sessions() + sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1)) + data = get_sim_data(sid) + return render_template_string(TEMPLATE, + tab="sim", + current_sid=sid, + all_sessions=all_sessions, + flash_msg=None, + flash_ok=True, + modes={}, + cfg={}, + sim_configs_dir=SIM_CONFIGS_DIR, + **data, + ) + +@app.route("/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 = cfg.get("modes", {}) + flash_msg = request.args.get("msg") + flash_ok = request.args.get("ok", "1") == "1" + return render_template_string(TEMPLATE, + tab="params", + current_sid=sid, + all_sessions=all_sessions, + session=session, + cfg=cfg, + modes=modes, + sim_configs_dir=SIM_CONFIGS_DIR, + flash_msg=flash_msg, + flash_ok=flash_ok, + # unused in params tab but required by template + characters=[], alive_count=0, total_count=0, + events=[], encounters=[], world_state=[], + faction_relations=[], tier_stats=[], zone_stats=[], + stats={}, boss_inventory=[], + ) + +@app.route("/params/save", methods=["POST"]) +def params_save(): + sid = int(request.args.get("sid", 1)) + cfg = load_sim_config(sid) + if not cfg: + return redirect(url_for("params", sid=sid, msg="Fichier config introuvable", ok=0)) + + f = request.form + + cfg["name"] = f.get("name", cfg.get("name", "")) + cfg["mode"] = f.get("mode", cfg.get("mode", "pacifiste")) + + cfg.setdefault("tick", {})["tick_sleep_sec"] = int(f.get("tick_sleep_sec", 60)) + + cfg.setdefault("survival", {}).update({ + "drain_speed": float(f.get("survival_drain_speed", 1.0)), + "fatigue_hp_per_2pts": int(f.get("survival_fatigue_hp", 2)), + "enabled": "survival_enabled" in f, + }) + + cfg.setdefault("encounter", {}).update({ + "global_rate_multiplier": float(f.get("encounter_rate", 1.0)), + "enabled": "encounter_enabled" in f, + }) + + cfg.setdefault("economy", {})["caps_global_multiplier"] = float(f.get("caps_multiplier", 1.0)) + + try: + save_sim_config(sid, cfg) + execute("UPDATE sessions SET mode=%s WHERE id=%s", (cfg["mode"], sid)) + msg = f"Config sim_{sid:03d}.json sauvegardée. Mode DB mis à jour → {cfg['mode']}." + ok = 1 + except Exception as e: + msg = f"Erreur: {e}" + ok = 0 + + return redirect(url_for("params", sid=sid, msg=msg, ok=ok)) + +@app.route("/params/setstatus", methods=["POST"]) +def params_setstatus(): + sid = int(request.args.get("sid", 1)) + status = request.form.get("status", "paused") + try: + execute("UPDATE sessions SET status=%s WHERE id=%s", (status, sid)) + msg = f"Session {sid} → status '{status}'. Le process run.py sortira au prochain tick si arrêté." + ok = 1 + except Exception as e: + msg = f"Erreur: {e}" + ok = 0 + return redirect(url_for("params", sid=sid, msg=msg, ok=ok)) + +@app.route("/params/resetday", methods=["POST"]) +def params_resetday(): + sid = int(request.args.get("sid", 1)) + day = int(request.form.get("day", 1)) + tick = int(request.form.get("tick", 0)) + try: + execute("UPDATE sessions SET current_day=%s, current_tick=%s WHERE id=%s", (day, tick, sid)) + msg = f"Session {sid} remise à J{day} T{tick}h." + ok = 1 + except Exception as e: + msg = f"Erreur: {e}" + ok = 0 + return redirect(url_for("params", sid=sid, msg=msg, ok=ok)) + @app.route("/api/state") def api_state(): - session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1") - sid = session["id"] if session else 1 - alive = query("SELECT id, name, hp, max_hp, tier, faction_slug, location_slug FROM characters WHERE session_id=%s AND is_alive=TRUE", (sid,)) + all_sessions = get_all_sessions() + sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1)) + session = query_one("SELECT * FROM sessions WHERE id=%s", (sid,)) + alive = query(""" + SELECT id, name, hp, max_hp, tier, faction_slug, location_slug + FROM characters WHERE session_id=%s AND is_alive=TRUE + """, (sid,)) return jsonify({"session": session, "alive_pnj": alive}) if __name__ == "__main__":