Catalogue de flux
++ Une sélection de flux RSS maintenue par BloomFluxDB — ajoutez-les à votre collection en un clic. +
+{{ $item['description'] ?: $item['url'] }}
+diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 585491d..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dd9a2b5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5d70b7b --- /dev/null +++ b/.env.example @@ -0,0 +1,74 @@ +APP_NAME=BloomFeed +APP_ENV=production +APP_KEY= +APP_DEBUG=false +APP_URL=http://localhost:7081 + +APP_LOCALE=fr +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=fr_FR + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=db +DB_PORT=3306 +DB_DATABASE=bloomfeed +DB_USERNAME=bloomfeed +DB_PASSWORD= + +# Utilisé par docker-compose pour initialiser MariaDB (doit correspondre à DB_PASSWORD ci-dessus) +DB_ROOT_PASSWORD= + +# Catalogue central BloomFluxDB (lecture seule) +BLOOMFLUX_DB_HOST=163.5.159.95 +BLOOMFLUX_DB_PORT=3306 +BLOOMFLUX_DB_DATABASE=bloomfluxdb +BLOOMFLUX_DB_USERNAME=bloomflux_reader +BLOOMFLUX_DB_PASSWORD=BloomFlux_Public_Read_2026 +BLOOMFLUX_DB_TIMEOUT=4 + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b84690b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bfb424 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +*.log +.DS_Store +**/.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.claude +/.codex +/.cursor/ +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/fonts-manifest.dev.json +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +_ide_helper.php +composer.phar +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/README.md b/README.md index e133921..d858f07 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ n'a qu'un seul compte propriétaire, créé une seule fois au premier lancement. - Lecture approfondie intégrée : extraction du contenu principal (via [Readability](https://github.com/fivefilters/readability.php)) converti en Markdown, sans quitter BloomFeed. +- Catalogue de flux façon "market" alimenté par **BloomFluxDB** (base centrale en lecture seule), + avec cache local de secours si le serveur catalogue est injoignable. - Récupération périodique des flux via une tâche planifiée. - Thème clair/sombre. - Inscription fermée après la création du compte propriétaire — pas de SaaS ouvert. @@ -31,9 +33,9 @@ n'a qu'un seul compte propriétaire, créé une seule fois au premier lancement. |---|---| |  |  | -| Mode sombre | Connexion | +| Catalogue de flux | Mode sombre | |---|---| -|  |  | +|  |  | ## Stack technique @@ -63,7 +65,7 @@ planificateur, MariaDB). Voir la page **[/self-hosting](/self-hosting)** une foi l'application lancée, ou directement : ```bash -git clone https://git.eldorianet.work/esteban/RSS-Eldoria-Network bloomfeed +git clone https://git.eldorianet.work/esteban/BloomFeed-RSS bloomfeed cd bloomfeed bash scriptsite.sh ``` @@ -78,6 +80,23 @@ Pour désinstaller proprement (avec confirmation avant toute suppression de donn bash uninstall.sh ``` +## BloomFluxDB (catalogue central) + +Les instances BloomFeed consultent en lecture seule une base MariaDB centrale contenant une +liste de flux pré-sélectionnés, affichée dans l'onglet **Catalogue**. La liste est mise en cache +localement : si le serveur catalogue est injoignable, la dernière copie connue est servie, et +elle se rafraîchit automatiquement à la prochaine consultation réussie. + +Le script de création (base, table, utilisateur `bloomflux_reader` en `SELECT` uniquement, et +liste de flux initiale) est dans [docs/bloomfluxdb-setup.sql](docs/bloomfluxdb-setup.sql) — à +exécuter sur le serveur catalogue : + +```bash +mysql -u root -p < docs/bloomfluxdb-setup.sql +``` + +La connexion se configure dans le `.env` de chaque instance via les variables `BLOOMFLUX_DB_*`. + ## Licence MIT. diff --git a/app/Http/Controllers/CatalogController.php b/app/Http/Controllers/CatalogController.php new file mode 100644 index 0000000..32ea888 --- /dev/null +++ b/app/Http/Controllers/CatalogController.php @@ -0,0 +1,24 @@ +getCatalog(); + + $subscribedUrls = $request->user()->feeds()->pluck('url')->all(); + + return view('catalog', [ + 'feeds' => collect($result['feeds'])->groupBy('category'), + 'fromCache' => $result['fromCache'], + 'updatedAt' => $result['updatedAt'], + 'subscribedUrls' => $subscribedUrls, + ]); + } +} diff --git a/app/Services/FluxCatalogService.php b/app/Services/FluxCatalogService.php new file mode 100644 index 0000000..3bd420d --- /dev/null +++ b/app/Services/FluxCatalogService.php @@ -0,0 +1,54 @@ +, fromCache: bool, updatedAt: ?Carbon} + */ + public function getCatalog(): array + { + try { + $rows = DB::connection('bloomflux') + ->table('feeds') + ->where('is_active', 1) + ->orderBy('category') + ->orderBy('title') + ->get(['title', 'url', 'category', 'lang', 'description']) + ->map(fn ($row) => (array) $row) + ->all(); + + Cache::forever(self::CACHE_KEY, $rows); + Cache::forever(self::CACHE_TIME_KEY, now()->toIso8601String()); + + return ['feeds' => $rows, 'fromCache' => false, 'updatedAt' => now()]; + } catch (Throwable $e) { + Log::warning('BloomFluxDB unreachable, serving cached catalog', [ + 'error' => $e->getMessage(), + ]); + + $cached = Cache::get(self::CACHE_KEY, []); + $updatedAt = Cache::get(self::CACHE_TIME_KEY); + + return [ + 'feeds' => $cached, + 'fromCache' => true, + 'updatedAt' => $updatedAt ? Carbon::parse($updatedAt) : null, + ]; + } + } +} diff --git a/config/database.php b/config/database.php index abbb88e..6ba6aa4 100644 --- a/config/database.php +++ b/config/database.php @@ -64,6 +64,24 @@ return [ ]) : [], ], + // Catalogue central BloomFluxDB (lecture seule, distant) — timeout court + // pour ne jamais bloquer la page si le serveur catalogue est injoignable. + 'bloomflux' => [ + 'driver' => 'mysql', + 'host' => env('BLOOMFLUX_DB_HOST', '163.5.159.95'), + 'port' => env('BLOOMFLUX_DB_PORT', '3306'), + 'database' => env('BLOOMFLUX_DB_DATABASE', 'bloomfluxdb'), + 'username' => env('BLOOMFLUX_DB_USERNAME', 'bloomflux_reader'), + 'password' => env('BLOOMFLUX_DB_PASSWORD', 'BloomFlux_Public_Read_2026'), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'strict' => true, + 'options' => extension_loaded('pdo_mysql') ? [ + \PDO::ATTR_TIMEOUT => (int) env('BLOOMFLUX_DB_TIMEOUT', 4), + ] : [], + ], + 'mariadb' => [ 'driver' => 'mariadb', 'url' => env('DB_URL'), diff --git a/docs/bloomfluxdb-setup.sql b/docs/bloomfluxdb-setup.sql new file mode 100644 index 0000000..1977fe5 --- /dev/null +++ b/docs/bloomfluxdb-setup.sql @@ -0,0 +1,78 @@ +-- ============================================================ +-- BloomFluxDB — catalogue central de flux RSS pour les instances BloomFeed +-- À exécuter sur le serveur catalogue (163.5.159.95), en root MariaDB/MySQL : +-- mysql -u root -p < bloomfluxdb-setup.sql +-- +-- Prérequis réseau : MariaDB doit écouter sur l'extérieur +-- (bind-address = 0.0.0.0 dans /etc/mysql/mariadb.conf.d/50-server.cnf, +-- puis systemctl restart mariadb) et le port 3306 doit être ouvert. +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS bloomfluxdb + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE bloomfluxdb; + +CREATE TABLE IF NOT EXISTS feeds ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + url VARCHAR(2048) NOT NULL, + category VARCHAR(100) NOT NULL DEFAULT 'Divers', + lang CHAR(2) NOT NULL DEFAULT 'fr', + description VARCHAR(500) NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uq_feeds_url (url(500)), + KEY idx_feeds_category (category), + KEY idx_feeds_active (is_active) +) ENGINE = InnoDB; + +-- ------------------------------------------------------------ +-- Utilisateur en LECTURE SEULE pour les instances BloomFeed ('%' = accessible +-- depuis n'importe quelle IP). Changez le mot de passe si vous voulez restreindre, +-- et reportez-le dans le .env des instances (BLOOMFLUX_DB_PASSWORD). +-- ------------------------------------------------------------ +CREATE USER IF NOT EXISTS 'bloomflux_reader'@'%' IDENTIFIED BY 'BloomFlux_Public_Read_2026'; +GRANT SELECT ON bloomfluxdb.feeds TO 'bloomflux_reader'@'%'; +FLUSH PRIVILEGES; + +-- ------------------------------------------------------------ +-- Liste de flux pré-faite (extensible à volonté : INSERT depuis root/localhost) +-- ------------------------------------------------------------ +INSERT IGNORE INTO feeds (title, url, category, lang, description) VALUES +-- Actualités générales +('Le Monde', 'https://www.lemonde.fr/rss/une.xml', 'Actualités', 'fr', 'La une du journal Le Monde.'), +('France Info', 'https://www.franceinfo.fr/titres.rss', 'Actualités', 'fr', 'Les titres de France Info en continu.'), +('Courrier International', 'https://www.courrierinternational.com/feed/all/rss.xml', 'Actualités', 'fr', 'L''actualité internationale vue par la presse étrangère.'), +('BBC News', 'https://feeds.bbci.co.uk/news/rss.xml', 'Actualités', 'en', 'Fil d''actualité mondial de la BBC.'), +-- Tech & informatique +('Korben', 'https://korben.info/feed', 'Tech', 'fr', 'Actu tech, astuces et bidouilles.'), +('Numerama', 'https://www.numerama.com/feed/', 'Tech', 'fr', 'Société numérique, tech et sciences.'), +('Next', 'https://next.ink/feed/', 'Tech', 'fr', 'Actualité informatique et numérique de fond.'), +('Journal du hacker', 'https://www.journalduhacker.net/rss', 'Tech', 'fr', 'Liens tech partagés par la communauté francophone.'), +('Ars Technica', 'https://feeds.arstechnica.com/arstechnica/index', 'Tech', 'en', 'Analyses tech approfondies.'), +('Hacker News', 'https://news.ycombinator.com/rss', 'Tech', 'en', 'Les liens les plus discutés de la communauté HN.'), +('The Verge', 'https://www.theverge.com/rss/index.xml', 'Tech', 'en', 'Tech, science et culture numérique.'), +-- Linux & open source +('Phoronix', 'https://www.phoronix.com/rss.php', 'Open source', 'en', 'Linux, matériel et open source.'), +('LinuxFr', 'https://linuxfr.org/news.atom', 'Open source', 'fr', 'L''actualité du logiciel libre en français.'), +('It''s FOSS', 'https://itsfoss.com/rss/', 'Open source', 'en', 'Tutoriels et actus Linux/open source.'), +-- Sécurité +('CERT-FR', 'https://www.cert.ssi.gouv.fr/feed/', 'Sécurité', 'fr', 'Alertes et avis de sécurité de l''ANSSI.'), +('Krebs on Security', 'https://krebsonsecurity.com/feed/', 'Sécurité', 'en', 'Investigations cybersécurité de Brian Krebs.'), +('Zataz', 'https://www.zataz.com/feed/', 'Sécurité', 'fr', 'Actualité cybersécurité et fuites de données.'), +-- Développement +('Dev.to', 'https://dev.to/feed', 'Développement', 'en', 'Articles de la communauté dev.to.'), +('GitHub Blog', 'https://github.blog/feed/', 'Développement', 'en', 'Nouveautés et ingénierie GitHub.'), +('Laravel News', 'https://feed.laravel-news.com/', 'Développement', 'en', 'L''écosystème Laravel au quotidien.'), +-- Sciences +('Futura Sciences', 'https://www.futura-sciences.com/rss/actualites.xml', 'Sciences', 'fr', 'Actualités scientifiques tous domaines.'), +('CNRS Le Journal', 'https://lejournal.cnrs.fr/rss', 'Sciences', 'fr', 'Le journal de la recherche française.'), +-- Jeux vidéo +('Gamekult', 'https://www.gamekult.com/feed.xml', 'Jeux vidéo', 'fr', 'Actualité et critiques jeux vidéo.'), +('Rock Paper Shotgun', 'https://www.rockpapershotgun.com/feed', 'Jeux vidéo', 'en', 'Jeux PC, tests et actus.'); + +-- Vérification rapide +SELECT category, COUNT(*) AS nb FROM feeds GROUP BY category ORDER BY category; diff --git a/docs/screenshots/catalog-light.png b/docs/screenshots/catalog-light.png new file mode 100644 index 0000000..f1d4d9d Binary files /dev/null and b/docs/screenshots/catalog-light.png differ diff --git a/docs/screenshots/dashboard-dark.png b/docs/screenshots/dashboard-dark.png index f9fb516..6447fa2 100644 Binary files a/docs/screenshots/dashboard-dark.png and b/docs/screenshots/dashboard-dark.png differ diff --git a/docs/screenshots/dashboard-light.png b/docs/screenshots/dashboard-light.png index 613a9e8..7a4de1e 100644 Binary files a/docs/screenshots/dashboard-light.png and b/docs/screenshots/dashboard-light.png differ diff --git a/docs/screenshots/home-light.png b/docs/screenshots/home-light.png index da4c71b..b51a00a 100644 Binary files a/docs/screenshots/home-light.png and b/docs/screenshots/home-light.png differ diff --git a/docs/screenshots/login-light.png b/docs/screenshots/login-light.png deleted file mode 100644 index ea35161..0000000 Binary files a/docs/screenshots/login-light.png and /dev/null differ diff --git a/docs/screenshots/reader-light.png b/docs/screenshots/reader-light.png index c45b03c..d5c006d 100644 Binary files a/docs/screenshots/reader-light.png and b/docs/screenshots/reader-light.png differ diff --git a/public/css/app.css b/public/css/app.css index ccfaa84..ca484fa 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -998,6 +998,148 @@ h1, h2, h3 { background: var(--accent-soft); } +/* ---------- Catalogue (market) ---------- */ + +.market-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: var(--space-3); + flex-wrap: wrap; +} + +.cache-notice { + background: var(--accent-soft); + border: 1px solid var(--accent-soft-strong); + border-radius: var(--radius-sm); + padding: 12px 16px; + font-size: var(--font-sm); + font-weight: 600; + color: var(--accent-strong); + animation: fade-up 350ms var(--ease-out) both; +} + +.market-search { + font-size: var(--font-base); + padding: 13px 18px; + border-radius: var(--radius); +} + +.market-chips { + display: flex; + gap: var(--space-2); + flex-wrap: wrap; +} + +.chip { + background: var(--surface); + border: 1.5px solid var(--border); + border-radius: var(--radius-full); + padding: 6px 15px; + font-size: var(--font-xs); + font-weight: 700; + color: var(--text-muted); + cursor: pointer; + font-family: inherit; + transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast), transform var(--t-fast); +} + +.chip:hover { + border-color: var(--accent); + color: var(--accent-strong); + transform: translateY(-1px); +} + +.chip.active { + background: var(--gradient-accent); + border-color: transparent; + color: var(--accent-contrast); + box-shadow: var(--shadow-sm); +} + +.chip-static { + cursor: default; + background: var(--accent-soft); + border-color: transparent; + color: var(--accent-strong); + font-size: 10px; + padding: 3px 10px; +} + +.chip-static:hover { + transform: none; + color: var(--accent-strong); + border-color: transparent; +} + +.market-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); + gap: var(--space-4); +} + +.market-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + padding: var(--space-4) var(--space-5); + display: flex; + flex-direction: column; + gap: var(--space-2); + animation: fade-up 450ms var(--ease-out) both; + transition: transform var(--t-base), box-shadow var(--t-base), border-color var(--t-base); +} + +.market-card:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-md); + border-color: var(--accent-soft-strong); +} + +.market-card-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); +} + +.market-card-title { + font-weight: 700; + font-size: var(--font-base); +} + +.market-card-desc { + font-size: var(--font-sm); + color: var(--text-muted); + margin: 0; + flex: 1; + overflow-wrap: anywhere; +} + +.market-card-foot { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: var(--space-1); +} + +.market-lang { + font-size: 10px; + font-weight: 800; + letter-spacing: 0.08em; + color: var(--text-faint); + border: 1px solid var(--border); + border-radius: 6px; + padding: 2px 6px; +} + +.market-added { + color: var(--green); + font-size: var(--font-sm); + font-weight: 700; +} + /* ---------- Responsive ---------- */ @media (max-width: 720px) { diff --git a/public/js/app.js b/public/js/app.js index f7459e0..99014c3 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -31,4 +31,49 @@ event.preventDefault(); } }); + + // Catalogue : recherche + filtre par catégorie côté client + document.addEventListener('DOMContentLoaded', function () { + var grid = document.getElementById('market-grid'); + if (!grid) { + return; + } + + var search = document.getElementById('market-search'); + var chips = document.getElementById('market-chips'); + var emptyNotice = document.getElementById('market-empty'); + var activeCategory = ''; + + function applyFilters() { + var query = (search.value || '').toLowerCase().trim(); + var visible = 0; + + grid.querySelectorAll('.market-card').forEach(function (card) { + var matchesCategory = !activeCategory || card.getAttribute('data-category') === activeCategory; + var matchesQuery = !query || card.getAttribute('data-search').indexOf(query) !== -1; + var show = matchesCategory && matchesQuery; + card.hidden = !show; + if (show) { + visible++; + } + }); + + emptyNotice.hidden = visible > 0; + } + + search.addEventListener('input', applyFilters); + + chips.addEventListener('click', function (event) { + var chip = event.target.closest('.chip'); + if (!chip) { + return; + } + chips.querySelectorAll('.chip').forEach(function (c) { + c.classList.remove('active'); + }); + chip.classList.add('active'); + activeCategory = chip.getAttribute('data-category'); + applyFilters(); + }); + }); })(); diff --git a/resources/views/catalog.blade.php b/resources/views/catalog.blade.php new file mode 100644 index 0000000..f855993 --- /dev/null +++ b/resources/views/catalog.blade.php @@ -0,0 +1,74 @@ +@extends('layouts.app', ['title' => 'Catalogue — BloomFeed']) + +@section('content') +
+ Une sélection de flux RSS maintenue par BloomFluxDB — ajoutez-les à votre collection en un clic. +
+{{ $item['description'] ?: $item['url'] }}
+