Add opt-in Daily Digest (daily email of last 24h of library recaps)

Multi-mode, off by default. Each new recap is synthesized into a 1-2
paragraph overview via the relay (operator-absorbed) and cached onto the
session JSON; a daily 08:00 scan emails opted-in users their fresh
recaps, deduped by a per-user watermark that never skips a failed or
over-cap recap. One-click tokenized unsubscribe; settings-modal toggle;
admin test trigger. Bumps to 0.2.158.
This commit is contained in:
Keysat
2026-06-15 19:50:48 -05:00
parent 962423ca10
commit b4fa5d7be8
14 changed files with 1144 additions and 17 deletions
+64
View File
@@ -84,6 +84,70 @@ export function setupAdminRoutes(app) {
}
});
// Daily-digest test trigger. With {test_email}, sends a sample digest
// to that address so the operator can eyeball the rendering without
// opted-in users or waiting for the send hour. Without it, forces a
// real scan now (bypassing the 08:00 gate, NOT the per-user resend gate).
app.post("/api/admin/digest/run", requireOperator, async (req, res) => {
try {
const testEmail =
typeof req.body?.test_email === "string" ? req.body.test_email.trim() : "";
if (testEmail) {
const { isSmtpReady, sendMail } = await import("./smtp.js");
if (!isSmtpReady()) {
return res.status(503).json({
error: "smtp_not_ready",
message: "Configure StartOS System SMTP first.",
});
}
const { renderDigestEmail } = await import("./email-template.js");
const { getConfigSnapshot } = await import("./config.js");
const snap = await getConfigSnapshot();
const publicUrl = (snap.recap_public_url || "")
.trim()
.replace(/\/$/, "");
const msg = renderDigestEmail({
brandName: "Recaps",
episodes: [
{
title: "Sample podcast episode",
type: "podcast",
url: "https://example.com/episode",
overview:
"This is a sample overview paragraph so you can see how a digest entry renders. The real thing is synthesized from each recap's stored topic summaries.",
},
{
title: "Sample YouTube video",
type: "youtube",
url: "https://youtube.com/watch?v=example",
overview:
"A second sample entry, showing how multiple recaps stack in one email.",
},
],
overflowCount: 0,
manageUrl: `${publicUrl}/`,
unsubscribeUrl: `${publicUrl}/api/digest/unsubscribe?token=sample`,
});
await sendMail({
to: testEmail,
subject: msg.subject,
text: msg.text,
html: msg.html,
});
return res.json({ ok: true, test_email_sent_to: testEmail });
}
const { runDigestScan } = await import("./daily-digest.js");
const result = await runDigestScan({ force: true });
res.json({ ok: true, ...result });
} catch (err) {
console.error("[admin] digest run failed:", err?.message || err);
res.status(500).json({
error: "digest_run_failed",
message: err?.message || String(err),
});
}
});
// ── List all tenants ───────────────────────────────────────────────────
app.get("/api/admin/tenants", requireOperator, (req, res) => {
try {