mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Mirrors the mots-clé tag-search system: dropdown suggestions from existing languages via HTMX, pill display with bin-icon remove buttons, 'Créer' option for new languages. Replaces the plain text input. - New partial: templates/partials/form/language-search.php - New fragment: public/partage/language-search-fragment.php - Admin wrapper: public/admin/language-search-fragment.php - Updated language-autre-fragment to return just the required asterisk indicator - Updated both controllers to handle language_autre as array (pill-based) with backward-compatible string path - Updated edit form to compute selectedOtherLanguages from DB - Registered new route in partage/index.php - Fix CSV importer: split comma-separated language column into individual entries - Add htmx active search to admin index, title line-clamp, predefined languages only in checkboxes - Admin index: filter form now uses htmx triggers (input delay:300ms on search, change on selects) to actively search without page reload - Sort links include hx-push-url for back-button support - Added loading indicator bar (.admin-search-indicator) - Title column: line-clamp at 2 lines with overflow hidden, native title attr tooltip for full text - Language checkboxes now show only 3 predefined languages (Français, Anglais, Néerlandais); all others go via the Autre langue search component - Added Database::getPredefinedLanguages() and excluded predefined from language-search-fragment suggestions - Included hidden sort/dir inputs in table-wrap so sort state preserved across filter changes - Fix language-search: block 'Créer' for predefined languages in dropdown The 'Créer' option in the language-search dropdown now also checks against the predefined set (français, anglais, néerlandais) to avoid offering creation of languages that already exist as checkboxes.
543 lines
29 KiB
PHP
543 lines
29 KiB
PHP
<main id="main-content">
|
||
<h1>Paramètres</h1>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════
|
||
MAINTENANCE
|
||
══════════════════════════════════════════════════════════════ -->
|
||
<section aria-labelledby="settings-maintenance-title">
|
||
<h2 id="settings-maintenance-title">Maintenance</h2>
|
||
|
||
<div class="param-maintenance-row">
|
||
<?php if ($maintenanceOn): ?>
|
||
<p>
|
||
<strong>⚠ Mode maintenance activé</strong> — le site public est inaccessible.
|
||
</p>
|
||
<form method="post" action="actions/maintenance.php">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||
<input type="hidden" name="action" value="disable_maintenance">
|
||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||
<button type="submit">Désactiver la maintenance</button>
|
||
</form>
|
||
<?php else: ?>
|
||
<p>Site public : <strong>en ligne</strong></p>
|
||
<form method="post" action="actions/maintenance.php" id="enable-maintenance-form">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||
<input type="hidden" name="action" value="enable_maintenance">
|
||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||
<button type="button" class="btn btn--warning"
|
||
onclick="document.getElementById('enable-maintenance-dialog').showModal()">
|
||
Activer la maintenance
|
||
</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════
|
||
RELAY SMTP
|
||
══════════════════════════════════════════════════════════════ -->
|
||
<section aria-labelledby="settings-smtp-title">
|
||
<h2 id="settings-smtp-title">Relay SMTP</h2>
|
||
<p>
|
||
Identifiants du serveur SMTP utilisé pour l'envoi d'e-mails
|
||
(notifications, partage de TFE, etc.).
|
||
</p>
|
||
<div class="param-smtp-status">
|
||
<?php if ($smtpConfigured): ?>
|
||
<span class="param-badge-ok">✓ Configuré</span>
|
||
<span><?= htmlspecialchars($smtpSettings['host']) ?>:<?= (int)$smtpSettings['port'] ?> (<?= htmlspecialchars($smtpSettings['encryption']) ?>)</span>
|
||
<?php else: ?>
|
||
<span class="param-badge-warn">✗ Non configuré</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php
|
||
// Inline helper: emit aria-invalid + error <small> when this field is the culprit
|
||
$smtpFieldErr = function(string $id) use ($smtpErrorField): string {
|
||
return $smtpErrorField === $id ? ' aria-invalid="true"' : '';
|
||
};
|
||
$smtpFieldMsg = function(string $id, string $msg) use ($smtpErrorField): string {
|
||
return $smtpErrorField === $id
|
||
? '<small class="param-field-error" id="' . $id . '-error">' . htmlspecialchars($msg) . '</small>'
|
||
: '';
|
||
};
|
||
// Human-readable hints per field (brief — the full message is in the toast)
|
||
$smtpHints = [
|
||
'smtp_host' => 'Vérifiez l’adresse du serveur SMTP.',
|
||
'smtp_port' => 'Vérifiez le numéro de port.',
|
||
'smtp_encryption' => 'Vérifiez le mode de chiffrement.',
|
||
'smtp_username' => 'Vérifiez le nom d’utilisateur.',
|
||
'smtp_password' => 'Mot de passe incorrect.',
|
||
];
|
||
?>
|
||
<form method="post" action="actions/settings.php" class="param-form"
|
||
<?= $smtpErrorField ? 'data-smtp-error-field="' . htmlspecialchars($smtpErrorField) . '"' : '' ?>>
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||
<input type="hidden" name="section" value="smtp">
|
||
|
||
<div class="param-grid">
|
||
<div>
|
||
<label for="smtp_host">Hôte SMTP</label>
|
||
<input type="text" id="smtp_host" name="smtp_host"
|
||
value="<?= htmlspecialchars($smtpSettings['host']) ?>"
|
||
placeholder="smtp.example.com"
|
||
<?= $smtpFieldErr('smtp_host') ?>
|
||
<?= $smtpErrorField === 'smtp_host' ? 'aria-describedby="smtp_host-error"' : '' ?>>
|
||
<?= $smtpFieldMsg('smtp_host', $smtpHints['smtp_host']) ?>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="smtp_port">Port</label>
|
||
<input type="number" id="smtp_port" name="smtp_port"
|
||
value="<?= (int)$smtpSettings['port'] ?>"
|
||
min="1" max="65535"
|
||
<?= $smtpFieldErr('smtp_port') ?>
|
||
<?= $smtpErrorField === 'smtp_port' ? 'aria-describedby="smtp_port-error"' : '' ?>>
|
||
<?= $smtpFieldMsg('smtp_port', $smtpHints['smtp_port']) ?>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="smtp_encryption">Chiffrement</label>
|
||
<select id="smtp_encryption" name="smtp_encryption"
|
||
<?= $smtpFieldErr('smtp_encryption') ?>
|
||
<?= $smtpErrorField === 'smtp_encryption' ? 'aria-describedby="smtp_encryption-error"' : '' ?>>
|
||
<option value="tls" <?= $smtpSettings['encryption'] === 'tls' ? 'selected' : '' ?>>TLS (STARTTLS)</option>
|
||
<option value="ssl" <?= $smtpSettings['encryption'] === 'ssl' ? 'selected' : '' ?>>SSL (SMTPS)</option>
|
||
<option value="none" <?= $smtpSettings['encryption'] === 'none' ? 'selected' : '' ?>>Aucun</option>
|
||
</select>
|
||
<?= $smtpFieldMsg('smtp_encryption', $smtpHints['smtp_encryption']) ?>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="smtp_username">Nom d'utilisateur</label>
|
||
<input type="text" id="smtp_username" name="smtp_username"
|
||
value="<?= htmlspecialchars($smtpSettings['username']) ?>"
|
||
<?= $smtpFieldErr('smtp_username') ?>
|
||
<?= $smtpErrorField === 'smtp_username' ? 'aria-describedby="smtp_username-error"' : '' ?>>
|
||
<?= $smtpFieldMsg('smtp_username', $smtpHints['smtp_username']) ?>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="smtp_password">Mot de passe</label>
|
||
<input type="password" id="smtp_password" name="smtp_password"
|
||
value=""
|
||
autocomplete="new-password"
|
||
placeholder="Laissez vide pour ne pas modifier"
|
||
<?= $smtpFieldErr('smtp_password') ?>
|
||
<?= $smtpErrorField === 'smtp_password' ? 'aria-describedby="smtp_password-error"' : '' ?>>
|
||
<?= $smtpFieldMsg('smtp_password', $smtpHints['smtp_password']) ?>
|
||
</div>
|
||
</div>
|
||
|
||
<fieldset>
|
||
<legend>Expéditeur par défaut</legend>
|
||
<div class="param-grid">
|
||
<div>
|
||
<label for="smtp_from_email">Adresse e-mail d'expédition</label>
|
||
<input type="email" id="smtp_from_email" name="smtp_from_email"
|
||
value="<?= htmlspecialchars($smtpSettings['from_email']) ?>"
|
||
placeholder="noreply@example.com">
|
||
<small>Adresse utilisée comme expéditeur (champ From:).</small>
|
||
</div>
|
||
<div>
|
||
<label for="smtp_from_name">Nom d'expéditeur</label>
|
||
<input type="text" id="smtp_from_name" name="smtp_from_name"
|
||
value="<?= htmlspecialchars($smtpSettings['from_name']) ?>">
|
||
</div>
|
||
<div>
|
||
<label for="smtp_notify_email">Adresse de notification admin</label>
|
||
<input type="email" id="smtp_notify_email" name="smtp_notify_email"
|
||
value="<?= htmlspecialchars($smtpSettings['notify_email'] ?? '') ?>"
|
||
placeholder="admin@example.com">
|
||
<small>Reçoit les notifications (demandes d’accès, etc.). Si vide, utilise l’adresse d’expédition.</small>
|
||
</div>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<button type="submit" class="btn btn--primary">Enregistrer</button>
|
||
</form>
|
||
|
||
<!-- Test d'envoi -->
|
||
<fieldset class="param-smtp-test">
|
||
<legend>Tester l'envoi d'un e-mail</legend>
|
||
<p>Envoie un e-mail de test via le relay SMTP configuré ci-dessus.</p>
|
||
<?php if (!$smtpConfigured): ?>
|
||
<p class="param-note">⚠ Configurez le relay SMTP avant de pouvoir tester l'envoi.</p>
|
||
<?php else: ?>
|
||
<form method="post" action="actions/smtp-test.php" class="param-form">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||
<div class="param-smtp-test-row">
|
||
<div>
|
||
<label for="smtp_test_email">Adresse de destination</label>
|
||
<input type="email" id="smtp_test_email" name="test_email"
|
||
placeholder="test@example.com" required>
|
||
</div>
|
||
<button type="submit" class="btn btn--primary">Envoyer le test</button>
|
||
</div>
|
||
</form>
|
||
<?php endif; ?>
|
||
</fieldset>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════
|
||
PEERTUBE
|
||
══════════════════════════════════════════════════════════════ -->
|
||
<section aria-labelledby="settings-peertube-title">
|
||
<h2 id="settings-peertube-title">PeerTube</h2>
|
||
<p>
|
||
Intégration avec une instance PeerTube pour l'hébergement des vidéos et fichiers audio.
|
||
Les fichiers sont uploadés via l'API PeerTube et intégrés comme lecteurs embarqués sur la page du TFE.
|
||
</p>
|
||
<p class="param-note">
|
||
⚠ L'activation nécessite un quota d'upload suffisant sur l'instance PeerTube.
|
||
Laissez désactivé jusqu'à obtention du quota.
|
||
</p>
|
||
|
||
<div class="param-smtp-status">
|
||
<?php if ($peerTubeConfigured): ?>
|
||
<span class="param-badge-ok">✓ Configuré</span>
|
||
<span><?= htmlspecialchars($peerTubeSettings['instance_url']) ?></span>
|
||
<?php else: ?>
|
||
<span class="param-badge-warn">✗ Non configuré</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<form method="post" action="actions/settings.php" class="param-form">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||
<input type="hidden" name="section" value="peertube">
|
||
|
||
<fieldset>
|
||
<legend>Activation</legend>
|
||
<label class="param-checkbox">
|
||
<input type="checkbox" name="peertube_upload_enabled" value="1"
|
||
<?= $peerTubeEnabled ? 'checked' : '' ?>>
|
||
<span>
|
||
<strong>Activer l'upload PeerTube</strong><br>
|
||
<small>Lorsqu'activé, les fichiers vidéo et audio soumis via le formulaire sont uploadés
|
||
sur l'instance PeerTube plutôt que stockés localement sur le serveur.</small>
|
||
</span>
|
||
</label>
|
||
</fieldset>
|
||
|
||
<div class="param-grid">
|
||
<div>
|
||
<label for="peertube_instance_url">URL de l'instance PeerTube</label>
|
||
<input type="url" id="peertube_instance_url" name="peertube_instance_url"
|
||
value="<?= htmlspecialchars($peerTubeSettings['instance_url']) ?>"
|
||
placeholder="https://peertube.example.com">
|
||
<small>Sans slash final. Ex : <code>https://peertube.erg.be</code></small>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="peertube_username">Nom d'utilisateur</label>
|
||
<input type="text" id="peertube_username" name="peertube_username"
|
||
value="<?= htmlspecialchars($peerTubeSettings['username']) ?>"
|
||
autocomplete="username">
|
||
</div>
|
||
|
||
<div>
|
||
<label for="peertube_password">Mot de passe</label>
|
||
<input type="password" id="peertube_password" name="peertube_password"
|
||
value=""
|
||
autocomplete="new-password"
|
||
placeholder="Laissez vide pour ne pas modifier">
|
||
</div>
|
||
|
||
<div>
|
||
<label for="peertube_channel_id">ID de la chaîne</label>
|
||
<input type="number" id="peertube_channel_id" name="peertube_channel_id"
|
||
value="<?= (int)$peerTubeSettings['channel_id'] ?>"
|
||
min="1">
|
||
<small>Identifiant numérique de la chaîne PeerTube cible.</small>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="peertube_privacy">Visibilité des vidéos</label>
|
||
<select id="peertube_privacy" name="peertube_privacy">
|
||
<option value="1" <?= (int)$peerTubeSettings['privacy'] === 1 ? 'selected' : '' ?>>Publique</option>
|
||
<option value="2" <?= (int)$peerTubeSettings['privacy'] === 2 ? 'selected' : '' ?>>Non listée</option>
|
||
<option value="3" <?= (int)$peerTubeSettings['privacy'] === 3 ? 'selected' : '' ?>>Privée</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn--primary">Enregistrer</button>
|
||
</form>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════
|
||
COMPTE ADMINISTRATEUR
|
||
══════════════════════════════════════════════════════════════ -->
|
||
<section aria-labelledby="settings-account-title">
|
||
<h2 id="settings-account-title">Compte administrateur</h2>
|
||
|
||
<dl class="param-account-status">
|
||
<div>
|
||
<dt>Authentification PHP</dt>
|
||
<dd><?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Active'; $badgeWarnLabel = 'Non configurée'; include APP_ROOT . '/templates/partials/status-badge.php'; ?></dd>
|
||
</div>
|
||
<div>
|
||
<dt>Stockage du hash</dt>
|
||
<dd>
|
||
<code>site_settings (DB)</code>
|
||
<?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Présent'; $badgeWarnLabel = 'Absent'; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
|
||
<?php if (!$hasPassword): ?>
|
||
<p class="param-note">
|
||
Aucun mot de passe PHP configuré. Le formulaire ci-dessous stockera
|
||
un hash bcrypt dans la base de données.
|
||
</p>
|
||
<?php endif; ?>
|
||
|
||
<form method="post" action="/admin/actions/account.php" class="param-form" autocomplete="off">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||
|
||
<?php if ($hasPassword): ?>
|
||
<div>
|
||
<label for="current_password">Mot de passe actuel</label>
|
||
<input type="password" id="current_password"
|
||
name="current_password" required autocomplete="current-password">
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div>
|
||
<label for="new_password">Nouveau mot de passe</label>
|
||
<input type="password" id="new_password"
|
||
name="new_password" required autocomplete="new-password"
|
||
minlength="12">
|
||
<small>Minimum 12 caractères.</small>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="confirm_password">Confirmer le mot de passe</label>
|
||
<input type="password" id="confirm_password"
|
||
name="confirm_password" required autocomplete="new-password">
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn--primary">
|
||
<?= $hasPassword ? 'Mettre à jour le mot de passe' : 'Définir le mot de passe' ?>
|
||
</button>
|
||
</form>
|
||
|
||
<!-- Danger zone: remove credentials -->
|
||
<?php if ($hasPassword): ?>
|
||
<fieldset class="param-danger-zone">
|
||
<legend>Supprimer la configuration du mot de passe PHP</legend>
|
||
<p>
|
||
Supprime le hash de la base de données. L'accès admin
|
||
dépendra uniquement de l'authentification nginx Basic Auth si elle est configurée.
|
||
</p>
|
||
<?php /* TODO: replace this browser confirm() with a proper <dialog> modal like the other confirmations */ ?>
|
||
<form method="post" action="/admin/actions/account.php"
|
||
onsubmit="return confirm('Supprimer le mot de passe PHP ? L\'accès admin ne sera protégé que par nginx Basic Auth.')">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||
<input type="hidden" name="action" value="remove_credentials">
|
||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||
<input type="hidden" name="current_password_remove" value="">
|
||
<button type="submit" class="btn btn--danger">Supprimer le mot de passe</button>
|
||
</form>
|
||
</fieldset>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════
|
||
SYSTÈME
|
||
══════════════════════════════════════════════════════════════ -->
|
||
<section aria-labelledby="settings-system-title">
|
||
<h2 id="settings-system-title">Système</h2>
|
||
|
||
<p class="sys-refresh-note">
|
||
Affiché le <?= date('d/m/Y à H:i:s') ?> —
|
||
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&n=<?= $selectedN ?>">Rafraîchir</a> —
|
||
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&n=<?= $selectedN ?>&refresh=1">Forcer actualisation</a>
|
||
</p>
|
||
|
||
<div class="sys-status-header">
|
||
<h3 class="srv-section-title srv-section-title--compact">Statut
|
||
<?php if ($statusCached && $statusCacheAge !== null): ?>
|
||
<span class="sys-cache-badge sys-cache-badge--hit" title="Données en cache">
|
||
⚡ Cache — il y a <?= $statusCacheAge ?>s
|
||
</span>
|
||
<?php else: ?>
|
||
<span class="sys-cache-badge sys-cache-badge--miss" title="Données fraîches">
|
||
⟳ Actualisé
|
||
</span>
|
||
<?php endif; ?>
|
||
</h3>
|
||
<button id="sys-status-toggle" class="sys-status-toggle"
|
||
aria-expanded="<?= $statusInitiallyCollapsed ? 'false' : 'true' ?>" aria-controls="sys-status-body"
|
||
type="button"
|
||
onclick="var b=document.getElementById('sys-status-body');var c=b.hidden;b.hidden=!c;this.setAttribute('aria-expanded',c);this.textContent=c?'▲ Réduire':'▼ Développer';document.cookie='sys_collapsed='+(!c)+';path=/;max-age=31536000';return false">
|
||
<?= $statusInitiallyCollapsed ? '▼ Développer' : '▲ Réduire' ?>
|
||
</button>
|
||
</div>
|
||
|
||
<div id="sys-status-body"<?= $statusInitiallyCollapsed ? ' hidden' : '' ?>>
|
||
<div class="srv-grid">
|
||
<?php foreach ($checks as $check): ?>
|
||
<?php $st = $check['status'] ?? 'unknown'; ?>
|
||
<div class="srv-card">
|
||
<div class="srv-card__header">
|
||
<span class="srv-card__name"><?= htmlspecialchars($check['label']) ?></span>
|
||
<span class="<?= SystemController::statusClass($st) ?>"><?= SystemController::statusLabel($st) ?></span>
|
||
</div>
|
||
<?php if (!empty($check['detail'])): ?>
|
||
<div class="srv-card__detail"><?= htmlspecialchars($check['detail']) ?></div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<div class="sys-status-meta">
|
||
<div>
|
||
<h4 class="srv-section-title srv-section-title--sub">Environnement PHP</h4>
|
||
<div class="php-grid php-grid--flush">
|
||
<?php foreach ($phpInfo as $key => $val): ?>
|
||
<div class="php-item">
|
||
<div class="php-item__key"><?= htmlspecialchars($key) ?></div>
|
||
<div class="php-item__val"><?= htmlspecialchars($val) ?></div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h4 class="srv-section-title srv-section-title--sub">Espace disque</h4>
|
||
<div class="disk-bar-wrap">
|
||
<div class="disk-bar" style="--disk-pct:<?= $diskPct ?>%;--disk-color:<?= $diskColor ?>"></div>
|
||
</div>
|
||
<div class="disk-stats">
|
||
<span><?= SystemController::humanBytes($diskUsed) ?> utilisé (<?= $diskPct ?>%)</span>
|
||
<span><?= SystemController::humanBytes($diskFree) ?> libre / <?= SystemController::humanBytes($diskTotal) ?></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════
|
||
JOURNAUX
|
||
══════════════════════════════════════════════════════════════ -->
|
||
<section aria-labelledby="settings-logs-title">
|
||
<h2 id="settings-logs-title">Journaux</h2>
|
||
|
||
<nav class="sys-tabs" aria-label="Journaux et configuration">
|
||
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
|
||
<a href="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
|
||
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||
hx-target="#sys-tab-panel"
|
||
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||
hx-swap="innerHTML"
|
||
hx-indicator="#sys-tab-panel"
|
||
data-tab="<?= htmlspecialchars($key) ?>"
|
||
<?= $activeTab === $key ? 'aria-current="page"' : '' ?>>
|
||
<?= htmlspecialchars($def['label']) ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
<a href="?tab=nginx_config"
|
||
class="sys-tab <?= $activeTab === 'nginx_config' ? 'active' : '' ?>"
|
||
hx-get="/admin/system-fragment.php?tab=nginx_config"
|
||
hx-target="#sys-tab-panel"
|
||
hx-push-url="?tab=nginx_config"
|
||
hx-swap="innerHTML"
|
||
hx-indicator="#sys-tab-panel"
|
||
data-tab="nginx_config"
|
||
<?= $activeTab === 'nginx_config' ? 'aria-current="page"' : '' ?>>nginx — config</a>
|
||
</nav>
|
||
|
||
<div id="sys-tab-panel">
|
||
<?php if ($activeTab === 'nginx_config'): ?>
|
||
<?php include APP_ROOT . '/templates/admin/partials/system-nginx-config-panel.php'; ?>
|
||
<?php else: ?>
|
||
<?php include APP_ROOT . '/templates/admin/partials/system-log-panel.php'; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<script>
|
||
function copyLogContent(btn) {
|
||
var logOut = document.querySelector('#log-output');
|
||
if (!logOut) return;
|
||
var text = Array.from(logOut.querySelectorAll('.log-line'))
|
||
.map(function(el){ return el.textContent; }).join('\n');
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(function(){
|
||
btn.textContent = '\u2713 Copi\u00e9';
|
||
btn.classList.add('copied');
|
||
setTimeout(function(){ btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000);
|
||
});
|
||
} else {
|
||
fallbackCopy(text, btn);
|
||
}
|
||
}
|
||
function fallbackCopy(text, btn) {
|
||
var ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
ta.style.cssText = 'position:fixed;opacity:0';
|
||
document.body.appendChild(ta); ta.select();
|
||
try { document.execCommand('copy'); btn.textContent = '\u2713 Copi\u00e9'; btn.classList.add('copied');
|
||
setTimeout(function(){ btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000);
|
||
} catch(e) {}
|
||
document.body.removeChild(ta);
|
||
}
|
||
// Focus the SMTP field that caused the probe error
|
||
(function () {
|
||
var form = document.querySelector('form[data-smtp-error-field]');
|
||
if (!form) return;
|
||
var fieldId = form.getAttribute('data-smtp-error-field');
|
||
var el = fieldId ? document.getElementById(fieldId) : null;
|
||
if (!el) return;
|
||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
el.focus();
|
||
}());
|
||
|
||
// Update active tab class after each HTMX swap on #sys-tab-panel
|
||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||
if (evt.detail.target && evt.detail.target.id === 'sys-tab-panel') {
|
||
var rc = evt.detail.requestConfig;
|
||
var tab = null;
|
||
var qIdx = rc.path.indexOf('?');
|
||
if (qIdx !== -1) {
|
||
tab = new URLSearchParams(rc.path.substring(qIdx + 1)).get('tab');
|
||
}
|
||
if (!tab && rc.parameters && rc.parameters.tab) {
|
||
tab = rc.parameters.tab;
|
||
}
|
||
if (tab) {
|
||
document.querySelectorAll('.sys-tabs .sys-tab').forEach(function(a) {
|
||
var isActive = a.getAttribute('data-tab') === tab;
|
||
a.classList.toggle('active', isActive);
|
||
if (isActive) a.setAttribute('aria-current', 'page');
|
||
else a.removeAttribute('aria-current');
|
||
});
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<!-- Enable maintenance confirm -->
|
||
<dialog id="enable-maintenance-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="enable-maint-title">
|
||
<div class="admin-dialog__header">
|
||
<h2 id="enable-maint-title">Activer la maintenance</h2>
|
||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||
onclick="this.closest('dialog').close()">✕</button>
|
||
</div>
|
||
<div class="admin-dialog__alert">
|
||
<p>Mettre le site en maintenance ? Les visiteurs verront une page 503.</p>
|
||
</div>
|
||
<div class="admin-dialog__footer">
|
||
<button type="button" class="btn btn--warning"
|
||
onclick="this.closest('dialog').close(); document.getElementById('enable-maintenance-form').submit()">
|
||
Activer
|
||
</button>
|
||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
|