history for users too
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m21s

This commit is contained in:
2026-03-19 16:07:47 -04:00
parent 9a87c0c7c5
commit aa7a80c3e7
5 changed files with 87 additions and 8 deletions
+12
View File
@@ -28,6 +28,7 @@ model User {
managedFunds FundManager[] managedFunds FundManager[]
fund HedgeFund? fund HedgeFund?
fundInvestments FundInvestment[] fundInvestments FundInvestment[]
portfolioHistory UserPortfolioHistory[]
} }
model HedgeFund { model HedgeFund {
@@ -143,6 +144,17 @@ model FundNavHistory {
@@index([fundId, recordedAt]) @@index([fundId, recordedAt])
} }
model UserPortfolioHistory {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
totalValue Float
portfolioValue Float
recordedAt DateTime @default(now())
@@index([userId, recordedAt])
}
model Position { model Position {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
+10 -7
View File
@@ -209,7 +209,7 @@ async function RecentTradesSection({ hashtagId }: { hashtagId: string }) {
where: { hashtagId }, where: { hashtagId },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
take: 20, take: 20,
include: { user: { select: { username: true } } }, include: { user: { select: { username: true, displayUsername: true, isFund: true } } },
}) })
if (trades.length === 0) return null if (trades.length === 0) return null
@@ -230,12 +230,15 @@ async function RecentTradesSection({ hashtagId }: { hashtagId: string }) {
> >
{t.type.replace('_', ' ')} {t.type.replace('_', ' ')}
</span> </span>
<a <div className="flex items-center gap-1">
href={`/profile/${t.user.username}`} {t.user.isFund && <span className="text-indigo-400">🏦</span>}
className="text-slate-400 hover:text-slate-200" <Link
> href={t.user.isFund ? `/fund/${t.user.username.replace('fund:', '')}` : `/profile/${t.user.username}`}
{t.user.username} className="text-slate-400 hover:text-slate-200"
</a> >
{t.user.displayUsername ?? t.user.username}
</Link>
</div>
</div> </div>
<div className="text-right"> <div className="text-right">
<span className="text-slate-300">{formatNumber(t.shares)} sh</span> <span className="text-slate-300">{formatNumber(t.shares)} sh</span>
+21
View File
@@ -9,6 +9,7 @@ import Link from 'next/link'
import { TrendingUp, TrendingDown, Coins, Building2 } from 'lucide-react' import { TrendingUp, TrendingDown, Coins, Building2 } from 'lucide-react'
import ChangePasswordForm from './ChangePasswordForm' import ChangePasswordForm from './ChangePasswordForm'
import AccountSettingsForm from './AccountSettingsForm' import AccountSettingsForm from './AccountSettingsForm'
import { PriceChart } from '@/components/PriceChart'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
@@ -79,6 +80,15 @@ export default async function ProfilePage({ params }: Props) {
}) })
const lotteryWinnings = lotteryAggregate._sum.profit ?? 0 const lotteryWinnings = lotteryAggregate._sum.profit ?? 0
const lotteryCount = lotteryAggregate._count const lotteryCount = lotteryAggregate._count
const portfolioHistory = await prisma.userPortfolioHistory.findMany({
where: {
userId: user.id,
recordedAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
},
orderBy: { recordedAt: 'asc' },
select: { totalValue: true, recordedAt: true },
})
return ( return (
<div className="max-w-4xl mx-auto space-y-8"> <div className="max-w-4xl mx-auto space-y-8">
{/* Header */} {/* Header */}
@@ -140,6 +150,17 @@ export default async function ProfilePage({ params }: Props) {
/> />
</div> </div>
{/* Portfolio value chart */}
{portfolioHistory.length > 0 && (
<div className="bg-surface-card border border-surface-border rounded-xl p-4">
<h2 className="text-sm font-medium text-slate-400 mb-3">Portfolio Value Last 7 Days</h2>
<PriceChart
data={portfolioHistory.map((p) => ({ price: p.totalValue, recordedAt: p.recordedAt.toISOString() }))}
height={200}
/>
</div>
)}
{/* Positions */} {/* Positions */}
{user.positions.length > 0 && ( {user.positions.length > 0 && (
<section> <section>
+43
View File
@@ -281,6 +281,16 @@ const maintenanceWorker = new Worker(
`[maintenance] pruned price history — active: ${deletedActive.count} rows removed (>${activeDays}d), ` + `[maintenance] pruned price history — active: ${deletedActive.count} rows removed (>${activeDays}d), ` +
`inactive: ${deletedInactive.count} rows removed (>${inactiveHours}h)`, `inactive: ${deletedInactive.count} rows removed (>${inactiveHours}h)`,
) )
// ── Snapshot history pruning (7 days for both) ─────────────────────────
const snapshotCutoff = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
const [deletedFundNav, deletedUserPortfolio] = await Promise.all([
prisma.fundNavHistory.deleteMany({ where: { recordedAt: { lt: snapshotCutoff } } }),
prisma.userPortfolioHistory.deleteMany({ where: { recordedAt: { lt: snapshotCutoff } } }),
])
console.log(
`[maintenance] pruned snapshots — fund NAV: ${deletedFundNav.count} rows, user portfolio: ${deletedUserPortfolio.count} rows (>7d)`,
)
}, },
{ connection: redisOpts() }, { connection: redisOpts() },
) )
@@ -329,6 +339,39 @@ const fundNavSnapshotWorker = new Worker(
} }
console.log(`[fund-nav] snapshotted ${funds.length} fund(s)`) console.log(`[fund-nav] snapshotted ${funds.length} fund(s)`)
// ── User portfolio snapshots ──────────────────────────────────────────
const regularUsers = await prisma.user.findMany({
where: { isFund: false },
select: {
id: true,
balance: true,
positions: {
where: { shares: { gt: 0 } },
select: {
shares: true,
positionType: true,
avgBuyPrice: true,
hashtag: { select: { currentPrice: true } },
},
},
},
})
for (const user of regularUsers) {
const portfolioValue = user.positions.reduce((sum, p) => {
const val = p.positionType === 'LONG'
? p.shares * p.hashtag.currentPrice
: p.avgBuyPrice * p.shares - (p.hashtag.currentPrice - p.avgBuyPrice) * p.shares
return sum + val
}, 0)
const totalValue = user.balance + portfolioValue
await prisma.userPortfolioHistory.create({
data: { userId: user.id, totalValue, portfolioValue },
})
}
console.log(`[fund-nav] snapshotted ${regularUsers.length} user portfolio(s)`)
}, },
{ connection: redisOpts() }, { connection: redisOpts() },
) )
+1 -1
View File
File diff suppressed because one or more lines are too long