From ea284a426f98f6290295136fc1c5323a3bb58809 Mon Sep 17 00:00:00 2001 From: Esteban Date: Mon, 6 Jul 2026 12:17:38 +0200 Subject: [PATCH] ADD UPDATE SCRIPT v0.23 --- README.md | 27 ++++++ app/Http/Controllers/PageController.php | 49 ++++++++++ auto-update.sh | 101 ++++++++++++++++++++ force-update.sh | 16 ++++ lang/de/app.php | 13 +++ lang/en/app.php | 13 +++ lang/es/app.php | 13 +++ lang/fr/app.php | 13 +++ public/css/app.css | 83 ++++++++++++++-- public/js/navbar-toggle.js | 27 ++++++ public/robots.txt | 2 - resources/views/home.blade.php | 31 +++++- resources/views/layouts/app.blade.php | 1 + resources/views/layouts/marketing.blade.php | 28 +++++- resources/views/partials/navbar.blade.php | 7 +- resources/views/self-hosting.blade.php | 14 ++- routes/web.php | 2 + scriptsite.sh | 22 +++++ 18 files changed, 446 insertions(+), 16 deletions(-) create mode 100755 auto-update.sh create mode 100755 force-update.sh create mode 100644 public/js/navbar-toggle.js delete mode 100644 public/robots.txt diff --git a/README.md b/README.md index 0215afd..da54c5e 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,33 @@ publiques se referment automatiquement après. Relancez simplement `bash scriptsite.sh` : il récupère les derniers changements et reconstruit la stack sans toucher à vos données. +Avant chaque redéploiement (s'il existe déjà une instance en cours), le script sauvegarde +automatiquement la base de données dans `backups/bloomfeed-.sql.gz` (les 10 dernières +sauvegardes sont conservées). Si la sauvegarde échoue, la mise à jour est annulée pour ne +jamais risquer de perdre des données. Pour restaurer une sauvegarde : + +```bash +gunzip -c backups/bloomfeed-20260706-120000.sql.gz \ + | sudo docker compose exec -T db sh -c 'exec mariadb -u root -p"$MARIADB_ROOT_PASSWORD"' +``` + +### Mise à jour automatique + +`auto-update.sh` vérifie périodiquement si le dépôt a de nouveaux commits sur `origin/main` et +ne redéploie (via `scriptsite.sh`) que si c'est le cas — une instance à jour ne reconstruit jamais +pour rien. Le mécanisme tourne sur l'hôte via une tâche cron dédiée (`/etc/cron.d/bloomfeed-auto-update`), +jamais depuis le conteneur applicatif (qui n'a pas accès au démon Docker, par sécurité). + +```bash +bash auto-update.sh --install # active la vérification toutes les 30 min (défaut) +bash auto-update.sh --install 60 # ou toutes les 60 min +bash auto-update.sh --status # état actuel + derniers logs +bash auto-update.sh --force # force un redéploiement immédiat +bash auto-update.sh --uninstall # désactive la mise à jour automatique +``` + +Les logs sont écrits dans `storage/logs/auto-update.log` à l'intérieur du dépôt. + ### Réinitialisation Pour repartir d'une instance vierge (efface comptes, flux, articles et notifieurs) sans toucher diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index eda8952..e7469c8 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Models\User; use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Response; use Illuminate\View\View; class PageController extends Controller @@ -23,4 +24,52 @@ class PageController extends Controller { return view('self-hosting'); } + + /** + * Allow/Disallow use paths, not absolute URLs, per the robots.txt spec. + * Only the Sitemap directive takes an absolute URL, generated from this + * instance's own domain so every self-hosted BloomFeed gets it for free. + */ + public function robots(): Response + { + $lines = [ + 'User-agent: *', + 'Allow: /', + 'Allow: /self-hosting', + 'Disallow: /login', + 'Disallow: /register', + 'Disallow: /dashboard', + 'Disallow: /feeds', + 'Disallow: /catalog', + 'Disallow: /notifiers', + 'Disallow: /settings', + 'Disallow: /articles', + '', + 'Sitemap: '.route('sitemap'), + ]; + + return response(implode("\n", $lines), 200)->header('Content-Type', 'text/plain'); + } + + public function sitemap(): Response + { + $urls = [ + ['loc' => route('home'), 'priority' => '1.0'], + ['loc' => route('self-hosting'), 'priority' => '0.8'], + ]; + + $xml = ''."\n"; + $xml .= ''."\n"; + + foreach ($urls as $url) { + $xml .= " \n"; + $xml .= ' '.e($url['loc'])."\n"; + $xml .= ' '.$url['priority']."\n"; + $xml .= " \n"; + } + + $xml .= ''; + + return response($xml, 200)->header('Content-Type', 'application/xml'); + } } diff --git a/auto-update.sh b/auto-update.sh new file mode 100755 index 0000000..0e45107 --- /dev/null +++ b/auto-update.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Checks whether the BloomFeed git repository has new commits on origin/main +# and, if so, redeploys via scriptsite.sh. Designed to be run periodically by +# a cron job installed with --install (see below). Only redeploys when there +# is actually something new, so an idle instance never rebuilds for nothing. + +APP_DIR="/opt/bloomfeed" +CRON_FILE="/etc/cron.d/bloomfeed-auto-update" +LOG_DIR="${APP_DIR}/storage/logs" +LOG_FILE="${LOG_DIR}/auto-update.log" +INTERVAL_MINUTES=30 +FORCE=false + +log() { + echo "$(date -Iseconds) $1" +} + +case "${1:-}" in + --install) + INTERVAL_MINUTES="${2:-30}" + if ! [[ "${INTERVAL_MINUTES}" =~ ^[0-9]+$ ]] || [[ "${INTERVAL_MINUTES}" -lt 1 || "${INTERVAL_MINUTES}" -gt 1440 ]]; then + echo "ERROR: interval must be a number of minutes between 1 and 1440." >&2 + exit 1 + fi + sudo mkdir -p "${LOG_DIR}" + echo "*/${INTERVAL_MINUTES} * * * * root ${APP_DIR}/auto-update.sh >> ${LOG_FILE} 2>&1" | sudo tee "${CRON_FILE}" >/dev/null + sudo chmod 644 "${CRON_FILE}" + echo "==> Auto-update installed: checking for new commits every ${INTERVAL_MINUTES} minute(s)." + echo " Cron file: ${CRON_FILE}" + echo " Logs: ${LOG_FILE}" + exit 0 + ;; + --uninstall) + sudo rm -f "${CRON_FILE}" + echo "==> Auto-update disabled (cron file removed)." + exit 0 + ;; + --status) + if [[ -f "${CRON_FILE}" ]]; then + echo "==> Auto-update is ENABLED:" + cat "${CRON_FILE}" + else + echo "==> Auto-update is DISABLED (no ${CRON_FILE})." + fi + if [[ -f "${LOG_FILE}" ]]; then + echo "==> Last log entries:" + tail -n 10 "${LOG_FILE}" + fi + exit 0 + ;; + --force) + FORCE=true + ;; + --help|-h) + echo "Usage: bash auto-update.sh [--install [minutes]|--uninstall|--status|--force]" + echo " (no args) Check for new commits and redeploy if found — meant to be run by cron." + echo " --install [N] Install a cron job checking every N minutes (default: 30)." + echo " --uninstall Remove the cron job." + echo " --status Show whether auto-update is enabled and recent log entries." + echo " --force Redeploy immediately, even without a new commit." + exit 0 + ;; + "") ;; + *) + echo "Unknown option: ${1} (use --help)" >&2 + exit 1 + ;; +esac + +if [[ ! -d "${APP_DIR}/.git" ]]; then + echo "ERROR: ${APP_DIR} is not a git repository — run scriptsite.sh first." >&2 + exit 1 +fi + +mkdir -p "${LOG_DIR}" +cd "${APP_DIR}" + +git fetch origin main --quiet + +LOCAL_SHA="$(git rev-parse HEAD)" +REMOTE_SHA="$(git rev-parse origin/main)" + +if [[ "${FORCE}" != true && "${LOCAL_SHA}" == "${REMOTE_SHA}" ]]; then + log "No update available (up to date at ${LOCAL_SHA:0:7})." + exit 0 +fi + +if [[ "${FORCE}" == true ]]; then + log "Forced redeploy requested (currently at ${LOCAL_SHA:0:7})." +else + log "Update found: ${LOCAL_SHA:0:7} -> ${REMOTE_SHA:0:7}. Redeploying..." +fi + +if bash "${APP_DIR}/scriptsite.sh"; then + log "Auto-update finished successfully (now at $(git rev-parse HEAD | cut -c1-7))." +else + log "Auto-update FAILED — the instance may be left in a partially deployed state, check the output above." + exit 1 +fi diff --git a/force-update.sh b/force-update.sh new file mode 100755 index 0000000..ad3ff2b --- /dev/null +++ b/force-update.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Forces an immediate redeploy of BloomFeed, regardless of whether a new +# commit is actually available. Thin wrapper around auto-update.sh --force +# so the intent is explicit and easy to remember/document. + +APP_DIR="/opt/bloomfeed" + +if [[ ! -f "${APP_DIR}/auto-update.sh" ]]; then + echo "ERROR: ${APP_DIR}/auto-update.sh not found — run scriptsite.sh first." >&2 + exit 1 +fi + +echo "==> Forcing an immediate BloomFeed update/redeploy..." +exec bash "${APP_DIR}/auto-update.sh" --force diff --git a/lang/de/app.php b/lang/de/app.php index 45d0cbd..d759c7d 100644 --- a/lang/de/app.php +++ b/lang/de/app.php @@ -8,6 +8,7 @@ return [ 'settings' => 'Einstellungen', 'logout' => 'Abmelden', 'change_theme' => 'Design wechseln', + 'nav_menu' => 'Menü', 'language' => 'Sprache', 'self_hosting' => 'Self-Hosting', 'login' => 'Anmelden', @@ -140,6 +141,7 @@ return [ 'config_catalog' => 'Der integrierte Feed-Katalog lässt sich durch Bearbeiten von resources/data/bloomflux.json und erneutes Deployment anpassen.', 'update_title' => 'Aktualisieren', 'update_text' => 'Führe einfach das Deployment-Skript erneut aus: Es holt die neuesten Änderungen und baut den Stack neu, ohne deine Daten anzutasten.', + 'backup_note' => 'Vor jedem Redeploy wird die Datenbank automatisch nach backups/ gesichert (die letzten 10 Sicherungen werden aufbewahrt). Schlägt die Sicherung fehl, wird das Update abgebrochen, damit deine Daten niemals gefährdet werden.', 'uninstall_title' => 'Deinstallieren', 'uninstall_text' => 'Das Skript uninstall.sh stoppt und entfernt den Docker-Stack sauber. Es fragt vor dem Löschen deiner Daten explizit nach und entfernt den Quellcode nie ohne separate Bestätigung.', 'source_title' => 'Quellcode', @@ -191,4 +193,15 @@ return [ 'reset_title' => 'Zurücksetzen', 'reset_text' => 'Das Skript reset.sh setzt die Instanz auf einen leeren Zustand zurück (Konten, Feeds, Artikel, Benachrichtigungen), ohne Quellcode, .env oder Docker-Images anzutasten — nützlich, um mit einer sauberen Instanz neu zu starten.', + + // Automatische Updates + 'autoupdate_title' => 'Automatische Updates', + 'autoupdate_text' => 'Das Skript auto-update.sh prüft regelmäßig auf neue Commits im Repository und stellt nur bei Bedarf neu bereit, über einen geplanten Task auf dem Server (niemals aus dem Anwendungscontainer heraus).', + 'autoupdate_comment_install' => 'prüft alle 30 Minuten', + 'autoupdate_comment_status' => 'zeigt Status und letzte Logs', + 'autoupdate_comment_uninstall' => 'deaktiviert automatische Updates', + + // SEO + 'seo_home_title' => 'BloomFeed — Selbst gehosteter, quelloffener RSS-Aggregator', + 'seo_selfhost_title' => 'BloomFeed selbst hosten — Docker-Installationsanleitung', ]; diff --git a/lang/en/app.php b/lang/en/app.php index 62e9d47..50a385b 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -8,6 +8,7 @@ return [ 'settings' => 'Settings', 'logout' => 'Log out', 'change_theme' => 'Switch theme', + 'nav_menu' => 'Menu', 'language' => 'Language', 'self_hosting' => 'Self-hosting', 'login' => 'Log in', @@ -140,6 +141,7 @@ return [ 'config_catalog' => 'The built-in feed catalog can be customized by editing resources/data/bloomflux.json and redeploying.', 'update_title' => 'Update', 'update_text' => 'Simply re-run the deployment script: it pulls the latest changes and rebuilds the stack without touching your data.', + 'backup_note' => 'Before every redeploy, the database is automatically backed up to backups/ (the last 10 backups are kept). If the backup fails, the update is aborted so your data is never put at risk.', 'uninstall_title' => 'Uninstall', 'uninstall_text' => 'The uninstall.sh script cleanly stops and removes the Docker stack. It asks for explicit confirmation before deleting your data and never removes the source code without a separate confirmation.', 'source_title' => 'Source code', @@ -191,4 +193,15 @@ return [ 'reset_title' => 'Reset', 'reset_text' => 'The reset.sh script wipes the instance back to a blank state (accounts, feeds, articles, notifiers) without touching the source code, .env or Docker images — useful to start over on a clean instance.', + + // Auto-update + 'autoupdate_title' => 'Automatic updates', + 'autoupdate_text' => 'The auto-update.sh script periodically checks for new commits on the repository and only redeploys when needed, via a scheduled task on the server (never from the application container).', + 'autoupdate_comment_install' => 'checks every 30 minutes', + 'autoupdate_comment_status' => 'shows status and recent logs', + 'autoupdate_comment_uninstall' => 'disables automatic updates', + + // SEO + 'seo_home_title' => 'BloomFeed — Self-hosted, open-source RSS aggregator', + 'seo_selfhost_title' => 'Self-host BloomFeed — Docker installation guide', ]; diff --git a/lang/es/app.php b/lang/es/app.php index adf92af..2f5b57b 100644 --- a/lang/es/app.php +++ b/lang/es/app.php @@ -8,6 +8,7 @@ return [ 'settings' => 'Ajustes', 'logout' => 'Cerrar sesión', 'change_theme' => 'Cambiar de tema', + 'nav_menu' => 'Menú', 'language' => 'Idioma', 'self_hosting' => 'Autoalojamiento', 'login' => 'Iniciar sesión', @@ -140,6 +141,7 @@ return [ 'config_catalog' => 'El catálogo de fuentes integrado se personaliza editando resources/data/bloomflux.json y volviendo a desplegar.', 'update_title' => 'Actualizar', 'update_text' => 'Simplemente vuelve a ejecutar el script de despliegue: recupera los últimos cambios y reconstruye la pila sin tocar tus datos.', + 'backup_note' => 'Antes de cada redespliegue, la base de datos se respalda automáticamente en backups/ (se conservan las últimas 10 copias). Si el respaldo falla, la actualización se cancela para no arriesgar nunca tus datos.', 'uninstall_title' => 'Desinstalar', 'uninstall_text' => 'El script uninstall.sh detiene y elimina limpiamente la pila Docker. Pide confirmación explícita antes de borrar tus datos y nunca elimina el código fuente sin una confirmación separada.', 'source_title' => 'Código fuente', @@ -191,4 +193,15 @@ return [ 'reset_title' => 'Reiniciar', 'reset_text' => 'El script reset.sh restablece la instancia a un estado en blanco (cuentas, fuentes, artículos, notificadores) sin tocar el código fuente, el .env ni las imágenes Docker — útil para empezar de cero.', + + // Actualización automática + 'autoupdate_title' => 'Actualizaciones automáticas', + 'autoupdate_text' => 'El script auto-update.sh comprueba periódicamente si hay nuevos commits en el repositorio y solo vuelve a desplegar cuando es necesario, mediante una tarea programada en el servidor (nunca desde el contenedor de la aplicación).', + 'autoupdate_comment_install' => 'comprueba cada 30 minutos', + 'autoupdate_comment_status' => 'muestra el estado y los últimos registros', + 'autoupdate_comment_uninstall' => 'desactiva las actualizaciones automáticas', + + // SEO + 'seo_home_title' => 'BloomFeed — Agregador RSS autoalojado y de código abierto', + 'seo_selfhost_title' => 'Autoalojar BloomFeed — guía de instalación con Docker', ]; diff --git a/lang/fr/app.php b/lang/fr/app.php index 379b142..9751e13 100644 --- a/lang/fr/app.php +++ b/lang/fr/app.php @@ -8,6 +8,7 @@ return [ 'settings' => 'Paramètres', 'logout' => 'Déconnexion', 'change_theme' => 'Changer de thème', + 'nav_menu' => 'Menu', 'language' => 'Langue', 'self_hosting' => 'Self-hosting', 'login' => 'Se connecter', @@ -140,6 +141,7 @@ return [ 'config_catalog' => "Le catalogue de flux intégré se personnalise en éditant resources/data/bloomflux.json puis en redéployant.", 'update_title' => 'Mettre à jour', 'update_text' => 'Relancez simplement le script de déploiement : il récupère les derniers changements et reconstruit la stack sans toucher à vos données.', + 'backup_note' => 'Avant chaque redéploiement, la base de données est automatiquement sauvegardée dans backups/ (les 10 dernières sauvegardes sont conservées). Si la sauvegarde échoue, la mise à jour est annulée pour ne jamais risquer de perdre des données.', 'uninstall_title' => 'Désinstaller', 'uninstall_text' => "Le script uninstall.sh arrête et supprime proprement la stack Docker. Il demande une confirmation explicite avant de supprimer vos données et ne supprime jamais le code source sans confirmation séparée.", 'source_title' => 'Code source', @@ -191,4 +193,15 @@ return [ 'reset_title' => 'Réinitialiser', 'reset_text' => "Le script reset.sh remet l'instance à zéro (comptes, flux, articles, notifieurs) sans toucher au code, au .env ni aux images Docker — utile pour repartir sur une instance vierge.", + + // Mise à jour automatique + 'autoupdate_title' => 'Mise à jour automatique', + 'autoupdate_text' => "Le script auto-update.sh vérifie périodiquement les nouveaux commits sur le dépôt et ne redéploie que si nécessaire, via une tâche planifiée sur le serveur (jamais depuis le conteneur applicatif).", + 'autoupdate_comment_install' => 'active la vérification toutes les 30 min', + 'autoupdate_comment_status' => 'affiche l\'état et les derniers logs', + 'autoupdate_comment_uninstall' => 'désactive la mise à jour automatique', + + // SEO + 'seo_home_title' => 'BloomFeed — Agrégateur RSS auto-hébergé et open-source', + 'seo_selfhost_title' => "Auto-héberger BloomFeed — guide d'installation Docker", ]; diff --git a/public/css/app.css b/public/css/app.css index b463e0a..e9ef6c1 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -224,20 +224,85 @@ h1, h2, h3 { color: var(--accent-contrast); } -@media (max-width: 640px) { - .navbar { - padding: 12px 16px; +.navbar-toggle { + display: none; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 5px; + width: 40px; + height: 40px; + background: transparent; + border: 1.5px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; + flex-shrink: 0; +} + +.navbar-toggle-bar { + display: block; + width: 18px; + height: 2px; + border-radius: var(--radius-full); + background: var(--text); + transition: transform var(--t-fast), opacity var(--t-fast); +} + +.navbar-toggle.is-active .navbar-toggle-bar:nth-child(1) { + transform: translateY(7px) rotate(45deg); +} + +.navbar-toggle.is-active .navbar-toggle-bar:nth-child(2) { + opacity: 0; +} + +.navbar-toggle.is-active .navbar-toggle-bar:nth-child(3) { + transform: translateY(-7px) rotate(-45deg); +} + +@media (max-width: 900px) { + .navbar-toggle { + display: flex; } .navbar-links { - gap: 8px; - flex-wrap: wrap; - justify-content: flex-end; - row-gap: 6px; + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + flex-direction: column; + align-items: stretch; + gap: 4px; + padding: var(--space-3) 16px calc(var(--space-4) + 4px); + background: var(--surface); + border-bottom: 1px solid var(--border); + box-shadow: var(--shadow-md); } - .nav-self-hosting { - display: none; + .navbar-links.is-open { + display: flex; + } + + .navbar-links a, + .navbar-links form { + width: 100%; + } + + .navbar-links a { + padding: 10px 4px; + } + + .navbar-links a.btn, + .navbar-links form button { + width: 100%; + text-align: center; + } + + .navbar-links .locale-form, + .navbar-links .theme-toggle { + align-self: flex-start; + margin-top: 4px; } } diff --git a/public/js/navbar-toggle.js b/public/js/navbar-toggle.js new file mode 100644 index 0000000..972367f --- /dev/null +++ b/public/js/navbar-toggle.js @@ -0,0 +1,27 @@ +(function () { + document.addEventListener('DOMContentLoaded', function () { + var toggle = document.querySelector('[data-navbar-toggle]'); + if (!toggle) { + return; + } + + var links = document.getElementById(toggle.getAttribute('aria-controls')); + if (!links) { + return; + } + + toggle.addEventListener('click', function () { + var isOpen = links.classList.toggle('is-open'); + toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + toggle.classList.toggle('is-active', isOpen); + }); + + document.addEventListener('keydown', function (event) { + if (event.key === 'Escape' && links.classList.contains('is-open')) { + links.classList.remove('is-open'); + toggle.setAttribute('aria-expanded', 'false'); + toggle.classList.remove('is-active'); + } + }); + }); +})(); diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index eb05362..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 1760f61..6073c3d 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -1,4 +1,7 @@ -@extends('layouts.marketing', ['title' => 'BloomFeed']) +@extends('layouts.marketing', [ + 'title' => __('app.seo_home_title'), + 'description' => __('app.meta_description'), +]) @php // Screenshots live under images/screenshots/{locale}/ so each language can show its own @@ -129,4 +132,30 @@ + +@push('structured-data') + +@endpush @endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index defce12..1dc6378 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -27,6 +27,7 @@ + diff --git a/resources/views/layouts/marketing.blade.php b/resources/views/layouts/marketing.blade.php index 32cc5df..fe977ae 100644 --- a/resources/views/layouts/marketing.blade.php +++ b/resources/views/layouts/marketing.blade.php @@ -5,9 +5,27 @@ {{ $title ?? 'BloomFeed' }} - + + + + + + + + + + + + + + + + + + + @stack('structured-data') @@ -27,7 +45,12 @@ -