first commit

This commit is contained in:
dekun
2026-08-01 10:33:19 +08:00
commit d9a34d4f20
72 changed files with 5499 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import {
fetchAuthStatus,
fetchHealth,
fetchLatest,
Health,
Latest,
logout,
} from "../api/client";
import LoginGate from "../components/LoginGate";
function fmt(n?: number | null, d = 1) {
if (n == null || Number.isNaN(n)) return "—";
return n.toFixed(d);
}
export default function Dashboard() {
const [needLogin, setNeedLogin] = useState(false);
const [ready, setReady] = useState(false);
const [health, setHealth] = useState<Health | null>(null);
const [latest, setLatest] = useState<Latest | null>(null);
const [err, setErr] = useState<string | null>(null);
const bootstrap = async () => {
const st = await fetchAuthStatus();
if (!st.auth_required) {
setNeedLogin(false);
setReady(true);
return;
}
try {
await fetchLatest();
setNeedLogin(false);
} catch {
setNeedLogin(true);
}
setReady(true);
};
useEffect(() => {
bootstrap().catch((e) => setErr(String(e)));
}, []);
useEffect(() => {
if (!ready || needLogin) return;
let alive = true;
const load = async () => {
try {
const [h, m] = await Promise.all([fetchHealth(), fetchLatest()]);
if (!alive) return;
setHealth(h);
setLatest(m);
setErr(null);
} catch (e) {
if (!alive) return;
const msg = String(e);
if (msg.includes("unauthorized")) setNeedLogin(true);
else setErr(msg);
}
};
load();
const t = setInterval(load, 10000);
return () => {
alive = false;
clearInterval(t);
};
}, [ready, needLogin]);
if (!ready) {
return (
<div className="layout">
<div className="sub"></div>
</div>
);
}
if (needLogin) {
return (
<LoginGate
onOk={() => {
setNeedLogin(false);
setErr(null);
}}
/>
);
}
const lag = health?.collector_lag_ms;
const ok =
!!health?.ok && (lag == null || lag < 120_000) && (health.consecutive_failures || 0) < 5;
const meta = latest?.heartbeat?.meta;
return (
<div className="layout">
<div className="brand"></div>
<div className="sub"> · = ÷ · Asia/Shanghai</div>
<div className="nav">
<Link to="/"></Link>
<Link to="/ops-map"></Link>
<a
href="#logout"
onClick={(e) => {
e.preventDefault();
logout().finally(() => setNeedLogin(true));
}}
>
退
</a>
</div>
{err && <p style={{ color: "#e85d5d" }}>{err}</p>}
<div className="row">
<div className="tile">
<div className="label"></div>
<div className="value" style={{ color: ok ? "var(--ok)" : "var(--warn)" }}>
{health ? (ok ? "正常" : "异常/等待") : "…"}
</div>
<div className="hint">
{lag == null
? "尚无采样"
: `延迟 ${Math.round(lag / 1000)}s · 样本 ${health?.option_quotes ?? 0}`}
</div>
</div>
<div className="tile">
<div className="label">ATM Call </div>
<div className="value">{fmt(latest?.call?.leverage)}</div>
<div className="hint">{latest?.call?.inst_id ?? "—"}</div>
</div>
<div className="tile">
<div className="label">ATM Put </div>
<div className="value">{fmt(latest?.put?.leverage)}</div>
<div className="hint">{latest?.put?.inst_id ?? "—"}</div>
</div>
<div className="tile">
<div className="label"></div>
<div className="value">{fmt(meta?.index_px ?? latest?.call?.index_px, 2)}</div>
<div className="hint">
{meta?.expiry_ymd ? `到期 ${meta.expiry_ymd} · 行权 ${meta.strike ?? "—"}` : "—"}
</div>
</div>
</div>
</div>
);
}
+219
View File
@@ -0,0 +1,219 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import {
fetchAuthStatus,
fetchOpsMap,
LeverageStats,
logout,
MovePointsStats,
OpsMap,
} from "../api/client";
import LeverageChart from "../components/LeverageChart";
import LoginGate from "../components/LoginGate";
import MovePointsChart from "../components/MovePointsChart";
type RangeKey = "day" | "week" | "month";
type SideKey = "both" | "C" | "P";
function todayYmd() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
export default function OpsMapPage() {
const [needLogin, setNeedLogin] = useState(false);
const [ready, setReady] = useState(false);
const [range, setRange] = useState<RangeKey>("day");
const [side, setSide] = useState<SideKey>("both");
const [date, setDate] = useState(todayYmd());
const [moveMode, setMoveMode] = useState<"abs" | "signed">("abs");
const [data, setData] = useState<OpsMap | null>(null);
const [err, setErr] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchAuthStatus()
.then(async (st) => {
if (!st.auth_required) {
setNeedLogin(false);
setReady(true);
return;
}
try {
await fetchOpsMap({ range: "day", bucket_minutes: 60 });
setNeedLogin(false);
} catch {
setNeedLogin(true);
}
setReady(true);
})
.catch((e) => setErr(String(e)));
}, []);
useEffect(() => {
if (!ready || needLogin) return;
let alive = true;
setLoading(true);
fetchOpsMap({ range, date, side, bucket_minutes: 60 })
.then((d) => {
if (!alive) return;
setData(d);
setErr(null);
})
.catch((e) => {
if (!alive) return;
const msg = String(e);
if (msg.includes("unauthorized")) setNeedLogin(true);
else setErr(msg);
})
.finally(() => {
if (alive) setLoading(false);
});
return () => {
alive = false;
};
}, [range, side, date, ready, needLogin]);
if (!ready) {
return (
<div className="layout">
<div className="sub"></div>
</div>
);
}
if (needLogin) {
return <LoginGate onOk={() => setNeedLogin(false)} />;
}
const lev: LeverageStats | undefined = data?.leverage;
const mov: MovePointsStats | undefined = data?.move_points;
const rangeLabel = range === "day" ? "日" : range === "week" ? "近7日" : "近30日";
return (
<div className="layout">
<div className="brand"></div>
<div className="sub"> · × </div>
<div className="nav">
<Link to="/"></Link>
<Link to="/ops-map"></Link>
<a
href="#logout"
onClick={(e) => {
e.preventDefault();
logout().finally(() => setNeedLogin(true));
}}
>
退
</a>
</div>
<div className="toolbar">
<div className="seg">
{(["day", "week", "month"] as RangeKey[]).map((k) => (
<button
key={k}
type="button"
className={range === k ? "active" : ""}
onClick={() => setRange(k)}
>
{k === "day" ? "日" : k === "week" ? "周" : "月"}
</button>
))}
</div>
<div className="seg">
{(["both", "C", "P"] as SideKey[]).map((k) => (
<button
key={k}
type="button"
className={side === k ? "active" : ""}
onClick={() => setSide(k)}
>
{k === "both" ? "双边" : k === "C" ? "Call" : "Put"}
</button>
))}
</div>
<label className="date-field">
<input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
</label>
</div>
{err && <p style={{ color: "#e85d5d" }}>{err}</p>}
{loading && !data && <p style={{ color: "var(--muted)" }}></p>}
{lev && (
<>
<div className="row" style={{ marginBottom: "1rem" }}>
<div className="tile">
<div className="label"></div>
<div className="value" style={{ fontSize: "1.1rem" }}>
{lev.start_ymd} {lev.end_ymd}
</div>
<div className="hint"> {lev.sample_count}</div>
</div>
<div className="tile">
<div className="label">线</div>
<div className="value">{lev.min_leverage}</div>
<div className="hint"> = ÷ </div>
</div>
<div className="tile">
<div className="label"></div>
<div className="value">{mov?.settled_count ?? 0}</div>
<div className="hint">
{mov?.pending_expiry
? `pending ${mov.pending_count ?? 0}(未到期已排除)`
: "全部已结算"}
</div>
</div>
</div>
<LeverageChart
buckets={lev.buckets}
minLeverage={lev.min_leverage}
title={`上图 · 时段杠杆均值(${rangeLabel}`}
/>
<div className="toolbar" style={{ marginTop: "1.25rem" }}>
<div className="seg">
<button
type="button"
className={moveMode === "abs" ? "active" : ""}
onClick={() => setMoveMode("abs")}
>
</button>
<button
type="button"
className={moveMode === "signed" ? "active" : ""}
onClick={() => setMoveMode("signed")}
>
</button>
</div>
{mov?.pending_expiry && (
<span style={{ color: "var(--warn)", fontSize: "0.85rem" }}>
pending_expiry=true
</span>
)}
</div>
<MovePointsChart
buckets={mov?.buckets ?? []}
mode={moveMode}
title={`下图 · 时段→到期波动(${rangeLabel} · ${
moveMode === "abs" ? "abs" : "signed"
}`}
/>
{mov?.message && (
<p style={{ color: "var(--muted)", fontSize: "0.85rem", marginTop: "0.75rem" }}>
{mov.message}
</p>
)}
</>
)}
</div>
);
}