From 86de45dbc358a04638e54f8d9a522231db4df4d5 Mon Sep 17 00:00:00 2001 From: nmemmert Date: Wed, 9 Sep 2026 08:38:02 -0400 Subject: [PATCH] Make recording guidelines editable from admin panel; v2.0.18 Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- server/config.js | 1 + server/routes/nuggets-host-upload.js | 50 +++++++++++++++ src/AdminPage.tsx | 93 +++++++++++++++++++++++++++- src/components/NuggetsHostPortal.tsx | 19 ++++-- 5 files changed, 157 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 02cbb63..62bce56 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.0.17", + "version": "2.0.18", "type": "module", "scripts": { "dev": "vite", diff --git a/server/config.js b/server/config.js index cf4c102..e841999 100644 --- a/server/config.js +++ b/server/config.js @@ -62,6 +62,7 @@ export const NUGGETS_UPLOAD_HOSTS_FILE = path.join(DATA_DIR, 'nuggets-upload-hos export const NUGGETS_SEAFILE_SEEN_FILE = path.join(DATA_DIR, 'nuggets-seafile-seen.json') export const NUGGETS_UPLOAD_HISTORY_FILE = path.join(DATA_DIR, 'nuggets-upload-history.json') export const NUGGETS_PORTAL_ANNOUNCEMENT_FILE = path.join(DATA_DIR, 'nuggets-portal-announcement.json') +export const NUGGETS_PORTAL_GUIDELINES_FILE = path.join(DATA_DIR, 'nuggets-portal-guidelines.json') export const SEAFILE_NUGGETS_SHARE_URL = process.env.SEAFILE_NUGGETS_SHARE_URL ?? 'https://seafile.necloud.us/d/437ee6b8476d4b7e90a2/' // Email address for Nuggets upload notifications (defaults to the admin inbox) diff --git a/server/routes/nuggets-host-upload.js b/server/routes/nuggets-host-upload.js index 169f0c9..b643bed 100644 --- a/server/routes/nuggets-host-upload.js +++ b/server/routes/nuggets-host-upload.js @@ -16,6 +16,7 @@ import { NUGGETS_UPLOAD_HOSTS_FILE, NUGGETS_UPLOAD_HISTORY_FILE, NUGGETS_PORTAL_ANNOUNCEMENT_FILE, + NUGGETS_PORTAL_GUIDELINES_FILE, NUGGETS_NOTIFY_EMAIL, SEAFILE_URL, SEAFILE_TOKEN, @@ -139,6 +140,30 @@ async function saveAnnouncement(text) { return data } +// ── Guidelines helpers ──────────────────────────────────────────────────── +const DEFAULT_GUIDELINES = [ + 'Export as MP3 or WAV at 44.1 kHz, 128 kbps minimum.', + 'File name: YYYY-MM-DD-EpisodeName.mp3 (e.g. 2026-09-09-Romans-5.mp3).', + 'Recommended length: 8–15 minutes. Keep a 0.5s lead-in and 1s fade-out.', + 'Normalize to -1 dBFS before exporting.', + 'Match the episode name here to the file name exactly so search stays clean.', +] + +async function loadGuidelines() { + try { + const raw = await readFile(NUGGETS_PORTAL_GUIDELINES_FILE, 'utf8') + return JSON.parse(raw) + } catch { + return { lines: DEFAULT_GUIDELINES, updatedAt: null } + } +} + +async function saveGuidelines(lines) { + const data = { lines, updatedAt: new Date().toISOString() } + await writeFile(NUGGETS_PORTAL_GUIDELINES_FILE, JSON.stringify(data, null, 2), 'utf8') + return data +} + // ── Multer (disk storage, temp dir) ────────────────────────────────────── const UPLOAD_TEMP_DIR = path.join(DATA_DIR, 'upload-tmp') @@ -475,6 +500,13 @@ export function register(app) { res.json(data) }) + // ── Public: recording guidelines ───────────────────────────────────── + + app.get('/api/nuggets-upload/guidelines', async (_req, res) => { + const data = await loadGuidelines() + res.json(data) + }) + // ── Admin: manage upload host accounts ─────────────────────────────── app.get('/api/admin/nuggets-upload/hosts', requireAdminAuth, async (_req, res) => { @@ -597,6 +629,24 @@ export function register(app) { res.json({ ok: true, ...data }) }) + // ── Admin: recording guidelines ─────────────────────────────────────── + + app.post('/api/admin/nuggets-upload/guidelines', requireAdminAuth, async (req, res) => { + const raw = typeof req.body?.lines === 'string' ? req.body.lines : '' + const lines = raw.split('\n').map(l => l.trim()).filter(Boolean) + if (lines.length > 20) { + res.status(400).json({ message: 'Maximum 20 guideline lines.' }) + return + } + const data = await saveGuidelines(lines) + res.json({ ok: true, ...data }) + }) + + app.delete('/api/admin/nuggets-upload/guidelines', requireAdminAuth, async (_req, res) => { + const data = await saveGuidelines(DEFAULT_GUIDELINES) + res.json({ ok: true, ...data }) + }) + // ── Admin: upload history ───────────────────────────────────────────── app.get('/api/admin/nuggets-upload/history', requireAdminAuth, async (_req, res) => { diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 1a95ff0..bb672ae 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -666,7 +666,7 @@ type AdminView = | 'about:show-copy' | 'about:bio' | 'about:testimonials' | 'contact:profile' | 'contact:copy' | 'podcast:current-series' | 'podcast:highlights' | 'podcast:finished-books' | 'podcast:scripts' | 'podcast:rss' - | 'nuggets:settings' | 'nuggets:hosts' | 'nuggets:upload-portal' | 'nuggets:announcement' | 'nuggets:history' + | 'nuggets:settings' | 'nuggets:hosts' | 'nuggets:upload-portal' | 'nuggets:announcement' | 'nuggets:guidelines' | 'nuggets:history' | 'downloads' | 'custom-links' | 'content-blocks' | 'books' | 'recommended-resources' | 'questions' | 'study-comments' | 'analytics' | 'assets' | 'colossians-study' | 'qr-codes' | 'subscribers' | 'study-users' | 'email-templates' | 'broadcast' @@ -723,6 +723,7 @@ const ADMIN_NAV: NavGroup[] = [ { kind: 'leaf', view: 'nuggets:hosts', label: 'Hosts' }, { kind: 'leaf', view: 'nuggets:upload-portal', label: 'Upload Portal' }, { kind: 'leaf', view: 'nuggets:announcement', label: 'Announcement' }, + { kind: 'leaf', view: 'nuggets:guidelines', label: 'Recording Guidelines' }, { kind: 'leaf', view: 'nuggets:history', label: 'Upload History' }, ]}, { kind: 'leaf', view: 'colossians-study', label: 'Studies' }, @@ -1356,6 +1357,93 @@ function NuggetsAnnouncementAdmin() { ) } +// ── Nuggets Guidelines admin ─────────────────────────────────────────────── +const DEFAULT_GUIDELINE_LINES = [ + 'Export as MP3 or WAV at 44.1 kHz, 128 kbps minimum.', + 'File name: YYYY-MM-DD-EpisodeName.mp3 (e.g. 2026-09-09-Romans-5.mp3).', + 'Recommended length: 8–15 minutes. Keep a 0.5s lead-in and 1s fade-out.', + 'Normalize to -1 dBFS before exporting.', + 'Match the episode name here to the file name exactly so search stays clean.', +].join('\n') + +function NuggetsGuidelinesAdmin() { + const [lines, setLines] = useState('') + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [msg, setMsg] = useState('') + + useEffect(() => { + fetch('/api/nuggets-upload/guidelines') + .then(r => r.json()) + .then(d => setLines(Array.isArray(d.lines) ? d.lines.join('\n') : DEFAULT_GUIDELINE_LINES)) + .finally(() => setLoading(false)) + }, []) + + async function handleSave(e: React.FormEvent) { + e.preventDefault() + setSaving(true) + setMsg('') + try { + const r = await fetch('/api/admin/nuggets-upload/guidelines', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ lines }), + }) + const d = await r.json() + if (!r.ok) { setMsg(d.message ?? 'Failed.'); return } + setMsg('✓ Guidelines saved.') + } finally { + setSaving(false) + } + } + + async function handleReset() { + setSaving(true) + setMsg('') + try { + await fetch('/api/admin/nuggets-upload/guidelines', { method: 'DELETE' }) + setLines(DEFAULT_GUIDELINE_LINES) + setMsg('✓ Reset to defaults.') + } finally { + setSaving(false) + } + } + + return ( +
+
+

Nuggets — Recording Guidelines

+

+ One guideline per line. These appear in the collapsible "Recording Guidelines" panel on the host upload portal. + Max 20 lines. +

+
+ {loading ?

Loading…

: ( +
+
+ +