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:
2026-08-19 17:55:27 +02:00
commit 3897113313
534 changed files with 64050 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Filament\Pages\SiteHealth;
use App\Models\Page;
use App\Models\Project;
use App\Models\Service;
use App\Support\Health\Finding;
use App\Support\Health\HealthReport;
use App\Support\Health\Severity;
use App\Support\ReviewTarget;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Support\Facades\Auth;
/**
* What is waiting for somebody, at the top of the dashboard.
*
* The counters it replaces said "7 projects, 10 members" — numbers that never
* change and that nobody reads twice. What does get looked at every morning is
* whether anything needs doing, and the health checks already know: this is
* the same answers, in the first place anybody lands.
*/
class WhatNeedsYou extends StatsOverviewWidget
{
protected static ?int $sort = -10;
protected int|string|array $columnSpan = 'full';
public static function canView(): bool
{
return Auth::user()?->can('panel.access') ?? false;
}
/**
* @return array<int, Stat>
*/
protected function getStats(): array
{
$findings = app(HealthReport::class)
->runFor(Auth::user())
->filter(fn (Finding $f) => $f->severity->isProblem());
// The one stat here that is about the reader rather than the site.
// It goes first, and it goes before "all clear", because a colleague
// waiting on you is more urgent than anything a check could find —
// and unlike the bell that announced it, this does not disappear the
// moment somebody clicks it.
$reviews = $this->reviewStats();
if ($findings->isEmpty()) {
return $reviews !== [] ? $reviews : [
Stat::make(__('panel.today.all_clear'), '✓')
->description(__('panel.today.all_clear_hint'))
->descriptionIcon('heroicon-m-check-circle')
->color('success'),
];
}
// Worst first, and only a few: a dashboard listing fourteen things is
// a dashboard nobody acts on.
return [
...$reviews,
...$findings
->take($reviews === [] ? 4 : 3)
->map(fn (Finding $finding) => Stat::make($finding->title(), (string) $finding->count)
->description($finding->samples[0] ?? $finding->description())
->descriptionIcon($finding->severity === Severity::Error
? 'heroicon-m-x-circle'
: 'heroicon-m-exclamation-triangle')
->color($finding->severity->getColor())
->url($finding->url ?? SiteHealth::getUrl()))
->values()
->all(),
];
}
/**
* Work handed to this person and not yet answered, across the three things
* that can be handed over. One number rather than three: what a reviewer
* needs to know is whether anybody is waiting, not the breakdown.
*
* @return array<int, Stat>
*/
protected function reviewStats(): array
{
$user = Auth::user();
$waiting = collect([Page::class, Project::class, Service::class])
->sum(fn (string $model) => ReviewTarget::waitingFor($model, $user) ?? 0);
if ($waiting < 1) {
return [];
}
return [
Stat::make(__('panel.reviews.waiting_for_you'), (string) $waiting)
->description(__('panel.reviews.waiting_for_you_hint'))
->descriptionIcon('heroicon-m-hand-raised')
->color('warning'),
];
}
}