- Atomic temp-file + rename for all JSON data writes (no truncated files on crash)
- /api/bible-passage: rate limit, range caps (60 verses), bounded cache
- Admin webhooks: https-only, block private/internal/Tailscale addresses (checked at registration and delivery), no redirects
- Resend webhook token compare is timing-safe
- DOMPurify on rendered study notes and broadcast preview
- CI runs lint + tests before build
- Permissions-Policy and HSTS (production) headers; JSON error handler for /api
- Tests for URL safety and atomic writes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Scripts keyed "<season>-<episode>" (legacy ones migrate on first start; unseasoned ones keep their plain key)
- Season backfill handles episode numbers shared across seasons (title match fallback)
- Fix: "Backfill seasons from RSS" was swallowed by the upload route
- Global search labels script results with the title of the series playing now (both lanes), not just finished books
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>
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>
New uploads route to R2 with folder selection (uploads/images/guides/finished).
Existing local files continue to be served via express.static for backward compat.
Adds R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET,
R2_PUBLIC_URL env vars. listUploadedAssets merges R2 objects + local files.
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>
Replaces outbound-only tracking with a useLinkClickTracking hook that
captures every <a> click on the site. Stores as link_click events keyed
by destination URL, with an internal flag. Admin analytics page gains a
Link Clicks table sorted by count, showing type (internal/external).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sends 7/14/30-day inactivity emails to study users who haven't logged in,
with one-click HMAC-signed unsubscribe and automatic re-arm on next login.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Four event types on the calendar: General, Recording, Social, Task — each color-coded
- + Event button opens form with type picker, title, date, notes, reminder
- Task events have a Mark Complete toggle; completed events show strikethrough
- Edit/delete popover for each event
- Email reminders work for calendar events same as episodes
- Added Email/Calendar nav links to /contacts header
- Added Email link to /calendar header (already had Admin)
- /email already links to Contacts and Calendar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical fixes:
- sanitizeLoadedHitStats/VisitorStats: restore full state shape so a
snapshot restore no longer crashes hit-counting middleware (missing
byPathReal, byPathBot, byDayReal, byDayBot, botReasons, ipHashIndex)
- /questions/share/🆔 read state.questions only, not draft questions
- inbound-email: validate date with Number.isFinite before toISOString
- study-reminders: wrap each send in try/catch so one failure doesn't
block remaining users; persist sent-markers after each success
Security:
- getClientIp: use req.ip (trust-proxy-resolved) instead of raw
x-forwarded-for header to prevent IP spoofing
- env-snapshot.env: delete immediately after backup tar stream ends
so secrets don't linger on disk between exports
Correctness / UX:
- contact form: email failures no longer 500 the user after the
submission is already saved; log and fall through instead
- study-account profile: cap data URI avatar at 6 MB
- admin enrollment PATCH: validate slug against study catalog
- signup: return 503 at MAX_STUDY_USERS instead of silently dropping
oldest accounts
Memory leaks:
- contactHits, downloadHits Maps: prune stale entries at 5000 entries
- resendEmailSubmissionIndex: trim to 2000 entries (oldest first)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- server/data.js: preserve source/htmlBody/inboundTo/messageId across
server restarts (sanitizeLoadedContactSubmissions was silently
dropping them on reload from disk)
- cloudflare/email-worker.js: rewrite MIME parsing to split on the
actual boundary marker instead of any literal "--", unfold
multi-line headers, and correctly recombine multi-byte UTF-8 in
quoted-printable decoding
- server/routes/inbound-email.js: validate Message-ID against RFC 5322
grammar before storing/using it, and compare the webhook secret with
timingSafeEqual to match the rest of the codebase's auth checks
- server/routes/contact.js: re-validate messageId at the point it's
injected into outgoing In-Reply-To/References headers; move the
allowed reply-from addresses into a shared config constant
- src/AdminPage.tsx: 30s inbox poll now syncs field updates (e.g.
archived) on already-loaded submissions instead of only appending
new ones; consolidate the duplicated from-address list
- .claude/launch.json: add a vite dev server preview config used to
verify these changes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tracks scroll depth (25/50/75/90%), time on page, UTM parameters, outbound
link clicks, search queries, audio pause/completion/listen time, and 404s.
Logged-in study users are now tied to their visitor record and surfaced in
the admin recent visits table. Scroll position resets on every route change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /api/analytics/play records a play event (title + date) on first
play per player mount, skipping admin sessions
- Plays persisted to data/episode-plays.json with total + byDay buckets
- Admin stats response includes episodePlays sorted by total plays
- AnalyticsPanel shows horizontal bar chart + summary cards for all
episodes dynamically — new episodes appear automatically as played
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each QR code gets a /qr/<slug> redirect that logs scans (IP, user agent,
timestamp) to disk. The admin QR Codes view lets you add, edit, enable/disable,
and delete codes, with per-code scan history inline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 1 — Quick wins:
- Image lazy-loading on series/resource cards
- Newsletter signup added to Episodes page (before highlights)
- Per-route meta tags via usePageMeta hook (title, og:title, og:description)
- Breadcrumbs on study index, section, and notes pages
- SVG completion checkmark badges on study section list
- Analytics time-range filter (7d / 30d / 90d) in admin panel
Phase 2 — Medium features:
- Related episodes on archived series detail pages
- Resource library two-tier filter (type + tag chips)
- Global search (Fuse.js) moved below sticky header as full-width bar
- Q&A anonymous upvoting with localStorage dedup + admin pin/unpin
- Study enrollment funnel tracking (firstVisitAt, firstCompletionAt) with funnel chart in analytics
Phase 3 — Larger features:
- Study section comments (auto-approve for enrolled users, admin moderation panel)
- Study completion certificate (canvas render, PNG download, shareable public URL)
- Episode script full-text search (mammoth docx extraction, server-side search, admin upload UI)
- Reflection questions renamed from Discussion Questions; quiz answers can be shared to section discussion
- Public certificate route at /certificate/:token with og meta tags
- Comment moderation panel added to admin under Manage > Study Comments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>