coldwire-rails 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -8
- data/README.md +1 -1
- data/VERSION +1 -1
- data/app/assets/javascripts/coldwire/cache_controller.js +78 -46
- data/app/controllers/coldwire/caches_controller.rb +1 -1
- data/app/views/coldwire/caches/show.html.erb +129 -80
- data/app/views/coldwire/service_worker/show.js.erb +11 -0
- data/docs/configuration.md +62 -10
- data/docs/how-it-works.md +21 -0
- data/docs/setup.md +5 -3
- data/lib/coldwire/debug.css +37 -7
- data/lib/coldwire/precache.rb +56 -0
- data/lib/coldwire/worker/collect.js +71 -5
- data/lib/coldwire/worker/events.js +7 -0
- data/lib/coldwire/worker/inspect.js +107 -8
- data/lib/coldwire/worker/rules.js +5 -1
- data/lib/coldwire/worker/serve.js +132 -13
- data/lib/coldwire/worker/sync.js +37 -17
- data/lib/coldwire.rb +1 -0
- data/lib/generators/coldwire/install/templates/coldwire.rb +2 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b29177b5dd7843a32faa8d813d3e69cbac5e17c080740944203d0b441fd9fd2
|
|
4
|
+
data.tar.gz: 5a0623dd26cce626b03e899aacba3cf4514423dd204609d7a6be214b619bc78c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c97aaf5882d7203931d463a8f9f0bb825bea50c5b8ddd5871db33411587359d1caf943c51d19f8a7629ac152b5310d466dc9504e078e9e7d47212202dbe796b7
|
|
7
|
+
data.tar.gz: 5be03d9e50e9cb0f85e7ae4a4233a65a249b709f4c9eb994a2c2b699fdb682a8d4350e087d1674be978b6ec22ecbd500c2e3a634b88eda91ddcc9c49168c011b
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.5.0]
|
|
6
|
+
|
|
7
|
+
- **A Turbo Frame is cached apart from its page.** A frame navigation used to overwrite the
|
|
8
|
+
page at the same URL, so a later visit got a fragment as a whole document — a blank screen,
|
|
9
|
+
and in Hotwire Native no `window.Turbo`. Frame requests now take their own entry first and
|
|
10
|
+
the page second, the way Turbo does online.
|
|
11
|
+
- **Every `respond_to` format is cached apart from the page.** One URL answering HTML, JSON,
|
|
12
|
+
CSV, or RSS no longer shares a single slot. A path that already names its format
|
|
13
|
+
(`/report.json`, `/app.css`) is left alone. A request asking for data gets data or nothing,
|
|
14
|
+
never the page.
|
|
15
|
+
- **The precache manifest can name a frame or a format.** A listing may be a Hash:
|
|
16
|
+
`{ url: feature_path(f), frame: "map_feature_popup" }` or `{ url: report_path(r), format: :json }`.
|
|
17
|
+
`accept:` takes a media type outright. A bare URL is still the page.
|
|
18
|
+
- **A Turbo Stream is never stored.** It is a list of mutations, not a page, and replaying a
|
|
19
|
+
stale one would apply yesterday's changes to today's DOM.
|
|
20
|
+
|
|
21
|
+
## [0.4.0]
|
|
22
|
+
|
|
23
|
+
- Updated the offline page to restructure storage and downloads.
|
|
24
|
+
- Updated the storage so when you change storage size it triggers a garbage collection.
|
|
25
|
+
|
|
5
26
|
## [0.3.0]
|
|
6
27
|
|
|
7
28
|
- **`cache_origins` is `cacheable_hosts`**, and takes bare hosts: `"tiles.example.com"` rather
|
|
@@ -25,14 +46,8 @@ First release. The API may still change before 1.0.
|
|
|
25
46
|
with every option and its default, registers the Stimulus controller, and tags the
|
|
26
47
|
layout. Safe to run twice.
|
|
27
48
|
- **Garbage collection.** `config.garbage_collection` sweeps entries nothing has used in
|
|
28
|
-
`max_age` (
|
|
29
|
-
|
|
30
|
-
browse does not fill forever, on a device that revisits nothing as much as on one that
|
|
31
|
-
revisits everything. The ceiling is offered as a ladder of sizes on the offline settings
|
|
32
|
-
page and remembered per device, since how much of a phone to spend is not something an app
|
|
33
|
-
can know. It measures only what a sweep may take: downloaded archives are an opt-in spend
|
|
34
|
-
of somebody's data plan, so they are neither counted nor evicted.
|
|
35
|
-
On by default, unlike syncing: it spends no data. A sweep runs only with a
|
|
49
|
+
`max_age` (30 days by default), so a cache that fills as people browse does not fill
|
|
50
|
+
forever. On by default, unlike syncing: it spends no data. A sweep runs only with a
|
|
36
51
|
connection it has confirmed by pinging `probe_path`, because deleting is the one cache
|
|
37
52
|
operation with no way back. Age is measured from last use, not from when an entry was
|
|
38
53
|
fetched — storing a page renews everything it names, so the stylesheet every page loads
|
data/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Works the same in a plain web app, an installed PWA, or Hotwire Native.
|
|
|
25
25
|
|
|
26
26
|
## What you get
|
|
27
27
|
|
|
28
|
-
- **[Setup in 30 seconds](docs/setup.md):**
|
|
28
|
+
- **[Setup in 30 seconds](docs/setup.md):** Install gem, then `bin/rails coldwire:install`. The rest has working defaults.
|
|
29
29
|
- **[Cache as you go](docs/configuration.md#cache_as_you_go):** Defaults to all pages get cached as your navigate, customize to only cache certain parts of the app.
|
|
30
30
|
- **[Auto Sycn precaching](docs/configuration.md#auto_sync):** Define urls that can be pre-cached automatically in the background.
|
|
31
31
|
- **[Garbage collection](docs/configuration.md#garbage_collection):** Entries nothing has used in two months are swept, and anything over the storage ceiling goes least recently read first — so the cache doesn't grow forever. Only with a connection; anything a page still loads is renewed on the way past. People can set the ceiling for their own device on `/offline`.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.5.0
|
|
@@ -43,6 +43,7 @@ export default class extends Controller {
|
|
|
43
43
|
"autoSync",
|
|
44
44
|
"syncedAt",
|
|
45
45
|
"syncStatus",
|
|
46
|
+
"syncDetails",
|
|
46
47
|
"syncButton",
|
|
47
48
|
"syncLabel",
|
|
48
49
|
"search",
|
|
@@ -129,6 +130,7 @@ export default class extends Controller {
|
|
|
129
130
|
|
|
130
131
|
if (data.state === "started") {
|
|
131
132
|
this.syncRunning = true
|
|
133
|
+
this.revealSync()
|
|
132
134
|
const retired = data.retired ? `, retired ${data.retired}` : ""
|
|
133
135
|
this.setSyncStatus(data.pending
|
|
134
136
|
? `Syncing ${data.pending} file${data.pending === 1 ? "" : "s"}${retired}…`
|
|
@@ -143,6 +145,7 @@ export default class extends Controller {
|
|
|
143
145
|
// A page that opened mid-run arrives here without ever having seen "started", so this
|
|
144
146
|
// branch has to be able to put the page into the running state on its own.
|
|
145
147
|
this.syncRunning = true
|
|
148
|
+
this.revealSync()
|
|
146
149
|
this.toggleSyncing(true)
|
|
147
150
|
if (this.hasProgressTarget) this.progressTarget.hidden = false
|
|
148
151
|
// The bar carries the live count; the line above stays on the high-level "what".
|
|
@@ -214,6 +217,7 @@ export default class extends Controller {
|
|
|
214
217
|
async syncNow(event) {
|
|
215
218
|
event?.preventDefault()
|
|
216
219
|
this.syncSettled = false
|
|
220
|
+
this.revealSync()
|
|
217
221
|
// Reaching the worker takes a moment, and the ticker keeps ticking while it does.
|
|
218
222
|
this.syncStarting = true
|
|
219
223
|
this.toggleBusy(true)
|
|
@@ -243,6 +247,13 @@ export default class extends Controller {
|
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
249
|
|
|
250
|
+
// Everything a sync has to say lives behind the disclosure, so a run has to open it — a bar
|
|
251
|
+
// filling where nobody can see it is the same as no bar. Left open afterwards: somebody who
|
|
252
|
+
// has just watched a sync is the one person who might want the detail.
|
|
253
|
+
revealSync() {
|
|
254
|
+
if (this.hasSyncDetailsTarget) this.syncDetailsTarget.open = true
|
|
255
|
+
}
|
|
256
|
+
|
|
246
257
|
setSyncStatus(text) {
|
|
247
258
|
if (!this.hasSyncStatusTarget) return
|
|
248
259
|
|
|
@@ -250,24 +261,24 @@ export default class extends Controller {
|
|
|
250
261
|
this.syncStatusTarget.hidden = !text
|
|
251
262
|
}
|
|
252
263
|
|
|
264
|
+
// Inside the details, under the switch that says whether syncing is on at all. This says
|
|
265
|
+
// how often, and when it is off, that the button below still works.
|
|
253
266
|
renderAutoSync() {
|
|
254
267
|
if (!this.hasAutoSyncTarget) return
|
|
255
268
|
|
|
256
|
-
// The switch beside this already says whether it is on, so this line carries the one
|
|
257
|
-
// thing the switch cannot: how often.
|
|
258
269
|
if (!this.autoSyncValue) {
|
|
259
|
-
this.autoSyncTarget.textContent = "
|
|
270
|
+
this.autoSyncTarget.textContent = "Runs only when you press Sync now."
|
|
260
271
|
return
|
|
261
272
|
}
|
|
262
273
|
|
|
263
274
|
if (!this.autoSyncOn()) {
|
|
264
|
-
this.autoSyncTarget.textContent = "Off for this device"
|
|
275
|
+
this.autoSyncTarget.textContent = "Off for this device. Sync now still runs a pass."
|
|
265
276
|
return
|
|
266
277
|
}
|
|
267
278
|
|
|
268
279
|
this.autoSyncTarget.textContent = this.syncIntervalValue > 0
|
|
269
|
-
? `Syncs every ${formatInterval(this.syncIntervalValue)}
|
|
270
|
-
: "On"
|
|
280
|
+
? `Syncs every ${formatInterval(this.syncIntervalValue)}.`
|
|
281
|
+
: "On."
|
|
271
282
|
}
|
|
272
283
|
|
|
273
284
|
renderSyncedAt() {
|
|
@@ -428,6 +439,7 @@ export default class extends Controller {
|
|
|
428
439
|
// What this page can answer by itself comes first: the cache is read directly and the
|
|
429
440
|
// probe is one request. Behind the worker questions they waited out a registration that
|
|
430
441
|
// may never arrive, and the page sat on "Checking…" with an empty list.
|
|
442
|
+
await this.loadSpared()
|
|
431
443
|
await this.renderCache()
|
|
432
444
|
await this.renderConnection()
|
|
433
445
|
|
|
@@ -835,73 +847,93 @@ export default class extends Controller {
|
|
|
835
847
|
select.disabled = true
|
|
836
848
|
|
|
837
849
|
try {
|
|
838
|
-
// A
|
|
839
|
-
//
|
|
840
|
-
// connection
|
|
841
|
-
await this.
|
|
850
|
+
// A ceiling that waits for the next scheduled sweep reads as a setting that did nothing,
|
|
851
|
+
// so it is applied here and now. Deliberate, so it does not stand down for want of a
|
|
852
|
+
// connection the way an automatic sweep does.
|
|
853
|
+
await this.applyCeiling()
|
|
842
854
|
} finally {
|
|
843
855
|
select.disabled = false
|
|
844
856
|
}
|
|
845
857
|
}
|
|
846
858
|
|
|
847
|
-
async
|
|
848
|
-
|
|
859
|
+
async applyCeiling() {
|
|
860
|
+
// A trim over a thousand entries is a second or two, and a silent pause reads as a
|
|
861
|
+
// setting that did nothing.
|
|
862
|
+
if (this.hasUsageLabelTarget) this.usageLabelTarget.textContent = "Applying…"
|
|
849
863
|
|
|
850
864
|
try {
|
|
851
|
-
|
|
852
|
-
} catch {
|
|
853
|
-
//
|
|
854
|
-
//
|
|
865
|
+
await sendToWorker("trim", { maxSize: this.store.maxSize() }, 60000)
|
|
866
|
+
} catch (error) {
|
|
867
|
+
// Say so. Swallowing this is what turned a stale worker — one registered before the app
|
|
868
|
+
// knew what a ceiling was, and still controlling this page until it is reloaded — into
|
|
869
|
+
// a setting that silently refused to bite.
|
|
870
|
+
if (this.hasUsageLabelTarget) {
|
|
871
|
+
this.usageLabelTarget.textContent = `Could not apply it: ${error.message}. Reload and try again.`
|
|
872
|
+
}
|
|
855
873
|
return
|
|
856
874
|
}
|
|
857
875
|
|
|
858
|
-
|
|
859
|
-
// sweep still due — exactly as the head snippet treats a refused run.
|
|
860
|
-
this.sweepWaiting = Boolean(result?.offline)
|
|
861
|
-
if (!this.sweepWaiting) this.store.set(this.store.keys.collectedAt, Date.now())
|
|
862
|
-
|
|
876
|
+
await this.loadSpared()
|
|
863
877
|
await this.renderCache()
|
|
864
878
|
}
|
|
865
879
|
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
|
|
880
|
+
// The worker is the only thing that knows what the offline page needs, and those entries are
|
|
881
|
+
// never collected. Read once and kept: it does not change while this page is open.
|
|
882
|
+
async loadSpared() {
|
|
883
|
+
try {
|
|
884
|
+
const result = await sendToWorker("spared", {}, 5000)
|
|
885
|
+
this.spared = new Set(result?.urls || [])
|
|
886
|
+
} catch {
|
|
887
|
+
// An older worker, or none yet. Falling back to counting everything overstates what a
|
|
888
|
+
// sweep can take, which is the safer direction for a figure somebody sets a limit from.
|
|
889
|
+
this.spared = this.spared || new Set()
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// What the ceiling is measured against, which has to be what a sweep can actually take, or
|
|
894
|
+
// the bar shows an overage nothing will ever bring down. Three exclusions:
|
|
895
|
+
//
|
|
896
|
+
// Downloads, an opt-in spend of somebody's data plan, are never collected however full the
|
|
897
|
+
// cache gets — a 300 MB archive counted here would pin the bar full of untouchable files.
|
|
898
|
+
// Their sizes are on their own rows, which is where somebody reclaims that space.
|
|
899
|
+
//
|
|
900
|
+
// The offline page's own assets, which the worker spares and names for us: working them out
|
|
901
|
+
// means parsing the offline page, and only the worker holds it.
|
|
869
902
|
//
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
// right way round for a number somebody is deciding a limit from.
|
|
903
|
+
// Any cache but the worker's own. Bumping cache_name leaves the old one behind, and the
|
|
904
|
+
// worker only ever sweeps the one it is configured with.
|
|
873
905
|
managedBytes(entries) {
|
|
874
|
-
|
|
906
|
+
const swept = window.COLDWIRE?.cacheName
|
|
907
|
+
const spared = this.spared
|
|
908
|
+
|
|
909
|
+
return this.totalBytes((entries || []).filter((entry) =>
|
|
910
|
+
!entry.download &&
|
|
911
|
+
!(spared && spared.has(entry.url)) &&
|
|
912
|
+
(!swept || !entry.cache || entry.cache === swept)))
|
|
875
913
|
}
|
|
876
914
|
|
|
877
915
|
renderStorage() {
|
|
878
916
|
if (!this.hasUsageLabelTarget) return
|
|
879
917
|
|
|
880
918
|
const limit = this.store.maxSize()
|
|
881
|
-
const
|
|
919
|
+
const managed = this.managedBytes(this.entries)
|
|
882
920
|
|
|
883
921
|
if (this.hasUsageTarget) this.usageTarget.hidden = limit === null
|
|
884
922
|
|
|
885
923
|
if (limit === null) {
|
|
886
|
-
this.usageLabelTarget.textContent = `${formatBytes(
|
|
887
|
-
|
|
888
|
-
|
|
924
|
+
this.usageLabelTarget.textContent = `${formatBytes(managed)} of cached pages, with no limit set.`
|
|
925
|
+
} else {
|
|
926
|
+
const percent = Math.round(Math.min(managed / limit, 1) * 100)
|
|
927
|
+
if (this.hasUsageBarTarget) this.usageBarTarget.style.width = `${percent}%`
|
|
928
|
+
if (this.hasUsageTarget) this.usageTarget.setAttribute("aria-valuenow", String(percent))
|
|
889
929
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const parts = [ `${formatBytes(used)} of ${formatLimit(limit)}` ]
|
|
895
|
-
if (used > limit) {
|
|
896
|
-
// Over the ceiling is not a fault and not a promise of instant deletion: a sweep needs
|
|
897
|
-
// a connection, and says so rather than leaving somebody watching a bar that will not
|
|
898
|
-
// move.
|
|
899
|
-
parts.push(this.sweepWaiting || this.online === false
|
|
900
|
-
? "over, waiting for a connection to sweep"
|
|
901
|
-
: "over, the oldest go on the next sweep")
|
|
902
|
-
}
|
|
930
|
+
const parts = [ `${formatBytes(managed)} of ${formatLimit(limit)} used by cached pages` ]
|
|
931
|
+
// Choosing a ceiling applies it on the spot, so this is what is left between sweeps: a
|
|
932
|
+
// cache that has grown past a ceiling nobody has touched since. The next sweep takes it.
|
|
933
|
+
if (managed > limit) parts.push("over, the oldest go on the next sweep")
|
|
903
934
|
|
|
904
|
-
|
|
935
|
+
this.usageLabelTarget.textContent = `${parts.join(" · ")}.`
|
|
936
|
+
}
|
|
905
937
|
}
|
|
906
938
|
|
|
907
939
|
renderEntries() {
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
<span>
|
|
82
82
|
<span class="coldwire-setting-title">Offline support</span>
|
|
83
83
|
<span class="coldwire-setting-note">
|
|
84
|
-
|
|
84
|
+
Enable page caching so they work without a connection.
|
|
85
85
|
</span>
|
|
86
86
|
</span>
|
|
87
87
|
</label>
|
|
@@ -102,11 +102,84 @@
|
|
|
102
102
|
</span>
|
|
103
103
|
</span>
|
|
104
104
|
</label>
|
|
105
|
+
|
|
106
|
+
<%# Syncing sits with the other two switches rather than in a card of its own: all three
|
|
107
|
+
are the same kind of thing, a choice this device makes about how Coldwire behaves. %>
|
|
108
|
+
<hr>
|
|
109
|
+
|
|
110
|
+
<% if Coldwire.config.auto_sync.enabled %>
|
|
111
|
+
<%# Only where the app configured automatic syncing at all: a switch for something that
|
|
112
|
+
was never going to happen is a switch that lies. %>
|
|
113
|
+
<label class="coldwire-setting">
|
|
114
|
+
<span class="coldwire-switch coldwire-switch--on">
|
|
115
|
+
<input type="checkbox"
|
|
116
|
+
data-coldwire-cache-target="autoSyncToggle"
|
|
117
|
+
data-action="change->coldwire-cache#toggleAutoSync">
|
|
118
|
+
<span aria-hidden="true"></span>
|
|
119
|
+
</span>
|
|
120
|
+
<span>
|
|
121
|
+
<span class="coldwire-setting-title">Sync on its own</span>
|
|
122
|
+
<span class="coldwire-setting-note">
|
|
123
|
+
Automatically back up pages in the background.
|
|
124
|
+
</span>
|
|
125
|
+
</span>
|
|
126
|
+
</label>
|
|
127
|
+
<% else %>
|
|
128
|
+
<p class="coldwire-facts" style="margin-top: 0;">
|
|
129
|
+
Automatically back up pages in the background.
|
|
130
|
+
<strong>Sync now</strong> runs a pass immediately.
|
|
131
|
+
</p>
|
|
132
|
+
<% end %>
|
|
133
|
+
|
|
134
|
+
<%# The clock, the button and the bar behind a disclosure. They are worth having, and
|
|
135
|
+
worth going to: under a switch you are not using they are three lines of furniture
|
|
136
|
+
on every visit. It opens itself while a sync runs, so progress is never hidden
|
|
137
|
+
behind a tap nobody knew to make. %>
|
|
138
|
+
<details class="coldwire-sync" data-coldwire-cache-target="syncDetails">
|
|
139
|
+
<summary><span>Sync details</span></summary>
|
|
140
|
+
|
|
141
|
+
<div class="coldwire-sync-body">
|
|
142
|
+
<%# How often, which the switch cannot show and the note above no longer says. %>
|
|
143
|
+
<p class="coldwire-facts" data-coldwire-cache-target="autoSync">Checking…</p>
|
|
144
|
+
<p class="coldwire-facts" data-coldwire-cache-target="syncedAt">Never synced</p>
|
|
145
|
+
|
|
146
|
+
<%# Empty until a sync has something to say, so the card does not carry a permanent
|
|
147
|
+
"Idle" that reads like debug output left in by accident. %>
|
|
148
|
+
<p class="coldwire-facts" data-coldwire-cache-target="syncStatus" hidden></p>
|
|
149
|
+
|
|
150
|
+
<div class="coldwire-actions coldwire-actions--spaced">
|
|
151
|
+
<button type="button"
|
|
152
|
+
data-coldwire-cache-target="syncButton"
|
|
153
|
+
class="coldwire-button"
|
|
154
|
+
data-action="click->coldwire-cache#syncNow">
|
|
155
|
+
<span class="coldwire-spinner" data-coldwire-cache-target="spinner" aria-hidden="true" hidden></span>
|
|
156
|
+
<span data-coldwire-cache-target="syncLabel">Sync now</span>
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div style="margin-top: 0.75rem;"
|
|
161
|
+
role="progressbar"
|
|
162
|
+
aria-valuemin="0"
|
|
163
|
+
aria-valuemax="100"
|
|
164
|
+
data-coldwire-cache-target="progress"
|
|
165
|
+
hidden>
|
|
166
|
+
<div class="coldwire-track">
|
|
167
|
+
<div class="coldwire-bar" data-coldwire-cache-target="progressBar"></div>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="coldwire-progress-label" data-coldwire-cache-target="progressLabel"></div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</details>
|
|
105
173
|
</div>
|
|
106
174
|
</div>
|
|
107
175
|
|
|
108
176
|
<div data-coldwire-cache-target="whenOn">
|
|
109
177
|
<% gc = Coldwire.config.garbage_collection %>
|
|
178
|
+
<% archives = Coldwire.config.cache_archives %>
|
|
179
|
+
<%# Two cards, because they answer two questions. This one is what the app keeps as you
|
|
180
|
+
browse and how much of it you will allow; the next is what you chose to keep yourself.
|
|
181
|
+
One card had the first card's figure sitting a finger's width from files it does not
|
|
182
|
+
count, which read as an error in the figure. %>
|
|
110
183
|
<% if gc.enabled %>
|
|
111
184
|
<%# What the cache is allowed to grow to on this device. The choice is remembered here the
|
|
112
185
|
way Force offline and Sync on its own are — the app sets the starting point, the person
|
|
@@ -150,94 +223,61 @@
|
|
|
150
223
|
</div>
|
|
151
224
|
|
|
152
225
|
<p class="coldwire-facts" data-coldwire-cache-target="usageLabel">Reading the cache…</p>
|
|
226
|
+
|
|
227
|
+
<% if archives.any? %>
|
|
228
|
+
<%# Said where the question comes up. Somebody reading a figure that does not match the
|
|
229
|
+
one in the header is owed the reason on the spot, not a card further down. This
|
|
230
|
+
line is about what the figure counts; what becomes of a download is the other
|
|
231
|
+
card's business, and saying it twice made both cards read as hedging. %>
|
|
232
|
+
<p class="coldwire-downloads-note">
|
|
233
|
+
Cached pages only. Downloads are counted separately, under Downloads below.
|
|
234
|
+
</p>
|
|
235
|
+
<% end %>
|
|
153
236
|
</div>
|
|
154
237
|
<% end %>
|
|
155
238
|
|
|
156
|
-
|
|
157
|
-
|
|
239
|
+
<% if archives.any? %>
|
|
240
|
+
<%# One row per configured file, each reporting its own size. The words are the app's — the
|
|
241
|
+
page knows only that these are large, optional, and worth keeping. %>
|
|
242
|
+
<div class="coldwire-card coldwire-downloads" data-coldwire-cache-target="archives">
|
|
243
|
+
<h2>Downloads</h2>
|
|
158
244
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
data-coldwire-cache-target="autoSyncToggle"
|
|
167
|
-
data-action="change->coldwire-cache#toggleAutoSync">
|
|
168
|
-
<span aria-hidden="true"></span>
|
|
169
|
-
</span>
|
|
170
|
-
<span>
|
|
171
|
-
<span class="coldwire-setting-title">Sync on its own</span>
|
|
172
|
-
<span class="coldwire-setting-note">
|
|
173
|
-
Automatically runs in the background backing up pages to be used offline.
|
|
174
|
-
</span>
|
|
175
|
-
</span>
|
|
176
|
-
</label>
|
|
177
|
-
<% else %>
|
|
178
|
-
<p class="coldwire-facts" style="margin-top: 0;">
|
|
179
|
-
Automatically runs in the background backing up pages to be used offline.
|
|
180
|
-
<strong>Sync now</strong> runs that pass immediately.
|
|
181
|
-
</p>
|
|
182
|
-
<% end %>
|
|
183
|
-
|
|
184
|
-
<p class="coldwire-facts">
|
|
185
|
-
<span data-coldwire-cache-target="autoSync">Checking…</span><br>
|
|
186
|
-
<span data-coldwire-cache-target="syncedAt">Never synced</span>
|
|
187
|
-
</p>
|
|
188
|
-
|
|
189
|
-
<%# Empty until a sync has something to say, so the card does not carry a permanent
|
|
190
|
-
"Idle" that reads like debug output left in by accident. %>
|
|
191
|
-
<p class="coldwire-facts" data-coldwire-cache-target="syncStatus" hidden></p>
|
|
192
|
-
|
|
193
|
-
<div class="coldwire-actions coldwire-actions--spaced">
|
|
194
|
-
<button type="button"
|
|
195
|
-
data-coldwire-cache-target="syncButton"
|
|
196
|
-
class="coldwire-button"
|
|
197
|
-
data-action="click->coldwire-cache#syncNow">
|
|
198
|
-
<span class="coldwire-spinner" data-coldwire-cache-target="spinner" aria-hidden="true" hidden></span>
|
|
199
|
-
<span data-coldwire-cache-target="syncLabel">Sync now</span>
|
|
200
|
-
</button>
|
|
201
|
-
</div>
|
|
245
|
+
<%# The name and what you can do about it on one line, the words underneath. The button
|
|
246
|
+
sits hard right, where the eye goes for the action, and what is happening reads under
|
|
247
|
+
both when a download is running. %>
|
|
248
|
+
<% archives.each do |archive| %>
|
|
249
|
+
<div class="coldwire-archive" data-archive-url="<%= archive[:url] %>">
|
|
250
|
+
<div class="coldwire-archive-head">
|
|
251
|
+
<div class="coldwire-archive-title"><%= archive[:title] %></div>
|
|
202
252
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
253
|
+
<div class="coldwire-actions coldwire-archive-actions">
|
|
254
|
+
<button type="button" class="coldwire-button"
|
|
255
|
+
data-url="<%= archive[:url] %>"
|
|
256
|
+
data-action="click->coldwire-cache#downloadArchive">
|
|
257
|
+
<span class="coldwire-spinner" data-archive-spinner aria-hidden="true" hidden></span>
|
|
258
|
+
<span data-archive-download-label>Download</span>
|
|
259
|
+
</button>
|
|
260
|
+
<%# Unlabelled but for its shape, as the trash is everywhere else on this page,
|
|
261
|
+
so the pair fits beside the title rather than under it. %>
|
|
262
|
+
<button type="button"
|
|
263
|
+
class="coldwire-button coldwire-icon-button coldwire-button--danger"
|
|
264
|
+
title="Delete this download"
|
|
265
|
+
aria-label="Delete <%= archive[:title] %>"
|
|
266
|
+
data-url="<%= archive[:url] %>"
|
|
267
|
+
data-action="click->coldwire-cache#removeArchive"
|
|
268
|
+
data-archive-remove hidden>
|
|
269
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
270
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="<%= trash_path %>" />
|
|
271
|
+
</svg>
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
215
275
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
are large, optional, and worth keeping. %>
|
|
219
|
-
<div class="coldwire-card" data-coldwire-cache-target="archives">
|
|
220
|
-
<% Coldwire.config.cache_archives.each do |archive| %>
|
|
221
|
-
<div class="coldwire-archive" data-archive-url="<%= archive[:url] %>">
|
|
222
|
-
<div class="coldwire-archive-title"><%= archive[:title] %></div>
|
|
276
|
+
<%# The words run the full width under both, rather than in a column the button has
|
|
277
|
+
squeezed to three syllables a line. %>
|
|
223
278
|
<% if archive[:description].present? %>
|
|
224
279
|
<p class="coldwire-facts"><%= archive[:description] %></p>
|
|
225
280
|
<% end %>
|
|
226
|
-
<div class="coldwire-actions">
|
|
227
|
-
<button type="button" class="coldwire-button"
|
|
228
|
-
data-url="<%= archive[:url] %>"
|
|
229
|
-
data-action="click->coldwire-cache#downloadArchive">
|
|
230
|
-
<span class="coldwire-spinner" data-archive-spinner aria-hidden="true" hidden></span>
|
|
231
|
-
<span data-archive-download-label>Download</span>
|
|
232
|
-
</button>
|
|
233
|
-
<button type="button" class="coldwire-button coldwire-button--danger"
|
|
234
|
-
data-url="<%= archive[:url] %>"
|
|
235
|
-
data-action="click->coldwire-cache#removeArchive"
|
|
236
|
-
data-archive-remove hidden>
|
|
237
|
-
Delete
|
|
238
|
-
</button>
|
|
239
|
-
</div>
|
|
240
|
-
|
|
241
281
|
<p class="coldwire-facts coldwire-archive-meta" data-archive-status>Checking…</p>
|
|
242
282
|
|
|
243
283
|
<div style="margin-top: 0.75rem;" role="progressbar" aria-valuemin="0" aria-valuemax="100"
|
|
@@ -247,6 +287,15 @@
|
|
|
247
287
|
</div>
|
|
248
288
|
</div>
|
|
249
289
|
<% end %>
|
|
290
|
+
|
|
291
|
+
<% if gc.enabled %>
|
|
292
|
+
<%# The reassurance belongs with the files it is about: a 300 MB download sitting under
|
|
293
|
+
a storage limit raises the question of whether the app is about to take it back. %>
|
|
294
|
+
<p class="coldwire-downloads-note">
|
|
295
|
+
Yours until you delete them. <strong>Keep at most</strong> never counts a download,
|
|
296
|
+
and never clears one to make room.
|
|
297
|
+
</p>
|
|
298
|
+
<% end %>
|
|
250
299
|
</div>
|
|
251
300
|
<% end %>
|
|
252
301
|
|
|
@@ -26,6 +26,17 @@ const CACHE_RANGES = compileRules(<%= raw Coldwire.cache_rules(Coldwire.config.c
|
|
|
26
26
|
// A range entry is stored under its own key, with the range in the query and the archive's
|
|
27
27
|
// total size in a header — the two things needed to rebuild a 206 that was never storable.
|
|
28
28
|
const RANGE_PARAM = "__coldwire_range"
|
|
29
|
+
// Turbo sends `Turbo-Frame` on a frame navigation, and an app that answers it with just the
|
|
30
|
+
// frame returns something that is not the page at that URL. Keyed apart, the same way, or one
|
|
31
|
+
// would answer for the other: a fragment served to a cold visit is a document with no <html>.
|
|
32
|
+
const FRAME_PARAM = "__coldwire_frame"
|
|
33
|
+
// And the same URL answers a `respond_to` block in as many formats as the app defines. A page
|
|
34
|
+
// is the default and carries no param, so every entry stored before this existed keeps the key
|
|
35
|
+
// it already had; anything else names what it is.
|
|
36
|
+
const FORMAT_PARAM = "__coldwire_format"
|
|
37
|
+
// A stream is a list of changes to make to a page, not a page. Replaying a stale one later
|
|
38
|
+
// would apply yesterday's mutations to today's DOM, so it is never stored at all.
|
|
39
|
+
const STREAM_TYPE = "text/vnd.turbo-stream.html"
|
|
29
40
|
// Archives offered for download in full, and the size of the pieces they arrive in. 8 MB is
|
|
30
41
|
// small enough that a dropped connection loses seconds rather than minutes, and large enough
|
|
31
42
|
// that a 300 MB archive is tens of requests rather than thousands.
|