fix: Dashboard cycle coherence — scoring_run_id linkage + cycle-scoped cards
- Backend: get_status() now resolves scoring_run_id by querying trade_entry_prices within the cycle time window, fixing the mismatch between cycle run_id and the id actually written to trades - Dashboard: Trades du cycle filters by scoring_run_id (no stale fallback) - Dashboard: Pattern du cycle shows only patterns added in last cycle (created_at >= started_at), renamed from Top Patterns - Dashboard: Dernier Cycle now shows 4 stats (patterns/scorés/loggés/fermés) + IA commentary snippet - Dashboard: P&L simulated mode bottom half shows open/closed/capital/profit - Dashboard: Régime Macro shows top 4 scenario score bars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1090,6 +1090,25 @@ def get_status() -> Dict[str, Any]:
|
||||
recent = get_cycle_runs(limit=1)
|
||||
last = recent[0] if recent else None
|
||||
|
||||
# Enrich last_cycle with the scoring_run_id used for trade_entry_prices
|
||||
if last:
|
||||
try:
|
||||
from services.database import get_conn
|
||||
_conn = get_conn()
|
||||
started = (last.get("started_at") or "").replace(" ", "T")
|
||||
completed = last.get("completed_at") or ""
|
||||
if started and completed:
|
||||
row = _conn.execute(
|
||||
"SELECT run_id FROM trade_entry_prices WHERE run_id >= ? AND run_id <= ?"
|
||||
" ORDER BY run_id DESC LIMIT 1",
|
||||
(started, completed),
|
||||
).fetchone()
|
||||
if row:
|
||||
last = dict(last)
|
||||
last["scoring_run_id"] = row["run_id"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
**_current_status,
|
||||
"enabled": enabled,
|
||||
|
||||
@@ -32,6 +32,12 @@ const ASSET_COLORS: Record<string, string> = {
|
||||
rates: '#64748b', unknown: '#334155',
|
||||
}
|
||||
|
||||
const REGIME_LABELS: Record<string, string> = {
|
||||
goldilocks: 'Goldilocks', desinflation: 'Désinflation', stagflation: 'Stagflation',
|
||||
recession: 'Récession', crise_liquidite: 'Crise liq.', reflation: 'Reflation',
|
||||
soft_landing: 'Soft Landing', inflation_shock: 'Infl. Choc', incertain: 'Incertain',
|
||||
}
|
||||
|
||||
function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) {
|
||||
const click = (v: 'simulated' | 'portfolio') => (e: React.MouseEvent) => {
|
||||
e.preventDefault(); e.stopPropagation(); onChange(v)
|
||||
@@ -113,7 +119,9 @@ export default function Dashboard() {
|
||||
const dom = sc.dominant ?? 'incertain'
|
||||
const m = sc.meta?.[dom] ?? { label: dom, color: '#94a3b8', emoji: '?' }
|
||||
const assetBias: Record<string, string> = sc.asset_bias?.[dom] ?? {}
|
||||
return { dominant: dom, label: m.label, color: m.color, emoji: m.emoji, assetBias }
|
||||
// Top 3 scenario scores (excluding dominant, sorted desc)
|
||||
const ranked: [string, number][] = (sc.ranked ?? []).slice(0, 4)
|
||||
return { dominant: dom, label: m.label, color: m.color, emoji: m.emoji, assetBias, ranked }
|
||||
}, [macroData])
|
||||
|
||||
const gauge = riskScore ? riskGauge(riskScore.score) : null
|
||||
@@ -121,19 +129,21 @@ export default function Dashboard() {
|
||||
? Object.entries(riskScore.breakdown).map(([k, v]) => ({ subject: k.replace('_', ' '), score: v }))
|
||||
: []
|
||||
|
||||
// Last cycle run_id for filtering trades
|
||||
const lastRunId: string | null = (cycleStatusData as any)?.last_cycle?.run_id ?? null
|
||||
// Cycle trades: use scoring_run_id (which differs from cycle run_id)
|
||||
const lastCycle = (cycleStatusData as any)?.last_cycle ?? null
|
||||
const scoringRunId: string | null = lastCycle?.scoring_run_id ?? null
|
||||
const mtmTrades: any[] = (tradeMtmData as any)?.trades ?? []
|
||||
const cycleTradesAll = lastRunId ? mtmTrades.filter((t: any) => t.run_id === lastRunId) : []
|
||||
const cycleTrades = cycleTradesAll.length > 0 ? cycleTradesAll : mtmTrades.slice(0, 4)
|
||||
const cycleTrades = scoringRunId ? mtmTrades.filter((t: any) => t.run_id === scoringRunId) : []
|
||||
|
||||
// Top scored patterns
|
||||
const topScoredPatterns = useMemo(() =>
|
||||
[...allPatterns]
|
||||
.filter(p => scoreMap[p.id]?.score != null)
|
||||
.sort((a, b) => (scoreMap[b.id]?.score ?? 0) - (scoreMap[a.id]?.score ?? 0))
|
||||
// Patterns from the last cycle only (filter by created_at >= cycle started_at)
|
||||
const cyclePatterns = useMemo(() => {
|
||||
const started = lastCycle?.started_at ?? null
|
||||
if (!started) return []
|
||||
return [...allPatterns]
|
||||
.filter(p => p.created_at && p.created_at >= started)
|
||||
.sort((a, b) => (scoreMap[b.id]?.score ?? -1) - (scoreMap[a.id]?.score ?? -1))
|
||||
.slice(0, 4)
|
||||
, [allPatterns, scoreMap])
|
||||
}, [allPatterns, scoreMap, lastCycle])
|
||||
|
||||
// Top impactful news
|
||||
const topNews = useMemo(() =>
|
||||
@@ -302,6 +312,11 @@ export default function Dashboard() {
|
||||
: null
|
||||
const winners = withPnl.filter((t: any) => t.pnl_pct > 0).length
|
||||
const losers = withPnl.filter((t: any) => t.pnl_pct < 0).length
|
||||
const closedTrades = trades.filter((t: any) => t.status === 'closed')
|
||||
const openTrades = trades.filter((t: any) => t.status !== 'closed')
|
||||
const totalCapital = trades.reduce((s: number, t: any) => s + (t.capital_invested ?? 0), 0)
|
||||
const totalProfit = withPnl.reduce((s: number, t: any) =>
|
||||
s + ((t.capital_invested ?? 0) * t.pnl_pct / 100), 0)
|
||||
const targetHit = trades.filter((t: any) => t.alert_type === 'target_reached').length
|
||||
const stopHit = trades.filter((t: any) => t.alert_type === 'stop_loss').length
|
||||
|
||||
@@ -321,23 +336,49 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
{pnlView === 'simulated' ? (
|
||||
<div className="flex items-end justify-between mt-1">
|
||||
<div>
|
||||
<div className={clsx('text-2xl font-bold font-mono',
|
||||
avgPnl === null ? 'text-slate-500' : avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'}
|
||||
<>
|
||||
<div className="flex items-end justify-between mt-1">
|
||||
<div>
|
||||
<div className={clsx('text-2xl font-bold font-mono',
|
||||
avgPnl === null ? 'text-slate-500' : avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{trades.length} trades · <span className="text-emerald-500">{winners}✓</span>{' '}
|
||||
<span className="text-red-400">{losers}✗</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{trades.length} trades · <span className="text-emerald-500">{winners}✓</span>{' '}
|
||||
<span className="text-red-400">{losers}✗</span>
|
||||
<div className="text-right space-y-0.5">
|
||||
{targetHit > 0 && <div className="text-[10px] text-emerald-400">🎯 {targetHit} cible{targetHit > 1 ? 's' : ''}</div>}
|
||||
{stopHit > 0 && <div className="text-[10px] text-red-400">⛔ {stopHit} stop</div>}
|
||||
<div className="text-[10px] text-slate-600">{withPnl.length} pricés</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right space-y-0.5">
|
||||
{targetHit > 0 && <div className="text-[10px] text-emerald-400">🎯 {targetHit} cible{targetHit > 1 ? 's' : ''}</div>}
|
||||
{stopHit > 0 && <div className="text-[10px] text-red-400">⛔ {stopHit} stop</div>}
|
||||
<div className="text-[10px] text-slate-600">{withPnl.length} pricés</div>
|
||||
<div className="mt-2 pt-2 border-t border-slate-700/30 flex justify-between gap-2">
|
||||
<div className="text-center">
|
||||
<div className="text-[10px] text-slate-600">Ouvert</div>
|
||||
<div className="text-xs font-bold text-slate-300">{openTrades.length}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-[10px] text-slate-600">Fermé</div>
|
||||
<div className="text-xs font-bold text-slate-400">{closedTrades.length}</div>
|
||||
</div>
|
||||
{totalCapital > 0 && (
|
||||
<div className="text-center">
|
||||
<div className="text-[10px] text-slate-600">Capital</div>
|
||||
<div className="text-xs font-bold text-slate-300 font-mono">{totalCapital.toFixed(0)}€</div>
|
||||
</div>
|
||||
)}
|
||||
{totalCapital > 0 && (
|
||||
<div className="text-center">
|
||||
<div className="text-[10px] text-slate-600">Profit</div>
|
||||
<div className={clsx('text-xs font-bold font-mono', totalProfit >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{totalProfit >= 0 ? '+' : ''}{totalProfit.toFixed(0)}€
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-end justify-between mt-1">
|
||||
<div>
|
||||
@@ -375,7 +416,6 @@ export default function Dashboard() {
|
||||
.sort(([, a], [, b]) => (b as any).pct - (a as any).pct)
|
||||
.slice(0, 5)
|
||||
|
||||
const pf = portfolio as any
|
||||
return (
|
||||
<Link to="/risk" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
@@ -388,7 +428,6 @@ export default function Dashboard() {
|
||||
<div className={clsx('text-lg font-bold mt-0.5', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}>
|
||||
{alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
|
||||
</div>
|
||||
{/* Allocation bars */}
|
||||
{sortedClasses.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{sortedClasses.map(([cls, data]: [string, any]) => (
|
||||
@@ -425,7 +464,7 @@ export default function Dashboard() {
|
||||
|
||||
{/* Dernier Cycle — bilan complet */}
|
||||
{(() => {
|
||||
const last = (cycleStatusData as any)?.last_cycle
|
||||
const last = lastCycle
|
||||
const ts = last?.ts
|
||||
? new Date(last.ts.endsWith('Z') ? last.ts : last.ts + 'Z')
|
||||
: null
|
||||
@@ -436,9 +475,11 @@ export default function Dashboard() {
|
||||
: `${Math.round(elapsed / 1440)}j`
|
||||
const patternsAdded: number = last?.patterns_added ?? 0
|
||||
const patternsScored: number = last?.patterns_scored ?? 0
|
||||
const tradesLogged: number = lastRunId
|
||||
? mtmTrades.filter((t: any) => t.run_id === lastRunId).length
|
||||
: 0
|
||||
const tradesLogged: number = cycleTrades.length
|
||||
const tradesClosed = cycleTrades.filter((t: any) => t.status === 'closed').length
|
||||
const commentary = last?.commentary
|
||||
? (() => { try { return typeof last.commentary === 'string' ? JSON.parse(last.commentary) : last.commentary } catch { return null } })()
|
||||
: null
|
||||
return (
|
||||
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
@@ -447,20 +488,31 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<div className="text-xl font-bold text-blue-400 mt-0.5 font-mono">{elapsedStr}</div>
|
||||
{last ? (
|
||||
<div className="mt-1.5 grid grid-cols-3 gap-1 text-center">
|
||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||
<div className="text-sm font-bold text-slate-200">+{patternsAdded}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight">patterns</div>
|
||||
<>
|
||||
<div className="mt-1.5 grid grid-cols-4 gap-1 text-center">
|
||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||
<div className="text-sm font-bold text-slate-200">+{patternsAdded}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight">patterns</div>
|
||||
</div>
|
||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||
<div className="text-sm font-bold text-emerald-400">{patternsScored}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight">scorés</div>
|
||||
</div>
|
||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||
<div className="text-sm font-bold text-blue-400">{tradesLogged}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight">loggés</div>
|
||||
</div>
|
||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||
<div className="text-sm font-bold text-slate-500">{tradesClosed}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight">fermés</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||
<div className="text-sm font-bold text-emerald-400">{patternsScored}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight">scorés</div>
|
||||
</div>
|
||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||
<div className="text-sm font-bold text-blue-400">{tradesLogged}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight">loggés</div>
|
||||
</div>
|
||||
</div>
|
||||
{commentary?.commentary && (
|
||||
<div className="mt-1.5 text-[9px] text-slate-500 line-clamp-2 italic leading-tight">
|
||||
{commentary.commentary.slice(0, 100)}…
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Aucun cycle enregistré</div>
|
||||
)}
|
||||
@@ -471,24 +523,46 @@ export default function Dashboard() {
|
||||
{/* Régime Macro */}
|
||||
{(() => {
|
||||
const dom = macroInfo?.dominant
|
||||
const colorClass = dom === 'growth' ? 'text-emerald-400'
|
||||
: dom === 'stagflation' || dom === 'recession' ? 'text-red-400'
|
||||
: dom === 'deflation' ? 'text-blue-300'
|
||||
const colorClass = dom === 'growth' || dom === 'goldilocks' || dom === 'soft_landing' ? 'text-emerald-400'
|
||||
: dom === 'stagflation' || dom === 'recession' || dom === 'crise_liquidite' ? 'text-red-400'
|
||||
: dom === 'deflation' || dom === 'desinflation' ? 'text-blue-300'
|
||||
: dom === 'inflation_shock' ? 'text-orange-400'
|
||||
: 'text-slate-400'
|
||||
const ranked = macroInfo?.ranked ?? []
|
||||
return (
|
||||
<Link to="/macro" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🌐 Régime Macro</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className={clsx('text-lg font-bold mt-1', colorClass)}>
|
||||
<div className={clsx('text-base font-bold mt-1', colorClass)}>
|
||||
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1 line-clamp-1">
|
||||
{macroInfo
|
||||
? Object.entries(macroInfo.assetBias).slice(0, 2).map(([k, v]) => `${k}→${v}`).join(' · ')
|
||||
: 'Chargement...'}
|
||||
</div>
|
||||
{ranked.length > 0 && (
|
||||
<div className="mt-1.5 space-y-1">
|
||||
{ranked.slice(0, 4).map(([key, score]: [string, number]) => {
|
||||
const maxScore = ranked[0]?.[1] ?? 1
|
||||
const pct = Math.round((score / Math.max(maxScore, 1)) * 100)
|
||||
const isDom = key === dom
|
||||
return (
|
||||
<div key={key} className="flex items-center gap-1.5">
|
||||
<span className={clsx('text-[9px] w-16 truncate', isDom ? 'text-slate-200 font-semibold' : 'text-slate-500')}>
|
||||
{REGIME_LABELS[key] ?? key}
|
||||
</span>
|
||||
<div className="flex-1 h-1.5 bg-dark-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={clsx('h-full rounded-full transition-all', isDom ? 'bg-blue-500' : 'bg-slate-600')}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className={clsx('text-[9px] font-mono w-5 text-right', isDom ? 'text-blue-400' : 'text-slate-600')}>
|
||||
{score}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
@@ -526,28 +600,28 @@ export default function Dashboard() {
|
||||
|
||||
{/* Trades du dernier cycle — détaillé */}
|
||||
{(() => {
|
||||
const last = (cycleStatusData as any)?.last_cycle
|
||||
const added: number = last?.patterns_added ?? 0
|
||||
const patternsAdded: number = lastCycle?.patterns_added ?? 0
|
||||
return (
|
||||
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">📥 Trades du cycle</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className="text-xl font-bold text-emerald-400 font-mono">+{added}</div>
|
||||
<div className="text-xl font-bold text-emerald-400 font-mono">+{patternsAdded} patterns</div>
|
||||
{cycleTrades.length > 0 ? (
|
||||
<div className="space-y-1 mt-1.5">
|
||||
{cycleTrades.slice(0, 3).map((t: any, i) => {
|
||||
const pnl: number | null = t.pnl_pct ?? null
|
||||
const isBear = t.direction === 'bearish'
|
||||
const ev: number | null = t.ev_net ?? t.ev_at_entry ?? null
|
||||
const isBear = t.strategy?.toLowerCase().includes('put') || t.strategy?.toLowerCase().includes('bear')
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-1.5 text-[10px]">
|
||||
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
|
||||
<span className="font-mono text-slate-300 font-semibold w-10 truncate">{t.underlying ?? '—'}</span>
|
||||
<span className="text-slate-600 truncate flex-1">{t.strategy ?? ''}</span>
|
||||
{pnl !== null && (
|
||||
<span className={clsx('font-mono font-bold shrink-0', pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
|
||||
{ev !== null && (
|
||||
<span className={clsx('font-mono shrink-0 text-[9px]', ev >= 0.5 ? 'text-emerald-400' : ev >= 0 ? 'text-yellow-400' : 'text-red-400')}>
|
||||
EV{ev >= 0 ? '+' : ''}{ev.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
{t.score_at_entry != null && (
|
||||
@@ -555,28 +629,35 @@ export default function Dashboard() {
|
||||
{t.score_at_entry}
|
||||
</span>
|
||||
)}
|
||||
{pnl !== null && (
|
||||
<span className={clsx('font-mono font-bold shrink-0', pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
|
||||
<div className="text-[10px] text-slate-600 mt-1">
|
||||
{scoringRunId ? 'Aucun trade loggé ce cycle' : 'En attente du prochain cycle'}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Top patterns scorés du cycle */}
|
||||
{/* Patterns du dernier cycle */}
|
||||
{(() => {
|
||||
return (
|
||||
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">⭐ Top Patterns</span>
|
||||
<span className="section-title mb-0 text-[10px]">⭐ Pattern du cycle</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
{topScoredPatterns.length > 0 ? (
|
||||
{cyclePatterns.length > 0 ? (
|
||||
<div className="space-y-1.5 mt-1">
|
||||
{topScoredPatterns.map((p, i) => {
|
||||
{cyclePatterns.map((p, i) => {
|
||||
const sp = scoreMap[p.id]
|
||||
const score = sp?.score ?? null
|
||||
const ticker = sp?.recommended_trade?.underlying ?? null
|
||||
@@ -587,15 +668,19 @@ export default function Dashboard() {
|
||||
<div className="text-[10px] text-slate-300 truncate leading-tight">{p.name}</div>
|
||||
{ticker && <div className="text-[9px] text-slate-600 font-mono">{ticker}</div>}
|
||||
</div>
|
||||
{score !== null && (
|
||||
{score !== null ? (
|
||||
<span className={clsx('text-sm font-bold font-mono shrink-0', scoreColor(score))}>{score}</span>
|
||||
) : (
|
||||
<span className="text-[9px] text-slate-700 shrink-0">—</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Aucun scoré — lancer le scoring IA</div>
|
||||
<div className="text-[10px] text-slate-600 mt-1">
|
||||
{lastCycle ? 'Aucun pattern ajouté ce cycle' : 'Aucun scoré — lancer le scoring IA'}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user