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:
+16
-39
@@ -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 ────────────────────────────────
|
||||
|
||||
+75
-40
@@ -26,6 +26,17 @@ require_once BASE_PATH . '/src/ArticleManager.php';
|
||||
|
||||
$articles = new ArticleManager(BASE_PATH . '/data');
|
||||
|
||||
// ─── Mode maintenance ──────────────────────────────────────────────────────
|
||||
if (file_exists(BASE_PATH . '/data/.maintenance')) {
|
||||
http_response_code(503);
|
||||
header('Retry-After: 60');
|
||||
include BASE_PATH . '/templates/maintenance.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once BASE_PATH . '/src/UpdateChecker.php';
|
||||
$_updateChecker = new UpdateChecker(BASE_PATH . '/data', BASE_PATH);
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$uuid = $_GET['uuid'] ?? '';
|
||||
$slug = $_GET['slug'] ?? '';
|
||||
@@ -512,28 +523,24 @@ switch ($action) {
|
||||
|
||||
case 1:
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$content = $_POST['content'] ?? '';
|
||||
$content = str_replace("\r\n", "\n", $_POST['content'] ?? '');
|
||||
$title = extractMarkdownTitle($content) ?: ($draft['title'] ?? 'Sans titre');
|
||||
$postSlug = trim($_POST['slug'] ?? '');
|
||||
if ($title === '') {
|
||||
$errors[] = 'Le titre est obligatoire.';
|
||||
} else {
|
||||
if ($draft === null) {
|
||||
$uuid = $articles->create($title, $content, false, $postSlug, date('Y-m-d H:i:s'), currentUserEmail() ?? '', '', '', '', '', []);
|
||||
foreach ($_FILES['files']['tmp_name'] ?? [] as $_fi => $_tmpName) {
|
||||
if ($_FILES['files']['error'][$_fi] === UPLOAD_ERR_OK) {
|
||||
$articles->addFile($uuid, ['name' => $_FILES['files']['name'][$_fi], 'tmp_name' => $_tmpName, 'error' => UPLOAD_ERR_OK]);
|
||||
}
|
||||
if ($draft === null) {
|
||||
$uuid = $articles->create($title, $content, false, $postSlug, date('Y-m-d H:i:s'), currentUserEmail() ?? '', '', '', '', '', []);
|
||||
foreach ($_FILES['files']['tmp_name'] ?? [] as $_fi => $_tmpName) {
|
||||
if ($_FILES['files']['error'][$_fi] === UPLOAD_ERR_OK) {
|
||||
$articles->addFile($uuid, ['name' => $_FILES['files']['name'][$_fi], 'tmp_name' => $_tmpName, 'error' => UPLOAD_ERR_OK]);
|
||||
}
|
||||
$_SESSION['wizard_create'] = $uuid;
|
||||
} else {
|
||||
$articles->autosave($uuid, $title, $content, $postSlug);
|
||||
}
|
||||
header('Location: /new/' . rawurlencode($uuid) . '/2');
|
||||
exit;
|
||||
$_SESSION['wizard_create'] = $uuid;
|
||||
} else {
|
||||
$articles->autosave($uuid, $title, $content, $postSlug);
|
||||
}
|
||||
header('Location: /new/' . rawurlencode($uuid) . '/2');
|
||||
exit;
|
||||
}
|
||||
$title = $draft['title'] ?? ($_POST['title'] ?? '');
|
||||
$title = $draft['title'] ?? '';
|
||||
$content = $draft['content'] ?? '';
|
||||
$postSlug = $draft['slug'] ?? '';
|
||||
$existingFiles = $uuid !== '' ? $articles->getFiles($uuid) : [];
|
||||
@@ -865,15 +872,11 @@ switch ($action) {
|
||||
|
||||
case 1:
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$content = $_POST['content'] ?? '';
|
||||
if ($title === '') {
|
||||
$errors[] = 'Le titre est obligatoire.';
|
||||
} else {
|
||||
$articles->saveDraftOverlay($uuid, ['title' => $title, 'slug' => trim($_POST['slug'] ?? $draft['slug'])], $content);
|
||||
header('Location: /edit/' . rawurlencode($uuid) . '/2');
|
||||
exit;
|
||||
}
|
||||
$content = str_replace("\r\n", "\n", $_POST['content'] ?? '');
|
||||
$title = extractMarkdownTitle($content) ?: ($draft['title'] ?? 'Sans titre');
|
||||
$articles->saveDraftOverlay($uuid, ['title' => $title, 'slug' => trim($_POST['slug'] ?? $draft['slug'] ?? '')], $content);
|
||||
header('Location: /edit/' . rawurlencode($uuid) . '/2');
|
||||
exit;
|
||||
}
|
||||
$title = $draft['title'];
|
||||
$content = $draft['content'];
|
||||
@@ -1465,15 +1468,13 @@ switch ($action) {
|
||||
echo json_encode(['ok' => false]);
|
||||
exit;
|
||||
}
|
||||
$asTitle = trim($_POST['title'] ?? '');
|
||||
$asContent = $_POST['content'] ?? '';
|
||||
$asContent = str_replace("\r\n", "\n", $_POST['content'] ?? '');
|
||||
$asSlug = trim($_POST['slug'] ?? '');
|
||||
if ($asTitle === '') {
|
||||
echo json_encode(['ok' => false]);
|
||||
exit;
|
||||
}
|
||||
$_asCurrent = $articles->getByUuid($uuid);
|
||||
$asTitle = extractMarkdownTitle($asContent) ?: ($_asCurrent['title'] ?? 'Sans titre');
|
||||
$ok = $articles->autosave($uuid, $asTitle, $asContent, $asSlug);
|
||||
echo json_encode(['ok' => $ok, 'time' => date('H:i:s')]);
|
||||
$_asSlugFinal = $ok ? ($articles->getByUuid($uuid)['slug'] ?? '') : '';
|
||||
echo json_encode(['ok' => $ok, 'time' => date('H:i:s'), 'title' => $asTitle, 'slug' => $_asSlugFinal]);
|
||||
exit;
|
||||
|
||||
case 'autosave_draft':
|
||||
@@ -1488,14 +1489,12 @@ switch ($action) {
|
||||
echo json_encode(['ok' => false]);
|
||||
exit;
|
||||
}
|
||||
$_adTitle = trim($_POST['title'] ?? '');
|
||||
$_adContent = $_POST['content'] ?? null;
|
||||
if ($_adTitle === '') {
|
||||
echo json_encode(['ok' => false]);
|
||||
exit;
|
||||
}
|
||||
$_adContent = isset($_POST['content']) ? str_replace("\r\n", "\n", $_POST['content']) : null;
|
||||
$_adTitle = $_adContent !== null
|
||||
? (extractMarkdownTitle($_adContent) ?: ($_adArticle['title'] ?? 'Sans titre'))
|
||||
: ($_adArticle['title'] ?? 'Sans titre');
|
||||
$articles->saveDraftOverlay($uuid, ['title' => $_adTitle], $_adContent);
|
||||
echo json_encode(['ok' => true, 'time' => date('H:i:s')]);
|
||||
echo json_encode(['ok' => true, 'time' => date('H:i:s'), 'title' => $_adTitle]);
|
||||
exit;
|
||||
|
||||
case 'edit_discard_draft':
|
||||
@@ -2733,6 +2732,42 @@ switch ($action) {
|
||||
header('Location: /admin/users');
|
||||
exit;
|
||||
|
||||
case 'run_content_migrations':
|
||||
requireAuth();
|
||||
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
$_cmDataDir = BASE_PATH . '/data';
|
||||
$_cmTrack = $_cmDataDir . '/.content_migrations.json';
|
||||
$_cmFlag = $_cmDataDir . '/.maintenance';
|
||||
$_cmApplied = file_exists($_cmTrack) ? (json_decode((string) file_get_contents($_cmTrack), true) ?? []) : [];
|
||||
$_cmFiles = glob(BASE_PATH . '/scripts/content/migration_*.php') ?: [];
|
||||
sort($_cmFiles);
|
||||
$_cmPending = array_values(array_filter($_cmFiles, fn ($f) => !isset($_cmApplied[basename($f)])));
|
||||
if (empty($_cmPending)) {
|
||||
header('Location: /admin?tab=dashboard¬ice=no_migrations');
|
||||
exit;
|
||||
}
|
||||
file_put_contents($_cmFlag, date('Y-m-d H:i:s'));
|
||||
$_cmErrors = 0;
|
||||
$dataDir = $_cmDataDir;
|
||||
foreach ($_cmPending as $_cmFile) {
|
||||
try {
|
||||
require $_cmFile;
|
||||
$_cmApplied[basename($_cmFile)] = date('Y-m-d H:i:s');
|
||||
file_put_contents($_cmTrack, json_encode($_cmApplied, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n");
|
||||
} catch (Throwable $_cmEx) {
|
||||
$_cmErrors++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (file_exists($_cmFlag)) {
|
||||
unlink($_cmFlag);
|
||||
}
|
||||
header('Location: /admin?tab=dashboard¬ice=' . ($_cmErrors ? 'migration_error' : 'migrated'));
|
||||
exit;
|
||||
|
||||
case 'admin_save_site':
|
||||
requireAuth();
|
||||
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1.2.0
|
||||
Reference in New Issue
Block a user