mirror of
https://github.com/SrIzan10/hc-harbor.git
synced 2026-05-01 10:45:21 +00:00
Add new setup instructions for time tracking
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
18
app/jobs/wakatime_clear_test_heartbeats_job.rb
Normal file
18
app/jobs/wakatime_clear_test_heartbeats_job.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
<% end %>
|
||||
<% if current_user %>
|
||||
|
||||
<% if @show_wakatime_setup_notice %>
|
||||
<p class="subtitle">
|
||||
<%= link_to "Setup time tracking", my_wakatime_setup_path %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% if @show_logged_time_sentence %>
|
||||
You've logged
|
||||
<%= short_time_detailed current_user.heartbeats.today.duration_seconds %>
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
<h1><%= @is_own_settings ? "My Settings" : "Settings for #{@user.username}" %></h1>
|
||||
|
||||
<div>
|
||||
<h2>Time tracking wizard</h2>
|
||||
<%= link_to "Setup time tracking", my_wakatime_setup_path %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Slack status</h2>
|
||||
<% unless @can_enable_slack_status %>
|
||||
|
||||
112
app/views/users/wakatime_setup.html.erb
Normal file
112
app/views/users/wakatime_setup.html.erb
Normal file
@@ -0,0 +1,112 @@
|
||||
<h1>Wakatime Setup</h1>
|
||||
<p>Step 1 of 4</p>
|
||||
|
||||
<h2>Copy and run the following commands in your terminal</h2>
|
||||
|
||||
<div class="setup-instructions">
|
||||
<h3>Mac/Linux users:</h3>
|
||||
<pre><code>export HACKATIME_API_KEY="<%= @current_user_api_key %>" && curl -sSL <%= root_url %>hackatime/setup.sh | bash</code></pre>
|
||||
|
||||
<h3>Windows users:</h3>
|
||||
<pre><code>$env:HACKATIME_API_KEY="<%= @current_user_api_key %>"; iwr <%= root_url %>hackatime/setup.ps1 -UseBasicParsing | iex</code></pre>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-sm text-gray-600">This will create your WakaTime config file and send a test heartbeat to verify the setup.</p>
|
||||
|
||||
<div id="heartbeat-status" class="mt-4">
|
||||
<p>Waiting for your first heartbeat...</p>
|
||||
<div class="progress-indicator"></div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<%= link_to "Next Step", my_wakatime_setup_step_2_path, id: "next-step", style: "display: none;" %>
|
||||
</p>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const statusDiv = document.getElementById('heartbeat-status');
|
||||
const nextStepLink = document.getElementById('next-step');
|
||||
let checkCount = 0;
|
||||
const maxChecks = 60; // Stop checking after 5 minutes (60 * 5s = 5min)
|
||||
|
||||
function timeAgoInWords(date) {
|
||||
const diffInSeconds = Math.floor((new Date() - new Date(date)) / 1000);
|
||||
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
||||
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||
const diffInDays = Math.floor(diffInHours / 24);
|
||||
const diffInMonths = Math.floor(diffInDays / 30);
|
||||
const diffInYears = Math.floor(diffInDays / 365);
|
||||
|
||||
if (diffInYears > 0) {
|
||||
return `${diffInYears} years ago`;
|
||||
} else if (diffInMonths > 0) {
|
||||
return `${diffInMonths} months ago`;
|
||||
} else if (diffInDays > 0) {
|
||||
return `${diffInDays} days ago`;
|
||||
} else if (diffInHours > 0) {
|
||||
return `${diffInHours} hours ago`;
|
||||
} else if (diffInMinutes > 0) {
|
||||
return `${diffInMinutes} minutes ago`;
|
||||
} else {
|
||||
return `${diffInSeconds} seconds ago`;
|
||||
}
|
||||
}
|
||||
|
||||
function checkForHeartbeats() {
|
||||
fetch('<%= api_v1_my_heartbeats_most_recent_path %>', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer <%= @current_user_api_key %>'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.has_heartbeat) {
|
||||
// show time ago in human readable format
|
||||
const timeAgo = timeAgoInWords(data.heartbeat.created_at);
|
||||
statusDiv.innerHTML = `<p class="success">✅ Heartbeat received ${timeAgo}! You can proceed to the next step.</p>`;
|
||||
nextStepLink.style.display = 'inline-block';
|
||||
return; // Stop checking once we get a heartbeat
|
||||
}
|
||||
throw new Error('No heartbeats yet');
|
||||
})
|
||||
.catch(error => {
|
||||
checkCount++;
|
||||
if (checkCount >= maxChecks) {
|
||||
statusDiv.innerHTML = '<p class="error">❌ No heartbeats detected after 5 minutes. Please make sure you\'ve run the command above and have the WakaTime plugin installed in your editor.</p>';
|
||||
return; // Stop checking after max attempts
|
||||
}
|
||||
// Continue checking every 5 seconds
|
||||
setTimeout(checkForHeartbeats, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// Start checking
|
||||
checkForHeartbeats();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.progress-indicator {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #eee;
|
||||
border-top: 3px solid #666;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #2da44e;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #cf222e;
|
||||
}
|
||||
</style>
|
||||
23
app/views/users/wakatime_setup_step_2.html.erb
Normal file
23
app/views/users/wakatime_setup_step_2.html.erb
Normal file
@@ -0,0 +1,23 @@
|
||||
<h1>Wakatime Setup</h1>
|
||||
<p>Step 2 of 4</p>
|
||||
|
||||
<h2>Pick your coding editors</h2>
|
||||
<em>You can change this later!</em>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<%= link_to "vs-code", my_wakatime_setup_step_3_path(anchor: "vs-code") %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to "kicad", my_wakatime_setup_step_3_path(anchor: "kicad") %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to "vim", my_wakatime_setup_step_3_path(anchor: "vim") %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to "emacs", my_wakatime_setup_step_3_path %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to "other", my_wakatime_setup_step_3_path %>
|
||||
</li>
|
||||
</ul>
|
||||
40
app/views/users/wakatime_setup_step_3.html.erb
Normal file
40
app/views/users/wakatime_setup_step_3.html.erb
Normal file
@@ -0,0 +1,40 @@
|
||||
<h1>Wakatime Setup</h1>
|
||||
<p>Step 3 of 4</p>
|
||||
|
||||
<h2>Install!</h2>
|
||||
|
||||
<section>
|
||||
<h2>Other</h2>
|
||||
<p>
|
||||
<em>Hackatime is WakaTime-compatible, so you can use it with any editor that WakaTime supports. For other editors, please refer to the <a href="https://wakatime.com/docs/editors">WakaTime docs</a>.</em>
|
||||
|
||||
<strong>⚠️ Note: skip any steps that update your ~/.wakatime.cfg file or change the api_url inside it.</strong>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="kicad">
|
||||
<h2>Kicad</h2>
|
||||
<p>
|
||||
<em>Hackatime is WakaTime-compatible, so you can use it with any editor that WakaTime supports. For Kicad,
|
||||
we recommend this <a href="https://github.com/hackclub/kicad-wakatime?tab=readme-ov-file#installation">Kicad plugin built by hack clubbers</a>.</em>
|
||||
</a>.</em>
|
||||
</section>
|
||||
|
||||
<section id="vs-code">
|
||||
<h2>VS Code</h2>
|
||||
<p>
|
||||
<em>Hackatime is WakaTime-compatible, so you can use it with any editor that WakaTime supports. For VS Code, install the <a href="https://marketplace.visualstudio.com/items?itemName=WakaTime.vscode-wakatime">WakaTime extension</a>.</em>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function autoScroll() {
|
||||
const editor = window.location.hash.replace("#", "");
|
||||
if (!editor) { return }
|
||||
const section = document.getElementById(editor);
|
||||
if (!section) { return }
|
||||
section.scrollIntoView({ behavior: "smooth" });
|
||||
section.classList.add('auto-scroll')
|
||||
}
|
||||
autoScroll();
|
||||
</script>
|
||||
@@ -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"
|
||||
|
||||
|
||||
52
public/hackatime/setup.ps1
Normal file
52
public/hackatime/setup.ps1
Normal file
@@ -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
|
||||
}
|
||||
46
public/hackatime/setup.sh
Normal file
46
public/hackatime/setup.sh
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user