<%= content_for :title, "Leaderboard" %> <%= turbo_frame_tag 'main' do %> <%= turbo_stream.append 'flash', partial: 'layouts/flash' %>

<%= yield(:title) %>

<% if admin? %> <%= link_to "Clear cache", admin_leaderboard_cache_path, class: "analytics-period", data: { turbo_method: :delete, turbo_frame: "main" } %> <% end %>
<% if DemoActivity::ENABLED %>

Demo data is on. DemoActivity invents players on every cache refresh, so the tomato counts below are fake. Set DemoActivity::ENABLED = false and wipe the players table before go-live.

<% end %>

This is the payload served by GET /api/v1/leaderboard — 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.

<% if @live %>

Nothing is published at the moment. 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.

<% end %>
<%= 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") %>

Badges rotating banner messages, resampled on every refresh

<% if @badges.blank? %>

No badges in this payload.

<% else %>
<% @badges.each do |badge| %>
<%= badge_type_label(badge[:type]) %>
<%= country_flag(badge[:country_code]) %> <%= badge[:message] %>
<%= badge[:country_code] %>
<% end %>
<% end %>

Ranking <%= (LeaderboardScore::IMPACT_WEIGHT * 100).round %>% impact · <%= (LeaderboardScore::EFFICIENCY_WEIGHT * 100).round %>% per-employee efficiency

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.

<% max_score = @leaderboard.map { |row| row[:score].to_i }.max.to_i %>
#
Country
Score
Tomatoes
Per employee
<% @leaderboard.each_with_index do |row, index| %>
">
<%= index + 1 %>
<%= country_flag(row[:country_code]) %> <%= leaderboard_country_name(row[:country_code]) %> <%= row[:country_code] %> <% if row[:per_employee].nil? %> no headcount <% end %>
<%= row[:score] %>
<%= number_with_delimiter(row[:tomatoes]) %>
<%= row[:per_employee] || "—" %>
<% end %>

How the score is calculated

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.

  1. Tomatoes How many people in that country finished a game. Only completed sessions count, and only players tagged with a country we know about.
  2. Impact index — raw volume tomatoes ÷ highest tomato count × 100
    The busiest country gets 100, everyone else is scored relative to it. This is the “how much did you actually do” axis.
  3. Efficiency — participation rate (tomatoes ÷ employees) ÷ highest ratio × 100
    Employee headcounts come from config/country_headcounts.yml. This is the “what share of you turned up” axis, so a 60-person country can beat a 19,000-person one.
  4. Blend impact × <%= LeaderboardScore::IMPACT_WEIGHT %> + efficiency × <%= LeaderboardScore::EFFICIENCY_WEIGHT %>, rounded to a whole number. Change the split in LeaderboardScore::IMPACT_WEIGHT — raising efficiency leans further toward fairness across team sizes.
<% 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 %>

Right now, <%= country_flag(example[:country_code]) %> <%= leaderboard_country_name(example[:country_code]) %>: <%= 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 <%= example[:score] %>.

<% end %>

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.

Raw payload what the endpoint returns verbatim

Show JSON
<%= JSON.pretty_generate(
                   leaderboard: @leaderboard,
                   badges: @badges,
                   updated_at: @updated_at&.iso8601
                 ) %>
<% end %>