Merge branch 'main' of github.com:hackclub/harbor

This commit is contained in:
Max Wofford 2025-04-03 02:00:15 -04:00
commit 7c33ca11be
3 changed files with 146 additions and 106 deletions

View file

@ -22,35 +22,37 @@ class StaticPagesController < ApplicationController
@setup_social_proof = get_setup_social_proof if @show_wakatime_setup_notice
# Get languages and editors in a single query using window functions
results = current_user.heartbeats.today
.select(
:language,
:editor,
"COUNT(*) OVER (PARTITION BY language) as language_count",
"COUNT(*) OVER (PARTITION BY editor) as editor_count"
)
.distinct
.to_a
Time.use_zone(current_user.timezone) do
results = current_user.heartbeats.today
.select(
:language,
:editor,
"COUNT(*) OVER (PARTITION BY language) as language_count",
"COUNT(*) OVER (PARTITION BY editor) as editor_count"
)
.distinct
.to_a
# Process results to get sorted languages and editors
language_counts = results
.map { |r| [ r.language, r.language_count ] }
.reject { |lang, _| lang.nil? || lang.empty? }
.uniq
.sort_by { |_, count| -count }
# Process results to get sorted languages and editors
language_counts = results
.map { |r| [ r.language, r.language_count ] }
.reject { |lang, _| lang.nil? || lang.empty? }
.uniq
.sort_by { |_, count| -count }
editor_counts = results
.map { |r| [ r.editor, r.editor_count ] }
.reject { |ed, _| ed.nil? || ed.empty? }
.uniq
.sort_by { |_, count| -count }
editor_counts = results
.map { |r| [ r.editor, r.editor_count ] }
.reject { |ed, _| ed.nil? || ed.empty? }
.uniq
.sort_by { |_, count| -count }
@todays_languages = language_counts.map(&:first)
@todays_editors = editor_counts.map(&:first)
@todays_duration = current_user.heartbeats.today.duration_seconds
@todays_languages = language_counts.map(&:first)
@todays_editors = editor_counts.map(&:first)
@todays_duration = current_user.heartbeats.today.duration_seconds
if @todays_duration > 1.minute
@show_logged_time_sentence = @todays_languages.any? || @todays_editors.any?
if @todays_duration > 1.minute
@show_logged_time_sentence = @todays_languages.any? || @todays_editors.any?
end
end
cached_data = filterable_dashboard_data
@ -263,76 +265,78 @@ class StaticPagesController < ApplicationController
Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
result = {}
# Load filter options
filters.each do |filter|
group_by_time = current_user.heartbeats.group(filter).duration_seconds
result[filter] = group_by_time.sort_by { |k, v| v }
.reverse.map(&:first)
.compact_blank
Time.use_zone(current_user.timezone) do
filters.each do |filter|
group_by_time = current_user.heartbeats.group(filter).duration_seconds
result[filter] = group_by_time.sort_by { |k, v| v }
.reverse.map(&:first)
.compact_blank
if params[filter].present?
filter_arr = params[filter].split(",")
filtered_heartbeats = filtered_heartbeats.where(filter => filter_arr)
if params[filter].present?
filter_arr = params[filter].split(",")
filtered_heartbeats = filtered_heartbeats.where(filter => filter_arr)
result["singular_#{filter}"] = filter_arr.length == 1
result["singular_#{filter}"] = filter_arr.length == 1
end
end
end
result[:filtered_heartbeats] = filtered_heartbeats
result[:filtered_heartbeats] = filtered_heartbeats
# Calculate stats for filtered data
result[:total_time] = filtered_heartbeats.duration_seconds
result[:total_heartbeats] = filtered_heartbeats.count
# Calculate stats for filtered data
result[:total_time] = filtered_heartbeats.duration_seconds
result[:total_heartbeats] = filtered_heartbeats.count
filters.each do |filter|
result["top_#{filter}"] = filtered_heartbeats.group(filter)
.duration_seconds
.max_by { |_, v| v }
&.first
end
filters.each do |filter|
result["top_#{filter}"] = filtered_heartbeats.group(filter)
.duration_seconds
.max_by { |_, v| v }
&.first
end
# Prepare project durations data
result[:project_durations] = filtered_heartbeats
.group(:project)
.duration_seconds
.sort_by { |_, duration| -duration }
.first(10)
.to_h unless result["singular_project"]
# Prepare pie chart data
result[:language_stats] = filtered_heartbeats
.group(:language)
.duration_seconds
.sort_by { |_, duration| -duration }
.first(10)
.map { |k, v| [ k.presence || "Unknown", v ] }
.to_h unless result["singular_language"]
result[:editor_stats] = filtered_heartbeats
.group(:editor)
.duration_seconds
.sort_by { |_, duration| -duration }
.map { |k, v| [ k.presence || "Unknown", v ] }
.to_h unless result["singular_editor"]
result[:operating_system_stats] = filtered_heartbeats
.group(:operating_system)
.duration_seconds
.sort_by { |_, duration| -duration }
.map { |k, v| [ k.presence || "Unknown", v ] }
.to_h unless result["singular_operating_system"]
# Calculate weekly project stats for the last 6 months
result[:weekly_project_stats] = {}
(0..25).each do |week_offset| # 26 weeks = 6 months
week_start = week_offset.weeks.ago.beginning_of_week
week_end = week_offset.weeks.ago.end_of_week
week_stats = filtered_heartbeats
.where(time: week_start.to_f..week_end.to_f)
# Prepare project durations data
result[:project_durations] = filtered_heartbeats
.group(:project)
.duration_seconds
.sort_by { |_, duration| -duration }
.first(10)
.to_h unless result["singular_project"]
result[:weekly_project_stats][week_start.to_date.iso8601] = week_stats
# Prepare pie chart data
result[:language_stats] = filtered_heartbeats
.group(:language)
.duration_seconds
.sort_by { |_, duration| -duration }
.first(10)
.map { |k, v| [ k.presence || "Unknown", v ] }
.to_h unless result["singular_language"]
result[:editor_stats] = filtered_heartbeats
.group(:editor)
.duration_seconds
.sort_by { |_, duration| -duration }
.map { |k, v| [ k.presence || "Unknown", v ] }
.to_h unless result["singular_editor"]
result[:operating_system_stats] = filtered_heartbeats
.group(:operating_system)
.duration_seconds
.sort_by { |_, duration| -duration }
.map { |k, v| [ k.presence || "Unknown", v ] }
.to_h unless result["singular_operating_system"]
# Calculate weekly project stats for the last 6 months
result[:weekly_project_stats] = {}
(0..25).each do |week_offset| # 26 weeks = 6 months
week_start = week_offset.weeks.ago.beginning_of_week
week_end = week_offset.weeks.ago.end_of_week
week_stats = filtered_heartbeats
.where(time: week_start.to_f..week_end.to_f)
.group(:project)
.duration_seconds
result[:weekly_project_stats][week_start.to_date.iso8601] = week_stats
end
end
result

