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:
+45
-1
@@ -41,10 +41,24 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
-- NOT used for auth decisions — just data for the operator to grep
|
||||
-- when an abuse pattern shows up in the admin dashboard.
|
||||
signup_ip TEXT,
|
||||
signup_user_agent TEXT
|
||||
signup_user_agent TEXT,
|
||||
-- Daily Digest (opt-in, multi-mode): a daily email of the user's last
|
||||
-- ~24h of library recaps. Off by default. last_digest_at is the
|
||||
-- ms-epoch watermark of the last send; the scan covers recaps created
|
||||
-- AFTER it (dedup), and opt-in stamps it to "now" so the first digest
|
||||
-- doesn't dump the whole backlog. NULL = never sent.
|
||||
-- digest_unsub_token is a per-user random string for the one-click
|
||||
-- unsubscribe link in each digest email (no login needed); minted
|
||||
-- lazily on first send.
|
||||
digest_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
last_digest_at INTEGER,
|
||||
digest_unsub_token TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_signup_ip ON users(signup_ip);
|
||||
-- NB: idx_users_unsub_token is created in migrateUserDigestPrefs, not here
|
||||
-- — SCHEMA_SQL runs before the column migration on existing DBs, so an
|
||||
-- index over digest_unsub_token here would fail with "no such column".
|
||||
|
||||
-- ── sessions ───────────────────────────────────────────────────────────
|
||||
-- Server-side session store so we can revoke individual sessions from
|
||||
@@ -293,6 +307,7 @@ export async function initDb({ dataDir }) {
|
||||
migrateTenantCreditsSchema(db);
|
||||
migrateMagicLinkTokensTrialCookie(db);
|
||||
migrateUsersTier(db);
|
||||
migrateUserDigestPrefs(db);
|
||||
|
||||
dbInstance = db;
|
||||
console.log(`[db] opened ${dbPath} (multi-tenant store)`);
|
||||
@@ -314,6 +329,35 @@ function migrateUsersTier(db) {
|
||||
}
|
||||
}
|
||||
|
||||
// Daily Digest — add the opt-in columns to existing DBs (fresh installs get
|
||||
// them from SCHEMA_SQL). Idempotent: ALTERs only the columns still missing.
|
||||
function migrateUserDigestPrefs(db) {
|
||||
let cols;
|
||||
try {
|
||||
cols = db.prepare("PRAGMA table_info(users)").all();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!cols.some((c) => c.name === "digest_enabled")) {
|
||||
db.exec("ALTER TABLE users ADD COLUMN digest_enabled INTEGER NOT NULL DEFAULT 0");
|
||||
console.log("[db] added users.digest_enabled column (daily-digest)");
|
||||
}
|
||||
if (!cols.some((c) => c.name === "last_digest_at")) {
|
||||
db.exec("ALTER TABLE users ADD COLUMN last_digest_at INTEGER");
|
||||
console.log("[db] added users.last_digest_at column (daily-digest)");
|
||||
}
|
||||
if (!cols.some((c) => c.name === "digest_unsub_token")) {
|
||||
db.exec("ALTER TABLE users ADD COLUMN digest_unsub_token TEXT");
|
||||
console.log("[db] added users.digest_unsub_token column (daily-digest)");
|
||||
}
|
||||
// Created here (not in SCHEMA_SQL) so it runs AFTER the column exists on
|
||||
// both fresh and migrated DBs. Idempotent. Keeps the public unsubscribe
|
||||
// token lookup off a full-table scan.
|
||||
db.exec(
|
||||
"CREATE INDEX IF NOT EXISTS idx_users_unsub_token ON users(digest_unsub_token)",
|
||||
);
|
||||
}
|
||||
|
||||
// v0.2.92 — split the single tenant_credits.balance into two buckets
|
||||
// (purchased + replenish) so we can refill the latter periodically
|
||||
// without wiping the former.
|
||||
|
||||
Reference in New Issue
Block a user