Initial commit
This commit is contained in:
commit
dbee8fba4d
51
.dockerignore
Normal file
51
.dockerignore
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.
|
||||||
|
|
||||||
|
# Ignore git directory.
|
||||||
|
/.git/
|
||||||
|
/.gitignore
|
||||||
|
|
||||||
|
# Ignore bundler config.
|
||||||
|
/.bundle
|
||||||
|
|
||||||
|
# Ignore all environment files.
|
||||||
|
/.env*
|
||||||
|
|
||||||
|
# Ignore all default key files.
|
||||||
|
/config/master.key
|
||||||
|
/config/credentials/*.key
|
||||||
|
|
||||||
|
# Ignore all logfiles and tempfiles.
|
||||||
|
/log/*
|
||||||
|
/tmp/*
|
||||||
|
!/log/.keep
|
||||||
|
!/tmp/.keep
|
||||||
|
|
||||||
|
# Ignore pidfiles, but keep the directory.
|
||||||
|
/tmp/pids/*
|
||||||
|
!/tmp/pids/.keep
|
||||||
|
|
||||||
|
# Ignore storage (uploaded files in development and any SQLite databases).
|
||||||
|
/storage/*
|
||||||
|
!/storage/.keep
|
||||||
|
/tmp/storage/*
|
||||||
|
!/tmp/storage/.keep
|
||||||
|
|
||||||
|
# Ignore assets.
|
||||||
|
/node_modules/
|
||||||
|
/app/assets/builds/*
|
||||||
|
!/app/assets/builds/.keep
|
||||||
|
/public/assets
|
||||||
|
|
||||||
|
# Ignore CI service files.
|
||||||
|
/.github
|
||||||
|
|
||||||
|
# Ignore Kamal files.
|
||||||
|
/config/deploy*.yml
|
||||||
|
/.kamal
|
||||||
|
|
||||||
|
# Ignore development files
|
||||||
|
/.devcontainer
|
||||||
|
|
||||||
|
# Ignore Docker-related files
|
||||||
|
/.dockerignore
|
||||||
|
/Dockerfile*
|
||||||
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# See https://git-scm.com/docs/gitattributes for more about git attribute files.
|
||||||
|
|
||||||
|
# Mark the database schema as having been generated.
|
||||||
|
db/schema.rb linguist-generated
|
||||||
|
|
||||||
|
# Mark any vendored files as having been vendored.
|
||||||
|
vendor/* linguist-vendored
|
||||||
|
config/credentials/*.yml.enc diff=rails_credentials
|
||||||
|
config/credentials.yml.enc diff=rails_credentials
|
||||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: bundler
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
73
.github/workflows/ci.yml
vendored
Normal file
73
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-test:
|
||||||
|
name: Lint & Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17.5
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
POSTGRES_DB: starter_test
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd="pg_isready -U postgres"
|
||||||
|
--health-interval=5s
|
||||||
|
--health-timeout=5s
|
||||||
|
--health-retries=5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:8.4-alpine
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
options: >-
|
||||||
|
--health-cmd="redis-cli ping"
|
||||||
|
--health-interval=5s
|
||||||
|
--health-timeout=5s
|
||||||
|
--health-retries=5
|
||||||
|
|
||||||
|
env:
|
||||||
|
RAILS_ENV: test
|
||||||
|
DB_HOST: localhost
|
||||||
|
DB_PASS: password
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_DIRECT_HOST: localhost
|
||||||
|
DB_DIRECT_PORT: "5432"
|
||||||
|
REDIS_URL: redis://localhost:6379/1
|
||||||
|
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: "4.0.1"
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Install Chromium
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y --no-install-recommends chromium-browser chromium-chromedriver
|
||||||
|
|
||||||
|
- name: Prepare database
|
||||||
|
run: |
|
||||||
|
bin/rails db:prepare
|
||||||
|
bin/rails parallel:prepare
|
||||||
|
|
||||||
|
- name: Run check-fast
|
||||||
|
run: bin/check-fast
|
||||||
45
.github/workflows/pages.yml
vendored
Normal file
45
.github/workflows/pages.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
name: Deploy Status Page
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["CI"]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Build & Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build status page
|
||||||
|
run: |
|
||||||
|
mkdir -p _site
|
||||||
|
cp docs/status.html _site/index.html
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v4
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: _site
|
||||||
|
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||||
|
#
|
||||||
|
# Temporary files generated by your text editor or operating system
|
||||||
|
# belong in git's global ignore instead:
|
||||||
|
# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore`
|
||||||
|
|
||||||
|
# Ignore bundler config.
|
||||||
|
/.bundle
|
||||||
|
|
||||||
|
# Ignore all environment files.
|
||||||
|
/.env*
|
||||||
|
|
||||||
|
# Ignore all logfiles and tempfiles.
|
||||||
|
/log/*
|
||||||
|
/tmp/*
|
||||||
|
!/log/.keep
|
||||||
|
!/tmp/.keep
|
||||||
|
|
||||||
|
# Ignore pidfiles, but keep the directory.
|
||||||
|
/tmp/pids/*
|
||||||
|
!/tmp/pids/
|
||||||
|
!/tmp/pids/.keep
|
||||||
|
|
||||||
|
# Ignore storage (uploaded files in development and any SQLite databases).
|
||||||
|
/storage/*
|
||||||
|
!/storage/.keep
|
||||||
|
/tmp/storage/*
|
||||||
|
!/tmp/storage/
|
||||||
|
!/tmp/storage/.keep
|
||||||
|
|
||||||
|
/public/assets
|
||||||
|
/app/assets/builds/*
|
||||||
|
!/app/assets/builds/.keep
|
||||||
|
# Ignore key files for decrypting credentials and more.
|
||||||
|
/config/*.key
|
||||||
|
|
||||||
53
.reek.yml
Normal file
53
.reek.yml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
detectors:
|
||||||
|
DuplicateMethodCall:
|
||||||
|
enabled: false
|
||||||
|
LongParameterList:
|
||||||
|
enabled: false
|
||||||
|
TooManyStatements:
|
||||||
|
enabled: false
|
||||||
|
MissingSafeMethod:
|
||||||
|
enabled: false
|
||||||
|
TooManyMethods:
|
||||||
|
max_methods: 25
|
||||||
|
TooManyInstanceVariables:
|
||||||
|
enabled: false
|
||||||
|
ClassVariable:
|
||||||
|
enabled: false
|
||||||
|
TooManyConstants:
|
||||||
|
enabled: false
|
||||||
|
IrresponsibleModule:
|
||||||
|
enabled: false
|
||||||
|
UncommunicativeVariableName:
|
||||||
|
enabled: false
|
||||||
|
UncommunicativeMethodName:
|
||||||
|
enabled: false
|
||||||
|
UncommunicativeParameterName:
|
||||||
|
enabled: false
|
||||||
|
UtilityFunction:
|
||||||
|
enabled: true
|
||||||
|
exclude:
|
||||||
|
- ExampleRecurringJob
|
||||||
|
FeatureEnvy:
|
||||||
|
enabled: false
|
||||||
|
InstanceVariableAssumption:
|
||||||
|
enabled: true
|
||||||
|
exclude:
|
||||||
|
- TodosController
|
||||||
|
NilCheck:
|
||||||
|
enabled: true
|
||||||
|
BooleanParameter:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
|
||||||
|
exclude_paths:
|
||||||
|
- db/migrate
|
||||||
|
- db/seeds
|
||||||
|
- vendor
|
||||||
|
- config
|
||||||
|
- bin
|
||||||
|
- spec
|
||||||
|
- test
|
||||||
|
- tmp
|
||||||
|
- lib/tasks
|
||||||
55
.rubocop.yml
Normal file
55
.rubocop.yml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
# Shopify Ruby styling
|
||||||
|
inherit_gem:
|
||||||
|
rubocop-shopify: rubocop.yml
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
|
||||||
|
- rubocop-factory_bot
|
||||||
|
- rubocop-rails
|
||||||
|
- rubocop-rspec
|
||||||
|
- rubocop-rspec_rails
|
||||||
|
- rubocop-haml
|
||||||
|
|
||||||
|
AllCops:
|
||||||
|
Exclude:
|
||||||
|
- 'app/javascript/**/*'
|
||||||
|
- 'node_modules/**/*'
|
||||||
|
- 'vendor/**/*'
|
||||||
|
- 'db/**/*'
|
||||||
|
- 'sorbet/**/*'
|
||||||
|
- 'agent-scripts/**/*'
|
||||||
|
|
||||||
|
RSpec/MultipleExpectations:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/ExampleLength:
|
||||||
|
Enabled: false
|
||||||
|
# Disable missing enable directive cop so our inline disables don't throw warnings
|
||||||
|
Lint/MissingCopEnableDirective:
|
||||||
|
Enabled: false
|
||||||
|
Naming/PredicatePrefix:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/ContextWording:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/InstanceVariable:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/IndexedLet:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/LetSetup:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/NamedSubject:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/DescribeMethod:
|
||||||
|
Enabled: false
|
||||||
|
Metrics/ParameterLists:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/MultipleMemoizedHelpers:
|
||||||
|
Enabled: false
|
||||||
|
Rails/UnknownEnv:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# Overwrite or add rules to create your own house style
|
||||||
|
#
|
||||||
|
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
|
||||||
|
# Layout/SpaceInsideArrayLiteralBrackets:
|
||||||
|
# Enabled: false
|
||||||
1
.ruby-version
Normal file
1
.ruby-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
ruby-4.0.1
|
||||||
87
Dockerfile
Normal file
87
Dockerfile
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# check=error=true
|
||||||
|
|
||||||
|
# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
|
||||||
|
# docker build -t starter .
|
||||||
|
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name starter starter
|
||||||
|
|
||||||
|
# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html
|
||||||
|
|
||||||
|
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
|
||||||
|
ARG RUBY_VERSION=4.0.1
|
||||||
|
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
|
||||||
|
|
||||||
|
# Rails app lives here
|
||||||
|
WORKDIR /rails
|
||||||
|
|
||||||
|
# Install base packages
|
||||||
|
RUN apt-get update -qq && \
|
||||||
|
apt-get install --no-install-recommends -y curl libjemalloc2 postgresql-client && \
|
||||||
|
ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
|
||||||
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
|
# Install AnyCable Go
|
||||||
|
RUN ARCH=$(uname -m) && \
|
||||||
|
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
|
||||||
|
ANYCABLE_ARCH="arm64"; \
|
||||||
|
else \
|
||||||
|
ANYCABLE_ARCH="amd64"; \
|
||||||
|
fi && \
|
||||||
|
curl -L "https://github.com/anycable/anycable-go/releases/download/v1.6.3/anycable-go-linux-${ANYCABLE_ARCH}" -o /usr/local/bin/anycable-go && \
|
||||||
|
chmod +x /usr/local/bin/anycable-go
|
||||||
|
|
||||||
|
# Set production environment variables and enable jemalloc for reduced memory usage and latency.
|
||||||
|
ENV RAILS_ENV="production" \
|
||||||
|
BUNDLE_DEPLOYMENT="1" \
|
||||||
|
BUNDLE_PATH="/usr/local/bundle" \
|
||||||
|
BUNDLE_WITHOUT="development" \
|
||||||
|
LD_PRELOAD="/usr/local/lib/libjemalloc.so"
|
||||||
|
|
||||||
|
# Throw-away build stage to reduce size of final image
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
|
# Install packages needed to build gems
|
||||||
|
RUN apt-get update -qq && \
|
||||||
|
apt-get install --no-install-recommends -y build-essential git libpq-dev libyaml-dev pkg-config && \
|
||||||
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
|
# Install application gems
|
||||||
|
COPY vendor/* ./vendor/
|
||||||
|
COPY Gemfile Gemfile.lock* ./
|
||||||
|
|
||||||
|
RUN bundle install && \
|
||||||
|
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
|
||||||
|
# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
|
||||||
|
bundle exec bootsnap precompile -j 1 --gemfile
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Precompile bootsnap code for faster boot times.
|
||||||
|
# -j 1 disable parallel compilation to avoid a QEMU bug: https://github.com/rails/bootsnap/issues/495
|
||||||
|
RUN bundle exec bootsnap precompile -j 1 app/ lib/
|
||||||
|
|
||||||
|
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
|
||||||
|
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Final stage for app image
|
||||||
|
FROM base
|
||||||
|
|
||||||
|
# Run and own only the runtime files as a non-root user for security
|
||||||
|
RUN groupadd --system --gid 1000 rails && \
|
||||||
|
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
|
||||||
|
USER 1000:1000
|
||||||
|
|
||||||
|
# Copy built artifacts: gems, application
|
||||||
|
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
|
||||||
|
COPY --chown=rails:rails --from=build /rails /rails
|
||||||
|
|
||||||
|
# Entrypoint prepares the database.
|
||||||
|
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
|
||||||
|
|
||||||
|
# Start server via Thruster by default, this can be overwritten at runtime
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["./bin/thrust", "./bin/rails", "server"]
|
||||||
46
Dockerfile.dev
Normal file
46
Dockerfile.dev
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
ARG RUBY_VERSION=4.0.1
|
||||||
|
FROM ruby:${RUBY_VERSION}-slim
|
||||||
|
|
||||||
|
RUN apt-get update -qq && \
|
||||||
|
apt-get install --no-install-recommends -y \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
libyaml-dev \
|
||||||
|
postgresql-client \
|
||||||
|
nodejs \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
libjemalloc2 \
|
||||||
|
chromium \
|
||||||
|
chromium-driver \
|
||||||
|
&& rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
|
RUN ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so
|
||||||
|
|
||||||
|
# Install AnyCable Go
|
||||||
|
RUN ARCH=$(uname -m) && \
|
||||||
|
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
|
||||||
|
ANYCABLE_ARCH="arm64"; \
|
||||||
|
else \
|
||||||
|
ANYCABLE_ARCH="amd64"; \
|
||||||
|
fi && \
|
||||||
|
curl -L "https://github.com/anycable/anycable-go/releases/download/v1.5.6/anycable-go-linux-${ANYCABLE_ARCH}" -o /usr/local/bin/anycable-go && \
|
||||||
|
chmod +x /usr/local/bin/anycable-go
|
||||||
|
|
||||||
|
ENV RAILS_ENV=development \
|
||||||
|
BUNDLE_PATH=/usr/local/bundle \
|
||||||
|
RAILS_LOG_TO_STDOUT=true \
|
||||||
|
LD_PRELOAD="/usr/local/lib/libjemalloc.so"
|
||||||
|
|
||||||
|
WORKDIR /rails
|
||||||
|
|
||||||
|
COPY Gemfile Gemfile.lock* ./
|
||||||
|
RUN bundle install && gem install foreman
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN chmod +x bin/*
|
||||||
|
|
||||||
|
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["./bin/rails", "server", "-b", "0.0.0.0", "-p", "3000"]
|
||||||
102
Gemfile
Normal file
102
Gemfile
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
|
||||||
|
gem "rails", github: "rails/rails", branch: "main"
|
||||||
|
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
|
||||||
|
gem "propshaft"
|
||||||
|
# Use postgresql as the database for Active Record
|
||||||
|
gem "pg"
|
||||||
|
# Use the Puma web server [https://github.com/puma/puma]
|
||||||
|
gem "puma", ">= 5.0"
|
||||||
|
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
|
||||||
|
gem "importmap-rails"
|
||||||
|
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
|
||||||
|
gem "turbo-rails"
|
||||||
|
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
|
||||||
|
gem "stimulus-rails"
|
||||||
|
|
||||||
|
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
|
||||||
|
# gem "bcrypt", "~> 3.1.7"
|
||||||
|
|
||||||
|
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||||
|
gem "tzinfo-data", platforms: [:windows, :jruby]
|
||||||
|
|
||||||
|
# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable
|
||||||
|
# gem "solid_cache"
|
||||||
|
# gem "solid_queue"
|
||||||
|
# gem "solid_cable"
|
||||||
|
# gem "mission_control-jobs"
|
||||||
|
|
||||||
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
|
gem "bootsnap", require: false
|
||||||
|
|
||||||
|
# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
|
||||||
|
gem "kamal", require: false
|
||||||
|
|
||||||
|
# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/]
|
||||||
|
gem "thruster", require: false
|
||||||
|
|
||||||
|
# Use Tailwind CSS [https://github.com/rails/tailwindcss-rails]
|
||||||
|
gem "tailwindcss-rails"
|
||||||
|
|
||||||
|
# Translations for Rails standard helpers
|
||||||
|
gem "rails-i18n"
|
||||||
|
|
||||||
|
gem "sidekiq"
|
||||||
|
gem "sidekiq-cron"
|
||||||
|
gem "anycable-rails"
|
||||||
|
gem "redis"
|
||||||
|
|
||||||
|
# Use S3 for Active Storage
|
||||||
|
gem "aws-sdk-s3"
|
||||||
|
|
||||||
|
gem "haml-rails"
|
||||||
|
gem "cancancan"
|
||||||
|
gem "rack-attack"
|
||||||
|
gem "maintenance_tasks"
|
||||||
|
gem "ssrf_filter"
|
||||||
|
|
||||||
|
group :development, :test do
|
||||||
|
gem "parallel_tests"
|
||||||
|
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||||
|
gem "debug", platforms: [:mri, :windows], require: "debug/prelude"
|
||||||
|
|
||||||
|
# Audits gems for known security defects (use config/bundler-audit.yml to ignore issues)
|
||||||
|
gem "bundler-audit", require: false
|
||||||
|
|
||||||
|
# Static analysis for security vulnerabilities [https://brakemanscanner.org/]
|
||||||
|
gem "brakeman", require: false
|
||||||
|
|
||||||
|
# Shopify Ruby styling [https://github.com/Shopify/ruby-style-guide]
|
||||||
|
gem "rubocop-shopify", require: false
|
||||||
|
gem "rubocop-factory_bot", require: false
|
||||||
|
gem "rubocop-rails", require: false
|
||||||
|
gem "rubocop-rspec", require: false
|
||||||
|
gem "rubocop-rspec_rails", require: false
|
||||||
|
gem "rubocop-haml", require: false
|
||||||
|
gem "i18n-tasks", require: false
|
||||||
|
gem "haml_lint", require: false
|
||||||
|
|
||||||
|
gem "flog", require: false
|
||||||
|
gem "flay", require: false
|
||||||
|
gem "reek", require: false
|
||||||
|
gem "skunk", require: false
|
||||||
|
end
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem "web-console"
|
||||||
|
gem "foreman"
|
||||||
|
end
|
||||||
|
|
||||||
|
group :test do
|
||||||
|
gem "rspec-rails"
|
||||||
|
gem "shoulda-matchers"
|
||||||
|
gem "factory_bot_rails"
|
||||||
|
gem "faker"
|
||||||
|
gem "rspec-parameterized"
|
||||||
|
gem "rspec-retry"
|
||||||
|
gem "capybara"
|
||||||
|
gem "selenium-webdriver"
|
||||||
|
end
|
||||||
972
Gemfile.lock
Normal file
972
Gemfile.lock
Normal file
@ -0,0 +1,972 @@
|
|||||||
|
GIT
|
||||||
|
remote: https://github.com/rails/rails.git
|
||||||
|
revision: 706b9daab37664710de20cca273eb9ffa5361ec3
|
||||||
|
branch: main
|
||||||
|
specs:
|
||||||
|
actioncable (8.2.0.alpha)
|
||||||
|
actionpack (= 8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
nio4r (~> 2.0)
|
||||||
|
websocket-driver (>= 0.6.1)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
actionmailbox (8.2.0.alpha)
|
||||||
|
actionpack (= 8.2.0.alpha)
|
||||||
|
activejob (= 8.2.0.alpha)
|
||||||
|
activerecord (= 8.2.0.alpha)
|
||||||
|
activestorage (= 8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
mail (>= 2.8.0)
|
||||||
|
actionmailer (8.2.0.alpha)
|
||||||
|
actionpack (= 8.2.0.alpha)
|
||||||
|
actionview (= 8.2.0.alpha)
|
||||||
|
activejob (= 8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
mail (>= 2.8.0)
|
||||||
|
rails-dom-testing (~> 2.2)
|
||||||
|
actionpack (8.2.0.alpha)
|
||||||
|
actionview (= 8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
nokogiri (>= 1.8.5)
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
rack-session (>= 1.0.1)
|
||||||
|
rack-test (>= 0.6.3)
|
||||||
|
rails-dom-testing (~> 2.2)
|
||||||
|
rails-html-sanitizer (~> 1.7)
|
||||||
|
useragent (~> 0.16)
|
||||||
|
actiontext (8.2.0.alpha)
|
||||||
|
action_text-trix (~> 2.1.16)
|
||||||
|
actionpack (= 8.2.0.alpha)
|
||||||
|
activerecord (= 8.2.0.alpha)
|
||||||
|
activestorage (= 8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
globalid (>= 0.6.0)
|
||||||
|
nokogiri (>= 1.8.5)
|
||||||
|
actionview (8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
builder (~> 3.1)
|
||||||
|
erubi (~> 1.11)
|
||||||
|
rails-dom-testing (~> 2.2)
|
||||||
|
rails-html-sanitizer (~> 1.7)
|
||||||
|
activejob (8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
globalid (>= 0.3.6)
|
||||||
|
activemodel (8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
activerecord (8.2.0.alpha)
|
||||||
|
activemodel (= 8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
timeout (>= 0.4.0)
|
||||||
|
activestorage (8.2.0.alpha)
|
||||||
|
actionpack (= 8.2.0.alpha)
|
||||||
|
activejob (= 8.2.0.alpha)
|
||||||
|
activerecord (= 8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
marcel (~> 1.0)
|
||||||
|
activesupport (8.2.0.alpha)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
|
i18n (>= 1.6, < 2)
|
||||||
|
json
|
||||||
|
logger (>= 1.4.2)
|
||||||
|
minitest (>= 5.1)
|
||||||
|
psych (>= 4)
|
||||||
|
securerandom (>= 0.3)
|
||||||
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
|
uri (>= 0.13.1)
|
||||||
|
rails (8.2.0.alpha)
|
||||||
|
actioncable (= 8.2.0.alpha)
|
||||||
|
actionmailbox (= 8.2.0.alpha)
|
||||||
|
actionmailer (= 8.2.0.alpha)
|
||||||
|
actionpack (= 8.2.0.alpha)
|
||||||
|
actiontext (= 8.2.0.alpha)
|
||||||
|
actionview (= 8.2.0.alpha)
|
||||||
|
activejob (= 8.2.0.alpha)
|
||||||
|
activemodel (= 8.2.0.alpha)
|
||||||
|
activerecord (= 8.2.0.alpha)
|
||||||
|
activestorage (= 8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
bundler (>= 1.15.0)
|
||||||
|
railties (= 8.2.0.alpha)
|
||||||
|
railties (8.2.0.alpha)
|
||||||
|
actionpack (= 8.2.0.alpha)
|
||||||
|
activesupport (= 8.2.0.alpha)
|
||||||
|
irb (~> 1.13)
|
||||||
|
rackup (>= 1.0.0)
|
||||||
|
rake (>= 12.2)
|
||||||
|
thor (~> 1.0, >= 1.2.2)
|
||||||
|
tsort (>= 0.2)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
|
||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
action_text-trix (2.1.18)
|
||||||
|
railties
|
||||||
|
addressable (2.8.9)
|
||||||
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
|
anycable (1.6.3)
|
||||||
|
anycable-core (= 1.6.3)
|
||||||
|
grpc (~> 1.6)
|
||||||
|
anycable-core (1.6.3)
|
||||||
|
anyway_config (~> 2.2)
|
||||||
|
base64 (>= 0.2)
|
||||||
|
google-protobuf (~> 4)
|
||||||
|
stringio (~> 3)
|
||||||
|
anycable-rails (1.6.1)
|
||||||
|
anycable
|
||||||
|
anycable-rails-core (= 1.6.1)
|
||||||
|
anycable-rails-core (1.6.1)
|
||||||
|
actioncable (>= 7.0, < 9.0)
|
||||||
|
anycable-core (~> 1.6.0)
|
||||||
|
globalid
|
||||||
|
anyway_config (2.8.0)
|
||||||
|
ruby-next-core (~> 1.0)
|
||||||
|
ast (2.4.3)
|
||||||
|
aws-eventstream (1.4.0)
|
||||||
|
aws-partitions (1.1231.0)
|
||||||
|
aws-sdk-core (3.244.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
|
jmespath (~> 1, >= 1.6.1)
|
||||||
|
logger
|
||||||
|
aws-sdk-kms (1.123.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sdk-s3 (1.217.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sigv4 (1.12.1)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
axiom-types (0.1.1)
|
||||||
|
descendants_tracker (~> 0.0.4)
|
||||||
|
ice_nine (~> 0.11.0)
|
||||||
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
|
base64 (0.3.0)
|
||||||
|
bcrypt_pbkdf (1.1.2)
|
||||||
|
bigdecimal (4.1.0)
|
||||||
|
bindex (0.8.1)
|
||||||
|
binding_of_caller (2.0.0)
|
||||||
|
debug_inspector (>= 1.2.0)
|
||||||
|
bootsnap (1.23.0)
|
||||||
|
msgpack (~> 1.2)
|
||||||
|
brakeman (8.0.4)
|
||||||
|
racc
|
||||||
|
builder (3.3.0)
|
||||||
|
bundler-audit (0.9.3)
|
||||||
|
bundler (>= 1.2.0)
|
||||||
|
thor (~> 1.0)
|
||||||
|
cancancan (3.6.1)
|
||||||
|
capybara (3.40.0)
|
||||||
|
addressable
|
||||||
|
matrix
|
||||||
|
mini_mime (>= 0.1.3)
|
||||||
|
nokogiri (~> 1.11)
|
||||||
|
rack (>= 1.6.0)
|
||||||
|
rack-test (>= 0.6.3)
|
||||||
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
|
xpath (~> 3.2)
|
||||||
|
childprocess (5.1.0)
|
||||||
|
logger (~> 1.5)
|
||||||
|
coercible (1.0.0)
|
||||||
|
descendants_tracker (~> 0.0.1)
|
||||||
|
concurrent-ruby (1.3.6)
|
||||||
|
connection_pool (3.0.2)
|
||||||
|
crass (1.0.6)
|
||||||
|
cronex (0.15.0)
|
||||||
|
tzinfo
|
||||||
|
unicode (>= 0.4.4.5)
|
||||||
|
csv (3.3.5)
|
||||||
|
date (3.5.1)
|
||||||
|
debug (1.11.1)
|
||||||
|
irb (~> 1.10)
|
||||||
|
reline (>= 0.3.8)
|
||||||
|
debug_inspector (1.2.0)
|
||||||
|
descendants_tracker (0.0.4)
|
||||||
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
|
diff-lcs (1.6.2)
|
||||||
|
docile (1.4.1)
|
||||||
|
dotenv (3.2.0)
|
||||||
|
drb (2.2.3)
|
||||||
|
dry-configurable (1.3.0)
|
||||||
|
dry-core (~> 1.1)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-core (1.2.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
logger
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-inflector (1.3.1)
|
||||||
|
dry-initializer (3.2.0)
|
||||||
|
dry-logic (1.6.0)
|
||||||
|
bigdecimal
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.1)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-schema (1.16.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-configurable (~> 1.0, >= 1.0.1)
|
||||||
|
dry-core (~> 1.1)
|
||||||
|
dry-initializer (~> 3.2)
|
||||||
|
dry-logic (~> 1.6)
|
||||||
|
dry-types (~> 1.9, >= 1.9.1)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
dry-types (1.9.1)
|
||||||
|
bigdecimal (>= 3.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.0)
|
||||||
|
dry-inflector (~> 1.0)
|
||||||
|
dry-logic (~> 1.4)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
|
ed25519 (1.4.0)
|
||||||
|
erb (6.0.2)
|
||||||
|
erubi (1.13.1)
|
||||||
|
et-orbi (1.4.0)
|
||||||
|
tzinfo
|
||||||
|
factory_bot (6.5.6)
|
||||||
|
activesupport (>= 6.1.0)
|
||||||
|
factory_bot_rails (6.5.1)
|
||||||
|
factory_bot (~> 6.5)
|
||||||
|
railties (>= 6.1.0)
|
||||||
|
faker (3.6.1)
|
||||||
|
i18n (>= 1.8.11, < 2)
|
||||||
|
flay (2.14.3)
|
||||||
|
erubi (~> 1.10)
|
||||||
|
path_expander (~> 2.0)
|
||||||
|
prism (~> 1.7)
|
||||||
|
sexp_processor (~> 4.0)
|
||||||
|
flog (4.9.4)
|
||||||
|
path_expander (~> 2.0)
|
||||||
|
prism (~> 1.7)
|
||||||
|
sexp_processor (~> 4.8)
|
||||||
|
foreman (0.90.0)
|
||||||
|
thor (~> 1.4)
|
||||||
|
fugit (1.12.1)
|
||||||
|
et-orbi (~> 1.4)
|
||||||
|
raabro (~> 1.4)
|
||||||
|
globalid (1.3.0)
|
||||||
|
activesupport (>= 6.1)
|
||||||
|
google-protobuf (4.34.1)
|
||||||
|
bigdecimal
|
||||||
|
rake (~> 13.3)
|
||||||
|
google-protobuf (4.34.1-aarch64-linux-gnu)
|
||||||
|
bigdecimal
|
||||||
|
rake (~> 13.3)
|
||||||
|
google-protobuf (4.34.1-aarch64-linux-musl)
|
||||||
|
bigdecimal
|
||||||
|
rake (~> 13.3)
|
||||||
|
google-protobuf (4.34.1-arm64-darwin)
|
||||||
|
bigdecimal
|
||||||
|
rake (~> 13.3)
|
||||||
|
google-protobuf (4.34.1-x86_64-linux-gnu)
|
||||||
|
bigdecimal
|
||||||
|
rake (~> 13.3)
|
||||||
|
google-protobuf (4.34.1-x86_64-linux-musl)
|
||||||
|
bigdecimal
|
||||||
|
rake (~> 13.3)
|
||||||
|
googleapis-common-protos-types (1.22.0)
|
||||||
|
google-protobuf (~> 4.26)
|
||||||
|
grpc (1.78.1)
|
||||||
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
grpc (1.78.1-aarch64-linux-gnu)
|
||||||
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
grpc (1.78.1-aarch64-linux-musl)
|
||||||
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
grpc (1.78.1-arm64-darwin)
|
||||||
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
grpc (1.78.1-x86_64-linux-gnu)
|
||||||
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
grpc (1.78.1-x86_64-linux-musl)
|
||||||
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
haml (7.2.0)
|
||||||
|
temple (>= 0.8.2)
|
||||||
|
thor
|
||||||
|
tilt
|
||||||
|
haml-rails (3.0.0)
|
||||||
|
actionpack (>= 5.1)
|
||||||
|
activesupport (>= 5.1)
|
||||||
|
haml (>= 4.0.6)
|
||||||
|
railties (>= 5.1)
|
||||||
|
haml_lint (0.72.0)
|
||||||
|
haml (>= 5.0)
|
||||||
|
parallel (~> 1.10)
|
||||||
|
rainbow
|
||||||
|
rubocop (>= 1.0)
|
||||||
|
sysexits (~> 1.1)
|
||||||
|
hamli (0.5.1)
|
||||||
|
temple
|
||||||
|
highline (3.1.2)
|
||||||
|
reline
|
||||||
|
i18n (1.14.8)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
i18n-tasks (1.1.2)
|
||||||
|
activesupport (>= 4.0.2)
|
||||||
|
ast (>= 2.1.0)
|
||||||
|
erubi
|
||||||
|
highline (>= 3.0.0)
|
||||||
|
i18n
|
||||||
|
parser (>= 3.2.2.1)
|
||||||
|
prism
|
||||||
|
rails-i18n
|
||||||
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
ruby-progressbar (~> 1.8, >= 1.8.1)
|
||||||
|
terminal-table (>= 1.5.1)
|
||||||
|
ice_nine (0.11.2)
|
||||||
|
importmap-rails (2.2.3)
|
||||||
|
actionpack (>= 6.0.0)
|
||||||
|
activesupport (>= 6.0.0)
|
||||||
|
railties (>= 6.0.0)
|
||||||
|
io-console (0.8.2)
|
||||||
|
irb (1.17.0)
|
||||||
|
pp (>= 0.6.0)
|
||||||
|
prism (>= 1.3.0)
|
||||||
|
rdoc (>= 4.0.0)
|
||||||
|
reline (>= 0.4.2)
|
||||||
|
jmespath (1.6.2)
|
||||||
|
job-iteration (1.13.0)
|
||||||
|
activejob (>= 7.0)
|
||||||
|
json (2.19.3)
|
||||||
|
kamal (2.11.0)
|
||||||
|
activesupport (>= 7.0)
|
||||||
|
base64 (~> 0.2)
|
||||||
|
bcrypt_pbkdf (~> 1.0)
|
||||||
|
concurrent-ruby (~> 1.2)
|
||||||
|
dotenv (~> 3.1)
|
||||||
|
ed25519 (~> 1.4)
|
||||||
|
net-ssh (~> 7.3)
|
||||||
|
sshkit (>= 1.23.0, < 2.0)
|
||||||
|
thor (~> 1.3)
|
||||||
|
zeitwerk (>= 2.6.18, < 3.0)
|
||||||
|
language_server-protocol (3.17.0.5)
|
||||||
|
launchy (3.1.1)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
childprocess (~> 5.0)
|
||||||
|
logger (~> 1.6)
|
||||||
|
lint_roller (1.1.0)
|
||||||
|
logger (1.7.0)
|
||||||
|
loofah (2.25.1)
|
||||||
|
crass (~> 1.0.2)
|
||||||
|
nokogiri (>= 1.12.0)
|
||||||
|
mail (2.9.0)
|
||||||
|
logger
|
||||||
|
mini_mime (>= 0.1.1)
|
||||||
|
net-imap
|
||||||
|
net-pop
|
||||||
|
net-smtp
|
||||||
|
maintenance_tasks (2.14.0)
|
||||||
|
actionpack (>= 7.1)
|
||||||
|
activejob (>= 7.1)
|
||||||
|
activerecord (>= 7.1)
|
||||||
|
csv
|
||||||
|
job-iteration (>= 1.3.6)
|
||||||
|
railties (>= 7.1)
|
||||||
|
zeitwerk (>= 2.6.2)
|
||||||
|
marcel (1.1.0)
|
||||||
|
matrix (0.4.3)
|
||||||
|
mini_mime (1.1.5)
|
||||||
|
minitest (6.0.2)
|
||||||
|
drb (~> 2.0)
|
||||||
|
prism (~> 1.5)
|
||||||
|
msgpack (1.8.0)
|
||||||
|
net-imap (0.6.3)
|
||||||
|
date
|
||||||
|
net-protocol
|
||||||
|
net-pop (0.1.2)
|
||||||
|
net-protocol
|
||||||
|
net-protocol (0.2.2)
|
||||||
|
timeout
|
||||||
|
net-scp (4.1.0)
|
||||||
|
net-ssh (>= 2.6.5, < 8.0.0)
|
||||||
|
net-sftp (4.0.0)
|
||||||
|
net-ssh (>= 5.0.0, < 8.0.0)
|
||||||
|
net-smtp (0.5.1)
|
||||||
|
net-protocol
|
||||||
|
net-ssh (7.3.2)
|
||||||
|
nio4r (2.7.5)
|
||||||
|
nokogiri (1.19.2-aarch64-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.2-aarch64-linux-musl)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.2-arm-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.2-arm-linux-musl)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.2-arm64-darwin)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.2-x86_64-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.2-x86_64-linux-musl)
|
||||||
|
racc (~> 1.4)
|
||||||
|
ostruct (0.6.3)
|
||||||
|
parallel (1.27.0)
|
||||||
|
parallel_tests (5.6.0)
|
||||||
|
parallel
|
||||||
|
parser (3.3.11.1)
|
||||||
|
ast (~> 2.4.1)
|
||||||
|
racc
|
||||||
|
path_expander (2.0.1)
|
||||||
|
pg (1.6.3)
|
||||||
|
pg (1.6.3-aarch64-linux)
|
||||||
|
pg (1.6.3-aarch64-linux-musl)
|
||||||
|
pg (1.6.3-arm64-darwin)
|
||||||
|
pg (1.6.3-x86_64-linux)
|
||||||
|
pg (1.6.3-x86_64-linux-musl)
|
||||||
|
pp (0.6.3)
|
||||||
|
prettyprint
|
||||||
|
prettyprint (0.2.0)
|
||||||
|
prism (1.9.0)
|
||||||
|
proc_to_ast (0.2.0)
|
||||||
|
parser
|
||||||
|
rouge
|
||||||
|
unparser
|
||||||
|
propshaft (1.3.1)
|
||||||
|
actionpack (>= 7.0.0)
|
||||||
|
activesupport (>= 7.0.0)
|
||||||
|
rack
|
||||||
|
psych (5.3.1)
|
||||||
|
date
|
||||||
|
stringio
|
||||||
|
public_suffix (7.0.5)
|
||||||
|
puma (7.2.0)
|
||||||
|
nio4r (~> 2.0)
|
||||||
|
raabro (1.4.0)
|
||||||
|
racc (1.8.1)
|
||||||
|
rack (3.2.5)
|
||||||
|
rack-attack (6.8.0)
|
||||||
|
rack (>= 1.0, < 4)
|
||||||
|
rack-session (2.1.1)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
|
rack (>= 3.0.0)
|
||||||
|
rack-test (2.2.0)
|
||||||
|
rack (>= 1.3)
|
||||||
|
rackup (2.3.1)
|
||||||
|
rack (>= 3)
|
||||||
|
rails-dom-testing (2.3.0)
|
||||||
|
activesupport (>= 5.0.0)
|
||||||
|
minitest
|
||||||
|
nokogiri (>= 1.6)
|
||||||
|
rails-html-sanitizer (1.7.0)
|
||||||
|
loofah (~> 2.25)
|
||||||
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||||
|
rails-i18n (8.1.0)
|
||||||
|
i18n (>= 0.7, < 2)
|
||||||
|
railties (>= 8.0.0, < 9)
|
||||||
|
rainbow (3.1.1)
|
||||||
|
rake (13.3.1)
|
||||||
|
rdoc (7.2.0)
|
||||||
|
erb
|
||||||
|
psych (>= 4.0.0)
|
||||||
|
tsort
|
||||||
|
redis (5.4.1)
|
||||||
|
redis-client (>= 0.22.0)
|
||||||
|
redis-client (0.28.0)
|
||||||
|
connection_pool
|
||||||
|
reek (6.5.0)
|
||||||
|
dry-schema (~> 1.13)
|
||||||
|
logger (~> 1.6)
|
||||||
|
parser (~> 3.3.0)
|
||||||
|
rainbow (>= 2.0, < 4.0)
|
||||||
|
rexml (~> 3.1)
|
||||||
|
regexp_parser (2.11.3)
|
||||||
|
reline (0.6.3)
|
||||||
|
io-console (~> 0.5)
|
||||||
|
rexml (3.4.4)
|
||||||
|
rouge (4.7.0)
|
||||||
|
rspec (3.13.2)
|
||||||
|
rspec-core (~> 3.13.0)
|
||||||
|
rspec-expectations (~> 3.13.0)
|
||||||
|
rspec-mocks (~> 3.13.0)
|
||||||
|
rspec-core (3.13.6)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-expectations (3.13.5)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-mocks (3.13.8)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-parameterized (2.0.1)
|
||||||
|
rspec-parameterized-core (>= 2, < 3)
|
||||||
|
rspec-parameterized-table_syntax (>= 2, < 3)
|
||||||
|
rspec-parameterized-core (2.0.1)
|
||||||
|
parser
|
||||||
|
prism
|
||||||
|
proc_to_ast (>= 0.2.0)
|
||||||
|
rspec (>= 2.13, < 4)
|
||||||
|
unparser
|
||||||
|
rspec-parameterized-table_syntax (2.1.1)
|
||||||
|
binding_of_caller (>= 2)
|
||||||
|
rspec-parameterized-core (>= 2, < 3)
|
||||||
|
rspec-rails (8.0.4)
|
||||||
|
actionpack (>= 7.2)
|
||||||
|
activesupport (>= 7.2)
|
||||||
|
railties (>= 7.2)
|
||||||
|
rspec-core (>= 3.13.0, < 5.0.0)
|
||||||
|
rspec-expectations (>= 3.13.0, < 5.0.0)
|
||||||
|
rspec-mocks (>= 3.13.0, < 5.0.0)
|
||||||
|
rspec-support (>= 3.13.0, < 5.0.0)
|
||||||
|
rspec-retry (0.6.2)
|
||||||
|
rspec-core (> 3.3)
|
||||||
|
rspec-support (3.13.7)
|
||||||
|
rubocop (1.86.0)
|
||||||
|
json (~> 2.3)
|
||||||
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
lint_roller (~> 1.1.0)
|
||||||
|
parallel (~> 1.10)
|
||||||
|
parser (>= 3.3.0.2)
|
||||||
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
|
rubocop-ast (>= 1.49.0, < 2.0)
|
||||||
|
ruby-progressbar (~> 1.7)
|
||||||
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
|
rubocop-ast (1.49.1)
|
||||||
|
parser (>= 3.3.7.2)
|
||||||
|
prism (~> 1.7)
|
||||||
|
rubocop-factory_bot (2.28.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
|
rubocop-haml (0.3.1)
|
||||||
|
hamli (~> 0.5)
|
||||||
|
lint_roller (>= 1.1)
|
||||||
|
rubocop (>= 1.72.1)
|
||||||
|
rubocop-rails (2.34.3)
|
||||||
|
activesupport (>= 4.2.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rack (>= 1.1)
|
||||||
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
|
rubocop-rspec (3.9.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.81)
|
||||||
|
rubocop-rspec_rails (2.32.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
|
rubocop-rspec (~> 3.5)
|
||||||
|
rubocop-shopify (2.18.0)
|
||||||
|
rubocop (~> 1.62)
|
||||||
|
ruby-next-core (1.2.0)
|
||||||
|
ruby-progressbar (1.13.0)
|
||||||
|
ruby_parser (3.22.0)
|
||||||
|
racc (~> 1.5)
|
||||||
|
sexp_processor (~> 4.16)
|
||||||
|
rubycritic (4.12.0)
|
||||||
|
flay (~> 2.13)
|
||||||
|
flog (~> 4.7)
|
||||||
|
launchy (>= 2.5.2)
|
||||||
|
parser (>= 3.3.0.5)
|
||||||
|
prism (>= 1.6.0)
|
||||||
|
rainbow (~> 3.1.1)
|
||||||
|
reek (~> 6.5.0, < 7.0)
|
||||||
|
rexml
|
||||||
|
ruby_parser (~> 3.21)
|
||||||
|
simplecov (>= 0.22.0)
|
||||||
|
tty-which (~> 0.5.0)
|
||||||
|
virtus (~> 2.0)
|
||||||
|
rubyzip (3.2.2)
|
||||||
|
securerandom (0.4.1)
|
||||||
|
selenium-webdriver (4.41.0)
|
||||||
|
base64 (~> 0.2)
|
||||||
|
logger (~> 1.4)
|
||||||
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
|
rubyzip (>= 1.2.2, < 4.0)
|
||||||
|
websocket (~> 1.0)
|
||||||
|
sexp_processor (4.17.5)
|
||||||
|
shoulda-matchers (7.0.1)
|
||||||
|
activesupport (>= 7.1)
|
||||||
|
sidekiq (8.1.2)
|
||||||
|
connection_pool (>= 3.0.0)
|
||||||
|
json (>= 2.16.0)
|
||||||
|
logger (>= 1.7.0)
|
||||||
|
rack (>= 3.2.0)
|
||||||
|
redis-client (>= 0.26.0)
|
||||||
|
sidekiq-cron (2.3.1)
|
||||||
|
cronex (>= 0.13.0)
|
||||||
|
fugit (~> 1.8, >= 1.11.1)
|
||||||
|
globalid (>= 1.0.1)
|
||||||
|
sidekiq (>= 6.5.0)
|
||||||
|
simplecov (0.22.0)
|
||||||
|
docile (~> 1.1)
|
||||||
|
simplecov-html (~> 0.11)
|
||||||
|
simplecov_json_formatter (~> 0.1)
|
||||||
|
simplecov-html (0.13.2)
|
||||||
|
simplecov_json_formatter (0.1.4)
|
||||||
|
skunk (0.5.4)
|
||||||
|
rubycritic (>= 4.5.2, < 5.0)
|
||||||
|
terminal-table (~> 3.0)
|
||||||
|
sshkit (1.25.0)
|
||||||
|
base64
|
||||||
|
logger
|
||||||
|
net-scp (>= 1.1.2)
|
||||||
|
net-sftp (>= 2.1.2)
|
||||||
|
net-ssh (>= 2.8.0)
|
||||||
|
ostruct
|
||||||
|
ssrf_filter (1.3.0)
|
||||||
|
stimulus-rails (1.3.4)
|
||||||
|
railties (>= 6.0.0)
|
||||||
|
stringio (3.2.0)
|
||||||
|
sysexits (1.2.0)
|
||||||
|
tailwindcss-rails (4.4.0)
|
||||||
|
railties (>= 7.0.0)
|
||||||
|
tailwindcss-ruby (~> 4.0)
|
||||||
|
tailwindcss-ruby (4.2.1)
|
||||||
|
tailwindcss-ruby (4.2.1-aarch64-linux-gnu)
|
||||||
|
tailwindcss-ruby (4.2.1-aarch64-linux-musl)
|
||||||
|
tailwindcss-ruby (4.2.1-arm64-darwin)
|
||||||
|
tailwindcss-ruby (4.2.1-x86_64-linux-gnu)
|
||||||
|
tailwindcss-ruby (4.2.1-x86_64-linux-musl)
|
||||||
|
temple (0.10.4)
|
||||||
|
terminal-table (3.0.2)
|
||||||
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
|
thor (1.5.0)
|
||||||
|
thread_safe (0.3.6)
|
||||||
|
thruster (0.1.20)
|
||||||
|
thruster (0.1.20-aarch64-linux)
|
||||||
|
thruster (0.1.20-arm64-darwin)
|
||||||
|
thruster (0.1.20-x86_64-linux)
|
||||||
|
tilt (2.7.0)
|
||||||
|
timeout (0.6.1)
|
||||||
|
tsort (0.2.0)
|
||||||
|
tty-which (0.5.0)
|
||||||
|
turbo-rails (2.0.23)
|
||||||
|
actionpack (>= 7.1.0)
|
||||||
|
railties (>= 7.1.0)
|
||||||
|
tzinfo (2.0.6)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
unicode (0.4.4.5)
|
||||||
|
unicode-display_width (2.6.0)
|
||||||
|
unparser (0.9.0)
|
||||||
|
diff-lcs (>= 1.6, < 3)
|
||||||
|
parser (>= 3.3.0)
|
||||||
|
prism (>= 1.5.1)
|
||||||
|
uri (1.1.1)
|
||||||
|
useragent (0.16.11)
|
||||||
|
virtus (2.0.0)
|
||||||
|
axiom-types (~> 0.1)
|
||||||
|
coercible (~> 1.0)
|
||||||
|
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||||
|
web-console (4.3.0)
|
||||||
|
actionview (>= 8.0.0)
|
||||||
|
bindex (>= 0.4.0)
|
||||||
|
railties (>= 8.0.0)
|
||||||
|
websocket (1.2.11)
|
||||||
|
websocket-driver (0.8.0)
|
||||||
|
base64
|
||||||
|
websocket-extensions (>= 0.1.0)
|
||||||
|
websocket-extensions (0.1.5)
|
||||||
|
xpath (3.2.0)
|
||||||
|
nokogiri (~> 1.8)
|
||||||
|
zeitwerk (2.7.5)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
aarch64-linux-gnu
|
||||||
|
aarch64-linux-musl
|
||||||
|
arm-linux-gnu
|
||||||
|
arm-linux-musl
|
||||||
|
arm64-darwin-24
|
||||||
|
x86_64-linux
|
||||||
|
x86_64-linux-gnu
|
||||||
|
x86_64-linux-musl
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
anycable-rails
|
||||||
|
aws-sdk-s3
|
||||||
|
bootsnap
|
||||||
|
brakeman
|
||||||
|
bundler-audit
|
||||||
|
cancancan
|
||||||
|
capybara
|
||||||
|
debug
|
||||||
|
factory_bot_rails
|
||||||
|
faker
|
||||||
|
flay
|
||||||
|
flog
|
||||||
|
foreman
|
||||||
|
haml-rails
|
||||||
|
haml_lint
|
||||||
|
i18n-tasks
|
||||||
|
importmap-rails
|
||||||
|
kamal
|
||||||
|
maintenance_tasks
|
||||||
|
parallel_tests
|
||||||
|
pg
|
||||||
|
propshaft
|
||||||
|
puma (>= 5.0)
|
||||||
|
rack-attack
|
||||||
|
rails!
|
||||||
|
rails-i18n
|
||||||
|
redis
|
||||||
|
reek
|
||||||
|
rspec-parameterized
|
||||||
|
rspec-rails
|
||||||
|
rspec-retry
|
||||||
|
rubocop-factory_bot
|
||||||
|
rubocop-haml
|
||||||
|
rubocop-rails
|
||||||
|
rubocop-rspec
|
||||||
|
rubocop-rspec_rails
|
||||||
|
rubocop-shopify
|
||||||
|
selenium-webdriver
|
||||||
|
shoulda-matchers
|
||||||
|
sidekiq
|
||||||
|
sidekiq-cron
|
||||||
|
skunk
|
||||||
|
ssrf_filter
|
||||||
|
stimulus-rails
|
||||||
|
tailwindcss-rails
|
||||||
|
thruster
|
||||||
|
turbo-rails
|
||||||
|
tzinfo-data
|
||||||
|
web-console
|
||||||
|
|
||||||
|
CHECKSUMS
|
||||||
|
action_text-trix (2.1.18) sha256=3fdb83f8bff4145d098be283cdd47ac41caf5110bfa6df4695ed7127d7fb3642
|
||||||
|
actioncable (8.2.0.alpha)
|
||||||
|
actionmailbox (8.2.0.alpha)
|
||||||
|
actionmailer (8.2.0.alpha)
|
||||||
|
actionpack (8.2.0.alpha)
|
||||||
|
actiontext (8.2.0.alpha)
|
||||||
|
actionview (8.2.0.alpha)
|
||||||
|
activejob (8.2.0.alpha)
|
||||||
|
activemodel (8.2.0.alpha)
|
||||||
|
activerecord (8.2.0.alpha)
|
||||||
|
activestorage (8.2.0.alpha)
|
||||||
|
activesupport (8.2.0.alpha)
|
||||||
|
addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485
|
||||||
|
anycable (1.6.3) sha256=30092c7687c5670c240d1809f417565829e8b6fabd19975219ef28713da0217f
|
||||||
|
anycable-core (1.6.3) sha256=12f0398509b69e51ee6488bf3724ee2c97f01dfc279001feef21242fc51feca3
|
||||||
|
anycable-rails (1.6.1) sha256=b924a83cb30c13f71688e73c75aa7c9a0d1fe364306fee04d4932e26e77396d8
|
||||||
|
anycable-rails-core (1.6.1) sha256=97fd349dad1bb1f5fa45ab630cb31ba521af0b12e8c64b46a451043f7170ae7d
|
||||||
|
anyway_config (2.8.0) sha256=f6797a7231f81202dcd3d0c07284e836e45713e761d320180348b13a5c7c9306
|
||||||
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
||||||
|
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
|
||||||
|
aws-partitions (1.1231.0) sha256=5142ba78d5226deb5dff6c8d17dfc52b0bcec3451d47fcb70fbe8b744b85d004
|
||||||
|
aws-sdk-core (3.244.0) sha256=3e458c078b0c5bdee95bc370c3a483374b3224cf730c1f9f0faf849a5d9a18ea
|
||||||
|
aws-sdk-kms (1.123.0) sha256=d405f37e82f8fa32045ca8980be266c0b45b37aaf2012afe0254321a1e811f20
|
||||||
|
aws-sdk-s3 (1.217.0) sha256=6ea709272c666888b14e9c62345abd9a6a967759ae13667c28f01fde6823c24b
|
||||||
|
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
|
||||||
|
axiom-types (0.1.1) sha256=c1ff113f3de516fa195b2db7e0a9a95fd1b08475a502ff660d04507a09980383
|
||||||
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
||||||
|
bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6
|
||||||
|
bigdecimal (4.1.0) sha256=6dc07767aa3dc456ccd48e7ae70a07b474e9afd7c5bc576f80bd6da5c8dd6cae
|
||||||
|
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
|
||||||
|
binding_of_caller (2.0.0) sha256=e53eebf8428a85587aede7b43a83e7419eb85b8b690938a96aa330d03052d517
|
||||||
|
bootsnap (1.23.0) sha256=c1254f458d58558b58be0f8eb8f6eec2821456785b7cdd1e16248e2020d3f214
|
||||||
|
brakeman (8.0.4) sha256=7bf921fa9638544835df9aa7b3e720a9a72c0267f34f92135955edd80d4dcf6f
|
||||||
|
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
|
||||||
|
bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
|
||||||
|
cancancan (3.6.1) sha256=975c1d5cbf58d5df48a9452a7f61ae3d254608cd87570402f5925a8864c56b62
|
||||||
|
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
|
||||||
|
childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec
|
||||||
|
coercible (1.0.0) sha256=5081ad24352cc8435ce5472bc2faa30260c7ea7f2102cc6a9f167c4d9bffaadc
|
||||||
|
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
||||||
|
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
||||||
|
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
|
||||||
|
cronex (0.15.0) sha256=21c794e085fad2951c4f2e279f440340a35ba2297e0b738f22f263f69fbe2186
|
||||||
|
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
|
||||||
|
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
|
||||||
|
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
|
||||||
|
debug_inspector (1.2.0) sha256=9bdfa02eebc3da163833e6a89b154084232f5766087e59573b70521c77ea68a2
|
||||||
|
descendants_tracker (0.0.4) sha256=e9c41dd4cfbb85829a9301ea7e7c48c2a03b26f09319db230e6479ccdc780897
|
||||||
|
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
||||||
|
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
||||||
|
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
|
||||||
|
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
||||||
|
dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8
|
||||||
|
dry-core (1.2.0) sha256=0cc5a7da88df397f153947eeeae42e876e999c1e30900f3c536fb173854e96a1
|
||||||
|
dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc
|
||||||
|
dry-initializer (3.2.0) sha256=37d59798f912dc0a1efe14a4db4a9306989007b302dcd5f25d0a2a20c166c4e3
|
||||||
|
dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2
|
||||||
|
dry-schema (1.16.0) sha256=cd3aaeabc0f1af66ec82a29096d4c4fb92a0a58b9dae29a22b1bbceb78985727
|
||||||
|
dry-types (1.9.1) sha256=baebeecdb9f8395d6c9d227b62011279440943e3ef2468fe8ccc1ba11467f178
|
||||||
|
ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506
|
||||||
|
erb (6.0.2) sha256=9fe6264d44f79422c87490a1558479bd0e7dad4dd0e317656e67ea3077b5242b
|
||||||
|
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
|
||||||
|
et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc
|
||||||
|
factory_bot (6.5.6) sha256=12beb373214dccc086a7a63763d6718c49769d5606f0501e0a4442676917e077
|
||||||
|
factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68
|
||||||
|
faker (3.6.1) sha256=c513d23c1d26b426283e913b25e0b714576011d7c1ad368a9ac6ea2113a1ccde
|
||||||
|
flay (2.14.3) sha256=7f96a495f4bde880460e77e7345464e752bd44f09f5cd30c80af02afe0ed3f29
|
||||||
|
flog (4.9.4) sha256=12cc054fab7a2cbd2a906514397c4d7788954d530564782d6f14939dc2dfbcbb
|
||||||
|
foreman (0.90.0) sha256=ff675e2d47b607ac58714a6d4ac3e1ee8f06f41d8db084531c31961e2c3f117c
|
||||||
|
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
|
||||||
|
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
|
||||||
|
google-protobuf (4.34.1) sha256=347181542b8d659c60f028fa3791c9cccce651a91ad27782dbc5c5e374796cdc
|
||||||
|
google-protobuf (4.34.1-aarch64-linux-gnu) sha256=f9c07607dc139c895f2792a7740fcd01cd94d4d7b0e0a939045b50d7999f0b1d
|
||||||
|
google-protobuf (4.34.1-aarch64-linux-musl) sha256=db58e5a4a492b43c6614486aea31b7fb86955b175d1d48f28ebf388f058d78a9
|
||||||
|
google-protobuf (4.34.1-arm64-darwin) sha256=2745061f973119e6e7f3c81a0c77025d291a3caa6585a2cd24a25bbc7bedb267
|
||||||
|
google-protobuf (4.34.1-x86_64-linux-gnu) sha256=87088c9fd8e47b5b40ca498fc1195add6149e941ff7e81c532a5b0b8876d4cc9
|
||||||
|
google-protobuf (4.34.1-x86_64-linux-musl) sha256=8c0e91436fbe504ffc64f0bd621f2e69adbcce8ed2c58439d7a21117069cfdd7
|
||||||
|
googleapis-common-protos-types (1.22.0) sha256=f97492b77bd6da0018c860d5004f512fe7cd165554d7019a8f4df6a56fbfc4c7
|
||||||
|
grpc (1.78.1) sha256=c9e8a54cf901263c25c1e17af116f9145fbfdf4b69931234581b0be2059f6758
|
||||||
|
grpc (1.78.1-aarch64-linux-gnu) sha256=11597d75fd0efc855a5d8898867d00179ad215ddca2ec53ddb4d1b3a9bf313a4
|
||||||
|
grpc (1.78.1-aarch64-linux-musl) sha256=ba7d5b82cf935ffd2476196b53f5abce152f1e640fb2011d937a2a6965686e3b
|
||||||
|
grpc (1.78.1-arm64-darwin) sha256=7378b58db8dc65b19dce8006a47c0da30f1faf5d07160d2e1816c31126d3a388
|
||||||
|
grpc (1.78.1-x86_64-linux-gnu) sha256=c0715fe9bc80cf21fbaca2713abf8db6f628f6f31ec1d8762b667151e6ab0c9f
|
||||||
|
grpc (1.78.1-x86_64-linux-musl) sha256=4e90a3d0f2baa1cd70756ffe116bd81bb7b398ff774272828a23a89ac9a30b06
|
||||||
|
haml (7.2.0) sha256=87fd2b71f7feab1724337b090a7d767f5ab2d42f08c974f3ead673f18cfcd55a
|
||||||
|
haml-rails (3.0.0) sha256=091fe496a85ca521d9cac87fb50f1ecf77a57974a99aa7e267130add81a5585f
|
||||||
|
haml_lint (0.72.0) sha256=23acb3f5db1602eb99bccec8009372465321702e1229017cca53272957bfc9c8
|
||||||
|
hamli (0.5.1) sha256=575321dde83636a7ff5a4f0f7d9d48d59384b6388a2e2b1cfb1d8c3769a616cb
|
||||||
|
highline (3.1.2) sha256=67cbd34d19f6ef11a7ee1d82ffab5d36dfd5b3be861f450fc1716c7125f4bb4a
|
||||||
|
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
||||||
|
i18n-tasks (1.1.2) sha256=4dcfba49e52a623f30661cb316cb80d84fbba5cb8c6d88ef5e02545fffa3637a
|
||||||
|
ice_nine (0.11.2) sha256=5d506a7d2723d5592dc121b9928e4931742730131f22a1a37649df1c1e2e63db
|
||||||
|
importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a
|
||||||
|
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
||||||
|
irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
|
||||||
|
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
|
||||||
|
job-iteration (1.13.0) sha256=3300844e81309fbd06fd2310d6aa8e1f43bf30fe03a3fc5067580b62f456b7e1
|
||||||
|
json (2.19.3) sha256=289b0bb53052a1fa8c34ab33cc750b659ba14a5c45f3fcf4b18762dc67c78646
|
||||||
|
kamal (2.11.0) sha256=1408864425e0dec7e0a14d712a3b13f614e9f3a425b7661d3f9d287a51d7dd75
|
||||||
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
||||||
|
launchy (3.1.1) sha256=72b847b5cc961589dde2c395af0108c86ff0119f42d4648d25b5440ebb10059e
|
||||||
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
||||||
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
||||||
|
loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04
|
||||||
|
mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941
|
||||||
|
maintenance_tasks (2.14.0) sha256=7331852afdc6d38dd604166cd47ccbb4bd418df1f5aaf707810634ed1255769b
|
||||||
|
marcel (1.1.0) sha256=fdcfcfa33cc52e93c4308d40e4090a5d4ea279e160a7f6af988260fa970e0bee
|
||||||
|
matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b
|
||||||
|
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
|
||||||
|
minitest (6.0.2) sha256=db6e57956f6ecc6134683b4c87467d6dd792323c7f0eea7b93f66bd284adbc3d
|
||||||
|
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
|
||||||
|
net-imap (0.6.3) sha256=9bab75f876596d09ee7bf911a291da478e0cd6badc54dfb82874855ccc82f2ad
|
||||||
|
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
|
||||||
|
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
|
||||||
|
net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d
|
||||||
|
net-sftp (4.0.0) sha256=65bb91c859c2f93b09826757af11b69af931a3a9155050f50d1b06d384526364
|
||||||
|
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
|
||||||
|
net-ssh (7.3.2) sha256=65029e213c380e20e5fd92ece663934ab0a0fe888e0cd7cc6a5b664074362dd4
|
||||||
|
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
|
||||||
|
nokogiri (1.19.2-aarch64-linux-gnu) sha256=c34d5c8208025587554608e98fd88ab125b29c80f9352b821964e9a5d5cfbd19
|
||||||
|
nokogiri (1.19.2-aarch64-linux-musl) sha256=7f6b4b0202d507326841a4f790294bf75098aef50c7173443812e3ac5cb06515
|
||||||
|
nokogiri (1.19.2-arm-linux-gnu) sha256=b7fa1139016f3dc850bda1260988f0d749934a939d04ef2da13bec060d7d5081
|
||||||
|
nokogiri (1.19.2-arm-linux-musl) sha256=61114d44f6742ff72194a1b3020967201e2eb982814778d130f6471c11f9828c
|
||||||
|
nokogiri (1.19.2-arm64-darwin) sha256=58d8ea2e31a967b843b70487a44c14c8ba1866daa1b9da9be9dbdf1b43dee205
|
||||||
|
nokogiri (1.19.2-x86_64-linux-gnu) sha256=fa8feca882b73e871a9845f3817a72e9734c8e974bdc4fbad6e4bc6e8076b94f
|
||||||
|
nokogiri (1.19.2-x86_64-linux-musl) sha256=93128448e61a9383a30baef041bf1f5817e22f297a1d400521e90294445069a8
|
||||||
|
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
|
||||||
|
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
|
||||||
|
parallel_tests (5.6.0) sha256=b2d7af382d1c0289daba65308d9143b3ad82d817d500c0ad1b4bde468beb13af
|
||||||
|
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
|
||||||
|
path_expander (2.0.1) sha256=2de201164bff4719cc4d0b3767286e9977cc832a59c4d70abab571ec86cb41e4
|
||||||
|
pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99
|
||||||
|
pg (1.6.3-aarch64-linux) sha256=0698ad563e02383c27510b76bf7d4cd2de19cd1d16a5013f375dd473e4be72ea
|
||||||
|
pg (1.6.3-aarch64-linux-musl) sha256=06a75f4ea04b05140146f2a10550b8e0d9f006a79cdaf8b5b130cde40e3ecc2c
|
||||||
|
pg (1.6.3-arm64-darwin) sha256=7240330b572e6355d7c75a7de535edb5dfcbd6295d9c7777df4d9dddfb8c0e5f
|
||||||
|
pg (1.6.3-x86_64-linux) sha256=5d9e188c8f7a0295d162b7b88a768d8452a899977d44f3274d1946d67920ae8d
|
||||||
|
pg (1.6.3-x86_64-linux-musl) sha256=9c9c90d98c72f78eb04c0f55e9618fe55d1512128e411035fe229ff427864009
|
||||||
|
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
|
||||||
|
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
||||||
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
||||||
|
proc_to_ast (0.2.0) sha256=4bb446419c3878c21d8792f8a129616690168f636b9e460b5a0ed26dd6680bbe
|
||||||
|
propshaft (1.3.1) sha256=9acc664ef67e819ffa3d95bd7ad4c3623ea799110c5f4dee67fa7e583e74c392
|
||||||
|
psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
|
||||||
|
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
|
||||||
|
puma (7.2.0) sha256=bf8ef4ab514a4e6d4554cb4326b2004eba5036ae05cf765cfe51aba9706a72a8
|
||||||
|
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
|
||||||
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
||||||
|
rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3
|
||||||
|
rack-attack (6.8.0) sha256=f2499fdebf85bcc05573a22dff57d24305ac14ec2e4156cd3c28d47cafeeecf2
|
||||||
|
rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
|
||||||
|
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
|
||||||
|
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
||||||
|
rails (8.2.0.alpha)
|
||||||
|
rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d
|
||||||
|
rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89
|
||||||
|
rails-i18n (8.1.0) sha256=52d5fd6c0abef28d84223cc05647f6ae0fd552637a1ede92deee9545755b6cf3
|
||||||
|
railties (8.2.0.alpha)
|
||||||
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
||||||
|
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
|
||||||
|
rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
|
||||||
|
redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
|
||||||
|
redis-client (0.28.0) sha256=888892f9cd8787a41c0ece00bdf5f556dfff7770326ce40bb2bc11f1bfec824b
|
||||||
|
reek (6.5.0) sha256=d26d3a492773b2bbc228888067a21afe33ac07954a17dbd64cdeae42c4c69be1
|
||||||
|
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
||||||
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
||||||
|
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
||||||
|
rouge (4.7.0) sha256=dba5896715c0325c362e895460a6d350803dbf6427454f49a47500f3193ea739
|
||||||
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
||||||
|
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
||||||
|
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
||||||
|
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
|
||||||
|
rspec-parameterized (2.0.1) sha256=5507c3bde0e0f1d6c7183e4936ddf1646a898c00db20c12d03984a8ea25a567c
|
||||||
|
rspec-parameterized-core (2.0.1) sha256=4f93067b89b8043357e0589d9302a9c856f100e498faf7e49df9094aa3e5211a
|
||||||
|
rspec-parameterized-table_syntax (2.1.1) sha256=59cc68506162adef218b59952bf62142fd25079d01061f4ff115dcddd91a49df
|
||||||
|
rspec-rails (8.0.4) sha256=06235692fc0892683d3d34977e081db867434b3a24ae0dd0c6f3516bad4e22df
|
||||||
|
rspec-retry (0.6.2) sha256=6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858
|
||||||
|
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
|
||||||
|
rubocop (1.86.0) sha256=4ff1186fe16ebe9baff5e7aad66bb0ad4cabf5cdcd419f773146dbba2565d186
|
||||||
|
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
|
||||||
|
rubocop-factory_bot (2.28.0) sha256=4b17fc02124444173317e131759d195b0d762844a71a29fe8139c1105d92f0cb
|
||||||
|
rubocop-haml (0.3.1) sha256=f9125a3996079e3c93ed002cd420fd7e8744a01d8e007eafa0bf0350f6b24c96
|
||||||
|
rubocop-rails (2.34.3) sha256=10d37989024865ecda8199f311f3faca990143fbac967de943f88aca11eb9ad2
|
||||||
|
rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2
|
||||||
|
rubocop-rspec_rails (2.32.0) sha256=4a0d641c72f6ebb957534f539d9d0a62c47abd8ce0d0aeee1ef4701e892a9100
|
||||||
|
rubocop-shopify (2.18.0) sha256=dafa25e5617ce4600ff86b1de3d5b78e43ab3d58cc5729df38e492b8e10294eb
|
||||||
|
ruby-next-core (1.2.0) sha256=f6a7d00bb5186cecbb02f7f1845a0f3a2c9788d35b6ccff5c9be3f0d46799b86
|
||||||
|
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
||||||
|
ruby_parser (3.22.0) sha256=1eb4937cd9eb220aa2d194e352a24dba90aef00751e24c8dfffdb14000f15d23
|
||||||
|
rubycritic (4.12.0) sha256=024fed90fe656fa939f6ea80aab17569699ac3863d0b52fd72cb99892247abc8
|
||||||
|
rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
|
||||||
|
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
||||||
|
selenium-webdriver (4.41.0) sha256=cdc1173cd55cf186022cea83156cc2d0bec06d337e039b02ad25d94e41bedd22
|
||||||
|
sexp_processor (4.17.5) sha256=ae2b48ba98353d5d465ce8759836b7a05f2e12c5879fcd14d7815b026de32f0e
|
||||||
|
shoulda-matchers (7.0.1) sha256=b4bfd8744c10e0a36c8ac1a687f921ee7e25ed529e50488d61b79a8688749c77
|
||||||
|
sidekiq (8.1.2) sha256=4a1266f22bb1dad675d77bf32e8d4c04e990640e3b271650f47ea7299c38fceb
|
||||||
|
sidekiq-cron (2.3.1) sha256=96ab3da372289a30dc744c1daa2ae2e85960b2444a6f6ed68df1ff7882c44aac
|
||||||
|
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
||||||
|
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
||||||
|
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
|
||||||
|
skunk (0.5.4) sha256=627331dc78aec1fdf614feabcf05a88ca84025e364be27aeddca5d7531d1b682
|
||||||
|
sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744
|
||||||
|
ssrf_filter (1.3.0) sha256=66882d7de7d09c019098d6d7372412950ae184ebbc7c51478002058307aba6f2
|
||||||
|
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
|
||||||
|
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
||||||
|
sysexits (1.2.0) sha256=598241c4ae57baa403c125182dfdcc0d1ac4c0fb606dd47fbed57e4aaf795662
|
||||||
|
tailwindcss-rails (4.4.0) sha256=efa2961351a52acebe616e645a81a30bb4f27fde46cc06ce7688d1cd1131e916
|
||||||
|
tailwindcss-ruby (4.2.1) sha256=95886a1e24b42d76792c787d34e47098b53cb3b5a6363845bca4486f52b2e66a
|
||||||
|
tailwindcss-ruby (4.2.1-aarch64-linux-gnu) sha256=de457ddfc999c6bbbe1a59fbc11eb2168d619f6e0cb72d8d3334d372b331e36f
|
||||||
|
tailwindcss-ruby (4.2.1-aarch64-linux-musl) sha256=e6ed27704263201f8366316354aa45f9016cc9378ce8fac46fbbe65fafd4da5e
|
||||||
|
tailwindcss-ruby (4.2.1-arm64-darwin) sha256=bcf222fb8542cf5433925623e5e7b257897fbb8291a2350daae870a32f2eeb91
|
||||||
|
tailwindcss-ruby (4.2.1-x86_64-linux-gnu) sha256=201d0e5e5d4aba52cae4ee4bd1acd497d2790c83e7f15da964aab8ec93876831
|
||||||
|
tailwindcss-ruby (4.2.1-x86_64-linux-musl) sha256=79fa48ad51e533545f9fdbb04227e1342a65a42c2bd1314118b95473d5612007
|
||||||
|
temple (0.10.4) sha256=b7a1e94b6f09038ab0b6e4fe0126996055da2c38bec53a8a336f075748fff72c
|
||||||
|
terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91
|
||||||
|
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
||||||
|
thread_safe (0.3.6) sha256=9ed7072821b51c57e8d6b7011a8e282e25aeea3a4065eab326e43f66f063b05a
|
||||||
|
thruster (0.1.20) sha256=c05f2fbcae527bbe093a6e6d84fb12d9d680617e7c162325d9b97e8e9d4b5201
|
||||||
|
thruster (0.1.20-aarch64-linux) sha256=754f1701061235235165dde31e7a3bc87ec88066a02981ff4241fcda0d76d397
|
||||||
|
thruster (0.1.20-arm64-darwin) sha256=630cf8c273f562063b92ea5ccd7a721d7ba6130cc422c823727f4744f6d0770e
|
||||||
|
thruster (0.1.20-x86_64-linux) sha256=d579f252bf67aee6ba6d957e48f566b72e019d7657ba2f267a5db1e4d91d2479
|
||||||
|
tilt (2.7.0) sha256=0d5b9ba69f6a36490c64b0eee9f6e9aad517e20dcc848800a06eb116f08c6ab3
|
||||||
|
timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
|
||||||
|
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
||||||
|
tty-which (0.5.0) sha256=5824055f0d6744c97e7c4426544f01d519c40d1806ef2ef47d9854477993f466
|
||||||
|
turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f
|
||||||
|
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
|
||||||
|
unicode (0.4.4.5) sha256=42f294bfc8e186d29da89d1f766071505a20a22776168a31bb3408e03fa7a9d7
|
||||||
|
unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
|
||||||
|
unparser (0.9.0) sha256=4331f174a73a23b69250b13d47da3794ed1449711ee0f9ed8947dc020ba76067
|
||||||
|
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
|
||||||
|
useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
|
||||||
|
virtus (2.0.0) sha256=8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2
|
||||||
|
web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4
|
||||||
|
websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737
|
||||||
|
websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
|
||||||
|
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
|
||||||
|
xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e
|
||||||
|
zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
4.0.3
|
||||||
4
Procfile.dev
Normal file
4
Procfile.dev
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
web: bin/rails server -p 3000 -b 0.0.0.0
|
||||||
|
css: bin/rails tailwindcss:watch
|
||||||
|
jobs: bundle exec sidekiq -c 5
|
||||||
|
# ws: env ANYCABLE_PORT=8081 ANYCABLE_BROADCAST_ADAPTER=redis ANYCABLE_REDIS_URL=${REDIS_URL:-redis://localhost:6379/1} ANYCABLE_RPC_HOST=http://localhost:3000/_anycable anycable-go
|
||||||
260
README.md
Normal file
260
README.md
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
# Rails 8 Starter Kit
|
||||||
|
|
||||||
|
> A production-grade, AI-native fullstack starter kit for Rails 8.
|
||||||
|
> Zero-config Docker. Parallel tests. Real-time WebSockets. Premium dark UI.
|
||||||
|
> From `git clone` to running app in one command.
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it. The `setup` container creates your database, seeds sample data, and the `tests` container validates everything with parallel linting and specs — all before your app boots.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's Inside
|
||||||
|
|
||||||
|
| Layer | Technology | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| **Framework** | Rails 8.1 (edge) + Ruby 4.0 | Latest Rails with all modern defaults |
|
||||||
|
| **Frontend** | Hotwire (Turbo + Stimulus) + Tailwind CSS v4 + DaisyUI | SPA-like UX without a JS build step |
|
||||||
|
| **Real-time** | AnyCable + AnyCable-Go + Turbo Streams | WebSocket broadcasting at scale |
|
||||||
|
| **Database** | PostgreSQL 17.5 + PgBouncer | Connection pooling out of the box |
|
||||||
|
| **Background Jobs** | Sidekiq + Sidekiq-Cron | Reliable async processing with recurring jobs |
|
||||||
|
| **Object Storage** | Active Storage + LocalStack (S3) | Production-parity file uploads locally |
|
||||||
|
| **3D Graphics** | Three.js (via importmap) | Ready for WebGL/3D without bundlers |
|
||||||
|
| **Testing** | RSpec + Capybara + Parallel Tests | Full-stack E2E with headless Chromium |
|
||||||
|
| **Code Quality** | RuboCop (Shopify) + Brakeman + Reek + Flog + Flay | 8 linters running in parallel |
|
||||||
|
| **Auth Framework** | CanCanCan | Authorization scaffolding baked in |
|
||||||
|
| **Security** | Rack::Attack + SSRF Filter + Bundler Audit | Rate limiting and vulnerability scanning |
|
||||||
|
| **Deployment** | Kamal + Thruster + Multi-stage Dockerfile | Production-ready container deployment |
|
||||||
|
| **CI** | GitHub Actions + Dependabot | Automated testing and dependency updates |
|
||||||
|
| **Dev Tools** | Foreman + jemalloc + Bootsnap | Fast boot, low memory, process management |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Docker (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/viktorvsk/rails-8-starter-kit.git
|
||||||
|
cd rails-8-starter-kit
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
The startup pipeline runs automatically:
|
||||||
|
|
||||||
|
```
|
||||||
|
setup → creates DB, runs migrations, seeds data
|
||||||
|
tests → runs all 8 linters + parallel specs
|
||||||
|
app → boots Puma + Tailwind watcher + Sidekiq
|
||||||
|
ws → boots AnyCable-Go WebSocket server
|
||||||
|
```
|
||||||
|
|
||||||
|
Your app is live at `http://localhost:3000`, WebSockets at `ws://localhost:8081/cable`.
|
||||||
|
|
||||||
|
### Local (without Docker)
|
||||||
|
|
||||||
|
Requirements: Ruby 4.0+, PostgreSQL, Redis, `anycable-go`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/setup # install deps, create DB, seed, start server
|
||||||
|
bin/check-fast # run all linters in parallel
|
||||||
|
bin/prspec # run specs with parallel_tests
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||||
|
│ Browser │◄──►│ Puma :3000 │◄──►│ PostgreSQL │
|
||||||
|
│ │ │ (Rails 8) │ │ + PgBouncer │
|
||||||
|
└──────┬──────┘ └──────┬───────┘ └─────────────┘
|
||||||
|
│ │
|
||||||
|
│ WebSocket │ Pub/Sub
|
||||||
|
▼ ▼
|
||||||
|
┌──────────────┐ ┌──────────┐ ┌─────────────┐
|
||||||
|
│ AnyCable-Go │◄──►│ Redis │◄──►│ Sidekiq │
|
||||||
|
│ :8081 │ │ │ │ (workers) │
|
||||||
|
└──────────────┘ └──────────┘ └─────────────┘
|
||||||
|
│
|
||||||
|
┌────▼────────┐
|
||||||
|
│ LocalStack │
|
||||||
|
│ (S3) :4566 │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Parallel Execution
|
||||||
|
|
||||||
|
Tests run across all available CPU cores via `parallel_tests`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/prspec # parallel specs (default)
|
||||||
|
bin/check-fast # 8 linters + specs, all parallel
|
||||||
|
bin/check --parallel=rubocop,rspec --then=brakeman
|
||||||
|
```
|
||||||
|
|
||||||
|
### Headless Browser Testing
|
||||||
|
|
||||||
|
Capybara system specs run with headless Chromium. The driver auto-detects the environment:
|
||||||
|
- **Docker**: uses `/usr/bin/chromium` (pre-installed in `Dockerfile.dev`)
|
||||||
|
- **macOS/Linux**: uses Selenium Manager to find native Chrome
|
||||||
|
|
||||||
|
Animations and transitions are automatically disabled in `RAILS_ENV=test` for deterministic, fast specs.
|
||||||
|
|
||||||
|
### Test Pipeline in Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up # setup → tests → app (sequential gates)
|
||||||
|
docker compose run --rm tests # run tests only
|
||||||
|
```
|
||||||
|
|
||||||
|
The `tests` service uses `service_completed_successfully` as a gate — your app literally won't boot unless every linter and spec passes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Quality Gates
|
||||||
|
|
||||||
|
| Linter | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `rubocop` | Style enforcement (Shopify conventions) |
|
||||||
|
| `brakeman` | Static security analysis |
|
||||||
|
| `bundler-audit` | Known CVE scanning in dependencies |
|
||||||
|
| `reek` | Code smell detection |
|
||||||
|
| `flog` | Complexity scoring |
|
||||||
|
| `flay` | Duplication detection |
|
||||||
|
| `haml-syntax` | Template validation |
|
||||||
|
| `rspec` | Unit + system specs (parallel) |
|
||||||
|
|
||||||
|
All 8 run in parallel via `bin/check-fast`. Sequential chains are supported with `bin/check --then=`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker Compose Services
|
||||||
|
|
||||||
|
| Service | Image | Port | Role |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `postgres` | `postgres:17.5` | 5432 | Primary database with tuned config |
|
||||||
|
| `pgbouncer` | `edoburu/pgbouncer` | — | Transaction-mode connection pooling |
|
||||||
|
| `redis` | `redis:8.4-alpine` | 6379 | Pub/sub + cache + job queue backend |
|
||||||
|
| `localstack` | `localstack:4.13.1` | 4566 | S3-compatible object storage |
|
||||||
|
| `ws` | `anycable-go:1.6` | 8081 | WebSocket server |
|
||||||
|
| `setup` | (app image) | — | DB creation, migrations, seeds |
|
||||||
|
| `tests` | (app image) | — | Linters + parallel specs gate |
|
||||||
|
| `app` | (app image) | 3000 | Rails + Tailwind + Sidekiq |
|
||||||
|
|
||||||
|
All three app-derived containers (`setup`, `tests`, `app`) share a single Docker image build to prevent wasteful rebuilds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### Why AnyCable over Action Cable?
|
||||||
|
|
||||||
|
AnyCable-Go handles WebSocket connections in a dedicated Go process, keeping Ruby threads free for business logic. It's a drop-in replacement — the Rails side uses standard Action Cable APIs via `anycable-rails`.
|
||||||
|
|
||||||
|
### Why PgBouncer?
|
||||||
|
|
||||||
|
Transaction-mode connection pooling prevents the "too many connections" problem in production. The `setup` and `tests` services bypass PgBouncer and connect directly to Postgres (required for `CREATE DATABASE`).
|
||||||
|
|
||||||
|
### Why Sidekiq over Solid Queue?
|
||||||
|
|
||||||
|
Sidekiq provides battle-tested async processing with a mature web UI, cron scheduling (`sidekiq-cron`), and deep Redis integration. The `recurring.yml` has example jobs pre-configured.
|
||||||
|
|
||||||
|
### Why jemalloc?
|
||||||
|
|
||||||
|
`LD_PRELOAD=/usr/local/lib/libjemalloc.so` — reduces Ruby memory fragmentation by 30-40% in production. Pre-configured in both Dockerfiles.
|
||||||
|
|
||||||
|
### Why Bootsnap Cache Isolation?
|
||||||
|
|
||||||
|
Each Docker container gets an anonymous volume (`/rails/tmp/cache`) to prevent concurrent Bootsnap compilation from crashing on shared macOS mounts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Demo Content
|
||||||
|
|
||||||
|
> ⚠️ **The following components exist purely for demonstration.** They show how each layer of the stack works together. **Delete them when starting a real application.**
|
||||||
|
|
||||||
|
| Component | Files | Demonstrates |
|
||||||
|
|---|---|---|
|
||||||
|
| **Todo CRUD** | `app/models/todo.rb`, `app/controllers/todos_controller.rb`, `app/views/todos/`, `db/migrate/*_create_todos.rb` | Model → Controller → View with Turbo Frames |
|
||||||
|
| **Turbo Stream broadcasting** | `after_create_commit` in `todo.rb`, `turbo_stream_from` in `index.html.erb` | Real-time push via AnyCable |
|
||||||
|
| **Stimulus controller** | `app/javascript/controllers/hello_controller.js` | Basic Stimulus wiring |
|
||||||
|
| **Three.js import** | `config/importmap.rb` (the `three` pin) | Importmap-based JS library usage |
|
||||||
|
| **Sample seed** | `db/seeds.rb` (the `Todo.create!` block) | Idempotent database seeding |
|
||||||
|
| **System spec** | `spec/system/todos_spec.rb` | Capybara + headless Chromium E2E test |
|
||||||
|
| **Home controller** | `app/controllers/home_controller.rb`, `app/views/home/` | Static page routing |
|
||||||
|
| **Example job** | `app/jobs/example_recurring_job.rb`, `config/recurring.yml` | Sidekiq-Cron recurring task |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── app/
|
||||||
|
│ ├── assets/tailwind/ # Tailwind v4 + DaisyUI theme (Halloween dark)
|
||||||
|
│ ├── controllers/ # Rails controllers
|
||||||
|
│ ├── javascript/ # Stimulus controllers + application.js
|
||||||
|
│ ├── models/ # ActiveRecord models
|
||||||
|
│ └── views/ # ERB + HAML templates
|
||||||
|
├── bin/
|
||||||
|
│ ├── check # Orchestrator: parallel + sequential task runner
|
||||||
|
│ ├── check-fast # Fast CI mode (8 linters in parallel)
|
||||||
|
│ ├── prspec # Parallel RSpec runner
|
||||||
|
│ ├── setup # One-command dev environment setup
|
||||||
|
│ └── linters/ # Individual linter scripts
|
||||||
|
├── config/
|
||||||
|
│ ├── cable.yml # AnyCable adapter config
|
||||||
|
│ ├── database.yml # PostgreSQL (env-driven, PgBouncer-aware)
|
||||||
|
│ └── recurring.yml # Sidekiq-Cron job schedules
|
||||||
|
├── docker-compose.yml # Full 8-service development stack
|
||||||
|
├── Dockerfile # Multi-stage production image
|
||||||
|
├── Dockerfile.dev # Development image (with Chromium)
|
||||||
|
└── spec/
|
||||||
|
├── support/capybara.rb # Cross-env headless browser driver
|
||||||
|
└── system/ # E2E browser specs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
All configuration is driven by environment variables with sensible defaults. See `env.example` for the complete list. Key variables:
|
||||||
|
|
||||||
|
| Variable | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `DB_HOST` | `localhost` | PostgreSQL host (use `pgbouncer` in Docker) |
|
||||||
|
| `DB_DIRECT_HOST` | `localhost` | Direct PG host (bypasses PgBouncer for migrations) |
|
||||||
|
| `REDIS_URL` | `redis://localhost:6379/1` | Redis connection string |
|
||||||
|
| `RAILS_MASTER_KEY` | from `config/master.key` | Credentials decryption key |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For AI Agents
|
||||||
|
|
||||||
|
This repository is optimized for LLM-assisted development:
|
||||||
|
|
||||||
|
- **Deterministic setup**: `docker compose up` produces identical environments
|
||||||
|
- **Fast feedback**: `bin/check-fast` validates all changes in seconds
|
||||||
|
- **Clear conventions**: Shopify Ruby style, RSpec, Stimulus naming
|
||||||
|
- **Minimal surface area**: one model, one controller, flat config files
|
||||||
|
- **No magic**: explicit imports, no metaprogramming in app code
|
||||||
|
|
||||||
|
To modify this project, start by reading:
|
||||||
|
1. `config/routes.rb` — all routes
|
||||||
|
2. `Gemfile` — all dependencies
|
||||||
|
3. `docker-compose.yml` — full infrastructure
|
||||||
|
4. `bin/check-fast` — validation pipeline
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
8
Rakefile
Normal file
8
Rakefile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
||||||
|
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
||||||
|
|
||||||
|
require_relative "config/application"
|
||||||
|
|
||||||
|
Rails.application.load_tasks
|
||||||
0
app/assets/images/.keep
Normal file
0
app/assets/images/.keep
Normal file
123
app/assets/stylesheets/application.css
Normal file
123
app/assets/stylesheets/application.css
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* This is a manifest file that'll be compiled into application.css.
|
||||||
|
*
|
||||||
|
* With Propshaft, assets are served efficiently without preprocessing steps. You can still include
|
||||||
|
* application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
|
||||||
|
* cascading order, meaning styles declared later in the document or manifest will override earlier ones,
|
||||||
|
* depending on specificity.
|
||||||
|
*
|
||||||
|
* Consider organizing styles into separate files for maintainability.
|
||||||
|
*/
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-form { margin-bottom: 16px; }
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-input:focus { border-color: #4f46e5; }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 18px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover { opacity: 0.85; }
|
||||||
|
.btn-primary { background: #4f46e5; color: #fff; }
|
||||||
|
.btn-secondary { background: #e5e7eb; color: #374151; }
|
||||||
|
.btn-sm { padding: 5px 10px; font-size: 0.8rem; }
|
||||||
|
.btn-danger { background: #ef4444; color: #fff; }
|
||||||
|
|
||||||
|
.todo-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item.completed { opacity: 0.55; }
|
||||||
|
.todo-item.completed .todo-title { text-decoration: line-through; color: #9ca3af; }
|
||||||
|
|
||||||
|
.toggle-btn { background: none; border: none; padding: 0; cursor: pointer; }
|
||||||
|
.toggle-btn form { margin: 0; }
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 2px solid #4f46e5;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #fff;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item.completed .checkbox { background: #4f46e5; color: #fff; }
|
||||||
|
.todo-title { flex: 1; font-size: 1rem; }
|
||||||
|
.todo-actions { display: flex; gap: 6px; }
|
||||||
|
|
||||||
|
.error-messages {
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
307
app/assets/tailwind/application.css
Normal file
307
app/assets/tailwind/application.css
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@layer theme, base, components, utilities, daisyui, daisyui.l1, daisyui.l1.l2, daisyui.l1.l2.l3;
|
||||||
|
@source not "../tailwind/daisyui{,*}.mjs";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@plugin "../tailwind/daisyui.mjs";
|
||||||
|
|
||||||
|
[data-theme="halloween"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
/* Deep dark backgrounds for premium dark mode */
|
||||||
|
--color-base-100: oklch(20% 0.035 278);
|
||||||
|
--color-base-200: oklch(12% 0.025 278);
|
||||||
|
--color-base-300: oklch(8% 0.02 278);
|
||||||
|
--color-base-content: oklch(93% 0.008 265);
|
||||||
|
|
||||||
|
/* Violet primary matching --primary: #a78bfa / #7c3aed */
|
||||||
|
--color-primary: oklch(65% 0.2 293);
|
||||||
|
--color-primary-content: oklch(100% 0 0);
|
||||||
|
|
||||||
|
--color-secondary: oklch(60% 0.2 277);
|
||||||
|
--color-secondary-content: oklch(96% 0.015 277);
|
||||||
|
|
||||||
|
--color-accent: oklch(68% 0.18 293);
|
||||||
|
--color-accent-content: oklch(100% 0 0);
|
||||||
|
|
||||||
|
/* Neutral with subtle violet tint */
|
||||||
|
--color-neutral: oklch(22% 0.03 278);
|
||||||
|
--color-neutral-content: oklch(88% 0.015 270);
|
||||||
|
|
||||||
|
--color-info: oklch(72% 0.16 237);
|
||||||
|
--color-info-content: oklch(100% 0 0);
|
||||||
|
|
||||||
|
--color-success: oklch(72% 0.18 155);
|
||||||
|
--color-success-content: oklch(100% 0 0);
|
||||||
|
|
||||||
|
--color-warning: oklch(82% 0.16 75);
|
||||||
|
--color-warning-content: oklch(95% 0.04 85);
|
||||||
|
|
||||||
|
--color-error: oklch(70% 0.2 25);
|
||||||
|
--color-error-content: oklch(100% 0 0);
|
||||||
|
|
||||||
|
--radius-selector: 0.5rem;
|
||||||
|
--radius-field: 0.375rem;
|
||||||
|
--radius-box: 0.75rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .card {
|
||||||
|
border: 1px solid oklch(28% 0.04 280);
|
||||||
|
box-shadow: 0 4px 16px oklch(0% 0 0 / 0.4), 0 0 0 1px oklch(30% 0.04 285 / 0.2), 0 0 30px oklch(65% 0.2 293 / 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness: override DaisyUI defaults for small screens */
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.label {
|
||||||
|
display: flex !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
gap: 0.25rem;
|
||||||
|
max-width: 100% !important;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card, .card-body {
|
||||||
|
min-width: 0 !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
HALLOWEEN THEME — Visual Polish (via Tailwind pipeline)
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ── CodeMirror Dark Override ── */
|
||||||
|
[data-theme="halloween"] .cm-editor,
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-scroller {
|
||||||
|
background-color: #0f0f14 !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .cm-editor {
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.15) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-gutters {
|
||||||
|
background-color: #1a1a20 !important;
|
||||||
|
color: rgba(167, 139, 250, 0.4) !important;
|
||||||
|
border-right: 1px solid rgba(167, 139, 250, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-activeLineGutter {
|
||||||
|
background-color: rgba(167, 139, 250, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-activeLine {
|
||||||
|
background-color: rgba(167, 139, 250, 0.04) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-cursor,
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-dropCursor {
|
||||||
|
border-left-color: #a78bfa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-content {
|
||||||
|
caret-color: #a78bfa;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-selectionBackground {
|
||||||
|
background: rgba(167, 139, 250, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .cm-editor ::selection {
|
||||||
|
background: rgba(167, 139, 250, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Syntax highlighting tokens */
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-meta { color: #a78bfa; }
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-keyword { color: #c4b5fd; }
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-atom { color: #f472b6; }
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-number { color: #34d399; }
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-string { color: #fbbf24; }
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-def { color: #93c5fd; }
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-comment { color: #6b7280; }
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-tag { color: #a78bfa; }
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-attribute { color: #93c5fd; }
|
||||||
|
[data-theme="halloween"] .cm-editor .cm-property { color: #c4b5fd; }
|
||||||
|
|
||||||
|
/* ── Headings: Tight typography ── */
|
||||||
|
[data-theme="halloween"] h1,
|
||||||
|
[data-theme="halloween"] h2,
|
||||||
|
[data-theme="halloween"] h3,
|
||||||
|
[data-theme="halloween"] h4 {
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Links: Violet accent ── */
|
||||||
|
[data-theme="halloween"] a:not(.btn):not(.no-accent):not(.menu a) {
|
||||||
|
color: #a78bfa;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] a:not(.btn):not(.no-accent):not(.menu a):hover {
|
||||||
|
color: #c4b5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Collapse: Violet borders ── */
|
||||||
|
[data-theme="halloween"] .collapse {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .collapse:focus-within {
|
||||||
|
border-color: rgba(167, 139, 250, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Code blocks: Dark violet ── */
|
||||||
|
[data-theme="halloween"] code:not(.cm-content code) {
|
||||||
|
background: rgba(167, 139, 250, 0.08);
|
||||||
|
color: #c4b5fd;
|
||||||
|
padding: 0.15em 0.4em;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] pre {
|
||||||
|
background: #0f0f14 !important;
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Base badge refinement ── */
|
||||||
|
[data-theme="halloween"] .badge {
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status badge styles ── */
|
||||||
|
[data-theme="halloween"] .badge-success {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.35);
|
||||||
|
color: #4ade80;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .badge-error {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.35);
|
||||||
|
color: #f87171;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .badge-warning {
|
||||||
|
background: rgba(234, 179, 8, 0.15);
|
||||||
|
border: 1px solid rgba(234, 179, 8, 0.35);
|
||||||
|
color: #fbbf24;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .badge-info {
|
||||||
|
background: rgba(56, 189, 248, 0.15);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.30);
|
||||||
|
color: #7dd3fc;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .badge-primary {
|
||||||
|
background: rgba(167, 139, 250, 0.15);
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.30);
|
||||||
|
color: #c4b5fd;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .badge-accent {
|
||||||
|
background: rgba(192, 132, 252, 0.15);
|
||||||
|
border: 1px solid rgba(192, 132, 252, 0.30);
|
||||||
|
color: #d8b4fe;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .badge-ghost {
|
||||||
|
background: rgba(229, 231, 235, 0.06);
|
||||||
|
border: 1px solid rgba(229, 231, 235, 0.12);
|
||||||
|
color: rgba(229, 231, 235, 0.7);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Badge-outline dark contrast: background tints ── */
|
||||||
|
[data-theme="halloween"] .badge-outline.badge-info { background: rgba(56, 189, 248, 0.10); }
|
||||||
|
[data-theme="halloween"] .badge-outline.badge-success { background: rgba(34, 197, 94, 0.10); }
|
||||||
|
[data-theme="halloween"] .badge-outline.badge-error { background: rgba(239, 68, 68, 0.10); }
|
||||||
|
[data-theme="halloween"] .badge-outline.badge-warning { background: rgba(234, 179, 8, 0.10); }
|
||||||
|
[data-theme="halloween"] .badge-outline.badge-primary { background: rgba(167, 139, 250, 0.10); }
|
||||||
|
[data-theme="halloween"] .badge-outline.badge-accent { background: rgba(192, 132, 252, 0.10); }
|
||||||
|
[data-theme="halloween"] .badge-outline:not([class*="badge-info"]):not([class*="badge-success"]):not([class*="badge-error"]):not([class*="badge-warning"]):not([class*="badge-primary"]):not([class*="badge-accent"]) { background: rgba(229, 231, 235, 0.06); }
|
||||||
|
|
||||||
|
/* ── Alert dark-theme text contrast ── */
|
||||||
|
[data-theme="halloween"] .alert-warning { color: oklch(90% 0.1 85); }
|
||||||
|
[data-theme="halloween"] .alert-warning svg { color: oklch(82% 0.16 75); }
|
||||||
|
[data-theme="halloween"] .alert-info { color: oklch(90% 0.05 237); }
|
||||||
|
[data-theme="halloween"] .alert-info svg { color: oklch(72% 0.16 237); }
|
||||||
|
|
||||||
|
/* ── Menu hover: Violet glow ── */
|
||||||
|
[data-theme="halloween"] .menu li > a:hover,
|
||||||
|
[data-theme="halloween"] .menu li > button:hover {
|
||||||
|
background: rgba(167, 139, 250, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] .menu li > .active,
|
||||||
|
[data-theme="halloween"] .menu li > a.active {
|
||||||
|
background: rgba(167, 139, 250, 0.12);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Select dark options ── */
|
||||||
|
[data-theme="halloween"] select option {
|
||||||
|
background: #1a1a20;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tooltip: Glass ── */
|
||||||
|
[data-theme="halloween"] .tooltip::before {
|
||||||
|
background: rgba(26, 26, 32, 0.95);
|
||||||
|
border: 1px solid rgba(167, 139, 250, 0.15);
|
||||||
|
color: #e5e7eb;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Progress: Violet ── */
|
||||||
|
[data-theme="halloween"] progress::-webkit-progress-value {
|
||||||
|
background: linear-gradient(90deg, #7c3aed, #a78bfa);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="halloween"] progress::-webkit-progress-bar {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Footer: Subtle ── */
|
||||||
|
[data-theme="halloween"] footer {
|
||||||
|
color: rgba(229, 231, 235, 0.4);
|
||||||
|
border-top: 1px solid rgba(167, 139, 250, 0.08);
|
||||||
|
}
|
||||||
93
app/assets/tailwind/daisyui-theme.mjs
Normal file
93
app/assets/tailwind/daisyui-theme.mjs
Normal file
File diff suppressed because one or more lines are too long
1167
app/assets/tailwind/daisyui.mjs
Normal file
1167
app/assets/tailwind/daisyui.mjs
Normal file
File diff suppressed because one or more lines are too long
9
app/controllers/application_controller.rb
Normal file
9
app/controllers/application_controller.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ApplicationController < ActionController::Base
|
||||||
|
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||||
|
allow_browser versions: :modern
|
||||||
|
|
||||||
|
# Changes to the importmap will invalidate the etag for HTML responses
|
||||||
|
stale_when_importmap_changes
|
||||||
|
end
|
||||||
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
6
app/controllers/home_controller.rb
Normal file
6
app/controllers/home_controller.rb
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class HomeController < ApplicationController
|
||||||
|
def index
|
||||||
|
end
|
||||||
|
end
|
||||||
69
app/controllers/todos_controller.rb
Normal file
69
app/controllers/todos_controller.rb
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TodosController < ApplicationController
|
||||||
|
before_action :set_todo, only: [:edit, :update, :destroy, :toggle]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@todos = Todo.by_created
|
||||||
|
@todo = Todo.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@todo = Todo.new(todo_params)
|
||||||
|
if @todo.save
|
||||||
|
respond_to do |format|
|
||||||
|
format.turbo_stream
|
||||||
|
format.html { redirect_to(todos_path) }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@todos = Todo.by_created
|
||||||
|
respond_to do |format|
|
||||||
|
format.turbo_stream { render(turbo_stream: turbo_stream.replace("todo_form", partial: "todos/form", locals: { todo: @todo })) }
|
||||||
|
format.html { render(:index, status: :unprocessable_entity) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @todo.update(todo_params)
|
||||||
|
respond_to do |format|
|
||||||
|
format.turbo_stream
|
||||||
|
format.html { redirect_to(todos_path) }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
respond_to do |format|
|
||||||
|
format.turbo_stream { render(turbo_stream: turbo_stream.replace(dom_id(@todo), partial: "todos/todo", locals: { todo: @todo })) }
|
||||||
|
format.html { render(:edit, status: :unprocessable_entity) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@todo.destroy
|
||||||
|
respond_to do |format|
|
||||||
|
format.turbo_stream
|
||||||
|
format.html { redirect_to(todos_path) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def toggle
|
||||||
|
@todo.update(completed: !@todo.completed)
|
||||||
|
respond_to do |format|
|
||||||
|
format.turbo_stream { render(turbo_stream: turbo_stream.replace(dom_id(@todo), partial: "todos/todo", locals: { todo: @todo })) }
|
||||||
|
format.html { redirect_to(todos_path) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_todo
|
||||||
|
@todo = Todo.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def todo_params
|
||||||
|
params.require(:todo).permit(:title)
|
||||||
|
end
|
||||||
|
end
|
||||||
4
app/helpers/application_helper.rb
Normal file
4
app/helpers/application_helper.rb
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ApplicationHelper
|
||||||
|
end
|
||||||
2
app/javascript/application.js
Normal file
2
app/javascript/application.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
|
||||||
|
import "controllers"
|
||||||
9
app/javascript/controllers/application.js
Normal file
9
app/javascript/controllers/application.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Application } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
const application = Application.start()
|
||||||
|
|
||||||
|
// Configure Stimulus development experience
|
||||||
|
application.debug = false
|
||||||
|
window.Stimulus = application
|
||||||
|
|
||||||
|
export { application }
|
||||||
7
app/javascript/controllers/hello_controller.js
Normal file
7
app/javascript/controllers/hello_controller.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
connect() {
|
||||||
|
this.element.textContent = "Hello World!"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
app/javascript/controllers/index.js
Normal file
4
app/javascript/controllers/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Import and register all your controllers from the importmap via controllers/**/*_controller
|
||||||
|
import { application } from "controllers/application"
|
||||||
|
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
||||||
|
eagerLoadControllersFrom("controllers", application)
|
||||||
9
app/jobs/application_job.rb
Normal file
9
app/jobs/application_job.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ApplicationJob < ActiveJob::Base
|
||||||
|
# Automatically retry jobs that encountered a deadlock
|
||||||
|
# retry_on ActiveRecord::Deadlocked
|
||||||
|
|
||||||
|
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||||
|
# discard_on ActiveJob::DeserializationError
|
||||||
|
end
|
||||||
9
app/jobs/example_recurring_job.rb
Normal file
9
app/jobs/example_recurring_job.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ExampleRecurringJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform
|
||||||
|
Rails.logger.info("[ExampleRecurringJob] ran at #{Time.current}")
|
||||||
|
end
|
||||||
|
end
|
||||||
5
app/models/application_record.rb
Normal file
5
app/models/application_record.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ApplicationRecord < ActiveRecord::Base
|
||||||
|
primary_abstract_class
|
||||||
|
end
|
||||||
0
app/models/concerns/.keep
Normal file
0
app/models/concerns/.keep
Normal file
11
app/models/todo.rb
Normal file
11
app/models/todo.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Todo < ApplicationRecord
|
||||||
|
validates :title, presence: true, length: { maximum: 255 }
|
||||||
|
|
||||||
|
after_create_commit { broadcast_prepend_to "todos", target: "todos" }
|
||||||
|
|
||||||
|
scope :pending, -> { where(completed: false) }
|
||||||
|
scope :completed, -> { where(completed: true) }
|
||||||
|
scope :by_created, -> { order(created_at: :asc) }
|
||||||
|
end
|
||||||
24
app/views/home/index.html.erb
Normal file
24
app/views/home/index.html.erb
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<div style="font-family: system-ui, sans-serif; max-width: 640px; margin: 60px auto; padding: 0 20px;">
|
||||||
|
<h1>Rails 8 Starter Kit</h1>
|
||||||
|
<p>Rails <%= Rails.version %> · Ruby <%= RUBY_VERSION %></p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="/up">Health check</a></li>
|
||||||
|
<li><a href="/jobs">Mission Control — Jobs dashboard</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Stack</h2>
|
||||||
|
<ul>
|
||||||
|
<li>PostgreSQL via pg</li>
|
||||||
|
<li>Solid Queue — background jobs</li>
|
||||||
|
<li>Solid Cache — database-backed cache</li>
|
||||||
|
<li>Solid Cable — Action Cable adapter</li>
|
||||||
|
<li>Mission Control Jobs — jobs dashboard</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Processes (<code>bin/dev</code>)</h2>
|
||||||
|
<ul>
|
||||||
|
<li><code>web</code> — Puma web server</li>
|
||||||
|
<li><code>worker</code> — Solid Queue worker + recurring jobs dispatcher</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
47
app/views/layouts/application.html.erb
Normal file
47
app/views/layouts/application.html.erb
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= content_for(:title) || "Starter" %></title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="application-name" content="Starter">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
|
<%= yield :head %>
|
||||||
|
|
||||||
|
<%# Enable PWA manifest for installable apps %>
|
||||||
|
<%= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||||
|
|
||||||
|
<link rel="icon" href="/icon.png" type="image/png">
|
||||||
|
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href="/icon.png">
|
||||||
|
<meta name="theme-color" content="#0f0f14">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/service-worker.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||||
|
<%= javascript_importmap_tags %>
|
||||||
|
<% if Rails.env.test? %>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after {
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<% end %>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<%= yield %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
app/views/pwa/manifest.json.erb
Normal file
22
app/views/pwa/manifest.json.erb
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "Rails 8 Starter Kit",
|
||||||
|
"short_name": "Starter",
|
||||||
|
"description": "A production-grade, AI-native fullstack starter kit.",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0f0f14",
|
||||||
|
"theme_color": "#0f0f14",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icon.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icon.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
39
app/views/pwa/service-worker.js
Normal file
39
app/views/pwa/service-worker.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Network-first strategy with offline fallback for PWA standalone mode
|
||||||
|
|
||||||
|
const CACHE_NAME = "rails-8-starter-offline";
|
||||||
|
const OFFLINE_URL = "/offline.html";
|
||||||
|
|
||||||
|
// Pre-cache the offline fallback page on install
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.add(OFFLINE_URL))
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up old caches on activate
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((keys) =>
|
||||||
|
Promise.all(
|
||||||
|
keys
|
||||||
|
.filter((key) => key !== CACHE_NAME)
|
||||||
|
.map((key) => caches.delete(key))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Network-first for navigation requests; fallback to offline page on failure
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
if (event.request.mode !== "navigate") return;
|
||||||
|
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request).catch(() =>
|
||||||
|
caches.match(OFFLINE_URL).then((cached) => cached || new Response("Offline", { status: 503 }))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
16
app/views/todos/_form.html.erb
Normal file
16
app/views/todos/_form.html.erb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<%= form_with model: todo, id: "todo_form", class: "todo-form" do |f| %>
|
||||||
|
<% if todo.errors.any? %>
|
||||||
|
<div class="error-messages">
|
||||||
|
<% todo.errors.full_messages.each do |msg| %>
|
||||||
|
<p><%= msg %></p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="form-row">
|
||||||
|
<%= f.text_field :title, placeholder: "What needs to be done?", autofocus: true, class: "todo-input", autocomplete: "off" %>
|
||||||
|
<%= f.submit todo.new_record? ? "Add" : "Save", class: "btn btn-primary" %>
|
||||||
|
<% unless todo.new_record? %>
|
||||||
|
<%= link_to "Cancel", todos_path, class: "btn btn-secondary" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
10
app/views/todos/_todo.html.erb
Normal file
10
app/views/todos/_todo.html.erb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<li id="<%= dom_id(todo) %>" class="todo-item <%= todo.completed? ? 'completed' : '' %>">
|
||||||
|
<%= button_to toggle_todo_path(todo), method: :patch, class: "toggle-btn", "data-turbo-stream": true do %>
|
||||||
|
<span class="checkbox"><%= todo.completed? ? "✓" : "" %></span>
|
||||||
|
<% end %>
|
||||||
|
<span class="todo-title"><%= todo.title %></span>
|
||||||
|
<div class="todo-actions">
|
||||||
|
<%= link_to "Edit", edit_todo_path(todo), class: "btn btn-sm" %>
|
||||||
|
<%= button_to "Delete", todo_path(todo), method: :delete, class: "btn btn-sm btn-danger", "data-turbo-confirm": "Delete this todo?", "data-turbo-stream": true %>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
2
app/views/todos/create.turbo_stream.erb
Normal file
2
app/views/todos/create.turbo_stream.erb
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<%= turbo_stream.append "todos", partial: "todos/todo", locals: { todo: @todo } %>
|
||||||
|
<%= turbo_stream.replace "todo_form", partial: "todos/form", locals: { todo: Todo.new } %>
|
||||||
1
app/views/todos/destroy.turbo_stream.erb
Normal file
1
app/views/todos/destroy.turbo_stream.erb
Normal file
@ -0,0 +1 @@
|
|||||||
|
<%= turbo_stream.remove dom_id(@todo) %>
|
||||||
6
app/views/todos/edit.html.erb
Normal file
6
app/views/todos/edit.html.erb
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<% content_for :title, "Edit Todo" %>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Edit Todo</h1>
|
||||||
|
<%= render "form", todo: @todo %>
|
||||||
|
</div>
|
||||||
14
app/views/todos/index.html.erb
Normal file
14
app/views/todos/index.html.erb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<% content_for :title, "Todo App" %>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<%= turbo_stream_from "todos" %>
|
||||||
|
<h1>Todos</h1>
|
||||||
|
|
||||||
|
<div id="todo_form">
|
||||||
|
<%= render "form", todo: @todo %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul id="todos" class="todo-list">
|
||||||
|
<%= render @todos %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
1
app/views/todos/update.turbo_stream.erb
Normal file
1
app/views/todos/update.turbo_stream.erb
Normal file
@ -0,0 +1 @@
|
|||||||
|
<%= turbo_stream.replace dom_id(@todo), partial: "todos/todo", locals: { todo: @todo } %>
|
||||||
7
bin/brakeman
Executable file
7
bin/brakeman
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
ARGV.unshift("--ensure-latest")
|
||||||
|
|
||||||
|
load Gem.bin_path("brakeman", "brakeman")
|
||||||
6
bin/bundler-audit
Executable file
6
bin/bundler-audit
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
require_relative "../config/boot"
|
||||||
|
require "bundler/audit/cli"
|
||||||
|
|
||||||
|
ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check")
|
||||||
|
Bundler::Audit::CLI.start
|
||||||
84
bin/check
Executable file
84
bin/check
Executable file
@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'optparse'
|
||||||
|
|
||||||
|
options = {
|
||||||
|
parallel: [],
|
||||||
|
then: []
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.on("--parallel=LIST", Array) { |list| options[:parallel] = list }
|
||||||
|
opts.on("--then=LIST", Array) { |list| options[:then] = list }
|
||||||
|
end.parse!
|
||||||
|
|
||||||
|
if options[:parallel].empty? && options[:then].empty?
|
||||||
|
puts "Usage: #{$PROGRAM_NAME} [--parallel=task1,task2] [--then=task3,task4]"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
ENV["SILENT_TESTS"] = "1"
|
||||||
|
|
||||||
|
def resolve_cmd(name)
|
||||||
|
paths = [
|
||||||
|
"./bin/linters/#{name}",
|
||||||
|
"./bin/linters/helpers/#{name}"
|
||||||
|
]
|
||||||
|
# Find an executable path, or default to executing the name directly
|
||||||
|
paths.find { |p| File.executable?(p) } || name
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_task(name)
|
||||||
|
cmd = resolve_cmd(name)
|
||||||
|
start_time = Time.now
|
||||||
|
|
||||||
|
output = `DISABLE_SPRING=1 #{cmd} 2>&1`
|
||||||
|
ext_code = $?.exitstatus
|
||||||
|
elapsed = (Time.now - start_time).round
|
||||||
|
success = (ext_code == 0)
|
||||||
|
|
||||||
|
# Check if the script wraps its own output
|
||||||
|
if output.include?("✅ SUCCESS") || output.include?("❌ FAILED")
|
||||||
|
puts output
|
||||||
|
else
|
||||||
|
if success
|
||||||
|
puts "✅ SUCCESS: #{cmd} (#{elapsed}s)"
|
||||||
|
else
|
||||||
|
puts "========================================"
|
||||||
|
puts "❌ FAILED: #{cmd} (#{elapsed}s)"
|
||||||
|
puts output
|
||||||
|
puts ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
success
|
||||||
|
end
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
results = {}
|
||||||
|
mutex = Mutex.new
|
||||||
|
|
||||||
|
options[:parallel].each do |task_name|
|
||||||
|
threads << Thread.new do
|
||||||
|
success = run_task(task_name)
|
||||||
|
mutex.synchronize { results[task_name] = success }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unless options[:then].empty?
|
||||||
|
threads << Thread.new do
|
||||||
|
options[:then].each do |task_name|
|
||||||
|
success = run_task(task_name)
|
||||||
|
mutex.synchronize { results[task_name] = success }
|
||||||
|
break unless success # Stop the sequential chain on failure!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
threads.each(&:join)
|
||||||
|
|
||||||
|
if results.values.any? { |r| !r }
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
4
bin/check-fast
Executable file
4
bin/check-fast
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Fast linting mode (replaces bin/lint)
|
||||||
|
exec ./bin/check --parallel=rspec,flog,flay,rubocop,brakeman,bundler-audit,reek,haml-syntax
|
||||||
6
bin/ci
Executable file
6
bin/ci
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
require_relative "../config/boot"
|
||||||
|
require "active_support/continuous_integration"
|
||||||
|
|
||||||
|
CI = ActiveSupport::ContinuousIntegration
|
||||||
|
require_relative "../config/ci.rb"
|
||||||
2
bin/dev
Executable file
2
bin/dev
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
exec foreman start -f Procfile.dev "$@"
|
||||||
9
bin/docker-entrypoint
Executable file
9
bin/docker-entrypoint
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
# If running the rails server or bundle install, run standard setup
|
||||||
|
if [ "$1" == "./bin/rails" ] && [ "$2" == "server" ] || [ "$1" == "bin/dev" ]; then
|
||||||
|
bundle check || bundle install
|
||||||
|
./bin/rails db:prepare 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "${@}"
|
||||||
4
bin/importmap
Executable file
4
bin/importmap
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require_relative "../config/application"
|
||||||
|
require "importmap/commands"
|
||||||
6
bin/jobs
Executable file
6
bin/jobs
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require_relative "../config/environment"
|
||||||
|
require "solid_queue/cli"
|
||||||
|
|
||||||
|
SolidQueue::Cli.start(ARGV)
|
||||||
17
bin/linters/brakeman
Executable file
17
bin/linters/brakeman
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
config_dir = File.expand_path("../../config", __dir__)
|
||||||
|
ARGV.unshift(
|
||||||
|
"--no-pager",
|
||||||
|
"--ensure-latest",
|
||||||
|
"--config-file",
|
||||||
|
File.join(config_dir, "brakeman.yml"),
|
||||||
|
"--ignore-config",
|
||||||
|
File.join(config_dir, "brakeman.ignore"),
|
||||||
|
)
|
||||||
|
|
||||||
|
load Gem.bin_path("brakeman", "brakeman")
|
||||||
8
bin/linters/bundler-audit
Executable file
8
bin/linters/bundler-audit
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative "../../config/boot"
|
||||||
|
require "bundler/audit/cli"
|
||||||
|
|
||||||
|
ARGV.push("--config", "../config/bundler-audit.yml") if ARGV.empty? || ARGV.include?("check")
|
||||||
|
Bundler::Audit::CLI.start
|
||||||
76
bin/linters/coverage
Executable file
76
bin/linters/coverage
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
MARKER_FILE = "coverage/coverage.json"
|
||||||
|
|
||||||
|
unless File.exist?(MARKER_FILE)
|
||||||
|
puts "Coverage check failed: no coverage.json found."
|
||||||
|
puts "Please run `bin/linters/nplus1` or `bin/coverage` first to generate the report."
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
data = JSON.parse(File.read(MARKER_FILE))
|
||||||
|
|
||||||
|
unless data.key?("groups")
|
||||||
|
puts "Coverage check failed: 'groups' key not found in coverage.json format."
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
failed_groups = []
|
||||||
|
|
||||||
|
data["groups"].each do |group_name, group_data|
|
||||||
|
# Depending on SimpleCov format, it might be nested under `lines`
|
||||||
|
# Check both structured formats.
|
||||||
|
percent = group_data.dig("lines", "covered_percent") || group_data["covered_percent"]
|
||||||
|
|
||||||
|
if percent.nil?
|
||||||
|
puts "Coverage check failed: Could not determine covered_percent for group '#{group_name}'."
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
if percent < 100.0
|
||||||
|
failed_groups << { name: group_name, percent: percent }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if failed_groups.any?
|
||||||
|
puts "Coverage check failed: Not all groups have 100% coverage."
|
||||||
|
puts "The following groups need more tests:"
|
||||||
|
failed_groups.sort_by { |fg| fg[:name] }.each do |fg|
|
||||||
|
# Format percentage to 2 decimal places max
|
||||||
|
percent_str = fg[:percent] == fg[:percent].to_i ? fg[:percent].to_i : format("%.2f", fg[:percent])
|
||||||
|
puts " - #{fg[:name]}: #{percent_str}%"
|
||||||
|
end
|
||||||
|
|
||||||
|
if data["coverage"]
|
||||||
|
puts "\nUncovered lines:"
|
||||||
|
data["coverage"].each do |file, file_data|
|
||||||
|
next unless file_data["lines"]
|
||||||
|
next if file_data["lines"].compact.all? { |l| l.to_i > 0 }
|
||||||
|
|
||||||
|
lines = file_data["lines"]
|
||||||
|
missing_lines = []
|
||||||
|
|
||||||
|
lines.each_with_index do |visits, idx|
|
||||||
|
missing_lines << (idx + 1) if visits && visits.to_i == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
if missing_lines.any?
|
||||||
|
relative_file = file.sub(%r{^#{Regexp.escape(Dir.pwd)}/}, "")
|
||||||
|
puts " - #{relative_file}: #{missing_lines.count} missing lines (#{missing_lines.join(", ")})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Coverage check passed: All groups have 100% coverage."
|
||||||
|
exit(0)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
puts "Coverage check failed: coverage.json is not valid JSON."
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
43
bin/linters/flay
Executable file
43
bin/linters/flay
Executable file
@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
flay_args=()
|
||||||
|
paths=()
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == -* ]]; then
|
||||||
|
flay_args+=("$arg")
|
||||||
|
else
|
||||||
|
paths+=("$arg")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#paths[@]} -eq 0 ]; then
|
||||||
|
paths=(app lib)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run flay and filter out similar-code blocks that reference only excluded MCP tool DSL files.
|
||||||
|
# get_play*.rb and list_play*.rb files have structurally required DSL duplication.
|
||||||
|
EXCLUDED_PATTERN="app/mcp/tools/get_play\|app/mcp/tools/list_play"
|
||||||
|
|
||||||
|
RAW=$(bundle exec flay --mass 35 "${flay_args[@]}" "${paths[@]}" 2>&1 || true)
|
||||||
|
|
||||||
|
# Filter: remove any "Similar code found" block where ALL file references are excluded files.
|
||||||
|
# We use Ruby to parse blocks and drop those that only contain excluded paths.
|
||||||
|
RESULT=$(echo "$RAW" | ruby -e '
|
||||||
|
excluded = /app\/mcp\/tools\/(get_play|list_play|list_templates|create_mutation|search_templates)/
|
||||||
|
blocks = STDIN.read.split(/\n(?=\d+\))/)
|
||||||
|
header = blocks.shift || ""
|
||||||
|
kept = blocks.reject { |b| b.scan(%r{app/mcp/tools/\S+\.rb}).all? { |f| f.match?(excluded) } }
|
||||||
|
kept_mass = kept.sum { |b| b.match(/mass = (\d+)/i) ? $1.to_i : 0 }
|
||||||
|
puts "Total score (lower is better) = #{kept_mass}"
|
||||||
|
puts kept.join("\n")
|
||||||
|
')
|
||||||
|
|
||||||
|
SCORE=$(echo "$RESULT" | grep 'Total score' | awk -F= '{print $2}' | tr -d ' ')
|
||||||
|
|
||||||
|
if [ "$SCORE" != "0" ] && echo "$RESULT" | grep -q 'Similar code found'; then
|
||||||
|
echo "$RESULT"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
32
bin/linters/flog
Executable file
32
bin/linters/flog
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
export FLOG_SCORE=${FLOG_SCORE:-50}
|
||||||
|
|
||||||
|
flog_args=()
|
||||||
|
paths=()
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == -* ]]; then
|
||||||
|
flog_args+=("$arg")
|
||||||
|
else
|
||||||
|
paths+=("$arg")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#paths[@]} -eq 0 ]; then
|
||||||
|
paths=(app lib)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Use $() to actually capture the pipeline's output into the RESULT variable
|
||||||
|
RESULT=$(bundle exec flog "${flog_args[@]}" "${paths[@]}" | awk -v threshold="$FLOG_SCORE" '{ if ($1+0 > threshold) print $0 }' | grep -v ': flog total' | grep -v '#none' || true)
|
||||||
|
|
||||||
|
|
||||||
|
# 2. Check if the string is non-empty
|
||||||
|
if [ -n "$RESULT" ]; then
|
||||||
|
# RESULT has content, exit with code 1
|
||||||
|
echo "$RESULT"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
# RESULT is empty, exit with code 0
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
31
bin/linters/haml-hardcoded-strings
Executable file
31
bin/linters/haml-hardcoded-strings
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
require "haml_lint"
|
||||||
|
require "haml_lint/cli"
|
||||||
|
|
||||||
|
require_relative "../../lib/haml_lint/linter/find_hardcoded_strings"
|
||||||
|
|
||||||
|
require "tempfile"
|
||||||
|
|
||||||
|
# Write a temporary config to enable only this linter
|
||||||
|
config_content = <<~YAML
|
||||||
|
require:
|
||||||
|
- '#{File.expand_path("../../lib/haml_lint/linter/find_hardcoded_strings.rb", __dir__)}'
|
||||||
|
linters:
|
||||||
|
FindHardcodedStrings:
|
||||||
|
enabled: true
|
||||||
|
YAML
|
||||||
|
|
||||||
|
Tempfile.open([".haml-lint-hardcoded", ".yml"]) do |f|
|
||||||
|
f.write(config_content)
|
||||||
|
f.flush
|
||||||
|
|
||||||
|
args = ARGV.empty? ? ["app/views"] : ARGV
|
||||||
|
args += ["-c", f.path, "--include-linter", "FindHardcodedStrings"]
|
||||||
|
|
||||||
|
success = system("bundle", "exec", "haml-lint", *args)
|
||||||
|
exit success ? 0 : 1
|
||||||
|
end
|
||||||
79
bin/linters/haml-syntax
Executable file
79
bin/linters/haml-syntax
Executable file
@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
require "find"
|
||||||
|
require "haml"
|
||||||
|
|
||||||
|
error_count = 0
|
||||||
|
warning_count = 0
|
||||||
|
|
||||||
|
# Patterns that compile but produce wrong HTML in Haml 7.
|
||||||
|
# e.g. `.text-base-content\\/60` creates class "text-base-content" with text "\/60"
|
||||||
|
# instead of class "text-base-content/60". Use {class: "..."} attribute hashes.
|
||||||
|
SUSPECT_CLASS_PATTERNS = [
|
||||||
|
%r{^\s*[%.]\S*\\/}, # escaped slash in dot-notation: .foo\/bar
|
||||||
|
/^\s*[%.]\S*\\:/, # escaped colon in dot-notation: .hover\:bg-red
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
Find.find(File.expand_path("../../app/views", __dir__)) do |path|
|
||||||
|
next unless path.end_with?(".haml")
|
||||||
|
|
||||||
|
source = File.read(path)
|
||||||
|
|
||||||
|
# --- Errors ---
|
||||||
|
begin
|
||||||
|
compiled = Haml::Engine.new.call(source)
|
||||||
|
# Check for Ruby syntax errors in the compiled code
|
||||||
|
# Wrapping in a `def` so `yield` and `return` in templates don't trigger syntax errors.
|
||||||
|
RubyVM::InstructionSequence.compile("def _haml_compilation_test\n#{compiled}\nend\n")
|
||||||
|
rescue Haml::SyntaxError => e
|
||||||
|
$stderr.puts "\nFile: #{path}"
|
||||||
|
$stderr.puts e.message
|
||||||
|
error_count += 1
|
||||||
|
next
|
||||||
|
rescue SyntaxError => e
|
||||||
|
$stderr.puts "\nFile: #{path}"
|
||||||
|
$stderr.puts "Ruby syntax error in compiled Haml:"
|
||||||
|
$stderr.puts e.message
|
||||||
|
error_count += 1
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
# Haml 7 defers certain errors (e.g. illegal nesting) into the compiled
|
||||||
|
# Ruby source as `raise Haml::SyntaxError.new(...)` instead of failing at
|
||||||
|
# parse time. Detect those deferred errors by scanning the output.
|
||||||
|
if compiled.include?("Haml::SyntaxError")
|
||||||
|
if (match = compiled.match(/Haml::SyntaxError\.new\(%q\[(.+?)\]/))
|
||||||
|
message = match[1]
|
||||||
|
end
|
||||||
|
if (line_match = compiled.match(/Haml::SyntaxError\.new\(%q\[.+?\],\s*(\d+)\)/))
|
||||||
|
line = line_match[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
$stderr.puts "\nFile: #{path}#{line ? ":#{line}" : ""}"
|
||||||
|
$stderr.puts message || "Deferred Haml::SyntaxError detected in compiled output"
|
||||||
|
error_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Recommendations ---
|
||||||
|
# Detect escaped class patterns that compile but produce incorrect HTML.
|
||||||
|
source.each_line.with_index(1) do |line, lineno|
|
||||||
|
SUSPECT_CLASS_PATTERNS.each do |pattern|
|
||||||
|
next unless line.match?(pattern)
|
||||||
|
|
||||||
|
$stderr.puts "\n[warning] File: #{path}:#{lineno}"
|
||||||
|
$stderr.puts " Escaped class in dot-notation produces incorrect HTML in Haml 7."
|
||||||
|
$stderr.puts " Use {class: \"...\"} attribute hash instead."
|
||||||
|
$stderr.puts " > #{line.strip}"
|
||||||
|
warning_count += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if warning_count > 0
|
||||||
|
$stderr.puts "\n#{warning_count} warning(s) found (do not cause build failure)"
|
||||||
|
end
|
||||||
|
|
||||||
|
exit(error_count.zero? ? 0 : 1)
|
||||||
55
bin/linters/helpers/merge_coverage
Executable file
55
bin/linters/helpers/merge_coverage
Executable file
@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Merges multiple SimpleCov result sets in .resultset.json into a single
|
||||||
|
# combined entry. This is necessary because Skunk (via RubyCritic) reads
|
||||||
|
# only the FIRST entry from the resultset, missing coverage data from
|
||||||
|
# other parallel test groups.
|
||||||
|
# It also synchronously formats the final coverage to avoid parallel test
|
||||||
|
# race conditions corrupting coverage/coverage.json
|
||||||
|
|
||||||
|
require "simplecov"
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
RESULTSET_PATH = File.expand_path("../../..", __dir__) + "/coverage/.resultset.json"
|
||||||
|
|
||||||
|
unless File.exist?(RESULTSET_PATH)
|
||||||
|
warn "[merge_coverage] No .resultset.json found"
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
|
|
||||||
|
data = JSON.parse(File.read(RESULTSET_PATH))
|
||||||
|
suite_count = data.keys.reject { |k| k == "merged" || k.include?(",") }.size
|
||||||
|
|
||||||
|
# Run standard collation which generates coverage/coverage.json perfectly
|
||||||
|
SimpleCov.collate(Dir[RESULTSET_PATH], "rails") do
|
||||||
|
add_filter "/spec/"
|
||||||
|
add_filter "/config/"
|
||||||
|
add_filter "/vendor/"
|
||||||
|
|
||||||
|
add_group "Channels", "app/channels"
|
||||||
|
add_group "Controllers", "app/controllers"
|
||||||
|
add_group "Models", "app/models"
|
||||||
|
add_group "MCP Tools", "app/mcp"
|
||||||
|
add_group "Helpers", "app/helpers"
|
||||||
|
add_group "Jobs", "app/jobs"
|
||||||
|
add_group "Services", "app/services"
|
||||||
|
|
||||||
|
if ENV["COVERAGE_FORMAT"] == "json"
|
||||||
|
require "simplecov_json_formatter"
|
||||||
|
formatter SimpleCov::Formatter::JSONFormatter
|
||||||
|
else
|
||||||
|
formatter SimpleCov::Formatter::HTMLFormatter
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Now clean up .resultset.json so that Skunk parses it correctly.
|
||||||
|
# SimpleCov.collate joined all command names into a single massive key.
|
||||||
|
# We extract that largest merged value, and rewrite .resultset.json with ONLY "merged"
|
||||||
|
new_data = JSON.parse(File.read(RESULTSET_PATH))
|
||||||
|
# Find the collated key (it will contain commas since it joined multiple suite names, or it will be the most recently touched)
|
||||||
|
merged_payload = new_data.values.max_by { |v| v["timestamp"].to_i }
|
||||||
|
|
||||||
|
File.write(RESULTSET_PATH, JSON.generate({ "merged" => merged_payload }))
|
||||||
|
|
||||||
|
puts "[merge_coverage] Merged #{suite_count > 0 ? suite_count : 1} suites into single entry"
|
||||||
14
bin/linters/helpers/nplus1_analysis
Executable file
14
bin/linters/helpers/nplus1_analysis
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
NPLUS1_MARKER="/tmp/nplus1_detected"
|
||||||
|
NPLUS1_REPORT="/tmp/nplus1_report.txt"
|
||||||
|
|
||||||
|
if [ -f "$NPLUS1_MARKER" ]; then
|
||||||
|
n1_details=""
|
||||||
|
[ -f "$NPLUS1_REPORT" ] && n1_details=$(cat "$NPLUS1_REPORT")
|
||||||
|
printf "========================================\n❌ FAILED: N+1 queries detected\n%s\n\n" "$n1_details"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
printf "✅ SUCCESS: No N+1 queries detected\n"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
5
bin/linters/i18n-tasks
Executable file
5
bin/linters/i18n-tasks
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Run i18n-tasks health which checks missing, unused, interpolations, and normalization
|
||||||
|
bundle exec i18n-tasks health
|
||||||
|
exit $?
|
||||||
37
bin/linters/nplus1
Executable file
37
bin/linters/nplus1
Executable file
@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# N+1 query linter — runs ALL specs via parallel_rspec for speed.
|
||||||
|
# Uses SIMPLECOV_PREFIX so SimpleCov generates unique command names per worker
|
||||||
|
# (spec_helper.rb builds "nplus1-{TEST_ENV_NUMBER}").
|
||||||
|
#
|
||||||
|
# Exit code reflects ONLY N+1 failures (via marker file), not other test failures.
|
||||||
|
|
||||||
|
export NPLUS1_CHECK=1
|
||||||
|
export SILENT_TESTS=${SILENT_TESTS:-1}
|
||||||
|
export RSPEC_RETRY_NUMBER=${RSPEC_RETRY_NUMBER:-3}
|
||||||
|
export COVERAGE_FORMAT=${COVERAGE_FORMAT:-json}
|
||||||
|
export SIMPLECOV_PREFIX="nplus1"
|
||||||
|
|
||||||
|
NPLUS1_MARKER="/tmp/nplus1_detected"
|
||||||
|
NPLUS1_REPORT="/tmp/nplus1_report.txt"
|
||||||
|
rm -f "$NPLUS1_MARKER" "$NPLUS1_REPORT"
|
||||||
|
export NPLUS1_MARKER_FILE="$NPLUS1_MARKER"
|
||||||
|
export NPLUS1_REPORT_FILE="$NPLUS1_REPORT"
|
||||||
|
|
||||||
|
NPLUS1_FORMATTER="--require ./spec/support/nplus1_formatter.rb --format progress --format Nplus1Formatter"
|
||||||
|
|
||||||
|
printf "▶ nplus1: scanning all specs in parallel\n"
|
||||||
|
bundle exec parallel_rspec --serialize-stdout \
|
||||||
|
--test-options "-f progress -f failures $NPLUS1_FORMATTER" \
|
||||||
|
spec || true
|
||||||
|
|
||||||
|
# Exit based on N+1 detection only, not other test failures
|
||||||
|
if [ -f "$NPLUS1_MARKER" ]; then
|
||||||
|
echo "❌ FAILED: N+1 queries detected"
|
||||||
|
[ -f "$NPLUS1_REPORT" ] && cat "$NPLUS1_REPORT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ SUCCESS: No N+1 queries detected"
|
||||||
|
|
||||||
|
exit 0
|
||||||
18
bin/linters/prspec_fast
Executable file
18
bin/linters/prspec_fast
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
start_time=$SECONDS
|
||||||
|
output=$(DISABLE_SPRING=1 ./bin/prspec --format=failures --tag=~integration --tag=~slow 2>&1)
|
||||||
|
exit_code=$?
|
||||||
|
elapsed=$(( SECONDS - start_time ))
|
||||||
|
|
||||||
|
specs_count=$(echo "$output" | grep -oE '^[0-9]+ examples,' | tail -n 1 | grep -oE '[0-9]+')
|
||||||
|
specs_str=""
|
||||||
|
[ -n "$specs_count" ] && specs_str=" ($specs_count specs)"
|
||||||
|
|
||||||
|
if [ "$exit_code" -ne 0 ]; then
|
||||||
|
filtered_output=$(echo "$output" | grep -vE '^(Coverage report|Randomized with seed|\[\{\"id\":)' | sed '/^$/N;/^\n$/D')
|
||||||
|
printf "========================================\n❌ FAILED: ./bin/prspec --format=failures --tag=~integration --tag=~slow 2>&1 (%ss)%s\n%s\n\n" "$elapsed" "$specs_str" "$filtered_output"
|
||||||
|
exit $exit_code
|
||||||
|
else
|
||||||
|
printf "✅ SUCCESS: ./bin/prspec --format=failures --tag=~integration --tag=~slow 2>&1 (%ss)%s\n" "$elapsed" "$specs_str"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
28
bin/linters/prspec_long
Executable file
28
bin/linters/prspec_long
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
export NPLUS1_MARKER_FILE="/tmp/nplus1_detected"
|
||||||
|
export NPLUS1_REPORT_FILE="/tmp/nplus1_report.txt"
|
||||||
|
export NPLUS1_CHECK=1
|
||||||
|
export SPEC_OPTS="--require ./spec/support/nplus1_formatter.rb --format Nplus1Formatter"
|
||||||
|
export COVERAGE_FORMAT=json
|
||||||
|
export RSPEC_RETRY_NUMBER=${RSPEC_RETRY_NUMBER:-3}
|
||||||
|
export SIMPLECOV_PREFIX=nplus1
|
||||||
|
|
||||||
|
rm -f "$NPLUS1_MARKER_FILE" "$NPLUS1_REPORT_FILE"
|
||||||
|
|
||||||
|
start_time=$SECONDS
|
||||||
|
output=$(DISABLE_SPRING=1 ./bin/prspec --format=failures --tag=~integration 2>&1)
|
||||||
|
exit_code=$?
|
||||||
|
elapsed=$(( SECONDS - start_time ))
|
||||||
|
|
||||||
|
specs_count=$(echo "$output" | grep -oE '^[0-9]+ examples,' | tail -n 1 | grep -oE '[0-9]+')
|
||||||
|
specs_str=""
|
||||||
|
[ -n "$specs_count" ] && specs_str=" ($specs_count specs)"
|
||||||
|
|
||||||
|
if [ "$exit_code" -ne 0 ]; then
|
||||||
|
filtered_output=$(echo "$output" | grep -vE '^(JSON Coverage report|Randomized with seed|\{"id":|Run options: exclude|[0-9]+ / [0-9]+ LOC|\s*$)' || true)
|
||||||
|
printf "========================================\n❌ FAILED: ./bin/prspec --format=failures --tag=~integration 2>&1 (%ss)%s\n%s\n\n" "$elapsed" "$specs_str" "$filtered_output"
|
||||||
|
exit $exit_code
|
||||||
|
else
|
||||||
|
printf "✅ SUCCESS: ./bin/prspec --format=failures --tag=~integration 2>&1 (%ss)%s\n" "$elapsed" "$specs_str"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
10
bin/linters/reek
Executable file
10
bin/linters/reek
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
ARGV.unshift("--config", File.expand_path("../../.reek.yml", __dir__))
|
||||||
|
ARGV.push("app", "lib") unless ARGV.any? { |a| !a.start_with?("-") }
|
||||||
|
|
||||||
|
load Gem.bin_path("reek", "reek")
|
||||||
2
bin/linters/rspec
Executable file
2
bin/linters/rspec
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
exec ./bin/prspec "$@"
|
||||||
10
bin/linters/rubocop
Executable file
10
bin/linters/rubocop
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
# Explicit RuboCop config increases performance slightly while avoiding config confusion.
|
||||||
|
ARGV.unshift("--config", File.expand_path("../../.rubocop.yml", __dir__))
|
||||||
|
|
||||||
|
load Gem.bin_path("rubocop", "rubocop")
|
||||||
39
bin/linters/skunk
Executable file
39
bin/linters/skunk
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
MAX_AVERAGE_SCORE=${MAX_SKUNK_AVG_SCORE:-600}
|
||||||
|
MAX_FILE_SCORE=${MAX_SKUNK_FILE_SCORE:-1500}
|
||||||
|
|
||||||
|
OUTPUT=$(if [ $# -eq 0 ]; then bundle exec skunk app lib 2>&1; else bundle exec skunk "$@" 2>&1; fi)
|
||||||
|
EXIT_CODE=$?
|
||||||
|
echo "$OUTPUT"
|
||||||
|
|
||||||
|
if [ $EXIT_CODE -ne 0 ]; then
|
||||||
|
exit $EXIT_CODE
|
||||||
|
fi
|
||||||
|
|
||||||
|
AVERAGE=$(echo "$OUTPUT" | grep "SkunkScore Average:" | sed 's/.*Average: *//' | awk '{print $1}')
|
||||||
|
WORST_LINE=$(echo "$OUTPUT" | grep "Worst SkunkScore:")
|
||||||
|
WORST=$(echo "$WORST_LINE" | sed 's/.*SkunkScore: *//' | awk '{print $1}')
|
||||||
|
|
||||||
|
if [ -z "$AVERAGE" ] || [ -z "$WORST" ]; then
|
||||||
|
echo "⚠️ Could not parse SkunkScore values"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
if awk "BEGIN {exit !($AVERAGE > $MAX_AVERAGE_SCORE)}"; then
|
||||||
|
echo "❌ SkunkScore Average ($AVERAGE) exceeds threshold ($MAX_AVERAGE_SCORE)"
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if awk "BEGIN {exit !($WORST > $MAX_FILE_SCORE)}"; then
|
||||||
|
echo "❌ Worst SkunkScore ($WORST) exceeds threshold ($MAX_FILE_SCORE)"
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $FAILED -eq 0 ]; then
|
||||||
|
echo "✅ SkunkScore within thresholds (avg: $AVERAGE <= $MAX_AVERAGE_SCORE, worst: $WORST <= $MAX_FILE_SCORE)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $FAILED
|
||||||
23
bin/prspec
Executable file
23
bin/prspec
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export SILENT_TESTS=${SILENT_TESTS:-}
|
||||||
|
|
||||||
|
OPTS=""
|
||||||
|
FILES=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == -* ]]; then
|
||||||
|
OPTS="$OPTS $arg"
|
||||||
|
else
|
||||||
|
FILES="$FILES $arg"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Default to spec directory if no files specified
|
||||||
|
if [ -z "$FILES" ]; then
|
||||||
|
FILES="spec"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Running Parallel RSpec with options: $OPTS"
|
||||||
|
|
||||||
|
# FORMATTER="${FORMATTER:--f progress -f failures}"
|
||||||
|
bundle exec parallel_rspec --serialize-stdout --test-options "$FORMATTER$OPTS" $FILES
|
||||||
4
bin/rails
Executable file
4
bin/rails
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
APP_PATH = File.expand_path("../config/application", __dir__)
|
||||||
|
require_relative "../config/boot"
|
||||||
|
require "rails/commands"
|
||||||
4
bin/rake
Executable file
4
bin/rake
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
require_relative "../config/boot"
|
||||||
|
require "rake"
|
||||||
|
Rake.application.run
|
||||||
8
bin/rubocop
Executable file
8
bin/rubocop
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
# Explicit RuboCop config increases performance slightly while avoiding config confusion.
|
||||||
|
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
|
||||||
|
|
||||||
|
load Gem.bin_path("rubocop", "rubocop")
|
||||||
36
bin/setup
Executable file
36
bin/setup
Executable file
@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
require "fileutils"
|
||||||
|
|
||||||
|
APP_ROOT = File.expand_path("..", __dir__)
|
||||||
|
|
||||||
|
def system!(*args)
|
||||||
|
system(*args, exception: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
FileUtils.chdir APP_ROOT do
|
||||||
|
# This script is a way to set up or update your development environment automatically.
|
||||||
|
# This script is idempotent, so that you can run it at any time and get an expectable outcome.
|
||||||
|
# Add necessary setup steps to this file.
|
||||||
|
|
||||||
|
puts "== Installing dependencies =="
|
||||||
|
system("bundle check") || system!("bundle install")
|
||||||
|
|
||||||
|
# puts "\n== Copying sample files =="
|
||||||
|
# unless File.exist?("config/database.yml")
|
||||||
|
# FileUtils.cp "config/database.yml.sample", "config/database.yml"
|
||||||
|
# end
|
||||||
|
|
||||||
|
puts "\n== Preparing database =="
|
||||||
|
system! "bin/rails db:prepare"
|
||||||
|
system! "bin/rails db:reset" if ARGV.include?("--reset")
|
||||||
|
system! "bin/rails parallel:prepare" if ENV["RAILS_ENV"] == "test" || ARGV.include?("--prepare-test")
|
||||||
|
|
||||||
|
puts "\n== Removing old logs and tempfiles =="
|
||||||
|
system! "bin/rails log:clear tmp:clear"
|
||||||
|
|
||||||
|
unless ARGV.include?("--skip-server")
|
||||||
|
puts "\n== Starting development server =="
|
||||||
|
STDOUT.flush # flush the output before exec(2) so that it displays
|
||||||
|
exec "bin/dev"
|
||||||
|
end
|
||||||
|
end
|
||||||
5
bin/thrust
Executable file
5
bin/thrust
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
load Gem.bin_path("thruster", "thrust")
|
||||||
8
config.ru
Normal file
8
config.ru
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# This file is used by Rack-based servers to start the application.
|
||||||
|
|
||||||
|
require_relative "config/environment"
|
||||||
|
|
||||||
|
run Rails.application
|
||||||
|
Rails.application.load_server
|
||||||
48
config/application.rb
Normal file
48
config/application.rb
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative "boot"
|
||||||
|
|
||||||
|
require "rails"
|
||||||
|
# Pick the frameworks you want:
|
||||||
|
require "active_model/railtie"
|
||||||
|
require "active_job/railtie"
|
||||||
|
require "active_record/railtie"
|
||||||
|
require "active_storage/engine"
|
||||||
|
require "action_controller/railtie"
|
||||||
|
# require "action_mailer/railtie"
|
||||||
|
# require "action_mailbox/engine"
|
||||||
|
# require "action_text/engine"
|
||||||
|
require "action_view/railtie"
|
||||||
|
require "action_cable/engine"
|
||||||
|
# require "rails/test_unit/railtie"
|
||||||
|
|
||||||
|
# Require the gems listed in Gemfile, including any gems
|
||||||
|
# you've limited to :test, :development, or :production.
|
||||||
|
Bundler.require(*Rails.groups)
|
||||||
|
|
||||||
|
module Starter
|
||||||
|
class Application < Rails::Application
|
||||||
|
# Initialize configuration defaults for originally generated Rails version.
|
||||||
|
config.load_defaults(8.1)
|
||||||
|
|
||||||
|
config.i18n.available_locales = [:en, :uk]
|
||||||
|
config.i18n.default_locale = :en
|
||||||
|
|
||||||
|
# Please, add to the `ignore` list any other `lib` subdirectories that do
|
||||||
|
# not contain `.rb` files, or that should not be reloaded or eager loaded.
|
||||||
|
# Common ones are `templates`, `generators`, or `middleware`, for example.
|
||||||
|
config.autoload_lib(ignore: ["assets", "tasks"])
|
||||||
|
|
||||||
|
# Configuration for the application, engines, and railties goes here.
|
||||||
|
#
|
||||||
|
# These settings can be overridden in specific environments using the files
|
||||||
|
# in config/environments, which are processed later.
|
||||||
|
#
|
||||||
|
# config.time_zone = "Central Time (US & Canada)"
|
||||||
|
# config.eager_load_paths << Rails.root.join("extras")
|
||||||
|
|
||||||
|
# Don't generate system test files.
|
||||||
|
config.generators.system_tests = nil
|
||||||
|
config.active_job.queue_adapter = :sidekiq
|
||||||
|
end
|
||||||
|
end
|
||||||
6
config/boot.rb
Normal file
6
config/boot.rb
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
||||||
|
|
||||||
|
require "bundler/setup" # Set up gems listed in the Gemfile.
|
||||||
|
require "bootsnap/setup" # Speed up boot time by caching expensive operations.
|
||||||
5
config/bundler-audit.yml
Normal file
5
config/bundler-audit.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit.
|
||||||
|
# CVEs that are not relevant to the application can be enumerated on the ignore list below.
|
||||||
|
|
||||||
|
ignore:
|
||||||
|
- CVE-THAT-DOES-NOT-APPLY
|
||||||
8
config/cable.yml
Normal file
8
config/cable.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
development:
|
||||||
|
adapter: anycable
|
||||||
|
|
||||||
|
test:
|
||||||
|
adapter: test
|
||||||
|
|
||||||
|
production:
|
||||||
|
adapter: anycable
|
||||||
11
config/cache.yml
Normal file
11
config/cache.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
development:
|
||||||
|
redis:
|
||||||
|
url: <%= ENV.fetch("REDIS_URL", "redis://localhost:6379/1") %>
|
||||||
|
|
||||||
|
test:
|
||||||
|
redis:
|
||||||
|
url: <%= ENV.fetch("REDIS_URL", "redis://localhost:6379/1") %>
|
||||||
|
|
||||||
|
production:
|
||||||
|
redis:
|
||||||
|
url: <%= ENV.fetch("REDIS_URL", "redis://localhost:6379/1") %>
|
||||||
21
config/ci.rb
Normal file
21
config/ci.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Run using bin/ci
|
||||||
|
|
||||||
|
CI.run do
|
||||||
|
step "Setup", "bin/setup --skip-server"
|
||||||
|
|
||||||
|
step "Style: Ruby", "bin/rubocop"
|
||||||
|
|
||||||
|
step "Security: Gem audit", "bin/bundler-audit"
|
||||||
|
step "Security: Importmap vulnerability audit", "bin/importmap audit"
|
||||||
|
step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
|
||||||
|
|
||||||
|
# Optional: set a green GitHub commit status to unblock PR merge.
|
||||||
|
# Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
|
||||||
|
# if success?
|
||||||
|
# step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
|
||||||
|
# else
|
||||||
|
# failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
|
||||||
|
# end
|
||||||
|
end
|
||||||
31
config/database.yml
Normal file
31
config/database.yml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
default: &default
|
||||||
|
adapter: postgresql
|
||||||
|
encoding: unicode
|
||||||
|
prepared_statements: false
|
||||||
|
advisory_locks: false
|
||||||
|
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
||||||
|
host: "<%= ENV.fetch('DB_HOST', 'localhost') %>"
|
||||||
|
port: "<%= ENV.fetch('DB_PORT', '5432') %>"
|
||||||
|
username: "<%= ENV.fetch('DB_USER', 'postgres') %>"
|
||||||
|
password: "<%= ENV.fetch('DB_PASS', 'password') %>"
|
||||||
|
gssencmode: disable
|
||||||
|
connect_timeout: 5
|
||||||
|
checkout_timeout: 5
|
||||||
|
reaping_frequency: 10
|
||||||
|
|
||||||
|
development:
|
||||||
|
primary:
|
||||||
|
<<: *default
|
||||||
|
database: "<%= ENV.fetch('DB_NAME', 'starter') %>_development"
|
||||||
|
|
||||||
|
test:
|
||||||
|
primary:
|
||||||
|
<<: *default
|
||||||
|
database: "<%= ENV.fetch('DB_NAME', 'starter') %>_test<%= ENV['TEST_ENV_NUMBER'] %>"
|
||||||
|
|
||||||
|
production:
|
||||||
|
primary:
|
||||||
|
<<: *default
|
||||||
|
host: <%= ENV.fetch("DB_PRIMARY_HOST") { ENV.fetch("DB_HOST", "localhost") } %>
|
||||||
|
port: <%= ENV.fetch("DB_PRIMARY_PORT") { ENV.fetch("DB_PORT", "5432") } %>
|
||||||
|
database: <%= ENV.fetch("DB_NAME", "starter") %>_production
|
||||||
7
config/environment.rb
Normal file
7
config/environment.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Load the Rails application.
|
||||||
|
require_relative "application"
|
||||||
|
|
||||||
|
# Initialize the Rails application.
|
||||||
|
Rails.application.initialize!
|
||||||
70
config/environments/development.rb
Normal file
70
config/environments/development.rb
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "active_support/core_ext/integer/time"
|
||||||
|
|
||||||
|
Rails.application.configure do
|
||||||
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|
||||||
|
# Make code changes take effect immediately without server restart.
|
||||||
|
config.enable_reloading = true
|
||||||
|
|
||||||
|
# Do not eager load code on boot.
|
||||||
|
config.eager_load = false
|
||||||
|
|
||||||
|
# Show full error reports.
|
||||||
|
config.consider_all_requests_local = true
|
||||||
|
|
||||||
|
# Enable server timing.
|
||||||
|
config.server_timing = true
|
||||||
|
|
||||||
|
# Enable/disable Action Controller caching. By default Action Controller caching is disabled.
|
||||||
|
# Run rails dev:cache to toggle Action Controller caching.
|
||||||
|
if Rails.root.join("tmp/caching-dev.txt").exist?
|
||||||
|
config.action_controller.perform_caching = true
|
||||||
|
config.action_controller.enable_fragment_cache_logging = true
|
||||||
|
config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
|
||||||
|
else
|
||||||
|
config.action_controller.perform_caching = false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Change to :null_store to avoid any caching.
|
||||||
|
config.cache_store = :memory_store
|
||||||
|
|
||||||
|
# Print deprecation notices to the Rails logger.
|
||||||
|
config.active_support.deprecation = :log
|
||||||
|
|
||||||
|
# Raise an error on page load if there are pending migrations.
|
||||||
|
config.active_record.migration_error = :page_load
|
||||||
|
|
||||||
|
# Highlight code that triggered database queries in logs.
|
||||||
|
config.active_record.verbose_query_logs = true
|
||||||
|
|
||||||
|
# Append comments with runtime information tags to SQL queries in logs.
|
||||||
|
config.active_record.query_log_tags_enabled = true
|
||||||
|
|
||||||
|
# Highlight code that enqueued background job in logs.
|
||||||
|
config.active_job.verbose_enqueue_logs = true
|
||||||
|
|
||||||
|
# Highlight code that triggered redirect in logs.
|
||||||
|
config.action_dispatch.verbose_redirect_logs = true
|
||||||
|
|
||||||
|
# Suppress logger output for asset requests.
|
||||||
|
config.assets.quiet = true
|
||||||
|
|
||||||
|
# Raises error for missing translations.
|
||||||
|
# config.i18n.raise_on_missing_translations = true
|
||||||
|
|
||||||
|
# Annotate rendered view with file names.
|
||||||
|
config.action_view.annotate_rendered_view_with_filenames = true
|
||||||
|
|
||||||
|
# Uncomment if you wish to allow Action Cable access from any origin.
|
||||||
|
# config.action_cable.disable_request_forgery_protection = true
|
||||||
|
|
||||||
|
# Raise error when a before_action's only/except options reference missing actions.
|
||||||
|
config.action_controller.raise_on_missing_callback_actions = true
|
||||||
|
|
||||||
|
config.hosts.clear
|
||||||
|
|
||||||
|
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
|
||||||
|
# config.generators.apply_rubocop_autocorrect_after_generate!
|
||||||
|
end
|
||||||
73
config/environments/production.rb
Normal file
73
config/environments/production.rb
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "active_support/core_ext/integer/time"
|
||||||
|
|
||||||
|
Rails.application.configure do
|
||||||
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|
||||||
|
# Code is not reloaded between requests.
|
||||||
|
config.enable_reloading = false
|
||||||
|
|
||||||
|
# Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
|
||||||
|
config.eager_load = true
|
||||||
|
|
||||||
|
# Full error reports are disabled.
|
||||||
|
config.consider_all_requests_local = false
|
||||||
|
|
||||||
|
# Turn on fragment caching in view templates.
|
||||||
|
config.action_controller.perform_caching = true
|
||||||
|
|
||||||
|
# Cache assets for far-future expiry since they are all digest stamped.
|
||||||
|
config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }
|
||||||
|
|
||||||
|
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
|
||||||
|
# config.asset_host = "http://assets.example.com"
|
||||||
|
|
||||||
|
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
|
||||||
|
# config.assume_ssl = true
|
||||||
|
|
||||||
|
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
|
||||||
|
# config.force_ssl = true
|
||||||
|
|
||||||
|
# Skip http-to-https redirect for the default health check endpoint.
|
||||||
|
# config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
|
||||||
|
|
||||||
|
# Log to STDOUT with the current request id as a default log tag.
|
||||||
|
config.log_tags = [:request_id]
|
||||||
|
config.logger = ActiveSupport::TaggedLogging.logger($stdout)
|
||||||
|
|
||||||
|
# Change to "debug" to log everything (including potentially personally-identifiable information!).
|
||||||
|
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
|
||||||
|
|
||||||
|
# Prevent health checks from clogging up the logs.
|
||||||
|
config.silence_healthcheck_path = "/up"
|
||||||
|
|
||||||
|
# Don't log any deprecations.
|
||||||
|
config.active_support.report_deprecations = false
|
||||||
|
|
||||||
|
# Replace the default in-process memory cache store with a durable alternative.
|
||||||
|
config.cache_store = :solid_cache_store
|
||||||
|
|
||||||
|
# Replace the default in-process and non-durable queuing backend for Active Job.
|
||||||
|
config.active_job.queue_adapter = :solid_queue
|
||||||
|
config.solid_queue.connects_to = { database: { writing: :queue } }
|
||||||
|
|
||||||
|
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||||
|
# the I18n.default_locale when a translation cannot be found).
|
||||||
|
config.i18n.fallbacks = true
|
||||||
|
|
||||||
|
# Do not dump schema after migrations.
|
||||||
|
config.active_record.dump_schema_after_migration = false
|
||||||
|
|
||||||
|
# Only use :id for inspections in production.
|
||||||
|
config.active_record.attributes_for_inspect = [:id]
|
||||||
|
|
||||||
|
# Enable DNS rebinding protection and other `Host` header attacks.
|
||||||
|
# config.hosts = [
|
||||||
|
# "example.com", # Allow requests from example.com
|
||||||
|
# /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# Skip DNS rebinding protection for the default health check endpoint.
|
||||||
|
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
|
||||||
|
end
|
||||||
44
config/environments/test.rb
Normal file
44
config/environments/test.rb
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# The test environment is used exclusively to run your application's
|
||||||
|
# test suite. You never need to work with it otherwise. Remember that
|
||||||
|
# your test database is "scratch space" for the test suite and is wiped
|
||||||
|
# and recreated between test runs. Don't rely on the data there!
|
||||||
|
|
||||||
|
Rails.application.configure do
|
||||||
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|
||||||
|
# While tests run files are not watched, reloading is not necessary.
|
||||||
|
config.enable_reloading = false
|
||||||
|
|
||||||
|
# Eager loading loads your entire application. When running a single test locally,
|
||||||
|
# this is usually not necessary, and can slow down your test suite. However, it's
|
||||||
|
# recommended that you enable it in continuous integration systems to ensure eager
|
||||||
|
# loading is working properly before deploying your code.
|
||||||
|
config.eager_load = ENV["CI"].present?
|
||||||
|
|
||||||
|
# Configure public file server for tests with cache-control for performance.
|
||||||
|
config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }
|
||||||
|
|
||||||
|
# Show full error reports.
|
||||||
|
config.consider_all_requests_local = true
|
||||||
|
config.cache_store = :null_store
|
||||||
|
|
||||||
|
# Render exception templates for rescuable exceptions and raise for other exceptions.
|
||||||
|
config.action_dispatch.show_exceptions = :rescuable
|
||||||
|
|
||||||
|
# Disable request forgery protection in test environment.
|
||||||
|
config.action_controller.allow_forgery_protection = false
|
||||||
|
|
||||||
|
# Print deprecation notices to the stderr.
|
||||||
|
config.active_support.deprecation = :stderr
|
||||||
|
|
||||||
|
# Raises error for missing translations.
|
||||||
|
# config.i18n.raise_on_missing_translations = true
|
||||||
|
|
||||||
|
# Annotate rendered view with file names.
|
||||||
|
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||||
|
|
||||||
|
# Raise error when a before_action's only/except options reference missing actions.
|
||||||
|
config.action_controller.raise_on_missing_callback_actions = true
|
||||||
|
end
|
||||||
9
config/importmap.rb
Normal file
9
config/importmap.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Pin npm packages by running ./bin/importmap
|
||||||
|
|
||||||
|
pin "application"
|
||||||
|
pin "three", to: "https://ga.jspm.io/npm:three@0.183.2/build/three.module.js"
|
||||||
|
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
||||||
|
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
||||||
|
pin_all_from "app/javascript/controllers", under: "controllers"
|
||||||
36
config/initializers/app_creds.rb
Normal file
36
config/initializers/app_creds.rb
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Polyfill for Rails 8.2 CombinedCredentials and Rails.app alias
|
||||||
|
unless Rails.respond_to?(:app)
|
||||||
|
Rails.singleton_class.alias_method(:app, :application)
|
||||||
|
end
|
||||||
|
|
||||||
|
module CombinedCredentialsPolyfill
|
||||||
|
class CombinedCreds
|
||||||
|
def require(*keys)
|
||||||
|
env_key = keys.join("__").upcase
|
||||||
|
return ENV.fetch(env_key) if ENV.key?(env_key)
|
||||||
|
|
||||||
|
val = Rails.application.credentials.dig(*keys)
|
||||||
|
raise KeyError, "Key not found: #{keys.join(".")}" if val.nil?
|
||||||
|
|
||||||
|
val
|
||||||
|
end
|
||||||
|
|
||||||
|
def option(*keys, default: nil)
|
||||||
|
env_key = keys.join("__").upcase
|
||||||
|
return ENV.fetch(env_key) if ENV.key?(env_key)
|
||||||
|
|
||||||
|
val = Rails.application.credentials.dig(*keys)
|
||||||
|
return val unless val.nil?
|
||||||
|
|
||||||
|
default.respond_to?(:call) ? default.call : default
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def creds
|
||||||
|
@creds ||= CombinedCreds.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Rails::Application.include(CombinedCredentialsPolyfill) unless Rails::Application.method_defined?(:creds)
|
||||||
9
config/initializers/assets.rb
Normal file
9
config/initializers/assets.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
|
# Version of your assets, change this if you want to expire all your assets.
|
||||||
|
Rails.application.config.assets.version = "1.0"
|
||||||
|
|
||||||
|
# Add additional assets to the asset load path.
|
||||||
|
# Rails.application.config.assets.paths << Emoji.images_path
|
||||||
31
config/initializers/content_security_policy.rb
Normal file
31
config/initializers/content_security_policy.rb
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
|
# Define an application-wide content security policy.
|
||||||
|
# See the Securing Rails Applications Guide for more information:
|
||||||
|
# https://guides.rubyonrails.org/security.html#content-security-policy-header
|
||||||
|
|
||||||
|
# Rails.application.configure do
|
||||||
|
# config.content_security_policy do |policy|
|
||||||
|
# policy.default_src :self, :https
|
||||||
|
# policy.font_src :self, :https, :data
|
||||||
|
# policy.img_src :self, :https, :data
|
||||||
|
# policy.object_src :none
|
||||||
|
# policy.script_src :self, :https
|
||||||
|
# policy.style_src :self, :https
|
||||||
|
# # Specify URI for violation reports
|
||||||
|
# # policy.report_uri "/csp-violation-report-endpoint"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
|
||||||
|
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
|
||||||
|
# config.content_security_policy_nonce_directives = %w(script-src style-src)
|
||||||
|
#
|
||||||
|
# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
|
||||||
|
# # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
|
||||||
|
# # config.content_security_policy_nonce_auto = true
|
||||||
|
#
|
||||||
|
# # Report violations without enforcing the policy.
|
||||||
|
# # config.content_security_policy_report_only = true
|
||||||
|
# end
|
||||||
10
config/initializers/filter_parameter_logging.rb
Normal file
10
config/initializers/filter_parameter_logging.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
|
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
|
||||||
|
# Use this to limit dissemination of sensitive information.
|
||||||
|
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
|
||||||
|
Rails.application.config.filter_parameters += [
|
||||||
|
:passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc,
|
||||||
|
]
|
||||||
18
config/initializers/inflections.rb
Normal file
18
config/initializers/inflections.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
|
# Add new inflection rules using the following format. Inflections
|
||||||
|
# are locale specific, and you may define rules for as many different
|
||||||
|
# locales as you wish. All of these examples are active by default:
|
||||||
|
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||||
|
# inflect.plural /^(ox)$/i, "\\1en"
|
||||||
|
# inflect.singular /^(ox)en/i, "\\1"
|
||||||
|
# inflect.irregular "person", "people"
|
||||||
|
# inflect.uncountable %w( fish sheep )
|
||||||
|
# end
|
||||||
|
|
||||||
|
# These inflection rules are supported but not enabled by default:
|
||||||
|
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||||
|
# inflect.acronym "RESTful"
|
||||||
|
# end
|
||||||
54
config/initializers/rack_attack.rb
Normal file
54
config/initializers/rack_attack.rb
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# typed: false
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Rack::Attack configuration for rate limiting.
|
||||||
|
# Uses Redis as shared store (important since web containers scale to 3 replicas).
|
||||||
|
|
||||||
|
# ── Store ────────────────────────────────────────────────────────
|
||||||
|
redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379/1")
|
||||||
|
Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(url: redis_url)
|
||||||
|
|
||||||
|
# Disable in test/e2e unless explicitly enabled
|
||||||
|
unless ENV["ENABLE_RACK_ATTACK"] == "1"
|
||||||
|
Rack::Attack.enabled = false if Rails.env.test? || Rails.env.e2e?
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Defaults ─────────────────────────────────────────────────────
|
||||||
|
DEFAULT_UI_RPH = ENV.fetch("RACK_ATTACK_UI_RPH", 5000).to_i
|
||||||
|
DEFAULT_API_RPH = ENV.fetch("RACK_ATTACK_API_RPH", 5000).to_i
|
||||||
|
|
||||||
|
# ── Safelist ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Rack::Attack.safelist("health-check") do |req|
|
||||||
|
req.path == "/up"
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Throttle: UI (by IP) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
Rack::Attack.throttle("ui/ip", limit: DEFAULT_UI_RPH, period: 3600) do |req|
|
||||||
|
next if req.path.start_with?("/api/", "/api")
|
||||||
|
|
||||||
|
req.ip
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Throttle: API (by IP) ────────────────────────────────────────
|
||||||
|
|
||||||
|
Rack::Attack.throttle("api/ip", limit: DEFAULT_API_RPH, period: 3600) do |req|
|
||||||
|
next unless req.path.start_with?("/api/")
|
||||||
|
|
||||||
|
req.ip
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Throttled Response ───────────────────────────────────────────
|
||||||
|
|
||||||
|
Rack::Attack.throttled_responder = lambda { |req|
|
||||||
|
matched = req.env["rack.attack.match_data"] || {}
|
||||||
|
retry_after = (matched[:period] || 60) - (matched[:epoch_time] % (matched[:period] || 60))
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type" => "application/json",
|
||||||
|
"Retry-After" => retry_after.to_s,
|
||||||
|
}
|
||||||
|
body = { error: { code: "RATE_LIMITED", message: "Rate limit exceeded. Retry after #{retry_after} seconds." } }.to_json
|
||||||
|
[429, headers, [body]]
|
||||||
|
}
|
||||||
37
config/initializers/sidekiq.rb
Normal file
37
config/initializers/sidekiq.rb
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# typed: false
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "sidekiq"
|
||||||
|
require "sidekiq-cron"
|
||||||
|
|
||||||
|
redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379/1")
|
||||||
|
redis_config = { url: redis_url }
|
||||||
|
redis_config[:ssl_params] = { verify_mode: OpenSSL::SSL::VERIFY_NONE } if redis_url.start_with?("rediss://")
|
||||||
|
|
||||||
|
Sidekiq.configure_server do |config|
|
||||||
|
config.redis = redis_config
|
||||||
|
config.logger.level = Logger::WARN if Rails.env.e2e?
|
||||||
|
|
||||||
|
schedule_file = "config/recurring.yml"
|
||||||
|
|
||||||
|
if File.exist?(schedule_file)
|
||||||
|
schedule = YAML.load_file(schedule_file, aliases: true)[Rails.env]
|
||||||
|
if schedule
|
||||||
|
# Map Solid Queue's "schedule" key to "cron" for sidekiq-cron (which uses Fugit internally)
|
||||||
|
cron_hash = schedule.transform_values do |job|
|
||||||
|
{
|
||||||
|
"cron" => job["schedule"],
|
||||||
|
"class" => job["class"],
|
||||||
|
"queue" => job["queue"] || "default",
|
||||||
|
"active_job" => true,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
Sidekiq::Cron::Job.load_from_hash(cron_hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Sidekiq.configure_client do |config|
|
||||||
|
config.redis = redis_config
|
||||||
|
config.logger.level = Logger::WARN if Rails.env.e2e?
|
||||||
|
end
|
||||||
31
config/locales/en.yml
Normal file
31
config/locales/en.yml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Files in the config/locales directory are used for internationalization and
|
||||||
|
# are automatically loaded by Rails. If you want to use locales other than
|
||||||
|
# English, add the necessary files in this directory.
|
||||||
|
#
|
||||||
|
# To use the locales, use `I18n.t`:
|
||||||
|
#
|
||||||
|
# I18n.t "hello"
|
||||||
|
#
|
||||||
|
# In views, this is aliased to just `t`:
|
||||||
|
#
|
||||||
|
# <%= t("hello") %>
|
||||||
|
#
|
||||||
|
# To use a different locale, set it with `I18n.locale`:
|
||||||
|
#
|
||||||
|
# I18n.locale = :es
|
||||||
|
#
|
||||||
|
# This would use the information in config/locales/es.yml.
|
||||||
|
#
|
||||||
|
# To learn more about the API, please read the Rails Internationalization guide
|
||||||
|
# at https://guides.rubyonrails.org/i18n.html.
|
||||||
|
#
|
||||||
|
# Be aware that YAML interprets the following case-insensitive strings as
|
||||||
|
# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
|
||||||
|
# must be quoted to be interpreted as strings. For example:
|
||||||
|
#
|
||||||
|
# en:
|
||||||
|
# "yes": yup
|
||||||
|
# enabled: "ON"
|
||||||
|
|
||||||
|
en:
|
||||||
|
hello: "Hello world"
|
||||||
44
config/puma.rb
Normal file
44
config/puma.rb
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# This configuration file will be evaluated by Puma. The top-level methods that
|
||||||
|
# are invoked here are part of Puma's configuration DSL. For more information
|
||||||
|
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
|
||||||
|
#
|
||||||
|
# Puma starts a configurable number of processes (workers) and each process
|
||||||
|
# serves each request in a thread from an internal thread pool.
|
||||||
|
#
|
||||||
|
# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You
|
||||||
|
# should only set this value when you want to run 2 or more workers. The
|
||||||
|
# default is already 1. You can set it to `auto` to automatically start a worker
|
||||||
|
# for each available processor.
|
||||||
|
#
|
||||||
|
# The ideal number of threads per worker depends both on how much time the
|
||||||
|
# application spends waiting for IO operations and on how much you wish to
|
||||||
|
# prioritize throughput over latency.
|
||||||
|
#
|
||||||
|
# As a rule of thumb, increasing the number of threads will increase how much
|
||||||
|
# traffic a given process can handle (throughput), but due to CRuby's
|
||||||
|
# Global VM Lock (GVL) it has diminishing returns and will degrade the
|
||||||
|
# response time (latency) of the application.
|
||||||
|
#
|
||||||
|
# The default is set to 3 threads as it's deemed a decent compromise between
|
||||||
|
# throughput and latency for the average Rails application.
|
||||||
|
#
|
||||||
|
# Any libraries that use a connection pool or another resource pool should
|
||||||
|
# be configured to provide at least as many connections as the number of
|
||||||
|
# threads. This includes Active Record's `pool` parameter in `database.yml`.
|
||||||
|
threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
|
||||||
|
threads threads_count, threads_count
|
||||||
|
|
||||||
|
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
|
||||||
|
port ENV.fetch("PORT", 3000)
|
||||||
|
|
||||||
|
# Allow puma to be restarted by `bin/rails restart` command.
|
||||||
|
plugin :tmp_restart
|
||||||
|
|
||||||
|
# Run the Solid Queue supervisor inside of Puma for single-server deployments.
|
||||||
|
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]
|
||||||
|
|
||||||
|
# Specify the PID file. Defaults to tmp/pids/server.pid in development.
|
||||||
|
# In other environments, only set the PID file if requested.
|
||||||
|
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user