bible270 1.4.0 → 1.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8975632aa3e90165634356f25fc0c8cff2909e84b3177a58e192b16ee325b745
4
- data.tar.gz: 543aa029054018c26f87d2d1836302ab671de51b70caa7a531ed0011a405f5e6
3
+ metadata.gz: bcb59bf731c08118644e9303ed749397492c6c7fea523a20cf4eb3c17b84281f
4
+ data.tar.gz: e1f6b1be1b245e859370ae745eb05f8adf47df953f89ad7cb28c700ad705830c
5
5
  SHA512:
6
- metadata.gz: c76e854eb6449addef5b1deb2066d70cc6c51e8650817efa7c4fef8efa0d847d0b1e85c75d9faa91dde497495a01c980c674342148326f8c43f146a3321175d4
7
- data.tar.gz: 80761d784e504c167954827dbe3e3e8193e571e91eee67b8bd9aa12b7990b9845020f20399c3367f63b41d441107fa15748e6e4199d327ce2588d1976a6227ca
6
+ metadata.gz: 312efc5cd9bfcb503430d1adc601b21a93658c9d378060182d3453b747d43674a736a83b753accaf0f0d738b0da005656c87490f17a51814800b3798632c09ab
7
+ data.tar.gz: c45129d6950f6b40f8bb0477b9de95ed9e98bb5ad3c1f9fa36bfb1b44eb5c1b26e337fc9d31e7dc9554115f2c4ecde14427127333bf2287925a0c0a33457b470
data/CHANGELOG.md CHANGED
@@ -4,17 +4,43 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
4
4
 
5
5
  ## Unreleased Changes
6
6
 
7
+ ### Changed
8
+
9
+ - simplify day completion to one continuous server-confirmed dove flight from the lower center, eliminating the stagger between a pre-submit animation and the Turbo or HTML response
10
+
11
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.4.0...main)**
12
+
13
+ ## [1.4.0] - 2026-09-02
14
+
7
15
  ### Added
8
16
 
9
17
  - mark a newly completed day with a brief, translucent dove animation that begins immediately on the final checkoff in JavaScript-capable browsers and remains available after Turbo or HTML-fallback responses; it respects reduced-motion preferences, replaces the final generic success message, and never blocks interaction
10
18
 
19
+ ### Changed
20
+
21
+ - refine the completion animation to 1.6 seconds with smoother easing, limit checkoff transitions to relevant visual properties, and make reduced-motion transitions effectively instantaneous
22
+
11
23
  ### Fixed
12
24
 
13
- - correct Day 27's New Testament reading to include Matthew 26:75
14
- - silently recover all Turbo interactions from stale sessions, retry once with a fresh CSRF token, and reload rather than submit with a known-stale token if refreshing fails
15
25
  - keep repeated or concurrent explicit checkoffs idempotent instead of raising a duplicate-validation Rails 422 error
16
26
 
17
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.3.0...main)**
27
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.3.2...v1.4.0)**
28
+
29
+ ## [1.3.2] - 2026-09-02
30
+
31
+ ### Fixed
32
+
33
+ - silently recover all Turbo interactions from stale sessions, retry once with a fresh CSRF token, and reload rather than submit with a known-stale token if refreshing fails
34
+
35
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.3.1...v1.3.2)**
36
+
37
+ ## [1.3.1] - 2026-09-01
38
+
39
+ ### Fixed
40
+
41
+ - correct Day 27's New Testament reading to include Matthew 26:75
42
+
43
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.3.0...v1.3.1)**
18
44
 
19
45
  ## [1.3.0] - 2026-08-29
20
46
 
@@ -596,6 +622,9 @@ This is the initial public release.
596
622
 
597
623
  * Email sign-in requires working Action Mailer delivery in the host application. Set `config.mailer_from`; delivery is inline by default, or set `email_sign_in_deliver_later` when a queue backend is available.
598
624
 
625
+ [1.4.0]: https://github.com/avonderluft/bible270/releases/tag/v1.4.0
626
+ [1.3.2]: https://github.com/avonderluft/bible270/releases/tag/v1.3.2
627
+ [1.3.1]: https://github.com/avonderluft/bible270/releases/tag/v1.3.1
599
628
  [1.3.0]: https://github.com/avonderluft/bible270/releases/tag/v1.3.0
