├── .dockerignore ├── .env.example ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .kamal ├── hooks │ ├── docker-setup.sample │ ├── post-app-boot.sample │ ├── post-deploy.sample │ ├── post-proxy-reboot.sample │ ├── pre-app-boot.sample │ ├── pre-build.sample │ ├── pre-connect.sample │ ├── pre-deploy.sample │ └── pre-proxy-reboot.sample └── secrets ├── .rubocop.yml ├── .ruby-version ├── AGENT.md ├── Dockerfile ├── Dockerfile.dev ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── TODO.md ├── app ├── assets │ ├── images │ │ ├── .keep │ │ └── favicon.png │ └── stylesheets │ │ ├── active_users_graph.css │ │ ├── activity_graph.css │ │ ├── admin_timeline.css │ │ ├── application.css │ │ ├── currently_hacking.css │ │ ├── filterable_dashboard.css │ │ ├── flash.css │ │ ├── leaderboard.css │ │ ├── nav.css │ │ ├── project_duration.css │ │ ├── scrapyard.css │ │ ├── scrapyard_leaderboard.css │ │ ├── tools.css │ │ └── user_mention.css ├── controllers │ ├── admin │ │ ├── base_controller.rb │ │ ├── post_reviews_controller.rb │ │ ├── timeline_controller.rb │ │ └── ysws_reviews_controller.rb │ ├── api │ │ ├── hackatime │ │ │ └── v1 │ │ │ │ └── hackatime_controller.rb │ │ ├── summary_controller.rb │ │ └── v1 │ │ │ ├── external_slack_controller.rb │ │ │ ├── my │ │ │ └── heartbeats_controller.rb │ │ │ ├── stats_controller.rb │ │ │ ├── users_controller.rb │ │ │ └── ysws_programs_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ ├── docs_controller.rb │ ├── leaderboards_controller.rb │ ├── my │ │ ├── mailing_addresses_controller.rb │ │ ├── mailroom_controller.rb │ │ └── project_repo_mappings_controller.rb │ ├── scrapyard_leaderboards_controller.rb │ ├── sessions_controller.rb │ ├── sitemap_controller.rb │ ├── slack_controller.rb │ ├── static_pages_controller.rb │ ├── users_controller.rb │ └── wakatime_mirrors_controller.rb ├── helpers │ └── application_helper.rb ├── javascript │ ├── application.js │ └── controllers │ │ ├── admin_timeline_user_selector_controller.js │ │ ├── application.js │ │ ├── currently_hacking_controller.js │ │ ├── hello_controller.js │ │ ├── index.js │ │ ├── nav_controller.js │ │ └── trust_level_controller.js ├── jobs │ ├── application_job.rb │ ├── attempt_project_repo_mapping_job.rb │ ├── attempt_to_deliver_physical_mail_job.rb │ ├── cache │ │ ├── active_projects_job.rb │ │ ├── active_users_graph_data_job.rb │ │ ├── activity_job.rb │ │ ├── currently_hacking_job.rb │ │ ├── heartbeat_counts_job.rb │ │ ├── home_stats_job.rb │ │ ├── minutes_logged_job.rb │ │ ├── setup_social_proof_job.rb │ │ └── usage_social_proof_job.rb │ ├── check_streak_physical_mail_job.rb │ ├── cleanup_expired_email_verification_requests_job.rb │ ├── cleanup_successful_jobs_job.rb │ ├── concerns │ │ └── has_enqueue_control.rb │ ├── fetch_mailing_address_job.rb │ ├── geocode_users_without_country_job.rb │ ├── handle_email_signin_job.rb │ ├── leaderboard_update_job.rb │ ├── migrate_user_from_hackatime_job.rb │ ├── neighborhood │ │ ├── sync_from_airtable_job.rb │ │ └── trigger_time_update_job.rb │ ├── one_time │ │ ├── backfill_email_sources_job.rb │ │ ├── backfill_heartbeat_editor_job.rb │ │ ├── generate_unique_heartbeat_hashes_job.rb │ │ ├── import_from_sailors_log_job.rb │ │ ├── log_to_sailors_log_channel_job.rb │ │ ├── recalc_heartbeat_field_hash_job.rb │ │ ├── reset_sailors_log_job.rb │ │ ├── set_neighborhood_channels_job.rb │ │ ├── set_user_timezone_from_slack_job.rb │ │ └── transfer_user_data_job.rb │ ├── process_commit_job.rb │ ├── pull_repo_commits_job.rb │ ├── repo_host │ │ └── sync_user_events_job.rb │ ├── sailors_log_notify_job.rb │ ├── sailors_log_poll_for_changes_job.rb │ ├── sailors_log_teletype_job.rb │ ├── scan_github_repos_job.rb │ ├── scan_repo_events_for_commits_job.rb │ ├── set_user_country_code_job.rb │ ├── slack_command │ │ ├── sailors_log_help_job.rb │ │ ├── sailors_log_job.rb │ │ ├── sailors_log_leaderboard_job.rb │ │ ├── sailors_log_on_off_job.rb │ │ └── update_slack_channel_cache_job.rb │ ├── slack_username_update_job.rb │ ├── sync_all_user_repo_events_job.rb │ ├── sync_repo_metadata_job.rb │ ├── sync_stale_repo_metadata_job.rb │ ├── update_airtable_user_data_job.rb │ ├── update_slack_neighborhood_channels_job.rb │ ├── user_slack_status_update_job.rb │ ├── wakatime_clear_test_heartbeats_job.rb │ └── wakatime_mirror_sync_job.rb ├── lib │ └── hack_club_geocoder_lookup.rb ├── mailers │ ├── application_mailer.rb │ ├── auth_mailer.rb │ ├── email_verification_mailer.rb │ └── loops_mailer.rb ├── models │ ├── ahoy │ │ ├── event.rb │ │ └── visit.rb │ ├── api_key.rb │ ├── application_direct_record.rb │ ├── application_record.rb │ ├── commit.rb │ ├── concerns │ │ ├── .keep │ │ ├── has_table_sync.rb │ │ ├── heartbeatable.rb │ │ └── time_range_filterable.rb │ ├── email_address.rb │ ├── email_verification_request.rb │ ├── hackatime │ │ ├── heartbeat.rb │ │ ├── project_label.rb │ │ └── user.rb │ ├── hackatime_record.rb │ ├── heartbeat.rb │ ├── leaderboard.rb │ ├── leaderboard_entry.rb │ ├── mailing_address.rb │ ├── neighborhood │ │ ├── app.rb │ │ ├── post.rb │ │ ├── project.rb │ │ └── ysws_submission.rb │ ├── physical_mail.rb │ ├── project_repo_mapping.rb │ ├── raw_heartbeat_upload.rb │ ├── repo_host_event.rb │ ├── repository.rb │ ├── sailors_log.rb │ ├── sailors_log_leaderboard.rb │ ├── sailors_log_notification_preference.rb │ ├── sailors_log_slack_notification.rb │ ├── sign_in_token.rb │ ├── user.rb │ ├── wakatime_mirror.rb │ ├── warehouse │ │ ├── scrapyard_event.rb │ │ └── scrapyard_local_attendee.rb │ └── warehouse_record.rb ├── services │ ├── hack_club_geocoder_service.rb │ └── repo_host │ │ ├── base_service.rb │ │ ├── github_service.rb │ │ └── service_factory.rb └── views │ ├── admin │ ├── post_reviews │ │ ├── _time_approval_form.html.erb │ │ └── show.html.erb │ ├── timeline │ │ └── show.html.erb │ └── ysws_reviews │ │ └── show.html.erb │ ├── auth_mailer │ └── sign_in_email.html.erb │ ├── docs │ ├── index.html.erb │ └── show.html.erb │ ├── email_verification_mailer │ ├── verify_email.html.erb │ └── verify_email.text.erb │ ├── good_job │ └── shared │ │ └── _secondary_navbar.erb │ ├── layouts │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb │ ├── leaderboards │ ├── _mini_leaderboard.html.erb │ ├── _mini_leaderboard_loading.html.erb │ └── index.html.erb │ ├── loops_mailer │ └── sign_in_email.text.erb │ ├── my │ ├── mailing_addresses │ │ └── show.html.erb │ ├── mailroom │ │ └── index.html.erb │ └── project_repo_mappings │ │ ├── edit.html.erb │ │ └── index.html.erb │ ├── pwa │ ├── manifest.json.erb │ └── service-worker.js │ ├── scrapyard_leaderboards │ ├── index.html.erb │ └── show.html.erb │ ├── sessions │ └── close_window.html.erb │ ├── shared │ ├── _interval_selector.html.erb │ ├── _multi_select.html.erb │ ├── _nav.html.erb │ ├── _slack_channel_mention.erb │ ├── _user_mention.html.erb │ └── _video_on_hover.html.erb │ ├── sitemap │ └── sitemap.xml.erb │ ├── static_pages │ ├── _active_users_graph.html.erb │ ├── _activity_graph.html.erb │ ├── _currently_hacking.erb │ ├── _filterable_dashboard.html.erb │ ├── _filterable_dashboard_content.html.erb │ ├── _project_durations.html.erb │ ├── _scrapyard.html.erb │ ├── _streak.erb │ ├── index.html.erb │ └── my_projects.html.erb │ └── users │ ├── _wakatime_config_display.html.erb │ ├── _wakatime_mirror_section.html.erb │ ├── edit.html.erb │ ├── show.html.erb │ ├── wakatime_setup.html.erb │ ├── wakatime_setup_step_2.html.erb │ ├── wakatime_setup_step_3.html.erb │ └── wakatime_setup_step_4.html.erb ├── bin ├── brakeman ├── bundle ├── dev ├── docker-entrypoint ├── importmap ├── jobs ├── kamal ├── rails ├── rake ├── rubocop ├── setup └── thrust ├── config.ru ├── config ├── application.rb ├── boot.rb ├── brakeman.ignore ├── cable.yml ├── cache.yml ├── credentials.yml.enc ├── database.yml ├── deploy.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── honeybadger.yml ├── importmap.rb ├── initializers │ ├── active_record_encryption.rb │ ├── ahoy.rb │ ├── ahoy_captain.rb │ ├── assets.rb │ ├── content_security_policy.rb │ ├── cors.rb │ ├── filter_parameter_logging.rb │ ├── geocoder.rb │ ├── git_version.rb │ ├── good_job.rb │ ├── inflections.rb │ ├── monkey_patches.rb │ ├── papertrail.rb │ └── sentry.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── skylight.yml └── storage.yml ├── db ├── cable_schema.rb ├── cache_schema.rb ├── migrate │ ├── 20240101000000_create_users.rb │ ├── 20240316000001_create_daily_leaderboard_entries.rb │ ├── 20240316000002_rename_daily_leaderboard_tables.rb │ ├── 20240316000003_remove_status_from_leaderboards.rb │ ├── 20240320000000_add_profile_fields_to_users.rb │ ├── 20240320000001_add_admin_at_to_users.rb │ ├── 20240320000002_switch_admin_at_to_is_admin.rb │ ├── 20250216173458_create_versions.rb │ ├── 20250216173459_add_object_changes_to_versions.rb │ ├── 20250221204236_add_uses_slack_status.rb │ ├── 20250221205702_add_slack_oauth_scopes_to_users.rb │ ├── 20250221224605_create_good_jobs.rb │ ├── 20250222002320_add_finished_generating_at_to_leaderboards.rb │ ├── 20250222004511_remove_start_date_unique_index_from_leaderboards.rb │ ├── 20250222005401_add_deleted_at_to_leaderboards.rb │ ├── 20250222031955_create_sailors_logs.rb │ ├── 20250222032551_create_sailors_log_notification_preferences.rb │ ├── 20250222032930_create_sailors_log_slack_notifications.rb │ ├── 20250222082500_add_unique_index_to_sailors_log_notification_preferences.rb │ ├── 20250223072034_create_sailors_log_leaderboards.rb │ ├── 20250223085114_add_deleted_at_to_sailors_log_leaderboard_job.rb │ ├── 20250303175821_create_api_keys.rb │ ├── 20250303180842_create_heartbeats.rb │ ├── 20250304032720_add_extension_text_type_to_users.rb │ ├── 20250305061242_uniqueness_index_to_hash_on_heartbeats.rb │ ├── 20250305194250_add_source_to_heartbeats.rb │ ├── 20250305195904_enforce_uniqueness_index_on_heartbeats.rb │ ├── 20250306033109_change_heartbeats_time_to_float8.rb │ ├── 20250307201004_rename_user_id_on_leaderboard_entries.rb │ ├── 20250307223936_create_email_addresses.rb │ ├── 20250307224352_move_emails_to_email_addresses.rb │ ├── 20250307225347_create_sign_in_tokens.rb │ ├── 20250310165010_rename_avatar_url_to_slack_avatar_url_on_users.rb │ ├── 20250312110220_add_period_type_to_leaderboards.rb │ ├── 20250312160326_create_project_repo_mappings.rb │ ├── 20250312172534_allow_null_slack_uid.rb │ ├── 20250313205725_switch_leaderboard_entries_to_user_id.rb │ ├── 20250315030446_add_ip_address_to_heartbeats.rb │ ├── 20250315214819_add_index_category_time_heartbeats.rb │ ├── 20250319142656_add_timezone_to_users.rb │ ├── 20250319165910_add_git_hub_fields_to_users.rb │ ├── 20250319193636_add_slack_username_to_users.rb │ ├── 20250324203539_add_streak_count_to_leaderboard_entries.rb │ ├── 20250425211619_add_ysws_program_to_heartbeats.rb │ ├── 20250503215404_add_source_to_email_addresses.rb │ ├── 20250505045020_create_ahoy_visits_and_events.rb │ ├── 20250505151057_create_email_verification_requests.rb │ ├── 20250505152654_add_deleted_at_to_email_verification_requests.rb │ ├── 20250506155521_add_slack_neighborhood_channel_to_users.rb │ ├── 20250507174855_add_omit_from_leaderboard_to_users.rb │ ├── 20250507182617_add_deleted_at_to_heartbeats.rb │ ├── 20250507183451_add_partial_unique_index_to_heartbeat_fields_hash.rb │ ├── 20250507184341_remove_fields_hash_index_from_heartbeats.rb │ ├── 20250507204732_add_trust_level_to_users.rb │ ├── 20250507211848_add_index_to_heartbeats.rb │ ├── 20250509160228_add_raw_data_to_heartbeats.rb │ ├── 20250509170101_add_country_code_to_users.rb │ ├── 20250509183721_create_mailing_addresses.rb │ ├── 20250509191155_add_mailing_address_otc_to_users.rb │ ├── 20250512205858_create_wakatime_mirrors.rb │ ├── 20250513183739_create_raw_heartbeat_uploads.rb │ ├── 20250513184040_add_raw_heartbeat_upload_to_heartbeats.rb │ ├── 20250514150404_create_physical_mails.rb │ ├── 20250514180503_create_repo_host_events.rb │ ├── 20250514212714_create_commits.rb │ ├── 20250516140014_create_neighborhood_posts.rb │ ├── 20250516142026_create_neighborhood_apps.rb │ ├── 20250516142043_create_neighborhood_projects.rb │ ├── 20250522062125_create_neighborhood_ysws_submissions.rb │ ├── 20250527052632_add_indexes_for_active_projects_query.rb │ ├── 20250530015016_create_repositories.rb │ ├── 20250530015024_update_project_repo_mappings_to_use_repository.rb │ ├── 20250530015530_add_repository_to_commits.rb │ └── 20250530135145_add_homepage_to_repositories.rb ├── schema.rb └── seeds.rb ├── docker-compose.pgbouncer.yml ├── docker-compose.yml ├── docs ├── api │ ├── authentication.md │ ├── endpoints.md │ └── overview.md ├── editors │ ├── android-studio.md │ ├── appcode.md │ ├── aptana.md │ ├── arduino-ide.md │ ├── azure-data-studio.md │ ├── blender.md │ ├── brackets.md │ ├── brave.md │ ├── c++-builder.md │ ├── canva.md │ ├── chrome.md │ ├── clion.md │ ├── cloud9.md │ ├── coda.md │ ├── codetasty.md │ ├── cursor.md │ ├── datagrip.md │ ├── dataspell.md │ ├── dbeaver.md │ ├── delphi.md │ ├── discord.md │ ├── eclipse.md │ ├── edge.md │ ├── emacs.md │ ├── eric.md │ ├── excel.md │ ├── figma.md │ ├── firefox.md │ ├── gedit.md │ ├── godot.md │ ├── goland.md │ ├── hbuilder-x.md │ ├── ida-pro.md │ ├── intellij-idea.md │ ├── jupyter.md │ ├── kakoune.md │ ├── kate.md │ ├── komodo.md │ ├── micro.md │ ├── mps.md │ ├── neovim.md │ ├── netbeans.md │ ├── notepad++.md │ ├── nova.md │ ├── obsidian.md │ ├── oxygen.md │ ├── phpstorm.md │ ├── postman.md │ ├── powerpoint.md │ ├── processing.md │ ├── pulsar.md │ ├── pycharm.md │ ├── reclassex.md │ ├── rider.md │ ├── roblox-studio.md │ ├── rubymine.md │ ├── rustrover.md │ ├── safari.md │ ├── siyuan.md │ ├── sketch.md │ ├── slickedit.md │ ├── sql-server-management-studio.md │ ├── sublime-text.md │ ├── terminal.md │ ├── texstudio.md │ ├── textmate.md │ ├── trae.md │ ├── unity.md │ ├── vim.md │ ├── visual-studio.md │ ├── vs-code.md │ ├── webstorm.md │ ├── windsurf.md │ ├── wing.md │ ├── word.md │ ├── xcode.md │ └── zed.md └── getting-started │ ├── configuration.md │ ├── installation.md │ └── quick-start.md ├── entrypoint.dev.sh ├── lib ├── flavor_text.rb ├── git_remote.rb ├── github_readme_stats.rb ├── slack_channel.rb ├── slack_neighborhood.rb ├── slack_username.rb ├── tasks │ └── .keep └── wakatime_service.rb ├── log └── .keep ├── public ├── 400.html ├── 404.html ├── 406-unsupported-browser.html ├── 422.html ├── 500.html ├── hackatime │ ├── setup.ps1 │ └── setup.sh ├── icon.png ├── icon.svg ├── images │ └── editor-icons │ │ ├── android-studio-128.png │ │ ├── appcode-128.png │ │ ├── aptana-128.png │ │ ├── arduino-ide-128.png │ │ ├── azure-data-studio-128.png │ │ ├── blender-128.png │ │ ├── brackets-128.png │ │ ├── brave-128.png │ │ ├── c++-builder-128.png │ │ ├── canva-128.png │ │ ├── chrome-128.png │ │ ├── clion-128.png │ │ ├── cloud9-128.png │ │ ├── coda-128.png │ │ ├── codetasty-128.png │ │ ├── cursor-128.png │ │ ├── datagrip-128.png │ │ ├── dataspell-128.png │ │ ├── dbeaver-128.png │ │ ├── delphi-128.png │ │ ├── discord-128.png │ │ ├── eclipse-128.png │ │ ├── edge-128.png │ │ ├── emacs-128.png │ │ ├── eric-128.png │ │ ├── excel-128.png │ │ ├── figma-128.png │ │ ├── firefox-128.png │ │ ├── gedit-128.png │ │ ├── godot-128.png │ │ ├── goland-128.png │ │ ├── hbuilder-x-128.png │ │ ├── ida-pro-128.png │ │ ├── intellij-idea-128.png │ │ ├── jupyter-128.png │ │ ├── kakoune-128.png │ │ ├── kate-128.png │ │ ├── komodo-128.png │ │ ├── micro-128.png │ │ ├── mps-128.png │ │ ├── neovim-128.png │ │ ├── netbeans-128.png │ │ ├── notepad++-128.png │ │ ├── nova-128.png │ │ ├── obsidian-128.png │ │ ├── oxygen-128.png │ │ ├── phpstorm-128.png │ │ ├── postman-128.png │ │ ├── powerpoint-128.png │ │ ├── processing-128.png │ │ ├── pulsar-128.png │ │ ├── pycharm-128.png │ │ ├── reclassex-128.png │ │ ├── rider-128.png │ │ ├── roblox-studio-128.png │ │ ├── rubymine-128.png │ │ ├── rustrover-128.png │ │ ├── safari-128.png │ │ ├── siyuan-128.png │ │ ├── sketch-128.png │ │ ├── slickedit-128.png │ │ ├── sql-server-management-studio-128.png │ │ ├── sublime-text-128.png │ │ ├── terminal-128.png │ │ ├── texstudio-128.png │ │ ├── textmate-128.png │ │ ├── trae-128.png │ │ ├── unity-128.png │ │ ├── vim-128.png │ │ ├── visual-studio-128.png │ │ ├── vs-code-128.png │ │ ├── webstorm-128.png │ │ ├── windsurf-128.png │ │ ├── wing-128.png │ │ ├── word-128.png │ │ ├── xcode-128.png │ │ └── zed-128.png ├── robots.txt └── success.txt ├── script └── .keep ├── slack_manifest_harbor.yml ├── slack_manifest_sailors_log.yml ├── storage └── .keep ├── test ├── application_system_test_case.rb ├── controllers │ └── .keep ├── fixtures │ ├── api_keys.yml │ ├── files │ │ └── .keep │ ├── heartbeats.yml │ ├── mailing_addresses.yml │ ├── project_repo_mappings.yml │ ├── repositories.yml │ ├── sailors_log_leaderboards.yml │ ├── sailors_log_notification_preferences.yml │ ├── sailors_log_slack_notifications.yml │ └── sailors_logs.yml ├── helpers │ └── .keep ├── integration │ └── .keep ├── lib │ └── wakatime_service_test.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── api_key_test.rb │ ├── heartbeat_test.rb │ ├── mailing_address_test.rb │ ├── project_repo_mapping_test.rb │ ├── repository_test.rb │ ├── sailors_log_leaderboard_test.rb │ ├── sailors_log_notification_preference_test.rb │ ├── sailors_log_slack_notification_test.rb │ └── sailors_log_test.rb ├── system │ └── .keep └── test_helper.rb ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep └── vendor ├── .keep └── javascript ├── .keep └── @rails--request.js.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. 2 | 3 | # Ignore git directory. 4 | /.git/ 5 | /.gitignore 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all default key files. 14 | /config/master.key 15 | /config/credentials/*.key 16 | 17 | # Ignore all logfiles and tempfiles. 18 | /log/* 19 | /tmp/* 20 | !/log/.keep 21 | !/tmp/.keep 22 | 23 | # Ignore pidfiles, but keep the directory. 24 | /tmp/pids/* 25 | !/tmp/pids/.keep 26 | 27 | # Ignore storage (uploaded files in development and any SQLite databases). 28 | /storage/* 29 | !/storage/.keep 30 | /tmp/storage/* 31 | !/tmp/storage/.keep 32 | 33 | # Ignore assets. 34 | /node_modules/ 35 | /app/assets/builds/* 36 | !/app/assets/builds/.keep 37 | /public/assets 38 | 39 | # Ignore CI service files. 40 | /.github 41 | 42 | # Ignore Kamal files. 43 | /config/deploy*.yml 44 | /.kamal 45 | 46 | # Ignore development files 47 | /.devcontainer 48 | 49 | # Ignore Docker-related files 50 | /.dockerignore 51 | /Dockerfile* 52 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | config/credentials/*.yml.enc diff=rails_credentials 9 | config/credentials.yml.enc diff=rails_credentials 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # Temporary files generated by your text editor or operating system 4 | # belong in git's global ignore instead: 5 | # `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log/* 15 | /tmp/* 16 | !/log/.keep 17 | !/tmp/.keep 18 | 19 | # Ignore pidfiles, but keep the directory. 20 | /tmp/pids/* 21 | !/tmp/pids/ 22 | !/tmp/pids/.keep 23 | 24 | # Ignore storage (uploaded files in development and any SQLite databases). 25 | /storage/* 26 | !/storage/.keep 27 | /tmp/storage/* 28 | !/tmp/storage/ 29 | !/tmp/storage/.keep 30 | 31 | /public/assets 32 | 33 | # Ignore master key for decrypting credentials and more. 34 | /config/master.key 35 | 36 | # Ignore environment variables 37 | .env 38 | .env.*.local 39 | !.env.example 40 | -------------------------------------------------------------------------------- /.kamal/hooks/docker-setup.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Docker set up on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/hooks/post-app-boot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/hooks/post-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample post-deploy hook 4 | # 5 | # These environment variables are available: 6 | # KAMAL_RECORDED_AT 7 | # KAMAL_PERFORMER 8 | # KAMAL_VERSION 9 | # KAMAL_HOSTS 10 | # KAMAL_ROLE (if set) 11 | # KAMAL_DESTINATION (if set) 12 | # KAMAL_RUNTIME 13 | 14 | echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" 15 | -------------------------------------------------------------------------------- /.kamal/hooks/post-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooted kamal-proxy on $KAMAL_HOSTS" 4 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-app-boot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-build.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample pre-build hook 4 | # 5 | # Checks: 6 | # 1. We have a clean checkout 7 | # 2. A remote is configured 8 | # 3. The branch has been pushed to the remote 9 | # 4. The version we are deploying matches the remote 10 | # 11 | # These environment variables are available: 12 | # KAMAL_RECORDED_AT 13 | # KAMAL_PERFORMER 14 | # KAMAL_VERSION 15 | # KAMAL_HOSTS 16 | # KAMAL_ROLE (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | if [ -n "$(git status --porcelain)" ]; then 20 | echo "Git checkout is not clean, aborting..." >&2 21 | git status --porcelain >&2 22 | exit 1 23 | fi 24 | 25 | first_remote=$(git remote) 26 | 27 | if [ -z "$first_remote" ]; then 28 | echo "No git remote set, aborting..." >&2 29 | exit 1 30 | fi 31 | 32 | current_branch=$(git branch --show-current) 33 | 34 | if [ -z "$current_branch" ]; then 35 | echo "Not on a git branch, aborting..." >&2 36 | exit 1 37 | fi 38 | 39 | remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) 40 | 41 | if [ -z "$remote_head" ]; then 42 | echo "Branch not pushed to remote, aborting..." >&2 43 | exit 1 44 | fi 45 | 46 | if [ "$KAMAL_VERSION" != "$remote_head" ]; then 47 | echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 48 | exit 1 49 | fi 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-connect.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-connect check 4 | # 5 | # Warms DNS before connecting to hosts in parallel 6 | # 7 | # These environment variables are available: 8 | # KAMAL_RECORDED_AT 9 | # KAMAL_PERFORMER 10 | # KAMAL_VERSION 11 | # KAMAL_HOSTS 12 | # KAMAL_ROLE (if set) 13 | # KAMAL_DESTINATION (if set) 14 | # KAMAL_RUNTIME 15 | 16 | hosts = ENV["KAMAL_HOSTS"].split(",") 17 | results = nil 18 | max = 3 19 | 20 | elapsed = Benchmark.realtime do 21 | results = hosts.map do |host| 22 | Thread.new do 23 | tries = 1 24 | 25 | begin 26 | Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) 27 | rescue SocketError 28 | if tries < max 29 | puts "Retrying DNS warmup: #{host}" 30 | tries += 1 31 | sleep rand 32 | retry 33 | else 34 | puts "DNS warmup failed: #{host}" 35 | host 36 | end 37 | end 38 | 39 | tries 40 | end 41 | end.map(&:value) 42 | end 43 | 44 | retries = results.sum - hosts.size 45 | nopes = results.count { |r| r == max } 46 | 47 | puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] 48 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/secrets: -------------------------------------------------------------------------------- 1 | # Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, 2 | # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either 3 | # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. 4 | 5 | # Example of extracting secrets from 1password (or another compatible pw manager) 6 | # SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) 7 | # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) 8 | # RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) 9 | 10 | # Use a GITHUB_TOKEN if private repositories are needed for the image 11 | # GITHUB_TOKEN=$(gh config get -h github.com oauth_token) 12 | 13 | # Grab the registry password from ENV 14 | KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD 15 | 16 | # Improve security by using a password manager. Never check config/master.key into git! 17 | RAILS_MASTER_KEY=$(cat config/master.key) 18 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Omakase Ruby styling for Rails 2 | inherit_gem: { rubocop-rails-omakase: rubocop.yml } 3 | 4 | # Overwrite or add rules to create your own house style 5 | # 6 | # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` 7 | # Layout/SpaceInsideArrayLiteralBrackets: 8 | # Enabled: false 9 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.4.3 2 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM ruby:3.4.3 2 | 3 | # Install system dependencies 4 | RUN apt-get update -qq && \ 5 | apt-get install --no-install-recommends -y \ 6 | build-essential \ 7 | git \ 8 | libpq-dev \ 9 | libyaml-dev \ 10 | postgresql-client \ 11 | libvips \ 12 | pkg-config \ 13 | curl \ 14 | vim \ 15 | && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives 16 | 17 | # Set working directory 18 | WORKDIR /app 19 | 20 | # Install application dependencies 21 | COPY Gemfile Gemfile.lock ./ 22 | RUN bundle install 23 | 24 | # Add a script to be executed every time the container starts 25 | COPY entrypoint.dev.sh /usr/bin/ 26 | RUN chmod +x /usr/bin/entrypoint.dev.sh 27 | ENTRYPOINT ["entrypoint.dev.sh"] 28 | 29 | EXPOSE 3000 30 | 31 | # Start the main process 32 | CMD ["rails", "server", "-b", "0.0.0.0"] -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Documentation System Implementation 2 | 3 | ## Tasks: 4 | - [x] Create docs controller with SEO-friendly routes 5 | - [x] Create docs views with breadcrumbs and proper styling 6 | - [x] Add markdown parsing capability (using redcarpet gem) 7 | - [x] Create docs directory structure for markdown files 8 | - [x] Add docs navigation link to sidebar 9 | - [x] Ensure docs are accessible without login 10 | - [x] Create sample documentation files 11 | - [x] Style docs pages to match existing site CSS 12 | - [x] Test the implementation 13 | 14 | ## Documentation System Complete! ✅ 15 | 16 | The docs system is now fully implemented with: 17 | - SEO-friendly URLs (/docs, /docs/path/to/page) 18 | - Markdown file support with proper parsing 19 | - Breadcrumb navigation 20 | - Responsive design matching the app's style 21 | - Public access (no login required) 22 | - Sample documentation covering getting started, API, etc. 23 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/app/assets/images/favicon.png -------------------------------------------------------------------------------- /app/assets/stylesheets/active_users_graph.css: -------------------------------------------------------------------------------- 1 | .active-users-graph { 2 | display: flex; 3 | flex-direction: row; 4 | gap: 0.5rem; 5 | margin-top: 1rem; 6 | } 7 | 8 | .active-users-graph__hour { 9 | background-color: var(--uchu-dark-gray); 10 | opacity: 0.1; 11 | flex-grow: 1; 12 | min-width: 10px; 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/scrapyard.css: -------------------------------------------------------------------------------- 1 | .scrapyard-notice { 2 | background-color: var(--uchu-light-green); 3 | border-radius: 5px; 4 | color: var(--uchu-yellow) !important; 5 | position: relative; 6 | padding: 1rem; 7 | margin-top: 3rem; 8 | max-width: 26em; 9 | } 10 | 11 | .scrapyard-notice img { 12 | position: absolute; 13 | max-width: 100px; 14 | max-height: 100px; 15 | top: -50px; 16 | right: 20px; 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | .scrapyard-notice { 21 | background-color: rgba(var(--uchu-green-rgb), 0.2); 22 | border: 1px solid var(--uchu-green); 23 | } 24 | } -------------------------------------------------------------------------------- /app/controllers/admin/base_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::BaseController < ApplicationController 2 | before_action :authenticate_admin! 3 | 4 | private 5 | 6 | def authenticate_admin! 7 | unless current_user&.admin? 8 | redirect_to root_path, alert: "You are not authorized to access this page." 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/admin/ysws_reviews_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::YswsReviewsController < Admin::BaseController 2 | include ApplicationHelper 3 | 4 | before_action :set_submission, only: [ :show ] 5 | 6 | def show 7 | end 8 | 9 | private 10 | 11 | def set_submission 12 | @submission = Neighborhood::YswsSubmission.find_by(airtable_id: params[:record_id]) 13 | ensure_exists @submission 14 | @app = @submission.app 15 | ensure_exists @app 16 | @posts = @app.posts 17 | end 18 | 19 | def ensure_exists(value) 20 | unless value.present? 21 | object_name = value.nil? ? "Record" : value.class.name.demodulize 22 | redirect_to admin_timeline_path, alert: "#{object_name} not found." 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::UsersController < ApplicationController 2 | before_action :ensure_authenticated! 3 | 4 | def lookup_email 5 | email = params[:email] 6 | 7 | user = EmailAddress.find_by_email(email)&.user 8 | 9 | if user.present? 10 | render json: { user_id: user.id, email: email } 11 | else 12 | render json: { error: "User not found", email: email }, status: :not_found 13 | end 14 | end 15 | 16 | def lookup_slack_uid 17 | slack_uid = params[:slack_uid] 18 | 19 | user = User.find_by(slack_uid: slack_uid) 20 | 21 | if user.present? 22 | render json: { user_id: user.id, slack_uid: slack_uid } 23 | else 24 | render json: { error: "User not found", slack_uid: slack_uid }, status: :not_found 25 | end 26 | end 27 | 28 | private 29 | 30 | def ensure_authenticated! 31 | return if Rails.env.development? 32 | 33 | token = request.headers["Authorization"]&.split(" ")&.last 34 | render json: { error: "Unauthorized" }, status: :unauthorized unless token == ENV["STATS_API_KEY"] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/my/mailing_addresses_controller.rb: -------------------------------------------------------------------------------- 1 | module My 2 | class MailingAddressesController < ApplicationController 3 | before_action :ensure_current_user 4 | 5 | def show 6 | @user = current_user 7 | 8 | # Generate OTC if it doesn't exist 9 | if params[:from_fillout] 10 | sleep 1 # unfortunate hack to make sure the job runs after airtable gets the data 11 | FetchMailingAddressJob.perform_now(@user.id) 12 | else 13 | @user.update_column(:mailing_address_otc, SecureRandom.hex(8)) 14 | end 15 | end 16 | 17 | def edit 18 | current_user.update_column(:mailing_address_otc, SecureRandom.hex(8)) 19 | redirect_to "https://forms.hackclub.com/t/mo6hitqC6Vus?otc=#{current_user.mailing_address_otc}", allow_other_host: true 20 | end 21 | 22 | private 23 | 24 | def ensure_current_user 25 | redirect_to root_path, alert: "You must be logged in to view this page" unless current_user 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/my/mailroom_controller.rb: -------------------------------------------------------------------------------- 1 | module My 2 | class MailroomController < ApplicationController 3 | before_action :ensure_current_user 4 | 5 | def index 6 | @user = current_user 7 | @physical_mails = @user.physical_mails.order(created_at: :desc) 8 | @has_mailing_address = @user.mailing_address.present? 9 | end 10 | 11 | private 12 | 13 | def ensure_current_user 14 | redirect_to root_path, alert: "You must be logged in to view this page" unless current_user 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/sitemap_controller.rb: -------------------------------------------------------------------------------- 1 | class SitemapController < ApplicationController 2 | def sitemap 3 | respond_to do |format| 4 | format.xml { render layout: false } 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/wakatime_mirrors_controller.rb: -------------------------------------------------------------------------------- 1 | class WakatimeMirrorsController < ApplicationController 2 | before_action :set_user 3 | before_action :require_current_user 4 | before_action :set_mirror, only: [ :destroy ] 5 | 6 | def create 7 | @mirror = @user.wakatime_mirrors.build(mirror_params) 8 | if @mirror.save 9 | redirect_to my_settings_path, notice: "WakaTime mirror added successfully" 10 | else 11 | redirect_to my_settings_path, alert: "Failed to add WakaTime mirror: #{@mirror.errors.full_messages.join(', ')}" 12 | end 13 | end 14 | 15 | def destroy 16 | @mirror.destroy 17 | redirect_to my_settings_path, notice: "WakaTime mirror removed successfully" 18 | end 19 | 20 | private 21 | 22 | def set_user 23 | @user = User.find(params[:user_id]) 24 | end 25 | 26 | def set_mirror 27 | @mirror = @user.wakatime_mirrors.find(params[:id]) 28 | end 29 | 30 | def mirror_params 31 | params.require(:wakatime_mirror).permit(:endpoint_url, :encrypted_api_key) 32 | end 33 | 34 | def require_current_user 35 | unless @user == current_user 36 | redirect_to root_path, alert: "You are not authorized to access this page" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | import "controllers" 4 | 5 | function setupCurrentlyHacking() { 6 | const header = document.querySelector('.currently-hacking'); 7 | // only if no existing event listener 8 | if (!header) { return } 9 | header.onclick = function() { 10 | const container = document.querySelector('.currently-hacking-container'); 11 | if (container) { 12 | container.classList.toggle('visible'); 13 | } 14 | } 15 | } 16 | 17 | // Handle both initial page load and subsequent Turbo navigations 18 | document.addEventListener('turbo:load', setupCurrentlyHacking); 19 | document.addEventListener('turbo:render', setupCurrentlyHacking); 20 | document.addEventListener('DOMContentLoaded', setupCurrentlyHacking); 21 | -------------------------------------------------------------------------------- /app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } 10 | -------------------------------------------------------------------------------- /app/javascript/controllers/hello_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | this.element.textContent = "Hello World!" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Import and register all your controllers from the importmap via controllers/**/*_controller 2 | import { application } from "controllers/application" 3 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 4 | eagerLoadControllersFrom("controllers", application) 5 | -------------------------------------------------------------------------------- /app/javascript/controllers/nav_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["nav", "overlay"] 5 | 6 | toggle() { 7 | this.navTarget.classList.toggle("open") 8 | this.overlayTarget.classList.toggle("open") 9 | } 10 | 11 | close() { 12 | this.navTarget.classList.remove("open") 13 | this.overlayTarget.classList.remove("open") 14 | } 15 | 16 | clickLink(event) { 17 | // Close nav when clicking links on mobile 18 | if (window.innerWidth <= 768) { 19 | this.close() 20 | } 21 | } 22 | 23 | resize() { 24 | // Close nav when window is resized to desktop 25 | if (window.innerWidth > 768) { 26 | this.close() 27 | } 28 | } 29 | 30 | connect() { 31 | // Listen for window resize 32 | window.addEventListener('resize', this.resize.bind(this)) 33 | } 34 | 35 | disconnect() { 36 | window.removeEventListener('resize', this.resize.bind(this)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/attempt_to_deliver_physical_mail_job.rb: -------------------------------------------------------------------------------- 1 | class AttemptToDeliverPhysicalMailJob < ApplicationJob 2 | queue_as :literally_whenever 3 | 4 | include HasEnqueueControl 5 | enqueue_limit 1 6 | 7 | def perform 8 | PhysicalMail.pending_delivery.find_each do |mail| 9 | mail.deliver! 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/jobs/cache/active_projects_job.rb: -------------------------------------------------------------------------------- 1 | class Cache::ActiveProjectsJob < Cache::ActivityJob 2 | queue_as :latency_10s 3 | 4 | private 5 | 6 | def cache_expiration 7 | 15.minutes 8 | end 9 | 10 | def calculate 11 | # Get recent heartbeats with matching project_repo_mappings in a single SQL query 12 | ProjectRepoMapping.joins("INNER JOIN heartbeats ON heartbeats.project = project_repo_mappings.project_name") 13 | .joins("INNER JOIN users ON users.id = heartbeats.user_id") 14 | .where("heartbeats.source_type = ?", Heartbeat.source_types[:direct_entry]) 15 | .where("heartbeats.time > ?", 5.minutes.ago.to_f) 16 | .select("DISTINCT ON (heartbeats.user_id) project_repo_mappings.*, heartbeats.user_id") 17 | .order("heartbeats.user_id, heartbeats.time DESC") 18 | .index_by(&:user_id) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/jobs/cache/active_users_graph_data_job.rb: -------------------------------------------------------------------------------- 1 | class Cache::ActiveUsersGraphDataJob < Cache::ActivityJob 2 | queue_as :latency_10s 3 | 4 | private 5 | 6 | def calculate 7 | # over the last 24 hours, count the number of people who were active each hour 8 | hours = Heartbeat.coding_only 9 | .with_valid_timestamps 10 | .where("time > ?", 24.hours.ago.to_f) 11 | .where("time < ?", Time.current.to_f) 12 | .select("(EXTRACT(EPOCH FROM to_timestamp(time))::bigint / 3600 * 3600) as hour, COUNT(DISTINCT user_id) as count") 13 | .group("hour") 14 | .order("hour DESC") 15 | 16 | top_hour_count = hours.max_by(&:count)&.count || 1 17 | 18 | hours = hours.map do |h| 19 | { 20 | hour: Time.at(h.hour), 21 | users: h.count, 22 | height: (h.count.to_f / top_hour_count * 100).round 23 | } 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/jobs/cache/activity_job.rb: -------------------------------------------------------------------------------- 1 | class Cache::ActivityJob < ApplicationJob 2 | include GoodJob::ActiveJobExtensions::Concurrency 3 | 4 | good_job_control_concurrency_with( 5 | total: 1, 6 | drop: true 7 | ) 8 | 9 | def self.priority 10 | 10 11 | end 12 | 13 | def perform(force_reload: false) 14 | key = cache_key 15 | expiration = cache_expiration 16 | Rails.cache.write(key, calculate, expires_in: expiration) if force_reload 17 | 18 | Rails.cache.fetch(key, expires_in: expiration) do 19 | calculate 20 | end 21 | end 22 | 23 | private 24 | 25 | def cache_key 26 | self.class.name.underscore 27 | end 28 | 29 | def cache_expiration 30 | 1.hour 31 | end 32 | 33 | def calculate 34 | raise NotImplementedError, "You must implement #calculate in your job class" 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/jobs/cache/heartbeat_counts_job.rb: -------------------------------------------------------------------------------- 1 | class Cache::HeartbeatCountsJob < Cache::ActivityJob 2 | queue_as :latency_10s 3 | 4 | def expires_in 5 | 5.hours 6 | end 7 | 8 | private 9 | 10 | def calculate 11 | { 12 | recent_count: recent_count, 13 | recent_imported_count: recent_imported_count 14 | } 15 | end 16 | 17 | def recent_count 18 | Heartbeat.recent.count 19 | end 20 | 21 | def recent_imported_count 22 | Heartbeat.recent.where.not(source_type: :direct_entry).count 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/jobs/cache/home_stats_job.rb: -------------------------------------------------------------------------------- 1 | class Cache::HomeStatsJob < Cache::ActivityJob 2 | queue_as :latency_5m 3 | 4 | private 5 | 6 | def calculate 7 | seconds_by_user = Heartbeat.group(:user_id).duration_seconds 8 | { 9 | users_tracked: seconds_by_user.size, 10 | seconds_tracked: seconds_by_user.values.sum 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/jobs/cache/minutes_logged_job.rb: -------------------------------------------------------------------------------- 1 | class Cache::MinutesLoggedJob < Cache::ActivityJob 2 | queue_as :latency_10s 3 | 4 | private 5 | 6 | def calculate 7 | Heartbeat.coding_only 8 | .with_valid_timestamps 9 | .where(time: 1.hour.ago..Time.current) 10 | .duration_seconds / 60 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/jobs/cache/usage_social_proof_job.rb: -------------------------------------------------------------------------------- 1 | class Cache::UsageSocialProofJob < Cache::ActivityJob 2 | queue_as :latency_10s 3 | 4 | private 5 | 6 | def calculate 7 | # Only run queries as needed, starting with the smallest time range 8 | if (past_hour_count = users_in_past(1.hour)) > 5 9 | "In the past hour, #{past_hour_count} Hack Clubbers have coded with Hackatime." 10 | elsif (past_day_count = users_in_past(1.day)) > 5 11 | "In the past day, #{past_day_count} Hack Clubbers have coded with Hackatime." 12 | elsif (past_week_count = users_in_past(1.week)) > 5 13 | "In the past week, #{past_week_count} Hack Clubbers have coded with Hackatime." 14 | end 15 | end 16 | 17 | def users_in_past(duration) 18 | Heartbeat.coding_only 19 | .with_valid_timestamps 20 | .where("time > ?", duration.ago.to_f) 21 | .distinct.count(:user_id) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/jobs/check_streak_physical_mail_job.rb: -------------------------------------------------------------------------------- 1 | class CheckStreakPhysicalMailJob < ApplicationJob 2 | queue_as :literally_whenever 3 | 4 | include GoodJob::ActiveJobExtensions::Concurrency 5 | 6 | good_job_control_concurrency_with( 7 | total_limit: 1, 8 | key: -> { "check_streak_physical_mail_job" }, 9 | drop: true 10 | ) 11 | 12 | def perform 13 | streaks = Heartbeat.daily_streaks_for_users(users_with_recent_heartbeats) 14 | 15 | over_7_day_streaks = streaks.select { |_, streak| streak > 7 }.keys 16 | 17 | over_7_day_streaks.each do |user_id| 18 | next if PhysicalMail.going_out.exists?(user_id: user_id, mission_type: :first_time_7_streak) 19 | 20 | user = User.find(user_id) 21 | 22 | # Create the physical mail record 23 | PhysicalMail.create!( 24 | user: user, 25 | mission_type: :first_time_7_streak, 26 | status: :pending 27 | ) 28 | end 29 | end 30 | 31 | private 32 | 33 | def users_with_recent_heartbeats 34 | Heartbeat.where(time: 1.hour.ago..Time.current) 35 | .distinct 36 | .pluck(:user_id) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/jobs/cleanup_expired_email_verification_requests_job.rb: -------------------------------------------------------------------------------- 1 | class CleanupExpiredEmailVerificationRequestsJob < ApplicationJob 2 | queue_as :latency_10s 3 | 4 | include GoodJob::ActiveJobExtensions::Concurrency 5 | 6 | good_job_control_concurrency_with( 7 | total_limit: 1, 8 | drop: true 9 | ) 10 | 11 | def perform 12 | # Soft delete all expired and non-deleted verification requests in a single query 13 | EmailVerificationRequest.expired.where(deleted_at: nil) 14 | .update_all(deleted_at: Time.current) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/jobs/cleanup_successful_jobs_job.rb: -------------------------------------------------------------------------------- 1 | class CleanupSuccessfulJobsJob < ApplicationJob 2 | queue_as :literally_whenever 3 | 4 | include HasEnqueueControl 5 | enqueue_limit 6 | 7 | def perform 8 | GoodJob.cleanup_preserved_jobs(older_than: 2.days, include_discarded: false) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/jobs/concerns/has_enqueue_control.rb: -------------------------------------------------------------------------------- 1 | module HasEnqueueControl 2 | extend ActiveSupport::Concern 3 | include GoodJob::ActiveJobExtensions::Concurrency 4 | 5 | class_methods do 6 | def enqueue_limit(limit = 1) 7 | good_job_control_concurrency_with( 8 | total_limit: limit, 9 | key: "enqueue_control_#{self.name.underscore}", 10 | drop: true 11 | ) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/jobs/fetch_mailing_address_job.rb: -------------------------------------------------------------------------------- 1 | class FetchMailingAddressJob < ApplicationJob 2 | queue_as :default 3 | 4 | Table = Norairrecord.table(ENV["ADDRESS_AIRTABLE_PAT"], "appEC3w8nxAvCwAjL", "tbldZMvkUWGkkteQu") 5 | 6 | def perform(user_id) 7 | user = User.find(user_id) 8 | return unless user.mailing_address_otc.present? 9 | 10 | # Search Airtable for the address with this OTC 11 | records = Table.all(filter: "{OTC} = '#{user.mailing_address_otc}'") 12 | return if records.empty? 13 | 14 | address_data = records.first.fields 15 | 16 | # Create or update the mailing address 17 | mailing_address = user.mailing_address || user.build_mailing_address 18 | mailing_address.update!( 19 | first_name: address_data["first_name"], 20 | last_name: address_data["last_name"], 21 | line_1: address_data["line_1"], 22 | line_2: address_data["line_2"], 23 | city: address_data["city"], 24 | state: address_data["state"], 25 | zip_code: address_data["zip_code"], 26 | country: address_data["country"] 27 | ) 28 | 29 | records.first.destroy 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/jobs/geocode_users_without_country_job.rb: -------------------------------------------------------------------------------- 1 | class GeocodeUsersWithoutCountryJob < ApplicationJob 2 | queue_as :literally_whenever 3 | 4 | include HasEnqueueControl 5 | 6 | enqueue_limit 1 7 | 8 | def perform 9 | return if geocodable_users.empty? 10 | 11 | description = "Geocoding #{geocodable_users.count} user(s) at #{Time.current.iso8601}" 12 | 13 | GoodJob::Batch.enqueue(description: description) do 14 | geocodable_users.find_each do |user| 15 | SetUserCountryCodeJob.perform_later(user.id) 16 | end 17 | end 18 | end 19 | 20 | private 21 | 22 | def geocodable_users 23 | @geocodable_users ||= User.where(country_code: nil) 24 | .joins(:heartbeats) 25 | .where.not(heartbeats: { ip_address: nil }) 26 | .distinct 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/jobs/handle_email_signin_job.rb: -------------------------------------------------------------------------------- 1 | class HandleEmailSigninJob < ApplicationJob 2 | queue_as :latency_10s 3 | 4 | def perform(email) 5 | email_address = ActiveRecord::Base.transaction do 6 | EmailAddress.find_by(email: email) || begin 7 | user = User.create! 8 | user.email_addresses.create!(email: email, source: :signing_in) 9 | end 10 | end 11 | 12 | token = email_address.user.create_email_signin_token.token 13 | LoopsMailer.sign_in_email(email_address.email, token).deliver_now 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/jobs/neighborhood/sync_from_airtable_job.rb: -------------------------------------------------------------------------------- 1 | class Neighborhood::SyncFromAirtableJob < ApplicationJob 2 | queue_as :literally_whenever 3 | 4 | def perform 5 | Neighborhood::App.pull_all_from_airtable! 6 | sleep 10 7 | 8 | Neighborhood::Project.pull_all_from_airtable! 9 | sleep 10 10 | 11 | Neighborhood::Post.pull_all_from_airtable! 12 | sleep 10 13 | 14 | Neighborhood::YswsSubmission.pull_all_from_airtable! 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/jobs/neighborhood/trigger_time_update_job.rb: -------------------------------------------------------------------------------- 1 | class Neighborhood::TriggerTimeUpdateJob < ApplicationJob 2 | queue_as :literally_whenever 3 | 4 | include HasEnqueueControl 5 | enqueue_limit 1 6 | 7 | def perform 8 | posts = Neighborhood::Post 9 | .where("(airtable_fields->>'action-triggerHackatimeTimeUpdate') IS NULL") 10 | .order(Arel.sql("(airtable_fields->>'lastTimeUpdateAt') IS NULL ASC, (airtable_fields->>'lastTimeUpdateAt')::timestamp ASC")) 11 | .limit(10) 12 | .pluck(:airtable_id) 13 | 14 | records = posts.map { |id| table.new({}, id: id).tap { |rec| rec["action-triggerHackatimeTimeUpdate"] = true } } 15 | updates = table.batch_update(records) 16 | 17 | return unless updates.any? 18 | 19 | upsert_fields = updates.map { |update| { airtable_id: update.id, airtable_fields: update.fields } } 20 | Neighborhood::Post.upsert_all(upsert_fields, unique_by: :airtable_id) 21 | end 22 | 23 | private 24 | 25 | def table 26 | @table ||= Neighborhood::Post.table 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/jobs/one_time/log_to_sailors_log_channel_job.rb: -------------------------------------------------------------------------------- 1 | class OneTime::LogToSailorsLogChannelJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform 5 | # every user should have a sailors_log & 6 | # every sailors_log should atleast have a notification_preference for the debug sailors_log channel 7 | User.where.missing(:sailors_log) 8 | .where.not(slack_uid: nil) 9 | .pluck(:slack_uid).each do |slack_uid| 10 | puts "creating sailors_log for #{slack_uid}" 11 | SailorsLog.create!(slack_uid: slack_uid) 12 | end 13 | 14 | # Find SailorsLogs that don't have a notification preference for the debug channel 15 | debug_channel_id = "C0835AZP9GB" 16 | 17 | # Get all SailorsLogs 18 | SailorsLog.find_each do |sailors_log| 19 | # Check if this SailorsLog already has a notification preference for the debug channel 20 | has_preference = sailors_log.notification_preferences.exists?(slack_channel_id: debug_channel_id) 21 | 22 | # If not, create one 23 | unless has_preference 24 | puts "Creating notification preference for #{sailors_log.slack_uid}" 25 | sailors_log.notification_preferences.create!( 26 | slack_channel_id: debug_channel_id, 27 | enabled: true 28 | ) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/jobs/one_time/recalc_heartbeat_field_hash_job.rb: -------------------------------------------------------------------------------- 1 | class OneTime::RecalcHeartbeatFieldHashJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform 5 | Heartbeat.find_each(batch_size: 2500) do |heartbeat| 6 | begin 7 | heartbeat.send(:set_fields_hash!) 8 | heartbeat.save! 9 | rescue ActiveRecord::RecordNotUnique 10 | # If we have a duplicate fields_hash, soft delete this record 11 | heartbeat.soft_delete 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/jobs/one_time/reset_sailors_log_job.rb: -------------------------------------------------------------------------------- 1 | class OneTime::ResetSailorsLogJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform 5 | SailorsLog.find_each do |sl| 6 | next if sl.user.blank? 7 | sl.projects_summary = nil 8 | sl.send(:initialize_projects_summary) 9 | sl.save! 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/jobs/one_time/set_neighborhood_channels_job.rb: -------------------------------------------------------------------------------- 1 | class OneTime::SetNeighborhoodChannelsJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform 5 | User.where.not(slack_uid: nil).find_each(batch_size: 100) do |user| 6 | user.set_neighborhood_channel 7 | user.save! if user.changed? 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/jobs/one_time/set_user_timezone_from_slack_job.rb: -------------------------------------------------------------------------------- 1 | class OneTime::SetUserTimezoneFromSlackJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform 5 | User.where.not(slack_uid: nil).find_each(batch_size: 100) do |user| 6 | begin 7 | user.set_timezone_from_slack 8 | user.save! 9 | rescue => e 10 | Rails.logger.error "Failed to update timezone for user #{user.id}: #{e.message}" 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/jobs/sailors_log_teletype_job.rb: -------------------------------------------------------------------------------- 1 | class SailorsLogTeletypeJob < ApplicationJob 2 | queue_as :latency_10s 3 | 4 | def perform(message) 5 | HTTP.auth("Bearer #{ENV['TELETYPE_API_KEY']}") 6 | .post("https://printer.schmitworks.dev/api/raw", 7 | body: message) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/jobs/set_user_country_code_job.rb: -------------------------------------------------------------------------------- 1 | class SetUserCountryCodeJob < ApplicationJob 2 | queue_as :literally_whenever 3 | 4 | def perform(user_id) 5 | ips = Heartbeat.where(user_id: user_id) 6 | .where.not(ip_address: nil) 7 | .distinct 8 | .pluck(:ip_address) 9 | return if ips.empty? 10 | 11 | ips.each do |ip| 12 | begin 13 | puts "Getting country code for IP #{ip}" 14 | result = Geocoder.search(ip).first 15 | next unless result&.country_code.present? 16 | 17 | country_code = result.country_code.upcase 18 | puts "Found country code: #{country_code}" 19 | 20 | if ISO3166::Country.codes.include?(country_code) 21 | user.update!(country_code: country_code) 22 | return 23 | end 24 | rescue => e 25 | Rails.logger.error "Error getting country code for IP #{ip}: #{e.message}" 26 | next 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/jobs/slack_command/sailors_log_help_job.rb: -------------------------------------------------------------------------------- 1 | class SlackCommand::SailorsLogHelpJob < ApplicationJob 2 | queue_as :latency_10s 3 | 4 | def perform(response_url) 5 | HTTP.post(response_url, json: { 6 | response_type: "ephemeral", 7 | text: "Available commands: `/sailorslog on`, `/sailorslog off`, `/sailorslog leaderboard`" 8 | }) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/jobs/slack_command/sailors_log_job.rb: -------------------------------------------------------------------------------- 1 | class SlackCommand::SailorsLogJob < ApplicationJob 2 | queue_as :latency_10s 3 | 4 | def perform(params) 5 | case params[:text].downcase.strip 6 | when "on", "off" 7 | SlackCommand::SailorsLogOnOffJob.perform_now( 8 | params[:user_id], 9 | params[:channel_id], 10 | params[:user_name], 11 | params[:response_url], 12 | params[:text].downcase.strip == "on", 13 | ) 14 | when "leaderboard" 15 | # Process in background 16 | SlackCommand::SailorsLogLeaderboardJob.perform_now( 17 | params[:user_id], 18 | params[:channel_id], 19 | params[:response_url], 20 | ) 21 | else 22 | SlackCommand::SailorsLogHelpJob.perform_now( 23 | params[:response_url], 24 | ) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/jobs/slack_command/sailors_log_on_off_job.rb: -------------------------------------------------------------------------------- 1 | class SlackCommand::SailorsLogOnOffJob < ApplicationJob 2 | queue_as :latency_10s 3 | 4 | def perform(slack_uid, slack_channel_id, user_name, response_url, enabled) 5 | # set preference for the user 6 | slnp = SailorsLogNotificationPreference.find_or_initialize_by(slack_uid: slack_uid, slack_channel_id: slack_channel_id) 7 | slnp.enabled = enabled 8 | slnp.save! 9 | 10 | # invalidate the leaderboard cache 11 | SailorsLogLeaderboard.where(slack_channel_id: slack_channel_id, deleted_at: nil).update_all(deleted_at: Time.current) 12 | 13 | if enabled 14 | HTTP.post(response_url, json: { 15 | response_type: "in_channel", 16 | text: "@#{user_name} ran `/sailorslog on` to turn on High Seas notifications in this channel. Every time they code an hour on a project, a short message celebrating will be posted to this channel. They will also show on `/sailorslog leaderboard`." 17 | }) 18 | else 19 | HTTP.post(response_url, json: { 20 | response_type: "ephemeral", 21 | text: ":white_check_mark: Coding notifications have been turned off in this channel." 22 | }) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/jobs/slack_command/update_slack_channel_cache_job.rb: -------------------------------------------------------------------------------- 1 | class SlackCommand::UpdateSlackChannelCacheJob < ApplicationJob 2 | queue_as :latency_5m 3 | 4 | def perform 5 | channels = SailorsLogNotificationPreference.where(enabled: true).distinct.pluck(:slack_channel_id) 6 | 7 | Rails.logger.info("Updating slack channel cache for #{channels.count} channels") 8 | channels.each do |channel_id| 9 | sleep 2 # slack rate limit is 50 per minute 10 | Rails.logger.info("Updating slack channel cache for #{channel_id}") 11 | SlackChannel.find_by_id(channel_id, force_refresh: true) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/jobs/slack_username_update_job.rb: -------------------------------------------------------------------------------- 1 | class SlackUsernameUpdateJob < ApplicationJob 2 | queue_as :latency_5m 3 | 4 | include GoodJob::ActiveJobExtensions::Concurrency 5 | 6 | # Limits concurrency to 1 job per date 7 | good_job_control_concurrency_with( 8 | total: 1, 9 | drop: true 10 | ) 11 | 12 | def perform 13 | # in batches of 100, update the slack info for each user 14 | User.where.not(slack_uid: nil).find_each(batch_size: 100) do |user| 15 | begin 16 | user.update_from_slack 17 | user.save! 18 | rescue => e 19 | Rails.logger.error "Failed to update Slack username and avatar for user #{user.id}: #{e.message}" 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/jobs/update_slack_neighborhood_channels_job.rb: -------------------------------------------------------------------------------- 1 | class UpdateSlackNeighborhoodChannelsJob < ApplicationJob 2 | queue_as :literally_whenever 3 | 4 | include HasEnqueueControl 5 | enqueue_limit 6 | 7 | def perform 8 | User.where.not(slack_uid: nil).find_each(batch_size: 1000) do |user| 9 | user.set_neighborhood_channel 10 | user.save! if user.changed? 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/jobs/user_slack_status_update_job.rb: -------------------------------------------------------------------------------- 1 | class UserSlackStatusUpdateJob < ApplicationJob 2 | queue_as :latency_10s 3 | 4 | BATCH_SIZE = 25 5 | 6 | def perform 7 | User.where(uses_slack_status: true).find_each(batch_size: BATCH_SIZE) do |user| 8 | begin 9 | user.update_slack_status 10 | rescue => e 11 | Rails.logger.error "Failed to update Slack status for user #{user.slack_uid}: #{e.message}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/jobs/wakatime_clear_test_heartbeats_job.rb: -------------------------------------------------------------------------------- 1 | class WakatimeClearTestHeartbeatsJob < ApplicationJob 2 | queue_as :default 3 | 4 | include GoodJob::ActiveJobExtensions::Concurrency 5 | 6 | # Limits concurrency to 1 job per date 7 | good_job_control_concurrency_with( 8 | total: 1, 9 | drop: true 10 | ) 11 | 12 | def perform 13 | Heartbeat.where(source_type: "test_entry") 14 | .where("created_at < ?", 7.days.ago) 15 | .delete_all 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/jobs/wakatime_mirror_sync_job.rb: -------------------------------------------------------------------------------- 1 | class WakatimeMirrorSyncJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(mirror) 5 | mirror.sync_heartbeats 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: ENV.fetch("SMTP_FROM_EMAIL", "noreply@timedump.hackclub.com") 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/auth_mailer.rb: -------------------------------------------------------------------------------- 1 | class AuthMailer < ApplicationMailer 2 | def sign_in_email(email_address, token) 3 | @token = token 4 | @user = email_address.user 5 | 6 | mail( 7 | to: email_address.email, 8 | subject: "Your Hackatime sign-in link" 9 | ) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/mailers/email_verification_mailer.rb: -------------------------------------------------------------------------------- 1 | class EmailVerificationMailer < ApplicationMailer 2 | # Subject can be set in your I18n file at config/locales/en.yml 3 | # with the following lookup: 4 | # 5 | # en.email_verification_mailer.verify_email.subject 6 | # 7 | def verify_email(verification_request) 8 | @verification_request = verification_request 9 | @user = verification_request.user 10 | @verification_url = auth_token_url(verification_request.token) 11 | 12 | mail( 13 | to: verification_request.email, 14 | subject: "Verify your email address for Hackatime" 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/mailers/loops_mailer.rb: -------------------------------------------------------------------------------- 1 | class LoopsMailer < ApplicationMailer 2 | if Rails.env.development? && ENV["LOOPS_API_KEY"].nil? 3 | self.delivery_method = :letter_opener 4 | else 5 | self.delivery_method = :smtp 6 | self.smtp_settings = { 7 | address: "smtp.loops.so", 8 | port: 587, 9 | user_name: "loops", 10 | password: ENV["LOOPS_API_KEY"], 11 | authentication: "plain", 12 | enable_starttls: true 13 | } 14 | end 15 | 16 | def sign_in_email(email, token) 17 | @email = email 18 | @token = token 19 | @sign_in_url = auth_token_url(@token) 20 | 21 | mail( 22 | to: @email, 23 | ) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/models/ahoy/event.rb: -------------------------------------------------------------------------------- 1 | class Ahoy::Event < ApplicationRecord 2 | include AhoyCaptain::Ahoy::EventMethods 3 | include Ahoy::QueryMethods 4 | 5 | self.table_name = "ahoy_events" 6 | 7 | belongs_to :visit 8 | belongs_to :user, optional: true 9 | end 10 | -------------------------------------------------------------------------------- /app/models/ahoy/visit.rb: -------------------------------------------------------------------------------- 1 | class Ahoy::Visit < ApplicationRecord 2 | include AhoyCaptain::Ahoy::VisitMethods 3 | self.table_name = "ahoy_visits" 4 | 5 | has_many :events, class_name: "Ahoy::Event" 6 | belongs_to :user, optional: true 7 | end 8 | -------------------------------------------------------------------------------- /app/models/api_key.rb: -------------------------------------------------------------------------------- 1 | class ApiKey < ApplicationRecord 2 | belongs_to :user 3 | 4 | validates :token, presence: true, uniqueness: true 5 | validates :name, presence: true, uniqueness: { scope: :user_id } 6 | 7 | before_validation :generate_token!, on: :create 8 | 9 | private 10 | 11 | def generate_token! 12 | # we need to keep ourselves compatible with WakaTime: https://github.com/wakatime/vscode-wakatime/blob/241b60c8491c14e3c093b1ef2a0276c38586a172/src/utils.ts#L24 13 | # they use a UUID v4 14 | self.token ||= SecureRandom.uuid_v4 15 | 16 | # Mark it as something not imported from WakaTime 17 | self.name ||= "Hackatime key" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/application_direct_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationDirectRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | connects_to database: { writing: :primary_direct } 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /app/models/commit.rb: -------------------------------------------------------------------------------- 1 | class Commit < ApplicationRecord 2 | # Explicitly set 'sha' as the primary key for ActiveRecord. 3 | # This is crucial because we defined it as such in the migration. 4 | self.primary_key = :sha 5 | 6 | belongs_to :user 7 | belongs_to :repository, optional: true 8 | 9 | validates :sha, presence: true, uniqueness: true 10 | validates :user_id, presence: true 11 | # `github_raw` could be validated for presence if a commit record implies it must have GitHub data. 12 | # validates :github_raw, presence: true 13 | 14 | # Note on timestamps: 15 | # Rails will automatically manage `updated_at`. 16 | # We will manually set `created_at` when creating a record, 17 | # based on the `committer.date` from the API. 18 | end 19 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/email_address.rb: -------------------------------------------------------------------------------- 1 | class EmailAddress < ApplicationRecord 2 | belongs_to :user 3 | has_paper_trail 4 | 5 | validates :email, presence: true, 6 | uniqueness: true, 7 | format: { with: URI::MailTo::EMAIL_REGEXP } 8 | 9 | enum :source, { 10 | signing_in: 0, 11 | github: 1, 12 | slack: 2 13 | }, prefix: true 14 | 15 | before_validation :downcase_email 16 | 17 | private 18 | 19 | def downcase_email 20 | self.email = email.downcase 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/models/hackatime/heartbeat.rb: -------------------------------------------------------------------------------- 1 | class Hackatime::Heartbeat < HackatimeRecord 2 | include Heartbeatable 3 | 4 | def self.cached_recent_count 5 | Rails.cache.fetch("heartbeats_recent_count", expires_in: 5.minutes) do 6 | recent.size 7 | end 8 | end 9 | 10 | scope :recent, -> { where("time > ?", 24.hours.ago) } 11 | scope :today, -> { where("DATE(time) = ?", Date.current) } 12 | 13 | # This is a hack to avoid using the default Rails inheritance column– Rails is confused by the field `type` in the db 14 | self.inheritance_column = nil 15 | # Prevent collision with Ruby's hash method 16 | self.ignored_columns += [ "hash" ] 17 | end 18 | -------------------------------------------------------------------------------- /app/models/hackatime/project_label.rb: -------------------------------------------------------------------------------- 1 | class Hackatime::ProjectLabel < HackatimeRecord 2 | self.table_name = "project_labels" 3 | 4 | has_many :heartbeats, 5 | ->(project) { where(user_id: project.user_id) }, 6 | foreign_key: :project, 7 | primary_key: :project_key, 8 | class_name: "Hackatime::Heartbeat" 9 | 10 | belongs_to :user, 11 | foreign_key: :user_id, 12 | primary_key: :slack_uid, 13 | class_name: "User" 14 | end 15 | -------------------------------------------------------------------------------- /app/models/hackatime/user.rb: -------------------------------------------------------------------------------- 1 | class Hackatime::User < HackatimeRecord 2 | self.table_name = "users" 3 | end 4 | -------------------------------------------------------------------------------- /app/models/hackatime_record.rb: -------------------------------------------------------------------------------- 1 | class HackatimeRecord < ApplicationRecord 2 | self.abstract_class = true 3 | connects_to database: { reading: :wakatime, writing: :wakatime } 4 | end 5 | -------------------------------------------------------------------------------- /app/models/leaderboard.rb: -------------------------------------------------------------------------------- 1 | class Leaderboard < ApplicationRecord 2 | GLOBAL_TIMEZONE = "UTC" 3 | 4 | has_many :entries, 5 | class_name: "LeaderboardEntry", 6 | dependent: :destroy 7 | 8 | validates :start_date, presence: true 9 | 10 | enum :period_type, { 11 | daily: 0, 12 | weekly: 1, 13 | last_7_days: 2 14 | } 15 | 16 | def finished_generating? 17 | finished_generating_at.present? 18 | end 19 | 20 | def period_end_date 21 | case period_type 22 | when "weekly" 23 | start_date + 6.days 24 | when "last_7_days" 25 | start_date 26 | else 27 | start_date 28 | end 29 | end 30 | 31 | def date_range_text 32 | if weekly? 33 | "#{start_date.strftime('%b %d')} - #{period_end_date.strftime('%b %d, %Y')}" 34 | else 35 | start_date.strftime("%B %d, %Y") 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/models/leaderboard_entry.rb: -------------------------------------------------------------------------------- 1 | class LeaderboardEntry < ApplicationRecord 2 | belongs_to :leaderboard 3 | belongs_to :user 4 | 5 | validates :total_seconds, presence: true, numericality: { greater_than_or_equal_to: 0 } 6 | validates :user_id, uniqueness: { scope: :leaderboard_id } 7 | end 8 | -------------------------------------------------------------------------------- /app/models/mailing_address.rb: -------------------------------------------------------------------------------- 1 | class MailingAddress < ApplicationRecord 2 | has_paper_trail 3 | belongs_to :user 4 | 5 | encrypts :first_name, deterministic: true 6 | encrypts :last_name, deterministic: true 7 | encrypts :zip_code, deterministic: true 8 | encrypts :line_1, deterministic: true 9 | encrypts :line_2, deterministic: true 10 | encrypts :city, deterministic: true 11 | encrypts :state, deterministic: true 12 | encrypts :country, deterministic: true 13 | 14 | after_save :update_user_country_code 15 | 16 | private 17 | 18 | def update_user_country_code 19 | return unless country.present? 20 | 21 | # Find the country by name and get its ISO code 22 | country_obj = ISO3166::Country.find_country_by_any_name(country) 23 | return unless country_obj 24 | 25 | user.update_column(:country_code, country_obj.alpha2) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/models/neighborhood/app.rb: -------------------------------------------------------------------------------- 1 | class Neighborhood::App < ApplicationRecord 2 | self.table_name = "neighborhood_apps" 3 | 4 | include HasTableSync 5 | 6 | has_table_sync base: "appnsN4MzbnfMY0ai", 7 | table: "tbls2fHyQYCtCbYbl", 8 | pat: ENV["NEIGHBORHOOD_AIRTABLE_PAT"] 9 | 10 | def posts 11 | return [] unless airtable_fields["devlog"]&.any? 12 | Neighborhood::Post.where(airtable_id: airtable_fields["devlog"]) 13 | end 14 | 15 | def projects 16 | return [] unless airtable_fields["hackatimeProjects"]&.any? 17 | Neighborhood::Project.where(airtable_id: airtable_fields["hackatimeProjects"]) 18 | end 19 | 20 | def ysws_submission 21 | return nil unless airtable_fields["YSWS Project Submission"]&.first 22 | Neighborhood::YswsSubmission.find_by(airtable_id: airtable_fields["YSWS Project Submission"].first) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/neighborhood/post.rb: -------------------------------------------------------------------------------- 1 | class Neighborhood::Post < ApplicationRecord 2 | self.table_name = "neighborhood_posts" 3 | 4 | include HasTableSync 5 | 6 | BASE_ID = "appnsN4MzbnfMY0ai" 7 | TABLE_ID = "tbl0iKxglbySiEbB4" 8 | 9 | has_table_sync base: BASE_ID, 10 | table: TABLE_ID, 11 | pat: ENV["NEIGHBORHOOD_AIRTABLE_PAT"] 12 | 13 | def app 14 | return nil unless airtable_fields["app"]&.first 15 | Neighborhood::App.find_by(airtable_id: airtable_fields["app"].first) 16 | end 17 | 18 | def push_to_airtable!(fields) 19 | response = HTTP.patch( 20 | "https://api.airtable.com/v0/#{BASE_ID}/#{TABLE_ID}/#{airtable_id}", 21 | headers: { 22 | "Authorization" => "Bearer #{ENV["NEIGHBORHOOD_AIRTABLE_PAT"]}", 23 | "Content-Type" => "application/json" 24 | }, 25 | json: { fields: fields } 26 | ) 27 | new_fields = JSON.parse(response.body)["fields"] 28 | if response.status.success? 29 | update(airtable_fields: new_fields) 30 | else 31 | raise "Failed to push to Airtable: #{response.body}" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/models/neighborhood/project.rb: -------------------------------------------------------------------------------- 1 | class Neighborhood::Project < ApplicationRecord 2 | self.table_name = "neighborhood_projects" 3 | 4 | include HasTableSync 5 | 6 | has_table_sync base: "appnsN4MzbnfMY0ai", 7 | table: "tblIqliBgKvoNT3uD", 8 | pat: ENV["NEIGHBORHOOD_AIRTABLE_PAT"] 9 | end 10 | -------------------------------------------------------------------------------- /app/models/neighborhood/ysws_submission.rb: -------------------------------------------------------------------------------- 1 | class Neighborhood::YswsSubmission < ApplicationRecord 2 | self.table_name = "neighborhood_ysws_submissions" 3 | 4 | include HasTableSync 5 | 6 | has_table_sync base: "appnsN4MzbnfMY0ai", 7 | table: "tblbyu0FABZJ0wvaJ", 8 | pat: ENV["NEIGHBORHOOD_AIRTABLE_PAT"] 9 | 10 | def app 11 | return nil unless airtable_fields["app"]&.first 12 | Neighborhood::App.find_by(airtable_id: airtable_fields["app"].first) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/raw_heartbeat_upload.rb: -------------------------------------------------------------------------------- 1 | class RawHeartbeatUpload < ApplicationRecord 2 | has_many :heartbeats 3 | 4 | validates :request_headers, presence: true 5 | validates :request_body, presence: true 6 | end 7 | -------------------------------------------------------------------------------- /app/models/repo_host_event.rb: -------------------------------------------------------------------------------- 1 | class RepoHostEvent < ApplicationRecord 2 | belongs_to :user 3 | 4 | # Tell ActiveRecord to use 'id' as the primary key, even though it's a string. 5 | self.primary_key = :id 6 | 7 | enum :provider, { github: 0, gitlab: 1 } 8 | 9 | # Validations 10 | validates :id, presence: true, uniqueness: true 11 | validates :raw_event_payload, presence: true 12 | validates :provider, presence: true 13 | validates :created_at, presence: true # This is the event's occurrence time from the provider 14 | 15 | # Ensure ID starts with a recognized provider prefix 16 | validates :id, format: { 17 | with: /\A(gh|gl)_.+\z/, # Allow gh_ or gl_ prefixes 18 | message: "must start with a provider prefix (e.g., gh_ or gl_)" 19 | } 20 | 21 | # Helper scope 22 | scope :for_user_and_provider, ->(user, provider_name) { 23 | where(user: user, provider: providers[provider_name.to_sym]) 24 | } 25 | 26 | # Helper to construct the prefixed ID 27 | def self.construct_event_id(provider_name, original_event_id) 28 | prefix = case provider_name.to_sym 29 | when :github then "gh_" 30 | when :gitlab then "gl_" # Example for future 31 | else 32 | raise ArgumentError, "Unknown provider: #{provider_name}" 33 | end 34 | "#{prefix}#{original_event_id}" 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/models/sailors_log_notification_preference.rb: -------------------------------------------------------------------------------- 1 | class SailorsLogNotificationPreference < ApplicationRecord 2 | before_validation :ensure_sailors_log_exists 3 | 4 | belongs_to :sailors_log, 5 | class_name: "SailorsLog", 6 | foreign_key: :slack_uid, 7 | primary_key: :slack_uid 8 | 9 | validates :slack_uid, uniqueness: { 10 | scope: :slack_channel_id, 11 | message: "already has a notification preference for this channel" 12 | } 13 | 14 | private 15 | 16 | def ensure_sailors_log_exists 17 | return if sailors_log.present? 18 | 19 | self.sailors_log = SailorsLog.find_or_create_by(slack_uid: slack_uid) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/models/sailors_log_slack_notification.rb: -------------------------------------------------------------------------------- 1 | class SailorsLogSlackNotification < ApplicationRecord 2 | def notify_user! 3 | return if sent? 4 | 5 | SailorsLogNotifyJob.perform_now(self.id) 6 | end 7 | 8 | def notify_user_later! 9 | SailorsLogNotifyJob.perform_later(self.id) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/sign_in_token.rb: -------------------------------------------------------------------------------- 1 | class SignInToken < ApplicationRecord 2 | belongs_to :user 3 | 4 | enum :auth_type, { 5 | email: 0, 6 | slack: 1 7 | } 8 | 9 | validates :token, presence: true, uniqueness: true 10 | validates :auth_type, presence: true 11 | validates :expires_at, presence: true 12 | 13 | before_validation :generate_token, on: :create 14 | before_validation :set_expiration, on: :create 15 | 16 | scope :valid, -> { where("expires_at > ? AND used_at IS NULL", Time.current) } 17 | 18 | def mark_used! 19 | update!(used_at: Time.current) 20 | end 21 | 22 | private 23 | 24 | def generate_token 25 | self.token ||= SecureRandom.urlsafe_base64(32) 26 | end 27 | 28 | def set_expiration 29 | self.expires_at ||= 30.minutes.from_now 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/models/warehouse/scrapyard_event.rb: -------------------------------------------------------------------------------- 1 | class Warehouse::ScrapyardEvent < WarehouseRecord 2 | self.table_name = "airtable_hack_club_scrapyard_appigkif7gbvisalg.events" 3 | 4 | # Prevent these columns from messing with acriverecord 5 | self.ignored_columns += [ "errors" ] 6 | 7 | # local attendees is a list of airtable ids 8 | has_many :local_attendees, class_name: "Warehouse::ScrapyardLocalAttendee", foreign_key: "event_id" 9 | end 10 | -------------------------------------------------------------------------------- /app/models/warehouse/scrapyard_local_attendee.rb: -------------------------------------------------------------------------------- 1 | class Warehouse::ScrapyardLocalAttendee < WarehouseRecord 2 | self.table_name = "airtable_hack_club_scrapyard_appigkif7gbvisalg.local_attendees" 3 | 4 | # The event field in Airtable is a JSONB array with a single event ID 5 | def event_id 6 | self[:event]&.first 7 | end 8 | 9 | # Override the event association to handle the array field 10 | def event 11 | return nil if event_id.nil? 12 | Warehouse::ScrapyardEvent.find_by(id: event_id) 13 | end 14 | 15 | # Find the associated user through their email 16 | def user 17 | return nil if self[:email].blank? 18 | EmailAddress.find_by(email: self[:email])&.user 19 | end 20 | 21 | # Scope to find attendees for a specific event 22 | scope :for_event, ->(event) { 23 | where("event ? :event_id", event_id: event.id) 24 | } 25 | end 26 | -------------------------------------------------------------------------------- /app/models/warehouse_record.rb: -------------------------------------------------------------------------------- 1 | class WarehouseRecord < ApplicationRecord 2 | self.abstract_class = true 3 | connects_to database: { reading: :warehouse, writing: :warehouse } 4 | end 5 | -------------------------------------------------------------------------------- /app/services/repo_host/service_factory.rb: -------------------------------------------------------------------------------- 1 | module RepoHost 2 | class ServiceFactory 3 | def self.for_url(user, repo_url) 4 | case repo_url 5 | when %r{github\.com} 6 | GithubService.new(user, repo_url) 7 | else 8 | raise ArgumentError, "Unsupported repository host: #{repo_url}. Currently only GitHub is supported." 9 | end 10 | end 11 | 12 | def self.supported_hosts 13 | %w[github.com] 14 | end 15 | 16 | def self.host_for_url(repo_url) 17 | uri = URI.parse(repo_url) 18 | uri.host 19 | rescue URI::InvalidURIError 20 | nil 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/auth_mailer/sign_in_email.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Welcome back to Hackatime!

