whole buncha stuff

This commit is contained in:
24c02 2026-01-29 00:51:48 -05:00
parent 3d8b180b11
commit 5ce2ea55ad
25 changed files with 568 additions and 17 deletions

View file

@ -5,6 +5,8 @@ class Components::Base < Phlex::HTML
register_value_helper :current_user
# Include any helpers you want to be available across all components
include Phlex::Rails::Helpers::Routes
include Phlex::Rails::Helpers::ButtonTo
include Phlex::Rails::Helpers::TimeAgoInWords
if Rails.env.development?
def before_template

View file

@ -22,7 +22,7 @@ class Components::HeaderBar < Components::Base
plain current_user.name
end
menu.with_item(label: "Log out", href: logout_path, data: { method: :delete }) do |item|
menu.with_item(label: "Log out", href: logout_path, form_arguments: { method: :delete }) do |item|
item.with_leading_visual_icon(icon: :"sign-out")
end
end

View file

@ -0,0 +1,51 @@
# frozen_string_literal: true
class Components::StaticPages::Base < Components::Base
def stat_card(title, value, icon)
div(style: "padding: 14px; background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px;") do
div(style: "display: flex; justify-content: space-between; align-items: flex-start;") do
div do
p(style: "font-size: 11px; color: var(--fgColor-muted, #656d76); margin: 0 0 4px; text-transform: uppercase; letter-spacing: 0.3px;") { title }
span(style: "font-size: 28px; font-weight: 600; line-height: 1;") { value.to_s }
end
span(style: "color: var(--fgColor-muted, #656d76);") do
render Primer::Beta::Octicon.new(icon: icon, size: :small)
end
end
end
end
def link_panel(title, links)
div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; overflow: hidden;") do
div(style: "padding: 12px 16px; border-bottom: 1px solid var(--borderColor-default, #d0d7de); background: var(--bgColor-muted, #f6f8fa);") do
h3(style: "font-size: 14px; font-weight: 600; margin: 0;") { title }
end
div(style: "padding: 8px 0;") do
links.each do |link|
a(
href: link[:href],
target: link[:href].start_with?("http") ? "_blank" : nil,
rel: link[:href].start_with?("http") ? "noopener" : nil,
style: "display: flex; align-items: center; gap: 12px; padding: 10px 16px; text-decoration: none; color: inherit;"
) do
span(style: "color: var(--fgColor-muted, #656d76);") do
render Primer::Beta::Octicon.new(icon: link[:icon], size: :small)
end
span(style: "font-size: 14px;") { link[:label] }
end
end
end
end
end
def resources_panel
links = [
{ label: "API Docs", href: "https://github.com/hackclub/cdn#-api-usage", icon: :book },
{ label: "GitHub Repo", href: "https://github.com/hackclub/cdn", icon: :"mark-github" },
{ label: "Use via Slack", href: "https://hackclub.enterprise.slack.com/archives/C016DEDUL87", icon: :"comment-discussion" },
{ label: "Help with development?", href: "https://hackclub.enterprise.slack.com/archives/C0ACGUA6XTJ", icon: :heart },
{ label: "Report an Issue", href: "https://github.com/hackclub/cdn/issues", icon: :"issue-opened" }
]
link_panel("Resources", links)
end
end

View file

