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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user