8 |

9 | Click the link below to sign in to your account: 10 |

11 |

12 | <%= link_to 'Sign in to Hackatime', auth_token_url(@token.token) %> 13 |

14 |

15 | This link will expire in 30 minutes and can only be used once. 16 |

17 |

18 | If you didn't request this email, you can safely ignore it. 19 |

20 | 21 | -------------------------------------------------------------------------------- /app/views/email_verification_mailer/verify_email.html.erb: -------------------------------------------------------------------------------- 1 |

Verify your email address for Hackatime

2 |

3 | Hi <%= @user.display_name %>, 4 |

5 |

6 | You've requested to add <%= @verification_request.email %> to your Hackatime account. 7 | Click the link below to verify this email address: 8 |

9 |

10 | <%= link_to 'Verify email address', @verification_url %> 11 |

12 |

13 | This link will expire in 30 minutes and can only be used once. 14 |

15 |

16 | If you didn't request this email, you can safely ignore it. 17 |

18 | -------------------------------------------------------------------------------- /app/views/email_verification_mailer/verify_email.text.erb: -------------------------------------------------------------------------------- 1 | Verify your email address for Hackatime 2 | 3 | Hi <%= @user.display_name %>, 4 | 5 | You've requested to add <%= @verification_request.email %> to your Hackatime account. 6 | Click the link below to verify this email address: 7 | 8 | <%= @verification_url %> 9 | 10 | This link will expire in 30 minutes and can only be used once. 11 | 12 | If you didn't request this email, you can safely ignore it. 13 | -------------------------------------------------------------------------------- /app/views/good_job/shared/_secondary_navbar.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/leaderboards/_mini_leaderboard_loading.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | This leaderboard is in <%= Leaderboard::GLOBAL_TIMEZONE %>. 4 | <% if current_user && timezone_difference_in_seconds(Leaderboard::GLOBAL_TIMEZONE, current_user.timezone) != 0 %> 5 | <%= timezone_difference_in_words(Leaderboard::GLOBAL_TIMEZONE, current_user.timezone) %> 6 | <% end %> 7 |

