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:
@@ -220,6 +220,43 @@ export async function loadSession(scope, id) {
|
||||
}
|
||||
}
|
||||
|
||||
// List a scope's saved sessions as lightweight metadata (no entries /
|
||||
// chunks), oldest first. The daily-digest scan uses this to pick recaps
|
||||
// created after a watermark before loading each full record for
|
||||
// synthesis. Returns [] when the scope has no library yet (or the id is
|
||||
// malformed — safeComponent throws inside scopeDir, caught here).
|
||||
export async function listScopeSessions(scope) {
|
||||
let dir;
|
||||
try {
|
||||
dir = scopeDir(scope);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
let files = [];
|
||||
try {
|
||||
files = await fs.readdir(dir);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const out = [];
|
||||
for (const file of files.filter(
|
||||
(f) => f.endsWith(".json") && !f.startsWith("_") && !ROOT_SIDECARS.has(f),
|
||||
)) {
|
||||
try {
|
||||
const data = JSON.parse(await fs.readFile(path.join(dir, file), "utf-8"));
|
||||
out.push({
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
type: data.type || "youtube",
|
||||
url: data.url,
|
||||
createdAt: data.createdAt,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
out.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Shallow-merge `patch` into a session record on disk (e.g. to stamp
|
||||
// `summaryAudio` availability). No-op-safe: returns null if the record
|
||||
// is missing rather than throwing.
|
||||
|
||||
Reference in New Issue
Block a user