mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 21:05:15 +00:00
Re-add test param (#921)
* Re-add test_param
* Revert "`types_from_initializers` + `js_from_routes` + performance fixes (#918)"
This reverts commit 384a618c15.
* bin/rubocop -A
This commit is contained in:
parent
82a11a52d0
commit
0d089a01e9
52 changed files with 436 additions and 568 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -49,6 +49,3 @@ public/vite-dev
|
|||
public/vite-ssr
|
||||
|
||||
.vite
|
||||
|
||||
app/javascript/api/
|
||||
app/javascript/types/serializers/
|
||||
3
Gemfile
3
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"
|
||||
|
|
|
|||
32
Gemfile.lock
32
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="nav-overlay" class:open={navOpen} onclick={closeNav} role="button" tabindex="0" onkeydown={(e) => e.key === 'Enter' && closeNav()}></div>
|
||||
<div class="nav-overlay" class:open={navOpen} onclick={closeNav}></div>
|
||||
|
||||
<aside
|
||||
class="flex flex-col min-h-screen w-52 bg-dark text-white px-3 py-4 rounded-r-lg overflow-y-auto lg:block"
|
||||
|
|
@ -176,10 +230,10 @@
|
|||
<nav class="space-y-1">
|
||||
{#each layout.nav.links as link}
|
||||
{#if link.action === "logout"}
|
||||
<button
|
||||
<a
|
||||
type="button"
|
||||
onclick={openLogout}
|
||||
class={`${navLinkClass(false)} cursor-pointer text-left w-full bg-transparent border-0`}>Logout</button
|
||||
class={`${navLinkClass(false)} cursor-pointer`}>Logout</a
|
||||
>
|
||||
{:else}
|
||||
<a
|
||||
|
|
@ -323,9 +377,8 @@
|
|||
<div
|
||||
class="fixed top-0 right-5 max-w-sm max-h-[80vh] bg-dark border border-darkless rounded-b-xl shadow-lg z-1000 overflow-hidden transform transition-transform duration-300 ease-out"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="currently-hacking p-3 bg-dark cursor-pointer select-none flex items-center justify-between w-full border-0"
|
||||
<div
|
||||
class="currently-hacking p-3 bg-dark cursor-pointer select-none flex items-center justify-between"
|
||||
onclick={toggleCurrentlyHacking}
|
||||
>
|
||||
<div class="text-white text-sm font-medium">
|
||||
|
|
@ -336,7 +389,7 @@
|
|||
<span class="text-base">{countLabel()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if currentlyExpanded}
|
||||
{#if layout.currently_hacking.users.length === 0}
|
||||
|
|
@ -381,24 +434,22 @@
|
|||
<div class="text-xs text-muted ml-8">
|
||||
working on
|
||||
{#if user.active_project.repo_url}
|
||||
{@const repoUrl = user.active_project.repo_url as string}
|
||||
<a
|
||||
href={repoUrl}
|
||||
href={user.active_project.repo_url}
|
||||
target="_blank"
|
||||
class="text-accent hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
{user.active_project.name as string}
|
||||
{user.active_project.name}
|
||||
</a>
|
||||
{@const vizUrl = visualizeGitUrl(repoUrl)}
|
||||
{#if vizUrl}
|
||||
<a
|
||||
href={vizUrl}
|
||||
target="_blank"
|
||||
class="ml-1">🌌</a
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
{user.active_project.name as string}
|
||||
{user.active_project.name}
|
||||
{/if}
|
||||
{#if visualizeGitUrl(user.active_project.repo_url)}
|
||||
<a
|
||||
href={visualizeGitUrl(user.active_project.repo_url)}
|
||||
target="_blank"
|
||||
class="ml-1">🌌</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -417,10 +468,6 @@
|
|||
class:pointer-events-none={!logoutOpen}
|
||||
style="background-color: rgba(0, 0, 0, 0.5);backdrop-filter: blur(4px);"
|
||||
onclick={(e) => e.target === e.currentTarget && closeLogout()}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
aria-label="Close logout modal"
|
||||
onkeydown={(e) => e.key === 'Escape' && closeLogout()}
|
||||
>
|
||||
<div
|
||||
class={`bg-dark border border-primary rounded-lg p-6 max-w-md w-full mx-4 flex flex-col items-center justify-center transform transition-transform duration-300 ease-in-out ${logoutOpen ? "scale-100" : "scale-95"}`}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import type InertiaDocsIndexProps from "../../types/serializers/Inertia/DocsIndexProps";
|
||||
import { users } from "../../api";
|
||||
|
||||
let {
|
||||
popular_editors,
|
||||
all_editors,
|
||||
}: InertiaDocsIndexProps = $props();
|
||||
}: {
|
||||
popular_editors: [string, string][];
|
||||
all_editors: [string, string][];
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<a
|
||||
href={users.wakatimeSetup.path()}
|
||||
href="/my/wakatime_setup"
|
||||
class="flex flex-col items-center justify-center p-6 bg-surface border border-surface-200 rounded-lg hover:border-primary transition-colors"
|
||||
>
|
||||
<div class="text-2xl mb-2">⚡</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { inertia } from "@inertiajs/svelte";
|
||||
import type InertiaDocsShowProps from "../../types/serializers/Inertia/DocsShowProps";
|
||||
|
||||
let {
|
||||
doc_path,
|
||||
|
|
@ -9,7 +8,14 @@
|
|||
breadcrumbs,
|
||||
edit_url,
|
||||
meta,
|
||||
}: InertiaDocsShowProps = $props();
|
||||
}: {
|
||||
doc_path: string;
|
||||
title: string;
|
||||
rendered_content: string;
|
||||
breadcrumbs: { name: string; href: string | null; is_link: boolean }[];
|
||||
edit_url: string;
|
||||
meta: { description: string; keywords: string };
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type InertiaDocsNotFoundProps from "../../types/serializers/Inertia/DocsNotFoundProps";
|
||||
import { staticPages } from "../../api";
|
||||
|
||||
let {
|
||||
status_code = 404,
|
||||
title = "Page Not Found",
|
||||
message = "The page you were looking for doesn't exist.",
|
||||
}: Partial<InertiaDocsNotFoundProps> = $props();
|
||||
}: {
|
||||
status_code?: number;
|
||||
title?: string;
|
||||
message?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -20,7 +21,7 @@
|
|||
<h2 class="text-2xl font-semibold text-white mb-4">{title}</h2>
|
||||
<p class="text-secondary mb-8">{message}</p>
|
||||
<a
|
||||
href={staticPages.index.path()}
|
||||
href="/"
|
||||
class="inline-flex items-center justify-center px-6 py-3 rounded-lg bg-primary text-white font-medium hover:brightness-110 transition-all"
|
||||
>
|
||||
Go Home
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type InertiaHomeSignedInProps from "../../types/serializers/Inertia/HomeSignedInProps";
|
||||
import type {
|
||||
ActivityGraphData,
|
||||
} from "../../types/index";
|
||||
import BanNotice from "./signedIn/BanNotice.svelte";
|
||||
import GitHubLinkBanner from "./signedIn/GitHubLinkBanner.svelte";
|
||||
import SetupNotice from "./signedIn/SetupNotice.svelte";
|
||||
|
|
@ -7,6 +9,36 @@
|
|||
import Dashboard from "./signedIn/Dashboard.svelte";
|
||||
import ActivityGraph from "./signedIn/ActivityGraph.svelte";
|
||||
|
||||
type SocialProofUser = { display_name: string; avatar_url: string };
|
||||
|
||||
type FilterableDashboardData = {
|
||||
total_time: number;
|
||||
total_heartbeats: number;
|
||||
top_project: string | null;
|
||||
top_language: string | null;
|
||||
top_editor: string | null;
|
||||
top_operating_system: string | null;
|
||||
project_durations: Record<string, number>;
|
||||
language_stats: Record<string, number>;
|
||||
editor_stats: Record<string, number>;
|
||||
operating_system_stats: Record<string, number>;
|
||||
category_stats: Record<string, number>;
|
||||
weekly_project_stats: Record<string, Record<string, number>>;
|
||||
project: string[];
|
||||
language: string[];
|
||||
editor: string[];
|
||||
operating_system: string[];
|
||||
category: string[];
|
||||
selected_interval: string;
|
||||
selected_from: string;
|
||||
selected_to: string;
|
||||
selected_project: string[];
|
||||
selected_language: string[];
|
||||
selected_editor: string[];
|
||||
selected_operating_system: string[];
|
||||
selected_category: string[];
|
||||
};
|
||||
|
||||
let {
|
||||
flavor_text,
|
||||
trust_level_red,
|
||||
|
|
@ -23,7 +55,23 @@
|
|||
todays_editors,
|
||||
filterable_dashboard_data,
|
||||
activity_graph,
|
||||
}: InertiaHomeSignedInProps = $props();
|
||||
}: {
|
||||
flavor_text: string;
|
||||
trust_level_red: boolean;
|
||||
show_wakatime_setup_notice: boolean;
|
||||
ssp_message?: string | null;
|
||||
ssp_users_recent: SocialProofUser[];
|
||||
ssp_users_size: number;
|
||||
github_uid_blank: boolean;
|
||||
github_auth_path: string;
|
||||
wakatime_setup_path: string;
|
||||
show_logged_time_sentence: boolean;
|
||||
todays_duration_display: string;
|
||||
todays_languages: string[];
|
||||
todays_editors: string[];
|
||||
filterable_dashboard_data: FilterableDashboardData;
|
||||
activity_graph: ActivityGraphData;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type InertiaHomeSignedOutProps from "../../types/serializers/Inertia/HomeSignedOutProps";
|
||||
type HomeStats = { seconds_tracked?: number; users_tracked?: number };
|
||||
|
||||
let {
|
||||
hca_auth_path,
|
||||
|
|
@ -10,7 +10,16 @@
|
|||
dev_magic_link,
|
||||
csrf_token,
|
||||
home_stats,
|
||||
}: InertiaHomeSignedOutProps = $props();
|
||||
}: {
|
||||
hca_auth_path: string;
|
||||
slack_auth_path: string;
|
||||
email_auth_path: string;
|
||||
sign_in_email: boolean;
|
||||
show_dev_tool: boolean;
|
||||
dev_magic_link?: string | null;
|
||||
csrf_token: string;
|
||||
home_stats: HomeStats;
|
||||
} = $props();
|
||||
|
||||
let isSigningIn = $state(false);
|
||||
|
||||
|
|
@ -31,15 +40,11 @@
|
|||
const formatNumber = (value: number) => numberFormatter.format(value);
|
||||
|
||||
const hoursTracked = $derived(
|
||||
home_stats?.seconds_tracked && typeof home_stats.seconds_tracked === 'number'
|
||||
home_stats?.seconds_tracked
|
||||
? Math.floor(home_stats.seconds_tracked / 3600)
|
||||
: 0,
|
||||
);
|
||||
const usersTracked = $derived(
|
||||
home_stats?.users_tracked && typeof home_stats.users_tracked === 'number'
|
||||
? home_stats.users_tracked
|
||||
: 0
|
||||
);
|
||||
const usersTracked = $derived(home_stats?.users_tracked ?? 0);
|
||||
|
||||
// Grid background pattern
|
||||
const gridPattern = `background-image: linear-gradient(to right, #4A2D3133 1px, transparent 1px), linear-gradient(to bottom, #4A2D3133 1px, transparent 1px); background-size: 6rem 6rem;`;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts">
|
||||
import type InertiaSspUser from "../../../types/serializers/Inertia/SspUser";
|
||||
import SocialProofUsers from "./SocialProofUsers.svelte";
|
||||
|
||||
type SocialProofUser = { display_name: string; avatar_url: string };
|
||||
|
||||
let {
|
||||
wakatime_setup_path,
|
||||
ssp_message,
|
||||
|
|
@ -10,7 +11,7 @@
|
|||
}: {
|
||||
wakatime_setup_path: string;
|
||||
ssp_message?: string | null;
|
||||
ssp_users_recent: InertiaSspUser[];
|
||||
ssp_users_recent: SocialProofUser[];
|
||||
ssp_users_size: number;
|
||||
} = $props();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import type InertiaSspUser from "../../../types/serializers/Inertia/SspUser";
|
||||
type SocialProofUser = { display_name: string; avatar_url: string };
|
||||
|
||||
let {
|
||||
users,
|
||||
total_size,
|
||||
message,
|
||||
}: {
|
||||
users: InertiaSspUser[];
|
||||
users: SocialProofUser[];
|
||||
total_size: number;
|
||||
message?: string | null;
|
||||
} = $props();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type InertiaWakatimeSetupIndexProps from "../../types/serializers/Inertia/WakatimeSetupIndexProps";
|
||||
import { users } from "../../api";
|
||||
import { Link } from "@inertiajs/svelte";
|
||||
import Stepper from "./Stepper.svelte";
|
||||
|
||||
let { current_user_api_key, setup_os, api_url, heartbeat_check_url }: InertiaWakatimeSetupIndexProps =
|
||||
interface Props {
|
||||
current_user_api_key: string;
|
||||
setup_os: string;
|
||||
api_url: string;
|
||||
heartbeat_check_url: string;
|
||||
}
|
||||
|
||||
let { current_user_api_key, setup_os, api_url, heartbeat_check_url }: Props =
|
||||
$props();
|
||||
|
||||
let activeSection = $derived(
|
||||
|
|
@ -111,7 +117,7 @@
|
|||
</p>
|
||||
|
||||
<a
|
||||
href={users.wakatimeSetupStep2.path()}
|
||||
href="/my/wakatime_setup/step-2"
|
||||
class="inline-flex items-center justify-center bg-primary text-white px-6 py-2 rounded-lg font-semibold transition-all"
|
||||
>
|
||||
Continue to Step 2 →
|
||||
|
|
@ -218,7 +224,6 @@
|
|||
class="mt-4 rounded-lg overflow-hidden border border-darkless"
|
||||
>
|
||||
<iframe
|
||||
title="Mac/Linux setup tutorial video"
|
||||
width="100%"
|
||||
height="300"
|
||||
src="https://www.youtube.com/embed/QTwhJy7nT_w?modestbranding=1&rel=0"
|
||||
|
|
@ -306,7 +311,6 @@
|
|||
class="mt-4 rounded-lg overflow-hidden border border-darkless"
|
||||
>
|
||||
<iframe
|
||||
title="Windows setup tutorial video"
|
||||
width="100%"
|
||||
height="300"
|
||||
src="https://www.youtube.com/embed/fX9tsiRvzhg?modestbranding=1&rel=0"
|
||||
|
|
@ -354,7 +358,7 @@ heartbeat_rate_limit_seconds = 30</code
|
|||
|
||||
<div class="text-center">
|
||||
<a
|
||||
href={users.wakatimeSetupStep2.path()}
|
||||
href="/my/wakatime_setup/step-2"
|
||||
class="text-xs text-secondary hover:text-white transition-colors"
|
||||
>Skip to next step</a
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { users } from "../../api";
|
||||
import { Link } from '@inertiajs/svelte';
|
||||
import Stepper from './Stepper.svelte';
|
||||
|
||||
const editors = [
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||
{#each editors as editor}
|
||||
<a
|
||||
href={`${users.wakatimeSetupStep3.path()}?editor=${editor.id}`}
|
||||
href={`/my/wakatime_setup/step-3?editor=${editor.id}`}
|
||||
class="group flex flex-col items-center justify-center p-6 bg-dark border border-darkless rounded-xl hover:border-primary transition-all duration-200 hover:shadow-lg hover:shadow-primary/10">
|
||||
<div class="w-16 h-16 mb-4 flex items-center justify-center transition-transform duration-200 group-hover:scale-110">
|
||||
{#if editor.icon}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type InertiaWakatimeSetupStep3Props from "../../types/serializers/Inertia/WakatimeSetupStep3Props";
|
||||
import { users } from "../../api";
|
||||
import { Link } from "@inertiajs/svelte";
|
||||
import Stepper from "./Stepper.svelte";
|
||||
|
||||
let { current_user_api_key, editor, heartbeat_check_url }: InertiaWakatimeSetupStep3Props = $props();
|
||||
interface Props {
|
||||
current_user_api_key: string;
|
||||
editor: string;
|
||||
heartbeat_check_url: string;
|
||||
}
|
||||
|
||||
let { current_user_api_key, editor, heartbeat_check_url }: Props = $props();
|
||||
|
||||
let hasHeartbeat = $state(false);
|
||||
let heartbeatTimeAgo = $state("");
|
||||
|
|
@ -275,7 +280,7 @@
|
|||
</p>
|
||||
|
||||
<a
|
||||
href={users.wakatimeSetupStep4.path()}
|
||||
href="/my/wakatime_setup/step-4"
|
||||
class="inline-flex items-center justify-center bg-primary hover:bg-primary/90 text-white px-8 py-3 rounded-lg font-semibold w-full transition-all transform hover:scale-[1.02] active:scale-[0.98]"
|
||||
>
|
||||
Continue →
|
||||
|
|
@ -286,7 +291,7 @@
|
|||
|
||||
<div class="text-center">
|
||||
<a
|
||||
href={users.wakatimeSetupStep4.path()}
|
||||
href="/my/wakatime_setup/step-4"
|
||||
class="text-xs text-secondary hover:text-white transition-colors"
|
||||
>Skip to finish</a
|
||||
>
|
||||
|
|
@ -343,23 +348,22 @@
|
|||
</div>
|
||||
|
||||
<a
|
||||
href={users.wakatimeSetupStep4.path()}
|
||||
href="/my/wakatime_setup/step-4"
|
||||
class="inline-flex items-center justify-center bg-primary hover:bg-primary/90 text-white px-8 py-3 rounded-lg font-semibold w-full"
|
||||
>
|
||||
Next Step
|
||||
</a>
|
||||
{:else if editor && editorData[editor]}
|
||||
{@const editorInfo = editorData[editor]}
|
||||
{:else if editorData[editor]}
|
||||
<div class="bg-dark border border-darkless rounded-xl p-8 shadow-sm mb-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<img
|
||||
src={editorInfo.icon}
|
||||
alt={editorInfo.name}
|
||||
src={editorData[editor].icon}
|
||||
alt={editorData[editor].name}
|
||||
class="w-12 h-12 object-contain"
|
||||
/>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">
|
||||
{editorInfo.name} Setup
|
||||
{editorData[editor].name} Setup
|
||||
</h3>
|
||||
<p class="text-secondary text-sm">
|
||||
Install the plugin with your preferred package manager.
|
||||
|
|
@ -368,7 +372,7 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
{#each editorInfo.methods as method, index}
|
||||
{#each editorData[editor].methods as method, index}
|
||||
{#if index > 0}
|
||||
<div class="pt-6 border-t border-darkless"></div>
|
||||
{/if}
|
||||
|
|
@ -393,7 +397,7 @@
|
|||
</div>
|
||||
|
||||
<a
|
||||
href={users.wakatimeSetupStep4.path()}
|
||||
href="/my/wakatime_setup/step-4"
|
||||
class="inline-flex items-center justify-center bg-primary hover:bg-primary/90 text-white px-8 py-3 rounded-lg font-semibold w-full"
|
||||
>
|
||||
Next Step
|
||||
|
|
@ -469,7 +473,7 @@
|
|||
</div>
|
||||
|
||||
<a
|
||||
href={users.wakatimeSetupStep4.path()}
|
||||
href="/my/wakatime_setup/step-4"
|
||||
class="inline-flex items-center justify-center bg-primary hover:bg-primary/90 text-white px-8 py-3 rounded-lg font-semibold w-full"
|
||||
>
|
||||
Next Step
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type InertiaWakatimeSetupStep4Props from "../../types/serializers/Inertia/WakatimeSetupStep4Props";
|
||||
import { staticPages } from "../../api";
|
||||
import { Link } from "@inertiajs/svelte";
|
||||
import Stepper from "./Stepper.svelte";
|
||||
|
||||
let { return_url, return_button_text }: InertiaWakatimeSetupStep4Props = $props();
|
||||
interface Props {
|
||||
return_url?: string;
|
||||
return_button_text: string;
|
||||
}
|
||||
|
||||
let { return_url, return_button_text }: Props = $props();
|
||||
|
||||
let agreed = $state(false);
|
||||
</script>
|
||||
|
|
@ -55,7 +59,7 @@
|
|||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a
|
||||
href={staticPages.index.path()}
|
||||
href="/"
|
||||
class="px-8 py-3 bg-primary border border-transparent text-white rounded-lg transition-all font-semibold transform active:scale-[0.98] text-center {agreed
|
||||
? ''
|
||||
: 'opacity-50 cursor-not-allowed pointer-events-none'}"
|
||||
|
|
|
|||
8
app/javascript/types/globals.d.ts
vendored
8
app/javascript/types/globals.d.ts
vendored
|
|
@ -7,11 +7,3 @@ declare module '@inertiajs/core' {
|
|||
errorValueType: string[]
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
showLogout?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
|
|
|
|||
|
|
@ -249,41 +249,6 @@ module Heartbeatable
|
|||
end
|
||||
end
|
||||
|
||||
def weekly_grouped_durations(scope, group_column:, timezone:, weeks: 12)
|
||||
scope = scope.with_valid_timestamps
|
||||
timeout = heartbeat_timeout_duration.to_i
|
||||
tz = timezone.presence || "UTC"
|
||||
weeks = weeks.to_i
|
||||
weeks = 1 if weeks < 1
|
||||
|
||||
group_expr = group_column.to_s.include?("(") ? group_column : connection.quote_column_name(group_column)
|
||||
week_trunc = "DATE_TRUNC('week', to_timestamp(time) AT TIME ZONE #{connection.quote(tz)})"
|
||||
|
||||
range_start, range_end = Time.use_zone(tz) do
|
||||
[ (weeks - 1).weeks.ago.beginning_of_week, Time.current.end_of_week ]
|
||||
end
|
||||
|
||||
scoped = scope.where(time: range_start.to_f..range_end.to_f)
|
||||
capped_diffs = scoped
|
||||
.select("#{group_expr} as grouped_time, #{week_trunc} as week_group, CASE
|
||||
WHEN LAG(time) OVER (PARTITION BY #{group_expr}, #{week_trunc} ORDER BY time) IS NULL THEN 0
|
||||
ELSE LEAST(time - LAG(time) OVER (PARTITION BY #{group_expr}, #{week_trunc} ORDER BY time), #{timeout})
|
||||
END as diff")
|
||||
.where.not(time: nil)
|
||||
.unscope(:group)
|
||||
|
||||
rows = connection.select_all(
|
||||
"SELECT week_group, grouped_time, COALESCE(SUM(diff), 0)::integer as duration
|
||||
FROM (#{capped_diffs.to_sql}) AS diffs
|
||||
GROUP BY week_group, grouped_time"
|
||||
)
|
||||
|
||||
rows.each_with_object(Hash.new { |h, k| h[k] = {} }) do |row, hash|
|
||||
week = row["week_group"].to_date
|
||||
hash[week][row["grouped_time"]] = row["duration"].to_i
|
||||
end
|
||||
end
|
||||
|
||||
def duration_seconds_boundary_aware(scope, start_time, end_time)
|
||||
scope = scope.with_valid_timestamps
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BaseSerializer < Oj::Serializer
|
||||
include TypesFromSerializers::DSL
|
||||
end
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class ActiveUsersGraphEntrySerializer < BaseSerializer
|
||||
attribute :height, type: :number
|
||||
attribute :title, type: :string
|
||||
end
|
||||
end
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class ActivityGraphSerializer < BaseSerializer
|
||||
attribute :start_date, type: :string
|
||||
attribute :end_date, type: :string
|
||||
attribute :duration_by_date, type: "Record<string, number>"
|
||||
attribute :busiest_day_seconds, type: :number
|
||||
attribute :timezone_label, type: :string
|
||||
attribute :timezone_settings_path, type: :string
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class CurrentlyHackingSerializer < BaseSerializer
|
||||
attribute :count, type: :number
|
||||
has_many :users, serializer: Inertia::CurrentlyHackingUserSerializer
|
||||
attribute :interval, type: :number
|
||||
end
|
||||
end
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class CurrentlyHackingUserSerializer < BaseSerializer
|
||||
attribute :id, type: :number
|
||||
attribute :display_name, type: "string | null"
|
||||
attribute :slack_uid, type: "string | null"
|
||||
attribute :avatar_url, type: "string | null"
|
||||
attribute :active_project, type: "Record<string, unknown> | null"
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class DocsBreadcrumbSerializer < BaseSerializer
|
||||
attribute :name, type: :string
|
||||
attribute :href, type: "string | null"
|
||||
attribute :is_link, type: :boolean
|
||||
end
|
||||
end
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class DocsIndexPropsSerializer < BaseSerializer
|
||||
attribute :popular_editors, type: "Array<[string, string]>"
|
||||
attribute :all_editors, type: "Array<[string, string]>"
|
||||
end
|
||||
end
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class DocsMetaSerializer < BaseSerializer
|
||||
attribute :description, type: :string
|
||||
attribute :keywords, type: :string
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class DocsNotFoundPropsSerializer < BaseSerializer
|
||||
attribute :status_code, type: :number
|
||||
attribute :title, type: :string
|
||||
attribute :message, type: :string
|
||||
end
|
||||
end
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class DocsShowPropsSerializer < BaseSerializer
|
||||
attribute :doc_path, type: :string
|
||||
attribute :title, type: :string
|
||||
attribute :rendered_content, type: :string
|
||||
has_many :breadcrumbs, serializer: Inertia::DocsBreadcrumbSerializer
|
||||
attribute :edit_url, type: :string
|
||||
has_one :meta, serializer: Inertia::DocsMetaSerializer
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class ExtensionsIndexPropsSerializer < BaseSerializer
|
||||
end
|
||||
end
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class FlashMessageSerializer < BaseSerializer
|
||||
attribute :message, type: :string
|
||||
attribute :class_name, type: :string
|
||||
end
|
||||
end
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class FooterSerializer < BaseSerializer
|
||||
attribute :git_version, type: :string
|
||||
attribute :commit_link, type: :string
|
||||
attribute :server_start_time_ago, type: :string
|
||||
attribute :heartbeat_recent_count, type: :number
|
||||
attribute :heartbeat_recent_imported_count, type: :number
|
||||
attribute :query_count, type: :number
|
||||
attribute :query_cache_count, type: :number
|
||||
attribute :cache_hits, type: :number
|
||||
attribute :cache_misses, type: :number
|
||||
attribute :requests_per_second, type: :string
|
||||
has_many :active_users_graph, serializer: Inertia::ActiveUsersGraphEntrySerializer
|
||||
end
|
||||
end
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class HomeSignedInPropsSerializer < BaseSerializer
|
||||
attribute :flavor_text, type: :string
|
||||
attribute :trust_level_red, type: :boolean
|
||||
attribute :show_wakatime_setup_notice, type: :boolean
|
||||
attribute :ssp_message, type: "string | null"
|
||||
has_many :ssp_users_recent, serializer: Inertia::SspUserSerializer
|
||||
attribute :ssp_users_size, type: :number
|
||||
attribute :github_uid_blank, type: :boolean
|
||||
attribute :github_auth_path, type: :string
|
||||
attribute :wakatime_setup_path, type: :string
|
||||
attribute :show_logged_time_sentence, type: :boolean
|
||||
attribute :todays_duration_display, type: :string
|
||||
attribute :todays_languages, type: "string[]"
|
||||
attribute :todays_editors, type: "string[]"
|
||||
attribute :filterable_dashboard_data, type: "Record<string, unknown>"
|
||||
has_one :activity_graph, serializer: Inertia::ActivityGraphSerializer
|
||||
end
|
||||
end
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class HomeSignedOutPropsSerializer < BaseSerializer
|
||||
attribute :flavor_text, type: :string
|
||||
attribute :hca_auth_path, type: :string
|
||||
attribute :slack_auth_path, type: :string
|
||||
attribute :email_auth_path, type: :string
|
||||
attribute :sign_in_email, type: :boolean
|
||||
attribute :show_dev_tool, type: :boolean
|
||||
attribute :dev_magic_link, type: "string | null"
|
||||
attribute :csrf_token, type: :string
|
||||
attribute :home_stats, type: "Record<string, unknown>"
|
||||
end
|
||||
end
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class LayoutNavSerializer < BaseSerializer
|
||||
has_many :flash, serializer: Inertia::FlashMessageSerializer
|
||||
attribute :user_present, type: :boolean
|
||||
attribute :user_mention_html, type: "string | null"
|
||||
attribute :streak_html, type: "string | null"
|
||||
attribute :admin_level_html, type: "string | null"
|
||||
attribute :login_path, type: :string
|
||||
has_many :links, serializer: Inertia::NavLinkSerializer
|
||||
has_many :dev_links, serializer: Inertia::NavLinkSerializer
|
||||
has_many :admin_links, serializer: Inertia::NavLinkSerializer
|
||||
has_many :viewer_links, serializer: Inertia::NavLinkSerializer
|
||||
has_many :superadmin_links, serializer: Inertia::NavLinkSerializer
|
||||
attribute :activities_html, type: "string | null"
|
||||
end
|
||||
end
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class LayoutPropsSerializer < BaseSerializer
|
||||
has_one :nav, serializer: Inertia::LayoutNavSerializer
|
||||
has_one :footer, serializer: Inertia::FooterSerializer
|
||||
has_one :currently_hacking, serializer: Inertia::CurrentlyHackingSerializer
|
||||
attribute :csrf_token, type: :string
|
||||
attribute :signout_path, type: :string
|
||||
attribute :show_stop_impersonating, type: :boolean
|
||||
attribute :stop_impersonating_path, type: :string
|
||||
end
|
||||
end
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class NavLinkSerializer < BaseSerializer
|
||||
attribute :label, type: :string
|
||||
attribute :href, type: :string, optional: true
|
||||
attribute :active, type: :boolean, optional: true
|
||||
attribute :badge, type: "number | null", optional: true
|
||||
attribute :action, type: :string, optional: true
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class SspUserSerializer < BaseSerializer
|
||||
attribute :id, type: :number
|
||||
attribute :avatar_url, type: :string
|
||||
attribute :display_name, type: :string
|
||||
end
|
||||
end
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class WakatimeSetupIndexPropsSerializer < BaseSerializer
|
||||
attribute :current_user_api_key, type: "string | null"
|
||||
attribute :setup_os, type: :string
|
||||
attribute :api_url, type: :string
|
||||
attribute :heartbeat_check_url, type: :string
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class WakatimeSetupStep2PropsSerializer < BaseSerializer
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class WakatimeSetupStep3PropsSerializer < BaseSerializer
|
||||
attribute :current_user_api_key, type: "string | null"
|
||||
attribute :editor, type: "string | null"
|
||||
attribute :heartbeat_check_url, type: :string
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Inertia
|
||||
class WakatimeSetupStep4PropsSerializer < BaseSerializer
|
||||
attribute :dino_video_url, type: :string
|
||||
attribute :return_url, type: "string | null"
|
||||
attribute :return_button_text, type: :string
|
||||
end
|
||||
end
|
||||
15
bun.lock
15
bun.lock
|
|
@ -6,16 +6,15 @@
|
|||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@inertiajs/svelte": "^2.3.13",
|
||||
"@js-from-routes/inertia": "^1.10.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"@tsconfig/svelte": "5",
|
||||
"d3-scale": "^4.0.2",
|
||||
"layerchart": "^1.0.13",
|
||||
"plur": "^6.0.0",
|
||||
"svelte": "^5.50.1",
|
||||
"svelte": "5",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tslib": "^2.8.1",
|
||||
|
|
@ -94,8 +93,6 @@
|
|||
|
||||
"@inertiajs/core": ["@inertiajs/core@2.3.13", "", { "dependencies": { "@types/lodash-es": "^4.17.12", "axios": "^1.13.2", "laravel-precognition": "^1.0.1", "lodash-es": "^4.17.23", "qs": "^6.14.1" } }, "sha512-qMHRnb59k/HehXw/WfQt5kPV0k9RapfFcWJZINJnYMwfHDEJ21iNVZjsJHmDN7yWdZmG1Dxi9FP4xarWWgdosQ=="],
|
||||
|
||||
"@inertiajs/inertia": ["@inertiajs/inertia@0.11.1", "", { "dependencies": { "axios": "^0.21.1", "deepmerge": "^4.0.0", "qs": "^6.9.0" } }, "sha512-btmV53c54oW4Z9XF0YyTdIUnM7ue0ONy3/KJOz6J1C5CYIwimiKfDMpz8ZbGJuxS+SPdOlNsqj2ZhlHslpJRZg=="],
|
||||
|
||||
"@inertiajs/svelte": ["@inertiajs/svelte@2.3.13", "", { "dependencies": { "@inertiajs/core": "2.3.13", "@types/lodash-es": "^4.17.12", "laravel-precognition": "^1.0.1", "lodash-es": "^4.17.23" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0" } }, "sha512-tKqAGn3FCdLA57bmZjm+26exVjZVQ0I15/KuoEofZKjZ8/4bndyHhhx79jmelZKlDNj4O3ECz15L5mHfo7YPSQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
|
@ -108,10 +105,6 @@
|
|||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@js-from-routes/core": ["@js-from-routes/core@1.0.3", "", {}, "sha512-rUYNMTYFKRoo4w28BJOAderskjhYOtkLgj00x14FNjGu5PYurnimjYQp70H34PTtWRXIqJKprGHr15VOZZzPiw=="],
|
||||
|
||||
"@js-from-routes/inertia": ["@js-from-routes/inertia@1.10.2", "", { "peerDependencies": { "@inertiajs/inertia": "^0.11", "@js-from-routes/core": "^1.0.0" } }, "sha512-4Lv016v6ir7Ayp8ZsWzlMZfOEHaajd53ZwiueUzrtzkkZ5+HmUW9AmtEK29jom4qmBLVksVyreQL6BjxdEx6/w=="],
|
||||
|
||||
"@layerstack/svelte-actions": ["@layerstack/svelte-actions@1.0.1", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@layerstack/utils": "1.0.1", "d3-array": "^3.2.4", "d3-scale": "^4.0.2", "date-fns": "^4.1.0", "lodash-es": "^4.17.21" } }, "sha512-Tv8B3TeT7oaghx0R0I4avnSdfAT6GxEK+StL8k/hEaa009iNOIGFl3f76kfvNvPioQHAMFGtnWGLPHfsfD41nQ=="],
|
||||
|
||||
"@layerstack/svelte-stores": ["@layerstack/svelte-stores@1.0.2", "", { "dependencies": { "@layerstack/utils": "1.0.1", "d3-array": "^3.2.4", "date-fns": "^4.1.0", "immer": "^10.1.1", "lodash-es": "^4.17.21", "zod": "^3.24.2" } }, "sha512-IxK0UKD0PVxg1VsyaR+n7NyJ+NlvyqvYYAp+J10lkjDQxm0yx58CaF2LBV08T22C3aY1iTlqJaatn/VHV4SoQg=="],
|
||||
|
|
@ -536,7 +529,7 @@
|
|||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"svelte": ["svelte@5.50.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-/Jlom4ddkISyVHXpM2O5dXP9pYnaiFrVQzPbIL1/pEoOa77ZunCb6nDgUCTNCQ/X3t64z9ukrK6R+BbB3kPR3A=="],
|
||||
"svelte": ["svelte@5.49.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-PYLwnngYzyhKzqDlGVlCH4z+NVI8mC0/bTv15vw25CcdOhxENsOHIbQ36oj5DIf3oBazM+STbCAvaskpxtBmWA=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.3.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q=="],
|
||||
|
||||
|
|
@ -572,8 +565,6 @@
|
|||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@inertiajs/inertia/axios": ["axios@0.21.4", "", { "dependencies": { "follow-redirects": "^1.14.0" } }, "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg=="],
|
||||
|
||||
"@layerstack/tailwind/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
JsFromRoutes.config do |config|
|
||||
config.client_library = "@js-from-routes/inertia"
|
||||
config.file_suffix = "Api.ts"
|
||||
config.all_helpers_file = "index.ts"
|
||||
config.output_folder = Rails.root.join("app/javascript/api")
|
||||
end
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
if Rails.env.development? then
|
||||
TypesFromSerializers.config do |config|
|
||||
config.output_dir = Rails.root.join("app/javascript/types/serializers")
|
||||
config.custom_types_dir = Rails.root.join("app/javascript/types")
|
||||
config.transform_keys = ->(key) { key.to_s }
|
||||
end
|
||||
end
|
||||
|
|
@ -20,9 +20,9 @@ Rails.application.routes.draw do
|
|||
mount Rswag::Ui::Engine => "/api-docs"
|
||||
use_doorkeeper
|
||||
|
||||
root "static_pages#index", defaults: { export: true }
|
||||
root "static_pages#index"
|
||||
|
||||
resources :extensions, only: [ :index ], defaults: { export: true }
|
||||
resources :extensions, only: [ :index ]
|
||||
|
||||
constraints AdminLevelConstraint.new(:superadmin) do
|
||||
mount GoodJob::Engine => "good_job"
|
||||
|
|
@ -114,8 +114,8 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
# Docs routes
|
||||
get "docs", to: "docs#index", as: :docs, defaults: { export: true }
|
||||
get "docs/*path", to: "docs#show", as: :doc, defaults: { export: true }
|
||||
get "docs", to: "docs#index", as: :docs
|
||||
get "docs/*path", to: "docs#show", as: :doc
|
||||
|
||||
# Nested under users for admin access
|
||||
resources :users, only: [] do
|
||||
|
|
@ -157,10 +157,10 @@ Rails.application.routes.draw do
|
|||
post "deletion", to: "deletion_requests#create", as: :create_deletion
|
||||
delete "deletion", to: "deletion_requests#cancel", as: :cancel_deletion
|
||||
|
||||
get "my/wakatime_setup", to: "users#wakatime_setup", defaults: { export: true }
|
||||
get "my/wakatime_setup/step-2", to: "users#wakatime_setup_step_2", defaults: { export: true }
|
||||
get "my/wakatime_setup/step-3", to: "users#wakatime_setup_step_3", defaults: { export: true }
|
||||
get "my/wakatime_setup/step-4", to: "users#wakatime_setup_step_4", defaults: { export: true }
|
||||
get "my/wakatime_setup", to: "users#wakatime_setup"
|
||||
get "my/wakatime_setup/step-2", to: "users#wakatime_setup_step_2"
|
||||
get "my/wakatime_setup/step-3", to: "users#wakatime_setup_step_3"
|
||||
get "my/wakatime_setup/step-4", to: "users#wakatime_setup_step_4"
|
||||
|
||||
post "/sailors_log/slack/commands", to: "slack#create"
|
||||
post "/timedump/slack/commands", to: "slack#create"
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
class AddIndexesForDashboardPerformance < ActiveRecord::Migration[8.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
add_index :heartbeats, [ :user_id, :language, :time ],
|
||||
name: "idx_heartbeats_user_language_time",
|
||||
where: "deleted_at IS NULL",
|
||||
algorithm: :concurrently,
|
||||
if_not_exists: true
|
||||
|
||||
add_index :heartbeats, [ :user_id, :editor, :time ],
|
||||
name: "idx_heartbeats_user_editor_time",
|
||||
where: "deleted_at IS NULL",
|
||||
algorithm: :concurrently,
|
||||
if_not_exists: true
|
||||
|
||||
add_index :heartbeats, [ :user_id, :operating_system, :time ],
|
||||
name: "idx_heartbeats_user_operating_system_time",
|
||||
where: "deleted_at IS NULL",
|
||||
algorithm: :concurrently,
|
||||
if_not_exists: true
|
||||
|
||||
add_index :heartbeats, [ :user_id, :category, :time ],
|
||||
name: "idx_heartbeats_user_category_time",
|
||||
where: "deleted_at IS NULL",
|
||||
algorithm: :concurrently,
|
||||
if_not_exists: true
|
||||
|
||||
add_index :heartbeats, [ :user_id, :project ],
|
||||
where: "deleted_at IS NULL",
|
||||
algorithm: :concurrently,
|
||||
if_not_exists: true
|
||||
end
|
||||
end
|
||||
8
db/schema.rb
generated
8
db/schema.rb
generated
|
|
@ -10,10 +10,11 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_02_10_094354) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_02_04_113033) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
enable_extension "pg_stat_statements"
|
||||
create_schema "pganalyze"
|
||||
|
||||
create_table "activities", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
|
|
@ -328,13 +329,8 @@ ActiveRecord::Schema[8.1].define(version: 2026_02_10_094354) do
|
|||
t.index ["raw_heartbeat_upload_id"], name: "index_heartbeats_on_raw_heartbeat_upload_id"
|
||||
t.index ["source_type", "time", "user_id", "project"], name: "index_heartbeats_on_source_type_time_user_project"
|
||||
t.index ["user_agent_id"], name: "index_heartbeats_on_user_agent_id"
|
||||
t.index ["user_id", "category", "time"], name: "idx_heartbeats_user_category_time", where: "(deleted_at IS NULL)"
|
||||
t.index ["user_id", "editor", "time"], name: "idx_heartbeats_user_editor_time", where: "(deleted_at IS NULL)"
|
||||
t.index ["user_id", "id"], name: "index_heartbeats_on_user_id_with_ip", order: { id: :desc }, where: "((ip_address IS NOT NULL) AND (deleted_at IS NULL))"
|
||||
t.index ["user_id", "language", "time"], name: "idx_heartbeats_user_language_time", where: "(deleted_at IS NULL)"
|
||||
t.index ["user_id", "operating_system", "time"], name: "idx_heartbeats_user_operating_system_time", where: "(deleted_at IS NULL)"
|
||||
t.index ["user_id", "project", "time"], name: "idx_heartbeats_user_project_time_stats", where: "((deleted_at IS NULL) AND (project IS NOT NULL))"
|
||||
t.index ["user_id", "project"], name: "index_heartbeats_on_user_id_and_project", where: "(deleted_at IS NULL)"
|
||||
t.index ["user_id", "time", "category"], name: "index_heartbeats_on_user_time_category"
|
||||
t.index ["user_id", "time", "language"], name: "idx_heartbeats_user_time_language_stats", where: "(deleted_at IS NULL)"
|
||||
t.index ["user_id", "time", "language_id"], name: "idx_heartbeats_user_time_language_id", where: "(deleted_at IS NULL)"
|
||||
|
|
|
|||
165
lib/test_wakatime_service.rb
Normal file
165
lib/test_wakatime_service.rb
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
include ApplicationHelper
|
||||
|
||||
class TestWakatimeService
|
||||
def initialize(user: nil, specific_filters: [], allow_cache: true, limit: 10, start_date: nil, end_date: nil, scope: nil, boundary_aware: false)
|
||||
@scope = scope || Heartbeat.all
|
||||
# trusting time from hackatime extensions.....
|
||||
# @scope = @scope.coding_only
|
||||
@scope = @scope.with_valid_timestamps
|
||||
|
||||
# yeah macha we're removing unwated categories
|
||||
@scope = @scope.where.not("LOWER(category) IN (?)", [ "browsing", "ai coding", "meeting", "communicating" ])
|
||||
@user = user
|
||||
@boundary_aware = boundary_aware
|
||||
|
||||
@start_date = convert_to_unix_timestamp(start_date)
|
||||
@end_date = convert_to_unix_timestamp(end_date)
|
||||
|
||||
# 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
|
||||
|
||||
@scope = @scope.where("time >= ? AND time < ?", @start_date, @end_date)
|
||||
|
||||
@limit = limit
|
||||
@limit = nil if @limit&.zero?
|
||||
|
||||
@scope = @scope.where(user_id: @user.id) if @user.present?
|
||||
|
||||
@specific_filters = specific_filters
|
||||
@allow_cache = allow_cache
|
||||
end
|
||||
|
||||
def generate_summary
|
||||
summary = {}
|
||||
|
||||
summary[:username] = @user.display_name if @user.present?
|
||||
summary[:user_id] = @user.id.to_s if @user.present?
|
||||
summary[:is_coding_activity_visible] = true if @user.present?
|
||||
summary[:is_other_usage_visible] = true if @user.present?
|
||||
summary[:status] = "ok"
|
||||
|
||||
@start_time = @start_date
|
||||
@end_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[:range] = "all_time"
|
||||
summary[:human_readable_range] = "All Time"
|
||||
|
||||
@total_seconds = if @boundary_aware
|
||||
result = Heartbeat.duration_seconds_boundary_aware(@scope, @start_date, @end_date) || 0
|
||||
result
|
||||
else
|
||||
result = @scope.duration_seconds || 0
|
||||
result
|
||||
end
|
||||
|
||||
summary[:total_seconds] = @total_seconds
|
||||
|
||||
@total_days = (@end_time - @start_time) / 86400
|
||||
summary[:daily_average] = @total_days.zero? ? 0 : @total_seconds / @total_days
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def generate_summary_chunk(group_by)
|
||||
result = []
|
||||
@scope.group(group_by).duration_seconds.each do |key, value|
|
||||
result << {
|
||||
name: key.presence || "Other",
|
||||
total_seconds: value,
|
||||
text: ApplicationController.helpers.short_time_simple(value),
|
||||
hours: value / 3600,
|
||||
minutes: (value % 3600) / 60,
|
||||
percent: (100.0 * value / @total_seconds).round(2),
|
||||
digital: ApplicationController.helpers.digital_time(value)
|
||||
}
|
||||
end
|
||||
result = result.sort_by { |item| -item[:total_seconds] }
|
||||
result = result.first(@limit) if @limit.present?
|
||||
result
|
||||
end
|
||||
|
||||
def self.parse_user_agent(user_agent)
|
||||
# Based on https://github.com/muety/wakapi/blob/b3668085c01dc0724d8330f4d51efd5b5aecaeb2/utils/http.go#L89
|
||||
|
||||
# Regex pattern to match wakatime client user agents
|
||||
user_agent_pattern = /wakatime\/[^ ]+ \(([^)]+)\)(?: [^ ]+ ([^\/]+)(?:\/([^\/]+))?)?/
|
||||
|
||||
if matches = user_agent.match(user_agent_pattern)
|
||||
os = matches[1].split("-").first
|
||||
|
||||
editor = matches[2]
|
||||
editor ||= ""
|
||||
|
||||
{ os: os, editor: editor, err: nil }
|
||||
else
|
||||
# Try parsing as browser user agent as fallback
|
||||
if browser_ua = user_agent.match(/^([^\/]+)\/([^\/\s]+)/)
|
||||
# If "wakatime" is present, assume it's the browser extension
|
||||
if user_agent.include?("wakatime") then
|
||||
full_os = user_agent.split(" ")[1]
|
||||
if full_os.present?
|
||||
os = full_os.include?("_") ? full_os.split("_")[0] : full_os
|
||||
{ os: os, editor: browser_ua[1].downcase, err: nil }
|
||||
else
|
||||
{ os: "", editor: "", err: "failed to parse user agent string" }
|
||||
end
|
||||
else
|
||||
{ os: browser_ua[1], editor: browser_ua[2], err: nil }
|
||||
end
|
||||
else
|
||||
{ os: "", editor: "", err: "failed to parse user agent string" }
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error("Error parsing user agent string: #{e.message}")
|
||||
{ os: "", editor: "", err: "failed to parse user agent string" }
|
||||
end
|
||||
|
||||
def categorize_os(os)
|
||||
case os.downcase
|
||||
when "win" then "Windows"
|
||||
when "darwin" then "MacOS"
|
||||
when os.include?("windows") then "Windows"
|
||||
else os.capitalize
|
||||
end
|
||||
end
|
||||
|
||||
def categorize_editor(editor)
|
||||
case editor.downcase
|
||||
when "vscode" then "VSCode"
|
||||
when "KTextEditor" then "Kate"
|
||||
else editor.capitalize
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def convert_to_unix_timestamp(timestamp)
|
||||
# our lord and savior stack overflow for this bit of code
|
||||
return nil if timestamp.nil?
|
||||
|
||||
case timestamp
|
||||
when String
|
||||
Time.parse(timestamp).to_i
|
||||
when Time, DateTime, Date
|
||||
timestamp.to_i
|
||||
when Numeric
|
||||
timestamp.to_i
|
||||
else
|
||||
nil
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
Rails.logger.error("Error converting timestamp: #{e.message}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
49
package-lock.json
generated
49
package-lock.json
generated
|
|
@ -7,16 +7,15 @@
|
|||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@inertiajs/svelte": "^2.3.13",
|
||||
"@js-from-routes/inertia": "^1.10.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"@tsconfig/svelte": "5",
|
||||
"d3-scale": "^4.0.2",
|
||||
"layerchart": "^1.0.13",
|
||||
"plur": "^6.0.0",
|
||||
"svelte": "^5.50.1",
|
||||
"svelte": "5",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tslib": "^2.8.1",
|
||||
|
|
@ -514,29 +513,6 @@
|
|||
"qs": "^6.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@inertiajs/inertia": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@inertiajs/inertia/-/inertia-0.11.1.tgz",
|
||||
"integrity": "sha512-btmV53c54oW4Z9XF0YyTdIUnM7ue0ONy3/KJOz6J1C5CYIwimiKfDMpz8ZbGJuxS+SPdOlNsqj2ZhlHslpJRZg==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"deepmerge": "^4.0.0",
|
||||
"qs": "^6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@inertiajs/inertia/node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@inertiajs/svelte": {
|
||||
"version": "2.3.13",
|
||||
"license": "MIT",
|
||||
|
|
@ -585,23 +561,6 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@js-from-routes/core": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@js-from-routes/core/-/core-1.0.3.tgz",
|
||||
"integrity": "sha512-rUYNMTYFKRoo4w28BJOAderskjhYOtkLgj00x14FNjGu5PYurnimjYQp70H34PTtWRXIqJKprGHr15VOZZzPiw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@js-from-routes/inertia": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@js-from-routes/inertia/-/inertia-1.10.2.tgz",
|
||||
"integrity": "sha512-4Lv016v6ir7Ayp8ZsWzlMZfOEHaajd53ZwiueUzrtzkkZ5+HmUW9AmtEK29jom4qmBLVksVyreQL6BjxdEx6/w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@inertiajs/inertia": "^0.11",
|
||||
"@js-from-routes/core": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@layerstack/svelte-actions": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@layerstack/svelte-actions/-/svelte-actions-1.0.1.tgz",
|
||||
|
|
@ -3462,9 +3421,7 @@
|
|||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.50.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.50.1.tgz",
|
||||
"integrity": "sha512-/Jlom4ddkISyVHXpM2O5dXP9pYnaiFrVQzPbIL1/pEoOa77ZunCb6nDgUCTNCQ/X3t64z9ukrK6R+BbB3kPR3A==",
|
||||
"version": "5.49.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
|
|
|
|||
|
|
@ -7,16 +7,15 @@
|
|||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@inertiajs/svelte": "^2.3.13",
|
||||
"@js-from-routes/inertia": "^1.10.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"@tsconfig/svelte": "5",
|
||||
"d3-scale": "^4.0.2",
|
||||
"layerchart": "^1.0.13",
|
||||
"plur": "^6.0.0",
|
||||
"svelte": "^5.50.1",
|
||||
"svelte": "5",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tslib": "^2.8.1",
|
||||
|
|
@ -24,4 +23,4 @@
|
|||
"vite": "^7.3.1",
|
||||
"vite-plugin-ruby": "^5.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,5 @@
|
|||
},
|
||||
|
||||
"include": ["app/javascript/**/*.ts", "app/javascript/**/*.js", "app/javascript/**/*.svelte"],
|
||||
"exclude": ["app/javascript/controllers"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue