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.
Files changed (172) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +31 -0
  3. data/.github/workflows/release.yml +459 -0
  4. data/.rubocop.yml +52 -0
  5. data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
  6. data/.tool-versions +2 -0
  7. data/CHANGELOG.md +28 -0
  8. data/CONTRIBUTING.md +120 -0
  9. data/README.md +76 -0
  10. data/Rakefile +18 -0
  11. data/config/generators.yml +105 -0
  12. data/docs/guide/installation.md +574 -0
  13. data/exe/develoz +6 -0
  14. data/install +98 -0
  15. data/lib/develoz/canonical_fetcher.rb +56 -0
  16. data/lib/develoz/cli.rb +84 -0
  17. data/lib/develoz/generators/base.rb +134 -0
  18. data/lib/develoz/manifest.rb +44 -0
  19. data/lib/develoz/options.rb +55 -0
  20. data/lib/develoz/version.rb +5 -0
  21. data/lib/develoz/version_resolver.rb +103 -0
  22. data/lib/develoz.rb +15 -0
  23. data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
  24. data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
  25. data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
  26. data/lib/generators/develoz/admin/admin_generator.rb +44 -0
  27. data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
  28. data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
  29. data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
  30. data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
  31. data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
  32. data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
  33. data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
  34. data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
  35. data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
  36. data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
  37. data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
  38. data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
  39. data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
  40. data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
  41. data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
  42. data/lib/generators/develoz/api/api_generator.rb +55 -0
  43. data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
  44. data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
  45. data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
  46. data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
  47. data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
  48. data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
  49. data/lib/generators/develoz/auth/auth_generator.rb +82 -0
  50. data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
  51. data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
  52. data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
  53. data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
  54. data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
  55. data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
  56. data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
  57. data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
  58. data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
  59. data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
  60. data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
  61. data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
  62. data/lib/generators/develoz/ci/ci_generator.rb +66 -0
  63. data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
  64. data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
  65. data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
  66. data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
  67. data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
  68. data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
  69. data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
  70. data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
  71. data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
  72. data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
  73. data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
  74. data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
  75. data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
  76. data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
  77. data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
  78. data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
  79. data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
  80. data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
  81. data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
  82. data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
  83. data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
  84. data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
  85. data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
  86. data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
  87. data/lib/generators/develoz/database/database_generator.rb +44 -0
  88. data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
  89. data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
  90. data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
  91. data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
  92. data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
  93. data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
  94. data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
  95. data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
  96. data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
  97. data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
  98. data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
  99. data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
  100. data/lib/generators/develoz/docker/docker_generator.rb +54 -0
  101. data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
  102. data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
  103. data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
  104. data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
  105. data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
  106. data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
  107. data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
  108. data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
  109. data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
  110. data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
  111. data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
  112. data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
  113. data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
  114. data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
  115. data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
  116. data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
  117. data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
  118. data/lib/generators/develoz/install/install_generator.rb +51 -0
  119. data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
  120. data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
  121. data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
  122. data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
  123. data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
  124. data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
  125. data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
  126. data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
  127. data/lib/generators/develoz/push/push_generator.rb +61 -0
  128. data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
  129. data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
  130. data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
  131. data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
  132. data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
  133. data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
  134. data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
  135. data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
  136. data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
  137. data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
  138. data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
  139. data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
  140. data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
  141. data/lib/generators/develoz/solid/solid_generator.rb +52 -0
  142. data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
  143. data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
  144. data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
  145. data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
  146. data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
  147. data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
  148. data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
  149. data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
  150. data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
  151. data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
  152. data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
  153. data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
  154. data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
  155. data/lib/generators/develoz/testing/testing_generator.rb +39 -0
  156. data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
  157. data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
  158. data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
  159. data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
  160. data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
  161. data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
  162. data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
  163. data/lib/generators/develoz/ui/ui_generator.rb +57 -0
  164. data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
  165. data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
  166. data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
  167. data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
  168. data/lib/tasks/canonical.rake +27 -0
  169. data/templates/.keep +0 -0
  170. data/templates/CANONICAL_SOURCES.md +93 -0
  171. data/templates/pull_request_template.md.tt +13 -0
  172. 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
+ }