Individual episode pages always use EpisodeAudioPlayer — the Spotify
iframe check is removed since all per-episode embeds use direct audio URLs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EpisodeAudioPlayer restored to original horizontal layout (unchanged
for individual episode detail pages chosen in /admin)
- LatestEpisodePlayer on /episodes page gets its own markup + CSS
(.latest-episode-player__*) with the centered layout A — large gold
play button, skip-back 15s / skip-forward 30s, full-width scrubber
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The per-episode player (chosen in /admin) reverts to the Spotify embed
iframe — only the /episodes listing page uses the new custom player.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LatestEpisodePlayer fetches the most recent episode from /api/episodes
(Anchor RSS) and renders it with the centered cinematic layout. The
Spotify show iframe is removed; platform buttons and HeadlinerWidget
remain below as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EpisodeAudioPlayer full variant: centered title, large gold play button,
skip-back 15s / skip-forward 30s flanking controls, full-width scrubber
- Compact variant (study sections) unchanged
- Episode detail page fetches direct Anchor MP3 URL from /api/episodes by
title match; falls back to resolvedEmbedUrl while loading
- Plays stream from Anchor CDN so stats still count
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch from toDataURL (which silently failed in browser) to toCanvas on
a real <canvas> ref — more reliable and renders immediately on mount.
Download PNG link appears once the canvas is drawn.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each QR code card now renders a 128px preview of the actual QR image
(pointing to /qr/<slug> on the current origin) and a Download PNG link
that saves it as qr-<label>.png. Uses the qrcode browser build with
@types/qrcode for TypeScript support.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Grouped contacts now have a Remove button that deletes all submissions
for that email address in one click (with confirmation).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Split "Manage" nav group into Audience, Insights, Tools
- Moved Email Templates into Configure group
- Added Testimonials to About jump nav
- Added RSS Feed tab to Podcast Hub
- Updated Study Users expanded card: shows last studied date, streak (current + longest), and per-study lesson completion count
- Streak pill shown in collapsed header when > 0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RSS Feed
- /feed.xml proxies the Anchor feed under the site's canonical domain
- Rewrites channel <link> and atom:link self-ref to the site URL
- Served with 30-min Cache-Control, reuses the existing episode cache
PWA
- vite-plugin-pwa installed; Workbox service worker auto-generated on build
- manifest.json inlined in vite.config.ts (name, icons, theme, standalone)
- pwa-192.png and pwa-512.png generated from existing book_icon.png
- StaleWhileRevalidate for /api/episodes and /api/questions; CacheFirst for images
- API, feed.xml, and uploads routes excluded from navigate fallback
Lesson Comments
- Import and wire StudySectionComments into ColossiansStudySectionPage
- Replaces the CommunityBoard in the Lesson Discussion section
- All routes and moderation already existed; only the render was missing
Progress Tracking + Streaks
- Mark-complete handler now records lastStudiedDate, currentStreak, longestStreak on the user record
- Streak increments on consecutive calendar days, resets on a gap
- /api/study-account/overview now returns streak fields
- Account page: 4-stat summary row (notes, streak 🔥, longest streak, member since)
- Per-study progress bars showing completedLessons/totalLessons with gold → green fill at 100%
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes CI lint errors — both functions were replaced by shareImageTo
in the previous commit but not deleted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced the separate text-link X/FB buttons and the duplicate image buttons
with a single X and Facebook button each that share the generated quote card.
Keeps button row to 5: Upvote, X Share, Facebook Share, Copy link, Save image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On mobile: Web Share API opens native share sheet with the PNG attached,
letting users pick X, Facebook, Instagram, etc. directly.
On desktop: downloads the image and opens the platform compose window.
"Save image" download button kept alongside the two new share buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Contact form: notifyOnAnswer checkbox (shown for Bible Questions only)
- Contact route: stores notifyOnAnswer flag on question record
- Email: buildQuestionAnsweredEmailTemplate for branded notification
- Questions route: sends notification email on approve when notifyOnAnswer + email + answer are set
- Q&A section: Canvas API "Save as image" button generates 1080x1080 PNG quote card
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add qa-cards--compact class to container when active. Compact mode now
tightens padding, reduces font sizes, hides related questions, and
reduces card gaps — making the density difference obvious.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Testimonials: new Testimonial type, CMS field, admin UI under About,
and TestimonialsSection rendered on homepage and About page (hidden when empty)
- Q&A: "Submit a Question →" CTA at bottom of Q&A page linking to /contact
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These rules became orphaned after converting QuestionsPage to a full site
layout and removing the back-to-site nav link from QASection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Podcast highlights now display newest-first (reversed array)
- Q&A page uses full site layout (header, nav, footer) instead of card/modal wrapper
- Remove redundant "Back to Site" link from QASection now that header is present
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the build stage from Dockerfile so npm install/build no longer run under
QEMU emulation — only the lightweight runtime stage runs per-platform, making
arm64 images build reliably.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deletes src/data/projects.ts (never imported), and the three unused
Vite boilerplate assets (react.svg, vite.svg, hero.png).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces outdated portfolio/project-cards description with accurate
documentation of the podcast/ministry site: features, all env vars,
routes, API endpoints, persistent data paths, and admin capabilities.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The runtime image doesn't include .git, so git rev-parse failed at
startup. Fix: pass COMMIT_SHA as a Docker build arg from github.sha
in CI and bake it into the image as an ENV var, which config.js
already prefers over running git.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- package.json: bump to 1.0.0
- server/config.js: export APP_VERSION (from package.json) and GIT_COMMIT
(from COMMIT_SHA env var set by CI, or git rev-parse --short HEAD fallback)
- GET /api/version: new public endpoint returning { version, commit }
- GET /api/admin-auth/status: includes version and commit in response
- AdminPage sidebar: displays version string (e.g. "v1.0.0 (1d43875)")
below the Log Out button, styled as muted metadata text
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>
Docker deployments have no .env file — settings live in container env
vars — so the export now writes env-snapshot.env (dotenv format) into
the data dir from a whitelist of portable keys (admin password, Resend
keys, webhook URLs, etc.) before archiving. Machine-specific vars
(PORT, NODE_ENV, SITEFORGE_DATA_DIR, ALLOW_INSECURE_COOKIES, build
metadata) are excluded. Values are not applied automatically on
restore; the file documents what to set on the new server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
iPadOS greys out .tar.gz files when the input has an accept filter, so
the archive could not be selected. The server validates the gzip
signature and archive contents before restoring, so no client-side
filter is needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Browsers drop Secure cookies on plain-http origins, so logging into the
admin over a LAN/VPN IP (e.g. during server migration, before TLS is in
front) silently failed every authenticated request. Setting
ALLOW_INSECURE_COOKIES=true omits the Secure flag; default behavior in
production is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New admin endpoints: GET /api/admin-backup/export streams the entire
data directory as tar.gz (after flushing queued writes); POST
/api/admin-backup/import validates the archive, snapshots current
data, replaces the folder, and reloads all in-memory state.
- Admin UI: "Full Data Backup" section with download and restore-from-
file controls in the Analytics panel.
- Fix admin TOTP secret path to honor SITEFORGE_DATA_DIR (moved base
path resolution to server/paths.js to avoid a circular import), so
2FA for admin and study users survives backup/restore.
Co-Authored-By: Claude Fable 5 <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>
- Reply composer shows a From dropdown (hello@ or nate@), defaulting
to whichever address the inbound email was sent to
- Server validates the chosen address against an allowlist before sending
- Inbox list shows colored address badges: blue for hello@, purple for nate@
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Silently checks for new contact submissions in the background and
prepends any new ones to the inbox without disrupting the current view.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Templates now only fill the message body, not the subject, so the
Re: [original subject] line stays intact for proper Gmail threading.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When replying to an inbound email that has a messageId, the outgoing
Resend payload now includes In-Reply-To and References headers so the
reply threads correctly in Gmail and other email clients.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Worker now extracts both text/plain and text/html MIME parts and sends
htmlBody in the webhook payload. Server stores it on the submission.
Admin inbox renders htmlBody in a sandboxed iframe when present, falling
back to plain text with whitespace preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cloudflare Email Worker forwards hello@ and nate@ to Gmail and POSTs
parsed MIME email to /api/inbound-email as admin inbox entries
- New server route authenticates via shared secret and deduplicates by
Message-ID before storing inbound emails as contact submissions
- Admin inbox shows "email" badge for inbound messages; reply composer
opens blank with auto-subject "Re: [original]" and signature preview
- Documents setup steps in cloudflare/email-worker.js and .env.example
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Public:
- /finished page with ornament heading, intro text, book grid, and styled empty state
- /finished/:id episode playlist page filtered by RSS season number
- Episodes nav becomes click-toggle dropdown (Current Series / Finished Books)
- Finished Books link in footer
- Dropdown font fixed to match other nav links
Admin:
- End Current Series & Start New Book wizard on Current Series tab
- Finished Books management tab with add/edit/remove and image asset picker
Data:
- FinishedBook type + finishedBooks[] field on SiteContent
- RSS parser extracts itunes:season per episode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New FinishedBook type in content.ts + finishedBooks[] field on SiteContent
- RSS parser now extracts itunes:season into each episode object
- /finished grid page and /finished/:id playlist page (filters episodes by season)
- Episodes nav link replaced with dropdown: Current Series / Finished Books
- Finished Books link added to footer
- Admin: "End Current Series & Start New Book" wizard on Current Series tab
- Admin: Finished Books management tab (add/edit/remove entries)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced broken srcdoc iframe approach with direct script injection so
the TFL syndicate.js runs in page context with access to the DOM it needs.
Also added truthforlife.org and ajax.googleapis.com to CSP script-src,
which was silently blocking the widget and its jQuery dependency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>