diff --git a/web/src/pages/Alerts.tsx b/web/src/pages/Alerts.tsx index b8bb226..13c990d 100644 --- a/web/src/pages/Alerts.tsx +++ b/web/src/pages/Alerts.tsx @@ -1,26 +1,44 @@ -import { createResource, createSignal, For } from "solid-js"; +import { createResource, createSignal, For, Show } from "solid-js"; import { api } from "../lib/api"; +import { EmptyState, Skeleton } from "../components/ui"; + +const FIELDS = ["score", "broker_score", "foreign_net", "volume_mult", "composite"]; +const OPS = [">=", "<=", ">", "<", "=="]; + +type Cond = { field: string; op: string; value: number }; export default function Alerts() { const [alerts, { refetch }] = createResource(() => api.alerts()); const [events, { refetch: refetchEv }] = createResource(() => api.alertEvents("2000-01-01").then((r) => r.events.slice(0, 30))); const [dests, { refetch: refetchDests }] = createResource(() => api.destinations()); const [name, setName] = createSignal("accumulation watcher"); - const [rule, setRule] = createSignal('{"all": true}'); + const [field, setField] = createSignal("score"); + const [op, setOp] = createSignal(">="); + const [value, setValue] = createSignal(60); + const [conds, setConds] = createSignal([]); const [channels, setChannels] = createSignal(""); + const [busy, setBusy] = createSignal(false); const [dkind, setDkind] = createSignal("telegram"); const [dlabel, setDlabel] = createSignal(""); const [dbot, setDbot] = createSignal(""); const [dchat, setDchat] = createSignal(""); const [dhook, setDhook] = createSignal(""); const [derr, setDerr] = createSignal(""); + const rulePreview = () => JSON.stringify(conds().length ? { all: conds() } : { all: true }); + function addCond() { + setConds((p) => [...p, { field: field(), op: op(), value: value() }]); + } async function create() { - await api.createAlert({ - name: name(), - rule: JSON.parse(rule()), - channels: channels().split(",").map((c) => c.trim()).filter(Boolean), - }); - refetch(); + setBusy(true); + try { + await api.createAlert({ + name: name(), + rule: JSON.parse(rulePreview()), + channels: channels().split(",").map((c) => c.trim()).filter(Boolean), + }); + setConds([]); + refetch(); + } finally { setBusy(false); } } async function del(id: number) { await api.deleteAlert(id); refetch(); } async function addDest() { @@ -41,49 +59,71 @@ export default function Alerts() { async function delDest(id: number) { await api.deleteDestination(id); refetchDests(); } return (
-

Alerts

+

Alerts

Rules evaluated over snapshots each cycle.

Push destinations (per login)

-

Secrets stay server-side — the list never shows tokens. Delivery uses your enabled destinations; server env is fallback.

- - setDlabel(e.currentTarget.value)} style={{ width: "160px" }} /> - {dkind() === "telegram" ? ( - <> - setDbot(e.currentTarget.value)} style={{ width: "220px" }} /> - setDchat(e.currentTarget.value)} style={{ width: "140px" }} /> - - ) : ( - setDhook(e.currentTarget.value)} style={{ width: "300px" }} /> - )} - - {derr() ?

{derr()}

: null} +

Secrets stay server-side — the list never shows tokens.

+
+ + setDlabel(e.currentTarget.value)} style={{ width: "160px" }} /> + {dkind() === "telegram" ? ( + <> + setDbot(e.currentTarget.value)} style={{ width: "200px" }} /> + setDchat(e.currentTarget.value)} style={{ width: "120px" }} /> + + ) : ( + setDhook(e.currentTarget.value)} style={{ flex: 1 }} /> + )} + +
+ {derr() ?

{derr()}

: null}
    {(dests()?.destinations || []).map((d) => ( -
  • {d.kind} — {d.label || `#${d.id}`} {d.enabled ? "on" : "off"} · {d.configured ? "configured" : "missing secret"} +
  • {d.kind} {d.label || `#${d.id}`} {d.enabled ? "on" : "off"} · {d.configured ? "configured" : "missing secret"} - +
  • ))}
-
+

New rule

- setName(e.currentTarget.value)} /> - setRule(e.currentTarget.value)} style={{ width: "280px" }} /> - setChannels(e.currentTarget.value)} style={{ width: "300px" }} /> - -

Delivery: your enabled destinations above (server env fallback). Empty = record only.

+
+ setName(e.currentTarget.value)} style={{ flex: 1 }} /> +
+
+ + + setValue(Number(e.currentTarget.value))} style={{ width: "100px" }} /> + +
+ +
    {(c, i) =>
  • {c.field} {c.op} {c.value}
  • }
+
+

Rule JSON: {rulePreview()}

+
+ setChannels(e.currentTarget.value)} style={{ flex: 1 }} /> + +
-
+

Rules

-
    {(a) =>
  • {a.name} last: {a.last_fired || "never"}
  • }
+ }> +
    {(a) =>
  • {a.name} last: {a.last_fired || "never"}
  • }
+

Event history

-
    {(e) =>
  • {String(e.ticker)} — {String(e.message)}
  • }
+ No events yet.

}> +
    {(e) =>
  • {String(e.ticker)} — {String(e.message)}
  • }
+
diff --git a/web/src/pages/Portfolio.tsx b/web/src/pages/Portfolio.tsx index c1b2f83..d607364 100644 --- a/web/src/pages/Portfolio.tsx +++ b/web/src/pages/Portfolio.tsx @@ -1,7 +1,8 @@ -import { createResource, For } from "solid-js"; +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"; Chart.register(...registerables); export default function Portfolio() { @@ -9,35 +10,37 @@ export default function Portfolio() { const [acc] = createResource(() => api.accuracy()); const chartData = () => ({ labels: (risk()?.concentration || []).map((c) => c.ticker), - datasets: [{ label: "Weight", data: (risk()?.concentration || []).map((c) => +(c.weight * 100).toFixed(1)) }], + datasets: [{ label: "Weight %", data: (risk()?.concentration || []).map((c) => +(c.weight * 100).toFixed(1)) }], }); return (
-

Portfolio Risk + Accuracy Ledger

-
-
-

Concentration

- -
    {(w) =>
  • {w}
  • }
+

Portfolio Risk + Accuracy Ledger

Computed from your watchlist.

+ }> +
+
+

Concentration

+ Add tickers to your watchlist to see concentration.

}> + +
+ {(w) =>

⚠️ {w}

}
+
+ +

Correlation heatmap

+ +
+
+

Agent accuracy

+ + + + + {(a) => } + + +
AgentCallsResolvedHit %
{a.agent}{a.calls}{a.resolved}{(a.hit_rate * 100).toFixed(0)}%
+
-
-

Beta vs index

-

{risk()?.beta ?? "…"}

-

Correlation heatmap

- -
-
-

Agent accuracy

- - - - - {(a) => } - - -
AgentCallsResolvedHit %
{a.agent}{a.calls}{a.resolved}{(a.hit_rate * 100).toFixed(0)}%
-
-
+
); } @@ -50,23 +53,25 @@ function CorrHeatmap(props: { matrix: Record> }) return bg; }; return ( -
- {(k) => } - - - {(r) => ( - - - - {(c) => { - const v = props.matrix[r]?.[c] ?? 0; - return ; - }} - - - )} - - -
{k}
{r}{v.toFixed(2)}
+ No correlation data yet.

}> +
+ {(k) => } + + + {(r) => ( + + + + {(c) => { + const v = props.matrix[r]?.[c] ?? 0; + return ; + }} + + + )} + + +
{k}
{r}{v.toFixed(2)}
+
); }