From 1d02f3003ea47b192fb99a77a814a916c82cc227 Mon Sep 17 00:00:00 2001 From: jeremylongshore Date: Sat, 14 Feb 2026 21:11:34 -0600 Subject: [PATCH] Remove relevance scoring from feed, sort by newest first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Articles were sorting by relevance_score DESC which is null for most articles (Agent 3 assigns scores but ingestion path skips it), causing unpredictable ordering. Feed now sorts purely by published_at DESC. Removed: TrendingSection component, calculateTrendingScore(), score legend, relevance composite Firestore index. Added ingestion-complete event listener to auto-refresh feed after ingestion completes. Relevance_score field kept in Firestore and agents — only dashboard display and query ordering changed. Co-Authored-By: Claude Opus 4.6 --- dashboard/src/pages/Articles.tsx | 198 ++--------------------- firestore.indexes.json | 17 +- tests/firebase/test_dashboard_queries.py | 10 +- 3 files changed, 14 insertions(+), 211 deletions(-) diff --git a/dashboard/src/pages/Articles.tsx b/dashboard/src/pages/Articles.tsx index 2428455..753325f 100644 --- a/dashboard/src/pages/Articles.tsx +++ b/dashboard/src/pages/Articles.tsx @@ -7,7 +7,7 @@ import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Skeleton, SkeletonArticle } from '@/components/ui/skeleton' import { EmptyState, NewspaperIcon } from '@/components/EmptyState' -import { getRelevanceColor, getCategoryColor } from '@/lib/design-tokens' +import { getCategoryColor } from '@/lib/design-tokens' import IngestionButton from '../components/IngestionButton' interface Article { @@ -21,7 +21,6 @@ interface Article { published_at: string relevance_score: number hn_points?: number - trending_score?: number } interface HNStory { @@ -32,16 +31,6 @@ interface HNStory { descendants: number } -function calculateTrendingScore(article: Article): number { - const now = new Date() - const published = new Date(article.published_at) - const hoursAgo = (now.getTime() - published.getTime()) / (1000 * 60 * 60) - const recencyBoost = Math.max(0, 5 - (hoursAgo / 5)) - const baseScore = article.relevance_score || 0 - const hnBoost = article.hn_points ? Math.min(5, article.hn_points / 100) : 0 - return baseScore + recencyBoost + hnBoost -} - async function fetchHNTopStories(): Promise> { const urlToStory = new Map() try { @@ -77,121 +66,6 @@ const CATEGORIES = [ { id: 'world', name: 'World' }, ] -// Trending section component -function TrendingSection({ - articles, - onArticleClick -}: { - articles: Article[] - onArticleClick: (url: string) => void -}) { - const trendingByCategory = articles.reduce((acc, article) => { - const cat = article.category || 'other' - if (!acc[cat]) acc[cat] = [] - acc[cat].push(article) - return acc - }, {} as Record) - - Object.keys(trendingByCategory).forEach(cat => { - trendingByCategory[cat] = trendingByCategory[cat] - .sort((a, b) => (b.trending_score || 0) - (a.trending_score || 0)) - .slice(0, 3) - }) - - const sortedCategories = Object.entries(trendingByCategory) - .filter(([_, articles]) => articles.length > 0) - .sort((a, b) => { - const maxA = Math.max(...a[1].map(x => x.trending_score || 0)) - const maxB = Math.max(...b[1].map(x => x.trending_score || 0)) - return maxB - maxA - }) - .slice(0, 4) - - const categoryNames: Record = { - 'tech': 'Tech', - 'hn-popular': 'HN Blogs', - 'saas_dev': 'SaaS/Dev', - 'engineering': 'Engineering', - 'infrastructure': 'Infra', - 'science': 'Science', - 'crypto': 'Crypto', - 'sports': 'Sports', - 'automotive': 'Auto', - 'world': 'World', - } - - if (sortedCategories.length === 0) return null - - return ( - -
- 🔥 -

Trending Now

-
- - -
- ) -} - function formatTimeAgo(dateString: string): string { const date = new Date(dateString) const now = new Date() @@ -217,9 +91,9 @@ function ArticleCard({ article, expanded, onToggle }: { className="border-b border-border py-4 last:border-0" >
- {/* Score indicator */} -
- {article.relevance_score} + {/* Time indicator */} +
+ {formatTimeAgo(article.published_at)}
@@ -336,52 +210,23 @@ function ArticlesSkeleton() { export default function Articles() { const [articles, setArticles] = useState([]) - const [allArticles, setAllArticles] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [selectedCategory, setSelectedCategory] = useState('all') const [expandedId, setExpandedId] = useState(null) const [hnStories, setHnStories] = useState>(new Map()) + const [refreshKey, setRefreshKey] = useState(0) useEffect(() => { fetchHNTopStories().then(setHnStories) }, []) useEffect(() => { - const fetchAllArticles = async () => { - try { - const articlesRef = collection(db, 'articles') - const q = query( - articlesRef, - orderBy('relevance_score', 'desc'), - orderBy('published_at', 'desc'), - limit(200) - ) - const snapshot = await getDocs(q) - const fetched: Article[] = [] - snapshot.forEach((doc) => { - fetched.push({ id: doc.id, ...doc.data() } as Article) - }) - setAllArticles(fetched) - } catch (err) { - console.error('Error fetching all articles:', err) - } - } - fetchAllArticles() + const handler = () => setRefreshKey(k => k + 1) + window.addEventListener('ingestion-complete', handler) + return () => window.removeEventListener('ingestion-complete', handler) }, []) - const articlesWithTrending = allArticles.map(article => { - const hnStory = hnStories.get(article.url) - const articleWithHN = { - ...article, - hn_points: hnStory?.score, - } - return { - ...articleWithHN, - trending_score: calculateTrendingScore(articleWithHN) - } - }) - useEffect(() => { const fetchArticles = async () => { setLoading(true) @@ -394,7 +239,6 @@ export default function Articles() { if (selectedCategory === 'all') { q = query( articlesRef, - orderBy('relevance_score', 'desc'), orderBy('published_at', 'desc'), limit(100) ) @@ -417,7 +261,6 @@ export default function Articles() { if (hnStory) { article.hn_points = hnStory.score } - article.trending_score = calculateTrendingScore(article) fetchedArticles.push(article) }) @@ -431,7 +274,7 @@ export default function Articles() { } fetchArticles() - }, [selectedCategory, hnStories]) + }, [selectedCategory, hnStories, refreshKey]) const groupedArticles = articles.reduce((acc, article) => { const cat = article.category || 'other' @@ -464,31 +307,8 @@ export default function Articles() {
- {/* Score Legend */} -
- - 7 - Relevance (1-10, AI-scored) - - - ▲ 42 - HN upvotes (live) - - - 12 pts - Trending (relevance + recency + HN) - -
- {/* Trending Section */} - {!loading && articlesWithTrending.length > 0 && ( - console.log('Clicked:', url)} - /> - )} - {/* Category filters */}
{/* Featured: HN Popular Blogs */} diff --git a/firestore.indexes.json b/firestore.indexes.json index 83b7957..2994081 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -1,19 +1,5 @@ { "indexes": [ - { - "collectionGroup": "articles", - "queryScope": "COLLECTION", - "fields": [ - { - "fieldPath": "relevance_score", - "order": "DESCENDING" - }, - { - "fieldPath": "published_at", - "order": "DESCENDING" - } - ] - }, { "collectionGroup": "articles", "queryScope": "COLLECTION", @@ -41,8 +27,7 @@ "order": "DESCENDING" } ] - } - , + }, { "collectionGroup": "ingestion_runs", "queryScope": "COLLECTION", diff --git a/tests/firebase/test_dashboard_queries.py b/tests/firebase/test_dashboard_queries.py index 20a8f76..9f8b5ab 100644 --- a/tests/firebase/test_dashboard_queries.py +++ b/tests/firebase/test_dashboard_queries.py @@ -201,13 +201,12 @@ def test_user_alerts_query( ) raise - def test_articles_by_relevance_query(self, firestore_db, skip_without_gcp_auth): + def test_articles_by_newest_query(self, firestore_db, skip_without_gcp_auth): """ - Articles sorted by relevance (used in recommendations). + Articles sorted by newest first (primary feed ordering). Query: collection('articles') - .orderBy('relevance_score', 'desc') .orderBy('published_at', 'desc') .limit(20) """ @@ -215,7 +214,6 @@ def test_articles_by_relevance_query(self, firestore_db, skip_without_gcp_auth): query = ( firestore_db.collection("articles") - .order_by("relevance_score", direction=Query.DESCENDING) .order_by("published_at", direction=Query.DESCENDING) .limit(20) ) @@ -225,8 +223,8 @@ def test_articles_by_relevance_query(self, firestore_db, skip_without_gcp_auth): except Exception as e: if "index" in str(e).lower(): pytest.fail( - f"Articles relevance query missing index: {e}\n" - "Add composite index: articles(relevance_score DESC, published_at DESC)" + f"Articles newest query missing index: {e}\n" + "Single-field orderBy shouldn't need composite index" ) elif "permission" in str(e).lower(): pytest.fail(f"Articles query permission denied: {e}")