- 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>
Community posts/replies fell back to the local part of the student's email
(e.g. "student.test" from student.test@versebyverse.local) whenever they
hadn't set a display name. The per-lesson discussion comments were worse —
they fell back to the FULL email address, and that endpoint has no auth
requirement, so it was publicly readable.
Adds getCommunityDisplayName() in study-helpers.js: uses the student's
display name when set, otherwise assigns a stable "Student XXXX" handle
derived from their account id — never their email. Both the community board
and lesson comments now resolve names live from the author's current
account at read time (falling back to the stored snapshot only if the
account was deleted), so this also fixes any already-posted content
without a data migration.
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>
Runner is on the ARM server so linux/arm64 builds natively — no QEMU
or cross-compilation needed. Drop setup-qemu-action and amd64 platform.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the existing .github/workflows/docker-publish.yml so Gitea
Actions picks it up from the standard .gitea/workflows/ location.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
server.js was calling recordVisitor(req, res).catch() but recordVisitor
returns undefined (it's synchronous). .catch() on undefined throws
TypeError on every request, causing Express to return 500 for every page.
Swap to try/catch since the function is synchronous.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace bare `void fn()` with `fn().catch(err => console.error(...))` for all
four startup-time scheduler invocations so an unexpected rejection cannot
crash the Node.js process (which exits on unhandled rejections in v15+).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If multer failed to write the temp file (e.g. data-volume permission issue),
the error was propagating unhandled through Express, returning a non-JSON 500
that the client couldn't parse — showing only "Upload failed." with no details.
Changes:
- Import mkdir and pre-create UPLOAD_TEMP_DIR on startup (non-blocking)
- Wrap upload.single('file') in a callback so multer errors return JSON
responses with the actual message instead of a bare HTML 500
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sanitizeFeaturedLinks was silently dropping showNotesSummary, showNotesThemes,
showNotesScripture, showNotesQuotes, and showNotesResources on every save — only
the legacy showNotes string was kept. Added sanitization for all five new fields
and updated the keep-filter to include them.
Also removed set -e from entrypoint.sh and made seed/merge steps non-fatal so a
permissions error on the mounted data volume no longer crash-loops the container.
Co-Authored-By: Claude Sonnet 4.6 <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>
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>
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>
- Dockerfile: expose BUILD_COMMIT and BUILD_NUMBER env vars from build args
(was COMMIT_SHA only; server was reading BUILD_COMMIT so it always fell through to null)
- CI: pass github.run_number as BUILD_NUMBER build arg
- state.js: record serverStartedAt at module load so deployedAt always shows
when the current container instance came up, even without an env var
- admin-content.js: fall back COMMIT_SHA → BUILD_COMMIT for older images,
use state.serverStartedAt when DEPLOYED_AT env var not 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>
Body text was #2c2417 inside the dark branded email wrapper. Updated
to light brand colors: #c8c0ac body, #f0ead8 emphasis, #c9a84c filename.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Seafile seafhttp returns a plain file ID string, not JSON. Replaced
res.json() with res.text() — the return value was unused anyway.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This Seafile instance exposes /api2/ not /api/v2.1/. Updated both the
upload-link call in the host upload route and the dir listing in the
Seafile watcher. Confirmed /api2/ works via node fetch test on the server.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Body text was #2c2417 (near-black) rendered inside the dark branded
email wrapper (#0f0f0c). Updated to light colors matching the template:
#c8c0ac for body, #f0ead8 for emphasis, #1a1a15/#2a2518 for table cells.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Seafile API uses ?p= for the directory path parameter. Using ?path= caused
the request to never match an API route, returning the web UI 404 HTML page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was passing data.hosts[idx].username (doesn't exist on stored hosts)
instead of data.hosts[idx].email for the welcome/reset email username param.
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>
- Portal uploads send one email immediately (host, episode, passage,
description, filenames) via Resend to NUGGETS_NOTIFY_EMAIL
- Seafile watcher polls /Episode Upload every 15 min for direct uploads
and sends a notification for any new file not already seen via portal
- Files uploaded through the portal are marked seen so the watcher
never sends a duplicate notification
- Credentials kept out of git: SEAFILE_TOKEN, SEAFILE_NUGGETS_REPO_ID,
NUGGETS_NOTIFY_EMAIL now sourced from docker .env file
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>