- Broadcast Email creates and sends a Resend Broadcast to the Resend segment (unsubscribe handling, reports, New Episode topic, scheduling, test send)
- Subscribers: import chosen Resend-only contacts into the app list
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Subscribers: compare in-app list with Resend audience/segment, with one-click fixes
- syncContactToResend no longer resets unsubscribed on existing contacts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Asset Manager: search/filter/sort, list+grid views, rename, move between R2 folders, tag chips, bulk actions, used-in counts; fix R2 delete lookup
- Inbox: readable wrapped rows with subject/preview, real read state, type filters, bulk actions, mojibake repair
- Broadcast: pick a Resend template to fill subject and body
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Web-hosted copies of episode newsletters, published from Resend broadcasts
tagged with the "New Episode" topic. Gives Facebook (and anyone else) a
proper link to share instead of raw HTML email, and makes issues findable
via site search and Google.
- Admin "Newsletter Archive" tab lists sent Resend broadcasts, flags ones
tagged "New Episode", and publishes them with one click (pulls the HTML
straight from Resend's API)
- Public /newsletter archive index and /newsletter/:slug issue pages,
server-rendered with per-issue Open Graph tags for social previews
- Unsubscribe merge-tag stripped from the web copy; archive + subscribe
links appended
- Issues added to sitemap.xml and the on-site search index
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Self-heal consent desync: if the server's consent cookie and the
client's localStorage flag fall out of sync, the client now notices
and stops silently sending events into a void.
- Stop double-counting hard page loads in hit/visitor stats.
- Flush pending disk writes on SIGTERM/SIGINT so redeploys don't drop
the last few events.
- Log rate-limiter drops once per window (diagnoses proxy misconfig).
- Widen the bot-detection allowlist to avoid flagging real browsers
(Chrome-iOS, Samsung Browser, etc.) as bots.
- Stop merging distinct visitors who share an IP when a real session
cookie is already present — only fall back to IP correlation when no
cookie was sent at all.
- Include analyticsEvents/episodePlays in backup snapshots and restore
them, so a restore no longer wipes engagement analytics.
- Regroup admin dashboard KPI cards by time scope (range vs all-time)
and remove a duplicated Top Countries/States block.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The "Discussion" nav link previously jumped straight into the featured
study's community board. Adds /study/discussion as a proper landing page
that lists every study track so students pick which one to discuss, then
links into /study/:slug/community from there. Both pages now require an
authenticated session to view at all (previously the per-study community
page rendered its shell to logged-out visitors, only gating the post feed
itself).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes two real bugs: useEffect and usePageMeta were called after an early
"study not found" return, violating React's rules of hooks (could corrupt
hook state if a study slug resolves inconsistently across renders). Also
removes dead code (unused NOTE_ANCHORS array, an unused state pair, a stale
eslint-disable comment), fixes three return-in-finally statements that could
mask exceptions, adds the missing study?.slug effect dependency, and cleans
up an unnecessary regex escape and an empty catch block.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds forgot/reset password, signup email verification, sign-out-of-all-devices,
note-usage and certificate display in account settings, and password-changed
security emails. Fixes study sessions being lost on server restart by
persisting them to disk, and fixes sanitizeStudyUsers silently dropping the
new pending-token fields on load. Also surfaces the lesson discussion board
in the main site nav, trims the sign-in page for returning users, and renames
colossiansStudy.tsx/colossiansStudyData.ts to studyPages.tsx/legacyColossiansData.ts
now that the study system covers more than Colossians.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tag scripts with a season number (with an RSS-feed backfill for
existing uploads), surface it in search results, and fix word-boundary
matching plus a title/script-match dedup bug in the search merge logic.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Upload dropdown lets you choose R2 folder (uploads/images/guides/finished)
or local server disk. Asset table shows location column for each file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
One scheduled date/time in the Current Series admin panel archives the active series to
Finished Books and atomically swaps in new content for the series card, Start Here steps,
and Companion Study Guide. Server checks every hour via setInterval; also runs on restart
so the transition fires even if the server was down at the scheduled moment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.scripture-index-main had no max-width — page stretched full viewport width.
Set to 900px with auto margins to match the site standard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The downloadQrUrl field was saved but never displayed — add the QR image
below the action buttons, wrapped in a link to the download URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add downloadUrl field to FinishedBook type
- Admin: paste any external URL (Drive, Dropbox, etc.) per finished book
- Public series page: replace square hero image with circular CD disc artwork
(conic-gradient sheen, center hole, drop shadow, slight tilt)
- Stream Episodes + Download Album action buttons in the hero
- Grid cards show a gold download badge when a download URL is set
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove raw IP storage; store only SHA-256 hash prefix in visitor records
- Non-blocking geo lookup: recordVisitor now synchronous, geo resolved in background
- UTM capture race condition fixed via sessionStorage pre-consent capture
- Add consent + bot checks to play endpoint (was missing)
- Add rate limiter to pageview endpoint (60 req/min per IP)
- Add 'All time' range option alongside 7d/30d/90d with trendData aggregation
- AnalyticsPanel: tabbed layout (Traffic/Engagement/System), dynamic chart titles,
range buttons with active state, visitor log uses ipHash instead of raw IP,
trendData drives main chart with weekly bucketing for all-time view
- Re-add prop-types dependency (required by react-simple-maps bundler)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Raise RSS description truncation from 220 → 800 chars so scripture
references in episode descriptions are preserved
- Global search now indexes Nuggets podcast episodes from
/api/nuggets/episodes/all (title, subtitle, description via Fuse)
- Scripture Index (/api/public/scripture-index) now extracts verse
references from both main podcast and Nuggets RSS descriptions
using a regex built from the shared BIBLE_BOOKS list; duplicate refs
across sources are merged by the existing client-side dedup logic
- Export fetchNuggetsEpisodes from nuggets.js for reuse
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New Inbox view in the Audience nav group shows all contact form submissions
- Filter by Unread / All / Archived with a search bar for name, email, or message
- Click any message to open a detail pane with full text, metadata, and a Reply via Email link
- Archive / Unarchive button to clear messages without deleting them
- Unread badge on the Inbox nav item; dashboard card now links directly to Inbox
- Added PATCH /api/admin-contact-submissions/:id to persist archived state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split bio on blank lines to render each paragraph as its own <p> tag.
Also grows the admin textarea from 3 to 8 rows so long bios are easier
to write and review.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the two-column hosts/episodes sidebar layout with a single full-width
column: host pills (photo + name, pill-shaped, clickable) in a horizontal row
at the top, episodes list filling the full width below.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each host card on /nuggets now links to /nuggets/hosts/:hostId showing
the host's full photo, bio, and listen links. Archived hosts return a
graceful "not found" page with a back link.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced --color-bg/surface/border/input-bg fallbacks (which don't exist
in the site's CSS) with the actual --brand-* tokens so the portal
renders in the site's dark theme instead of light fallback colors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Login form now asks for email + password (no username)
- Admin creates hosts with name + email + password; email is always
required and welcome email always sends on creation
- Duplicate email check replaces duplicate username check
- Admin table shows Email (login) column only; username field removed
throughout data model, routes, and UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Host accounts now have an optional email field
- Creating a host with an email sends a welcome email with the portal
URL, username, and password via Resend
- Resetting a password also emails the new credentials if the host
has an email on file; admin sees a confirmation either way
- Admin panel shows email column and confirmation messages
- Portal URL changed from /host-upload to /host
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace two-tab file picker with a combined form: episode name, passage,
description, and audio file in one submit
- Server now generates an Episode Info text file (name, host, passage,
description, timestamp) and uploads it to /Episode Info/ automatically,
while the audio goes to /Episode Upload/
- Drop zone changes color when a valid file is selected (green border)
- Success shows submitted episode name and filename in the session log
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /host-upload page: per-host login (username + password) with
session cookie, drag-and-drop file upload to Seafile via server proxy
- Two upload targets: Episode Upload (audio) and Episode Info (docs)
- Server route: /api/nuggets-upload/* for host auth + file proxy,
/api/admin/nuggets-upload/hosts for admin CRUD (create/disable/reset pw/delete)
- Admin panel: Nuggets → Upload Portal view to manage host accounts
- Seafile config via SEAFILE_URL / SEAFILE_TOKEN / SEAFILE_NUGGETS_REPO_ID env vars
- Directories default to /Episode Upload and /Episode Info (configurable)
- Uses multer for multipart uploads; 500 MB per-file limit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Restructure host card: image+name/role in a row header, bio spans full card width below
- Widen hosts column from 300px max to 360px max
- Add nuggets-host-card-header and nuggets-host-card-meta CSS classes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- analytics: union byDayReal+byDayBot keys so bot-only days aren't dropped from range totals
- analytics: exclude timestamp-less visitor rows from range filters (was unconditionally including them)
- App.tsx: init `configured` to false so a network failure doesn't show a misleading "no episodes" state; add r.ok guard before r.json()
- episodes.js: wrap fetch in try/catch so clearTimeout is called on network failure, matching the nuggets.js pattern
- helpers.js: add sanitizeNuggets + wire it into sanitizeSiteContent so corrupted hosts/urls can't crash NuggetsPage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New /nuggets page for the companion show, linked from the Episodes nav
dropdown. Hosts (name, role, bio, photo) show in a left column with the
episode list on the right — latest episode featured, rest below.
Episodes are pulled from an admin-configurable RSS feed URL (empty by
default, shows "coming soon" until set) rather than hardcoded like the
main show's feed, since the show doesn't have one yet.
Everything is editable in /admin under a new Nuggets section: Show
Settings (title, tagline, description, podcast art, RSS/platform URLs)
and Hosts (add freely; removing a host archives them instead of
deleting — kept on record and restorable, never lost).
Extracted the RSS item-parsing logic shared with the main show's feed
(server/routes/episodes.js) into server/rss-helpers.js so both feeds
use the same parser instead of duplicating it.
Also fixes the local Vite dev server not proxying /uploads/* to the
backend — it was silently serving the SPA shell (200, text/html)
instead of the actual file, so any admin-uploaded image (podcast art,
host photos, etc.) appeared broken in local dev only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Yvx4rPVy2HWe7ubuk3X3J
src/App.tsx (Scripture Index book map) and server/routes/bible-passage.js
(BSB/bolls.life passage fetch) each hand-maintained their own independent
66-book list with a different key format (3-letter USFM id vs bolls.life's
numeric id), so a future alias/rename fix in one would silently not apply to
the other.
Add shared/bible-books.js as the single canonical list (name + aliases +
USFM id, in canonical order); the server derives its numeric bolls.life ids
from the array position instead of hand-maintaining a second table. Requires
allowJs + including shared/ in tsconfig.app.json so the client can import the
plain-JS module alongside its TypeScript sources.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Yvx4rPVy2HWe7ubuk3X3J
- server/routes/contact.js: import fireWebhooks (was undefined, throwing on every submission)
- server/routes/analytics.js: filter admin-stats by row.at, the field recordVisitor actually sets, instead of the never-populated row.visitedAt
- src/App.tsx: parseSIRef now supports chapter-spanning verse ranges (e.g. "5:15-6:2"); SI_BOOK_MAP indexes both spaced and unspaced book names so multi-word Bible Book values from the admin field resolve correctly
- src/App.css / src/App.tsx: fix Episodes/Studies nav dropdown menus being unclickable — a positioning gap between the trigger and menu created a hover dead zone; menu is now flush with spacing moved into padding so hover carries through
- src/components/StudySectionComments.tsx: check response status before removing a comment locally, so a rejected delete no longer appears to succeed
- src/components/QASection.tsx: don't bail out of search scoring for 1-2 character queries, so short substrings like "OT" or "AI" can still match
- src/AdminPage.tsx: check response status in study-user, QR code, and episode-script delete handlers before updating local state
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Yvx4rPVy2HWe7ubuk3X3J
Previously it just grouped study lessons under each study (essentially
another list of lessons) and ignored podcast episodes entirely. Now it
parses every reference from studies and episode show notes, merges
entries that point at the same verse(s) into one row with links to all
sources, and groups rows by book in canonical Bible order. References
that don't parse (e.g. whole-chapter refs) fall into an "Other
References" section instead of being dropped.
Also fixes the reference passed to the index for studies without a
bibleBook set — it was falling back to the study's marketing title
instead of its slug, producing unparseable references.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Yvx4rPVy2HWe7ubuk3X3J
Adds a "Discuss this lesson" link in the classroom sidebar that takes
the user directly to the community page pre-filtered to the current
lesson's sectionId, so they can jump into discussion without navigating
back to the study index first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Twitter/X-style post cards: avatar thread line, author + relative timestamp
- Heart like button with optimistic toggle and like count per post
- Comment button with reply count; inline threaded replies always visible
- Compose card with user avatar, borderless textarea, pill Post button
- Horizontal scrollable lesson filter chips (All + per-lesson)
- New POST /api/study-community/posts/:postId/like endpoint (toggle)
- GET community posts now returns likedByMe + likeCount per post
- sanitizeStudyCommunityPosts preserves likedBy array on disk
- Relative timestamps (just now, Xm ago, Xh ago, Xd ago, then date)
- UserAvatar helper renders img or initials fallback consistently
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Progress bar: thin gold bar under lesson label showing completed/total lessons
- Sidebar lesson navigator: all lessons listed with ✓/current/○ states, replaces static Study Track + Lesson Navigation blocks
- Notes panel moves to sidebar: clicking a block's Note button opens NoteCard in sidebar instead of inline, preserving reading position
- Inline reflection answers: textareas under each question auto-save to quiz API; replaces separate quiz page link
- Note buttons: "Note" / "✎ Note" text label instead of bare pencil icon for better discoverability
- Greek Word Study block hidden when lesson has no greek notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace single showNotes textarea with five guided fields: Episode Summary,
Key Themes, Scripture References, Key Quotes, and Resources & Links. Legacy
showNotes content is preserved as a backward-compatible fallback — it
auto-populates the Summary field in the admin editor and renders on the public
episode detail page until overwritten with structured data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch from position:absolute (relative to trigger) to position:fixed
with getBoundingClientRect() so the popup escapes any overflow:hidden
ancestor. Also closes on scroll and window resize.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace cramped auto-fill grid with full-width row layout (reference | verse text | arrow)
- Fetch BSB passage text from bible.helloao.org, grouped by chapter to minimize API calls
- Display 2-line italic excerpt of each passage beneath the lesson title
- Hover state reveals arrow and highlights row in gold
- Mobile-responsive collapse to single column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Studies nav item is now a dropdown (same pattern as Episodes): hover to expand,
click the label to navigate directly to /study
- Dropdown contains: All Studies (/study) and Scripture Index (/scripture)
- Episodes dropdown follows the same split pattern: label navigates, caret toggles
- Dropdown menus open on CSS :hover (desktop) and stay toggle-able on mobile
- v1.2.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Admin > Audience > Broadcast Email: compose and send one-off HTML emails
to all subscribers via Resend batch API; {{name}} personalization; confirm step
- Admin > Configure > Webhooks: CRUD UI for managing POST event webhooks;
backend GET/POST/DELETE /api/admin-webhooks routes wired to existing state.webhooks
- Sitemap: dynamically appends released study lesson URLs (/study/:slug/:id)
and episode highlight URLs (/episodes/:id) to /sitemap.xml
- Scripture Index: public /scripture page listing every released lesson grouped
by study with passage reference links; served by /api/public/scripture-index
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>