600
629
  [1.2.0]: https://github.com/avonderluft/bible270/releases/tag/v1.2.0
601
630
  [1.1.7]: https://github.com/avonderluft/bible270/releases/tag/v1.1.7
@@ -20,12 +20,15 @@ module Bible270
20
20
  # ---- enrolment --------------------------------------------------------
21
21
 
22
22
  def update_enrollment
23
- if params[:state] == 'closed'
23
+ case params[:state]
24
+ when 'closed'
24
25
  Setting.close_enrollment!
25
26
  redirect_to admin_path, notice: 'Closed to new readers. Existing readers can still sign in.'
26
- else
27
+ when 'open'
27
28
  Setting.open_enrollment!
28
29
  redirect_to admin_path, notice: 'Open to new readers.'
30
+ else
31
+ redirect_to admin_path, alert: 'Choose whether enrollment is open or closed. Nothing was changed.'
29
32
  end
30
33
  end
31
34
 
@@ -122,12 +125,23 @@ module Bible270
122
125
  recipients = Reader.where.not(email: [nil, '']).order(:id)
123
126
  return redirect_to(admin_path, alert: 'Nobody has an email address.') if recipients.empty?
124
127
 
128
+ recipient_count = recipients.count
125
129
  sent = deliver_broadcast(recipients, subject, body)
130
+ later = Bible270.config.registration_notice_deliver_later
131
+
132
+ if sent.zero?
133
+ action = later ? 'queued' : 'sent'
134
+ return redirect_to admin_path, alert: "The message could not be #{action} to any readers."
135
+ end
136
+
126
137
  Setting.write(LAST_BROADCAST_AT, Time.current.iso8601)
127
138
  Setting.write(LAST_BROADCAST_SUBJECT, subject)
128
139
 
129
- redirect_to admin_path,
130
- notice: "Sent to #{sent} #{'reader'.pluralize(sent)}."
140
+ action = later ? 'Queued for' : 'Sent to'
141
+ failures = recipient_count - sent
142
+ notice = "#{action} #{sent} #{'reader'.pluralize(sent)}."
143
+ notice += " #{failures} #{'reader'.pluralize(failures)} could not be #{later ? 'queued' : 'sent'}." if failures.positive?
144
+ redirect_to admin_path, notice: notice
131
145
  end
132
146
 
133
147
  def update_bible_version
@@ -169,9 +183,14 @@ module Bible270
169
183
  redirect_to admin_reader_path(@reader),
170
184
  alert: "Couldn't read #{params[:start_date].inspect} as a date."
171
185
  end
172
- elsif params[:day].present? && @reader.restart_on!(day: params[:day])
173
- redirect_to admin_reader_path(@reader),
174
- notice: "#{@reader.display_name} is now on day #{params[:day]}."
186
+ elsif params[:day].present?
187
+ day = strict_integer(params[:day], 1..Plan::DAYS)
188
+ if day && @reader.restart_on!(day: day)
189
+ redirect_to admin_reader_path(@reader),
190
+ notice: "#{@reader.display_name} is now on day #{day}."
191
+ else
192
+ redirect_to admin_reader_path(@reader), alert: 'Give a start date or a day between 1 and 270.'
193
+ end
175
194
  else
176
195
  redirect_to admin_reader_path(@reader), alert: 'Give a start date or a day between 1 and 270.'
177
196
  end
@@ -181,12 +200,15 @@ module Bible270
181
200
  # themselves on their own profile.
182
201
  def update_profile
183
202
  problems = []
184
- if (params[:first_name].present? || params[:last_name].present?) && !@reader.update_names(params[:first_name],
185
- params[:last_name])
186
- problems << 'both a first and last name'
187
- end
188
- if params[:avatar].present? && !@reader.attach_avatar(params[:avatar])
189
- problems << (@reader.errors[:avatar].first || 'a valid image')
203
+ @reader.transaction do
204
+ if (params[:first_name].present? || params[:last_name].present?) && !@reader.update_names(params[:first_name],
205
+ params[:last_name])
206
+ problems << 'both a first and last name'
207
+ end
208
+ if problems.empty? && params[:avatar].present? && !@reader.attach_avatar(params[:avatar])
209
+ problems << (@reader.errors[:avatar].first || 'a valid image')
210
+ end
211
+ raise ActiveRecord::Rollback if problems.any?
190
212
  end
