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:
2026-05-26 18:18:47 +02:00
parent 3d3420cc4a
commit 3a5649f482
26 changed files with 983 additions and 55 deletions

View File

@@ -0,0 +1,27 @@
module Admin
class DashboardStats
ACTIVE_STATUSES = %w[live reconnecting connecting paused].freeze
def call
now = Time.current
day_start = now.beginning_of_day
month_start = now.beginning_of_month
ended_today = StreamSession.where(status: "ended").where("ended_at >= ?", day_start)
ended_month = StreamSession.where(status: "ended").where("ended_at >= ?", month_start)
{
active_sessions: StreamSession.where(status: ACTIVE_STATUSES).count,
sessions_today: ended_today.count,
sessions_month: ended_month.count,
stream_minutes_today: (ended_today.sum(:total_duration_secs).to_i / 60.0).round,
stream_minutes_month: (ended_month.sum(:total_duration_secs).to_i / 60.0).round,
teams_count: Team.count,
users_count: User.count,
matches_count: Match.count,
recordings_count: Recording.count,
recordings_ready: Recording.where(status: "ready").count
}
end
end
end

View File

@@ -0,0 +1,153 @@
module Admin
class HostMetrics
RECORDINGS_PATH = ENV.fetch("RECORDINGS_PATH", "/recordings")
ROOT_PATH = ENV.fetch("ADMIN_DISK_ROOT_PATH", "/")
def sample!
cpu = read_cpu_percent
memory = read_memory
disk = read_disk
network = read_network
sample = {
at: Time.current.iso8601,
cpu: cpu,
mem: memory[:used_percent]
}
MetricsStore.push_sample(sample)
{
cpu: cpu,
memory: memory,
disk: disk,
network: network,
history: MetricsStore.history,
load_avg: read_load_avg
}
end
private
def read_cpu_percent
a = cpu_jiffies
sleep 0.08
b = cpu_jiffies
return 0.0 unless a && b
idle_delta = b[:idle] - a[:idle]
total_delta = b[:total] - a[:total]
return 0.0 if total_delta <= 0
((1.0 - (idle_delta.to_f / total_delta)) * 100).round(1)
rescue StandardError
0.0
end
def cpu_jiffies
line = File.read("/proc/stat").each_line.find { |l| l.start_with?("cpu ") }
return nil unless line
parts = line.split[1..-1].map(&:to_i)
idle = parts[3] + (parts[4] || 0)
total = parts.sum
{ idle: idle, total: total }
rescue StandardError
nil
end
def read_memory
info = {}
File.read("/proc/meminfo").each_line do |line|
key, value = line.split(":", 2)
info[key.strip] = value.to_i
end
total = info["MemTotal"] * 1024
available = (info["MemAvailable"] || info["MemFree"]) * 1024
used = [total - available, 0].max
{
total_bytes: total,
used_bytes: used,
available_bytes: available,
used_percent: total.positive? ? ((used.to_f / total) * 100).round(1) : 0.0
}
rescue StandardError
{ total_bytes: 0, used_bytes: 0, available_bytes: 0, used_percent: 0.0 }
end
def read_disk
{
root: disk_usage_for(ROOT_PATH),
recordings: disk_usage_for(RECORDINGS_PATH)
}
end
def disk_usage_for(path)
return unavailable_disk unless path.present? && (Dir.exist?(path) || File.exist?(path))
stat = File.statvfs(path)
total = stat.blocks * stat.frsize
free = stat.bavail * stat.frsize
used = total - free
{
path: path,
total_bytes: total,
used_bytes: used,
free_bytes: free,
used_percent: total.positive? ? ((used.to_f / total) * 100).round(1) : 0.0,
dir_size_bytes: recordings_dir_size(path)
}
rescue StandardError
unavailable_disk
end
def recordings_dir_size(path)
return nil unless path == RECORDINGS_PATH && Dir.exist?(path)
`du -sb #{Shellwords.escape(path)} 2>/dev/null`.split.first.to_i
rescue StandardError
nil
end
def unavailable_disk
{ path: nil, total_bytes: nil, used_bytes: nil, free_bytes: nil, used_percent: nil, dir_size_bytes: nil }
end
def read_network
rx, tx = parse_net_bytes
prev = MetricsStore.net_totals
MetricsStore.save_net_totals(rx: rx, tx: tx)
delta_rx = prev[:rx] ? [rx - prev[:rx], 0].max : 0
delta_tx = prev[:tx] ? [tx - prev[:tx], 0].max : 0
{
rx_bytes_total: rx,
tx_bytes_total: tx,
total_bytes: rx + tx
}
rescue StandardError
{ rx_bytes_total: 0, tx_bytes_total: 0, total_bytes: 0 }
end
def parse_net_bytes
rx = 0
tx = 0
File.read("/proc/net/dev").each_line.drop(2).each do |line|
iface = line.split(":").first.strip
next if iface == "lo"
cols = line.split(":").last.split.map(&:to_i)
rx += cols[0]
tx += cols[8]
end
[rx, tx]
end
def read_load_avg
loads = File.read("/proc/loadavg").split[0..2].map(&:to_f)
{ "1m" => loads[0], "5m" => loads[1], "15m" => loads[2] }
rescue StandardError
{ "1m" => 0.0, "5m" => 0.0, "15m" => 0.0 }
end
end
end

View File

@@ -0,0 +1,42 @@
module Admin
class MetricsStore
HISTORY_KEY = "admin:metrics:history"
NET_RX_KEY = "admin:metrics:net_rx"
NET_TX_KEY = "admin:metrics:net_tx"
HISTORY_LIMIT = 90
class << self
def redis
@redis ||= Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
rescue Redis::CannotConnectError
nil
end
def push_sample(sample)
return unless redis
redis.lpush(HISTORY_KEY, sample.to_json)
redis.ltrim(HISTORY_KEY, 0, HISTORY_LIMIT - 1)
end
def history
return [] unless redis
redis.lrange(HISTORY_KEY, 0, HISTORY_LIMIT - 1).map { |row| JSON.parse(row) }.reverse
end
def net_totals
return { rx: nil, tx: nil } unless redis
{ rx: redis.get(NET_RX_KEY)&.to_i, tx: redis.get(NET_TX_KEY)&.to_i }
end
def save_net_totals(rx:, tx:)
return unless redis
redis.set(NET_RX_KEY, rx)
redis.set(NET_TX_KEY, tx)
end
end
end
end