349 lines
11 KiB
Ruby
349 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Game Admin Dashboard Component
|
|
# Rails 8 ViewComponent for game data administration
|
|
#
|
|
# Features:
|
|
# - Real-time player analytics via Turbo Streams
|
|
# - Moderation tools with audit logging
|
|
# - Ethical engagement monitoring
|
|
# - Accessibility-first design
|
|
#
|
|
# @security Brakeman scans for mass assignment (permit! forbidden)
|
|
# @test RSpec component rendering tests
|
|
# @accessibility WCAG AA compliance
|
|
|
|
module GameAdmin
|
|
class AdminDashboardComponent < AdminComponent
|
|
# Dashboard component for game administration
|
|
#
|
|
# Displays:
|
|
# - Active players, retention metrics, economy health
|
|
# - Moderation alerts and player reports
|
|
# - Loot table transparency (regulatory compliance)
|
|
#
|
|
# @param current_admin [AdminUser] authenticated admin
|
|
# @param filters [Hash] query filters
|
|
# @return [String] rendered HTML component
|
|
def render(current_admin:, filters: {})
|
|
@current_admin = current_admin
|
|
@filters = filters.permit(:timeframe, :region, :player_segment)
|
|
|
|
content_tag :div, class: 'admin-dashboard', data: {
|
|
controller: 'dashboard',
|
|
dashboard_refresh_interval: 30,
|
|
dashboard_time_zone: Time.zone.name
|
|
}, aria: {
|
|
label: 'Game Administration Dashboard',
|
|
role: 'main'
|
|
} do
|
|
concat render_header
|
|
concat render_metrics_panel
|
|
concat render_alerts_panel
|
|
concat render_ethical_engagement
|
|
concat render_player_table
|
|
end
|
|
end
|
|
|
|
# Dashboard header with navigation
|
|
def render_header
|
|
content_tag :header, class: 'dashboard-header' do
|
|
content_tag :h1, 'Game Admin Dashboard', class: 'dashboard-title'
|
|
concat render_navigation
|
|
concat render_user_menu(@current_admin)
|
|
end
|
|
end
|
|
|
|
# Navigation with accessibility landmarks
|
|
def render_navigation
|
|
tag.nav(aria: { label: 'Dashboard navigation' }, class: 'dashboard-nav') do
|
|
concat link_to('Players', '/admin/players', class: 'nav-link')
|
|
concat link_to('Economy', '/admin/economy', class: 'nav-link')
|
|
concat link_to('Analytics', '/admin/analytics', class: 'nav-link')
|
|
concat link_to('Moderation', '/admin/moderation', class: 'nav-link')
|
|
end
|
|
end
|
|
|
|
# User menu with admin profile
|
|
def render_user_menu(admin)
|
|
content_tag :div, class: 'user-menu' do
|
|
concat content_tag(:span, admin.name, class: 'admin-name')
|
|
concat link_to('Profile', '/admin/profile', class: 'menu-link')
|
|
concat link_to('Logout', '/admin/logout', class: 'menu-link')
|
|
end
|
|
end
|
|
|
|
# Metrics panel with real-time updates
|
|
#
|
|
# Uses Turbo Streams for live data refresh
|
|
def render_metrics_panel
|
|
content_tag :section, class: 'metrics-panel', aria: {
|
|
label: 'Game Metrics',
|
|
role: 'region'
|
|
}, data: {
|
|
dashboard_target: 'metrics',
|
|
turbo_stream_source: 'metrics'
|
|
} do
|
|
concat content_tag(:h2, 'Live Metrics', class: 'panel-title')
|
|
|
|
concat render_metric_card(
|
|
id: 'active_players',
|
|
title: 'Active Players',
|
|
value: @metrics[:active_players],
|
|
change: @metrics[:player_change],
|
|
aria_live: 'polite'
|
|
)
|
|
|
|
concat render_metric_card(
|
|
id: 'retention_rate',
|
|
title: 'D1 Retention',
|
|
value: @metrics[:retention_d1],
|
|
change: @metrics[:retention_change],
|
|
format: 'percentage'
|
|
)
|
|
|
|
concat render_metric_card(
|
|
id: 'economy_health',
|
|
title: 'Economy Health Index',
|
|
value: @metrics[:economy_index],
|
|
change: @metrics[:economy_change],
|
|
tooltip: 'Faucet/Sink balance ratio'
|
|
)
|
|
end
|
|
end
|
|
|
|
# Individual metric card with accessibility
|
|
#
|
|
# @param id [String] metric identifier
|
|
# @param title [String] display title
|
|
# @param value [Numeric] current value
|
|
# @param change [Float] percent change
|
|
# @param format [String] value format
|
|
# @param aria_live [String] ARIA live region policy
|
|
def render_metric_card(id:, title:, value:, change:, format: 'number', aria_live: 'off', tooltip: nil)
|
|
content_tag :div, class: 'metric-card', id: id, data: {
|
|
metric_id: id,
|
|
metric_format: format
|
|
} do
|
|
concat content_tag(:h3, title, class: 'metric-title')
|
|
|
|
value_display = format_value(value, format)
|
|
|
|
concat content_tag(:p, value_display, class: 'metric-value', aria: {
|
|
live: aria_live,
|
|
atomic: true
|
|
})
|
|
|
|
if change
|
|
change_class = change > 0 ? 'positive' : 'negative'
|
|
concat content_tag(:span, "#{change}%", class: "metric-change #{change_class}")
|
|
end
|
|
|
|
concat render_tooltip(tooltip) if tooltip
|
|
end
|
|
end
|
|
|
|
# Format value based on metric type
|
|
def format_value(value, format)
|
|
case format
|
|
when 'percentage'
|
|
"#{value.round(1)}%"
|
|
when 'currency'
|
|
"$#{value}"
|
|
when 'number'
|
|
value.to_s
|
|
else
|
|
value.to_s
|
|
end
|
|
end
|
|
|
|
# Accessibility tooltip component
|
|
def render_tooltip(text)
|
|
content_tag :div, class: 'tooltip', tabindex: 0, role: 'tooltip', aria: {
|
|
label: text
|
|
} do
|
|
content_tag(:span, '?', class: 'tooltip-icon')
|
|
end
|
|
end
|
|
|
|
# Alerts panel for moderation
|
|
def render_alerts_panel
|
|
content_tag :section, class: 'alerts-panel', aria: {
|
|
label: 'Moderation Alerts',
|
|
role: 'region'
|
|
}, data: {
|
|
dashboard_target: 'alerts',
|
|
turbo_stream_source: 'alerts'
|
|
} do
|
|
concat content_tag(:h2, 'Active Alerts', class: 'panel-title')
|
|
|
|
@alerts.each do |alert|
|
|
concat render_alert_card(alert)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Individual alert card
|
|
#
|
|
# @param alert [ModerationAlert] alert object
|
|
def render_alert_card(alert)
|
|
content_tag :div, class: "alert-card #{alert.severity}", role: 'article' do
|
|
concat content_tag(:span, alert.alert_type, class: 'alert-type')
|
|
concat content_tag(:span, alert.player_id.to_s, class: 'player-id')
|
|
concat content_tag(:span, alert.timestamp.strftime('%H:%M'), class: 'alert-time')
|
|
concat link_to('Review', "/admin/moderation/#{alert.id}", class: 'alert-action')
|
|
end
|
|
end
|
|
|
|
# Ethical engagement monitoring panel
|
|
#
|
|
# Displays loot table transparency and rest-state mechanics
|
|
def render_ethical_engagement
|
|
content_tag :section, class: 'ethical-panel', aria: {
|
|
label: 'Ethical Engagement',
|
|
role: 'region'
|
|
} do
|
|
concat content_tag(:h2, 'Ethical Engagement', class: 'panel-title')
|
|
|
|
# Loot table transparency (regulatory compliance)
|
|
concat content_tag :div, class: 'loot-transparency', data: {
|
|
stimulus_target: 'lootOdds'
|
|
} do
|
|
concat content_tag(:p, 'Loot Table Transparency', class: 'subsection-title')
|
|
concat render_loot_table_status
|
|
end
|
|
|
|
# Rest-state mechanics (offline regeneration)
|
|
concat content_tag :div, class: 'rest-state-monitor', data: {
|
|
stimulus_target: 'restState'
|
|
} do
|
|
concat content_tag(:p, 'Rest-State Mechanics', class: 'subsection-title')
|
|
concat render_rest_state_summary
|
|
end
|
|
end
|
|
end
|
|
|
|
# Loot table status with regulatory compliance
|
|
def render_loot_table_status
|
|
active_tables = LootTable.active.count
|
|
compliant_tables = LootTable.compliant.count
|
|
|
|
content_tag :div, class: 'loot-status', aria: {
|
|
label: 'Loot table compliance status'
|
|
} do
|
|
concat content_tag(:p, "#{compliant_tables}/#{active_tables} tables compliant")
|
|
|
|
if active_tables != compliant_tables
|
|
concat content_tag(:span, 'Action Required', class: 'status-warning')
|
|
else
|
|
concat content_tag(:span, 'All Tables Compliant', class: 'status-ok')
|
|
end
|
|
end
|
|
end
|
|
|
|
# Rest-state mechanics summary
|
|
def render_rest_state_summary
|
|
avg_offline = PlayerOfflineResources.average_resources_per_hour
|
|
max_cap_hours = PlayerOfflineResources.maximum_cap_hours
|
|
|
|
content_tag :div, class: 'rest-state-data', aria: {
|
|
label: 'Offline regeneration statistics'
|
|
} do
|
|
concat content_tag(:p, "Avg: #{avg_offline} resources/hour")
|
|
concat content_tag(:p, "Max cap: #{max_cap_hours} hours")
|
|
end
|
|
end
|
|
|
|
# Player data table with sorting and filtering
|
|
def render_player_table
|
|
content_tag :section, class: 'player-table-section', data: {
|
|
dashboard_target: 'filters'
|
|
} do
|
|
concat render_filter_controls
|
|
concat render_table
|
|
end
|
|
end
|
|
|
|
# Filter controls with accessibility
|
|
def render_filter_controls
|
|
content_tag :div, class: 'filter-controls', role: 'search' do
|
|
concat tag.form do
|
|
concat tag.label 'Timeframe', for: 'timeframe_filter'
|
|
concat tag.select(name: 'timeframe', id: 'timeframe_filter') do
|
|
concat tag.option('Last 24h', value: '24h')
|
|
concat tag.option('Last 7d', value: '7d')
|
|
concat tag.option('Last 30d', value: '30d')
|
|
end
|
|
|
|
concat tag.label 'Region', for: 'region_filter'
|
|
concat tag.select(name: 'region', id: 'region_filter') do
|
|
concat tag.option('All', value: '')
|
|
concat tag.option('NA', value: 'na')
|
|
concat tag.option('EU', value: 'eu')
|
|
concat tag.option('APAC', value: 'apac')
|
|
end
|
|
|
|
concat tag.button('Apply Filters', type: 'submit', class: 'btn-primary')
|
|
end
|
|
end
|
|
end
|
|
|
|
# Player data table
|
|
def render_table
|
|
tag.table(class: 'data-table', role: 'grid') do
|
|
concat tag.thead do
|
|
concat tag.tr do
|
|
concat tag.th('Player', scope: 'col')
|
|
concat tag.th('Region', scope: 'col')
|
|
concat tag.th('Level', scope: 'col')
|
|
concat tag.th('Status', scope: 'col')
|
|
concat tag.th('Actions', scope: 'col')
|
|
end
|
|
end
|
|
|
|
concat tag.tbody do
|
|
@players.each do |player|
|
|
concat tag.tr(data: { player_id: player.id }) do
|
|
concat tag.td(player.name)
|
|
concat tag.td(player.region)
|
|
concat tag.td(player.level)
|
|
concat tag.td(player.status)
|
|
concat tag.td do
|
|
concat link_to('View', "/admin/players/#{player.id}", class: 'btn-link')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :current_admin, :filters
|
|
|
|
def metrics
|
|
@metrics ||= fetch_metrics(filters)
|
|
end
|
|
|
|
def alerts
|
|
@alerts ||= ModerationAlert.active.limit(10).ordered
|
|
end
|
|
|
|
def players
|
|
@players ||= Player.scope(filters).limit(50).ordered
|
|
end
|
|
|
|
def fetch_metrics(filters)
|
|
# In production: Redis-cached metrics
|
|
# Real-time aggregation from analytics pipeline
|
|
{
|
|
active_players: 12_450,
|
|
player_change: 3.2,
|
|
retention_d1: 42.5,
|
|
retention_change: -0.8,
|
|
economy_index: 87.3,
|
|
economy_change: 1.5
|
|
}
|
|
end
|
|
end
|
|
end |