298f18dabe
- #96 : boutons IA sidebar éditeur (analyse critique / réécriture) via Anthropic API - #97 : onglet admin /admin/ia — provider anthropic/claude_code, modèle, procédure CLI - #95 : extraction scripts inline vers fichiers JS (comments.js, post_confirm.js, admin.js) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1624 lines
83 KiB
PHP
1624 lines
83 KiB
PHP
<?php
|
||
ob_start();
|
||
|
||
$now = time();
|
||
|
||
function adminStatusBadge(array $a, int $now): string
|
||
{
|
||
if (!$a['published']) {
|
||
return '<span class="badge bg-secondary">Brouillon</span>';
|
||
}
|
||
if (strtotime((string)($a['published_at'] ?? '')) > $now) {
|
||
return '<span class="badge bg-warning text-dark">Avant-première</span>';
|
||
}
|
||
return '<span class="badge bg-success">Publié</span>';
|
||
}
|
||
?>
|
||
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h1 class="h3 mb-0">Administration</h1>
|
||
<a href="/new" class="btn btn-primary btn-sm">+ Nouvel article</a>
|
||
</div>
|
||
|
||
<!-- Onglets -->
|
||
<ul class="nav nav-tabs mb-4">
|
||
<?php if (isAdmin()): ?>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'dashboard' ? 'active' : '' ?>"
|
||
href="/admin/dashboard">Tableau de bord</a>
|
||
</li>
|
||
<?php endif; ?>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'articles' ? 'active' : '' ?>"
|
||
href="/admin/articles"><?= isAdmin() ? 'Articles' : 'Mes articles' ?></a>
|
||
</li>
|
||
<?php if (isAdmin()): ?>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'users' ? 'active' : '' ?>"
|
||
href="/admin/users">Utilisateurs</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'roles' ? 'active' : '' ?>"
|
||
href="/admin/roles">Rôles</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'categories' ? 'active' : '' ?>"
|
||
href="/admin/categories">Catégories & Tags</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'site' ? 'active' : '' ?>"
|
||
href="/admin/site">Site</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'comments' ? 'active' : '' ?>"
|
||
href="/admin/comments">Commentaires</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'emails' ? 'active' : '' ?>"
|
||
href="/admin/emails">Emails</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'smtp' ? 'active' : '' ?>"
|
||
href="/admin/smtp">SMTP</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'searches' ? 'active' : '' ?>"
|
||
href="/admin/searches">Recherches</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'books' ? 'active' : '' ?>"
|
||
href="/admin/books">Livres</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'flux' ? 'active' : '' ?>"
|
||
href="/admin/flux">Flux</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'ia' ? 'active' : '' ?>"
|
||
href="/admin/ia">IA</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link <?= $tab === 'stats' ? 'active' : '' ?>"
|
||
href="/admin/stats">Statistiques</a>
|
||
</li>
|
||
<?php endif; ?>
|
||
</ul>
|
||
|
||
<!-- ─────────────────────────── DASHBOARD ─────────────────────────── -->
|
||
<?php if ($tab === 'dashboard' && isAdmin()): ?>
|
||
|
||
<div class="row g-3 mb-4">
|
||
<?php
|
||
$stats = [
|
||
['label' => 'Publiés', 'value' => $adminData['published'], 'color' => 'success'],
|
||
['label' => 'Avant-premières', 'value' => $adminData['previews'], 'color' => 'warning'],
|
||
['label' => 'Brouillons', 'value' => $adminData['drafts'], 'color' => 'secondary'],
|
||
['label' => 'Total', 'value' => $adminData['total'], 'color' => 'primary'],
|
||
];
|
||
foreach ($stats as $s): ?>
|
||
<div class="col-6 col-md-3">
|
||
<div class="card text-center">
|
||
<div class="card-body">
|
||
<div class="display-6 fw-bold text-<?= $s['color'] ?>"><?= $s['value'] ?></div>
|
||
<div class="text-muted small"><?= $s['label'] ?></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<!-- Version Folio ──────────────────────────────────────────────────────── -->
|
||
<?php
|
||
$_deployedVer = trim((string) @file_get_contents(BASE_PATH . '/public/version.txt'));
|
||
$_deployedLabel = $_deployedVer !== '' ? $_deployedVer : '—';
|
||
$_notices = isset($_updateChecker) ? $_updateChecker->adminNotices() : [];
|
||
$_branch = isset($_updateChecker) ? $_updateChecker->getBranch() : 'main';
|
||
$_lastChecked = isset($_updateChecker) ? $_updateChecker->getLastChecked() : null;
|
||
$_upgradeLog = isset($_updateChecker) ? $_updateChecker->getLastUpgradeLog() : null;
|
||
$_repoConfigured = folioRepoUrl() !== '';
|
||
$_remoteLabel = '—';
|
||
foreach ($_notices as $_n) {
|
||
if ($_n['type'] === 'info' && preg_match('/v([\d]+\.[\d]+\.[\d]+)/', $_n['message'], $_m)) {
|
||
$_remoteLabel = $_m[1];
|
||
}
|
||
}
|
||
?>
|
||
<div class="card mb-4">
|
||
<div class="card-header bg-transparent py-2 small fw-semibold">Moteur Folio</div>
|
||
<div class="card-body py-2">
|
||
<table class="table table-sm table-borderless mb-0 small">
|
||
<tbody>
|
||
<tr>
|
||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap" style="width:160px">Version déployée</th>
|
||
<td><?= htmlspecialchars($_deployedLabel) ?></td>
|
||
</tr>
|
||
<tr>
|
||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap">Dernière version disponible</th>
|
||
<td class="d-flex align-items-center gap-2 flex-wrap">
|
||
<span><?= htmlspecialchars($_remoteLabel) ?></span>
|
||
<?php if ($_remoteLabel !== '—' && $_remoteLabel !== $_deployedLabel): ?>
|
||
<form method="POST" action="/?action=run_engine_update" class="d-inline">
|
||
<button type="submit" class="btn btn-primary btn-sm">Mettre à jour vers v<?= htmlspecialchars($_remoteLabel) ?></button>
|
||
</form>
|
||
<?php elseif ($_repoConfigured): ?>
|
||
<form method="POST" action="/?action=force_update_check" class="d-inline">
|
||
<button type="submit" class="btn btn-outline-secondary btn-sm py-0">Vérifier</button>
|
||
</form>
|
||
<?php else: ?>
|
||
<span class="text-muted small">(<code>FOLIO_REPO_URL</code> non configuré)</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap">Branche suivie</th>
|
||
<td><code><?= htmlspecialchars($_branch) ?></code><?= $_lastChecked !== null ? ' <span class="text-muted ms-2">· vérifié le ' . date('d/m/Y à H:i', $_lastChecked) . '</span>' : '' ?></td>
|
||
</tr>
|
||
<?php if (($_GET['notice'] ?? '') === 'engine_updated'): ?>
|
||
<tr><td colspan="2"><div class="alert alert-success py-1 mb-0 small">Moteur mis à jour avec succès.</div></td></tr>
|
||
<?php elseif (($_GET['notice'] ?? '') === 'upgrade_error'): ?>
|
||
<tr><td colspan="2">
|
||
<div class="alert alert-danger py-1 mb-0 small">
|
||
Erreur lors de la mise à jour.
|
||
<?php if (!empty($_SESSION['_upgrade_log'])): ?>
|
||
<pre class="mt-1 mb-0 small"><?= htmlspecialchars($_SESSION['_upgrade_log']) ?></pre>
|
||
<?php unset($_SESSION['_upgrade_log']); ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
</td></tr>
|
||
<?php endif; ?>
|
||
<?php if ($_upgradeLog !== null): ?>
|
||
<tr>
|
||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap align-top">Journal</th>
|
||
<td>
|
||
<details>
|
||
<summary class="small text-muted" style="cursor:pointer">Dernière mise à jour</summary>
|
||
<pre class="mt-1 mb-0 small"><?= htmlspecialchars($_upgradeLog) ?></pre>
|
||
</details>
|
||
</td>
|
||
</tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<h5>Activité récente</h5>
|
||
<table class="table table-sm table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>Titre</th>
|
||
<th>Auteur</th>
|
||
<th>Statut</th>
|
||
<th>Modifié le</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($adminData['recent'] as $a): ?>
|
||
<tr>
|
||
<td>
|
||
<a href="/post/<?= htmlspecialchars($a['slug'] ?? '') ?>">
|
||
<?= htmlspecialchars($a['title']) ?>
|
||
</a>
|
||
</td>
|
||
<td class="text-muted small"><?= htmlspecialchars($a['author'] ?? '–') ?></td>
|
||
<td><?= adminStatusBadge($a, $now) ?></td>
|
||
<td class="text-muted small">
|
||
<?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($a['updated_at'] ?? $a['created_at'] ?? '')))) ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- ─────────────────────────── ARTICLES ─────────────────────────── -->
|
||
<?php elseif ($tab === 'articles'): ?>
|
||
|
||
<?php
|
||
$_sortBy = $adminData['sort_by'] ?? 'updated';
|
||
$_sortDir = $adminData['sort_dir'] ?? 'desc';
|
||
$_mkSortUrl = function (string $col) use ($_sortBy, $_sortDir, $adminData): string {
|
||
$dir = ($_sortBy === $col && $_sortDir === 'asc') ? 'desc' : 'asc';
|
||
$p = array_filter([
|
||
'filter_author' => $adminData['filter_author'] ?? '',
|
||
'filter_category' => $adminData['filter_category'] ?? '',
|
||
'filter_status' => $adminData['filter_status'] ?? '',
|
||
'filter_search' => $adminData['filter_search'] ?? '',
|
||
'filter_featured' => $adminData['filter_featured'] ?? '',
|
||
], fn ($v) => $v !== '');
|
||
$p['sort'] = $col;
|
||
$p['dir'] = $dir;
|
||
return '/admin/articles?' . http_build_query($p);
|
||
};
|
||
$_sortIcon = function (string $col) use ($_sortBy, $_sortDir): string {
|
||
if ($_sortBy !== $col) { return '<span class="text-muted ms-1" style="font-size:.75em">↕</span>'; }
|
||
return '<span class="ms-1" style="font-size:.75em">' . ($_sortDir === 'asc' ? '↑' : '↓') . '</span>';
|
||
};
|
||
?>
|
||
|
||
<!-- Filtres -->
|
||
<form class="row g-2 align-items-center mb-3" method="get" action="/admin/articles">
|
||
<input type="hidden" name="sort" value="<?= htmlspecialchars($_sortBy) ?>">
|
||
<input type="hidden" name="dir" value="<?= htmlspecialchars($_sortDir) ?>">
|
||
<?php if (isAdmin() && !empty($adminData['filter_authors'])): ?>
|
||
<div class="col-auto">
|
||
<select name="filter_author" class="form-select form-select-sm">
|
||
<option value="">Tous les auteurs</option>
|
||
<?php foreach ($adminData['filter_authors'] as $_fa): ?>
|
||
<option value="<?= htmlspecialchars($_fa) ?>"
|
||
<?= ($adminData['filter_author'] ?? '') === $_fa ? 'selected' : '' ?>>
|
||
<?= htmlspecialchars($_fa) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php if (!empty($adminData['filter_categories'])): ?>
|
||
<div class="col-auto">
|
||
<select name="filter_category" class="form-select form-select-sm">
|
||
<option value="">Toutes les catégories</option>
|
||
<?php foreach ($adminData['filter_categories'] as $_fc): ?>
|
||
<option value="<?= htmlspecialchars($_fc) ?>"
|
||
<?= ($adminData['filter_category'] ?? '') === $_fc ? 'selected' : '' ?>>
|
||
<?= htmlspecialchars($_fc) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<?php endif; ?>
|
||
<div class="col-auto">
|
||
<select name="filter_status" class="form-select form-select-sm">
|
||
<option value="">Tous les statuts</option>
|
||
<option value="published" <?= ($adminData['filter_status'] ?? '') === 'published' ? 'selected' : '' ?>>Publié</option>
|
||
<option value="draft" <?= ($adminData['filter_status'] ?? '') === 'draft' ? 'selected' : '' ?>>Brouillon</option>
|
||
<option value="preview" <?= ($adminData['filter_status'] ?? '') === 'preview' ? 'selected' : '' ?>>Avant-première</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-auto">
|
||
<select name="filter_featured" class="form-select form-select-sm">
|
||
<option value="">Tous</option>
|
||
<option value="yes" <?= ($adminData['filter_featured'] ?? '') === 'yes' ? 'selected' : '' ?>>★ À la une</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-auto">
|
||
<input type="text" name="filter_search" class="form-control form-control-sm"
|
||
placeholder="Rechercher…" value="<?= htmlspecialchars($adminData['filter_search'] ?? '') ?>">
|
||
</div>
|
||
<div class="col-auto d-flex gap-2">
|
||
<button type="submit" class="btn btn-secondary btn-sm">Filtrer</button>
|
||
<?php $hasFilter = ($adminData['filter_author'] ?? '') !== '' || ($adminData['filter_category'] ?? '') !== '' || ($adminData['filter_status'] ?? '') !== '' || ($adminData['filter_search'] ?? '') !== '' || ($adminData['filter_featured'] ?? '') !== ''; ?>
|
||
<?php if ($hasFilter): ?>
|
||
<a href="/admin/articles" class="btn btn-link btn-sm p-0">Réinitialiser</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php if ($hasFilter): ?>
|
||
<div class="col-auto">
|
||
<span class="text-muted small"><?= count($adminData['articles']) ?> résultat(s)</span>
|
||
</div>
|
||
<?php endif; ?>
|
||
</form>
|
||
|
||
<?php if (empty($adminData['articles'])): ?>
|
||
<p class="text-muted">Aucun article.</p>
|
||
<?php else: ?>
|
||
<form method="post" action="/?action=admin_bulk_delete" id="bulk-form">
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<div class="form-check mb-0">
|
||
<input class="form-check-input" type="checkbox" id="check-all">
|
||
<label class="form-check-label small text-muted" for="check-all">Tout sélectionner</label>
|
||
</div>
|
||
<button type="submit" id="bulk-delete-btn" class="btn btn-danger btn-sm"
|
||
data-confirm-bulk="Supprimer les articles sélectionnés ? Cette action est irréversible.">
|
||
Supprimer la sélection
|
||
</button>
|
||
</div>
|
||
<table class="table table-sm table-hover align-middle">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:2rem"></th>
|
||
<th>
|
||
<a href="<?= htmlspecialchars($_mkSortUrl('title')) ?>"
|
||
class="text-decoration-none text-reset">
|
||
Titre<?= $_sortIcon('title') ?>
|
||
</a>
|
||
</th>
|
||
<?php if (isAdmin()): ?><th>Auteur</th><?php endif; ?>
|
||
<th>Catégorie</th>
|
||
<th>Statut</th>
|
||
<th title="À la une">★</th>
|
||
<th>
|
||
<a href="<?= htmlspecialchars($_mkSortUrl('published')) ?>"
|
||
class="text-decoration-none text-reset">
|
||
Date<?= $_sortIcon('published') ?>
|
||
</a>
|
||
</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($adminData['articles'] as $a): ?>
|
||
<tr>
|
||
<td>
|
||
<input class="form-check-input bulk-check" type="checkbox"
|
||
name="uuids[]" value="<?= htmlspecialchars($a['uuid']) ?>">
|
||
</td>
|
||
<td>
|
||
<a href="/post/<?= htmlspecialchars($a['slug'] ?? '') ?>">
|
||
<?= htmlspecialchars($a['title']) ?>
|
||
</a>
|
||
</td>
|
||
<?php if (isAdmin()): ?>
|
||
<td class="text-muted small"><?= htmlspecialchars($a['author'] ?? '–') ?></td>
|
||
<?php endif; ?>
|
||
<td class="text-muted small"><?= htmlspecialchars($a['category'] ?? '–') ?></td>
|
||
<td><?= adminStatusBadge($a, $now) ?></td>
|
||
<td class="text-center">
|
||
<?php if (isAdmin()): ?>
|
||
<?php $_isFeatured = !empty($a['featured']); ?>
|
||
<button type="submit" form="toggle-featured-<?= htmlspecialchars($a['uuid']) ?>"
|
||
class="btn btn-link p-0 border-0 lh-1 fs-6"
|
||
title="<?= $_isFeatured ? 'Retirer de la une' : 'Mettre à la une' ?>">
|
||
<?= $_isFeatured ? '★' : '<span class="text-muted">☆</span>' ?>
|
||
</button>
|
||
<?php else: ?>
|
||
<?= !empty($a['featured']) ? '★' : '' ?>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td class="text-muted small text-nowrap">
|
||
<?= htmlspecialchars(date('d/m/Y', strtotime((string)($a['published_at'] ?? $a['created_at'] ?? '')))) ?>
|
||
</td>
|
||
<td class="text-end text-nowrap">
|
||
<a href="/edit/<?= htmlspecialchars($a['uuid']) ?>"
|
||
class="btn btn-outline-secondary btn-sm">Modifier</a>
|
||
<button type="submit" form="dup-<?= htmlspecialchars($a['uuid']) ?>"
|
||
class="btn btn-outline-secondary btn-sm ms-1"
|
||
title="Dupliquer en brouillon">⧉</button>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</form>
|
||
<?php
|
||
/* Formulaires hors bulk-form (nested forms invalides en HTML) */
|
||
$_backUrl = '/admin/articles?' . http_build_query(array_filter([
|
||
'filter_author' => $adminData['filter_author'] ?? '',
|
||
'filter_category' => $adminData['filter_category'] ?? '',
|
||
'filter_status' => $adminData['filter_status'] ?? '',
|
||
'filter_search' => $adminData['filter_search'] ?? '',
|
||
'filter_featured' => $adminData['filter_featured'] ?? '',
|
||
'sort' => $_sortBy,
|
||
'dir' => $_sortDir,
|
||
], fn ($v) => $v !== ''));
|
||
foreach ($adminData['articles'] as $_fa):
|
||
?>
|
||
<form id="toggle-featured-<?= htmlspecialchars($_fa['uuid']) ?>" method="post" action="/?action=admin_toggle_featured" hidden>
|
||
<input type="hidden" name="uuid" value="<?= htmlspecialchars($_fa['uuid']) ?>">
|
||
<input type="hidden" name="_back" value="<?= htmlspecialchars($_backUrl) ?>">
|
||
</form>
|
||
<form id="dup-<?= htmlspecialchars($_fa['uuid']) ?>" method="post" action="/duplicate/<?= htmlspecialchars($_fa['uuid']) ?>" hidden>
|
||
</form>
|
||
<?php endforeach; ?>
|
||
<script src="/assets/js/admin.js" defer></script>
|
||
<?php endif; ?>
|
||
|
||
<!-- ─────────────────────────── UTILISATEURS ─────────────────────────── -->
|
||
<?php elseif ($tab === 'users' && isAdmin()): ?>
|
||
|
||
<?php if (($_GET['error'] ?? '') === 'last_admin'): ?>
|
||
<div class="alert alert-danger py-2 mb-3">
|
||
Impossible de retirer le rôle Administrateur : il doit rester au moins un administrateur.
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Ajouter / attribuer un rôle -->
|
||
<div class="card mb-4">
|
||
<div class="card-header">Attribuer un rôle</div>
|
||
<div class="card-body">
|
||
<form method="post" action="/?action=admin_grant_role" class="row g-2 align-items-end">
|
||
<div class="col-md-5">
|
||
<label class="form-label small">Email</label>
|
||
<input type="email" name="email" class="form-control form-control-sm"
|
||
placeholder="utilisateur@exemple.fr" required>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label small">Rôle</label>
|
||
<select name="role" class="form-select form-select-sm" required>
|
||
<?php foreach ($adminData['roles'] as $r): ?>
|
||
<option value="<?= htmlspecialchars($r['name']) ?>">
|
||
<?= htmlspecialchars($r['label']) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<button type="submit" class="btn btn-primary btn-sm w-100">Attribuer</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Liste des utilisateurs -->
|
||
<?php if (empty($adminData['users'])): ?>
|
||
<p class="text-muted">Aucun utilisateur.</p>
|
||
<?php else: ?>
|
||
<table class="table table-sm table-hover align-middle">
|
||
<thead>
|
||
<tr>
|
||
<th>Email</th>
|
||
<th>Statut</th>
|
||
<th>Rôles</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($adminData['users'] as $u): ?>
|
||
<tr>
|
||
<td class="small"><?= htmlspecialchars($u['email']) ?></td>
|
||
<td>
|
||
<?php if ($u['is_active'] === null): ?>
|
||
<span class="badge bg-light text-muted">Pré-inscrit</span>
|
||
<?php elseif ($u['is_active']): ?>
|
||
<span class="badge bg-success">Actif</span>
|
||
<?php else: ?>
|
||
<span class="badge bg-danger">Inactif</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td>
|
||
<?php foreach ($u['roles'] as $role): ?>
|
||
<span class="badge bg-primary me-1"><?= htmlspecialchars($role['label']) ?></span>
|
||
<form method="post" action="/?action=admin_revoke_role"
|
||
class="d-inline"
|
||
data-confirm="Retirer le rôle «<?= htmlspecialchars($role['label']) ?>» à <?= htmlspecialchars($u['email']) ?> ?">
|
||
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
|
||
<input type="hidden" name="role" value="<?= htmlspecialchars($role['name']) ?>">
|
||
<button type="submit" class="btn btn-link btn-sm p-0 text-danger" title="Retirer">×</button>
|
||
</form>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($u['roles'])): ?>
|
||
<span class="text-muted small">–</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td class="text-end">
|
||
<!-- Ajout rapide d'un rôle existant -->
|
||
<?php
|
||
$currentRoleNames = array_column($u['roles'], 'name');
|
||
$missing = array_filter($adminData['roles'], fn ($r) => !in_array($r['name'], $currentRoleNames, true));
|
||
?>
|
||
<?php if ($missing): ?>
|
||
<form method="post" action="/?action=admin_grant_role" class="d-inline-flex gap-1">
|
||
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
|
||
<select name="role" class="form-select form-select-sm" style="width:auto">
|
||
<?php foreach ($missing as $r): ?>
|
||
<option value="<?= htmlspecialchars($r['name']) ?>">
|
||
<?= htmlspecialchars($r['label']) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="submit" class="btn btn-outline-primary btn-sm">+</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
|
||
<!-- ─────────────────────────── RÔLES ─────────────────────────── -->
|
||
<?php elseif ($tab === 'roles' && isAdmin()): ?>
|
||
|
||
<div class="row g-4">
|
||
|
||
<!-- Tableau des rôles existants -->
|
||
<div class="col-lg-8">
|
||
<?php if (empty($adminData['roles'])): ?>
|
||
<p class="text-muted">Aucun rôle défini.</p>
|
||
<?php else: ?>
|
||
<table class="table table-hover align-middle">
|
||
<thead>
|
||
<tr>
|
||
<th>Rôle</th>
|
||
<th class="text-center" style="width:100px">Utilisateurs</th>
|
||
<th style="width:110px"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($adminData['roles'] as $r): ?>
|
||
<tr>
|
||
<td>
|
||
<?= htmlspecialchars($r['label']) ?>
|
||
<?php if ($r['name'] !== 'admin'): ?>
|
||
<div class="text-muted small">
|
||
<?php
|
||
$capLabels = array_map(
|
||
fn ($c) => KNOWN_CAPABILITIES[$c] ?? $c,
|
||
$r['capabilities']
|
||
);
|
||
echo htmlspecialchars(implode(', ', $capLabels) ?: '–');
|
||
?>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="text-muted small">Toutes les permissions</div>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td class="text-center">
|
||
<span class="badge bg-secondary"><?= (int)$r['user_count'] ?></span>
|
||
</td>
|
||
<td class="text-end d-flex gap-1 justify-content-end">
|
||
<a href="/admin/role/<?= rawurlencode($r['name']) ?>"
|
||
class="btn btn-outline-secondary btn-sm">Éditer</a>
|
||
<?php if ((int)$r['user_count'] === 0 && $r['name'] !== 'admin'): ?>
|
||
<form method="post" action="/?action=admin_delete_role"
|
||
data-confirm="Supprimer le rôle «<?= htmlspecialchars($r['name']) ?>» ?">
|
||
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
|
||
<button type="submit" class="btn btn-outline-danger btn-sm">×</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- Créer un rôle -->
|
||
<div class="col-lg-4">
|
||
<div class="card">
|
||
<div class="card-header">Nouveau rôle</div>
|
||
<div class="card-body">
|
||
<form method="post" action="/?action=admin_create_role">
|
||
<div class="mb-3">
|
||
<label for="role-label" class="form-label small fw-semibold">Nom du rôle</label>
|
||
<input type="text" id="role-label" name="label" class="form-control form-control-sm"
|
||
placeholder="ex : Modérateur" required autocomplete="off">
|
||
</div>
|
||
<input type="hidden" id="role-name" name="name">
|
||
<button type="submit" class="btn btn-primary btn-sm w-100">Créer</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<?php elseif ($tab === 'site' && isAdmin()): ?>
|
||
|
||
<?php if (!empty($siteSettingsSaved)): ?>
|
||
<div class="alert alert-success py-2 mb-3">Paramètres enregistrés.</div>
|
||
<?php elseif (!empty($siteSettingsError)): ?>
|
||
<div class="alert alert-danger py-2 mb-3">Impossible d'enregistrer : le fichier n'est pas accessible en écriture.</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="card" style="max-width:540px">
|
||
<div class="card-header">Paramètres du site</div>
|
||
<div class="card-body">
|
||
<form method="post" action="/?action=admin_save_site">
|
||
<div class="mb-3">
|
||
<label for="site-title" class="form-label small fw-semibold">Titre du site</label>
|
||
<input type="text" id="site-title" name="site_title"
|
||
class="form-control form-control-sm"
|
||
value="<?= htmlspecialchars(siteTitle()) ?>"
|
||
required maxlength="80">
|
||
<div class="form-text">Affiché dans la barre de navigation et les onglets.</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="site-claim" class="form-label small fw-semibold">Claim / accroche</label>
|
||
<input type="text" id="site-claim" name="site_claim"
|
||
class="form-control form-control-sm"
|
||
value="<?= htmlspecialchars(siteClaim()) ?>"
|
||
required maxlength="200">
|
||
<div class="form-text">Affiché sous le titre dans la navbar et dans le pied de page.</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="site-lang" class="form-label small fw-semibold">Langue du site</label>
|
||
<input type="text" id="site-lang" name="site_lang"
|
||
class="form-control form-control-sm"
|
||
value="<?= htmlspecialchars(siteLang()) ?>"
|
||
maxlength="20" placeholder="ex : fr-FR">
|
||
<div class="form-text">Format BCP 47 (ex : <code>fr</code>, <code>fr-FR</code>). Utilisé dans <code><html lang></code>, og:locale, RSS et JSON-LD.</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="posts-per-page" class="form-label small fw-semibold">Articles par page</label>
|
||
<input type="number" id="posts-per-page" name="posts_per_page"
|
||
class="form-control form-control-sm"
|
||
value="<?= postsPerPage() ?>"
|
||
min="1" max="100">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="site-license-label" class="form-label small fw-semibold">Licence (libellé)</label>
|
||
<input type="text" id="site-license-label" name="site_license_label"
|
||
class="form-control form-control-sm"
|
||
value="<?= htmlspecialchars(siteLicenseLabel()) ?>"
|
||
maxlength="80" placeholder="ex : CC BY 4.0">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="site-license-url" class="form-label small fw-semibold">Licence (URL)</label>
|
||
<input type="url" id="site-license-url" name="site_license_url"
|
||
class="form-control form-control-sm"
|
||
value="<?= htmlspecialchars(siteLicenseUrl()) ?>"
|
||
maxlength="200">
|
||
<div class="form-text">Affiché dans le footer.</div>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if (($_GET['notice'] ?? '') === 'folio_saved'): ?>
|
||
<div class="alert alert-success py-2 mt-3 small">Configuration Folio enregistrée.</div>
|
||
<?php elseif (($_GET['notice'] ?? '') === 'folio_error'): ?>
|
||
<div class="alert alert-danger py-2 mt-3 small">Impossible d'enregistrer.</div>
|
||
<?php endif; ?>
|
||
<div class="card mt-4" style="max-width:540px">
|
||
<div class="card-header bg-transparent py-2 small fw-semibold">Mises à jour du moteur</div>
|
||
<div class="card-body">
|
||
<form method="POST" action="/?action=admin_save_folio_config">
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-semibold mb-1">URL du dépôt Folio</label>
|
||
<input type="url" name="folio_repo_url" class="form-control form-control-sm font-monospace"
|
||
placeholder="https://git.abonnel.fr/cedricAbonnel/folio"
|
||
value="<?= htmlspecialchars(folioRepoUrl()) ?>">
|
||
<div class="form-text">Sans slash final. Laissez vide pour utiliser <code>FOLIO_REPO_URL</code> du .env.</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-semibold mb-1">Branche suivie</label>
|
||
<input type="text" name="folio_update_branch" class="form-control form-control-sm font-monospace"
|
||
placeholder="main"
|
||
value="<?= htmlspecialchars(folioUpdateBranch()) ?>">
|
||
</div>
|
||
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<?php endif; ?>
|
||
|
||
<!-- ─────────────────────────── CATÉGORIES & TAGS ─────────────────── -->
|
||
<?php if ($tab === 'categories'): ?>
|
||
|
||
<?php
|
||
$_cats = $adminData['cats'] ?? [];
|
||
$_privateCats = $adminData['privateCats'] ?? [];
|
||
$_tagTypes = $adminData['tagTypes'] ?? [];
|
||
?>
|
||
|
||
<div class="row g-4 mb-5">
|
||
|
||
<!-- Liste des catégories -->
|
||
<div class="col-lg-9">
|
||
<h5 class="mb-3">Catégories existantes</h5>
|
||
<?php if (empty($_cats)): ?>
|
||
<p class="text-muted">Aucune catégorie définie.</p>
|
||
<?php else: ?>
|
||
<div class="d-flex flex-column gap-2">
|
||
<?php foreach ($_cats as $_cat => $_count):
|
||
$_gradient = coverGradient($_cat, $_cats); ?>
|
||
<div class="card">
|
||
<div class="card-body py-2 px-3 d-flex align-items-center gap-3">
|
||
|
||
<div style="width:40px;height:40px;border-radius:8px;flex-shrink:0;background:<?= htmlspecialchars($_gradient) ?>"></div>
|
||
|
||
<div style="min-width:140px">
|
||
<strong><?= htmlspecialchars($_cat) ?></strong>
|
||
<small class="text-muted ms-2"><?= $_count ?> article<?= $_count > 1 ? 's' : '' ?></small>
|
||
</div>
|
||
|
||
<form method="POST" action="/?action=rename_category"
|
||
class="d-flex align-items-center gap-2 flex-grow-1"
|
||
data-confirm="Renommer « <?= htmlspecialchars($_cat) ?> » ?">
|
||
<input type="hidden" name="old" value="<?= htmlspecialchars($_cat) ?>">
|
||
<input type="text" name="new" class="form-control form-control-sm"
|
||
placeholder="Nouveau nom" required>
|
||
<button type="submit" class="btn btn-outline-primary btn-sm text-nowrap">Renommer</button>
|
||
</form>
|
||
|
||
<?php $_isPriv = in_array($_cat, $_privateCats, true); ?>
|
||
<form method="POST" action="/?action=toggle_private_category">
|
||
<input type="hidden" name="category" value="<?= htmlspecialchars($_cat) ?>">
|
||
<button type="submit"
|
||
class="btn btn-sm text-nowrap <?= $_isPriv ? 'btn-secondary' : 'btn-outline-secondary' ?>">
|
||
🔒 <?= $_isPriv ? 'Privée' : 'Publique' ?>
|
||
</button>
|
||
</form>
|
||
|
||
<form method="POST" action="/?action=delete_category"
|
||
data-confirm="Retirer la catégorie « <?= htmlspecialchars($_cat) ?> » de tous les articles ?">
|
||
<input type="hidden" name="category" value="<?= htmlspecialchars($_cat) ?>">
|
||
<button type="submit" class="btn btn-outline-danger btn-sm">Supprimer</button>
|
||
</form>
|
||
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- Palette -->
|
||
<div class="col-lg-3">
|
||
<h5 class="mb-3">Prochaine couleur</h5>
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<p class="text-muted small mb-3">
|
||
La prochaine catégorie reçoit la couleur n°<?= count($_cats) % 16 + 1 ?>.
|
||
</p>
|
||
<div class="d-flex flex-wrap gap-2">
|
||
<?php
|
||
$_nextIdx = count($_cats) % 16;
|
||
foreach (COLOR_PALETTE_16 as $_i => $_rgb):
|
||
$_g = _paletteGradient($_rgb, 0);
|
||
$_active = $_i === $_nextIdx;
|
||
?>
|
||
<div title="Couleur <?= $_i + 1 ?>"
|
||
style="width:28px;height:28px;border-radius:6px;background:<?= htmlspecialchars($_g) ?>;
|
||
<?= $_active ? 'outline:2px solid #0d6efd;outline-offset:2px' : 'opacity:.75' ?>">
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<?php if (isAdmin()): ?>
|
||
<hr class="my-4">
|
||
|
||
<div class="row g-4">
|
||
|
||
<div class="col-lg-9">
|
||
<h5 class="mb-3">Types de tags</h5>
|
||
<?php if (empty($_tagTypes)): ?>
|
||
<p class="text-muted">Aucun type de tag défini.</p>
|
||
<?php else: ?>
|
||
<div class="d-flex flex-column gap-2">
|
||
<?php foreach ($_tagTypes as $_key => $_label): ?>
|
||
<div class="card">
|
||
<div class="card-body py-2 px-3 d-flex align-items-center gap-3">
|
||
<code class="text-muted small" style="min-width:8rem"><?= htmlspecialchars($_key) ?></code>
|
||
<strong class="flex-grow-1"><?= htmlspecialchars($_label) ?></strong>
|
||
<form method="POST" action="/?action=delete_tag_type"
|
||
data-confirm="Supprimer le type «<?= htmlspecialchars($_label) ?>» ? Les tags des articles ne sont pas supprimés.">
|
||
<input type="hidden" name="type_key" value="<?= htmlspecialchars($_key) ?>">
|
||
<button type="submit" class="btn btn-outline-danger btn-sm">Supprimer</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="col-lg-3">
|
||
<h5 class="mb-3">Nouveau type</h5>
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<form method="POST" action="/?action=create_tag_type">
|
||
<div class="mb-2">
|
||
<label class="form-label small fw-semibold">Identifiant</label>
|
||
<input type="text" name="type_key" class="form-control form-control-sm font-monospace"
|
||
placeholder="ex : logiciels" required
|
||
pattern="[a-z0-9_]+" title="Minuscules, chiffres et _ uniquement">
|
||
<div class="form-text">Minuscules, chiffres, _</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-semibold">Libellé</label>
|
||
<input type="text" name="type_label" class="form-control form-control-sm"
|
||
placeholder="ex : Logiciels" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary btn-sm w-100">Créer</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<script src="/assets/js/admin.js" defer></script>
|
||
|
||
<?php endif; ?>
|
||
|
||
<!-- ─────────────────────────── SMTP ─────────────────────────────── -->
|
||
<?php if ($tab === 'smtp' && isAdmin()): ?>
|
||
|
||
<?php $sc = $adminData['smtp_config']; ?>
|
||
|
||
<?php if (isset($_GET['saved'])): ?>
|
||
<div class="alert alert-success py-2 mb-3">Paramètres SMTP enregistrés.</div>
|
||
<?php elseif (($_GET['error'] ?? '') === 'write'): ?>
|
||
<div class="alert alert-danger py-2 mb-3">Impossible d'enregistrer : le fichier n'est pas accessible en écriture.</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="row g-4">
|
||
|
||
<!-- Formulaire config + actions -->
|
||
<div class="col-lg-5">
|
||
<div class="card">
|
||
<div class="card-header">Configuration SMTP</div>
|
||
<div class="card-body">
|
||
<form method="post" id="smtp-config-form">
|
||
<div class="mb-3">
|
||
<label for="smtp-host" class="form-label small fw-semibold">Serveur</label>
|
||
<input type="text" id="smtp-host" name="smtp_host"
|
||
class="form-control form-control-sm font-monospace"
|
||
value="<?= htmlspecialchars($sc['host']) ?>"
|
||
placeholder="smtp.exemple.fr">
|
||
</div>
|
||
<div class="row g-2 mb-3">
|
||
<div class="col-5">
|
||
<label for="smtp-port" class="form-label small fw-semibold">Port</label>
|
||
<input type="number" id="smtp-port" name="smtp_port"
|
||
class="form-control form-control-sm"
|
||
value="<?= htmlspecialchars($sc['port']) ?>"
|
||
placeholder="587" min="1" max="65535">
|
||
</div>
|
||
<div class="col-7">
|
||
<label for="smtp-secure" class="form-label small fw-semibold">Chiffrement</label>
|
||
<select id="smtp-secure" name="smtp_secure" class="form-select form-select-sm">
|
||
<option value="" <?= $sc['secure'] === '' ? 'selected' : '' ?>>Aucun</option>
|
||
<option value="tls" <?= strtolower($sc['secure']) === 'tls' ? 'selected' : '' ?>>STARTTLS (587)</option>
|
||
<option value="ssl" <?= strtolower($sc['secure']) === 'ssl' ? 'selected' : '' ?>>SSL/TLS (465)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="smtp-user" class="form-label small fw-semibold">Utilisateur</label>
|
||
<input type="text" id="smtp-user" name="smtp_user"
|
||
class="form-control form-control-sm"
|
||
value="<?= htmlspecialchars($sc['user']) ?>"
|
||
placeholder="user@exemple.fr"
|
||
autocomplete="off">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="smtp-pass" class="form-label small fw-semibold">Mot de passe</label>
|
||
<input type="password" id="smtp-pass" name="smtp_pass"
|
||
class="form-control form-control-sm"
|
||
placeholder="<?= $sc['has_pass'] ? '(inchangé si vide)' : '' ?>"
|
||
autocomplete="new-password">
|
||
<?php if ($sc['has_pass']): ?>
|
||
<div class="form-text">Laisser vide pour conserver le mot de passe actuel.</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="smtp-from" class="form-label small fw-semibold">Email expéditeur</label>
|
||
<input type="email" id="smtp-from" name="smtp_from"
|
||
class="form-control form-control-sm"
|
||
value="<?= htmlspecialchars($sc['from']) ?>"
|
||
placeholder="no-reply@exemple.fr">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="smtp-from-name" class="form-label small fw-semibold">Nom expéditeur</label>
|
||
<input type="text" id="smtp-from-name" name="smtp_from_name"
|
||
class="form-control form-control-sm"
|
||
value="<?= htmlspecialchars($sc['from_name']) ?>"
|
||
placeholder="Mon Site">
|
||
</div>
|
||
<div class="d-flex flex-wrap gap-2">
|
||
<button type="submit" formaction="/?action=admin_smtp_save"
|
||
class="btn btn-primary btn-sm" id="smtp-save-btn">
|
||
Enregistrer
|
||
</button>
|
||
<button type="submit" formaction="/?action=admin_smtp_test"
|
||
name="mode" value="connect"
|
||
class="btn btn-outline-secondary btn-sm">
|
||
Tester la connexion
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Envoi de test + résultats -->
|
||
<div class="col-lg-7">
|
||
<div class="card mb-3">
|
||
<div class="card-body">
|
||
<form method="post" action="/?action=admin_smtp_test" id="smtp-test-form">
|
||
<input type="hidden" name="mode" value="send">
|
||
<label for="smtp-email" class="form-label small fw-semibold">Envoyer un email de test</label>
|
||
<div class="d-flex gap-2">
|
||
<input type="email" id="smtp-email" name="test_email"
|
||
class="form-control form-control-sm"
|
||
placeholder="destinataire@exemple.fr"
|
||
value="<?= htmlspecialchars($adminData['smtp_test']['email'] ?? '') ?>"
|
||
required>
|
||
<button type="submit" class="btn btn-outline-secondary btn-sm text-nowrap" id="smtp-send-btn">
|
||
Envoyer
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if (isset($adminData['smtp_test'])): ?>
|
||
<?php $st = $adminData['smtp_test']; ?>
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span>
|
||
<?php if ($st['success']): ?>
|
||
<span class="text-success fw-semibold">✓ Succès</span>
|
||
<?php else: ?>
|
||
<span class="text-danger fw-semibold">✗ Échec</span>
|
||
<?php endif; ?>
|
||
—
|
||
<?= $st['mode'] === 'send'
|
||
? 'Envoi vers ' . htmlspecialchars($st['email'])
|
||
: 'Test de connexion' ?>
|
||
</span>
|
||
<span class="text-muted small"><?= htmlspecialchars($st['ts']) ?></span>
|
||
</div>
|
||
<?php if (!$st['success'] && $st['error'] !== ''): ?>
|
||
<div class="alert alert-danger mb-0 rounded-0 border-0 border-bottom py-2 px-3 small">
|
||
<?= htmlspecialchars($st['error']) ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php if (!empty($st['logs'])): ?>
|
||
<pre class="p-3 mb-0 small" style="max-height:420px;overflow-y:auto;font-size:0.75rem;background:var(--vl-code-bg,#f8f9fa);border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)"><?php
|
||
foreach ($st['logs'] as $line) {
|
||
echo htmlspecialchars($line) . "\n";
|
||
}
|
||
?></pre>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
</div>
|
||
<script src="/assets/js/admin.js" defer></script>
|
||
|
||
<?php endif; ?>
|
||
|
||
<!-- ─────────────────────────── EMAILS ───────────────────────────── -->
|
||
<?php if ($tab === 'emails' && isAdmin()): ?>
|
||
|
||
<?php
|
||
$emlFilter = $adminData['eml_filter'] ?? '';
|
||
$emlCounts = $adminData['eml_counts'] ?? ['all' => 0, 'sent' => 0, 'error' => 0, 'queued' => 0];
|
||
$emlFilters = [
|
||
'' => ['label' => 'Tous', 'count' => $emlCounts['all']],
|
||
'sent' => ['label' => 'Envoyés', 'count' => $emlCounts['sent']],
|
||
'error' => ['label' => 'Erreurs', 'count' => $emlCounts['error']],
|
||
'queued' => ['label' => 'En file', 'count' => $emlCounts['queued']],
|
||
];
|
||
?>
|
||
|
||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||
<h5 class="mb-0">Logs emails
|
||
<span class="badge bg-secondary ms-1"><?= (int)$emlCounts['all'] ?></span>
|
||
</h5>
|
||
<div class="btn-group btn-group-sm" role="group">
|
||
<?php foreach ($emlFilters as $fVal => $fInfo): ?>
|
||
<a href="/admin/emails<?= $fVal !== '' ? '?filter=' . rawurlencode($fVal) : '' ?>"
|
||
class="btn <?= $emlFilter === $fVal ? 'btn-primary' : 'btn-outline-secondary' ?>">
|
||
<?= htmlspecialchars($fInfo['label']) ?>
|
||
<span class="badge <?= $emlFilter === $fVal ? 'bg-light text-primary' : 'bg-secondary' ?> ms-1"><?= (int)$fInfo['count'] ?></span>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if (empty($adminData['emails'])): ?>
|
||
<p class="text-muted">Aucun email enregistré.</p>
|
||
<?php else: ?>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover align-middle">
|
||
<thead>
|
||
<tr>
|
||
<th>Date</th>
|
||
<th>Destinataire</th>
|
||
<th>Sujet</th>
|
||
<th>Statut</th>
|
||
<th>Contenu</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($adminData['emails'] as $em): ?>
|
||
<?php
|
||
$emDate = date('d/m/Y H:i', strtotime((string)$em['created_at']));
|
||
$emSentAt = $em['sent_at'] ? date('d/m/Y H:i', strtotime((string)$em['sent_at'])) : null;
|
||
if ($em['status'] === 'sent') {
|
||
$emBadge = '<span class="badge bg-success">Envoyé' . ($emSentAt ? ' ' . $emSentAt : '') . '</span>';
|
||
} elseif ($em['status'] === 'error') {
|
||
$emBadge = '<span class="badge bg-danger" title="' . htmlspecialchars((string)($em['error_message'] ?? '')) . '">Erreur</span>';
|
||
} else {
|
||
$emBadge = '<span class="badge bg-warning text-dark">En file</span>';
|
||
}
|
||
?>
|
||
<tr>
|
||
<td class="text-muted small text-nowrap"><?= $emDate ?></td>
|
||
<td class="small"><?= htmlspecialchars((string)$em['to_email']) ?></td>
|
||
<td class="small"><?= htmlspecialchars((string)$em['subject']) ?></td>
|
||
<td><?= $emBadge ?></td>
|
||
<td>
|
||
<?php if (!empty($em['content_html']) || !empty($em['content_text'])): ?>
|
||
<a href="/admin/email-preview/<?= (int)$em['id'] ?>" target="_blank" rel="noopener"
|
||
class="btn btn-outline-secondary btn-sm">Voir ↗</a>
|
||
<?php endif; ?>
|
||
<?php if (!empty($em['error_message'])): ?>
|
||
<span class="text-danger small d-block mt-1" title="<?= htmlspecialchars((string)$em['error_message']) ?>">⚠ Erreur</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<?php if ($adminData['eml_page'] > 0 || count($adminData['emails']) === 50): ?>
|
||
<nav class="d-flex gap-2 mt-3">
|
||
<?php if ($adminData['eml_page'] > 0): ?>
|
||
<a href="/admin/emails?<?= $emlFilter ? 'filter=' . rawurlencode($emlFilter) . '&' : '' ?>page=<?= $adminData['eml_page'] - 1 ?>"
|
||
class="btn btn-outline-secondary btn-sm">← Plus récents</a>
|
||
<?php endif; ?>
|
||
<?php if (count($adminData['emails']) === 50): ?>
|
||
<a href="/admin/emails?<?= $emlFilter ? 'filter=' . rawurlencode($emlFilter) . '&' : '' ?>page=<?= $adminData['eml_page'] + 1 ?>"
|
||
class="btn btn-outline-secondary btn-sm ms-auto">Plus anciens →</a>
|
||
<?php endif; ?>
|
||
</nav>
|
||
<?php endif; ?>
|
||
|
||
<?php endif; ?>
|
||
|
||
<?php endif; ?>
|
||
|
||
<!-- ─────────────────────────── COMMENTAIRES ──────────────────────── -->
|
||
<?php if ($tab === 'comments' && isAdmin()): ?>
|
||
|
||
<?php
|
||
$cmtFilter = $adminData['cmt_filter_status'] ?? '';
|
||
$cmtCounts = $adminData['cmt_counts'] ?? ['all' => 0, 'pending' => 0, 'verified' => 0, 'hidden' => 0];
|
||
$cmtFilters = [
|
||
'' => ['label' => 'Tous', 'count' => $cmtCounts['all']],
|
||
'pending' => ['label' => 'En attente', 'count' => $cmtCounts['pending']],
|
||
'verified' => ['label' => 'Vérifiés', 'count' => $cmtCounts['verified']],
|
||
'hidden' => ['label' => 'Masqués', 'count' => $cmtCounts['hidden']],
|
||
];
|
||
?>
|
||
|
||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||
<h5 class="mb-0">Commentaires
|
||
<span class="badge bg-secondary ms-1"><?= count($adminData['comments']) ?></span>
|
||
</h5>
|
||
<div class="btn-group btn-group-sm" role="group" aria-label="Filtres commentaires">
|
||
<?php foreach ($cmtFilters as $fVal => $fInfo): ?>
|
||
<a href="/admin/comments<?= $fVal !== '' ? '?filter_status=' . rawurlencode($fVal) : '' ?>"
|
||
class="btn <?= $cmtFilter === $fVal ? 'btn-primary' : 'btn-outline-secondary' ?>">
|
||
<?= htmlspecialchars($fInfo['label']) ?>
|
||
<span class="badge <?= $cmtFilter === $fVal ? 'bg-light text-primary' : 'bg-secondary' ?> ms-1"><?= (int)$fInfo['count'] ?></span>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if (empty($adminData['comments'])): ?>
|
||
<p class="text-muted">Aucun commentaire pour ce filtre.</p>
|
||
<?php else: ?>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover align-middle">
|
||
<thead>
|
||
<tr>
|
||
<th>Article</th>
|
||
<th>Auteur</th>
|
||
<th>Commentaire</th>
|
||
<th>Date</th>
|
||
<th>Email</th>
|
||
<th>Statut</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($adminData['comments'] as $c): ?>
|
||
<?php
|
||
$cSlug = $adminData['articleSlugs'][$c['article_uuid']] ?? null;
|
||
$cDate = date('d/m/Y H:i', strtotime((string)$c['created_at']));
|
||
$cVerified = (bool)($c['verified'] === true || $c['verified'] === 't' || $c['verified'] === '1');
|
||
$cPublished = (bool)($c['published'] === true || $c['published'] === 't' || $c['published'] === '1');
|
||
|
||
// Badge statut email
|
||
$mailStatus = $c['mail_status'] ?? null;
|
||
if ($mailStatus === 'sent') {
|
||
$mailBadge = '<span class="badge bg-success" title="Envoyé le ' . htmlspecialchars((string)($c['mail_sent_at'] ?? '')) . '">Envoyé</span>';
|
||
} elseif ($mailStatus === 'error') {
|
||
$mailBadge = '<span class="badge bg-danger" title="' . htmlspecialchars((string)($c['mail_error'] ?? '')) . '">Erreur</span>';
|
||
} elseif ($mailStatus === 'queued') {
|
||
$mailBadge = '<span class="badge bg-warning text-dark">En file</span>';
|
||
} else {
|
||
$mailBadge = '<span class="badge bg-light text-muted border">-</span>';
|
||
}
|
||
?>
|
||
<tr>
|
||
<td class="small" style="max-width:18ch">
|
||
<?php if ($cSlug): ?>
|
||
<a href="/post/<?= rawurlencode($cSlug) ?>#comments"
|
||
title="<?= htmlspecialchars($cSlug) ?>">
|
||
<?= htmlspecialchars(mb_strimwidth($cSlug, 0, 28, '…')) ?>
|
||
</a>
|
||
<?php else: ?>
|
||
<span class="text-muted"><?= htmlspecialchars(substr($c['article_uuid'], 0, 8)) ?>…</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td class="small">
|
||
<div><?= htmlspecialchars($c['author_name']) ?></div>
|
||
<div class="text-muted" style="font-size:.8em"><?= htmlspecialchars($c['author_email']) ?></div>
|
||
</td>
|
||
<td class="small" style="max-width:28ch">
|
||
<span title="<?= htmlspecialchars($c['content']) ?>">
|
||
<?= htmlspecialchars(mb_strimwidth($c['content'], 0, 100, '…')) ?>
|
||
</span>
|
||
<?php if (!empty($c['verification_code'])): ?>
|
||
<br><span class="text-muted" style="font-size:.8em">Code : <?= htmlspecialchars($c['verification_code']) ?></span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td class="text-muted small text-nowrap"><?= htmlspecialchars($cDate) ?></td>
|
||
<td><?= $mailBadge ?></td>
|
||
<td>
|
||
<?php if (!$cVerified): ?>
|
||
<span class="badge bg-warning text-dark">En attente</span>
|
||
<?php else: ?>
|
||
<span class="badge bg-success">Vérifié</span>
|
||
<?php if ($cPublished): ?>
|
||
<span class="badge bg-primary">Publié</span>
|
||
<?php else: ?>
|
||
<span class="badge bg-secondary">Masqué</span>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td class="text-nowrap">
|
||
<?php if (!$cVerified): ?>
|
||
<form method="post" action="/?action=comment_resend" class="d-inline">
|
||
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
||
<button type="submit" class="btn btn-sm btn-outline-info">Renvoyer</button>
|
||
</form>
|
||
<?php elseif ($cPublished): ?>
|
||
<form method="post" action="/comment-moderate" class="d-inline">
|
||
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
||
<input type="hidden" name="pub" value="0">
|
||
<button type="submit" class="btn btn-sm btn-outline-danger">Masquer</button>
|
||
</form>
|
||
<?php else: ?>
|
||
<form method="post" action="/comment-moderate" class="d-inline">
|
||
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
||
<input type="hidden" name="pub" value="1">
|
||
<button type="submit" class="btn btn-sm btn-outline-success">Publier</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<form method="post" action="/?action=comment_delete" class="d-inline"
|
||
data-confirm="Supprimer ce commentaire définitivement ?">
|
||
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
||
<button type="submit" class="btn btn-sm btn-danger">✕</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
<script src="/assets/js/admin.js" defer></script>
|
||
|
||
<?php endif; ?>
|
||
|
||
<!-- ─────────────────────────── RECHERCHES ─────────────────────────── -->
|
||
<?php if ($tab === 'searches' && isAdmin()): ?>
|
||
|
||
<?php if (isset($_GET['saved'])): ?>
|
||
<div class="alert alert-success py-2 mb-3">Configuration enregistrée.</div>
|
||
<?php elseif (($_GET['error'] ?? '') === 'write'): ?>
|
||
<div class="alert alert-danger py-2 mb-3">Impossible d'enregistrer : le fichier n'est pas accessible en écriture.</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="card mb-4" style="max-width:540px">
|
||
<div class="card-header bg-transparent py-2 small fw-semibold">Configuration des logs</div>
|
||
<div class="card-body py-3">
|
||
<form method="post" action="/?action=admin_save_searches_config">
|
||
<div class="mb-3">
|
||
<label for="apache-access-log" class="form-label small fw-semibold">Pattern des logs d'accès</label>
|
||
<input type="text" id="apache-access-log" name="apache_access_log"
|
||
class="form-control form-control-sm font-monospace"
|
||
value="<?= htmlspecialchars(apacheAccessLog()) ?>"
|
||
maxlength="200" placeholder="ex : *-access.log">
|
||
<div class="form-text">Pattern glob dans <code>/var/log/apache2/</code>. Les rotations (<code>.gz</code>, <code>.tar.gz</code>) sont automatiquement incluses.</div>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||
<h5 class="mb-0">Termes recherchés
|
||
<span class="badge bg-secondary ms-1"><?= count($adminData['search_terms'] ?? []) ?></span>
|
||
</h5>
|
||
<div class="d-flex align-items-center gap-3">
|
||
<span class="text-muted small">Derniers <?= (int)($adminData['search_days'] ?? 14) ?> jours · cache 10 min</span>
|
||
<div class="btn-group btn-group-sm" role="group">
|
||
<a href="/admin/searches?days=7"
|
||
class="btn <?= ($adminData['search_days'] ?? 14) === 7 ? 'btn-primary' : 'btn-outline-secondary' ?>">7 j</a>
|
||
<a href="/admin/searches"
|
||
class="btn <?= ($adminData['search_days'] ?? 14) === 14 ? 'btn-primary' : 'btn-outline-secondary' ?>">14 j</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if (!($adminData['search_log_readable'] ?? false)): ?>
|
||
<div class="alert alert-warning py-2">
|
||
Les logs Apache ne sont pas lisibles par PHP. Vérifiez que <code>www-data</code> appartient au groupe <code>adm</code>.
|
||
</div>
|
||
<?php elseif (empty($adminData['search_terms'])): ?>
|
||
<p class="text-muted">Aucune recherche trouvée dans les logs.</p>
|
||
<?php else: ?>
|
||
<?php
|
||
$terms = $adminData['search_terms'];
|
||
$maxCount = max($terms);
|
||
$rank = 0;
|
||
?>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover align-middle">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:3rem">#</th>
|
||
<th>Terme recherché</th>
|
||
<th style="width:6rem" class="text-end">Visiteurs</th>
|
||
<th style="width:12rem"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($terms as $term => $count): ?>
|
||
<?php $rank++;
|
||
$pct = round($count / $maxCount * 100); ?>
|
||
<tr>
|
||
<td class="text-muted small"><?= $rank ?></td>
|
||
<td>
|
||
<a href="/search?q=<?= rawurlencode($term) ?>"
|
||
class="text-decoration-none fw-medium">
|
||
<?= htmlspecialchars($term) ?>
|
||
</a>
|
||
</td>
|
||
<td class="text-end fw-semibold"><?= $count ?></td>
|
||
<td>
|
||
<div class="progress" style="height:6px">
|
||
<div class="progress-bar" style="width:<?= $pct ?>%"></div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php endif; ?>
|
||
|
||
<!-- ─────────────────────────── FLUX RSS ─────────────────────────── -->
|
||
<?php if ($tab === 'flux' && isAdmin()): ?>
|
||
|
||
<?php if (($_GET['deleted'] ?? '') === '1'): ?>
|
||
<div class="alert alert-success py-2 small">Flux supprimé.</div>
|
||
<?php endif; ?>
|
||
|
||
<h5>Flux RSS agrégés</h5>
|
||
<p class="text-muted small">Tous les flux enregistrés par les utilisateurs. Seul un administrateur peut les supprimer.</p>
|
||
|
||
<?php if (empty($adminData['flux_feeds'] ?? [])): ?>
|
||
<p class="text-muted">Aucun flux enregistré.</p>
|
||
<?php else: ?>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover align-middle">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Utilisateur</th>
|
||
<th>Libellé</th>
|
||
<th>URL</th>
|
||
<th>Ajouté le</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($adminData['flux_feeds'] as $_feed): ?>
|
||
<tr>
|
||
<td class="small"><?= htmlspecialchars($_feed['user_email'] ?? '') ?></td>
|
||
<td class="small"><?= htmlspecialchars($_feed['label'] ?? '') ?></td>
|
||
<td class="small text-truncate" style="max-width:260px">
|
||
<a href="<?= htmlspecialchars($_feed['feed_url'] ?? '') ?>" target="_blank" rel="noopener" class="text-muted">
|
||
<?= htmlspecialchars($_feed['feed_url'] ?? '') ?>
|
||
</a>
|
||
</td>
|
||
<td class="small text-nowrap"><?= htmlspecialchars(substr($_feed['created_at'] ?? '', 0, 10)) ?></td>
|
||
<td>
|
||
<form method="POST" action="/?action=admin_delete_feed"
|
||
data-confirm="Supprimer ce flux ?">
|
||
<input type="hidden" name="id" value="<?= (int)$_feed['id'] ?>">
|
||
<button type="submit" class="btn btn-outline-danger btn-sm py-0">Supprimer</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php endif; ?>
|
||
|
||
<!-- ─────────────────────────── LIVRES ─────────────────────────── -->
|
||
<?php if ($tab === 'books' && isAdmin()): ?>
|
||
|
||
<?php if (($_GET['deleted'] ?? '') === '1'): ?>
|
||
<div class="alert alert-success py-2 small mb-3">Livre supprimé.</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="row g-4">
|
||
|
||
<!-- Liste des livres -->
|
||
<div class="col-md-4">
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<h5 class="mb-0">
|
||
Livres
|
||
<span class="badge bg-secondary ms-1"><?= count($adminData['books']) ?></span>
|
||
</h5>
|
||
<a href="/admin/books?new=1" class="btn btn-sm btn-primary">+ Nouveau</a>
|
||
</div>
|
||
|
||
<?php if (empty($adminData['books'])): ?>
|
||
<p class="text-muted small">Aucun livre pour l'instant.</p>
|
||
<?php else: ?>
|
||
<div class="list-group">
|
||
<?php foreach ($adminData['books'] as $bk):
|
||
$isEdited = ($adminData['edit_book']['slug'] ?? '') === $bk['slug'];
|
||
?>
|
||
<a href="/admin/books?edit=<?= rawurlencode($bk['slug']) ?>"
|
||
class="list-group-item list-group-item-action<?= $isEdited ? ' active' : '' ?>">
|
||
<div class="fw-medium"><?= htmlspecialchars($bk['title']) ?></div>
|
||
<div class="small <?= $isEdited ? 'text-white-50' : 'text-muted' ?>">
|
||
<?= count($bk['articles'] ?? []) ?> page<?= count($bk['articles'] ?? []) > 1 ? 's' : '' ?>
|
||
· <a href="/book/<?= rawurlencode($bk['slug']) ?>" target="_blank"
|
||
class="<?= $isEdited ? 'text-white-50' : 'text-muted' ?>">Voir ↗</a>
|
||
</div>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- Formulaire édition / création -->
|
||
<div class="col-md-8">
|
||
<?php if (($adminData['edit_book'] ?? null) !== null): ?>
|
||
<?php $eb = $adminData['edit_book']; ?>
|
||
<h5>Modifier le livre</h5>
|
||
|
||
<?php if (($_GET['saved'] ?? '') === '1'): ?>
|
||
<div class="alert alert-success py-2 small">Livre sauvegardé.</div>
|
||
<?php endif; ?>
|
||
|
||
<form method="POST" action="/?action=book_save">
|
||
<input type="hidden" name="slug" value="<?= htmlspecialchars($eb['slug']) ?>">
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-medium">Slug (identifiant URL)</label>
|
||
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($eb['slug']) ?>" readonly>
|
||
<div class="form-text">Le slug ne peut pas être modifié après création.</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-medium">Titre</label>
|
||
<input type="text" name="title" class="form-control" required
|
||
value="<?= htmlspecialchars($eb['title'] ?? '') ?>">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-medium">Description</label>
|
||
<textarea name="description" class="form-control" rows="2"><?= htmlspecialchars($eb['description'] ?? '') ?></textarea>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-medium">Pages (slugs dans l'ordre, un par ligne)</label>
|
||
<textarea name="articles" class="form-control font-monospace"
|
||
id="book-articles-ta"
|
||
rows="<?= max(6, count($eb['articles'] ?? []) + 2) ?>"><?= htmlspecialchars(implode("\n", $eb['articles'] ?? [])) ?></textarea>
|
||
<div class="form-text">Un slug par ligne. L'ordre définit la navigation précédent/suivant.</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-medium">Ajouter une page existante</label>
|
||
<input type="text" id="book-article-filter" class="form-control form-control-sm mb-1"
|
||
placeholder="Filtrer les articles…" autocomplete="off">
|
||
<select class="form-select" id="book-article-select">
|
||
<option value="">— Choisir un article —</option>
|
||
<?php
|
||
$alreadyIn = $eb['articles'] ?? [];
|
||
foreach ($adminData['all_articles'] as $aa):
|
||
if (in_array($aa['slug'] ?? '', $alreadyIn, true)) {
|
||
continue;
|
||
}
|
||
?>
|
||
<option value="<?= htmlspecialchars($aa['slug'] ?? '') ?>">
|
||
<?= htmlspecialchars($aa['title']) ?>
|
||
<?= !$aa['published'] ? ' (brouillon)' : '' ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
<button type="submit" class="btn btn-primary">Sauvegarder</button>
|
||
<a href="/book/<?= rawurlencode($eb['slug']) ?>" target="_blank"
|
||
class="btn btn-outline-secondary">Voir le livre ↗</a>
|
||
</div>
|
||
</form>
|
||
|
||
<hr class="my-4">
|
||
|
||
<form method="POST" action="/?action=book_delete"
|
||
data-confirm="Supprimer le livre « <?= htmlspecialchars($eb['title']) ?> » ? Les pages resteront intactes.">
|
||
<input type="hidden" name="slug" value="<?= htmlspecialchars($eb['slug']) ?>">
|
||
<button type="submit" class="btn btn-outline-danger btn-sm">🗑 Supprimer ce livre</button>
|
||
</form>
|
||
|
||
|
||
<?php elseif (isset($_GET['new'])): ?>
|
||
<h5>Nouveau livre</h5>
|
||
<form method="POST" action="/?action=book_save">
|
||
<input type="hidden" name="slug" id="new-book-slug-hidden">
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-medium">Titre</label>
|
||
<input type="text" name="title" id="new-book-title" class="form-control" required placeholder="Titre du livre">
|
||
</div>
|
||
<div class="mb-2">
|
||
<label class="form-label small fw-medium">Slug (généré automatiquement)</label>
|
||
<input type="text" id="new-book-slug-preview" class="form-control form-control-sm bg-light font-monospace"
|
||
readonly placeholder="slug-du-livre">
|
||
<div class="form-text">Minuscules, chiffres, tirets — basé sur le titre.</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-medium">Description (optionnelle)</label>
|
||
<textarea name="description" class="form-control" rows="2" placeholder="Courte description…"></textarea>
|
||
</div>
|
||
<input type="hidden" name="articles" value="">
|
||
<div class="d-flex gap-2">
|
||
<button type="submit" class="btn btn-primary">Créer le livre</button>
|
||
<a href="/admin/books" class="btn btn-outline-secondary">Annuler</a>
|
||
</div>
|
||
</form>
|
||
|
||
<?php else: ?>
|
||
<p class="text-muted mt-2">
|
||
<?php if (!empty($adminData['books'])): ?>
|
||
Sélectionnez un livre à gauche pour le modifier, ou créez-en un nouveau.
|
||
<?php else: ?>
|
||
Cliquez sur <strong>+ Nouveau</strong> pour créer votre premier livre.
|
||
<?php endif; ?>
|
||
</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<?php endif; ?>
|
||
|
||
<?php if ($tab === 'ia' && isAdmin()): ?>
|
||
|
||
<?php
|
||
$_aiNotice = $adminData['ai_notice'] ?? '';
|
||
$_aiProvider = $adminData['ai_provider'] ?? 'anthropic';
|
||
$_aiModel = $adminData['ai_model'] ?? '';
|
||
$_anthropicOk = $adminData['anthropic_key_set'] ?? false;
|
||
$_cliOk = $adminData['claude_cli_found'] ?? false;
|
||
?>
|
||
|
||
<?php if ($_aiNotice === 'saved'): ?>
|
||
<div class="alert alert-success py-2 small">Configuration IA enregistrée.</div>
|
||
<?php elseif ($_aiNotice === 'error'): ?>
|
||
<div class="alert alert-danger py-2 small">Erreur lors de l'enregistrement.</div>
|
||
<?php endif; ?>
|
||
|
||
<h5 class="mb-3">Intelligence artificielle</h5>
|
||
|
||
<!-- Section 1 — Statut -->
|
||
<div class="card mb-4">
|
||
<div class="card-header fw-semibold small">Statut</div>
|
||
<div class="card-body p-0">
|
||
<table class="table table-sm mb-0">
|
||
<tbody>
|
||
<tr>
|
||
<th class="ps-3" scope="row">Clé Anthropic (<code>ANTHROPIC_API_KEY</code>)</th>
|
||
<td><?= $_anthropicOk ? '<span class="text-success">✓ Configurée</span>' : '<span class="text-danger">✗ Absente</span>' ?></td>
|
||
</tr>
|
||
<tr>
|
||
<th class="ps-3" scope="row">Claude Code CLI (<code>/usr/local/bin/claude</code>)</th>
|
||
<td><?= $_cliOk ? '<span class="text-success">✓ Trouvé</span>' : '<span class="text-danger">✗ Introuvable</span>' ?></td>
|
||
</tr>
|
||
<tr>
|
||
<th class="ps-3" scope="row">Provider actif</th>
|
||
<td><code><?= htmlspecialchars($_aiProvider) ?></code></td>
|
||
</tr>
|
||
<tr>
|
||
<th class="ps-3" scope="row">Modèle actif</th>
|
||
<td><code><?= htmlspecialchars($_aiModel ?: 'claude-haiku-4-5-20251001 (défaut)') ?></code></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 2 — Configuration -->
|
||
<div class="card mb-4">
|
||
<div class="card-header fw-semibold small">Configuration</div>
|
||
<div class="card-body">
|
||
<form method="POST" action="/?action=admin_save_ai_config">
|
||
<div class="mb-3">
|
||
<label class="form-label fw-semibold small">Provider</label>
|
||
<div class="d-flex gap-3">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="ai_provider" id="ai_provider_anthropic"
|
||
value="anthropic" <?= $_aiProvider === 'anthropic' ? 'checked' : '' ?>>
|
||
<label class="form-check-label" for="ai_provider_anthropic">Anthropic (API)</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="radio" name="ai_provider" id="ai_provider_claude_code"
|
||
value="claude_code" <?= $_aiProvider === 'claude_code' ? 'checked' : '' ?>>
|
||
<label class="form-check-label" for="ai_provider_claude_code">Claude Code CLI</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="ai_model" class="form-label fw-semibold small">Modèle Anthropic</label>
|
||
<input type="text" class="form-control form-control-sm font-monospace" id="ai_model" name="ai_model"
|
||
value="<?= htmlspecialchars($_aiModel) ?>"
|
||
placeholder="claude-haiku-4-5-20251001">
|
||
<div class="form-text">Laisser vide pour utiliser le défaut (<code>claude-haiku-4-5-20251001</code>). Ignoré si le provider est Claude Code CLI.</div>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 3 — Clé Anthropic -->
|
||
<div class="card mb-4">
|
||
<div class="card-header fw-semibold small">Clé API Anthropic</div>
|
||
<div class="card-body">
|
||
<div class="alert alert-warning py-2 small mb-0">
|
||
<strong>La clé API Anthropic ne peut pas être saisie ici.</strong><br>
|
||
Elle doit être définie dans le fichier <code>.env</code> du serveur :
|
||
<pre class="mt-2 mb-0 small"><code>ANTHROPIC_API_KEY=sk-ant-...</code></pre>
|
||
<div class="mt-2">Statut actuel : <?= $_anthropicOk ? '<span class="text-success">✓ Configurée</span>' : '<span class="text-danger">✗ Absente</span>' ?></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 4 — Procédure Claude Code CLI -->
|
||
<div class="card mb-4">
|
||
<div class="card-header fw-semibold small">Procédure d'installation de Claude Code CLI</div>
|
||
<div class="card-body">
|
||
<?php if ($_cliOk): ?>
|
||
<div class="alert alert-success py-2 small mb-3">✓ <code>/usr/local/bin/claude</code> détecté.</div>
|
||
<?php else: ?>
|
||
<div class="alert alert-secondary py-2 small mb-3">✗ <code>/usr/local/bin/claude</code> introuvable — suivez les étapes ci-dessous.</div>
|
||
<?php endif; ?>
|
||
<p class="small text-muted">À exécuter en SSH sur le serveur (en root ou via sudo) :</p>
|
||
<pre class="bg-dark text-light p-3 rounded small"><code># 1. Installer Claude Code CLI (en root)
|
||
sudo npm install -g @anthropic-ai/claude-code
|
||
|
||
# Vérifier l'installation
|
||
/usr/local/bin/claude --version
|
||
|
||
# 2. Créer le répertoire HOME de www-data pour Claude
|
||
sudo mkdir -p /var/lib/claude-www
|
||
sudo chown www-data:www-data /var/lib/claude-www
|
||
|
||
# 3. Authentifier Claude en tant que www-data
|
||
sudo -u www-data HOME=/var/lib/claude-www /usr/local/bin/claude auth login
|
||
# → Suivre les instructions (OAuth navigateur ou clé API)
|
||
|
||
# 4. Vérifier que ça fonctionne
|
||
sudo -u www-data HOME=/var/lib/claude-www /usr/local/bin/claude --print "Réponds juste OK"</code></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<?php endif; ?>
|
||
|
||
<?php if ($tab === 'stats' && isAdmin()): ?>
|
||
|
||
<?php include __DIR__ . '/admin_stats.php'; ?>
|
||
|
||
<?php endif; ?>
|
||
|
||
<?php
|
||
$content = ob_get_clean();
|
||
$title = 'Administration — ' . siteTitle();
|
||
include __DIR__ . '/layout.php';
|