develoz-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +31 -0
- data/.github/workflows/release.yml +459 -0
- data/.rubocop.yml +52 -0
- data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
- data/.tool-versions +2 -0
- data/CHANGELOG.md +28 -0
- data/CONTRIBUTING.md +120 -0
- data/README.md +76 -0
- data/Rakefile +18 -0
- data/config/generators.yml +105 -0
- data/docs/guide/installation.md +574 -0
- data/exe/develoz +6 -0
- data/install +98 -0
- data/lib/develoz/canonical_fetcher.rb +56 -0
- data/lib/develoz/cli.rb +84 -0
- data/lib/develoz/generators/base.rb +134 -0
- data/lib/develoz/manifest.rb +44 -0
- data/lib/develoz/options.rb +55 -0
- data/lib/develoz/version.rb +5 -0
- data/lib/develoz/version_resolver.rb +103 -0
- data/lib/develoz.rb +15 -0
- data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
- data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
- data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
- data/lib/generators/develoz/admin/admin_generator.rb +44 -0
- data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
- data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
- data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
- data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
- data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
- data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
- data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
- data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
- data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
- data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
- data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
- data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
- data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
- data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
- data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
- data/lib/generators/develoz/api/api_generator.rb +55 -0
- data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
- data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
- data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
- data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
- data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
- data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
- data/lib/generators/develoz/auth/auth_generator.rb +82 -0
- data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
- data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
- data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
- data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
- data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
- data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
- data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
- data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
- data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
- data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
- data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
- data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
- data/lib/generators/develoz/ci/ci_generator.rb +66 -0
- data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
- data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
- data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
- data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
- data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
- data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
- data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
- data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
- data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
- data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
- data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
- data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
- data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
- data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
- data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
- data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
- data/lib/generators/develoz/database/database_generator.rb +44 -0
- data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
- data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
- data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
- data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
- data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
- data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
- data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
- data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
- data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
- data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
- data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
- data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
- data/lib/generators/develoz/docker/docker_generator.rb +54 -0
- data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
- data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
- data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
- data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
- data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
- data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
- data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
- data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
- data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
- data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
- data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
- data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
- data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
- data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
- data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
- data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
- data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
- data/lib/generators/develoz/install/install_generator.rb +51 -0
- data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
- data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
- data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
- data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
- data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
- data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
- data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
- data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
- data/lib/generators/develoz/push/push_generator.rb +61 -0
- data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
- data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
- data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
- data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
- data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
- data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
- data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
- data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
- data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
- data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
- data/lib/generators/develoz/solid/solid_generator.rb +52 -0
- data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
- data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
- data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
- data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
- data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
- data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
- data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
- data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
- data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
- data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
- data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
- data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
- data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
- data/lib/generators/develoz/testing/testing_generator.rb +39 -0
- data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
- data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
- data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
- data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
- data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
- data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
- data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
- data/lib/generators/develoz/ui/ui_generator.rb +57 -0
- data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
- data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
- data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
- data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
- data/lib/tasks/canonical.rake +27 -0
- data/templates/.keep +0 -0
- data/templates/CANONICAL_SOURCES.md +93 -0
- data/templates/pull_request_template.md.tt +13 -0
- metadata +383 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "develoz"
|
|
4
|
+
|
|
5
|
+
module Develoz
|
|
6
|
+
module Generators
|
|
7
|
+
class KamalGenerator < Develoz::Generators::Base
|
|
8
|
+
def self.source_root
|
|
9
|
+
File.expand_path("templates", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class_option :push, type: :boolean, default: false
|
|
13
|
+
|
|
14
|
+
def add_kamal_gem
|
|
15
|
+
add_gem "kamal", require: false, group: :development
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create_deploy_config
|
|
19
|
+
template "deploy.yml.tt", "config/deploy.yml"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_production_dockerfile
|
|
23
|
+
template "dockerfile_prod.tt", "Dockerfile.prod"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_kamal_secrets
|
|
27
|
+
template "kamal_secrets.tt", ".kamal/secrets"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_postgres_accessory
|
|
31
|
+
template "accessories_postgres.yml.tt", "config/accessories/postgres.yml"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ensure_secrets_gitignored
|
|
35
|
+
ensure_gitignore ".kamal/secrets"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def kamal_app_name
|
|
39
|
+
ENV.fetch("KAMAL_APP_NAME", app_name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def kamal_image
|
|
43
|
+
ENV.fetch("KAMAL_IMAGE", kamal_app_name)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def kamal_registry
|
|
47
|
+
ENV.fetch("KAMAL_REGISTRY", "")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def kamal_servers
|
|
51
|
+
ENV.fetch("KAMAL_SERVERS", "")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def push_enabled?
|
|
55
|
+
options[:push]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Postgres accessory for Kamal deployment of <%= kamal_app_name %>.
|
|
2
|
+
# This file defines the postgres database accessory. It is referenced from
|
|
3
|
+
# config/deploy.yml under the `accessories` key. See
|
|
4
|
+
# https://kamal-deploy.org/docs/configuration/accessories/ for full docs.
|
|
5
|
+
|
|
6
|
+
postgres:
|
|
7
|
+
image: postgres:18
|
|
8
|
+
host: <%= kamal_app_name %>-db.example.com
|
|
9
|
+
port: "127.0.0.1:5432:5432"
|
|
10
|
+
env:
|
|
11
|
+
secret:
|
|
12
|
+
- POSTGRES_PASSWORD
|
|
13
|
+
directories:
|
|
14
|
+
- postgres-data:/var/lib/postgresql/data
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Kamal 2 deployment configuration for <%= kamal_app_name %>.
|
|
2
|
+
# See https://kamal-deploy.org for documentation.
|
|
3
|
+
# Override defaults via ENV vars: KAMAL_APP_NAME, KAMAL_IMAGE, KAMAL_REGISTRY, KAMAL_SERVERS.
|
|
4
|
+
|
|
5
|
+
service: <%= kamal_app_name %>
|
|
6
|
+
image: <%= kamal_image %>
|
|
7
|
+
|
|
8
|
+
servers:
|
|
9
|
+
<% if kamal_servers.empty? %>
|
|
10
|
+
- <%= kamal_app_name %>.example.com
|
|
11
|
+
<% else %>
|
|
12
|
+
<% kamal_servers.split(",").each do |server| %>
|
|
13
|
+
- <%= server.strip %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
registry:
|
|
18
|
+
username: <%= kamal_registry.empty? ? "your-registry-user" : kamal_registry %>
|
|
19
|
+
password:
|
|
20
|
+
- KAMAL_REGISTRY_PASSWORD
|
|
21
|
+
|
|
22
|
+
builder:
|
|
23
|
+
arch: amd64
|
|
24
|
+
|
|
25
|
+
env:
|
|
26
|
+
secret:
|
|
27
|
+
- RAILS_MASTER_KEY
|
|
28
|
+
- DATABASE_URL
|
|
29
|
+
<% if push_enabled? %>
|
|
30
|
+
- VAPID_PUBLIC_KEY
|
|
31
|
+
- VAPID_PRIVATE_KEY
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
accessories:
|
|
35
|
+
postgres:
|
|
36
|
+
image: postgres:18
|
|
37
|
+
host: <%= kamal_app_name %>-db.example.com
|
|
38
|
+
port: "127.0.0.1:5432:5432"
|
|
39
|
+
env:
|
|
40
|
+
secret:
|
|
41
|
+
- POSTGRES_PASSWORD
|
|
42
|
+
directories:
|
|
43
|
+
- postgres-data:/var/lib/postgresql/data
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
# check=error=true
|
|
3
|
+
|
|
4
|
+
# Production Dockerfile for <%= kamal_app_name %>.
|
|
5
|
+
# Build with: docker build -t <%= kamal_app_name %> -f Dockerfile.prod .
|
|
6
|
+
# Run with: docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value> --name <%= kamal_app_name %> <%= kamal_app_name %>
|
|
7
|
+
|
|
8
|
+
ARG RUBY_VERSION=3.4
|
|
9
|
+
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
|
|
10
|
+
|
|
11
|
+
WORKDIR /rails
|
|
12
|
+
|
|
13
|
+
RUN apt-get update -qq && \
|
|
14
|
+
apt-get install --no-install-recommends -y curl libjemalloc2 libvips postgresql-client && \
|
|
15
|
+
ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
|
|
16
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
17
|
+
|
|
18
|
+
ENV RAILS_ENV="production" \
|
|
19
|
+
BUNDLE_DEPLOYMENT="1" \
|
|
20
|
+
BUNDLE_PATH="/usr/local/bundle" \
|
|
21
|
+
BUNDLE_WITHOUT="development" \
|
|
22
|
+
LD_PRELOAD="/usr/local/lib/libjemalloc.so"
|
|
23
|
+
|
|
24
|
+
FROM base AS build
|
|
25
|
+
|
|
26
|
+
RUN apt-get update -qq && \
|
|
27
|
+
apt-get install --no-install-recommends -y build-essential git libpq-dev libvips libyaml-dev pkg-config && \
|
|
28
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
29
|
+
|
|
30
|
+
COPY Gemfile Gemfile.lock ./
|
|
31
|
+
|
|
32
|
+
RUN bundle install && \
|
|
33
|
+
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
|
|
34
|
+
bundle exec bootsnap precompile -j 1 --gemfile
|
|
35
|
+
|
|
36
|
+
COPY . .
|
|
37
|
+
|
|
38
|
+
RUN bundle exec bootsnap precompile -j 1 app/ lib/
|
|
39
|
+
|
|
40
|
+
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
|
41
|
+
|
|
42
|
+
FROM base
|
|
43
|
+
|
|
44
|
+
RUN groupadd --system --gid 1000 rails && \
|
|
45
|
+
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
|
|
46
|
+
USER 1000:1000
|
|
47
|
+
|
|
48
|
+
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
|
|
49
|
+
COPY --chown=rails:rails --from=build /rails /rails
|
|
50
|
+
|
|
51
|
+
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
|
|
52
|
+
|
|
53
|
+
EXPOSE 80
|
|
54
|
+
CMD ["./bin/thrust", "./bin/rails", "server"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Kamal secrets for <%= kamal_app_name %>.
|
|
2
|
+
# This file is gitignored and should NEVER contain real secret values.
|
|
3
|
+
# Each entry references an ENV variable or command that resolves at deploy time.
|
|
4
|
+
# Populate the ENV vars in your shell or CI before running `kamal deploy`.
|
|
5
|
+
|
|
6
|
+
# Registry password for pushing the app image to the container registry.
|
|
7
|
+
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
|
|
8
|
+
|
|
9
|
+
# Rails master key for production (config/master.key on the deploy host).
|
|
10
|
+
RAILS_MASTER_KEY=$RAILS_MASTER_KEY
|
|
11
|
+
|
|
12
|
+
# Production database URL for the Rails app (points to the postgres accessory).
|
|
13
|
+
DATABASE_URL=$DATABASE_URL
|
|
14
|
+
|
|
15
|
+
# Postgres accessory password (must match POSTGRES_PASSWORD on the accessory).
|
|
16
|
+
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
|
17
|
+
<% if push_enabled? %>
|
|
18
|
+
|
|
19
|
+
# VAPID keys for Web Push notifications (generate with `rails webpush:generate_vapid_keys`).
|
|
20
|
+
VAPID_PUBLIC_KEY=$VAPID_PUBLIC_KEY
|
|
21
|
+
VAPID_PRIVATE_KEY=$VAPID_PRIVATE_KEY
|
|
22
|
+
<% end %>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "develoz"
|
|
4
|
+
|
|
5
|
+
module Develoz
|
|
6
|
+
module Generators
|
|
7
|
+
class MaintenanceGenerator < Develoz::Generators::Base
|
|
8
|
+
def self.source_root
|
|
9
|
+
File.expand_path("templates", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add_maintenance_tasks_gem
|
|
13
|
+
add_gem "maintenance_tasks"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def insert_maintenance_tasks_route
|
|
17
|
+
insert_route('mount MaintenanceTasks::Engine, at: "/maintenance_tasks"')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_example_task
|
|
21
|
+
template "app/tasks/maintenance/example_task.rb.tt", "app/tasks/maintenance/example_task.rb"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_rake_task
|
|
25
|
+
template "lib/tasks/maintenance_counters.rake.tt", "lib/tasks/maintenance_counters.rake"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maintenance
|
|
4
|
+
# Example maintenance task that touches all records in batches.
|
|
5
|
+
# Demonstrates the standard MaintenanceTasks::Task structure: collection,
|
|
6
|
+
# count, and process. Replace with your own task logic as needed.
|
|
7
|
+
class ExampleTask < MaintenanceTasks::Task
|
|
8
|
+
def collection
|
|
9
|
+
User.in_batches(of: 1000)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def count
|
|
13
|
+
User.count
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def process(batch)
|
|
17
|
+
batch.update_all(updated_at: Time.current)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :maintenance do
|
|
4
|
+
desc "Update counter caches for all models"
|
|
5
|
+
task update_counters: :environment do
|
|
6
|
+
puts "Updating counter caches..."
|
|
7
|
+
|
|
8
|
+
Rails.application.eager_load!
|
|
9
|
+
ActiveRecord::Base.descendants.select(&:table_exists?).each do |model|
|
|
10
|
+
puts " Processed #{model.name}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
puts "Counter caches updated!"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "Purge stale records older than RETENTION_DAYS (default: 90)"
|
|
17
|
+
task purge_stale: :environment do
|
|
18
|
+
days = ENV.fetch("RETENTION_DAYS", "90").to_i
|
|
19
|
+
cutoff = days.days.ago
|
|
20
|
+
puts "Purging records older than #{cutoff} (#{days} days)..."
|
|
21
|
+
|
|
22
|
+
# Add model-specific purge logic here, e.g.:
|
|
23
|
+
# StaleRecord.where("created_at < ?", cutoff).in_batches.delete_all
|
|
24
|
+
|
|
25
|
+
puts "Stale records purged!"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "develoz"
|
|
4
|
+
|
|
5
|
+
module Develoz
|
|
6
|
+
module Generators
|
|
7
|
+
class PushGenerator < Develoz::Generators::Base
|
|
8
|
+
class_option :pwa, type: :boolean, default: false
|
|
9
|
+
|
|
10
|
+
def self.source_root
|
|
11
|
+
File.expand_path("templates", __dir__)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ensure_pwa_prerequisite
|
|
15
|
+
return if options[:pwa]
|
|
16
|
+
|
|
17
|
+
say "develoz:push requires --pwa. Enabling PWA automatically.", :yellow
|
|
18
|
+
require "generators/develoz/pwa/pwa_generator"
|
|
19
|
+
pwa = PwaGenerator.new([], {}, destination_root: destination_root)
|
|
20
|
+
PwaGenerator.public_instance_methods(false).each { |m| pwa.public_send(m) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_web_push_gem
|
|
24
|
+
add_gem "web-push"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_push_subscription_model
|
|
28
|
+
template "app/models/push_subscription.rb.tt",
|
|
29
|
+
"app/models/push_subscription.rb"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create_push_subscription_migration
|
|
33
|
+
return if migration_exists?("create_push_subscriptions")
|
|
34
|
+
|
|
35
|
+
template "db/migrate/create_push_subscriptions.rb.tt",
|
|
36
|
+
"db/migrate/#{next_migration_timestamp}_create_push_subscriptions.rb"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_push_notification_service
|
|
40
|
+
template "app/services/push_notification_service.rb.tt",
|
|
41
|
+
"app/services/push_notification_service.rb"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create_service_worker_push_handlers
|
|
45
|
+
template "app/views/pwa/sw_push_handlers.js.tt",
|
|
46
|
+
"app/views/pwa/sw_push_handlers.js"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def create_subscription_js
|
|
50
|
+
template "app/javascript/pwa/subscription.js.tt",
|
|
51
|
+
"app/javascript/pwa/subscription.js"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def append_vapid_env
|
|
55
|
+
append_env "VAPID_PUBLIC_KEY", ""
|
|
56
|
+
append_env "VAPID_PRIVATE_KEY", ""
|
|
57
|
+
append_env "VAPID_SUBJECT", "mailto:noreply@example.com"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Develoz PWA push subscription logic
|
|
2
|
+
// Subscribes the user to web push notifications using VAPID public key
|
|
3
|
+
|
|
4
|
+
async function subscribeToPush() {
|
|
5
|
+
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
|
|
6
|
+
console.warn("Push notifications are not supported in this browser")
|
|
7
|
+
return null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const registration = await navigator.serviceWorker.ready
|
|
11
|
+
const existingSubscription = await registration.pushManager.getSubscription()
|
|
12
|
+
|
|
13
|
+
if (existingSubscription) {
|
|
14
|
+
return existingSubscription
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const response = await fetch("/push_subscriptions/vapid_public_key", {
|
|
18
|
+
headers: { Accept: "application/json" }
|
|
19
|
+
})
|
|
20
|
+
const { public_key } = await response.json()
|
|
21
|
+
|
|
22
|
+
const subscription = await registration.pushManager.subscribe({
|
|
23
|
+
userVisibleOnly: true,
|
|
24
|
+
applicationServerKey: urlBase64ToUint8Array(public_key)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await fetch("/push_subscriptions", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')?.content || ""
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
subscription: {
|
|
35
|
+
endpoint: subscription.endpoint,
|
|
36
|
+
keys: subscription.toJSON().keys
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return subscription
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function urlBase64ToUint8Array(base64String) {
|
|
45
|
+
const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
|
|
46
|
+
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/")
|
|
47
|
+
const rawData = atob(base64)
|
|
48
|
+
const outputArray = new Uint8Array(rawData.length)
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
51
|
+
outputArray[i] = rawData.charCodeAt(i)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return outputArray
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { subscribeToPush }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PushSubscription < ApplicationRecord
|
|
4
|
+
belongs_to :user, optional: true
|
|
5
|
+
|
|
6
|
+
validates :endpoint, presence: true, uniqueness: true
|
|
7
|
+
validates :p256dh, presence: true
|
|
8
|
+
validates :auth, presence: true
|
|
9
|
+
|
|
10
|
+
# Purge subscriptions whose endpoint is no longer valid (410 Gone).
|
|
11
|
+
def self.purge_invalid!(response)
|
|
12
|
+
where(endpoint: response&.request&.uri&.to_s).destroy_all if response&.code == 410
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "web-push"
|
|
4
|
+
|
|
5
|
+
class PushNotificationService
|
|
6
|
+
VAPID_SUBJECT_DEFAULT = "mailto:noreply@example.com".freeze
|
|
7
|
+
private_constant :VAPID_SUBJECT_DEFAULT
|
|
8
|
+
|
|
9
|
+
def self.send_notification(subscription, title:, body:, url: nil, icon: nil)
|
|
10
|
+
payload = { title: title, body: body }
|
|
11
|
+
payload[:url] = url if url
|
|
12
|
+
payload[:icon] = icon if icon
|
|
13
|
+
|
|
14
|
+
WebPush.payload_send(
|
|
15
|
+
message: payload.to_json,
|
|
16
|
+
endpoint: subscription.endpoint,
|
|
17
|
+
p256dh: subscription.p256dh,
|
|
18
|
+
auth: subscription.auth,
|
|
19
|
+
vapid: vapid_options,
|
|
20
|
+
ttl: 24 * 60 * 60
|
|
21
|
+
)
|
|
22
|
+
rescue WebPush::ExpiredSubscription, WebPush::InvalidSubscription
|
|
23
|
+
subscription.destroy
|
|
24
|
+
rescue WebPush::Error => e
|
|
25
|
+
Rails.logger.warn("Push notification failed: #{e.message}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.vapid_options
|
|
29
|
+
{
|
|
30
|
+
subject: ENV.fetch("VAPID_SUBJECT", VAPID_SUBJECT_DEFAULT),
|
|
31
|
+
public_key: ENV.fetch("VAPID_PUBLIC_KEY"),
|
|
32
|
+
private_key: ENV.fetch("VAPID_PRIVATE_KEY")
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
private_class_method :vapid_options
|
|
36
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Develoz PWA service worker push event handlers
|
|
2
|
+
// Imported by the main service worker via importScripts("/sw_push_handlers")
|
|
3
|
+
|
|
4
|
+
self.addEventListener("push", (event) => {
|
|
5
|
+
let payload = { title: "Notification", body: "" }
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
if (event.data) {
|
|
9
|
+
payload = event.data.json()
|
|
10
|
+
}
|
|
11
|
+
} catch {
|
|
12
|
+
payload = { title: "Notification", body: event.data ? event.data.text() : "" }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const options = {
|
|
16
|
+
body: payload.body || "",
|
|
17
|
+
icon: payload.icon || "/icon-192.png",
|
|
18
|
+
badge: payload.badge || "/icon-192.png",
|
|
19
|
+
data: { url: payload.url || "/" }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
event.waitUntil(
|
|
23
|
+
self.registration.showNotification(payload.title || "Notification", options)
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
self.addEventListener("notificationclick", (event) => {
|
|
28
|
+
event.notification.close()
|
|
29
|
+
|
|
30
|
+
const targetUrl = event.notification.data && event.notification.data.url
|
|
31
|
+
? event.notification.data.url
|
|
32
|
+
: "/"
|
|
33
|
+
|
|
34
|
+
event.waitUntil(
|
|
35
|
+
(async () => {
|
|
36
|
+
const allClients = await self.clients.matchAll({ type: "window", includeUncontrolled: true })
|
|
37
|
+
|
|
38
|
+
for (const client of allClients) {
|
|
39
|
+
if (client.url.includes(targetUrl) && "focus" in client) {
|
|
40
|
+
return client.focus()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (self.clients.openWindow) {
|
|
45
|
+
return self.clients.openWindow(targetUrl)
|
|
46
|
+
}
|
|
47
|
+
})()
|
|
48
|
+
)
|
|
49
|
+
})
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# VAPID keys for web push notifications
|
|
2
|
+
# Generate with: ruby -e "require 'web-push'; k = WebPush.generate_key; puts \"VAPID_PUBLIC_KEY=#{k.public_key}\"; puts \"VAPID_PRIVATE_KEY=#{k.private_key}\""
|
|
3
|
+
VAPID_PUBLIC_KEY=
|
|
4
|
+
VAPID_PRIVATE_KEY=
|
|
5
|
+
VAPID_SUBJECT=mailto:noreply@example.com
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreatePushSubscriptions < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :push_subscriptions do |t|
|
|
6
|
+
t.string :endpoint, null: false
|
|
7
|
+
t.string :p256dh, null: false
|
|
8
|
+
t.string :auth, null: false
|
|
9
|
+
t.references :user, foreign_key: true, index: true
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index :push_subscriptions, :endpoint, unique: true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "develoz"
|
|
4
|
+
|
|
5
|
+
module Develoz
|
|
6
|
+
module Generators
|
|
7
|
+
class PwaGenerator < Develoz::Generators::Base
|
|
8
|
+
def self.source_root
|
|
9
|
+
File.expand_path("templates", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create_pwa_controller
|
|
13
|
+
template "app/controllers/pwa_controller.rb.tt",
|
|
14
|
+
"app/controllers/pwa_controller.rb"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create_manifest_view
|
|
18
|
+
template "app/views/pwa/manifest.json.erb.tt",
|
|
19
|
+
"app/views/pwa/manifest.json.erb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_service_worker_view
|
|
23
|
+
template "app/views/pwa/service-worker.js.tt",
|
|
24
|
+
"app/views/pwa/service-worker.js.erb"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_offline_page
|
|
28
|
+
template "app/views/pwa/offline.html.erb.tt",
|
|
29
|
+
"app/views/pwa/offline.html.erb"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create_registration_js
|
|
33
|
+
template "app/javascript/pwa/registration.js.tt",
|
|
34
|
+
"app/javascript/pwa/registration.js"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def insert_pwa_routes
|
|
38
|
+
routes_snippet = File.read(File.join(self.class.source_root, "routes.rb.tt")).strip
|
|
39
|
+
inject_once(into: "config/routes.rb",
|
|
40
|
+
content: routes_snippet,
|
|
41
|
+
before: /^end\s*$/,
|
|
42
|
+
marker: "pwa#manifest")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PwaController < ApplicationController
|
|
4
|
+
skip_forgery_protection
|
|
5
|
+
|
|
6
|
+
def manifest
|
|
7
|
+
render template: "pwa/manifest", layout: false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def service_worker
|
|
11
|
+
render template: "pwa/service-worker", layout: false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def offline
|
|
15
|
+
render template: "pwa/offline", layout: false
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Develoz PWA service worker registration
|
|
2
|
+
// Registers the service worker and handles update found
|
|
3
|
+
|
|
4
|
+
if ("serviceWorker" in navigator) {
|
|
5
|
+
window.addEventListener("load", () => {
|
|
6
|
+
navigator.serviceWorker
|
|
7
|
+
.register("/service-worker")
|
|
8
|
+
.then((registration) => {
|
|
9
|
+
registration.addEventListener("updatefound", () => {
|
|
10
|
+
const installingWorker = registration.installing
|
|
11
|
+
if (installingWorker) {
|
|
12
|
+
installingWorker.addEventListener("statechange", () => {
|
|
13
|
+
if (installingWorker.state === "installed" && navigator.serviceWorker.controller) {
|
|
14
|
+
// New version available - reload to activate
|
|
15
|
+
window.location.reload()
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
.catch((error) => {
|
|
22
|
+
console.error("Service worker registration failed:", error)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= app_class %>",
|
|
3
|
+
"short_name": "<%= app_class %>",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "/icon.png",
|
|
7
|
+
"type": "image/png",
|
|
8
|
+
"sizes": "512x512"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"src": "/icon.png",
|
|
12
|
+
"type": "image/png",
|
|
13
|
+
"sizes": "512x512",
|
|
14
|
+
"purpose": "maskable"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"start_url": "/",
|
|
18
|
+
"scope": "/",
|
|
19
|
+
"display": "standalone",
|
|
20
|
+
"orientation": "natural",
|
|
21
|
+
"description": "<%= app_class %>.",
|
|
22
|
+
"theme_color": "#ffffff",
|
|
23
|
+
"background_color": "#ffffff"
|
|
24
|
+
}
|