Harden sync and security; add IQ Bible, templates, password reset, backups
Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Sync and data safety - Autosave flushes on leaving a study or the tab; storage-full errors are reported - Unsynced studies, deletes and ink are queued and retried when back online - Deletes propagate across devices (with a "Keep it" undo) and no longer resurrect - Server assigns study versions (no client-clock conflicts); "Combine both" merges a conflicted study chunk by chunk - Reading-plan progress made offline is merged, not overwritten; plans, ink and marks are cleared from the device on sign-out Security - Block SSRF through the ntfy reminder URL; failure-only rate limiting per IP and account; constant-time login; TOTP replay protection; input validation; SESSION_SECRET required in production; admin user delete fixed - Forgot password: one-time code sent to the account's ntfy topic Features - IQ Bible proxy (route allow-list, text-only caching per its terms): more commentaries, lexicons, study aids, daily verse and devotional, plan calendar export, narrations, podcast background - Study templates, reading-plan catch-up, account export, daily database snapshots Tests and tooling - ESLint config; App tests split into src/test; tests for sync queues, library hook, merge, templates, IQ Bible UI and reset form Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6bb392933a
commit
f274bc9c8a
67 files changed
+4734
-1569
No files matched your search
@@ -4,9 +4,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useToast } from '../components/Toasts.jsx';
|
||||
import { bookOptions, CHAPTER_COUNTS, makeId, parseBibleChapter } from '../utils/bibleUtils.jsx';
|
||||
import { fetchStudyChapter } from '../utils/studyText.js';
|
||||
import { templateStarter } from '../utils/studyTemplates.js';
|
||||
|
||||
// Every chunk starts blank; all note fields exist from the start.
|
||||
export function blankChunk(start, end, spilloverEndVerse = null) {
|
||||
export function blankChunk(start, end, spilloverEndVerse = null, templateId = 'blank') {
|
||||
return {
|
||||
id: makeId(),
|
||||
startVerse: start,
|
||||
@@ -22,6 +24,7 @@ export function blankChunk(start, end, spilloverEndVerse = null) {
|
||||
episodeTitle: '',
|
||||
finalScript: '',
|
||||
tags: [],
|
||||
...templateStarter(templateId),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,7 +84,7 @@ export function uncoveredRuns(chunks, from, to) {
|
||||
function initialSetupForm(project) {
|
||||
const last = project?.chapters?.at(-1);
|
||||
if (!last) {
|
||||
return { translation: 'BSB', book: 'Titus', bookAbbrev: 'TIT', chapter: '1', title: 'Titus 1 Study' };
|
||||
return { translation: 'BSB', book: 'Titus', bookAbbrev: 'TIT', chapter: '1', title: 'Titus 1 Study', template: 'blank' };
|
||||
}
|
||||
const next = Math.min(Number(last.chapter) + 1, CHAPTER_COUNTS[last.bookAbbrev] ?? Number(last.chapter));
|
||||
return {
|
||||
@@ -155,11 +158,7 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
|
||||
setErrorMessage('');
|
||||
setLoadingChapter(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://bible.helloao.org/api/${setup.translation}/${setup.bookAbbrev}/${setup.chapter}.json`,
|
||||
);
|
||||
if (!response.ok) throw new Error('Unable to load chapter.');
|
||||
const data = await response.json();
|
||||
const data = await fetchStudyChapter(setup.translation, setup.bookAbbrev, setup.chapter);
|
||||
const verses = parseBibleChapter(data);
|
||||
if (!data || !Array.isArray(verses) || verses.length === 0) {
|
||||
throw new Error('Invalid Bible data returned.');
|
||||
@@ -195,6 +194,7 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
|
||||
id: makeId(),
|
||||
title: setup.title,
|
||||
translation: setup.translation,
|
||||
templateId: setup.template ?? 'blank',
|
||||
lastEdited: Date.now(),
|
||||
selectedChunkId: null,
|
||||
chapters: [
|
||||
@@ -237,22 +237,7 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
|
||||
&& (c.spilloverEndVerse ?? null) === (spilloverEndVerse ?? null),
|
||||
);
|
||||
if (overlap) return ch;
|
||||
const newChunk = {
|
||||
id: makeId(),
|
||||
startVerse: start,
|
||||
endVerse: end,
|
||||
spilloverEndVerse,
|
||||
observation: '',
|
||||
interpretation: '',
|
||||
application: '',
|
||||
crossReferences: [],
|
||||
greekWords: [],
|
||||
generalNotes: '',
|
||||
episodeNumber: '',
|
||||
episodeTitle: '',
|
||||
finalScript: '',
|
||||
tags: [],
|
||||
};
|
||||
const newChunk = blankChunk(start, end, spilloverEndVerse, current.templateId);
|
||||
return { ...ch, chunks: [...ch.chunks, newChunk] };
|
||||
});
|
||||
// Find the newly added chunk id
|
||||
@@ -301,7 +286,7 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
|
||||
skippedCount += 1;
|
||||
return;
|
||||
}
|
||||
const newChunk = blankChunk(start, end, spilloverEndVerse);
|
||||
const newChunk = blankChunk(start, end, spilloverEndVerse, current.templateId);
|
||||
existing.add(key);
|
||||
newChunks.push(newChunk);
|
||||
addedCount += 1;
|
||||
@@ -592,12 +577,12 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
|
||||
if (i >= 0) {
|
||||
const c = chunks[i];
|
||||
chunks[i] = { ...c, endVerse: n, spilloverEndVerse: null };
|
||||
chunks.splice(i + 1, 0, blankChunk(n + 1, c.endVerse, c.spilloverEndVerse ?? null));
|
||||
chunks.splice(i + 1, 0, blankChunk(n + 1, c.endVerse, c.spilloverEndVerse ?? null, current.templateId));
|
||||
return { ...ch, chunks };
|
||||
}
|
||||
if (chunks.some((c) => c.startVerse <= n && n <= c.endVerse)) return ch; // already a boundary
|
||||
const start = Math.max(0, ...chunks.filter((c) => c.endVerse < n).map((c) => c.endVerse)) + 1;
|
||||
const created = blankChunk(start, n);
|
||||
const created = blankChunk(start, n, null, current.templateId);
|
||||
selected = created.id;
|
||||
const at = chunks.findIndex((c) => c.startVerse > n);
|
||||
chunks.splice(at < 0 ? chunks.length : at, 0, created);
|
||||
@@ -637,8 +622,7 @@ export function useChunkBuilder({ project, setProject, updateProject, allChunks,
|
||||
try {
|
||||
let starts = [];
|
||||
try {
|
||||
const res = await fetch(`https://bible.helloao.org/api/${project?.translation ?? 'BSB'}/${activeChapter.bookAbbrev}/${activeChapter.chapter}.json`);
|
||||
if (res.ok) starts = headingStarts(await res.json());
|
||||
starts = headingStarts(await fetchStudyChapter(project?.translation ?? 'BSB', activeChapter.bookAbbrev, activeChapter.chapter));
|
||||
} catch { /* headings are a nicety — fall back to even chunks */ }
|
||||
const usedHeadings = starts.length > 0;
|
||||
const first = activeChapter.verses[0]?.number ?? 1;
|
||||
|
||||
Reference in new issue
Block a user