"""
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 %}
{% endif %}
👥 PNJ ACTIFS ({{ alive_count }}/{{ total_count }})
📡 ÉVÉNEMENTS RÉCENTS (J{{ session.current_day if session else '?' }})
📊 STATS SIMULATION
| MÉTRIQUE | VALEUR |
{% for k, v in stats.items() %}
| {{ k }} | {{ v }} |
{% endfor %}
🎭 PNJ PAR TIER
| TIER | LABEL | VIVANTS | MORTS |
{% for row in tier_stats %}
| {{ row.tier }} |
{{ row.label }} |
{{ row.alive }} |
{{ row.dead }} |
{% endfor %}
🏙 PNJ PAR ZONE (TOP 5)
| ZONE | PNJ |
{% for row in zone_stats %}
| {{ row.location_slug }} | {{ row.cnt }} |
{% endfor %}
{% if boss_inventory %}
★ ÉQUIPEMENT CHEFS DE FACTION
| BOSS | ITEM | TYPE | QTÉ | ÉQUIPÉ |
{% for row in boss_inventory %}
| {{ row.char_name }} |
{{ row.item_name }} |
{{ row.item_type }} |
{{ row.quantity }} |
{{ '✓' if row.is_equipped else '' }} |
{% endfor %}
{% 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)