{/* Switching mode resets HIP-3 to off, so the default-off promise holds
on every mode change and can never be inherited from a previous pick. */}
{Object.entries(accountTypes).map(([key, t]) => (
))}
This closes all open positions on {statusPanel.account.id} at market within ~60s and stops all further trades for this account. No further position or alert messages will be sent for it. You can reactivate it here at any time.
) : (
Resurrect {statusPanel.account.id} to live trading
Current equity ${Number(statusPanel.account.current_equity || 0).toFixed(2)}.
The drawdown base resets to this equity (otherwise the risk gate would re-halt the account within a minute). Assign the drawdown limit to guard from here:
How much of this account funds each basket. Editing this doesn't touch the API keys. Nothing trades until the resolver applies it.
)}
);
})}
{!adding && msg &&
{msg.t}
}
);
}
// ---- Hash routing — every screen and every strategy gets a unique, shareable URL ----
// #/baskets · #/environments · #/strategies · #/analytics · #/trades · #/portfolio/id140
// The strategy page keeps the tab you came from highlighted (it's an overlay), and the
// browser back button works because navigation IS the hash.
const APP_ROUTES = ["baskets", "environments", "strategies", "analytics", "monitor", "trades"];
function parseAppHash() {
const parts = String(location.hash || "").replace(/^#\/?/, "").split("/").filter(Boolean);
if ((parts[0] === "portfolio" || parts[0] === "strategy") && /^[A-Za-z0-9_-]+$/.test(parts[1] || "")) return { selected: parts[1] };
if (APP_ROUTES.includes(parts[0])) return { route: parts[0], selected: null };
return { route: "baskets", selected: null };
}
function App() {
const [vault, setVault] = React.useState(null);
const initial = parseAppHash();
const [route, setRoute] = React.useState(initial.route || "baskets");
const [selected, setSelected] = React.useState(initial.selected || null);
// No global source toggle any more — every screen reads the combined ledger and labels
// provenance inline (SourceTags, chart markers). "both" is the only mode.
const mode = "both";
const [refreshing, setRefreshing] = React.useState(false);
const mainRef = React.useRef(null);
React.useEffect(() => {
function onHash() {
const h = parseAppHash();
if (h.selected !== undefined && h.selected !== null) {
setSelected(h.selected);
return;
}
setSelected(null);
if (h.route) setRoute(h.route);
}
window.addEventListener("hashchange", onHash);
return () => window.removeEventListener("hashchange", onHash);
}, []);
function onUnlocked(body) {
if (typeof APPVC.seedRegistryBacktests === "function") APPVC.seedRegistryBacktests(body.registry || []);
// list_vault already ships every vault strategy's closed-trade ledger in `trades`. Nothing
// used to cache it (cacheTrades was only reachable via ensureTrades, which Overview never
// calls), so the landing screen computed decay + equity + returns off an EMPTY cache and
// rendered N/A / "—" for every basket until you happened to visit Strategies. Free fix:
// it is already on the wire.
APPVC.cacheTrades(body.trades || {});
setVault(body);
APPVC.api2("list_baskets").then((r) => {
if (r.status === 200 && r.body.ok) {
setVault((v) => (v ? { ...v, builderBaskets: r.body.baskets || [] } : v));
}
});
}
function refresh() {
setRefreshing(true);
return APPVC.api("list_vault").then((r) => {
setRefreshing(false);
if (r.status === 200 && r.body.ok) { onUnlocked(r.body); }
else if (r.status === 401) lock();
}).catch(() => setRefreshing(false));
}
function lock() {
APPVC.clearSession(true);
setVault(null); setSelected(null);
}
// Navigation writes the hash; state follows via the hashchange listener. A strategy
// detail is an OVERLAY on whichever tab you're on — the route (and sidebar highlight)
// stays put, so Back always returns exactly where you were.
function go(r) { location.hash = "#/" + r; }
function openStrategy(id) {
if (!/^id\d+$/.test(String(id || ""))) return;
location.hash = "#/portfolio/" + id;
}
function closeStrategy() { location.hash = "#/" + route; }
if (!vault) return ;
const [title, sub] = TITLES[route] || TITLES.baskets;
const warns = vault.warnings || [];
let screen = null;
if (selected && /^id\d+$/.test(selected)) screen = ;
else if (selected) { location.hash = "#/strategies"; return null; }
else if (route === "baskets") screen = ;
else if (route === "environments") screen = ;
else if (route === "strategies") screen = ;
else if (route === "analytics") screen = ;
else if (route === "monitor") screen = ;
else if (route === "trades") screen = ;
return (
{!selected && route !== "baskets" &&
}
{/* Only genuinely actionable banners survive: demo mode and failed reads. The standing
"changes here are live" disclaimer was cut (2026-07-21) — arm/disarm dialogs carry
their own warning at the moment it matters. */}
{route !== "environments" && APPVC.DEMO && (
Demo data. Fixture payload — nothing here is real.
)}
{warns.length > 0 && (
Partial data. Some reads failed this refresh: {warns.join("; ")}