Redo /newsletter and /nuggets pages; add Nuggets episode tags with admin editor; v2.37.0
Publish Container / build (push) Successful in 16m20s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-13 13:49:15 -04:00
1 parent ba35ccab19
commit 5d5c026762
9 files changed
+446 -85

No files matched your search

+1
View File
@@ -73,6 +73,7 @@ export const AUDIT_LOG_FILE = path.join(DATA_DIR, 'audit-log.json')
export const WEBHOOKS_FILE = path.join(DATA_DIR, 'webhooks.json')
export const STUDY_SESSIONS_FILE = path.join(DATA_DIR, 'study-sessions.json')
export const NEWSLETTER_ISSUES_FILE = path.join(DATA_DIR, 'newsletter-issues.json')
export const NUGGETS_TAGS_FILE = path.join(DATA_DIR, 'nuggets-tags.json')
export const MAX_EPISODE_SCRIPT_LENGTH = 200_000 // ~150k words, well beyond any sermon
// Seafile integration for Nuggets episode uploads
+25
View File
@@ -37,6 +37,7 @@ import {
WEBHOOKS_FILE,
STUDY_SESSIONS_FILE,
NEWSLETTER_ISSUES_FILE,
NUGGETS_TAGS_FILE,
EMPTY_HIT_STATS,
EMPTY_VISITOR_STATS,
DEFAULT_REPLY_TEMPLATES,
@@ -1588,3 +1589,27 @@ export function loadNewsletterIssuesFromDisk() {
state.newsletterIssues = []
})
}
// ── Nuggets Episode Tags ─────────────────────────────────────────────────────
export function queueNuggetsTagsWrite() {
state.nuggetsEpisodeTagsWritePromise = state.nuggetsEpisodeTagsWritePromise
.then(async () => {
await mkdir(DATA_DIR, { recursive: true })
await writeFile(NUGGETS_TAGS_FILE, JSON.stringify({ tags: state.nuggetsEpisodeTags }, null, 2), 'utf8')
})
.catch(err => {
console.error('[nuggets-tags] failed to write:', err)
})
}
export function loadNuggetsTagsFromDisk() {
return readFile(NUGGETS_TAGS_FILE, 'utf8')
.then(raw => {
const parsed = JSON.parse(raw)
state.nuggetsEpisodeTags = parsed?.tags && typeof parsed.tags === 'object' ? parsed.tags : {}
})
.catch(() => {
state.nuggetsEpisodeTags = {}
})
}
+39 -2
View File
@@ -1,5 +1,7 @@
import { parseRssItems } from '../rss-helpers.js'
import { state } from '../state.js'
import { requireAdminAuth } from '../auth.js'
import { queueNuggetsTagsWrite } from '../data.js'
let cache = null // { url, episodes }
let cacheAt = 0
@@ -37,12 +39,47 @@ export async function fetchNuggetsEpisodes() {
}
}
function withTags(episodes) {
return episodes.map(ep => ({ ...ep, tags: state.nuggetsEpisodeTags[ep.audioUrl] ?? [] }))
}
function cleanTags(input) {
const list = Array.isArray(input) ? input : []
const seen = new Set()
const out = []
for (const t of list) {
const tag = String(t ?? '').trim().replace(/\s+/g, ' ').slice(0, 40)
const key = tag.toLowerCase()
if (tag && !seen.has(key)) { seen.add(key); out.push(tag) }
}
return out.slice(0, 12)
}
export function register(app) {
app.get('/api/admin/nuggets/episode-tags', requireAdminAuth, async (_req, res) => {
try {
const episodes = await fetchNuggetsEpisodes()
res.json({ episodes: withTags(episodes).map(({ title, pubDate, audioUrl, episode, tags }) => ({ title, pubDate, audioUrl, episode, tags })) })
} catch (err) {
res.status(502).json({ message: 'Could not load episodes from the feed.' })
}
})
app.put('/api/admin/nuggets/episode-tags', requireAdminAuth, (req, res) => {
const audioUrl = String(req.body?.audioUrl ?? '')
if (!audioUrl) { res.status(400).json({ message: 'audioUrl is required.' }); return }
const tags = cleanTags(req.body?.tags)
if (tags.length) state.nuggetsEpisodeTags[audioUrl] = tags
else delete state.nuggetsEpisodeTags[audioUrl]
queueNuggetsTagsWrite()
res.json({ ok: true, tags })
})
app.get('/api/nuggets/episodes', async (_req, res) => {
const configured = Boolean(feedUrl())
try {
const episodes = await fetchNuggetsEpisodes()
res.json({ episodes: episodes.slice(0, 6), configured })
res.json({ episodes: withTags(episodes.slice(0, 6)), configured })
} catch (err) {
console.error('[nuggets/episodes] RSS fetch error:', err.message)
res.json({ episodes: [], configured })
@@ -53,7 +90,7 @@ export function register(app) {
const configured = Boolean(feedUrl())
try {
const episodes = await fetchNuggetsEpisodes()
res.json({ episodes, configured })
res.json({ episodes: withTags(episodes), configured })
} catch (err) {
console.error('[nuggets/episodes/all] RSS fetch error:', err.message)
res.json({ episodes: [], configured })
+3
View File
@@ -96,6 +96,9 @@ export const state = {
// newsletterIssues: Array<{ id, resendBroadcastId, slug, title, subject, description, html, sentAt, publishedAt, public }>
newsletterIssues: [],
newsletterIssuesWritePromise: Promise.resolve(),
// nuggetsEpisodeTags: { [audioUrl]: string[] } — admin-assigned tags for Nuggets RSS episodes
nuggetsEpisodeTags: {},
nuggetsEpisodeTagsWritePromise: Promise.resolve(),
auditLog: [],