Push reminders: personal to the notes, generic without them
The daily reminder said one generic line to everyone, because the server
never sees a deck. Now the notification text is written in the browser
(lib/reminder.ts) and travels with the subscription:
- Personal: "3 cards are due in Sky deck", in the app's language, tapping
through to /review. Without notes (or with nothing due) it is the plain
nudge, translated in both languages — push.title / push.due.* / push.generic.
- POST /api/push accepts an optional message {title, body, url}; url must be
a path on this app, so a stolen subscription cannot make the notification
link elsewhere. The store keeps endpoint, keys, the message and createdAt
(a refresh no longer resets the timestamp).
- components/PushReminderSync.tsx refreshes the stored text when the app
opens and whenever the tab becomes visible (throttled to twice a day), so a
count is only ever sent while it is true.
- scripts/send-reminders.mjs sends each device its own text and falls back to
PUSH_REMINDER_TITLE/_BODY for a subscription that has none
(scripts/reminder-payload.mjs, unit-checked).
- /api/push/test previews the real reminder with a "Test:" prefix.
- deploy/freezmo-reminders.{service,timer}: the daily send as a systemd user
timer (18:00 local, Persistent, randomised), documented in the README.
- Settings copy, privacy page (en+cs) and .env.example say exactly what the
server now holds.
- phase2-test grew the whole path with a faked browser subscription: personal
text in en and cs, the generic fallback in both, the URL guard, and the
sender's preference order (81 checks).
Version 0.8.2, SW v104.
This commit is contained in:
parent
6036238bf0
commit
77819b5961
18 changed files with 498 additions and 49 deletions
|
|
@ -5,6 +5,7 @@ import { useEffect, useState, useSyncExternalStore } from "react";
|
|||
import { Button } from "@/components/ui";
|
||||
import { haptic } from "@/lib/haptics";
|
||||
import { localeHeader, useI18n } from "@/lib/i18n";
|
||||
import { buildReminderMessage } from "@/lib/reminder";
|
||||
import { authHeaders, saveSettings, settingsStore } from "@/lib/settings";
|
||||
|
||||
const GOALS = [10, 20, 30, 50];
|
||||
|
|
@ -93,10 +94,13 @@ export function LearningSection({ level = 2 }: { level?: 2 | 3 } = {}) {
|
|||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(status.publicKey) as unknown as BufferSource,
|
||||
});
|
||||
/* The reminder text travels with the subscription: the server has no
|
||||
access to the notes, so this is where it becomes personal. */
|
||||
const message = await buildReminderMessage();
|
||||
const response = await fetch("/api/push", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...authHeaders(), ...localeHeader() },
|
||||
body: JSON.stringify(subscription.toJSON()),
|
||||
body: JSON.stringify({ ...subscription.toJSON(), message }),
|
||||
});
|
||||
if (!response.ok) throw new Error("subscribe failed");
|
||||
setSubscribed(true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue