fix: blank company charts + competitor GUIDs instead of tickers

Trend charts blank:
- trend_windows uses upsert (1 row per ticker/window), so charts had
  at most 1 data point. Added trend_history table (migration 024) that
  appends every snapshot. New /api/trends/history endpoint serves the
  time series. Frontend now uses useTrendHistory for charts and
  useTrends for the latest summary card.

Competitor GUIDs:
- list_competitors query returned raw company_b_id UUIDs without
  joining companies table. Added LEFT JOIN with CASE to resolve the
  other company's ticker and legal_name. Updated Pydantic model to
  include enriched fields. Frontend fallback changed from truncated
  UUID to ticker/legal_name/Unknown.
This commit is contained in:
Celes Renata
2026-04-17 00:42:55 +00:00
parent f2d8744a4f
commit 7c589353f8
6 changed files with 169 additions and 16 deletions
+9
View File
@@ -236,6 +236,15 @@ export function useTrends(params?: { ticker?: string; window?: string; limit?: n
return useGet<TrendSummary[]>(['trends', params], 'query', path);
}
export function useTrendHistory(params?: { ticker?: string; window?: string; limit?: number }) {
const qs = new URLSearchParams();
if (params?.ticker) qs.set('ticker', params.ticker);
if (params?.window) qs.set('window', params.window);
if (params?.limit) qs.set('limit', String(params.limit ?? 200));
const path = `/api/trends/history${qs.toString() ? '?' + qs : ''}`;
return useGet<TrendSummary[]>(['trend-history', params], 'query', path);
}
export function useTrend(id: string | undefined) {
return useGet<TrendSummary>(['trend', id], 'query', `/api/trends/${id}`, !!id);
}
+16 -8
View File
@@ -12,6 +12,7 @@ import {
useCompetitiveSignals,
useCorporateDecisions,
useTrends,
useTrendHistory,
} from '../api/hooks';
import { StatusBadge, ConfidenceBar, LoadingSpinner, Card } from '../components/ui';
import { DataTable, type Column } from '../components/DataTable';
@@ -40,6 +41,7 @@ export function CompanyDetailPage() {
const { data: signals } = useCompetitiveSignals(company?.ticker);
const { data: decisions } = useCorporateDecisions(company?.ticker);
const { data: trends } = useTrends({ ticker: company?.ticker, limit: 200 });
const { data: trendHistory } = useTrendHistory({ ticker: company?.ticker, limit: 500 });
const [tab, setTab] = useState<'trends' | 'sources' | 'aliases' | 'macro' | 'competitors' | 'patterns' | 'signals' | 'decisions'>('trends');
if (isLoading || !company) return <LoadingSpinner />;
@@ -78,7 +80,7 @@ export function CompanyDetailPage() {
</div>
{tab === 'trends' && (
<TrendHistoryChart trends={trends ?? []} ticker={company.ticker} />
<TrendHistoryChart trends={trendHistory ?? []} latestTrends={trends ?? []} ticker={company.ticker} />
)}
{tab === 'sources' && (
@@ -360,7 +362,7 @@ function CompetitorsPanel({ competitors, onInfer, isInferring }: {
{competitors.map((c) => (
<div key={c.id} className="flex items-center justify-between rounded-lg border border-surface-700 bg-surface-950 p-3">
<div className="flex items-center gap-3">
<span className="font-mono font-semibold text-brand-300">{c.ticker ?? c.company_b_id.slice(0, 8)}</span>
<span className="font-mono font-semibold text-brand-300">{c.ticker ?? c.legal_name ?? 'Unknown'}</span>
<StatusBadge status={c.relationship_type} />
<ConfidenceBar value={c.strength} />
</div>
@@ -563,10 +565,10 @@ interface ChartPoint {
window: string;
}
function TrendHistoryChart({ trends, ticker }: { trends: TrendSummary[]; ticker: string }) {
function TrendHistoryChart({ trends, latestTrends, ticker }: { trends: TrendSummary[]; latestTrends: TrendSummary[]; ticker: string }) {
const [selectedWindow, setSelectedWindow] = useState('7d');
// Filter trends for this ticker and selected window, sorted by time
// Use history data for charts
const filtered = (trends ?? [])
.filter((t) => t.entity_id === ticker && t.window === selectedWindow)
.sort((a, b) => new Date(a.generated_at).getTime() - new Date(b.generated_at).getTime());
@@ -582,10 +584,17 @@ function TrendHistoryChart({ trends, ticker }: { trends: TrendSummary[]; ticker:
window: t.window,
}));
// Available windows from the data
const availableWindows = [...new Set((trends ?? []).filter((t) => t.entity_id === ticker).map((t) => t.window))];
// Available windows from the data (check both history and latest)
const allTrends = [...(trends ?? []), ...(latestTrends ?? [])];
const availableWindows = [...new Set(allTrends.filter((t) => t.entity_id === ticker).map((t) => t.window))];
availableWindows.sort((a, b) => WINDOW_ORDER.indexOf(a) - WINDOW_ORDER.indexOf(b));
// Use latest trends for the summary card
const latestForWindow = (latestTrends ?? [])
.filter((t) => t.entity_id === ticker && t.window === selectedWindow)
.sort((a, b) => new Date(b.generated_at).getTime() - new Date(a.generated_at).getTime());
const latest = latestForWindow[0] ?? (filtered.length > 0 ? filtered[filtered.length - 1] : null);
return (
<div className="space-y-4">
{/* Window selector */}
@@ -705,8 +714,7 @@ function TrendHistoryChart({ trends, ticker }: { trends: TrendSummary[]; ticker:
{/* Latest trend summary */}
<Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Latest Trend</h2>
{filtered.length > 0 && (() => {
const latest = filtered[filtered.length - 1];
{latest && (() => {
return (
<dl className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm sm:grid-cols-4">
<div>