Daily digest (opt-in) + finite "you're caught up" ending
Reader-retention as ritual, not capture (Codex's framing). Opt-in calm morning email of today's brief; the on-site twin is the finite end-of-feed nudge. * Schema: users.digest_enabled + digest_unsub_token; digest_sends (dedupe + visibility). auth.get_user now returns the digest fields. * goodnews/digest.py: build (dated calm subject, items w/ summary + "why it's here" + UB/source links + one-click unsubscribe, "you're caught up" sign-off) and send_due_digests (morning-window gated, >=4-item floor or skip quietly, deduped, reuses SMTP). No streaks/urgency/"you missed". * API: /auth/me exposes digest_enabled; POST /api/account/digest toggle; GET /api/digest/unsubscribe (token, no login, calm confirmation page). * CLI: cycle gains a morning-gated digest step (--no-digest) + a send-digests command (--force). * Frontend: digest toggle on the Account profile; the Highlights end-cap now says "you're caught up — see you tomorrow" with a one-tap "Get tomorrow's brief by email" (signed-in → enable; anon → sign in). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { getJSON } from '$lib/api.js';
|
||||
import { getJSON, postJSON } from '$lib/api.js';
|
||||
import { auth, savedIds, refresh } from '$lib/auth.svelte.js';
|
||||
import { prefs, initPrefs, persistPrefs } from '$lib/prefs.svelte.js';
|
||||
import { history, initHistory, loadServerHistory, removeOne, clearAll, record } from '$lib/history.svelte.js';
|
||||
@@ -43,6 +43,20 @@
|
||||
persistPrefs();
|
||||
}
|
||||
|
||||
let digestBusy = $state(false);
|
||||
async function toggleDigest() {
|
||||
if (!auth.user || digestBusy) return;
|
||||
digestBusy = true;
|
||||
try {
|
||||
await postJSON('/api/account/digest', { enabled: !auth.user.digest_enabled });
|
||||
await refresh(); // re-pull auth.user with the new digest_enabled
|
||||
} catch {
|
||||
/* leave as-is */
|
||||
} finally {
|
||||
digestBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the saved grid when entering that section while signed in.
|
||||
$effect(() => {
|
||||
if (section === 'saved' && auth.user && !savedReady) {
|
||||
@@ -128,6 +142,13 @@
|
||||
<!-- profile -->
|
||||
{#if auth.user}
|
||||
<AccountPanel onclose={() => {}} />
|
||||
<section class="panel digest">
|
||||
<h2>Daily digest</h2>
|
||||
<p class="dnote">A calm morning email — today's handful of good things, with a one-tap unsubscribe. No streaks, no noise; just the brief in your inbox.</p>
|
||||
<button class="dtoggle" class:on={auth.user.digest_enabled} onclick={toggleDigest} disabled={digestBusy} aria-pressed={auth.user.digest_enabled}>
|
||||
{#if digestBusy}…{:else if auth.user.digest_enabled}✓ On — you'll get tomorrow's brief{:else}Get the daily digest{/if}
|
||||
</button>
|
||||
</section>
|
||||
{:else}
|
||||
<p class="gate">Sign in from the home page to manage your profile, saved articles, and devices.</p>
|
||||
{/if}
|
||||
@@ -165,6 +186,16 @@
|
||||
}
|
||||
|
||||
.panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 20px 22px; }
|
||||
.digest { margin-top: 16px; }
|
||||
.digest h2 { font-size: 1.1rem; margin: 0 0 6px; }
|
||||
.dnote { color: var(--muted); font-size: 0.9rem; margin: 0 0 14px; line-height: 1.5; }
|
||||
.dtoggle {
|
||||
font: inherit; font-size: 0.9rem; border-radius: 999px; padding: 9px 18px; cursor: pointer;
|
||||
border: 1px solid var(--accent); background: var(--surface); color: var(--accent-deep);
|
||||
}
|
||||
.dtoggle:hover { border-color: var(--accent-deep); }
|
||||
.dtoggle.on { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
.dtoggle:disabled { opacity: 0.6; cursor: default; }
|
||||
.phead { display: flex; align-items: baseline; justify-content: space-between; }
|
||||
.phead h2 { font-size: 1.3rem; }
|
||||
.link { background: none; border: none; color: var(--accent-deep); font-size: 0.85rem; text-decoration: underline; cursor: pointer; }
|
||||
|
||||
Reference in New Issue
Block a user