From 82a5439a6b88f66cc1fdcf001f3114d404202bc1 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Wed, 12 Mar 2025 19:44:51 -0400 Subject: [PATCH] Add new setup instructions for time tracking --- app/assets/stylesheets/application.css | 12 ++ .../api/hackatime/v1/hackatime_controller.rb | 9 +- .../api/v1/my/heartbeats_controller.rb | 9 +- app/controllers/static_pages_controller.rb | 2 + app/controllers/users_controller.rb | 13 ++ .../wakatime_clear_test_heartbeats_job.rb | 18 +++ app/models/api_key.rb | 13 ++ app/models/heartbeat.rb | 3 +- app/views/static_pages/index.html.erb | 6 + app/views/users/edit.html.erb | 5 + app/views/users/wakatime_setup.html.erb | 112 ++++++++++++++++++ .../users/wakatime_setup_step_2.html.erb | 23 ++++ .../users/wakatime_setup_step_3.html.erb | 40 +++++++ config/routes.rb | 4 + public/hackatime/setup.ps1 | 52 ++++++++ public/hackatime/setup.sh | 46 +++++++ 16 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 app/jobs/wakatime_clear_test_heartbeats_job.rb create mode 100644 app/views/users/wakatime_setup.html.erb create mode 100644 app/views/users/wakatime_setup_step_2.html.erb create mode 100644 app/views/users/wakatime_setup_step_3.html.erb create mode 100644 public/hackatime/setup.ps1 create mode 100644 public/hackatime/setup.sh diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 89f66a9..30b2c4a 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -137,3 +137,15 @@ color: #666; margin: 0 0 0.1rem; } + +.auto-scroll { + /* flash a little bit yellow & leave a little bit of a border */ + animation: flash 1s ease-in-out; + border: 2px solid var(--uchu-yellow); + border-radius: 5px; +} + +@keyframes flash { + 0% { background-color: var(--uchu-yellow); } + 100% { background-color: transparent; } +} diff --git a/app/controllers/api/hackatime/v1/hackatime_controller.rb b/app/controllers/api/hackatime/v1/hackatime_controller.rb index 9fe8880..5b43ffe 100644 --- a/app/controllers/api/hackatime/v1/hackatime_controller.rb +++ b/app/controllers/api/hackatime/v1/hackatime_controller.rb @@ -54,7 +54,14 @@ class Api::Hackatime::V1::HackatimeController < ApplicationController def handle_heartbeat(heartbeat_array) results = [] heartbeat_array.each do |heartbeat| - attrs = heartbeat.merge({ user_id: @user.id, source_type: :direct_entry }) + source_type = :direct_entry + + # special case: if the entity is "test.txt", this is a test heartbeat + if heartbeat[:entity] == "test.txt" + source_type = :test_entry + end + + attrs = heartbeat.merge({ user_id: @user.id, source_type: source_type }) new_heartbeat = Heartbeat.find_or_create_by(attrs) results << [ new_heartbeat.attributes, 201 ] rescue => e diff --git a/app/controllers/api/v1/my/heartbeats_controller.rb b/app/controllers/api/v1/my/heartbeats_controller.rb index 836f56b..481275d 100644 --- a/app/controllers/api/v1/my/heartbeats_controller.rb +++ b/app/controllers/api/v1/my/heartbeats_controller.rb @@ -6,11 +6,10 @@ class Api::V1::My::HeartbeatsController < ApplicationController .order(time: :desc) .first - if heartbeat - render json: heartbeat - else - render json: { error: "No heartbeats found" }, status: :not_found - end + render json: { + has_heartbeat: heartbeat.present?, + heartbeat: heartbeat + } end def index diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index 899bda0..134c341 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -7,6 +7,8 @@ class StaticPagesController < ApplicationController redirect_to FlavorText.random_time_video.sample, allow_other_host: allowed_hosts end + @show_wakatime_setup_notice = current_user.heartbeats.empty? + @project_names = current_user.project_names @projects = current_user.project_labels @current_project = current_user.active_project diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index aaab157..f4a94ce 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -34,6 +34,19 @@ class UsersController < ApplicationController notice: "Heartbeats & api keys migration started" end + def wakatime_setup + api_key = current_user&.api_keys&.last + api_key ||= current_user.api_keys.create!(name: "Wakatime API Key") + @current_user_api_key = api_key&.token + end + + def wakatime_setup_step_2 + end + + def wakatime_setup_step_3 + end + + private def require_admin diff --git a/app/jobs/wakatime_clear_test_heartbeats_job.rb b/app/jobs/wakatime_clear_test_heartbeats_job.rb new file mode 100644 index 0000000..4f908d3 --- /dev/null +++ b/app/jobs/wakatime_clear_test_heartbeats_job.rb @@ -0,0 +1,18 @@ +class WakatimeClearTestHeartbeatsJob < ApplicationJob + queue_as :default + + include GoodJob::ActiveJobExtensions::Concurrency + + # Limits concurrency to 1 job per date + good_job_control_concurrency_with( + key: -> { arguments.first || Date.current.to_s }, + total: 1, + drop: true + ) + + def perform + Heartbeat.where(source_type: "test_entry") + .where("created_at < ?", 7.days.ago) + .delete_all + end +end diff --git a/app/models/api_key.rb b/app/models/api_key.rb index ddbd150..aa11ab6 100644 --- a/app/models/api_key.rb +++ b/app/models/api_key.rb @@ -3,4 +3,17 @@ class ApiKey < ApplicationRecord validates :token, presence: true, uniqueness: true validates :name, presence: true, uniqueness: { scope: :user_id } + + before_validation :generate_token!, on: :create + + private + + def generate_token! + # we need to keep ourselves compatible with WakaTime: https://github.com/wakatime/vscode-wakatime/blob/241b60c8491c14e3c093b1ef2a0276c38586a172/src/utils.ts#L24 + # they use a UUID v4 + self.token ||= SecureRandom.uuid_v4 + + # Mark it as something not imported from WakaTime + self.name ||= "Hackatime key" + end end diff --git a/app/models/heartbeat.rb b/app/models/heartbeat.rb index c0c2df2..0e82617 100644 --- a/app/models/heartbeat.rb +++ b/app/models/heartbeat.rb @@ -7,7 +7,8 @@ class Heartbeat < ApplicationRecord enum :source_type, { direct_entry: 0, - wakapi_import: 1 + wakapi_import: 1, + test_entry: 2 } # This is to prevent Rails from trying to use STI even though we have a "type" column diff --git a/app/views/static_pages/index.html.erb b/app/views/static_pages/index.html.erb index 514eb18..164eae5 100644 --- a/app/views/static_pages/index.html.erb +++ b/app/views/static_pages/index.html.erb @@ -11,6 +11,12 @@ <% end %> <% if current_user %> + <% if @show_wakatime_setup_notice %> +

