alpinux.site.2026/admin/templates/index.html
Cédrix 60eb8bc952 feat: interface admin portail.alpinux.org/admin/ — déclencher mkdocs build
Mini-app Flask (admin/) accessible à https://portail.alpinux.org/admin/ :
- Authentification AlpID OIDC, accès restreint au groupe « admins » Keycloak
- Bouton « Lancer mkdocs build » avec confirmation
- Exécution de deploy-wiki.sh en arrière-plan (thread), log capturé en direct
- Polling JS toutes les 2s pendant le build (status + log + historique)
- Affichage du journal en terminal sombre avec suivi automatique
- Historique des 20 derniers builds (date, déclencheur, résultat, durée)
- ProxyFix pour X-Forwarded-Proto / X-Script-Name (Apache reverse proxy)

Infrastructure :
- scripts/portail.alpinux.org.admin.conf : bloc ProxyPass pour ISPConfig
- scripts/alpinux-admin.service : systemd Gunicorn port 5002
- admin/.env.example, admin/requirements.txt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 12:34:18 +02:00

215 lines
7.3 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin — Alpinux</title>
<link rel="icon" type="image/x-icon" href="https://static.alpinux.org/logo/favicon.ico">
<link rel="stylesheet" href="{{ url_for('static', filename='admin.css') }}">
</head>
<body>
<header>
<div class="header-inner">
<a href="/" class="brand">
<img src="https://static.alpinux.org/logo/alpinux-logo.png" alt="Alpinux" width="36" height="36">
<span>A<strong>l</strong>p<strong>inux</strong> <span class="brand-sub">Admin</span></span>
</a>
<div class="header-user">
<span>{{ user.name }}</span>
<a href="{{ url_for('logout') }}" class="btn-logout">Déconnexion</a>
</div>
</div>
</header>
<main>
<!-- ── Carte de build ─────────────────────────────────────────── -->
<section class="build-card" id="build-card">
<div class="build-card-header">
<div>
<h2>📖 Wiki — <code>wiki.alpinux.org</code></h2>
<p class="subtitle">Lance <code>git pull</code> + <code>mkdocs build --strict</code> sur le serveur</p>
</div>
<div class="build-status" id="build-status">
{% if state.running %}
<span class="status-badge running">⏳ En cours…</span>
{% elif state.last_success is none %}
<span class="status-badge idle">En attente</span>
{% elif state.last_success %}
<span class="status-badge success">✓ Succès</span>
{% else %}
<span class="status-badge failure">✗ Échec</span>
{% endif %}
</div>
</div>
<div class="build-meta" id="build-meta">
{% if state.triggered_by %}
<span>Dernier déclencheur : <strong>{{ state.triggered_by }}</strong></span>
{% endif %}
{% if state.last_duration_s %}
<span>Durée : <strong>{{ state.last_duration_s }}s</strong></span>
{% endif %}
</div>
<button class="btn btn-build" id="btn-build"
{% if state.running %}disabled{% endif %}
onclick="triggerBuild()">
{% if state.running %}
⏳ Build en cours…
{% else %}
🚀 Lancer mkdocs build
{% endif %}
</button>
</section>
<!-- ── Log ───────────────────────────────────────────────────── -->
<section class="log-section">
<div class="log-header">
<h3>Journal du dernier build</h3>
<label class="scroll-toggle">
<input type="checkbox" id="auto-scroll" checked>
Suivi auto
</label>
</div>
<pre class="log-output" id="log-output">{{ log }}</pre>
</section>
<!-- ── Historique ────────────────────────────────────────────── -->
<section class="history-section">
<h3>Historique des builds</h3>
<table class="history-table" id="history-table">
<thead>
<tr><th>Date</th><th>Déclenché par</th><th>Résultat</th><th>Durée</th></tr>
</thead>
<tbody id="history-body">
{% for entry in state.history %}
<tr>
<td>{{ entry.at }}</td>
<td>{{ entry.triggered_by }}</td>
<td>
{% if entry.success %}
<span class="status-badge success">✓ Succès</span>
{% else %}
<span class="status-badge failure">✗ Échec</span>
{% endif %}
</td>
<td>{{ entry.duration_s }}s</td>
</tr>
{% else %}
<tr><td colspan="4" class="empty">Aucun build enregistré</td></tr>
{% endfor %}
</tbody>
</table>
</section>
</main>
<script>
const POLL_MS = 2000;
let polling = false;
function triggerBuild() {
if (!confirm('Lancer mkdocs build sur le serveur ?')) return;
document.getElementById('btn-build').disabled = true;
document.getElementById('btn-build').textContent = '⏳ Démarrage…';
fetch('/api/build', { method: 'POST' })
.then(r => r.json())
.then(data => {
if (data.started) {
startPolling();
} else {
alert(data.reason || 'Impossible de démarrer le build.');
document.getElementById('btn-build').disabled = false;
document.getElementById('btn-build').textContent = '🚀 Lancer mkdocs build';
}
})
.catch(() => {
document.getElementById('btn-build').disabled = false;
document.getElementById('btn-build').textContent = '🚀 Lancer mkdocs build';
});
}
function startPolling() {
if (polling) return;
polling = true;
poll();
}
function poll() {
Promise.all([
fetch('/api/status').then(r => r.json()),
fetch('/api/log').then(r => r.json()),
]).then(([status, logData]) => {
updateStatus(status);
updateLog(logData.log || '');
if (status.running) {
setTimeout(poll, POLL_MS);
} else {
polling = false;
updateHistory(status.history || []);
}
}).catch(() => {
if (polling) setTimeout(poll, POLL_MS * 2);
});
}
function updateStatus(s) {
const badge = document.getElementById('build-status');
const btn = document.getElementById('btn-build');
const meta = document.getElementById('build-meta');
if (s.running) {
badge.innerHTML = '<span class="status-badge running">⏳ En cours…</span>';
btn.disabled = true;
btn.textContent = '⏳ Build en cours…';
} else if (s.last_success === true) {
badge.innerHTML = '<span class="status-badge success">✓ Succès</span>';
btn.disabled = false;
btn.textContent = '🚀 Lancer mkdocs build';
} else if (s.last_success === false) {
badge.innerHTML = '<span class="status-badge failure">✗ Échec</span>';
btn.disabled = false;
btn.textContent = '🚀 Lancer mkdocs build';
}
let metaHtml = '';
if (s.triggered_by) metaHtml += `<span>Dernier déclencheur : <strong>${s.triggered_by}</strong></span>`;
if (s.last_duration) metaHtml += `<span>Durée : <strong>${s.last_duration}s</strong></span>`;
meta.innerHTML = metaHtml;
}
function updateLog(text) {
const el = document.getElementById('log-output');
el.textContent = text || '(aucun log)';
if (document.getElementById('auto-scroll').checked) {
el.scrollTop = el.scrollHeight;
}
}
function updateHistory(entries) {
const tbody = document.getElementById('history-body');
if (!entries.length) return;
tbody.innerHTML = entries.map(e => `
<tr>
<td>${e.at}</td>
<td>${e.triggered_by}</td>
<td><span class="status-badge ${e.success ? 'success' : 'failure'}">${e.success ? '✓ Succès' : '✗ Échec'}</span></td>
<td>${e.duration_s}s</td>
</tr>`).join('');
}
/* Démarre le polling si un build tourne déjà au chargement de la page */
{% if state.running %}startPolling();{% endif %}
/* Scroll initial vers la fin du log */
window.addEventListener('load', function () {
const log = document.getElementById('log-output');
log.scrollTop = log.scrollHeight;
});
</script>
</body>
</html>