refacto: dashboard Flask Blueprints + templates Jinja2, engine run/lore_enricher mis a jour
This commit is contained in:
@@ -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 %}
|
||||
Reference in New Issue
Block a user