*/ #[Validate('required|array|min:1')] public array $components = []; #[Validate('required|date')] public string $startedAt = ''; #[Validate('required|string|max:2000')] public string $firstUpdate = ''; /** The incident an update is being written for, by uuid. */ public ?string $updatingUuid = null; #[Validate('nullable|in:investigating,identified,monitoring,resolved')] public string $updateStage = 'identified'; #[Validate('nullable|string|max:2000')] public string $updateBody = ''; public function mount(): void { // Now, in the operator's own time — they are reporting something that // is happening, not filing a historical record (R19). $this->startedAt = LocalTime::toField(now()); } public function create(): void { $this->authorize('maintenance.manage'); $this->validateOnly('title'); $this->validateOnly('impact'); $this->validateOnly('components'); $this->validateOnly('startedAt'); $this->validateOnly('firstUpdate'); // One transaction: an incident with no first update is a headline with // nothing under it, and that is what a reader would see if the second // insert failed. DB::transaction(function () { $incident = Incident::query()->create([ 'title' => $this->title, 'impact' => $this->impact, 'components' => array_values(array_intersect(ServiceHealth::COMPONENTS, $this->components)), 'started_at' => LocalTime::fromField($this->startedAt), 'published_at' => now(), 'created_by' => Auth::guard('operator')->user()?->email, ]); $incident->updates()->create([ 'status' => 'investigating', 'body' => $this->firstUpdate, 'created_by' => Auth::guard('operator')->user()?->email, ]); }); $this->reset(['title', 'impact', 'components', 'firstUpdate']); $this->startedAt = LocalTime::toField(now()); $this->dispatch('notify', message: __('admin_incidents.created')); } /** Open the update box for one incident. */ public function startUpdate(string $uuid): void { $this->authorize('maintenance.manage'); $this->updatingUuid = $uuid; $this->updateStage = 'identified'; $this->updateBody = ''; } public function cancelUpdate(): void { $this->reset(['updatingUuid', 'updateStage', 'updateBody']); } public function postUpdate(): void { $this->authorize('maintenance.manage'); $this->validate([ 'updateStage' => 'required|in:investigating,identified,monitoring,resolved', 'updateBody' => 'required|string|max:2000', ]); $incident = Incident::query()->where('uuid', $this->updatingUuid)->firstOrFail(); DB::transaction(function () use ($incident) { $incident->updates()->create([ 'status' => $this->updateStage, 'body' => $this->updateBody, 'created_by' => Auth::guard('operator')->user()?->email, ]); // "Resolved" is the update AND the closing of the incident. Two // separate actions is how a status page ends up with a resolved // note under an incident that still reports as ongoing. if ($this->updateStage === 'resolved' && ! $incident->isResolved()) { $incident->forceFill(['resolved_at' => now()])->save(); } }); $this->cancelUpdate(); $this->dispatch('notify', message: __('admin_incidents.update_posted')); } /** @return array */ public function componentOptions(): array { return ServiceHealth::COMPONENTS; } public function render() { $this->authorize('maintenance.manage'); return view('livewire.admin.incidents', [ 'ongoing' => Incident::query()->ongoing()->with('updates')->orderByDesc('started_at')->get(), 'resolved' => Incident::query() ->whereNotNull('resolved_at') ->with('updates') ->orderByDesc('started_at') ->limit(25) ->get(), ]); } }