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>
This commit is contained in:
160
app/Support/Health/HealthReport.php
Normal file
160
app/Support/Health/HealthReport.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user