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

+70
View File
@@ -0,0 +1,70 @@
import { test, expect } from 'vitest';
import { daysToAnswer, filterPrayers, markAnswered, markDeleted, mergePrayers, newPrayer, parseVerses, prayersToMarkdown, reopen, togglePrayedToday } from './prayers.js';
const DAY = 86_400_000;
const T0 = Date.UTC(2026, 9, 1);
test('a new prayer is trimmed and starts open', () => {
const p = newPrayer({ text: ' Wisdom for the job ', forWhom: ' Sam ' }, T0);
expect(p).toMatchObject({ text: 'Wisdom for the job', forWhom: 'Sam', answeredAt: null, prayedOn: [], createdAt: T0, updatedAt: T0 });
expect(typeof p.id).toBe('string');
});
test('praying today can be switched on and off, and answering or reopening is tracked', () => {
let p = newPrayer({ text: 'x' }, T0);
p = togglePrayedToday(p, '2026-10-02', T0 + 1);
expect(p.prayedOn).toEqual(['2026-10-02']);
p = togglePrayedToday(p, '2026-10-02', T0 + 2);
expect(p.prayedOn).toEqual([]);
p = markAnswered(p, ' He did it ', T0 + 5 * DAY);
expect(p).toMatchObject({ answerNote: 'He did it', answeredAt: T0 + 5 * DAY });
expect(daysToAnswer(p)).toBe(5);
expect(reopen(p, T0 + 6 * DAY)).toMatchObject({ answeredAt: null, answerNote: '' });
expect(daysToAnswer(newPrayer({ text: 'y' }))).toBeNull();
});
test('verses are read from what you type; unreadable bits are reported', () => {
const { verses, unknown } = parseVerses('John 3:16, Philippians 4:6-7; Psalm 23; blah blah');
expect(verses.map((v) => [v.book, v.chapter, v.verse])).toEqual([['JHN', 3, 16], ['PHP', 4, 6], ['PSA', 23, null]]);
expect(verses[1].label).toBe('Philippians 4:6-7');
expect(unknown).toEqual(['blah blah']);
expect(parseVerses('')).toEqual({ verses: [], unknown: [] });
});
test('devices merge: the newer prayer wins, days combine, a deletion sticks', () => {
const a = newPrayer({ text: 'A' }, T0);
const here = [{ ...a, prayedOn: ['2026-10-01'], updatedAt: T0 + 10 }];
const there = [{ ...a, text: 'A (edited)', prayedOn: ['2026-10-02'], updatedAt: T0 + 20 }];
const merged = mergePrayers(here, there, T0 + 30);
expect(merged).toHaveLength(1);
expect(merged[0]).toMatchObject({ text: 'A (edited)', prayedOn: ['2026-10-01', '2026-10-02'] });
const afterDelete = mergePrayers(merged, [markDeleted(a, T0 + 40)], T0 + 50);
expect(afterDelete).toEqual([{ id: a.id, deleted: true, updatedAt: T0 + 40 }]);
expect(mergePrayers(afterDelete, here, T0 + 60)[0].deleted).toBe(true); // an older copy can't bring it back
});
test('the list can be filtered by tab and searched', () => {
const open = newPrayer({ text: 'Peace at home', forWhom: 'Family' }, T0);
const answered = markAnswered(newPrayer({ text: 'A new job', verses: [{ book: 'PHP', chapter: 4, verse: 6, label: 'Philippians 4:6' }] }, T0), 'Hired!', T0 + DAY);
const gone = markDeleted(newPrayer({ text: 'Removed' }, T0), T0 + 5);
const all = [open, answered, gone];
expect(filterPrayers(all, { tab: 'open' })).toEqual([open]);
expect(filterPrayers(all, { tab: 'answered' })).toEqual([answered]);
expect(filterPrayers(all, { tab: 'all' })).toHaveLength(2);
expect(filterPrayers(all, { tab: 'all', query: 'philippians' })).toEqual([answered]); // searches verses
expect(filterPrayers(all, { tab: 'all', query: 'hired' })).toEqual([answered]); // and the answer
expect(filterPrayers(all, { tab: 'all', query: 'family' })).toEqual([open]);
});
test('the Markdown separates what is being prayed for from what was answered', () => {
const open = { ...newPrayer({ text: 'Peace at home', forWhom: 'Family' }, T0), prayedOn: ['2026-10-01', '2026-10-02'] };
const answered = markAnswered(newPrayer({ text: 'A new job' }, T0), 'Hired!', T0 + DAY);
const md = prayersToMarkdown([open, answered], { date: new Date(2026, 9, 6) });
expect(md).toContain('## Praying');
expect(md).toContain('- **Family:** Peace at home');
expect(md).toContain('prayed on 2 days');
expect(md).toContain('## Answered');
expect(md).toContain('Answered');
expect(md).toContain(': Hired!');
expect(prayersToMarkdown([])).toContain('No prayers yet.');
});