mirror of
https://github.com/SrIzan10/hc-harbor.git
synced 2026-05-01 10:45:21 +00:00
BIN
app/assets/fonts/Bold.woff
Normal file
BIN
app/assets/fonts/Bold.woff
Normal file
Binary file not shown.
BIN
app/assets/fonts/Bold.woff2
Normal file
BIN
app/assets/fonts/Bold.woff2
Normal file
Binary file not shown.
BIN
app/assets/fonts/Italic.woff
Normal file
BIN
app/assets/fonts/Italic.woff
Normal file
Binary file not shown.
BIN
app/assets/fonts/Italic.woff2
Normal file
BIN
app/assets/fonts/Italic.woff2
Normal file
Binary file not shown.
BIN
app/assets/fonts/Regular.woff
Normal file
BIN
app/assets/fonts/Regular.woff
Normal file
Binary file not shown.
BIN
app/assets/fonts/Regular.woff2
Normal file
BIN
app/assets/fonts/Regular.woff2
Normal file
Binary file not shown.
@@ -9,9 +9,50 @@
|
||||
* Consider organizing styles into separate files for maintainability.
|
||||
*/
|
||||
|
||||
/* Phantom Sans Font Faces */
|
||||
@font-face {
|
||||
font-family: 'Phantom Sans';
|
||||
src: url('/assets/Regular.woff2') format('woff2'),
|
||||
url('/assets/Regular.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Phantom Sans';
|
||||
src: url('/assets/Italic.woff2') format('woff2'),
|
||||
url('/assets/Italic.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Phantom Sans';
|
||||
src: url('/assets/Bold.woff2') format('woff2'),
|
||||
url('/assets/Bold.woff') format('woff');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@import "https://uchu.style/color.css";
|
||||
@import "settings.css";
|
||||
|
||||
body, html {
|
||||
font-family: 'Phantom Sans', 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
code, pre, kbd, samp {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
}
|
||||
|
||||
|
||||
/* Force dark mode for all elements */
|
||||
*, *::before, *::after {
|
||||
color-scheme: dark;
|
||||
|
||||
@@ -3,6 +3,7 @@ class ApplicationController < ActionController::Base
|
||||
before_action :honeybadger_context, if: :current_user
|
||||
before_action :initialize_cache_counters
|
||||
before_action :try_rack_mini_profiler_enable
|
||||
before_action :track_request
|
||||
after_action :track_action
|
||||
|
||||
around_action :switch_time_zone, if: :current_user
|
||||
@@ -30,6 +31,10 @@ class ApplicationController < ActionController::Base
|
||||
ahoy.track "Ran action", request.path_parameters
|
||||
end
|
||||
|
||||
def track_request
|
||||
RequestCounter.increment
|
||||
end
|
||||
|
||||
def try_rack_mini_profiler_enable
|
||||
if current_user && current_user.is_admin?
|
||||
Rack::MiniProfiler.authorize_request
|
||||
|
||||
@@ -92,6 +92,10 @@ class StaticPagesController < ApplicationController
|
||||
@leaderboard = LeaderboardGenerator.generate_timezone_offset_leaderboard(
|
||||
Date.current, current_user.timezone_utc_offset, :daily
|
||||
)
|
||||
|
||||
if @leaderboard&.entries&.empty?
|
||||
Rails.logger.warn "[MiniLeaderboard] Regional leaderboard empty for offset #{current_user.timezone_utc_offset}"
|
||||
end
|
||||
else
|
||||
# Use global leaderboard
|
||||
@leaderboard = Leaderboard.where.associated(:entries)
|
||||
@@ -102,6 +106,16 @@ class StaticPagesController < ApplicationController
|
||||
.first
|
||||
end
|
||||
|
||||
if @leaderboard.nil? || @leaderboard.entries.empty?
|
||||
Rails.logger.info "[MiniLeaderboard] Falling back to global leaderboard"
|
||||
@leaderboard = Leaderboard.where.associated(:entries)
|
||||
.where(start_date: Date.current)
|
||||
.where(deleted_at: nil)
|
||||
.where(period_type: :daily)
|
||||
.distinct
|
||||
.first
|
||||
end
|
||||
|
||||
@active_projects = Cache::ActiveProjectsJob.perform_now
|
||||
|
||||
render partial: "leaderboards/mini_leaderboard", locals: {
|
||||
|
||||
@@ -5,6 +5,11 @@ module ApplicationHelper
|
||||
{ hits: hits, misses: misses }
|
||||
end
|
||||
|
||||
def requests_per_second
|
||||
rps = RequestCounter.per_second
|
||||
rps == :high_load ? "lots of req/sec" : "#{rps} req/sec"
|
||||
end
|
||||
|
||||
def admin_tool(class_name = "", element = "div", **options, &block)
|
||||
return unless current_user&.is_admin?
|
||||
concat content_tag(element, class: "admin-tool #{class_name}", **options, &block)
|
||||
|
||||
20
app/jobs/warm_mini_leaderboard_cache_job.rb
Normal file
20
app/jobs/warm_mini_leaderboard_cache_job.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class WarmMiniLeaderboardCacheJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
offsets = [ -8, -7, -6, -5, -4, -3, 0, 1, 2, 8, 9, 10, 11, 12 ]
|
||||
|
||||
offsets.each do |offset|
|
||||
begin
|
||||
LeaderboardGenerator.generate_timezone_offset_leaderboard(
|
||||
Date.current,
|
||||
offset,
|
||||
:daily
|
||||
)
|
||||
Rails.logger.info "Warmed mini leaderboard cache for UTC#{offset >= 0 ? '+' : ''}#{offset}"
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to warm cache for UTC#{offset >= 0 ? '+' : ''}#{offset}: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
57
app/lib/request_counter.rb
Normal file
57
app/lib/request_counter.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
class RequestCounter
|
||||
WINDOW_SIZE = 10 # seconds - shorter window for more responsive rates
|
||||
HIGH_LOAD_THRESHOLD = 500 # req/sec to disable tracking
|
||||
CIRCUIT_BREAKER_DURATION = 30 # seconds to stay disabled
|
||||
|
||||
@buckets = {}
|
||||
@disabled_until = nil
|
||||
|
||||
class << self
|
||||
def increment
|
||||
return if disabled?
|
||||
|
||||
current_time = Time.current.to_i
|
||||
@buckets[current_time] = (@buckets[current_time] || 0) + 1
|
||||
|
||||
# Check if we should disable due to high load
|
||||
check_circuit_breaker(current_time)
|
||||
|
||||
# Periodically clean old buckets (1% chance)
|
||||
cleanup if rand(100) == 0
|
||||
end
|
||||
|
||||
def per_second
|
||||
return :high_load if disabled?
|
||||
|
||||
current_time = Time.current.to_i
|
||||
cutoff = current_time - WINDOW_SIZE
|
||||
|
||||
total = @buckets.select { |timestamp, _| timestamp >= cutoff }.values.sum
|
||||
(total.to_f / WINDOW_SIZE).round(2)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def disabled?
|
||||
@disabled_until && Time.current.to_i < @disabled_until
|
||||
end
|
||||
|
||||
def check_circuit_breaker(current_time)
|
||||
# Check last 5 seconds for high load
|
||||
recent_total = @buckets.select { |ts, _| ts >= current_time - 5 }.values.sum
|
||||
|
||||
if recent_total > HIGH_LOAD_THRESHOLD * 5 # 5 seconds worth
|
||||
@disabled_until = current_time + CIRCUIT_BREAKER_DURATION
|
||||
@buckets.clear # Clear to reduce memory
|
||||
end
|
||||
end
|
||||
|
||||
def cleanup
|
||||
return if disabled? # Skip cleanup when disabled
|
||||
|
||||
current_time = Time.current.to_i
|
||||
cutoff = current_time - WINDOW_SIZE - 10 # extra buffer
|
||||
@buckets.reject! { |timestamp, _| timestamp < cutoff }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -98,9 +98,19 @@ module Heartbeatable
|
||||
end
|
||||
|
||||
def daily_streaks_for_users(user_ids, start_date: 31.days.ago)
|
||||
# First get the raw durations using window function
|
||||
return {} if user_ids.empty?
|
||||
start_date = [ start_date, 30.days.ago ].max
|
||||
keys = user_ids.map { |id| "user_streak_#{id}" }
|
||||
streak_cache = Rails.cache.read_multi(*keys)
|
||||
|
||||
uncached_users = user_ids.select { |id| streak_cache["user_streak_#{id}"].nil? }
|
||||
|
||||
if uncached_users.empty?
|
||||
return user_ids.index_with { |id| streak_cache["user_streak_#{id}"] || 0 }
|
||||
end
|
||||
|
||||
raw_durations = joins(:user)
|
||||
.where(user_id: user_ids)
|
||||
.where(user_id: uncached_users)
|
||||
.coding_only
|
||||
.with_valid_timestamps
|
||||
.where(time: start_date..Time.current)
|
||||
@@ -128,8 +138,7 @@ module Heartbeatable
|
||||
}
|
||||
end
|
||||
|
||||
# Initialize the result hash with zeros for all users
|
||||
result = user_ids.index_with { 0 }
|
||||
result = user_ids.index_with { |id| streak_cache["user_streak_#{id}"] || 0 }
|
||||
|
||||
# Then calculate streaks for each user
|
||||
daily_durations.each do |user_id, data|
|
||||
@@ -158,6 +167,9 @@ module Heartbeatable
|
||||
end
|
||||
|
||||
result[user_id] = streak
|
||||
|
||||
# Cache the streak for 1 hour
|
||||
Rails.cache.write("user_streak_#{user_id}", streak, expires_in: 1.hour)
|
||||
end
|
||||
|
||||
result
|
||||
|
||||
@@ -11,14 +11,24 @@ class LeaderboardGenerator
|
||||
|
||||
def generate_timezone_offset_leaderboard(date, utc_offset, period_type = :daily)
|
||||
date = Date.current if date.blank?
|
||||
users = User.users_in_timezone_offset(utc_offset).not_convicted
|
||||
generate_leaderboard_for_users(users, date, "UTC#{utc_offset >= 0 ? '+' : ''}#{utc_offset}", period_type)
|
||||
|
||||
cache_key = "timezone_leaderboard_#{utc_offset}_#{date}_#{period_type}"
|
||||
|
||||
Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
|
||||
users = User.users_in_timezone_offset(utc_offset).not_convicted
|
||||
generate_leaderboard_for_users(users, date, "UTC#{utc_offset >= 0 ? '+' : ''}#{utc_offset}", period_type)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_timezone_leaderboard(date, timezone, period_type = :daily)
|
||||
date = Date.current if date.blank?
|
||||
users = User.users_in_timezone(timezone).not_convicted
|
||||
generate_leaderboard_for_users(users, date, timezone, period_type)
|
||||
|
||||
cache_key = "timezone_leaderboard_#{timezone.gsub('/', '_')}_#{date}_#{period_type}"
|
||||
|
||||
Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
|
||||
users = User.users_in_timezone(timezone).not_convicted
|
||||
generate_leaderboard_for_users(users, date, timezone, period_type)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -34,10 +44,13 @@ class LeaderboardGenerator
|
||||
finished_generating_at: Time.current
|
||||
)
|
||||
|
||||
# Get user IDs
|
||||
# Get user IDs and preload users hash for faster lookups
|
||||
user_ids = users.pluck(:id)
|
||||
return leaderboard if user_ids.empty?
|
||||
|
||||
# Preload users into a hash for O(1) lookups
|
||||
users_hash = users.index_by(&:id)
|
||||
|
||||
# Calculate heartbeats for the date range in UTC
|
||||
date_range = case period_type
|
||||
when :weekly
|
||||
@@ -48,7 +61,7 @@ class LeaderboardGenerator
|
||||
date.all_day
|
||||
end
|
||||
|
||||
# Get heartbeats for these users
|
||||
# Get heartbeats for these users - limit to reduce query size
|
||||
heartbeats = Heartbeat.where(user_id: user_ids, time: date_range)
|
||||
.coding_only
|
||||
.with_valid_timestamps
|
||||
@@ -59,8 +72,10 @@ class LeaderboardGenerator
|
||||
user_totals = heartbeats.group(:user_id).duration_seconds
|
||||
user_totals = user_totals.filter { |_, total_seconds| total_seconds > 60 }
|
||||
|
||||
# Get streaks for all users at once
|
||||
streaks = Heartbeat.daily_streaks_for_users(user_totals.keys) if user_totals.any?
|
||||
# Only calculate streaks for users who actually have time today
|
||||
# This significantly reduces the streak calculation overhead
|
||||
streak_user_ids = user_totals.keys
|
||||
streaks = streak_user_ids.any? ? Heartbeat.daily_streaks_for_users(streak_user_ids, start_date: 30.days.ago) : {}
|
||||
|
||||
# Create virtual leaderboard entries
|
||||
entries = user_totals.map do |user_id, total_seconds|
|
||||
@@ -71,8 +86,8 @@ class LeaderboardGenerator
|
||||
streak_count: streaks[user_id] || 0
|
||||
)
|
||||
|
||||
# Manually set the user association to avoid N+1 queries
|
||||
entry.user = users.find { |u| u.id == user_id }
|
||||
# Use preloaded users hash instead of find
|
||||
entry.user = users_hash[user_id]
|
||||
entry
|
||||
end.sort_by(&:total_seconds).reverse
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
<style>
|
||||
.post-review-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
padding: 1rem; background-color: #1F2937; color: #E5E7EB;
|
||||
min-height: calc(100vh - 4rem); display: flex; flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%# app/views/admin/ysws_reviews/show.html.erb %>
|
||||
<% content_for :title, "YSWS Review: #{@submission.airtable_fields['name']&.truncate(30) || 'Untitled Submission'}" %>
|
||||
|
||||
<div class="ysws-review-container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; padding: 1rem; background-color: #1F2937; color: #E5E7EB; min-height: calc(100vh - 4rem);">
|
||||
<div class="ysws-review-container" style="padding: 1rem; background-color: #1F2937; color: #E5E7EB; min-height: calc(100vh - 4rem);">
|
||||
<div class="ysws-review-header">
|
||||
<h1 style="color: #FFF; border-bottom: 1px solid #4A5568; padding-bottom: 0.5rem; margin-bottom: 1rem; font-size: 1.5rem;">
|
||||
YSWS Submission Review
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
in the last 24 hours.
|
||||
(DB: <%= pluralize(QueryCount::Counter.counter, "query") %>, <%= QueryCount::Counter.counter_cache %> cached)
|
||||
(CACHE: <%= cache_stats[:hits] %> hits, <%= cache_stats[:misses] %> misses)
|
||||
(<%= requests_per_second %>)
|
||||
</p>
|
||||
<% if session[:impersonater_user_id] %>
|
||||
<%= link_to "Stop impersonating", stop_impersonating_path, class: "impersonate-link", data: { turbo_prefetch: "false" } %>
|
||||
|
||||
@@ -2,23 +2,19 @@
|
||||
<% if leaderboard.present? %>
|
||||
<%
|
||||
entries = leaderboard.entries.respond_to?(:order) ? leaderboard.entries.order(total_seconds: :desc) : leaderboard.entries
|
||||
if current_user
|
||||
user_rank = entries.find_index { |entry| entry.user_id == current_user.id }
|
||||
if user_rank && user_rank >= 3
|
||||
# Show top 2 entries and immediate competition
|
||||
top_entries = entries[0..1]
|
||||
competition_entries = entries[[user_rank - 1, 2].max..[user_rank + 1, entries.size - 1].min]
|
||||
mini_leaderboard_entries = top_entries + competition_entries
|
||||
show_top_entries = false
|
||||
else
|
||||
# Show top 3 entries (either user is in top 3 or not on leaderboard)
|
||||
mini_leaderboard_entries = entries.first(3)
|
||||
show_top_entries = true
|
||||
end
|
||||
|
||||
top_entries = entries.first(3)
|
||||
user_entry = current_user ? entries.find { |entry| entry.user_id == current_user.id } : nil
|
||||
user_rank = user_entry ? entries.index(user_entry) : nil
|
||||
|
||||
if user_entry && user_rank && user_rank >= 3
|
||||
person_above = entries[user_rank - 1] if user_rank > 0
|
||||
context_entries = person_above ? [person_above, user_entry] : [user_entry]
|
||||
mini_leaderboard_entries = top_entries + context_entries
|
||||
show_user_separately = true
|
||||
else
|
||||
# Not logged in, show top 3
|
||||
mini_leaderboard_entries = entries.first(3)
|
||||
show_top_entries = true
|
||||
mini_leaderboard_entries = top_entries
|
||||
show_user_separately = false
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -36,21 +32,19 @@
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
<% mini_leaderboard_entries.each_with_index do |entry, idx| %>
|
||||
<% is_competition = !show_top_entries && idx >= 2 %>
|
||||
<div class="flex items-center p-3 rounded-lg hover:bg-dark transition-colors duration-200 <%= 'bg-dark border border-red' if entry.user_id == current_user&.id %>">
|
||||
<% if !is_competition %>
|
||||
<% rank_emoji = case entries.index(entry)
|
||||
when 0 then "🥇"
|
||||
when 1 then "🥈"
|
||||
when 2 then "🥉"
|
||||
end %>
|
||||
<div class="w-8 text-center text-lg"><%= rank_emoji %></div>
|
||||
<% else %>
|
||||
<% if idx == 2 && entries.index(entry) - entries.index(mini_leaderboard_entries[1]) > 1 %>
|
||||
<div class="text-center text-muted font-bold tracking-widest text-lg py-1">...</div>
|
||||
<% end %>
|
||||
<div class="w-8 text-center text-lg font-medium text-muted"><%= entries.index(entry) + 1 %></div>
|
||||
<% end %>
|
||||
<%
|
||||
actual_rank = entries.index(entry)
|
||||
is_separator_needed = show_user_separately && idx == 3 && actual_rank > 3
|
||||
%>
|
||||
|
||||
<div class="flex items-center p-3 rounded-lg bg-dark transition-colors duration-200 <%= 'bg-dark border border-red' if entry.user_id == current_user&.id %>">
|
||||
<% rank_emoji = case actual_rank
|
||||
when 0 then "🥇"
|
||||
when 1 then "🥈"
|
||||
when 2 then "🥉"
|
||||
else actual_rank + 1
|
||||
end %>
|
||||
<div class="w-8 text-center text-lg"><%= rank_emoji %></div>
|
||||
<div class="flex-1 mx-3 min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<%= render "shared/user_mention", user: entry.user, show: [:neighborhood] %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<aside class="flex flex-col min-h-screen w-[250px] bg-dark text-white px-2 py-4 border-r border-red overflow-y-auto lg:block" data-nav-target="nav">
|
||||
<aside class="flex flex-col min-h-screen w-[250px] bg-dark text-white px-2 py-4 rounded-r-lg overflow-y-auto lg:block" data-nav-target="nav" style="scrollbar-width: none; -ms-overflow-style: none;">
|
||||
<div class="space-y-4">
|
||||
<% flash.each do |name, msg| %>
|
||||
<div>
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<p>
|
||||
<% if @show_logged_time_sentence %>
|
||||
You've logged
|
||||
@@ -123,23 +122,20 @@
|
||||
No time logged today... but you can change that!
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<%#= turbo_frame_tag "mini_leaderboard", src: mini_leaderboard_static_pages_path do %>
|
||||
<%#= render "leaderboards/mini_leaderboard_loading" %>
|
||||
<%# end %>
|
||||
|
||||
<%= turbo_frame_tag "mini_leaderboard", src: mini_leaderboard_static_pages_path do %>
|
||||
<%= render "leaderboards/mini_leaderboard_loading" %>
|
||||
<% end %>
|
||||
<%= turbo_frame_tag "filterable_dashboard", src: filterable_dashboard_static_pages_path do %>
|
||||
<span>Loading...</span>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_frame_tag "activity_graph", src: activity_graph_static_pages_path do %>
|
||||
<span>Loading...</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%# if @leaderboard %>
|
||||
<!--<h3>Today's Top Hack Clubbers</h3>-->
|
||||
<%#= render "leaderboards/mini_leaderboard", leaderboard: @leaderboard, current_user: nil %>
|
||||
<%# end %>
|
||||
<% if @leaderboard %>
|
||||
<h3>Today's Top Hack Clubbers</h3>
|
||||
<%= render "leaderboards/mini_leaderboard", leaderboard: @leaderboard, current_user: nil %>
|
||||
<% end %>
|
||||
|
||||
<div class="login-grid">
|
||||
<div class="video-container">
|
||||
|
||||
@@ -357,25 +357,28 @@
|
||||
</div>
|
||||
|
||||
<% if current_user.wakatime_mirrors.any? %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<% grid_cols = current_user.wakatime_mirrors.size > 1 ? "md:grid-cols-2" : "" %>
|
||||
<div class="grid grid-cols-1 <%= grid_cols %> gap-4 mb-4">
|
||||
<% current_user.wakatime_mirrors.each do |mirror| %>
|
||||
<div class="p-4 bg-gray-800 border border-gray-600 rounded">
|
||||
<h3 class="text-white font-medium"><%= mirror.name %></h3>
|
||||
<p class="text-gray-400 text-sm"><%= mirror.endpoint_url %></p>
|
||||
<h3 class="text-white font-medium"><%= mirror.endpoint_url %></h3>
|
||||
<p class="text-gray-400 text-sm">
|
||||
Last synced: <%= mirror.last_synced_at ? time_ago_in_words(mirror.last_synced_at) + " ago" : "Never" %>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form_with(model: [current_user, WakatimeMirror.new], local: true, class: "space-y-4") do |f| %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<%= f.label :name, class: "block text-sm font-medium text-gray-200 mb-2" %>
|
||||
<%= f.text_field :name, class: "w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-white focus:border-red focus:ring-1 focus:ring-primary" %>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<%= f.label :endpoint_url, class: "block text-sm font-medium text-gray-200 mb-2" %>
|
||||
<%= f.url_field :endpoint_url, class: "w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-white focus:border-red focus:ring-1 focus:ring-primary" %>
|
||||
<%= f.url_field :endpoint_url, value: "https://wakatime.com/api/v1", class: "w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-white focus:border-red focus:ring-1 focus:ring-primary" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= f.label :encrypted_api_key, "WakaTime API Key", class: "block text-sm font-medium text-gray-200 mb-2" %>
|
||||
<%= f.password_field :encrypted_api_key, placeholder: "Enter your WakaTime API key", class: "w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-white focus:border-red focus:ring-1 focus:ring-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
<%= f.submit "Add Mirror", class: "px-4 py-2 bg-primary text-white font-medium rounded transition-colors duration-200" %>
|
||||
|
||||
11
config/initializers/cache_warming.rb
Normal file
11
config/initializers/cache_warming.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# Schedule mini leaderboard cache warming
|
||||
# This will run every 5 minutes to keep the cache warm
|
||||
Rails.application.config.after_initialize do
|
||||
if defined?(Sidekiq::Cron::Job)
|
||||
Sidekiq::Cron::Job.create(
|
||||
name: "Warm Mini Leaderboard Cache",
|
||||
cron: "*/5 * * * *",
|
||||
class: "WarmMiniLeaderboardCacheJob"
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,71 +1,120 @@
|
||||
# config/initializers/rack_attack.rb
|
||||
|
||||
class Rack::Attack
|
||||
# kill switch in case you really wanna
|
||||
Rack::Attack.enabled = Rails.env.production? || ENV["RACK_ATTACK_ENABLED"] == "true"
|
||||
|
||||
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new if Rails.env.development?
|
||||
|
||||
if ENV["RACK_ATTACK_BYPASS"].present?
|
||||
Rack::Attack.safelist("mark any authenticated access safe") do |request|
|
||||
# Requests are allowed if the return value is truthy
|
||||
request.env["HTTP_RACK_ATTACK_BYPASS"] == ENV["RACK_ATTACK_BYPASS"]
|
||||
begin
|
||||
bypass_value = ENV["RACK_ATTACK_BYPASS"].strip
|
||||
bypass_value = bypass_value.gsub(/\A['"]|['"]\z/, "")
|
||||
bypass_value = bypass_value.gsub(/\\\"/, '"') if bypass_value.include?('\\\"')
|
||||
|
||||
TOKENS = JSON.parse(bypass_value).freeze
|
||||
unless TOKENS.is_a?(Array)
|
||||
Rails.logger.warn "RACK_ATTACK_BYPASS should be a array, tf is this #{TOKENS.class}"
|
||||
TOKENS = [].freeze
|
||||
end
|
||||
Rails.logger.info "RACK_ATTACK_BYPASS loaded #{TOKENS.length} let me in tokens"
|
||||
rescue JSON::ParserError => e
|
||||
Rails.logger.error "RACK_ATTACK_BYPASS failed to read, you fucked it up #{e.message} raw: #{ENV['RACK_ATTACK_BYPASS'].inspect}"
|
||||
TOKENS = [].freeze
|
||||
end
|
||||
|
||||
Rack::Attack.safelist("mark any authenticated access safe") do |request|
|
||||
bypass = request.env["HTTP_RACK_ATTACK_BYPASS"] || request.env["HTTP_X_RACK_ATTACK_BYPASS"]
|
||||
bypass.present? && TOKENS.include?(bypass)
|
||||
end
|
||||
else
|
||||
TOKENS = [].freeze
|
||||
end
|
||||
|
||||
# Always allow requests from localhost
|
||||
# Always allow requests from bogon ips
|
||||
# (blocklist & throttles are skipped)
|
||||
Rack::Attack.safelist("allow from localhost") do |req|
|
||||
# Requests are allowed if the return value is truthy
|
||||
"127.0.0.1" == req.ip || "::1" == req.ip
|
||||
Rack::Attack.safelist("allow from bogon ips") do |req|
|
||||
# max, thats a weird way, check out this method i stole from stack overflow
|
||||
ip = IPAddr.new(req.ip)
|
||||
ip.loopback? || ip.private?
|
||||
rescue IPAddr::InvalidAddressError
|
||||
false
|
||||
end
|
||||
|
||||
# Allow an IP address to make 5 requests per second
|
||||
throttle("req/ip", limit: 300, period: 5.minutes) do |req|
|
||||
req.ip
|
||||
Rack::Attack.throttle("general", limit: 300, period: 1.minute) do |req|
|
||||
req.ip unless req.path.start_with?("/assets")
|
||||
end
|
||||
|
||||
# Allow an IP address to make 5 POST requests per second
|
||||
throttle("post/ip", limit: 60, period: 5.minutes) do |req|
|
||||
Rack::Attack.throttle("posts by ip", limit: 60, period: 5.minutes) do |req|
|
||||
req.ip if req.post?
|
||||
end
|
||||
|
||||
# Throttle requests to /login by IP address
|
||||
throttle("login/ip", limit: 5, period: 20.seconds) do |req|
|
||||
if req.path == "/login" && req.post?
|
||||
req.ip
|
||||
end
|
||||
Rack::Attack.throttle("auth requests", limit: 5, period: 1.minute) do |req|
|
||||
req.ip if req.path.in?([ "/login", "/signup", "/auth", "/sessions" ]) && req.post?
|
||||
end
|
||||
|
||||
# Throttle requests to /api by IP address
|
||||
throttle("api/ip", limit: 100, period: 5.minutes) do |req|
|
||||
if req.path.start_with?("/api")
|
||||
req.ip
|
||||
end
|
||||
Rack::Attack.throttle("api requests", limit: 1000, period: 1.hour) do |req|
|
||||
req.ip if req.path.start_with?("/api/")
|
||||
end
|
||||
|
||||
# Log blocked requests
|
||||
Rack::Attack.throttle("heartbeat api", limit: 10000, period: 1.hour) do |req|
|
||||
req.ip if req.path.start_with?("/api/hackatime/v1/users/current/heartbeats")
|
||||
end
|
||||
|
||||
Rack::Attack.blocklist("block sussy") do |req|
|
||||
# somehow we can do this, so lets get all the cringe ones outta here
|
||||
user_agent = req.env["HTTP_USER_AGENT"].to_s.downcase
|
||||
sussy = %w[scanner bot crawler wget curl python-requests]
|
||||
|
||||
# if you spoof this i swear your actually cringe
|
||||
no_cap = %w[wakatime hackatime github slack discord uptime kuma]
|
||||
|
||||
is_sus = sussy.any? { |agent| user_agent.include?(agent) }
|
||||
allowed = no_cap.any? { |agent| user_agent.include?(agent) }
|
||||
|
||||
is_sus && !allowed
|
||||
end
|
||||
|
||||
# lets actually log things? thanks
|
||||
ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, request_id, payload|
|
||||
req = payload[:request]
|
||||
user_agent = req.env["HTTP_USER_AGENT"]
|
||||
|
||||
case name
|
||||
when "rack_attack.throttle"
|
||||
Rails.logger.warn "[Rack::Attack][Throttle] IP: #{req.ip}, Path: #{req.path}, Discriminator: #{payload[:discriminator]}, Matched: #{payload[:matched]}"
|
||||
Rails.logger.warn "[Rack::Attack][Throttle] IP: #{req.ip}, Path: #{req.path}, Rule: #{payload[:matched]}, UA: #{user_agent}"
|
||||
when "rack_attack.blocklist"
|
||||
Rails.logger.warn "[Rack::Attack][Blocklist] IP: #{req.ip}, Path: #{req.path}, Discriminator: #{payload[:discriminator]}, Matched: #{payload[:matched]}"
|
||||
Rails.logger.warn "[Rack::Attack][Block] IP: #{req.ip}, Path: #{req.path}, Rule: #{payload[:matched]}, UA: #{user_agent}"
|
||||
when "rack_attack.safelist"
|
||||
Rails.logger.info "[Rack::Attack][Safelist] IP: #{req.ip}, Path: #{req.path}, Discriminator: #{payload[:discriminator]}, Matched: #{payload[:matched]}"
|
||||
Rails.logger.info "[Rack::Attack][Bypass] IP: #{req.ip}, Path: #{req.path}, Rule: #{payload[:matched]}"
|
||||
end
|
||||
end
|
||||
|
||||
# Custom response for throttled requests
|
||||
self.throttled_responder = lambda do |request|
|
||||
retry_after = (request.env["rack.attack.match_data"] || {})[:period]
|
||||
[
|
||||
429,
|
||||
{
|
||||
"Content-Type" => "application/json",
|
||||
"Retry-After" => retry_after.to_s,
|
||||
"X-RateLimit-Limit" => request.env["rack.attack.matched"].to_s,
|
||||
"X-RateLimit-Remaining" => "0",
|
||||
"X-RateLimit-Reset" => (Time.now + retry_after).to_i.to_s
|
||||
},
|
||||
[ { error: "Too Many Requests", message: "Rate limit exceeded. Try again later." }.to_json ]
|
||||
]
|
||||
match = request.env["rack.attack.match_data"] || {}
|
||||
retry_after = match[:period] || 60
|
||||
limit = match[:limit] || "unknown"
|
||||
|
||||
now = Time.current
|
||||
reset_time = (now + retry_after).to_i
|
||||
|
||||
headers = {
|
||||
"Content-Type" => "application/json",
|
||||
"Retry-After" => retry_after.to_s,
|
||||
"X-RateLimit-Limit" => limit.to_s,
|
||||
"X-RateLimit-Remaining" => "0",
|
||||
"X-RateLimit-Reset" => reset_time.to_s,
|
||||
"X-RateLimit-Reset-At" => Time.at(reset_time).iso8601
|
||||
}
|
||||
|
||||
res = {
|
||||
error: "Rate limit exceeded",
|
||||
message: "Woah there, way too fast, take a chill pill speedy gonzales!",
|
||||
retry_after: retry_after,
|
||||
reset_at: Time.at(reset_time).iso8601
|
||||
}
|
||||
|
||||
[ 429, headers, [ res.to_json ] ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
class AddIndexesForLeaderboardPerformance < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_index :heartbeats, [ :user_id, :time, :category ], name: 'index_heartbeats_on_user_time_category'
|
||||
add_index :users, [ :timezone, :trust_level ], name: 'index_users_on_timezone_trust_level'
|
||||
add_index :users, :github_uid, name: 'index_users_on_github_uid'
|
||||
end
|
||||
end
|
||||
5
db/schema.rb
generated
5
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_06_23_135342) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_06_26_001500) do
|
||||
create_schema "pganalyze"
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
@@ -248,6 +248,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_23_135342) do
|
||||
t.index ["fields_hash"], name: "index_heartbeats_on_fields_hash_when_not_deleted", unique: true, where: "(deleted_at IS NULL)"
|
||||
t.index ["raw_heartbeat_upload_id"], name: "index_heartbeats_on_raw_heartbeat_upload_id"
|
||||
t.index ["source_type", "time", "user_id", "project"], name: "index_heartbeats_on_source_type_time_user_project"
|
||||
t.index ["user_id", "time", "category"], name: "index_heartbeats_on_user_time_category"
|
||||
t.index ["user_id", "time"], name: "idx_heartbeats_user_time_active", where: "(deleted_at IS NULL)"
|
||||
t.index ["user_id"], name: "index_heartbeats_on_user_id"
|
||||
end
|
||||
@@ -495,7 +496,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_23_135342) do
|
||||
t.boolean "allow_public_stats_lookup", default: true, null: false
|
||||
t.boolean "default_timezone_leaderboard", default: true, null: false
|
||||
t.index ["github_uid", "github_access_token"], name: "index_users_on_github_uid_and_access_token"
|
||||
t.index ["github_uid"], name: "index_users_on_github_uid"
|
||||
t.index ["slack_uid"], name: "index_users_on_slack_uid", unique: true
|
||||
t.index ["timezone", "trust_level"], name: "index_users_on_timezone_trust_level"
|
||||
t.index ["timezone"], name: "index_users_on_timezone"
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user