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
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
// Migration 001 : préfixe chaque article avec `# Titre` si aucun titre
// Markdown de niveau 1 n'est déjà présent dans le contenu.
//
// Variables disponibles depuis le runner : $dataDir
/** @var string $dataDir */
$updated = 0;
$skipped = 0;
foreach (glob($dataDir . '/*/meta.json') as $metaPath) {
$dir = dirname($metaPath);
$mdPath = $dir . '/index.md';
if (!file_exists($mdPath)) {
$skipped++;
continue;
}
$meta = json_decode((string) file_get_contents($metaPath), true);
if (!is_array($meta)) {
$skipped++;
continue;
}
$title = trim($meta['title'] ?? '');
$content = (string) file_get_contents($mdPath);
$content = str_replace("\r\n", "\n", $content);
// Déjà un titre Markdown niveau 1 → on ne touche pas
if (preg_match('/^\s*#\s+\S/', $content)) {
$skipped++;
continue;
}
if ($title === '') {
$skipped++;
continue;
}
file_put_contents($mdPath, '# ' . $title . "\n\n" . ltrim($content));
$updated++;
}
echo " $updated article(s) mis à jour, $skipped ignoré(s)\n";
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
// Runner de migrations de contenu (fichiers articles).
// Analogie avec database/migrate.php, mais pour les fichiers data/.
//
// Usage : php scripts/migrate_content.php [/chemin/vers/data]
//
// - Lit data/.content_migrations.json pour savoir ce qui a déjà tourné.
// - Active data/.maintenance pendant l'exécution (→ page 503 aux visiteurs).
// - Applique les scripts scripts/content/migration_*.php non encore appliqués.
$baseDir = dirname(__DIR__);
$dataDir = $argv[1] ?? ($baseDir . '/data');
if (!is_dir($dataDir)) {
fwrite(STDERR, "Répertoire data introuvable : $dataDir\n");
exit(1);
}
$trackFile = $dataDir . '/.content_migrations.json';
$maintenanceFlag = $dataDir . '/.maintenance';
$applied = [];
if (file_exists($trackFile)) {
$applied = json_decode((string) file_get_contents($trackFile), true) ?? [];
}
$files = glob(__DIR__ . '/content/migration_*.php') ?: [];
sort($files);
$pending = array_values(array_filter($files, fn ($f) => !isset($applied[basename($f)])));
if (empty($pending)) {
echo " (aucune migration de contenu en attente)\n";
exit(0);
}
file_put_contents($maintenanceFlag, date('Y-m-d H:i:s'));
echo "→ Mode maintenance activé\n";
$count = 0;
$errors = 0;
foreach ($pending as $file) {
$name = basename($file);
echo "$name ... ";
try {
require $file;
$applied[$name] = date('Y-m-d H:i:s');
file_put_contents(
$trackFile,
json_encode($applied, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"
);
echo "\n";
$count++;
} catch (Throwable $e) {
echo "" . $e->getMessage() . "\n";
$errors++;
break;
}
}
if (file_exists($maintenanceFlag)) {
unlink($maintenanceFlag);
}
echo "→ Mode maintenance désactivé\n";
echo "$count migration(s) appliquée(s)" . ($errors ? ", $errors erreur(s)" : '') . ".\n";
exit($errors > 0 ? 1 : 0);
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Pousse le code Folio vers git.abonnel.fr/cedricAbonnel/folio
# Usage : ./scripts/push.sh "message de commit"
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$SCRIPT_DIR/.."
MSG="${1:-}"
if [[ -z "$MSG" ]]; then
echo "Usage: $0 \"message de commit\""
exit 1
fi
cd "$ROOT"
if [ ! -d .git ]; then
git init -b main
git remote add origin https://git.abonnel.fr/cedricAbonnel/folio.git
echo "→ Dépôt git initialisé"
fi
# Extraire la version depuis CHANGELOG.md (première entrée ## [X.Y.Z])
FOLIO_VERSION=$(grep -m1 '^\#\# \[[0-9]' CHANGELOG.md | sed 's/.*\[\([^]]*\)\].*/\1/')
if [[ -z "$FOLIO_VERSION" ]]; then
echo "✗ Impossible d'extraire la version depuis CHANGELOG.md"
exit 1
fi
echo "$FOLIO_VERSION" > public/version.txt
echo "→ Version : $FOLIO_VERSION"
git add -A
git diff --cached --quiet && echo "(rien à committer)" && exit 0
git commit -m "$MSG"
git push origin main
echo "✓ Poussé vers folio"