mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 21:05:15 +00:00
366 lines
13 KiB
Ruby
366 lines
13 KiB
Ruby
class StaticPagesController < ApplicationController
|
||
before_action :ensure_current_user, only: %i[
|
||
filterable_dashboard
|
||
filterable_dashboard_content
|
||
]
|
||
|
||
def index
|
||
if current_user
|
||
flavor_texts = FlavorText.motto + FlavorText.conditional_mottos(current_user)
|
||
flavor_texts += FlavorText.rare_motto if Random.rand(10) < 1
|
||
@flavor_text = flavor_texts.sample
|
||
|
||
unless params[:date].blank?
|
||
# implement this later– for now just redirect to a random video
|
||
allowed_hosts = FlavorText.random_time_video.map { |v| URI.parse(v).host }
|
||
redirect_to FlavorText.random_time_video.sample, allow_other_host: allowed_hosts
|
||
end
|
||
|
||
if current_user.heartbeats.empty? || params[:show_wakatime_setup_notice]
|
||
@show_wakatime_setup_notice = true
|
||
|
||
setup_social_proof = Cache::SetupSocialProofJob.perform_now
|
||
if setup_social_proof.present?
|
||
@ssp_message = setup_social_proof[:message]
|
||
@ssp_users_recent = setup_social_proof[:users_recent]
|
||
@ssp_users_size = setup_social_proof[:users_size]
|
||
end
|
||
|
||
end
|
||
|
||
# Get languages and editors in a single query using window functions
|
||
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&.downcase, 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 }
|
||
|
||
@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?
|
||
end
|
||
end
|
||
|
||
cached_data = filterable_dashboard_data
|
||
cached_data.entries.each do |key, value|
|
||
instance_variable_set("@#{key}", value)
|
||
end
|
||
else
|
||
# Set homepage SEO content for logged-out users only
|
||
set_homepage_seo_content
|
||
|
||
@usage_social_proof = Cache::UsageSocialProofJob.perform_now
|
||
|
||
@home_stats = Cache::HomeStatsJob.perform_now
|
||
end
|
||
end
|
||
|
||
def mini_leaderboard
|
||
@leaderboard = Leaderboard.where.associated(:entries)
|
||
.where(start_date: Date.current)
|
||
.where(deleted_at: nil)
|
||
.where(period_type: :daily)
|
||
.distinct
|
||
.first
|
||
|
||
@active_projects = Cache::ActiveProjectsJob.perform_now
|
||
|
||
render partial: "leaderboards/mini_leaderboard", locals: {
|
||
leaderboard: @leaderboard,
|
||
current_user: current_user
|
||
}
|
||
end
|
||
|
||
def project_durations
|
||
return unless current_user
|
||
|
||
@project_repo_mappings = current_user.project_repo_mappings.includes(:repository)
|
||
cache_key = "user_#{current_user.id}_project_durations_#{params[:interval]}"
|
||
cache_key += "_#{params[:from]}_#{params[:to]}" if params[:interval] == "custom"
|
||
|
||
project_durations = Rails.cache.fetch(cache_key, expires_in: 1.minute) do
|
||
heartbeats = current_user.heartbeats.filter_by_time_range(params[:interval], params[:from], params[:to])
|
||
project_times = heartbeats.group(:project).duration_seconds
|
||
project_labels = current_user.project_labels
|
||
project_times.map do |project, duration|
|
||
mapping = @project_repo_mappings.find { |p| p.project_name == project }
|
||
{
|
||
project: project_labels.find { |p| p.project_key == project }&.label || project || "Unknown",
|
||
repo_url: mapping&.repo_url,
|
||
repository: mapping&.repository,
|
||
duration: duration
|
||
}
|
||
end.filter { |p| p[:duration].positive? }.sort_by { |p| p[:duration] }.reverse
|
||
end
|
||
render partial: "project_durations", locals: { project_durations: project_durations }
|
||
end
|
||
|
||
def activity_graph
|
||
return unless current_user
|
||
|
||
user_tz = current_user.timezone
|
||
cache_key = "user_#{current_user.id}_daily_durations_#{user_tz}"
|
||
|
||
daily_durations = Rails.cache.fetch(cache_key, expires_in: 1.minute) do
|
||
Time.use_zone(user_tz) do
|
||
current_user.heartbeats.daily_durations(user_timezone: user_tz).to_h
|
||
end
|
||
end
|
||
|
||
# Consider 8 hours as a "full" day of coding
|
||
length_of_busiest_day = 8.hours.to_i # 28800 seconds
|
||
|
||
render partial: "activity_graph", locals: {
|
||
daily_durations: daily_durations,
|
||
length_of_busiest_day: length_of_busiest_day
|
||
}
|
||
end
|
||
|
||
def currently_hacking
|
||
locals = Cache::CurrentlyHackingJob.perform_now
|
||
|
||
respond_to do |format|
|
||
format.html { render partial: "currently_hacking", locals: locals }
|
||
format.json do
|
||
json_response = { count: locals[:users].count }
|
||
|
||
# Only include HTML if explicitly requested (when list is visible)
|
||
if params[:include_list] == "true"
|
||
json_response[:html] = render_to_string(partial: "currently_hacking", locals: locals, formats: [ :html ])
|
||
end
|
||
|
||
render json: json_response
|
||
end
|
||
end
|
||
end
|
||
|
||
def streak
|
||
render partial: "streak"
|
||
end
|
||
|
||
def filterable_dashboard
|
||
cached_data = filterable_dashboard_data
|
||
cached_data.entries.each do |key, value|
|
||
instance_variable_set("@#{key}", value)
|
||
end
|
||
|
||
render partial: "filterable_dashboard"
|
||
end
|
||
|
||
def filterable_dashboard_content
|
||
cached_data = filterable_dashboard_data
|
||
cached_data.entries.each do |key, value|
|
||
instance_variable_set("@#{key}", value)
|
||
end
|
||
|
||
render partial: "filterable_dashboard_content"
|
||
end
|
||
|
||
def 🃏
|
||
redirect_to root_path unless current_user && current_user.slack_uid.present?
|
||
|
||
record = HTTP.auth("Bearer #{ENV.fetch("WILDCARD_AIRTABLE_KEY")}").patch("https://api.airtable.com/v0/appt3yVn2nbiUaijm/tblRCAMjfQ4MIsMPp",
|
||
json: {
|
||
records: [
|
||
{
|
||
fields: {
|
||
slack_id: current_user.slack_uid
|
||
}
|
||
}
|
||
],
|
||
performUpsert: {
|
||
fieldsToMergeOn: [ "slack_id" ]
|
||
}
|
||
}
|
||
)
|
||
record_data = JSON.parse(record.body)
|
||
|
||
record_id = record_data.dig("records", 0, "id")
|
||
|
||
redirect_to root_path unless record_id&.present?
|
||
|
||
# if record is created, set a new auth_key:
|
||
auth_key = SecureRandom.hex(16)
|
||
HTTP.auth("Bearer #{ENV.fetch("WILDCARD_AIRTABLE_KEY")}").patch("https://api.airtable.com/v0/appt3yVn2nbiUaijm/tblRCAMjfQ4MIsMPp",
|
||
json: {
|
||
records: [
|
||
{ id: record_id, fields: { auth_key: auth_key } }
|
||
]
|
||
}
|
||
)
|
||
|
||
wildcard_host = ENV.fetch("WILDCARD_HOST")
|
||
|
||
redirect_to "#{wildcard_host}?auth_key=#{auth_key}", allow_other_host: wildcard_host
|
||
end
|
||
|
||
private
|
||
|
||
def ensure_current_user
|
||
redirect_to root_path, alert: "You must be logged in to view this page" unless current_user
|
||
end
|
||
|
||
def set_homepage_seo_content
|
||
@page_title = "Hackatime - Free Coding Time Tracker"
|
||
@meta_description = "Track your coding time easily with Hackatime. See how long you spend programming in different languages. Free alternative to WakaTime. Join thousands of high schoolers!"
|
||
@meta_keywords = "coding time tracker, programming stats, wakatime alternative, free time tracking, code statistics, high school programming, coding analytics"
|
||
@og_title = "Hackatime - Free Coding Time Tracker"
|
||
@og_description = "Track your coding time easily with Hackatime. See how long you spend programming. Free and open source!"
|
||
@twitter_title = "Hackatime - Free Coding Time Tracker"
|
||
@twitter_description = "Track your coding time easily with Hackatime. See how long you spend programming. Free and open source!"
|
||
end
|
||
|
||
def filterable_dashboard_data
|
||
filters = %i[project language operating_system editor category]
|
||
|
||
# Cache key based on user and filter parameters
|
||
cache_key = []
|
||
cache_key << current_user
|
||
filters.each do |filter|
|
||
cache_key << params[filter]
|
||
end
|
||
|
||
filtered_heartbeats = current_user.heartbeats
|
||
# Load filter options and apply filters with caching
|
||
Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
|
||
result = {}
|
||
# Load filter options
|
||
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
|
||
.map { |k| %i[operating_system editor].include?(filter) ? k.capitalize : k }
|
||
.uniq
|
||
|
||
if params[filter].present?
|
||
filter_arr = params[filter].split(",")
|
||
if %i[operating_system editor].include?(filter)
|
||
# search for both lowercase and capitalized versions
|
||
normalized_arr = filter_arr.flat_map { |v| [ v.downcase, v.capitalize ] }.uniq
|
||
filtered_heartbeats = filtered_heartbeats.where(filter => normalized_arr)
|
||
else
|
||
filtered_heartbeats = filtered_heartbeats.where(filter => filter_arr)
|
||
end
|
||
|
||
|
||
result["singular_#{filter}"] = filter_arr.length == 1
|
||
end
|
||
end
|
||
|
||
# Only use the concern for time filtering
|
||
filtered_heartbeats = filtered_heartbeats.filter_by_time_range(params[:interval], params[:from], params[:to])
|
||
|
||
result[:filtered_heartbeats] = filtered_heartbeats
|
||
|
||
# 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
|
||
|
||
# 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
|
||
%i[language editor operating_system category].each do |filter|
|
||
# If the filter is editor or operating_system, normalize and sum the durations
|
||
stats = filtered_heartbeats
|
||
.group(filter)
|
||
.duration_seconds
|
||
.each_with_object({}) do |(raw_key, duration), agg|
|
||
key = raw_key.to_s.presence || "Unknown"
|
||
key = key.downcase if %i[editor operating_system].include?(filter)
|
||
agg[key] = (agg[key] || 0) + duration
|
||
end
|
||
|
||
result["#{filter}_stats"] =
|
||
stats
|
||
.sort_by { |_, duration| -duration }
|
||
.first(10)
|
||
.map { |k, v|
|
||
label = %i[language category].include?(filter) ? k : k.capitalize
|
||
[ label, v ]
|
||
}
|
||
.to_h unless result["singular_#{filter}"]
|
||
end
|
||
# 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"]
|
||
|
||
# result[:category_stats] = filtered_heartbeats
|
||
# .group(:category)
|
||
# .duration_seconds
|
||
# .sort_by { |_, duration| -duration }
|
||
# .map { |k, v| [ k.presence || "Unknown", v ] }
|
||
# .to_h unless result["singular_category"]
|
||
|
||
# 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
|
||
end
|
||
end
|
||
end
|