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

+17 -3
View File
@@ -15,7 +15,7 @@ test('a non-object body is rejected', () => {
test('good bookmarks and highlights pass through unchanged', () => {
const body = { bookmarks: { 'JHN-3-16': '#fef08a', '1SA-3-5': '#bbf7d0' }, highlights: [hl()] };
expect(cleanMarks(body)).toEqual(body);
expect(cleanMarks(body)).toEqual({ ...body, bookmarkNotes: {} });
});
test('malformed bookmarks are dropped, good ones kept', () => {
@@ -38,7 +38,7 @@ test('highlights are validated field by field and de-duplicated by id', () => {
});
test('missing sections become empty ones, and unknown fields are not stored', () => {
expect(cleanMarks({})).toEqual({ bookmarks: {}, highlights: [] });
expect(cleanMarks({})).toEqual({ bookmarks: {}, highlights: [], bookmarkNotes: {} });
expect(cleanMarks({ highlights: [{ ...hl(), evil: '<script>' }] }).highlights[0]).not.toHaveProperty('evil');
});
@@ -48,7 +48,7 @@ test('the database keeps one row per user and updatedAt only moves forward', asy
db.initDb();
db.createUser({ id: 'u1', email: 'a@example.test', passwordHash: 'x' });
db.createUser({ id: 'u2', email: 'b@example.test', passwordHash: 'x' });
expect(db.getReaderMarks('u1')).toEqual({ bookmarks: {}, highlights: [], updatedAt: 0 });
expect(db.getReaderMarks('u1')).toEqual({ bookmarks: {}, highlights: [], bookmarkNotes: {}, updatedAt: 0 });
const t1 = db.setReaderMarks('u1', { bookmarks: { 'JHN-3-16': '#fef08a' }, highlights: [] });
const t2 = db.setReaderMarks('u1', { bookmarks: {}, highlights: [hl()] });
expect(t2).toBeGreaterThan(t1);
@@ -70,3 +70,17 @@ test('a highlight keeps the translation it was made in and whether it covers the
expect(out[1].translation).toBeUndefined();
expect(out[1].whole).toBeUndefined();
});
test('notes on highlights and bookmarks are kept, trimmed, and dropped when empty or malformed', async () => {
const { cleanMarks } = await import('./readerMarks.js');
const base = { book: 'JHN', chapter: 3, verse: 16, startOffset: 0, endOffset: 40, color: '#fef08a' };
const out = cleanMarks({
bookmarks: { 'JHN-3-16': '#fef08a' },
bookmarkNotes: { 'JHN-3-16': 'Memorise this', 'JHN-3-17': ' ', 'bad key': 'x', 'ROM-8-1': 5 },
highlights: [{ ...base, id: 'a', note: 'The heart of the gospel' }, { ...base, id: 'b', note: ' ' }, { ...base, id: 'c', note: 'x'.repeat(900) }],
});
expect(out.bookmarkNotes).toEqual({ 'JHN-3-16': 'Memorise this' });
expect(out.highlights[0].note).toBe('The heart of the gospel');
expect(out.highlights[1].note).toBeUndefined();
expect(out.highlights[2].note).toHaveLength(500);
});