diff --git a/.gitignore b/.gitignore index 89a4c4f..5fdcc41 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,3 @@ public/vite-dev public/vite-ssr .vite - -app/javascript/api/ -app/javascript/types/serializers/ \ No newline at end of file diff --git a/Gemfile b/Gemfile index 7d48826..fd10c40 100644 --- a/Gemfile +++ b/Gemfile @@ -77,8 +77,6 @@ gem "activerecord-import" # Fast JSON parsing gem "oj" -gem "oj_serializers" -gem "types_from_serializers" # Rack Mini Profiler [https://github.com/MiniProfiler/rack-mini-profiler] gem "rack-mini-profiler" @@ -163,6 +161,5 @@ gem "tailwindcss-ruby", "~> 4.1" gem "tailwindcss-rails", "~> 4.2" gem "inertia_rails", "~> 3.17" -gem "js_from_routes" gem "vite_rails", "~> 3.0" diff --git a/Gemfile.lock b/Gemfile.lock index 6a974be..553c508 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,7 +151,7 @@ GEM tzinfo faker (3.6.0) i18n (>= 1.8.11, < 2) - faraday (2.14.1) + faraday (2.14.0) faraday-net_http (>= 2.0, < 3.5) json logger @@ -225,17 +225,14 @@ GEM inertia_rails (3.17.0) railties (>= 6) io-console (0.8.2) - irb (1.17.0) + irb (1.16.0) pp (>= 0.6.0) - prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) jbuilder (2.14.1) actionview (>= 7.0.0) activesupport (>= 7.0.0) - js_from_routes (4.0.2) - railties (>= 5.1, < 9) - json (2.18.1) + json (2.18.0) json-schema (6.1.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) @@ -263,10 +260,6 @@ GEM railties (>= 6.1) rexml lint_roller (1.1.0) - listen (3.10.0) - logger - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) llhttp-ffi (0.5.1) ffi-compiler (~> 1.0) rake (~> 13.0) @@ -326,11 +319,8 @@ GEM faraday (>= 1.0, < 3.0) faraday-net_http_persistent net-http-persistent - oj (3.16.15) + oj (3.16.14) bigdecimal (>= 3.0) - ostruct (>= 0.2) - oj_serializers (2.1.0) - oj (>= 3.14.0) ostruct (0.6.3) paper_trail (17.0.0) activerecord (>= 7.1) @@ -422,10 +412,7 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.1) - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) - rdoc (7.2.0) + rdoc (7.1.0) erb psych (>= 4.0.0) tsort @@ -464,7 +451,7 @@ GEM rswag-ui (2.17.0) actionpack (>= 5.2, < 8.2) railties (>= 5.2, < 8.2) - rubocop (1.84.1) + rubocop (1.84.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -568,10 +555,6 @@ GEM turbo-rails (2.0.23) actionpack (>= 7.1.0) railties (>= 7.1.0) - types_from_serializers (2.5.0) - listen (~> 3.2) - oj_serializers (~> 2.0, >= 2.0.2) - railties (>= 5.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unaccent (0.4.0) @@ -642,14 +625,12 @@ DEPENDENCIES importmap-rails inertia_rails (~> 3.17) jbuilder - js_from_routes kamal letter_opener letter_opener_web (~> 3.0) memory_profiler norairrecord (~> 0.5.1) oj - oj_serializers paper_trail pg propshaft @@ -681,7 +662,6 @@ DEPENDENCIES tailwindcss-ruby (~> 4.1) thruster turbo-rails - types_from_serializers tzinfo-data vite_rails (~> 3.0) web-console diff --git a/app/controllers/api/v1/stats_controller.rb b/app/controllers/api/v1/stats_controller.rb index de15359..89c0e81 100644 --- a/app/controllers/api/v1/stats_controller.rb +++ b/app/controllers/api/v1/stats_controller.rb @@ -63,34 +63,47 @@ class Api::V1::StatsController < ApplicationController service_params[:end_date] = end_date service_params[:scope] = scope if scope.present? - if params[:total_seconds] == "true" - query = @user.heartbeats - .coding_only - .with_valid_timestamps - .where(time: start_date..end_date) + # use TestWakatimeService when test_param=true for all requests + if params[:test_param] == "true" + service_params[:boundary_aware] = true # always and i mean always use boundary aware in testwakatime service - if params[:filter_by_project].present? - filter_by_project = params[:filter_by_project].split(",") - query = query.where(project: filter_by_project) + if params[:total_seconds] == "true" + summary = TestWakatimeService.new(**service_params).generate_summary + return render json: { total_seconds: summary[:total_seconds] } end - if params[:filter_by_category].present? - filter_by_category = params[:filter_by_category].split(",") - query = query.where(category: filter_by_category) + summary = TestWakatimeService.new(**service_params).generate_summary + else + if params[:total_seconds] == "true" + query = @user.heartbeats + .coding_only + .with_valid_timestamps + .where(time: start_date..end_date) + + if params[:filter_by_project].present? + filter_by_project = params[:filter_by_project].split(",") + query = query.where(project: filter_by_project) + end + + if params[:filter_by_category].present? + filter_by_category = params[:filter_by_category].split(",") + query = query.where(category: filter_by_category) + end + + # do the boundary thingie if requested + use_boundary_aware = params[:boundary_aware] == "true" + total_seconds = if use_boundary_aware + Heartbeat.duration_seconds_boundary_aware(query, start_date.to_f, end_date.to_f) || 0 + else + query.duration_seconds || 0 + end + + return render json: { total_seconds: total_seconds } end - use_boundary_aware = params[:boundary_aware] == "true" - total_seconds = if use_boundary_aware - Heartbeat.duration_seconds_boundary_aware(query, start_date.to_f, end_date.to_f) || 0 - else - query.duration_seconds || 0 - end - - return render json: { total_seconds: total_seconds } + summary = WakatimeService.new(**service_params).generate_summary end - summary = WakatimeService.new(**service_params).generate_summary - if params[:features]&.include?("projects") && params[:filter_by_project].present? filter_by_project = params[:filter_by_project].split(",") heartbeats = @user.heartbeats diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index d553da2..4106baa 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -209,18 +209,15 @@ class StaticPagesController < InertiaController interval = params[:interval] key = [ current_user ] + filters.map { |f| params[f] } + [ interval.to_s, params[:from], params[:to] ] hb = current_user.heartbeats - base_hb = hb h = ApplicationController.helpers Rails.cache.fetch(key, expires_in: 5.minutes) do archived = current_user.project_repo_mappings.archived.pluck(:project_name) result = {} - grouped_durations = {} Time.use_zone(current_user.timezone) do filters.each do |f| - raw_options = base_hb.distinct.pluck(f).compact_blank - options = raw_options + options = current_user.heartbeats.distinct.pluck(f).compact_blank options = options.reject { |n| archived.include?(n) } if f == :project result[f] = options.map { |k| f == :language ? k.categorize_language : (%i[operating_system editor].include?(f) ? k.capitalize : k) @@ -231,7 +228,7 @@ class StaticPagesController < InertiaController hb = if %i[operating_system editor].include?(f) hb.where(f => arr.flat_map { |v| [ v.downcase, v.capitalize ] }.uniq) elsif f == :language - raw = raw_options.select { |l| arr.include?(l.categorize_language) } + raw = current_user.heartbeats.distinct.pluck(f).compact_blank.select { |l| arr.include?(l.categorize_language) } raw.any? ? hb.where(f => raw) : hb else hb.where(f => arr) @@ -244,8 +241,7 @@ class StaticPagesController < InertiaController result[:total_heartbeats] = hb.count filters.each do |f| - grouped_durations[f] ||= hb.group(f).duration_seconds - stats = grouped_durations[f] + stats = hb.group(f).duration_seconds stats = stats.reject { |n, _| archived.include?(n) } if f == :project result["top_#{f}"] = stats.max_by { |_, v| v }&.first end @@ -255,15 +251,13 @@ class StaticPagesController < InertiaController result["top_language"] &&= h.display_language_name(result["top_language"]) unless result["singular_project"] - grouped_durations[:project] ||= hb.group(:project).duration_seconds - result[:project_durations] = grouped_durations[:project] + result[:project_durations] = hb.group(:project).duration_seconds .reject { |p, _| archived.include?(p) }.sort_by { |_, d| -d }.first(10).to_h end %i[language editor operating_system category].each do |f| next if result["singular_#{f}"] - grouped_durations[f] ||= hb.group(f).duration_seconds - stats = grouped_durations[f].each_with_object({}) do |(raw, dur), agg| + stats = hb.group(f).duration_seconds.each_with_object({}) do |(raw, dur), agg| k = raw.to_s.presence || "Unknown" k = f == :language ? (k == "Unknown" ? k : k.categorize_language) : (%i[editor operating_system].include?(f) ? k.downcase : k) agg[k] = (agg[k] || 0) + dur @@ -279,13 +273,10 @@ class StaticPagesController < InertiaController }.to_h end - weekly_project_durations = Heartbeat.weekly_grouped_durations(hb, - group_column: :project, - timezone: current_user.timezone, - weeks: 12) result[:weekly_project_stats] = (0..11).to_h do |w| - ws = w.weeks.ago.beginning_of_week.to_date - [ ws.iso8601, (weekly_project_durations[ws] || {}).reject { |p, _| archived.include?(p) } ] + ws = w.weeks.ago.beginning_of_week + [ ws.to_date.iso8601, hb.where(time: ws.to_f..w.weeks.ago.end_of_week.to_f) + .group(:project).duration_seconds.reject { |p, _| archived.include?(p) } ] end end result[:selected_interval] = interval.to_s diff --git a/app/javascript/layouts/AppLayout.svelte b/app/javascript/layouts/AppLayout.svelte index 621444a..611225e 100644 --- a/app/javascript/layouts/AppLayout.svelte +++ b/app/javascript/layouts/AppLayout.svelte @@ -3,9 +3,67 @@ import { usePoll } from "@inertiajs/svelte"; import { onMount, onDestroy } from "svelte"; import plur from "plur"; - import type InertiaLayoutProps from "../types/serializers/Inertia/LayoutProps"; - let { layout, children }: { layout: InertiaLayoutProps; children?: import('svelte').Snippet } = + type NavLink = { + label: string; + href?: string; + active?: boolean; + badge?: number | null; + action?: string; + }; + + type LayoutNav = { + flash: { message: string; class_name: string }[]; + user_present: boolean; + user_mention_html?: string | null; + streak_html?: string | null; + admin_level_html?: string | null; + login_path: string; + links: NavLink[]; + dev_links: NavLink[]; + admin_links: NavLink[]; + viewer_links: NavLink[]; + superadmin_links: NavLink[]; + activities_html?: string | null; + }; + + type Footer = { + git_version: string; + commit_link: string; + server_start_time_ago: string; + heartbeat_recent_count: number; + heartbeat_recent_imported_count: number; + query_count: number; + query_cache_count: number; + cache_hits: number; + cache_misses: number; + requests_per_second: string; + active_users_graph: { height: number; title: string }[]; + }; + + type CurrentlyHackingUser = { + id: number; + display_name?: string; + slack_uid?: string; + avatar_url?: string; + active_project?: { name: string; repo_url?: string | null }; + }; + + type LayoutProps = { + nav: LayoutNav; + footer: Footer; + currently_hacking: { + count: number; + users: CurrentlyHackingUser[]; + interval: number; + }; + csrf_token: string; + signout_path: string; + show_stop_impersonating: boolean; + stop_impersonating_path: string; + }; + + let { layout, children }: { layout: LayoutProps; children?: () => unknown } = $props(); const isBrowser = @@ -14,7 +72,7 @@ let navOpen = $state(false); let logoutOpen = $state(false); let currentlyExpanded = $state(false); - let flashVisible = $state(false); + let flashVisible = $state(layout.nav.flash.length > 0); let flashHiding = $state(false); const flashHideDelay = 6000; const flashExitDuration = 250; @@ -46,7 +104,7 @@ const countLabel = () => `${layout.currently_hacking.count} ${plur("person", layout.currently_hacking.count)} currently hacking`; - const visualizeGitUrl = (url?: string | null): string => + const visualizeGitUrl = (url?: string | null) => url?.startsWith("https://github.com/") ? url.replace( "https://github.com/", @@ -62,10 +120,6 @@ if (isBrowser) document.body.classList.toggle("overflow-hidden", navOpen); }); - $effect(() => { - flashVisible = layout.nav.flash.length > 0; - }); - $effect(() => { if (!layout.nav.flash.length) { flashVisible = false; @@ -144,7 +198,7 @@ /> -
+