From 7c3836068f1b9d7ea90cc8c07f471a598c4ed0a5 Mon Sep 17 00:00:00 2001 From: francy51 Date: Fri, 27 Feb 2026 09:57:44 -0500 Subject: [PATCH] Add company analysis view with financials, price history, filings, and AI reports --- .gitignore | 7 + app/analysis/page.tsx | 303 +++++++++++++++++++++++++++++++++ app/page.tsx | 6 +- app/watchlist/page.tsx | 7 + components/shell/app-shell.tsx | 3 +- lib/api.ts | 11 ++ lib/server/api/app.ts | 73 ++++++++ lib/server/prices.ts | 70 ++++++++ lib/types.ts | 34 ++++ 9 files changed, 512 insertions(+), 2 deletions(-) create mode 100644 app/analysis/page.tsx diff --git a/.gitignore b/.gitignore index 679496a..fc6c4b8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,11 @@ out/ # Local app runtime state data/*.json +data/*.sqlite +data/*.sqlite-shm +data/*.sqlite-wal .workflow-data/ +output/ + +# Local automation/test artifacts +.playwright-cli/ diff --git a/app/analysis/page.tsx b/app/analysis/page.tsx new file mode 100644 index 0000000..ab234ec --- /dev/null +++ b/app/analysis/page.tsx @@ -0,0 +1,303 @@ +'use client'; + +import Link from 'next/link'; +import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; +import { format } from 'date-fns'; +import { + Bar, + BarChart, + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis +} from 'recharts'; +import { BrainCircuit, ChartNoAxesCombined, RefreshCcw, Search } from 'lucide-react'; +import { useSearchParams } from 'next/navigation'; +import { AppShell } from '@/components/shell/app-shell'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Panel } from '@/components/ui/panel'; +import { useAuthGuard } from '@/hooks/use-auth-guard'; +import { getCompanyAnalysis } from '@/lib/api'; +import { asNumber, formatCompactCurrency, formatCurrency, formatPercent } from '@/lib/format'; +import type { CompanyAnalysis } from '@/lib/types'; + +function formatShortDate(value: string) { + return format(new Date(value), 'MMM yyyy'); +} + +export default function AnalysisPage() { + return ( + Loading analysis desk...}> + + + ); +} + +function AnalysisPageContent() { + const { isPending, isAuthenticated } = useAuthGuard(); + const searchParams = useSearchParams(); + + const [tickerInput, setTickerInput] = useState('MSFT'); + const [ticker, setTicker] = useState('MSFT'); + const [analysis, setAnalysis] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fromQuery = searchParams.get('ticker'); + if (!fromQuery) { + return; + } + + const normalized = fromQuery.trim().toUpperCase(); + if (!normalized) { + return; + } + + setTickerInput(normalized); + setTicker(normalized); + }, [searchParams]); + + const loadAnalysis = useCallback(async (symbol: string) => { + setLoading(true); + setError(null); + + try { + const response = await getCompanyAnalysis(symbol); + setAnalysis(response.analysis); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unable to load company analysis'); + setAnalysis(null); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + if (!isPending && isAuthenticated) { + void loadAnalysis(ticker); + } + }, [isPending, isAuthenticated, ticker, loadAnalysis]); + + const priceSeries = useMemo(() => { + return (analysis?.priceHistory ?? []).map((point) => ({ + ...point, + label: formatShortDate(point.date) + })); + }, [analysis?.priceHistory]); + + const financialSeries = useMemo(() => { + return (analysis?.financials ?? []) + .slice() + .reverse() + .map((item) => ({ + label: formatShortDate(item.filingDate), + revenue: item.revenue, + netIncome: item.netIncome, + assets: item.totalAssets + })); + }, [analysis?.financials]); + + if (isPending || !isAuthenticated) { + return
Loading analysis desk...
; + } + + return ( + void loadAnalysis(ticker)}> + + Refresh + + )} + > + +
{ + event.preventDefault(); + const normalized = tickerInput.trim().toUpperCase(); + if (!normalized) { + return; + } + setTicker(normalized); + }} + > + setTickerInput(event.target.value.toUpperCase())} + placeholder="Ticker (AAPL)" + className="max-w-xs" + /> + + {analysis ? ( + + Open filing stream + + ) : null} +
+
+ + {error ? ( + +

{error}

+
+ ) : null} + +
+ +