@ -0,0 +1,91 @@
# frozen_string_literal: true
class Components::StaticPages::Home < Components::StaticPages::Base
def initialize(stats:, user:)
@stats = stats
@user = user
end
def view_template
div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do
header_section
kpi_section
main_section
end
end
private
attr_reader :stats, :user
def header_section
header(style: "display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 16px; padding-bottom: 24px; margin-bottom: 24px; border-bottom: 1px solid var(--borderColor-default, #d0d7de);") do
div do
p(style: "color: var(--fgColor-muted, #656d76); margin: 0 0 4px; font-size: 14px;") do
plain "Welcome back, "
strong { user&.name || "friend" }
end
h1(style: "font-size: 2rem; font-weight: 300; margin: 0;") { "Your CDN Stash" }
end
div(style: "display: flex; gap: 8px; flex-wrap: wrap;") do
a(href: "https://github.com/hackclub/cdn", target: "_blank", rel: "noopener", class: "btn") do
render Primer::Beta::Octicon.new(icon: :"mark-github", mr: 1)
plain "View on GitHub"
end
a(href: "https://app.slack.com/client/T0266FRGM/C016DEDUL87", target: "_blank", rel: "noopener", class: "btn btn-primary") do
render Primer::Beta::Octicon.new(icon: :"comment-discussion", mr: 1)
plain "Join #cdn"
end
end
end
end
def kpi_section
div(style: "margin-bottom: 32px;") do
# Your stats section
h2(style: "font-size: 14px; font-weight: 600; color: var(--fgColor-muted, #656d76); text-transform: uppercase; letter-spacing: 0.5px; margin: 0 0 12px;") { "Your Stats" }
div(style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px;") do
stat_card("Total files", stats[:total_files], :archive)
stat_card("Storage used", stats[:storage_formatted], :database)
stat_card("Uploaded today", stats[:files_today], :upload)
stat_card("This week", stats[:files_this_week], :zap)
end
# Recent uploads
if stats[:recent_uploads].any?
h2(style: "font-size: 14px; font-weight: 600; color: var(--fgColor-muted, #656d76); text-transform: uppercase; letter-spacing: 0.5px; margin: 24px 0 12px;") { "Recent Uploads" }
recent_uploads_list
end
end
end
def recent_uploads_list
div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; overflow: hidden;") do
stats[:recent_uploads].each_with_index do |upload, index|
div(style: "padding: 12px 16px; #{index > 0 ? 'border-top: 1px solid var(--borderColor-default, #d0d7de);' : ''}") do
div(style: "display: flex; justify-content: space-between; align-items: center; gap: 16px;") do
div(style: "flex: 1; min-width: 0;") do
div(style: "font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;") do
render Primer::Beta::Octicon.new(icon: :file, size: :small, mr: 1)
plain upload.filename.to_s
end
div(style: "font-size: 12px; color: var(--fgColor-muted, #656d76); margin-top: 4px;") do
plain "#{upload.human_file_size}#{time_ago_in_words(upload.created_at)} ago"
end
end
a(href: upload.cdn_url, target: "_blank", rel: "noopener", class: "btn btn-sm") do
plain "View"
end
end
end
end
end
end
def main_section
div(style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;") do
resources_panel
end
end
end

View file

@ -0,0 +1,74 @@
# frozen_string_literal: true
class Components::StaticPages::LoggedOut < Components::StaticPages::Base
def initialize(stats:)
@stats = stats
end
def view_template
div(style: "max-width: 1200px; margin: 0 auto; padding: 48px 24px 24px;") do
header_section
stats_section
main_section
end
end
private
attr_reader :stats
def header_section
header(style: "display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 16px; padding-bottom: 24px; margin-bottom: 24px; border-bottom: 1px solid var(--borderColor-default, #d0d7de);") do
div do
h1(style: "font-size: 2rem; font-weight: 300; margin: 0 0 8px;") do
plain "Hack Club CDN"
sup(style: "font-size: 0.5em; margin-left: 4px;") { "v4" }
end
p(style: "color: var(--fgColor-muted, #656d76); margin: 0; max-width: 600px;") do
plain "File hosting for Hack Clubbers."
end
end
div(style: "display: flex; gap: 8px; flex-wrap: wrap;") do
button_to "Sign in with Hack Club", "/auth/hack_club", method: :post, class: "btn btn-primary", data: { turbo: false }
end
end
end
def stats_section
div(style: "margin-bottom: 32px;") do
h2(style: "font-size: 14px; font-weight: 600; color: var(--fgColor-muted, #656d76); text-transform: uppercase; letter-spacing: 0.5px; margin: 0 0 12px;") { "State of the Platform:" }
div(style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px;") do
stat_card("Total files", stats[:total_files], :archive)
stat_card("Storage used", stats[:storage_formatted], :database)
stat_card("Users", stats[:total_users], :people)
stat_card("Files this week", stats[:files_this_week], :zap)
end
h2(style: "font-size: 14px; font-weight: 600; color: var(--fgColor-muted, #656d76); text-transform: uppercase; letter-spacing: 0.5px; margin: 24px 0 12px;") { "New in V4:" }
div(style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;") do
feature_card(:lock, "Invincible", "Backups of the underlying storage exist.")
feature_card(:link, "No broken links, this time?", "it lives on a domain! that we own!")
feature_card(:"shield-check", "Hopefully reliable", 'Backed by the award-winning "cc @nora" service guarantee.')
end
end
end
def feature_card(icon, title, description)
div(style: "padding: 14px; background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px;") do
div(style: "display: flex; align-items: center; gap: 10px; margin-bottom: 6px;") do
span(style: "color: var(--fgColor-muted, #656d76);") do
render Primer::Beta::Octicon.new(icon: icon, size: :small)
end
h3(style: "font-size: 14px; font-weight: 600; margin: 0;") { title }
end
p(style: "font-size: 12px; color: var(--fgColor-muted, #656d76); margin: 0;") { description }
end
end
def main_section
div(style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;") do
resources_panel
end
end
end

View file

@ -13,7 +13,7 @@ class SessionsController < ApplicationController
def destroy
reset_session
redirect_to login_path, notice: "Signed out successfully!"
redirect_to root_path, notice: "Signed out successfully!"
end
def failure

View file

@ -2,5 +2,10 @@ class StaticPagesController < ApplicationController
skip_before_action :require_authentication!, only: [:home]
def home
if signed_in?
@user_stats = CDNStatsService.user_stats(current_user)
else
@global_stats = CDNStatsService.global_stats
end
end
end

View file

@ -1 +1,4 @@
import Rails from "@rails/ujs";
Rails.start();
import "@primer/view-components/app/components/primer/primer.js";

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class RefreshCDNStatsJob < ApplicationJob
queue_as :default
def perform
CDNStatsService.refresh_global_stats!
end
end

View file

@ -1,3 +1,12 @@
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
before_create :generate_uuid_v7
private
def generate_uuid_v7
return if self.class.attribute_types['id'].type != :uuid
self.id ||= SecureRandom.uuid_v7
end
end

57
app/models/upload.rb Normal file
View file

@ -0,0 +1,57 @@
# frozen_string_literal: true
class Upload < ApplicationRecord
# UUID v7 primary key (automatic via migration)
belongs_to :user
belongs_to :blob, class_name: 'ActiveStorage::Blob'
# Delegate file metadata to blob (no duplication!)
delegate :filename, :byte_size, :content_type, :checksum, to: :blob
# Aliases for consistency
alias_method :file_size, :byte_size
alias_method :mime_type, :content_type
# Provenance enum
enum :provenance, {
slack: 'slack',
web: 'web',
api: 'api',
rescued: 'rescued'
}, validate: true
validates :provenance, presence: true
scope :recent, -> { order(created_at: :desc) }
scope :by_user, ->(user) { where(user: user) }
scope :today, -> { where('created_at >= ?', Time.zone.now.beginning_of_day) }
scope :this_week, -> { where('created_at >= ?', Time.zone.now.beginning_of_week) }
scope :this_month, -> { where('created_at >= ?', Time.zone.now.beginning_of_month) }
def human_file_size
ActiveSupport::NumberHelper.number_to_human_size(byte_size)
end
# Get CDN URL (generated from blob)
def cdn_url
Rails.application.routes.url_helpers.rails_blob_url(blob, host: ENV['CDN_HOST'] || 'cdn.hackclub.com')
end
# Create upload from URL (for API/rescue operations)
def self.create_from_url(url, user:, provenance:, original_url: nil)
downloaded = URI.open(url)
blob = ActiveStorage::Blob.create_and_upload!(
io: downloaded,
filename: File.basename(URI.parse(url).path)
)
create!(
user: user,
blob: blob,
provenance: provenance,
original_url: original_url
)
end
end

View file

@ -9,6 +9,8 @@ class User < ApplicationRecord
validates :hca_id, presence: true, uniqueness: true
encrypts :hca_access_token
has_many :uploads, dependent: :destroy
def self.find_or_create_from_omniauth(auth)
hca_id = auth.uid
slack_id = auth.extra.raw_info.slack_id
@ -38,4 +40,16 @@ class User < ApplicationRecord
end
def hca_profile(access_token) = HCAService.new(access_token).me
def total_files
uploads.count
end
def total_storage_bytes
uploads.joins(:blob).sum('active_storage_blobs.byte_size')
end
def total_storage_gb
(total_storage_bytes / 1.gigabyte.to_f).round(2)
end
end

View file

@ -0,0 +1,49 @@
# frozen_string_literal: true
class CDNStatsService
CACHE_KEY_GLOBAL = "cdn:stats:global"
CACHE_DURATION = 5.minutes
# Global stats (cached) - for logged-out users
def self.global_stats
Rails.cache.fetch(CACHE_KEY_GLOBAL, expires_in: CACHE_DURATION) do
calculate_global_stats
end
end
# Force refresh global stats (called by background job)
def self.refresh_global_stats!
Rails.cache.delete(CACHE_KEY_GLOBAL)
global_stats
end
# User stats (live) - for logged-in users
def self.user_stats(user)
{
total_files: user.total_files,
total_storage: user.total_storage_gb,
storage_formatted: "#{user.total_storage_gb} GB",
files_today: user.uploads.today.count,
files_this_week: user.uploads.this_week.count,
recent_uploads: user.uploads.includes(:blob).recent.limit(5)
}
end
private
def self.calculate_global_stats
total_files = Upload.count
total_storage_bytes = Upload.joins(:blob).sum('active_storage_blobs.byte_size')
total_storage_gb = (total_storage_bytes / 1.gigabyte.to_f).round(2)
total_users = User.joins(:uploads).distinct.count
{
total_files: total_files,
total_storage_gb: total_storage_gb,
storage_formatted: "#{total_storage_gb} GB",
total_users: total_users,
files_today: Upload.today.count,
files_this_week: Upload.this_week.count
}
end
end

View file

@ -34,7 +34,7 @@
</head>
<body>
<%= render(Components::HeaderBar.new) %>
<%= render(Components::HeaderBar.new) if signed_in? %>
<%= yield %>
</body>
</html>

View file

@ -1,5 +1,5 @@
<% if signed_in? %>
<%= render Components::StaticPages::Home.new(stats: @user_stats, user: current_user) %>
<% else %>
<%= button_to "Sign in with Hack Club", "/auth/hack_club", method: :post, class: "btn btn-primary btn-large", data: { turbo: false } %>
<% end %>
<%= render Components::StaticPages::LoggedOut.new(stats: @global_stats) %>
<% end %>

View file

@ -21,8 +21,8 @@ Rails.application.configure do
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com"
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
# Store uploaded files on Amazon S3 or S3-compatible storage (see config/storage.yml for options).
config.active_storage.service = :amazon
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
config.assume_ssl = true

View file

@ -13,3 +13,6 @@ production:
clear_solid_queue_finished_jobs:
command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
schedule: every hour at minute 12
refresh_cdn_stats:
class: RefreshCDNStatsJob
schedule: every 5 minutes

View file

@ -6,13 +6,15 @@ local:
service: Disk
root: <%= Rails.root.join("storage") %>
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
# service: S3
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# region: us-east-1
# bucket: your_own_bucket-<%= Rails.env %>
# Cloudflare R2 (S3-compatible)
amazon:
service: S3
access_key_id: <%= ENV['R2_ACCESS_KEY_ID'] %>
secret_access_key: <%= ENV['R2_SECRET_ACCESS_KEY'] %>
region: auto
bucket: <%= ENV['R2_BUCKET_NAME'] %>
endpoint: <%= ENV['R2_ENDPOINT'] %> # Format: https://<account_id>.r2.cloudflarestorage.com
force_path_style: true
# Remember not to checkin your GCS keyfile to a repository
# google:

View file

@ -0,0 +1,57 @@
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :key ], unique: true
end
create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[ primary_key_type, foreign_key_type ]
end
end

View file

@ -0,0 +1,20 @@
class CreateUploads < ActiveRecord::Migration[8.0]
def change
create_table :uploads, id: :uuid do |t|
t.references :user, null: false, foreign_key: true, type: :bigint
t.references :blob, null: false, foreign_key: { to_table: :active_storage_blobs }
# Upload source tracking
t.string :provenance, null: false # enum: slack, web, api, rescued
# For rescued files from old hel1 bucket
t.string :original_url # Old CDN URL to fixup
t.timestamps
t.index [:user_id, :created_at]
t.index :created_at
t.index :provenance
end
end
end

49
db/schema.rb generated
View file

@ -10,10 +10,52 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_01_27_174404) do
ActiveRecord::Schema[8.0].define(version: 2026_01_29_051531) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
create_table "uploads", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.bigint "user_id", null: false
t.bigint "blob_id", null: false
t.string "provenance", null: false
t.string "original_url"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["blob_id"], name: "index_uploads_on_blob_id"
t.index ["created_at"], name: "index_uploads_on_created_at"
t.index ["provenance"], name: "index_uploads_on_provenance"
t.index ["user_id", "created_at"], name: "index_uploads_on_user_id_and_created_at"
t.index ["user_id"], name: "index_uploads_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "hca_id"
t.text "hca_access_token"
@ -26,4 +68,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_01_27_174404) do
t.index ["hca_id"], name: "index_users_on_hca_id", unique: true
t.index ["slack_id"], name: "index_users_on_slack_id", unique: true
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "uploads", "active_storage_blobs", column: "blob_id"
add_foreign_key "uploads", "users"
end

View file

@ -7,3 +7,41 @@
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
# MovieGenre.find_or_create_by!(name: genre_name)
# end
if Rails.env.development?
user = User.first || User.create!(
hca_id: 'dev_user',
email: 'dev@example.com',
name: 'Dev User'
)
provenances = [:web, :api, :slack, :rescued]
10.times do |i|
# Create dummy file content
content = "This is test file #{i} content. " * 100
# Create blob (ActiveStorage handles file storage to ./storage/ in dev)
blob = ActiveStorage::Blob.create_and_upload!(
io: StringIO.new(content),
filename: "test_file_#{i}.jpg",
content_type: 'image/jpeg'
)
# Create upload record
provenance = provenances.sample
Upload.create!(
user: user,
blob: blob,
provenance: provenance,
original_url: provenance == :rescued ? "https://hel1.cdn.hackclub.com/old_file_#{i}.jpg" : nil,
created_at: rand(30.days.ago..Time.current)
)
end
puts "Created #{Upload.count} sample uploads for #{user.name}"
puts "Provenance breakdown:"
Upload.group(:provenance).count.each do |prov, count|
puts " #{prov}: #{count}"
end
end

View file

@ -7,8 +7,10 @@
"vite-plugin-ruby": "5.1.1"
},
"dependencies": {
"@hotwired/turbo": "^8.0.22",
"@primer/css": "^22.1.0",
"@primer/primitives": "^11.3.2",
"@primer/view-components": "^0.49.0"
"@primer/view-components": "^0.49.0",
"@rails/ujs": "^7.1.3-4"
}
}