8 |
9 | <% (current_user ? 5 : 3).times do %> 10 |
11 | ... 12 | Loading... 13 | ... 14 |
15 | <% end %> 16 |
17 |
-------------------------------------------------------------------------------- /app/views/loops_mailer/sign_in_email.text.erb: -------------------------------------------------------------------------------- 1 | { 2 | "transactionalId": "cm86bg9vd00xuddjuh5u5mfuu", 3 | "email": "<%= @email %>", 4 | "dataVariables": { 5 | "auth_link": "<%= @sign_in_url %>" 6 | } 7 | } -------------------------------------------------------------------------------- /app/views/my/mailing_addresses/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title do %> 2 | My Mailing Address 3 | <% end %> 4 | 5 |
6 |
7 |

My Mailing Address

8 |
9 | 10 | <% if @user.mailing_address.present? %> 11 |
12 |
13 |

<%= @user.mailing_address.first_name %> <%= @user.mailing_address.last_name %>

14 |

<%= @user.mailing_address.line_1 %>

15 | <% if @user.mailing_address.line_2.present? %> 16 |

<%= @user.mailing_address.line_2 %>

17 | <% end %> 18 |

<%= @user.mailing_address.city %>, <%= @user.mailing_address.state %> <%= @user.mailing_address.zip_code %>

