fix: add vapid diagnostics
This commit is contained in:
parent
5198826f83
commit
f783794b86
@ -1,5 +1,7 @@
|
||||
// Served from / (not /static/) so its scope covers /admin/today/.
|
||||
const CACHE = "osiris-v1";
|
||||
// Bump on any change here or to the cached pages: activate() drops older caches,
|
||||
// which is what stops a stale Today page surviving a deploy.
|
||||
const CACHE = "osiris-v2";
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
self.skipWaiting();
|
||||
|
||||
@ -145,19 +145,34 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="osiris-card" id="reminders" hidden>
|
||||
<div class="osiris-card" id="reminders">
|
||||
<h2 style="margin-top:0">Reminders</h2>
|
||||
<p class="dose-detail" id="reminder-status">Checking…</p>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap">
|
||||
<button type="button" id="reminder-toggle" class="osiris-save" style="flex:1; min-width:140px"></button>
|
||||
<button type="button" id="reminder-toggle" class="osiris-save" style="flex:1; min-width:140px" hidden></button>
|
||||
<button type="button" id="reminder-test" class="osiris-save" style="flex:1; min-width:140px; background:transparent; color:inherit; border:1px solid var(--border-color)" hidden>
|
||||
Send test
|
||||
</button>
|
||||
</div>
|
||||
<p class="dose-detail" style="margin-bottom:0">
|
||||
<p class="dose-detail">
|
||||
A reminder is sent at each dose's reminder time, and only if it isn't already ticked off.
|
||||
Set those times on the medication in the admin.
|
||||
</p>
|
||||
<details class="osiris-debug">
|
||||
<summary class="dose-detail" style="cursor:pointer">Diagnostics</summary>
|
||||
<ul id="reminder-checks"></ul>
|
||||
<ul>
|
||||
<li class="dose-detail">
|
||||
{% if doses_with_reminders %}✅{% else %}❌{% endif %}
|
||||
Doses with a reminder time: {{ doses_with_reminders }} of {{ day.doses|length }}
|
||||
{% if not doses_with_reminders %}— set one on the medication in the admin, or nothing will ever fire{% endif %}
|
||||
</li>
|
||||
<li class="dose-detail">
|
||||
{% if subscribed_devices %}✅{% else %}❌{% endif %}
|
||||
Devices subscribed (this account): {{ subscribed_devices }}
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<p style="text-align:center"><a href="{% url 'admin:index' %}">Manage medication & history →</a></p>
|
||||
@ -165,19 +180,80 @@
|
||||
<script>
|
||||
(function () {
|
||||
const VAPID_PUBLIC_KEY = "{{ vapid_public_key|escapejs }}";
|
||||
const card = document.getElementById("reminders");
|
||||
const status = document.getElementById("reminder-status");
|
||||
const toggle = document.getElementById("reminder-toggle");
|
||||
const test = document.getElementById("reminder-test");
|
||||
|
||||
// No keys configured, or a browser without push: leave the card hidden entirely.
|
||||
if (!VAPID_PUBLIC_KEY || !("serviceWorker" in navigator) || !("PushManager" in window)) {
|
||||
return;
|
||||
}
|
||||
card.hidden = false;
|
||||
const checks = document.getElementById("reminder-checks");
|
||||
|
||||
const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value;
|
||||
|
||||
// Every precondition for Web Push, each one reported by name. Push fails silently
|
||||
// in a dozen ways; the point here is to say which one, rather than hide the card.
|
||||
function preconditions() {
|
||||
return [
|
||||
{
|
||||
name: "Server VAPID keys",
|
||||
ok: Boolean(VAPID_PUBLIC_KEY),
|
||||
detail: VAPID_PUBLIC_KEY
|
||||
? VAPID_PUBLIC_KEY.slice(0, 12) + "…"
|
||||
: "not set — run: manage.py generate_vapid_keys, then set VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY in the server environment and restart",
|
||||
},
|
||||
{
|
||||
name: "Secure context (HTTPS)",
|
||||
ok: window.isSecureContext,
|
||||
detail: window.isSecureContext
|
||||
? location.origin
|
||||
: location.origin + " — push needs https:// (or localhost)",
|
||||
},
|
||||
{
|
||||
name: "Service worker support",
|
||||
ok: "serviceWorker" in navigator,
|
||||
detail: "serviceWorker" in navigator ? "supported" : "not supported by this browser",
|
||||
},
|
||||
{
|
||||
name: "Push support",
|
||||
ok: "PushManager" in window,
|
||||
detail:
|
||||
"PushManager" in window
|
||||
? "supported"
|
||||
: "not supported — on Android use Chrome; Firefox in a private window will not do",
|
||||
},
|
||||
{
|
||||
name: "Notification permission",
|
||||
ok: typeof Notification !== "undefined" && Notification.permission !== "denied",
|
||||
detail:
|
||||
typeof Notification === "undefined"
|
||||
? "Notification API missing"
|
||||
: Notification.permission,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function renderChecks(extra) {
|
||||
const rows = preconditions().concat(extra || []);
|
||||
checks.innerHTML = "";
|
||||
for (const row of rows) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "dose-detail";
|
||||
li.textContent = `${row.ok ? "✅" : "❌"} ${row.name}: ${row.detail}`;
|
||||
checks.appendChild(li);
|
||||
}
|
||||
return rows.every((row) => row.ok);
|
||||
}
|
||||
|
||||
const blocking = preconditions().filter((row) => !row.ok);
|
||||
if (blocking.length) {
|
||||
// Notification.permission === "denied" is recoverable, so it is not fatal here;
|
||||
// anything else means the button would do nothing at all.
|
||||
const fatal = blocking.filter((row) => row.name !== "Notification permission");
|
||||
if (fatal.length) {
|
||||
status.textContent = `Reminders unavailable — ${fatal[0].name}: ${fatal[0].detail}`;
|
||||
renderChecks();
|
||||
document.querySelector(".osiris-debug").open = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function post(url, body) {
|
||||
return fetch(url, {
|
||||
method: "POST",
|
||||
@ -202,16 +278,37 @@
|
||||
}
|
||||
|
||||
let subscription = null;
|
||||
let worker = null;
|
||||
|
||||
function render() {
|
||||
const blocked = Notification.permission === "denied";
|
||||
if (blocked) {
|
||||
renderChecks([
|
||||
{
|
||||
name: "Service worker",
|
||||
ok: Boolean(worker),
|
||||
detail: worker ? "active at " + worker.scope : "not registered yet — reload the page",
|
||||
},
|
||||
{
|
||||
name: "This device subscribed",
|
||||
ok: Boolean(subscription),
|
||||
detail: subscription ? subscription.endpoint.slice(0, 48) + "…" : "no",
|
||||
},
|
||||
{
|
||||
name: "Installed as an app",
|
||||
ok: window.matchMedia("(display-mode: standalone)").matches,
|
||||
detail: window.matchMedia("(display-mode: standalone)").matches
|
||||
? "yes"
|
||||
: "no — works in the browser too, but Android delivers reliably once installed",
|
||||
},
|
||||
]);
|
||||
|
||||
if (Notification.permission === "denied") {
|
||||
status.textContent =
|
||||
"Notifications are blocked for this site. Re-allow them in Chrome's site settings.";
|
||||
"Notifications are blocked for this site. Re-allow them in Chrome → site settings → Notifications.";
|
||||
toggle.hidden = true;
|
||||
test.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
toggle.hidden = false;
|
||||
toggle.disabled = false;
|
||||
toggle.textContent = subscription ? "Turn reminders off" : "Turn reminders on";
|
||||
@ -222,10 +319,18 @@
|
||||
}
|
||||
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => registration.pushManager.getSubscription())
|
||||
.then((registration) => {
|
||||
worker = registration;
|
||||
return registration.pushManager.getSubscription();
|
||||
})
|
||||
.then((existing) => {
|
||||
subscription = existing;
|
||||
render();
|
||||
})
|
||||
.catch((error) => {
|
||||
status.textContent = "Service worker failed: " + error.message;
|
||||
renderChecks();
|
||||
document.querySelector(".osiris-debug").open = true;
|
||||
});
|
||||
|
||||
toggle.addEventListener("click", async () => {
|
||||
|
||||
@ -259,6 +259,34 @@ class PwaTests(TrackerTestCase):
|
||||
self.assertContains(response, 'rel="manifest"')
|
||||
self.assertContains(response, "serviceWorker.register")
|
||||
|
||||
@override_settings(VAPID_PUBLIC_KEY="", VAPID_PRIVATE_KEY="")
|
||||
def test_the_reminder_card_explains_itself_when_keys_are_missing(self):
|
||||
# It used to hide entirely, which is indistinguishable from a broken page.
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("tracker:today"))
|
||||
|
||||
self.assertContains(response, "Reminders")
|
||||
self.assertContains(response, "Diagnostics")
|
||||
self.assertContains(response, "generate_vapid_keys")
|
||||
|
||||
@override_settings(VAPID_PUBLIC_KEY="BMn-test-public-key")
|
||||
def test_the_diagnostics_report_reminder_times_and_devices(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("tracker:today"))
|
||||
self.assertContains(response, "Doses with a reminder time: 0 of 3")
|
||||
self.assertContains(response, "Devices subscribed (this account): 0")
|
||||
|
||||
self.morning.reminder_time = "08:00"
|
||||
self.morning.save()
|
||||
PushSubscription.objects.create(
|
||||
user=self.user, endpoint="https://push.example/a", p256dh="k", auth="s"
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("tracker:today"))
|
||||
self.assertContains(response, "Doses with a reminder time: 1 of 3")
|
||||
self.assertContains(response, "Devices subscribed (this account): 1")
|
||||
|
||||
def test_the_manifest_is_valid_json_pointing_at_real_icons(self):
|
||||
response = self.client.get(reverse("manifest"))
|
||||
|
||||
|
||||
@ -104,7 +104,14 @@ def today(request):
|
||||
"next_date": day + timedelta(days=1),
|
||||
"chart": build_chart(readings),
|
||||
"reading_count": len(readings),
|
||||
# Empty when push is not configured; the page then hides the card.
|
||||
# Empty when push is not configured; the card then says so rather than
|
||||
# vanishing, because a missing card looks like a bug in the page.
|
||||
"vapid_public_key": settings.VAPID_PUBLIC_KEY,
|
||||
"doses_with_reminders": ScheduledDose.objects.filter(
|
||||
is_active=True,
|
||||
medication__is_active=True,
|
||||
reminder_time__isnull=False,
|
||||
).count(),
|
||||
"subscribed_devices": request.user.push_subscriptions.count(),
|
||||
},
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user