fix(chart): keep zero-year chara and ephemeris dates apart

A zero-year Chara period is shown as 0 years instead of failing the whole chart. Switching ephemeris dates drops the previous day immediately and can be retried. Missing birth data links to chart settings.
This commit is contained in:
jesse-ux
2026-09-25 02:10:48 +08:00
parent 82c8aeb06a
commit 19ea0f06c1
25 changed files with 744 additions and 60 deletions
+2 -1
View File
@@ -499,7 +499,8 @@ export default function Home() {
const applySettingsFromSearch = (search: string) => {
const query = parseSettingsQuery(search);
if (!query) return;
openAccountDialog("billing", { source: query.source ?? undefined, plan: query.plan ?? undefined, tab: query.tab ?? undefined });
if (query.pane === "chart") openAccountDialog("chart-library");
else openAccountDialog("billing", { source: query.source ?? undefined, plan: query.plan ?? undefined, tab: query.tab ?? undefined });
const next = stripSettingsQuery(search, window.location.pathname);
if (`${window.location.pathname}${window.location.search}` !== next) window.history.replaceState(null, "", next);
};
+44 -9
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { chartViewEngineCacheKey, readChartViewEngineCache, writeChartViewEngineCache } from "@/lib/chart-view-engine";
import { calendarDateInTimeZone } from "@/lib/daily-starlanguage";
import { ephemerisIsoDateSchema, ephemerisOkResponseSchema, type EphemerisOkResponse } from "@/lib/ephemeris-contract";
import {
@@ -23,18 +24,54 @@ const engineTimeoutMs = 20_000;
type Profile = ReturnType<typeof globalBirthProfileFromAccountRow>;
type BirthPayload = NonNullable<Awaited<ReturnType<typeof dailyProfilePayload>>>;
async function fetchEngine(path: string, body: Record<string, unknown>) {
function engineSignal(request: Request): AbortSignal {
return AbortSignal.any([AbortSignal.timeout(engineTimeoutMs), request.signal]);
}
async function fetchEngine(path: string, body: Record<string, unknown>, signal: AbortSignal) {
const response = await fetch(`${jyotishApiBase}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
cache: "no-store",
signal: AbortSignal.timeout(engineTimeoutMs),
signal,
});
if (!response.ok) throw new Error(`jyotish_api_${response.status}`);
return await response.json() as unknown;
}
function natalCacheKey(userId: string, birth: BirthPayload): string {
const month = String(birth.month).padStart(2, "0");
const day = String(birth.day).padStart(2, "0");
const hour = String(birth.hour).padStart(2, "0");
const minute = String(birth.minute).padStart(2, "0");
return chartViewEngineCacheKey({
userId,
date: `${birth.year}-${month}-${day}`,
time: `${hour}:${minute}`,
latitude: birth.lat,
longitude: birth.lon,
timezoneOffset: birth.tz,
ayanamsa: birth.ayanamsa,
nodeMode: birth.node_mode,
path: "/api/ephemeris/natal-chart",
});
}
async function fetchNatalChart(userId: string, birth: BirthPayload, signal: AbortSignal): Promise<unknown> {
const key = natalCacheKey(userId, birth);
const hit = readChartViewEngineCache(key);
if (hit) return hit;
const payload = await fetchEngine("/api/chart", {
...birth,
skip_vedastro_main_entry_overview: true,
}, signal);
if (!signal.aborted && payload && typeof payload === "object" && !Array.isArray(payload)) {
writeChartViewEngineCache(key, payload as Record<string, unknown>);
}
return payload;
}
function requestedDate(request: Request, timezoneId?: string | null): string {
const raw = new URL(request.url).searchParams.get("date")?.trim() ?? "";
if (raw && isEphemerisIsoDate(raw) && ephemerisIsoDateSchema.safeParse(raw).success) return raw;
@@ -75,6 +112,7 @@ export async function GET(request: Request) {
const birth = await dailyProfilePayload(profile, date).catch(() => null);
const location = locationBody(profile);
const signal = engineSignal(request);
const panchangaBody: Record<string, unknown> = { start_date: date, end_date: date };
if (location) {
panchangaBody.lat = location.lat;
@@ -82,23 +120,20 @@ export async function GET(request: Request) {
panchangaBody.tz = location.tz;
}
const panchangaPromise = fetchEngine("/api/panchanga_range", panchangaBody).catch((error: unknown) => {
const panchangaPromise = fetchEngine("/api/panchanga_range", panchangaBody, signal).catch((error: unknown) => {
console.warn("ephemeris_panchanga_unavailable", error);
return null;
});
const natalPromise = birth
? fetchEngine("/api/chart", {
...birth,
skip_vedastro_main_entry_overview: true,
}).catch((error: unknown) => {
? fetchNatalChart(user.id, birth, signal).catch((error: unknown) => {
console.warn("ephemeris_natal_chart_unavailable", error);
return null;
})
: Promise.resolve(null);
const transitPromise = birth
? fetchEngine("/api/chart", transitChartBody(birth, date)).catch((error: unknown) => {
? fetchEngine("/api/chart", transitChartBody(birth, date), signal).catch((error: unknown) => {
console.warn("ephemeris_transit_chart_unavailable", error);
return null;
})
@@ -109,7 +144,7 @@ export async function GET(request: Request) {
end_date: addDaysIso(date, 90),
ayanamsa: birth?.ayanamsa ?? profile.ayanamsa ?? "raman",
node_mode: "mean",
}).catch((error: unknown) => {
}, signal).catch((error: unknown) => {
console.warn("ephemeris_events_unavailable", error);
return null;
});
+3
View File
@@ -2193,6 +2193,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
font-size: var(--type-title-lg);
font-weight: 500;
}
.chart-page-message h2 {
font-size: var(--type-title-md);
}
@media (max-width: 767px) {
/* Replaces the three `*-shell { padding-inline }` rules the standalone pages