19 |

<%= @user.mailing_address.country %>

20 |
21 |
22 | <% end %> 23 | 24 |

25 | <%= link_to @user.mailing_address.present? ? "Update this address" : "Add your address", 26 | edit_my_mailing_address_path, 27 | class: "button" %> 28 |

29 |
-------------------------------------------------------------------------------- /app/views/my/project_repo_mappings/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Edit Repository Mapping

3 | 4 | <%= form_with(model: @project_repo_mapping, url: my_project_repo_mapping_path(@project_repo_mapping.project_name), method: :patch, local: true) do |f| %> 5 |
6 | <%= f.label :project_name %> 7 | <%= f.text_field :project_name, value: @project_repo_mapping.project_name, disabled: true %> 8 |
9 | 10 |
11 | <%= f.label :repo_url %> 12 | <%= f.url_field :repo_url, placeholder: "https://github.com/username/repo" %> 13 |
14 | 15 |
16 | <%= f.submit "Update Mapping" %> 17 | <%= link_to "Cancel", root_path, class: "button" %> 18 |
19 | <% end %> 20 |
-------------------------------------------------------------------------------- /app/views/my/project_repo_mappings/index.html.erb: -------------------------------------------------------------------------------- 1 |

My Projects

2 | 3 | <% if current_user.github_uid.blank? %> 4 |
5 | You can't tie your projects to GitHub until you connect your GitHub account. 6 | <%= link_to "Sign in with GitHub", github_auth_path, class: "btn btn-primary" %> 7 |
8 | <% end %> 9 | 10 | <%= render "shared/interval_selector" %> 11 | 12 | <%= turbo_frame_tag "project_durations", src: project_durations_static_pages_path(interval: params[:interval], from: params[:from], to: params[:to]), target: "_top" do %> 13 |
Loading projects...
14 | <% end %> -------------------------------------------------------------------------------- /app/views/pwa/manifest.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hackatime", 3 | "icons": [ 4 | { 5 | "src": "/icon.png", 6 | "type": "image/png", 7 | "sizes": "512x512" 8 | }, 9 | { 10 | "src": "/icon.png", 11 | "type": "image/png", 12 | "sizes": "512x512", 13 | "purpose": "maskable" 14 | } 15 | ], 16 | "start_url": "/", 17 | "display": "standalone", 18 | "scope": "/", 19 | "description": "Track your coding time with Hackatime.", 20 | "theme_color": "red", 21 | "background_color": "red" 22 | } 23 | -------------------------------------------------------------------------------- /app/views/pwa/service-worker.js: -------------------------------------------------------------------------------- 1 | // Add a service worker for processing Web Push notifications: 2 | // 3 | // self.addEventListener("push", async (event) => { 4 | // const { title, options } = await event.data.json() 5 | // event.waitUntil(self.registration.showNotification(title, options)) 6 | // }) 7 | // 8 | // self.addEventListener("notificationclick", function(event) { 9 | // event.notification.close() 10 | // event.waitUntil( 11 | // clients.matchAll({ type: "window" }).then((clientList) => { 12 | // for (let i = 0; i < clientList.length; i++) { 13 | // let client = clientList[i] 14 | // let clientPath = (new URL(client.url)).pathname 15 | // 16 | // if (clientPath == event.notification.data.path && "focus" in client) { 17 | // return client.focus() 18 | // } 19 | // } 20 | // 21 | // if (clients.openWindow) { 22 | // return clients.openWindow(event.notification.data.path) 23 | // } 24 | // }) 25 | // ) 26 | // }) 27 | -------------------------------------------------------------------------------- /app/views/scrapyard_leaderboards/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

