Version history, notes on highlights and bookmarks, "you've studied this", prayer journal
Build and Push Docker Image / build-and-push (push) Successful in 5m13s

- Earlier versions of a study: the server keeps compressed copies (about one per ten minutes of
  editing, the latest 30); Export shows what differs and puts one back (the replaced copy is kept)
- Notes on highlights and bookmarks: synced, merged without loss, in exports and backups
- Reader verse panel lists your studies and chunks that cover the verse, and opens them
- Prayer journal: requests, who for, linked verses, days prayed, answers; synced, in backups
  and the account export; daily prayer reminder through ntfy
- TODO.md lists the ideas not built yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 10:45:32 -04:00
1 parent 54cb1c217d
commit 207bfcb919
45 files changed
+1669 -75

No files matched your search

+33 -1
View File
@@ -4,7 +4,8 @@
// No API keys or push-subscription plumbing needed.
import { lookup } from 'dns/promises';
import { isIP } from 'net';
import { getUsersWithRemindersEnabled, setLastReminderDate, getReadingProgress, getUsersWithVotdEnabled, setLastVotdDate } from './db.js';
import { getUsersWithRemindersEnabled, setLastReminderDate, getReadingProgress, getUsersWithVotdEnabled, setLastVotdDate, getUsersWithPrayerReminder, setLastPrayerDate, getPrayers } from './db.js';
import { openPrayers } from './prayers.js';
import { fetchVerseOfDay, votdMessage } from './votd.js';
// A reminder still goes out if the server was briefly down (or restarting) at the
@@ -142,10 +143,41 @@ export async function checkVotd(now = new Date()) {
}
}
// The message for the daily prayer reminder: how many requests are open, and the first few.
export function prayerMessage(open) {
const lines = open.slice(0, 3).map((p) => `• ${p.forWhom ? `${p.forWhom}: ` : ''}${p.text.replace(/\s+/g, ' ').slice(0, 80)}${p.text.length > 80 ? '…' : ''}`);
const more = open.length - lines.length;
return {
title: open.length === 1 ? 'Time to pray: 1 request' : `Time to pray: ${open.length} requests`,
body: `${lines.join('\n')}${more > 0 ? `\n…and ${more} more` : ''}`,
tags: 'pray',
};
}
// Sends each person who asked for it a nudge to pray, with what they are praying about. Nothing is sent when
// the journal has no open requests.
export async function checkPrayerReminders(now = new Date()) {
for (const user of getUsersWithPrayerReminder().filter((u) => isDue({ reminderTime: u.prayerTime, reminderTz: u.reminderTz, lastReminderDate: u.lastPrayerDate }, now))) {
const open = openPrayers(getPrayers(user.id));
if (open.length === 0) continue;
const key = `prayer:${user.id}`;
if (now.getTime() - (_lastAttempt.get(key) ?? 0) < RETRY_AFTER_MS) continue;
_lastAttempt.set(key, now.getTime());
try {
await sendNtfy(user, prayerMessage(open));
_lastAttempt.delete(key);
setLastPrayerDate(user.id, zonedNow(now, user.reminderTz).date);
} catch (err) {
console.error(`Prayer reminder failed for user ${user.id}:`, err.message);
}
}
}
/** Checks once a minute whether any user's reminder time has arrived. */
export function startReminderScheduler() {
setInterval(() => {
checkReminders().catch((err) => console.error('Reminder scheduler error:', err));
checkVotd().catch((err) => console.error('Verse of the day scheduler error:', err));
checkPrayerReminders().catch((err) => console.error('Prayer reminder scheduler error:', err));
}, 60 * 1000);
}