feat(ux): welcome banner, dashboard auth fix, docs update

- WelcomeBanner.tsx: guest-only dismissible onboarding card on dashboard
  (localStorage pv_seen_welcome, Browse Bills CTA, X dismiss)
- useDashboard: add !!token to query key so login/logout triggers
  a fresh fetch without manual refresh
- ARCHITECTURE.md: WelcomeBanner component, auth-aware query keys,
  v0.6.1 feature history entry
- Roadmap: mark welcome banner items complete
- Add MVP planning notes (Phase 3-6 roadmap draft)

Co-Authored-By: Jack Levy
This commit is contained in:
Jack Levy
2026-03-01 21:22:16 -05:00
parent 1e37c99599
commit a0e7ab4cd3
6 changed files with 190 additions and 4 deletions

View File

@@ -764,6 +764,9 @@ Compact bill preview showing bill ID, title, sponsor with party badge, latest ac
**`TrendChart.tsx`** **`TrendChart.tsx`**
Line chart of `composite_score` over time with tooltip breakdown of each data source. Line chart of `composite_score` over time with tooltip breakdown of each data source.
**`WelcomeBanner.tsx`**
Dismissible onboarding card rendered at the top of the dashboard. Shown only to guests (no JWT token). On dismiss — via the × button, the "Dismiss" link, or the "Browse Bills" CTA — sets `localStorage["pv_seen_welcome"] = "1"` and hides permanently. Reads localStorage after mount to avoid hydration mismatch; renders nothing until client-side state is resolved.
### Utility Functions (`lib/utils.ts`) ### Utility Functions (`lib/utils.ts`)
```typescript ```typescript
@@ -794,6 +797,8 @@ interface AuthState {
// Persisted to localStorage as "pocketveto-auth" // Persisted to localStorage as "pocketveto-auth"
``` ```
**Auth-aware query keys:** TanStack Query keys that return different data for guests vs authenticated users include `!!token` in their key (e.g. `["dashboard", !!token]`). This ensures a fresh fetch fires automatically on login or logout without manual cache invalidation.
--- ---
## Authentication ## Authentication
@@ -993,6 +998,18 @@ Nginx uses `resolver 127.0.0.11 valid=10s` (Docker's internal DNS) so upstream c
- Manual Controls split into two sections: always-visible recurring controls (Poll, Members, Trends, Actions, Resume) and a collapsible **Maintenance** section for one-time backfill tasks - Manual Controls split into two sections: always-visible recurring controls (Poll, Members, Trends, Actions, Resume) and a collapsible **Maintenance** section for one-time backfill tasks
- Maintenance section header shows "⚠ action needed" when any backfill has a non-zero count - Maintenance section header shows "⚠ action needed" when any backfill has a non-zero count
### v0.6.1 — Welcome Banner & Dashboard Auth Fix
**Welcome Banner:**
- `WelcomeBanner.tsx` — dismissible onboarding card shown only to guests at the top of the dashboard
- Three bullet points: follow bills/members/topics, see what changed, Back to Source citations
- "Browse Bills" CTA navigates to `/bills` and dismisses; × and "Dismiss" button also dismiss
- Dismissed state stored in `localStorage["pv_seen_welcome"]`; never shown to logged-in users
**Dashboard Auth-Aware Query Key:**
- `useDashboard` hook query key changed from `["dashboard"]` to `["dashboard", !!token]`
- Fixes stale cache issue where logging in showed the guest feed until a manual refresh
### v0.2.2 — Sponsor Linking & Search Fixes ### v0.2.2 — Sponsor Linking & Search Fixes
- **Root cause fixed:** Congress.gov list API does not return sponsor data — only the detail endpoint does. Poller now calls the detail endpoint for each new bill to get the sponsor and populate `bill.sponsor_id` - **Root cause fixed:** Congress.gov list API does not return sponsor data — only the detail endpoint does. Poller now calls the detail endpoint for each new bill to get the sponsor and populate `bill.sponsor_id`
- **Backfill task:** `backfill_sponsor_ids` Celery task + `/api/admin/backfill-sponsors` endpoint + "Backfill Sponsors" button in Admin UI — fixes existing bills with `NULL` sponsor_id (~10 req/sec, ~3 min for 1,600 bills) - **Backfill task:** `backfill_sponsor_ids` Celery task + `/api/admin/backfill-sponsors` endpoint + "Backfill Sponsors" button in Admin UI — fixes existing bills with `NULL` sponsor_id (~10 req/sec, ~3 min for 1,600 bills)

View File

@@ -0,0 +1,93 @@
### Phase 3 — Personal Workflow & Context
_Focus: Allowing users to organize data and adding the "Cross-Bill" intelligence layer._
- **Collections / Watchlists:** `collections` table for public/private bill groupings shareable via UUID tokens.
- **Personal Notes:** Private `bill_notes` table to store user stances, tags, and pinned insights on bill detail pages.
- **Cross-Bill Referencing (New):**
- **Extraction:** Update LLM prompts to identify and extract related bill identifiers (e.g., "H.R. 1234") from text.
- **Referential Table:** Implement `bill_references` to map source-to-target bill relationships with context quotes.
- **Ghost Hydration:** Create the `/api/admin/bills/hydrate` endpoint to fetch and analyze referenced bills not currently in the DB.
- **UI Hover Previews:** Use `Radix UI` popovers in the `BriefPanel` to show snapshots of referenced legislation without leaving the page.
- **Weekly Digest:** Celery beat task to dispatch 7-day summaries of followed bills via ntfy and RSS.
---
### Phase 4 — Accountability & Member Scoring
_Focus: Deep-diving into legislative performance and voting records._
- **Votes & Committees:** Fetch roll-call votes and committee actions from Congress.gov into a new `bill_votes` table.
- **Member Effectiveness Score:** Nightly Celery task calculating scores based on sponsored bills, advancement milestones, and "bills enacted" metrics.
- **Bipartisan Progress Metric (Proposed):** Enhance effectiveness scores by weighting co-sponsorship across party lines and committee movement in opposing chambers.
- **Representation Alignment:** Neutral view showing how member votes align with a users followed topics.
---
### Phase 5 — Systemic Intelligence
_Focus: Enhancing search and providing broader legislative context._
- **Search Improvements:** Add filters for bill type, status, chamber, and date; implement "Search within member bills".
- **Topic-Scoped Search:** Allow users to search legislation exclusively within followed topics like "Healthcare" or "Energy".
- **Member Effectiveness Transparency:** Display the "Member Effectiveness" formula on profile pages to maintain non-partisan trust.
---
### Phase 6 — Desktop & Polish
_Focus: Optimization for professional-grade power users._
- **Desktop View:** Multi-column layout with sticky sidebars and expanded grids optimized for large screens.
- **Notification Channels v2:** Expand beyond ntfy/RSS to include Discord webhooks, Telegram, and SMTP email.
- **Backfill Tasks:** Automate `first_name` and `last_name` population for all member records.
---
## 🛠️ Updated Technical Implementation Details
### Database Schema Updates
|**Table**|**New Columns / Purpose**|
|---|---|
|**`bill_references`**|`source_id`, `target_id`, `context_quote` to link related bills.|
|**`bill_notes`**|`user_id`, `bill_id`, `content`, `stance`, `tags`, `pinned`.|
|**`collections`**|`user_id`, `name`, `slug`, `is_public` for shared watchlists.|
|**`bill_votes`**|Stores roll-call data and results for accountability tracking.|
### Enhanced LLM Pipeline
The `llm_processor.py` task will now perform a dual-pass:
1. **Analysis:** Generate the standard brief with citations and fact/inference labels.
2. **Mapping:** Extract bill IDs found in the text and insert them into the `bill_references` table. If a target ID is missing from the `bills` table, it triggers a low-priority `hydrate_bill` task.
### Performance Guardrails
- **Selective Hydration:** The poller will only "Ghost Hydrate" referenced bills if they are from the current or previous Congress to prevent API exhaustion.
- **Cached Previews:** Referenced bill snapshots for hover tooltips will be cached in Redis to minimize database hits during dashboard scrolling.

View File

@@ -206,9 +206,9 @@ Dont store it server-side unless you already have user accounts and its pa
### Backlog item (checkboxes) ### Backlog item (checkboxes)
* [ ] First-visit welcome UI (banner/card + optional toast) * [x] First-visit welcome UI (banner/card — guests only, shown on dashboard)
* [ ] Dismiss + “dont show again” (localStorage) * [x] Dismiss + “dont show again” (localStorage `pv_seen_welcome`)
* [ ] CTA: Add first follow * [x] CTA: Browse Bills
* [ ] CTA: Load demo data (optional) * [ ] CTA: Load demo data (optional)
* [ ] Link: “How it works” page/modal (optional) * [ ] Link: “How it works” page/modal (optional)

View File

@@ -4,6 +4,7 @@ import { TrendingUp, BookOpen, Flame, RefreshCw } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useDashboard } from "@/lib/hooks/useDashboard"; import { useDashboard } from "@/lib/hooks/useDashboard";
import { BillCard } from "@/components/shared/BillCard"; import { BillCard } from "@/components/shared/BillCard";
import { WelcomeBanner } from "@/components/shared/WelcomeBanner";
import { adminAPI } from "@/lib/api"; import { adminAPI } from "@/lib/api";
import { useState } from "react"; import { useState } from "react";
import { useAuthStore } from "@/stores/authStore"; import { useAuthStore } from "@/stores/authStore";
@@ -40,6 +41,8 @@ export default function DashboardPage() {
</button> </button>
</div> </div>
<WelcomeBanner />
{isLoading ? ( {isLoading ? (
<div className="text-center py-20 text-muted-foreground">Loading dashboard...</div> <div className="text-center py-20 text-muted-foreground">Loading dashboard...</div>
) : ( ) : (

View File

@@ -0,0 +1,71 @@
"use client";
import { useEffect, useState } from "react";
import { X, BookOpen, GitCompare, ShieldCheck } from "lucide-react";
import Link from "next/link";
import { useAuthStore } from "@/stores/authStore";
const STORAGE_KEY = "pv_seen_welcome";
export function WelcomeBanner() {
const token = useAuthStore((s) => s.token);
const [visible, setVisible] = useState(false);
useEffect(() => {
if (!token && localStorage.getItem(STORAGE_KEY) !== "1") {
setVisible(true);
}
}, [token]);
const dismiss = () => {
localStorage.setItem(STORAGE_KEY, "1");
setVisible(false);
};
if (!visible) return null;
return (
<div className="relative bg-card border border-border rounded-lg p-5 pr-10">
<button
onClick={dismiss}
title="Dismiss"
className="absolute top-3 right-3 p-1 rounded text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
>
<X className="w-4 h-4" />
</button>
<h2 className="font-semibold text-base mb-3">Welcome to PocketVeto</h2>
<ul className="space-y-2 mb-4">
<li className="flex items-start gap-2.5 text-sm text-muted-foreground">
<BookOpen className="w-4 h-4 mt-0.5 shrink-0 text-primary" />
Follow bills, members, or topics get low-noise alerts when things actually move
</li>
<li className="flex items-start gap-2.5 text-sm text-muted-foreground">
<GitCompare className="w-4 h-4 mt-0.5 shrink-0 text-primary" />
See <em>what changed</em> in plain English when bills are amended
</li>
<li className="flex items-start gap-2.5 text-sm text-muted-foreground">
<ShieldCheck className="w-4 h-4 mt-0.5 shrink-0 text-primary" />
Verify every AI claim with <strong>Back to Source</strong> citations from the bill text
</li>
</ul>
<div className="flex items-center gap-2">
<Link
href="/bills"
onClick={dismiss}
className="px-3 py-1.5 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
Browse Bills
</Link>
<button
onClick={dismiss}
className="px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Dismiss
</button>
</div>
</div>
);
}

View File

@@ -1,9 +1,11 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { dashboardAPI } from "../api"; import { dashboardAPI } from "../api";
import { useAuthStore } from "@/stores/authStore";
export function useDashboard() { export function useDashboard() {
const token = useAuthStore((s) => s.token);
return useQuery({ return useQuery({
queryKey: ["dashboard"], queryKey: ["dashboard", !!token],
queryFn: () => dashboardAPI.get(), queryFn: () => dashboardAPI.get(),
staleTime: 2 * 60 * 1000, staleTime: 2 * 60 * 1000,
refetchInterval: 5 * 60 * 1000, refetchInterval: 5 * 60 * 1000,