Admin protetto, dashboard KPI e aggiornamenti sito marketing.
Login admin con cambio password, metriche server (CPU/RAM/disco/banda), grafici e statistiche streaming; sezione piani ridimensionata. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
26
backend/app/views/admin/auth/new.html.erb
Normal file
26
backend/app/views/admin/auth/new.html.erb
Normal file
@@ -0,0 +1,26 @@
|
||||
<div style="max-width:360px;margin:4rem auto">
|
||||
<h2>Accesso admin</h2>
|
||||
<% if flash[:alert] %>
|
||||
<p style="color:#ff6b6b"><%= flash[:alert] %></p>
|
||||
<% end %>
|
||||
<%= form_with url: admin_login_path, method: :post, local: true do %>
|
||||
<p>
|
||||
<label for="username">Username</label><br>
|
||||
<input type="text" name="username" id="username" required autofocus autocomplete="username"
|
||||
style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
|
||||
</p>
|
||||
<p>
|
||||
<label for="password">Password</label><br>
|
||||
<input type="password" name="password" id="password" required autocomplete="current-password"
|
||||
style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit" style="background:#FF2D2D;color:#fff;border:0;padding:0.6rem 1.2rem;border-radius:6px;cursor:pointer;font-weight:700">
|
||||
Accedi
|
||||
</button>
|
||||
</p>
|
||||
<% end %>
|
||||
<p style="color:#888;font-size:0.85rem;margin-top:1.5rem">
|
||||
Credenziali iniziali: <code>admin</code> / <code>admin</code>. Cambia la password dopo il primo accesso.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,25 +1,131 @@
|
||||
<h2>Sessioni attive</h2>
|
||||
<% if @active_sessions.any? %>
|
||||
<table>
|
||||
<thead><tr><th>Match</th><th>Status</th><th>Iniziata</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<% @active_sessions.each do |s| %>
|
||||
<tr>
|
||||
<td><%= s.match.team.name %> vs <%= s.match.opponent_name %></td>
|
||||
<td><span class="badge live"><%= s.status %></span></td>
|
||||
<td><%= s.started_at %></td>
|
||||
<td><%= link_to "Dettaglio", admin_session_path(s) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>Nessuna sessione attiva.</p>
|
||||
<% end %>
|
||||
<% content_for :body_class, "admin-body" %>
|
||||
|
||||
<h2>Squadre</h2>
|
||||
<ul>
|
||||
<% @teams.each do |t| %>
|
||||
<li><%= link_to t.name, admin_team_path(t) %> — <%= t.matches.count %> partite</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<section class="kpi-grid">
|
||||
<div class="kpi-card kpi-card--accent">
|
||||
<div class="kpi-label">Sessioni attive</div>
|
||||
<div class="kpi-value kpi-value--live" id="kpi-active-sessions"><%= @stats[:active_sessions] %></div>
|
||||
<div class="kpi-sub">in diretta ora</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">Trasmesse oggi</div>
|
||||
<div class="kpi-value" id="kpi-sessions-today"><%= @stats[:sessions_today] %></div>
|
||||
<div class="kpi-sub"><%= format_duration_minutes(@stats[:stream_minutes_today]) %> di streaming</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">Trasmesse questo mese</div>
|
||||
<div class="kpi-value" id="kpi-sessions-month"><%= @stats[:sessions_month] %></div>
|
||||
<div class="kpi-sub"><%= format_duration_minutes(@stats[:stream_minutes_month]) %> totali</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">Squadre</div>
|
||||
<div class="kpi-value"><%= @stats[:teams_count] %></div>
|
||||
<div class="kpi-sub"><%= @stats[:users_count] %> utenti</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">CPU</div>
|
||||
<div class="kpi-value" id="kpi-cpu"><%= @host[:cpu] %>%</div>
|
||||
<div class="kpi-sub">load <%= @host[:load_avg]["1m"] %> / <%= @host[:load_avg]["5m"] %> / <%= @host[:load_avg]["15m"] %></div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">RAM</div>
|
||||
<div class="kpi-value" id="kpi-mem"><%= @host[:memory][:used_percent] %>%</div>
|
||||
<div class="kpi-sub"><%= format_bytes(@host[:memory][:used_bytes]) %> / <%= format_bytes(@host[:memory][:total_bytes]) %></div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">Banda (totale)</div>
|
||||
<div class="kpi-value" style="font-size:1.2rem" id="kpi-network"><%= format_bytes(@host[:network][:total_bytes]) %></div>
|
||||
<div class="kpi-sub">↑ <%= format_bytes(@host[:network][:tx_bytes_total]) %> · ↓ <%= format_bytes(@host[:network][:rx_bytes_total]) %></div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">Replay in archivio</div>
|
||||
<div class="kpi-value"><%= @stats[:recordings_ready] %></div>
|
||||
<div class="kpi-sub"><%= @stats[:recordings_count] %> registrazioni</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="charts-grid">
|
||||
<div class="chart-card">
|
||||
<h3>CPU (%)</h3>
|
||||
<div class="chart-wrap"><canvas id="chart-cpu"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>RAM (%)</h3>
|
||||
<div class="chart-wrap"><canvas id="chart-mem"></canvas></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="charts-grid" style="grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));">
|
||||
<div class="chart-card">
|
||||
<h3>Disco sistema</h3>
|
||||
<% root = @host[:disk][:root] %>
|
||||
<% if root[:total_bytes] %>
|
||||
<div class="disk-row">
|
||||
<header><span><%= root[:path] %></span><span><%= format_bytes(root[:used_bytes]) %> / <%= format_bytes(root[:total_bytes]) %></span></header>
|
||||
<div class="progress-bar <%= 'progress-bar--ok' if root[:used_percent].to_f < 80 %>"><span style="width:<%= root[:used_percent] %>%"></span></div>
|
||||
<p class="kpi-sub" style="margin-top:0.35rem">Libero: <%= format_bytes(root[:free_bytes]) %> (<%= root[:used_percent] %>% usato)</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="empty">Metriche disco non disponibili</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Archivio registrazioni</h3>
|
||||
<% rec = @host[:disk][:recordings] %>
|
||||
<% if rec[:total_bytes] %>
|
||||
<div class="disk-row">
|
||||
<header><span><%= rec[:path] %></span><span><%= format_bytes(rec[:used_bytes]) %> / <%= format_bytes(rec[:total_bytes]) %></span></header>
|
||||
<div class="progress-bar <%= 'progress-bar--ok' if rec[:used_percent].to_f < 80 %>"><span style="width:<%= rec[:used_percent] %>%"></span></div>
|
||||
<% if rec[:dir_size_bytes] %>
|
||||
<p class="kpi-sub" style="margin-top:0.35rem">Contenuto registrazioni: <%= format_bytes(rec[:dir_size_bytes]) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="empty">Percorso <%= Admin::HostMetrics::RECORDINGS_PATH %> non montato</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="admin-panels">
|
||||
<div class="panel">
|
||||
<h2>Sessioni attive</h2>
|
||||
<% if @active_sessions.any? %>
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr><th>Partita</th><th>Stato</th><th>Inizio</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody id="active-sessions-body">
|
||||
<% @active_sessions.each do |s| %>
|
||||
<tr>
|
||||
<td><%= s.match.team.name %> vs <%= s.match.opponent_name %></td>
|
||||
<td><span class="badge badge--<%= s.status == 'live' ? 'live' : (s.status == 'paused' ? 'paused' : 'connecting') %>"><%= s.status %></span></td>
|
||||
<td class="muted"><%= s.started_at&.strftime("%d/%m %H:%M") || "—" %></td>
|
||||
<td><%= link_to "Dettaglio", admin_session_path(s) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p class="empty">Nessuna sessione attiva in questo momento.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Squadre</h2>
|
||||
<ul class="team-list">
|
||||
<% @teams.each do |t| %>
|
||||
<li>
|
||||
<%= link_to t.name, admin_team_path(t) %>
|
||||
<span class="muted"><%= t.matches.count %> partite</span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% if @stats[:teams_count] > @teams.size %>
|
||||
<p class="kpi-sub" style="margin-top:0.75rem"><%= link_to "Vedi tutte (#{@stats[:teams_count]})", admin_teams_path %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
window.adminMetricsUrl = "<%= admin_metrics_path %>";
|
||||
window.adminInitialHistory = <%= raw @host[:history].to_json %>;
|
||||
</script>
|
||||
|
||||
29
backend/app/views/admin/passwords/edit.html.erb
Normal file
29
backend/app/views/admin/passwords/edit.html.erb
Normal file
@@ -0,0 +1,29 @@
|
||||
<div style="max-width:400px">
|
||||
<h2>Cambia password</h2>
|
||||
<% if flash[:alert] %>
|
||||
<p style="color:#ff6b6b"><%= flash[:alert] %></p>
|
||||
<% end %>
|
||||
<%= form_with url: admin_password_path, method: :patch, local: true do %>
|
||||
<p>
|
||||
<label for="current_password">Password attuale</label><br>
|
||||
<input type="password" name="current_password" id="current_password" required autocomplete="current-password"
|
||||
style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
|
||||
</p>
|
||||
<p>
|
||||
<label for="password">Nuova password (min. 8 caratteri)</label><br>
|
||||
<input type="password" name="password" id="password" required autocomplete="new-password"
|
||||
style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
|
||||
</p>
|
||||
<p>
|
||||
<label for="password_confirmation">Conferma nuova password</label><br>
|
||||
<input type="password" name="password_confirmation" id="password_confirmation" required autocomplete="new-password"
|
||||
style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit" style="background:#FF2D2D;color:#fff;border:0;padding:0.6rem 1.2rem;border-radius:6px;cursor:pointer;font-weight:700">
|
||||
Salva password
|
||||
</button>
|
||||
<%= link_to "Annulla", admin_root_path %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,11 +1,11 @@
|
||||
<h2>Stream Sessions</h2>
|
||||
<table>
|
||||
<table class="admin-table">
|
||||
<thead><tr><th>Match</th><th>Status</th><th>Disconnects</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<% @sessions.each do |s| %>
|
||||
<tr>
|
||||
<td><%= s.match.team.name %> vs <%= s.match.opponent_name %></td>
|
||||
<td><span class="badge <%= s.live? ? 'live' : 'ended' %>"><%= s.status %></span></td>
|
||||
<td><span class="badge <%= s.status == 'live' ? 'badge--live' : 'badge--paused' %>"><%= s.status %></span></td>
|
||||
<td><%= s.disconnection_count %></td>
|
||||
<td><%= link_to "Dettaglio", admin_session_path(s) %></td>
|
||||
</tr>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<h2>Teams</h2>
|
||||
<table>
|
||||
<table class="admin-table">
|
||||
<thead><tr><th>Nome</th><th>Sport</th><th>YouTube</th></tr></thead>
|
||||
<tbody>
|
||||
<% @teams.each do |t| %>
|
||||
|
||||
@@ -3,30 +3,31 @@
|
||||
<head>
|
||||
<title>Match Live TV Admin</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root { --red: #FF2D2D; --bg: #0A0A0A; --card: #1E1E1E; --text: #fff; }
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 1rem 2rem; }
|
||||
a { color: var(--red); }
|
||||
header { border-bottom: 1px solid #333; padding-bottom: 1rem; margin-bottom: 2rem; }
|
||||
h1 span { color: var(--red); }
|
||||
table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: 8px; overflow: hidden; }
|
||||
th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #333; }
|
||||
.badge { display: inline-block; padding: 0.2rem 0.6rem; border-radius: 4px; font-size: 0.8rem; }
|
||||
.live { background: var(--red); }
|
||||
.ended { background: #555; }
|
||||
nav a { margin-right: 1rem; }
|
||||
</style>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="stylesheet" href="/admin.css?v=1">
|
||||
<% if controller_name == "dashboard" %>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" crossorigin="anonymous"></script>
|
||||
<script src="/admin-dashboard.js?v=1" defer></script>
|
||||
<% end %>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<body class="<%= content_for?(:body_class) ? yield(:body_class) : 'admin-body' %>">
|
||||
<header class="admin-header">
|
||||
<h1>MATCH <span>LIVE</span> TV — Admin</h1>
|
||||
<nav>
|
||||
<%= link_to "Dashboard", admin_root_path %>
|
||||
<%= link_to "Teams", admin_teams_path %>
|
||||
<%= link_to "Sessions", admin_sessions_path %>
|
||||
<nav class="admin-nav">
|
||||
<% if admin_logged_in? %>
|
||||
<%= link_to "Dashboard", admin_root_path, class: ("active" if controller_name == "dashboard") %>
|
||||
<%= link_to "Teams", admin_teams_path, class: ("active" if controller_name == "teams") %>
|
||||
<%= link_to "Sessions", admin_sessions_path, class: ("active" if controller_name == "sessions") %>
|
||||
<%= link_to "Password", edit_admin_password_path %>
|
||||
<%= button_to "Esci", admin_logout_path, method: :delete %>
|
||||
<% end %>
|
||||
</nav>
|
||||
</header>
|
||||
<%= yield %>
|
||||
|
||||
<main class="admin-main">
|
||||
<% if flash[:notice] %><div class="admin-flash"><%= flash[:notice] %></div><% end %>
|
||||
<% if flash[:alert] %><div class="admin-flash"><%= flash[:alert] %></div><% end %>
|
||||
<%= yield %>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
|
||||
<%= render "shared/meta_tags" %>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
<link rel="stylesheet" href="/marketing.css?v=14">
|
||||
<link rel="stylesheet" href="/marketing.css?v=15">
|
||||
</head>
|
||||
<body>
|
||||
<%= render "shared/marketing_nav" %>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
|
||||
<%= render "shared/meta_tags" %>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
<link rel="stylesheet" href="/marketing.css?v=14">
|
||||
<link rel="stylesheet" href="/marketing.css?v=15">
|
||||
<link rel="stylesheet" href="/live.css">
|
||||
<%= yield :head %>
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user