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>
215 lines
7.3 KiB
HTML
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>
|