View file

@ -21,10 +21,6 @@ class LeaderboardUpdateJob < ApplicationJob
period_type: period_type
)
# Get list of valid user IDs from our database
valid_user_ids = User.pluck(:id)
return if valid_user_ids.empty?
date_range = case period_type
when :weekly
(parsed_date.beginning_of_day...(parsed_date + 7.days).beginning_of_day)
@ -59,8 +55,6 @@ class LeaderboardUpdateJob < ApplicationJob
LeaderboardEntry.insert_all!(entries_data) if entries_data.any?
end
Rails.logger.info "\nTiming breakdown:\n#{timing_info.join("\n")}"
leaderboard.finished_generating_at = Time.current
leaderboard.save!

View file

@ -78,27 +78,69 @@ module Heartbeatable
end
def daily_streaks_for_users(user_ids, start_date: 8.days.ago)
require "set"
heartbeats = where(user_id: user_ids)
# First get the raw durations using window function
raw_durations = joins(:user)
.where(user_id: user_ids)
.coding_only
.with_valid_timestamps
.where(time: start_date..Time.current)
.select(
:user_id,
"users.timezone as user_timezone",
Arel.sql("DATE_TRUNC('day', to_timestamp(time) AT TIME ZONE users.timezone) as day_group"),
Arel.sql("LEAST(EXTRACT(EPOCH FROM (to_timestamp(time) - to_timestamp(LAG(time) OVER (PARTITION BY user_id, DATE_TRUNC('day', to_timestamp(time) AT TIME ZONE users.timezone) ORDER BY time)))), #{heartbeat_timeout_duration.to_i}) as diff")
)
user_ids.each_with_object(Hash.new(0)) do |user_id, hash|
# Then aggregate the results
daily_durations = connection.select_all(
"SELECT user_id, user_timezone, day_group, COALESCE(SUM(diff), 0)::integer as duration
FROM (#{raw_durations.to_sql}) AS diffs
GROUP BY user_id, user_timezone, day_group"
).group_by { |row| row["user_id"] }
.transform_values do |rows|
timezone = rows.first["user_timezone"]
current_date = Time.current.in_time_zone(timezone).to_date
{
current_date: current_date,
days: rows.map do |row|
[ row["day_group"].to_date, row["duration"].to_i ]
end.sort_by { |date, _| date }.reverse
}
end
# Initialize the result hash with zeros for all users
result = user_ids.index_with { 0 }
# Then calculate streaks for each user
daily_durations.each do |user_id, data|
current_date = data[:current_date]
days = data[:days]
# Calculate streak
streak = 0
days_for_user = heartbeats.where(user_id: user_id).daily_durations(start_date: start_date)
days_for_user.sort_by { |date, _| date }
.reverse
.each do |_, duration|
if duration >= 15 * 60
streak += 1
else
break
end
end
hash[user_id] = streak
days.each do |date, duration|
# Skip if this day is in the future
next if date > current_date
# If they didn't code enough today, just skip
if date == current_date
next unless duration >= 15 * 60
streak += 1
next
end
# For previous days, check if it's the next day in the streak
if date == current_date - streak.days && duration >= 15 * 60
streak += 1
else
break
end
end
result[user_id] = streak
end
result
end
def daily_durations(start_date: 365.days.ago, end_date: Time.current)