mirror of
https://github.com/HyPnoTiiK/stream-fusion.git
synced 2026-07-28 15:02:46 +00:00
introduce initial torrent grouping: add torrent_groups table and group_id on torrent_items. include migrations, ORM models, and DAOs. expose admin/web endpoints to group by info_hash and by title/size. propagate tmdb_id across grouped items. non-destructive and backwards compatible with existing data
786 lines
39 KiB
HTML
786 lines
39 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Maintenance — StreamFusion Admin{% endblock %}
|
|
{% block page_title %}Maintenance{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.maint-card {
|
|
border: 1px solid var(--bs-border-color);
|
|
border-radius: 10px;
|
|
background: var(--bs-secondary-bg);
|
|
overflow: hidden;
|
|
}
|
|
.maint-card-header {
|
|
padding: 13px 20px;
|
|
border-bottom: 1px solid var(--bs-border-color);
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.maint-group-label {
|
|
padding: 7px 20px;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.07em;
|
|
text-transform: uppercase;
|
|
color: var(--bs-secondary-color);
|
|
background: var(--bs-tertiary-bg);
|
|
border-bottom: 1px solid var(--bs-border-color);
|
|
}
|
|
.maint-action {
|
|
padding: 14px 20px;
|
|
border-bottom: 1px solid var(--bs-border-color);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
}
|
|
.maint-action:last-child { border-bottom: none; }
|
|
.maint-action-info { flex: 1; min-width: 0; }
|
|
.maint-action-title { font-size: 0.875rem; font-weight: 500; margin-bottom: 2px; }
|
|
.maint-action-desc { font-size: 0.78rem; color: var(--bs-secondary-color); }
|
|
.result-toast {
|
|
position: fixed; bottom: 24px; right: 24px;
|
|
z-index: 9999; min-width: 300px; max-width: 420px;
|
|
}
|
|
.days-input {
|
|
width: 72px; padding: 3px 8px; font-size: 0.85rem;
|
|
border-radius: 6px; border: 1px solid var(--bs-border-color);
|
|
background: var(--bs-body-bg); color: var(--bs-body-color); text-align: center;
|
|
}
|
|
.days-input:focus { outline: none; border-color: #0d6efd; box-shadow: 0 0 0 2px rgba(13,110,253,0.2); }
|
|
|
|
/* Stats section */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
gap: 12px;
|
|
padding: 16px 20px;
|
|
}
|
|
.stat-pill {
|
|
border: 1px solid var(--bs-border-color);
|
|
border-radius: 8px;
|
|
padding: 10px 14px;
|
|
text-align: center;
|
|
}
|
|
.stat-pill-value { font-size: 1.3rem; font-weight: 700; line-height: 1.1; }
|
|
.stat-pill-label { font-size: 0.72rem; color: var(--bs-secondary-color); margin-top: 3px; }
|
|
.stat-pill-sub { font-size: 0.7rem; color: var(--bs-secondary-color); margin-top: 2px; }
|
|
|
|
.key-count-badge {
|
|
font-size: 0.7rem;
|
|
font-weight: 500;
|
|
padding: 1px 6px;
|
|
border-radius: 10px;
|
|
background: var(--bs-tertiary-bg);
|
|
border: 1px solid var(--bs-border-color);
|
|
color: var(--bs-secondary-color);
|
|
display: inline-block;
|
|
min-width: 28px;
|
|
text-align: center;
|
|
}
|
|
.expired-badge {
|
|
font-size: 0.7rem;
|
|
font-weight: 500;
|
|
padding: 1px 6px;
|
|
border-radius: 10px;
|
|
background: rgba(220,53,69,0.1);
|
|
border: 1px solid rgba(220,53,69,0.3);
|
|
color: #dc3545;
|
|
display: inline-block;
|
|
min-width: 28px;
|
|
text-align: center;
|
|
}
|
|
.stats-indexers { padding: 0 20px 14px; }
|
|
.indexer-bar {
|
|
display: flex; align-items: center; gap: 8px;
|
|
font-size: 0.78rem; margin-bottom: 5px;
|
|
}
|
|
.indexer-bar-name { min-width: 90px; color: var(--bs-secondary-color); }
|
|
.indexer-bar-track { flex: 1; height: 6px; border-radius: 3px; background: var(--bs-tertiary-bg); overflow: hidden; }
|
|
.indexer-bar-fill { height: 100%; border-radius: 3px; background: var(--bs-info); }
|
|
.indexer-bar-count { font-size: 0.72rem; color: var(--bs-secondary-color); min-width: 50px; text-align: right; }
|
|
.danger-zone {
|
|
border-top: 2px solid rgba(220,53,69,0.3);
|
|
background: rgba(220,53,69,0.03);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
<!-- Toast notification -->
|
|
<div class="result-toast" id="resultToast" style="display:none;">
|
|
<div class="toast show align-items-center border-0" id="toastEl" role="alert">
|
|
<div class="d-flex">
|
|
<div class="toast-body" id="toastMsg"></div>
|
|
<button type="button" class="btn-close me-2 m-auto" id="toastClose"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Confirmation modal -->
|
|
<div class="modal fade" id="confirmModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h6 class="modal-title fw-semibold" id="confirmTitle">Confirmer l'action</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body pt-2" id="confirmBody"></div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Annuler</button>
|
|
<button type="button" class="btn btn-danger btn-sm" id="confirmBtn">Confirmer</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
|
|
<!-- ── Stats snapshot ── -->
|
|
<div class="col-12">
|
|
<div class="maint-card">
|
|
<div class="maint-card-header d-flex justify-content-between">
|
|
<span><i class="bi bi-bar-chart-line text-secondary me-2"></i>Aperçu instantané</span>
|
|
<button class="btn btn-link btn-sm p-0 text-secondary" id="refreshStats" title="Rafraîchir">
|
|
<i class="bi bi-arrow-clockwise"></i>
|
|
</button>
|
|
</div>
|
|
<div id="statsContent">
|
|
<div class="text-center text-secondary py-3" style="font-size:0.85rem;" id="statsLoading">
|
|
<span class="spinner-border spinner-border-sm me-2"></span>Chargement…
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── PostgreSQL ── -->
|
|
<div class="col-12 col-xl-6">
|
|
<div class="maint-card">
|
|
<div class="maint-card-header">
|
|
<i class="bi bi-database text-primary"></i> PostgreSQL
|
|
</div>
|
|
|
|
<!-- Clés API -->
|
|
<div class="maint-group-label">Clés API</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Désactiver les clés API expirées
|
|
<span class="expired-badge ms-1" id="expiredApiCount">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">Passe <code>is_active = false</code> sur les clés dont la date d'expiration est dépassée. Non destructif — les clés restent en base.</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="disable-expired-api-keys"
|
|
data-confirm-title="Désactiver les clés API expirées ?"
|
|
data-confirm-body="Les clés expirées encore actives seront désactivées (is_active = false). Opération réversible.">
|
|
<i class="bi bi-toggle-off me-1"></i>Désactiver
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">Supprimer les clés API expirées et inactives</div>
|
|
<div class="maint-action-desc">
|
|
Supprime définitivement les clés inactives non utilisées depuis plus de
|
|
<input type="number" class="days-input" id="apiKeyDays" value="30" min="1" max="365"> jours.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-danger flex-shrink-0 maint-btn"
|
|
data-action="clean-expired-api-keys"
|
|
data-confirm-title="Supprimer les clés API expirées ?"
|
|
data-confirm-body="Les clés non utilisées depuis le nombre de jours spécifié seront définitivement supprimées."
|
|
data-extra-field="apiKeyDays"
|
|
data-extra-param="days">
|
|
<i class="bi bi-trash3 me-1"></i>Supprimer
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">Réinitialiser les compteurs des clés API</div>
|
|
<div class="maint-action-desc">Remet à zéro <code>total_queries</code> et <code>latest_query_date</code> pour toutes les clés.</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary flex-shrink-0 maint-btn"
|
|
data-action="reset-api-key-counters"
|
|
data-confirm-title="Réinitialiser les compteurs ?"
|
|
data-confirm-body="Les compteurs de requêtes de toutes les clés API seront remis à zéro.">
|
|
<i class="bi bi-arrow-counterclockwise me-1"></i>Reset
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Peer Keys -->
|
|
<div class="maint-group-label">Peer Keys</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Désactiver les peer keys expirées
|
|
<span class="expired-badge ms-1" id="expiredPeerCount">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">Passe <code>is_active = false</code> sur les peer keys dont <code>expires_at</code> est dépassé. Non destructif.</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="disable-expired-peer-keys"
|
|
data-confirm-title="Désactiver les peer keys expirées ?"
|
|
data-confirm-body="Les peer keys expirées encore actives seront désactivées. Opération réversible.">
|
|
<i class="bi bi-toggle-off me-1"></i>Désactiver
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">Réinitialiser les compteurs des peer keys</div>
|
|
<div class="maint-action-desc">Remet à zéro <code>total_queries</code> et <code>last_used_at</code> pour toutes les peer keys.</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary flex-shrink-0 maint-btn"
|
|
data-action="reset-peer-key-counters"
|
|
data-confirm-title="Réinitialiser les compteurs peer ?"
|
|
data-confirm-body="Les compteurs de toutes les peer keys seront remis à zéro.">
|
|
<i class="bi bi-arrow-counterclockwise me-1"></i>Reset
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Cache Debrid PG -->
|
|
<div class="maint-group-label">Cache Debrid (PostgreSQL)</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Nettoyer le cache debrid expiré
|
|
<span class="expired-badge ms-1" id="expiredDebridCount">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">Supprime les entrées dont <code>expires_at</code> est dépassé.</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="clean-expired-debrid-cache"
|
|
data-confirm-title="Nettoyer le cache debrid expiré ?"
|
|
data-confirm-body="Les entrées dont expires_at est dépassé seront supprimées.">
|
|
<i class="bi bi-trash3 me-1"></i>Nettoyer
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">Vider tout le cache debrid</div>
|
|
<div class="maint-action-desc text-danger-emphasis">
|
|
Supprime <strong>toutes</strong> les entrées de <code>debrid_cache</code>.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-danger flex-shrink-0 maint-btn"
|
|
data-action="purge-debrid-cache"
|
|
data-confirm-title="Vider tout le cache debrid ?"
|
|
data-confirm-body="TOUTES les entrées de debrid_cache seront supprimées. Les prochaines requêtes seront plus lentes.">
|
|
<i class="bi bi-lightning me-1"></i>Tout vider
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Torrents -->
|
|
<div class="maint-group-label">Torrents</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">Nettoyer les torrents anciens</div>
|
|
<div class="maint-action-desc">
|
|
Supprime les <code>torrent_items</code> non mis à jour depuis plus de
|
|
<input type="number" class="days-input" id="torrentDays" value="90" min="1" max="730"> jours.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="clean-old-torrents"
|
|
data-confirm-title="Supprimer les anciens torrents ?"
|
|
data-confirm-body="Les entrées torrent_items non mises à jour depuis le nombre de jours spécifié seront supprimées."
|
|
data-extra-field="torrentDays"
|
|
data-extra-param="days">
|
|
<i class="bi bi-trash3 me-1"></i>Nettoyer
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Nettoyer les torrents orphelins
|
|
<span class="expired-badge ms-1" id="orphanTorrentCount">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">
|
|
Supprime les <code>torrent_items</code> sans <code>tmdb_id</code> créés il y a plus de
|
|
<input type="number" class="days-input" id="orphanDays" value="30" min="7" max="365"> jours.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="clean-orphan-torrents"
|
|
data-confirm-title="Supprimer les torrents orphelins ?"
|
|
data-confirm-body="Les torrents sans tmdb_id (non résolus) plus vieux que le nombre de jours spécifié seront supprimés."
|
|
data-extra-field="orphanDays"
|
|
data-extra-param="days">
|
|
<i class="bi bi-trash3 me-1"></i>Nettoyer
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Groupement de torrents -->
|
|
<div class="maint-group-label mt-3">Groupement de torrents (déduplication)</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Grouper par info_hash identique
|
|
<span class="badge bg-secondary ms-1" id="groupedItemsBadge">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">
|
|
Relie les <code>torrent_items</code> qui partagent exactement le même <code>info_hash</code>
|
|
(certitude 100% — même torrent repris par plusieurs indexeurs). Propagation automatique du TMDB ID.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-info flex-shrink-0 maint-btn"
|
|
data-action="group-by-infohash"
|
|
data-confirm-title="Grouper par info_hash ?"
|
|
data-confirm-body="Les torrents partageant le même info_hash seront reliés dans la table torrent_groups. Aucune suppression.">
|
|
<i class="bi bi-diagram-3 me-1"></i>Grouper
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">Grouper par titre normalisé + taille similaire</div>
|
|
<div class="maint-action-desc">
|
|
Détecte les torrents identiques publiés sous des titres légèrement différents (normalisation)
|
|
et avec une taille quasi-identique (±0,5 %). Couvre les cas où des trackers recalculent l'hash.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-info flex-shrink-0 maint-btn"
|
|
data-action="group-by-title-size"
|
|
data-confirm-title="Grouper par titre + taille ?"
|
|
data-confirm-body="Les torrents avec le même titre normalisé et une taille similaire (±0,5 %) seront groupés. Aucune suppression.">
|
|
<i class="bi bi-diagram-2 me-1"></i>Grouper
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">Propager les TMDB IDs dans les groupes</div>
|
|
<div class="maint-action-desc">
|
|
Pour chaque groupe existant, propage le <code>tmdb_id</code> consensuel
|
|
aux membres qui n'en ont pas encore un.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-success flex-shrink-0 maint-btn"
|
|
data-action="propagate-tmdb-groups"
|
|
data-confirm-title="Propager les TMDB IDs ?"
|
|
data-confirm-body="Le tmdb_id le plus fréquent au sein de chaque groupe sera assigné aux membres qui n'en ont pas. Les mismatches connus sont respectés.">
|
|
<i class="bi bi-arrow-repeat me-1"></i>Propager
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">Nettoyer les groupes vides</div>
|
|
<div class="maint-action-desc">
|
|
Supprime les entrées <code>torrent_groups</code> qui n'ont plus aucun membre.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary flex-shrink-0 maint-btn"
|
|
data-action="cleanup-empty-groups"
|
|
data-confirm-title="Supprimer les groupes vides ?"
|
|
data-confirm-body="Les groupes sans aucun torrent_item associé seront supprimés définitivement.">
|
|
<i class="bi bi-trash3 me-1"></i>Nettoyer
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Redis ── -->
|
|
<div class="col-12 col-xl-6">
|
|
<div class="maint-card">
|
|
<div class="maint-card-header">
|
|
<i class="bi bi-hdd-stack text-danger"></i> Redis Cache
|
|
</div>
|
|
|
|
<!-- Cache Recherche -->
|
|
<div class="maint-group-label">Cache Recherche</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Cache films & séries
|
|
<span class="key-count-badge ms-1" data-key-cat="searchcache">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">
|
|
<code>search:*</code> — Résultats de recherche de torrents. TTL : 7 jours.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-pattern"
|
|
data-pattern="searchcache"
|
|
data-confirm-title="Vider le cache de recherche ?"
|
|
data-confirm-body="Les résultats de recherche en cache (films et séries) seront supprimés. Les prochaines recherches interrogeront directement les indexers.">
|
|
<i class="bi bi-trash3 me-1"></i>Vider
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Cache Debrid Redis -->
|
|
<div class="maint-group-label">Cache Disponibilité Debrid</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Tous les services
|
|
<span class="key-count-badge ms-1" data-key-cat="debrid_avail">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">
|
|
<code>debrid_avail:*</code> — TTL : 10 jours (en cache) / 3 h (non dispo).
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-danger flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-pattern"
|
|
data-pattern="debrid_avail"
|
|
data-confirm-title="Vider tout le cache debrid Redis ?"
|
|
data-confirm-body="Toutes les entrées debrid_avail:* seront supprimées (tous services). Les prochaines requêtes repasseront par l'API debrid.">
|
|
<i class="bi bi-lightning me-1"></i>Tout vider
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Real-Debrid
|
|
<span class="key-count-badge ms-1" data-key-cat="debrid_avail_rd">—</span>
|
|
</div>
|
|
<div class="maint-action-desc"><code>debrid_avail:realdebrid:*</code></div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-pattern"
|
|
data-pattern="debrid_avail_rd"
|
|
data-confirm-title="Vider le cache Real-Debrid ?"
|
|
data-confirm-body="Toutes les entrées debrid_avail:realdebrid:* seront supprimées.">
|
|
<i class="bi bi-trash3 me-1"></i>Vider
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
AllDebrid
|
|
<span class="key-count-badge ms-1" data-key-cat="debrid_avail_ad">—</span>
|
|
</div>
|
|
<div class="maint-action-desc"><code>debrid_avail:alldebrid:*</code></div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-pattern"
|
|
data-pattern="debrid_avail_ad"
|
|
data-confirm-title="Vider le cache AllDebrid ?"
|
|
data-confirm-body="Toutes les entrées debrid_avail:alldebrid:* seront supprimées.">
|
|
<i class="bi bi-trash3 me-1"></i>Vider
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
TorBox
|
|
<span class="key-count-badge ms-1" data-key-cat="debrid_avail_tb">—</span>
|
|
</div>
|
|
<div class="maint-action-desc"><code>debrid_avail:torbox:*</code></div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-pattern"
|
|
data-pattern="debrid_avail_tb"
|
|
data-confirm-title="Vider le cache TorBox ?"
|
|
data-confirm-body="Toutes les entrées debrid_avail:torbox:* seront supprimées.">
|
|
<i class="bi bi-trash3 me-1"></i>Vider
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Premiumize
|
|
<span class="key-count-badge ms-1" data-key-cat="debrid_avail_pm">—</span>
|
|
</div>
|
|
<div class="maint-action-desc"><code>debrid_avail:premiumize:*</code></div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-pattern"
|
|
data-pattern="debrid_avail_pm"
|
|
data-confirm-title="Vider le cache Premiumize ?"
|
|
data-confirm-body="Toutes les entrées debrid_avail:premiumize:* seront supprimées.">
|
|
<i class="bi bi-trash3 me-1"></i>Vider
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Règles & Auth -->
|
|
<div class="maint-group-label">Règles & Auth</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Règles de matching de titres
|
|
<span class="key-count-badge ms-1" data-key-cat="title_matching">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">
|
|
<code>title_matching:*</code> — Règles de normalisation et de langue. Pas de TTL.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-pattern"
|
|
data-pattern="title_matching"
|
|
data-confirm-title="Vider le cache des règles de matching ?"
|
|
data-confirm-body="Les règles de normalisation et de langue en cache seront supprimées. Elles seront rechargées depuis la base au prochain appel.">
|
|
<i class="bi bi-trash3 me-1"></i>Vider
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Tokens Real-Debrid OAuth
|
|
<span class="key-count-badge ms-1" data-key-cat="rd_tokens">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">
|
|
<code>rd_access_token:*</code> — Tokens d'accès en cache. TTL : 12 h.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-pattern"
|
|
data-pattern="rd_tokens"
|
|
data-confirm-title="Supprimer les tokens Real-Debrid ?"
|
|
data-confirm-body="Les tokens d'accès Real-Debrid en cache seront supprimés. Ils seront renouvelés automatiquement au prochain appel.">
|
|
<i class="bi bi-trash3 me-1"></i>Vider
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Jobs & Limites -->
|
|
<div class="maint-group-label">Jobs & Limites</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Rate limits peers
|
|
<span class="key-count-badge ms-1" data-key-cat="ratelimit_peer">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">
|
|
<code>ratelimit:peer:*</code> — Compteurs de limitation de débit. TTL dynamique.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-pattern"
|
|
data-pattern="ratelimit_peer"
|
|
data-confirm-title="Réinitialiser les rate limits peers ?"
|
|
data-confirm-body="Tous les compteurs de limitation de débit peers seront remis à zéro.">
|
|
<i class="bi bi-arrow-counterclockwise me-1"></i>Reset
|
|
</button>
|
|
</div>
|
|
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title">
|
|
Verrous de rafraîchissement background
|
|
<span class="key-count-badge ms-1" data-key-cat="bg_refresh">—</span>
|
|
</div>
|
|
<div class="maint-action-desc">
|
|
<code>bg_refresh:*</code> — Verrous distribués par indexer/média. TTL : 6 h.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-secondary flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-pattern"
|
|
data-pattern="bg_refresh"
|
|
data-confirm-title="Supprimer les verrous background ?"
|
|
data-confirm-body="Les verrous de rafraîchissement background seront supprimés. Utile si un verrou est bloqué.">
|
|
<i class="bi bi-unlock me-1"></i>Débloquer
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Zone dangereuse -->
|
|
<div class="danger-zone">
|
|
<div class="maint-group-label" style="color:rgba(220,53,69,0.7);background:rgba(220,53,69,0.05);">Zone dangereuse</div>
|
|
<div class="maint-action">
|
|
<div class="maint-action-info">
|
|
<div class="maint-action-title text-danger">Flush complet Redis (FLUSHDB)</div>
|
|
<div class="maint-action-desc text-danger-emphasis">
|
|
Vide <strong>toute</strong> la base Redis — inclut les sessions actives et tous les caches.
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-danger flex-shrink-0 maint-btn"
|
|
data-action="flush-redis-all"
|
|
data-confirm-title="Vider tout Redis ?"
|
|
data-confirm-body="FLUSHDB sera exécuté. Toutes les clés Redis seront supprimées, y compris les sessions admin actives. Vous serez déconnecté.">
|
|
<i class="bi bi-lightning me-1"></i>FLUSHDB
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
|
|
let pendingAction = null;
|
|
|
|
// Wire up all maintenance buttons
|
|
document.querySelectorAll('.maint-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const action = btn.dataset.action;
|
|
const title = btn.dataset.confirmTitle;
|
|
const body = btn.dataset.confirmBody;
|
|
const pattern = btn.dataset.pattern || null;
|
|
const extraField = btn.dataset.extraField || null;
|
|
const extraParam = btn.dataset.extraParam || null;
|
|
|
|
document.getElementById('confirmTitle').textContent = title;
|
|
document.getElementById('confirmBody').textContent = body;
|
|
|
|
pendingAction = () => {
|
|
const fd = new FormData();
|
|
if (pattern) fd.append('pattern', pattern);
|
|
if (extraField) fd.append(extraParam, document.getElementById(extraField).value);
|
|
runAction('/admin/maintenance/' + action, fd);
|
|
};
|
|
modal.show();
|
|
});
|
|
});
|
|
|
|
document.getElementById('confirmBtn').addEventListener('click', () => {
|
|
modal.hide();
|
|
if (pendingAction) { pendingAction(); pendingAction = null; }
|
|
});
|
|
document.getElementById('toastClose').addEventListener('click', hideToast);
|
|
|
|
async function runAction(url, fd) {
|
|
fd.append('csrf_token', window.__ADMIN_CSRF__ || '');
|
|
try {
|
|
const res = await fetch(url, { method: 'POST', body: fd });
|
|
const data = await res.json();
|
|
showToast(data.success, data.message || (data.success ? 'Opération réussie' : 'Erreur'));
|
|
if (data.success) loadStats();
|
|
} catch (e) {
|
|
showToast(false, 'Erreur réseau : ' + e.message);
|
|
}
|
|
}
|
|
|
|
function showToast(success, message) {
|
|
const el = document.getElementById('toastEl');
|
|
const wrap = document.getElementById('resultToast');
|
|
el.className = 'toast show align-items-center border-0 text-bg-' + (success ? 'success' : 'danger');
|
|
document.getElementById('toastMsg').textContent = message;
|
|
wrap.style.display = 'block';
|
|
setTimeout(hideToast, 5000);
|
|
}
|
|
function hideToast() { document.getElementById('resultToast').style.display = 'none'; }
|
|
|
|
// ── Stats ──
|
|
async function loadStats() {
|
|
try {
|
|
const res = await fetch('/admin/maintenance/stats');
|
|
const data = await res.json();
|
|
const pg = data.pg || {};
|
|
const rd = data.redis || {};
|
|
|
|
const fmt = n => (typeof n === 'number') ? n.toLocaleString('fr-FR') : (n ?? '?');
|
|
const apiK = pg.api_keys || {};
|
|
const peerK = pg.peer_keys || {};
|
|
const dCache= pg.debrid_cache|| {};
|
|
const tItems= pg.torrent_items || {};
|
|
const sizes = pg.table_sizes || {};
|
|
|
|
// Update expired badges in the action list
|
|
if (typeof apiK.expired_active === 'number')
|
|
document.getElementById('expiredApiCount').textContent = fmt(apiK.expired_active);
|
|
if (typeof peerK.expired_active === 'number')
|
|
document.getElementById('expiredPeerCount').textContent = fmt(peerK.expired_active);
|
|
if (typeof dCache.expired === 'number')
|
|
document.getElementById('expiredDebridCount').textContent = fmt(dCache.expired);
|
|
if (typeof tItems.orphans === 'number')
|
|
document.getElementById('orphanTorrentCount').textContent = fmt(tItems.orphans);
|
|
const tGroups = pg.torrent_groups || {};
|
|
if (typeof tGroups.grouped_items === 'number')
|
|
document.getElementById('groupedItemsBadge').textContent = fmt(tGroups.grouped_items) + ' groupé(s)';
|
|
|
|
// Update Redis key count badges
|
|
const keyCounts = rd.key_counts || {};
|
|
document.querySelectorAll('[data-key-cat]').forEach(el => {
|
|
const cat = el.dataset.keyCat;
|
|
el.textContent = cat in keyCounts ? fmt(keyCounts[cat]) : '—';
|
|
});
|
|
|
|
// Build PostgreSQL pills
|
|
const pgItems = [
|
|
{ label: 'Clés API (total)', value: fmt(apiK.total), color: 'text-primary',
|
|
sub: apiK.expired_active > 0 ? `${fmt(apiK.expired_active)} expirée(s) active(s)` : null },
|
|
{ label: 'Peer keys (total)', value: fmt(peerK.total), color: 'text-success',
|
|
sub: peerK.expired_active > 0 ? `${fmt(peerK.expired_active)} expirée(s) active(s)` : null },
|
|
{ label: 'Cache debrid', value: fmt(dCache.total), color: 'text-warning',
|
|
sub: dCache.expired > 0 ? `${fmt(dCache.expired)} expirée(s)` : null },
|
|
{ label: 'Torrents indexés', value: fmt(tItems.total), color: 'text-info',
|
|
sub: [
|
|
tItems.orphans > 0 ? `${fmt(tItems.orphans)} orphelins` : null,
|
|
tItems.old_90d > 0 ? `${fmt(tItems.old_90d)} >90j` : null,
|
|
].filter(Boolean).join(' · ') || null },
|
|
{ label: 'Groupes torrents', value: fmt(tGroups.total_groups), color: 'text-info',
|
|
sub: [
|
|
tGroups.grouped_items > 0 ? `${fmt(tGroups.grouped_items)} groupé(s)` : null,
|
|
tGroups.ungrouped_items > 0 ? `${fmt(tGroups.ungrouped_items)} non groupé(s)` : null,
|
|
tGroups.groups_with_tmdb > 0 ? `${fmt(tGroups.groups_with_tmdb)} avec TMDB` : null,
|
|
].filter(Boolean).join(' · ') || null },
|
|
{ label: 'Mappings méta', value: fmt(pg.metadata_mappings),color: 'text-secondary', sub: null },
|
|
];
|
|
if (sizes.torrent_items)
|
|
pgItems[3].sub = (pgItems[3].sub ? pgItems[3].sub + ' · ' : '') + sizes.torrent_items;
|
|
|
|
// Build Redis pills
|
|
const rdItems = [
|
|
{ label: 'Clés Redis (total)', value: fmt(rd.total_keys), color: 'text-danger', sub: rd.used_memory_human || null },
|
|
{ label: 'Cache recherche', value: fmt(keyCounts.searchcache), color: 'text-info', sub: 'search:*' },
|
|
{ label: 'Debrid avail', value: fmt(keyCounts.debrid_avail),color: 'text-warning',sub: 'debrid_avail:*' },
|
|
{ label: 'Rate limits', value: fmt(keyCounts.ratelimit_peer), color: 'text-secondary', sub: 'ratelimit:peer:*' },
|
|
{ label: 'BG refresh locks', value: fmt(keyCounts.bg_refresh), color: 'text-secondary', sub: 'bg_refresh:*' },
|
|
];
|
|
|
|
const makePills = items => items.map(it => `
|
|
<div class="stat-pill">
|
|
<div class="stat-pill-value ${it.color}">${it.value}</div>
|
|
<div class="stat-pill-label">${it.label}</div>
|
|
${it.sub ? `<div class="stat-pill-sub">${it.sub}</div>` : ''}
|
|
</div>`).join('');
|
|
|
|
// Top indexers bar chart
|
|
const topIdx = pg.top_indexers || [];
|
|
let indexerHtml = '';
|
|
if (topIdx.length) {
|
|
const maxCount = topIdx[0].count || 1;
|
|
indexerHtml = `
|
|
<div style="padding:4px 20px 0;font-size:0.72rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--bs-secondary-color);border-top:1px solid var(--bs-border-color);">Top indexers (torrents)</div>
|
|
<div class="stats-indexers">${topIdx.map(row => `
|
|
<div class="indexer-bar">
|
|
<div class="indexer-bar-name">${row.indexer || '(null)'}</div>
|
|
<div class="indexer-bar-track"><div class="indexer-bar-fill" style="width:${Math.round(row.count/maxCount*100)}%"></div></div>
|
|
<div class="indexer-bar-count">${fmt(row.count)}</div>
|
|
</div>`).join('')}
|
|
</div>`;
|
|
}
|
|
|
|
document.getElementById('statsContent').innerHTML = `
|
|
<div style="padding:10px 20px 4px;font-size:0.72rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--bs-secondary-color);">PostgreSQL</div>
|
|
<div class="stats-grid" style="padding-top:8px;">${makePills(pgItems)}</div>
|
|
${indexerHtml}
|
|
<div style="padding:4px 20px 4px;font-size:0.72rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--bs-secondary-color);border-top:1px solid var(--bs-border-color);">Redis</div>
|
|
<div class="stats-grid" style="padding-top:8px;">${makePills(rdItems)}</div>`;
|
|
} catch (e) {
|
|
document.getElementById('statsContent').innerHTML =
|
|
'<div class="text-secondary text-center py-3" style="font-size:0.85rem;">Impossible de charger les statistiques.</div>';
|
|
}
|
|
}
|
|
|
|
document.getElementById('refreshStats').addEventListener('click', loadStats);
|
|
loadStats();
|
|
</script>
|
|
{% endblock %}
|