Redo /newsletter and /nuggets pages; add Nuggets episode tags with admin editor; v2.37.0
Publish Container / build (push) Successful in 16m20s
Publish Container / build (push) Successful in 16m20s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ba35ccab19
commit
5d5c026762
9 files changed
+446
-85
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 = {}
|
||||
})
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
Reference in new issue
Block a user