feat : versionnage semver, migrations contenu, bandeau mise à jour admin

- CHANGELOG.md : structure semver (1.0.0 / 1.1.0 / 1.2.0) remplace le journal non versionné
- public/version.txt : généré à chaque push depuis la première entrée CHANGELOG
- scripts/push.sh : extrait la version CHANGELOG avant git add
- src/UpdateChecker.php : compare version déployée vs version Gitea (raw file), cache 1 h
- templates/layout.php : bandeau alerte admin (nouvelle version / migrations en attente)
- templates/admin.php : dashboard moteur Folio (version déployée / disponible)
- scripts/migrate_content.php + migration_001 : ajout # titre dans les articles existants
- templates/maintenance.php : page HTTP 503 pendant une migration
- src/helpers.php : extractMarkdownTitle(), normalisation \r\n dans lineDiff()
- templates/wizard/step1.php : suppression champ titre, plan TOC dynamique
- public/assets/js/wizard.js : scope titleEl, scrollToCursor, buildToc, handlers externalisés

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 22:45:35 +02:00
parent c503f1dd66
commit 1dbe6d8dd3
13 changed files with 565 additions and 219 deletions
+16 -39
View File
@@ -37,40 +37,12 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
// ─── Génération slug automatique (étape 1 / création) ───────────────────
var titleInput = document.getElementById('wz-title');
var slugField = document.getElementById('slug');
var slugPreview = document.getElementById('slug-preview');
function slugify(s) {
var map = {'à':'a','â':'a','ä':'a','é':'e','è':'e','ê':'e','ë':'e','î':'i','ï':'i','ô':'o','ö':'o','ù':'u','û':'u','ü':'u','ç':'c','æ':'ae','œ':'oe'};
return s.toLowerCase()
.replace(/[àâäéèêëîïôöùûüçæœ]/g, function(c) { return map[c] || c; })
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
if (titleInput && slugField) {
if (slugField.value !== '') slugField._auto = false;
titleInput.addEventListener('input', function () {
if (slugField._auto !== false) {
var gen = slugify(this.value);
slugField.value = gen;
if (slugPreview) slugPreview.textContent = gen;
}
});
slugField.addEventListener('input', function () {
this._auto = (this.value === '');
if (slugPreview) slugPreview.textContent = this.value;
});
}
var slugField = document.getElementById('slug');
// ─── Autosave ────────────────────────────────────────────────────────────
var indicator = document.getElementById('autosave-indicator');
if (indicator && uuid && autosaveUrl) {
var timer = null;
var titleEl = document.getElementById('wz-title');
var contentEl = document.getElementById('wz-content');
var timer = null;
function scheduleAutosave() {
clearTimeout(timer);
@@ -78,27 +50,32 @@ document.addEventListener('DOMContentLoaded', function () {
}
async function doAutosave() {
if (!titleEl || !contentEl) return;
if (!ta) return;
indicator.textContent = 'Sauvegarde…';
try {
var res = await fetch(autosaveUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
title: titleEl.value,
content: contentEl.value,
slug: slugField ? slugField.value : '',
}),
body: new URLSearchParams({ content: ta.value }),
});
var data = await res.json();
indicator.textContent = data.ok ? 'Brouillon sauvegardé à ' + data.time : 'Erreur de sauvegarde';
if (data.ok) {
indicator.textContent = 'Sauvegardé à ' + data.time;
// Mettre à jour le titre de la page si le serveur l'a extrait
var pageTitle = document.getElementById('wz-page-title');
if (pageTitle && data.title) {
var prefix = pageTitle.dataset.prefix || '';
pageTitle.textContent = prefix ? prefix + data.title : data.title;
}
} else {
indicator.textContent = 'Erreur de sauvegarde';
}
} catch (err) {
indicator.textContent = 'Erreur de sauvegarde';
}
}
if (titleEl) titleEl.addEventListener('input', scheduleAutosave);
if (ta) ta.addEventListener('input', scheduleAutosave);
if (ta) ta.addEventListener('input', scheduleAutosave);
}
// ─── Insertion Markdown depuis miniatures ────────────────────────────────