From 810f09828c5319a1df0352820d8622297810a4e3 Mon Sep 17 00:00:00 2001 From: Echo Date: Sun, 16 Nov 2025 00:23:42 -0500 Subject: [PATCH] bug fixes (#630) * fix on old repos * clean up broken leaderboards * Update app/jobs/sync_repo_metadata_job.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * remove broken lb logic * Update db/migrate/20251116045400_clean_up_weekly_leaderboards.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/jobs/leaderboard_update_job.rb | 66 +-- app/jobs/sync_repo_metadata_job.rb | 11 +- app/services/leaderboard_service.rb | 38 +- config/initializers/good_job.rb | 6 - ...1116045400_clean_up_weekly_leaderboards.rb | 10 + db/schema.rb | 426 +++++++++--------- 6 files changed, 244 insertions(+), 313 deletions(-) create mode 100644 db/migrate/20251116045400_clean_up_weekly_leaderboards.rb diff --git a/app/jobs/leaderboard_update_job.rb b/app/jobs/leaderboard_update_job.rb index 4c629bc..bef5f5f 100644 --- a/app/jobs/leaderboard_update_job.rb +++ b/app/jobs/leaderboard_update_job.rb @@ -12,73 +12,34 @@ class LeaderboardUpdateJob < ApplicationJob def perform(period = :daily, date = Date.current, force_update: false) date = LeaderboardDateRange.normalize_date(date, period) - - # global - build_leaderboard(date, period, nil, nil, force_update) - - # Build timezone leaderboards - range = LeaderboardDateRange.calculate(date, period) - timezones_for_users_in(range).each do |timezone| - offset = User.timezone_to_utc_offset(timezone) - build_leaderboard(date, period, offset, timezone, force_update) - end + build_leaderboard(date, period, force_update) end private - def timezones_for_users_in(range) - # Expand range by 1 day in both directions to catch users in all timezones - expanded_range = (range.begin - 1.day)...(range.end + 1.day) - - User.joins(:heartbeats) - .where(heartbeats: { time: expanded_range }) - .where.not(timezone: nil) - .distinct - .pluck(:timezone) - .compact - end - - - def build_leaderboard(date, period, timezone_offset = nil, timezone = nil, force_update = false) + def build_leaderboard(date, period, force_update = false) board = ::Leaderboard.find_or_create_by!( start_date: date, period_type: period, - timezone_utc_offset: timezone_offset + timezone_utc_offset: nil ) return board if board.finished_generating_at.present? && !force_update - if timezone_offset - Rails.logger.info "Building timezone leaderboard for #{timezone} (UTC#{timezone_offset >= 0 ? '+' : ''}#{timezone_offset})" - else - Rails.logger.info "Building global leaderboard" - end + Rails.logger.info "Building leaderboard for #{period} on #{date}" - # Calculate timezone-aware range - range = if timezone - Time.use_zone(timezone) { LeaderboardDateRange.calculate(date, period) } - else - LeaderboardDateRange.calculate(date, period) - end + range = LeaderboardDateRange.calculate(date, period) ActiveRecord::Base.transaction do board.entries.delete_all # Build the base heartbeat query heartbeat_query = Heartbeat.where(time: range) - .with_valid_timestamps - .joins(:user) - .coding_only - .where.not(users: { github_uid: nil }) - .where.not(users: { trust_level: User.trust_levels[:red] }) - - # Filter by timezone if specified - if timezone_offset - users_in_tz = User.users_in_timezone_offset(timezone_offset).not_convicted - user_ids = users_in_tz.pluck(:id) - return board if user_ids.empty? - heartbeat_query = heartbeat_query.where(user_id: user_ids) - end + .with_valid_timestamps + .joins(:user) + .coding_only + .where.not(users: { github_uid: nil }) + .where.not(users: { trust_level: User.trust_levels[:red] }) data = heartbeat_query.group(:user_id).duration_seconds .filter { |_, seconds| seconds > 60 } @@ -101,13 +62,10 @@ class LeaderboardUpdateJob < ApplicationJob end # Cache the board - cache_key = timezone_offset ? - LeaderboardCache.timezone_key(timezone_offset, date, period) : - LeaderboardCache.global_key(period, date) - + cache_key = LeaderboardCache.global_key(period, date) LeaderboardCache.write(cache_key, board) - Rails.logger.debug "Persisted #{timezone_offset ? 'timezone' : 'global'} leaderboard with #{board.entries.count} entries" + Rails.logger.debug "Persisted leaderboard for #{period} with #{board.entries.count} entries" board end diff --git a/app/jobs/sync_repo_metadata_job.rb b/app/jobs/sync_repo_metadata_job.rb index 156761a..6c5b769 100644 --- a/app/jobs/sync_repo_metadata_job.rb +++ b/app/jobs/sync_repo_metadata_job.rb @@ -16,11 +16,6 @@ class SyncRepoMetadataJob < ApplicationJob return unless user service = RepoHost::ServiceFactory.for_url(user, repository.url) - unless service - Rails.logger.info "[SyncRepoMetadataJob] Unsupported repository host for #{repository.url}" - return - end - metadata = service.fetch_repo_metadata if metadata @@ -29,6 +24,12 @@ class SyncRepoMetadataJob < ApplicationJob else Rails.logger.warn "[SyncRepoMetadataJob] No metadata returned for #{repository.url}" end + rescue ArgumentError => e + if e.message.include?("Unsupported repository host") + Rails.logger.debug "[SyncRepoMetadataJob] Skipping unsupported host: #{repository.url}" + else + raise + end rescue => e Rails.logger.error "[SyncRepoMetadataJob] Unexpected error: #{e.message}" raise # Retry for other errors diff --git a/app/services/leaderboard_service.rb b/app/services/leaderboard_service.rb index 4506fc7..d7f0d32 100644 --- a/app/services/leaderboard_service.rb +++ b/app/services/leaderboard_service.rb @@ -1,46 +1,14 @@ class LeaderboardService - include TimezoneRegions - - def self.get(period: :daily, date: Date.current, offset: nil) - new.get(period: period, date: date, offset: offset) + def self.get(period: :daily, date: Date.current) + new.get(period: period, date: date) end - def get(period: :daily, date: Date.current, offset: nil) + def get(period: :daily, date: Date.current) date = Date.current if date.blank? - - if offset.present? - get_timezone(date, period, offset) - else - get_global(date, period) - end - end - - private - - def get_timezone(date, period, offset) date = LeaderboardDateRange.normalize_date(date, period) - key = LeaderboardCache.timezone_key(offset, date, period) - board = LeaderboardCache.read(key) - return board if board.present? - - board = ::Leaderboard.where.not(finished_generating_at: nil) - .find_by(start_date: date, period_type: period, timezone_utc_offset: offset, deleted_at: nil) - - if board.present? - LeaderboardCache.write(key, board) - return board - end - - ::LeaderboardUpdateJob.perform_later(period, date) - get_global(date, period) - end - - def get_global(date, period) - date = LeaderboardDateRange.normalize_date(date, period) key = LeaderboardCache.global_key(period, date) board = LeaderboardCache.read(key) - return board if board.present? board = ::Leaderboard.where.not(finished_generating_at: nil) diff --git a/config/initializers/good_job.rb b/config/initializers/good_job.rb index 0b6fd59..56cadb1 100644 --- a/config/initializers/good_job.rb +++ b/config/initializers/good_job.rb @@ -32,12 +32,6 @@ Rails.application.configure do args: [ :daily ], kwargs: { force_update: true } }, - weekly_leaderboard_update: { - cron: "*/2 * * * *", - class: "LeaderboardUpdateJob", - args: [ :weekly ], - kwargs: { force_update: true } - }, last_7_days_leaderboard_update: { cron: "*/7 * * * *", class: "LeaderboardUpdateJob", diff --git a/db/migrate/20251116045400_clean_up_weekly_leaderboards.rb b/db/migrate/20251116045400_clean_up_weekly_leaderboards.rb new file mode 100644 index 0000000..99eaa1c --- /dev/null +++ b/db/migrate/20251116045400_clean_up_weekly_leaderboards.rb @@ -0,0 +1,10 @@ +class CleanUpWeeklyLeaderboards < ActiveRecord::Migration[8.1] + def up + execute "DELETE FROM leaderboards WHERE period_type = 1" + execute "DELETE FROM leaderboards WHERE timezone_utc_offset IS NOT NULL" + end + + def down + # no revert + end +end diff --git a/db/schema.rb b/db/schema.rb index 2a7bd55..b59a176 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do +ActiveRecord::Schema[8.1].define(version: 2025_11_16_045500) do create_schema "pganalyze" # These are extensions that must be enabled in order to support this database @@ -18,15 +18,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do enable_extension "pg_stat_statements" create_table "activities", force: :cascade do |t| - t.string "trackable_type" - t.bigint "trackable_id" - t.string "owner_type" - t.bigint "owner_id" - t.string "key" - t.text "parameters" - t.string "recipient_type" - t.bigint "recipient_id" t.datetime "created_at", null: false + t.string "key" + t.bigint "owner_id" + t.string "owner_type" + t.text "parameters" + t.bigint "recipient_id" + t.string "recipient_type" + t.bigint "trackable_id" + t.string "trackable_type" t.datetime "updated_at", null: false t.index ["owner_id", "owner_type"], name: "index_activities_on_owner_id_and_owner_type" t.index ["owner_type", "owner_id"], name: "index_activities_on_owner" @@ -37,23 +37,23 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do end create_table "admin_api_keys", force: :cascade do |t| - t.bigint "user_id", null: false - t.text "name", null: false - t.text "token", null: false - t.datetime "revoked_at" t.datetime "created_at", null: false + t.text "name", null: false + t.datetime "revoked_at" + t.text "token", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["token"], name: "index_admin_api_keys_on_token", unique: true t.index ["user_id", "name"], name: "index_admin_api_keys_on_user_id_and_name", unique: true t.index ["user_id"], name: "index_admin_api_keys_on_user_id" end create_table "ahoy_events", force: :cascade do |t| - t.bigint "visit_id" - t.bigint "user_id" t.string "name" t.jsonb "properties" t.datetime "time" + t.bigint "user_id" + t.bigint "visit_id" t.index ["name", "time"], name: "index_ahoy_events_on_name_and_time" t.index ["properties"], name: "index_ahoy_events_on_properties", opclass: :jsonb_path_ops, using: :gin t.index ["time"], name: "index_ahoy_events_on_time" @@ -62,31 +62,31 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do end create_table "ahoy_visits", force: :cascade do |t| - t.string "visit_token" - t.string "visitor_token" - t.bigint "user_id" - t.string "ip" - t.text "user_agent" - t.text "referrer" - t.string "referring_domain" - t.text "landing_page" + t.string "app_version" t.string "browser" - t.string "os" - t.string "device_type" - t.string "country" - t.string "region" t.string "city" + t.string "country" + t.string "device_type" + t.string "ip" + t.text "landing_page" t.float "latitude" t.float "longitude" - t.string "utm_source" - t.string "utm_medium" - t.string "utm_term" - t.string "utm_content" - t.string "utm_campaign" - t.string "app_version" + t.string "os" t.string "os_version" t.string "platform" + t.text "referrer" + t.string "referring_domain" + t.string "region" t.datetime "started_at" + t.text "user_agent" + t.bigint "user_id" + t.string "utm_campaign" + t.string "utm_content" + t.string "utm_medium" + t.string "utm_source" + t.string "utm_term" + t.string "visit_token" + t.string "visitor_token" t.index ["started_at"], name: "index_ahoy_visits_on_started_at" t.index ["started_at"], name: "index_ahoy_visits_started_at_with_referring_domain", where: "(referring_domain IS NOT NULL)" t.index ["user_id"], name: "index_ahoy_visits_on_user_id" @@ -95,11 +95,11 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do end create_table "api_keys", force: :cascade do |t| - t.bigint "user_id", null: false + t.datetime "created_at", null: false t.text "name", null: false t.text "token", null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["token"], name: "index_api_keys_on_token", unique: true t.index ["user_id", "name"], name: "index_api_keys_on_user_id_and_name", unique: true t.index ["user_id", "token"], name: "index_api_keys_on_user_id_and_token", unique: true @@ -107,127 +107,127 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do end create_table "commits", primary_key: "sha", id: :string, force: :cascade do |t| - t.bigint "user_id", null: false - t.jsonb "github_raw" t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.jsonb "github_raw" t.bigint "repository_id" + t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["repository_id"], name: "index_commits_on_repository_id" t.index ["user_id", "created_at"], name: "index_commits_on_user_id_and_created_at" t.index ["user_id"], name: "index_commits_on_user_id" end create_table "email_addresses", force: :cascade do |t| - t.string "email" - t.bigint "user_id", null: false t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "email" t.integer "source" + t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["email"], name: "index_email_addresses_on_email", unique: true t.index ["user_id"], name: "index_email_addresses_on_user_id" end create_table "email_verification_requests", force: :cascade do |t| - t.string "email" - t.bigint "user_id", null: false - t.string "token" - t.datetime "expires_at" t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.datetime "deleted_at" + t.string "email" + t.datetime "expires_at" + t.string "token" + t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["email"], name: "index_email_verification_requests_on_email", unique: true t.index ["user_id"], name: "index_email_verification_requests_on_user_id" end create_table "flipper_features", force: :cascade do |t| - t.string "key", null: false t.datetime "created_at", null: false + t.string "key", null: false t.datetime "updated_at", null: false t.index ["key"], name: "index_flipper_features_on_key", unique: true end create_table "flipper_gates", force: :cascade do |t| + t.datetime "created_at", null: false t.string "feature_key", null: false t.string "key", null: false - t.text "value" - t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.text "value" t.index ["feature_key", "key", "value"], name: "index_flipper_gates_on_feature_key_and_key_and_value", unique: true end create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "description" - t.jsonb "serialized_properties" - t.text "on_finish" - t.text "on_success" - t.text "on_discard" - t.text "callback_queue_name" t.integer "callback_priority" - t.datetime "enqueued_at" + t.text "callback_queue_name" + t.datetime "created_at", null: false + t.text "description" t.datetime "discarded_at" + t.datetime "enqueued_at" t.datetime "finished_at" t.datetime "jobs_finished_at" + t.text "on_discard" + t.text "on_finish" + t.text "on_success" + t.jsonb "serialized_properties" + t.datetime "updated_at", null: false end create_table "good_job_executions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.uuid "active_job_id", null: false - t.text "job_class" - t.text "queue_name" - t.jsonb "serialized_params" - t.datetime "scheduled_at" - t.datetime "finished_at" - t.text "error" - t.integer "error_event", limit: 2 - t.text "error_backtrace", array: true - t.uuid "process_id" + t.datetime "created_at", null: false t.interval "duration" + t.text "error" + t.text "error_backtrace", array: true + t.integer "error_event", limit: 2 + t.datetime "finished_at" + t.text "job_class" + t.uuid "process_id" + t.text "queue_name" + t.datetime "scheduled_at" + t.jsonb "serialized_params" + t.datetime "updated_at", null: false t.index ["active_job_id", "created_at"], name: "index_good_job_executions_on_active_job_id_and_created_at" t.index ["process_id", "created_at"], name: "index_good_job_executions_on_process_id_and_created_at" end create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.jsonb "state" t.integer "lock_type", limit: 2 + t.jsonb "state" + t.datetime "updated_at", null: false end create_table "good_job_settings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.text "key" + t.datetime "updated_at", null: false t.jsonb "value" t.index ["key"], name: "index_good_job_settings_on_key", unique: true end create_table "good_jobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.text "queue_name" - t.integer "priority" - t.jsonb "serialized_params" - t.datetime "scheduled_at" - t.datetime "performed_at" - t.datetime "finished_at" - t.text "error" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.uuid "active_job_id" - t.text "concurrency_key" - t.text "cron_key" - t.uuid "retried_good_job_id" - t.datetime "cron_at" - t.uuid "batch_id" t.uuid "batch_callback_id" - t.boolean "is_discrete" - t.integer "executions_count" - t.text "job_class" + t.uuid "batch_id" + t.text "concurrency_key" + t.datetime "created_at", null: false + t.datetime "cron_at" + t.text "cron_key" + t.text "error" t.integer "error_event", limit: 2 + t.integer "executions_count" + t.datetime "finished_at" + t.boolean "is_discrete" + t.text "job_class" t.text "labels", array: true - t.uuid "locked_by_id" t.datetime "locked_at" + t.uuid "locked_by_id" + t.datetime "performed_at" + t.integer "priority" + t.text "queue_name" + t.uuid "retried_good_job_id" + t.datetime "scheduled_at" + t.jsonb "serialized_params" + t.datetime "updated_at", null: false t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at" t.index ["batch_callback_id"], name: "index_good_jobs_on_batch_callback_id", where: "(batch_callback_id IS NOT NULL)" t.index ["batch_id"], name: "index_good_jobs_on_batch_id", where: "(batch_id IS NOT NULL)" @@ -246,35 +246,35 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do end create_table "heartbeats", force: :cascade do |t| - t.bigint "user_id", null: false t.string "branch" t.string "category" + t.datetime "created_at", null: false + t.integer "cursorpos" + t.datetime "deleted_at" t.string "dependencies", default: [], array: true t.string "editor" t.string "entity" + t.text "fields_hash" + t.inet "ip_address" + t.boolean "is_write" t.string "language" - t.string "machine" - t.string "operating_system" - t.string "project" - t.string "type" - t.string "user_agent" t.integer "line_additions" t.integer "line_deletions" t.integer "lineno" t.integer "lines" - t.integer "cursorpos" + t.string "machine" + t.string "operating_system" + t.string "project" t.integer "project_root_count" - t.float "time", null: false - t.boolean "is_write" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "fields_hash" - t.integer "source_type", null: false - t.inet "ip_address" - t.integer "ysws_program", default: 0, null: false - t.datetime "deleted_at" t.jsonb "raw_data" t.bigint "raw_heartbeat_upload_id" + t.integer "source_type", null: false + t.float "time", null: false + t.string "type" + t.datetime "updated_at", null: false + t.string "user_agent" + t.bigint "user_id", null: false + t.integer "ysws_program", default: 0, null: false t.index ["category", "time"], name: "index_heartbeats_on_category_and_time" t.index ["fields_hash"], name: "index_heartbeats_on_fields_hash_when_not_deleted", unique: true, where: "(deleted_at IS NULL)" t.index ["ip_address"], name: "index_heartbeats_on_ip_address" @@ -292,101 +292,101 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do end create_table "leaderboard_entries", force: :cascade do |t| - t.bigint "leaderboard_id", null: false - t.integer "total_seconds", default: 0, null: false - t.integer "rank" t.datetime "created_at", null: false + t.bigint "leaderboard_id", null: false + t.integer "rank" + t.integer "streak_count", default: 0 + t.integer "total_seconds", default: 0, null: false t.datetime "updated_at", null: false t.bigint "user_id", null: false - t.integer "streak_count", default: 0 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.datetime "finished_generating_at" t.datetime "deleted_at" + t.datetime "finished_generating_at" t.integer "period_type", default: 0, null: false - t.integer "timezone_utc_offset" + t.date "start_date", null: false t.integer "timezone_offset" + t.integer "timezone_utc_offset" + t.datetime "updated_at", null: false t.index ["start_date", "period_type", "timezone_offset"], name: "index_leaderboards_on_start_date_period_type_timezone_offset", where: "(deleted_at IS NULL)" t.index ["start_date"], name: "index_leaderboards_on_start_date", where: "(deleted_at IS NULL)" end create_table "mailing_addresses", force: :cascade do |t| - t.bigint "user_id", null: false - t.string "first_name", null: false - t.string "last_name", null: false - t.string "zip_code", null: false - t.string "line_1", null: false - t.string "line_2" t.string "city", null: false - t.string "state", null: false t.string "country", null: false t.datetime "created_at", null: false + t.string "first_name", null: false + t.string "last_name", null: false + t.string "line_1", null: false + t.string "line_2" + t.string "state", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false + t.string "zip_code", null: false t.index ["user_id"], name: "index_mailing_addresses_on_user_id" end create_table "neighborhood_apps", force: :cascade do |t| - t.string "airtable_id", null: false t.jsonb "airtable_fields" + t.string "airtable_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["airtable_id"], name: "index_neighborhood_apps_on_airtable_id", unique: true end create_table "neighborhood_posts", force: :cascade do |t| - t.string "airtable_id", null: false t.jsonb "airtable_fields" + t.string "airtable_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["airtable_id"], name: "index_neighborhood_posts_on_airtable_id", unique: true end create_table "neighborhood_projects", force: :cascade do |t| - t.string "airtable_id", null: false t.jsonb "airtable_fields" + t.string "airtable_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["airtable_id"], name: "index_neighborhood_projects_on_airtable_id", unique: true end create_table "neighborhood_ysws_submissions", force: :cascade do |t| - t.string "airtable_id", null: false t.jsonb "airtable_fields" + t.string "airtable_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["airtable_id"], name: "index_neighborhood_ysws_submissions_on_airtable_id", unique: true end create_table "oauth_access_grants", force: :cascade do |t| - t.bigint "resource_owner_id", null: false t.bigint "application_id", null: false - t.string "token", null: false + t.datetime "created_at", null: false t.integer "expires_in", null: false t.text "redirect_uri", null: false - t.string "scopes", default: "", null: false - t.datetime "created_at", null: false + t.bigint "resource_owner_id", null: false t.datetime "revoked_at" + t.string "scopes", default: "", null: false + t.string "token", null: false t.index ["application_id"], name: "index_oauth_access_grants_on_application_id" t.index ["resource_owner_id"], name: "index_oauth_access_grants_on_resource_owner_id" t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true end create_table "oauth_access_tokens", force: :cascade do |t| - t.bigint "resource_owner_id" t.bigint "application_id", null: false - t.string "token", null: false - t.string "refresh_token" - t.integer "expires_in" - t.string "scopes" t.datetime "created_at", null: false - t.datetime "revoked_at" + t.integer "expires_in" t.string "previous_refresh_token", default: "", null: false + t.string "refresh_token" + t.bigint "resource_owner_id" + t.datetime "revoked_at" + t.string "scopes" + t.string "token", null: false t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" @@ -394,34 +394,34 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do end create_table "oauth_applications", force: :cascade do |t| - t.string "name", null: false - t.string "uid", null: false - t.string "secret", null: false - t.text "redirect_uri", null: false - t.string "scopes", default: "", null: false t.boolean "confidential", default: true, null: false t.datetime "created_at", null: false + t.string "name", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.string "secret", null: false + t.string "uid", null: false t.datetime "updated_at", null: false t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end create_table "physical_mails", force: :cascade do |t| - t.bigint "user_id", null: false + t.datetime "created_at", null: false t.integer "mission_type", null: false t.integer "status", default: 0, null: false t.string "theseus_id" - t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["user_id"], name: "index_physical_mails_on_user_id" end create_table "project_repo_mappings", force: :cascade do |t| - t.bigint "user_id", null: false + t.datetime "created_at", null: false t.string "project_name", null: false t.string "repo_url", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.bigint "repository_id" + t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["project_name"], name: "index_project_repo_mappings_on_project_name" t.index ["repository_id"], name: "index_project_repo_mappings_on_repository_id" t.index ["user_id", "project_name"], name: "index_project_repo_mappings_on_user_id_and_project_name", unique: true @@ -429,110 +429,110 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do end create_table "raw_heartbeat_uploads", force: :cascade do |t| - t.jsonb "request_headers", null: false - t.jsonb "request_body", null: false t.datetime "created_at", null: false + t.jsonb "request_body", null: false + t.jsonb "request_headers", null: false t.datetime "updated_at", null: false end create_table "repo_host_events", id: :string, force: :cascade do |t| - t.bigint "user_id", null: false - t.jsonb "raw_event_payload", null: false - t.integer "provider", default: 0, null: false t.datetime "created_at", null: false + t.integer "provider", default: 0, null: false + t.jsonb "raw_event_payload", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["provider"], name: "index_repo_host_events_on_provider" t.index ["user_id", "provider", "created_at"], name: "index_repo_host_events_on_user_provider_created_at" t.index ["user_id"], name: "index_repo_host_events_on_user_id" end create_table "repositories", force: :cascade do |t| - t.string "url" - t.string "host" - t.string "owner" - t.string "name" - t.integer "stars" + t.integer "commit_count" + t.datetime "created_at", null: false t.text "description" + t.string "homepage" + t.string "host" t.string "language" t.text "languages" - t.integer "commit_count" t.datetime "last_commit_at" t.datetime "last_synced_at" - t.datetime "created_at", null: false + t.string "name" + t.string "owner" + t.integer "stars" t.datetime "updated_at", null: false - t.string "homepage" + t.string "url" t.index ["url"], name: "index_repositories_on_url", unique: true end create_table "sailors_log_leaderboards", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "deleted_at" + t.text "message" t.string "slack_channel_id" t.string "slack_uid" - t.text "message" - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.datetime "deleted_at" end create_table "sailors_log_notification_preferences", force: :cascade do |t| - t.string "slack_uid", null: false - t.string "slack_channel_id", null: false - t.boolean "enabled", default: true, null: false t.datetime "created_at", null: false + t.boolean "enabled", default: true, null: false + t.string "slack_channel_id", null: false + t.string "slack_uid", null: false t.datetime "updated_at", null: false t.index ["slack_uid", "slack_channel_id"], name: "idx_sailors_log_notification_preferences_unique_user_channel", unique: true end create_table "sailors_log_slack_notifications", force: :cascade do |t| - t.string "slack_uid", null: false - t.string "slack_channel_id", null: false - t.string "project_name", null: false - t.integer "project_duration", null: false - t.boolean "sent", default: false, null: false t.datetime "created_at", null: false + t.integer "project_duration", null: false + t.string "project_name", null: false + t.boolean "sent", default: false, null: false + t.string "slack_channel_id", null: false + t.string "slack_uid", null: false t.datetime "updated_at", null: false end create_table "sailors_logs", force: :cascade do |t| - t.string "slack_uid", null: false - t.jsonb "projects_summary", default: {}, null: false t.datetime "created_at", null: false + t.jsonb "projects_summary", default: {}, null: false + t.string "slack_uid", null: false t.datetime "updated_at", null: false end create_table "sign_in_tokens", force: :cascade do |t| - t.string "token" - t.bigint "user_id", null: false t.integer "auth_type" - t.datetime "expires_at" - t.datetime "used_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.string "continue_param" + t.datetime "created_at", null: false + t.datetime "expires_at" t.jsonb "return_data" + t.string "token" + t.datetime "updated_at", null: false + t.datetime "used_at" + t.bigint "user_id", null: false t.index ["token"], name: "index_sign_in_tokens_on_token" t.index ["user_id"], name: "index_sign_in_tokens_on_user_id" end create_table "solid_cache_entries", force: :cascade do |t| - t.binary "key", null: false - t.binary "value", null: false - t.datetime "created_at", null: false - t.bigint "key_hash", null: false t.integer "byte_size", null: false + t.datetime "created_at", null: false + t.binary "key", null: false + t.bigint "key_hash", null: false + t.binary "value", null: false t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true end create_table "trust_level_audit_logs", force: :cascade do |t| - t.bigint "user_id", null: false t.bigint "changed_by_id", null: false - t.string "previous_trust_level", null: false - t.string "new_trust_level", null: false - t.text "reason" - t.text "notes" t.datetime "created_at", null: false + t.string "new_trust_level", null: false + t.text "notes" + t.string "previous_trust_level", null: false + t.text "reason" t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["changed_by_id", "created_at"], name: "index_trust_level_audit_logs_on_changed_by_and_created_at" t.index ["changed_by_id"], name: "index_trust_level_audit_logs_on_changed_by_id" t.index ["user_id", "created_at"], name: "index_trust_level_audit_logs_on_user_and_created_at" @@ -540,29 +540,29 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do end create_table "users", force: :cascade do |t| - t.string "slack_uid" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "deprecated_name" - t.string "slack_avatar_url" - t.boolean "uses_slack_status", default: false, null: false - t.string "slack_scopes", default: [], array: true - t.text "slack_access_token" - t.integer "hackatime_extension_text_type", default: 0, null: false - t.string "timezone", default: "UTC" - t.string "github_uid" - t.string "github_avatar_url" - t.text "github_access_token" - t.string "github_username" - t.string "slack_username" - t.string "slack_neighborhood_channel" - t.integer "trust_level", default: 0, null: false - t.string "country_code" - t.string "mailing_address_otc" - t.boolean "allow_public_stats_lookup", default: true, null: false - t.boolean "default_timezone_leaderboard", default: true, null: false t.integer "admin_level", default: 0, null: false + t.boolean "allow_public_stats_lookup", default: true, null: false + t.string "country_code" + t.datetime "created_at", null: false + t.boolean "default_timezone_leaderboard", default: true, null: false + t.string "deprecated_name" + t.text "github_access_token" + t.string "github_avatar_url" + t.string "github_uid" + t.string "github_username" + t.integer "hackatime_extension_text_type", default: 0, null: false + t.string "mailing_address_otc" + t.text "slack_access_token" + t.string "slack_avatar_url" + t.string "slack_neighborhood_channel" + t.string "slack_scopes", default: [], array: true + t.string "slack_uid" + t.string "slack_username" + t.string "timezone", default: "UTC" + t.integer "trust_level", default: 0, null: false + t.datetime "updated_at", null: false t.string "username" + t.boolean "uses_slack_status", default: false, null: false t.index ["github_uid", "github_access_token"], name: "index_users_on_github_uid_and_access_token" t.index ["github_uid"], name: "index_users_on_github_uid" t.index ["slack_uid"], name: "index_users_on_slack_uid", unique: true @@ -571,23 +571,23 @@ ActiveRecord::Schema[8.0].define(version: 2025_10_21_211039) do end create_table "versions", force: :cascade do |t| - t.string "whodunnit" t.datetime "created_at" + t.string "event", null: false t.bigint "item_id", null: false t.string "item_type", null: false - t.string "event", null: false t.text "object" t.text "object_changes" + t.string "whodunnit" t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" end create_table "wakatime_mirrors", force: :cascade do |t| - t.bigint "user_id", null: false - t.string "endpoint_url", default: "https://wakatime.com/api/v1", null: false - t.string "encrypted_api_key", null: false - t.datetime "last_synced_at" t.datetime "created_at", null: false + t.string "encrypted_api_key", null: false + t.string "endpoint_url", default: "https://wakatime.com/api/v1", null: false + t.datetime "last_synced_at" t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["user_id", "endpoint_url"], name: "index_wakatime_mirrors_on_user_id_and_endpoint_url", unique: true t.index ["user_id"], name: "index_wakatime_mirrors_on_user_id" end