Files
Jyotisha/frontend/src/lib/chart-view-load.ts
T
jesse-ux 62be52dacf
Independent Staging Quality Gate / validate (push) Canceled after 2m16s
Independent Staging Quality Gate / publish (push) Canceled after 0s
fix(chart): skip VedAstro on chart-page first paint
The natal /api/chart request from the chart page now sends skip_vedastro_main_entry_overview, matching the ephemeris page. Mapper and contract never read that evidence. BUG-718 recurs from BUG-161. Tests lock both foreground natal paths.
2026-09-16 00:22:00 +08:00

179 lines
4.9 KiB
TypeScript

import { resolveAyanamsa } from "./ayanamsa.ts";
import {
chartViewMessageSchema,
type ChartViewResponse,
} from "./chart-view-contract.ts";
import {
enginePayload,
logEngineCallFailure,
type ChartViewLayer,
type EngineCallResult,
} from "./chart-view-engine.ts";
import { CHART_VIEW_COPY, VARGA_CHIP_ORDER } from "./chart-view-labels.ts";
import {
buildChartView,
engineChartHasPlanets,
type ChartViewProfileInput,
} from "./chart-view-mapper.ts";
export type ChartViewEnginePost = (
path: string,
body: Record<string, unknown>,
) => Promise<EngineCallResult>;
export type ChartViewLoadInput = {
userId: string | null;
profile: ChartViewProfileInput | null;
postEngine: ChartViewEnginePost;
asOf: string;
rateLimited?: boolean;
layers?: readonly ChartViewLayer[];
};
function message(
status: "unauthenticated" | "birth_profile_incomplete" | "chart_unavailable" | "rate_limited",
copy: string,
): ChartViewResponse {
return chartViewMessageSchema.parse({ status, billed: false, message: copy });
}
function birthPayload(profile: ChartViewProfileInput): Record<string, unknown> {
const [year, month, day] = profile.date.split("-").map(Number);
const [hour, minute] = profile.time.split(":").map(Number);
return {
year,
month,
day,
hour,
minute,
second: 0,
lat: profile.latitude,
lon: profile.longitude,
tz: profile.timezoneOffset,
latitude: profile.latitude,
longitude: profile.longitude,
timezone: profile.timezoneOffset,
ayanamsa: resolveAyanamsa({ ayanamsa: profile.ayanamsa }),
node_mode: "mean",
house_system: "P",
skip_vedastro_main_entry_overview: true,
};
}
function wantedLayers(layers: readonly ChartViewLayer[] | undefined): Set<ChartViewLayer> {
return new Set(layers ?? []);
}
async function callEngine(
postEngine: ChartViewEnginePost,
path: string,
body: Record<string, unknown>,
): Promise<EngineCallResult> {
const result = await postEngine(path, body);
if (result.status !== "ok") logEngineCallFailure(result);
return result;
}
export async function assembleChartView(input: ChartViewLoadInput): Promise<{
httpStatus: number;
body: ChartViewResponse;
}> {
if (!input.userId) {
return {
httpStatus: 401,
body: message("unauthenticated", CHART_VIEW_COPY.unauthenticated),
};
}
if (input.rateLimited) {
return {
httpStatus: 429,
body: message("rate_limited", CHART_VIEW_COPY.rateLimited),
};
}
const profile = input.profile;
if (!profile) {
return {
httpStatus: 200,
body: message("birth_profile_incomplete", CHART_VIEW_COPY.incomplete),
};
}
const payload = birthPayload(profile);
const chartResult = await callEngine(input.postEngine, "/api/chart", payload);
if (chartResult.status !== "ok") {
return {
httpStatus: 200,
body: message(
"chart_unavailable",
chartResult.status === "busy" ? CHART_VIEW_COPY.busy : CHART_VIEW_COPY.unavailable,
),
};
}
const chart = chartResult.payload;
if (chart.success === false || !engineChartHasPlanets(chart)) {
logEngineCallFailure({
status: "bad_payload",
path: "/api/chart",
elapsedMs: 0,
errorName: "engine_rejected",
});
return {
httpStatus: 200,
body: message("chart_unavailable", CHART_VIEW_COPY.unavailable),
};
}
const planets = chart.planets;
const ascendant = chart.ascendant;
const houses = chart.houses;
const followUp = {
...payload,
planets,
ascendant,
houses,
};
const layers = wantedLayers(input.layers);
const [varga, chara, western, qizheng] = await Promise.all([
layers.has("varga")
? callEngine(input.postEngine, "/api/varga_full", {
...followUp,
divisions: VARGA_CHIP_ORDER.map((division) => `D${division}`),
}).then(enginePayload)
: Promise.resolve(null),
layers.has("chara")
? callEngine(input.postEngine, "/api/dasha/chara", {
...followUp,
antardasha: true,
mode: "dasha",
}).then(enginePayload)
: Promise.resolve(null),
layers.has("western")
? callEngine(input.postEngine, "/api/western", payload).then(enginePayload)
: Promise.resolve(null),
layers.has("qizheng")
? callEngine(input.postEngine, "/api/qizheng", payload).then(enginePayload)
: Promise.resolve(null),
]);
try {
const body = buildChartView({
chart,
varga,
chara,
western,
qizheng,
profile,
asOf: input.asOf,
});
return { httpStatus: 200, body };
} catch (error) {
const errorName = error instanceof Error ? error.name : "Error";
const errorMessage = error instanceof Error ? error.message : "unknown";
console.warn("chart_view_assemble_failed", { errorName, message: errorMessage });
return {
httpStatus: 200,
body: message("chart_unavailable", CHART_VIEW_COPY.unavailable),
};
}
}