User feedback + expanded privacy-respecting admin stats

Feedback:
- feedback table; POST /api/feedback (anonymous-ok, optional category/email,
  honeypot + per-day flood cap) stores + emails the admin; GET /api/admin/feedback.
- Shared feedback store + FeedbackModal; a speech-bubble opens it from the desktop
  header, the mobile top bar (logo moves left), the footer, and /account. Feedback
  section in /admin.

Stats (additive, same privacy model — no IP/UA/referrer/raw terms):
- Event vocab: summary_viewed (fired on /a load), full_story (card → source),
  not_today/less_like_this/hide_topic, replace_used/replace_none, paywall_replace,
  paywalled_source_open. Card title/image opens /a (no double-count); history
  records via keepalive so it survives the nav.
- Dashboard: Accounts card (counts only), reading funnel (summary→source rate),
  emotional-mix & friction, paywall, returning-visitor buckets. (Health metrics
  deferred to a future monitoring dashboard.) 131 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-05 12:58:49 +00:00
parent cfde4e22db
commit 427210ac3e
16 changed files with 460 additions and 38 deletions
+8
View File
@@ -1,12 +1,18 @@
<script>
import '../app.css';
import FeedbackModal from '$lib/components/FeedbackModal.svelte';
import { fb, openFeedback, closeFeedback } from '$lib/feedback.svelte.js';
let { children } = $props();
</script>
{@render children()}
{#if fb.open}<FeedbackModal onclose={closeFeedback} />{/if}
<footer class="site">
<div class="container">
<button class="fb" onclick={openFeedback}>Send feedback</button>
<span class="dot">·</span>
Upbeat Bytes · metadata &amp; links only, no stored articles · <a href="/docs">API</a>
</div>
</footer>
@@ -20,6 +26,8 @@
border-top: 1px solid var(--line);
}
footer.site a { color: var(--accent-deep); }
footer.site .fb { background: none; border: none; color: var(--accent-deep); font: inherit; font-size: 0.82rem; cursor: pointer; text-decoration: underline; padding: 0; }
footer.site .dot { margin: 0 4px; }
/* room for the mobile bottom tab bar */
@media (max-width: 720px) {
footer.site { padding-bottom: calc(34px + 64px + env(safe-area-inset-bottom)); }