191
213
 
192
214
  if problems.empty?
@@ -220,22 +242,24 @@ module Bible270
220
242
  end
221
243
 
222
244
  def complete_through
223
- day = params[:day].to_i
224
- if @reader.mark_through!(day)
225
- redirect_to admin_reader_path(@reader),
226
- notice: day.zero? ? 'All check-offs cleared.' : "Marked days 1-#{day} complete."
227
- else
228
- redirect_to admin_reader_path(@reader), alert: 'Give a day between 0 and 270.'
245
+ day = strict_integer(params[:day], 0..Plan::DAYS)
246
+ unless day
247
+ return redirect_to admin_reader_path(@reader), alert: 'Give a day between 0 and 270.'
229
248
  end
249
+
250
+ @reader.mark_through!(day)
251
+ redirect_to admin_reader_path(@reader),
252
+ notice: day.zero? ? 'All check-offs cleared.' : "Marked days 1-#{day} complete."
230
253
  end
231
254
 
232
255
  def toggle_day
233
- day = params[:day].to_i
234
- if @reader.toggle_day!(day)
235
- redirect_to admin_reader_path(@reader, anchor: "day-#{day}")
236
- else
237
- redirect_to admin_reader_path(@reader), alert: 'That day is outside the plan.'
256
+ day = strict_integer(params[:day], 1..Plan::DAYS)
257
+ unless day
258
+ return redirect_to admin_reader_path(@reader), alert: 'That day is outside the plan.'
238
259
  end
260
+
261
+ @reader.toggle_day!(day)
262
+ redirect_to admin_reader_path(@reader, anchor: "day-#{day}")
239
263
  end
240
264
 
241
265
  # Failures are counted rather than raised: one bad address must not stop the
@@ -282,16 +306,25 @@ module Bible270
282
306
 
283
307
  def load_comment
284
308
  @comment = Comment.find_by(id: params[:id])
285
- redirect_to(admin_comments_path, alert: 'Reflection not found.') if @comment.nil?
309
+ return if @comment
310
+
311
+ redirect_to admin_comments_path, alert: 'Reflection not found.'
286
312
  end
287
313
 
288
314
  def redirect_back_to_comments(notice)
289
- redirect_to(params[:return_to].presence || admin_comments_path, notice: notice)
315
+ redirect_to(url_from(params[:return_to]) || admin_comments_path, notice: notice)
290
316
  end
291
317
 
292
318
  def load_reader
293
319
  @reader = Reader.find_by(id: params[:id])
294
- redirect_to(admin_path, alert: 'Reader not found.') if @reader.nil?
320
+ return if @reader
321
+
322
+ redirect_to admin_path, alert: 'Reader not found.'
323
+ end
324
+
325
+ def strict_integer(value, range)
326
+ integer = Integer(value.to_s, 10, exception: false)
327
+ integer if range.cover?(integer)
295
328
  end
296
329
  end
297
330
  end
@@ -34,4 +34,3 @@
34
34
  </div>
35
35
 
36
36
  <%= render "bible270/shared/interaction_ui" %>
37
- <%= render "bible270/shared/day_completion_effect", day: @day %>
@@ -201,6 +201,9 @@
201
201
  }
202
202
  });
203
203
  document.addEventListener("turbo:load", reset);
204
+ document.addEventListener("animationend", (event) => {
205
+ if (event.target.matches("[data-b270-completion-dove]")) event.target.remove();
206
+ });
204
207
 
205
208
  window.Bible270InteractionUI = { reset };
206
209
  reset();
@@ -147,14 +147,14 @@
147
147
  .b270-day-complete strong{font-family:"Spectral",serif;font-size:16px}
148
148
  .b270-day-complete span{font-size:12px;color:var(--muted)}
149
149
  .b270 .b270-day-complete .b270-btn{flex:none;margin:0 0 0 auto}
