From d0fb153e46dbac10192c8cefec2d38f0b656dac4 Mon Sep 17 00:00:00 2001 From: jay Date: Tue, 9 Jun 2026 20:38:12 -0400 Subject: [PATCH] "Since you last visited" cue + PWA install (add to home screen) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two calm returning-reader features. Since-last-visit (Highlights companion, not a nav lane — per Codex): * queries.feed gains a `since` filter; GET /api/since?ts= returns the count + a few accepted/non-dup/visible articles discovered since the reader's last visit (boundary-respecting; invalid/future ts → 0, no error). * Home stores last_seen in localStorage (reads prev, then stamps now); on Highlights, a gentle "Since you were last here, N new calm reads came in" note with a "See what's new" reveal of a compact inline section. Dismissible. No badges, no unread counts, no "missed" language. PWA: * Real PNG icons (192/512 + full-bleed maskable) rasterized from favicon.svg; manifest fixed (azure theme to match the brand, PNG icons); apple-touch-icon. * Minimal service worker: precache the app shell, always-fresh API + /a/ pages. * Gentle, dismissible install banner (beforeinstallprompt → Install; iOS → the Share → Add to Home Screen hint). Never nags. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/app.html | 1 + frontend/src/lib/pwa.svelte.js | 42 +++++++++++++ frontend/src/routes/+page.svelte | 85 ++++++++++++++++++++++++++ frontend/src/service-worker.js | 40 ++++++++++++ frontend/static/icon-192.png | Bin 0 -> 8616 bytes frontend/static/icon-512.png | Bin 0 -> 24620 bytes frontend/static/icon-maskable-512.png | Bin 0 -> 18506 bytes frontend/static/manifest.webmanifest | 9 ++- goodnews/api.py | 21 +++++++ goodnews/queries.py | 6 ++ tests/test_admin.py | 16 +++++ 11 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 frontend/src/lib/pwa.svelte.js create mode 100644 frontend/src/service-worker.js create mode 100644 frontend/static/icon-192.png create mode 100644 frontend/static/icon-512.png create mode 100644 frontend/static/icon-maskable-512.png diff --git a/frontend/src/app.html b/frontend/src/app.html index 9204a02..b730922 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -3,6 +3,7 @@ + diff --git a/frontend/src/lib/pwa.svelte.js b/frontend/src/lib/pwa.svelte.js new file mode 100644 index 0000000..cb1b6bc --- /dev/null +++ b/frontend/src/lib/pwa.svelte.js @@ -0,0 +1,42 @@ +// Calm PWA install state. Captures the browser's install prompt (Android/desktop +// Chrome), detects iOS (which has no prompt — needs the Share → Add to Home Screen +// hint), and remembers a dismissal so we never nag. +import { browser } from '$app/environment'; + +export const pwa = $state({ canInstall: false, isIOS: false, isStandalone: false, dismissed: false }); +let deferred = null; + +if (browser) { + try { + pwa.isStandalone = + window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true; + pwa.isIOS = /iphone|ipad|ipod/i.test(window.navigator.userAgent); + pwa.dismissed = localStorage.getItem('goodnews:pwa_dismissed') === '1'; + } catch { + /* private mode / unavailable */ + } + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + deferred = e; + pwa.canInstall = true; + }); + window.addEventListener('appinstalled', () => { + pwa.canInstall = false; + pwa.isStandalone = true; + }); +} + +export async function installApp() { + if (!deferred) return; + deferred.prompt(); + try { await deferred.userChoice; } catch { /* dismissed */ } + deferred = null; + pwa.canInstall = false; +} + +export function dismissPwa() { + pwa.dismissed = true; + if (browser) { + try { localStorage.setItem('goodnews:pwa_dismissed', '1'); } catch { /* ignore */ } + } +} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 984a980..a1f8d96 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -15,6 +15,7 @@ import { prefs, initPrefs, active as prefsActive, applyPrefAction, persistPrefs, syncPrefsOnLogin } from '$lib/prefs.svelte.js'; import { initHistory, deviceIds, record, loadServerHistory } from '$lib/history.svelte.js'; import { trackVisit, track } from '$lib/analytics.js'; + import { pwa, installApp, dismissPwa } from '$lib/pwa.svelte.js'; let moods = $state([]); let topics = $state([]); @@ -75,6 +76,25 @@ else showSignIn = true; } + // "Since you last visited" — a calm welcome-back cue on Highlights only. Read + // the previous visit time, ask how many new calm reads arrived, then stamp now. + const LAST_SEEN_KEY = 'goodnews:last_seen'; + let sinceCount = $state(0); + let sinceItems = $state([]); + let sinceOpen = $state(false); + let sinceDismissed = $state(false); + async function checkSince() { + let prev = null; + try { prev = localStorage.getItem(LAST_SEEN_KEY); localStorage.setItem(LAST_SEEN_KEY, new Date().toISOString()); } + catch { return; } + if (!prev) return; // first visit on this device → no note + try { + const q = P.param(prefs.data); + const r = await getJSON(`/api/since?ts=${encodeURIComponent(prev)}${q ? '&' + q : ''}`); + if (r.count > 0) { sinceCount = r.count; sinceItems = r.items; } + } catch { /* quiet */ } + } + // React to sign-in only (untrack the body so browsing doesn't retrigger it). $effect(() => { const u = auth.user; @@ -421,6 +441,7 @@ error = 'Could not reach Upbeat Bytes.'; } loading = false; + checkSince(); // after the first paint; non-blocking }); @@ -467,6 +488,25 @@ {#if selected === 'today'} + {#if sinceCount > 0 && !sinceDismissed} +
+

