diff --git a/app/controllers/leaderboards_controller.rb b/app/controllers/leaderboards_controller.rb
new file mode 100644
index 0000000..c906eff
--- /dev/null
+++ b/app/controllers/leaderboards_controller.rb
@@ -0,0 +1,9 @@
+class LeaderboardsController < ApplicationController
+ def index
+ @leaderboard = Leaderboard.find_by(start_date: Date.current)
+
+ @entries = @leaderboard.entries
+ .includes(:user)
+ .order(total_seconds: :desc)
+ end
+end
diff --git a/app/jobs/leaderboard_update_job.rb b/app/jobs/leaderboard_update_job.rb
new file mode 100644
index 0000000..5e7da3f
--- /dev/null
+++ b/app/jobs/leaderboard_update_job.rb
@@ -0,0 +1,36 @@
+class LeaderboardUpdateJob < ApplicationJob
+ queue_as :default
+ limits_concurrency to: 1, key: :date, duration: 5.minutes
+
+ def perform(date = nil, leaderboard = nil)
+ if !leaderboard
+ date ||= Date.current
+ leaderboard = Leaderboard.find_or_initialize_by(start_date: date)
+ end
+
+ ActiveRecord::Base.transaction do
+ # Reset the leaderboard to recalculate
+ # leaderboard.calculating!
+ leaderboard.entries.destroy_all
+
+ begin
+ User.find_each do |user|
+ seconds = user.heartbeats.where("DATE(time) = ?", date).duration_seconds
+ next if seconds.zero?
+
+ leaderboard.entries.build(
+ user_id: user.slack_uid,
+ total_seconds: seconds
+ )
+ end
+
+ leaderboard.touch(:updated_at) unless leaderboard.new_record?
+ leaderboard.save!
+ rescue => e
+ Rails.logger.error "Failed to update current leaderboard: #{e.message}"
+ # leaderboard.failed!
+ raise
+ end
+ end
+ end
+end
diff --git a/app/models/leaderboard.rb b/app/models/leaderboard.rb
new file mode 100644
index 0000000..77933f9
--- /dev/null
+++ b/app/models/leaderboard.rb
@@ -0,0 +1,7 @@
+class Leaderboard < ApplicationRecord
+ has_many :entries,
+ class_name: "LeaderboardEntry",
+ dependent: :destroy
+
+ validates :start_date, presence: true, uniqueness: true
+end
diff --git a/app/models/leaderboard_entry.rb b/app/models/leaderboard_entry.rb
new file mode 100644
index 0000000..b610ed2
--- /dev/null
+++ b/app/models/leaderboard_entry.rb
@@ -0,0 +1,8 @@
+class LeaderboardEntry < ApplicationRecord
+ belongs_to :leaderboard
+ belongs_to :user, primary_key: :slack_uid
+
+ validates :user_id, presence: true
+ validates :total_seconds, presence: true, numericality: { greater_than_or_equal_to: 0 }
+ validates :user_id, uniqueness: { scope: :leaderboard_id }
+end
diff --git a/app/views/leaderboards/index.html.erb b/app/views/leaderboards/index.html.erb
new file mode 100644
index 0000000..0384726
--- /dev/null
+++ b/app/views/leaderboards/index.html.erb
@@ -0,0 +1,37 @@
+
+
+
+
+ <% if @entries.any? %>
+
+
+
+ | Rank |
+ User |
+ Time |
+
+
+
+ <% @entries.each_with_index do |entry, index| %>
+
+ | <%= (index + 1).ordinalize %> |
+ <%= render "shared/user_mention", user: entry.user %> |
+ <%= Time.at(entry.total_seconds).utc.strftime("%H:%M:%S") %> |
+
+ <% end %>
+
+
+ <% else %>
+
+
No data available
+
Check back later for today's results!
+
+ <% end %>
+
+
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 3c6d041..d60f833 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -17,4 +17,6 @@ Rails.application.routes.draw do
get "/auth/slack", to: "sessions#new", as: :slack_auth
get "/auth/slack/callback", to: "sessions#create"
delete "signout", to: "sessions#destroy", as: "signout"
+
+ resources :leaderboards, only: [ :index ]
end
diff --git a/config/schedule.rb b/config/schedule.rb
new file mode 100644
index 0000000..b74d24b
--- /dev/null
+++ b/config/schedule.rb
@@ -0,0 +1,3 @@
+every 1.hour do
+ runner "UpdateLeaderboardJob.perform_later"
+end
diff --git a/db/migrate/20240316000001_create_daily_leaderboard_entries.rb b/db/migrate/20240316000001_create_daily_leaderboard_entries.rb
new file mode 100644
index 0000000..d04979a
--- /dev/null
+++ b/db/migrate/20240316000001_create_daily_leaderboard_entries.rb
@@ -0,0 +1,13 @@
+class CreateDailyLeaderboardEntries < ActiveRecord::Migration[8.0]
+ def change
+ create_table :daily_leaderboard_entries do |t|
+ t.references :daily_leaderboard, null: false, foreign_key: true
+ t.string :user_id, null: false
+ t.integer :total_seconds, null: false, default: 0
+ t.integer :rank
+ t.timestamps
+
+ t.index [ :daily_leaderboard_id, :user_id ], unique: true, name: 'idx_leaderboard_entries_on_leaderboard_and_user'
+ end
+ end
+end
diff --git a/db/migrate/20240316000002_rename_daily_leaderboard_tables.rb b/db/migrate/20240316000002_rename_daily_leaderboard_tables.rb
new file mode 100644
index 0000000..553cf6d
--- /dev/null
+++ b/db/migrate/20240316000002_rename_daily_leaderboard_tables.rb
@@ -0,0 +1,9 @@
+class RenameDailyLeaderboardTables < ActiveRecord::Migration[8.0]
+ def change
+ rename_table :daily_leaderboards, :leaderboards
+ rename_table :daily_leaderboard_entries, :leaderboard_entries
+
+ # Update the foreign key
+ rename_column :leaderboard_entries, :daily_leaderboard_id, :leaderboard_id
+ end
+end
diff --git a/db/migrate/20240316000003_remove_status_from_leaderboards.rb b/db/migrate/20240316000003_remove_status_from_leaderboards.rb
new file mode 100644
index 0000000..1b7dbb6
--- /dev/null
+++ b/db/migrate/20240316000003_remove_status_from_leaderboards.rb
@@ -0,0 +1,5 @@
+class RemoveStatusFromLeaderboards < ActiveRecord::Migration[8.0]
+ def change
+ remove_column :leaderboards, :status, :integer
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 016c19a..14a17b7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,6 +11,27 @@
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_02_16_173459) do
+ # These are extensions that must be enabled in order to support this database
+ enable_extension "pg_catalog.plpgsql"
+
+ create_table "leaderboard_entries", force: :cascade do |t|
+ t.bigint "leaderboard_id", null: false
+ t.string "user_id", null: false
+ t.integer "total_seconds", default: 0, null: false
+ t.integer "rank"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["leaderboard_id", "user_id"], name: "idx_leaderboard_entries_on_leaderboard_and_user", unique: true
+ t.index ["leaderboard_id"], name: "index_leaderboard_entries_on_leaderboard_id"
+ end
+
+ create_table "leaderboards", force: :cascade do |t|
+ t.date "start_date", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["start_date"], name: "index_leaderboards_on_start_date", unique: true
+ end
+
create_table "users", force: :cascade do |t|
t.string "slack_uid", null: false
t.string "email", null: false
@@ -28,8 +49,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_16_173459) do
t.bigint "item_id", null: false
t.string "item_type", null: false
t.string "event", null: false
- t.text "object", limit: 1073741823
- t.text "object_changes", limit: 1073741823
+ t.text "object"
+ t.text "object_changes"
t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
end
+
+ add_foreign_key "leaderboard_entries", "leaderboards"
end