Permette titolo, testo e link diversi per Android e iOS negli avvisi.
Separa i canali app e mostra il nodo ingest nelle sessioni admin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,7 +11,8 @@ module Admin
|
||||
kind: "info",
|
||||
severity: "info",
|
||||
status: "draft",
|
||||
show_on_app: true,
|
||||
show_on_android: true,
|
||||
show_on_ios: true,
|
||||
show_on_web_public: true,
|
||||
show_on_web_private: true,
|
||||
dismissible: true
|
||||
@@ -55,12 +56,18 @@ module Admin
|
||||
params.require(:app_announcement).permit(
|
||||
:kind, :severity, :status, :title, :body,
|
||||
:action_url, :action_label, :dismissible, :starts_at, :ends_at,
|
||||
:show_on_app, :show_on_web_public, :show_on_web_private
|
||||
:show_on_android, :show_on_ios, :show_on_web_public, :show_on_web_private,
|
||||
:android_title, :android_body, :android_action_url, :android_action_label,
|
||||
:ios_title, :ios_body, :ios_action_url, :ios_action_label
|
||||
).tap do |permitted|
|
||||
%i[dismissible show_on_app show_on_web_public show_on_web_private].each do |key|
|
||||
%i[dismissible show_on_android show_on_ios show_on_web_public show_on_web_private].each do |key|
|
||||
permitted[key] = ActiveModel::Type::Boolean.new.cast(permitted[key])
|
||||
end
|
||||
%i[starts_at ends_at action_url action_label].each do |key|
|
||||
%i[
|
||||
starts_at ends_at action_url action_label
|
||||
android_title android_body android_action_url android_action_label
|
||||
ios_title ios_body ios_action_url ios_action_label
|
||||
].each do |key|
|
||||
permitted[key] = nil if permitted[key].blank?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ module Admin
|
||||
@host = HostMetrics.new.sample!
|
||||
@active_sessions = StreamSession
|
||||
.where(status: DashboardStats::ACTIVE_STATUSES)
|
||||
.includes(match: :team)
|
||||
.includes(:stream_node, match: :team)
|
||||
.order(started_at: :desc)
|
||||
@teams = Team.includes(:matches).order(:name).limit(8)
|
||||
end
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
module Admin
|
||||
class SessionsController < Admin::BaseController
|
||||
def index
|
||||
@sessions = StreamSession.includes(:match, :user).order(created_at: :desc).limit(50)
|
||||
@sessions = StreamSession.includes(:stream_node, :user, match: :team).order(created_at: :desc).limit(50)
|
||||
end
|
||||
|
||||
def show
|
||||
@session = StreamSession.find(params[:id])
|
||||
@session = StreamSession.includes(:stream_node, match: :team).find(params[:id])
|
||||
@events = @session.stream_events.recent.limit(50)
|
||||
end
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ module Api
|
||||
|
||||
def index
|
||||
platform = params[:platform].to_s.presence
|
||||
announcements = AppAnnouncement.for_app.newest
|
||||
announcements = AppAnnouncement.for_platform(platform).newest
|
||||
render json: announcements.map { |item| item.as_api_json(platform: platform) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,6 +30,19 @@ module AdminHelper
|
||||
links
|
||||
end
|
||||
|
||||
def admin_session_ingest_badge_class(node)
|
||||
case node.role
|
||||
when "home" then "badge--ingest-home"
|
||||
when "lab" then "badge--ingest-lab"
|
||||
when "cloud" then "badge--ingest-cloud"
|
||||
else "badge--paused"
|
||||
end
|
||||
end
|
||||
|
||||
def admin_session_ingest_role_label(node)
|
||||
I18n.t("admin.sessions.ingest.role.#{node.role}", default: node.role.to_s.humanize)
|
||||
end
|
||||
|
||||
def admin_regia_expires_label(iso_time)
|
||||
return if iso_time.blank?
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ class AppAnnouncement < ApplicationRecord
|
||||
SEVERITIES = %w[info warning critical].freeze
|
||||
STATUSES = %w[draft published archived].freeze
|
||||
CHANNELS = {
|
||||
app: :show_on_app,
|
||||
android: :show_on_android,
|
||||
ios: :show_on_ios,
|
||||
web_public: :show_on_web_public,
|
||||
web_private: :show_on_web_private
|
||||
}.freeze
|
||||
URL_FIELDS = %i[action_url android_action_url ios_action_url].freeze
|
||||
|
||||
belongs_to :created_by_admin, class_name: "AdminAccount", optional: true
|
||||
|
||||
@@ -14,10 +16,12 @@ class AppAnnouncement < ApplicationRecord
|
||||
validates :kind, inclusion: { in: KINDS }
|
||||
validates :severity, inclusion: { in: SEVERITIES }
|
||||
validates :status, inclusion: { in: STATUSES }
|
||||
validates :title, length: { maximum: 120 }
|
||||
validates :body, length: { maximum: 2000 }
|
||||
validates :action_label, length: { maximum: 40 }, allow_blank: true
|
||||
validates :action_url, format: { with: /\Ahttps?:\/\/.+/i, allow_blank: true }
|
||||
validates :title, :android_title, :ios_title, length: { maximum: 120 }, allow_blank: true
|
||||
validates :body, :android_body, :ios_body, length: { maximum: 2000 }, allow_blank: true
|
||||
validates :action_label, :android_action_label, :ios_action_label, length: { maximum: 40 }, allow_blank: true
|
||||
URL_FIELDS.each do |field|
|
||||
validates field, format: { with: /\Ahttps?:\/\/.+/i, allow_blank: true }
|
||||
end
|
||||
validate :ends_at_after_starts_at
|
||||
validate :at_least_one_channel
|
||||
|
||||
@@ -38,7 +42,15 @@ class AppAnnouncement < ApplicationRecord
|
||||
end
|
||||
|
||||
def self.for_app
|
||||
for_channel(:app)
|
||||
active_now.where("show_on_android = TRUE OR show_on_ios = TRUE")
|
||||
end
|
||||
|
||||
def self.for_platform(platform)
|
||||
case platform.to_s
|
||||
when "android" then for_channel(:android)
|
||||
when "ios" then for_channel(:ios)
|
||||
else for_app
|
||||
end
|
||||
end
|
||||
|
||||
def published?
|
||||
@@ -56,17 +68,19 @@ class AppAnnouncement < ApplicationRecord
|
||||
id: id,
|
||||
kind: kind,
|
||||
severity: severity,
|
||||
title: title,
|
||||
body: body,
|
||||
title: copy_for(platform, :title),
|
||||
body: copy_for(platform, :body),
|
||||
dismissible: dismissible,
|
||||
action_url: resolved_action_url(platform),
|
||||
action_label: action_label.presence,
|
||||
action_label: copy_for(platform, :action_label).presence,
|
||||
starts_at: starts_at&.iso8601,
|
||||
ends_at: ends_at&.iso8601
|
||||
}
|
||||
end
|
||||
|
||||
def resolved_action_url(platform)
|
||||
override = platform_override(platform, :action_url)
|
||||
return override if override.present?
|
||||
return action_url if action_url.present?
|
||||
return nil unless kind == "update"
|
||||
|
||||
@@ -78,8 +92,23 @@ class AppAnnouncement < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def copy_for(platform, field)
|
||||
override = platform_override(platform, field)
|
||||
override.presence || public_send(field)
|
||||
end
|
||||
|
||||
def platform_override(platform, field)
|
||||
prefix = case platform.to_s
|
||||
when "android" then :android
|
||||
when "ios" then :ios
|
||||
end
|
||||
return if prefix.blank?
|
||||
|
||||
public_send("#{prefix}_#{field}")
|
||||
end
|
||||
|
||||
def at_least_one_channel
|
||||
return if show_on_app? || show_on_web_public? || show_on_web_private?
|
||||
return if show_on_android? || show_on_ios? || show_on_web_public? || show_on_web_private?
|
||||
|
||||
errors.add(:base, :no_channel)
|
||||
end
|
||||
|
||||
@@ -19,8 +19,12 @@
|
||||
<fieldset class="admin-channels">
|
||||
<legend><%= t("admin.announcements.form.channels") %></legend>
|
||||
<label class="admin-checkbox">
|
||||
<%= f.check_box :show_on_app %>
|
||||
<span><%= t("admin.announcements.channels.app") %></span>
|
||||
<%= f.check_box :show_on_android %>
|
||||
<span><%= t("admin.announcements.channels.android") %></span>
|
||||
</label>
|
||||
<label class="admin-checkbox">
|
||||
<%= f.check_box :show_on_ios %>
|
||||
<span><%= t("admin.announcements.channels.ios") %></span>
|
||||
</label>
|
||||
<label class="admin-checkbox">
|
||||
<%= f.check_box :show_on_web_public %>
|
||||
@@ -43,6 +47,59 @@
|
||||
<%= f.text_area :body, required: true, rows: 5, maxlength: 2000 %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :action_url, t("admin.announcements.form.action_url") %>
|
||||
<%= f.url_field :action_url, placeholder: "https://" %>
|
||||
</div>
|
||||
<p class="muted admin-form-hint"><%= t("admin.announcements.form.action_url_hint") %></p>
|
||||
|
||||
<div>
|
||||
<%= f.label :action_label, t("admin.announcements.form.action_label") %>
|
||||
<%= f.text_field :action_label, maxlength: 40 %>
|
||||
</div>
|
||||
|
||||
<fieldset class="admin-channels admin-platform-copy">
|
||||
<legend><%= t("admin.announcements.form.android_override") %></legend>
|
||||
<p class="muted admin-form-hint"><%= t("admin.announcements.form.platform_override_hint") %></p>
|
||||
<div>
|
||||
<%= f.label :android_title, t("admin.announcements.form.title") %>
|
||||
<%= f.text_field :android_title, maxlength: 120 %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :android_body, t("admin.announcements.form.body") %>
|
||||
<%= f.text_area :android_body, rows: 4, maxlength: 2000 %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :android_action_url, t("admin.announcements.form.action_url") %>
|
||||
<%= f.url_field :android_action_url, placeholder: "https://play.google.com/..." %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :android_action_label, t("admin.announcements.form.action_label") %>
|
||||
<%= f.text_field :android_action_label, maxlength: 40 %>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="admin-channels admin-platform-copy">
|
||||
<legend><%= t("admin.announcements.form.ios_override") %></legend>
|
||||
<p class="muted admin-form-hint"><%= t("admin.announcements.form.platform_override_hint") %></p>
|
||||
<div>
|
||||
<%= f.label :ios_title, t("admin.announcements.form.title") %>
|
||||
<%= f.text_field :ios_title, maxlength: 120 %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :ios_body, t("admin.announcements.form.body") %>
|
||||
<%= f.text_area :ios_body, rows: 4, maxlength: 2000 %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :ios_action_url, t("admin.announcements.form.action_url") %>
|
||||
<%= f.url_field :ios_action_url, placeholder: "https://apps.apple.com/..." %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :ios_action_label, t("admin.announcements.form.action_label") %>
|
||||
<%= f.text_field :ios_action_label, maxlength: 40 %>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="admin-form-row">
|
||||
<div>
|
||||
<%= f.label :starts_at, t("admin.announcements.form.starts_at") %>
|
||||
@@ -55,17 +112,6 @@
|
||||
</div>
|
||||
<p class="muted admin-form-hint"><%= t("admin.announcements.form.window_hint") %></p>
|
||||
|
||||
<div>
|
||||
<%= f.label :action_url, t("admin.announcements.form.action_url") %>
|
||||
<%= f.url_field :action_url, placeholder: "https://" %>
|
||||
</div>
|
||||
<p class="muted admin-form-hint"><%= t("admin.announcements.form.action_url_hint") %></p>
|
||||
|
||||
<div>
|
||||
<%= f.label :action_label, t("admin.announcements.form.action_label") %>
|
||||
<%= f.text_field :action_label, maxlength: 40 %>
|
||||
</div>
|
||||
|
||||
<label class="admin-checkbox">
|
||||
<%= f.check_box :dismissible %>
|
||||
<span><%= t("admin.announcements.form.dismissible") %></span>
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
<tr>
|
||||
<th><%= t("admin.dashboard.sessions.table.match") %></th>
|
||||
<th><%= t("admin.dashboard.sessions.table.status") %></th>
|
||||
<th><%= t("admin.dashboard.sessions.table.ingest") %></th>
|
||||
<th><%= t("admin.dashboard.sessions.table.start") %></th>
|
||||
<th><%= t("admin.dashboard.sessions.table.link") %></th>
|
||||
<th></th>
|
||||
@@ -117,6 +118,7 @@
|
||||
<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><%= render "admin/sessions/ingest_cell", session: s %></td>
|
||||
<td class="muted"><%= s.started_at&.strftime("%d/%m %H:%M") || t("admin.common.dash") %></td>
|
||||
<td class="admin-link-compact">
|
||||
<% admin_session_watch_links(s).each do |link| %>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<% node = session.stream_node %>
|
||||
<% if node %>
|
||||
<div class="admin-ingest">
|
||||
<code class="admin-ingest__slug"><%= node.slug %></code>
|
||||
<span class="badge <%= admin_session_ingest_badge_class(node) %>"><%= admin_session_ingest_role_label(node) %></span>
|
||||
</div>
|
||||
<% else %>
|
||||
<span class="muted"><%= t("admin.sessions.ingest.none") %></span>
|
||||
<% end %>
|
||||
@@ -4,6 +4,7 @@
|
||||
<tr>
|
||||
<th><%= t("admin.sessions.index.table.match") %></th>
|
||||
<th><%= t("admin.sessions.index.table.status") %></th>
|
||||
<th><%= t("admin.sessions.index.table.ingest") %></th>
|
||||
<th><%= t("admin.sessions.index.table.disconnects") %></th>
|
||||
<th><%= t("admin.sessions.index.table.link") %></th>
|
||||
<th></th>
|
||||
@@ -14,6 +15,7 @@
|
||||
<tr>
|
||||
<td><%= s.match.team.name %> vs <%= s.match.opponent_name %></td>
|
||||
<td><span class="badge <%= s.status == 'live' ? 'badge--live' : 'badge--paused' %>"><%= s.status %></span></td>
|
||||
<td><%= render "admin/sessions/ingest_cell", session: s %></td>
|
||||
<td><%= s.disconnection_count %></td>
|
||||
<td class="admin-link-compact">
|
||||
<% admin_session_watch_links(s).each do |link| %>
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
<% if @session.youtube_broadcast_id %>
|
||||
<p><%= t("admin.sessions.show.youtube_studio") %>: <a href="https://studio.youtube.com/video/<%= @session.youtube_broadcast_id %>/livestreaming" target="_blank" rel="noopener"><%= t("admin.sessions.show.broadcast") %></a></p>
|
||||
<% end %>
|
||||
<p>
|
||||
<%= t("admin.sessions.show.ingest_node") %>
|
||||
<% if @session.stream_node %>
|
||||
<code><%= @session.stream_node.slug %></code>
|
||||
<span class="badge <%= admin_session_ingest_badge_class(@session.stream_node) %>"><%= admin_session_ingest_role_label(@session.stream_node) %></span>
|
||||
<span class="muted">(<%= @session.stream_node.provider %>)</span>
|
||||
<% else %>
|
||||
<span class="muted"><%= t("admin.sessions.ingest.none") %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
<p><%= t("admin.sessions.show.rtmp_ingest") %> <code><%= @session.rtmp_ingest_url %></code></p>
|
||||
|
||||
<h3><%= t("admin.sessions.show.events_title") %></h3>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<title><%= t("admin.layout.title") %></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="stylesheet" href="/admin.css?v=6">
|
||||
<link rel="stylesheet" href="/admin.css?v=8">
|
||||
<% if content_for?(:replay_archive_styles) %>
|
||||
<link rel="stylesheet" href="/marketing.css?v=42">
|
||||
<% end %>
|
||||
|
||||
@@ -110,6 +110,7 @@ de:
|
||||
table:
|
||||
match: Spiel
|
||||
status: Status
|
||||
ingest: Ingest
|
||||
start: Start
|
||||
link: Link
|
||||
none: Derzeit keine aktive Sitzung.
|
||||
@@ -253,6 +254,7 @@ de:
|
||||
table:
|
||||
match: Spiel
|
||||
status: Status
|
||||
ingest: Ingest
|
||||
disconnects: Verbindungsabbrüche
|
||||
link: Link
|
||||
detail: Details
|
||||
@@ -265,6 +267,7 @@ de:
|
||||
match_label: "Spiel: %{team} gegen %{opponent}"
|
||||
youtube_studio: YouTube Studio
|
||||
broadcast: Übertragung
|
||||
ingest_node: "Ingest-Knoten:"
|
||||
rtmp_ingest: "RTMP-Ingest:"
|
||||
events_title: Ereignisse
|
||||
table:
|
||||
@@ -281,6 +284,13 @@ de:
|
||||
active_exists: Es existiert bereits ein aktiver Regie-Link (die URL kann nicht abgerufen werden). Erzeuge einen neuen, wenn du ihn erneut teilen musst.
|
||||
none_active: Kein aktiver Regie-Link. Erzeuge einen Link zum Teilen mit der Person, die den Spielstand verwaltet.
|
||||
generate_button: Regie-Link erzeugen
|
||||
ingest:
|
||||
none: "—"
|
||||
role:
|
||||
home: Home-lab
|
||||
lab: Lab
|
||||
cloud: Hetzner
|
||||
|
||||
billing:
|
||||
index:
|
||||
title: Zu berechnende Zahlungen
|
||||
@@ -398,7 +408,8 @@ de:
|
||||
published: Veröffentlicht
|
||||
archived: Archiviert
|
||||
channels:
|
||||
app: Apps (Android und iOS)
|
||||
android: Android-App
|
||||
ios: iOS-App
|
||||
web_public: Öffentliche Website
|
||||
web_private: Login-Bereich
|
||||
window:
|
||||
@@ -410,15 +421,18 @@ de:
|
||||
severity: Schwere
|
||||
status: Status
|
||||
channels: Wo anzeigen
|
||||
channels_hint: Eine oder mehrere Ziele wählen. Der Login-Bereich ist nach der Anmeldung sichtbar (Verein, Teams, Konto).
|
||||
channels_hint: Eine oder mehrere Ziele wählen. Android und iOS sind unabhängig. Der Login-Bereich ist nach der Anmeldung sichtbar (Verein, Teams, Konto).
|
||||
title: Titel
|
||||
body: Text
|
||||
starts_at: Sichtbar ab
|
||||
ends_at: Sichtbar bis
|
||||
window_hint: Leer lassen, um sofort und solange veröffentlicht anzuzeigen.
|
||||
action_url: Link (optional)
|
||||
action_url_hint: Bei Updates öffnet eine leere URL den Store. Sonst eine https-URL.
|
||||
action_url_hint: Bei Updates öffnet eine leere URL den Store der Plattform. Unten können Titel, Text und Link für Android und iOS unterschiedlich sein.
|
||||
action_label: Button-Text
|
||||
android_override: Nur Android (optional)
|
||||
ios_override: Nur iOS (optional)
|
||||
platform_override_hint: Leer lassen, um gemeinsame Texte und Links zu nutzen. Nur ausfüllen, wenn diese Plattform etwas anderes sehen soll.
|
||||
dismissible: Nutzer können den Hinweis schließen
|
||||
dismissible_hint: Haken entfernen für Wartungshinweise, die sichtbar bleiben sollen.
|
||||
submit_create: Hinweis speichern
|
||||
|
||||
@@ -110,6 +110,7 @@ en:
|
||||
table:
|
||||
match: Match
|
||||
status: Status
|
||||
ingest: Ingest
|
||||
start: Start
|
||||
link: Link
|
||||
none: No active session right now.
|
||||
@@ -253,6 +254,7 @@ en:
|
||||
table:
|
||||
match: Match
|
||||
status: Status
|
||||
ingest: Ingest
|
||||
disconnects: Disconnects
|
||||
link: Link
|
||||
detail: Details
|
||||
@@ -265,6 +267,7 @@ en:
|
||||
match_label: "Match: %{team} vs %{opponent}"
|
||||
youtube_studio: YouTube Studio
|
||||
broadcast: Broadcast
|
||||
ingest_node: "Ingest node:"
|
||||
rtmp_ingest: "RTMP ingest:"
|
||||
events_title: Events
|
||||
table:
|
||||
@@ -281,6 +284,13 @@ en:
|
||||
active_exists: An active control link already exists (the URL cannot be retrieved). Generate a new one if you need to share it again.
|
||||
none_active: No active control link. Generate a link to share with whoever manages the score.
|
||||
generate_button: Generate control link
|
||||
ingest:
|
||||
none: "—"
|
||||
role:
|
||||
home: Home-lab
|
||||
lab: Lab
|
||||
cloud: Hetzner
|
||||
|
||||
billing:
|
||||
index:
|
||||
title: Payments to invoice
|
||||
@@ -398,7 +408,8 @@ en:
|
||||
published: Published
|
||||
archived: Archived
|
||||
channels:
|
||||
app: Apps (Android and iOS)
|
||||
android: Android app
|
||||
ios: iOS app
|
||||
web_public: Public website
|
||||
web_private: Logged-in area
|
||||
window:
|
||||
@@ -410,15 +421,18 @@ en:
|
||||
severity: Severity
|
||||
status: Status
|
||||
channels: Where to show it
|
||||
channels_hint: Select one or more destinations. The logged-in area is visible after sign-in (club, teams, account).
|
||||
channels_hint: Select one or more destinations. Android and iOS are independent. The logged-in area is visible after sign-in (club, teams, account).
|
||||
title: Title
|
||||
body: Body
|
||||
starts_at: Visible from
|
||||
ends_at: Visible until
|
||||
window_hint: Leave empty to show immediately while published.
|
||||
action_url: Link (optional)
|
||||
action_url_hint: For updates, an empty URL opens the store. Otherwise use an https URL.
|
||||
action_url_hint: For updates, an empty URL opens that platform's store. Below you can set a different title, body and link for Android and iOS.
|
||||
action_label: Button label
|
||||
android_override: Android only (optional)
|
||||
ios_override: iOS only (optional)
|
||||
platform_override_hint: Leave empty to use the shared title, body and link. Fill in only if this platform should see different copy or a different URL.
|
||||
dismissible: Users can dismiss the notice
|
||||
dismissible_hint: Uncheck for maintenance notices that should stay visible.
|
||||
submit_create: Save notice
|
||||
|
||||
@@ -110,6 +110,7 @@ es:
|
||||
table:
|
||||
match: Partido
|
||||
status: Estado
|
||||
ingest: Ingest
|
||||
start: Inicio
|
||||
link: Enlace
|
||||
none: No hay ninguna sesión activa en este momento.
|
||||
@@ -253,6 +254,7 @@ es:
|
||||
table:
|
||||
match: Partido
|
||||
status: Estado
|
||||
ingest: Ingest
|
||||
disconnects: Desconexiones
|
||||
link: Enlace
|
||||
detail: Detalle
|
||||
@@ -265,6 +267,7 @@ es:
|
||||
match_label: "Partido: %{team} vs %{opponent}"
|
||||
youtube_studio: YouTube Studio
|
||||
broadcast: Emisión
|
||||
ingest_node: "Nodo ingest:"
|
||||
rtmp_ingest: "Ingesta RTMP:"
|
||||
events_title: Eventos
|
||||
table:
|
||||
@@ -281,6 +284,13 @@ es:
|
||||
active_exists: Ya existe un enlace de regie activo (la URL no se puede recuperar). Genera uno nuevo si necesitas compartirlo de nuevo.
|
||||
none_active: No hay ningún enlace de regie activo. Genera un enlace para compartir con quien gestione el marcador.
|
||||
generate_button: Generar enlace de regie
|
||||
ingest:
|
||||
none: "—"
|
||||
role:
|
||||
home: Home-lab
|
||||
lab: Lab
|
||||
cloud: Hetzner
|
||||
|
||||
billing:
|
||||
index:
|
||||
title: Pagos por facturar
|
||||
@@ -398,7 +408,8 @@ es:
|
||||
published: Publicado
|
||||
archived: Archivado
|
||||
channels:
|
||||
app: Apps (Android e iOS)
|
||||
android: App Android
|
||||
ios: App iOS
|
||||
web_public: Sitio público
|
||||
web_private: Área privada
|
||||
window:
|
||||
@@ -410,15 +421,18 @@ es:
|
||||
severity: Gravedad
|
||||
status: Estado
|
||||
channels: Dónde mostrarlo
|
||||
channels_hint: Elige uno o más destinos. El área privada se ve tras iniciar sesión (club, equipos, cuenta).
|
||||
channels_hint: Elige uno o más destinos. Android e iOS son independientes. El área privada se ve tras iniciar sesión (club, equipos, cuenta).
|
||||
title: Título
|
||||
body: Texto
|
||||
starts_at: Visible desde
|
||||
ends_at: Visible hasta
|
||||
window_hint: Déjalo vacío para mostrarlo de inmediato mientras esté publicado.
|
||||
action_url: Enlace (opcional)
|
||||
action_url_hint: En actualizaciones, una URL vacía abre la tienda. Si no, usa una URL https.
|
||||
action_url_hint: En actualizaciones, una URL vacía abre la tienda de esa plataforma. Abajo puedes poner título, texto y enlace distintos para Android e iOS.
|
||||
action_label: Texto del botón
|
||||
android_override: Solo Android (opcional)
|
||||
ios_override: Solo iOS (opcional)
|
||||
platform_override_hint: Déjalo vacío para usar el título, texto y enlace comunes. Rellena solo si esta plataforma debe ver un mensaje o un enlace distinto.
|
||||
dismissible: El usuario puede cerrar el aviso
|
||||
dismissible_hint: Quita la marca para avisos de mantenimiento que deben permanecer visibles.
|
||||
submit_create: Guardar aviso
|
||||
|
||||
@@ -110,6 +110,7 @@ fr:
|
||||
table:
|
||||
match: Match
|
||||
status: Statut
|
||||
ingest: Ingest
|
||||
start: Début
|
||||
link: Lien
|
||||
none: Aucune session active pour le moment.
|
||||
@@ -253,6 +254,7 @@ fr:
|
||||
table:
|
||||
match: Match
|
||||
status: Statut
|
||||
ingest: Ingest
|
||||
disconnects: Déconnexions
|
||||
link: Lien
|
||||
detail: Détail
|
||||
@@ -265,6 +267,7 @@ fr:
|
||||
match_label: "Match : %{team} contre %{opponent}"
|
||||
youtube_studio: YouTube Studio
|
||||
broadcast: Diffusion
|
||||
ingest_node: "Nœud ingest :"
|
||||
rtmp_ingest: "Ingestion RTMP :"
|
||||
events_title: Événements
|
||||
table:
|
||||
@@ -281,6 +284,13 @@ fr:
|
||||
active_exists: Un lien de régie actif existe déjà (l'URL n'est pas récupérable). Générez-en un nouveau si vous devez le repartager.
|
||||
none_active: Aucun lien de régie actif. Générez un lien à partager avec la personne qui gère le score.
|
||||
generate_button: Générer le lien de régie
|
||||
ingest:
|
||||
none: "—"
|
||||
role:
|
||||
home: Home-lab
|
||||
lab: Lab
|
||||
cloud: Hetzner
|
||||
|
||||
billing:
|
||||
index:
|
||||
title: Paiements à facturer
|
||||
@@ -398,7 +408,8 @@ fr:
|
||||
published: Publié
|
||||
archived: Archivé
|
||||
channels:
|
||||
app: Apps (Android et iOS)
|
||||
android: App Android
|
||||
ios: App iOS
|
||||
web_public: Site public
|
||||
web_private: Espace connecté
|
||||
window:
|
||||
@@ -410,15 +421,18 @@ fr:
|
||||
severity: Gravité
|
||||
status: Statut
|
||||
channels: Où l'afficher
|
||||
channels_hint: Choisissez une ou plusieurs destinations. L'espace connecté est visible après connexion (club, équipes, compte).
|
||||
channels_hint: Choisissez une ou plusieurs destinations. Android et iOS sont indépendants. L'espace connecté est visible après connexion (club, équipes, compte).
|
||||
title: Titre
|
||||
body: Texte
|
||||
starts_at: Visible à partir de
|
||||
ends_at: Visible jusqu'à
|
||||
window_hint: Laissez vide pour afficher immédiatement tant que l'alerte est publiée.
|
||||
action_url: Lien (facultatif)
|
||||
action_url_hint: Pour une mise à jour, une URL vide ouvre le store. Sinon une URL https.
|
||||
action_url_hint: Pour une mise à jour, une URL vide ouvre le store de la plateforme. Ci-dessous vous pouvez définir titre, texte et lien différents pour Android et iOS.
|
||||
action_label: Libellé du bouton
|
||||
android_override: Android uniquement (facultatif)
|
||||
ios_override: iOS uniquement (facultatif)
|
||||
platform_override_hint: Laissez vide pour utiliser le titre, le texte et le lien communs. Remplissez seulement si cette plateforme doit voir un message ou un lien différent.
|
||||
dismissible: L'utilisateur peut fermer l'alerte
|
||||
dismissible_hint: Décochez pour une maintenance qui doit rester visible.
|
||||
submit_create: Enregistrer l'alerte
|
||||
|
||||
@@ -114,6 +114,7 @@ it:
|
||||
table:
|
||||
match: Partita
|
||||
status: Stato
|
||||
ingest: Ingest
|
||||
start: Inizio
|
||||
link: Link
|
||||
none: Nessuna sessione attiva in questo momento.
|
||||
@@ -274,6 +275,7 @@ it:
|
||||
table:
|
||||
match: Match
|
||||
status: Stato
|
||||
ingest: Ingest
|
||||
disconnects: Disconnessioni
|
||||
link: Link
|
||||
detail: Dettaglio
|
||||
@@ -286,6 +288,7 @@ it:
|
||||
match_label: "Match: %{team} vs %{opponent}"
|
||||
youtube_studio: YouTube Studio
|
||||
broadcast: Broadcast
|
||||
ingest_node: "Nodo ingest:"
|
||||
rtmp_ingest: "RTMP ingest:"
|
||||
events_title: Eventi
|
||||
table:
|
||||
@@ -302,6 +305,13 @@ it:
|
||||
active_exists: Esiste già un link regia attivo (l’URL non è recuperabile). Generane uno nuovo se serve condividerlo di nuovo.
|
||||
none_active: Nessun link regia attivo. Genera un link da condividere con chi gestisce il punteggio.
|
||||
generate_button: Genera link regia
|
||||
ingest:
|
||||
none: "—"
|
||||
role:
|
||||
home: Home-lab
|
||||
lab: Lab
|
||||
cloud: Hetzner
|
||||
|
||||
billing:
|
||||
index:
|
||||
title: Pagamenti da fatturare
|
||||
@@ -436,7 +446,8 @@ it:
|
||||
published: Pubblicato
|
||||
archived: Archiviato
|
||||
channels:
|
||||
app: App (Android e iOS)
|
||||
android: App Android
|
||||
ios: App iOS
|
||||
web_public: Sito pubblico
|
||||
web_private: Area riservata
|
||||
window:
|
||||
@@ -448,15 +459,18 @@ it:
|
||||
severity: Gravità
|
||||
status: Stato
|
||||
channels: Dove mostrarlo
|
||||
channels_hint: Puoi selezionare una o più destinazioni. L'area riservata è visibile dopo il login (società, squadre, account).
|
||||
channels_hint: Puoi selezionare una o più destinazioni. Android e iOS sono indipendenti. L'area riservata è visibile dopo il login (società, squadre, account).
|
||||
title: Titolo
|
||||
body: Testo
|
||||
starts_at: Inizio visibilità
|
||||
ends_at: Fine visibilità
|
||||
window_hint: Lascia vuoto per mostrare subito e finché resta pubblicato.
|
||||
action_url: Link (facoltativo)
|
||||
action_url_hint: Per gli aggiornamenti, se vuoto l'app apre lo store. Altrimenti un URL https.
|
||||
action_url_hint: Per gli aggiornamenti, se vuoto l'app apre lo store della piattaforma. Sotto puoi impostare titolo, testo e link diversi per Android e iOS.
|
||||
action_label: Etichetta del pulsante
|
||||
android_override: Solo Android (facoltativo)
|
||||
ios_override: Solo iOS (facoltativo)
|
||||
platform_override_hint: Lascia vuoto per usare titolo, testo e link comuni. Compila solo se questa piattaforma deve vedere un messaggio o un link diverso.
|
||||
dismissible: L'utente può chiudere l'avviso
|
||||
dismissible_hint: Togli la spunta per avvisi di manutenzione che devono restare visibili.
|
||||
submit_create: Salva avviso
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
class SplitAnnouncementAppPlatforms < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :app_announcements, :show_on_android, :boolean, null: false, default: true
|
||||
add_column :app_announcements, :show_on_ios, :boolean, null: false, default: true
|
||||
add_column :app_announcements, :android_title, :string
|
||||
add_column :app_announcements, :android_body, :text
|
||||
add_column :app_announcements, :android_action_url, :string
|
||||
add_column :app_announcements, :android_action_label, :string
|
||||
add_column :app_announcements, :ios_title, :string
|
||||
add_column :app_announcements, :ios_body, :text
|
||||
add_column :app_announcements, :ios_action_url, :string
|
||||
add_column :app_announcements, :ios_action_label, :string
|
||||
|
||||
reversible do |dir|
|
||||
dir.up do
|
||||
execute <<~SQL.squish
|
||||
UPDATE app_announcements
|
||||
SET show_on_android = show_on_app,
|
||||
show_on_ios = show_on_app
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
remove_column :app_announcements, :show_on_app, :boolean, null: false, default: true
|
||||
end
|
||||
end
|
||||
Generated
+11
-2
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2026_08_20_140000) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2026_08_20_180000) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
@@ -55,7 +55,6 @@ ActiveRecord::Schema[7.2].define(version: 2026_08_20_140000) do
|
||||
t.string "kind", default: "info", null: false
|
||||
t.string "severity", default: "info", null: false
|
||||
t.string "status", default: "draft", null: false
|
||||
t.boolean "show_on_app", default: true, null: false
|
||||
t.boolean "show_on_web_public", default: false, null: false
|
||||
t.boolean "show_on_web_private", default: false, null: false
|
||||
t.string "title", null: false
|
||||
@@ -68,6 +67,16 @@ ActiveRecord::Schema[7.2].define(version: 2026_08_20_140000) do
|
||||
t.uuid "created_by_admin_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.boolean "show_on_android", default: true, null: false
|
||||
t.boolean "show_on_ios", default: true, null: false
|
||||
t.string "android_title"
|
||||
t.text "android_body"
|
||||
t.string "android_action_url"
|
||||
t.string "android_action_label"
|
||||
t.string "ios_title"
|
||||
t.text "ios_body"
|
||||
t.string "ios_action_url"
|
||||
t.string "ios_action_label"
|
||||
t.index ["created_by_admin_id"], name: "index_app_announcements_on_created_by_admin_id"
|
||||
t.index ["status", "starts_at", "ends_at"], name: "index_app_announcements_on_status_and_starts_at_and_ends_at"
|
||||
t.index ["status"], name: "index_app_announcements_on_status"
|
||||
|
||||
@@ -230,6 +230,20 @@ body.admin-body {
|
||||
.badge--paused { background: #555; color: #fff; }
|
||||
.badge--ready { background: #1b5e20; color: #c8e6c9; }
|
||||
.badge--ok { background: #1b5e20; color: #c8e6c9; }
|
||||
.badge--ingest-home { background: #1565c0; color: #e3f2fd; }
|
||||
.badge--ingest-lab { background: #6a1b9a; color: #f3e5f5; }
|
||||
.badge--ingest-cloud { background: #e65100; color: #fff3e0; }
|
||||
|
||||
.admin-ingest {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
.admin-ingest__slug {
|
||||
font-size: 0.85rem;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.team-list {
|
||||
list-style: none;
|
||||
@@ -652,6 +666,15 @@ body.admin-body {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.admin-platform-copy {
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.admin-platform-copy > div {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.admin-form-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -10,7 +10,8 @@ RSpec.describe AppAnnouncement do
|
||||
status: "published",
|
||||
title: "Avviso",
|
||||
body: "Testo",
|
||||
show_on_app: true,
|
||||
show_on_android: true,
|
||||
show_on_ios: true,
|
||||
show_on_web_public: false,
|
||||
show_on_web_private: false
|
||||
}.merge(attrs))
|
||||
@@ -35,22 +36,55 @@ RSpec.describe AppAnnouncement do
|
||||
|
||||
it "filters by selected channels" do
|
||||
public_item = create_notice!(
|
||||
title: "Pubblico", show_on_app: false, show_on_web_public: true
|
||||
title: "Pubblico", show_on_android: false, show_on_ios: false, show_on_web_public: true
|
||||
)
|
||||
private_item = create_notice!(
|
||||
title: "Privato", show_on_app: false, show_on_web_private: true
|
||||
title: "Privato", show_on_android: false, show_on_ios: false, show_on_web_private: true
|
||||
)
|
||||
app_item = create_notice!(title: "App")
|
||||
android_item = create_notice!(title: "Android", show_on_ios: false)
|
||||
ios_item = create_notice!(title: "iOS", show_on_android: false, show_on_ios: true)
|
||||
|
||||
expect(described_class.for_channel(:web_public)).to contain_exactly(public_item)
|
||||
expect(described_class.for_channel(:web_private)).to contain_exactly(private_item)
|
||||
expect(described_class.for_app).to contain_exactly(app_item)
|
||||
expect(described_class.for_platform("android")).to contain_exactly(android_item)
|
||||
expect(described_class.for_platform("ios")).to contain_exactly(ios_item)
|
||||
expect(described_class.for_app).to contain_exactly(android_item, ios_item)
|
||||
end
|
||||
|
||||
it "uses platform-specific copy and links when present" do
|
||||
item = create_notice!(
|
||||
title: "Comune",
|
||||
body: "Testo comune",
|
||||
action_url: "https://example.com/comune",
|
||||
action_label: "Apri",
|
||||
android_title: "Aggiorna su Android",
|
||||
android_body: "Apri Play Store",
|
||||
android_action_url: "https://play.google.com/store/apps/details?id=com.matchlivetv.match_live_tv",
|
||||
android_action_label: "Play Store",
|
||||
ios_title: "Aggiorna su iOS",
|
||||
ios_body: "Apri App Store",
|
||||
ios_action_url: "https://apps.apple.com/app/id000",
|
||||
ios_action_label: "App Store"
|
||||
)
|
||||
|
||||
android = item.as_api_json(platform: "android")
|
||||
ios = item.as_api_json(platform: "ios")
|
||||
|
||||
expect(android[:title]).to eq("Aggiorna su Android")
|
||||
expect(android[:body]).to eq("Apri Play Store")
|
||||
expect(android[:action_url]).to include("play.google.com")
|
||||
expect(android[:action_label]).to eq("Play Store")
|
||||
expect(ios[:title]).to eq("Aggiorna su iOS")
|
||||
expect(ios[:body]).to eq("Apri App Store")
|
||||
expect(ios[:action_url]).to include("apps.apple.com")
|
||||
expect(ios[:action_label]).to eq("App Store")
|
||||
end
|
||||
|
||||
it "requires at least one channel" do
|
||||
item = described_class.new(
|
||||
kind: "info", status: "draft", title: "X", body: "Y",
|
||||
show_on_app: false, show_on_web_public: false, show_on_web_private: false
|
||||
show_on_android: false, show_on_ios: false,
|
||||
show_on_web_public: false, show_on_web_private: false
|
||||
)
|
||||
expect(item).not_to be_valid
|
||||
expect(item.errors[:base]).to be_present
|
||||
|
||||
@@ -18,7 +18,8 @@ RSpec.describe "Admin announcements", type: :request do
|
||||
title: "Nuova versione",
|
||||
body: "Aggiorna l'app dallo store.",
|
||||
dismissible: "1",
|
||||
show_on_app: "1",
|
||||
show_on_android: "1",
|
||||
show_on_ios: "1",
|
||||
show_on_web_public: "1",
|
||||
show_on_web_private: "0"
|
||||
}
|
||||
@@ -28,7 +29,8 @@ RSpec.describe "Admin announcements", type: :request do
|
||||
notice = AppAnnouncement.last
|
||||
expect(notice.title).to eq("Nuova versione")
|
||||
expect(notice).to be_published
|
||||
expect(notice.show_on_app).to be(true)
|
||||
expect(notice.show_on_android).to be(true)
|
||||
expect(notice.show_on_ios).to be(true)
|
||||
expect(notice.show_on_web_public).to be(true)
|
||||
expect(notice.show_on_web_private).to be(false)
|
||||
|
||||
|
||||
@@ -3,21 +3,23 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Api::V1::Announcements", type: :request do
|
||||
it "GET /api/v1/announcements senza autenticazione restituisce solo gli avvisi attivi per l'app" do
|
||||
it "GET /api/v1/announcements senza autenticazione restituisce solo gli avvisi attivi per la piattaforma" do
|
||||
AppAnnouncement.create!(
|
||||
kind: "maintenance",
|
||||
severity: "warning",
|
||||
status: "published",
|
||||
title: "Manutenzione stasera",
|
||||
body: "Dalle 23:00 alle 01:00 il servizio sarà offline.",
|
||||
show_on_app: true
|
||||
show_on_android: true,
|
||||
show_on_ios: true
|
||||
)
|
||||
AppAnnouncement.create!(
|
||||
kind: "info",
|
||||
status: "published",
|
||||
title: "Solo sito",
|
||||
body: "Non in app",
|
||||
show_on_app: false,
|
||||
show_on_android: false,
|
||||
show_on_ios: false,
|
||||
show_on_web_public: true
|
||||
)
|
||||
AppAnnouncement.create!(
|
||||
@@ -26,14 +28,27 @@ RSpec.describe "Api::V1::Announcements", type: :request do
|
||||
title: "Bozza",
|
||||
body: "Non deve uscire"
|
||||
)
|
||||
AppAnnouncement.create!(
|
||||
kind: "update",
|
||||
status: "published",
|
||||
title: "Aggiorna",
|
||||
body: "Versione nuova",
|
||||
show_on_android: true,
|
||||
show_on_ios: false,
|
||||
android_title: "Aggiorna Android",
|
||||
android_action_url: "https://play.google.com/store/apps/details?id=com.matchlivetv.match_live_tv"
|
||||
)
|
||||
|
||||
get "/api/v1/announcements", params: { platform: "android" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
body = JSON.parse(response.body)
|
||||
expect(body.length).to eq(1)
|
||||
expect(body.first["title"]).to eq("Manutenzione stasera")
|
||||
expect(body.first["kind"]).to eq("maintenance")
|
||||
expect(body.first["dismissible"]).to eq(true)
|
||||
android_body = JSON.parse(response.body)
|
||||
expect(android_body.map { |row| row["title"] }).to contain_exactly("Manutenzione stasera", "Aggiorna Android")
|
||||
|
||||
get "/api/v1/announcements", params: { platform: "ios" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
ios_body = JSON.parse(response.body)
|
||||
expect(ios_body.map { |row| row["title"] }).to eq(["Manutenzione stasera"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,12 +11,12 @@ RSpec.describe "Public site announcements", type: :request do
|
||||
AppAnnouncement.create!(
|
||||
kind: "maintenance", status: "published", title: "Manutenzione sito",
|
||||
body: "Stasera il sito sarà offline.",
|
||||
show_on_app: false, show_on_web_public: true, show_on_web_private: false
|
||||
show_on_android: false, show_on_ios: false, show_on_web_public: true, show_on_web_private: false
|
||||
)
|
||||
AppAnnouncement.create!(
|
||||
kind: "info", status: "published", title: "Solo soci",
|
||||
body: "Messaggio interno.",
|
||||
show_on_app: false, show_on_web_public: false, show_on_web_private: true
|
||||
show_on_android: false, show_on_ios: false, show_on_web_public: false, show_on_web_private: true
|
||||
)
|
||||
|
||||
get root_path
|
||||
@@ -30,12 +30,12 @@ RSpec.describe "Public site announcements", type: :request do
|
||||
AppAnnouncement.create!(
|
||||
kind: "info", status: "published", title: "Solo soci",
|
||||
body: "Messaggio interno.",
|
||||
show_on_app: false, show_on_web_public: false, show_on_web_private: true
|
||||
show_on_android: false, show_on_ios: false, show_on_web_public: false, show_on_web_private: true
|
||||
)
|
||||
AppAnnouncement.create!(
|
||||
kind: "info", status: "published", title: "Manutenzione sito",
|
||||
body: "Stasera il sito sarà offline.",
|
||||
show_on_app: false, show_on_web_public: true, show_on_web_private: false
|
||||
show_on_android: false, show_on_ios: false, show_on_web_public: true, show_on_web_private: false
|
||||
)
|
||||
|
||||
post public_login_path, params: { email: coach.email, password: "Password123" }
|
||||
|
||||
Reference in New Issue
Block a user