refactor: enhance stock change display and improve hashtag card status logic and timeline fetch logic
Build Images and Deploy / Update-PROD-Stack (push) Failing after 32s

This commit is contained in:
2026-03-19 15:00:46 -04:00
parent fdb234641f
commit 40a1034000
3 changed files with 18 additions and 18 deletions
+5 -7
View File
@@ -1,5 +1,5 @@
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { formatCurrency } from '@/lib/utils' import { formatCurrency, pnlColor } from '@/lib/utils'
import Link from 'next/link' import Link from 'next/link'
import { ArrowUp, ArrowDown, ArrowUpDown, BarChart2, Building2 } from 'lucide-react' import { ArrowUp, ArrowDown, ArrowUpDown, BarChart2, Building2 } from 'lucide-react'
import { formatDistanceToNow } from 'date-fns' import { formatDistanceToNow } from 'date-fns'
@@ -265,8 +265,6 @@ export default async function StocksPage({ searchParams }: PageProps) {
const prev = stock.previousPrice const prev = stock.previousPrice
const change = prev != null ? stock.currentPrice - prev : null const change = prev != null ? stock.currentPrice - prev : null
const changePct = prev != null && prev > 0 ? ((stock.currentPrice - prev) / prev) * 100 : null const changePct = prev != null && prev > 0 ? ((stock.currentPrice - prev) / prev) * 100 : null
const up = change == null ? null : change >= 0
return ( return (
<div <div
key={stock.id} key={stock.id}
@@ -296,11 +294,11 @@ export default async function StocksPage({ searchParams }: PageProps) {
<span className="text-slate-600 text-xs"></span> <span className="text-slate-600 text-xs"></span>
) : ( ) : (
<div> <div>
<p className={`text-sm ${up ? 'text-emerald-400' : 'text-red-400'}`}> <p className={`text-sm ${pnlColor(change)}`}>
{up ? '+' : ''}{formatCurrency(change)} {change > 0 ? '+' : ''}{formatCurrency(change)}
</p> </p>
<p className={`text-xs ${up ? 'text-emerald-400' : 'text-red-400'}`}> <p className={`text-xs ${pnlColor(changePct)}`}>
{up ? '+' : ''}{changePct!.toFixed(2)}% {changePct! > 0 ? '+' : ''}{changePct!.toFixed(2)}%
</p> </p>
</div> </div>
)} )}
+8 -6
View File
@@ -1,5 +1,5 @@
import Link from 'next/link' import Link from 'next/link'
import { TrendingUp, TrendingDown } from 'lucide-react' import { TrendingUp, TrendingDown, Minus } from 'lucide-react'
import { formatCurrency } from '@/lib/utils' import { formatCurrency } from '@/lib/utils'
interface Props { interface Props {
@@ -16,7 +16,7 @@ export function HashtagCard({ tag, displayTag, currentPrice, previousPrice, post
? ((currentPrice - previousPrice) / previousPrice) * 100 ? ((currentPrice - previousPrice) / previousPrice) * 100
: null : null
const up = pctChange === null ? null : pctChange >= 0 const up = pctChange === null ? null : pctChange > 0 ? 'up' : pctChange < 0 ? 'down' : 'flat'
return ( return (
<Link <Link
@@ -36,14 +36,16 @@ export function HashtagCard({ tag, displayTag, currentPrice, previousPrice, post
<p className="font-bold text-sm">{formatCurrency(currentPrice)}</p> <p className="font-bold text-sm">{formatCurrency(currentPrice)}</p>
{pctChange !== null && ( {pctChange !== null && (
<div <div
className={`flex items-center justify-end gap-0.5 text-xs mt-0.5 ${up ? 'text-emerald-400' : 'text-red-400'}`} className={`flex items-center justify-end gap-0.5 text-xs mt-0.5 ${up === 'up' ? 'text-emerald-400' : up === 'down' ? 'text-red-400' : 'text-slate-400'}`}
> >
{up ? ( {up === 'up' ? (
<TrendingUp className="h-3 w-3" /> <TrendingUp className="h-3 w-3" />
) : ( ) : up === 'down' ? (
<TrendingDown className="h-3 w-3" /> <TrendingDown className="h-3 w-3" />
) : (
<Minus className="h-3 w-3" />
)} )}
{up ? '+' : ''} {up === 'up' ? '+' : ''}
{pctChange.toFixed(1)}% {pctChange.toFixed(1)}%
</div> </div>
)} )}
+5 -5
View File
@@ -31,12 +31,11 @@ function extractTagsFromHtml(html: string): string[] {
return results return results
} }
async function fetchPage(tag: string, maxId?: string): Promise<TimelineResult> { async function fetchPage(tag: string, maxId?: string, postLimit = 20): Promise<TimelineResult> {
const instance = process.env.MASTODON_INSTANCE const instance = process.env.MASTODON_INSTANCE
if (!instance) throw new Error('MASTODON_INSTANCE is not configured') if (!instance) throw new Error('MASTODON_INSTANCE is not configured')
let url = `${instance}/api/v1/timelines/tag/${encodeURIComponent(tag)}` let url = `${instance}/api/v1/timelines/tag/${encodeURIComponent(tag)}?limit=${postLimit}`
// ?limit=50 was here but it seemed too aggressive. Default is 20 which feels more balanced for pricing purposes and reduces risk of hitting rate limits on very active tags.
if (maxId) url += `&max_id=${maxId}` if (maxId) url += `&max_id=${maxId}`
const headers: HeadersInit = { Accept: 'application/json' } const headers: HeadersInit = { Accept: 'application/json' }
@@ -83,6 +82,7 @@ export async function getPostsData(
tag: string, tag: string,
): Promise<{ postsPerHour: number; relatedTags: string[]; displayTag?: string }> { ): Promise<{ postsPerHour: number; relatedTags: string[]; displayTag?: string }> {
const maxPages = parseInt(process.env.MAX_PAGES_PER_HASHTAG ?? '5', 10) const maxPages = parseInt(process.env.MAX_PAGES_PER_HASHTAG ?? '5', 10)
const postLimit = Math.min(parseInt(process.env.MASTODON_POST_LIMIT ?? '20', 10), 40)
const ONE_HOUR_MS = 60 * 60 * 1000 const ONE_HOUR_MS = 60 * 60 * 1000
const now = Date.now() const now = Date.now()
const cutoff = now - ONE_HOUR_MS const cutoff = now - ONE_HOUR_MS
@@ -91,13 +91,13 @@ export async function getPostsData(
let maxId: string | undefined let maxId: string | undefined
for (let page = 0; page < maxPages; page++) { for (let page = 0; page < maxPages; page++) {
const { posts, nextMaxId } = await fetchPage(tag, maxId) const { posts, nextMaxId } = await fetchPage(tag, maxId, postLimit)
if (posts.length === 0) break if (posts.length === 0) break
allPosts = [...allPosts, ...posts] allPosts = [...allPosts, ...posts]
// End of timeline or no more pages // End of timeline or no more pages
if (posts.length < 40 || !nextMaxId) break if (posts.length < postLimit || !nextMaxId) break
// If the oldest post in this batch is already beyond 1 hour, we have a full window // If the oldest post in this batch is already beyond 1 hour, we have a full window
const oldestInBatch = Math.min(...posts.map((p) => new Date(p.created_at).getTime())) const oldestInBatch = Math.min(...posts.map((p) => new Date(p.created_at).getTime()))