app/Services/PageOverview.php
Rama main · réplica de lectura del repositorio original.
<?php
namespace App\Services;
use App\Enums\ComponentStatus;
use App\Models\Component;
use App\Models\ComponentGroup;
use App\Models\Incident;
use App\Models\Subscriber;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
/**
* Everything the Overview screen shows for the selected page, gathered in one
* place so the numbers can be tested without rendering.
*
* Every query goes through the page scope of the models, so nothing from
* another page can end up here.
*/
class PageOverview
{
/** Days in the per service strips; the page wide strip uses Uptime::WINDOW_DAYS. */
public const SERVICE_DAYS = 30;
public function __construct(protected Uptime $uptime) {}
/** @return array<string, mixed> */
public function build(?Carbon $today = null): array
{
$components = Component::query()->where('enabled', true)
->with('check:id,component_id,type,enabled')
->orderBy('position')->get();
$bars = $this->uptime->barsFor($components, $today);
$percentages = array_map($this->uptime->percentageOf(...), $bars);
$counts = [];
foreach (ComponentStatus::cases() as $case) {
$counts[$case->value] = $components->filter(fn (Component $c) => $c->status === $case)->count();
}
$worst = ComponentStatus::from((int) ($components->max(fn (Component $c) => $c->status->value) ?? 1));
$open = Incident::query()->whereNull('resolved_at')
->with('updates', 'components')->orderByDesc('occurred_at')->get();
$recent = Incident::query()->where('occurred_at', '>=', now()->subDays(30))->get();
$resolved = $recent->whereNotNull('resolved_at');
return [
'components' => $components,
'counts' => $counts,
'worst' => $worst,
'affected' => $components->filter(fn (Component $c) => $c->status !== ComponentStatus::Operational)->values(),
'operational' => $counts[ComponentStatus::Operational->value],
'open' => $open,
'lastResolved' => Incident::query()->whereNotNull('resolved_at')->orderByDesc('resolved_at')->first(),
'days' => $this->uptime->aggregate($bars),
'uptime' => Uptime::average($percentages),
'incidents30' => $recent->count(),
// Same figure as the Incidents screen: mean minutes from start to resolved, last 30 days.
'mttr' => $resolved->isEmpty() ? null : (int) round(
$resolved->avg(fn (Incident $i) => $i->occurred_at->diffInMinutes($i->resolved_at)),
),
'subscribers' => Subscriber::query()->active()->count(),
'subscriptions' => Subscriptions::enabled(),
'services' => $this->services($components, $bars),
];
}
/** "34m", "5h 10m", "2d 3h": short enough for a card. */
public static function duration(?int $minutes): string
{
if ($minutes === null) {
return 'No data';
}
if ($minutes < 60) {
return $minutes.'m';
}
if ($minutes < 1440) {
return intdiv($minutes, 60).'h'.($minutes % 60 ? ' '.($minutes % 60).'m' : '');
}
return intdiv($minutes, 1440).'d'.(intdiv($minutes % 1440, 60) ? ' '.intdiv($minutes % 1440, 60).'h' : '');
}
/**
* Components per service in page order, ungrouped ones last, each with the
* last SERVICE_DAYS of its bar and the uptime over those days.
*
* @param Collection<int, Component> $components
* @param array<int, array<int, array{day: string, tone: string, pct: float, known: bool}>> $bars
* @return list<array{name: string, rows: list<array{component: Component, days: array, uptime: ?float}>}>
*/
protected function services(Collection $components, array $bars): array
{
$sections = [];
$section = function (string $name, ?int $groupId) use ($components, $bars, &$sections): void {
$rows = [];
foreach ($components->where('component_group_id', $groupId) as $component) {
$days = array_slice($bars[$component->id], -self::SERVICE_DAYS);
$rows[] = ['component' => $component, 'days' => $days, 'uptime' => $this->uptime->percentageOf($days)];
}
if ($rows !== []) {
$sections[] = ['name' => $name, 'rows' => $rows];
}
};
foreach (ComponentGroup::query()->orderBy('position')->get(['id', 'name']) as $group) {
$section($group->name, $group->id);
}
$section('Not in a service', null);
return $sections;
}
}