{% if session %}
Jour {{ session.day_current }}/{{ session.day_total }}{{ session.mode | upper }}
{% else %}
DB OFFLINE
{% endif %}
{{ content | safe }}
"""
def render(title, content, active="", session=None):
return render_template_string(SHELL, title=title, content=content,
nav=nav(active), session=session)
# ── Helpers ───────────────────────────────────────────────────────────────────
def get_session():
rows = query("SELECT * FROM sessions ORDER BY id DESC LIMIT 1")
return rows[0] if rows else None
def hp_class(cur, mx):
if mx == 0: return "warning"
pct = cur / mx
return "danger" if pct < 0.33 else ("warning" if pct < 0.66 else "")
def bar_class(cur, mx):
if mx == 0: return "warning"
pct = cur / mx
return "danger" if pct < 0.33 else ("warning" if pct < 0.66 else "")
# ── Routes ────────────────────────────────────────────────────────────────────
@app.route("/")
def index():
sess = get_session()
chars = query("SELECT * FROM characters WHERE session_id = %s ORDER BY faction, name",
(sess["id"],)) if sess else []
recent = query("""SELECT e.*, c.name as char_name FROM events e
JOIN characters c ON c.id = e.character_id
WHERE e.session_id = %s ORDER BY e.day_in_game DESC, e.hour_in_game DESC LIMIT 12""",
(sess["id"],)) if sess else []
factions = query("""SELECT DISTINCT faction, COUNT(*) as nb,
SUM(CASE WHEN status='alive' THEN 1 ELSE 0 END) as alive
FROM characters WHERE session_id = %s GROUP BY faction ORDER BY faction""",
(sess["id"],)) if sess else []
pending = query("SELECT COUNT(*) as n FROM action_queue WHERE session_id = %s AND status='pending'",
(sess["id"],))[0]["n"] if sess else 0
lore_last = query("""SELECT * FROM lore_avancee WHERE session_id = %s
ORDER BY day_start DESC LIMIT 1""", (sess["id"],)) if sess else []
# Characters grid
char_cards = ""
if chars:
for c in chars:
hp_pct = int(c["hp_current"] / max(c["hp_max"], 1) * 100)
bc = bar_class(c["hp_current"], c["hp_max"])
nc = hp_class(c["hp_current"], c["hp_max"])
st_cls = f"status-{c['status']}"
char_cards += f"""
"""
return render("Agents", content, "chars", sess)
@app.route("/character/")
def character(cid):
sess = get_session()
chars = query("SELECT * FROM characters WHERE id = %s", (cid,))
if not chars: abort(404)
c = chars[0]
inv = query("SELECT * FROM inventory WHERE character_id = %s ORDER BY item_name", (cid,))
events = query("""SELECT * FROM events WHERE character_id = %s
ORDER BY day_in_game, hour_in_game""", (cid,))
SPECIALS = [("S","strength"),("P","perception"),("E","endurance"),
("C","charisma"),("I","intelligence"),("A","agility"),("L","luck")]
sp_html = "".join(f"""
{c[key]}
{abbr}
""" for abbr, key in SPECIALS)
hp_pct = int(c["hp_current"] / max(c["hp_max"], 1) * 100)
bc = bar_class(c["hp_current"], c["hp_max"])
nc = hp_class(c["hp_current"], c["hp_max"])
inv_rows = "".join(f"
{i['item_name']}
{i.get('quantity',1)}
{i.get('description','')}
"
for i in inv) if inv else "
Inventaire vide
"
ev_html = ""
if events:
ev_html = '
'
for e in events:
ev_html += f"""
Jour {e['day_in_game']}h{e['hour_in_game']:02d} · {e['location'] or '?'}
▶ {e['action']}
{'
⟹ ' + (e['result'] or '') + '
' if e['result'] else ''}
"""
ev_html += '
'
else:
ev_html = '
Aucune action enregistrée
'
bio_html = f'
{c["bio"] or "Dossier vide."}
'
content = f"""
{c['name']}
{c['faction'] or '—'}
Localisation: {c['location'] or '?'} · Caps: {c['caps']} · XP: {c['xp']}
{c['status'].upper()}
{c['hp_current']}
/{c['hp_max']} HP
{sp_html}
{bio_html}
Inventaire
Objet
Qté
Description
{inv_rows}
Journal d'actions
{ev_html}
"""
return render(c["name"], content, "chars", sess)
@app.route("/actions")
def actions():
sess = get_session()
rows = query("""
SELECT aq.*, c.name as char_name, t.name as target_name
FROM action_queue aq
LEFT JOIN characters c ON c.id = aq.character_id
LEFT JOIN characters t ON t.id = aq.target_char_id
WHERE aq.session_id = %s
ORDER BY aq.day_in_game DESC, aq.hour_in_game DESC, aq.id DESC
LIMIT 100""", (sess["id"],)) if sess else []
def status_badge(s):
cls = {"pending":"amber","resolving":"green","resolved":"dim","cancelled":"red"}.get(s,"dim")
return f'{s.upper()}'
trs = "".join(f"""
{r['action_text'][:80]}{'…' if len(r['action_text'])>80 else ''}
{r['target_name'] or '—'}
{r['scene_id'] or '—'}
{status_badge(r['status'])}
{(r['result_text'] or '')[:60]}
""" for r in rows) if rows else "
Aucune action
"
content = f"""
Moment
Agent
Lieu
Action
Cible
Scène
Status
Résultat
{trs}
"""
return render("File d'Actions", content, "actions", sess)
@app.route("/world")
def world():
sess = get_session()
if not sess:
return render("État du Monde", '
DB offline
', "world")
day = sess["day_current"]
states = query("""SELECT * FROM world_state WHERE session_id = %s AND day_in_game = %s
ORDER BY location""", (sess["id"], day))
factions = query("""SELECT * FROM faction_relations WHERE session_id = %s
ORDER BY faction_a, faction_b""", (sess["id"],))
def stab_color(s):
if s >= 70: return "var(--green)"
if s >= 40: return "var(--amber)"
return "var(--red)"
world_rows = "".join(f"""
{s['location']}
{s['controlling_faction'] or '—'}
{s['stability']}%
{s['population'] or '?'}
{s['notes'] or ''}
""" for s in states) if states else "
Aucune donnée
"
fac_rows = "".join(f"""
{f['faction_a']}
{f['faction_b']}
{'+' if f['relation_score']>0 else ''}{f['relation_score']}
{f.get('notes','')}
""" for f in factions) if factions else "
Aucune donnée
"
content = f"""
Zones — Jour {day}
Lieu
Faction
Stabilité
Pop.
Notes
{world_rows}
Relations entre factions
Faction A
Faction B
Score
Notes
{fac_rows}
"""
return render("État du Monde", content, "world", sess)
@app.route("/lore")
def lore():
sess = get_session()
entries = query("""SELECT * FROM lore_avancee WHERE session_id = %s
ORDER BY day_start DESC""", (sess["id"],)) if sess else []
cards = ""
if entries:
for e in entries:
facs = ", ".join(e["factions_involved"] or []) or "—"
locs = ", ".join(e["locations_involved"] or []) or "—"
cards += f"""
{e['title'] or f"Chronique jours {e['day_start']}–{e['day_end']}"}
J{e['day_start']}–{e['day_end']}
Factions: {facs} · Lieux: {locs}
{e['content']}
"""
else:
cards = '
◎
Aucune synthèse narrative générée Le MJ produira des entrées toutes les 24h de jeu simulé
'
return render("Lore Avancée", cards, "lore", sess)
@app.route("/events")
def events():
sess = get_session()
evs = query("""SELECT e.*, c.name as char_name FROM events e
JOIN characters c ON c.id = e.character_id
WHERE e.session_id = %s
ORDER BY e.day_in_game DESC, e.hour_in_game DESC
LIMIT 200""", (sess["id"],)) if sess else []
rows = "".join(f"""
{e['action'][:80]}{'…' if len(e['action'])>80 else ''}
{(e['result'] or '')[:100]}{'…' if e['result'] and len(e['result'])>100 else ''}
""" for e in evs) if evs else "
Journal vide
"
content = f"""
Moment
Agent
Lieu
Action
Résultat
{rows}
"""
return render("Journal des Événements", content, "events", sess)
@app.route("/api/summary")
def api_summary():
"""Pour le widget dashboard dash.coyoteos.ovh"""
sess = get_session()
if not sess:
return jsonify({"ok": False})
chars = query("SELECT status, COUNT(*) n FROM characters WHERE session_id=%s GROUP BY status", (sess["id"],))
pending = query("SELECT COUNT(*) n FROM action_queue WHERE session_id=%s AND status='pending'", (sess["id"],))
return jsonify({
"ok": True,
"session": sess["name"],
"day": sess["day_current"],
"day_total": sess["day_total"],
"chars": {r["status"]: r["n"] for r in chars},
"pending_actions": pending[0]["n"] if pending else 0,
})
# ── Main ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
t = threading.Thread(target=start_tunnel, daemon=True)
t.start()
print("VaultCom :5000")
app.run(host="0.0.0.0", port=5000, debug=False)