View file

@ -10,6 +10,14 @@ export default defineConfig({
minify: false
},
resolve: {
dedupe: ['@primer/view-components', '@github/catalyst']
},
optimizeDeps: {
include: ['@primer/view-components']
},
server: {
hmr: {
overlay: true

View file

@ -201,6 +201,11 @@
resolved "https://registry.yarnpkg.com/@github/tab-container-element/-/tab-container-element-3.4.0.tgz#0a167ed81c9f4a1c1c79f0d0a6a8c012c49a6147"
integrity sha512-Yx70pO8A0p7Stnm9knKkUNX8i4bjuwDYZarRkM8JH0Z+ffhpe++oNAPbzGI9GEcGugRHvKuSC6p4YOdoHtTniQ==
"@hotwired/turbo@^8.0.22":
version "8.0.22"
resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.22.tgz#f6f8c0756f48ce83c31675998d55c37fdf1a6828"
integrity sha512-A7M8vBgsmZ8W55IOEhTN7o0++zaJTavyGa1DW1rt+/b4VTr8QUWC/zu8wxMmwqvczaOe1nI/MkMq4lm2NmiHPg==
"@lit-labs/ssr-dom-shim@^1.2.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz#3166900c0d481f03d6d4133686e0febf760d521d"
@ -362,6 +367,11 @@
"@primer/behaviors" "^1.3.4"
"@primer/live-region-element" "^0.8.0"
"@rails/ujs@^7.1.3-4":
version "7.1.3-4"
resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.1.3-4.tgz#1dddea99d5c042e8513973ea709b2cb7e840dc2d"
integrity sha512-z0ckI5jrAJfImcObjMT1RBz2IxH6I5q6ZTMFex6AfxSQKZuuL8JxAXvg2CvBuodGCxKvybFVolDyMHXlBLeYAA==
"@rollup/rollup-android-arm-eabi@4.57.0":
version "4.57.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz#f762035679a6b168138c94c960fda0b0cdb00d98"