feat : DATA_PATH configurable, DataGit auto-commit, UpdateChecker branche (v1.4.0)

- DATA_PATH : chemin /data hors document root, configurable via .env
  (fallback sur BASE_PATH/data si absent)
- DataGit : auto-commit git sur toutes les écritures articles/livres
  (create, update, delete, meta, tags, fichiers, liens…) sauf autosave
- UpdateChecker : getBranch() / getLastChecked() / clearCache(),
  branche configurable via FOLIO_UPDATE_BRANCH (plus de main hardcodé)
- Admin dashboard : affiche la branche suivie, date du dernier contrôle,
  bouton Vérifier pour forcer le check sans attendre le TTL
- CLAUDE.md : architecture DATA_PATH et flux de déploiement documentés

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 09:17:55 +02:00
parent 55a2120be1
commit 16965ee8cb
18 changed files with 236 additions and 37 deletions
+52 -11
View File
@@ -9,7 +9,7 @@ class ArticleManager
private ?array $allCache = null;
private ?array $searchIndexCache = null;
public function __construct(private string $dataDir)
public function __construct(private string $dataDir, private ?DataGit $git = null)
{
}
@@ -132,11 +132,12 @@ class ArticleManager
file_put_contents($dir . '/index.md', ltrim($content));
$this->rebuildSearchIndex();
$this->rebuildBacklinksCache();
$this->git?->commit("add: $title");
return $uuid;
}
public function update(string $uuid, string $title, string $content, bool $published, string $slug, string $publishedAt, string $revisionComment = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = '', string $category = '', ?array $tags = null): void
public function update(string $uuid, string $title, string $content, bool $published, string $slug, string $publishedAt, string $revisionComment = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = '', string $category = '', ?array $tags = null, bool $skipGit = false): void
{
$article = $this->getByUuid($uuid);
if (!$article) {
@@ -199,6 +200,9 @@ class ArticleManager
file_put_contents($dir . '/index.md', ltrim($content));
$this->rebuildSearchIndex();
$this->rebuildBacklinksCache();
if (!$skipGit) {
$this->git?->commit("update: $title");
}
}
public function autosave(string $uuid, string $title, string $content, string $slug): bool
@@ -247,6 +251,7 @@ class ArticleManager
}
$meta['updated_at'] = date('Y-m-d H:i:s');
$this->writeMeta($dir, $meta);
$this->git?->commit("meta: " . ($meta['title'] ?? $uuid));
}
public function saveDraftOverlay(string $uuid, array $metaFields, ?string $content = null): void
@@ -269,6 +274,9 @@ class ArticleManager
if ($content !== null) {
file_put_contents($dir . '/draft_overlay.md', $content);
}
$raw2 = @file_get_contents($dir . '/meta.json');
$title = is_string($raw2) ? (json_decode($raw2, true)['title'] ?? $uuid) : $uuid;
$this->git?->commit("draft: $title");
}
public function getDraftOverlay(string $uuid): ?array
@@ -315,14 +323,22 @@ class ArticleManager
return file_exists($this->dataDir . '/' . $uuid . '/draft_overlay.json');
}
public function discardDraftOverlay(string $uuid): void
public function discardDraftOverlay(string $uuid, bool $skipGit = false): void
{
if (!$this->isValidUuid($uuid)) {
return;
}
$dir = $this->dataDir . '/' . $uuid;
$dir = $this->dataDir . '/' . $uuid;
$title = null;
if (!$skipGit && $this->git !== null) {
$raw = @file_get_contents($dir . '/meta.json');
$title = is_string($raw) ? (json_decode($raw, true)['title'] ?? $uuid) : $uuid;
}
@unlink($dir . '/draft_overlay.json');
@unlink($dir . '/draft_overlay.md');
if ($title !== null) {
$this->git?->commit("discard-draft: $title");
}
}
public function commitDraftOverlay(string $uuid, string $revisionComment = ''): void
@@ -331,9 +347,10 @@ class ArticleManager
if (!$draft) {
return;
}
$title = $draft['title'];
$this->update(
$uuid,
$draft['title'],
$title,
$draft['content'],
(bool)$draft['published'],
$draft['slug'] ?? '',
@@ -343,12 +360,14 @@ class ArticleManager
$draft['seo_description'] ?? '',
$draft['og_image'] ?? '',
$draft['category'] ?? '',
$draft['tags'] ?? []
$draft['tags'] ?? [],
true // skipGit — commit unique ci-dessous
);
$this->discardDraftOverlay($uuid);
$this->discardDraftOverlay($uuid, skipGit: true);
$this->git?->commit("publish: $title");
}
public function addFileMeta(string $uuid, string $filename, string $author, string $sourceUrl, string $title = '', array $extraMeta = []): void
public function addFileMeta(string $uuid, string $filename, string $author, string $sourceUrl, string $title = '', array $extraMeta = [], bool $skipGit = false): void
{
if (!$this->isValidUuid($uuid)) {
return;
@@ -377,6 +396,9 @@ class ArticleManager
}
$meta['files_meta'][$filename] = $entry;
$this->writeMeta($this->dataDir . '/' . $uuid, $meta);
if (!$skipGit) {
$this->git?->commit("file-meta: {$uuid}/{$filename}");
}
}
public function setCover(string $uuid, string $filename): void
@@ -424,6 +446,7 @@ class ArticleManager
}
$meta['cover'] = $coverName;
$this->writeMeta($this->dataDir . '/' . $uuid, $meta);
$this->git?->commit("cover: " . ($article['title'] ?? $uuid));
}
public function addFileFromUrl(string $uuid, string $url, bool $isCover = false, string $author = '', string $sourceUrl = '', string $title = '', array $extraMeta = []): ?string
@@ -499,7 +522,7 @@ class ArticleManager
rename($tmp, $filesDir . '/' . $filename);
if ($author !== '' || $sourceUrl !== '' || $title !== '' || !empty($extraMeta)) {
$this->addFileMeta($uuid, $filename, $author, $sourceUrl, $title, $extraMeta);
$this->addFileMeta($uuid, $filename, $author, $sourceUrl, $title, $extraMeta, skipGit: true);
}
if ($isCover && $isImage) {
@@ -513,6 +536,7 @@ class ArticleManager
}
}
$this->git?->commit("add-file: {$uuid}/{$filename}");
return $filename;
}
@@ -553,6 +577,7 @@ class ArticleManager
$meta['external_links'][] = $entry;
$this->writeMeta($dir, $meta);
$this->rebuildBacklinksCache();
$this->git?->commit("link: {$uuid}");
return true;
}
@@ -583,6 +608,7 @@ class ArticleManager
return false;
}
$this->writeMeta($dir, $meta);
$this->git?->commit("link-meta: {$uuid}");
return true;
}
@@ -606,6 +632,7 @@ class ArticleManager
));
$this->writeMeta($dir, $meta);
$this->rebuildBacklinksCache();
$this->git?->commit("unlink: {$uuid}");
return true;
}
@@ -623,7 +650,7 @@ class ArticleManager
return $cats;
}
public function renameCategory(string $old, string $new): void
public function renameCategory(string $old, string $new, bool $skipGit = false): void
{
if (!is_dir($this->dataDir)) {
return;
@@ -647,11 +674,15 @@ class ArticleManager
$meta['category'] = $new;
$this->writeMeta($this->dataDir . '/' . $entry, $meta);
}
if (!$skipGit) {
$this->git?->commit("rename-cat: $old$new");
}
}
public function deleteCategory(string $name): void
{
$this->renameCategory($name, '');
$this->renameCategory($name, '', skipGit: true);
$this->git?->commit("delete-cat: $name");
}
public function getPrivateCategories(): array
@@ -676,6 +707,7 @@ class ArticleManager
$this->dataDir . '/private_cats.json',
json_encode(array_values($cats), JSON_UNESCAPED_UNICODE)
);
$this->git?->commit("private-cat: $cat");
}
// ─── Tag types ──────────────────────────────────────────────────────────────
@@ -701,6 +733,7 @@ class ArticleManager
$this->tagTypesPath(),
json_encode($types, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"
);
$this->git?->commit("tag-types");
}
/** Enregistre les tags d'un article directement (utile pour les scripts de migration). */
@@ -720,6 +753,7 @@ class ArticleManager
$meta['tags'] = $this->normalizeTags($tags);
$this->writeMeta($dir, $meta);
$this->rebuildSearchIndex();
$this->git?->commit("tags: " . ($meta['title'] ?? $uuid));
}
/** @return list<string> Toutes les valeurs distinctes d'un type de tag, triées. */
@@ -769,6 +803,7 @@ class ArticleManager
$this->writeMeta($dir, $meta);
$this->allCache = null;
@unlink($this->articleCachePath($uuid));
$this->git?->commit("featured: " . ($meta['title'] ?? $uuid) . " (" . ($featured ? 'on' : 'off') . ")");
}
public function delete(string $uuid): void
@@ -777,6 +812,11 @@ class ArticleManager
return;
}
$dir = $this->dataDir . '/' . $uuid;
$title = null;
if ($this->git !== null && is_dir($dir)) {
$raw = @file_get_contents($dir . '/meta.json');
$title = is_string($raw) ? (json_decode($raw, true)['title'] ?? null) : null;
}
if (is_dir($dir)) {
$this->allCache = null;
@unlink($this->articleCachePath($uuid));
@@ -785,6 +825,7 @@ class ArticleManager
}
$this->rebuildSearchIndex();
$this->rebuildBacklinksCache();
$this->git?->commit("delete: " . ($title ?? $uuid));
}
// ------------------------------------------------------------------ //