diff --git a/app/controllers/api/hackatime/v1/hackatime_controller.rb b/app/controllers/api/hackatime/v1/hackatime_controller.rb index 31ef8e3e..902bf017 100644 --- a/app/controllers/api/hackatime/v1/hackatime_controller.rb +++ b/app/controllers/api/hackatime/v1/hackatime_controller.rb @@ -50,94 +50,17 @@ def status_bar_today end end - def stats_last_7_days - Time.use_zone(@user.timezone) do - # Calculate time range within the user's timezone - start_time = (Time.current - 7.days).beginning_of_day - end_time = Time.current.end_of_day - - # Convert to Unix timestamps - start_timestamp = start_time.to_i - end_timestamp = end_time.to_i - - # Get heartbeats in the time range - heartbeats = @user.heartbeats.where(time: start_timestamp..end_timestamp) - - # Calculate total seconds - total_seconds = heartbeats.duration_seconds.to_i - - # Get unique days - days = [] - heartbeats.pluck(:time).each do |timestamp| - day = Time.at(timestamp).in_time_zone(@user.timezone).to_date - days << day unless days.include?(day) - end - days_covered = days.length - - # Calculate daily average - daily_average = days_covered > 0 ? (total_seconds.to_f / days_covered).round(1) : 0 - - # Format human readable strings - hours = total_seconds / 3600 - minutes = (total_seconds % 3600) / 60 - human_readable_total = "#{hours} hrs #{minutes} mins" - - avg_hours = daily_average.to_i / 3600 - avg_minutes = (daily_average.to_i % 3600) / 60 - human_readable_daily_average = "#{avg_hours} hrs #{avg_minutes} mins" - - # Calculate statistics for different categories - editors_data = calculate_category_stats(heartbeats, "editor") - languages_data = calculate_category_stats(heartbeats, "language") - projects_data = calculate_category_stats(heartbeats, "project") - machines_data = calculate_category_stats(heartbeats, "machine") - os_data = calculate_category_stats(heartbeats, "operating_system") - - # Categories data - hours = total_seconds / 3600 - minutes = (total_seconds % 3600) / 60 - seconds = total_seconds % 60 - - categories = [ - { - name: "coding", - total_seconds: total_seconds, - percent: 100.0, - digital: format("%d:%02d:%02d", hours, minutes, seconds), - text: human_readable_total, - hours: hours, - minutes: minutes, - seconds: seconds - } - ] + # GET /api/hackatime/v1/users/:id/stats/:range + def stats + range = params[:range] || "last_7_days" + range_config = TimeRangeFilterable::RANGES[range.to_sym] + unless range_config.present? + return render json: { error: "Invalid range", message: "Invalid range, valid ranges are: #{TimeRangeFilterable::RANGES.keys.join(", ")}" }, status: :bad_request + end - result = { - data: { - username: @user.slack_uid, - user_id: @user.slack_uid, - start: start_time.iso8601, - end: end_time.iso8601, - status: "ok", - total_seconds: total_seconds, - daily_average: daily_average, - days_including_holidays: days_covered, - range: "last_7_days", - human_readable_range: "Last 7 Days", - human_readable_total: human_readable_total, - human_readable_daily_average: human_readable_daily_average, - is_coding_activity_visible: true, - is_other_usage_visible: true, - editors: editors_data, - languages: languages_data, - machines: machines_data, - projects: projects_data, - operating_systems: os_data, - categories: categories - } - } + summary = WakatimeService.new(user: @user, range: range, specific_filters: [ :editors, :languages, :projects, :machines, :operating_systems ]).generate_summary - render json: result - end + render json: { data: summary }, status: :ok and return end private @@ -267,6 +190,8 @@ def queue_project_mapping(project_name) end def set_user + @user = User.find_by(id: params[:id]) and return if Rails.env.development? + api_header = request.headers["Authorization"] raw_token = api_header&.split(" ")&.last header_type = api_header&.split(" ")&.first diff --git a/config/routes.rb b/config/routes.rb index ae89bfe3..f95a9842 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -138,7 +138,7 @@ def self.matches?(request) get "/", to: "hackatime#index" # many clients seem to link this as the user's dashboard get "/users/:id/statusbar/today", to: "hackatime#status_bar_today" post "/users/:id/heartbeats", to: "hackatime#push_heartbeats" - get "/users/current/stats/last_7_days", to: "hackatime#stats_last_7_days" + get "/users/:id/stats/:range", to: "hackatime#stats" end end end diff --git a/lib/wakatime_service.rb b/lib/wakatime_service.rb index bc69b690..896841c7 100644 --- a/lib/wakatime_service.rb +++ b/lib/wakatime_service.rb @@ -1,13 +1,31 @@ include ApplicationHelper class WakatimeService - def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, start_date: nil, end_date: nil) + def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, start_date: nil, end_date: nil, range: nil) @scope = Heartbeat.all @user = user - # Default to 1 year ago if no start_date provided or if no data exists - @start_date = start_date || @scope.minimum(:time) || 1.year.ago.to_i - @end_date = end_date || @scope.maximum(:time) || Time.current.to_i + @tz = @user&.timezone || "UTC" + @range_name = range.to_s + + Time.use_zone(@tz) do + if start_date.present? || end_date.present? + @start_date = start_date || @scope.minimum(:time) || 1.year.ago.to_i + @end_date = end_date || @scope.maximum(:time) || Time.current.to_i + @range_name = "custom" + else + if range.present? + @range = TimeRangeFilterable::RANGES&.[](range.to_sym) || TimeRangeFilterable::RANGES[:all_time] + @range_name = range.to_s + else + @range = TimeRangeFilterable::RANGES&.[](:all_time) + @range_name = "all_time" + end + + @start_date = @range[:calculate].call.first + @end_date = @range[:calculate].call.last + end + end @scope = @scope.where(time: @start_date..@end_date) @@ -32,23 +50,28 @@ def generate_summary @start_time = @scope.minimum(:time) || @start_date @end_time = @scope.maximum(:time) || @end_date - summary[:start] = Time.at(@start_time).strftime("%Y-%m-%dT%H:%M:%SZ") - summary[:end] = Time.at(@end_time).strftime("%Y-%m-%dT%H:%M:%SZ") + summary[:start] = Time.at(@start_time).iso8601 + summary[:end] = Time.at(@end_time).iso8601 - summary[:range] = "all_time" - summary[:human_readable_range] = "All Time" + summary[:range] = @range_name || "custom" + summary[:human_readable_range] = @range&.[](:human_name) || "custom" @total_seconds = @scope.duration_seconds || 0 summary[:total_seconds] = @total_seconds @total_days = (@end_time - @start_time) / 86400 summary[:daily_average] = @total_days.zero? ? 0 : @total_seconds / @total_days + @days_including_holidays = @scope.distinct.count(Arel.sql("DATE(timezone(?, to_timestamp(time)))", @tz)) + summary[:days_including_holidays] = @days_including_holidays summary[:human_readable_total] = ApplicationController.helpers.short_time_detailed(@total_seconds) summary[:human_readable_daily_average] = ApplicationController.helpers.short_time_detailed(summary[:daily_average]) summary[:languages] = generate_summary_chunk(:language) if @specific_filters.include?(:languages) summary[:projects] = generate_summary_chunk(:project) if @specific_filters.include?(:projects) + summary[:editors] = generate_summary_chunk(:editor) if @specific_filters.include?(:editors) + summary[:machines] = generate_summary_chunk(:machine) if @specific_filters.include?(:machines) + summary[:operating_systems] = generate_summary_chunk(:operating_system) if @specific_filters.include?(:operating_systems) summary end