diff --git a/.env.example b/.env.example index 55820d9..478ec7b 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,11 @@ SMTP_FROM_NAME= CONTACT_EMAIL= CONTACT_FROM_EMAIL= +# Chemin absolu vers le répertoire des articles (data/) +# Par défaut : BASE_PATH/data (dans le répertoire de l'application) +# Recommandé en production : chemin hors du répertoire web, ex. /srv/data/folio +DATA_PATH=/srv/data/folio + # Logs Apache (onglet Recherches dans /admin) # Nom du fichier de log d'accès du vhost dans /var/log/apache2/ APACHE_ACCESS_LOG=lan.acegrp.varlog-access.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f01fc..e217f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag --- +## [1.4.0] - 2026-05-15 + +### Ajouté +- **`DATA_PATH`** : chemin des articles configurable via `.env`, indépendant du document root — permet de stocker `/data` hors de l'arborescence web (ex. `/srv/data/folio`) +- **`DataGit`** : auto-commit git sur toutes les écritures articles et livres (création, modification, suppression, métadonnées, tags, fichiers, liens…) sauf `autosave` — no-op silencieux si `DATA_PATH` n'est pas un dépôt git +- **Admin — Moteur Folio** : affiche la branche suivie pour les mises à jour (`FOLIO_UPDATE_BRANCH`, défaut `main`), la date du dernier contrôle, et un bouton **Vérifier** pour forcer la vérification sans attendre le TTL du cache (1 h) + +### Modifié +- `UpdateChecker` : branche cible configurable via `FOLIO_UPDATE_BRANCH` (plus de `main` hardcodé dans l'URL Gitea) + +--- + ## [1.3.0] - 2026-05-15 ### Ajouté diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c68c7ee --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,62 @@ +# CLAUDE.md + +## Ce qu'est ce dépôt + +**Folio** est un moteur de blog PHP. +Ce répertoire est la **copie locale du dépôt Git** (`https://git.abonnel.fr/cedricAbonnel/folio`), branche DEV. +Il contient uniquement le code du moteur — pas de données, pas de credentials. + +## Architecture + +| Répertoire local | Site distant | Rôle | +|-----------------|-------------|------| +| `~/Projects/folio/` | — | Copie du dépôt Folio (branche DEV). On code ici. | +| `~/Projects/varlog/` | varlog.a5l.fr | Sync bidirectionnelle des articles varlog. Sert de site de test pour le moteur. | +| `~/Projects/fr.abonnel.www/` | www.abonnel.fr | Sync bidirectionnelle des articles abonnel.fr. A aussi servi au déploiement initial. | + +**abonnel.fr** utilise Folio mais se met à jour seul via son UpdateChecker interne (vérifie `version.txt` sur Gitea). Aucune action manuelle nécessaire côté serveur. + +## Articles (`data/`) + +Les articles ne sont pas versionnés dans ce dépôt. Ils ont leur propre git local dans chaque workspace site (`~/Projects/varlog/data/`, `~/Projects/fr.abonnel.www/data/`), synchronisé de façon bidirectionnelle avec le serveur distant. + +## Modifier le moteur + +Pour toute correction ou fonctionnalité : **créer un ticket et une PR**. + +1. Coder ici dans `~/Projects/folio/` (branche feature) +2. **Tester sur varlog.a5l.fr** : + ```bash + ~/Projects/varlog/scripts/sync.sh + # puis tester sur http://varlog.acegrp.lan + ``` +3. Une fois validé, ouvrir une PR sur Gitea. Le commit doit inclure : + - `public/version.txt` (bump semver) + - `CHANGELOG.md` (entrée `### Ajouté / Corrigé / Modifié`) +4. Merger la PR → abonnel.fr se met à jour automatiquement. + +## Données articles (`DATA_PATH`) + +Les articles sont stockés dans un répertoire **hors du dépôt Folio**, configurable via `DATA_PATH` dans `.env`. + +| Environnement | Chemin local | Chemin serveur | +|--------------|-------------|----------------| +| varlog | `~/Projects/varlog-data/` | `/srv/data/folio` | +| abonnel.fr | `~/Projects/fr.abonnel.www-data/` | `/srv/data/folio` | + +Les scripts de sync (`pull-data.sh`, `push-data.sh`, `sync.sh`) utilisent `DATA_DIR` (overridable via env) pointant vers ces chemins locaux. + +## Asymétrie de déploiement moteur + +| Site | Mécanisme | Raison | +|------|-----------|--------| +| varlog (test) | rsync depuis `~/Projects/folio/` | Itération rapide, pas de contrainte de stabilité | +| abonnel.fr (prod) | `git pull origin main` sur le serveur | Contrôle via PR/merge, UpdateChecker autonome | + +Pour initialiser git sur un serveur abonnel.fr déployé via rsync : `scripts/git-init-remote.sh` + +## Ne pas mettre ici + +- `.env` (credentials → dans chaque workspace site) +- `data/` (articles → dans chaque workspace site) +- `vendor/` (non versionné) diff --git a/bootstrap.php b/bootstrap.php index 903574b..adc19e1 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -6,6 +6,12 @@ if (!defined('BASE_PATH')) { define('BASE_PATH', __DIR__); } +if (!defined('DATA_PATH')) { + $__dataPath = $_ENV['DATA_PATH'] ?? getenv('DATA_PATH') ?: ''; + define('DATA_PATH', $__dataPath !== '' ? rtrim($__dataPath, '/') : BASE_PATH . '/data'); + unset($__dataPath); +} + if (session_status() === PHP_SESSION_NONE) { $isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; $sessionName = $_ENV['SESSION_NAME'] ?? (getenv('SESSION_NAME') ?: null); diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php index 647c135..23e30ae 100644 --- a/phpstan-bootstrap.php +++ b/phpstan-bootstrap.php @@ -3,3 +3,4 @@ declare(strict_types=1); define('BASE_PATH', __DIR__); +define('DATA_PATH', BASE_PATH . '/data'); diff --git a/public/feed.php b/public/feed.php index 302820e..2b12f95 100644 --- a/public/feed.php +++ b/public/feed.php @@ -12,7 +12,7 @@ require_once BASE_PATH . '/src/Parsedown.php'; const FEED_PAGE_SIZE = 20; -$articles = new ArticleManager(BASE_PATH . '/data'); +$articles = new ArticleManager(DATA_PATH); $privateCats = $articles->getPrivateCategories(); $Parsedown = new Parsedown(); diff --git a/public/file.php b/public/file.php index 7dbb4ff..b9b947a 100644 --- a/public/file.php +++ b/public/file.php @@ -20,7 +20,7 @@ if ($name === '' || $name[0] === '.') { exit; } -$path = BASE_PATH . '/data/' . $uuid . '/files/' . $name; +$path = DATA_PATH . '/' . $uuid . '/files/' . $name; if (!is_file($path)) { http_response_code(404); diff --git a/public/index.php b/public/index.php index e74687b..4b2b797 100644 --- a/public/index.php +++ b/public/index.php @@ -24,12 +24,14 @@ require_once BASE_PATH . '/src/auth.php'; require_once BASE_PATH . '/src/SiteSettings.php'; require_once BASE_PATH . '/src/ArticleManager.php'; require_once BASE_PATH . '/src/BookManager.php'; +require_once BASE_PATH . '/src/DataGit.php'; -$articles = new ArticleManager(BASE_PATH . '/data'); -$books = new BookManager(BASE_PATH . '/data/books'); +$_dataGit = new DataGit(DATA_PATH); +$articles = new ArticleManager(DATA_PATH, $_dataGit); +$books = new BookManager(DATA_PATH . '/books', $_dataGit); // ─── Mode maintenance ────────────────────────────────────────────────────── -if (file_exists(BASE_PATH . '/data/.maintenance')) { +if (file_exists(DATA_PATH . '/.maintenance')) { http_response_code(503); header('Retry-After: 60'); include BASE_PATH . '/templates/maintenance.php'; @@ -37,7 +39,7 @@ if (file_exists(BASE_PATH . '/data/.maintenance')) { } require_once BASE_PATH . '/src/UpdateChecker.php'; -$_updateChecker = new UpdateChecker(BASE_PATH . '/data', BASE_PATH); +$_updateChecker = new UpdateChecker(DATA_PATH, BASE_PATH); $action = $_GET['action'] ?? 'list'; $uuid = $_GET['uuid'] ?? ''; @@ -78,7 +80,7 @@ function searchAndRedirect(string $rawPath, ArticleManager $articles): void // ─── Pages statiques depuis data/site/ ────────────────────────────────────── function loadSitePageData(string $slug): array { - $base = BASE_PATH . '/data/site'; + $base = DATA_PATH . '/site'; $meta = []; $raw = @file_get_contents($base . '/' . $slug . '.json'); if ($raw !== false) { @@ -1383,7 +1385,7 @@ switch ($action) { case 'flux': require_once BASE_PATH . '/src/FeedFetcher.php'; - $fetcher = new FeedFetcher(BASE_PATH . '/data/_cache/feeds'); + $fetcher = new FeedFetcher(DATA_PATH . '/_cache/feeds'); $fluxItems = []; $pdo = dbPdo(); if ($pdo) { @@ -1535,8 +1537,8 @@ switch ($action) { echo json_encode(['ok' => false, 'error' => 'Paramètres invalides']); exit; } - $cfSrc = BASE_PATH . '/data/' . $cfFrom . '/files/' . $cfName; - $cfDstDir = BASE_PATH . '/data/' . $cfTo . '/files'; + $cfSrc = DATA_PATH . '/' . $cfFrom . '/files/' . $cfName; + $cfDstDir = DATA_PATH . '/' . $cfTo . '/files'; $cfDst = $cfDstDir . '/' . $cfName; if (!file_exists($cfSrc)) { echo json_encode(['ok' => false, 'error' => 'Fichier source introuvable']); @@ -1649,7 +1651,7 @@ switch ($action) { // Capture d'écran pour prévisualisation (pages HTML uniquement, URL externes uniquement) $step2Screenshot = null; if (!$step2IsInternal && str_starts_with($step2Meta['mime'] ?? '', 'text/html')) { - $filesDir = BASE_PATH . '/data/' . $uuid . '/files'; + $filesDir = DATA_PATH . '/' . $uuid . '/files'; if (!is_dir($filesDir)) { mkdir($filesDir, 0755, true); } @@ -1725,7 +1727,7 @@ switch ($action) { header('Location: /import/' . rawurlencode($urlUuid) . '?error=1'); exit; } - $filesDir = BASE_PATH . '/data/' . $urlUuid . '/files'; + $filesDir = DATA_PATH . '/' . $urlUuid . '/files'; $previewPath = $filesDir . '/' . $screenshotFile; if (!file_exists($previewPath)) { header('Location: /import/' . rawurlencode($urlUuid) . '?error=1'); @@ -1744,7 +1746,7 @@ switch ($action) { } if ($mode === 'link') { - $filesDir = BASE_PATH . '/data/' . $urlUuid . '/files'; + $filesDir = DATA_PATH . '/' . $urlUuid . '/files'; if (!is_dir($filesDir)) { mkdir($filesDir, 0755, true); } @@ -1895,7 +1897,7 @@ switch ($action) { $done = $fail = $skip = 0; foreach ($articles->getAll() as $article) { $artUuid = $article['uuid']; - $filesDir = BASE_PATH . '/data/' . $artUuid . '/files'; + $filesDir = DATA_PATH . '/' . $artUuid . '/files'; foreach ($article['external_links'] ?? [] as $link) { $lMeta = $link['meta'] ?? []; $lMime = $lMeta['mime'] ?? 'text/html'; @@ -2784,7 +2786,7 @@ switch ($action) { http_response_code(403); exit; } - $_cmDataDir = BASE_PATH . '/data'; + $_cmDataDir = DATA_PATH; $_cmTrack = $_cmDataDir . '/.content_migrations.json'; $_cmFlag = $_cmDataDir . '/.maintenance'; $_cmApplied = file_exists($_cmTrack) ? (json_decode((string) file_get_contents($_cmTrack), true) ?? []) : []; @@ -2814,6 +2816,16 @@ switch ($action) { header('Location: /admin?tab=dashboard¬ice=' . ($_cmErrors ? 'migration_error' : 'migrated')); exit; + case 'force_update_check': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + $_updateChecker->clearCache(); + header('Location: /admin?tab=dashboard'); + exit; + case 'admin_save_site': requireAuth(); if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { diff --git a/public/sitemap.php b/public/sitemap.php index 5ed94f9..a1a4e2d 100644 --- a/public/sitemap.php +++ b/public/sitemap.php @@ -8,7 +8,7 @@ require_once BASE_PATH . '/src/helpers.php'; require_once BASE_PATH . '/config/config.php'; require_once BASE_PATH . '/src/ArticleManager.php'; -$articles = new ArticleManager(BASE_PATH . '/data'); +$articles = new ArticleManager(DATA_PATH); $privateCats = $articles->getPrivateCategories(); $published = array_filter($articles->getAll(true), static function (array $a) use ($privateCats): bool { diff --git a/public/version.txt b/public/version.txt index f0bb29e..88c5fb8 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -1.3.0 +1.4.0 diff --git a/src/ArticleManager.php b/src/ArticleManager.php index 769e235..a69cbe7 100644 --- a/src/ArticleManager.php +++ b/src/ArticleManager.php @@ -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 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)); } // ------------------------------------------------------------------ // diff --git a/src/BookManager.php b/src/BookManager.php index d62b2cf..6d6749c 100644 --- a/src/BookManager.php +++ b/src/BookManager.php @@ -4,7 +4,7 @@ declare(strict_types=1); class BookManager { - public function __construct(private string $booksDir) + public function __construct(private string $booksDir, private ?DataGit $git = null) { } @@ -95,14 +95,17 @@ class BookManager $this->bookPath($slug), json_encode($book, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n" ); + $this->git?->commit("book: " . ($book['title'] ?? $slug)); } public function delete(string $slug): void { - $path = $this->bookPath($slug); + $title = $this->getBySlug($slug)['title'] ?? $slug; + $path = $this->bookPath($slug); if (file_exists($path)) { @unlink($path); } + $this->git?->commit("delete-book: $title"); } // ------------------------------------------------------------------ // diff --git a/src/DataGit.php b/src/DataGit.php new file mode 100644 index 0000000..c3ab886 --- /dev/null +++ b/src/DataGit.php @@ -0,0 +1,22 @@ +dataDir . '/.git')) { + return; + } + $dir = escapeshellarg($this->dataDir); + $msg = escapeshellarg($message); + shell_exec("git -C $dir add -A 2>/dev/null"); + exec("git -C $dir diff --cached --quiet 2>/dev/null", $_, $rc); + if ($rc !== 0) { + shell_exec("git -C $dir commit -m $msg 2>/dev/null"); + } + } +} diff --git a/src/SiteSettings.php b/src/SiteSettings.php index 13da22a..37d187b 100644 --- a/src/SiteSettings.php +++ b/src/SiteSettings.php @@ -4,7 +4,7 @@ declare(strict_types=1); function siteSettingsPath(): string { - return BASE_PATH . '/data/site_settings.json'; + return DATA_PATH . '/site_settings.json'; } function siteSettings(): array diff --git a/src/SmtpSettings.php b/src/SmtpSettings.php index a4b0765..f0857c3 100644 --- a/src/SmtpSettings.php +++ b/src/SmtpSettings.php @@ -4,7 +4,7 @@ declare(strict_types=1); function smtpSettingsPath(): string { - return BASE_PATH . '/data/smtp_settings.json'; + return DATA_PATH . '/smtp_settings.json'; } function smtpSettings(): array diff --git a/src/UpdateChecker.php b/src/UpdateChecker.php index d46643d..3962b4b 100644 --- a/src/UpdateChecker.php +++ b/src/UpdateChecker.php @@ -89,8 +89,31 @@ class UpdateChecker return version_compare($remoteVer, $deployedVer, '>') ? $remoteVer : null; } + public function getBranch(): string + { + return (string) ($_ENV['FOLIO_UPDATE_BRANCH'] ?? getenv('FOLIO_UPDATE_BRANCH') ?: 'main'); + } + + public function getLastChecked(): ?int + { + $cacheFile = $this->dataDir . '/.version_check_cache.json'; + if (!file_exists($cacheFile)) { + return null; + } + $cache = json_decode((string) file_get_contents($cacheFile), true) ?? []; + return isset($cache['fetched_at']) ? (int) $cache['fetched_at'] : null; + } + + public function clearCache(): void + { + $cacheFile = $this->dataDir . '/.version_check_cache.json'; + if (file_exists($cacheFile)) { + unlink($cacheFile); + } + } + /** - * Récupère `public/version.txt` depuis le dépôt Gitea (branche main). + * Récupère `public/version.txt` depuis le dépôt Gitea. * Résultat mis en cache 1 h dans `data/.version_check_cache.json`. */ private function fetchRemoteVersion(string $repoUrl): ?string @@ -107,8 +130,9 @@ class UpdateChecker } } - // URL du fichier brut : {repo}/raw/branch/main/public/version.txt - $rawUrl = $repoUrl . '/raw/branch/main/public/version.txt'; + $branch = $this->getBranch(); + // URL du fichier brut : {repo}/raw/branch/{branch}/public/version.txt + $rawUrl = $repoUrl . '/raw/branch/' . $branch . '/public/version.txt'; $token = (string) ($_ENV['GITEA_TOKEN'] ?? getenv('GITEA_TOKEN') ?: ''); $opts = [ diff --git a/templates/admin.php b/templates/admin.php index ff5bd77..73c19da 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -104,6 +104,8 @@ function adminStatusBadge(array $a, int $now): string $_deployedVer = trim((string) @file_get_contents(BASE_PATH . '/public/version.txt')); $_deployedLabel = $_deployedVer !== '' ? $_deployedVer : '—'; $_notices = isset($_updateChecker) ? $_updateChecker->adminNotices() : []; + $_branch = isset($_updateChecker) ? $_updateChecker->getBranch() : 'main'; + $_lastChecked = isset($_updateChecker) ? $_updateChecker->getLastChecked() : null; $_remoteLabel = '—'; foreach ($_notices as $_n) { if ($_n['type'] === 'info' && preg_match('/v([\d]+\.[\d]+\.[\d]+)/', $_n['message'], $_m)) { @@ -122,7 +124,16 @@ function adminStatusBadge(array $a, int $now): string Dernière version disponible - Mise à jour disponible' : '' ?> + + Mise à jour disponible' : '' ?> +
+ +
+ + + + Branche suivie + · vérifié le ' . date('d/m/Y à H:i', $_lastChecked) . '' : '' ?> diff --git a/templates/import_image_step2.php b/templates/import_image_step2.php index 29df54d..03cbd3a 100644 --- a/templates/import_image_step2.php +++ b/templates/import_image_step2.php @@ -60,7 +60,7 @@ $preSource = $step2Meta['canonical'] ?? $step2Meta['source'] ?? $step2Url;

Aperçu de la page