├── .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 |
2 |
3 |
4 | <%= link_to '<– back to app', '/' %> – you're doing a great job ^w^
5 |
6 |
7 | <% last_updated_at = Time.current.utc.iso8601 %>
8 | <%= t(".last_updated") %> <%= last_updated_at %>
9 |
10 |
11 |
--------------------------------------------------------------------------------
/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 |
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 |
▼ <%= label %>
3 |
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 |
9 | >
10 | Your browser does not support the video tag.
11 |
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 |
5 | <% users.each do |user| %>
6 | <%= render "shared/user_mention", user: user, show: [:slack] %>
7 | <% if active_projects[user.id].present? %>
8 |
9 | working on <%= link_to active_projects[user.id].project_name, active_projects[user.id].repo_url, target: "_blank" %>
10 | <%= link_to "🌌", visualize_git_url(active_projects[user.id].repo_url), target: "_blank" %>
11 |
12 | <% end %>
13 | <% if user == current_user && user.github_username.blank? %>
14 |
15 | <%= link_to "Link active projects", my_settings_path(anchor: "user_github_account"), target: "_blank" %>
16 |
17 | <% end %>
18 | <% end %>
19 |
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 |
8 |
9 | <%= link_to "vs-code (visual studio code)", my_wakatime_setup_step_3_path(anchor: "vs-code") %>
10 |
11 |
12 | <%= link_to "kicad", my_wakatime_setup_step_3_path(anchor: "kicad") %>
13 |
14 |
15 | <%= link_to "vim", my_wakatime_setup_step_3_path(anchor: "vim") %>
16 |
17 |
18 | <%= link_to "emacs", my_wakatime_setup_step_3_path %>
19 |
20 |
21 | <%= link_to "other", my_wakatime_setup_step_3_path %>
22 |
23 |
--------------------------------------------------------------------------------
/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 | Set up another editor
15 | <% end %>
16 |
17 | <%= link_to root_path do %>
18 | Done
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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
--------------------------------------------------------------------------------