210 lines
12 KiB
PHP
210 lines
12 KiB
PHP
@php
|
|
$field = 'h-9 w-full rounded-md border border-line bg-inset px-3 text-sm text-ink focus:border-accent/40 focus:outline-none';
|
|
$label = 'mb-1 block font-mono text-[11px] uppercase tracking-wider text-ink-3';
|
|
$scopeName = function ($rule) use ($groupNames, $serverNames) {
|
|
return match ($rule->scope_type) {
|
|
'group' => $groupNames[$rule->scope_id] ?? __('alerts.scope_group'),
|
|
'server' => $serverNames[$rule->scope_id] ?? __('alerts.scope_server'),
|
|
default => __('alerts.scope_all'),
|
|
};
|
|
};
|
|
@endphp
|
|
|
|
{{-- Poll so firing/resolved incidents appear live, matching the dashboard, without a manual refresh. --}}
|
|
<div class="space-y-5" wire:poll.10s>
|
|
{{-- Header --}}
|
|
<div>
|
|
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-accent-text">{{ __('alerts.eyebrow') }}</p>
|
|
<h2 class="mt-0.5 font-display text-xl font-semibold text-ink sm:text-2xl">{{ __('alerts.title') }}</h2>
|
|
<p class="mt-1 text-sm text-ink-3">{{ __('alerts.subtitle') }}</p>
|
|
</div>
|
|
|
|
{{-- No delivery channel reaches anyone → alerts would fire silently. Warn prominently. --}}
|
|
@unless ($channelsReach)
|
|
<div class="flex items-start gap-2.5 rounded-lg border border-warning/25 bg-warning/10 px-4 py-3">
|
|
<x-icon name="alert" class="mt-0.5 h-4 w-4 shrink-0 text-warning" />
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-medium text-ink">{{ __('alerts.no_channel_title') }}</p>
|
|
<p class="mt-0.5 text-xs text-ink-3">{{ __('alerts.no_channel_hint') }}</p>
|
|
</div>
|
|
</div>
|
|
@endunless
|
|
|
|
{{-- Active incidents --}}
|
|
<x-panel :title="__('alerts.incidents_title')" :subtitle="__('alerts.incidents_subtitle')" :padded="false">
|
|
@if ($incidents->isEmpty())
|
|
<div class="flex flex-col items-center gap-2 py-8 text-center">
|
|
<x-status-dot status="online" class="h-2.5 w-2.5" />
|
|
<p class="font-display text-sm font-semibold text-ink">{{ __('alerts.no_incidents_title') }}</p>
|
|
<p class="text-xs text-ink-3">{{ __('alerts.no_incidents') }}</p>
|
|
</div>
|
|
@else
|
|
<div class="divide-y divide-line">
|
|
@foreach ($incidents as $incident)
|
|
<div wire:key="inc-{{ $incident->id }}" class="flex flex-wrap items-center gap-3 px-4 py-3 sm:px-5">
|
|
<x-status-dot status="offline" :ping="true" class="h-2.5 w-2.5" />
|
|
<div class="min-w-0 flex-1">
|
|
<p class="truncate text-sm font-medium text-ink">{{ $incident->rule?->name ?? '—' }}</p>
|
|
<p class="truncate font-mono text-[11px] text-ink-3">{{ $incident->server?->name ?? '—' }} · {{ __('alerts.incident_value', ['value' => $incident->value]) }}</p>
|
|
</div>
|
|
<span class="shrink-0 font-mono text-[11px] text-ink-4">{{ __('alerts.incident_since', ['time' => $incident->started_at?->diffForHumans() ?? '—']) }}</span>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</x-panel>
|
|
|
|
{{-- New rule --}}
|
|
<x-panel :title="__('alerts.new_rule')">
|
|
<form wire:submit="createRule" class="space-y-4">
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.name_label') }}</label>
|
|
<input wire:model="name" type="text" maxlength="80" placeholder="{{ __('alerts.name_placeholder') }}" class="{{ $field }} placeholder:text-ink-4" />
|
|
@error('name') <p class="mt-1 flex items-center gap-1.5 font-mono text-[11px] text-offline"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p> @enderror
|
|
</div>
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.metric_label') }}</label>
|
|
<select wire:model.live="metric" class="{{ $field }}">
|
|
@foreach ($metrics as $m)
|
|
<option value="{{ $m }}">{{ __('alerts.metric_'.$m) }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
@if ($metric !== 'offline')
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.comparator_label') }}</label>
|
|
<select wire:model="comparator" class="{{ $field }}">
|
|
@foreach ($comparators as $c)
|
|
<option value="{{ $c }}">{{ __('alerts.comparator_'.$c) }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.threshold_label') }}</label>
|
|
<input wire:model="threshold" type="number" min="0" max="100" class="{{ $field }} font-mono" />
|
|
@error('threshold') <p class="mt-1 flex items-center gap-1.5 font-mono text-[11px] text-offline"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p> @enderror
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.scope_label') }}</label>
|
|
<select wire:model.live="scopeType" class="{{ $field }}">
|
|
@foreach ($scopes as $s)
|
|
<option value="{{ $s }}">{{ __('alerts.scope_'.$s) }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
@if ($scopeType === 'group')
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.scope_target_label') }}</label>
|
|
<select wire:model="scopeUuid" class="{{ $field }}">
|
|
<option value="">—</option>
|
|
@foreach ($groups as $grp)
|
|
<option value="{{ $grp->uuid }}">{{ $grp->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
@elseif ($scopeType === 'server')
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.scope_target_label') }}</label>
|
|
<select wire:model="scopeUuid" class="{{ $field }}">
|
|
<option value="">—</option>
|
|
@foreach ($servers as $srv)
|
|
<option value="{{ $srv->uuid }}">{{ $srv->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="flex justify-end">
|
|
<x-btn variant="primary" type="submit">{{ __('alerts.create') }}</x-btn>
|
|
</div>
|
|
</form>
|
|
</x-panel>
|
|
|
|
{{-- Rules --}}
|
|
<x-panel :title="__('alerts.rules_title')" :padded="false">
|
|
@if ($rules->isEmpty())
|
|
<div class="px-4 py-8 text-center sm:px-5">
|
|
<p class="font-display text-sm font-semibold text-ink">{{ __('alerts.no_rules_title') }}</p>
|
|
<p class="mt-1 text-xs text-ink-3">{{ __('alerts.no_rules') }}</p>
|
|
</div>
|
|
@else
|
|
<div class="divide-y divide-line">
|
|
@foreach ($rules as $rule)
|
|
<div wire:key="rule-{{ $rule->uuid }}" class="flex flex-wrap items-center gap-3 px-4 py-3 sm:px-5">
|
|
<div class="min-w-0 flex-1">
|
|
<p class="truncate text-sm font-medium text-ink">{{ $rule->name }}</p>
|
|
<p class="truncate font-mono text-[11px] text-ink-3">
|
|
@if ($rule->metric === 'offline')
|
|
{{ __('alerts.rule_summary_offline', ['scope' => $scopeName($rule)]) }}
|
|
@else
|
|
{{ __('alerts.rule_summary', ['scope' => $scopeName($rule), 'metric' => __('alerts.metric_'.$rule->metric), 'comparator' => __('alerts.comparator_'.$rule->comparator), 'threshold' => $rule->threshold]) }}
|
|
@endif
|
|
</p>
|
|
</div>
|
|
<button type="button" wire:click="toggleRule('{{ $rule->uuid }}')"
|
|
@class([
|
|
'inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border px-2.5 text-xs transition-colors',
|
|
'border-online/20 bg-online/10 text-online' => $rule->enabled,
|
|
'border-line bg-inset text-ink-3 hover:bg-raised' => ! $rule->enabled,
|
|
])>
|
|
<x-status-dot :status="$rule->enabled ? 'online' : 'offline'" class="h-2 w-2" />
|
|
{{ $rule->enabled ? __('alerts.enabled') : __('alerts.disabled') }}
|
|
</button>
|
|
<x-modal-trigger variant="danger-soft" action="confirmDeleteRule('{{ $rule->uuid }}')">{{ __('common.delete') }}</x-modal-trigger>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</x-panel>
|
|
|
|
{{-- Channels --}}
|
|
<x-panel :title="__('alerts.channels_title')" :subtitle="__('alerts.channels_subtitle')">
|
|
<form wire:submit="saveChannels" class="space-y-4">
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.email_label') }}</label>
|
|
<textarea wire:model="emailTo" rows="2" class="{{ $field }} h-auto py-2.5 font-mono" placeholder="ops@example.com"></textarea>
|
|
<p class="mt-1 font-mono text-[11px] text-ink-4">{{ __('alerts.email_hint') }}</p>
|
|
</div>
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.webhooks_label') }}</label>
|
|
<textarea wire:model="webhooks" rows="2" class="{{ $field }} h-auto py-2.5 font-mono" placeholder="https://hooks.slack.com/…"></textarea>
|
|
@error('webhooks') <p class="mt-1 flex items-center gap-1.5 font-mono text-[11px] text-offline"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p> @enderror
|
|
<p class="mt-1 font-mono text-[11px] text-ink-4">{{ __('alerts.webhooks_hint') }}</p>
|
|
</div>
|
|
|
|
{{-- Gotify push (self-hostable) — a real push when a service goes down, no e-mail needed. --}}
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.gotify_url_label') }}</label>
|
|
<input wire:model="gotifyUrl" type="url" class="{{ $field }} font-mono" placeholder="https://gotify.example.com" />
|
|
@error('gotifyUrl') <p class="mt-1 flex items-center gap-1.5 font-mono text-[11px] text-offline"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p> @enderror
|
|
</div>
|
|
<div>
|
|
<label class="{{ $label }}">{{ __('alerts.gotify_token_label') }}</label>
|
|
<div class="flex items-center gap-2">
|
|
<input wire:model="gotifyToken" type="password" autocomplete="off" class="{{ $field }} font-mono"
|
|
placeholder="{{ $gotifyTokenSet ? __('alerts.gotify_token_set') : __('alerts.gotify_token_placeholder') }}" />
|
|
@if ($gotifyTokenSet)
|
|
<x-btn variant="danger-soft" size="field" wire:click="clearGotifyToken" type="button" class="shrink-0">{{ __('common.remove') }}</x-btn>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="font-mono text-[11px] text-ink-4">{{ __('alerts.gotify_hint') }}</p>
|
|
|
|
<div class="flex justify-end">
|
|
<x-btn variant="primary" type="submit">{{ __('alerts.save') }}</x-btn>
|
|
</div>
|
|
</form>
|
|
</x-panel>
|
|
</div>
|