stream-fusion-better/stream_fusion/static/admin/dashboard.html

211 lines
11 KiB
HTML

{% extends "base.html" %}
{% block title %}Dashboard — StreamFusion Admin{% endblock %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
{# ── Ligne 1 : 5 stat cards ── #}
<div class="row g-3 mb-3">
<div class="col-sm-6 col-xl">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-value text-primary">{{ active_keys }}</div>
<div class="stat-label">Clés API actives</div>
</div>
<i class="bi bi-key stat-icon text-primary"></i>
</div>
</div>
</div>
<div class="col-sm-6 col-xl">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-value text-success">{{ active_peers }}</div>
<div class="stat-label">Peer Keys actives</div>
</div>
<i class="bi bi-diagram-3 stat-icon text-success"></i>
</div>
</div>
</div>
<div class="col-sm-6 col-xl">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-value text-info">{{ "{:,}".format(torrent_count) }}</div>
<div class="stat-label">Torrents indexés</div>
</div>
<i class="bi bi-magnet stat-icon text-info"></i>
</div>
</div>
</div>
<div class="col-sm-6 col-xl">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-value text-warning">
{{ "%.1f"|format(torrent_matched_count / torrent_count * 100 if torrent_count else 0) }}%
</div>
<div class="stat-label">Taux matching TMDB</div>
</div>
<i class="bi bi-crosshair stat-icon text-warning"></i>
</div>
</div>
</div>
<div class="col-sm-6 col-xl">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-value text-secondary">{{ "{:,}".format(debrid_cache_count) }}</div>
<div class="stat-label">Cache debrid</div>
</div>
<i class="bi bi-database stat-icon text-secondary"></i>
</div>
</div>
</div>
</div>
{# ── Ligne 2 : Actions rapides + Scheduler ── #}
<div class="row g-3 mb-3">
<div class="col-md-6">
<div class="stat-card h-100">
<h6 class="fw-semibold mb-3"><i class="bi bi-lightning-charge me-2 text-primary"></i>Actions rapides</h6>
<div class="d-flex flex-column gap-2">
<a href="{{ url_for('create_api_key_page')(request) }}" class="btn btn-sm btn-outline-primary text-start">
<i class="bi bi-plus-circle me-2"></i>Créer une clé API
</a>
<a href="{{ url_for('create_peer_key_page')(request) }}" class="btn btn-sm btn-outline-success text-start">
<i class="bi bi-plus-circle me-2"></i>Créer une peer key
</a>
<a href="{{ url_for('list_mappings')(request) }}" class="btn btn-sm btn-outline-info text-start">
<i class="bi bi-arrow-left-right me-2"></i>Gérer les mappings
</a>
<a href="{{ url_for('config_page')(request) }}" class="btn btn-sm btn-outline-secondary text-start">
<i class="bi bi-gear me-2"></i>Configuration de l'instance
</a>
<a href="{{ url_for('maintenance_page')(request) }}" class="btn btn-sm btn-outline-warning text-start">
<i class="bi bi-tools me-2"></i>Maintenance
</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="stat-card h-100">
<h6 class="fw-semibold mb-3"><i class="bi bi-clock-history me-2 text-info"></i>Scheduler</h6>
<div id="scheduler-widget" style="font-size:0.85rem;">
<div class="text-secondary"><i class="bi bi-hourglass-split me-1"></i>Chargement…</div>
</div>
</div>
</div>
</div>
{# ── Ligne 3 : Base de données + Recherche ── #}
<div class="row g-3">
<div class="col-md-6">
<div class="stat-card h-100">
<h6 class="fw-semibold mb-3"><i class="bi bi-info-circle me-2 text-secondary"></i>Base de données</h6>
<dl class="mb-0" style="font-size:0.85rem;">
<div class="d-flex justify-content-between py-1 border-bottom border-secondary border-opacity-25">
<dt class="fw-normal" style="color:var(--bs-secondary-color);">Redis</dt>
<dd class="mb-0 {% if redis_ok %}text-success{% else %}text-danger{% endif %}">
{% if redis_ok %}<i class="bi bi-check-circle-fill"></i> Connecté{% else %}<i class="bi bi-x-circle-fill"></i> Hors ligne{% endif %}
</dd>
</div>
<div class="d-flex justify-content-between py-1 border-bottom border-secondary border-opacity-25">
<dt class="fw-normal" style="color:var(--bs-secondary-color);">PostgreSQL</dt>
<dd class="mb-0 text-success"><i class="bi bi-check-circle-fill"></i> Connecté</dd>
</div>
<div class="d-flex justify-content-between py-1 border-bottom border-secondary border-opacity-25">
<dt class="fw-normal" style="color:var(--bs-secondary-color);">Torrents matchés</dt>
<dd class="mb-0">{{ "{:,}".format(torrent_matched_count) }} <span class="text-secondary" style="font-size:0.8rem;">({{ "%.1f"|format(torrent_matched_count / torrent_count * 100 if torrent_count else 0) }}%)</span></dd>
</div>
<div class="d-flex justify-content-between py-1 border-bottom border-secondary border-opacity-25">
<dt class="fw-normal" style="color:var(--bs-secondary-color);">Torrents orphelins</dt>
<dd class="mb-0 {% if torrent_orphan_count > 0 %}text-warning{% endif %}">{{ "{:,}".format(torrent_orphan_count) }}</dd>
</div>
<div class="d-flex justify-content-between py-1">
<dt class="fw-normal" style="color:var(--bs-secondary-color);">Cache debrid</dt>
<dd class="mb-0">{{ "{:,}".format(debrid_cache_count) }} entrées</dd>
</div>
</dl>
</div>
</div>
<div class="col-md-6">
<div class="stat-card h-100">
<h6 class="fw-semibold mb-3"><i class="bi bi-search me-2 text-info"></i>Recherche dans la base</h6>
<div class="d-flex flex-column gap-3">
<form action="{{ url_for('search_hash_page')(request) }}" method="get" class="d-flex gap-2">
<input type="text" name="q" class="form-control form-control-sm font-monospace"
placeholder="info_hash (40 car. hex)" maxlength="40"
style="font-size:0.82rem;">
<button type="submit" class="btn btn-sm btn-outline-info flex-shrink-0">
<i class="bi bi-hash me-1"></i>Par hash
</button>
</form>
<form action="{{ url_for('search_tmdb_page')(request) }}" method="get" class="d-flex gap-2">
<input type="number" name="q" class="form-control form-control-sm"
placeholder="TMDB ID (ex : 272565)" min="1"
style="font-size:0.82rem;">
<button type="submit" class="btn btn-sm btn-outline-primary flex-shrink-0">
<i class="bi bi-film me-1"></i>Par TMDB ID
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
(function () {
fetch('/admin/scheduler/status')
.then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
.then(function (data) {
var jobs = Array.isArray(data.jobs) ? data.jobs : [];
var leaderBadge = data.is_leader
? '<span class="badge bg-success">Leader</span>'
: '<span class="badge bg-secondary">Replica</span>';
var runBadge = data.scheduler_running
? '<span class="badge bg-success bg-opacity-25 text-success">Actif</span>'
: '<span class="badge bg-danger bg-opacity-25 text-danger">Arrêté</span>';
var html = '<div class="d-flex align-items-center gap-2 mb-3">'
+ leaderBadge + ' ' + runBadge
+ '<span class="text-secondary ms-auto" style="font-size:0.8rem;">'
+ jobs.length + ' job' + (jobs.length !== 1 ? 's' : '')
+ '</span></div>';
if (jobs.length > 0) {
html += '<div style="max-height:160px;overflow-y:auto;">';
jobs.forEach(function (job) {
// next_run_at est un timestamp Unix (secondes)
var nextRun = job.next_run_at
? new Date(job.next_run_at * 1000).toLocaleTimeString('fr-FR', {hour: '2-digit', minute: '2-digit'})
: '—';
var statusIcon = job.last_status === 'ok'
? '<i class="bi bi-check-circle-fill text-success" style="font-size:0.75rem;"></i>'
: job.last_status === 'error'
? '<i class="bi bi-x-circle-fill text-danger" style="font-size:0.75rem;"></i>'
: '<i class="bi bi-dash-circle text-secondary" style="font-size:0.75rem;"></i>';
var iconHtml = job.icon ? '<i class="bi ' + job.icon + '" style="font-size:0.8rem;width:14px;text-align:center;flex-shrink:0;"></i> ' : '';
html += '<div class="d-flex align-items-center justify-content-between py-1 border-bottom border-secondary border-opacity-25">'
+ '<span style="font-size:0.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60%;display:flex;align-items:center;gap:5px;">'
+ iconHtml + (job.name || job.id) + '</span>'
+ '<div class="d-flex align-items-center gap-2 flex-shrink-0">'
+ statusIcon
+ '<span style="font-size:0.75rem;color:var(--bs-secondary-color);">→ ' + nextRun + '</span>'
+ '</div></div>';
});
html += '</div>';
}
document.getElementById('scheduler-widget').innerHTML = html;
})
.catch(function () {
document.getElementById('scheduler-widget').innerHTML =
'<span class="text-danger"><i class="bi bi-x-circle-fill me-1"></i>Indisponible</span>';
});
})();
</script>
{% endblock %}