validate(['month' => ['sometimes', 'date_format:Y-m']])['month'] ?? now('UTC')->format('Y-m'); if ($month < '2000-01' || $month > now('UTC')->format('Y-m')) { throw ValidationException::withMessages(['month' => __('Choose a month between January 2000 and this month.')]); } return $month; } public function build(string $month, bool $public = true): array { $start = CarbonImmutable::createFromFormat('!Y-m', $month, 'UTC'); $end = $start->addMonth()->min(CarbonImmutable::now('UTC')); $query = $public ? PublicComponents::query() : Component::query(); $components = $query->orderBy('position')->limit(501)->get(); if ($components->count() > 500) { throw ValidationException::withMessages(['month' => __('Reports support up to 500 components per page. Split this page to report all components.')]); } $totals = UptimeDay::whereIn('component_id', $components->modelKeys())->where('day', '>=', $start)->where('day', '<', $start->addMonth()) ->groupBy('component_id')->selectRaw('component_id, SUM(up_seconds) as up_seconds, SUM(down_seconds) as down_seconds')->get()->keyBy('component_id'); $windows = Maintenance::with('components')->whereNotNull('started_at')->where('starts_at', '<', $end)->where('ends_at', '>', $start)->limit(2001)->get(); if ($windows->count() > 2000) { throw ValidationException::withMessages(['month' => __('This month has too many maintenance windows to report accurately.')]); } $elapsed = max(0, (int) $start->diffInSeconds($end)); $rows = []; foreach ($components as $component) { $up = (int) ($totals->get($component->id)->up_seconds ?? 0); $down = (int) ($totals->get($component->id)->down_seconds ?? 0); $excluded = MaintenanceIntervals::excluded($windows, $component->id, $start, $end); $eligible = max(0, $elapsed - $excluded); $measured = $up + $down; $rows[] = ['id' => $component->id, 'name' => $component->name, 'up_seconds' => $up, 'down_seconds' => $down, 'uptime' => $measured > 0 ? round($up / $measured * 100, 2) : null, 'coverage' => $eligible > 0 ? round(min(100, $measured / $eligible * 100), 2) : null, 'excluded_seconds' => $excluded, 'unobserved_seconds' => max(0, $eligible - $measured)]; } return ['month' => $month, 'timezone' => 'UTC', 'rows' => $rows, 'elapsed_seconds' => $elapsed]; } }