4c3c8c6f87
Backend KOL_FEEDS trimmed from 29 to 25 (dead feeds removed).
Sync all hardcoded count mentions:
- layout.tsx JSON-LD, page.tsx (metric + comparison + copy)
- kol/page.tsx, KolPageClient.tsx ("and 26 more" → "and 22 more")
- glossary/page.tsx, opengraph-image.tsx
- public/llms.txt, llms-full.txt
- drop removed KOLs (Dragonfly Capital, Nic Carter) from named lists
Bundles other in-flight frontend work already in the working tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
4.0 KiB
TypeScript
119 lines
4.0 KiB
TypeScript
import type { TrumpPost } from '@/types'
|
|
import { getPosts, getPostsPage, type PostListResponse } from './api'
|
|
|
|
const LIVE_ARCHIVE_SOURCES = new Set([
|
|
'truth',
|
|
'btc_bottom_reversal',
|
|
'funding_reversal',
|
|
'kol_divergence',
|
|
])
|
|
|
|
function isAiScored(post: Pick<TrumpPost, 'ai_confidence' | 'ai_reasoning'>): boolean {
|
|
return (post.ai_confidence ?? 0) > 0 || !!post.ai_reasoning
|
|
}
|
|
|
|
function buildSourceCounts(items: TrumpPost[]): PostListResponse['source_counts'] {
|
|
const counts = new Map<string, { count: number; latest: string | null }>()
|
|
for (const post of items) {
|
|
const hit = counts.get(post.source)
|
|
if (!hit) {
|
|
counts.set(post.source, { count: 1, latest: post.published_at })
|
|
continue
|
|
}
|
|
hit.count += 1
|
|
if (!hit.latest || post.published_at > hit.latest) hit.latest = post.published_at
|
|
}
|
|
return Array.from(counts.entries())
|
|
.map(([source, meta]) => ({ source, count: meta.count, latest: meta.latest }))
|
|
.sort((a, b) => b.count - a.count || a.source.localeCompare(b.source))
|
|
}
|
|
|
|
export function buildPostListFallbackResponse(
|
|
items: TrumpPost[],
|
|
source: string,
|
|
limit: number,
|
|
): PostListResponse {
|
|
return {
|
|
items,
|
|
total: items.length,
|
|
page: 1,
|
|
limit,
|
|
counts: {
|
|
all: items.length,
|
|
actionable: items.filter(p => p.signal === 'buy' || p.signal === 'short').length,
|
|
buy: items.filter(p => p.signal === 'buy').length,
|
|
short: items.filter(p => p.signal === 'short').length,
|
|
off_topic: items.filter(p => !isAiScored(p)).length,
|
|
},
|
|
source_counts: [{ source, count: items.length, latest: items[0]?.published_at ?? null }],
|
|
}
|
|
}
|
|
|
|
export function buildArchiveFallbackResponse(
|
|
allPosts: TrumpPost[],
|
|
page: number,
|
|
limit: number,
|
|
source = 'all',
|
|
): PostListResponse {
|
|
const archivePosts = allPosts.filter(post => !LIVE_ARCHIVE_SOURCES.has(post.source))
|
|
const sourceCounts = buildSourceCounts(archivePosts)
|
|
const filtered = source === 'all'
|
|
? archivePosts
|
|
: archivePosts.filter(post => post.source === source)
|
|
const offset = (page - 1) * limit
|
|
const items = filtered.slice(offset, offset + limit)
|
|
|
|
return {
|
|
items,
|
|
total: filtered.length,
|
|
page,
|
|
limit,
|
|
counts: {
|
|
all: filtered.length,
|
|
actionable: filtered.filter(p => p.signal === 'buy' || p.signal === 'short').length,
|
|
buy: filtered.filter(p => p.signal === 'buy').length,
|
|
short: filtered.filter(p => p.signal === 'short').length,
|
|
off_topic: filtered.filter(p => !isAiScored(p)).length,
|
|
},
|
|
source_counts: sourceCounts,
|
|
}
|
|
}
|
|
|
|
export async function getInitialPostPage(
|
|
limit: number,
|
|
page: number,
|
|
options: {
|
|
source?: string
|
|
filters?: {
|
|
sourceIn?: string[]
|
|
sourceNotIn?: string[]
|
|
archiveOnly?: boolean
|
|
sentiment?: 'bullish' | 'bearish' | 'neutral'
|
|
signal?: 'buy' | 'short' | 'actionable'
|
|
aiScoredOnly?: boolean
|
|
}
|
|
legacyFallbackSource?: string
|
|
legacyFallback?: () => Promise<PostListResponse | null>
|
|
},
|
|
): Promise<PostListResponse | null> {
|
|
return getPostsPage(limit, page, options.source, options.filters).catch(async (e) => {
|
|
// Only fall back to legacy /posts on 404 (new endpoint not yet deployed).
|
|
// Non-404 errors (500, network) should NOT silently mask as empty data —
|
|
// a server-side fallback on 500 produces initial HTML that the client
|
|
// cannot reproduce on re-fetch (it shows an error instead), causing a
|
|
// hydration mismatch. Return null so the page renders empty and the
|
|
// client fetches cleanly on mount.
|
|
const detail = e instanceof Error ? e.message : ''
|
|
if (!detail.includes('404')) return null
|
|
|
|
if (options.legacyFallback) {
|
|
return options.legacyFallback()
|
|
}
|
|
if (!options.legacyFallbackSource || options.filters?.archiveOnly || options.filters?.sourceIn || options.filters?.sourceNotIn) {
|
|
return null
|
|
}
|
|
const fallbackItems = await getPosts(limit, page, options.legacyFallbackSource).catch(() => null)
|
|
return fallbackItems ? buildPostListFallbackResponse(fallbackItems, options.legacyFallbackSource, limit) : null
|
|
})
|
|
}
|