diff --git a/src/app/page.tsx b/src/app/page.tsx index 3ccaefa..6260335 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,9 +2,13 @@ import { prisma } from '@/lib/prisma' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { HashtagCard } from '@/components/HashtagCard' -import { TrendingUp, Users, Hash } from 'lucide-react' +import { TrendingUp, Users, Hash, AlertTriangle } from 'lucide-react' import Link from 'next/link' import { formatPnl, pnlColor } from '@/lib/utils' +import { formatDistanceToNow } from 'date-fns' + +const ZOMBIE_ZERO_COUNT = parseInt(process.env.ZOMBIE_ZERO_COUNT ?? '1000', 10) +const PRICE_UPDATE_INTERVAL_MINUTES = parseInt(process.env.PRICE_UPDATE_INTERVAL_MINUTES ?? '60', 10) export const dynamic = 'force-dynamic' export const revalidate = 0 @@ -59,10 +63,33 @@ async function getHoldings(userId: string) { } } +async function getZombieWarnings(userId: string) { + const threshold = Math.floor(ZOMBIE_ZERO_COUNT * 0.9) + const positions = await prisma.position.findMany({ + where: { userId, shares: { gt: 0 }, hashtag: { zeroCount: { gte: threshold } } }, + select: { hashtag: { select: { tag: true, displayTag: true, zeroCount: true } } }, + }) + return positions.map((p) => ({ + tag: p.hashtag.tag, + displayTag: p.hashtag.displayTag, + zombieSince: formatDistanceToNow( + new Date(Date.now() - p.hashtag.zeroCount * PRICE_UPDATE_INTERVAL_MINUTES * 60 * 1000), + ), + zombieEta: formatDistanceToNow( + new Date(Date.now() + (ZOMBIE_ZERO_COUNT - p.hashtag.zeroCount) * PRICE_UPDATE_INTERVAL_MINUTES * 60 * 1000), + { addSuffix: true }, + ), + })) +} + export default async function HomePage() { const session = await getServerSession(authOptions) - const [{ userCount, hashtagCount, tradeCount, topHashtags, recentTrades }, holdings] = - await Promise.all([getStats(), session ? getHoldings(session.user.id) : Promise.resolve(null)]) + const [{ userCount, hashtagCount, tradeCount, topHashtags, recentTrades }, holdings, zombieWarnings] = + await Promise.all([ + getStats(), + session ? getHoldings(session.user.id) : Promise.resolve(null), + session ? getZombieWarnings(session.user.id) : Promise.resolve([]), + ]) return (
@@ -118,6 +145,34 @@ export default async function HomePage() { } label="Trades executed" value={tradeCount.toLocaleString()} />
+ {/* Zombie / at-risk position warnings */} + {zombieWarnings.length > 0 && ( +
+

+ + At-risk positions +

+

+ The following hashtags have had no activity for an extended period and may be auto-liquidated soon. + Consider closing these positions manually. +

+
+ {zombieWarnings.map((w) => ( + + + #{w.displayTag} + · liquidates {w.zombieEta} + + ))} +
+
+ )} + {/* Holdings summary — biggest gain + biggest loss for signed-in users */} {holdings && (holdings.biggestGain ?? holdings.biggestLoss) && (
diff --git a/src/app/positions/page.tsx b/src/app/positions/page.tsx index afa7586..dc19305 100644 --- a/src/app/positions/page.tsx +++ b/src/app/positions/page.tsx @@ -4,11 +4,7 @@ import { authOptions } from '@/lib/auth' import { redirect } from 'next/navigation' import { formatCurrency, formatNumber, formatPnl, pnlColor } from '@/lib/utils' import Link from 'next/link' -import { Coins, AlertTriangle } from 'lucide-react' -import { formatDistanceToNow } from 'date-fns' - -const ZOMBIE_ZERO_COUNT = parseInt(process.env.ZOMBIE_ZERO_COUNT ?? '1000', 10) -const PRICE_UPDATE_INTERVAL_MINUTES = parseInt(process.env.PRICE_UPDATE_INTERVAL_MINUTES ?? '60', 10) +import { Coins } from 'lucide-react' export const dynamic = 'force-dynamic' @@ -54,7 +50,6 @@ export default async function PositionsPage() { tag: true, displayTag: true, currentPrice: true, - zeroCount: true, priceHistory: { orderBy: { recordedAt: 'asc' }, take: 20, @@ -103,18 +98,6 @@ export default async function PositionsPage() { const currentValue = pos.hashtag.currentPrice * pos.shares const pnlPct = costBasis > 0 ? (pnl / costBasis) * 100 : 0 const sparkPrices = pos.hashtag.priceHistory.map((h) => h.price) - const isZombieWarning = pos.hashtag.zeroCount >= ZOMBIE_ZERO_COUNT * 0.9 - const zombieSince = isZombieWarning - ? formatDistanceToNow( - new Date(Date.now() - pos.hashtag.zeroCount * PRICE_UPDATE_INTERVAL_MINUTES * 60 * 1000), - ) - : null - const zombieEta = isZombieWarning - ? formatDistanceToNow( - new Date(Date.now() + (ZOMBIE_ZERO_COUNT - pos.hashtag.zeroCount) * PRICE_UPDATE_INTERVAL_MINUTES * 60 * 1000), - { addSuffix: true }, - ) - : null return (
#{pos.hashtag.displayTag} - {isZombieWarning && zombieSince && zombieEta && ( - - - No activity for {zombieSince} · Liquidates {zombieEta} - - )}