520be53901
- Add proper Alembic initial migration (0001_initial_schema.py) - Migrate refresh tokens from JSON file to SQLite (RefreshTokenTable) - Remove Neko-Sama provider entirely (redirects to Gupy, not a host) - Fix provider health check always showing UNKNOWN - Run check_all_health() on startup - Fix POST /providers/health/check background task bug - Add HTMX refresh after manual health check trigger - Fix anime search relevance scoring with MIN_RELEVANCE_THRESHOLD=0.5 - Replace bare 'except:' with 'except Exception:' across codebase - Add Playwright E2E test suite (12 tests, auth setup, helpers) - Fix toast container blocking clicks via pointer-events: none - Remove obsolete Jest/Vite test files and config - Clean up obsolete test_watchlist scripts - Update sonarr model comment for active providers
230 lines
12 KiB
HTML
230 lines
12 KiB
HTML
<div class="settings-container section-container">
|
|
<div class="section-header">
|
|
<h2>Parametres</h2>
|
|
</div>
|
|
|
|
<!-- General Preferences -->
|
|
<div class="settings-card card" style="margin-bottom: 30px; padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid rgba(255,255,255,0.05);">
|
|
<h3 style="margin-bottom: 20px; color: var(--primary);">General</h3>
|
|
|
|
<form id="settings-form" class="settings-form">
|
|
<div class="form-group">
|
|
<label for="default_lang">Langue par defaut</label>
|
|
<select name="default_lang" id="default_lang" class="btn btn-secondary btn-block" style="text-align: left; padding: 12px 15px;">
|
|
<option value="vostfr" {% if settings.default_lang == 'vostfr' %}selected{% endif %}>VOSTFR</option>
|
|
<option value="vf" {% if settings.default_lang == 'vf' %}selected{% endif %}>VF</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group" style="margin-top: 20px;">
|
|
<label for="theme">Theme</label>
|
|
<select name="theme" id="theme" class="btn btn-secondary btn-block" style="text-align: left; padding: 12px 15px;">
|
|
<option value="dark" {% if settings.theme == 'dark' %}selected{% endif %}>Sombre (Premium Dark)</option>
|
|
<option value="light" {% if settings.theme == 'light' %}selected{% endif %}>Clair (Soon)</option>
|
|
<option value="oled" {% if settings.theme == 'oled' %}selected{% endif %}>OLED (Noir absolu)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group" style="margin-top: 20px;">
|
|
<label for="download_dir">Repertoire de telechargement</label>
|
|
<div style="display: flex; gap: 8px;">
|
|
<input type="text" name="download_dir" id="download_dir" value="{{ settings.download_dir }}"
|
|
class="btn btn-secondary" style="flex: 1; text-align: left; padding: 12px 15px;">
|
|
</div>
|
|
<small style="color: var(--text-dim); font-size: 12px; margin-top: 5px; display: block;">
|
|
Repertoire ou les fichiers seront telecharges (defaut: downloads/)
|
|
</small>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary" style="margin-top: 20px; width: 100%;" onclick="event.preventDefault(); saveSettings();">
|
|
<i class="fas fa-save"></i> Enregistrer les preferences
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Content Filters -->
|
|
<div class="settings-card card" style="margin-bottom: 30px; padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid rgba(255,255,255,0.05);">
|
|
<h3 style="margin-bottom: 20px; color: var(--primary);">Filtres de contenu</h3>
|
|
|
|
<div class="form-group">
|
|
<label for="recommendations_filter">Recommande pour vous : afficher</label>
|
|
<select name="recommendations_filter" id="recommendations_filter" class="btn btn-secondary btn-block" style="text-align: left; padding: 12px 15px;" onchange="saveFilter('recommendations_filter', this.value)">
|
|
<option value="all" {% if settings.recommendations_filter == 'all' %}selected{% endif %}>Les deux (Animes + Series)</option>
|
|
<option value="anime" {% if settings.recommendations_filter == 'anime' %}selected{% endif %}>Animes uniquement</option>
|
|
<option value="series" {% if settings.recommendations_filter == 'series' %}selected{% endif %}>Series uniquement</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group" style="margin-top: 15px;">
|
|
<label for="releases_filter">Dernieres sorties : afficher</label>
|
|
<select name="releases_filter" id="releases_filter" class="btn btn-secondary btn-block" style="text-align: left; padding: 12px 15px;" onchange="saveFilter('releases_filter', this.value)">
|
|
<option value="all" {% if settings.releases_filter == 'all' %}selected{% endif %}>Les deux (Animes + Series)</option>
|
|
<option value="anime" {% if settings.releases_filter == 'anime' %}selected{% endif %}>Animes uniquement</option>
|
|
<option value="series" {% if settings.releases_filter == 'series' %}selected{% endif %}>Series uniquement</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Categories -->
|
|
<div class="settings-card card" style="margin-bottom: 30px; padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid rgba(255,255,255,0.05);">
|
|
<h3 style="margin-bottom: 20px; color: var(--primary);">Categories</h3>
|
|
<p style="color: var(--text-dim); font-size: 13px; margin-bottom: 15px;">Activez ou desactivez les categories. Au moins une doit rester active.</p>
|
|
|
|
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
|
|
<label class="toggle-card" style="flex: 1; min-width: 200px; padding: 15px; background: rgba(255,255,255,0.02); border-radius: 10px; border: 1px solid rgba(255,255,255,0.05); display: flex; align-items: center; justify-content: space-between; cursor: pointer;">
|
|
<div>
|
|
<div style="font-weight: 600; font-size: 1.1rem;">Animes</div>
|
|
<div style="font-size: 0.8rem; color: var(--text-dim);">Films et series anime</div>
|
|
</div>
|
|
<input type="checkbox" id="anime_enabled" {% if settings.anime_enabled %}checked{% endif %} onchange="toggleCategory('anime_enabled', this.checked)" style="width: 20px; height: 20px; cursor: pointer; accent-color: var(--primary);">
|
|
</label>
|
|
|
|
<label class="toggle-card" style="flex: 1; min-width: 200px; padding: 15px; background: rgba(255,255,255,0.02); border-radius: 10px; border: 1px solid rgba(255,255,255,0.05); display: flex; align-items: center; justify-content: space-between; cursor: pointer;">
|
|
<div>
|
|
<div style="font-weight: 600; font-size: 1.1rem;">Series TV</div>
|
|
<div style="font-size: 0.8rem; color: var(--text-dim);">Series americaines et europeennes</div>
|
|
</div>
|
|
<input type="checkbox" id="series_enabled" {% if settings.series_enabled %}checked{% endif %} onchange="toggleCategory('series_enabled', this.checked)" style="width: 20px; height: 20px; cursor: pointer; accent-color: var(--primary);">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Providers Management -->
|
|
<div class="settings-card card" style="padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid rgba(255,255,255,0.05);">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
<h3 style="margin: 0; color: var(--primary);">Disponibilite des Fournisseurs</h3>
|
|
<button class="btn btn-secondary btn-small" hx-post="/api/providers/health/check" hx-swap="none"
|
|
hx-on::after-request="htmx.trigger('#tab-settings > div', 'refresh-settings')">
|
|
<i class="fas fa-sync-alt"></i> Forcer verification
|
|
</button>
|
|
</div>
|
|
|
|
<div class="providers-settings-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;">
|
|
{% for provider in providers %}
|
|
<div class="provider-status-card" style="padding: 15px; background: rgba(255,255,255,0.02); border-radius: 10px; border: 1px solid rgba(255,255,255,0.05); display: flex; align-items: center; justify-content: space-between;">
|
|
<div style="display: flex; align-items: center; gap: 12px;">
|
|
<span style="font-size: 1.5rem;">{{ provider.icon }}</span>
|
|
<div>
|
|
<div style="font-weight: 600;">{{ provider.name }}</div>
|
|
<div style="font-size: 0.75rem; display: flex; align-items: center; gap: 5px;">
|
|
<span class="status-dot" style="width: 8px; height: 8px; border-radius: 50%; background: {% if provider.status == 'up' %}var(--accent){% elif provider.status == 'down' %}var(--danger){% else %}#aaa{% endif %};"></span>
|
|
<span style="color: {% if provider.status == 'up' %}var(--accent){% elif provider.status == 'down' %}var(--danger){% else %}var(--text-dim){% endif %}; text-transform: uppercase; font-weight: 800;">
|
|
{{ provider.status | upper }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn {% if provider.enabled %}btn-secondary{% else %}btn-accent{% endif %} btn-sm"
|
|
hx-post="/api/settings/providers/{{ provider.id }}/toggle"
|
|
hx-swap="none"
|
|
hx-on::after-request="htmx.trigger('#tab-settings > div', 'refresh-settings')"
|
|
style="min-width: 100px;">
|
|
{% if provider.enabled %}Desactiver{% else %}Activer{% endif %}
|
|
</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function getToken() {
|
|
return localStorage.getItem('auth_token') || null;
|
|
}
|
|
|
|
async function saveSettings() {
|
|
const token = getToken();
|
|
if (!token) return;
|
|
|
|
const data = {
|
|
default_lang: document.getElementById('default_lang').value,
|
|
theme: document.getElementById('theme').value,
|
|
download_dir: document.getElementById('download_dir').value,
|
|
};
|
|
|
|
try {
|
|
const r = await fetch('/api/settings', {
|
|
method: 'PATCH',
|
|
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (r.ok) {
|
|
showToast('Preferences enregistrees', 'success');
|
|
}
|
|
} catch (e) {
|
|
showToast('Erreur: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function saveFilter(field, value) {
|
|
const token = getToken();
|
|
if (!token) return;
|
|
|
|
try {
|
|
const r = await fetch('/api/settings', {
|
|
method: 'PATCH',
|
|
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ [field]: value })
|
|
});
|
|
if (r.ok) {
|
|
showToast('Filtre mis a jour', 'success');
|
|
}
|
|
} catch (e) {
|
|
showToast('Erreur: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function toggleCategory(field, value) {
|
|
const token = getToken();
|
|
if (!token) return;
|
|
|
|
// Prevent disabling both
|
|
if (!value) {
|
|
const otherField = field === 'anime_enabled' ? 'series_enabled' : 'anime_enabled';
|
|
const otherCheckbox = document.getElementById(otherField);
|
|
if (otherCheckbox && !otherCheckbox.checked) {
|
|
showToast('Au moins une categorie doit rester active', 'error');
|
|
document.getElementById(field).checked = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const r = await fetch('/api/settings', {
|
|
method: 'PATCH',
|
|
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ [field]: value })
|
|
});
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(() => ({}));
|
|
showToast(err.detail || 'Erreur', 'error');
|
|
document.getElementById(field).checked = !value;
|
|
} else {
|
|
showToast('Categorie ' + (value ? 'activee' : 'desactivee'), 'success');
|
|
}
|
|
} catch (e) {
|
|
showToast('Erreur: ' + e.message, 'error');
|
|
document.getElementById(field).checked = !value;
|
|
}
|
|
}
|
|
|
|
function showToast(message, type) {
|
|
const event = new CustomEvent('show-toast', { detail: { message, type } });
|
|
document.dispatchEvent(event);
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.settings-form label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 600;
|
|
color: var(--text-dim);
|
|
}
|
|
.status-dot {
|
|
display: inline-block;
|
|
box-shadow: 0 0 5px currentColor;
|
|
}
|
|
</style>
|