phase-6: typescript introduction

This commit is contained in:
Kazuma
2026-06-04 22:16:48 -04:00
parent 16bd95aa85
commit 69d13c3dbe
69 changed files with 2323 additions and 1036 deletions
+3 -2
View File
@@ -1,7 +1,8 @@
<script>
<script lang="ts">
import Spinner from '$lib/Spinner.svelte';
import type { SidebarState } from '$lib/types.js';
let { sidebar, onClose } = $props();
let { sidebar, onClose }: { sidebar: SidebarState; onClose: () => void } = $props();
</script>
{#if sidebar.open}
+14 -3
View File
@@ -1,10 +1,21 @@
<script>
import { sigOrd, sorted, verdictShort, vClass } from '$lib/utils.js';
<script lang="ts">
import { sigOrd, sorted } from '$lib/utils.js';
import VerdictPill from '$lib/VerdictPill.svelte';
import SignalBadge from '$lib/SignalBadge.svelte';
import Spinner from '$lib/Spinner.svelte';
import type { AssetType, AssetResult } from '$lib/types.js';
let { type, rows, analyzeLoading = false, onAnalyze } = $props();
let {
type,
rows,
analyzeLoading = false,
onAnalyze,
}: {
type: AssetType;
rows: AssetResult[];
analyzeLoading?: boolean;
onAnalyze: () => void;
} = $props();
// Mode state is self-contained — each table independently tracks inflated vs fundamental
let mode = $state('inflated');
+3 -2
View File
@@ -1,7 +1,8 @@
<script>
<script lang="ts">
import { untrack } from 'svelte';
import type { MarketContext } from '$lib/types.js';
let { ctx, collapsible = false } = $props();
let { ctx, collapsible = false }: { ctx: MarketContext; collapsible?: boolean } = $props();
// Read collapsible once for initial state — untrack avoids a reactive dep on the prop
let expanded = $state(untrack(() => !collapsible));
+3 -2
View File
@@ -1,6 +1,7 @@
<script>
<script lang="ts">
import { fmtPE } from '$lib/utils.js';
let { ctx } = $props();
import type { MarketContext } from '$lib/types.js';
let { ctx }: { ctx: MarketContext } = $props();
// Flat list of chips so the template stays declarative
const chips = $derived([
+3 -2
View File
@@ -1,5 +1,6 @@
<script>
let { signal } = $props();
<script lang="ts">
import type { Signal } from '$lib/types.js';
let { signal }: { signal: Signal | null | undefined } = $props();
const cls = () => {
if (signal?.includes('Strong')) return 'strong';
+2 -4
View File
@@ -1,7 +1,5 @@
<script>
// size: 'sm' | 'md' | 'lg'
// label: optional text shown below (lg only)
let { size = 'md', label = null } = $props();
<script lang="ts">
let { size = 'md', label = null }: { size?: 'sm' | 'md' | 'lg'; label?: string | null } = $props();
</script>
{#if size === 'sm'}
+2 -2
View File
@@ -1,6 +1,6 @@
<script>
<script lang="ts">
import { verdictShort, vClass } from '$lib/utils.js';
let { label } = $props();
let { label }: { label: string | null | undefined } = $props();
</script>
<span class="verdict-pill {vClass(label)}">{verdictShort(label)}</span>
+43 -12
View File
@@ -1,6 +1,19 @@
import type {
ScreenerResult,
MarketContext,
MarketCall,
CalendarEvent,
CatalystStory,
LLMAnalysis,
PortfolioHolding,
PortfolioAdvice,
} from '$lib/types.js';
const BASE = '/api';
export async function screenTickers(tickers) {
// ── Screener ──────────────────────────────────────────────────────────────────
export async function screenTickers(tickers: string[]): Promise<ScreenerResult> {
const res = await fetch(`${BASE}/screen`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -10,13 +23,13 @@ export async function screenTickers(tickers) {
return res.json();
}
export async function fetchCatalysts() {
export async function fetchCatalysts(): Promise<{ tickers: string[]; stories: CatalystStory[] }> {
const res = await fetch(`${BASE}/screen/catalysts`);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function analyzeTickers(tickers) {
export async function analyzeTickers(tickers: string[]): Promise<{ analysis: LLMAnalysis | null }> {
const res = await fetch(`${BASE}/analyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -26,13 +39,23 @@ export async function analyzeTickers(tickers) {
return res.json();
}
export async function fetchPortfolio() {
// ── Finance / Portfolio ───────────────────────────────────────────────────────
export async function fetchPortfolio(): Promise<{
advice: PortfolioAdvice[];
holdings: PortfolioHolding[];
marketContext: MarketContext | null;
netWorth: number | null;
error?: string;
}> {
const res = await fetch(`${BASE}/finance/portfolio`);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function addHolding(holding) {
export async function addHolding(
holding: Omit<PortfolioHolding, never>,
): Promise<{ holdings: PortfolioHolding[] }> {
const res = await fetch(`${BASE}/finance/holdings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -42,7 +65,7 @@ export async function addHolding(holding) {
return res.json();
}
export async function removeHolding(ticker) {
export async function removeHolding(ticker: string): Promise<{ holdings: PortfolioHolding[] }> {
const res = await fetch(`${BASE}/finance/holdings/${ticker}`, {
method: 'DELETE',
});
@@ -50,7 +73,7 @@ export async function removeHolding(ticker) {
return res.json();
}
export async function fetchMarketContext() {
export async function fetchMarketContext(): Promise<MarketContext> {
const res = await fetch(`${BASE}/finance/market-context`);
if (!res.ok) throw new Error(await res.text());
return res.json();
@@ -58,19 +81,25 @@ export async function fetchMarketContext() {
// ── Market Calls ──────────────────────────────────────────────────────────────
export async function fetchCalls() {
export async function fetchCalls(): Promise<{ calls: MarketCall[] }> {
const res = await fetch(`${BASE}/calls`);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function fetchCall(id) {
export async function fetchCall(id: string): Promise<MarketCall & { current: ScreenerResult }> {
const res = await fetch(`${BASE}/calls/${id}`);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function createCall(payload) {
export async function createCall(payload: {
title: string;
quarter: string;
thesis: string;
tickers: string[];
date?: string;
}): Promise<MarketCall> {
const res = await fetch(`${BASE}/calls`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -80,13 +109,15 @@ export async function createCall(payload) {
return res.json();
}
export async function deleteCall(id) {
export async function deleteCall(id: string): Promise<{ ok: boolean }> {
const res = await fetch(`${BASE}/calls/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function fetchCallsCalendar(tickers = null) {
export async function fetchCallsCalendar(
tickers: string[] | null = null,
): Promise<{ events: CalendarEvent[] }> {
const url = tickers?.length
? `${BASE}/calls/calendar?tickers=${tickers.join(',')}`
: `${BASE}/calls/calendar`;
+139
View File
@@ -0,0 +1,139 @@
// ── Shared UI types ───────────────────────────────────────────────────────
// Mirror of the server's domain types, used across Svelte components.
export type Signal =
| '✅ Strong Buy'
| '⚡ Momentum'
| '⚠️ Speculation'
| '🔄 Neutral'
| '❌ Avoid';
export type AssetType = 'STOCK' | 'ETF' | 'BOND';
export type ScoreMode = 'inflated' | 'fundamental';
export interface Benchmarks {
marketPE: number | null;
techPE: number | null;
reitYield: number | null;
igSpread: number | null;
}
export interface MarketContext {
sp500Price: number | null;
riskFreeRate: number | null;
vixLevel: number | null;
rateRegime: 'HIGH' | 'NORMAL' | 'LOW';
volatilityRegime: 'HIGH' | 'NORMAL' | 'LOW';
benchmarks: Benchmarks;
}
export interface ScoreResult {
label: string;
score: number;
scoreSummary: string;
audit: {
riskFlags?: string[];
[key: string]: unknown;
};
}
export interface AssetDisplayMetrics {
Price?: string;
Sector?: string;
'P/E'?: string;
PEG?: string;
'ROE%'?: string;
'OpMgn%'?: string;
'FCF Yld%'?: string;
'D/E'?: string;
'Exp Ratio%'?: string;
'Yield%'?: string;
AUM?: string;
'5Y Return%'?: string;
'YTM%'?: string;
Duration?: string;
Rating?: string;
[key: string]: string | null | undefined;
}
export interface AssetResult {
asset: {
ticker: string;
currentPrice: number;
type: AssetType;
displayMetrics: AssetDisplayMetrics;
};
signal: Signal;
inflated: ScoreResult;
fundamental: ScoreResult;
}
export interface ScreenerResult {
STOCK: AssetResult[];
ETF: AssetResult[];
BOND: AssetResult[];
ERROR: Array<{ ticker: string; message: string }>;
marketContext: MarketContext;
}
export interface LLMAnalysis {
summary: string;
sentiment: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
affectedIndustries: Array<{ name: string; reason: string }>;
relatedTickers: Array<{ ticker: string; reason: string }>;
}
export interface SidebarState {
open: boolean;
loading: boolean;
analysis: LLMAnalysis | null;
type: AssetType | null;
error: string | null;
}
export interface PortfolioHolding {
ticker: string;
shares: number;
costBasis: number;
source: string;
type: 'stock' | 'etf' | 'bond' | 'crypto';
}
export interface TickerSnapshot {
price: number | null;
signal: Signal | null;
}
export interface MarketCall {
id: string;
title: string;
quarter: string;
date: string;
thesis: string;
tickers: string[];
snapshot: Record<string, TickerSnapshot>;
}
export interface CalendarEvent {
ticker: string;
type: 'earnings' | 'dividend';
date: string;
[key: string]: unknown;
}
export interface CatalystStory {
title: string;
link: string;
publisher: string;
publishedAt: string;
relatedTickers: string[];
}
export interface PortfolioAdvice {
ticker: string;
action: 'hold' | 'sell' | 'add' | 'watch';
reason: string;
signal: Signal | null;
currentPrice: number | null;
gainLossPct: number | null;
}
+3 -2
View File
@@ -1,8 +1,9 @@
<script>
<script lang="ts">
import { page, navigating } from '$app/stores';
import '../styles/app.scss';
import Spinner from '$lib/Spinner.svelte';
let { children } = $props();
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
// Resolve active path optimistically — use the destination during navigation
// so the nav link highlights immediately on click, not after load completes.
+20 -20
View File
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { screenTickers, analyzeTickers } from '$lib/api.js';
import { sigOrd, sorted, verdictShort, vClass } from '$lib/utils.js';
import SignalBadge from '$lib/SignalBadge.svelte';
@@ -7,23 +7,24 @@
import MarketContextStrip from '$lib/MarketContextStrip.svelte';
import AssetTable from '$lib/AssetTable.svelte';
import AnalysisSidebar from '$lib/AnalysisSidebar.svelte';
import type { ScreenerResult, AssetType, SidebarState } from '$lib/types.js';
// Initial data comes from +page.js load (replaces _booted / $effect hack)
let { data } = $props();
interface PageData { results: ScreenerResult; catalystInput: string }
let { data }: { data: PageData } = $props();
let input = $state(data.catalystInput);
let results = $state(data.results);
let screenedAt = $state(new Date().toLocaleTimeString());
let loading = $state(false);
let loadingCats = $state(false);
let error = $state(null);
let searchOpen = $state(false);
let input: string = $state(data.catalystInput);
let results: ScreenerResult = $state(data.results);
let screenedAt: string = $state(new Date().toLocaleTimeString());
let loading: boolean = $state(false);
let loadingCats: boolean = $state(false);
let error: string | null = $state(null);
let searchOpen: boolean = $state(false);
// ── LLM Analysis sidebar ────────────────────────────────────────────────
let sidebar = $state({ open: false, loading: false, analysis: null, type: null, error: null });
let sidebar: SidebarState = $state({ open: false, loading: false, analysis: null, type: null, error: null });
async function runTabAnalysis(type) {
const tickers = (results?.[type] ?? []).map(r => r.asset.ticker);
async function runTabAnalysis(type: AssetType): Promise<void> {
const tickers = (results?.[type] ?? []).map((r) => r.asset.ticker);
if (!tickers.length) return;
sidebar = { open: true, loading: true, analysis: null, type, error: null };
try {
@@ -32,12 +33,12 @@
sidebar = { open: true, loading: false, analysis: res.analysis, type,
error: res.analysis ? null : (reason ?? 'Analysis failed — check server logs for details.') };
} catch (e) {
sidebar = { open: true, loading: false, analysis: null, type, error: e.message };
sidebar = { open: true, loading: false, analysis: null, type, error: (e as Error).message };
}
}
// ── Manual ticker search ─────────────────────────────────────────────────
async function screen() {
async function screen(): Promise<void> {
error = null;
loading = true;
try {
@@ -45,15 +46,14 @@
results = await screenTickers(tickers);
screenedAt = new Date().toLocaleTimeString();
} catch (e) {
error = e.message;
error = (e as Error).message;
} finally {
loading = false;
}
}
// ── Re-fetch today's catalysts ───────────────────────────────────────────
// Splits fetch (news) from screen (Yahoo) — each step has its own loading flag.
async function reloadCatalysts() {
async function reloadCatalysts(): Promise<void> {
const { fetchCatalysts } = await import('$lib/api.js');
loadingCats = true;
error = null;
@@ -64,7 +64,7 @@
results = await screenTickers(cat.tickers);
screenedAt = new Date().toLocaleTimeString();
} catch (e) {
error = e.message;
error = (e as Error).message;
} finally {
loading = false;
loadingCats = false;
@@ -157,7 +157,7 @@
</section>
<!-- ── Per-type detail tables ────────────────────────────────────── -->
{#each ['STOCK', 'ETF', 'BOND'] as type}
{#each (['STOCK', 'ETF', 'BOND'] as const) as type}
{#if results[type]?.length}
<AssetTable
{type}
@@ -1,13 +1,14 @@
import { fetchCatalysts, screenTickers } from '$lib/api.js';
import type { PageLoad } from './$types.js';
// Client-only — the API lives at localhost:3000, not accessible during SSR
export const ssr = false;
export async function load() {
export const load: PageLoad = async () => {
const cat = await fetchCatalysts();
const results = await screenTickers(cat.tickers);
return {
results,
catalystInput: cat.tickers.join(', '),
};
}
};
-8
View File
@@ -1,8 +0,0 @@
export async function load({ fetch }) {
const [callsRes, calRes] = await Promise.all([fetch('/api/calls'), fetch('/api/calls/calendar')]);
const { calls } = callsRes.ok ? await callsRes.json() : { calls: [] };
const { events } = calRes.ok ? await calRes.json() : { events: [] };
return { calls, events };
}
+30 -13
View File
@@ -1,15 +1,31 @@
<script>
<script lang="ts">
import { createCall, deleteCall } from '$lib/api.js';
import SignalBadge from '$lib/SignalBadge.svelte';
import Spinner from '$lib/Spinner.svelte';
import { invalidateAll } from '$app/navigation';
let { data } = $props();
interface MarketCall {
id: string;
title: string;
quarter: string;
date: string;
thesis: string;
tickers: string[];
snapshot: Record<string, { price: number | null; signal: string | null }>;
}
interface PageData {
calls: MarketCall[];
events: unknown[];
error?: string;
}
let { data }: { data: PageData } = $props();
// New call form state
let showForm = $state(false);
let saving = $state(false);
let formError = $state(null);
let showForm: boolean = $state(false);
let saving: boolean = $state(false);
let formError: string|null = $state(null);
let form = $state({
title: '',
quarter: currentQuarter(),
@@ -43,28 +59,29 @@
form = { title: '', quarter: currentQuarter(), date: today(), thesis: '', tickers: '' };
await invalidateAll(); // re-run load() to refresh the list
} catch (e) {
formError = e.message;
formError = (e as Error).message;
} finally {
saving = false;
}
}
async function remove(id) {
async function remove(id: string): Promise<void> {
if (!confirm('Delete this market call?')) return;
await deleteCall(id);
await invalidateAll();
}
const signalColor = s => {
if (s?.includes('Strong')) return '#4ade80';
if (s?.includes('Momentum')) return '#60a5fa';
if (s?.includes('Neutral')) return '#94a3b8';
const signalColor = (s: string | null | undefined): string => {
if (s?.includes('Strong')) return '#4ade80';
if (s?.includes('Momentum')) return '#60a5fa';
if (s?.includes('Neutral')) return '#94a3b8';
if (s?.includes('Speculation')) return '#fb923c';
return '#f87171';
};
const eventIcon = type => ({ earnings: '📊', exdividend: '💰', dividend: '💵' })[type] ?? '📅';
const eventColor = type => ({ earnings: '#60a5fa', exdividend: '#facc15', dividend: '#4ade80' })[type] ?? '#94a3b8';
type EventType = 'earnings' | 'exdividend' | 'dividend';
const eventIcon = (type: EventType): string => ({ earnings: '📊', exdividend: '💰', dividend: '💵' })[type] ?? '📅';
const eventColor = (type: EventType): string => ({ earnings: '#60a5fa', exdividend: '#facc15', dividend: '#4ade80' })[type] ?? '#94a3b8';
const upcoming = $derived((data.events ?? []).filter(e => !e.isPast).slice(0, 20));
const past = $derived((data.events ?? []).filter(e => e.isPast).slice(0, 10));
+11
View File
@@ -0,0 +1,11 @@
import type { PageLoad } from './$types.js';
import type { MarketCall, CalendarEvent } from '$lib/types.js';
export const load: PageLoad = async ({ fetch }) => {
const [callsRes, calRes] = await Promise.all([fetch('/api/calls'), fetch('/api/calls/calendar')]);
const { calls }: { calls: MarketCall[] } = callsRes.ok ? await callsRes.json() : { calls: [] };
const { events }: { events: CalendarEvent[] } = calRes.ok ? await calRes.json() : { events: [] };
return { calls, events };
};
@@ -1,5 +1,7 @@
export async function load({ fetch, params }) {
import type { PageLoad } from './$types.js';
export const load: PageLoad = async ({ fetch, params }) => {
const res = await fetch(`/api/calls/${params.id}`);
if (!res.ok) return { error: await res.text() };
return res.json();
}
};
+41 -19
View File
@@ -1,28 +1,50 @@
<script>
<script lang="ts">
import SignalBadge from '$lib/SignalBadge.svelte';
import MarketContext from '$lib/MarketContext.svelte';
import Spinner from '$lib/Spinner.svelte';
import { addHolding, removeHolding } from '$lib/api.js';
import { sigOrd, fmt, fmtShort, glClass, advClass } from '$lib/utils.js';
import type { Signal, MarketContext as MarketContextType, PortfolioHolding } from '$lib/types.js';
interface AdviceRow {
ticker: string;
type: string;
source: string;
shares: number;
costBasis: number;
currentPrice: string | null;
marketValue: string | null;
gainLossPct: string | null;
signal: Signal | null;
advice: string;
reason: string;
}
interface PortfolioData {
advice: AdviceRow[];
marketContext: MarketContextType | null;
personalFinance: Record<string, unknown> | null;
}
let { data: _data } = $props(); // unused — we load client-side
let data = $state(null);
let loading = $state(true);
let refreshing = $state(false); // background refresh — keeps page visible
let loadError = $state(null);
let data: PortfolioData | null = $state(null);
let loading: boolean = $state(true);
let refreshing: boolean = $state(false);
let loadError: string | null = $state(null);
// ── Add holding form (new holdings only) ────────────────────────────────────
let formOpen = $state(false);
let saving = $state(false);
let formError = $state(null);
let formOpen: boolean = $state(false);
let saving: boolean = $state(false);
let formError: string|null = $state(null);
let form = $state({ ticker: '', shares: '', costBasis: '', type: 'stock', source: 'Robinhood' });
// ── Inline row editing ───────────────────────────────────────────────────────
let inlineEdit = $state(null); // { ticker, shares, costBasis, type, source } or null
let inlineSaving = $state(false);
interface InlineEdit { ticker: string; shares: string; costBasis: string; type: string; source: string }
let inlineEdit: InlineEdit | null = $state(null);
let inlineSaving: boolean = $state(false);
function startInlineEdit(a) {
function startInlineEdit(a: AdviceRow) {
inlineEdit = {
ticker: a.ticker,
shares: String(a.shares),
@@ -52,7 +74,7 @@
advice: data.advice.map(a =>
a.ticker === updated.ticker
? { ...a, shares: updated.shares, costBasis: updated.costBasis, type: updated.type, source: updated.source,
marketValue: updated.shares * (parseFloat(a.currentPrice) || 0),
marketValue: String(updated.shares * (parseFloat(a.currentPrice ?? '0') || 0)),
gainLossPct: a.currentPrice ? (((parseFloat(a.currentPrice) - updated.costBasis) / updated.costBasis) * 100).toFixed(1) : null }
: a
),
@@ -61,7 +83,7 @@
inlineEdit = null;
fetchPortfolioData(false); // background: update prices + signals
} catch (e) {
loadError = e.message;
loadError = (e as Error).message;
} finally {
inlineSaving = false;
}
@@ -101,13 +123,13 @@
formOpen = false;
fetchPortfolioData(false); // background: get real price + signal
} catch (e) {
formError = e.message;
formError = (e as Error).message;
} finally {
saving = false;
}
}
async function deleteHolding(ticker) {
async function deleteHolding(ticker: string): Promise<void> {
if (!confirm(`Remove ${ticker} from your portfolio?`)) return;
// Optimistic remove — drop the row immediately
if (data?.advice) {
@@ -117,7 +139,7 @@
await removeHolding(ticker);
fetchPortfolioData(false); // background: recalculate totals
} catch (e) {
loadError = e.message;
loadError = (e as Error).message;
}
}
@@ -128,7 +150,7 @@
fetch('/api/finance/portfolio')
.then(res => res.ok ? res.json() : res.text().then(t => { throw new Error(t); }))
.then(json => { data = json; })
.catch(e => { loadError = e.message; })
.catch(e => { loadError = (e as Error).message; })
.finally(() => { loading = false; refreshing = false; });
}
@@ -143,7 +165,7 @@
let sortCol = $state('ticker');
let sortDir = $state(1); // 1 = asc, -1 = desc
function toggleSort(col) {
function toggleSort(col: string): void {
if (sortCol === col) sortDir = sortDir === 1 ? -1 : 1;
else { sortCol = col; sortDir = 1; }
}
@@ -169,7 +191,7 @@
});
});
const sortIcon = (col) => sortCol !== col ? '⇅' : sortDir === 1 ? '↑' : '↓';
const sortIcon = (col: string): string => sortCol !== col ? '⇅' : sortDir === 1 ? '↑' : '↓';
const totalValue = $derived(data?.advice?.reduce((s, a) => s + (parseFloat(a.marketValue) || 0), 0) ?? 0);
@@ -1,8 +1,10 @@
import type { PageLoad } from './$types.js';
// Disable SSR — data is fetched client-side in the component so navigation
// is instant instead of blocking until all Yahoo Finance calls resolve.
export const ssr = false;
export const prerender = false;
export function load() {
export const load: PageLoad = () => {
return {};
}
};
+9 -2
View File
@@ -1,10 +1,17 @@
<script>
<script lang="ts">
import MarketContext from '$lib/MarketContext.svelte';
import SignalBadge from '$lib/SignalBadge.svelte';
import VerdictPill from '$lib/VerdictPill.svelte';
import { sorted } from '$lib/utils.js';
import type { AssetResult, MarketContext as MarketContextType } from '$lib/types.js';
let { data } = $props();
interface PageData {
ETF: AssetResult[];
BOND: AssetResult[];
marketContext: MarketContextType | null;
error?: string;
}
let { data }: { data: PageData } = $props();
const SIGNAL_STRONG = '✅ Strong Buy';
@@ -1,6 +1,9 @@
import type { PageLoad } from './$types.js';
import type { AssetResult, MarketContext } from '$lib/types.js';
// Curated watchlist of well-established, low-cost ETFs and investment-grade bond funds.
// Screened for Strong Buy signal under both Market-Adjusted and Fundamental lenses.
const SAFE_WATCHLIST = [
const SAFE_WATCHLIST: string[] = [
// ── Broad Market ETFs
'VOO', // S&P 500 — Vanguard (0.03%)
'IVV', // S&P 500 — iShares (0.03%)
@@ -40,21 +43,28 @@ const SAFE_WATCHLIST = [
'TIP', // TIPS — iShares
];
export async function load({ fetch }) {
export const load: PageLoad = async ({ fetch }) => {
const res = await fetch('/api/screen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tickers: SAFE_WATCHLIST }),
});
if (!res.ok)
return { ETF: [], BOND: [], ERROR: [], marketContext: null, error: await res.text() };
if (!res.ok) {
return {
ETF: [] as AssetResult[],
BOND: [] as AssetResult[],
ERROR: [] as Array<{ ticker: string; message: string }>,
marketContext: null as MarketContext | null,
error: await res.text(),
};
}
const data = await res.json();
return {
ETF: data.ETF ?? [],
BOND: data.BOND ?? [],
ERROR: data.ERROR ?? [],
marketContext: data.marketContext ?? null,
ETF: (data.ETF ?? []) as AssetResult[],
BOND: (data.BOND ?? []) as AssetResult[],
ERROR: (data.ERROR ?? []) as Array<{ ticker: string; message: string }>,
marketContext: (data.marketContext ?? null) as MarketContext | null,
};
}
};