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
@@ -2,6 +2,8 @@
|
||||
// write the final script beside your study notes. Claude prompts and the
|
||||
// pronunciation guide live here because this is where they're needed.
|
||||
import { useState } from 'react';
|
||||
import { useIqAvailable } from '../hooks/useIq.js';
|
||||
import { chunkRefs, gatherIqContext } from '../utils/iqbible.js';
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import { useToast } from '../components/Toasts.jsx';
|
||||
import StudioShell from '../components/study/StudioShell.jsx';
|
||||
@@ -16,6 +18,8 @@ const btn = 'rounded-lg border border-[var(--r-line)] px-3 py-1.5 text-xs hover:
|
||||
export default function ScriptPage() {
|
||||
const { project, allChunks, updateProject, updateChunk, authUser, setSaveStatus, setCurrentPage } = useApp();
|
||||
const [notice, setNotice] = useState('');
|
||||
const iq = useIqAvailable();
|
||||
const [gathering, setGathering] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const chunk = allChunks.find((c) => c.id === project?.selectedChunkId) ?? allChunks[0] ?? null;
|
||||
@@ -53,7 +57,22 @@ export default function ScriptPage() {
|
||||
setSaveStatus('Copied! Opening Claude…');
|
||||
window.setTimeout(() => setSaveStatus(''), 3000);
|
||||
window.open('https://claude.ai/new', '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
}).catch(() => toast("Couldn't copy to the clipboard. Click the button again.", { tone: 'error' }));
|
||||
};
|
||||
|
||||
// The podcast prompt plus IQ Bible's background on each passage (commentary, cross-references, word glosses).
|
||||
const copyPodcastWithContext = async () => {
|
||||
setGathering(true);
|
||||
try {
|
||||
const { text, count, skipped } = await gatherIqContext(project);
|
||||
const base = buildPodcastPrompt(project, authUser?.podcastName);
|
||||
const prompt = text ? `${base}\n\n---\nBACKGROUND REFERENCE (from IQ Bible; use it to inform the script, don't quote it at length):\n\n${text}\n` : base;
|
||||
if (!text) toast('IQ Bible had no background to add; copied the standard prompt', { tone: 'error' });
|
||||
else if (skipped > 0) toast(`Added background for ${count} passages (the first ${count} of ${count + skipped})`);
|
||||
copyPrompt(prompt);
|
||||
} finally {
|
||||
setGathering(false);
|
||||
}
|
||||
};
|
||||
|
||||
const noteBlock = (label, text) => has(text) && (
|
||||
@@ -112,6 +131,12 @@ export default function ScriptPage() {
|
||||
title="Copies your study notes as a Claude prompt, then opens claude.ai">Study guide → Claude ↗</button>
|
||||
<button type="button" onClick={() => copyPrompt(buildPodcastPrompt(project, authUser?.podcastName))} className={btn}
|
||||
title="Copies a podcast script prompt for Claude — set your show name in Account Settings first">Podcast → Claude ↗</button>
|
||||
{iq && (
|
||||
<button type="button" disabled={gathering} onClick={copyPodcastWithContext} className={btn}
|
||||
title={`Adds IQ Bible commentary, cross-references and word glosses for up to ${chunkRefs(project).refs.length} passages (each uses a few API requests)`}>
|
||||
{gathering ? 'Gathering…' : 'Podcast + background → Claude ↗'}
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={() => setCurrentPage('pronunciation')} className={btn}>Pronunciation guide</button>
|
||||
{notice && <span role="status" className="text-xs text-[var(--r-accent)]">{notice}</span>}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user