150
- .b270-completion-dove{position:fixed;z-index:1000;top:var(--b270-dove-origin-y,72vh);left:var(--b270-dove-origin-x,50vw);
150
+ .b270-completion-dove{position:fixed;z-index:1000;top:72vh;left:50vw;
151
151
  width:min(75vw,97.5vh);height:auto;overflow:visible;pointer-events:none;opacity:0;
152
152
  filter:drop-shadow(0 0 8px rgba(255,255,255,1)) drop-shadow(0 0 24px rgba(255,252,220,1)) drop-shadow(0 0 52px rgba(225,191,92,.82));
153
153
  will-change:top,left,transform,opacity;animation:b270-dove-flight 1.6s cubic-bezier(.18,.72,.28,1) both}
154
154
  .b270-dove-silhouette{fill:url(#b270-dove-light);stroke:none}
155
155
  @keyframes b270-dove-flight{
156
- 0%{top:var(--b270-dove-origin-y,72vh);left:var(--b270-dove-origin-x,50vw);opacity:.22;visibility:visible;transform:translate(-50%,-50%) scale(.02) rotate(-8deg)}
157
- 8%{top:calc(var(--b270-dove-origin-y,72vh) - 10px);left:var(--b270-dove-origin-x,50vw);opacity:.78;transform:translate(-50%,-50%) scale(.08) rotate(-5deg)}
156
+ 0%{top:72vh;left:50vw;opacity:.22;visibility:visible;transform:translate(-50%,-50%) scale(.02) rotate(-8deg)}
157
+ 8%{top:calc(72vh - 10px);left:50vw;opacity:.78;transform:translate(-50%,-50%) scale(.08) rotate(-5deg)}
158
158
  64%{top:38vh;left:52vw;opacity:.92;transform:translate(-50%,-50%) scale(1) rotate(4deg)}
159
159
  99%{visibility:visible}
160
160
  100%{top:-25vh;left:115vw;opacity:0;visibility:hidden;transform:translate(-50%,-50%) scale(1.55) rotate(12deg)}
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '1.4.0'
4
+ VERSION = '1.4.1'
5
5
  end
@@ -18,10 +18,12 @@ module Bible270
18
18
  # --mailer-from=no-reply@example.org
19
19
  #
20
20
  class InstallGenerator < Rails::Generators::Base
21
- MOUNT_LINE = 'mount Bible270::Engine, at: Bible270.config.mount_at'
22
- DEFAULT_MOUNT = '/daily-bread'
23
- DEFAULT_PROVIDERS = 'github'
24
- KNOWN_PROVIDERS = {
21
+ MOUNT_LINE = 'mount Bible270::Engine, at: Bible270.config.mount_at'
22
+ MOUNT_LINE_PATTERN = %r{^\s*mount\s*\(?\s*Bible270::Engine\b}
23
+ PROVIDER_IDENTIFIER = %r{\A[a-z][a-z0-9_]*\z}
24
+ DEFAULT_MOUNT = '/daily-bread'
25
+ DEFAULT_PROVIDERS = 'github'
26
+ KNOWN_PROVIDERS = {
25
27
  'github' => 'omniauth-github',
26
28
  'google_oauth2' => 'omniauth-google-oauth2',
27
29
  'facebook' => 'omniauth-facebook',
@@ -97,18 +99,23 @@ module Bible270
97
99
  def add_strategy_gems
98
100
  return if @providers.empty?
99
101
 
100
- missing = @providers.filter_map { |p| KNOWN_PROVIDERS[p] }.reject { |g| gemfile_has?(g) }
101
- unknown = @providers.reject { |p| KNOWN_PROVIDERS.key?(p) }
102
+ missing = @providers.filter_map { |provider| KNOWN_PROVIDERS[provider] }
103
+ .reject { |gem_name| gemfile_has?(gem_name) }
104
+ @unknown_providers = @providers.reject { |provider| KNOWN_PROVIDERS.key?(provider) }
105
+ @strategy_gems_added = missing.any?
102
106
 
103
107
  missing.each { |name| gem name }
104
108
 
105
- return if unknown.empty?
109
+ return if @unknown_providers.empty?
106
110
 
107
- say " Unrecognised provider(s): #{unknown.join(', ')}. Add their strategy gems yourself.", :yellow
111
+ identifiers = @unknown_providers.map { |provider| provider.to_sym.inspect }.join(', ')
112
+ say " Unrecognised provider(s): #{@unknown_providers.join(', ')}.", :yellow
113
+ say " Add an OmniAuth strategy gem that registers #{identifiers} to the Gemfile, then run bundle install.",
114
+ :yellow
108
115
  end
109
116
 
110
117
  def install_bundle
111
- return if @providers.empty?
118
+ return unless @strategy_gems_added
112
119
 
113
120
  @bundled = decide(:bundle, 'Run bundle install now (needed before the app can boot)?', true)
114
121
  run 'bundle install' if @bundled
@@ -121,7 +128,16 @@ module Bible270
121
128
  # rather than assuming a fresh app has it.
122
129
 
123
130
  def ensure_active_storage
124
- return if skip_rails_commands?
131
+ if skip_rails_commands?
132
+ @active_storage = if !active_storage_enabled?
133
+ :unavailable
134
+ elsif active_storage_installed?
135
+ :present
136
+ else
137
+ :pending
138
+ end
139
+ return
140
+ end
125
141
 
126
142
  unless active_storage_enabled?
127
143
  @active_storage = :unavailable
@@ -153,11 +169,15 @@ module Bible270
153
169
  # Migrations run before the initializers are written, so a problem in the
154
170
  # generated config can never block the database work.
155
171
  def copy_migrations
156
- return if skip_rails_commands?
172
+ if skip_rails_commands?
173
+ @migrations_copied = false
174
+ return
175
+ end
157
176
 
158
177
  say ''
159
178
  say 'Copying migrations into db/migrate', :yellow
160
179
  rails_command 'bible270:install:migrations'
180
+ @migrations_copied = true
161
181
  end
162
182
 
163
183
  def run_migrations
@@ -191,7 +211,11 @@ module Bible270
191
211
 
192
212
  def add_route
193
213
  if routes_mounted?
194
- say " config/routes.rb already mounts Bible270::Engine — left as is.#{cms_order_hint}", :yellow
214
+ if move_mount_above_catch_all
215
+ say ' config/routes.rb already mounted Bible270::Engine; moved it above the catch-all route.', :yellow
216
+ else
217
+ say " config/routes.rb already mounts Bible270::Engine — left as is.#{cms_order_hint}", :yellow
218
+ end
195
219
  return
196
220
  end
197
221
 
@@ -202,7 +226,9 @@ module Bible270
202
226
  # across files, an unusual layout, a missing file). Verify rather than
203
227
  # assume, since a silent miss leaves the plan unreachable.
204
228
  if routes_mounted?
205
- say " Mounted in config/routes.rb.#{cms_order_hint}", :green
229
+ moved = move_mount_above_catch_all
230
+ detail = moved ? ' Mounted above the catch-all route.' : cms_order_hint
231
+ say " Mounted in config/routes.rb.#{detail}", :green
206
232
  else
207
233
  @route_failed = true
208
234
  say ' Could not edit config/routes.rb automatically.', :red
@@ -220,7 +246,7 @@ module Bible270
220
246
  say " Plan mounted at #{@mount_at}"
221
247
  say " Sign-in #{auth_summary}"
222
248
  say " Initializers #{initializer_summary}"
223
- say " Migrations #{@migrated ? 'copied and run' : 'copied (not yet run)'}"
249
+ say " Migrations #{migration_summary}"
224
250
  say " Picture uploads #{active_storage_summary}"
225
251
  say ''
226
252
 
@@ -241,16 +267,13 @@ module Bible270
241
267
  # bin/rails call fail ("Could not find ... in locally installed gems"), so
242
268
  # don't even try.
243
269
  def skip_rails_commands?
244
- return false unless @providers.any? && @bundled == false
270
+ return false unless @strategy_gems_added && @bundled == false
245
271
  return true if @warned_unbundled
246
272
 
247
273
  @warned_unbundled = true
248
274
  say ''
249
275
  say ' Gemfile changed but not bundled, so bin/rails cannot run yet.', :yellow
250
- say ' Skipping the migration steps run these once you have bundled:', :yellow
251
- say ' bundle install'
252
- say ' bin/rails bible270:install:migrations'
253
- say ' bin/rails db:migrate'
276
+ say ' Skipping Active Storage and migration commands until bundle install succeeds.', :yellow
254
277
  true
255
278
  end
256
279
 
@@ -320,10 +343,10 @@ module Bible270
320
343
 
321
344
  def ask_mailer_from
322
345
  default = "no-reply@#{app_domain}"
323
- address = prompt('From: address for sign-in emails?', default)
346
+ address = options[:mailer_from] || prompt('From: address for sign-in emails?', default)
324
347
 
325
348
  until valid_email?(address)
326
- say " '#{address}' doesn't look like an email address.", :red
349
+ say " #{address.inspect} doesn't look like an email address.", :red
327
350
  address = prompt('From: address for sign-in emails?', default)
328
351
  end
329
352
  address
@@ -352,7 +375,15 @@ module Bible270
352
375
  end
353
376
 
354
377
  def parse_providers(value)
355
- value.to_s.split(',').map { |p| p.strip.downcase.tr('-', '_') }.reject(&:empty?).uniq
378
+ providers = value.to_s.split(',').map { |provider| provider.strip.downcase.tr('-', '_') }
379
+ .reject(&:empty?).uniq
380
+ invalid = providers.grep_v(PROVIDER_IDENTIFIER)
381
+ if invalid.any?
382
+ say " Ignoring unsafe provider identifier(s): #{invalid.join(', ')}.", :red
383
+ say ' Provider names must start with a letter and contain only lowercase letters, numbers, and underscores.',
384
+ :red
385
+ end
386
+ providers - invalid
356
387
  end
357
388
 
358
389
  def app_domain
@@ -363,7 +394,11 @@ module Bible270
363
394
  end
364
395
 
365
396
  def providers_literal
366
- @providers.map { |p| ":#{p}" }.join(', ')
397
+ @providers.map { |provider| provider.to_sym.inspect }.join(', ')
398
+ end
399
+
400
+ def ruby_literal(value)
401
+ value.to_s.dump
367
402
  end
368
403
 
369
404
  def auth_path_prefix
@@ -382,15 +417,40 @@ module Bible270
382
417
  path = File.join(destination_root, 'config', 'routes.rb')
383
418
  return '' unless File.exist?(path)
384
419
 
385
- body = File.read(path)
386
- return '' unless body.match?(%r{comfy_route|match\s+['"]\*|get\s+['"]\*})
420
+ lines = File.readlines(path)
421
+ mount_index = lines.index { |line| line.match?(MOUNT_LINE_PATTERN) }
422
+ catch_all_index = catch_all_route_index(lines)
423
+ return '' unless catch_all_index && (!mount_index || mount_index > catch_all_index)
387
424
 
388
- ' Check the mount line sits ABOVE any catch-all route.'
425
+ ' Move the mount line ABOVE the catch-all route.'
426
+ end
427
+
428
+ def move_mount_above_catch_all
429
+ path = File.join(destination_root, 'config', 'routes.rb')
430
+ lines = File.readlines(path)
431
+ mount_index = lines.index { |line| line.match?(MOUNT_LINE_PATTERN) }
432
+ catch_all_index = catch_all_route_index(lines)
433
+ return false unless mount_index && catch_all_index && mount_index > catch_all_index
434
+
435
+ mount_line = lines.delete_at(mount_index)
436
+ lines.insert(catch_all_index, mount_line)
437
+ File.write(path, lines.join)
438
+ true
439
+ rescue StandardError
440
+ false
441
+ end
442
+
443
+ def catch_all_route_index(lines)
444
+ lines.index do |line|
445
+ code = line.sub(%r{#.*\z}, '')
446
+ code.match?(%r{\bcomfy_route\s+:cms\b.*\bpath:\s*['"]/['"]}) ||
447
+ code.match?(%r{\b(?:get|match)\s*\(?\s*['"]\*})
448
+ end
389
449
  end
390
450
 
391
451
  def routes_mounted?
392
452
  path = File.join(destination_root, 'config', 'routes.rb')
393
- File.exist?(path) && File.read(path).include?('Bible270::Engine')
453
+ File.exist?(path) && File.foreach(path).any? { |line| line.match?(MOUNT_LINE_PATTERN) }
394
454
  end
395
455
 
396
456
  def report_auth_choice
@@ -434,17 +494,30 @@ module Bible270
434
494
  body.include?('path_prefix') && !body.include?('Bible270.config.auth_path_prefix')
435
495
  end
436
496
 
497
+ def migration_summary
498
+ if @migrations_copied
499
+ @migrated ? 'copied and run' : 'copied (not yet run)'
500
+ else
501
+ 'not copied (not run)'
502
+ end
503
+ end
504
+
437
505
  def active_storage_summary
438
506
  case @active_storage
439
507
  when :installed then 'Active Storage installed'
440
508
  when :present then 'Active Storage already present'
441
509
  when :declined then 'off — Active Storage not installed'
442
510
  when :unavailable then 'off — Active Storage not enabled in this app'
511
+ when :pending then 'not installed — waiting for bundle install'
443
512
  else 'unchanged'
444
513
  end
445
514
  end
446
515
 
447
516
  def outstanding_steps
517
+ route_and_initializer_steps + installation_steps + provider_setup_steps + mailer_steps
518
+ end
519
+
520
+ def route_and_initializer_steps
448
521
  steps = []
449
522
  steps << "Add to config/routes.rb: #{MOUNT_LINE}" if @route_failed
450
523
  if @providers.any? && !initializer_present?('omniauth.rb')
@@ -453,22 +526,36 @@ module Bible270
453
526
  if stale_omniauth_prefix?
454
527
  steps << "config/initializers/omniauth.rb sets its own path_prefix — make it 'Bible270.config.auth_path_prefix'"
455
528
  end
456
- steps << 'bundle install' if @providers.any? && !@bundled
457
- steps << 'bin/rails bible270:install:migrations' if skip_rails_commands?
529
+ steps
530
+ end
531
+
532
+ def installation_steps
533
+ steps = []
534
+ steps << 'bundle install' if @strategy_gems_added && !@bundled
535
+ steps << unknown_provider_gem_step if @unknown_providers&.any?
536
+ steps << 'bin/rails active_storage:install' if @active_storage == :pending
537
+ steps << 'bin/rails bible270:install:migrations' unless @migrations_copied
458
538
  steps << 'bin/rails db:migrate' unless @migrated
459
539
  if @active_storage == :declined
460
540
  steps << 'bin/rails active_storage:install && bin/rails db:migrate — for picture uploads'
461
541
  end
542
+ steps
543
+ end
462
544
 
463
- if @providers.any?
464
- steps << 'Set each provider\'s client id/secret in credentials or ENV'
465
- @providers.each { |p| steps << "Register callback URL: https://YOUR-HOST#{auth_path_prefix}/#{p}/callback" }
466
- end
545
+ def unknown_provider_gem_step
546
+ providers = @unknown_providers.map { |provider| provider.to_sym.inspect }.join(', ')
547
+ "Add an OmniAuth strategy gem registering #{providers} to Gemfile, then run bundle install"
548
+ end
467
549
 
468
- if @email
469
- steps << 'Confirm Action Mailer can deliver mail in this environment'
470
- end
471
- steps
550
+ def provider_setup_steps
551
+ return [] if @providers.empty?
552
+
553
+ ['Set each provider\'s client id/secret in credentials or ENV'] +
554
+ @providers.map { |provider| "Register callback URL: https://YOUR-HOST#{auth_path_prefix}/#{provider}/callback" }
555
+ end
556
+
557
+ def mailer_steps
558
+ @email ? ['Confirm Action Mailer can deliver mail in this environment'] : []
472
559
  end
473
560
 
474
561
  # ---- file bodies ------------------------------------------------------
@@ -489,7 +576,7 @@ module Bible270
489
576
  # Change this one value to move the plan. config/routes.rb and
490
577
  # config/initializers/omniauth.rb both read it, so nothing else needs editing
491
578
  # (but do update the callback URLs registered with each OAuth provider).
492
- config.mount_at = '#{@mount_at}'
579
+ config.mount_at = #{ruby_literal(@mount_at)}
493
580
 
494
581
  config.app_name = 'Daily Bread'
495
582
  config.tagline = 'A 270-day journey through Scripture'
@@ -532,7 +619,7 @@ module Bible270
532
619
  # Passwordless email sign-in: a one-time link, no password, no third-party
533
620
  # account. This is what lets anyone take part. Needs Action Mailer delivery.
534
621
  config.email_sign_in = true
535
- config.mailer_from = '#{@mailer_from}'
622
+ config.mailer_from = #{ruby_literal(@mailer_from)}
536
623
  config.email_sign_in_ask_name = true
537
624
  # config.email_sign_in_ttl = 20 * 60 # link lifetime, seconds
538
625
  # config.email_sign_in_max_per_window = 5 # per address, per window
@@ -607,11 +694,12 @@ module Bible270
607
694
  end
608
695
 
609
696
  def provider_line(provider)
697
+ provider_symbol = provider.to_sym.inspect
610
698
  env = provider.upcase
611
699
  <<~RUBY.chomp
612
- provider :#{provider},
613
- Rails.application.credentials.dig(:#{provider}, :client_id) || ENV.fetch('#{env}_CLIENT_ID', nil),
614
- Rails.application.credentials.dig(:#{provider}, :client_secret) || ENV.fetch('#{env}_CLIENT_SECRET', nil)
700
+ provider #{provider_symbol},
701
+ Rails.application.credentials.dig(#{provider_symbol}, :client_id) || ENV.fetch(#{ruby_literal("#{env}_CLIENT_ID")}, nil),
702
+ Rails.application.credentials.dig(#{provider_symbol}, :client_secret) || ENV.fetch(#{ruby_literal("#{env}_CLIENT_SECRET")}, nil)
615
703
  RUBY
616
704
  end
617
705
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bible270
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew vonderLuft
@@ -139,7 +139,6 @@ files:
139
139
  - app/views/bible270/shared/_comment_drafts.html.erb
140
140
  - app/views/bible270/shared/_comment_notification_choices.html.erb
141
141
  - app/views/bible270/shared/_day_completion_dove.html.erb
142
- - app/views/bible270/shared/_day_completion_effect.html.erb
143
142
  - app/views/bible270/shared/_day_index.html.erb
144
143
  - app/views/bible270/shared/_footer.html.erb
145
144
  - app/views/bible270/shared/_grid_legend.html.erb
@@ -1,67 +0,0 @@
1
- <%# locals: (day:) %>
2
- <template data-b270-completion-dove-template data-b270-day="<%= day %>">
3
- <%= render "bible270/shared/day_completion_dove", day: day %>
4
- </template>
5
- <script nonce="<%= content_security_policy_nonce %>">
6
- (() => {
7
- if (window.Bible270DayCompletionEffect) return;
8
-
9
- function willCompleteDay(form) {
10
- const desiredState = form.querySelector("input[name='checked']");
11
- if (!desiredState || desiredState.value !== "1") return false;
12
-
13
- const unfinished = Array.from(
14
- document.querySelectorAll("form[data-b270-checkoff='true'] input[name='checked'][value='1']")
15
- );
16
- return unfinished.length === 1 && unfinished[0].form === form;
17
- }
18
-
19
- function startDove(day) {
20
- if (document.querySelector("[data-b270-completion-dove][data-b270-immediate]")) return;
21
-
22
- const template = document.querySelector("[data-b270-completion-dove-template]");
23
- if (!template || template.dataset.b270Day !== day) return;
24
-
25
- const dove = template.content.querySelector("[data-b270-completion-dove]").cloneNode(true);
26
- dove.dataset.b270Immediate = "true";
27
- document.body.append(dove);
28
- }
29
-
30
- function removeServerDuplicate() {
31
- const immediate = document.querySelector("[data-b270-completion-dove][data-b270-immediate]");
32
- if (!immediate) return;
33
-
34
- document.querySelectorAll("[data-b270-completion-dove]").forEach((dove) => {
35
- if (dove !== immediate) dove.remove();
36
- });
37
- }
38
-
39
- document.addEventListener("submit", (event) => {
40
- const form = event.target.closest("form[data-b270-checkoff='true']");
41
- const check = form && form.querySelector(".b270-check");
42
- if (!check) return;
43
-
44
- const bounds = check.getBoundingClientRect();
45
- document.documentElement.style.setProperty(
46
- "--b270-dove-origin-x",
47
- `${bounds.left + bounds.width / 2}px`
48
- );
49
- document.documentElement.style.setProperty(
50
- "--b270-dove-origin-y",
51
- `${bounds.top + bounds.height / 2}px`
52
- );
53
-
54
- if (willCompleteDay(form)) startDove(form.action.match(/\/day\/(\d+)/)?.[1] || "");
55
- });
56
-
57
- document.addEventListener("animationend", (event) => {
58
- if (event.target.matches("[data-b270-completion-dove]")) event.target.remove();
59
- });
60
-
61
- new MutationObserver(removeServerDuplicate).observe(document.documentElement, {
62
- childList: true,
63
- subtree: true
64
- });
65
- window.Bible270DayCompletionEffect = true;
66
- })();
67
- </script>