+ <%= link_to "Setup time tracking", my_wakatime_setup_path %> +

+ <% end %> + <% if @show_logged_time_sentence %> You've logged <%= short_time_detailed current_user.heartbeats.today.duration_seconds %> diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 4acdc89..236c68f 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -4,6 +4,11 @@

<%= @is_own_settings ? "My Settings" : "Settings for #{@user.username}" %>

+
+

Time tracking wizard

+ <%= link_to "Setup time tracking", my_wakatime_setup_path %> +
+

Slack status

<% unless @can_enable_slack_status %> diff --git a/app/views/users/wakatime_setup.html.erb b/app/views/users/wakatime_setup.html.erb new file mode 100644 index 0000000..ff9860d --- /dev/null +++ b/app/views/users/wakatime_setup.html.erb @@ -0,0 +1,112 @@ +

Wakatime Setup

+

Step 1 of 4

+ +

Copy and run the following commands in your terminal

+ +
+

Mac/Linux users:

+
export HACKATIME_API_KEY="<%= @current_user_api_key %>" && curl -sSL <%= root_url %>hackatime/setup.sh | bash
+ +

Windows users:

+
$env:HACKATIME_API_KEY="<%= @current_user_api_key %>"; iwr <%= root_url %>hackatime/setup.ps1 -UseBasicParsing | iex
+
+ +

This will create your WakaTime config file and send a test heartbeat to verify the setup.

+ +
+

Waiting for your first heartbeat...

+
+
+ +

+ <%= link_to "Next Step", my_wakatime_setup_step_2_path, id: "next-step", style: "display: none;" %> +

+ + + + diff --git a/app/views/users/wakatime_setup_step_2.html.erb b/app/views/users/wakatime_setup_step_2.html.erb new file mode 100644 index 0000000..85a079f --- /dev/null +++ b/app/views/users/wakatime_setup_step_2.html.erb @@ -0,0 +1,23 @@ +

Wakatime Setup

+

Step 2 of 4

+ +

Pick your coding editors

+You can change this later! + + \ No newline at end of file diff --git a/app/views/users/wakatime_setup_step_3.html.erb b/app/views/users/wakatime_setup_step_3.html.erb new file mode 100644 index 0000000..50be552 --- /dev/null +++ b/app/views/users/wakatime_setup_step_3.html.erb @@ -0,0 +1,40 @@ +

Wakatime Setup

+

Step 3 of 4

+ +

Install!

+ +
+

Other

+

+ Hackatime is WakaTime-compatible, so you can use it with any editor that WakaTime supports. For other editors, please refer to the WakaTime docs. + + ⚠️ Note: skip any steps that update your ~/.wakatime.cfg file or change the api_url inside it. +

+
+ +
+

Kicad

+

+ Hackatime is WakaTime-compatible, so you can use it with any editor that WakaTime supports. For Kicad, + we recommend this Kicad plugin built by hack clubbers. + . +

