<%= content_for :title, "Leaderboard" %>
|
|
|
|
<%= turbo_frame_tag 'main' do %>
|
|
|
|
<%= turbo_stream.append 'flash', partial: 'layouts/flash' %>
|
|
|
|
<div class="list-title">
|
|
<h1><%= yield(:title) %></h1>
|
|
|
|
<%= link_to "Clear cache",
|
|
admin_leaderboard_cache_path,
|
|
class: "analytics-period",
|
|
data: { turbo_method: :delete, turbo_frame: "main" } %>
|
|
</div>
|
|
|
|
<div class="analytics leaderboard">
|
|
|
|
<% if DemoActivity::ENABLED %>
|
|
<p class="analytics-warning">
|
|
<strong>Demo data is on.</strong>
|
|
<code>DemoActivity</code> invents players on every cache refresh, so the tomato counts
|
|
below are fake. Set <code>DemoActivity::ENABLED = false</code> and wipe the players table
|
|
before go-live.
|
|
</p>
|
|
<% end %>
|
|
|
|
<p class="analytics-note">
|
|
This is the payload served by <code>GET /api/v1/leaderboard</code> — the banner reads it,
|
|
not the players table directly. The endpoint caches for
|
|
<%= distance_of_time_in_words(LeaderboardPayload::TTL) %>, so these are the numbers
|
|
consumers are actually being served right now.
|
|
</p>
|
|
|
|
<% if @live %>
|
|
<p class="analytics-warning">
|
|
<strong>Nothing is published at the moment.</strong>
|
|
The cache has expired and no one has called the endpoint since. Below is a preview of
|
|
what the next call will publish — badges are sampled per refresh, so the ones that go
|
|
out will be a different pick.
|
|
</p>
|
|
<% end %>
|
|
|
|
<section class="analytics-cards">
|
|
<%= render "admin/analytics/card",
|
|
label: "Tomatoes published",
|
|
value: number_with_delimiter(@leaderboard.sum { |row| row[:tomatoes].to_i }),
|
|
sub: "finished play sessions" %>
|
|
|
|
<%= render "admin/analytics/card",
|
|
label: "Countries scoring",
|
|
value: @leaderboard.count { |row| row[:tomatoes].to_i.positive? },
|
|
sub: "of #{@leaderboard.size} listed" %>
|
|
|
|
<% leader = @leaderboard.first %>
|
|
<%= render "admin/analytics/card",
|
|
label: "Leading",
|
|
value: leader ? "#{country_flag(leader[:country_code])} #{leader[:country_code]}" : "—",
|
|
sub: leader ? "#{leader[:score]} pts · #{leaderboard_country_name(leader[:country_code])}" : nil %>
|
|
|
|
<%= render "admin/analytics/card",
|
|
label: @live ? "Computed" : "Published",
|
|
value: @updated_at ? "#{time_ago_in_words(@updated_at)} ago" : "—",
|
|
sub: @updated_at&.strftime("%-d %b %Y, %H:%M:%S %Z") %>
|
|
</section>
|
|
|
|
|
|
<section class="analytics-panel">
|
|
<h2>
|
|
Badges
|
|
<small>rotating banner messages, resampled on every refresh</small>
|
|
</h2>
|
|
|
|
<% if @badges.blank? %>
|
|
<p class="analytics-empty">No badges in this payload.</p>
|
|
<% else %>
|
|
<div class="leaderboard-badges">
|
|
<% @badges.each do |badge| %>
|
|
<div class="leaderboard-badge <%= badge[:type] %>">
|
|
<div class="leaderboard-badge-type"><%= badge_type_label(badge[:type]) %></div>
|
|
<div class="leaderboard-badge-message">
|
|
<span class="leaderboard-flag"><%= country_flag(badge[:country_code]) %></span>
|
|
<%= badge[:message] %>
|
|
</div>
|
|
<div class="leaderboard-badge-code"><%= badge[:country_code] %></div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</section>
|
|
|
|
|
|
<section class="analytics-panel">
|
|
<h2>
|
|
Ranking
|
|
<small>
|
|
<%= (LeaderboardScore::IMPACT_WEIGHT * 100).round %>% impact ·
|
|
<%= (LeaderboardScore::EFFICIENCY_WEIGHT * 100).round %>% per-employee efficiency
|
|
</small>
|
|
</h2>
|
|
|
|
<p class="analytics-note">
|
|
Score blends total tomatoes against tomatoes per employee, so a big country cannot win on
|
|
headcount alone. Countries with no headcount on file fall back to their impact score
|
|
rather than being penalised — those rows are marked.
|
|
</p>
|
|
|
|
<% max_score = @leaderboard.map { |row| row[:score].to_i }.max.to_i %>
|
|
|
|
<div class="leaderboard-table">
|
|
<div class="leaderboard-row leaderboard-head">
|
|
<div class="leaderboard-rank">#</div>
|
|
<div class="leaderboard-country">Country</div>
|
|
<div class="leaderboard-track"></div>
|
|
<div class="leaderboard-number">Score</div>
|
|
<div class="leaderboard-number">Tomatoes</div>
|
|
<div class="leaderboard-number">Per employee</div>
|
|
</div>
|
|
|
|
<% @leaderboard.each_with_index do |row, index| %>
|
|
<div class="leaderboard-row <%= "is-idle" if row[:tomatoes].to_i.zero? %>">
|
|
<div class="leaderboard-rank"><%= index + 1 %></div>
|
|
|
|
<div class="leaderboard-country">
|
|
<span class="leaderboard-flag"><%= country_flag(row[:country_code]) %></span>
|
|
<%= leaderboard_country_name(row[:country_code]) %>
|
|
<em><%= row[:country_code] %></em>
|
|
<% if row[:per_employee].nil? %>
|
|
<em class="leaderboard-tag">no headcount</em>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="leaderboard-track">
|
|
<div class="analytics-bar-track">
|
|
<div class="analytics-bar-fill"
|
|
style="width: <%= max_score.positive? ? (row[:score].to_i * 100.0 / max_score).round(1) : 0 %>%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="leaderboard-number"><strong><%= row[:score] %></strong></div>
|
|
<div class="leaderboard-number"><%= number_with_delimiter(row[:tomatoes]) %></div>
|
|
<div class="leaderboard-number"><%= row[:per_employee] || "—" %></div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="leaderboard-explainer">
|
|
<h3>How the score is calculated</h3>
|
|
|
|
<p>
|
|
A country's score is a blend of two numbers, so a big country cannot win on headcount
|
|
alone and a small one cannot win on a single enthusiastic office.
|
|
</p>
|
|
|
|
<ol class="leaderboard-steps">
|
|
<li>
|
|
<strong>Tomatoes</strong>
|
|
<span>
|
|
How many people in that country finished a game. Only completed sessions count,
|
|
and only players tagged with a country we know about.
|
|
</span>
|
|
</li>
|
|
<li>
|
|
<strong>Impact index — raw volume</strong>
|
|
<span>
|
|
<code>tomatoes ÷ highest tomato count × 100</code><br>
|
|
The busiest country gets 100, everyone else is scored relative to it. This is the
|
|
“how much did you actually do” axis.
|
|
</span>
|
|
</li>
|
|
<li>
|
|
<strong>Efficiency — participation rate</strong>
|
|
<span>
|
|
<code>(tomatoes ÷ employees) ÷ highest ratio × 100</code><br>
|
|
Employee headcounts come from <code>config/country_headcounts.yml</code>. This is
|
|
the “what share of you turned up” axis, so a 60-person country can beat
|
|
a 19,000-person one.
|
|
</span>
|
|
</li>
|
|
<li>
|
|
<strong>Blend</strong>
|
|
<span>
|
|
<code>impact × <%= LeaderboardScore::IMPACT_WEIGHT %> + efficiency × <%= LeaderboardScore::EFFICIENCY_WEIGHT %></code>,
|
|
rounded to a whole number. Change the split in
|
|
<code>LeaderboardScore::IMPACT_WEIGHT</code> — raising efficiency leans further
|
|
toward fairness across team sizes.
|
|
</span>
|
|
</li>
|
|
</ol>
|
|
|
|
<% example = @leaderboard.find { |row| row[:tomatoes].to_i.positive? } %>
|
|
<% if example %>
|
|
<% max_tomatoes = @leaderboard.map { |row| row[:tomatoes].to_i }.max.to_f %>
|
|
<% max_per_emp = @leaderboard.filter_map { |row| row[:per_employee] }.max.to_f %>
|
|
<% impact = max_tomatoes.positive? ? (example[:tomatoes] / max_tomatoes * 100) : 0 %>
|
|
<% efficiency = if example[:per_employee] && max_per_emp.positive?
|
|
example[:per_employee] / max_per_emp * 100
|
|
else
|
|
impact
|
|
end %>
|
|
|
|
<p class="leaderboard-worked">
|
|
<strong>Right now, <%= country_flag(example[:country_code]) %>
|
|
<%= leaderboard_country_name(example[:country_code]) %>:</strong>
|
|
<%= number_with_delimiter(example[:tomatoes]) %>
|
|
<%= "tomato".pluralize(example[:tomatoes]) %>
|
|
against a best of <%= number_with_delimiter(max_tomatoes.to_i) %> gives an impact of
|
|
<%= impact.round(1) %>;
|
|
<% if example[:per_employee] %>
|
|
<%= example[:per_employee] %> per employee against a best of <%= max_per_emp.round(2) %>
|
|
gives an efficiency of <%= efficiency.round(1) %>.
|
|
<% else %>
|
|
with no headcount on file, efficiency falls back to the impact score
|
|
(<%= efficiency.round(1) %>).
|
|
<% end %>
|
|
Blended, that lands on <strong><%= example[:score] %></strong>.
|
|
</p>
|
|
<% end %>
|
|
|
|
<p class="analytics-note">
|
|
Two details worth knowing: every country we hold a name for is listed even with zero
|
|
tomatoes, so the banner can always name anyone; and ties are broken alphabetically by
|
|
country code, not randomly, so the order is stable between refreshes.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
<section class="analytics-panel">
|
|
<h2>Raw payload <small>what the endpoint returns verbatim</small></h2>
|
|
|
|
<details class="leaderboard-raw">
|
|
<summary>Show JSON</summary>
|
|
<pre><%= JSON.pretty_generate(
|
|
leaderboard: @leaderboard,
|
|
badges: @badges,
|
|
updated_at: @updated_at&.iso8601
|
|
) %></pre>
|
|
</details>
|
|
</section>
|
|
|
|
</div>
|
|
<% end %>
|