diff --git a/app/controllers/my/heartbeats_controller.rb b/app/controllers/my/heartbeats_controller.rb index 61811f64..d5c2f771 100644 --- a/app/controllers/my/heartbeats_controller.rb +++ b/app/controllers/my/heartbeats_controller.rb @@ -64,7 +64,7 @@ def export end } - filename = "heartbeats_#{current_user.slack_uid}_#{start_date.strftime('%Y%m%d')}_#{end_date.strftime('%Y%m%d')}.json" + filename = "heartbeats_#{current_user.slack_uid}_#{start_date.strftime("%Y%m%d")}_#{end_date.strftime("%Y%m%d")}.json" respond_to do |format| format.json { @@ -76,6 +76,56 @@ def export end end + def import + unless Rails.env.development? + redirect_to my_settings_path, alert: "Hey you! This is noit a dev env, STOP DOING THIS!!!!!) Also, idk why this is happning, you should not be able to see this button hmm...." + return + end + + unless params[:heartbeat_file].present? + redirect_to my_settings_path, alert: "pls select a file to import" + return + end + + file = params[:heartbeat_file] + + unless file.content_type == "application/json" || file.original_filename.ends_with?(".json") + redirect_to my_settings_path, alert: "pls upload only json (download from the button above it)" + return + end + + begin + file_content = file.read.force_encoding("UTF-8") + rescue => e + redirect_to my_settings_path, alert: "error reading file: #{e.message}" + return + end + + result = HeartbeatImportService.import_from_file(file_content, current_user) + + if result[:success] + message = "Imported #{result[:imported_count]} out of #{result[:total_count]} heartbeats" + if result[:skipped_count] > 0 + message += " (#{result[:skipped_count]} skipped cause they were duplicates)" + end + if result[:errors].any? + error_count = result[:errors].length + if error_count <= 3 + message += ". Errors occurred: #{result[:errors].join("; ")}" + else + message += ". #{error_count} errors occurred. First few: #{result[:errors].first(2).join("; ")}..." + end + end + redirect_to root_path, notice: message + else + error_message = "Import failed: #{result[:error]}" + if result[:errors].any? && result[:errors].length > 1 + error_message += "Errors: #{result[:errors][1..2].join("; ")}" + end + redirect_to my_settings_path, alert: error_message + end + end + private def ensure_current_user diff --git a/app/services/heartbeat_import_service.rb b/app/services/heartbeat_import_service.rb new file mode 100644 index 00000000..f3d8e921 --- /dev/null +++ b/app/services/heartbeat_import_service.rb @@ -0,0 +1,105 @@ +class HeartbeatImportService + def self.import_from_file(file_content, user) + unless Rails.env.development? + raise StandardError, "Not dev env, not running" + end + + begin + parsed_data = JSON.parse(file_content) + rescue JSON::ParserError => e + raise StandardError, "Not json: #{e.message}" + end + + unless parsed_data.is_a?(Hash) && parsed_data["heartbeats"].is_a?(Array) + raise StandardError, "Not correct format, download from /my/settings on the offical hackatime then import here" + end + + heartbeats_data = parsed_data["heartbeats"] + imported_count = 0 + skipped_count = 0 + errors = [] + cc = 817263 + heartbeats_data.each_slice(100) do |batch| + records_to_upsert = [] + + batch.each_with_index do |heartbeat_data, index| + begin + time_value = if heartbeat_data["time"].is_a?(String) + Time.parse(heartbeat_data["time"]).to_f + else + heartbeat_data["time"].to_f + end + + attrs = { + user_id: user.id, + time: time_value, + entity: heartbeat_data["entity"], + type: heartbeat_data["type"], + category: heartbeat_data["category"] || "coding", + project: heartbeat_data["project"], + language: heartbeat_data["language"], + editor: heartbeat_data["editor"], + operating_system: heartbeat_data["operating_system"], + machine: heartbeat_data["machine"], + branch: heartbeat_data["branch"], + user_agent: heartbeat_data["user_agent"], + is_write: heartbeat_data["is_write"] || false, + line_additions: heartbeat_data["line_additions"], + line_deletions: heartbeat_data["line_deletions"], + lineno: heartbeat_data["lineno"], + lines: heartbeat_data["lines"], + cursorpos: heartbeat_data["cursorpos"], + dependencies: heartbeat_data["dependencies"] || [], + project_root_count: heartbeat_data["project_root_count"], + source_type: :wakapi_import, + raw_data: heartbeat_data.slice(*Heartbeat.indexed_attributes) + } + + attrs[:fields_hash] = Heartbeat.generate_fields_hash(attrs) + print(attrs[:fields_hash]) + print("\n") + records_to_upsert << attrs + + rescue => e + errors << "Row #{index + 1}: #{e.message}" + next + end + end + + if records_to_upsert.any? + print("importing!!!!!!!!!!!!!!!!!!!!!!") + print("\n") + begin + # Copied from migrate user from hackatime (app\jobs\migrate_user_from_hackatime_job.rb) + records_to_upsert = records_to_upsert.group_by { |r| r[:fields_hash] }.map do |_, records| + records.max_by { |r| r[:time] } + end + result = Heartbeat.upsert_all(records_to_upsert, unique_by: [ :fields_hash ]) + imported_count += result.length + rescue => e + errors << "Import error: #{e.message}" + print(e.message) + print("\n") + end + end + end + + { + success: true, + imported_count: imported_count, + total_count: heartbeats_data.length, + skipped_count: heartbeats_data.length - imported_count, + errors: errors + } + + rescue => e + { + success: false, + error: e.message, + imported_count: 0, + total_count: 0, + skipped_count: 0, + errors: [ e.message ] + } + end +end diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 7e6c5710..23bcc139 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -420,6 +420,38 @@ + <% dev_tool do %> +
+
+
+ + + +
+

Import Heartbeat Data

+
+

Import ur data from real hackatime to test stuff with.

+

PS: your console will be spammed and might crash ur dev env so be carefull if the file is very big

+ <%= form_with url: import_my_heartbeats_path, method: :post, multipart: true, local: true, class: "space-y-4" do |form| %> +
+ + <%= form.file_field :heartbeat_file, + accept: ".json,application/json", + class: "w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-primary file:text-white hover:file:bg-red transition-colors", + required: true %> +
+ +
+ <%= form.submit "Import Heartbeats", + class: "bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded font-medium transition-colors inline-flex items-center gap-2", + data: { confirm: "Are you sure you want to import heartbeats? This will add new data to your account." } %> +
+ <% end %> + + +
+ <% end %> + diff --git a/config/routes.rb b/config/routes.rb index 67aa8eb5..091223ed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -113,6 +113,7 @@ def matches?(request) resources :heartbeats, only: [] do collection do get :export + post :import end end end diff --git a/db/seeds.rb b/db/seeds.rb index eda300e9..3061d068 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -7,7 +7,9 @@ # Creating test user test_user = User.find_or_create_by(slack_uid: 'TEST123456') do |user| user.username = 'testuser' - user.is_admin = true + + # Before you had user.is_admin = true, does not work, changed it to that, looks like it works but idk how to use the admin pages so pls check this, i just guess coded this, the cmd to seed the db works without errors + user.set_admin_level(:superadmin) # Ensure timezone is set to avoid nil timezone issues user.timezone = 'America/New_York' end