+ +
+

VS Code

+

+ Hackatime is WakaTime-compatible, so you can use it with any editor that WakaTime supports. For VS Code, install the WakaTime extension. +

+
+ + diff --git a/config/routes.rb b/config/routes.rb index 9a29a01..cdd9908 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,6 +56,10 @@ Rails.application.routes.draw do patch "my/settings", to: "users#update" post "my/settings/migrate_heartbeats", to: "users#migrate_heartbeats", as: :my_settings_migrate_heartbeats + get "my/wakatime_setup", to: "users#wakatime_setup", as: :my_wakatime_setup + get "my/wakatime_setup/step-2", to: "users#wakatime_setup_step_2", as: :my_wakatime_setup_step_2 + get "my/wakatime_setup/step-3", to: "users#wakatime_setup_step_3", as: :my_wakatime_setup_step_3 + post "/sailors_log/slack/commands", to: "slack#create" post "/timedump/slack/commands", to: "slack#create" diff --git a/public/hackatime/setup.ps1 b/public/hackatime/setup.ps1 new file mode 100644 index 0000000..132fecf --- /dev/null +++ b/public/hackatime/setup.ps1 @@ -0,0 +1,52 @@ +# Create config file +New-Item -Path $env:USERPROFILE\.wakatime.cfg -Force +Set-Content -Path $env:USERPROFILE\.wakatime.cfg -Value @" +[settings] +api_url = https://hackatime.hackclub.com/api/hackatime/v1 +api_key = $env:HACKATIME_API_KEY +"@ + +Write-Host "Config file created at $env:USERPROFILE\.wakatime.cfg" + +# Read values from config to verify +if (-not (Test-Path $env:USERPROFILE\.wakatime.cfg)) { + Write-Error "Config file not found" + exit 1 +} + +$config = Get-Content $env:USERPROFILE\.wakatime.cfg +$apiUrl = $config | Select-String "api_url" | ForEach-Object { $_.ToString().Split('"')[1] } +$apiKey = $config | Select-String "api_key" | ForEach-Object { $_.ToString().Split('"')[1] } + +if (-not $apiUrl -or -not $apiKey) { + Write-Error "Could not read api_url or api_key from config" + exit 1 +} + +Write-Host "Successfully read config:" +Write-Host "API URL: $apiUrl" +Write-Host "API Key: $($apiKey.Substring(0,8)..." # Show only first 8 chars for security + +# Send test heartbeat using values from config +Write-Host "Sending test heartbeat..." +$time = [Math]::Floor([decimal](Get-Date(Get-Date).ToUniversalTime()-uformat '%s')) +$heartbeat = @{ + type = 'file' + time = $time + entity = 'test.txt' + language = 'Text' +} +$body = "[$($heartbeat | ConvertTo-Json)]" + +try { + $response = Invoke-WebRequest -Uri "$apiUrl/users/current/heartbeats" ` + -Method Post ` + -Headers @{Authorization="Bearer $apiKey"} ` + -ContentType 'application/json' ` + -Body $body + + Write-Host "Test heartbeat sent successfully" +} catch { + Write-Error "Error sending heartbeat: $($_.Exception.Response.StatusCode.Value__) $($_.Exception.Message)" + exit 1 +} \ No newline at end of file diff --git a/public/hackatime/setup.sh b/public/hackatime/setup.sh new file mode 100644 index 0000000..ea04108 --- /dev/null +++ b/public/hackatime/setup.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +# Create or update config file +cat > ~/.wakatime.cfg << EOL +[settings] +api_url = https://hackatime.hackclub.com/api/hackatime/v1 +api_key = ${HACKATIME_API_KEY} +EOL + +echo "Config file created at ~/.wakatime.cfg" + +# Read values from config to verify +if [ ! -f ~/.wakatime.cfg ]; then + echo "Error: Config file not found" + exit 1 +fi + +API_URL=$(sed -n 's/.*api_url = \(.*\)/\1/p' ~/.wakatime.cfg) +API_KEY=$(sed -n 's/.*api_key = \(.*\)/\1/p' ~/.wakatime.cfg) + +if [ -z "$API_URL" ] || [ -z "$API_KEY" ]; then + echo "Error: Could not read api_url or api_key from config" + exit 1 +fi + +echo "Successfully read config:" +echo "API URL: $API_URL" +echo "API Key: ${API_KEY:0:8}..." # Show only first 8 chars for security + +# Send test heartbeat using values from config +echo "Sending test heartbeat..." +response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL/users/current/heartbeats" \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "[{\"type\":\"file\",\"time\":$(date +%s),\"entity\":\"test.txt\",\"language\":\"Text\"}]") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [ "$http_code" = "200" ] || [ "$http_code" = "202" ]; then + echo -e "\nTest heartbeat sent successfully" +else + echo -e "\nError sending heartbeat: $body" + exit 1 +fi \ No newline at end of file