<%= @event.name %>

4 | <%= link_to "← Back to all scrapyards", scrapyard_leaderboards_path, class: "back-link" %> 5 |
6 | 7 |
8 |

Hackatime Users

9 | 10 | <% if @attendee_stats.any? %> 11 |
12 | <% @attendee_stats.each_with_index do |stats, index| %> 13 |
14 |
15 |
16 | <%= stats[:display_name] %> 17 |
18 |
19 | <%= ApplicationController.helpers.short_time_simple(stats[:total_seconds]) %> 20 |
21 |
22 |
23 | <% end %> 24 |
25 | <% else %> 26 |

No coding activity recorded for this event yet.

27 | <% end %> 28 |
29 |
30 | 31 | <% content_for :head do %> 32 | <% end %> -------------------------------------------------------------------------------- /app/views/sessions/close_window.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "Successfully signed in!" %> 2 | 7 |

Successfully signed in! You can close this window.

8 | -------------------------------------------------------------------------------- /app/views/shared/_multi_select.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | Filter by <%= label.downcase %>... 7 |
8 | 9 |
10 |
11 | 12 |
13 | <% values.reject(&:blank?).each do |value| %> 14 | 22 | <% end %> 23 |
24 |
25 |
26 |
-------------------------------------------------------------------------------- /app/views/shared/_slack_channel_mention.erb: -------------------------------------------------------------------------------- 1 | <%= link_to SlackChannel.find_by_id(channel_id), "https://slack.com/app_redirect?channel=#{channel_id}", target: "_blank" %> -------------------------------------------------------------------------------- /app/views/shared/_video_on_hover.html.erb: -------------------------------------------------------------------------------- 1 | <%# This is a partial for a video that plays on hover. it's provided a src %> 2 | 12 | 13 | -------------------------------------------------------------------------------- /app/views/sitemap/sitemap.xml.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://hackatime.hackclub.com/ 5 | <%= Date.current.iso8601 %> 6 | daily 7 | 1.0 8 | 9 | 10 | https://hackatime.hackclub.com/leaderboard 11 | <%= Date.current.iso8601 %> 12 | hourly 13 | 0.8 14 | 15 | 16 | https://hackatime.hackclub.com/docs 17 | <%= Date.current.iso8601 %> 18 | weekly 19 | 0.7 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/views/static_pages/_active_users_graph.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | <% hours.each_with_index do |h, i| %> 4 |
7 |
8 | <% end %> 9 |
10 | -------------------------------------------------------------------------------- /app/views/static_pages/_currently_hacking.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_frame_tag "currently_hacking" do %> 2 | <% if users&.any? %> 3 |
4 | 20 |
21 | <% else %> 22 |
23 | No one is currently hacking 24 |
25 | <% end %> 26 | <% end %> 27 | 28 | -------------------------------------------------------------------------------- /app/views/static_pages/_scrapyard.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Went to Scrapyard?

4 |

The event is over, but you can still see the <%= link_to "leaderboards", scrapyard_leaderboards_path %>!

5 |
6 | -------------------------------------------------------------------------------- /app/views/static_pages/_streak.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_frame_tag "streak" do %> 2 | <% if current_user.streak_days_formatted %> 3 |

4 | <%= current_user.streak_days_formatted %> 🔥 5 |

6 | <% end %> 7 | <% end %> -------------------------------------------------------------------------------- /app/views/static_pages/my_projects.html.erb: -------------------------------------------------------------------------------- 1 |

My Projects

2 | 3 | <% if current_user.github_uid.blank? %> 4 |
5 | You can't tie your projects to GitHub until you connect your GitHub account. 6 | <%= link_to "Sign in with GitHub", github_auth_path, class: "btn btn-primary" %> 7 |
8 | <% end %> 9 | 10 | <%= render "shared/interval_selector" %> 11 | 12 | <%= turbo_frame_tag "project_durations", src: project_durations_static_pages_path(interval: params[:interval], from: params[:from], to: params[:to]), target: "_top" do %> 13 |
Loading projects...
14 | <% end %> -------------------------------------------------------------------------------- /app/views/users/_wakatime_config_display.html.erb: -------------------------------------------------------------------------------- 1 | <% if @user.api_keys.any? %> 2 |
 3 | # put this in your ~/.wakatime.cfg file
 4 | 
 5 | [settings]
 6 | api_url = https://<%= request.host_with_port %>/api/hackatime/v1
 7 | api_key = <%= @user.api_keys.last.token %>
 8 | heartbeat_rate_limit_seconds = 30
 9 | 
10 | # any other wakatime configs you want to add: https://github.com/wakatime/wakatime-cli/blob/develop/USAGE.md#ini-config-file
11 |   
12 | <% else %> 13 |

14 | No API keys found. Please migrate your keys from waka.hackclub.com below. New API key generation has yet to be implemented. 15 |

16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :head do %> 2 | 3 | <% end %> 4 | 5 |
6 |
7 |

Welcome, <%= @user.display_name %>

8 |
9 |
10 | 11 | <%= turbo_frame_tag "filterable_dashboard", src: filterable_dashboard_static_pages_path do %> 12 | Loading... 13 | <% end %> -------------------------------------------------------------------------------- /app/views/users/wakatime_setup_step_2.html.erb: -------------------------------------------------------------------------------- 1 |

Hackatime Set Up

2 |

Step 2 of 4

3 | 4 |

Pick your coding editors

5 | You can change this later! 6 | 7 | -------------------------------------------------------------------------------- /app/views/users/wakatime_setup_step_4.html.erb: -------------------------------------------------------------------------------- 1 |

Hackatime Set Up

2 | 3 |

Step 4 of 4

4 | 5 |

6 | <%= @no_instruction_wording %> You're already done! 7 |

