phase-7: code restructure
This commit is contained in:
+58
-105
@@ -1,80 +1,26 @@
|
||||
<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';
|
||||
import Spinner from '$lib/Spinner.svelte';
|
||||
import VerdictPill from '$lib/VerdictPill.svelte';
|
||||
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';
|
||||
import { screenerStore } from '$lib/stores/screener.store.svelte.js';
|
||||
import SignalBadge from '$lib/SignalBadge.svelte';
|
||||
import Spinner from '$lib/Spinner.svelte';
|
||||
import VerdictPill from '$lib/VerdictPill.svelte';
|
||||
import MarketContextStrip from '$lib/MarketContextStrip.svelte';
|
||||
import AssetTable from '$lib/AssetTable.svelte';
|
||||
import AnalysisSidebar from '$lib/AnalysisSidebar.svelte';
|
||||
|
||||
interface PageData { results: ScreenerResult; catalystInput: string }
|
||||
let { data }: { data: PageData } = $props();
|
||||
const s = screenerStore;
|
||||
|
||||
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);
|
||||
let { data: _data } = $props();
|
||||
|
||||
// ── LLM Analysis sidebar ────────────────────────────────────────────────
|
||||
let sidebar: SidebarState = $state({ open: false, loading: false, analysis: null, type: null, error: null });
|
||||
// Pure UI state — not shared, kept local
|
||||
let searchOpen = $state(false);
|
||||
|
||||
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 {
|
||||
const res = await analyzeTickers(tickers);
|
||||
const reason = res.reason === 'no_stories' ? 'No recent news found for these tickers.' : null;
|
||||
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 as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
// ── Manual ticker search ─────────────────────────────────────────────────
|
||||
async function screen(): Promise<void> {
|
||||
error = null;
|
||||
loading = true;
|
||||
try {
|
||||
const tickers = input.split(/[\s,]+/).map(t => t.trim().toUpperCase()).filter(Boolean);
|
||||
results = await screenTickers(tickers);
|
||||
screenedAt = new Date().toLocaleTimeString();
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Re-fetch today's catalysts ───────────────────────────────────────────
|
||||
async function reloadCatalysts(): Promise<void> {
|
||||
const { fetchCatalysts } = await import('$lib/api.js');
|
||||
loadingCats = true;
|
||||
error = null;
|
||||
try {
|
||||
const cat = await fetchCatalysts();
|
||||
input = cat.tickers.join(', ');
|
||||
loading = true;
|
||||
results = await screenTickers(cat.tickers);
|
||||
screenedAt = new Date().toLocaleTimeString();
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
} finally {
|
||||
loading = false;
|
||||
loadingCats = false;
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = $derived(results?.marketContext ?? null);
|
||||
const allAssets = $derived(results
|
||||
? sorted([...results.STOCK, ...results.ETF, ...results.BOND])
|
||||
: []);
|
||||
// Boot — fetch catalysts + screen on mount
|
||||
let _booted = false;
|
||||
$effect(() => {
|
||||
if (_booted) return;
|
||||
_booted = true;
|
||||
s.reloadCatalysts();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
@@ -82,8 +28,8 @@
|
||||
<!-- ── Toolbar ────────────────────────────────────────────────────── -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-top">
|
||||
<button onclick={reloadCatalysts} disabled={loading || loadingCats} class="btn-catalyst">
|
||||
{#if loadingCats}<Spinner size="sm" />{:else}📰 Today Market{/if}
|
||||
<button onclick={() => s.reloadCatalysts()} disabled={s.loading || s.loadingCats} class="btn-catalyst">
|
||||
{#if s.loadingCats}<Spinner size="sm" />{:else}📰 Today Market{/if}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => searchOpen = !searchOpen}
|
||||
@@ -92,43 +38,45 @@
|
||||
>
|
||||
🔍 {searchOpen ? 'Hide search' : 'Search tickers'}
|
||||
</button>
|
||||
{#if screenedAt}
|
||||
<span class="screened-at">Last screened {screenedAt}</span>
|
||||
{#if s.screenedAt}
|
||||
<span class="screened-at">Last screened {s.screenedAt}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if searchOpen}
|
||||
<div class="search-row">
|
||||
<input
|
||||
bind:value={input}
|
||||
placeholder="AAPL, MSFT, VOO …"
|
||||
onkeydown={e => e.key === 'Enter' && screen()}
|
||||
/>
|
||||
<button onclick={screen} disabled={loading || loadingCats} class="btn-screen">
|
||||
{#if loading}<Spinner size="sm" />{:else}Screen{/if}
|
||||
</button>
|
||||
</div>
|
||||
<div class="search-row">
|
||||
<input
|
||||
bind:value={s.input}
|
||||
placeholder="AAPL, MSFT, VOO …"
|
||||
onkeydown={e => e.key === 'Enter' && s.screen()}
|
||||
/>
|
||||
<button onclick={() => s.screen()} disabled={s.loading || s.loadingCats} class="btn-screen">
|
||||
{#if s.loading}<Spinner size="sm" />{:else}Screen{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if s.ctx}
|
||||
<MarketContextStrip ctx={s.ctx} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="error-banner">⚠ {error}</div>
|
||||
{#if s.error}
|
||||
<div class="error-banner">⚠ {s.error}</div>
|
||||
{/if}
|
||||
|
||||
{#if loading || loadingCats}
|
||||
{#if s.loading || s.loadingCats}
|
||||
<div class="loading-area">
|
||||
<Spinner size="lg" label={loadingCats ? 'Fetching news catalysts…' : 'Screening tickers…'} />
|
||||
<Spinner size="lg" label={s.loadingCats ? 'Fetching news catalysts…' : 'Screening tickers…'} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if ctx}
|
||||
<MarketContextStrip {ctx} />
|
||||
|
||||
{#if s.results && !s.loading && !s.loadingCats}
|
||||
<!-- ── Signal Summary ───────────────────────────────────────────── -->
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Signal Summary</h2>
|
||||
<span class="count">{allAssets.length} assets</span>
|
||||
<span class="count">{s.allAssets.length} assets</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@@ -139,16 +87,21 @@
|
||||
<th>Signal</th>
|
||||
<th>Mkt-Adjusted</th>
|
||||
<th>Fundamental</th>
|
||||
<th title="Market cap tier (stocks only)">Cap</th>
|
||||
<th title="Growth / style classification (stocks only)">Style</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each allAssets as r}
|
||||
{#each s.allAssets as r}
|
||||
{@const dm = r.asset.displayMetrics ?? {}}
|
||||
<tr>
|
||||
<td class="ticker">{r.asset.ticker}</td>
|
||||
<td><span class="tag">{r.asset.type}</span></td>
|
||||
<td><SignalBadge signal={r.signal} /></td>
|
||||
<td><VerdictPill label={r.inflated.label} /></td>
|
||||
<td><VerdictPill label={r.fundamental.label} /></td>
|
||||
<td class="dim-cell">{dm['Cap Tier'] ?? '—'}</td>
|
||||
<td class="dim-cell">{dm['Style'] ?? '—'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
@@ -158,22 +111,22 @@
|
||||
|
||||
<!-- ── Per-type detail tables ────────────────────────────────────── -->
|
||||
{#each (['STOCK', 'ETF', 'BOND'] as const) as type}
|
||||
{#if results[type]?.length}
|
||||
{#if s.results[type]?.length}
|
||||
<AssetTable
|
||||
{type}
|
||||
rows={results[type]}
|
||||
analyzeLoading={sidebar.loading && sidebar.type === type}
|
||||
onAnalyze={() => runTabAnalysis(type)}
|
||||
rows={s.results[type]}
|
||||
analyzeLoading={s.sidebar.loading && s.sidebar.type === type}
|
||||
onAnalyze={() => s.runTabAnalysis(type)}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- ── Failed tickers ────────────────────────────────────────────── -->
|
||||
{#if results.ERROR?.length}
|
||||
{#if s.results.ERROR?.length}
|
||||
<section class="section">
|
||||
<h2>Failed <span class="count">{results.ERROR.length}</span></h2>
|
||||
<h2>Failed <span class="count">{s.results.ERROR.length}</span></h2>
|
||||
<div class="error-list">
|
||||
{#each results.ERROR as e}
|
||||
{#each s.results.ERROR as e}
|
||||
<div class="error-item"><span class="ticker">{e.ticker}</span> {e.message}</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -182,12 +135,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<AnalysisSidebar {sidebar} onClose={() => sidebar = { ...sidebar, open: false }} />
|
||||
<AnalysisSidebar sidebar={s.sidebar} onClose={() => s.closeSidebar()} />
|
||||
|
||||
<style>
|
||||
.page { max-width: 1400px; padding-bottom: 60px; }
|
||||
|
||||
/* ── Toolbar ─────────────────────────────────────────────────────── */
|
||||
.toolbar { margin-bottom: 20px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.toolbar-top { display: flex; align-items: center; gap: 8px; }
|
||||
.search-row { display: flex; gap: 8px; align-items: center; }
|
||||
@@ -225,7 +177,8 @@
|
||||
color: var(--text-dimmer);
|
||||
}
|
||||
|
||||
/* ── Error list ──────────────────────────────────────────────────── */
|
||||
.dim-cell { font-size: var(--fs-sm); color: var(--text-dim); white-space: nowrap; }
|
||||
|
||||
.error-list { padding: 12px var(--space-xl); display: flex; flex-direction: column; gap: 6px; }
|
||||
.error-item { color: var(--text-dim); font-size: 12px; }
|
||||
.error-item :global(.ticker) { color: var(--red); font-weight: 700; margin-right: 8px; }
|
||||
|
||||
Reference in New Issue
Block a user