Aggiunge monitoraggio ops: health check, log scanner, dashboard e notifiche.

Introduce incidenti con dedup, job Sidekiq ogni 3 min, scan log cron, /up/deep,
dashboard /admin/ops e push ntfy; include Sentry opzionale e fix test suite.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 08:16:47 +02:00
parent ce8939fffb
commit 52cfffb83f
44 changed files with 1396 additions and 24 deletions

View File

@@ -54,6 +54,19 @@
</div>
</section>
<section class="panel" style="margin-bottom:1.25rem">
<h2>Stato piattaforma (Ops)</h2>
<% critical = Ops::Incident.critical_open.count %>
<% warnings = Ops::Incident.open.where(severity: "warning").count %>
<% if critical.positive? %>
<p class="admin-flash" style="margin-bottom:0.75rem"><%= critical %> incidente/i critico/i aperto/i — <%= link_to "Vedi dashboard Ops", admin_ops_path %></p>
<% elsif warnings.positive? %>
<p class="kpi-sub" style="margin-bottom:0.75rem"><%= warnings %> warning — <%= link_to "Dashboard Ops", admin_ops_path %></p>
<% else %>
<p class="empty" style="margin:0">Nessun incidente aperto. <%= link_to "Dashboard Ops", admin_ops_path %></p>
<% end %>
</section>
<section class="charts-grid" style="grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));">
<div class="chart-card">
<h3>Disco sistema</h3>

View File

@@ -0,0 +1,83 @@
<% content_for :body_class, "admin-body" %>
<section class="kpi-grid">
<div class="kpi-card <%= @summary[:open_critical].positive? ? 'kpi-card--danger' : 'kpi-card--accent' %>">
<div class="kpi-label">Critici aperti</div>
<div class="kpi-value"><%= @summary[:open_critical] %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Warning aperti</div>
<div class="kpi-value"><%= @summary[:open_warning] %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Totale aperti</div>
<div class="kpi-value"><%= @summary[:open_total] %></div>
</div>
</section>
<div class="panel" style="margin-bottom:1.5rem">
<h2>Incidenti aperti</h2>
<% if @open_incidents.any? %>
<table class="admin-table">
<thead>
<tr>
<th>Severità</th>
<th>Tipo</th>
<th>Titolo</th>
<th>Occ.</th>
<th>Ultimo</th>
<th></th>
</tr>
</thead>
<tbody>
<% @open_incidents.each do |inc| %>
<tr>
<td><span class="badge badge--<%= inc.severity == 'critical' ? 'live' : 'paused' %>"><%= inc.severity %></span></td>
<td class="muted"><%= inc.kind %></td>
<td>
<strong><%= inc.title %></strong>
<% if inc.message.present? %>
<div class="muted" style="font-size:0.85rem;margin-top:0.25rem"><%= truncate(inc.message, length: 120) %></div>
<% end %>
</td>
<td><%= inc.occurrence_count %></td>
<td class="muted"><%= inc.last_seen_at&.strftime("%d/%m %H:%M") %></td>
<td class="admin-actions">
<% unless inc.status == 'acknowledged' %>
<%= button_to "Preso in carico", acknowledge_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm" %>
<% end %>
<%= button_to "Risolvi", resolve_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm" %>
<%= button_to "Mute 24h", mute_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm admin-btn--outline" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p class="empty">Nessun incidente aperto. La piattaforma risulta sana.</p>
<% end %>
</div>
<div class="panel">
<h2>Risolti (ultimi 30 giorni)</h2>
<% if @resolved_incidents.any? %>
<table class="admin-table">
<thead>
<tr><th>Severità</th><th>Tipo</th><th>Titolo</th><th>Occ.</th><th>Risolto</th></tr>
</thead>
<tbody>
<% @resolved_incidents.each do |inc| %>
<tr>
<td><span class="badge badge--connecting"><%= inc.severity %></span></td>
<td class="muted"><%= inc.kind %></td>
<td><%= inc.title %></td>
<td><%= inc.occurrence_count %></td>
<td class="muted"><%= inc.updated_at.strftime("%d/%m %H:%M") %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p class="empty">Nessun incidente risolto di recente.</p>
<% end %>
</div>

View File

@@ -16,6 +16,10 @@
<nav class="admin-nav">
<% if admin_logged_in? %>
<%= link_to "Dashboard", admin_root_path, class: ("active" if controller_name == "dashboard") %>
<% ops_critical = Ops::Incident.critical_open.count %>
<%= link_to admin_ops_path, class: ("active" if controller_name == "ops") do %>
Ops<% if ops_critical.positive? %> <span class="admin-nav-badge"><%= ops_critical %></span><% end %>
<% end %>
<%= link_to "Società e squadre", admin_clubs_path, class: ("active" if controller_name.in?(%w[clubs teams])) %>
<%= link_to "Fatturazione", admin_billing_path, class: ("active" if controller_name.in?(%w[billing billing_invoices])) %>
<%= link_to "YouTube", admin_youtube_platform_path, class: ("active" if controller_name == "youtube") %>

View File

@@ -0,0 +1,10 @@
<%= @incident.title %>
Severità: <%= @incident.severity %>
Tipo: <%= @incident.kind %>
Occorrenze: <%= @incident.occurrence_count %>
Ultimo avvistamento: <%= @incident.last_seen_at %>
<%= @incident.message %>
Dashboard: <%= ENV.fetch("APP_PUBLIC_URL", "https://www.matchlivetv.it").chomp("/") %>/admin/ops