claw-code/.guardrails/examples/ruby/rails-ui/game-admin-dashboard.rb

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