feat: notification system via Mercure for scraping events

- NotificationInterface: add sendInfo() and sendWarning() levels
- SymfonyNotification: implement new levels (publishes to 'notifications' topic)
- ChapterScrapingStarted: carry mangaTitle + chapterNumber, now dispatched
- ScrapeChapterHandler: dispatch ChapterScrapingStarted before scraping loop
- ScrapingEventSubscriber: wire NotificationInterface for started/scraped/failed events
- useMercureNotifications: new global Vue composable subscribing to 'notifications' topic
- App.vue: mount useMercureNotifications() at app root
- SendTestNotificationCommand: `app:notify:test --type --message` for dev/prod testing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-03-12 00:57:21 +01:00
parent 95f224d69a
commit 41ca08f20e
9 changed files with 171 additions and 50 deletions

View File

@@ -5,6 +5,7 @@ namespace App\Tests\Domain\Scraping\Application\CommandHandler;
use App\Domain\Scraping\Application\Command\ScrapeChapter;
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
use App\Domain\Scraping\Domain\Event\ChapterScrapingStarted;
use App\Domain\Scraping\Domain\Model\Chapter;
use App\Domain\Shared\Domain\Event\ChapterScraped;
use App\Tests\Domain\Scraping\Adapter\InMemoryChapterRepository;
@@ -81,11 +82,15 @@ class ScrapeChapterHandlerTest extends TestCase
$job = array_values($job)[0];
$dispatchedMessages = $this->eventBus->getDispatchedMessages();
$this->assertCount(1, $dispatchedMessages);
$this->assertInstanceOf(ChapterScraped::class, $dispatchedMessages[0]);
$this->assertEquals($job->id, $dispatchedMessages[0]->getJobId());
$this->assertEquals('1', $dispatchedMessages[0]->chapterId);
$this->assertEquals('/fake/pages/1', $dispatchedMessages[0]->pagesDirectory);
$this->assertCount(2, $dispatchedMessages);
$this->assertInstanceOf(ChapterScrapingStarted::class, $dispatchedMessages[0]);
$this->assertSame(2.0, $dispatchedMessages[0]->getChapterNumber());
$this->assertInstanceOf(ChapterScraped::class, $dispatchedMessages[1]);
$this->assertEquals($job->id, $dispatchedMessages[1]->getJobId());
$this->assertEquals('1', $dispatchedMessages[1]->chapterId);
$this->assertEquals('/fake/pages/1', $dispatchedMessages[1]->pagesDirectory);
$this->assertNotNull($this->imageStorage->stored['1'] ?? null);
}