refactor(dashboard): /positions hands off to the dedicated tracker

angel.manohargupta.com (standalone position-tracker) is the single owner of
live positions UI; the replicated page here drifted against the same Angel
One data. Page now auto-redirects with a fallback link.
This commit is contained in:
Manohar 2026-06-10 14:59:41 +00:00
parent 03123d1ff7
commit 0142b1bfe7

View file

@ -1,230 +1,47 @@
"use client"
import * as React from "react"
import { TrendingUp, TrendingDown, RefreshCw, Activity, DollarSign, BarChart2, Layers } from "lucide-react"
import { StatCard } from "@/components/stat-card"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
/**
* /positions intentionally NOT a positions UI.
*
* Live positions have a dedicated home: the standalone position-tracker at
* https://angel.manohargupta.com (its own repo, own deploy, market-hours
* aware). This dashboard previously replicated that UI here, which meant
* two implementations drifting against the same Angel One data. One owner
* per concern: the tracker owns positions; this page just hands you over.
*/
interface Position {
key: string
tradingsymbol: string
exchange: string
instrumenttype: string
producttype: string
netqty: number
ltp: number
avg_price: number
unrealised_pnl: number
realised_pnl: number
total_pnl: number
is_closed: number
updated_at: string
}
import { useEffect } from "react"
import { ExternalLink, TrendingUp } from "lucide-react"
import { Card } from "@/components/ui/card"
interface Summary {
totalUnrealised: number
totalRealised: number
totalPnl: number
openPositions: number
asOf?: string
}
function fmt(n: number) {
const sign = n >= 0 ? "+" : ""
return `${sign}${Math.abs(n).toLocaleString("en-IN", { maximumFractionDigits: 0 })}`
}
function PnlCell({ value }: { value: number }) {
return (
<span className={cn("font-mono tabular-nums", value > 0 ? "text-emerald-500" : value < 0 ? "text-rose-500" : "text-muted-foreground")}>
{fmt(value)}
</span>
)
}
const TRACKER_URL = "https://angel.manohargupta.com"
export default function PositionsPage() {
const [positions, setPositions] = React.useState<Position[]>([])
const [summary, setSummary] = React.useState<Summary | null>(null)
const [loading, setLoading] = React.useState(true)
const [refreshing, setRefreshing] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
const [lastUpdated, setLastUpdated] = React.useState<Date | null>(null)
const load = React.useCallback(async (silent = false) => {
if (!silent) setLoading(true)
setError(null)
try {
const res = await fetch("/api/positions", { cache: "no-store" })
const data = await res.json()
if (!data.ok) throw new Error(data.error ?? "Failed to load")
setPositions(data.positions ?? [])
setSummary(data.summary ?? null)
setLastUpdated(new Date())
} catch (e: any) {
setError(e.message)
} finally {
setLoading(false)
}
useEffect(() => {
// Auto-redirect after a beat — the card below is the no-JS / slow-net fallback.
const t = setTimeout(() => {
window.location.href = TRACKER_URL
}, 800)
return () => clearTimeout(t)
}, [])
const handleRefresh = async () => {
setRefreshing(true)
try {
await fetch("/api/positions", { method: "POST" })
} catch { /* ignore */ }
await load(true)
setRefreshing(false)
}
React.useEffect(() => {
load()
const id = setInterval(() => load(true), 30_000)
return () => clearInterval(id)
}, [load])
const open = positions.filter(p => p.netqty !== 0 && !p.is_closed)
const closed = positions.filter(p => p.netqty === 0 && p.is_closed && p.realised_pnl !== 0)
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<BarChart2 className="h-6 w-6 text-primary" />
Positions
</h1>
<p className="text-muted-foreground text-sm">
{lastUpdated ? `Updated ${lastUpdated.toLocaleTimeString("en-IN", { timeZone: "Asia/Kolkata", hour12: false })} IST` : "Live positions from Angel One"}
<div className="flex min-h-[60vh] items-center justify-center p-6">
<Card className="bg-card/40 p-8 max-w-md text-center">
<TrendingUp className="h-8 w-8 text-primary mx-auto mb-4" />
<h1 className="text-lg font-semibold mb-2">Positions live on the tracker</h1>
<p className="text-sm text-muted-foreground mb-6">
Redirecting you to the dedicated position tracker single source of
truth for live P&amp;L, bands and alerts.
</p>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={refreshing || loading}>
<RefreshCw className={cn("h-4 w-4 mr-2", refreshing && "animate-spin")} />
Refresh
</Button>
</div>
{error && (
<Card className="border-rose-500/30 bg-rose-500/10">
<CardContent className="pt-4 text-sm text-rose-400">{error}</CardContent>
<a
href={TRACKER_URL}
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity"
>
Open angel.manohargupta.com
<ExternalLink className="h-4 w-4" />
</a>
</Card>
)}
{/* Summary stat cards */}
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
<StatCard
title="Total P&L"
value={summary ? fmt(summary.totalPnl) : "—"}
icon={summary && summary.totalPnl >= 0 ? TrendingUp : TrendingDown}
className={summary && summary.totalPnl < 0 ? "border-rose-500/30" : "border-emerald-500/30"}
/>
<StatCard
title="Unrealised"
value={summary ? fmt(summary.totalUnrealised) : "—"}
icon={Activity}
description="Open positions"
/>
<StatCard
title="Realised"
value={summary ? fmt(summary.totalRealised) : "—"}
icon={DollarSign}
description="Closed today"
/>
<StatCard
title="Open Positions"
value={loading ? "…" : open.length}
icon={Layers}
description={closed.length > 0 ? `${closed.length} closed today` : undefined}
/>
</div>
{/* Open positions table */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Open Positions ({open.length})</CardTitle>
</CardHeader>
<CardContent className="p-0">
{loading ? (
<div className="p-8 text-center text-muted-foreground text-sm">Loading</div>
) : open.length === 0 ? (
<div className="p-8 text-center text-muted-foreground text-sm">No open positions</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-muted-foreground text-xs">
<th className="px-4 py-2 text-left font-medium">Symbol</th>
<th className="px-4 py-2 text-right font-medium">Qty</th>
<th className="px-4 py-2 text-right font-medium">Avg</th>
<th className="px-4 py-2 text-right font-medium">LTP</th>
<th className="px-4 py-2 text-right font-medium">Unrealised</th>
<th className="px-4 py-2 text-right font-medium">Total P&L</th>
<th className="px-4 py-2 text-left font-medium">Type</th>
</tr>
</thead>
<tbody className="divide-y">
{open.map(p => (
<tr key={p.key} className="hover:bg-muted/30 transition-colors">
<td className="px-4 py-2.5 font-mono font-medium">{p.tradingsymbol}</td>
<td className="px-4 py-2.5 text-right tabular-nums">
<span className={p.netqty > 0 ? "text-emerald-500" : "text-rose-500"}>
{p.netqty > 0 ? "+" : ""}{p.netqty}
</span>
</td>
<td className="px-4 py-2.5 text-right font-mono tabular-nums">{p.avg_price.toFixed(2)}</td>
<td className="px-4 py-2.5 text-right font-mono tabular-nums">{p.ltp.toFixed(2)}</td>
<td className="px-4 py-2.5 text-right"><PnlCell value={p.unrealised_pnl} /></td>
<td className="px-4 py-2.5 text-right"><PnlCell value={p.total_pnl} /></td>
<td className="px-4 py-2.5">
<Badge variant="secondary" className="text-xs font-normal">
{p.instrumenttype || p.producttype}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
{/* Closed today */}
{!loading && closed.length > 0 && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base text-muted-foreground">Closed Today ({closed.length})</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-muted-foreground text-xs">
<th className="px-4 py-2 text-left font-medium">Symbol</th>
<th className="px-4 py-2 text-right font-medium">Realised P&L</th>
<th className="px-4 py-2 text-left font-medium">Type</th>
</tr>
</thead>
<tbody className="divide-y">
{closed.map(p => (
<tr key={p.key} className="hover:bg-muted/30 transition-colors">
<td className="px-4 py-2.5 font-mono font-medium text-muted-foreground">{p.tradingsymbol}</td>
<td className="px-4 py-2.5 text-right"><PnlCell value={p.realised_pnl} /></td>
<td className="px-4 py-2.5">
<Badge variant="outline" className="text-xs font-normal opacity-60">
{p.instrumenttype || p.producttype}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
</div>
)
}