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:
@@ -2362,6 +2362,9 @@
|
||||
// User's own active sessions (lite-settings tenant view). Loaded
|
||||
// on demand when the Settings modal opens.
|
||||
mySessions: { rows: null, loading: false, error: null },
|
||||
// Daily Digest opt-in (lite-settings tenant view). enabled is null
|
||||
// until loaded from /api/account/digest when the modal opens.
|
||||
digest: { enabled: null, loading: false, saving: false },
|
||||
// "Take Recap home" — fetches the tenant's raw license key on
|
||||
// demand. We don't load this in /api/account/whoami because the
|
||||
// key is a bearer credential we'd rather not pass through the
|
||||
@@ -7343,6 +7346,7 @@
|
||||
${state.account?.user ? renderClaimPurchaseBlock() : ""}
|
||||
${state.account?.user ? renderPasswordBlock() : ""}
|
||||
${state.account?.user ? renderMySessionsBlock() : ""}
|
||||
${state.account?.user ? renderDigestBlock() : ""}
|
||||
${renderLibraryTransfer()}
|
||||
${state.account?.user ? renderTenantDangerZone() : ""}
|
||||
</div>
|
||||
@@ -7874,6 +7878,33 @@
|
||||
`;
|
||||
}
|
||||
|
||||
// Daily Digest opt-in toggle. Off by default; a single switch that
|
||||
// POSTs /api/account/digest. enabled is null until loaded — show a
|
||||
// muted "Loading…" rather than a misleading unchecked box during the
|
||||
// round-trip so the user never sees the wrong initial state.
|
||||
function renderDigestBlock() {
|
||||
const d = state.digest || {};
|
||||
const checkboxAttrs =
|
||||
d.enabled === null || d.loading || d.saving ? "disabled" : "";
|
||||
return `
|
||||
<label class="field-label" style="margin-top:14px;">Daily digest</label>
|
||||
<div style="border:1px solid #1e293b;background:rgba(15,23,42,0.4);border-radius:8px;padding:12px;">
|
||||
<label style="display:flex;align-items:flex-start;gap:8px;font-size:12px;color:#cbd5e1;cursor:${checkboxAttrs ? "default" : "pointer"};">
|
||||
<input type="checkbox" ${d.enabled ? "checked" : ""} ${checkboxAttrs}
|
||||
onchange="setDigestEnabled(this.checked)" style="margin:2px 0 0;" />
|
||||
<span>
|
||||
Email me a daily digest
|
||||
<span style="color:#64748b;display:block;font-size:10px;margin-top:2px;line-height:1.5;">
|
||||
${d.enabled === null
|
||||
? "Loading…"
|
||||
: "A once-a-day email summarizing the recaps you added to your library in the last 24 hours. Off by default; skipped on days with nothing new."}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Danger Zone — sits at the very bottom of the tenant lite-settings
|
||||
// modal. One action for now (Delete Account); future destructive
|
||||
// self-actions land here. Visually muted by default to avoid being
|
||||
@@ -10264,6 +10295,7 @@
|
||||
loadAdminActivity(state.ops.activityHours || 24);
|
||||
} else if (state.account?.user) {
|
||||
loadMySessions();
|
||||
loadMyDigest();
|
||||
}
|
||||
}
|
||||
render();
|
||||
@@ -12012,6 +12044,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMyDigest() {
|
||||
state.digest.loading = true;
|
||||
render();
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/account/digest`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
state.digest.enabled = !!data.enabled;
|
||||
} catch (e) {
|
||||
// Leave enabled null; the block keeps showing "Loading…" rather
|
||||
// than asserting a state we couldn't confirm.
|
||||
} finally {
|
||||
state.digest.loading = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function setDigestEnabled(enabled) {
|
||||
const prev = state.digest.enabled;
|
||||
// Optimistic flip so the switch responds instantly; revert on error.
|
||||
state.digest.enabled = enabled;
|
||||
state.digest.saving = true;
|
||||
render();
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/account/digest`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
showToast(enabled ? "Daily digest on" : "Daily digest off", "✓");
|
||||
} catch (e) {
|
||||
state.digest.enabled = prev;
|
||||
showToast("Couldn't save that — try again", "!");
|
||||
} finally {
|
||||
state.digest.saving = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeMySession(sessionId) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
|
||||
Reference in New Issue
Block a user