{analysis?.company.companyName ?? ticker}

+

{analysis?.company.ticker ?? ticker}

+

{analysis?.company.sector ?? 'Sector unavailable'}

+
+ + +

{formatCurrency(analysis?.quote ?? 0)}

+

CIK {analysis?.company.cik ?? 'n/a'}

+
+ + +

{formatCurrency(analysis?.position?.market_value)}

+

{analysis?.position ? `${asNumber(analysis.position.shares).toLocaleString()} shares` : 'Not held in portfolio'}

+
+ + +

= 0 ? 'text-[#96f5bf]' : 'text-[#ff9f9f]'}`}> + {formatCurrency(analysis?.position?.gain_loss)} +

+

{formatPercent(analysis?.position?.gain_loss_pct)}

+
+
+ +
+ + {loading ? ( +

Loading price history...

+ ) : priceSeries.length === 0 ? ( +

No price history available.

+ ) : ( +
+ + + + + `$${value.toFixed(0)}`} /> + formatCurrency(value)} /> + + + +
+ )} +
+ + + {loading ? ( +

Loading financials...

+ ) : financialSeries.length === 0 ? ( +

No parsed filing metrics yet.

+ ) : ( +
+ + + + + `$${Math.round(value / 1_000_000_000)}B`} /> + formatCompactCurrency(value)} /> + + + + + + +
+ )} +
+
+ + + {loading ? ( +

Loading filings...

+ ) : !analysis || analysis.filings.length === 0 ? ( +

No filings available for this ticker.

+ ) : ( +
+ + + + + + + + + + + + + {analysis.filings.map((filing) => ( + + + + + + + + + ))} + +
FiledTypeRevenueNet IncomeAssetsDocument
{format(new Date(filing.filing_date), 'MMM dd, yyyy')}{filing.filing_type}{filing.metrics?.revenue ? formatCompactCurrency(filing.metrics.revenue) : 'n/a'}{filing.metrics?.netIncome ? formatCompactCurrency(filing.metrics.netIncome) : 'n/a'}{filing.metrics?.totalAssets ? formatCompactCurrency(filing.metrics.totalAssets) : 'n/a'} + {filing.filing_url ? ( + + SEC filing + + ) : ( + 'n/a' + )} +
+
+ )} +
+ + + {loading ? ( +

Loading AI reports...

+ ) : !analysis || analysis.aiReports.length === 0 ? ( +

No AI reports generated yet. Run filing analysis from the filings stream.

+ ) : ( +
+ {analysis.aiReports.map((report) => ( +
+
+
+

+ {report.filingType} ยท {format(new Date(report.filingDate), 'MMM dd, yyyy')} +

+

{report.provider} / {report.model}

+
+ +
+

{report.summary}

+
+ ))} +
+ )} +
+ + +
+ + Analysis scope: price + filings + ai synthesis +
+
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 7778541..4613689 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -202,7 +202,11 @@ export default function CommandCenterPage() { -
+
+ +

Analysis

+

Inspect one company across prices, filings, financials, and AI reports.

+

Filings

Sync SEC filings and trigger AI memo analysis.

diff --git a/app/watchlist/page.tsx b/app/watchlist/page.tsx index 4383d03..29313bd 100644 --- a/app/watchlist/page.tsx +++ b/app/watchlist/page.tsx @@ -133,6 +133,13 @@ export default function WatchlistPage() { Open stream + + Analyze + +