summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-03-30 19:03:22 +0900
committerAdam Malczewski <[email protected]>2026-03-30 19:03:22 +0900
commit952c6b565832dd0dbcef7a9a80edc871f79e15a8 (patch)
tree698db27a35f8f40356a0381a32a02767481dc6a9
downloaddispatch-api-main.tar.gz
dispatch-api-main.zip
-rw-r--r--.dockerignore45
-rw-r--r--.gitattributes9
-rw-r--r--.github/dependabot.yml12
-rw-r--r--.github/workflows/ci.yml52
-rw-r--r--.gitignore33
-rwxr-xr-x.kamal/hooks/docker-setup.sample3
-rwxr-xr-x.kamal/hooks/post-app-boot.sample3
-rwxr-xr-x.kamal/hooks/post-deploy.sample14
-rwxr-xr-x.kamal/hooks/post-proxy-reboot.sample3
-rwxr-xr-x.kamal/hooks/pre-app-boot.sample3
-rwxr-xr-x.kamal/hooks/pre-build.sample51
-rwxr-xr-x.kamal/hooks/pre-connect.sample47
-rwxr-xr-x.kamal/hooks/pre-deploy.sample122
-rwxr-xr-x.kamal/hooks/pre-proxy-reboot.sample3
-rw-r--r--.kamal/secrets20
-rw-r--r--.rspec1
-rw-r--r--.rubocop.yml8
-rw-r--r--.ruby-version1
-rw-r--r--Dockerfile74
-rw-r--r--Gemfile52
-rw-r--r--Gemfile.lock508
-rw-r--r--README.md24
-rw-r--r--Rakefile6
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/concerns/.keep0
-rw-r--r--app/jobs/application_job.rb7
-rw-r--r--app/mailers/application_mailer.rb4
-rw-r--r--app/models/application_record.rb3
-rw-r--r--app/models/concerns/.keep0
-rw-r--r--app/views/layouts/mailer.html.erb13
-rw-r--r--app/views/layouts/mailer.text.erb1
-rwxr-xr-xbin/brakeman7
-rwxr-xr-xbin/bundler-audit6
-rwxr-xr-xbin/ci6
-rwxr-xr-xbin/dev2
-rwxr-xr-xbin/docker-entrypoint8
-rwxr-xr-xbin/jobs6
-rwxr-xr-xbin/kamal16
-rwxr-xr-xbin/rails4
-rwxr-xr-xbin/rake4
-rwxr-xr-xbin/rubocop8
-rwxr-xr-xbin/setup35
-rwxr-xr-xbin/thrust5
-rw-r--r--config.ru6
-rw-r--r--config/application.rb44
-rw-r--r--config/boot.rb4
-rw-r--r--config/bundler-audit.yml5
-rw-r--r--config/cable.yml17
-rw-r--r--config/cache.yml16
-rw-r--r--config/ci.rb19
-rw-r--r--config/credentials.yml.enc1
-rw-r--r--config/database.yml104
-rw-r--r--config/deploy.yml115
-rw-r--r--config/environment.rb5
-rw-r--r--config/environments/development.rb73
-rw-r--r--config/environments/production.rb87
-rw-r--r--config/environments/test.rb53
-rw-r--r--config/initializers/cors.rb16
-rw-r--r--config/initializers/filter_parameter_logging.rb8
-rw-r--r--config/initializers/inflections.rb16
-rw-r--r--config/locales/en.yml31
-rw-r--r--config/puma.rb42
-rw-r--r--config/queue.yml18
-rw-r--r--config/recurring.yml15
-rw-r--r--config/routes.rb10
-rw-r--r--config/storage.yml27
-rw-r--r--db/cable_schema.rb11
-rw-r--r--db/cache_schema.rb12
-rw-r--r--db/queue_schema.rb129
-rw-r--r--db/seeds.rb9
-rw-r--r--lib/tasks/.keep0
-rw-r--r--log/.keep0
-rw-r--r--public/robots.txt1
-rw-r--r--script/.keep0
-rw-r--r--spec/rails_helper.rb72
-rw-r--r--spec/spec_helper.rb94
-rw-r--r--storage/.keep0
-rw-r--r--tmp/.keep0
-rw-r--r--tmp/pids/.keep0
-rw-r--r--tmp/storage/.keep0
-rw-r--r--vendor/.keep0
81 files changed, 2291 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..9355f68
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,45 @@
+# 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 CI service files.
+/.github
+
+# Ignore Kamal files.
+/config/deploy*.yml
+/.kamal
+
+# Ignore development files
+/.devcontainer
+
+# Ignore Docker-related files
+/.dockerignore
+/Dockerfile*
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..8dc4323
--- /dev/null
+++ b/.gitattributes
@@ -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
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..83610cf
--- /dev/null
+++ b/.github/dependabot.yml
@@ -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
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..2098f1f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,52 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches: [ main ]
+
+jobs:
+ scan_ruby:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+
+ - name: Scan for common Rails security vulnerabilities using static analysis
+ run: bin/brakeman --no-pager
+
+ - name: Scan for known security vulnerabilities in gems used
+ run: bin/bundler-audit
+
+ lint:
+ runs-on: ubuntu-latest
+ env:
+ RUBOCOP_CACHE_ROOT: tmp/rubocop
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+
+ - name: Prepare RuboCop cache
+ uses: actions/cache@v4
+ env:
+ DEPENDENCIES_HASH: ${{ hashFiles('.ruby-version', '**/.rubocop.yml', '**/.rubocop_todo.yml', 'Gemfile.lock') }}
+ with:
+ path: ${{ env.RUBOCOP_CACHE_ROOT }}
+ key: rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-${{ github.ref_name == github.event.repository.default_branch && github.run_id || 'default' }}
+ restore-keys: |
+ rubocop-${{ runner.os }}-${{ env.DEPENDENCIES_HASH }}-
+
+ - name: Lint code for consistent style
+ run: bin/rubocop -f github
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e30bb69
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,33 @@
+# 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
+
+# Ignore key files for decrypting credentials and more.
+/config/*.key
+
diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample
new file mode 100755
index 0000000..2fb07d7
--- /dev/null
+++ b/.kamal/hooks/docker-setup.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Docker set up on $KAMAL_HOSTS..."
diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample
new file mode 100755
index 0000000..70f9c4b
--- /dev/null
+++ b/.kamal/hooks/post-app-boot.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."
diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample
new file mode 100755
index 0000000..fd364c2
--- /dev/null
+++ b/.kamal/hooks/post-deploy.sample
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+# A sample post-deploy hook
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_ROLES (if set)
+# KAMAL_DESTINATION (if set)
+# KAMAL_RUNTIME
+
+echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample
new file mode 100755
index 0000000..1435a67
--- /dev/null
+++ b/.kamal/hooks/post-proxy-reboot.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Rebooted kamal-proxy on $KAMAL_HOSTS"
diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample
new file mode 100755
index 0000000..45f7355
--- /dev/null
+++ b/.kamal/hooks/pre-app-boot.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."
diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample
new file mode 100755
index 0000000..c5a5567
--- /dev/null
+++ b/.kamal/hooks/pre-build.sample
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+# A sample pre-build hook
+#
+# Checks:
+# 1. We have a clean checkout
+# 2. A remote is configured
+# 3. The branch has been pushed to the remote
+# 4. The version we are deploying matches the remote
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_ROLES (if set)
+# KAMAL_DESTINATION (if set)
+
+if [ -n "$(git status --porcelain)" ]; then
+ echo "Git checkout is not clean, aborting..." >&2
+ git status --porcelain >&2
+ exit 1
+fi
+
+first_remote=$(git remote)
+
+if [ -z "$first_remote" ]; then
+ echo "No git remote set, aborting..." >&2
+ exit 1
+fi
+
+current_branch=$(git branch --show-current)
+
+if [ -z "$current_branch" ]; then
+ echo "Not on a git branch, aborting..." >&2
+ exit 1
+fi
+
+remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
+
+if [ -z "$remote_head" ]; then
+ echo "Branch not pushed to remote, aborting..." >&2
+ exit 1
+fi
+
+if [ "$KAMAL_VERSION" != "$remote_head" ]; then
+ echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
+ exit 1
+fi
+
+exit 0
diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample
new file mode 100755
index 0000000..77744bd
--- /dev/null
+++ b/.kamal/hooks/pre-connect.sample
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+
+# A sample pre-connect check
+#
+# Warms DNS before connecting to hosts in parallel
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_ROLES (if set)
+# KAMAL_DESTINATION (if set)
+# KAMAL_RUNTIME
+
+hosts = ENV["KAMAL_HOSTS"].split(",")
+results = nil
+max = 3
+
+elapsed = Benchmark.realtime do
+ results = hosts.map do |host|
+ Thread.new do
+ tries = 1
+
+ begin
+ Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
+ rescue SocketError
+ if tries < max
+ puts "Retrying DNS warmup: #{host}"
+ tries += 1
+ sleep rand
+ retry
+ else
+ puts "DNS warmup failed: #{host}"
+ host
+ end
+ end
+
+ tries
+ end
+ end.map(&:value)
+end
+
+retries = results.sum - hosts.size
+nopes = results.count { |r| r == max }
+
+puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample
new file mode 100755
index 0000000..05b3055
--- /dev/null
+++ b/.kamal/hooks/pre-deploy.sample
@@ -0,0 +1,122 @@
+#!/usr/bin/env ruby
+
+# A sample pre-deploy hook
+#
+# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
+#
+# Fails unless the combined status is "success"
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_COMMAND
+# KAMAL_SUBCOMMAND
+# KAMAL_ROLES (if set)
+# KAMAL_DESTINATION (if set)
+
+# Only check the build status for production deployments
+if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
+ exit 0
+end
+
+require "bundler/inline"
+
+# true = install gems so this is fast on repeat invocations
+gemfile(true, quiet: true) do
+ source "https://rubygems.org"
+
+ gem "octokit"
+ gem "faraday-retry"
+end
+
+MAX_ATTEMPTS = 72
+ATTEMPTS_GAP = 10
+
+def exit_with_error(message)
+ $stderr.puts message
+ exit 1
+end
+
+class GithubStatusChecks
+ attr_reader :remote_url, :git_sha, :github_client, :combined_status
+
+ def initialize
+ @remote_url = github_repo_from_remote_url
+ @git_sha = `git rev-parse HEAD`.strip
+ @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
+ refresh!
+ end
+
+ def refresh!
+ @combined_status = github_client.combined_status(remote_url, git_sha)
+ end
+
+ def state
+ combined_status[:state]
+ end
+
+ def first_status_url
+ first_status = combined_status[:statuses].find { |status| status[:state] == state }
+ first_status && first_status[:target_url]
+ end
+
+ def complete_count
+ combined_status[:statuses].count { |status| status[:state] != "pending"}
+ end
+
+ def total_count
+ combined_status[:statuses].count
+ end
+
+ def current_status
+ if total_count > 0
+ "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
+ else
+ "Build not started..."
+ end
+ end
+
+ private
+ def github_repo_from_remote_url
+ url = `git config --get remote.origin.url`.strip.delete_suffix(".git")
+ if url.start_with?("https://github.com/")
+ url.delete_prefix("https://github.com/")
+ elsif url.start_with?("[email protected]:")
+ url.delete_prefix("[email protected]:")
+ else
+ url
+ end
+ end
+end
+
+
+$stdout.sync = true
+
+begin
+ puts "Checking build status..."
+
+ attempts = 0
+ checks = GithubStatusChecks.new
+
+ loop do
+ case checks.state
+ when "success"
+ puts "Checks passed, see #{checks.first_status_url}"
+ exit 0
+ when "failure"
+ exit_with_error "Checks failed, see #{checks.first_status_url}"
+ when "pending"
+ attempts += 1
+ end
+
+ exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
+
+ puts checks.current_status
+ sleep(ATTEMPTS_GAP)
+ checks.refresh!
+ end
+rescue Octokit::NotFound
+ exit_with_error "Build status could not be found"
+end
diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample
new file mode 100755
index 0000000..061f805
--- /dev/null
+++ b/.kamal/hooks/pre-proxy-reboot.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."
diff --git a/.kamal/secrets b/.kamal/secrets
new file mode 100644
index 0000000..b3089d6
--- /dev/null
+++ b/.kamal/secrets
@@ -0,0 +1,20 @@
+# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
+# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
+# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
+
+# Example of extracting secrets from 1password (or another compatible pw manager)
+# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
+# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS})
+# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS})
+
+# Example of extracting secrets from Rails credentials
+# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)
+
+# Use a GITHUB_TOKEN if private repositories are needed for the image
+# GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
+
+# Grab the registry password from ENV
+# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
+
+# Improve security by using a password manager. Never check config/master.key into git!
+RAILS_MASTER_KEY=$(cat config/master.key)
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..c99d2e7
--- /dev/null
+++ b/.rspec
@@ -0,0 +1 @@
+--require spec_helper
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..f9d86d4
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,8 @@
+# Omakase Ruby styling for Rails
+inherit_gem: { rubocop-rails-omakase: rubocop.yml }
+
+# Overwrite or add rules to create your own house style
+#
+# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
+# Layout/SpaceInsideArrayLiteralBrackets:
+# Enabled: false
diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 0000000..90cdbdc
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+ruby-4.0.1
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..42f29b9
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,74 @@
+# 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 dispatch_api .
+# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name dispatch_api dispatch_api
+
+# 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 libvips 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
+
+# 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 libvips 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/
+
+
+
+
+# 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"]
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..222a1e7
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,52 @@
+source "https://rubygems.org"
+
+# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
+gem "rails", "~> 8.1.3"
+# Use postgresql as the database for Active Record
+gem "pg", "~> 1.1"
+# Use the Puma web server [https://github.com/puma/puma]
+gem "puma", ">= 5.0"
+# Build JSON APIs with ease [https://github.com/rails/jbuilder]
+# gem "jbuilder"
+
+# 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: %i[ windows jruby ]
+
+# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable
+gem "solid_cache"
+gem "solid_queue"
+gem "solid_cable"
+
+# 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 Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
+gem "image_processing", "~> 1.2"
+
+# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible
+# gem "rack-cors"
+
+group :development, :test do
+ # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
+ gem "debug", platforms: %i[ 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
+
+ # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
+ gem "rubocop-rails-omakase", require: false
+end
+
+gem "rspec-rails", "~> 8.0", groups: [:development, :test]
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..a4686a3
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,508 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ action_text-trix (2.1.18)
+ railties
+ actioncable (8.1.3)
+ actionpack (= 8.1.3)
+ activesupport (= 8.1.3)
+ nio4r (~> 2.0)
+ websocket-driver (>= 0.6.1)
+ zeitwerk (~> 2.6)
+ actionmailbox (8.1.3)
+ actionpack (= 8.1.3)
+ activejob (= 8.1.3)
+ activerecord (= 8.1.3)
+ activestorage (= 8.1.3)
+ activesupport (= 8.1.3)
+ mail (>= 2.8.0)
+ actionmailer (8.1.3)
+ actionpack (= 8.1.3)
+ actionview (= 8.1.3)
+ activejob (= 8.1.3)
+ activesupport (= 8.1.3)
+ mail (>= 2.8.0)
+ rails-dom-testing (~> 2.2)
+ actionpack (8.1.3)
+ actionview (= 8.1.3)
+ activesupport (= 8.1.3)
+ 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.6)
+ useragent (~> 0.16)
+ actiontext (8.1.3)
+ action_text-trix (~> 2.1.15)
+ actionpack (= 8.1.3)
+ activerecord (= 8.1.3)
+ activestorage (= 8.1.3)
+ activesupport (= 8.1.3)
+ globalid (>= 0.6.0)
+ nokogiri (>= 1.8.5)
+ actionview (8.1.3)
+ activesupport (= 8.1.3)
+ builder (~> 3.1)
+ erubi (~> 1.11)
+ rails-dom-testing (~> 2.2)
+ rails-html-sanitizer (~> 1.6)
+ activejob (8.1.3)
+ activesupport (= 8.1.3)
+ globalid (>= 0.3.6)
+ activemodel (8.1.3)
+ activesupport (= 8.1.3)
+ activerecord (8.1.3)
+ activemodel (= 8.1.3)
+ activesupport (= 8.1.3)
+ timeout (>= 0.4.0)
+ activestorage (8.1.3)
+ actionpack (= 8.1.3)
+ activejob (= 8.1.3)
+ activerecord (= 8.1.3)
+ activesupport (= 8.1.3)
+ marcel (~> 1.0)
+ activesupport (8.1.3)
+ 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)
+ securerandom (>= 0.3)
+ tzinfo (~> 2.0, >= 2.0.5)
+ uri (>= 0.13.1)
+ ast (2.4.3)
+ base64 (0.3.0)
+ bcrypt_pbkdf (1.1.2)
+ bigdecimal (4.1.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)
+ concurrent-ruby (1.3.6)
+ connection_pool (3.0.2)
+ crass (1.0.6)
+ date (3.5.1)
+ debug (1.11.1)
+ irb (~> 1.10)
+ reline (>= 0.3.8)
+ diff-lcs (1.6.2)
+ dotenv (3.2.0)
+ drb (2.2.3)
+ ed25519 (1.4.0)
+ erb (6.0.2)
+ erubi (1.13.1)
+ et-orbi (1.4.0)
+ tzinfo
+ ffi (1.17.4-aarch64-linux-gnu)
+ ffi (1.17.4-aarch64-linux-musl)
+ ffi (1.17.4-arm-linux-gnu)
+ ffi (1.17.4-arm-linux-musl)
+ ffi (1.17.4-x86_64-linux-gnu)
+ ffi (1.17.4-x86_64-linux-musl)
+ fugit (1.12.1)
+ et-orbi (~> 1.4)
+ raabro (~> 1.4)
+ globalid (1.3.0)
+ activesupport (>= 6.1)
+ i18n (1.14.8)
+ concurrent-ruby (~> 1.0)
+ image_processing (1.14.0)
+ mini_magick (>= 4.9.5, < 6)
+ ruby-vips (>= 2.0.17, < 3)
+ 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)
+ 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)
+ 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
+ marcel (1.1.0)
+ mini_magick (5.3.1)
+ logger
+ 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-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)
+ parser (3.3.11.1)
+ ast (~> 2.4.1)
+ racc
+ pg (1.6.3)
+ pg (1.6.3-aarch64-linux)
+ pg (1.6.3-aarch64-linux-musl)
+ 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)
+ psych (5.3.1)
+ date
+ stringio
+ puma (7.2.0)
+ nio4r (~> 2.0)
+ raabro (1.4.0)
+ racc (1.8.1)
+ rack (3.2.5)
+ 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 (8.1.3)
+ actioncable (= 8.1.3)
+ actionmailbox (= 8.1.3)
+ actionmailer (= 8.1.3)
+ actionpack (= 8.1.3)
+ actiontext (= 8.1.3)
+ actionview (= 8.1.3)
+ activejob (= 8.1.3)
+ activemodel (= 8.1.3)
+ activerecord (= 8.1.3)
+ activestorage (= 8.1.3)
+ activesupport (= 8.1.3)
+ bundler (>= 1.15.0)
+ railties (= 8.1.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)
+ railties (8.1.3)
+ actionpack (= 8.1.3)
+ activesupport (= 8.1.3)
+ irb (~> 1.13)
+ rackup (>= 1.0.0)
+ rake (>= 12.2)
+ thor (~> 1.0, >= 1.2.2)
+ tsort (>= 0.2)
+ zeitwerk (~> 2.6)
+ rainbow (3.1.1)
+ rake (13.3.1)
+ rdoc (7.2.0)
+ erb
+ psych (>= 4.0.0)
+ tsort
+ regexp_parser (2.11.3)
+ reline (0.6.3)
+ io-console (~> 0.5)
+ 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-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-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-performance (1.26.1)
+ lint_roller (~> 1.1)
+ rubocop (>= 1.75.0, < 2.0)
+ rubocop-ast (>= 1.47.1, < 2.0)
+ 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-rails-omakase (1.1.0)
+ rubocop (>= 1.72)
+ rubocop-performance (>= 1.24)
+ rubocop-rails (>= 2.30)
+ ruby-progressbar (1.13.0)
+ ruby-vips (2.3.0)
+ ffi (~> 1.12)
+ logger
+ securerandom (0.4.1)
+ solid_cable (3.0.12)
+ actioncable (>= 7.2)
+ activejob (>= 7.2)
+ activerecord (>= 7.2)
+ railties (>= 7.2)
+ solid_cache (1.0.10)
+ activejob (>= 7.2)
+ activerecord (>= 7.2)
+ railties (>= 7.2)
+ solid_queue (1.4.0)
+ activejob (>= 7.1)
+ activerecord (>= 7.1)
+ concurrent-ruby (>= 1.3.1)
+ fugit (~> 1.11)
+ railties (>= 7.1)
+ thor (>= 1.3.1)
+ sshkit (1.25.0)
+ base64
+ logger
+ net-scp (>= 1.1.2)
+ net-sftp (>= 2.1.2)
+ net-ssh (>= 2.8.0)
+ ostruct
+ stringio (3.2.0)
+ thor (1.5.0)
+ thruster (0.1.20)
+ thruster (0.1.20-aarch64-linux)
+ thruster (0.1.20-x86_64-linux)
+ timeout (0.6.1)
+ tsort (0.2.0)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ unicode-display_width (3.2.0)
+ unicode-emoji (~> 4.1)
+ unicode-emoji (4.2.0)
+ uri (1.1.1)
+ useragent (0.16.11)
+ websocket-driver (0.8.0)
+ base64
+ websocket-extensions (>= 0.1.0)
+ websocket-extensions (0.1.5)
+ zeitwerk (2.7.5)
+
+PLATFORMS
+ aarch64-linux
+ aarch64-linux-gnu
+ aarch64-linux-musl
+ arm-linux-gnu
+ arm-linux-musl
+ x86_64-linux
+ x86_64-linux-gnu
+ x86_64-linux-musl
+
+DEPENDENCIES
+ bootsnap
+ brakeman
+ bundler-audit
+ debug
+ image_processing (~> 1.2)
+ kamal
+ pg (~> 1.1)
+ puma (>= 5.0)
+ rails (~> 8.1.3)
+ rspec-rails (~> 8.0)
+ rubocop-rails-omakase
+ solid_cable
+ solid_cache
+ solid_queue
+ thruster
+ tzinfo-data
+
+CHECKSUMS
+ action_text-trix (2.1.18) sha256=3fdb83f8bff4145d098be283cdd47ac41caf5110bfa6df4695ed7127d7fb3642
+ actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0
+ actionmailbox (8.1.3) sha256=df7da474eaa0e70df4ed5a6fef66eb3b3b0f2dbf7f14518deee8d77f1b4aae59
+ actionmailer (8.1.3) sha256=831f724891bb70d0aaa4d76581a6321124b6a752cb655c9346aae5479318448d
+ actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3
+ actiontext (8.1.3) sha256=d291019c00e1ea9e6463011fa214f6081a56d7b9a1d224e7d3f6384c1dafc7d2
+ actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d
+ activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3
+ activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219
+ activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab
+ activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d
+ activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
+ bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6
+ bigdecimal (4.1.0) sha256=6dc07767aa3dc456ccd48e7ae70a07b474e9afd7c5bc576f80bd6da5c8dd6cae
+ 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
+ concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
+ connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
+ crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
+ date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
+ debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
+ dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
+ drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
+ 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
+ ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
+ ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
+ ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
+ ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95
+ ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
+ ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
+ fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
+ globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
+ i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
+ image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
+ io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
+ irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
+ json (2.19.3) sha256=289b0bb53052a1fa8c34ab33cc750b659ba14a5c45f3fcf4b18762dc67c78646
+ kamal (2.11.0) sha256=1408864425e0dec7e0a14d712a3b13f614e9f3a425b7661d3f9d287a51d7dd75
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
+ 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
+ marcel (1.1.0) sha256=fdcfcfa33cc52e93c4308d40e4090a5d4ea279e160a7f6af988260fa970e0bee
+ mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4
+ 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-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
+ parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
+ 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-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
+ psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
+ 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-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
+ rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
+ rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
+ rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3
+ rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d
+ rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89
+ railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
+ rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
+ rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
+ regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
+ reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
+ rspec-rails (8.0.4) sha256=06235692fc0892683d3d34977e081db867434b3a24ae0dd0c6f3516bad4e22df
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
+ rubocop (1.86.0) sha256=4ff1186fe16ebe9baff5e7aad66bb0ad4cabf5cdcd419f773146dbba2565d186
+ rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
+ rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
+ rubocop-rails (2.34.3) sha256=10d37989024865ecda8199f311f3faca990143fbac967de943f88aca11eb9ad2
+ rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
+ ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
+ securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
+ solid_cable (3.0.12) sha256=a168a54731a455d5627af48d8441ea3b554b8c1f6e6cd6074109de493e6b0460
+ solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41
+ solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a
+ sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744
+ stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
+ thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
+ thruster (0.1.20) sha256=c05f2fbcae527bbe093a6e6d84fb12d9d680617e7c162325d9b97e8e9d4b5201
+ thruster (0.1.20-aarch64-linux) sha256=754f1701061235235165dde31e7a3bc87ec88066a02981ff4241fcda0d76d397
+ thruster (0.1.20-x86_64-linux) sha256=d579f252bf67aee6ba6d957e48f566b72e019d7657ba2f267a5db1e4d91d2479
+ timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
+ tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
+ useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
+ websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
+ websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
+ zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
+
+BUNDLED WITH
+ 4.0.9
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7db80e4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# README
+
+This README would normally document whatever steps are necessary to get the
+application up and running.
+
+Things you may want to cover:
+
+* Ruby version
+
+* System dependencies
+
+* Configuration
+
+* Database creation
+
+* Database initialization
+
+* How to run the test suite
+
+* Services (job queues, cache servers, search engines, etc.)
+
+* Deployment instructions
+
+* ...
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..9a5ea73
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,6 @@
+# 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
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
new file mode 100644
index 0000000..4ac8823
--- /dev/null
+++ b/app/controllers/application_controller.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::API
+end
diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/controllers/concerns/.keep
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
new file mode 100644
index 0000000..d394c3d
--- /dev/null
+++ b/app/jobs/application_job.rb
@@ -0,0 +1,7 @@
+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
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
new file mode 100644
index 0000000..3c34c81
--- /dev/null
+++ b/app/mailers/application_mailer.rb
@@ -0,0 +1,4 @@
+class ApplicationMailer < ActionMailer::Base
+ default from: "[email protected]"
+ layout "mailer"
+end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
new file mode 100644
index 0000000..b63caeb
--- /dev/null
+++ b/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ primary_abstract_class
+end
diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/models/concerns/.keep
diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb
new file mode 100644
index 0000000..3aac900
--- /dev/null
+++ b/app/views/layouts/mailer.html.erb
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <style>
+ /* Email styles need to be inline */
+ </style>
+ </head>
+
+ <body>
+ <%= yield %>
+ </body>
+</html>
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
new file mode 100644
index 0000000..37f0bdd
--- /dev/null
+++ b/app/views/layouts/mailer.text.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/bin/brakeman b/bin/brakeman
new file mode 100755
index 0000000..ace1c9b
--- /dev/null
+++ b/bin/brakeman
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+require "rubygems"
+require "bundler/setup"
+
+ARGV.unshift("--ensure-latest")
+
+load Gem.bin_path("brakeman", "brakeman")
diff --git a/bin/bundler-audit b/bin/bundler-audit
new file mode 100755
index 0000000..e2ef226
--- /dev/null
+++ b/bin/bundler-audit
@@ -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
diff --git a/bin/ci b/bin/ci
new file mode 100755
index 0000000..4137ad5
--- /dev/null
+++ b/bin/ci
@@ -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"
diff --git a/bin/dev b/bin/dev
new file mode 100755
index 0000000..5f91c20
--- /dev/null
+++ b/bin/dev
@@ -0,0 +1,2 @@
+#!/usr/bin/env ruby
+exec "./bin/rails", "server", *ARGV
diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint
new file mode 100755
index 0000000..ed31659
--- /dev/null
+++ b/bin/docker-entrypoint
@@ -0,0 +1,8 @@
+#!/bin/bash -e
+
+# If running the rails server then create or migrate existing database
+if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
+ ./bin/rails db:prepare
+fi
+
+exec "${@}"
diff --git a/bin/jobs b/bin/jobs
new file mode 100755
index 0000000..dcf59f3
--- /dev/null
+++ b/bin/jobs
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+
+require_relative "../config/environment"
+require "solid_queue/cli"
+
+SolidQueue::Cli.start(ARGV)
diff --git a/bin/kamal b/bin/kamal
new file mode 100755
index 0000000..d9ba276
--- /dev/null
+++ b/bin/kamal
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'kamal' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("kamal", "kamal")
diff --git a/bin/rails b/bin/rails
new file mode 100755
index 0000000..efc0377
--- /dev/null
+++ b/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path("../config/application", __dir__)
+require_relative "../config/boot"
+require "rails/commands"
diff --git a/bin/rake b/bin/rake
new file mode 100755
index 0000000..4fbf10b
--- /dev/null
+++ b/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative "../config/boot"
+require "rake"
+Rake.application.run
diff --git a/bin/rubocop b/bin/rubocop
new file mode 100755
index 0000000..5a20504
--- /dev/null
+++ b/bin/rubocop
@@ -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")
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..81be011
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,35 @@
+#!/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")
+
+ 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
diff --git a/bin/thrust b/bin/thrust
new file mode 100755
index 0000000..36bde2d
--- /dev/null
+++ b/bin/thrust
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("thruster", "thrust")
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000..4a3c09a
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,6 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative "config/environment"
+
+run Rails.application
+Rails.application.load_server
diff --git a/config/application.rb b/config/application.rb
new file mode 100644
index 0000000..480437a
--- /dev/null
+++ b/config/application.rb
@@ -0,0 +1,44 @@
+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 DispatchApi
+ class Application < Rails::Application
+ # Initialize configuration defaults for originally generated Rails version.
+ config.load_defaults 8.1
+
+ # 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: %w[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")
+
+ # Only loads a smaller set of middleware suitable for API only apps.
+ # Middleware like session, flash, cookies can be added back manually.
+ # Skip views, helpers and assets when generating a new resource.
+ config.api_only = true
+ end
+end
diff --git a/config/boot.rb b/config/boot.rb
new file mode 100644
index 0000000..988a5dd
--- /dev/null
+++ b/config/boot.rb
@@ -0,0 +1,4 @@
+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.
diff --git a/config/bundler-audit.yml b/config/bundler-audit.yml
new file mode 100644
index 0000000..e74b3af
--- /dev/null
+++ b/config/bundler-audit.yml
@@ -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
diff --git a/config/cable.yml b/config/cable.yml
new file mode 100644
index 0000000..b9adc5a
--- /dev/null
+++ b/config/cable.yml
@@ -0,0 +1,17 @@
+# Async adapter only works within the same process, so for manually triggering cable updates from a console,
+# and seeing results in the browser, you must do so from the web console (running inside the dev process),
+# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
+# to make the web console appear.
+development:
+ adapter: async
+
+test:
+ adapter: test
+
+production:
+ adapter: solid_cable
+ connects_to:
+ database:
+ writing: cable
+ polling_interval: 0.1.seconds
+ message_retention: 1.day
diff --git a/config/cache.yml b/config/cache.yml
new file mode 100644
index 0000000..19d4908
--- /dev/null
+++ b/config/cache.yml
@@ -0,0 +1,16 @@
+default: &default
+ store_options:
+ # Cap age of oldest cache entry to fulfill retention policies
+ # max_age: <%= 60.days.to_i %>
+ max_size: <%= 256.megabytes %>
+ namespace: <%= Rails.env %>
+
+development:
+ <<: *default
+
+test:
+ <<: *default
+
+production:
+ database: cache
+ <<: *default
diff --git a/config/ci.rb b/config/ci.rb
new file mode 100644
index 0000000..cf3f194
--- /dev/null
+++ b/config/ci.rb
@@ -0,0 +1,19 @@
+# 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: 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
diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc
new file mode 100644
index 0000000..1a751d2
--- /dev/null
+++ b/config/credentials.yml.enc
@@ -0,0 +1 @@
+t07jJPpg6SruGTIVWICyvJUY1fJ21KAHntvDOsYtteLaMwk1N/N6hg4wU0XE/jAc9bxP43kaQhPPO480QO930YLHEsJyN44YTj6iwnGoXKBuDSINPz+J1h3hZdrtGWnM7z2stCsEc5Qbe2agIzf0zasll1ttJCu/ZUpHPdtb2HXFuMB/I4++Hda8tyVI71nevg+WP6uOFq8ocC3lpgvCkU2bvWe6ztySz2V3PvjGtEnsA4v6iGW6qsSP2Ox2UWVUefET9yfw6VNfqUb7JdnkQbLAQ+/qH6UJrsHw+RM5s6vCfPx23zkh2lo96HoRj0WA2rztw4MqckSfLo2X7B2cZCSRXvwkRxTtm1l9HiCtk4awFnLJO+yQyln9yDebqGWIWD2dHk0WrNmKS9E/50ZYqdvhzNFKvY53Zvr/GWFubXyAOtpRf8R32RSB1EzaGJZih5yuKKvMfDKqO6eeCi6AJDdEdGZr6RU7QrnQbYbdo26FXp41Zc/wGdw/--tddex5XK7DaCs8EO--b/aJmA9laPKqhpaVJkosqQ== \ No newline at end of file
diff --git a/config/database.yml b/config/database.yml
new file mode 100644
index 0000000..f4f0cd0
--- /dev/null
+++ b/config/database.yml
@@ -0,0 +1,104 @@
+# PostgreSQL. Versions 9.5 and up are supported.
+#
+# Install the pg driver:
+# gem install pg
+# On macOS with Homebrew:
+# gem install pg -- --with-pg-config=/opt/homebrew/bin/pg_config
+# On Windows:
+# gem install pg
+# Choose the win32 build.
+# Install PostgreSQL and put its /bin directory on your path.
+#
+# Configure Using Gemfile
+# gem "pg"
+#
+default: &default
+ adapter: postgresql
+ encoding: unicode
+ # For details on connection pooling, see Rails configuration guide
+ # https://guides.rubyonrails.org/configuring.html#database-pooling
+ max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+
+
+development:
+ <<: *default
+ database: dispatch_api_development
+
+ # The specified database role being used to connect to PostgreSQL.
+ # To create additional roles in PostgreSQL see `$ createuser --help`.
+ # When left blank, PostgreSQL will use the default role. This is
+ # the same name as the operating system user running Rails.
+ #username: dispatch_api
+
+ # The password associated with the PostgreSQL role (username).
+ #password:
+
+ # Connect on a TCP socket. Omitted by default since the client uses a
+ # domain socket that doesn't need configuration. Windows does not have
+ # domain sockets, so uncomment these lines.
+ #host: localhost
+
+ # The TCP port the server listens on. Defaults to 5432.
+ # If your server runs on a different port number, change accordingly.
+ #port: 5432
+
+ # Schema search path. The server defaults to $user,public
+ #schema_search_path: myapp,sharedapp,public
+
+ # Minimum log levels, in increasing order:
+ # debug5, debug4, debug3, debug2, debug1,
+ # log, notice, warning, error, fatal, and panic
+ # Defaults to warning.
+ #min_messages: notice
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: dispatch_api_test
+
+# As with config/credentials.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password or a full connection URL as an environment
+# variable when you boot the app. For example:
+#
+# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
+#
+# If the connection URL is provided in the special DATABASE_URL environment
+# variable, Rails will automatically merge its configuration values on top of
+# the values provided in this file. Alternatively, you can specify a connection
+# URL environment variable explicitly:
+#
+# production:
+# url: <%= ENV["MY_APP_DATABASE_URL"] %>
+#
+# Connection URLs for non-primary databases can also be configured using
+# environment variables. The variable name is formed by concatenating the
+# connection name with `_DATABASE_URL`. For example:
+#
+# CACHE_DATABASE_URL="postgres://cacheuser:cachepass@localhost/cachedatabase"
+#
+# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full overview on how database connection configuration can be specified.
+#
+production:
+ primary: &primary_production
+ <<: *default
+ database: dispatch_api_production
+ username: dispatch_api
+ password: <%= ENV["DISPATCH_API_DATABASE_PASSWORD"] %>
+ cache:
+ <<: *primary_production
+ database: dispatch_api_production_cache
+ migrations_paths: db/cache_migrate
+ queue:
+ <<: *primary_production
+ database: dispatch_api_production_queue
+ migrations_paths: db/queue_migrate
+ cable:
+ <<: *primary_production
+ database: dispatch_api_production_cable
+ migrations_paths: db/cable_migrate
diff --git a/config/deploy.yml b/config/deploy.yml
new file mode 100644
index 0000000..e003107
--- /dev/null
+++ b/config/deploy.yml
@@ -0,0 +1,115 @@
+# Name of your application. Used to uniquely configure containers.
+service: dispatch_api
+
+# Name of the container image (use your-user/app-name on external registries).
+image: dispatch_api
+
+# Deploy to these servers.
+servers:
+ web:
+ - 192.168.0.1
+ # job:
+ # hosts:
+ # - 192.168.0.1
+ # cmd: bin/jobs
+
+# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
+# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
+#
+# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb!
+#
+# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer).
+#
+# proxy:
+# ssl: true
+# host: app.example.com
+
+# Where you keep your container images.
+registry:
+ # Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ...
+ server: localhost:5555
+
+ # Needed for authenticated registries.
+ # username: your-user
+
+ # Always use an access token rather than real password when possible.
+ # password:
+ # - KAMAL_REGISTRY_PASSWORD
+
+# Inject ENV variables into containers (secrets come from .kamal/secrets).
+env:
+ secret:
+ - RAILS_MASTER_KEY
+ clear:
+ # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
+ # When you start using multiple servers, you should split out job processing to a dedicated machine.
+ SOLID_QUEUE_IN_PUMA: true
+
+ # Set number of processes dedicated to Solid Queue (default: 1)
+ # JOB_CONCURRENCY: 3
+
+ # Set number of cores available to the application on each server (default: 1).
+ # WEB_CONCURRENCY: 2
+
+ # Match this to any external database server to configure Active Record correctly
+ # Use dispatch_api-db for a db accessory server on same machine via local kamal docker network.
+ # DB_HOST: 192.168.0.2
+
+ # Log everything from Rails
+ # RAILS_LOG_LEVEL: debug
+
+# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
+# "bin/kamal logs -r job" will tail logs from the first server in the job section.
+aliases:
+ console: app exec --interactive --reuse "bin/rails console"
+ shell: app exec --interactive --reuse "bash"
+ logs: app logs -f
+ dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"
+
+# Use a persistent storage volume for sqlite database files and local Active Storage files.
+# Recommended to change this to a mounted volume path that is backed up off server.
+volumes:
+ - "dispatch_api_storage:/rails/storage"
+
+
+# Configure the image builder.
+builder:
+ arch: amd64
+
+ # # Build image via remote server (useful for faster amd64 builds on arm64 computers)
+ # remote: ssh://docker@docker-builder-server
+ #
+ # # Pass arguments and secrets to the Docker build process
+ # args:
+ # RUBY_VERSION: ruby-4.0.1
+ # secrets:
+ # - GITHUB_TOKEN
+ # - RAILS_MASTER_KEY
+
+# Use a different ssh user than root
+# ssh:
+# user: app
+
+# Use accessory services (secrets come from .kamal/secrets).
+# accessories:
+# db:
+# image: mysql:8.0
+# host: 192.168.0.2
+# # Change to 3306 to expose port to the world instead of just local network.
+# port: "127.0.0.1:3306:3306"
+# env:
+# clear:
+# MYSQL_ROOT_HOST: '%'
+# secret:
+# - MYSQL_ROOT_PASSWORD
+# files:
+# - config/mysql/production.cnf:/etc/mysql/my.cnf
+# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
+# directories:
+# - data:/var/lib/mysql
+# redis:
+# image: valkey/valkey:8
+# host: 192.168.0.2
+# port: 6379
+# directories:
+# - data:/data
diff --git a/config/environment.rb b/config/environment.rb
new file mode 100644
index 0000000..cac5315
--- /dev/null
+++ b/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative "application"
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
new file mode 100644
index 0000000..c05ef19
--- /dev/null
+++ b/config/environments/development.rb
@@ -0,0 +1,73 @@
+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.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
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+
+ # Make template changes take effect immediately.
+ config.action_mailer.perform_caching = false
+
+ # Set localhost to be used by links generated in mailer templates.
+ config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
+
+ # 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
+
+ # 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
+
+ # Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
+ # config.generators.apply_rubocop_autocorrect_after_generate!
+end
diff --git a/config/environments/production.rb b/config/environments/production.rb
new file mode 100644
index 0000000..feb933a
--- /dev/null
+++ b/config/environments/production.rb
@@ -0,0 +1,87 @@
+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
+
+ # 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"
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ # 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 } }
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Set host to be used by links generated in mailer templates.
+ config.action_mailer.default_url_options = { host: "example.com" }
+
+ # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
+ # config.action_mailer.smtp_settings = {
+ # user_name: Rails.application.credentials.dig(:smtp, :user_name),
+ # password: Rails.application.credentials.dig(:smtp, :password),
+ # address: "smtp.example.com",
+ # port: 587,
+ # authentication: :plain
+ # }
+
+ # 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
diff --git a/config/environments/test.rb b/config/environments/test.rb
new file mode 100644
index 0000000..c2095b1
--- /dev/null
+++ b/config/environments/test.rb
@@ -0,0 +1,53 @@
+# 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
+
+ # Store uploaded files on the local file system in a temporary directory.
+ config.active_storage.service = :test
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Set host to be used by links generated in mailer templates.
+ config.action_mailer.default_url_options = { host: "example.com" }
+
+ # 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
diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb
new file mode 100644
index 0000000..0c5dd99
--- /dev/null
+++ b/config/initializers/cors.rb
@@ -0,0 +1,16 @@
+# Be sure to restart your server when you modify this file.
+
+# Avoid CORS issues when API is called from the frontend app.
+# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests.
+
+# Read more: https://github.com/cyu/rack-cors
+
+# Rails.application.config.middleware.insert_before 0, Rack::Cors do
+# allow do
+# origins "example.com"
+#
+# resource "*",
+# headers: :any,
+# methods: [:get, :post, :put, :patch, :delete, :options, :head]
+# end
+# end
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000..c0b717f
--- /dev/null
+++ b/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,8 @@
+# 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
+]
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
new file mode 100644
index 0000000..3860f65
--- /dev/null
+++ b/config/initializers/inflections.rb
@@ -0,0 +1,16 @@
+# 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
diff --git a/config/locales/en.yml b/config/locales/en.yml
new file mode 100644
index 0000000..6c349ae
--- /dev/null
+++ b/config/locales/en.yml
@@ -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"
diff --git a/config/puma.rb b/config/puma.rb
new file mode 100644
index 0000000..38c4b86
--- /dev/null
+++ b/config/puma.rb
@@ -0,0 +1,42 @@
+# 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"]
diff --git a/config/queue.yml b/config/queue.yml
new file mode 100644
index 0000000..9eace59
--- /dev/null
+++ b/config/queue.yml
@@ -0,0 +1,18 @@
+default: &default
+ dispatchers:
+ - polling_interval: 1
+ batch_size: 500
+ workers:
+ - queues: "*"
+ threads: 3
+ processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
+ polling_interval: 0.1
+
+development:
+ <<: *default
+
+test:
+ <<: *default
+
+production:
+ <<: *default
diff --git a/config/recurring.yml b/config/recurring.yml
new file mode 100644
index 0000000..b4207f9
--- /dev/null
+++ b/config/recurring.yml
@@ -0,0 +1,15 @@
+# examples:
+# periodic_cleanup:
+# class: CleanSoftDeletedRecordsJob
+# queue: background
+# args: [ 1000, { batch_size: 500 } ]
+# schedule: every hour
+# periodic_cleanup_with_command:
+# command: "SoftDeletedRecord.due.delete_all"
+# priority: 2
+# schedule: at 5am every day
+
+production:
+ clear_solid_queue_finished_jobs:
+ command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
+ schedule: every hour at minute 12
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000..a125ef0
--- /dev/null
+++ b/config/routes.rb
@@ -0,0 +1,10 @@
+Rails.application.routes.draw do
+ # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
+
+ # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
+ # Can be used by load balancers and uptime monitors to verify that the app is live.
+ get "up" => "rails/health#show", as: :rails_health_check
+
+ # Defines the root path route ("/")
+ # root "posts#index"
+end
diff --git a/config/storage.yml b/config/storage.yml
new file mode 100644
index 0000000..927dc53
--- /dev/null
+++ b/config/storage.yml
@@ -0,0 +1,27 @@
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+
+# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
+# amazon:
+# service: S3
+# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
+# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
+# region: us-east-1
+# bucket: your_own_bucket-<%= Rails.env %>
+
+# Remember not to checkin your GCS keyfile to a repository
+# google:
+# service: GCS
+# project: your_project
+# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
+# bucket: your_own_bucket-<%= Rails.env %>
+
+# mirror:
+# service: Mirror
+# primary: local
+# mirrors: [ amazon, google, microsoft ]
diff --git a/db/cable_schema.rb b/db/cable_schema.rb
new file mode 100644
index 0000000..2366660
--- /dev/null
+++ b/db/cable_schema.rb
@@ -0,0 +1,11 @@
+ActiveRecord::Schema[7.1].define(version: 1) do
+ create_table "solid_cable_messages", force: :cascade do |t|
+ t.binary "channel", limit: 1024, null: false
+ t.binary "payload", limit: 536870912, null: false
+ t.datetime "created_at", null: false
+ t.integer "channel_hash", limit: 8, null: false
+ t.index ["channel"], name: "index_solid_cable_messages_on_channel"
+ t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
+ t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
+ end
+end
diff --git a/db/cache_schema.rb b/db/cache_schema.rb
new file mode 100644
index 0000000..81a410d
--- /dev/null
+++ b/db/cache_schema.rb
@@ -0,0 +1,12 @@
+ActiveRecord::Schema[7.2].define(version: 1) do
+ create_table "solid_cache_entries", force: :cascade do |t|
+ t.binary "key", limit: 1024, null: false
+ t.binary "value", limit: 536870912, null: false
+ t.datetime "created_at", null: false
+ t.integer "key_hash", limit: 8, null: false
+ t.integer "byte_size", limit: 4, null: false
+ t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size"
+ t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
+ t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true
+ end
+end
diff --git a/db/queue_schema.rb b/db/queue_schema.rb
new file mode 100644
index 0000000..85194b6
--- /dev/null
+++ b/db/queue_schema.rb
@@ -0,0 +1,129 @@
+ActiveRecord::Schema[7.1].define(version: 1) do
+ create_table "solid_queue_blocked_executions", force: :cascade do |t|
+ t.bigint "job_id", null: false
+ t.string "queue_name", null: false
+ t.integer "priority", default: 0, null: false
+ t.string "concurrency_key", null: false
+ t.datetime "expires_at", null: false
+ t.datetime "created_at", null: false
+ t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release"
+ t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance"
+ t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
+ end
+
+ create_table "solid_queue_claimed_executions", force: :cascade do |t|
+ t.bigint "job_id", null: false
+ t.bigint "process_id"
+ t.datetime "created_at", null: false
+ t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
+ t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
+ end
+
+ create_table "solid_queue_failed_executions", force: :cascade do |t|
+ t.bigint "job_id", null: false
+ t.text "error"
+ t.datetime "created_at", null: false
+ t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true
+ end
+
+ create_table "solid_queue_jobs", force: :cascade do |t|
+ t.string "queue_name", null: false
+ t.string "class_name", null: false
+ t.text "arguments"
+ t.integer "priority", default: 0, null: false
+ t.string "active_job_id"
+ t.datetime "scheduled_at"
+ t.datetime "finished_at"
+ t.string "concurrency_key"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id"
+ t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name"
+ t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at"
+ t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering"
+ t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting"
+ end
+
+ create_table "solid_queue_pauses", force: :cascade do |t|
+ t.string "queue_name", null: false
+ t.datetime "created_at", null: false
+ t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true
+ end
+
+ create_table "solid_queue_processes", force: :cascade do |t|
+ t.string "kind", null: false
+ t.datetime "last_heartbeat_at", null: false
+ t.bigint "supervisor_id"
+ t.integer "pid", null: false
+ t.string "hostname"
+ t.text "metadata"
+ t.datetime "created_at", null: false
+ t.string "name", null: false
+ t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at"
+ t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
+ t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id"
+ end
+
+ create_table "solid_queue_ready_executions", force: :cascade do |t|
+ t.bigint "job_id", null: false
+ t.string "queue_name", null: false
+ t.integer "priority", default: 0, null: false
+ t.datetime "created_at", null: false
+ t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true
+ t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all"
+ t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue"
+ end
+
+ create_table "solid_queue_recurring_executions", force: :cascade do |t|
+ t.bigint "job_id", null: false
+ t.string "task_key", null: false
+ t.datetime "run_at", null: false
+ t.datetime "created_at", null: false
+ t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
+ t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
+ end
+
+ create_table "solid_queue_recurring_tasks", force: :cascade do |t|
+ t.string "key", null: false
+ t.string "schedule", null: false
+ t.string "command", limit: 2048
+ t.string "class_name"
+ t.text "arguments"
+ t.string "queue_name"
+ t.integer "priority", default: 0
+ t.boolean "static", default: true, null: false
+ t.text "description"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true
+ t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static"
+ end
+
+ create_table "solid_queue_scheduled_executions", force: :cascade do |t|
+ t.bigint "job_id", null: false
+ t.string "queue_name", null: false
+ t.integer "priority", default: 0, null: false
+ t.datetime "scheduled_at", null: false
+ t.datetime "created_at", null: false
+ t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
+ t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all"
+ end
+
+ create_table "solid_queue_semaphores", force: :cascade do |t|
+ t.string "key", null: false
+ t.integer "value", default: 1, null: false
+ t.datetime "expires_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at"
+ t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value"
+ t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true
+ end
+
+ add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+end
diff --git a/db/seeds.rb b/db/seeds.rb
new file mode 100644
index 0000000..4fbd6ed
--- /dev/null
+++ b/db/seeds.rb
@@ -0,0 +1,9 @@
+# This file should ensure the existence of records required to run the application in every environment (production,
+# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
+# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
+#
+# Example:
+#
+# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
+# MovieGenre.find_or_create_by!(name: genre_name)
+# end
diff --git a/lib/tasks/.keep b/lib/tasks/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/tasks/.keep
diff --git a/log/.keep b/log/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/log/.keep
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..c19f78a
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
diff --git a/script/.keep b/script/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/script/.keep
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
new file mode 100644
index 0000000..ef75d46
--- /dev/null
+++ b/spec/rails_helper.rb
@@ -0,0 +1,72 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require 'spec_helper'
+ENV['RAILS_ENV'] ||= 'test'
+require_relative '../config/environment'
+# Prevent database truncation if the environment is production
+abort("The Rails environment is running in production mode!") if Rails.env.production?
+# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file
+# that will avoid rails generators crashing because migrations haven't been run yet
+# return unless Rails.env.test?
+require 'rspec/rails'
+# Add additional requires below this line. Rails is not loaded until this point!
+
+# Requires supporting ruby files with custom matchers and macros, etc, in
+# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
+# run as spec files by default. This means that files in spec/support that end
+# in _spec.rb will both be required and run as specs, causing the specs to be
+# run twice. It is recommended that you do not name files matching this glob to
+# end with _spec.rb. You can configure this pattern with the --pattern
+# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
+#
+# The following line is provided for convenience purposes. It has the downside
+# of increasing the boot-up time by auto-requiring all files in the support
+# directory. Alternatively, in the individual `*_spec.rb` files, manually
+# require only the support files necessary.
+#
+# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
+
+# Ensures that the test database schema matches the current schema file.
+# If there are pending migrations it will invoke `db:test:prepare` to
+# recreate the test database by loading the schema.
+# If you are not using ActiveRecord, you can remove these lines.
+begin
+ ActiveRecord::Migration.maintain_test_schema!
+rescue ActiveRecord::PendingMigrationError => e
+ abort e.to_s.strip
+end
+RSpec.configure do |config|
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+ config.fixture_paths = [
+ Rails.root.join('spec/fixtures')
+ ]
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, remove the following line or assign false
+ # instead of true.
+ config.use_transactional_fixtures = true
+
+ # You can uncomment this line to turn off ActiveRecord support entirely.
+ # config.use_active_record = false
+
+ # RSpec Rails uses metadata to mix in different behaviours to your tests,
+ # for example enabling you to call `get` and `post` in request specs. e.g.:
+ #
+ # RSpec.describe UsersController, type: :request do
+ # # ...
+ # end
+ #
+ # The different available types are documented in the features, such as in
+ # https://rspec.info/features/8-0/rspec-rails
+ #
+ # You can also infer these behaviours automatically by location, e.g.
+ # /spec/models would pull in the same behaviour as `type: :model` but this
+ # behaviour is considered legacy and will be removed in a future version.
+ #
+ # To enable this behaviour uncomment the line below.
+ # config.infer_spec_type_from_file_location!
+
+ # Filter lines from Rails gems in backtraces.
+ config.filter_rails_from_backtrace!
+ # arbitrary gems may also be filtered via:
+ # config.filter_gems_from_backtrace("gem name")
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..327b58e
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,94 @@
+# This file was generated by the `rails generate rspec:install` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+ # have no way to turn it off -- the option exists only for backwards
+ # compatibility in RSpec 3). It causes shared context metadata to be
+ # inherited by the metadata hash of host groups and examples, rather than
+ # triggering implicit auto-inclusion in groups with matching metadata.
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+
+# The settings below are suggested to provide a good initial experience
+# with RSpec, but feel free to customize to your heart's content.
+=begin
+ # This allows you to limit a spec run to individual examples or groups
+ # you care about by tagging them with `:focus` metadata. When nothing
+ # is tagged with `:focus`, all examples get run. RSpec also provides
+ # aliases for `it`, `describe`, and `context` that include `:focus`
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ config.filter_run_when_matching :focus
+
+ # Allows RSpec to persist some state between runs in order to support
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # you configure your source control system to ignore this file.
+ config.example_status_persistence_file_path = "spec/examples.txt"
+
+ # Limits the available syntax to the non-monkey patched syntax that is
+ # recommended. For more details, see:
+ # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
+ config.disable_monkey_patching!
+
+ # Many RSpec users commonly either run the entire suite or an individual
+ # file, and it's useful to allow more verbose output when running an
+ # individual spec file.
+ if config.files_to_run.one?
+ # Use the documentation formatter for detailed output,
+ # unless a formatter has already been configured
+ # (e.g. via a command-line flag).
+ config.default_formatter = "doc"
+ end
+
+ # Print the 10 slowest examples and example groups at the
+ # end of the spec run, to help surface which specs are running
+ # particularly slow.
+ config.profile_examples = 10
+
+ # Run specs in random order to surface order dependencies. If you find an
+ # order dependency and want to debug it, you can fix the order by providing
+ # the seed, which is printed after each run.
+ # --seed 1234
+ config.order = :random
+
+ # Seed global randomization in this process using the `--seed` CLI option.
+ # Setting this allows you to use `--seed` to deterministically reproduce
+ # test failures related to randomization by passing the same `--seed` value
+ # as the one that triggered the failure.
+ Kernel.srand config.seed
+=end
+end
diff --git a/storage/.keep b/storage/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/storage/.keep
diff --git a/tmp/.keep b/tmp/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tmp/.keep
diff --git a/tmp/pids/.keep b/tmp/pids/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tmp/pids/.keep
diff --git a/tmp/storage/.keep b/tmp/storage/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tmp/storage/.keep
diff --git a/vendor/.keep b/vendor/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/vendor/.keep