+ Since you were last here, {sinceCount} new calm read{sinceCount === 1 ? '' : 's'} came in. + {#if !sinceOpen}{/if} +

+ +
+ {#if sinceOpen && sinceItems.length} +
+

New since your last visit

+
+ {#each sinceItems as a (a.id)} + drill('tag:' + t)} onsource={(id, name) => drill('source:' + id, { id, name })} onview={record} /> + {/each} +
+
+ {/if} + {/if} {#if brief?.items?.length}
drill('tag:' + t)} onsource={(id, name) => drill('source:' + id, { id, name })} onview={record} onimageerror={heroImageFailed} /> @@ -538,6 +578,20 @@
{/if} + + {#if !pwa.isStandalone && !pwa.dismissed && (pwa.canInstall || pwa.isIOS)} + + {/if} {/if} @@ -613,6 +667,37 @@ } .endcap .digestcta:hover { background: var(--accent-deep); } .endcap .digestcta:disabled { opacity: 0.6; cursor: default; } + + /* "Since you last visited" — a calm welcome-back cue on Highlights */ + .welcomeback { + display: flex; align-items: center; gap: 12px; justify-content: space-between; + background: var(--accent-soft); border: 1px solid var(--accent-soft); color: var(--accent-deep); + border-radius: 14px; padding: 12px 16px; margin: 4px 0 18px; + } + .welcomeback .wb-text { margin: 0; font-size: 0.95rem; } + .wb-cta { background: none; border: none; color: var(--accent-deep); font: inherit; font-weight: 600; + cursor: pointer; text-decoration: underline; margin-left: 6px; padding: 0; } + .wb-x { background: none; border: none; color: var(--accent-deep); font-size: 1.3rem; line-height: 1; + cursor: pointer; padding: 0 4px; opacity: 0.7; flex-shrink: 0; } + .wb-x:hover { opacity: 1; } + .sincesec { margin: 0 0 26px; } + .since-h { font-size: 1.1rem; margin: 0 0 12px; color: var(--ink); } + + /* PWA install banner — gentle, dismissible, never nagging */ + .install { + display: flex; align-items: center; gap: 14px; justify-content: space-between; flex-wrap: wrap; + background: var(--surface); border: 1px solid var(--line); border-radius: 16px; + padding: 16px 20px; margin: 28px 0 0; + } + .install-text { font-size: 0.92rem; color: var(--ink); line-height: 1.5; } + .install-text strong { display: block; margin-bottom: 2px; } + .ios-share { color: var(--accent-deep); font-weight: 600; } + .install-actions { display: flex; gap: 10px; align-items: center; flex-shrink: 0; } + .install-go { background: var(--accent); color: #fff; border: none; border-radius: 999px; + padding: 9px 20px; font: inherit; font-weight: 600; cursor: pointer; } + .install-go:hover { background: var(--accent-deep); } + .install-x { background: none; border: none; color: var(--muted); font: inherit; font-size: 0.88rem; cursor: pointer; } + .install-x:hover { color: var(--accent-deep); } .loadmore { display: flex; justify-content: center; margin: 30px 0 6px; } .loadmore button { background: var(--surface); border: 1px solid var(--line); color: var(--accent-deep); diff --git a/frontend/src/service-worker.js b/frontend/src/service-worker.js new file mode 100644 index 0000000..0a5f17f --- /dev/null +++ b/frontend/src/service-worker.js @@ -0,0 +1,40 @@ +/// +// Minimal, calm service worker: precache the app shell + static assets so the +// site is installable (PWA) and the shell opens fast / works offline. Live data +// (the API and server-rendered /a/ pages) is always fetched fresh, never staled. +import { build, files, version } from '$service-worker'; + +const CACHE = `upbeat-${version}`; +const SHELL = [...build, ...files]; + +self.addEventListener('install', (event) => { + event.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)).then(() => self.skipWaiting())); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))) + .then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + if (request.method !== 'GET') return; + const url = new URL(request.url); + if (url.origin !== location.origin) return; + + // Always-fresh, never cached: the API and the server-rendered article pages. + if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/a/')) return; + + // Navigations: serve the cached app shell when offline (SPA fallback). + if (request.mode === 'navigate') { + event.respondWith(fetch(request).catch(() => caches.match('/') || caches.match('/index.html'))); + return; + } + + // Static assets (JS/CSS/icons/fonts): cache-first, fall back to network. + event.respondWith(caches.match(request).then((cached) => cached || fetch(request))); +}); diff --git a/frontend/static/icon-192.png b/frontend/static/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..0e8db9619ea7f64674c1dcafff62818d5851c0d6 GIT binary patch literal 8616 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 FastAPI: items=[Article.from_row(r) for r in rows], ) + @app.get("/api/since", response_model=FeedResponse) + def feed_since(ts: str = Query(...), prefs: str | None = Query(None)) -> FeedResponse: + # A calm welcome-back cue: accepted/non-dup/visible articles discovered + # since the reader's last visit (boundary-respecting). count = how many; + # items = a few to show inline. No nagging, no unread state stored. + try: + norm = ts.replace("Z", "+00:00") + dt = datetime.fromisoformat(norm) + since = (dt.astimezone(timezone.utc) if dt.tzinfo else dt).strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, TypeError): + return FeedResponse(topic=None, flavor=None, count=0, items=[]) + fp = prefs_from_json(prefs) + now = datetime.now(timezone.utc) + kw = _prefs_sql_kw(fp, now) + with get_conn() as conn: + rows = queries.feed(conn, sort="latest", since=since, limit=60, **kw) + if fp.avoid_terms: + rows = filter_articles(rows, fp, now) + rows = sorted(rows, key=lambda r: is_paywalled(r["canonical_url"])) + return FeedResponse(topic=None, flavor=None, count=len(rows), items=[Article.from_row(r) for r in rows[:8]]) + @app.get("/api/brief", response_model=BriefResponse) def brief( date: str | None = Query(None), diff --git a/goodnews/queries.py b/goodnews/queries.py index d2715b2..9b2b765 100644 --- a/goodnews/queries.py +++ b/goodnews/queries.py @@ -64,6 +64,7 @@ def feed( sort: str = "ranked", follow_sources: list[int] | None = None, follow_tags: list[str] | None = None, + since: str | None = None, ) -> list[dict]: """Return articles with categorical filters applied in SQL. @@ -116,6 +117,11 @@ def feed( clauses.append("a.source_id = ?") params.append(source_id) + if since: + # "New since last visit": articles discovered after the reader's last visit. + clauses.append("a.discovered_at > ?") + params.append(since) + # "Following" feed: articles from a followed source OR carrying a followed tag. # Passing either list (even empty) switches to following mode; no follows → none. if follow_sources is not None or follow_tags is not None: diff --git a/tests/test_admin.py b/tests/test_admin.py index ef049a1..3684419 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -265,3 +265,19 @@ def test_follows_and_following_feed(tmp_path, monkeypatch): assert TestClient(app).get("/api/feed?following=true").json()["count"] == 0 assert TestClient(app).get("/api/follows").status_code == 401 assert tc.post("/api/follows", json={"kind": "source", "value": "999"}).status_code == 404 + + +def test_since_endpoint(tmp_path, monkeypatch): + import os, sqlite3 + app, api = _make(tmp_path, monkeypatch, admin_email="boss@x.com") + c = sqlite3.connect(os.environ["GOODNEWS_DB"]) + for aid, when in [(2, "2020-01-01 00:00:00"), (3, "2030-01-01 00:00:00")]: + c.execute("INSERT INTO articles (id,source_id,canonical_url,title,url_hash,discovered_at) VALUES (?,1,?,?,?,?)", + (aid, f"http://s/{aid}", f"t{aid}", f"h{aid}", when)) + c.execute("INSERT INTO article_scores (article_id,accepted) VALUES (?,1)", (aid,)) + c.commit(); c.close() + tc = TestClient(app) + r = tc.get("/api/since?ts=2027-01-01T00:00:00Z").json() + assert r["count"] == 1 and [i["id"] for i in r["items"]] == [3] # only the post-2027 article + assert tc.get("/api/since?ts=2099-01-01T00:00:00Z").json()["count"] == 0 # nothing newer + assert tc.get("/api/since?ts=not-a-date").json()["count"] == 0 # invalid ts → quiet 0