bible270 1.1.5 → 1.1.7
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 +4 -4
- data/CHANGELOG.md +28 -2
- data/README.md +2 -0
- data/app/controllers/bible270/admin_controller.rb +1 -3
- data/app/controllers/bible270/application_controller.rb +13 -0
- data/app/controllers/bible270/readers_controller.rb +2 -0
- data/app/controllers/bible270/sessions_controller.rb +10 -0
- data/app/models/bible270/reader.rb +6 -0
- data/app/views/bible270/readers/index.html.erb +6 -1
- data/app/views/bible270/shared/_interaction_ui.html.erb +68 -2
- data/app/views/bible270/shared/_styles.html.erb +2 -0
- data/config/routes.rb +1 -0
- data/lib/bible270/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7014361816b071aaacc54d569f247ff116cc2c5c0f49ea4490e46a2e62375f4
|
|
4
|
+
data.tar.gz: d61942b56c9409563d03e8b76e2f3dea0ba6a73d0119e4d0dade38bdcb177ec2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee0357bfb87e84ab59b359ba11a274c380834587008263b7229e3d8f3edd04c02a1211045ebabec90545c4384c0404d872913c1caa2461609ec09aa4339bce88
|
|
7
|
+
data.tar.gz: 2e413b639bdccfbc194a44a7e30051d5c304ed2205625c3b3c79749da20f463178c538b6b9db43233843d783e2bfb91fce2e43121f55b7ff798a55798564a1a7
|
data/CHANGELOG.md
CHANGED
|
@@ -6,13 +6,36 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
|
|
|
6
6
|
|
|
7
7
|
### Changed
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- show administrators a quiet, linked completed-day count on Community reader rows while keeping progress hidden—and its aggregate query skipped—for ordinary readers and visitors; the link stays unadorned at rest and uses the site’s gold hover and keyboard-focus treatment
|
|
10
|
+
|
|
11
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.1.5...main)**
|
|
12
|
+
|
|
13
|
+
## [1.1.5] - 2026-08-25
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- keep Bible270 open while using one Scripture tab for Bible Gateway and Blue Letter Bible: desktop browsers reuse the existing tab, while iOS Safari replaces its background tab so each click reliably switches to the reading
|
|
18
|
+
- centralize daily-reading and Reflections Scripture anchors in one passage-link helper with secure no-JavaScript fallback behavior
|
|
10
19
|
|
|
11
20
|
### Removed
|
|
12
21
|
|
|
13
22
|
- remove the unused `passage_url_builder` configuration hook; Bible Gateway and Blue Letter Bible are now the intentionally fixed passage providers
|
|
14
23
|
|
|
15
|
-
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.1.
|
|
24
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.1.4...v1.1.5)**
|
|
25
|
+
|
|
26
|
+
## [1.1.4] - 2026-08-24
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- let administrators persistently override the current run start date, see which readers are affected, and reset to the configured date without changing check-offs or reflections
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- link reader avatars in Admin to the same reader-management pages as their names
|
|
35
|
+
- show a signed-in reader's effective plan start date beside Home progress without repeating it on My Progress
|
|
36
|
+
- increase the default parallel test workers to six while retaining `PARALLEL_WORKERS` for constrained systems
|
|
37
|
+
|
|
38
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.1.3...v1.1.4)**
|
|
16
39
|
|
|
17
40
|
## [1.1.3] - 2026-08-23
|
|
18
41
|
|
|
@@ -529,6 +552,9 @@ This is the initial public release.
|
|
|
529
552
|
|
|
530
553
|
* 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.
|
|
531
554
|
|
|
555
|
+
[1.1.6]: https://github.com/avonderluft/bible270/releases/tag/v1.1.6
|
|
556
|
+
[1.1.5]: https://github.com/avonderluft/bible270/releases/tag/v1.1.5
|
|
557
|
+
[1.1.4]: https://github.com/avonderluft/bible270/releases/tag/v1.1.4
|
|
532
558
|
[1.1.3]: https://github.com/avonderluft/bible270/releases/tag/v1.1.3
|
|
533
559
|
[1.1.2]: https://github.com/avonderluft/bible270/releases/tag/v1.1.2
|
|
534
560
|
[1.1.1]: https://github.com/avonderluft/bible270/releases/tag/v1.1.1
|
data/README.md
CHANGED
|
@@ -6,6 +6,8 @@ A mountable **Rails engine** that drops a 270-day (9 month) interactive Bible re
|
|
|
6
6
|
|
|
7
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 working on a 9 month schedule.
|
|
8
8
|
|
|
9
|
+
For a guided tour of the implementation, see the [architecture walkthrough](docs/architecture.md).
|
|
10
|
+
|
|
9
11
|
## The plan
|
|
10
12
|
|
|
11
13
|
Three readings a day, sized by **verse count** so each day takes roughly the same time.
|
|
@@ -62,9 +62,7 @@ module Bible270
|
|
|
62
62
|
@shared_calendar_readers = @readers.size
|
|
63
63
|
@personal_calendar_readers = 0
|
|
64
64
|
end
|
|
65
|
-
|
|
66
|
-
@days_completed = Hash.new(0)
|
|
67
|
-
counts.each { |(rid, day), n| @days_completed[rid] += 1 if n >= Plan.total_parts(day) }
|
|
65
|
+
@days_completed = Reader.completed_days_by_id
|
|
68
66
|
@reachable = Reader.where.not(email: [nil, '']).count
|
|
69
67
|
@last_broadcast_at = parsed_time(Setting.read(LAST_BROADCAST_AT))
|
|
70
68
|
@last_broadcast_subject = Setting.read(LAST_BROADCAST_SUBJECT)
|
|
@@ -26,6 +26,8 @@ module Bible270
|
|
|
26
26
|
|
|
27
27
|
layout :bible270_layout
|
|
28
28
|
|
|
29
|
+
rescue_from ActionController::InvalidAuthenticityToken, with: :recover_from_stale_page
|
|
30
|
+
|
|
29
31
|
helper_method :current_reader, :signed_in?, :b270_config, :enrollment_closed?
|
|
30
32
|
|
|
31
33
|
private
|
|
@@ -101,6 +103,17 @@ module Bible270
|
|
|
101
103
|
current_reader.present?
|
|
102
104
|
end
|
|
103
105
|
|
|
106
|
+
# A suspended mobile tab can outlive its Rails session cookie. The page still
|
|
107
|
+
# contains the old session's CSRF token, so Rails rejects its next form before
|
|
108
|
+
# current_reader has a chance to restore the session from the remember cookie.
|
|
109
|
+
# Never execute that unverified request; return to a fresh page for a safe retry.
|
|
110
|
+
def recover_from_stale_page(error)
|
|
111
|
+
Rails.logger.info("[bible270] refreshing a page after #{error.class}")
|
|
112
|
+
redirect_back fallback_location: root_path, allow_other_host: false,
|
|
113
|
+
alert: 'This page had been open for a while, so it was refreshed. Please try again.',
|
|
114
|
+
status: :see_other
|
|
115
|
+
end
|
|
116
|
+
|
|
104
117
|
# Guard participation (checking off / commenting). Viewing is always open.
|
|
105
118
|
def require_reader!
|
|
106
119
|
return true if signed_in?
|
|
@@ -6,6 +6,8 @@ module Bible270
|
|
|
6
6
|
# Community is a directory of fellow readers, not a leaderboard. Keep the
|
|
7
7
|
# order predictable without implying that progress determines standing.
|
|
8
8
|
@readers = Reader.all.to_a.sort_by(&:sort_name)
|
|
9
|
+
@show_admin_progress = Bible270.config.admin?(current_reader)
|
|
10
|
+
@days_completed = Reader.completed_days_by_id if @show_admin_progress
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
# The reader's own progress. Deliberately open to visitors: the panel shows an
|
|
@@ -17,6 +17,16 @@ module Bible270
|
|
|
17
17
|
@origin = safe_origin(params[:origin])
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# Refresh a resumed page's session and CSRF token without reloading its content.
|
|
21
|
+
# Calling current_reader first lets the long-lived remember cookie rebuild a
|
|
22
|
+
# browser session that the phone may have discarded while the tab was asleep.
|
|
23
|
+
def refresh
|
|
24
|
+
current_reader
|
|
25
|
+
response.headers['X-CSRF-Token'] = form_authenticity_token
|
|
26
|
+
response.headers['Cache-Control'] = 'no-store'
|
|
27
|
+
head :no_content
|
|
28
|
+
end
|
|
29
|
+
|
|
20
30
|
def create
|
|
21
31
|
auth = request.env['omniauth.auth']
|
|
22
32
|
redirect_to(sign_in_path, alert: "Sign in didn't complete. Please try again.") and return unless auth
|
|
@@ -55,6 +55,12 @@ module Bible270
|
|
|
55
55
|
value.to_s.match?(DAILY_REMINDER_TIME_FORMAT)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
def self.completed_days_by_id
|
|
59
|
+
Checkoff.group(:reader_id, :day).count.each_with_object(Hash.new(0)) do |((reader_id, day), done), totals|
|
|
60
|
+
totals[reader_id] += 1 if done >= Plan.total_parts(day)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
58
64
|
def self.reflections_seen_column?
|
|
59
65
|
column_names.include?(REFLECTIONS_SEEN_COLUMN)
|
|
60
66
|
rescue ActiveRecord::StatementInvalid
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
<%= link_to b270_avatar(r, size: 72), reader_path(r), "aria-label": r.display_name %>
|
|
11
11
|
<div>
|
|
12
12
|
<div class="name"><%= link_to r.display_name, reader_path(r), style: "text-decoration:none" %></div>
|
|
13
|
-
<div class="sub">
|
|
13
|
+
<div class="sub">
|
|
14
|
+
joined <%= Bible270.local_time(r.created_at).strftime("%b %-d %Y") %><% if @show_admin_progress %>
|
|
15
|
+
· <%= link_to "#{@days_completed[r.id]} of #{Bible270::Plan::DAYS} days complete",
|
|
16
|
+
admin_reader_path(r), class: "b270-admin-progress" %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
14
19
|
</div>
|
|
15
20
|
</div>
|
|
16
21
|
<% end %>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
<script nonce="<%= content_security_policy_nonce %>">
|
|
1
|
+
<script nonce="<%= content_security_policy_nonce %>" data-refresh-path="<%= refresh_session_path %>">
|
|
2
2
|
(() => {
|
|
3
|
+
const refreshSessionPath = document.currentScript.dataset.refreshPath;
|
|
3
4
|
if (window.Bible270InteractionUI) {
|
|
4
5
|
window.Bible270InteractionUI.reset();
|
|
5
6
|
return;
|
|
@@ -30,6 +31,42 @@
|
|
|
30
31
|
document.querySelectorAll("form[data-b270-submit='true']").forEach(resetForm);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
const SESSION_REFRESH_AFTER = 5 * 60 * 1000;
|
|
35
|
+
let sessionRefreshedAt = Date.now();
|
|
36
|
+
let sessionRefresh = null;
|
|
37
|
+
let hiddenAt = null;
|
|
38
|
+
|
|
39
|
+
function updateAuthenticityToken(token) {
|
|
40
|
+
const meta = document.querySelector("meta[name='csrf-token']");
|
|
41
|
+
if (meta) meta.content = token;
|
|
42
|
+
document.querySelectorAll("input[name='authenticity_token']").forEach((input) => {
|
|
43
|
+
input.value = token;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function refreshSession() {
|
|
48
|
+
if (sessionRefresh) return sessionRefresh;
|
|
49
|
+
|
|
50
|
+
sessionRefresh = fetch(refreshSessionPath, {
|
|
51
|
+
credentials: "same-origin",
|
|
52
|
+
cache: "no-store",
|
|
53
|
+
headers: { Accept: "text/plain" }
|
|
54
|
+
}).then((response) => {
|
|
55
|
+
const token = response.headers.get("X-CSRF-Token");
|
|
56
|
+
if (!response.ok || !token) throw new Error("Could not refresh the session");
|
|
57
|
+
|
|
58
|
+
updateAuthenticityToken(token);
|
|
59
|
+
sessionRefreshedAt = Date.now();
|
|
60
|
+
}).finally(() => {
|
|
61
|
+
sessionRefresh = null;
|
|
62
|
+
});
|
|
63
|
+
return sessionRefresh;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sessionRefreshNeeded() {
|
|
67
|
+
return Date.now() - sessionRefreshedAt >= SESSION_REFRESH_AFTER;
|
|
68
|
+
}
|
|
69
|
+
|
|
33
70
|
let scriptureWindow = null;
|
|
34
71
|
const replacesBackgroundTabs = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
|
35
72
|
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
|
@@ -62,6 +99,24 @@
|
|
|
62
99
|
document.addEventListener("submit", (event) => {
|
|
63
100
|
const form = event.target.closest("form[data-b270-submit='true']");
|
|
64
101
|
if (!form) return;
|
|
102
|
+
|
|
103
|
+
if (sessionRefresh || sessionRefreshNeeded()) {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
if (form.dataset.b270AwaitingSession === "true") return;
|
|
106
|
+
|
|
107
|
+
form.dataset.b270AwaitingSession = "true";
|
|
108
|
+
const submitter = event.submitter;
|
|
109
|
+
refreshSession().then(() => {
|
|
110
|
+
delete form.dataset.b270AwaitingSession;
|
|
111
|
+
form.requestSubmit(submitter || undefined);
|
|
112
|
+
}).catch(() => {
|
|
113
|
+
delete form.dataset.b270AwaitingSession;
|
|
114
|
+
sessionRefreshedAt = Date.now();
|
|
115
|
+
form.requestSubmit(submitter || undefined);
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
65
120
|
if (form.dataset.b270Submitting === "true") {
|
|
66
121
|
event.preventDefault();
|
|
67
122
|
return;
|
|
@@ -89,7 +144,18 @@
|
|
|
89
144
|
}
|
|
90
145
|
});
|
|
91
146
|
|
|
92
|
-
window.addEventListener("pageshow",
|
|
147
|
+
window.addEventListener("pageshow", (event) => {
|
|
148
|
+
reset();
|
|
149
|
+
if (event.persisted) refreshSession().catch(() => {});
|
|
150
|
+
});
|
|
151
|
+
document.addEventListener("visibilitychange", () => {
|
|
152
|
+
if (document.visibilityState === "hidden") {
|
|
153
|
+
hiddenAt = Date.now();
|
|
154
|
+
} else if (hiddenAt && Date.now() - hiddenAt >= SESSION_REFRESH_AFTER) {
|
|
155
|
+
refreshSession().catch(() => {});
|
|
156
|
+
hiddenAt = null;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
93
159
|
document.addEventListener("turbo:load", reset);
|
|
94
160
|
|
|
95
161
|
window.Bible270InteractionUI = { reset };
|
|
@@ -259,6 +259,8 @@
|
|
|
259
259
|
.b270-li+.b270-li{border-top:1px solid var(--line-soft)}
|
|
260
260
|
.b270-li .name{font-weight:600}
|
|
261
261
|
.b270-li .sub{font-size:12.5px;color:var(--faint)}
|
|
262
|
+
.b270-li .b270-admin-progress{text-decoration:none}
|
|
263
|
+
.b270-li .b270-admin-progress:hover,.b270-li .b270-admin-progress:focus-visible{color:var(--gold)}
|
|
262
264
|
.b270-li .stat{margin-left:auto;text-align:right}
|
|
263
265
|
.b270-li .stat .big{font-family:"Spectral",serif;font-size:20px;font-weight:600}
|
|
264
266
|
|
data/config/routes.rb
CHANGED
|
@@ -50,6 +50,7 @@ Bible270::Engine.routes.draw do
|
|
|
50
50
|
# Built-in OmniAuth sign-in. The request phase (POST <mount>/auth/:provider)
|
|
51
51
|
# is handled by the OmniAuth middleware in the host app, not by these routes.
|
|
52
52
|
get 'sign_in', to: 'sessions#new', as: :sign_in
|
|
53
|
+
get 'session/refresh', to: 'sessions#refresh', as: :refresh_session
|
|
53
54
|
|
|
54
55
|
# Email (magic link) sign-in, for readers without a social account.
|
|
55
56
|
post 'sign_in/email', to: 'sessions#email_link', as: :email_sign_in_link
|
data/lib/bible270/version.rb
CHANGED