Files
landing-api/app/Support/Health/HealthReport.php
Josep Piguillem 3897113313 Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer
collective, backed by the Pireon OAuth identity server. The public site is a
separate, server-rendered application consuming /api/public/v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:55:27 +02:00

161 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Health;
use App\Models\User;
use App\Support\Health\Checks\BrokenImages;
use App\Support\Health\Checks\BrokenRedirects;
use App\Support\Health\Checks\ClientsWithoutLogo;
use App\Support\Health\Checks\DuplicateSlugs;
use App\Support\Health\Checks\ExpiredCalls;
use App\Support\Health\Checks\ImagesWithoutAlt;
use App\Support\Health\Checks\IncompleteSeo;
use App\Support\Health\Checks\MissingSlugs;
use App\Support\Health\Checks\MissingTranslations;
use App\Support\Health\Checks\RedirectsToNowhere;
use App\Support\Health\Checks\ServicesWithoutWork;
use App\Support\Health\Checks\StaleRepositories;
use App\Support\Health\Checks\StaleReviews;
use App\Support\Health\Checks\StaleTranslations;
use App\Support\Health\Checks\StalledMediaQueue;
use App\Support\Health\Checks\TalksWithoutSpeaker;
use App\Support\Health\Checks\TestimonialsMissingOriginal;
use App\Support\Health\Checks\UnansweredSubmissions;
use App\Support\Health\Checks\UnresolvedMissingUrls;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
/**
* What is wrong with the public site, seen from here.
*
* The reason this page exists is that the frontend lives in another repository
* on another host: a missing translation, a slug that resolves in one language
* and not another, an image whose file is gone — none of it shows up here,
* where the data is edited. It shows up over there, usually late, usually to a
* visitor.
*/
final class HealthReport
{
/**
* @var array<int, class-string<Check>>
*/
public const CHECKS = [
// Things already wrong on the public site come first.
MissingSlugs::class,
DuplicateSlugs::class,
BrokenImages::class,
TestimonialsMissingOriginal::class,
BrokenRedirects::class,
// Then things that will bite later.
MissingTranslations::class,
// The same fault one step later: translated, and left behind by
// the language it was translated from. Nothing else in the panel
// would ever mention it — by every other measure those records
// are finished.
StaleTranslations::class,
ImagesWithoutAlt::class,
IncompleteSeo::class,
ExpiredCalls::class,
UnansweredSubmissions::class,
// Somebody handed work over and nobody answered. The failure
// mode the review step introduces: before it, an editor who
// could not publish was stuck and knew it.
StaleReviews::class,
RedirectsToNowhere::class,
// What visitors could not reach. Last of the redirect family because
// it is the one that arrives on its own: the others are found by
// looking at what is stored, this one by somebody hitting a dead link.
UnresolvedMissingUrls::class,
ServicesWithoutWork::class,
ClientsWithoutLogo::class,
TalksWithoutSpeaker::class,
StalledMediaQueue::class,
StaleRepositories::class,
];
/**
* Every check there is, worst first.
*
* For the command line, which runs as a deploy rather than as a person:
* there is nobody to hide a result from, and a release that skipped the
* checks it could not attribute to a user would be worse than useless.
*
* @return Collection<int, Finding>
*/
public function runAll(): Collection
{
return $this->execute(collect(self::CHECKS)->map(fn (string $check) => app($check)));
}
/**
* Only the checks this user is allowed to see.
*
* @return Collection<int, Finding>
*/
public function runFor(?User $user): Collection
{
return $this->execute(
collect(self::CHECKS)
->map(fn (string $check) => app($check))
->filter(fn (Check $check) => $this->visibleTo($check, $user)),
);
}
/**
* How many checks are failing, for the badge in the sidebar.
*
* Cached, because this is read on every panel page render and running ten
* checks to draw a number would make every page in the panel slower.
*/
public function problemCount(?User $user): int
{
return Cache::remember(
$this->cacheKey($user),
now()->addMinutes(5),
fn () => $this->runFor($user)->filter(fn (Finding $f) => $f->severity->isProblem())->count(),
);
}
public function forget(?User $user): void
{
Cache::forget($this->cacheKey($user));
}
/**
* @param Collection<int, Check> $checks
* @return Collection<int, Finding>
*/
protected function execute(Collection $checks): Collection
{
return $checks
->map(fn (Check $check) => $check->run())
->sortBy([
fn (Finding $a, Finding $b) => $a->severity->weight() <=> $b->severity->weight(),
fn (Finding $a, Finding $b) => $b->count <=> $a->count,
])
->values();
}
protected function visibleTo(Check $check, ?User $user): bool
{
$permission = $check->permission();
if ($permission === null) {
return true;
}
return $user?->can($permission) ?? false;
}
protected function cacheKey(?User $user): string
{
return 'health:problems:'.($user?->id ?? 'guest');
}
}