diff --git a/.env.example b/.env.example index 3c93f6c..ae01e25 100644 --- a/.env.example +++ b/.env.example @@ -63,3 +63,13 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +# ── Mises à jour automatiques ───────────────────────────────────────────────── +# Serveur Gitea hébergeant les releases publiques +GITEA_URL=https://git.barbel.synology.me +GITEA_OWNER=CGL +GITEA_REPO=mesreleves-php +# Si true, la mise à jour est appliquée automatiquement lors de la vérification planifiée +AUTO_UPDATE=false +# Nombre de sauvegardes PostgreSQL conservées avant suppression +UPDATE_BACKUPS_TO_KEEP=5 diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/app/Console/Commands/ApplyUpdateCommand.php b/app/Console/Commands/ApplyUpdateCommand.php new file mode 100644 index 0000000..5cd3878 --- /dev/null +++ b/app/Console/Commands/ApplyUpdateCommand.php @@ -0,0 +1,60 @@ +getInstalledVersion(); + $this->line("Version installée : v{$installed}"); + + $this->line("Vérification des mises à jour..."); + $latest = $updates->fetchLatestRelease(useCache: false); + + if (! $latest) { + $this->error("Impossible de contacter le serveur de mises à jour."); + return self::FAILURE; + } + + if (! version_compare($latest['version'], $installed, '>')) { + $this->info("L'application est déjà à jour (v{$installed})."); + return self::SUCCESS; + } + + $this->info("Mise à jour disponible : v{$installed} → v{$latest['version']}"); + + if ($latest['body']) { + $this->line($latest['body']); + } + + if ($this->option('check')) { + return self::SUCCESS; + } + + if (! $this->option('force') && ! $this->confirm('Procéder à la mise à jour ?', true)) { + return self::SUCCESS; + } + + $this->line(""); + + try { + $updates->applyUpdate($latest, fn ($msg) => $this->line($msg)); + } catch (\Exception $e) { + $this->error("La mise à jour a échoué : " . $e->getMessage()); + $this->warn("Vérifiez les sauvegardes dans storage/app/backups/ pour une restauration manuelle."); + return self::FAILURE; + } + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/CheckUpdateCommand.php b/app/Console/Commands/CheckUpdateCommand.php new file mode 100644 index 0000000..0f01ce8 --- /dev/null +++ b/app/Console/Commands/CheckUpdateCommand.php @@ -0,0 +1,46 @@ +getInstalledVersion(); + $this->line("Version installée : v{$installed}"); + + $this->line("Interrogation du serveur de mises à jour..."); + $latest = $updates->fetchLatestRelease(useCache: ! $this->option('force')); + + if (! $latest) { + $this->warn("Impossible de contacter le serveur de mises à jour."); + return self::FAILURE; + } + + if (version_compare($latest['version'], $installed, '>')) { + $this->info("Nouvelle version disponible : v{$latest['version']}"); + if ($latest['body']) { + $this->line("Notes de version :"); + $this->line($latest['body']); + } + $this->line(""); + $this->line("Pour mettre à jour : php artisan app:update"); + + if (config('update.auto_update')) { + $this->line("AUTO_UPDATE=true détecté — application automatique..."); + $this->call('app:update', ['--force' => true]); + } + + return self::SUCCESS; + } + + $this->info("L'application est à jour (v{$installed})."); + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/RollbackCommand.php b/app/Console/Commands/RollbackCommand.php new file mode 100644 index 0000000..28f5be3 --- /dev/null +++ b/app/Console/Commands/RollbackCommand.php @@ -0,0 +1,65 @@ +listBackups(); + + if (empty($backups)) { + $this->warn("Aucune sauvegarde disponible dans storage/app/backups/."); + return self::FAILURE; + } + + // --list : afficher la liste et sortir + if ($this->option('list')) { + $rows = array_map(fn ($b) => [ + basename($b), + $this->humanSize(filesize($b)), + date('d/m/Y H:i:s', filemtime($b)), + ], $backups); + $this->table(['Fichier', 'Taille', 'Date'], $rows); + return self::SUCCESS; + } + + // Sélection interactive + $choices = array_map('basename', $backups); + $choice = $this->choice('Choisir la sauvegarde à restaurer :', $choices, 0); + $path = storage_path("app/backups/{$choice}"); + + $this->warn("Sauvegarde sélectionnée : {$choice}"); + $this->warn("ATTENTION : cette opération remplace la base de données actuelle."); + + if (! $this->confirm('Continuer ?')) { + $this->line("Annulé."); + return self::SUCCESS; + } + + try { + $updates->restoreBackup($path); + $this->info("Base de données restaurée avec succès."); + } catch (\Exception $e) { + $this->error("Restauration échouée : " . $e->getMessage()); + return self::FAILURE; + } + + return self::SUCCESS; + } + + private function humanSize(int $bytes): string + { + if ($bytes >= 1_048_576) return round($bytes / 1_048_576, 1) . ' Mo'; + if ($bytes >= 1_024) return round($bytes / 1_024, 0) . ' Ko'; + return $bytes . ' o'; + } +} diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index 16f184f..a26875b 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -7,12 +7,18 @@ use App\Http\Controllers\Controller; use App\Models\Releve; use App\Models\Source; use App\Models\User; +use App\Services\UpdateService; use Illuminate\View\View; class DashboardController extends Controller { - public function index(): View + public function index(UpdateService $updates): View { + $installedVersion = $updates->getInstalledVersion(); + $latestRelease = $updates->fetchLatestRelease(); // depuis le cache Redis + $updateAvailable = $latestRelease + && version_compare($latestRelease['version'], $installedVersion, '>'); + // Compteurs sources par statut $sourcesByStatus = Source::selectRaw('status, count(*) as total') ->groupBy('status') @@ -54,7 +60,8 @@ class DashboardController extends Controller return view('admin.dashboard', compact( 'sourcesByStatus', 'totalSources', 'totalReleves', 'usersByRole', 'totalUsers', - 'sourcesAValider', 'relevesRecents', 'activiteMensuelle' + 'sourcesAValider', 'relevesRecents', 'activiteMensuelle', + 'installedVersion', 'latestRelease', 'updateAvailable' )); } } diff --git a/app/Services/UpdateService.php b/app/Services/UpdateService.php new file mode 100644 index 0000000..db33362 --- /dev/null +++ b/app/Services/UpdateService.php @@ -0,0 +1,297 @@ +get($url); + + if (! $response->successful()) { + return null; + } + + $data = $response->json(); + $version = ltrim($data['tag_name'] ?? '', 'v'); + + $downloadUrl = null; + foreach ($data['assets'] ?? [] as $asset) { + if (str_ends_with($asset['name'] ?? '', '.tar.gz')) { + $downloadUrl = $asset['browser_download_url']; + break; + } + } + + if (! $version || ! $downloadUrl) { + return null; + } + + $release = [ + 'version' => $version, + 'tag' => $data['tag_name'], + 'name' => $data['name'] ?? "v{$version}", + 'download_url' => $downloadUrl, + 'published_at' => $data['published_at'] ?? null, + 'body' => $data['body'] ?? '', + ]; + + Cache::put(self::CACHE_KEY, $release, self::CACHE_TTL); + return $release; + + } catch (\Exception $e) { + Log::warning('UpdateService: impossible de contacter le serveur de mises à jour.', [ + 'error' => $e->getMessage(), + ]); + return null; + } + } + + public function getLatestIfNewer(): ?array + { + $latest = $this->fetchLatestRelease(); + if (! $latest) { + return null; + } + + return version_compare($latest['version'], $this->getInstalledVersion(), '>') ? $latest : null; + } + + // ─── Téléchargement ───────────────────────────────────────────────────── + + public function download(array $release): string + { + $path = sys_get_temp_dir() . "/mesreleves-{$release['version']}.tar.gz"; + + $response = Http::timeout(300)->sink($path)->get($release['download_url']); + + if (! $response->successful() || ! file_exists($path) || filesize($path) === 0) { + @unlink($path); + throw new RuntimeException("Téléchargement échoué (HTTP {$response->status()})."); + } + + return $path; + } + + // ─── Application de la mise à jour ────────────────────────────────────── + + public function applyUpdate(array $release, callable $log = null): void + { + $log ??= static fn ($msg) => null; + $version = $release['version']; + $appPath = base_path(); + + // 1. Sauvegarde BDD + $log("Sauvegarde de la base de données..."); + $backupPath = $this->backupDatabase($version); + $log(" → {$backupPath}"); + + // 2. Téléchargement + $log("Téléchargement de mesreleves-{$version}.tar.gz..."); + $archivePath = $this->download($release); + $log(" → {$archivePath} (" . $this->humanSize(filesize($archivePath)) . ")"); + + // 3. Maintenance + $log("Activation du mode maintenance..."); + Artisan::call('down'); + + try { + // 4. Extraction + $log("Extraction de l'archive..."); + $tmpDir = sys_get_temp_dir() . "/mesreleves-update-{$version}"; + if (is_dir($tmpDir)) { + $this->exec("rm -rf " . escapeshellarg($tmpDir)); + } + mkdir($tmpDir, 0755, true); + $this->exec("tar -xzf " . escapeshellarg($archivePath) . " -C " . escapeshellarg($tmpDir)); + + $dirs = glob($tmpDir . '/*/'); + if (empty($dirs)) { + throw new RuntimeException("L'archive est vide ou mal formée."); + } + $srcDir = rtrim($dirs[0], '/'); + + // 5. Synchronisation des fichiers (préserve .env, storage, vendor) + $log("Synchronisation des fichiers..."); + $this->exec(sprintf( + 'rsync -a --delete --exclude=".env" --exclude="storage/" --exclude="vendor/" %s %s', + escapeshellarg($srcDir . '/'), + escapeshellarg($appPath . '/'), + )); + + // 6. Dépendances Composer (prod) + $log("Installation des dépendances (composer)..."); + $this->exec('composer install --no-dev --optimize-autoloader --quiet', $appPath); + + // 7. Mise à jour du fichier VERSION + file_put_contents($appPath . '/VERSION', $version . PHP_EOL); + + // 8. Migrations + $log("Migration de la base de données..."); + Artisan::call('migrate', ['--force' => true]); + $log(trim(Artisan::output())); + + // 9. Nettoyage et optimisation des caches + $log("Optimisation..."); + Artisan::call('optimize:clear'); + Artisan::call('optimize'); + + // 10. Rechargement php-fpm (opcache) + $log("Rechargement php-fpm..."); + $this->reloadPhpFpm(); + + // 11. Purge des anciennes sauvegardes + $this->pruneBackups(); + + // 12. Nettoyage temporaire + $this->exec("rm -rf " . escapeshellarg($tmpDir)); + @unlink($archivePath); + + Cache::forget(self::CACHE_KEY); + $log("✓ Mise à jour vers v{$version} réussie."); + + } catch (\Exception $e) { + $log("✗ Erreur : " . $e->getMessage()); + $log(" La base de données peut être restaurée depuis : {$backupPath}"); + throw $e; + } finally { + Artisan::call('up'); + } + } + + // ─── Sauvegardes ──────────────────────────────────────────────────────── + + public function listBackups(): array + { + $dir = storage_path('app/backups'); + $backups = glob($dir . '/db-*.sql') ?: []; + rsort($backups); + return $backups; + } + + public function restoreBackup(string $path): void + { + if (! file_exists($path)) { + throw new RuntimeException("Fichier introuvable : {$path}"); + } + + $host = config('database.connections.pgsql.host'); + $port = config('database.connections.pgsql.port', 5432); + $db = config('database.connections.pgsql.database'); + $user = config('database.connections.pgsql.username'); + $password = config('database.connections.pgsql.password'); + + $cmd = sprintf( + 'PGPASSWORD=%s psql -h %s -p %s -U %s %s < %s', + escapeshellarg($password), + escapeshellarg($host), + (int) $port, + escapeshellarg($user), + escapeshellarg($db), + escapeshellarg($path), + ); + + Artisan::call('down'); + $this->exec($cmd); + Artisan::call('up'); + } + + // ─── Méthodes privées ─────────────────────────────────────────────────── + + private function backupDatabase(string $version): string + { + $dir = storage_path('app/backups'); + if (! is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $filename = "db-before-{$version}-" . date('Ymd-His') . ".sql"; + $path = "{$dir}/{$filename}"; + + $host = config('database.connections.pgsql.host'); + $port = config('database.connections.pgsql.port', 5432); + $db = config('database.connections.pgsql.database'); + $user = config('database.connections.pgsql.username'); + $password = config('database.connections.pgsql.password'); + + $cmd = sprintf( + 'PGPASSWORD=%s pg_dump -h %s -p %s -U %s %s > %s', + escapeshellarg($password), + escapeshellarg($host), + (int) $port, + escapeshellarg($user), + escapeshellarg($db), + escapeshellarg($path), + ); + + $this->exec($cmd); + + if (! file_exists($path) || filesize($path) === 0) { + throw new RuntimeException("La sauvegarde PostgreSQL a échoué (pg_dump disponible dans le container ?)"); + } + + return $path; + } + + private function pruneBackups(): void + { + $backups = $this->listBackups(); + $keep = (int) config('update.backups_to_keep', 5); + + foreach (array_slice($backups, $keep) as $old) { + @unlink($old); + } + } + + private function reloadPhpFpm(): void + { + // Envoie USR2 au process php-fpm (rechargement gracieux sans downtime) + // PID 1 = php-fpm master dans le container Docker + @shell_exec('kill -USR2 1 2>/dev/null || true'); + } + + private function exec(string $cmd, ?string $cwd = null): string + { + $fullCmd = $cwd ? "cd " . escapeshellarg($cwd) . " && {$cmd}" : $cmd; + $output = shell_exec("{$fullCmd} 2>&1") ?? ''; + return $output; + } + + private function humanSize(int $bytes): string + { + if ($bytes >= 1_048_576) return round($bytes / 1_048_576, 1) . ' Mo'; + if ($bytes >= 1_024) return round($bytes / 1_024, 0) . ' Ko'; + return $bytes . ' o'; + } +} diff --git a/bin/build-release.sh b/bin/build-release.sh new file mode 100755 index 0000000..8b19016 --- /dev/null +++ b/bin/build-release.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# MesRelevés — Script de construction d'une archive de release +# +# Usage : bin/build-release.sh +# +# Prérequis : node/npm (pour les assets), rsync, tar +# Produit : mesreleves-X.Y.Z.tar.gz + mesreleves-X.Y.Z.tar.gz.sha256 +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail + +# Se positionner à la racine du projet +cd "$(git rev-parse --show-toplevel)" + +VERSION=$(cat VERSION | tr -d '[:space:]') +ARCHIVE="mesreleves-${VERSION}.tar.gz" + +echo "──────────────────────────────────────────────" +echo " MesRelevés — Build de la release v${VERSION}" +echo "──────────────────────────────────────────────" + +# ── 1. Vérifications ────────────────────────────────────────────────────────── +if git diff --quiet HEAD 2>/dev/null && git diff --cached --quiet 2>/dev/null; then + : +else + echo "AVERTISSEMENT : des modifications non commitées seront incluses." +fi + +# ── 2. Build des assets frontend ───────────────────────────────────────────── +echo "→ Build des assets frontend (npm run build)..." +npm run build --silent + +# ── 3. Copie dans un répertoire temporaire (sans vendor, sans fichiers dev) ── +TMPDIR=$(mktemp -d) +BUILDDIR="${TMPDIR}/mesreleves-${VERSION}" +trap 'rm -rf "${TMPDIR}"' EXIT + +echo "→ Copie des fichiers dans ${BUILDDIR}..." +rsync -a \ + --exclude='.git/' \ + --exclude='.env' \ + --exclude='node_modules/' \ + --exclude='vendor/' \ + --exclude='storage/logs/' \ + --exclude='storage/framework/cache/' \ + --exclude='storage/framework/sessions/' \ + --exclude='storage/framework/views/' \ + --exclude='tests/' \ + --exclude='phpunit.xml' \ + --exclude='.editorconfig' \ + --exclude='.gitattributes' \ + --exclude='.gitignore' \ + --exclude='*.tar.gz' \ + --exclude='*.sha256' \ + --exclude='.phpunit.result.cache' \ + . "${BUILDDIR}/" + +# ── 4. Création de l'archive ───────────────────────────────────────────────── +echo "→ Création de l'archive ${ARCHIVE}..." +tar -czf "${ARCHIVE}" -C "${TMPDIR}" "mesreleves-${VERSION}/" + +# ── 5. Somme de contrôle SHA-256 ───────────────────────────────────────────── +sha256sum "${ARCHIVE}" > "${ARCHIVE}.sha256" +CHECKSUM=$(awk '{print $1}' "${ARCHIVE}.sha256") + +# ── Résumé ──────────────────────────────────────────────────────────────────── +SIZE=$(du -sh "${ARCHIVE}" | cut -f1) +echo "" +echo "✓ Archive créée : ${ARCHIVE} (${SIZE})" +echo " SHA-256 : ${CHECKSUM}" +echo "" +echo "Prochaines étapes :" +echo " 1. git tag v${VERSION} && git push origin v${VERSION}" +echo " 2. Créer une release sur Gitea (tag v${VERSION})" +echo " 3. Joindre ${ARCHIVE} et ${ARCHIVE}.sha256 à la release" diff --git a/config/update.php b/config/update.php new file mode 100644 index 0000000..c6ced2d --- /dev/null +++ b/config/update.php @@ -0,0 +1,23 @@ + env('GITEA_URL', 'https://git.barbel.synology.me'), + 'gitea_owner' => env('GITEA_OWNER', 'CGL'), + 'gitea_repo' => env('GITEA_REPO', 'mesreleves-php'), + + /* + * Si true, la mise à jour est téléchargée et appliquée automatiquement + * lors de la vérification quotidienne planifiée. + * Par défaut false : seule une notification admin est générée. + */ + 'auto_update' => env('AUTO_UPDATE', false), + + /* + * Nombre de sauvegardes PostgreSQL à conserver dans storage/app/backups/. + */ + 'backups_to_keep' => env('UPDATE_BACKUPS_TO_KEEP', 5), +]; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 46190bb..8e5da65 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -26,6 +26,9 @@ services: redis: condition: service_started volumes: + # Le code est monté depuis l'hôte : les mises à jour ne nécessitent pas de rebuild image + - .:/var/www/html + # Le storage est un volume nommé (uploads, logs, sessions — persisté indépendamment du code) - app_storage:/var/www/html/storage networks: - internal @@ -39,8 +42,8 @@ services: - "443:443" volumes: - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro - - app_storage:/var/www/html/storage:ro - ./public:/var/www/html/public:ro + - app_storage:/var/www/html/storage:ro depends_on: - app networks: diff --git a/docker/Dockerfile b/docker/Dockerfile index 7902702..7a8007d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,29 +1,23 @@ FROM php:8.5-fpm-alpine +# Runtime : extensions PHP + outils nécessaires aux mises à jour RUN apk add --no-cache \ postgresql-dev \ - redis \ + postgresql-client \ nodejs \ npm \ + rsync \ && docker-php-ext-install pdo pdo_pgsql opcache -# Installer Composer +# Composer (runtime, pas de copie du code dans l'image) COPY --from=composer:2 /usr/bin/composer /usr/bin/composer +# Entrypoint : composer install + caches au démarrage du container +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + WORKDIR /var/www/html -COPY composer.json composer.lock ./ -RUN composer install --no-dev --optimize-autoloader --no-scripts - -COPY . . -RUN composer run-script post-autoload-dump \ - && npm ci \ - && npm run build \ - && rm -rf node_modules \ - && php artisan config:cache \ - && php artisan route:cache \ - && php artisan view:cache \ - && chown -R www-data:www-data storage bootstrap/cache - EXPOSE 9000 +ENTRYPOINT ["/entrypoint.sh"] CMD ["php-fpm"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..2930646 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +# Installer les dépendances Composer si nécessaire +if [ ! -f vendor/autoload.php ] || [ composer.lock -nt vendor/autoload.php ] 2>/dev/null; then + echo "[entrypoint] composer install --no-dev ..." + composer install --no-dev --optimize-autoloader --quiet +fi + +# Générer la clé applicative si absente +if grep -q "^APP_KEY=$" .env 2>/dev/null; then + echo "[entrypoint] Génération de la clé applicative..." + php artisan key:generate --force +fi + +# Caches (silencieux si pas encore de .env complet) +php artisan config:cache 2>/dev/null || true +php artisan route:cache 2>/dev/null || true +php artisan view:cache 2>/dev/null || true + +# Permissions storage +chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || true +chmod -R 775 storage bootstrap/cache 2>/dev/null || true + +echo "[entrypoint] Démarrage php-fpm..." +exec "$@" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..4113106 --- /dev/null +++ b/install.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# MesRelevés — Installation initiale (déploiement Docker) +# +# Usage : ./install.sh +# +# Prérequis : Docker + Docker Compose v2 +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail + +COMPOSE="docker compose -f docker-compose.prod.yml" + +echo "────────────────────────────────────────────" +echo " MesRelevés — Installation" +echo "────────────────────────────────────────────" + +# ── Prérequis ───────────────────────────────────────────────────────────────── +command -v docker >/dev/null 2>&1 || { echo "Erreur : Docker n'est pas installé."; exit 1; } +docker compose version >/dev/null 2>&1 || { echo "Erreur : Docker Compose v2 requis."; exit 1; } + +# ── Fichier .env ────────────────────────────────────────────────────────────── +if [ ! -f .env ]; then + cp .env.example .env + echo "" + echo "→ Fichier .env créé depuis .env.example." + echo " Éditez .env avec vos paramètres (DB_PASSWORD, MAIL_*, APP_URL, etc.)" + echo " puis relancez ce script." + echo "" + exit 0 +fi + +# Vérification DB_PASSWORD obligatoire +if grep -qE "^DB_PASSWORD=$" .env; then + echo "Erreur : DB_PASSWORD n'est pas défini dans .env." + exit 1 +fi + +# ── Structure storage ───────────────────────────────────────────────────────── +mkdir -p storage/{app/backups,framework/{cache,sessions,views},logs} +mkdir -p bootstrap/cache + +# ── Démarrage des services ──────────────────────────────────────────────────── +echo "→ Construction de l'image Docker..." +$COMPOSE build + +echo "→ Démarrage des services (db, redis, app, nginx)..." +$COMPOSE up -d + +echo "→ Attente de la base de données..." +until $COMPOSE exec -T db pg_isready -U "${DB_USERNAME:-mesreleves}" >/dev/null 2>&1; do + sleep 2 +done +echo " Base de données prête." + +# Attendre que l'entrypoint termine (composer install, caches) +sleep 8 + +# ── Migrations ──────────────────────────────────────────────────────────────── +echo "→ Migration de la base de données..." +$COMPOSE exec app php artisan migrate --force + +# ── Lien storage ────────────────────────────────────────────────────────────── +$COMPOSE exec app php artisan storage:link 2>/dev/null || true + +# ── Tâche planifiée (cron sur l'hôte) ──────────────────────────────────────── +CRON_CMD="* * * * * cd $(pwd) && docker compose -f docker-compose.prod.yml exec -T app php artisan schedule:run >> /dev/null 2>&1" +echo "" +echo "→ Pour activer les tâches planifiées (vérification automatique des mises à jour)," +echo " ajoutez cette ligne à votre crontab (crontab -e) :" +echo "" +echo " ${CRON_CMD}" +echo "" + +# ── Résumé ──────────────────────────────────────────────────────────────────── +VERSION=$(cat VERSION 2>/dev/null || echo "?") +echo "────────────────────────────────────────────" +echo "✓ MesRelevés v${VERSION} installé avec succès !" +echo "" +echo " URL : $(grep APP_URL .env | cut -d= -f2)" +echo "" +echo "Commandes utiles :" +echo " Voir les logs : docker compose -f docker-compose.prod.yml logs -f app" +echo " Vérifier updates : docker compose -f docker-compose.prod.yml exec app php artisan app:check-update" +echo " Mettre à jour : docker compose -f docker-compose.prod.yml exec app php artisan app:update" +echo "────────────────────────────────────────────" diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index d510990..a801a7a 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -5,6 +5,32 @@
+ {{-- Bandeau mise à jour disponible --}} + @if($updateAvailable) +
+
+ + + +
+

+ Mise à jour disponible : v{{ $latestRelease['version'] }} + (installé : v{{ $installedVersion }}) +

+

+ docker compose exec app php artisan app:update +

+
+
+ @if($latestRelease['published_at']) + + Publié {{ \Carbon\Carbon::parse($latestRelease['published_at'])->diffForHumans() }} + + @endif +
+ @endif + {{-- Compteurs globaux --}}
@php @@ -135,5 +161,17 @@ @endforelse
+ {{-- Version --}} +
+ MesRelevés v{{ $installedVersion }} + @if(! $updateAvailable) + + + + + à jour + + @endif +
diff --git a/routes/console.php b/routes/console.php index 3c9adf1..bb643e8 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,8 +1,11 @@ comment(Inspiring::quote()); -})->purpose('Display an inspiring quote'); +// Vérification quotidienne des mises à jour disponibles. +// Si AUTO_UPDATE=true dans .env, la mise à jour est aussi appliquée automatiquement. +// Pour activer : ajouter à crontab (voir install.sh) +Schedule::command('app:check-update') + ->daily() + ->withoutOverlapping() + ->runInBackground();