Rende leggibile la pagina admin Nodi streaming con KPI e toolbar.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,48 +1,66 @@
|
|||||||
<% content_for :body_class, "admin-body" %>
|
<% content_for :body_class, "admin-body" %>
|
||||||
<h2><%= t("admin.stream_nodes.title") %></h2>
|
|
||||||
<p class="admin-muted">
|
<div class="admin-page-head">
|
||||||
|
<h2 class="admin-page-title"><%= t("admin.stream_nodes.title") %></h2>
|
||||||
|
<p class="muted admin-page-sub">
|
||||||
<%= t("admin.stream_nodes.providers", cloud: @cloud_provider, dns: @dns_provider) %>
|
<%= t("admin.stream_nodes.providers", cloud: @cloud_provider, dns: @dns_provider) %>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="admin-muted">
|
<% m = @autoscale_metrics %>
|
||||||
<%= t(
|
<section class="kpi-grid">
|
||||||
"admin.stream_nodes.autoscale",
|
<div class="kpi-card <%= m[:enabled] ? 'kpi-card--accent' : '' %> <%= 'kpi-card--danger' if m[:kill_switch] %>">
|
||||||
enabled: (@autoscale_metrics[:enabled] ? "ON" : "OFF"),
|
<div class="kpi-label"><%= t("admin.stream_nodes.kpi.autoscaler") %></div>
|
||||||
free: @autoscale_metrics[:free_slots],
|
<div class="kpi-value"><%= m[:kill_switch] ? t("admin.stream_nodes.kpi.kill_switch") : (m[:enabled] ? "ON" : "OFF") %></div>
|
||||||
soft: @autoscale_metrics[:soft_free_slots],
|
<div class="kpi-sub"><%= t("admin.stream_nodes.kpi.kind", kind: m[:kind]) %></div>
|
||||||
spare: @autoscale_metrics[:spare_ready],
|
</div>
|
||||||
warm: @autoscale_metrics[:warm_spare_min],
|
<div class="kpi-card <%= m[:free_slots] <= m[:soft_free_slots] ? 'kpi-card--danger' : '' %>">
|
||||||
overflow: @autoscale_metrics[:overflow_nodes],
|
<div class="kpi-label"><%= t("admin.stream_nodes.kpi.free_slots") %></div>
|
||||||
max: @autoscale_metrics[:max_overflow_nodes],
|
<div class="kpi-value"><%= m[:free_slots] %></div>
|
||||||
kind: @autoscale_metrics[:kind]
|
<div class="kpi-sub"><%= t("admin.stream_nodes.kpi.soft_slots", soft: m[:soft_free_slots]) %></div>
|
||||||
) %>
|
</div>
|
||||||
· <%= t(
|
<div class="kpi-card">
|
||||||
"admin.stream_nodes.autoscale_budget",
|
<div class="kpi-label"><%= t("admin.stream_nodes.kpi.spare") %></div>
|
||||||
eur: @autoscale_metrics[:estimated_monthly_eur],
|
<div class="kpi-value"><%= m[:spare_ready] %><span class="kpi-value-unit">/<%= m[:warm_spare_min] %></span></div>
|
||||||
budget: @autoscale_metrics[:monthly_budget_eur],
|
<div class="kpi-sub"><%= t("admin.stream_nodes.kpi.warm_spare") %></div>
|
||||||
ok: (@autoscale_metrics[:within_budget] ? "OK" : "OVER")
|
</div>
|
||||||
) %>
|
<div class="kpi-card">
|
||||||
<% if @autoscale_metrics[:kill_switch] %>
|
<div class="kpi-label"><%= t("admin.stream_nodes.kpi.overflow") %></div>
|
||||||
· <strong><%= t("admin.stream_nodes.kill_switch_active") %></strong>
|
<div class="kpi-value"><%= m[:overflow_nodes] %><span class="kpi-value-unit">/<%= m[:max_overflow_nodes] %></span></div>
|
||||||
<% end %>
|
<div class="kpi-sub"><%= t("admin.stream_nodes.kpi.overflow_nodes") %></div>
|
||||||
</p>
|
</div>
|
||||||
|
<div class="kpi-card <%= m[:within_budget] ? '' : 'kpi-card--danger' %>">
|
||||||
|
<div class="kpi-label"><%= t("admin.stream_nodes.kpi.budget") %></div>
|
||||||
|
<div class="kpi-value kpi-value--sm">€<%= m[:estimated_monthly_eur] %></div>
|
||||||
|
<div class="kpi-sub"><%= t("admin.stream_nodes.kpi.budget_of", budget: m[:monthly_budget_eur], ok: (m[:within_budget] ? "OK" : "OVER")) %></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<p>
|
<div class="panel" style="margin-bottom:1.5rem">
|
||||||
<%= button_to t("admin.stream_nodes.provision_lab"), admin_stream_nodes_path, method: :post, params: { kind: "lab" }, class: "admin-btn" %>
|
<div class="admin-panel-head">
|
||||||
|
<h2><%= t("admin.stream_nodes.actions_title") %></h2>
|
||||||
|
</div>
|
||||||
|
<div class="admin-toolbar">
|
||||||
|
<%= button_to t("admin.stream_nodes.provision_lab"), admin_stream_nodes_path, method: :post, params: { kind: "lab" }, class: "admin-btn admin-btn--secondary" %>
|
||||||
<% if @hetzner_configured %>
|
<% if @hetzner_configured %>
|
||||||
<%= button_to t("admin.stream_nodes.provision_cloud"), admin_stream_nodes_path, method: :post, params: { kind: "cloud" }, class: "admin-btn",
|
<%= button_to t("admin.stream_nodes.provision_cloud"), admin_stream_nodes_path, method: :post, params: { kind: "cloud" }, class: "admin-btn admin-btn--primary",
|
||||||
form: { data: { confirm: t("admin.stream_nodes.provision_cloud_confirm") } } %>
|
form: { data: { confirm: t("admin.stream_nodes.provision_cloud_confirm") } } %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="admin-muted"><%= t("admin.stream_nodes.hetzner_token_missing") %></span>
|
<span class="muted"><%= t("admin.stream_nodes.hetzner_token_missing") %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @autoscale_metrics[:kill_switch] %>
|
<% if m[:kill_switch] %>
|
||||||
<%= button_to t("admin.stream_nodes.clear_kill_switch"), clear_kill_switch_admin_stream_nodes_path, method: :delete, class: "admin-btn admin-btn-secondary" %>
|
<%= button_to t("admin.stream_nodes.clear_kill_switch"), clear_kill_switch_admin_stream_nodes_path, method: :delete, class: "admin-btn admin-btn--secondary" %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= button_to t("admin.stream_nodes.engage_kill_switch"), kill_switch_admin_stream_nodes_path, method: :post, class: "admin-btn admin-btn-danger",
|
<%= button_to t("admin.stream_nodes.engage_kill_switch"), kill_switch_admin_stream_nodes_path, method: :post, class: "admin-btn admin-btn--danger",
|
||||||
form: { data: { confirm: t("admin.stream_nodes.kill_switch_confirm") } } %>
|
form: { data: { confirm: t("admin.stream_nodes.kill_switch_confirm") } } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2><%= t("admin.stream_nodes.table_title") %></h2>
|
||||||
|
<% if @nodes.any? %>
|
||||||
|
<div class="admin-table-wrap">
|
||||||
<table class="admin-table">
|
<table class="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -57,27 +75,55 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% @nodes.each do |node| %>
|
<% @nodes.each do |node| %>
|
||||||
|
<%
|
||||||
|
badge =
|
||||||
|
case node.status
|
||||||
|
when "ready" then "badge--ready"
|
||||||
|
when "provisioning", "draining" then "badge--connecting"
|
||||||
|
when "error", "offline" then "badge--paused"
|
||||||
|
else "badge--paused"
|
||||||
|
end
|
||||||
|
%>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code><%= node.slug %></code></td>
|
<td><strong><%= node.slug %></strong></td>
|
||||||
<td><%= node.role %></td>
|
<td class="muted"><%= node.role %></td>
|
||||||
<td><%= node.status %></td>
|
<td><span class="badge <%= badge %>"><%= node.status %></span></td>
|
||||||
<td><%= node.active_publishers %> / <%= node.max_publishers %> (free <%= node.free_slots %>)</td>
|
<td>
|
||||||
<td><code><%= node.hostname %></code></td>
|
<strong><%= node.active_publishers %></strong>
|
||||||
<td><%= node.provider %><% if node.provider_instance_id.present? %> · <%= node.provider_instance_id %><% end %></td>
|
<span class="muted">/ <%= node.max_publishers %></span>
|
||||||
|
<div class="muted" style="font-size:0.8rem"><%= t("admin.stream_nodes.free_slots", count: node.free_slots) %></div>
|
||||||
|
</td>
|
||||||
|
<td class="admin-mono"><%= node.hostname %></td>
|
||||||
|
<td class="muted">
|
||||||
|
<%= node.provider %>
|
||||||
|
<% if node.provider_instance_id.present? %>
|
||||||
|
<div class="admin-mono" style="font-size:0.75rem;margin-top:0.2rem"><%= node.provider_instance_id %></div>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
<td class="admin-actions">
|
<td class="admin-actions">
|
||||||
<% if node.slug != "home" %>
|
<% if node.slug != "home" %>
|
||||||
<% if node.status == "ready" %>
|
<% if node.status == "ready" %>
|
||||||
<%= button_to t("admin.stream_nodes.drain"), drain_admin_stream_node_path(node), method: :post, class: "admin-btn admin-btn-secondary" %>
|
<%= button_to t("admin.stream_nodes.drain"), drain_admin_stream_node_path(node), method: :post, class: "admin-btn admin-btn--sm admin-btn--secondary" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= button_to t("admin.stream_nodes.destroy"), admin_stream_node_path(node), method: :delete, class: "admin-btn admin-btn-danger", form: { data: { confirm: t("admin.stream_nodes.destroy_confirm", slug: node.slug) } } %>
|
<%= button_to t("admin.stream_nodes.destroy"), admin_stream_node_path(node), method: :delete, class: "admin-btn admin-btn--sm admin-btn--danger",
|
||||||
|
form: { data: { confirm: t("admin.stream_nodes.destroy_confirm", slug: node.slug) } } %>
|
||||||
|
<% else %>
|
||||||
|
<span class="muted">—</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="empty"><%= t("admin.stream_nodes.empty") %></p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% if @lab_hosts.present? %>
|
<% if @lab_hosts.present? %>
|
||||||
<h3><%= t("admin.stream_nodes.hosts_title") %></h3>
|
<div class="panel" style="margin-top:1.5rem">
|
||||||
|
<h2><%= t("admin.stream_nodes.hosts_title") %></h2>
|
||||||
<pre class="admin-pre"><%= @lab_hosts %></pre>
|
<pre class="admin-pre"><%= @lab_hosts %></pre>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<title><%= t("admin.layout.title") %></title>
|
<title><%= t("admin.layout.title") %></title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
<link rel="stylesheet" href="/admin.css?v=2">
|
<link rel="stylesheet" href="/admin.css?v=4">
|
||||||
<% if content_for?(:replay_archive_styles) %>
|
<% if content_for?(:replay_archive_styles) %>
|
||||||
<link rel="stylesheet" href="/marketing.css?v=42">
|
<link rel="stylesheet" href="/marketing.css?v=42">
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -119,14 +119,35 @@ de:
|
|||||||
view_all: "Vereine ansehen (%{count} Teams)"
|
view_all: "Vereine ansehen (%{count} Teams)"
|
||||||
stream_nodes:
|
stream_nodes:
|
||||||
title: Stream-Knoten
|
title: Stream-Knoten
|
||||||
providers: "Cloud-Provider: %{cloud} · DNS-Provider: %{dns}"
|
providers: "Cloud: %{cloud} · DNS: %{dns}"
|
||||||
|
actions_title: Aktionen
|
||||||
|
table_title: Registrierte Knoten
|
||||||
|
empty: Keine Knoten registriert.
|
||||||
|
free_slots:
|
||||||
|
one: "%{count} frei"
|
||||||
|
other: "%{count} frei"
|
||||||
|
kpi:
|
||||||
|
autoscaler: Autoscaler
|
||||||
|
kill_switch: KILL
|
||||||
|
kind: "Modus %{kind}"
|
||||||
|
free_slots: Freie Slots
|
||||||
|
soft_slots: "Soft-Schwelle ≤ %{soft}"
|
||||||
|
spare: Spare
|
||||||
|
warm_spare: Warm-Spares bereit
|
||||||
|
overflow: Overflow
|
||||||
|
overflow_nodes: Cloud/Lab-Knoten
|
||||||
|
budget: Geschätztes Budget
|
||||||
|
budget_of: "von €%{budget}/Monat (%{ok})"
|
||||||
autoscale: "Autoscaler %{enabled} · free_slots=%{free} (soft≤%{soft}) · spare=%{spare}/%{warm} · overflow=%{overflow}/%{max} · kind=%{kind}"
|
autoscale: "Autoscaler %{enabled} · free_slots=%{free} (soft≤%{soft}) · spare=%{spare}/%{warm} · overflow=%{overflow}/%{max} · kind=%{kind}"
|
||||||
autoscale_budget: "Budget €%{eur}/€%{budget} (%{ok})"
|
autoscale_budget: "Budget €%{eur}/€%{budget} (%{ok})"
|
||||||
kill_switch_active: "KILL-SWITCH AKTIV"
|
kill_switch_active: "KILL-SWITCH AKTIV"
|
||||||
engage_kill_switch: "Kill-switch ON"
|
engage_kill_switch: "Kill-switch ON"
|
||||||
clear_kill_switch: "Kill-switch OFF"
|
clear_kill_switch: "Kill-switch OFF"
|
||||||
kill_switch_confirm: "Autoscaler sofort blockieren?"
|
kill_switch_confirm: "Autoscaler sofort blockieren?"
|
||||||
provision_lab: Lab-Knoten bereitstellen
|
provision_lab: Lab bereitstellen
|
||||||
|
provision_cloud: Hetzner bereitstellen
|
||||||
|
provision_cloud_confirm: "Hetzner Cloud Server + DNS auf mltv-stream.net erstellen?"
|
||||||
|
hetzner_token_missing: "HCLOUD_TOKEN setzen, um Cloud-Provisioning zu aktivieren."
|
||||||
drain: Drain
|
drain: Drain
|
||||||
destroy: Löschen
|
destroy: Löschen
|
||||||
destroy_confirm: "Knoten %{slug} löschen?"
|
destroy_confirm: "Knoten %{slug} löschen?"
|
||||||
|
|||||||
@@ -119,15 +119,33 @@ en:
|
|||||||
view_all: "View clubs (%{count} teams)"
|
view_all: "View clubs (%{count} teams)"
|
||||||
stream_nodes:
|
stream_nodes:
|
||||||
title: Streaming nodes
|
title: Streaming nodes
|
||||||
providers: "Cloud provider: %{cloud} · DNS provider: %{dns}"
|
providers: "Cloud: %{cloud} · DNS: %{dns}"
|
||||||
|
actions_title: Actions
|
||||||
|
table_title: Registered nodes
|
||||||
|
empty: No nodes registered.
|
||||||
|
free_slots:
|
||||||
|
one: "%{count} free"
|
||||||
|
other: "%{count} free"
|
||||||
|
kpi:
|
||||||
|
autoscaler: Autoscaler
|
||||||
|
kill_switch: KILL
|
||||||
|
kind: "%{kind} mode"
|
||||||
|
free_slots: Free slots
|
||||||
|
soft_slots: "soft threshold ≤ %{soft}"
|
||||||
|
spare: Spare
|
||||||
|
warm_spare: warm spares ready
|
||||||
|
overflow: Overflow
|
||||||
|
overflow_nodes: cloud/lab nodes
|
||||||
|
budget: Estimated budget
|
||||||
|
budget_of: "of €%{budget}/mo (%{ok})"
|
||||||
autoscale: "Autoscaler %{enabled} · free_slots=%{free} (soft≤%{soft}) · spare=%{spare}/%{warm} · overflow=%{overflow}/%{max} · kind=%{kind}"
|
autoscale: "Autoscaler %{enabled} · free_slots=%{free} (soft≤%{soft}) · spare=%{spare}/%{warm} · overflow=%{overflow}/%{max} · kind=%{kind}"
|
||||||
autoscale_budget: "budget €%{eur}/€%{budget} (%{ok})"
|
autoscale_budget: "budget €%{eur}/€%{budget} (%{ok})"
|
||||||
kill_switch_active: "KILL-SWITCH ACTIVE"
|
kill_switch_active: "KILL-SWITCH ACTIVE"
|
||||||
engage_kill_switch: "Kill-switch ON (block autoscaler)"
|
engage_kill_switch: "Kill-switch ON"
|
||||||
clear_kill_switch: "Kill-switch OFF"
|
clear_kill_switch: "Kill-switch OFF"
|
||||||
kill_switch_confirm: "Immediately block the autoscaler? Existing nodes stay up."
|
kill_switch_confirm: "Immediately block the autoscaler? Existing nodes stay up."
|
||||||
provision_lab: Provision lab node
|
provision_lab: Provision lab
|
||||||
provision_cloud: Provision Hetzner node
|
provision_cloud: Provision Hetzner
|
||||||
provision_cloud_confirm: "Create a Hetzner Cloud server + DNS record on mltv-stream.net? Billing applies until destroyed."
|
provision_cloud_confirm: "Create a Hetzner Cloud server + DNS record on mltv-stream.net? Billing applies until destroyed."
|
||||||
hetzner_token_missing: "Set HCLOUD_TOKEN to enable Cloud provisioning."
|
hetzner_token_missing: "Set HCLOUD_TOKEN to enable Cloud provisioning."
|
||||||
drain: Drain
|
drain: Drain
|
||||||
|
|||||||
@@ -119,14 +119,35 @@ es:
|
|||||||
view_all: "Ver clubes (%{count} equipos)"
|
view_all: "Ver clubes (%{count} equipos)"
|
||||||
stream_nodes:
|
stream_nodes:
|
||||||
title: Nodos streaming
|
title: Nodos streaming
|
||||||
providers: "Cloud provider: %{cloud} · DNS provider: %{dns}"
|
providers: "Cloud: %{cloud} · DNS: %{dns}"
|
||||||
|
actions_title: Acciones
|
||||||
|
table_title: Nodos registrados
|
||||||
|
empty: No hay nodos registrados.
|
||||||
|
free_slots:
|
||||||
|
one: "%{count} libre"
|
||||||
|
other: "%{count} libres"
|
||||||
|
kpi:
|
||||||
|
autoscaler: Autoscaler
|
||||||
|
kill_switch: KILL
|
||||||
|
kind: "modo %{kind}"
|
||||||
|
free_slots: Slots libres
|
||||||
|
soft_slots: "umbral soft ≤ %{soft}"
|
||||||
|
spare: Spare
|
||||||
|
warm_spare: warm spares listos
|
||||||
|
overflow: Overflow
|
||||||
|
overflow_nodes: nodos cloud/lab
|
||||||
|
budget: Presupuesto estimado
|
||||||
|
budget_of: "de €%{budget}/mes (%{ok})"
|
||||||
autoscale: "Autoscaler %{enabled} · free_slots=%{free} (soft≤%{soft}) · spare=%{spare}/%{warm} · overflow=%{overflow}/%{max} · kind=%{kind}"
|
autoscale: "Autoscaler %{enabled} · free_slots=%{free} (soft≤%{soft}) · spare=%{spare}/%{warm} · overflow=%{overflow}/%{max} · kind=%{kind}"
|
||||||
autoscale_budget: "presupuesto €%{eur}/€%{budget} (%{ok})"
|
autoscale_budget: "presupuesto €%{eur}/€%{budget} (%{ok})"
|
||||||
kill_switch_active: "KILL-SWITCH ACTIVO"
|
kill_switch_active: "KILL-SWITCH ACTIVO"
|
||||||
engage_kill_switch: "Kill-switch ON"
|
engage_kill_switch: "Kill-switch ON"
|
||||||
clear_kill_switch: "Kill-switch OFF"
|
clear_kill_switch: "Kill-switch OFF"
|
||||||
kill_switch_confirm: "¿Bloquear el autoscaler inmediatamente?"
|
kill_switch_confirm: "¿Bloquear el autoscaler inmediatamente?"
|
||||||
provision_lab: Provisionar nodo lab
|
provision_lab: Provisionar lab
|
||||||
|
provision_cloud: Provisionar Hetzner
|
||||||
|
provision_cloud_confirm: "¿Crear un servidor Hetzner Cloud + DNS en mltv-stream.net?"
|
||||||
|
hetzner_token_missing: "Configura HCLOUD_TOKEN para habilitar el provisioning Cloud."
|
||||||
drain: Drain
|
drain: Drain
|
||||||
destroy: Eliminar
|
destroy: Eliminar
|
||||||
destroy_confirm: "¿Eliminar el nodo %{slug}?"
|
destroy_confirm: "¿Eliminar el nodo %{slug}?"
|
||||||
|
|||||||
@@ -119,14 +119,35 @@ fr:
|
|||||||
view_all: "Voir les clubs (%{count} équipes)"
|
view_all: "Voir les clubs (%{count} équipes)"
|
||||||
stream_nodes:
|
stream_nodes:
|
||||||
title: Nœuds streaming
|
title: Nœuds streaming
|
||||||
providers: "Cloud provider: %{cloud} · DNS provider: %{dns}"
|
providers: "Cloud: %{cloud} · DNS: %{dns}"
|
||||||
|
actions_title: Actions
|
||||||
|
table_title: Nœuds enregistrés
|
||||||
|
empty: Aucun nœud enregistré.
|
||||||
|
free_slots:
|
||||||
|
one: "%{count} libre"
|
||||||
|
other: "%{count} libres"
|
||||||
|
kpi:
|
||||||
|
autoscaler: Autoscaler
|
||||||
|
kill_switch: KILL
|
||||||
|
kind: "mode %{kind}"
|
||||||
|
free_slots: Slots libres
|
||||||
|
soft_slots: "seuil soft ≤ %{soft}"
|
||||||
|
spare: Spare
|
||||||
|
warm_spare: warm spares prêts
|
||||||
|
overflow: Overflow
|
||||||
|
overflow_nodes: nœuds cloud/lab
|
||||||
|
budget: Budget estimé
|
||||||
|
budget_of: "sur €%{budget}/mois (%{ok})"
|
||||||
autoscale: "Autoscaler %{enabled} · free_slots=%{free} (soft≤%{soft}) · spare=%{spare}/%{warm} · overflow=%{overflow}/%{max} · kind=%{kind}"
|
autoscale: "Autoscaler %{enabled} · free_slots=%{free} (soft≤%{soft}) · spare=%{spare}/%{warm} · overflow=%{overflow}/%{max} · kind=%{kind}"
|
||||||
autoscale_budget: "budget €%{eur}/€%{budget} (%{ok})"
|
autoscale_budget: "budget €%{eur}/€%{budget} (%{ok})"
|
||||||
kill_switch_active: "KILL-SWITCH ACTIF"
|
kill_switch_active: "KILL-SWITCH ACTIF"
|
||||||
engage_kill_switch: "Kill-switch ON"
|
engage_kill_switch: "Kill-switch ON"
|
||||||
clear_kill_switch: "Kill-switch OFF"
|
clear_kill_switch: "Kill-switch OFF"
|
||||||
kill_switch_confirm: "Bloquer immédiatement l'autoscaler ?"
|
kill_switch_confirm: "Bloquer immédiatement l'autoscaler ?"
|
||||||
provision_lab: Provisionner un nœud lab
|
provision_lab: Provisionner lab
|
||||||
|
provision_cloud: Provisionner Hetzner
|
||||||
|
provision_cloud_confirm: "Créer un serveur Hetzner Cloud + DNS sur mltv-stream.net ?"
|
||||||
|
hetzner_token_missing: "Définir HCLOUD_TOKEN pour activer le provisioning Cloud."
|
||||||
drain: Drain
|
drain: Drain
|
||||||
destroy: Supprimer
|
destroy: Supprimer
|
||||||
destroy_confirm: "Supprimer le nœud %{slug} ?"
|
destroy_confirm: "Supprimer le nœud %{slug} ?"
|
||||||
|
|||||||
@@ -119,15 +119,33 @@ it:
|
|||||||
view_all: "Vedi società (%{count} squadre)"
|
view_all: "Vedi società (%{count} squadre)"
|
||||||
stream_nodes:
|
stream_nodes:
|
||||||
title: Nodi streaming
|
title: Nodi streaming
|
||||||
providers: "Cloud provider: %{cloud} · DNS provider: %{dns}"
|
providers: "Cloud: %{cloud} · DNS: %{dns}"
|
||||||
|
actions_title: Azioni
|
||||||
|
table_title: Nodi registrati
|
||||||
|
empty: Nessun nodo registrato.
|
||||||
|
free_slots:
|
||||||
|
one: "%{count} libero"
|
||||||
|
other: "%{count} liberi"
|
||||||
|
kpi:
|
||||||
|
autoscaler: Autoscaler
|
||||||
|
kill_switch: KILL
|
||||||
|
kind: "modalità %{kind}"
|
||||||
|
free_slots: Slot liberi
|
||||||
|
soft_slots: "soglia soft ≤ %{soft}"
|
||||||
|
spare: Spare
|
||||||
|
warm_spare: warm spare pronti
|
||||||
|
overflow: Overflow
|
||||||
|
overflow_nodes: nodi cloud/lab
|
||||||
|
budget: Budget stimato
|
||||||
|
budget_of: "su €%{budget}/mese (%{ok})"
|
||||||
autoscale: "Autoscaler %{enabled} · free_slots=%{free} (soft≤%{soft}) · spare=%{spare}/%{warm} · overflow=%{overflow}/%{max} · kind=%{kind}"
|
autoscale: "Autoscaler %{enabled} · free_slots=%{free} (soft≤%{soft}) · spare=%{spare}/%{warm} · overflow=%{overflow}/%{max} · kind=%{kind}"
|
||||||
autoscale_budget: "budget €%{eur}/€%{budget} (%{ok})"
|
autoscale_budget: "budget €%{eur}/€%{budget} (%{ok})"
|
||||||
kill_switch_active: "KILL-SWITCH ATTIVO"
|
kill_switch_active: "KILL-SWITCH ATTIVO"
|
||||||
engage_kill_switch: "Kill-switch ON (blocca autoscaler)"
|
engage_kill_switch: "Kill-switch ON"
|
||||||
clear_kill_switch: "Kill-switch OFF"
|
clear_kill_switch: "Kill-switch OFF"
|
||||||
kill_switch_confirm: "Bloccare immediatamente l'autoscaler? I nodi esistenti restano accesi."
|
kill_switch_confirm: "Bloccare immediatamente l'autoscaler? I nodi esistenti restano accesi."
|
||||||
provision_lab: Provisiona nodo lab
|
provision_lab: Provisiona lab
|
||||||
provision_cloud: Provisiona nodo Hetzner
|
provision_cloud: Provisiona Hetzner
|
||||||
provision_cloud_confirm: "Creare un server Hetzner Cloud + record DNS su mltv-stream.net? Verrà addebitato fino allo spegnimento."
|
provision_cloud_confirm: "Creare un server Hetzner Cloud + record DNS su mltv-stream.net? Verrà addebitato fino allo spegnimento."
|
||||||
hetzner_token_missing: "Imposta HCLOUD_TOKEN per abilitare il provisioning Cloud."
|
hetzner_token_missing: "Imposta HCLOUD_TOKEN per abilitare il provisioning Cloud."
|
||||||
drain: Drain
|
drain: Drain
|
||||||
|
|||||||
+103
-10
@@ -228,6 +228,8 @@ body.admin-body {
|
|||||||
.badge--live { background: var(--red); color: #fff; }
|
.badge--live { background: var(--red); color: #fff; }
|
||||||
.badge--connecting { background: #ff9800; color: #111; }
|
.badge--connecting { background: #ff9800; color: #111; }
|
||||||
.badge--paused { background: #555; color: #fff; }
|
.badge--paused { background: #555; color: #fff; }
|
||||||
|
.badge--ready { background: #1b5e20; color: #c8e6c9; }
|
||||||
|
.badge--ok { background: #1b5e20; color: #c8e6c9; }
|
||||||
|
|
||||||
.team-list {
|
.team-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@@ -345,28 +347,40 @@ body.admin-body {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-btn--secondary {
|
|
||||||
background: #333;
|
|
||||||
color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-btn--secondary:hover {
|
|
||||||
background: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-btn {
|
.admin-btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.45rem 0.9rem;
|
padding: 0.45rem 0.9rem;
|
||||||
border: none;
|
border: 1px solid transparent;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
background: #2a2a36;
|
||||||
|
color: #eee;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-btn:hover { filter: brightness(1.08); }
|
||||||
|
|
||||||
.admin-btn--sm { padding: 0.25rem 0.55rem; font-size: 0.75rem; }
|
.admin-btn--sm { padding: 0.25rem 0.55rem; font-size: 0.75rem; }
|
||||||
|
|
||||||
|
.admin-btn--primary {
|
||||||
|
background: var(--red) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border-color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn--secondary {
|
||||||
|
background: #333;
|
||||||
|
color: #eee;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn--secondary:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-btn--danger {
|
.admin-btn--danger {
|
||||||
background: var(--red);
|
background: var(--red);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -374,6 +388,85 @@ body.admin-body {
|
|||||||
|
|
||||||
.admin-btn--danger:hover { filter: brightness(1.1); }
|
.admin-btn--danger:hover { filter: brightness(1.1); }
|
||||||
|
|
||||||
|
.admin-btn--outline {
|
||||||
|
background: transparent;
|
||||||
|
border-color: #555;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-page-head {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-page-title {
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-page-sub {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-panel-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-panel-head h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.55rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-toolbar form {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-mono {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
background: #0a0a0e;
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #cfcfd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-value-unit {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-left: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-value--sm {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
.muted { color: var(--muted); }
|
.muted { color: var(--muted); }
|
||||||
.empty { color: var(--muted); font-size: 0.9rem; margin: 0; }
|
.empty { color: var(--muted); font-size: 0.9rem; margin: 0; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user