components(); // An ongoing incident overrides the measurement. Monitoring can be // perfectly happy while something is broken in a way it does not look // at, and in that situation the operator's word beats the probe's. $ongoing = Incident::query() ->published()->ongoing() ->with('updates') ->orderByDesc('started_at') ->get(); $components = $this->applyIncidents($components, $ongoing); return view('status', [ 'components' => $components, 'overall' => $health->overall($components), 'history' => $this->history(), 'ongoing' => $ongoing, 'past' => $this->past(), 'maintenance' => $this->maintenance(), 'checkedAt' => Carbon::now(), 'historyDays' => self::HISTORY_DAYS, ]); } /** * Let an open incident set the state of the components it names. * * Only downwards. An incident cannot make a component look better than the * probes found it — that direction is how a status page ends up being used * to hide something. * * @param array> $components * @param Collection $ongoing * @return array> */ private function applyIncidents(array $components, Collection $ongoing): array { $rank = ['operational' => 0, 'unknown' => 1, 'degraded' => 2, 'down' => 3]; foreach ($components as $i => $component) { foreach ($ongoing as $incident) { if (! in_array($component['key'], $incident->components ?? [], true)) { continue; } // Maintenance is announced work, not a fault. It shows on the // component without claiming anything is broken. $state = $incident->impact === Incident::IMPACT_MAINTENANCE ? 'degraded' : $incident->impact; if (($rank[$state] ?? 0) > ($rank[$component['state']] ?? 0)) { $components[$i]['state'] = $state; $components[$i]['detail'] = null; } $components[$i]['incident'] = $incident; } } return $components; } /** * The daily bars, one array per component, oldest first. * * Every day in the window appears, including the ones nobody measured — * they come back as null and are drawn as a gap. Filling them in from the * days around them would be inventing the only number on this page a reader * can check against their own memory. * * @return array>, uptime: float|null}> */ private function history(): array { $from = Carbon::now()->local()->startOfDay()->subDays(self::HISTORY_DAYS - 1); $rows = StatusDay::query() ->where('day', '>=', $from->toDateString()) ->get() ->groupBy('component'); $history = []; foreach (ServiceHealth::COMPONENTS as $component) { $byDay = ($rows[$component] ?? collect())->keyBy(fn (StatusDay $d) => $d->day->toDateString()); $days = []; $measured = 0; $up = 0; for ($i = 0; $i < self::HISTORY_DAYS; $i++) { $date = $from->copy()->addDays($i); $row = $byDay[$date->toDateString()] ?? null; $days[] = [ 'date' => $date, 'state' => $row?->worst(), 'uptime' => $row?->uptimePercent(), ]; if ($row !== null) { $measured += $row->samples - $row->unknown; $up += $row->operational + $row->degraded; } } $history[$component] = [ 'days' => $days, // Over the whole window, not the mean of the daily figures: a // day with four samples must not weigh the same as a day with // three hundred. 'uptime' => $measured > 0 ? round($up / $measured * 100, 2) : null, ]; } return $history; } /** * Resolved incidents, newest first, grouped by the day they started. * * @return Collection> */ private function past(): Collection { return Incident::query() ->published() ->whereNotNull('resolved_at') ->where('started_at', '>=', Carbon::now()->subDays(self::INCIDENT_DAYS)) ->with('updates') ->orderByDesc('started_at') ->get() ->groupBy(fn (Incident $incident) => $incident->started_at->local()->toDateString()); } /** * Announced work that has not finished yet. * * Taken from the maintenance windows the console already keeps rather than * from a second list somebody has to remember to fill in. Published ones * only: a draft is a plan, not an announcement. * * @return Collection */ private function maintenance(): Collection { return MaintenanceWindow::query() ->whereNotNull('published_at') ->whereNull('cancelled_at') ->where('state', '!=', 'draft') ->where('ends_at', '>=', Carbon::now()) ->orderBy('starts_at') ->get(); } }