Accounts Phase 4: prefs sync + account/settings panel
- Prefs sync: GET/PUT /api/prefs store Calm Filters/Boundaries on the account. On sign-in the client adopts the account's prefs if present, else seeds them from the device; every change PUTs to the account so tuning follows you across devices. (Login side-effects run under untrack so browsing doesn't re-trigger.) - Account panel: GET /api/account (email, connected sign-in methods, saved count, active sessions); Export my data (GET /api/account/export → JSON download); Sign out everywhere (revoke all sessions); Delete account (cascades to all account data) with an inline confirm. Reachable from You → Account. Deferred to a follow-up: link/unlink a provider (OAuth link-mode) and per-session revoke. 118 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { getJSON, postJSON, delJSON } from '$lib/api.js';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { getJSON, postJSON, putJSON, delJSON } from '$lib/api.js';
|
||||
import * as P from '$lib/prefs.js';
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import BottomNav from '$lib/components/BottomNav.svelte';
|
||||
@@ -8,6 +8,7 @@
|
||||
import ArticleCard from '$lib/components/ArticleCard.svelte';
|
||||
import BoundariesPanel from '$lib/components/BoundariesPanel.svelte';
|
||||
import SignIn from '$lib/components/SignIn.svelte';
|
||||
import AccountPanel from '$lib/components/AccountPanel.svelte';
|
||||
import { auth, savedIds, refresh as refreshAuth, logout as authLogout } from '$lib/auth.svelte.js';
|
||||
|
||||
let moods = $state([]);
|
||||
@@ -22,6 +23,7 @@
|
||||
let showHistory = $state(false);
|
||||
let showYou = $state(false); // mobile "You" sheet
|
||||
let showSignIn = $state(false);
|
||||
let showAccount = $state(false);
|
||||
|
||||
function openAccount() {
|
||||
showYou = false;
|
||||
@@ -38,19 +40,39 @@
|
||||
loadServerHistory();
|
||||
}
|
||||
|
||||
// On first sign-in (per account, per device), fold this device's anonymous
|
||||
// history + saved into the account so nothing's lost.
|
||||
// React only to sign-in (auth.user); untrack so reading history/prefs inside
|
||||
// doesn't make this re-run on every browse.
|
||||
$effect(() => {
|
||||
const u = auth.user;
|
||||
if (!u || typeof window === 'undefined') return;
|
||||
const key = 'goodnews:imported:' + u.id;
|
||||
if (localStorage.getItem(key)) return;
|
||||
// Fold in this device's MEANINGFUL history (opened/replaced), not everything shown.
|
||||
const seen = history.map((a) => a.id);
|
||||
postJSON('/api/import', { seen, saved: [] })
|
||||
.then(() => { localStorage.setItem(key, '1'); loadServerHistory(); })
|
||||
.catch(() => {});
|
||||
if (u && typeof window !== 'undefined') untrack(() => onLogin(u));
|
||||
});
|
||||
|
||||
async function onLogin(u) {
|
||||
// One-time per account/device: fold this device's MEANINGFUL history
|
||||
// (opened/replaced — not everything shown) into the account.
|
||||
const key = 'goodnews:imported:' + u.id;
|
||||
if (!localStorage.getItem(key)) {
|
||||
try {
|
||||
await postJSON('/api/import', { seen: history.map((a) => a.id), saved: [] });
|
||||
localStorage.setItem(key, '1');
|
||||
} catch { /* best-effort */ }
|
||||
}
|
||||
loadServerHistory();
|
||||
// Prefs: adopt the account's saved prefs if any; otherwise seed from this device.
|
||||
try {
|
||||
const res = await getJSON('/api/prefs');
|
||||
if (res && res.prefs) {
|
||||
const incoming = Object.assign(P.blank(), res.prefs);
|
||||
if (JSON.stringify(incoming) !== JSON.stringify(userPrefs)) {
|
||||
userPrefs = incoming;
|
||||
P.save(userPrefs);
|
||||
select(selected, true);
|
||||
}
|
||||
} else {
|
||||
await putJSON('/api/prefs', { prefs: userPrefs });
|
||||
}
|
||||
} catch { /* best-effort */ }
|
||||
}
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
|
||||
@@ -223,6 +245,7 @@
|
||||
function refreshPrefs() {
|
||||
userPrefs = { ...userPrefs };
|
||||
P.save(userPrefs);
|
||||
if (auth.user) putJSON('/api/prefs', { prefs: userPrefs }).catch(() => {}); // sync to the account
|
||||
select(selected, true);
|
||||
}
|
||||
function applyAction(kind, value) {
|
||||
@@ -369,6 +392,7 @@
|
||||
<span>History</span>{#if historyItems.length}<span class="dot">{historyItems.length}</span>{/if}
|
||||
</button>
|
||||
{#if auth.user}
|
||||
<button class="yourow" onclick={() => { showYou = false; showAccount = true; }}><span>Account</span></button>
|
||||
<button class="yourow" onclick={signOut}><span>Sign out</span></button>
|
||||
{:else}
|
||||
<button class="yourow" onclick={() => { showYou = false; showSignIn = true; }}>
|
||||
@@ -378,6 +402,10 @@
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if showAccount}
|
||||
<AccountPanel onclose={() => (showAccount = false)} />
|
||||
{/if}
|
||||
|
||||
{#if notice}<p class="notice rise">{notice}</p>{/if}
|
||||
|
||||
{#if loading}
|
||||
|
||||
Reference in New Issue
Block a user