Add Insights, Observe-first, compare, handout layouts, richer search, backup nudge

- Commentary and study aids wait for 15 words of your own Observe notes.
- Insights page: this week's own work, themes across studies, words you keep
  studying, what slipped in recalls, open follow-ups.
- Recall can record what slipped; session notes (teach it back, who came,
  group notes, follow-ups); "Pray about this" from Apply.
- Compare any two passages side by side.
- Handout layouts: standard, questions only, large print, note-taking.
- Search understands "in <book>", "last week/month/year" and a year.
- Home shows the last backup age and nudges after two weeks.
- Phone: the stage bar scrolls instead of clipping Script and Export.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-14 12:42:11 -04:00
1 parent 2ddc4e1ff6
commit d8e522dd06
24 files changed
+540 -28

No files matched your search

+8 -1
View File
@@ -2,7 +2,7 @@
// conflict doesn't force a choice between them. Chunks are matched by id; each chunk carries the time it
// was last edited here (editedAt). Nothing the person wrote is dropped: where both copies changed the
// same note, the newer one stays and the older one is appended under a marker.
const TEXT_FIELDS = ['observation', 'interpretation', 'application', 'generalNotes', 'finalScript', 'episodeNumber', 'episodeTitle'];
const TEXT_FIELDS = ['observation', 'interpretation', 'application', 'generalNotes', 'finalScript', 'episodeNumber', 'episodeTitle', 'teachBack', 'attendees', 'groupNotes'];
const LIST_FIELDS = ['tags', 'crossReferences', 'greekWords'];
export const MERGE_MARKER = '— from your other device —';
@@ -22,6 +22,12 @@ function union(a = [], b = []) {
return [...a, ...b].filter((item) => { const k = JSON.stringify(item); if (seen.has(k)) return false; seen.add(k); return true; });
}
// Follow-ups are matched by id; the newer copy's wording and done-state win, nothing is dropped.
function mergeById(newer = [], older = []) {
const seen = new Set(newer.map((i) => i.id));
return [...newer, ...older.filter((i) => !seen.has(i.id))];
}
export function mergeChunk(mine, theirs) {
if (same(mine, theirs)) return { chunk: mine, changed: false };
const mineNewer = (mine.editedAt ?? 0) >= (theirs.editedAt ?? 0);
@@ -31,6 +37,7 @@ export function mergeChunk(mine, theirs) {
LIST_FIELDS.forEach((f) => { out[f] = union(newer[f], older[f]); });
// Pastes follow their text: keep the union per field so merged notes still show what was pasted.
out.aiPastes = Object.fromEntries(['observation', 'interpretation', 'application'].map((f) => [f, union(newer.aiPastes?.[f], older.aiPastes?.[f])]));
out.followUps = mergeById(newer.followUps, older.followUps);
out.recalls = union(newer.recalls, older.recalls);
out.editedAt = Math.max(mine.editedAt ?? 0, theirs.editedAt ?? 0) || undefined;
return { chunk: out, changed: !same(out, mine) };