8 | 9 |
10 | 11 |
12 | 13 | <%= link_to my_wakatime_setup_step_2_path do %> 14 | 15 | <% end %> 16 | 17 | <%= link_to root_path do %> 18 | 19 | <% end %> 20 | -------------------------------------------------------------------------------- /bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | ARGV.unshift("--ensure-latest") 6 | 7 | load Gem.bin_path("brakeman", "brakeman") 8 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | exec "./bin/rails", "server", *ARGV 3 | -------------------------------------------------------------------------------- /bin/docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Enable jemalloc for reduced memory usage and latency. 4 | if [ -z "${LD_PRELOAD+x}" ]; then 5 | LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) 6 | export LD_PRELOAD 7 | fi 8 | 9 | # If running the rails server then create or migrate existing database 10 | if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then 11 | ./bin/rails db:prepare 12 | fi 13 | 14 | exec "${@}" 15 | -------------------------------------------------------------------------------- /bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/application" 4 | require "importmap/commands" 5 | -------------------------------------------------------------------------------- /bin/jobs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/environment" 4 | require "solid_queue/cli" 5 | 6 | SolidQueue::Cli.start(ARGV) 7 | -------------------------------------------------------------------------------- /bin/kamal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'kamal' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("kamal", "kamal") 28 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts "== Installing dependencies ==" 16 | system("bundle check") || system!("bundle install") 17 | 18 | # puts "\n== Copying sample files ==" 19 | # unless File.exist?("config/database.yml") 20 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 21 | # end 22 | 23 | puts "\n== Preparing database ==" 24 | system! "bin/rails db:prepare" 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! "bin/rails log:clear tmp:clear" 28 | 29 | unless ARGV.include?("--skip-server") 30 | puts "\n== Starting development server ==" 31 | STDOUT.flush # flush the output before exec(2) so that it displays 32 | exec "bin/dev" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /bin/thrust: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | load Gem.bin_path("thruster", "thrust") 6 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | # Async adapter only works within the same process, so for manually triggering cable updates from a console, 2 | # and seeing results in the browser, you must do so from the web console (running inside the dev process), 3 | # not a terminal started via bin/rails console! Add "console" to any action or any ERB template view 4 | # to make the web console appear. 5 | development: 6 | adapter: async 7 | 8 | test: 9 | adapter: test 10 | 11 | production: 12 | adapter: solid_cable 13 | connects_to: 14 | database: 15 | writing: cable 16 | polling_interval: 0.1.seconds 17 | message_retention: 1.day 18 | -------------------------------------------------------------------------------- /config/cache.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | store_options: 3 | # Cap age of oldest cache entry to fulfill retention policies 4 | # max_age: <%= 60.days.to_i %> 5 | max_size: <%= 256.megabytes %> 6 | namespace: <%= Rails.env %> 7 | 8 | development: 9 | <<: *default 10 | 11 | test: 12 | <<: *default 13 | 14 | production: 15 | database: cache 16 | <<: *default 17 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | kWWykHBCYD/iY+RwU9owAghiv+8BCXPlAlMLk5lyiQwkdJDDIMoi4L10ADlZRC49tuuGROCwEBOoZ+W+57GiHtt9gzaF3QgI1RCvMxvEp69YcBV/RhSMKrMWsMBKCtTvAo5STufztsd7S0grzHi6BdhH2MIcGlqQ7+NOxZiHdkCLPxGTqV/FkbO5G12P7VrQ+ifnxQG4RFkFiViCGGTkJa7YxMZubjmCIfKzYBgVrk4nEs7VY2JwNxftJJWKK9gGPh1eYMHhKM9Rv/raAzEyu2XJJ3GOzJWRkoQFanbxKmOVp+26YC9JVlIxQg4DPyAEkTuxvYn8+9koAKqi9oVs8TsjXx9JENMFitP31PR4SvkFJa6Utgv5KTojc/90aChz5+paKgchhyGgbsasjdVgX3aVxfb0VTpOZ1rq0H+F7MJ46FkkeqJCVzfX3T9RNOVDEACSzDddnpFwo3u80osxldkPDCZnqDl/QjJ5yys/BAeI21TwdbH4/iNL--RiT2aAu+uPXDLOvZ--vIZG6ZKmCZgwr0DaK7uF2w== -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/honeybadger.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # For more options, see https://docs.honeybadger.io/lib/ruby/gem-reference/configuration 3 | 4 | api_key: 'hbp_IET1u9vKrC7IWjt45arAsfguTrGSFR1bSxEq' 5 | 6 | # The environment your app is running in. 7 | env: "<%= Rails.env %>" 8 | 9 | # The absolute path to your project folder. 10 | root: "<%= Rails.root.to_s %>" 11 | 12 | # Honeybadger won't report errors in these environments. 13 | development_environments: 14 | - test 15 | - development 16 | - cucumber 17 | 18 | # By default, Honeybadger won't report errors in the development_environments. 19 | # You can override this by explicitly setting report_data to true or false. 20 | # report_data: true 21 | 22 | # The current Git revision of your project. Defaults to the last commit hash. 23 | # revision: null 24 | 25 | # Enable verbose debug logging (useful for troubleshooting). 26 | debug: false 27 | 28 | # Enable Honeybadger Insights 29 | insights: 30 | enabled: true 31 | -------------------------------------------------------------------------------- /config/importmap.rb: -------------------------------------------------------------------------------- 1 | # Pin npm packages by running ./bin/importmap 2 | 3 | pin "application" 4 | pin "@hotwired/turbo-rails", to: "turbo.min.js" 5 | pin "@hotwired/stimulus", to: "stimulus.min.js" 6 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" 7 | pin_all_from "app/javascript/controllers", under: "controllers" 8 | pin "@rails/request.js", to: "https://ga.jspm.io/npm:@rails/request.js@0.0.8/src/index.js" 9 | -------------------------------------------------------------------------------- /config/initializers/active_record_encryption.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.active_record.encryption.primary_key = ENV["ENCRYPTION_PRIMARY_KEY"] 2 | Rails.application.config.active_record.encryption.deterministic_key = ENV["ENCRYPTION_DETERMINISTIC_KEY"] 3 | Rails.application.config.active_record.encryption.key_derivation_salt = ENV["ENCRYPTION_KEY_DERIVATION_SALT"] 4 | -------------------------------------------------------------------------------- /config/initializers/ahoy.rb: -------------------------------------------------------------------------------- 1 | class Ahoy::Store < Ahoy::DatabaseStore 2 | end 3 | 4 | # set to true for JavaScript tracking 5 | Ahoy.api = false 6 | 7 | # set to true for geocoding (and add the geocoder gem to your Gemfile) 8 | # we recommend configuring local geocoding as well 9 | # see https://github.com/ankane/ahoy#geocoding 10 | Ahoy.geocode = true 11 | 12 | Ahoy.job_queue = :literally_whenever 13 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 2 | allow do 3 | origins "*" 4 | 5 | # Allow CORS for the hackatime API endpoints 6 | resource "/api/hackatime/v1/*", 7 | headers: :any, 8 | methods: [ :get, :post, :options ], 9 | expose: [ "Authorization" ], 10 | max_age: 600 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/geocoder.rb: -------------------------------------------------------------------------------- 1 | require Rails.root.join("app/lib/hack_club_geocoder_lookup") 2 | 3 | # Ensure the lookup class is available in the Geocoder::Lookup namespace 4 | Geocoder::Lookup.const_set(:HackClub, HackClubGeocoderLookup) unless Geocoder::Lookup.const_defined?(:HackClub) 5 | 6 | Geocoder.configure( 7 | timeout: 15, 8 | lookup: HackClubGeocoderLookup, 9 | api_key: ENV["HACKCLUB_GEOCODER_API_KEY"], 10 | cache: Geocoder::CacheStore::Generic.new(Rails.cache, {}), 11 | ) 12 | -------------------------------------------------------------------------------- /config/initializers/git_version.rb: -------------------------------------------------------------------------------- 1 | # Get the first 6 characters of the current git commit hash 2 | git_hash = ENV["SOURCE_COMMIT"] || `git rev-parse HEAD` rescue "unknown" 3 | 4 | commit_link = git_hash != "unknown" ? "https://github.com/hackclub/harbor/commit/#{git_hash}" : nil 5 | 6 | short_hash = git_hash[0..7] 7 | 8 | commit_count = `git rev-list --count HEAD`.strip rescue 0 9 | 10 | # Check if there are any uncommitted changes 11 | is_dirty = `git status --porcelain`.strip.length > 0 rescue false 12 | 13 | # Append "-dirty" if there are uncommitted changes 14 | version = is_dirty ? "#{short_hash}-dirty" : short_hash 15 | 16 | # Store server start time 17 | Rails.application.config.server_start_time = Time.current 18 | 19 | # Store the version 20 | Rails.application.config.git_version = version 21 | Rails.application.config.git_commit_count = commit_count 22 | Rails.application.config.commit_link = commit_link 23 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/monkey_patches.rb: -------------------------------------------------------------------------------- 1 | # 🔧🐒 2 | 3 | Rails.configuration.to_prepare do 4 | PaperTrail::Version.class_eval do 5 | def user 6 | begin 7 | User.find(whodunnit) if whodunnit 8 | rescue ActiveRecord::RecordNotFound 9 | nil 10 | end 11 | end 12 | end 13 | 14 | # Monkeypatch Airtable rate limit to be more conservative 15 | Norairrecord::Client.send(:remove_const, :AIRTABLE_RPS_LIMIT) if Norairrecord::Client.const_defined?(:AIRTABLE_RPS_LIMIT) 16 | Norairrecord::Client.const_set(:AIRTABLE_RPS_LIMIT, 2) # Set to 2 requests per second 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/papertrail.rb: -------------------------------------------------------------------------------- 1 | class PaperTrail::Version < ActiveRecord::Base 2 | include PaperTrail::VersionConcern 3 | 4 | self.table_name = "versions" 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/sentry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sentry.init do |config| 4 | config.dsn = ENV["SENTRY_DSN"] 5 | config.breadcrumbs_logger = [ :active_support_logger, :http_logger ] 6 | config.send_default_pii = true 7 | config.traces_sample_rate = 1.0 8 | config.profiles_sample_rate = 1.0 9 | end 10 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization and 2 | # are automatically loaded by Rails. If you want to use locales other than 3 | # English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more about the API, please read the Rails Internationalization guide 20 | # at https://guides.rubyonrails.org/i18n.html. 21 | # 22 | # Be aware that YAML interprets the following case-insensitive strings as 23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings 24 | # must be quoted to be interpreted as strings. For example: 25 | # 26 | # en: 27 | # "yes": yup 28 | # enabled: "ON" 29 | 30 | en: 31 | hello: "Hello world" 32 | -------------------------------------------------------------------------------- /config/skylight.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # The authentication token for the application. 3 | authentication: SJGDElOyNN4OESRDxcL3qTgjIgJVyHnYCW6-XUddxgk 4 | ignored_endpoints: 5 | - SessionsController#impersonate 6 | - SessionsController#stop_impersonating 7 | - LetterOpenerWeb::Engine 8 | - ScrapyardLeaderboardsController#index 9 | - Api::Hackatime::V1::HackatimeController#push_heartbeats 10 | - Api::Hackatime::V1::HackatimeController#status_bar_today 11 | - Api::V1::StatsController#user_stats 12 | - Rails::HealthController#show 13 | - AhoyCaptain::StatsController#show 14 | - ActionDispatch::Routing::RouteSet#* 15 | - ActionDispatch::Static 16 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /db/cable_schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema[7.1].define(version: 1) do 2 | create_table "solid_cable_messages", force: :cascade do |t| 3 | t.binary "channel", limit: 1024, null: false 4 | t.binary "payload", limit: 536870912, null: false 5 | t.datetime "created_at", null: false 6 | t.integer "channel_hash", limit: 8, null: false 7 | t.index [ "channel" ], name: "index_solid_cable_messages_on_channel" 8 | t.index [ "channel_hash" ], name: "index_solid_cable_messages_on_channel_hash" 9 | t.index [ "created_at" ], name: "index_solid_cable_messages_on_created_at" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/cache_schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema[7.2].define(version: 1) do 4 | create_table "solid_cache_entries", force: :cascade do |t| 5 | t.binary "key", limit: 1024, null: false 6 | t.binary "value", limit: 536870912, null: false 7 | t.datetime "created_at", null: false 8 | t.integer "key_hash", limit: 8, null: false 9 | t.integer "byte_size", limit: 4, null: false 10 | t.index [ "byte_size" ], name: "index_solid_cache_entries_on_byte_size" 11 | t.index [ "key_hash", "byte_size" ], name: "index_solid_cache_entries_on_key_hash_and_byte_size" 12 | t.index [ "key_hash" ], name: "index_solid_cache_entries_on_key_hash", unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20240101000000_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[7.1] 2 | def change 3 | create_table :users do |t| 4 | t.string :slack_uid, null: false, index: { unique: true } 5 | t.string :email, null: false, index: { unique: true } 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20240316000001_create_daily_leaderboard_entries.rb: -------------------------------------------------------------------------------- 1 | class CreateDailyLeaderboardEntries < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :daily_leaderboard_entries do |t| 4 | t.references :daily_leaderboard, null: false, foreign_key: true 5 | t.string :user_id, null: false 6 | t.integer :total_seconds, null: false, default: 0 7 | t.integer :rank 8 | t.timestamps 9 | 10 | t.index [ :daily_leaderboard_id, :user_id ], unique: true, name: 'idx_leaderboard_entries_on_leaderboard_and_user' 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20240316000002_rename_daily_leaderboard_tables.rb: -------------------------------------------------------------------------------- 1 | class RenameDailyLeaderboardTables < ActiveRecord::Migration[8.0] 2 | def change 3 | rename_table :daily_leaderboards, :leaderboards 4 | rename_table :daily_leaderboard_entries, :leaderboard_entries 5 | 6 | # Update the foreign key 7 | rename_column :leaderboard_entries, :daily_leaderboard_id, :leaderboard_id 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20240316000003_remove_status_from_leaderboards.rb: -------------------------------------------------------------------------------- 1 | class RemoveStatusFromLeaderboards < ActiveRecord::Migration[8.0] 2 | def change 3 | remove_column :leaderboards, :status, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240320000000_add_profile_fields_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddProfileFieldsToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :username, :string 4 | add_column :users, :avatar_url, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20240320000001_add_admin_at_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAdminAtToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :admin_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240320000002_switch_admin_at_to_is_admin.rb: -------------------------------------------------------------------------------- 1 | class SwitchAdminAtToIsAdmin < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :is_admin, :boolean, default: false, null: false 4 | 5 | # Copy data from admin_at to is_admin 6 | User.reset_column_information 7 | User.where.not(admin_at: nil).update_all(is_admin: true) 8 | 9 | remove_column :users, :admin_at 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20250216173459_add_object_changes_to_versions.rb: -------------------------------------------------------------------------------- 1 | # This migration adds the optional `object_changes` column, in which PaperTrail 2 | # will store the `changes` diff for each update event. See the readme for 3 | # details. 4 | class AddObjectChangesToVersions < ActiveRecord::Migration[8.0] 5 | # The largest text column available in all supported RDBMS. 6 | # See `create_versions.rb` for details. 7 | TEXT_BYTES = 1_073_741_823 8 | 9 | def change 10 | add_column :versions, :object_changes, :text, limit: TEXT_BYTES 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20250221204236_add_uses_slack_status.rb: -------------------------------------------------------------------------------- 1 | class AddUsesSlackStatus < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :uses_slack_status, :boolean, default: false, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250221205702_add_slack_oauth_scopes_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddSlackOauthScopesToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :slack_scopes, :string, array: true, default: [] 4 | add_column :users, :slack_access_token, :text 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250222002320_add_finished_generating_at_to_leaderboards.rb: -------------------------------------------------------------------------------- 1 | class AddFinishedGeneratingAtToLeaderboards < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :leaderboards, :finished_generating_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250222004511_remove_start_date_unique_index_from_leaderboards.rb: -------------------------------------------------------------------------------- 1 | class RemoveStartDateUniqueIndexFromLeaderboards < ActiveRecord::Migration[8.0] 2 | def change 3 | remove_index :leaderboards, :start_date 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250222005401_add_deleted_at_to_leaderboards.rb: -------------------------------------------------------------------------------- 1 | class AddDeletedAtToLeaderboards < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :leaderboards, :deleted_at, :datetime, null: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250222031955_create_sailors_logs.rb: -------------------------------------------------------------------------------- 1 | class CreateSailorsLogs < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :sailors_logs do |t| 4 | t.string :slack_uid, null: false 5 | t.jsonb :projects_summary, null: false, default: {} 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20250222032551_create_sailors_log_notification_preferences.rb: -------------------------------------------------------------------------------- 1 | class CreateSailorsLogNotificationPreferences < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :sailors_log_notification_preferences do |t| 4 | t.string :slack_uid, null: false 5 | t.string :slack_channel_id, null: false 6 | t.boolean :enabled, null: false, default: true 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20250222032930_create_sailors_log_slack_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateSailorsLogSlackNotifications < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :sailors_log_slack_notifications do |t| 4 | t.string :slack_uid, null: false 5 | t.string :slack_channel_id, null: false 6 | t.string :project_name, null: false 7 | t.integer :project_duration, null: false 8 | 9 | t.boolean :sent, null: false, default: false 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20250222082500_add_unique_index_to_sailors_log_notification_preferences.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueIndexToSailorsLogNotificationPreferences < ActiveRecord::Migration[8.0] 2 | def change 3 | add_index :sailors_log_notification_preferences, 4 | [ :slack_uid, :slack_channel_id ], 5 | unique: true, 6 | name: 'idx_sailors_log_notification_preferences_unique_user_channel' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20250223072034_create_sailors_log_leaderboards.rb: -------------------------------------------------------------------------------- 1 | class CreateSailorsLogLeaderboards < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :sailors_log_leaderboards do |t| 4 | t.string :slack_channel_id 5 | t.string :slack_uid 6 | t.text :message 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20250223085114_add_deleted_at_to_sailors_log_leaderboard_job.rb: -------------------------------------------------------------------------------- 1 | class AddDeletedAtToSailorsLogLeaderboardJob < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :sailors_log_leaderboards, :deleted_at, :datetime, default: nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250303175821_create_api_keys.rb: -------------------------------------------------------------------------------- 1 | class CreateApiKeys < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :api_keys do |t| 4 | t.belongs_to :user, null: false, foreign_key: true 5 | t.text :name, null: false 6 | t.text :token, null: false, index: { unique: true } 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :api_keys, :token, unique: true, if_not_exists: true 12 | add_index :api_keys, [ :user_id, :token ], unique: true, if_not_exists: true 13 | add_index :api_keys, [ :user_id, :name ], unique: true, if_not_exists: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20250304032720_add_extension_text_type_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddExtensionTextTypeToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :hackatime_extension_text_type, :integer, default: 0, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250305061242_uniqueness_index_to_hash_on_heartbeats.rb: -------------------------------------------------------------------------------- 1 | 2 | class UniquenessIndexToHashOnHeartbeats < ActiveRecord::Migration[8.0] 3 | def change 4 | attributes = [ 5 | :user_id, 6 | :branch, 7 | :category, 8 | :dependencies, 9 | :editor, 10 | :entity, 11 | :language, 12 | :machine, 13 | :operating_system, 14 | :project, 15 | :type, 16 | :user_agent, 17 | :line_additions, 18 | :line_deletions, 19 | :lineno, 20 | :lines, 21 | :cursorpos, 22 | :project_root_count, 23 | :time, 24 | :is_write 25 | ] 26 | 27 | # clean up the index from ./20250303180842_create_heartbeats.rb 28 | remove_index :heartbeats, 29 | attributes, 30 | unique: true 31 | 32 | add_column :heartbeats, :fields_hash, :text 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /db/migrate/20250305194250_add_source_to_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class Heartbeat < ApplicationRecord 2 | end 3 | 4 | class AddSourceToHeartbeats < ActiveRecord::Migration[8.0] 5 | def change 6 | add_column :heartbeats, :source_type, :integer 7 | 8 | Heartbeat.update_all(source_type: 1) 9 | 10 | change_column_null :heartbeats, :source_type, false 11 | change_column_default :heartbeats, :source_type, to: 0 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20250305195904_enforce_uniqueness_index_on_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class EnforceUniquenessIndexOnHeartbeats < ActiveRecord::Migration[8.0] 2 | def change 3 | add_index :heartbeats, :fields_hash, unique: true, if_not_exists: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250306033109_change_heartbeats_time_to_float8.rb: -------------------------------------------------------------------------------- 1 | class ChangeHeartbeatsTimeToFloat8 < ActiveRecord::Migration[8.0] 2 | def change 3 | change_column :heartbeats, :time, :float8, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250307201004_rename_user_id_on_leaderboard_entries.rb: -------------------------------------------------------------------------------- 1 | class RenameUserIdOnLeaderboardEntries < ActiveRecord::Migration[8.0] 2 | def change 3 | rename_column :leaderboard_entries, :user_id, :slack_uid 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250307223936_create_email_addresses.rb: -------------------------------------------------------------------------------- 1 | class CreateEmailAddresses < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :email_addresses do |t| 4 | t.string :email 5 | t.references :user, null: false, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | add_index :email_addresses, :email, unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20250307224352_move_emails_to_email_addresses.rb: -------------------------------------------------------------------------------- 1 | class MoveEmailsToEmailAddresses < ActiveRecord::Migration[8.0] 2 | def up 3 | execute <<-SQL 4 | INSERT INTO email_addresses (email, user_id, created_at, updated_at) 5 | SELECT email, id, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP 6 | FROM users 7 | WHERE email IS NOT NULL 8 | SQL 9 | 10 | remove_column :users, :email 11 | end 12 | 13 | def down 14 | add_column :users, :email, :string 15 | 16 | execute <<-SQL 17 | UPDATE users 18 | SET email = ( 19 | SELECT email 20 | FROM email_addresses 21 | WHERE email_addresses.user_id = users.id 22 | LIMIT 1 23 | ) 24 | SQL 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /db/migrate/20250307225347_create_sign_in_tokens.rb: -------------------------------------------------------------------------------- 1 | class CreateSignInTokens < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :sign_in_tokens do |t| 4 | t.string :token 5 | t.references :user, null: false, foreign_key: true 6 | t.integer :auth_type 7 | t.datetime :expires_at 8 | t.datetime :used_at 9 | 10 | t.timestamps 11 | end 12 | add_index :sign_in_tokens, :token 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20250310165010_rename_avatar_url_to_slack_avatar_url_on_users.rb: -------------------------------------------------------------------------------- 1 | class RenameAvatarUrlToSlackAvatarUrlOnUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | rename_column :users, :avatar_url, :slack_avatar_url 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250312110220_add_period_type_to_leaderboards.rb: -------------------------------------------------------------------------------- 1 | class AddPeriodTypeToLeaderboards < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :leaderboards, :period_type, :integer, default: 0, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250312160326_create_project_repo_mappings.rb: -------------------------------------------------------------------------------- 1 | class CreateProjectRepoMappings < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :project_repo_mappings do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.string :project_name, null: false 6 | t.string :repo_url, null: false 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :project_repo_mappings, [ :user_id, :project_name ], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20250312172534_allow_null_slack_uid.rb: -------------------------------------------------------------------------------- 1 | class AllowNullSlackUid < ActiveRecord::Migration[8.0] 2 | def change 3 | change_column_null :users, :slack_uid, true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250315030446_add_ip_address_to_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class AddIpAddressToHeartbeats < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :heartbeats, :ip_address, :inet 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250315214819_add_index_category_time_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class AddIndexCategoryTimeHeartbeats < ActiveRecord::Migration[8.0] 2 | def change 3 | add_index :heartbeats, [ :category, :time ], name: 'index_heartbeats_on_category_and_time' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250319142656_add_timezone_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddTimezoneToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :timezone, :string, default: "UTC" 4 | add_index :users, :timezone 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250319165910_add_git_hub_fields_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddGitHubFieldsToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :github_uid, :string 4 | add_column :users, :github_avatar_url, :string 5 | add_column :users, :github_access_token, :text 6 | add_column :users, :github_username, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20250319193636_add_slack_username_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddSlackUsernameToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :slack_username, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250324203539_add_streak_count_to_leaderboard_entries.rb: -------------------------------------------------------------------------------- 1 | class AddStreakCountToLeaderboardEntries < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :leaderboard_entries, :streak_count, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250425211619_add_ysws_program_to_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class AddYswsProgramToHeartbeats < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :heartbeats, :ysws_program, :integer, default: 0, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250503215404_add_source_to_email_addresses.rb: -------------------------------------------------------------------------------- 1 | class AddSourceToEmailAddresses < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :email_addresses, :source, :integer, null: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250505151057_create_email_verification_requests.rb: -------------------------------------------------------------------------------- 1 | class CreateEmailVerificationRequests < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :email_verification_requests do |t| 4 | t.string :email 5 | t.references :user, null: false, foreign_key: true 6 | t.string :token 7 | t.datetime :expires_at 8 | 9 | t.timestamps 10 | end 11 | add_index :email_verification_requests, :email, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20250505152654_add_deleted_at_to_email_verification_requests.rb: -------------------------------------------------------------------------------- 1 | class AddDeletedAtToEmailVerificationRequests < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :email_verification_requests, :deleted_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250506155521_add_slack_neighborhood_channel_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddSlackNeighborhoodChannelToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :slack_neighborhood_channel, :string, null: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250507174855_add_omit_from_leaderboard_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddOmitFromLeaderboardToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :omit_from_leaderboard, :boolean, default: false, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250507182617_add_deleted_at_to_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class AddDeletedAtToHeartbeats < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :heartbeats, :deleted_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250507183451_add_partial_unique_index_to_heartbeat_fields_hash.rb: -------------------------------------------------------------------------------- 1 | class AddPartialUniqueIndexToHeartbeatFieldsHash < ActiveRecord::Migration[8.0] 2 | def change 3 | # Add a partial index that only applies to non-deleted records 4 | add_index :heartbeats, :fields_hash, 5 | unique: true, 6 | where: "deleted_at IS NULL", 7 | name: "index_heartbeats_on_fields_hash_when_not_deleted" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20250507184341_remove_fields_hash_index_from_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class RemoveFieldsHashIndexFromHeartbeats < ActiveRecord::Migration[8.0] 2 | def change 3 | remove_index :heartbeats, name: "index_heartbeats_on_fields_hash" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250507204732_add_trust_level_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddTrustLevelToUsers < ActiveRecord::Migration[7.1] 2 | def up 3 | add_column :users, :trust_level, :integer, default: 0, null: false 4 | 5 | # Convert existing omit_from_leaderboard values 6 | execute <<-SQL 7 | UPDATE users#{' '} 8 | SET trust_level = CASE#{' '} 9 | WHEN omit_from_leaderboard = true THEN 1 -- untrusted 10 | ELSE 0 -- default 11 | END 12 | SQL 13 | 14 | # Remove the old column 15 | remove_column :users, :omit_from_leaderboard 16 | end 17 | 18 | def down 19 | add_column :users, :omit_from_leaderboard, :boolean, default: false, null: false 20 | 21 | # Convert back 22 | execute <<-SQL 23 | UPDATE users#{' '} 24 | SET omit_from_leaderboard = CASE#{' '} 25 | WHEN trust_level = 1 THEN true -- untrusted 26 | ELSE false -- default 27 | END 28 | SQL 29 | 30 | remove_column :users, :trust_level 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /db/migrate/20250507211848_add_index_to_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToHeartbeats < ActiveRecord::Migration[8.0] 2 | def change 3 | add_index :heartbeats, [ :user_id, :time ], 4 | name: "idx_heartbeats_user_time_active", 5 | where: "deleted_at IS NULL", 6 | if_not_exists: true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20250509160228_add_raw_data_to_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class AddRawDataToHeartbeats < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :heartbeats, :raw_data, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250509170101_add_country_code_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddCountryCodeToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :country_code, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250509183721_create_mailing_addresses.rb: -------------------------------------------------------------------------------- 1 | class CreateMailingAddresses < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :mailing_addresses do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.string :first_name, null: false 6 | t.string :last_name, null: false 7 | t.string :zip_code, null: false 8 | t.string :line_1, null: false 9 | t.string :line_2 10 | t.string :city, null: false 11 | t.string :state, null: false 12 | t.string :country, null: false 13 | 14 | t.timestamps 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20250509191155_add_mailing_address_otc_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddMailingAddressOtcToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :mailing_address_otc, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250512205858_create_wakatime_mirrors.rb: -------------------------------------------------------------------------------- 1 | class CreateWakatimeMirrors < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :wakatime_mirrors do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.string :endpoint_url, null: false, default: "https://wakatime.com/api/v1" 6 | t.string :encrypted_api_key, null: false 7 | t.datetime :last_synced_at 8 | 9 | t.timestamps 10 | end 11 | 12 | add_index :wakatime_mirrors, [ :user_id, :endpoint_url ], unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20250513183739_create_raw_heartbeat_uploads.rb: -------------------------------------------------------------------------------- 1 | class CreateRawHeartbeatUploads < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :raw_heartbeat_uploads do |t| 4 | t.jsonb :request_headers, null: false 5 | t.jsonb :request_body, null: false 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20250513184040_add_raw_heartbeat_upload_to_heartbeats.rb: -------------------------------------------------------------------------------- 1 | class AddRawHeartbeatUploadToHeartbeats < ActiveRecord::Migration[8.0] 2 | def change 3 | add_reference :heartbeats, :raw_heartbeat_upload, null: true, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250514150404_create_physical_mails.rb: -------------------------------------------------------------------------------- 1 | class CreatePhysicalMails < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :physical_mails do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.integer :mission_type, null: false 6 | t.integer :status, null: false, default: 0 7 | t.string :theseus_id 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20250514212714_create_commits.rb: -------------------------------------------------------------------------------- 1 | class CreateCommits < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :commits, primary_key: :sha, id: :string do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.jsonb :github_raw 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20250516140014_create_neighborhood_posts.rb: -------------------------------------------------------------------------------- 1 | class CreateNeighborhoodPosts < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :neighborhood_posts do |t| 4 | t.string :airtable_id, null: false 5 | t.index :airtable_id, unique: true 6 | t.jsonb :airtable_fields 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20250516142026_create_neighborhood_apps.rb: -------------------------------------------------------------------------------- 1 | class CreateNeighborhoodApps < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :neighborhood_apps do |t| 4 | t.string :airtable_id, null: false 5 | t.index :airtable_id, unique: true 6 | t.jsonb :airtable_fields 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20250516142043_create_neighborhood_projects.rb: -------------------------------------------------------------------------------- 1 | class CreateNeighborhoodProjects < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :neighborhood_projects do |t| 4 | t.string :airtable_id, null: false 5 | t.index :airtable_id, unique: true 6 | t.jsonb :airtable_fields 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20250522062125_create_neighborhood_ysws_submissions.rb: -------------------------------------------------------------------------------- 1 | class CreateNeighborhoodYswsSubmissions < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :neighborhood_ysws_submissions do |t| 4 | t.string :airtable_id, null: false 5 | t.index :airtable_id, unique: true 6 | t.jsonb :airtable_fields 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20250527052632_add_indexes_for_active_projects_query.rb: -------------------------------------------------------------------------------- 1 | class AddIndexesForActiveProjectsQuery < ActiveRecord::Migration[8.0] 2 | def change 3 | add_index :heartbeats, [ :source_type, :time, :user_id, :project ], 4 | name: 'index_heartbeats_on_source_type_time_user_project' 5 | 6 | add_index :project_repo_mappings, :project_name 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20250530015016_create_repositories.rb: -------------------------------------------------------------------------------- 1 | class CreateRepositories < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :repositories do |t| 4 | t.string :url 5 | t.string :host 6 | t.string :owner 7 | t.string :name 8 | t.integer :stars 9 | t.text :description 10 | t.string :language 11 | t.text :languages 12 | t.integer :commit_count 13 | t.datetime :last_commit_at 14 | t.datetime :last_synced_at 15 | 16 | t.timestamps 17 | end 18 | add_index :repositories, :url, unique: true 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/migrate/20250530015024_update_project_repo_mappings_to_use_repository.rb: -------------------------------------------------------------------------------- 1 | class UpdateProjectRepoMappingsToUseRepository < ActiveRecord::Migration[8.0] 2 | def change 3 | # Add repository reference 4 | add_reference :project_repo_mappings, :repository, null: true, foreign_key: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250530015530_add_repository_to_commits.rb: -------------------------------------------------------------------------------- 1 | class AddRepositoryToCommits < ActiveRecord::Migration[8.0] 2 | def change 3 | add_reference :commits, :repository, null: true, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250530135145_add_homepage_to_repositories.rb: -------------------------------------------------------------------------------- 1 | class AddHomepageToRepositories < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :repositories, :homepage, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /docker-compose.pgbouncer.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | # This is run on coolify under a seperate service so we don't restart it on 3 | # every git push! 4 | services: 5 | pgbouncer: 6 | image: edoburu/pgbouncer:v1.24.0-p1 7 | environment: 8 | - DB_USER=${DB_USER:-postgres} 9 | - DB_PASSWORD=${DB_PASSWORD} 10 | - DB_HOST=${DB_HOST} 11 | - DB_PORT=${DB_PORT:-6382} 12 | - DB_NAME=${DB_NAME:-postgres} 13 | - POOL_MODE=transaction 14 | - MAX_CLIENT_CONN=1000 15 | - DEFAULT_POOL_SIZE=20 16 | - MIN_POOL_SIZE=5 17 | - AUTH_TYPE=scram-sha-256 18 | - ADMIN_USERS=${DB_USER:-postgres} 19 | ports: 20 | - "6432:6432" 21 | healthcheck: 22 | test: ["CMD", "pg_isready", "-h", "localhost"] 23 | interval: 10s 24 | timeout: 5s 25 | retries: 5 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile.dev 6 | ports: 7 | - "3000:3000" 8 | volumes: 9 | - .:/app 10 | - bundle_cache:/usr/local/bundle 11 | environment: 12 | - RAILS_ENV=development 13 | - DATABASE_URL=postgres://postgres:secureorpheus123@db:5432/app_development 14 | - POSTGRES_HOST=db 15 | - POSTGRES_USER=postgres 16 | - POSTGRES_PASSWORD=secureorpheus123 17 | depends_on: 18 | - db 19 | 20 | db: 21 | image: postgres:16 22 | volumes: 23 | - harbor_postgres_data:/var/lib/postgresql/data 24 | environment: 25 | - POSTGRES_PASSWORD=secureorpheus123 26 | - POSTGRES_USER=postgres 27 | - POSTGRES_DB=app_development 28 | ports: 29 | - "5432:5432" 30 | 31 | volumes: 32 | harbor_postgres_data: 33 | bundle_cache: -------------------------------------------------------------------------------- /docs/editors/aptana.md: -------------------------------------------------------------------------------- 1 | # Aptana Setup Guide 2 | 3 | ![Aptana](/images/editor-icons/aptana-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Aptana with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Aptana Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Aptana page](https://wakatime.com/aptana). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Aptana after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/canva.md: -------------------------------------------------------------------------------- 1 | # Canva Setup Guide 2 | 3 | ![Canva](/images/editor-icons/canva-128.png) 4 | 5 | Follow these steps to start tracking your design work in Canva with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Canva Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Canva page](https://wakatime.com/canva). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Canva after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your activity time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy designing! 30 | -------------------------------------------------------------------------------- /docs/editors/clion.md: -------------------------------------------------------------------------------- 1 | # CLion Setup Guide 2 | 3 | ![CLion](/images/editor-icons/clion-128.png) 4 | 5 | Follow these steps to start tracking your coding time in CLion with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install CLion Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime CLion page](https://wakatime.com/clion). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting CLion after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/cloud9.md: -------------------------------------------------------------------------------- 1 | # Cloud9 Setup Guide 2 | 3 | ![Cloud9](/images/editor-icons/cloud9-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Cloud9 with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Cloud9 Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Cloud9 page](https://wakatime.com/cloud9). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Cloud9 after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/coda.md: -------------------------------------------------------------------------------- 1 | # Coda Setup Guide 2 | 3 | ![Coda](/images/editor-icons/coda-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Coda with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Coda Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Coda page](https://wakatime.com/coda). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Coda after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/delphi.md: -------------------------------------------------------------------------------- 1 | # Delphi Setup Guide 2 | 3 | ![Delphi](/images/editor-icons/delphi-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Delphi with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Delphi Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Delphi page](https://wakatime.com/delphi). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Delphi after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/emacs.md: -------------------------------------------------------------------------------- 1 | # Emacs Setup Guide 2 | 3 | ![Emacs](/images/editor-icons/emacs-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Emacs with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Emacs Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Emacs page](https://wakatime.com/emacs). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Emacs after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/eric.md: -------------------------------------------------------------------------------- 1 | # Eric Setup Guide 2 | 3 | ![Eric](/images/editor-icons/eric-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Eric with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Eric Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Eric page](https://wakatime.com/eric). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Eric after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/excel.md: -------------------------------------------------------------------------------- 1 | # Excel Setup Guide 2 | 3 | ![Excel](/images/editor-icons/excel-128.png) 4 | 5 | Follow these steps to start tracking your document work in Excel with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Excel Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Excel page](https://wakatime.com/excel). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Excel after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your activity time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy productivity! 30 | -------------------------------------------------------------------------------- /docs/editors/figma.md: -------------------------------------------------------------------------------- 1 | # Figma Setup Guide 2 | 3 | ![Figma](/images/editor-icons/figma-128.png) 4 | 5 | Follow these steps to start tracking your design work in Figma with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Figma Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Figma page](https://wakatime.com/figma). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Figma after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your activity time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy designing! 30 | -------------------------------------------------------------------------------- /docs/editors/gedit.md: -------------------------------------------------------------------------------- 1 | # Gedit Setup Guide 2 | 3 | ![Gedit](/images/editor-icons/gedit-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Gedit with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Gedit Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Gedit page](https://wakatime.com/gedit). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Gedit after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/goland.md: -------------------------------------------------------------------------------- 1 | # GoLand Setup Guide 2 | 3 | ![GoLand](/images/editor-icons/goland-128.png) 4 | 5 | Follow these steps to start tracking your coding time in GoLand with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install GoLand Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime GoLand page](https://wakatime.com/goland). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting GoLand after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/kate.md: -------------------------------------------------------------------------------- 1 | # Kate Setup Guide 2 | 3 | ![Kate](/images/editor-icons/kate-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Kate with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Kate Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Kate page](https://wakatime.com/kate). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Kate after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/komodo.md: -------------------------------------------------------------------------------- 1 | # Komodo Setup Guide 2 | 3 | ![Komodo](/images/editor-icons/komodo-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Komodo with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Komodo Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Komodo page](https://wakatime.com/komodo). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Komodo after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/micro.md: -------------------------------------------------------------------------------- 1 | # Micro Setup Guide 2 | 3 | ![Micro](/images/editor-icons/micro-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Micro with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Micro Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Micro page](https://wakatime.com/micro). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Micro after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/mps.md: -------------------------------------------------------------------------------- 1 | # MPS Setup Guide 2 | 3 | ![MPS](/images/editor-icons/mps-128.png) 4 | 5 | Follow these steps to start tracking your coding time in MPS with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install MPS Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime MPS page](https://wakatime.com/mps). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting MPS after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/neovim.md: -------------------------------------------------------------------------------- 1 | # Neovim Setup Guide 2 | 3 | ![Neovim](/images/editor-icons/neovim-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Neovim with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Neovim Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Neovim page](https://wakatime.com/neovim). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Neovim after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/nova.md: -------------------------------------------------------------------------------- 1 | # Nova Setup Guide 2 | 3 | ![Nova](/images/editor-icons/nova-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Nova with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Nova Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Nova page](https://wakatime.com/nova). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Nova after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/rider.md: -------------------------------------------------------------------------------- 1 | # Rider Setup Guide 2 | 3 | ![Rider](/images/editor-icons/rider-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Rider with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Rider Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Rider page](https://wakatime.com/rider). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Rider after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/trae.md: -------------------------------------------------------------------------------- 1 | # Trae Setup Guide 2 | 3 | ![Trae](/images/editor-icons/trae-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Trae with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Trae Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Trae page](https://wakatime.com/trae). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Trae after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/wing.md: -------------------------------------------------------------------------------- 1 | # Wing Setup Guide 2 | 3 | ![Wing](/images/editor-icons/wing-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Wing with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Wing Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Wing page](https://wakatime.com/wing). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Wing after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/word.md: -------------------------------------------------------------------------------- 1 | # Word Setup Guide 2 | 3 | ![Word](/images/editor-icons/word-128.png) 4 | 5 | Follow these steps to start tracking your document work in Word with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Word Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Word page](https://wakatime.com/word). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Word after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your activity time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy productivity! 30 | -------------------------------------------------------------------------------- /docs/editors/xcode.md: -------------------------------------------------------------------------------- 1 | # Xcode Setup Guide 2 | 3 | ![Xcode](/images/editor-icons/xcode-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Xcode with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Xcode Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Xcode page](https://wakatime.com/xcode). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Xcode after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /docs/editors/zed.md: -------------------------------------------------------------------------------- 1 | # Zed Setup Guide 2 | 3 | ![Zed](/images/editor-icons/zed-128.png) 4 | 5 | Follow these steps to start tracking your coding time in Zed with Hackatime. 6 | 7 | ## Step 1: Log into Hackatime 8 | 9 | Make sure you have a [Hackatime account](https://hackatime.hackclub.com) and are logged in. 10 | 11 | ## Step 2: Run the Setup Script 12 | 13 | Visit the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) to automatically configure your API key and endpoint. This ensures everything works perfectly with Hackatime. 14 | 15 | ## Step 3: Install Zed Plugin 16 | 17 | Follow the detailed plugin installation instructions on the [WakaTime Zed page](https://wakatime.com/zed). 18 | 19 | The WakaTime plugin will automatically use your Hackatime configuration after running the setup script. 20 | 21 | ## Troubleshooting 22 | 23 | - **Not seeing your time?** Make sure you completed the [setup page](https://hackatime.hackclub.com/my/wakatime_setup) first 24 | - **Plugin not working?** Try restarting Zed after installation 25 | - **Still stuck?** Ask for help in [Hack Club Slack](https://hackclub.slack.com) (#hackatime-dev channel) 26 | 27 | ## Next Steps 28 | 29 | Once configured, your coding time will automatically appear on your [Hackatime dashboard](https://hackatime.hackclub.com). Happy coding! 30 | -------------------------------------------------------------------------------- /entrypoint.dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Remove a potentially pre-existing server.pid for Rails 5 | rm -f /app/tmp/pids/server.pid 6 | 7 | # Then exec the container's main process (what's set as CMD in the Dockerfile) 8 | exec "$@" -------------------------------------------------------------------------------- /lib/git_remote.rb: -------------------------------------------------------------------------------- 1 | require "open3" 2 | 3 | class GitRemote 4 | def self.check_remote_exists(repo_url) 5 | # only run check if git is installed and in path 6 | return true unless system("git --version") 7 | 8 | safe_repo_url = URI.parse(repo_url).to_s.gsub(" ", "").gsub("'", "") 9 | Open3.capture2e("git", "ls-remote", safe_repo_url).last.success? 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/slack_channel.rb: -------------------------------------------------------------------------------- 1 | class SlackChannel 2 | def self.find_by_id(id, force_refresh: false) 3 | cached_name = Rails.cache.fetch("slack_channel_#{id}", expires_in: 1.week, force: force_refresh) do 4 | response = HTTP.headers(Authorization: "Bearer #{ENV.fetch("SAILORS_LOG_SLACK_BOT_OAUTH_TOKEN")}").get("https://slack.com/api/conversations.info?channel=#{id}") 5 | data = JSON.parse(response.body) 6 | 7 | data.dig("channel", "name") 8 | end 9 | 10 | cached_name 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/slack_neighborhood.rb: -------------------------------------------------------------------------------- 1 | class SlackNeighborhood 2 | def self.find_by_id(id) 3 | # Get the neighborhood data from the cache or fetch it from the API 4 | key = "slack_neighborhood_#{id}" 5 | 6 | specific_neighborhood = Rails.cache.fetch(key, expires_in: 10.days) do 7 | neighborhood_data = Rails.cache.fetch("slack_neighborhood_list", expires_in: 3.days) do 8 | response = HTTP.get("https://skksk8sos4g4c0kw4cw0ks80.a.selfhosted.hackclub.com/personal") 9 | JSON.parse(response.body) 10 | end 11 | 12 | neighborhood_data.find { |neighborhood| neighborhood["channelManagers"].include?(id) } 13 | end 14 | 15 | Rails.cache.delete(key) if specific_neighborhood.nil? 16 | 17 | specific_neighborhood 18 | rescue StandardError => e 19 | Rails.logger.error("Error in SlackNeighborhood.find_by_id: #{e.message}") 20 | nil 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/slack_username.rb: -------------------------------------------------------------------------------- 1 | class SlackUsername 2 | def self.find_by_uid(uid) 3 | key = "slack_username_#{uid}" 4 | 5 | cached_name = Rails.cache.fetch(key, expires_in: 1.day) do 6 | response = HTTP.headers(Authorization: "Bearer #{ENV.fetch("SAILORS_LOG_SLACK_BOT_OAUTH_TOKEN")}").get("https://slack.com/api/users.info?user=#{uid}") 7 | data = JSON.parse(response.body) 8 | 9 | name = data.dig("user", "profile", "display_name") 10 | name ||= data.dig("user", "profile", "display_name_normalized") 11 | 12 | name 13 | end 14 | 15 | Rails.cache.delete(key) unless cached_name.present? 16 | 17 | cached_name 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/log/.keep -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/icon.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/editor-icons/android-studio-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/android-studio-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/appcode-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/appcode-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/aptana-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/aptana-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/arduino-ide-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/arduino-ide-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/azure-data-studio-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/azure-data-studio-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/blender-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/blender-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/brackets-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/brackets-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/brave-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/brave-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/c++-builder-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/c++-builder-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/canva-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/canva-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/chrome-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/chrome-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/clion-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/clion-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/cloud9-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/cloud9-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/coda-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/coda-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/codetasty-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/codetasty-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/cursor-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/cursor-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/datagrip-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/datagrip-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/dataspell-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/dataspell-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/dbeaver-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/dbeaver-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/delphi-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/delphi-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/discord-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/discord-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/eclipse-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/eclipse-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/edge-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/edge-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/emacs-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/emacs-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/eric-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/eric-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/excel-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/excel-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/figma-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/figma-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/firefox-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/firefox-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/gedit-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/gedit-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/godot-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/godot-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/goland-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/goland-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/hbuilder-x-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/hbuilder-x-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/ida-pro-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/ida-pro-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/intellij-idea-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/intellij-idea-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/jupyter-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/jupyter-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/kakoune-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/kakoune-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/kate-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/kate-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/komodo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/komodo-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/micro-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/micro-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/mps-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/mps-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/neovim-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/neovim-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/netbeans-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/netbeans-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/notepad++-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/notepad++-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/nova-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/nova-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/obsidian-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/obsidian-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/oxygen-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/oxygen-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/phpstorm-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/phpstorm-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/postman-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/postman-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/powerpoint-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/powerpoint-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/processing-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/processing-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/pulsar-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/pulsar-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/pycharm-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/pycharm-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/reclassex-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/reclassex-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/rider-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/rider-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/roblox-studio-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/roblox-studio-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/rubymine-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/rubymine-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/rustrover-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/rustrover-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/safari-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/safari-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/siyuan-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/siyuan-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/sketch-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/sketch-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/slickedit-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/slickedit-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/sql-server-management-studio-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/sql-server-management-studio-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/sublime-text-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/sublime-text-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/terminal-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/terminal-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/texstudio-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/texstudio-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/textmate-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/textmate-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/trae-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/trae-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/unity-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/unity-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/vim-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/vim-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/visual-studio-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/visual-studio-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/vs-code-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/vs-code-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/webstorm-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/webstorm-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/windsurf-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/windsurf-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/wing-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/wing-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/word-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/word-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/xcode-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/xcode-128.png -------------------------------------------------------------------------------- /public/images/editor-icons/zed-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/public/images/editor-icons/zed-128.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | # Important pages for crawling 5 | Allow: /docs 6 | Allow: /docs/* 7 | Allow: /leaderboard 8 | 9 | # Disallow private/internal pages 10 | Disallow: /my/ 11 | Disallow: /admin/ 12 | Disallow: /api/ 13 | Disallow: /auth/ 14 | 15 | # Sitemap 16 | Sitemap: https://hackatime.hackclub.com/sitemap.xml 17 | -------------------------------------------------------------------------------- /public/success.txt: -------------------------------------------------------------------------------- 1 | _____ _ 2 | / ____| | | 3 | | (___ _ _ ___ ___ ___ ___ ___| | 4 | \___ \| | | |/ __/ __/ _ \ __/ __| | 5 | ____) | |_| | (__ (__ __\__ \__ \_| 6 | |_____/ \__,_|\___\___\___|___/___(_) 7 | -------------------------------------------------------------------------------- /script/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/script/.keep -------------------------------------------------------------------------------- /slack_manifest_harbor.yml: -------------------------------------------------------------------------------- 1 | # from https://app.slack.com/app-settings/T0266FRGM/A08EJ0W7N82/app-manifest 2 | display_information: 3 | name: Hackatime 4 | description: Track your time on Hackatime! 5 | background_color: "#de9c0d" 6 | features: 7 | bot_user: 8 | display_name: hackatime 9 | always_online: false 10 | slash_commands: 11 | - command: /timedump 12 | url: https://timedump.hackclub.com/timedump/slack/commands 13 | description: check your time dump! 14 | should_escape: false 15 | oauth_config: 16 | redirect_urls: 17 | - http://localhost:3000/auth/slack/callback 18 | - https://hackatime.hackclub.com/auth/slack/callback 19 | - https://timedump.hackclub.com/auth/slack/callback 20 | - https://harbor.hackclub.com/auth/slack/callback 21 | scopes: 22 | user: 23 | - users.profile:read 24 | - users.profile:write 25 | - users:read 26 | - users:read.email 27 | settings: 28 | org_deploy_enabled: false 29 | socket_mode_enabled: false 30 | token_rotation_enabled: false 31 | -------------------------------------------------------------------------------- /slack_manifest_sailors_log.yml: -------------------------------------------------------------------------------- 1 | # from https://app.slack.com/app-settings/T0266FRGM/A08351N4KM1/app-manifest 2 | display_information: 3 | name: Sailor's Log 4 | description: Log your coding hours 5 | background_color: "#426ff5" 6 | features: 7 | bot_user: 8 | display_name: Sailor's Log 9 | always_online: false 10 | slash_commands: 11 | - command: /sailorslog 12 | url: https://timedump.hackclub.com/sailors_log/slack/commands 13 | description: Celebrate your coding hours 14 | usage_hint: "[on|off|leaderboard]" 15 | should_escape: true 16 | oauth_config: 17 | scopes: 18 | bot: 19 | - chat:write 20 | - chat:write.public 21 | - commands 22 | - users:read 23 | - channels:read 24 | - groups:read 25 | - mpim:read 26 | - im:read 27 | settings: 28 | org_deploy_enabled: false 29 | socket_mode_enabled: false 30 | token_rotation_enabled: false 31 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/storage/.keep -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ] 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/test/controllers/.keep -------------------------------------------------------------------------------- /test/fixtures/api_keys.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | user: one 5 | name: MyText 6 | token: MyText 7 | 8 | two: 9 | user: two 10 | name: MyText 11 | token: MyText 12 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/fixtures/heartbeats.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | user: one 5 | entity: MyText 6 | type: MyText 7 | category: MyString 8 | time: 2025-03-03 18:08:42 9 | project: MyString 10 | project_root_count: 1 11 | branch: MyString 12 | language: MyString 13 | dependencies: MyString 14 | lines: 1 15 | line_additions: 1 16 | line_deletions: 1 17 | lineno: 1 18 | cursorpos: 1 19 | is_write: false 20 | 21 | two: 22 | user: two 23 | entity: MyText 24 | type: MyText 25 | category: MyString 26 | time: 2025-03-03 18:08:42 27 | project: MyString 28 | project_root_count: 1 29 | branch: MyString 30 | language: MyString 31 | dependencies: MyString 32 | lines: 1 33 | line_additions: 1 34 | line_deletions: 1 35 | lineno: 1 36 | cursorpos: 1 37 | is_write: false 38 | -------------------------------------------------------------------------------- /test/fixtures/mailing_addresses.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | user: one 5 | first_name: MyString 6 | last_name: MyString 7 | zip_code: MyString 8 | line_1: MyString 9 | line_2: MyString 10 | city: MyString 11 | state: MyString 12 | country: MyString 13 | 14 | two: 15 | user: two 16 | first_name: MyString 17 | last_name: MyString 18 | zip_code: MyString 19 | line_1: MyString 20 | line_2: MyString 21 | city: MyString 22 | state: MyString 23 | country: MyString 24 | -------------------------------------------------------------------------------- /test/fixtures/project_repo_mappings.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | user: one 5 | project_name: MyString 6 | repo_url: MyString 7 | 8 | two: 9 | user: two 10 | project_name: MyString 11 | repo_url: MyString 12 | -------------------------------------------------------------------------------- /test/fixtures/repositories.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | url: MyString 5 | host: MyString 6 | owner: MyString 7 | name: MyString 8 | stars: 1 9 | description: MyText 10 | language: MyString 11 | languages: MyText 12 | commit_count: 1 13 | last_commit_at: 2025-05-30 01:50:16 14 | last_synced_at: 2025-05-30 01:50:16 15 | 16 | two: 17 | url: MyString 18 | host: MyString 19 | owner: MyString 20 | name: MyString 21 | stars: 1 22 | description: MyText 23 | language: MyString 24 | languages: MyText 25 | commit_count: 1 26 | last_commit_at: 2025-05-30 01:50:16 27 | last_synced_at: 2025-05-30 01:50:16 28 | -------------------------------------------------------------------------------- /test/fixtures/sailors_log_leaderboards.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the "{}" from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/sailors_log_notification_preferences.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | slack_uid: MyString 5 | enabled: false 6 | slack_channel_id: MyString 7 | 8 | two: 9 | slack_uid: MyString 10 | enabled: false 11 | slack_channel_id: MyString 12 | -------------------------------------------------------------------------------- /test/fixtures/sailors_log_slack_notifications.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | slack_uid: MyString 5 | slack_channel_id: MyString 6 | project_name: MyString 7 | 8 | two: 9 | slack_uid: MyString 10 | slack_channel_id: MyString 11 | project_name: MyString 12 | -------------------------------------------------------------------------------- /test/fixtures/sailors_logs.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | slack_uid: MyString 5 | 6 | two: 7 | slack_uid: MyString 8 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/test/models/.keep -------------------------------------------------------------------------------- /test/models/api_key_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApiKeyTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/heartbeat_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class HeartbeatTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/mailing_address_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MailingAddressTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/project_repo_mapping_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ProjectRepoMappingTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/repository_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RepositoryTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/sailors_log_leaderboard_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SailorsLogLeaderboardTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/sailors_log_notification_preference_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SailorsLogNotificationPreferenceTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/sailors_log_slack_notification_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SailorsLogSlackNotificationTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/sailors_log_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SailorsLogTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/test/system/.keep -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | module ActiveSupport 6 | class TestCase 7 | # Run tests in parallel with specified workers 8 | parallelize(workers: :number_of_processors) 9 | 10 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 11 | fixtures :all 12 | 13 | # Add more helper methods to be used by all tests here... 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/tmp/.keep -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/tmp/pids/.keep -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/tmp/storage/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/vendor/.keep -------------------------------------------------------------------------------- /vendor/javascript/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackatime/38c5d133fe126cf64347a1ff1aeac400e86d7d82/vendor/javascript/.keep --------------------------------------------------------------------------------