feat: FE solid-ui (shadcn port) — Tailwind + 14 components, all pages rewritten
This commit is contained in:
+89
-67
@@ -1,6 +1,10 @@
|
||||
import { createResource, createSignal, For, Show } from "solid-js";
|
||||
import { api } from "../lib/api";
|
||||
import { EmptyState, Skeleton } from "../components/ui";
|
||||
import { EmptyState, PageHead } from "../components/ui";
|
||||
import { Badge } from "../components/ui/badge";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
|
||||
import { TextField, TextFieldInput } from "../components/ui/text-field";
|
||||
|
||||
const FIELDS = ["score", "broker_score", "foreign_net", "volume_mult", "composite"];
|
||||
const OPS = [">=", "<=", ">", "<", "=="];
|
||||
@@ -58,73 +62,91 @@ export default function Alerts() {
|
||||
}
|
||||
async function delDest(id: number) { await api.deleteDestination(id); refetchDests(); }
|
||||
return (
|
||||
<div>
|
||||
<div class="fs-page-head"><h1>Alerts</h1><p class="fs-muted">Rules evaluated over snapshots each cycle.</p></div>
|
||||
<div class="fs-card">
|
||||
<h3>Push destinations (per login)</h3>
|
||||
<p class="fs-muted">Secrets stay server-side — the list never shows tokens.</p>
|
||||
<div class="fs-row">
|
||||
<select class="fs-input" value={dkind()} onChange={(e) => setDkind(e.currentTarget.value)}>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="discord">Discord</option>
|
||||
</select>
|
||||
<input class="fs-input" placeholder="label (e.g. my phone)" value={dlabel()} onInput={(e) => setDlabel(e.currentTarget.value)} style={{ width: "160px" }} />
|
||||
{dkind() === "telegram" ? (
|
||||
<>
|
||||
<input class="fs-input" placeholder="bot token" value={dbot()} onInput={(e) => setDbot(e.currentTarget.value)} style={{ width: "200px" }} />
|
||||
<input class="fs-input" placeholder="chat id" value={dchat()} onInput={(e) => setDchat(e.currentTarget.value)} style={{ width: "120px" }} />
|
||||
</>
|
||||
) : (
|
||||
<input class="fs-input" placeholder="https://discord webhook url" value={dhook()} onInput={(e) => setDhook(e.currentTarget.value)} style={{ flex: 1 }} />
|
||||
)}
|
||||
<button class="fs-btn" onClick={addDest}>Add destination</button>
|
||||
</div>
|
||||
{derr() ? <p style={{ color: "var(--fs-avoid)" }}>{derr()}</p> : null}
|
||||
<ul>
|
||||
{(dests()?.destinations || []).map((d) => (
|
||||
<li><span class={`fs-badge ${d.enabled ? "buy" : "info"}`}>{d.kind}</span> {d.label || `#${d.id}`} <span class="fs-muted">{d.enabled ? "on" : "off"} · {d.configured ? "configured" : "missing secret"}</span>
|
||||
<button class="fs-btn ghost" onClick={() => toggleDest(d.id, d.enabled)}>{d.enabled ? "Disable" : "Enable"}</button>
|
||||
<button class="fs-btn danger" onClick={() => delDest(d.id)}>Delete</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="fs-card" style={{ "margin-top": "14px" }}>
|
||||
<h3>New rule</h3>
|
||||
<div class="fs-row">
|
||||
<input class="fs-input" placeholder="rule name" value={name()} onInput={(e) => setName(e.currentTarget.value)} style={{ flex: 1 }} />
|
||||
</div>
|
||||
<div class="fs-row" style={{ "margin-top": "10px" }}>
|
||||
<select class="fs-input" value={field()} onChange={(e) => setField(e.currentTarget.value)}>
|
||||
<For each={FIELDS}>{(f) => <option value={f}>{f}</option>}</For>
|
||||
</select>
|
||||
<select class="fs-input" value={op()} onChange={(e) => setOp(e.currentTarget.value)}>
|
||||
<For each={OPS}>{(o) => <option value={o}>{o}</option>}</For>
|
||||
</select>
|
||||
<input class="fs-input" type="number" value={value()} onInput={(e) => setValue(Number(e.currentTarget.value))} style={{ width: "100px" }} />
|
||||
<button class="fs-btn ghost" onClick={addCond}>+ condition</button>
|
||||
</div>
|
||||
<Show when={conds().length}>
|
||||
<ul><For each={conds()}>{(c, i) => <li class="fs-muted">{c.field} {c.op} {c.value} <button class="fs-btn ghost" onClick={() => setConds((p) => p.filter((_, j) => j !== i()))}>×</button></li>}</For></ul>
|
||||
</Show>
|
||||
<p class="fs-muted">Rule JSON: <code>{rulePreview()}</code></p>
|
||||
<div class="fs-row">
|
||||
<input class="fs-input" placeholder="channels, comma-separated (blank = record only)" value={channels()} onInput={(e) => setChannels(e.currentTarget.value)} style={{ flex: 1 }} />
|
||||
<button class="fs-btn" onClick={create} disabled={busy()}>Create rule</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fs-grid" style={{ "margin-top": "14px" }}>
|
||||
<div class="fs-card"><h3>Rules</h3>
|
||||
<Show when={(alerts()?.alerts || []).length} fallback={<EmptyState icon="🔔" title="No rules" hint="Create one above." />}>
|
||||
<ul><For each={alerts()?.alerts || []}>{(a) => <li>{a.name} <span class="fs-muted">last: {a.last_fired || "never"}</span> <button class="fs-btn danger" onClick={() => del(a.id)}>Delete</button></li>}</For></ul>
|
||||
<div class="space-y-4">
|
||||
<PageHead title="Alerts" sub="Rules evaluated over snapshots each cycle." />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Push destinations (per login)</CardTitle>
|
||||
<CardDescription>Secrets stay server-side — the list never shows tokens.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="flex gap-1 rounded-md bg-muted p-1">
|
||||
<Button size="sm" variant={dkind() === "telegram" ? "default" : "ghost"} onClick={() => setDkind("telegram")}>Telegram</Button>
|
||||
<Button size="sm" variant={dkind() === "discord" ? "default" : "ghost"} onClick={() => setDkind("discord")}>Discord</Button>
|
||||
</div>
|
||||
<TextField class="w-40"><TextFieldInput placeholder="label (e.g. my phone)" value={dlabel()} onInput={(e) => setDlabel(e.currentTarget.value)} /></TextField>
|
||||
{dkind() === "telegram" ? (
|
||||
<>
|
||||
<TextField class="w-52"><TextFieldInput placeholder="bot token" value={dbot()} onInput={(e) => setDbot(e.currentTarget.value)} /></TextField>
|
||||
<TextField class="w-32"><TextFieldInput placeholder="chat id" value={dchat()} onInput={(e) => setDchat(e.currentTarget.value)} /></TextField>
|
||||
</>
|
||||
) : (
|
||||
<TextField class="min-w-64 flex-1"><TextFieldInput placeholder="https://discord webhook url" value={dhook()} onInput={(e) => setDhook(e.currentTarget.value)} /></TextField>
|
||||
)}
|
||||
<Button onClick={addDest}>Add destination</Button>
|
||||
</div>
|
||||
<Show when={derr()}><p class="text-sm text-destructive">{derr()}</p></Show>
|
||||
<ul class="space-y-1.5 text-sm">
|
||||
<For each={(dests()?.destinations || [])}>
|
||||
{(d) => (
|
||||
<li class="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={d.enabled ? "success" : "secondary"}>{d.kind}</Badge>
|
||||
<span>{d.label || `#${d.id}`}</span>
|
||||
<span class="text-muted-foreground">{d.enabled ? "on" : "off"} · {d.configured ? "configured" : "missing secret"}</span>
|
||||
<span class="flex-1" />
|
||||
<Button variant="outline" size="sm" onClick={() => toggleDest(d.id, d.enabled)}>{d.enabled ? "Disable" : "Enable"}</Button>
|
||||
<Button variant="destructive" size="sm" onClick={() => delDest(d.id)}>Delete</Button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>New rule</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<TextField><TextFieldInput placeholder="rule name" value={name()} onInput={(e) => setName(e.currentTarget.value)} /></TextField>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<select class="h-10 rounded-md border border-input bg-background px-3 text-sm" value={field()} onChange={(e) => setField(e.currentTarget.value)}>
|
||||
<For each={FIELDS}>{(f) => <option value={f}>{f}</option>}</For>
|
||||
</select>
|
||||
<select class="h-10 rounded-md border border-input bg-background px-3 text-sm" value={op()} onChange={(e) => setOp(e.currentTarget.value)}>
|
||||
<For each={OPS}>{(o) => <option value={o}>{o}</option>}</For>
|
||||
</select>
|
||||
<TextField class="w-24"><TextFieldInput type="number" value={value()} onInput={(e) => setValue(Number(e.currentTarget.value))} /></TextField>
|
||||
<Button variant="outline" onClick={addCond}>+ condition</Button>
|
||||
</div>
|
||||
<Show when={conds().length}>
|
||||
<ul class="space-y-1 text-sm text-muted-foreground"><For each={conds()}>{(c, i) => <li>{c.field} {c.op} {c.value} <Button variant="ghost" size="sm" onClick={() => setConds((p) => p.filter((_, j) => j !== i()))}>×</Button></li>}</For></ul>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="fs-card"><h3>Event history</h3>
|
||||
<button class="fs-btn ghost" onClick={() => refetchEv()}>Refresh</button>
|
||||
<Show when={(events() || []).length} fallback={<p class="fs-muted">No events yet.</p>}>
|
||||
<ul><For each={events() || []}>{(e) => <li><strong>{String(e.ticker)}</strong> — {String(e.message)}</li>}</For></ul>
|
||||
</Show>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">Rule JSON: <code class="font-mono">{rulePreview()}</code></p>
|
||||
<div class="flex gap-2">
|
||||
<TextField class="flex-1"><TextFieldInput placeholder="channels, comma-separated (blank = record only)" value={channels()} onInput={(e) => setChannels(e.currentTarget.value)} /></TextField>
|
||||
<Button onClick={create} disabled={busy()}>Create rule</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Rules</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<Show when={(alerts()?.alerts || []).length} fallback={<EmptyState icon="🔔" title="No rules" hint="Create one above." />}>
|
||||
<ul class="space-y-1.5 text-sm"><For each={alerts()?.alerts || []}>{(a) => <li class="flex items-center gap-2">{a.name} <span class="text-muted-foreground">last: {a.last_fired || "never"}</span><span class="flex-1" /><Button variant="destructive" size="sm" onClick={() => del(a.id)}>Delete</Button></li>}</For></ul>
|
||||
</Show>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle>Event history</CardTitle>
|
||||
<Button variant="ghost" size="sm" onClick={() => refetchEv()}>Refresh</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Show when={(events() || []).length} fallback={<p class="text-sm text-muted-foreground">No events yet.</p>}>
|
||||
<ul class="space-y-1.5 text-sm"><For each={events() || []}>{(e) => <li><Badge variant="secondary">{String(e.ticker)}</Badge> {String(e.message)}</li>}</For></ul>
|
||||
</Show>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+43
-31
@@ -2,7 +2,9 @@ import { createResource, createSignal, onCleanup, For, Show } from "solid-js";
|
||||
import { api, subscribeSSE, fmtIDR } from "../lib/api";
|
||||
import { AccumulationTable, ActivityFeed, RotationMap } from "../components/Cards";
|
||||
import { Citations } from "../components/Citations";
|
||||
import { StatCard, EmptyState, Skeleton } from "../components/ui";
|
||||
import { Badge } from "../components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
|
||||
import { Skeleton } from "../components/ui/skeleton";
|
||||
import { Line } from "solid-chartjs";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
Chart.register(...registerables);
|
||||
@@ -24,45 +26,55 @@ export default function Dashboard() {
|
||||
datasets: [{ label: "BBCA foreign net", data: (foreign()?.nets || []) as number[] }],
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div class="fs-page-head">
|
||||
<h1>Smart Money Dashboard</h1>
|
||||
<p class="fs-muted">Credits {health()?.credits_today ?? "…"} · Scheduler {health()?.scheduler_ok ? "ok" : "down"} · Last cycle {health()?.last_cycle_at || "never"}</p>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Smart Money Dashboard</h1>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Credits {health()?.credits_today ?? "…"} · Scheduler {health()?.scheduler_ok ? "ok" : "down"} · Last cycle {health()?.last_cycle_at || "never"}
|
||||
</p>
|
||||
</div>
|
||||
<Show when={health()?.stale_flags?.length}>
|
||||
<p class="fs-muted">Stale: <For each={health()!.stale_flags}>{(f) => <span>{f}; </span>}</For></p>
|
||||
<p class="text-sm text-muted-foreground">Stale: <For each={health()!.stale_flags}>{(f) => <Badge variant="outline">{f}</Badge>}</For></p>
|
||||
</Show>
|
||||
<Show when={!flow.loading} fallback={<Skeleton rows={4} />}>
|
||||
<div class="fs-grid">
|
||||
<StatCard title="Foreign net total" value={flow() ? fmtIDR(flow()!.foreign_net_total) : "…"}>
|
||||
<Citations items={flow()?.citations} />
|
||||
</StatCard>
|
||||
<Show when={!flow.loading} fallback={<div class="space-y-2"><Skeleton class="h-24 w-full" /><Skeleton class="h-24 w-full" /><Skeleton class="h-24 w-full" /></div>}>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Foreign net total</CardTitle><CardDescription>Aggregate across snapshots.</CardDescription></CardHeader>
|
||||
<CardContent>
|
||||
<p class="font-mono text-2xl font-bold">{flow() ? fmtIDR(flow()!.foreign_net_total) : "…"}</p>
|
||||
<Citations items={flow()?.citations} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AccumulationTable rows={flow()?.top_accumulation || []} />
|
||||
<RotationMap signal={undefined} citations={flow()?.citations} />
|
||||
<div class="fs-card">
|
||||
<h3>Foreign flow (BBCA)</h3>
|
||||
<Show when={(foreign()?.dates || []).length} fallback={<p class="fs-muted">No foreign-flow series yet.</p>}>
|
||||
<Line data={flowChart()} options={{ responsive: true }} />
|
||||
<Show when={foreign()?.reversal}><p>Reversal flag set.</p></Show>
|
||||
</Show>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Foreign flow (BBCA)</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<Show when={(foreign()?.dates || []).length} fallback={<p class="text-sm text-muted-foreground">No foreign-flow series yet.</p>}>
|
||||
<Line data={flowChart()} options={{ responsive: true }} />
|
||||
<Show when={foreign()?.reversal}><p class="text-sm">Reversal flag set.</p></Show>
|
||||
</Show>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ActivityFeed events={(events() || []).map((e) => ({ ticker: String(e.ticker || ""), message: String(e.message || ""), date: String(e.date || "") }))} />
|
||||
<div class="fs-card">
|
||||
<h3>Live agent panel</h3>
|
||||
<Show when={live().length} fallback={<p class="fs-muted">Waiting for SSE events…</p>}>
|
||||
<ul><For each={live()}>{(l) => <li class="fs-muted">{l}</li>}</For></ul>
|
||||
</Show>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Live agent panel</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<Show when={live().length} fallback={<p class="text-sm text-muted-foreground">Waiting for SSE events…</p>}>
|
||||
<ul class="space-y-1 text-sm text-muted-foreground"><For each={live()}>{(l) => <li>{l}</li>}</For></ul>
|
||||
</Show>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Show>
|
||||
<div style={{ "margin-top": "14px" }}>
|
||||
<div class="fs-card">
|
||||
<h3>Morning briefing</h3>
|
||||
<Show when={briefing()} fallback={<p class="fs-muted">No briefing yet — subscribe to the morning-briefing routine.</p>}>
|
||||
<pre class="fs-muted">{(briefing() as { payload: string })?.payload?.slice(0, 800)}</pre>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Morning briefing</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<Show when={briefing()} fallback={<p class="text-sm text-muted-foreground">No briefing yet — subscribe to the morning-briefing routine.</p>}>
|
||||
<pre class="whitespace-pre-wrap text-sm text-muted-foreground">{(briefing() as { payload: string })?.payload?.slice(0, 800)}</pre>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+49
-39
@@ -2,7 +2,10 @@ import { createResource, For, Show } from "solid-js";
|
||||
import { api } from "../lib/api";
|
||||
import { Bar } from "solid-chartjs";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
import { StatCard, Skeleton } from "../components/ui";
|
||||
import { PageHead } from "../components/ui";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
||||
import { Skeleton } from "../components/ui/skeleton";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
Chart.register(...registerables);
|
||||
|
||||
export default function Portfolio() {
|
||||
@@ -13,32 +16,40 @@ export default function Portfolio() {
|
||||
datasets: [{ label: "Weight %", data: (risk()?.concentration || []).map((c) => +(c.weight * 100).toFixed(1)) }],
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div class="fs-page-head"><h1>Portfolio Risk + Accuracy Ledger</h1><p class="fs-muted">Computed from your watchlist.</p></div>
|
||||
<Show when={!risk.loading && !acc.loading} fallback={<Skeleton rows={4} />}>
|
||||
<div class="fs-grid">
|
||||
<div class="fs-card">
|
||||
<h3>Concentration</h3>
|
||||
<Show when={(risk()?.concentration || []).length} fallback={<p class="fs-muted">Add tickers to your watchlist to see concentration.</p>}>
|
||||
<Bar data={chartData()} options={{ responsive: true }} />
|
||||
</Show>
|
||||
<For each={risk()?.warnings || []}>{(w) => <p>⚠️ {w}</p>}</For>
|
||||
</div>
|
||||
<StatCard title="Beta vs index" value={risk() ? String(risk()!.beta) : "…"}>
|
||||
<h3 style={{ "margin-top": "12px" }}>Correlation heatmap</h3>
|
||||
<CorrHeatmap matrix={risk()?.correlation || {}} />
|
||||
</StatCard>
|
||||
<div class="fs-card">
|
||||
<h3>Agent accuracy</h3>
|
||||
<table class="fs-table">
|
||||
<thead><tr><th>Agent</th><th>Calls</th><th>Resolved</th><th>Hit %</th></tr></thead>
|
||||
<tbody>
|
||||
<For each={acc()?.agents || []}>
|
||||
{(a) => <tr><td>{a.agent}</td><td>{a.calls}</td><td>{a.resolved}</td><td>{(a.hit_rate * 100).toFixed(0)}%</td></tr>}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<PageHead title="Portfolio Risk + Accuracy Ledger" sub="Computed from your watchlist." />
|
||||
<Show when={!risk.loading && !acc.loading} fallback={<div class="space-y-2"><Skeleton class="h-24 w-full" /><Skeleton class="h-24 w-full" /></div>}>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Concentration</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<Show when={(risk()?.concentration || []).length} fallback={<p class="text-sm text-muted-foreground">Add tickers to your watchlist to see concentration.</p>}>
|
||||
<Bar data={chartData()} options={{ responsive: true }} />
|
||||
</Show>
|
||||
<For each={risk()?.warnings || []}>{(w) => <p class="text-sm">⚠️ {w}</p>}</For>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Beta vs index</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<p class="font-mono text-2xl font-bold">{risk() ? String(risk()!.beta) : "…"}</p>
|
||||
<h3 class="text-sm font-semibold">Correlation heatmap</h3>
|
||||
<CorrHeatmap matrix={risk()?.correlation || {}} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Agent accuracy</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader><TableRow><TableHead>Agent</TableHead><TableHead class="text-right">Calls</TableHead><TableHead class="text-right">Resolved</TableHead><TableHead class="text-right">Hit %</TableHead></TableRow></TableHeader>
|
||||
<TableBody>
|
||||
<For each={acc()?.agents || []}>
|
||||
{(a) => <TableRow><TableCell>{a.agent}</TableCell><TableCell class="text-right">{a.calls}</TableCell><TableCell class="text-right">{a.resolved}</TableCell><TableCell class="text-right">{(a.hit_rate * 100).toFixed(0)}%</TableCell></TableRow>}
|
||||
</For>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -49,29 +60,28 @@ function CorrHeatmap(props: { matrix: Record<string, Record<string, number>> })
|
||||
const keys = () => Object.keys(props.matrix);
|
||||
const cell = (v: number) => {
|
||||
const a = Math.min(1, Math.abs(v));
|
||||
const bg = v >= 0 ? `rgba(63,185,80,${(0.15 + 0.65 * a).toFixed(2)})` : `rgba(248,81,73,${(0.15 + 0.65 * a).toFixed(2)})`;
|
||||
return bg;
|
||||
return v >= 0 ? `rgba(34,197,94,${(0.12 + 0.6 * a).toFixed(2)})` : `rgba(239,68,68,${(0.12 + 0.6 * a).toFixed(2)})`;
|
||||
};
|
||||
return (
|
||||
<Show when={keys().length} fallback={<p class="fs-muted">No correlation data yet.</p>}>
|
||||
<div class="fs-scroll"><table class="fs-table">
|
||||
<thead><tr><th></th><For each={keys()}>{(k) => <th>{k}</th>}</For></tr></thead>
|
||||
<tbody>
|
||||
<Show when={keys().length} fallback={<p class="text-sm text-muted-foreground">No correlation data yet.</p>}>
|
||||
<div class="overflow-x-auto"><Table>
|
||||
<TableHeader><TableRow><TableHead></TableHead><For each={keys()}>{(k) => <TableHead>{k}</TableHead>}</For></TableRow></TableHeader>
|
||||
<TableBody>
|
||||
<For each={keys()}>
|
||||
{(r) => (
|
||||
<tr>
|
||||
<td><strong>{r}</strong></td>
|
||||
<TableRow>
|
||||
<TableCell class="font-bold">{r}</TableCell>
|
||||
<For each={keys()}>
|
||||
{(c) => {
|
||||
const v = props.matrix[r]?.[c] ?? 0;
|
||||
return <td style={{ background: cell(v) }} title={`${r}/${c} = ${v.toFixed(2)}`}>{v.toFixed(2)}</td>;
|
||||
return <TableCell style={{ background: cell(v) }} title={`${r}/${c} = ${v.toFixed(2)}`}>{v.toFixed(2)}</TableCell>;
|
||||
}}
|
||||
</For>
|
||||
</tr>
|
||||
</TableRow>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</TableBody>
|
||||
</Table></div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
+46
-37
@@ -2,7 +2,12 @@ import { createSignal, For, Show, onMount } from "solid-js";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { api, type ReportPayload } from "../lib/api";
|
||||
import { Citations } from "../components/Citations";
|
||||
import { RecBadge, Skeleton } from "../components/ui";
|
||||
import { PageHead, RecBadge } from "../components/ui";
|
||||
import { Badge } from "../components/ui/badge";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
||||
import { Skeleton } from "../components/ui/skeleton";
|
||||
import { TextField, TextFieldInput } from "../components/ui/text-field";
|
||||
|
||||
export default function Report() {
|
||||
const params = useParams();
|
||||
@@ -35,54 +40,58 @@ export default function Report() {
|
||||
a.href = url; a.download = name; a.click();
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div class="fs-page-head">
|
||||
<h1>Report: {params.ticker}</h1>
|
||||
<button class="fs-btn ghost" onClick={load}>↻ Regenerate</button>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<PageHead title={`Report: ${params.ticker}`} />
|
||||
<Button variant="outline" onClick={load}>↻ Regenerate</Button>
|
||||
</div>
|
||||
<Show when={err()}><p style={{ color: "var(--fs-avoid)" }}>{err()}</p></Show>
|
||||
<Show when={busy()}><Skeleton rows={5} /></Show>
|
||||
<Show when={err()}><p class="text-sm text-destructive">{err()}</p></Show>
|
||||
<Show when={busy()}><div class="space-y-2"><Skeleton class="h-32 w-full" /><Skeleton class="h-24 w-full" /></div></Show>
|
||||
<Show when={rep()}>
|
||||
<div class="fs-card" style={{ "border-left": "4px solid var(--fs-accent)" }}>
|
||||
<div class="fs-row">
|
||||
<Card class="border-l-4 border-l-primary">
|
||||
<CardContent class="flex flex-wrap items-center gap-2 pt-6">
|
||||
<RecBadge rec={rep()!.synthesis.recommendation} />
|
||||
<strong>conviction {rep()!.synthesis.conviction}/5</strong>
|
||||
<Show when={rep()!.synthesis.conflict}><span class="fs-badge info">conflict flagged</span></Show>
|
||||
<span class="fs-muted">position {rep()!.synthesis.position_pct}%</span>
|
||||
</div>
|
||||
<p>{rep()!.synthesis.thesis}</p>
|
||||
<Citations items={rep()!.citations} />
|
||||
</div>
|
||||
<div class="fs-card" style={{ "margin-top": "14px" }}>
|
||||
<h3>Export</h3>
|
||||
<div class="fs-row">
|
||||
<button class="fs-btn ghost" onClick={exportMd}>Markdown</button>
|
||||
<button class="fs-btn ghost" onClick={async () => {
|
||||
<Show when={rep()!.synthesis.conflict}><Badge variant="secondary">conflict flagged</Badge></Show>
|
||||
<span class="text-sm text-muted-foreground">position {rep()!.synthesis.position_pct}%</span>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<p class="text-sm">{rep()!.synthesis.thesis}</p>
|
||||
<div class="mt-2"><Citations items={rep()!.citations} /></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Export</CardTitle></CardHeader>
|
||||
<CardContent class="flex flex-wrap gap-2">
|
||||
<Button variant="outline" onClick={exportMd}>Markdown</Button>
|
||||
<Button variant="outline" onClick={async () => {
|
||||
const r = await fetch(`/api/report/${params.ticker}?format=html`, { method: "POST", headers: authHeaders() });
|
||||
dl(URL.createObjectURL(new Blob([await r.text()], { type: "text/html" })), `${params.ticker}-report.html`);
|
||||
}}>HTML</button>
|
||||
<button class="fs-btn ghost" onClick={() => {
|
||||
}}>HTML</Button>
|
||||
<Button variant="outline" onClick={() => {
|
||||
dl(URL.createObjectURL(new Blob([JSON.stringify(rep(), null, 1)], { type: "application/json" })), `${params.ticker}-report.json`);
|
||||
}}>JSON</button>
|
||||
<button class="fs-btn ghost" onClick={async () => {
|
||||
}}>JSON</Button>
|
||||
<Button variant="outline" onClick={async () => {
|
||||
const r = await fetch(`/api/report/${params.ticker}?format=pdf`, { method: "POST", headers: authHeaders() });
|
||||
dl(URL.createObjectURL(await r.blob()), `${params.ticker}-report.pdf`);
|
||||
}}>PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
}}>PDF</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<For each={rep()!.sections}>
|
||||
{(s) => <div class="fs-card" style={{ "margin-top": "14px" }}><h3>{s.name}</h3><p style={{ "white-space": "pre-wrap" }}>{s.body}</p><Citations items={s.citations} /></div>}
|
||||
{(s) => <Card><CardHeader><CardTitle>{s.name}</CardTitle></CardHeader><CardContent><p class="whitespace-pre-wrap text-sm">{s.body}</p><div class="mt-2"><Citations items={s.citations} /></div></CardContent></Card>}
|
||||
</For>
|
||||
<div class="fs-card" style={{ "margin-top": "14px" }}>
|
||||
<h3>Interrogate this report</h3>
|
||||
<div class="fs-row">
|
||||
<input class="fs-input" placeholder="Ask about this report…" value={question()} onInput={(e) => setQuestion(e.currentTarget.value)} style={{ flex: 1 }} />
|
||||
<button class="fs-btn" onClick={ask} disabled={asking()}>{asking() ? "…" : "Ask"}</button>
|
||||
</div>
|
||||
<Show when={answer()}><p style={{ "margin-top": "10px" }}>{answer()}</p></Show>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Interrogate this report</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<div class="flex gap-2">
|
||||
<TextField class="flex-1"><TextFieldInput placeholder="Ask about this report…" value={question()} onInput={(e) => setQuestion(e.currentTarget.value)} /></TextField>
|
||||
<Button onClick={ask} disabled={asking()}>{asking() ? "…" : "Ask"}</Button>
|
||||
</div>
|
||||
<Show when={answer()}><p class="text-sm">{answer()}</p></Show>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Show>
|
||||
<Show when={md()}><div class="fs-card" style={{ "margin-top": "14px" }}><pre class="fs-muted">{md().slice(0, 2000)}</pre></div></Show>
|
||||
<Show when={md()}><Card><CardContent class="pt-6"><pre class="whitespace-pre-wrap text-sm text-muted-foreground">{md().slice(0, 2000)}</pre></CardContent></Card></Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+54
-42
@@ -1,7 +1,11 @@
|
||||
import { createResource, createSignal, For, Show } from "solid-js";
|
||||
import { api } from "../lib/api";
|
||||
import { Citations } from "../components/Citations";
|
||||
import { EmptyState } from "../components/ui";
|
||||
import { EmptyState, PageHead } from "../components/ui";
|
||||
import { Badge } from "../components/ui/badge";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
||||
import { TextField, TextFieldInput } from "../components/ui/text-field";
|
||||
|
||||
const TYPES = ["morning-briefing", "accumulation-radar", "foreign-reversal", "insider-tape", "earnings-countdown", "dividend-calendar", "weekend-review"];
|
||||
|
||||
@@ -12,7 +16,6 @@ export default function Routines() {
|
||||
const [schedule, setSchedule] = createSignal("");
|
||||
const [channels, setChannels] = createSignal("");
|
||||
const [busy, setBusy] = createSignal(false);
|
||||
const [runNowId, setRunNowId] = createSignal<number | null>(null);
|
||||
const [briefing] = createResource(() => api.briefing().catch(() => null));
|
||||
async function subscribe() {
|
||||
setBusy(true);
|
||||
@@ -34,54 +37,63 @@ export default function Routines() {
|
||||
refetch();
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div class="fs-page-head"><h1>Routine Manager</h1><p class="fs-muted">Scheduled deliveries with citations.</p></div>
|
||||
<div class="fs-card">
|
||||
<h3>Subscribe</h3>
|
||||
<div class="fs-row">
|
||||
<select class="fs-input" value={type_()} onChange={(e) => setType(e.currentTarget.value)}>
|
||||
<For each={TYPES}>{(t) => <option value={t}>{t}</option>}</For>
|
||||
</select>
|
||||
<input class="fs-input" placeholder="schedule cron (blank = default)" value={schedule()} onInput={(e) => setSchedule(e.currentTarget.value)} style={{ width: "240px" }} />
|
||||
<input class="fs-input" placeholder="channels, comma-separated" value={channels()} onInput={(e) => setChannels(e.currentTarget.value)} style={{ width: "200px" }} />
|
||||
<button class="fs-btn" onClick={subscribe} disabled={busy()}>Subscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<PageHead title="Routine Manager" sub="Scheduled deliveries with citations." />
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Subscribe</CardTitle></CardHeader>
|
||||
<CardContent class="flex flex-wrap gap-2">
|
||||
<TextField class="w-52">
|
||||
<select class="h-10 w-full rounded-md border border-input bg-background px-3 text-sm" value={type_()} onChange={(e) => setType(e.currentTarget.value)}>
|
||||
<For each={TYPES}>{(t) => <option value={t}>{t}</option>}</For>
|
||||
</select>
|
||||
</TextField>
|
||||
<TextField class="w-60"><TextFieldInput placeholder="schedule cron (blank = default)" value={schedule()} onInput={(e) => setSchedule(e.currentTarget.value)} /></TextField>
|
||||
<TextField class="w-52"><TextFieldInput placeholder="channels, comma-separated" value={channels()} onInput={(e) => setChannels(e.currentTarget.value)} /></TextField>
|
||||
<Button onClick={subscribe} disabled={busy()}>Subscribe</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Show when={(data()?.routines || []).length} fallback={
|
||||
<div style={{ "margin-top": "14px" }}><EmptyState icon="🗓️" title="No routines yet" hint="Subscribe above — the morning briefing runs 07:30 WIB daily." /></div>
|
||||
<EmptyState icon="🗓️" title="No routines yet" hint="Subscribe above — the morning briefing runs 07:30 WIB daily." />
|
||||
}>
|
||||
<div class="fs-grid" style={{ "margin-top": "14px" }}>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<For each={data()?.routines || []}>
|
||||
{(r) => (
|
||||
<div class="fs-card">
|
||||
<div class="fs-row" style={{ "justify-content": "space-between" }}>
|
||||
<strong>{r.type}</strong>
|
||||
<span class={`fs-badge ${r.enabled ? "buy" : "info"}`}>{r.enabled ? "on" : "off"}</span>
|
||||
</div>
|
||||
<p class="fs-muted">{r.schedule_cron}</p>
|
||||
<div class="fs-row">
|
||||
<button class="fs-btn ghost" onClick={() => toggle(r.id, r.enabled)}>{r.enabled ? "Disable" : "Enable"}</button>
|
||||
<button class="fs-btn ghost" disabled={runNowId() === r.id}>Run</button>
|
||||
<button class="fs-btn danger" onClick={() => del(r.id)}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-base">{r.type}</CardTitle>
|
||||
<Badge variant={r.enabled ? "success" : "secondary"}>{r.enabled ? "on" : "off"}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<p class="font-mono text-xs text-muted-foreground">{r.schedule_cron}</p>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => toggle(r.id, r.enabled)}>{r.enabled ? "Disable" : "Enable"}</Button>
|
||||
<Button variant="destructive" size="sm" onClick={() => del(r.id)}>Delete</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="fs-grid" style={{ "margin-top": "14px" }}>
|
||||
<div class="fs-card">
|
||||
<h3>Latest briefing</h3>
|
||||
<Show when={briefing()} fallback={<p class="fs-muted">No briefing yet — it generates after the first morning-briefing run.</p>}>
|
||||
<pre class="fs-muted" style={{ "white-space": "pre-wrap" }}>{(briefing() as { payload: string })?.payload?.slice(0, 800)}</pre>
|
||||
<Citations items={(briefing() as { citations: string })?.citations} />
|
||||
</Show>
|
||||
</div>
|
||||
<div class="fs-card">
|
||||
<h3>Run history</h3>
|
||||
<button class="fs-btn ghost" onClick={() => refetchRuns()}>Refresh</button>
|
||||
<ul><For each={runs() || []}>{(r) => <li class="fs-muted">{String(r.started_at)} — {String(r.status)}</li>}</For></ul>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Latest briefing</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<Show when={briefing()} fallback={<p class="text-sm text-muted-foreground">No briefing yet — it generates after the first morning-briefing run.</p>}>
|
||||
<pre class="whitespace-pre-wrap text-sm text-muted-foreground">{(briefing() as { payload: string })?.payload?.slice(0, 800)}</pre>
|
||||
<div class="mt-2"><Citations items={(briefing() as { citations: string })?.citations} /></div>
|
||||
</Show>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle>Run history</CardTitle>
|
||||
<Button variant="ghost" size="sm" onClick={() => refetchRuns()}>Refresh</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul class="space-y-1 text-sm text-muted-foreground"><For each={runs() || []}>{(r) => <li>{String(r.started_at)} — {String(r.status)}</li>}</For></ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+64
-41
@@ -1,11 +1,16 @@
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import { api, type ScreenRow } from "../lib/api";
|
||||
import { Citations } from "../components/Citations";
|
||||
import { BreakdownBars, EmptyState, RecBadge, Skeleton } from "../components/ui";
|
||||
import { BreakdownBars, EmptyState, PageHead, RecBadge } from "../components/ui";
|
||||
import { Badge } from "../components/ui/badge";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
||||
import { Skeleton } from "../components/ui/skeleton";
|
||||
import { TextField, TextFieldInput } from "../components/ui/text-field";
|
||||
import { Switch, SwitchControl, SwitchLabel, SwitchThumb } from "../components/ui/switch";
|
||||
|
||||
const SAVED_KEY = "fs-saved-screeners";
|
||||
|
||||
// scoreOf extracts a -100..+100 composite from the row breakdown for the badge.
|
||||
function scoreOf(r: ScreenRow): number {
|
||||
const b = r.breakdown as Record<string, unknown>;
|
||||
for (const k of ["composite", "score", "broker_score", "broker"]) {
|
||||
@@ -49,55 +54,73 @@ export default function Screener() {
|
||||
setSaved(all);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div class="fs-page-head"><h1>Institutional Screener</h1><p class="fs-muted">Rank tickers by smart-money signals.</p></div>
|
||||
<div class="fs-card">
|
||||
<div class="fs-row">
|
||||
<select class="fs-input" value={mode()} onChange={(e) => setMode(e.currentTarget.value as "where" | "q")}>
|
||||
<option value="q">Natural language (q)</option>
|
||||
<option value="where">SQL-like (where)</option>
|
||||
</select>
|
||||
<input class="fs-input" placeholder={mode() === "q" ? "e.g. large banks with foreign inflow" : "e.g. market_cap > 10T"} value={q()} onInput={(e) => setQ(e.currentTarget.value)} style={{ flex: 1, "min-width": "200px" }} />
|
||||
<button class="fs-btn" onClick={run} disabled={busy()}>{busy() ? " screening…" : "Screen"}</button>
|
||||
</div>
|
||||
<div class="fs-row" style={{ "margin-top": "10px" }}>
|
||||
<label class="fs-muted">broker ≥ <input class="fs-input" type="number" value={brokerMin()} onInput={(e) => setBrokerMin(Number(e.currentTarget.value))} style={{ width: "80px" }} /></label>
|
||||
<label class="fs-muted"><input type="checkbox" checked={foreignOnly()} onChange={(e) => setForeignOnly(e.currentTarget.checked)} /> foreign inflow</label>
|
||||
<label class="fs-muted"><input type="checkbox" checked={insiderOnly()} onChange={(e) => setInsiderOnly(e.currentTarget.checked)} /> insider buying</label>
|
||||
</div>
|
||||
<Show when={err()}><p style={{ color: "var(--fs-avoid)" }}>{err()}</p></Show>
|
||||
</div>
|
||||
<Show when={busy()}><div style={{ "margin-top": "14px" }}><Skeleton rows={3} /></div></Show>
|
||||
<div class="space-y-4">
|
||||
<PageHead title="Institutional Screener" sub="Rank tickers by smart-money signals." />
|
||||
<Card>
|
||||
<CardContent class="space-y-3 pt-6">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="flex gap-1 rounded-md bg-muted p-1">
|
||||
<Button size="sm" variant={mode() === "q" ? "default" : "ghost"} onClick={() => setMode("q")}>Natural language</Button>
|
||||
<Button size="sm" variant={mode() === "where" ? "default" : "ghost"} onClick={() => setMode("where")}>SQL-like</Button>
|
||||
</div>
|
||||
<TextField class="min-w-52 flex-1">
|
||||
<TextFieldInput placeholder={mode() === "q" ? "e.g. large banks with foreign inflow" : "e.g. market_cap > 10T"} value={q()} onInput={(e) => setQ(e.currentTarget.value)} />
|
||||
</TextField>
|
||||
<Button onClick={run} disabled={busy()}>{busy() ? "Screening…" : "Screen"}</Button>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm">
|
||||
<label class="flex items-center gap-2 text-muted-foreground">broker ≥
|
||||
<TextField class="w-20"><TextFieldInput type="number" value={brokerMin()} onInput={(e) => setBrokerMin(Number(e.currentTarget.value))} /></TextField>
|
||||
</label>
|
||||
<Switch checked={foreignOnly()} onChange={setForeignOnly}>
|
||||
<SwitchControl><SwitchThumb /></SwitchControl>
|
||||
<SwitchLabel>foreign inflow</SwitchLabel>
|
||||
</Switch>
|
||||
<Switch checked={insiderOnly()} onChange={setInsiderOnly}>
|
||||
<SwitchControl><SwitchThumb /></SwitchControl>
|
||||
<SwitchLabel>insider buying</SwitchLabel>
|
||||
</Switch>
|
||||
</div>
|
||||
<Show when={err()}><p class="text-sm text-destructive">{err()}</p></Show>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Show when={busy()}><div class="space-y-2"><Skeleton class="h-24 w-full" /><Skeleton class="h-24 w-full" /></div></Show>
|
||||
<Show when={ran() && !busy()}>
|
||||
<Show when={rows().length} fallback={<div style={{ "margin-top": "14px" }}><EmptyState icon="🔍" title="No matches" hint="Loosen the filters and try again." /></div>}>
|
||||
<div class="fs-grid" style={{ "margin-top": "14px" }}>
|
||||
<Show when={rows().length} fallback={<EmptyState icon="🔍" title="No matches" hint="Loosen the filters and try again." />}>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<For each={rows()}>
|
||||
{(r) => (
|
||||
<div class="fs-card">
|
||||
<div class="fs-row" style={{ "justify-content": "space-between" }}>
|
||||
<strong><a href={`/report/${r.symbol}`}>{r.symbol}</a></strong>
|
||||
<span class="fs-muted">{r.composite.toFixed(1)}</span>
|
||||
</div>
|
||||
<div style={{ "margin": "8px 0" }}><BreakdownBars breakdown={r.breakdown as Record<string, unknown>} /></div>
|
||||
<Citations items={r.citations} />
|
||||
<Show when={scoreOf(r) !== 0}><div style={{ "margin-top": "6px" }}><RecBadge rec={scoreOf(r) >= 40 ? "BUY" : scoreOf(r) <= -40 ? "AVOID" : "HOLD"} /></div></Show>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<a class="font-bold text-primary hover:underline" href={`/report/${r.symbol}`}>{r.symbol}</a>
|
||||
<Badge variant="secondary" class="font-mono">{r.composite.toFixed(1)}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<BreakdownBars breakdown={r.breakdown as Record<string, unknown>} />
|
||||
<Citations items={r.citations} />
|
||||
<Show when={scoreOf(r) !== 0}><RecBadge rec={scoreOf(r) >= 40 ? "BUY" : scoreOf(r) <= -40 ? "AVOID" : "HOLD"} /></Show>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={!ran() && !busy()}>
|
||||
<div style={{ "margin-top": "14px" }}><EmptyState icon="🔍" title="Run a screen" hint="Ranked rows with per-signal bars appear here." /></div>
|
||||
<EmptyState icon="🔍" title="Run a screen" hint="Ranked rows with per-signal bars appear here." />
|
||||
</Show>
|
||||
<div class="fs-card" style={{ "margin-top": "14px" }}>
|
||||
<h3>Saved screeners</h3>
|
||||
<div class="fs-row">
|
||||
<input class="fs-input" placeholder="name" value={sname()} onInput={(e) => setSname(e.currentTarget.value)} style={{ width: "140px" }} />
|
||||
<button class="fs-btn ghost" onClick={save}>Save current</button>
|
||||
</div>
|
||||
<ul><For each={Object.entries(saved())}>{([n, query]) => <li><button class="fs-btn ghost" onClick={() => { setQ(query); run(); }}>{n}</button> <span class="fs-muted">{query.slice(0, 60)}</span></li>}</For></ul>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Saved screeners</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<div class="flex gap-2">
|
||||
<TextField class="w-36"><TextFieldInput placeholder="name" value={sname()} onInput={(e) => setSname(e.currentTarget.value)} /></TextField>
|
||||
<Button variant="outline" onClick={save}>Save current</Button>
|
||||
</div>
|
||||
<ul class="space-y-1 text-sm">
|
||||
<For each={Object.entries(saved())}>{([n, query]) => <li><Button variant="ghost" size="sm" onClick={() => { setQ(query); run(); }}>{n}</Button> <span class="text-muted-foreground">{query.slice(0, 60)}</span></li>}</For>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user