feat(system): page Status avec endpoint API Platform et composants Vue
- Nouveau domaine System/Domain/Model/SystemStatus (value object) - QueryHandler agrégeant métriques mangas, chapitres, jobs (global/24h/7j), stockage et sources - Endpoint GET /api/system/status via API Platform (singleton) - Calcul de l'espace disque par RecursiveDirectoryIterator sur public/images - Page Vue /system/status avec 6 cards (Mangas, Chapitres, Jobs, Stockage, Sources, Système) - Nettoyage du router : suppression des PlaceholderComponent et routes placeholder - Sidebar : suppression des entrées sans page réelle
This commit is contained in:
parent
c2b55e9018
commit
ca8791cc0d
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\System\Application\Query;
|
||||
|
||||
final class GetSystemStatusQuery
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\System\Application\QueryHandler;
|
||||
|
||||
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
||||
use App\Domain\Shared\Domain\Model\JobStatus;
|
||||
use App\Domain\System\Application\Query\GetSystemStatusQuery;
|
||||
use App\Domain\System\Domain\Contract\Repository\SystemStatusRepositoryInterface;
|
||||
use App\Domain\System\Domain\Model\SystemStatus;
|
||||
|
||||
readonly class GetSystemStatusQueryHandler
|
||||
{
|
||||
public function __construct(
|
||||
private SystemStatusRepositoryInterface $systemStatusRepository,
|
||||
private JobRepositoryInterface $jobRepository,
|
||||
private string $mangaDataPath,
|
||||
private string $imagesStoragePath,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(GetSystemStatusQuery $query): SystemStatus
|
||||
{
|
||||
$now = new \DateTimeImmutable();
|
||||
$last24h = $now->modify('-24 hours');
|
||||
$last7d = $now->modify('-7 days');
|
||||
|
||||
$totalJobs = $this->jobRepository->countByCriteria([]);
|
||||
$completedJobs = $this->jobRepository->countByCriteria(['status' => JobStatus::COMPLETED]);
|
||||
$failedJobs = $this->jobRepository->countByCriteria(['status' => JobStatus::FAILED]);
|
||||
$pendingJobs = $this->jobRepository->countByCriteria(['status' => JobStatus::PENDING]);
|
||||
$inProgressJobs = $this->jobRepository->countByCriteria(['status' => JobStatus::IN_PROGRESS]);
|
||||
|
||||
$totalJobsLast24h = $this->jobRepository->countByCriteria(['createdAfter' => $last24h]);
|
||||
$completedJobsLast24h = $this->jobRepository->countByCriteria(['status' => JobStatus::COMPLETED, 'createdAfter' => $last24h]);
|
||||
$failedJobsLast24h = $this->jobRepository->countByCriteria(['status' => JobStatus::FAILED, 'createdAfter' => $last24h]);
|
||||
|
||||
$totalJobsLast7d = $this->jobRepository->countByCriteria(['createdAfter' => $last7d]);
|
||||
$completedJobsLast7d = $this->jobRepository->countByCriteria(['status' => JobStatus::COMPLETED, 'createdAfter' => $last7d]);
|
||||
$failedJobsLast7d = $this->jobRepository->countByCriteria(['status' => JobStatus::FAILED, 'createdAfter' => $last7d]);
|
||||
|
||||
$storagePath = $this->imagesStoragePath;
|
||||
$storageTotalBytes = (int) (@disk_total_space($storagePath) ?: 0);
|
||||
$storageFreeBytes = (int) (@disk_free_space($storagePath) ?: 0);
|
||||
$storageUsedBytes = $this->computeDirectorySize($storagePath);
|
||||
|
||||
return new SystemStatus(
|
||||
totalMangas: $this->systemStatusRepository->countMangas(),
|
||||
monitoredMangas: $this->systemStatusRepository->countMonitoredMangas(),
|
||||
mangasByStatus: $this->systemStatusRepository->countMangasByStatus(),
|
||||
totalChapters: $this->systemStatusRepository->countChapters(),
|
||||
downloadedChapters: $this->systemStatusRepository->countDownloadedChapters(),
|
||||
totalJobs: $totalJobs,
|
||||
completedJobs: $completedJobs,
|
||||
failedJobs: $failedJobs,
|
||||
pendingJobs: $pendingJobs,
|
||||
inProgressJobs: $inProgressJobs,
|
||||
totalJobsLast24h: $totalJobsLast24h,
|
||||
completedJobsLast24h: $completedJobsLast24h,
|
||||
failedJobsLast24h: $failedJobsLast24h,
|
||||
totalJobsLast7d: $totalJobsLast7d,
|
||||
completedJobsLast7d: $completedJobsLast7d,
|
||||
failedJobsLast7d: $failedJobsLast7d,
|
||||
storagePath: $this->mangaDataPath,
|
||||
storageTotalBytes: $storageTotalBytes,
|
||||
storageFreeBytes: $storageFreeBytes,
|
||||
storageUsedBytes: $storageUsedBytes,
|
||||
totalSources: $this->systemStatusRepository->countContentSources(),
|
||||
sourcesByHealth: $this->systemStatusRepository->countContentSourcesByHealth(),
|
||||
phpVersion: PHP_VERSION,
|
||||
generatedAt: $now,
|
||||
);
|
||||
}
|
||||
|
||||
private function computeDirectorySize(string $path): int
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$size = 0;
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$size += $file->getSize();
|
||||
}
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\System\Domain\Contract\Repository;
|
||||
|
||||
interface SystemStatusRepositoryInterface
|
||||
{
|
||||
public function countMangas(): int;
|
||||
|
||||
public function countMonitoredMangas(): int;
|
||||
|
||||
/** @return array<string, int> */
|
||||
public function countMangasByStatus(): array;
|
||||
|
||||
public function countChapters(): int;
|
||||
|
||||
public function countDownloadedChapters(): int;
|
||||
|
||||
public function countContentSources(): int;
|
||||
|
||||
/** @return array<string, int> */
|
||||
public function countContentSourcesByHealth(): array;
|
||||
}
|
||||
44
src/Domain/System/Domain/Model/SystemStatus.php
Normal file
44
src/Domain/System/Domain/Model/SystemStatus.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\System\Domain\Model;
|
||||
|
||||
readonly class SystemStatus
|
||||
{
|
||||
public function __construct(
|
||||
// Mangas
|
||||
public int $totalMangas,
|
||||
public int $monitoredMangas,
|
||||
/** @var array<string, int> */
|
||||
public array $mangasByStatus,
|
||||
// Chapitres
|
||||
public int $totalChapters,
|
||||
public int $downloadedChapters,
|
||||
// Jobs global
|
||||
public int $totalJobs,
|
||||
public int $completedJobs,
|
||||
public int $failedJobs,
|
||||
public int $pendingJobs,
|
||||
public int $inProgressJobs,
|
||||
// Jobs 24h
|
||||
public int $totalJobsLast24h,
|
||||
public int $completedJobsLast24h,
|
||||
public int $failedJobsLast24h,
|
||||
// Jobs 7j
|
||||
public int $totalJobsLast7d,
|
||||
public int $completedJobsLast7d,
|
||||
public int $failedJobsLast7d,
|
||||
// Stockage
|
||||
public string $storagePath,
|
||||
public int $storageTotalBytes,
|
||||
public int $storageFreeBytes,
|
||||
public int $storageUsedBytes,
|
||||
// Sources
|
||||
public int $totalSources,
|
||||
/** @var array<string, int> */
|
||||
public array $sourcesByHealth,
|
||||
// Système
|
||||
public string $phpVersion,
|
||||
public \DateTimeImmutable $generatedAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\System\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use App\Domain\System\Infrastructure\ApiPlatform\State\Provider\GetSystemStatusStateProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'System',
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/system/status',
|
||||
provider: GetSystemStatusStateProvider::class,
|
||||
)
|
||||
]
|
||||
)]
|
||||
class GetSystemStatusResource
|
||||
{
|
||||
#[ApiProperty(identifier: true)]
|
||||
public string $id = 'current';
|
||||
|
||||
public int $totalMangas = 0;
|
||||
public int $monitoredMangas = 0;
|
||||
/** @var array<string, int> */
|
||||
public array $mangasByStatus = [];
|
||||
public int $totalChapters = 0;
|
||||
public int $downloadedChapters = 0;
|
||||
public int $pendingChapters = 0;
|
||||
public int $totalJobs = 0;
|
||||
public int $completedJobs = 0;
|
||||
public int $failedJobs = 0;
|
||||
public int $pendingJobs = 0;
|
||||
public int $inProgressJobs = 0;
|
||||
public int $totalJobsLast24h = 0;
|
||||
public int $completedJobsLast24h = 0;
|
||||
public int $failedJobsLast24h = 0;
|
||||
public float $successRateLast24h = 0.0;
|
||||
public int $totalJobsLast7d = 0;
|
||||
public int $completedJobsLast7d = 0;
|
||||
public int $failedJobsLast7d = 0;
|
||||
public float $successRateLast7d = 0.0;
|
||||
public string $storagePath = '';
|
||||
public int $storageTotalBytes = 0;
|
||||
public int $storageFreeBytes = 0;
|
||||
public int $storageUsedBytes = 0;
|
||||
public string $storageTotalHuman = '';
|
||||
public string $storageFreeHuman = '';
|
||||
public string $storageUsedHuman = '';
|
||||
public int $totalSources = 0;
|
||||
/** @var array<string, int> */
|
||||
public array $sourcesByHealth = [];
|
||||
public string $phpVersion = '';
|
||||
public string $generatedAt = '';
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\System\Infrastructure\ApiPlatform\State\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Domain\System\Application\Query\GetSystemStatusQuery;
|
||||
use App\Domain\System\Application\QueryHandler\GetSystemStatusQueryHandler;
|
||||
use App\Domain\System\Domain\Model\SystemStatus;
|
||||
use App\Domain\System\Infrastructure\ApiPlatform\Resource\GetSystemStatusResource;
|
||||
|
||||
final class GetSystemStatusStateProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GetSystemStatusQueryHandler $handler,
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): GetSystemStatusResource
|
||||
{
|
||||
$response = $this->handler->handle(new GetSystemStatusQuery());
|
||||
|
||||
return $this->toResource($response);
|
||||
}
|
||||
|
||||
private function toResource(SystemStatus $response): GetSystemStatusResource
|
||||
{
|
||||
$resource = new GetSystemStatusResource();
|
||||
$resource->id = 'current';
|
||||
|
||||
$resource->totalMangas = $response->totalMangas;
|
||||
$resource->monitoredMangas = $response->monitoredMangas;
|
||||
$resource->mangasByStatus = $response->mangasByStatus;
|
||||
|
||||
$resource->totalChapters = $response->totalChapters;
|
||||
$resource->downloadedChapters = $response->downloadedChapters;
|
||||
$resource->pendingChapters = $response->totalChapters - $response->downloadedChapters;
|
||||
|
||||
$resource->totalJobs = $response->totalJobs;
|
||||
$resource->completedJobs = $response->completedJobs;
|
||||
$resource->failedJobs = $response->failedJobs;
|
||||
$resource->pendingJobs = $response->pendingJobs;
|
||||
$resource->inProgressJobs = $response->inProgressJobs;
|
||||
|
||||
$resource->totalJobsLast24h = $response->totalJobsLast24h;
|
||||
$resource->completedJobsLast24h = $response->completedJobsLast24h;
|
||||
$resource->failedJobsLast24h = $response->failedJobsLast24h;
|
||||
$resource->successRateLast24h = $response->totalJobsLast24h > 0
|
||||
? round($response->completedJobsLast24h / $response->totalJobsLast24h * 100, 1)
|
||||
: 0.0;
|
||||
|
||||
$resource->totalJobsLast7d = $response->totalJobsLast7d;
|
||||
$resource->completedJobsLast7d = $response->completedJobsLast7d;
|
||||
$resource->failedJobsLast7d = $response->failedJobsLast7d;
|
||||
$resource->successRateLast7d = $response->totalJobsLast7d > 0
|
||||
? round($response->completedJobsLast7d / $response->totalJobsLast7d * 100, 1)
|
||||
: 0.0;
|
||||
|
||||
$resource->storagePath = $response->storagePath;
|
||||
$resource->storageTotalBytes = $response->storageTotalBytes;
|
||||
$resource->storageFreeBytes = $response->storageFreeBytes;
|
||||
$resource->storageUsedBytes = $response->storageUsedBytes;
|
||||
$resource->storageTotalHuman = $this->formatBytes($response->storageTotalBytes);
|
||||
$resource->storageFreeHuman = $this->formatBytes($response->storageFreeBytes);
|
||||
$resource->storageUsedHuman = $this->formatBytes($response->storageUsedBytes);
|
||||
|
||||
$resource->totalSources = $response->totalSources;
|
||||
$resource->sourcesByHealth = $response->sourcesByHealth;
|
||||
|
||||
$resource->phpVersion = $response->phpVersion;
|
||||
$resource->generatedAt = $response->generatedAt->format(\DateTimeInterface::ATOM);
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes <= 0) {
|
||||
return '0 B';
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$exp = (int) floor(log($bytes, 1024));
|
||||
$exp = min($exp, count($units) - 1);
|
||||
|
||||
return round($bytes / (1024 ** $exp), 2) . ' ' . $units[$exp];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\System\Infrastructure\Persistence\Repository;
|
||||
|
||||
use App\Domain\System\Domain\Contract\Repository\SystemStatusRepositoryInterface;
|
||||
use App\Entity\Chapter;
|
||||
use App\Entity\ContentSource;
|
||||
use App\Entity\Manga;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class DoctrineSystemStatusRepository implements SystemStatusRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public function countMangas(): int
|
||||
{
|
||||
return (int) $this->entityManager
|
||||
->createQuery('SELECT COUNT(m) FROM ' . Manga::class . ' m')
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countMonitoredMangas(): int
|
||||
{
|
||||
return (int) $this->entityManager
|
||||
->createQuery('SELECT COUNT(m) FROM ' . Manga::class . ' m WHERE m.monitored = true')
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countMangasByStatus(): array
|
||||
{
|
||||
$results = $this->entityManager
|
||||
->createQuery('SELECT m.status, COUNT(m) as cnt FROM ' . Manga::class . ' m GROUP BY m.status')
|
||||
->getResult();
|
||||
|
||||
$counts = [];
|
||||
foreach ($results as $row) {
|
||||
$status = $row['status'] ?? 'unknown';
|
||||
$counts[$status] = (int) $row['cnt'];
|
||||
}
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
public function countChapters(): int
|
||||
{
|
||||
return (int) $this->entityManager
|
||||
->createQuery('SELECT COUNT(c) FROM ' . Chapter::class . ' c')
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countDownloadedChapters(): int
|
||||
{
|
||||
return (int) $this->entityManager
|
||||
->createQuery('SELECT COUNT(c) FROM ' . Chapter::class . ' c WHERE c.cbzPath IS NOT NULL')
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countContentSources(): int
|
||||
{
|
||||
return (int) $this->entityManager
|
||||
->createQuery('SELECT COUNT(s) FROM ' . ContentSource::class . ' s')
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countContentSourcesByHealth(): array
|
||||
{
|
||||
$results = $this->entityManager
|
||||
->createQuery('SELECT s.healthStatus, COUNT(s) as cnt FROM ' . ContentSource::class . ' s GROUP BY s.healthStatus')
|
||||
->getResult();
|
||||
|
||||
$counts = [];
|
||||
foreach ($results as $row) {
|
||||
$counts[$row['healthStatus']] = (int) $row['cnt'];
|
||||
}
|
||||
|
||||
return $counts;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user