""" VAULTCOM — Fallout: Venice of Wasteland Dashboard simulation. Schéma DB actuel (migration_003). """ import os, json import psycopg2 import psycopg2.extras from flask import Flask, render_template_string, jsonify app = Flask(__name__) DB_HOST = os.getenv("DB_HOST", "localhost") DB_PORT = int(os.getenv("DB_PORT", 5432)) DB_NAME = os.getenv("DB_NAME", "fallout") DB_USER = os.getenv("DB_USER", "fallout") DB_PASS = os.getenv("DB_PASS", "VeniceOfWasteland2026!") 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 # --------------------------------------------------------------------------- # HTML template # --------------------------------------------------------------------------- TEMPLATE = r""" VAULTCOM — Fallout: Venice of Wasteland

⚡ VAULTCOM — VENICE OF WASTELAND ⚡

{% if session %}
SESSION #{{ session.id }} — {{ session.name }} JOUR {{ session.current_day }} / TICK {{ session.current_tick }} MODE: {{ session.mode | upper }} | STATUS: {{ session.status | upper }} SEED: {{ session.seed_global }} 🔄 Actualisé toutes les 30s
{% endif %}

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

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

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

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

🗺 ÉTAT DU MONDE

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

Pas de données monde

{% endif %}

⚔ RELATIONS FACTIONS

{% for r in faction_relations %} {% endfor %} {% if not faction_relations %}{% endif %}
FACTION AFACTION BSCOREÉTAT
{{ r.faction_a[:12] }} {{ r.faction_b[:12] }} {{ r.relation_score }} {{ r.relation_label or ('HOSTILE' if r.relation_score < -20 else 'ALLIÉ' if r.relation_score > 20 else 'NEUTRE') }}
Pas de données

📊 STATS SIMULATION

{% for k, v in stats.items() %} {% endfor %}
MÉTRIQUEVALEUR
{{ k }}{{ v }}

🎭 PNJ PAR TIER

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

🏙 PNJ PAR ZONE (TOP 5)

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

⚡ RENCONTRES RÉCENTES

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

★ ÉQUIPEMENT CHEFS DE FACTION

{% for row in boss_inventory %} {% endfor %}
BOSSITEMTYPEQTÉÉQUIPÉ
{{ row.char_name }} {{ row.item_name }} {{ row.item_type }} {{ row.quantity }} {{ '✓' if row.is_equipped else '' }}
{% endif %} """ # --------------------------------------------------------------------------- # Routes # --------------------------------------------------------------------------- @app.route("/") def index(): session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1") sid = session["id"] if session else 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"]) total_count = len(characters) 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') 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,)) 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,)) 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, "Rencontres": enc_total["n"] if enc_total else 0, "PNJ vivants": alive_count, "PNJ morts": total_count - 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 render_template_string(TEMPLATE, session=session, characters=characters, alive_count=alive_count, total_count=total_count, 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, ) @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,)) return jsonify({"session": session, "alive_pnj": alive}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)