Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 91 additions & 4 deletions tests/fe8a_overview_runtime_ui_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ test("runtime KPI renders real observed coverage and explicit empty/error states
await build({
stdin: { resolveDir: root, contents: `import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { RunsKpi, AssetKpi, ReturnKpi, LastDayKpi } from './src/pages/overview/OverviewPage';
import { RunsKpi, AssetKpi, ReturnKpi, LastDayKpi, carryoverMonthForLatestDay } from './src/pages/overview/OverviewPage';
import { I18nProvider } from './src/i18n';
export { carryoverMonthForLatestDay };
export const render = (language, state) => renderToStaticMarkup(
createElement(I18nProvider, { language }, createElement(RunsKpi, { state })));
export const renderAsset = (language, state) => renderToStaticMarkup(
createElement(I18nProvider, { language }, createElement(AssetKpi, { state })));
export const renderReturn = (language, state) => renderToStaticMarkup(
createElement(I18nProvider, { language }, createElement(ReturnKpi, { state })));
export const renderLastDay = (language, state) => renderToStaticMarkup(
createElement(I18nProvider, { language }, createElement(LastDayKpi, { state })));` },
export const renderLastDay = (language, state, carryover, carryoverState) => renderToStaticMarkup(
createElement(I18nProvider, { language },
createElement(LastDayKpi, { state, carryover, carryoverState })));` },
bundle: true, platform: "node", format: "cjs", outfile, jsx: "automatic",
loader: { ".css": "empty" }, define: { "import.meta.env": "{}" },
plugins: [{ name: "test-runtime-card", setup(builder) {
Expand All @@ -49,7 +51,10 @@ test("runtime KPI renders real observed coverage and explicit empty/error states
const visibleText = html => outsideDetails(html).replace(/<[^>]*>/g, " ");
const meta = { schema_version: "console_api.v1", scope: "all", range: "YTD", coverage: [] };
const asOf = "2026-10-20T02:00:05Z";
const data = { status: "partial", date: "2026-10-22", counts: { configured: 8, observed: 2, runs: null } };
const data = {
status: "partial", date: "2026-10-22", market_session: "trading_day",
counts: { configured: 8, observed: 2, runs: null },
};
for (const language of ["zh", "en"]) {
const html = ui.render(language, { phase: "ready", data });
assert.ok(html.includes("2/8"));
Expand Down Expand Up @@ -171,6 +176,88 @@ test("runtime KPI renders real observed coverage and explicit empty/error states
assert.ok(!normalReturn.includes(`<summary>${DETAILS[language]}</summary>`), "normal values need no extra disclosure");
assert.ok(visibleText(normalReturn).includes(AS_OF[language]), "a normal card keeps as-of visible");
}

/* ---------- latest daily change is not bounded by the browser's month ---------- */
const dayCell = (date, amount, reason = null) => ({
date, amount, return_percent: null, currency: "USD", status: amount === null ? "no_data" : "ok",
value_kind: "asset_change", unavailable_reason: reason,
});
// Dates stay far in the past so no clock can make them future.
const emptyMonth = {
...meta, status: "no_data", unavailable_reason: "snapshot_history_missing", as_of: "2020-01-31",
value_kind: "asset_change", year: 2020, month: 2,
cells: [dayCell("2020-02-01", null, "snapshot_missing"), dayCell("2020-02-02", null, "non_trading_weekend")],
};
const carriedMonth = {
...meta, status: "stale", unavailable_reason: null, as_of: "2020-01-31",
value_kind: "asset_change", year: 2020, month: 1,
cells: [dayCell("2020-01-30", 456), dayCell("2020-01-31", 789)],
};
assert.deepEqual(ui.carryoverMonthForLatestDay(emptyMonth), { year: 2020, month: 1 },
"an empty request month resolves to the month that holds the latest observation");
assert.equal(ui.carryoverMonthForLatestDay(carriedMonth), null,
"a month that already has a value asks for nothing else");
assert.equal(ui.carryoverMonthForLatestDay({ ...emptyMonth, month: 1, as_of: "2020-02-05" }), null,
"a later month is never fetched to answer for an earlier one");
assert.equal(ui.carryoverMonthForLatestDay({ ...emptyMonth, as_of: null }), null,
"without a latest observation there is nothing to resolve");
assert.equal(ui.carryoverMonthForLatestDay({ ...emptyMonth, month: null }), null,
"the year view is not a month request and carries nothing over");
const NO_DATA = { zh: "暂无数据", en: "No data" };
for (const language of ["zh", "en"]) {
const unresolved = ui.renderLastDay(language, { phase: "ready", data: emptyMonth, error: null });
assert.ok(visibleText(unresolved).includes("—"), "an empty month keeps an honest blank");
assert.ok(visibleText(unresolved).includes(NO_DATA[language]));
assert.ok(!visibleText(unresolved).includes("789"), "no value is invented for the month that has none");

const carried = ui.renderLastDay(language, { phase: "ready", data: emptyMonth, error: null },
{ year: 2020, month: 1 }, { phase: "ready", data: carriedMonth, error: null });
assert.ok(visibleText(carried).includes("789"),
"the latest provable daily change surfaces instead of an empty current month");
assert.ok(visibleText(carried).includes(ASSET_CHANGE[language]), "the carried value keeps its method label");
assert.ok(visibleText(carried).includes(AS_OF[language]), "the carried value states which day it covers");
assert.ok(!visibleText(carried).includes("456"), "only the latest day is shown, not the whole month");
assert.ok(!visibleText(carried).includes(NO_DATA[language]), "a resolved month is not reported as having no data");
assert.ok(carried.includes('data-status="stale"'), "the cohort lag stays visible as a status on the carried month");

const pending = ui.renderLastDay(language, { phase: "ready", data: emptyMonth, error: null },
{ year: 2020, month: 1 }, { phase: "loading", data: null, error: null });
assert.ok(!visibleText(pending).includes(NO_DATA[language]),
"a month being fetched never announces no data first");
// The carry-over month is decided during render while useApi has not yet
// committed its new request, so that one frame must stay a skeleton.
const starting = ui.renderLastDay(language, { phase: "ready", data: emptyMonth, error: null },
{ year: 2020, month: 1 }, { phase: "ready", data: null, error: null });
assert.ok(!visibleText(starting).includes(NO_DATA[language]),
"the frame between deciding a carry-over and fetching it never announces no data");
assert.ok(!visibleText(starting).includes("—"),
"a month that is about to be fetched does not flash an unknown value");
const failedCarry = ui.renderLastDay(language, { phase: "ready", data: emptyMonth, error: null },
{ year: 2020, month: 1 }, { phase: "error", data: null, error: Error("synthetic_read_failed") });
assert.ok(visibleText(failedCarry).includes(NO_DATA[language]),
"a failed follow-up falls back to the month that was asked for instead of claiming a read failure");
}

/* ---------- a weekend date is never called a business day ---------- */
const WEEKEND = { zh: "纽约日期", en: "New York date" };
const BUSINESS = { zh: "纽约业务日", en: "New York business day" };
for (const language of ["zh", "en"]) {
const weekendData = { ...data, date: "2020-01-31", market_session: "weekend",
counts: { configured: 8, observed: 0, runs: null },
accounts: [{ status: "no_data", unavailable_reason: "non_trading_weekend" }] };
const weekend = ui.render(language, { phase: "ready", data: weekendData });
assert.ok(weekend.includes("0/8"), "the weekend count itself is unchanged");
assert.ok(weekend.includes(WEEKEND[language]), "the date next to the count is stated as a date");
assert.ok(!weekend.includes(BUSINESS[language]), "a Saturday is never labelled a business day");
assert.ok(weekend.includes(language === "zh" ? "非交易日" : "Non-trading day"), "the non-trading badge still carries the state");
const tradingDay = ui.render(language, { phase: "ready", data: { ...weekendData, market_session: "trading_day" } });
assert.ok(tradingDay.includes(BUSINESS[language]), "a trading day keeps the business-day wording");
// The field is optional, so a payload that never reports a session may not
// be handed a trading claim the API did not make.
const unknownSession = ui.render(language, { phase: "ready", data: { ...weekendData, market_session: undefined } });
assert.ok(unknownSession.includes(WEEKEND[language]), "an unreported session is stated as a plain date");
assert.ok(!unknownSession.includes(BUSINESS[language]), "a missing session is never assumed to be a business day");
}
} finally {
await rm(directory, { recursive: true, force: true });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const zh: Record<string, string> = {
"overview.mixed_currency_short": "多币种",
"overview.runs_card_title": "今日有记录账户",
"overview.business_date": "纽约业务日 {value}",
"overview.calendar_date": "纽约日期 {value}",
"overview.no_records_for_day": "当前未观测到该业务日记录",
"overview.non_trading_day_short": "非交易日",
};
Expand All @@ -20,6 +21,7 @@ export const en: Record<string, string> = {
"overview.mixed_currency_short": "Multiple currencies",
"overview.runs_card_title": "Accounts with records today",
"overview.business_date": "New York business day {value}",
"overview.calendar_date": "New York date {value}",
"overview.no_records_for_day": "No records observed for this business day",
"overview.non_trading_day_short": "Non-trading day",
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState } from "react";
import type { ReactNode } from "react";
import type {
AssetsResponse,
CalendarCell,
CalendarResponse,
HistoryCoverage,
InboxResponse,
Expand Down Expand Up @@ -57,6 +58,32 @@ function localToday(): string {
return `${now.getFullYear()}-${month}-${day}`;
}

/** Cells the API actually observed on a dated day that is not ahead of this clock. */
function datedValueCells(cells: CalendarCell[]): (CalendarCell & { date: string })[] {
const today = localToday();
return cells
.filter((cell): cell is CalendarCell & { date: string } =>
typeof cell.date === "string" && cell.date <= today && (cell.amount !== null || cell.return_percent !== null));
}

/**
* A calendar request is one month wide, so an empty current month is a request
* boundary rather than evidence that no daily change exists: `as_of` names the
* cohort's newest observation whatever month was asked for. Only an earlier
* month is returned, so the follow-up can never re-resolve to its own month.
*/
export function carryoverMonthForLatestDay(data: CalendarResponse): { year: number; month: number } | null {
if (data.month === null || datedValueCells(data.cells).length > 0) return null;
const latest = /^(\d{4})-(\d{2})-\d{2}$/.exec(data.as_of ?? "");
if (!latest) return null;
const year = Number(latest[1]);
const month = Number(latest[2]);
if (month < 1 || month > 12) return null;
const requested = `${data.year}-${String(data.month).padStart(2, "0")}`;
const target = `${year}-${String(month).padStart(2, "0")}`;
return target < requested ? { year, month } : null;
}

const usable = renderableStatus;

/** Missing values stay quiet; nearby visible copy retains the actual state or reason. */
Expand Down Expand Up @@ -133,12 +160,27 @@ function ReturnKpi({ state }: { state: Api<SummaryResponse> }) {
footer={footer} />;
}

function LastDayKpi({ state }: { state: Api<CalendarResponse> }) {
/** Latest daily change, following the cohort forward one month when the asked month is empty. */
function LastDayKpi({ state, carryover, carryoverState }: {
state: Api<CalendarResponse>;
carryover?: { year: number; month: number } | null;
carryoverState?: AsyncState<CalendarResponse | null>;
}) {
const t = useT();
const title = t("overview.last_day_change");
if (state.phase === "loading") return kpiLoading(title);
if (state.phase === "error") return <KpiCard title={title} value={<KpiState status="read_failed" />} sub={t("state.read_failed")} />;
const data = state.data;
// The earlier month is consulted only once it is actually requested; a failed
// follow-up leaves the month that was asked for on screen. Anything still
// unresolved reads as in flight, because the requested month is already known
// to be empty and rendering it would flash an unknown value for one frame.
let active: AsyncState<CalendarResponse> = state;
if (carryover && carryoverState?.phase !== "error") {
active = carryoverState?.phase === "ready" && carryoverState.data
? { phase: "ready", data: carryoverState.data, error: null }
: { phase: "loading", data: null, error: null };
}
if (active.phase === "loading") return kpiLoading(title);
if (active.phase === "error") return <KpiCard title={title} value={<KpiState status="read_failed" />} sub={t("state.read_failed")} />;
const data = active.data;
if (!usable(data.status)) {
return <KpiCard title={title} value={<KpiState status={data.status} />}
sub={reasonText(t, data.unavailable_reason) || t(`state.${data.status}`)}
Expand All @@ -149,10 +191,7 @@ function LastDayKpi({ state }: { state: Api<CalendarResponse> }) {
const footer = statusBadge || data.as_of
? <>{statusBadge}{data.as_of && <AsOfStamp value={data.as_of} />}</>
: undefined;
const today = localToday();
const cell = data.cells
.filter((item): item is typeof item & { date: string } => Boolean(item.date) && item.date! <= today && (item.amount !== null || item.return_percent !== null))
.sort((a, b) => (a.date < b.date ? 1 : -1))[0];
const cell = datedValueCells(data.cells).sort((a, b) => (a.date < b.date ? 1 : -1))[0];
if (!cell) {
return <KpiCard title={title} value={<span className="flat">—</span>}
sub={reasonText(t, data.unavailable_reason) || t("state.no_data")} footer={footer} />;
Expand Down Expand Up @@ -194,6 +233,7 @@ function TodosKpi({ state }: { state: Api<InboxResponse> }) {
);
}

/** Accounts with a run record on the New York day the API reports. */
function RunsKpi({ state }: { state: Api<TodayRunSummary> }) {
const t = useT();
const navigate = useNavigate();
Expand Down Expand Up @@ -227,9 +267,13 @@ function RunsKpi({ state }: { state: Api<TodayRunSummary> }) {
// On a weekend, absent records are the schedule, not degraded coverage.
const nonTrading = weekendWithoutScheduledRuns(data);
// The footer badge already names a non-trading day; the sub-line only adds the
// missing-record reason on a normal business day.
// missing-record reason on a normal business day. The session comes from the
// API because a browser weekday can disagree with New York, and only an
// explicit trading day may be called a business day: an older payload without
// the field must not assert a trading status the API never reported.
const dateLabelKey = data.market_session === "trading_day" ? "overview.business_date" : "overview.calendar_date";
const sub = denominatorKnown
? <>{t("overview.business_date", { value: data.date })}
? <>{t(dateLabelKey, { value: data.date })}
{!nonTrading && observed === 0 && <> · {t("overview.no_records_for_day")}</>}</>
: t("state.no_data");
return (
Expand Down Expand Up @@ -434,9 +478,18 @@ export default function OverviewPage() {
const refresh = () => setEpoch(value => value + 1);

const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
const assets = useApi(() => loadPerformanceAssets(scope, RANGE), [scope, epoch]);
const summary = useApi(() => loadPerformanceSummary(scope, RANGE), [scope, epoch]);
const calendar = useApi(() => loadPerformanceCalendar(scope, RANGE, now.getFullYear(), now.getMonth() + 1), [scope, epoch]);
const calendar = useApi(() => loadPerformanceCalendar(scope, RANGE, currentYear, currentMonth), [scope, epoch]);
const latestDayCarryover = calendar.phase === "ready" ? carryoverMonthForLatestDay(calendar.data) : null;
const calendarCarryover = useApi(
() => (latestDayCarryover
? loadPerformanceCalendar(scope, RANGE, latestDayCarryover.year, latestDayCarryover.month)
: Promise.resolve(null)),
[scope, epoch, latestDayCarryover ? `${latestDayCarryover.year}-${latestDayCarryover.month}` : ""],
);
const inbox = useApi(() => loadInbox(epoch > 0), [epoch]);
const todayRuns = useApi(() => loadTodayRuns(), [epoch]);
const returns = useApi(() => loadPerformanceReturns(scope, RANGE), [scope, epoch]);
Expand All @@ -455,7 +508,7 @@ export default function OverviewPage() {
<div className="overview-primary-kpi"><AssetKpi state={assets} /></div>
<div className="overview-secondary-kpis">
<ReturnKpi state={summary} />
<LastDayKpi state={calendar} />
<LastDayKpi state={calendar} carryover={latestDayCarryover} carryoverState={calendarCarryover} />
<TodosKpi state={inbox} />
<RunsKpi state={todayRuns} />
</div>
Expand Down
34 changes: 17 additions & 17 deletions web/strategy-switch-console/v2_asset_map.js

Large diffs are not rendered by default.

Loading