bible270 0.6.2 → 0.9.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.
data/README.md CHANGED
@@ -1,61 +1,44 @@
1
- # bible270
2
-
3
- A mountable **Rails engine** that adds a social, 270-day Bible reading plan to a
4
- host Rails application such as [ComfortableMediaSurfer](https://github.com/shakacode/comfortable-mexican-sofa).
5
-
6
- Each day presents three readings, sized by **verse count** so daily portions take roughly
7
- the same time to read. Chapters are kept **whole** — the plan never chops a chapter into
8
- verse ranges except for the one excessively long chapter, Psalm 119.
9
-
10
- - **Old Testament** — read once, cover to cover. Whole chapters grouped so each day is
11
- ~equal in verses (avg ~73/day, range 38–116).
12
- - **New Testament** — read **twice**. Each pass gets half the plan (135 days), so
13
- Revelation lands on day 135 and again on day 270. ~2 chapters/day, every day, verse-balanced
14
- within each pass.
15
- - **Psalms & Proverbs** — a whole-chapter companion that runs **twice** over the 270 days
16
- (135 portions x 2). Very short chapters merge with a neighbour so there are no trivial
17
- two-verse days (Psalm 117 reads as "Psalm 116–117"), and **Psalm 119 (176 verses) is the
18
- sole chapter divided** — into exactly two readings, 1–88 and 89–176.
19
-
20
- Genesis→Malachi, the second pass through Revelation, and Proverbs 31 all finish together on
21
- day 270. A typical day runs ~157 verses across the three tracks (range 102–238).
22
-
23
- Readers check off each track, leave reflections (comments) on any day, and can see everyone
24
- else's progress and reflections — similar to the social plans on Bible.com.
25
-
26
- The plan itself is pure, deterministic Ruby (`Bible270::Plan`) — no rows are stored for the
27
- schedule. Daily portions are sized using per-chapter verse counts (`Bible270::Versification`,
28
- standard English/KJV versification; verse counts are facts about the text's structure, so
29
- nothing copyrighted is bundled). The database only holds **readers, check-offs, and comments**.
30
-
31
- ## Requirements & compatibility
32
-
33
- - **Ruby** >= 3.0 (the gemspec's `required_ruby_version`). The plan logic is plain Ruby with
34
- no C extensions and no dependency on any gem that Ruby 4.0 unbundled.
35
- - **Ruby 4.0 notes.** Audited against the 4.0 breaking changes: the gem does not use `cgi`
36
- (removed from default gems in 4.0 — URL escaping goes through `URI.encode_www_form_component`
37
- instead, which is byte-identical to `CGI.escape`), `Set`/`SortedSet`, `Ractor`, `Net::HTTP`,
38
- `Process::Status`, `ObjectSpace`, or any of the gems promoted from default to bundled
39
- (`ostruct`, `logger`, `benchmark`, `pstore`, `irb`, `rdoc`). Every file carries a
40
- `# frozen_string_literal: true` magic comment and no string literal is mutated.
41
- - **Rails.** The gemspec allows `rails >= 7.0`, but on Ruby 4.0 the binding constraint is your
42
- host app, not this engine: Rails 8.0/8.1 require Ruby >= 3.2, and ComfortableMediaSurfer is a
43
- "Rails 7.0+" engine. Verify your own lockfile (e.g. with RailsBump) before pairing an older
44
- Rails with Ruby 4.0.
45
- - A host application (this is an engine, not a standalone app).
46
- - Turbo is used for seamless check-offs and degrades to full-page redirects if absent.
1
+ # Bible 270
47
2
 
48
- ## Install
3
+ [![Gem Version](https://badge.fury.io/rb/bible270.svg)](https://badge.fury.io/rb/bible270) [![Gem Downloads](https://img.shields.io/gem/dt/bible270.svg?style=flat)](http://rubygems.org/gems/bible270) [![GitHub Release Date - Published_At](https://img.shields.io/github/release-date/avonderluft/bible270?label=last%20release&color=seagreen)](https://github.com/avonderluft/bible270/releases)
4
+
5
+ A mountable **Rails engine** that drops a 270-day (9 month) interactive Bible reading plan into any Rails app, built with [ComfortableMediaSurfer](https://github.com/shakacode/comfortable-media-surfer) CMS in mind. Readers tick off each day's readings, post reflections, and see how everyone else is getting on, much like the social plans on Bible.com.
6
+
7
+ Initially created for use by students and faculty of [Kingdom Movement School of Ministry](https://www.kmsm.life/), to read through all of Scripture together, during the school year. Thus it is potentially useful for any Bible School, Seminary, or Discipleship school with a 9 month school year.
8
+
9
+ ## The plan
10
+
11
+ Three readings a day, sized by **verse count** so each day takes roughly the same time.
49
12
 
50
- Add to the host app's `Gemfile`:
13
+ - **Old Testament** — once through, with Psalms and Proverbs live in their own track. Whole chapters grouped so each day is ~73 verses (range 38–116).
14
+ - **New Testament** — once through, a reading every day. 260 chapters over 270 days, so the 10 longest are halved (Luke 1, Matthew 26–27, Mark 14, Luke 9, 12, 22, John 6,8, Acts 7).
15
+ - **Psalms & Proverbs** — Psalms once, Proverbs twice, interleaved in one track. Proverbs supplies 62 whole-chapter readings, leaving 208 days for the Psalms; longer psalms are divided to fill them. **Psalm 119 is pinned to 11 sections of exactly 16 verses** (176 = 11 × 16, i.e. two of its eight-verse acrostic stanzas per reading), and 41 psalms in total get divided so no single reading tops 20 verses.
16
+
17
+ Genesi → Malachi and Revelation 22 both land on day 270, as does the second Proverbs 31 — the first falls on day 135, the midpoint. Psalm 150 comes in on day 269. Every day carries all three tracks. A typical day is ~119 verses (range 76–175).
18
+
19
+ The schedule is pure deterministic Ruby (`Bible270::Plan`) — none of it is stored. Portions come from per-chapter verse counts in `Bible270::Versification`. The DB only holds readers, check-offs, comments, and sign-in tokens.
20
+
21
+ ## Requirements
22
+
23
+ - **Ruby** >= 3.0. Plain Ruby, no C extensions. Runs on 4.0.6 — it avoids everything 4.0 dropped or unbundled (notably `cgi`; URL escaping uses `URI.encode_www_form_component`).
24
+ - **Rails** >= 7.0. On Ruby 4.0 your host app is the real constraint, not this engine — check your lockfile with RailsBump first.
25
+ - A host app, and Turbo if you want check-offs without a page reload.
26
+
27
+ ## Install
51
28
 
52
29
  ```ruby
53
- gem "bible270", path: "vendor/gems/bible270"
54
- # or, once pushed to a git remote:
55
- # gem "bible270", git: "https://github.com/avonderluft/bible270.git", branch: "main"
30
+ # Gemfile
31
+ gem "bible270"
56
32
  ```
57
33
 
58
- Then:
34
+ <details>
35
+ <summary>Other sources</summary>
36
+
37
+ ```ruby
38
+ gem "bible270", git: "https://github.com/avonderluft/bible270.git", branch: "main"
39
+ gem "bible270", path: "<path_to_your_local_copy>/bible270" # local checkout
40
+ ```
41
+ </details>
59
42
 
60
43
  ```bash
61
44
  bundle install
@@ -63,97 +46,113 @@ bin/rails bible270:install:migrations
63
46
  bin/rails db:migrate
64
47
  ```
65
48
 
66
- Mount it in `config/routes.rb`:
49
+ That copies four migrations into your `db/migrate` — yours from then on, in your `schema.rb`. The engine doesn't touch your migration paths, so the copies are the only definitions in play.
50
+
51
+ Mount it:
67
52
 
68
53
  ```ruby
69
- mount Bible270::Engine, at: "/reading-plan"
54
+ # config/routes.rb
55
+ mount Bible270::Engine, at: Bible270.config.mount_at
70
56
  ```
71
57
 
72
- The plan is now live at `/reading-plan`.
58
+ Optionally generate the initializers:
73
59
 
74
- ---
60
+ ```bash
61
+ bin/rails generate bible270:install --providers=github
62
+ ```
75
63
 
76
- ## Authentication
64
+ You're live at `/daily-bread`.
77
65
 
78
- Two built-in ways to sign in, so **nobody is excluded**:
66
+ ### Upgrading
79
67
 
80
- 1. **Email link (passwordless)** — the reader types their address, gets a one-time link, and
81
- clicks it. No password, no account with anyone else. This is the default and it's what makes
82
- the plan usable by people who don't have (or don't want to use) GitHub, Google, and friends.
83
- 2. **OmniAuth social sign-in** — optional convenience for those who do.
68
+ ```bash
69
+ bundle update bible270
70
+ bin/rails bible270:install:migrations # skips ones you already have
71
+ bin/rails db:migrate
72
+ ```
84
73
 
85
- Either can run alone. `config.omniauth_providers = []` gives you an email-only site;
86
- `config.email_sign_in = false` gives you social-only. Viewing the plan and reading others'
87
- reflections stays public either way — only checking off and commenting require signing in.
74
+ ## Bible270 Mount point in your Rails app
88
75
 
89
- ### Email sign-in
76
+ The path lives in exactly **one** place, `config.mount_at`:
90
77
 
91
78
  ```ruby
79
+ # config/initializers/bible270.rb
92
80
  Bible270.configure do |config|
93
- config.email_sign_in = true
94
- config.mailer_from = "no-reply@gknt.org"
95
- config.email_sign_in_ask_name = true # reader picks their display name
96
- # config.email_sign_in_ttl = 20 * 60 # link lifetime (seconds)
97
- # config.email_sign_in_max_per_window = 5 # per address, per window
98
- # config.email_sign_in_window = 15 * 60
99
- # config.email_sign_in_deliver_later = true # needs an Active Job backend
81
+ config.mount_at = "/daily-bread" # or "/read270", "/manna", whatever you wish
100
82
  end
101
83
  ```
102
84
 
103
- The host app needs working Action Mailer delivery — nothing else. How it behaves:
85
+ Routes read it directly; OmniAuth reads `Bible270.config.auth_path_prefix` (just
86
+ `"<mount_at>/auth"`). The engine builds its own sign-in links from `request.script_name` at runtime, so there's no third spot to update.
104
87
 
105
- - Tokens are 256-bit, URL-safe, single-use, and expire (20 minutes by default).
106
- - **Only a SHA-256 digest of the token is stored**, so the table is useless to anyone who reads
107
- the database. There is no password column anywhere in this gem.
108
- - Consuming a link is a conditional update, so a double-clicked link can't sign in twice.
109
- - The "check your inbox" response is identical whether the address was known, unknown, or
110
- rate-limited — no account enumeration and no hint that a limit was hit.
111
- - Readers may set the display name shown beside their reflections; otherwise it's derived from the
112
- address (`mary.anne.smith@…` → "Mary Anne Smith").
113
- - Spent and stale tokens can be cleaned up with `Bible270::SignInToken.sweep!` from a cron/rake task.
88
+ Values are normalised — `"read270"`, `"/read270"`, `"/read270/"` all become `/read270`. Nested paths and mounting at `/` work too.
114
89
 
115
- An email reader gets `provider: "email"` in the same identity columns OmniAuth uses, so downstream
116
- (progress, comments, leaderboard) treats every reader identically.
90
+ Two things sit outside your app and still need doing by hand when you move it: the **OAuth callback URLs** in each provider's dashboard, and any **CMS links**.
117
91
 
118
- ### OmniAuth social sign-in
92
+ > **Load order matters.** `bible270.rb` has to be read before `omniauth.rb`, since the latter asks for `auth_path_prefix`. Rails loads initializers alphabetically so the default names work fine. If you rename either, keep that order — or set `mount_at` in `config/application.rb`, which loads before all initializers.
119
93
 
120
- ### Quick setup
94
+ Need OmniAuth somewhere unrelated? Set `config.omniauth_path_prefix` and it wins.
121
95
 
122
- ```bash
123
- bin/rails generate bible270:install --mount-at=/reading-plan --providers=github
124
- ```
96
+ ## Authentication
97
+
98
+ Two ways in, so **nobody's shut out**:
99
+
100
+ 1. **Email link** — type an address, get a one-time link, click it. No password, no third-party account. The default, and what makes this usable by people with no GitHub or Google login.
101
+ 2. **Social sign-in** via OmniAuth — for people who'd rather.
125
102
 
126
- That writes `config/initializers/bible270.rb` (email sign-in enabled) and
127
- `config/initializers/omniauth.rb` (with `path_prefix` already matching your mount point), and adds
128
- the `mount` line to your routes. Pass `--providers=` with an empty value for an email-only site.
129
- Then add the gems you need and migrate:
103
+ Either works alone: `omniauth_providers = []` for email-only, `email_sign_in = false` for
104
+ social-only. Reading is always public; only ticking off and commenting need an account.
105
+
106
+ ### Email links
130
107
 
131
108
  ```ruby
132
- # Gemfile — omniauth and omniauth-rails_csrf_protection come in with bible270;
133
- # the provider strategy is your choice:
134
- gem "omniauth-github"
109
+ Bible270.configure do |config|
110
+ config.email_sign_in = true
111
+ config.mailer_from = "no-reply@example.com"
112
+ config.email_sign_in_ask_name = true # let readers pick their display name
113
+ # config.email_sign_in_ttl = 20 * 60 # link lifetime, seconds
114
+ # config.email_sign_in_max_per_window = 5 # per address
115
+ # config.email_sign_in_window = 15 * 60
116
+ # config.email_sign_in_deliver_later = true # needs Active Job
117
+ end
135
118
  ```
136
119
 
120
+ All it needs is working Action Mailer delivery.
121
+
122
+ - Tokens: 256-bit, URL-safe, single-use, 20-minute expiry.
123
+ - **Only a SHA-256 digest is stored** — no password column anywhere, so the table is useless to
124
+ anyone reading your DB.
125
+ - Claiming is a conditional update, so a double-clicked link still signs you in once.
126
+ - "Check your inbox" reads the same whether the address was known, unknown, or rate-limited — no
127
+ account enumeration.
128
+ - Display name comes from the reader, else the address (`mary.anne.smith@…` → "Mary Anne Smith").
129
+ - `Bible270::SignInToken.sweep!` clears spent tokens. Good cron fodder.
130
+
131
+ Email readers get `provider: "email"` in the same columns OmniAuth uses, so everything downstream
132
+ treats them identically.
133
+
134
+ ### Social sign-in
135
+
136
+ The generator writes both initializers for you and adds the `mount` line:
137
+
137
138
  ```bash
138
- bundle install
139
- bin/rails bible270:install:migrations && bin/rails db:migrate
139
+ bin/rails generate bible270:install --providers=github
140
140
  ```
141
141
 
142
- Finally register the callback URL with the provider:
142
+ Add the strategy gem (`omniauth` and `omniauth-rails_csrf_protection` ride along with bible270):
143
143
 
144
- ```
145
- https://gknt.org/reading-plan/auth/github/callback
144
+ ```ruby
145
+ gem "omniauth-github"
146
146
  ```
147
147
 
148
- ### Manual setup
148
+ Register the callback with the provider — `https://example.com/daily-bread/auth/github/callback`.
149
149
 
150
- If you'd rather not use the generator, the only subtlety is that OmniAuth's `path_prefix` must
151
- line up with where the engine is mounted, so its callback lands on the engine's route:
150
+ Doing it by hand? The one thing to get right is lining up `path_prefix` with the mount point:
152
151
 
153
152
  ```ruby
154
153
  # config/initializers/omniauth.rb
155
154
  Rails.application.config.middleware.use OmniAuth::Builder do
156
- path_prefix "/reading-plan/auth" # == <mount point>/auth
155
+ path_prefix Bible270.config.auth_path_prefix
157
156
  provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"]
158
157
  end
159
158
 
@@ -163,31 +162,26 @@ OmniAuth.config.on_failure = proc { |env| Bible270::SessionsController.action(:f
163
162
  ```ruby
164
163
  # config/initializers/bible270.rb
165
164
  Bible270.configure do |config|
166
- config.omniauth_providers = [:github] # or [:github, [:google_oauth2, "Google"]]
165
+ config.mount_at = "/daily-bread"
166
+ config.omniauth_providers = [:github] # or [:github, [:google_oauth2, "Google"]]
167
167
  config.parent_controller = "::ApplicationController"
168
168
  end
169
169
  ```
170
170
 
171
- ### Notes worth knowing
171
+ Worth knowing:
172
172
 
173
- - **Sign-in is a POST, not a link.** OmniAuth 2.0+ refuses GET on its request phase
174
- (CVE-2015-9284), so every sign-in control the engine renders is a `button_to` carrying a CSRF
175
- token, and `omniauth-rails_csrf_protection` provides the Rails-aware verifier. If you build your
176
- own sign-in link, it must POST to `<mount>/auth/:provider`.
177
- - **One provider vs several.** With a single configured provider the header shows a direct
178
- "Sign in with X" button; with several it links to a sign-in page (`GET <mount>/sign_in`) listing
179
- them all.
180
- - **Return-to-origin.** Sign-in controls pass the current path as `origin`, so a reader who clicks
181
- a check-off while signed out lands back on that same day afterwards. Origins are validated to be
182
- local paths only.
183
- - **Session hygiene.** `reset_session` runs on both sign-in and sign-out to avoid session fixation.
184
- Sign-out is `DELETE <mount>/sign_out`.
185
- - **What's stored.** Provider, uid, display name, email, and avatar URL — no tokens, no passwords.
173
+ - **Sign-in is a POST.** OmniAuth 2.0+ refuses GET
174
+ ([CVE-2015-9284](https://nvd.nist.gov/vuln/detail/CVE-2015-9284)), so the engine's controls are
175
+ `button_to` forms with CSRF tokens. Build your own and it must POST to `<mount>/auth/:provider`.
176
+ - One provider → a button in the header. Several → a sign-in page at `<mount>/sign_in`.
177
+ - Sign-in carries the current path as `origin`, so clicking a check-off while signed out brings you
178
+ back to that day. Origins must be local paths.
179
+ - `reset_session` on sign-in and sign-out, against session fixation.
180
+ - Stored: provider, uid, display name, email, avatar URL. No tokens, no passwords.
186
181
 
187
- ### Alternative: bridge your host's own users
182
+ ### Or use your own users
188
183
 
189
- If the host app already has authentication and you'd rather not use OmniAuth at all, set a
190
- resolver instead and the engine will never touch sessions:
184
+ Already have auth? Hand the engine a resolver and it'll never touch sessions:
191
185
 
192
186
  ```ruby
193
187
  config.current_reader_resolver = lambda do |controller|
@@ -198,176 +192,200 @@ config.current_reader_resolver = lambda do |controller|
198
192
  end
199
193
  ```
200
194
 
201
- `Reader.for_owner` links a reader to any host model polymorphically via `owner`. Note that
202
- ComfortableMediaSurfer's admin authentication is separate from public site visitors, so for a
203
- public plan on a CMS front-end the OmniAuth path above is usually the right fit.
195
+ `Reader.for_owner` links to any host model polymorphically. Heads up: CMS admin auth is separate
196
+ from public visitors, so for a public plan the email/OmniAuth route above is usually what you want.
204
197
 
205
198
  ## Start dates
206
199
 
207
- The plan works either **undated** (day numbers only) or **dated** (day numbers mapped onto a
208
- calendar). There are two levers:
200
+ The plan runs **undated** (day numbers only) or **dated** (days mapped onto a calendar):
209
201
 
210
202
  ```ruby
211
- Bible270.configure do |config|
212
- # A community-wide start date — everyone reads together as a cohort.
213
- # Accepts a Date, Time, or "YYYY-MM-DD" string. Default: nil (undated).
214
- config.start_date = Date.new(2026, 9, 6)
215
-
216
- # May an individual reader set/change their own start date? Default: true.
217
- # Set false to pin everyone to config.start_date.
218
- config.allow_reader_start_date = true
219
- end
203
+ config.start_date = Date.new(2026, 9, 6) # Date, Time, or "YYYY-MM-DD". nil = undated
204
+ config.allow_reader_start_date = true # can readers set their own?
220
205
  ```
221
206
 
222
- How the two combine:
223
-
224
- | `start_date` | `allow_reader_start_date` | Behaviour |
207
+ | `start_date` | `allow_reader_start_date` | What happens |
225
208
  |---|---|---|
226
- | nil | true (default) | Each reader is stamped with their own start date the first time they check something off, and can change it. |
227
- | a date | true | The community date is the default; any reader may override it with their own. |
228
- | a date | false | Everyone is pinned to the community date. The per-reader form is hidden. |
229
- | nil | false | Fully undated — day numbers only, no calendar anywhere. |
230
-
231
- When a plan is dated, each day page shows its calendar date with a **Today** badge, the overview
232
- gains a "Go to today" link, and readers see whether they're ahead of or behind the pace.
209
+ | nil | true *(default)* | Each reader gets stamped on first check-off, and can change it |
210
+ | a date | true | Community date is the default; anyone may override |
211
+ | a date | false | Everyone pinned to the community date, form hidden |
212
+ | nil | false | Fully undated — no calendar anywhere |
233
213
 
234
- Readers manage their own date from the overview page (`PATCH /start-date`, `DELETE /start-date`).
235
- Changing a start date only re-maps days onto the calendar — check-offs and reflections are keyed
236
- to day numbers and are never touched.
214
+ Dated plans show each day's date with a **Today** badge, a "Go to today" link, and whether you're
215
+ ahead or behind. Readers set their own from the overview. Changing a start date only re-maps the
216
+ calendar — check-offs and reflections are keyed to day numbers, so nothing moves.
237
217
 
238
- The date helpers are pure functions on `Bible270::Plan`, so you can use them anywhere:
218
+ Date helpers are pure functions, usable anywhere:
239
219
 
240
220
  ```ruby
241
- Bible270::Plan.date_for(1, "2026-09-06") # => Sun, 06 Sep 2026
242
- Bible270::Plan.end_date_for("2026-09-06") # => Wed, 02 Jun 2027 (day 270)
243
- Bible270::Plan.day_for(Date.current, start) # => 42 (clamped to 1..270)
244
- Bible270::Plan.day_for(date, start, clamp: false) # => -3 or 271, to detect out-of-range
245
- Bible270::Plan.before_start?(date, start) # => true/false
221
+ Bible270::Plan.date_for(1, "2026-09-06") # => Sun, 06 Sep 2026
222
+ Bible270::Plan.end_date_for("2026-09-06") # => Wed, 02 Jun 2027 (day 270)
223
+ Bible270::Plan.day_for(Date.current, start) # => 42 (clamped 1..270)
224
+ Bible270::Plan.day_for(date, start, clamp: false) # => -3 or 271, to spot out-of-range
225
+ Bible270::Plan.before_start?(date, start)
246
226
  ```
247
227
 
248
228
  ## Configuration reference
249
229
 
250
230
  ```ruby
251
231
  Bible270.configure do |config|
232
+ config.mount_at = "/daily-bread" # single source of truth for the path
252
233
  config.app_name = "Daily Bread"
253
234
  config.tagline = "A 270-day journey through Scripture"
254
235
 
255
236
  config.start_date = nil # community start date, or nil for undated
256
- config.allow_reader_start_date = true # may readers set their own?
237
+ config.allow_reader_start_date = true
257
238
 
258
- config.parent_controller = "ActionController::Base" # or "::ApplicationController"
239
+ config.parent_controller = "ActionController::Base" # or "::ApplicationController"
259
240
  config.layout = "bible270/application"
260
241
 
261
- config.email_sign_in = true # passwordless email link
242
+ config.email_sign_in = true
262
243
  config.mailer_from = "no-reply@example.com"
263
244
  config.email_sign_in_ttl = 20 * 60
264
245
  config.email_sign_in_ask_name = true
265
- config.omniauth_providers = [:github] # optional social sign-in
266
- config.omniauth_path_prefix = nil # nil = derive "<mount>/auth"
267
- config.current_reader_resolver = nil # set to bridge host users instead
246
+ config.omniauth_providers = [:github]
247
+ config.omniauth_path_prefix = nil # nil = derive "<mount_at>/auth"
248
+ config.current_reader_resolver = nil # set to bridge your own users
268
249
  config.require_sign_in_to_participate = true
269
250
 
270
- config.after_sign_in_path = nil # defaults to plan root
251
+ config.after_sign_in_path = nil # defaults to the plan root
271
252
  config.after_sign_out_path = nil
272
253
 
273
- config.bible_version = "ESV"
254
+ config.bible_version = "NKJV"
274
255
  config.passage_url_builder = ->(reference, version) {
275
256
  "https://www.biblegateway.com/passage/?search=#{URI.encode_www_form_component(reference)}&version=#{version}"
276
257
  }
277
258
  end
278
259
  ```
279
260
 
280
- Every reading reference links out to the configured Bible reader (Bible Gateway by
281
- default). Point `passage_url_builder` at your own reader if you host one.
282
-
283
- ---
284
-
285
- ## Using it inside ComfortableMediaSurfer
286
-
287
- ComfortableMediaSurfer serves marketing/content pages; this engine is a separate
288
- mounted app. Two integration styles:
289
-
290
- 1. **Link to it** — mount at `/reading-plan` and add a CMS navigation link. The
291
- engine renders its own themed pages. Simplest and fully featured.
292
-
293
- 2. **Match the site chrome** — set `config.parent_controller = "::ApplicationController"`
294
- and `config.layout = "layouts/application"` (your site layout) so the plan pages
295
- sit inside your normal header/footer. The engine's scoped CSS (all classes are
296
- prefixed `b270-`) won't collide with CMS styles.
261
+ References link to Bible Gateway by default — point `passage_url_builder` at your another reader if you wish, or your own reader if you host one.
297
262
 
298
- Because CMS admin auth (`ComfortableMediaSurfer`) is separate from public readers,
299
- Option B (OmniAuth) is usually the right fit for public visitors on gknt.org.
263
+ ## With ComfortableMediaSurfer
300
264
 
301
- ---
265
+ The CMS serves your content; this is a separate mounted app. Either **just link to it** (mount, add a nav link, done — the engine renders its own themed pages), or **match your chrome** by setting `config.parent_controller = "::ApplicationController"` and `config.layout = "layouts/application"`
266
+ so it sits inside your header and footer. All engine CSS is prefixed `b270-`, so nothing collides.
302
267
 
303
268
  ## Data model
304
269
 
305
- | Table | Purpose |
306
- |-------|---------|
270
+ | Table | Holds |
271
+ |-------|-------|
307
272
  | `bible270_readers` | identity: display name, avatar, provider/uid or polymorphic `owner`, `started_on` |
308
- | `bible270_checkoffs` | one row per reader per day per track (`ot`/`nt`/`pp`); unique index prevents dups |
309
- | `bible270_sign_in_tokens` | short-lived magic-link tokens: email, **digest only**, expiry, consumed-at |
310
- | `bible270_comments` | a reflection on a day (optionally scoped to a track), public to all |
273
+ | `bible270_checkoffs` | one row per reader per day per track (`ot`/`nt`/`pp`), unique-indexed |
274
+ | `bible270_comments` | a reflection on a day, optionally scoped to a track, public to all |
275
+ | `bible270_sign_in_tokens` | magic-link tokens: email, **digest only**, expiry, consumed-at |
311
276
 
312
- A day counts as "complete" for a reader when they've checked every track that has
313
- content that day (2 on a light-NT day, otherwise 3). All progress and comments are
314
- **public** to other readers by design (mirroring Bible.com's shared plans).
277
+ A day is "complete" once all three tracks are ticked. Progress and comments are **public** by
278
+ design, same as Bible.com's shared plans.
315
279
 
316
- ## Routes (within the mount point)
280
+ ## Routes (inside the mount point)
317
281
 
318
282
  ```
319
283
  GET / days#index overview + community + calendar
320
- GET /day/:day days#show three readings, completers, reflections
284
+ GET /day/:day days#show readings, who's finished, reflections
321
285
  POST /day/:day/toggle/:track checkoffs#toggle
322
286
  POST /day/:day/comments comments#create
323
287
  DELETE /comments/:id comments#destroy
324
288
  PATCH /start-date readers#update_start_date
325
289
  DELETE /start-date readers#clear_start_date
326
290
  GET /community readers#index leaderboard
327
- GET /readers/:id readers#show a reader's progress + reflections
328
- GET /sign_in sessions#new email form + provider list
329
- POST /sign_in/email sessions#email_link send a magic link
330
- GET /sign_in/email/:token sessions#email_callback consume a magic link
331
- GET /auth/:provider/callback sessions#create (OmniAuth callback)
291
+ GET /readers/:id readers#show
292
+ GET /sign_in sessions#new email form + provider list
293
+ POST /sign_in/email sessions#email_link
294
+ GET /sign_in/email/:token sessions#email_callback
295
+ GET /auth/:provider/callback sessions#create
332
296
  POST /auth/:provider/callback sessions#create
333
297
  GET /auth/failure sessions#failure
334
298
  DELETE /sign_out sessions#destroy
335
299
 
336
- POST <mount>/auth/:provider handled by the OmniAuth middleware, not the engine
300
+ POST <mount>/auth/:provider OmniAuth middleware, not the engine
337
301
  ```
338
302
 
339
- ## Tuning the plan shape
303
+ ## Tuning the plan
340
304
 
341
- The whole schedule is derived from a handful of constants in `Bible270::Plan`, so the shape
342
- can be changed without touching anything else:
305
+ The whole schedule falls out of a few constants in `Bible270::Plan`:
343
306
 
344
307
  | Constant | Default | Effect |
345
308
  |----------|---------|--------|
346
309
  | `DAYS` | `270` | plan length |
347
- | `NT_PASSES` | `2` | how many times the New Testament is read (each pass gets `DAYS / NT_PASSES` days) |
348
- | `PP_LONG_CHAPTER` | `100` | a chapter longer than this is divided. At 100 only Psalm 119 (176v) qualifies, since the next longest is Psalm 78 at 72v. Raise it above 176 to never split anything |
349
- | `PP_SPLIT_PARTS` | `2` | how many readings such a chapter becomes |
350
- | `PP_MIN_DAY` / `PP_DAY_TARGET` | `12` / `22` | how aggressively short chapters merge. These two values are what make the companion come out to exactly 135 portions, i.e. two clean passes across 270 days |
310
+ | `PROVERBS_PASSES` | `2` | Proverbs laps. Each pass is 31 readings, so this decides how many days are left for the Psalms |
311
+ | `PSALM_119_SECTION_SIZE` | `16` | verses per Psalm 119 section. 176 divides evenly by 8, 11, 16, 22 and 44 |
351
312
 
352
- Everything is computed at load time and memoized; there is no generated schedule to migrate
353
- if you change these.
313
+ Change `PROVERBS_PASSES` or `DAYS` and both the New Testament and the Psalms re-divide automatically
314
+ to fill whatever's left. Divisions always go to whichever chapter currently carries the heaviest
315
+ reading, so the longest get split first and nothing is divided that doesn't need to be.
354
316
 
355
- ## Scaling note
317
+ All computed at load and memoized — no generated schedule, so nothing to migrate if you change them.
356
318
 
357
- The community leaderboard computes completed-days in Ruby from grouped check-off
358
- counts. That's ideal for a homelab/parish-sized community (hundreds of readers). If
359
- you grow to many thousands, add a cached `days_completed` counter on `Reader`
360
- (updated in a `Checkoff` after_commit) and sort in SQL.
319
+ ## Scaling
361
320
 
362
- ## Development / tests
321
+ The leaderboard counts completed days in Ruby from grouped check-offs — fine for hundreds of
322
+ readers. Past a few thousand, cache a `days_completed` counter on `Reader` via a `Checkoff`
323
+ `after_commit` and sort in SQL.
363
324
 
364
- The deterministic plan logic is fully unit-tested with no Rails dependency:
325
+ ## Troubleshooting
326
+
327
+ **`DuplicateMigrationNameError: Multiple migrations have the name CreateBible270Readers`**
328
+
329
+ 0.6.2 and earlier wrongly added the engine's `db/migrate` to your app's paths *and* shipped
330
+ `install:migrations`, so copying defined everything twice. `bundle update bible270` to >= 0.6.3,
331
+ keep your copies, migrate.
332
+
333
+ **404 on the provider callback**
334
+
335
+ `path_prefix` doesn't match the mount point. Use `Bible270.config.auth_path_prefix` rather than a
336
+ literal, and check the URL registered with the provider matches. Setting `mount_at` in an initializer
337
+ Rails loads *after* `omniauth.rb` gives you the default prefix — see [Mount point](#mount-point).
338
+
339
+ **Sign-in button does nothing, or `OmniAuth::AuthenticityError`**
340
+
341
+ Has to be a POST with a CSRF token. The engine's controls already are — if you built your own, make
342
+ it a `button_to`. Also check `omniauth-rails_csrf_protection` is in the bundle.
343
+
344
+ **No email arrives**
345
+
346
+ Needs working Action Mailer delivery — the engine just calls `deliver_now`. Check `mailer_from` is
347
+ an address your relay accepts, and watch the logs. Failures never surface to the reader, on purpose,
348
+ since that message can't reveal which addresses exist.
349
+
350
+ **"That link has expired or was already used"**
351
+
352
+ Links are single-use, 20-minute life. Some mail scanners and link-preview bots fetch URLs before the
353
+ recipient clicks, burning the token. If it keeps happening, lengthen `email_sign_in_ttl` or find
354
+ what's prefetching.
355
+
356
+ **Everything's unstyled in my layout**
357
+
358
+ The engine's CSS rides in a partial its own layout renders. Using your layout? Add
359
+ `<%= render "bible270/shared/styles" %>` to the `<head>`, or style the `b270-*` classes yourself.
360
+
361
+ ## Development
365
362
 
366
363
  ```bash
367
364
  bundle install
368
365
  rake test
369
366
  ```
370
367
 
368
+ Plan logic is fully unit-tested with no Rails dependency.
369
+
370
+ ## Contributing
371
+
372
+ Bug reports and pull requests are welcome at <https://github.com/avonderluft/bible270>.
373
+
374
+ 1. Fork it and clone your fork.
375
+ 2. Branch off `main`: `git checkout -b my-feature`.
376
+ 3. Make your change, and add tests — `rake test` should stay green.
377
+ 4. Commit with a clear message: `git commit -am "Add my feature"`.
378
+ 5. Push: `git push origin my-feature`.
379
+ 6. Open a pull request against `main`, saying what changed and why.
380
+
381
+ A few things that'll make review quick:
382
+
383
+ - Keep plan logic free of Rails so it stays unit-testable.
384
+ - Anything touching the reading schedule needs a test pinning the expected references — the plan is
385
+ deterministic, so assert exact values.
386
+ - Prefix new CSS classes and helpers with `b270`.
387
+ - Note anything user-visible in `CHANGELOG.md`.
388
+
371
389
  ## License
372
390
 
373
391
  MIT. See `MIT-LICENSE`.
@@ -19,9 +19,10 @@
19
19
  <% if flash[:notice] %><div class="b270-flash notice"><%= flash[:notice] %></div><% end %>
20
20
  <%= yield %>
21
21
  <footer class="b270-footer">
22
- <p>The Old Testament is read once, cover to cover. The New Testament is read twice —
23
- the second pass begins on day <%= Bible270::Plan.nt_second_pass_start_day %>.
24
- Psalms &amp; Proverbs runs alongside twice over, whole chapters at a time.</p>
22
+ <p>The Old Testament and New Testament are each read once across the 270 days, with a
23
+ reading from all three tracks every day. Alongside them the Psalms run once and
24
+ Proverbs twice; the longest chapters are divided so the readings fill each day
25
+ evenly, and Psalm 119 comes in eleven sections.</p>
25
26
  </footer>
26
27
  </div>
27
28
  </body>