diff --git a/README.md b/README.md index d3c797c..cf8931d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ MetaMap is a TypeScript + React (Next.js 14 App Router) web application for expl - ✅ **Visualisations** (timeline, compass, network, heatmap) powered by D3 and CSS variable palettes. - ✅ **Adjustable weights** (default HD 0.6, GK 0.5, others 1.0) stored in localStorage. - ✅ **RNG tools** for I Ching, Tarot, and Geomancy using `crypto.getRandomValues`. +- ✅ **Live calculator demos** for ephemeris, Chinese calendar, Zi Wei Dou Shu, Qi Men Dun Jia, Feng Shui, Human Design, and Gene Keys. - ✅ **Plug-in ready calculators** with TypeScript interfaces under `src/calculators`. - ✅ **Accessibility & responsiveness** (WCAG AA focus styles, prefers-reduced-motion support). @@ -40,7 +41,7 @@ Docker users can run `docker compose up --build` for a Node 20 Alpine environmen ### Provider registry -The app ships with demo providers for Ephemeris, Chinese calendar, Zi Wei Dou Shu, and Qi Men Dun Jia when `NEXT_PUBLIC_ENABLE_DEMO_PROVIDERS=true` (enabled by default in non-production). Register licensed providers by calling `registerProvider` in `src/providers/bootstrap.ts` or supplying your own bootstrap module. Production builds should disable the demo flag. +The app ships with demo providers for Ephemeris, Chinese calendar, Zi Wei Dou Shu, Qi Men Dun Jia, Feng Shui, Human Design, and Gene Keys when `NEXT_PUBLIC_ENABLE_DEMO_PROVIDERS=true` (enabled by default in non-production). Register licensed providers by calling `registerProvider` in `src/providers/bootstrap.ts` or supplying your own bootstrap module. Production builds should disable the demo flag. ### Testing in CI @@ -84,7 +85,7 @@ Utility helpers (`src/lib`) cover intervals, direction mapping, CSV serializatio 2. Use the **Import data** panel on the overview (`/`) to append or replace rows. Zod validates every line and surfaces row-level errors. 3. **Export data** downloads the currently filtered dataset to CSV/JSON, maintaining schema ordering and ISO timestamps. -Sample starter file lives at `public/sample.csv` with representative rows spanning astrology, Feng Shui, BaZi, numerology, and Tarot (including a `privacy:paid` note example). +Sample starter file lives at `public/sample.csv` with representative rows spanning natal astrology, Jyotiṣa, Feng Shui, BaZi, Qi Men Dun Jia, Human Design, Gene Keys, numerology, and Tarot (including a `privacy:paid` note example). --- diff --git a/next.config.ts b/next.config.ts index e9ffa30..b751ea7 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,24 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", + experimental: { + serverComponentsExternalPackages: ["swisseph"], + }, + webpack: (config, { isServer }) => { + if (isServer) { + const existing = config.externals ?? []; + const externals = Array.isArray(existing) ? [...existing] : [existing]; + + if (!externals.includes("swisseph")) { + externals.push("swisseph"); + } + + config.externals = externals; + } + + return config; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 7ecbda1..05afcee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tanstack/react-virtual": "^3.10.8", "@vvo/tzdb": "^6.128.0", + "astronomy-engine": "^2.1.19", "clsx": "^2.1.1", "d3": "^7.9.0", "luxon": "^3.5.0", @@ -17,6 +18,7 @@ "papaparse": "^5.4.1", "react": "19.2.0", "react-dom": "19.2.0", + "solarlunar": "^2.0.7", "swisseph": "^0.5.17", "zod": "^3.23.8", "zustand": "^5.0.1" @@ -4072,6 +4074,12 @@ "dev": true, "license": "MIT" }, + "node_modules/astronomy-engine": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/astronomy-engine/-/astronomy-engine-2.1.19.tgz", + "integrity": "sha512-8yWKNf7UeNbH458h3sAJ6ZgAjE5jTXp/mNNRFoC20j2SHwZIjAQeEsBB2Q3uCFRaTCCJRv33K2XhkhZQMXoX6w==", + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -10152,6 +10160,12 @@ "node": ">= 6.0.0" } }, + "node_modules/solarlunar": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/solarlunar/-/solarlunar-2.0.7.tgz", + "integrity": "sha512-2SfuCCgAAxFU5MTMYuKGbRgRLcPTJQf3azMEw/GmBpHXA7N2eAQJStSqktZJjnq4qRCboBPnqEB866+PCregag==", + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index 4e633db..3cbf38b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "@tanstack/react-virtual": "^3.10.8", "@vvo/tzdb": "^6.128.0", + "astronomy-engine": "^2.1.19", "clsx": "^2.1.1", "d3": "^7.9.0", "luxon": "^3.5.0", @@ -25,6 +26,7 @@ "papaparse": "^5.4.1", "react": "19.2.0", "react-dom": "19.2.0", + "solarlunar": "^2.0.7", "swisseph": "^0.5.17", "zod": "^3.23.8", "zustand": "^5.0.1" diff --git a/public/sample.csv b/public/sample.csv index 8fca48d..715921f 100644 --- a/public/sample.csv +++ b/public/sample.csv @@ -1,6 +1,10 @@ -person_id,birth_datetime_local,birth_timezone,system,subsystem,source_tool,source_url_or_ref,data_point,verbatim_text,category,subcategory,direction_cardinal,direction_degrees,timing_window_start,timing_window_end,polarity,strength,confidence,weight_system,notes -default-person,1992-09-01T06:03:00,Australia/Sydney,WA,"Tropical · Placidus","Swiss Ephemeris",https://example.com/swiss-ephemeris,"Sun 08° Virgo","Sun at 08° Virgo in 1st house",Personality,"Core Self",,,,,+,2,0.9,1.0,"privacy:paid; source:Swiss Ephemeris" -default-person,1992-09-01T06:03:00,Australia/Sydney,FS,"Period 8 Flying Stars","Feng Shui Toolkit",,Facing North prosperity sector,UNKNOWN,Property,"Wealth Sector",N,0,,,+,1,0.7,1.0,"period:8; facing:0" -default-person,1992-09-01T06:03:00,Australia/Sydney,BaZi,"Standard HKO","Lunisolar Calculator",https://example.com/hko,"BaZi Day Pillar Ji-Si (己巳)","Ji-Si (己巳) day stem/branch",Timing,"Day Master",,,,,+,1,0.8,1.0,"variant:solar-lunar" -default-person,1992-09-01T06:03:00,Australia/Sydney,Numerology_Pythagorean,,"MetaMap numerology",,Life Path 13/4,Life Path 13/4,Personality,"Life Path",,,,,+,1,0.6,1.0,"auto-calculated:numerology" -default-person,1992-09-01T06:03:00,Australia/Sydney,Tarot,"Celtic Cross","MetaMap RNG",,The Sun,UNKNOWN,Guidance,Outcome,,,2024-03-01T10:15:00+11:00,2024-03-01T10:15:00+11:00,+,0,0.5,1.0,"spread:celtic; position:Outcome" +person_id,birth_datetime_local,birth_timezone,system,subsystem,source_tool,source_url_or_ref,data_point,verbatim_text,category,subcategory,direction_cardinal,direction_degrees,timing_window_start,timing_window_end,polarity,strength,confidence,weight_system,notes +default-person,1992-09-01T06:03:00,Australia/Sydney,WA,Sidereal · Lahiri,Astronomy Engine Ephemeris,,Sun 08° Virgo,Sun at 08° Virgo in 1st house,Personality,Core Self,,,,,+,2,0.9,1.0,source:astronomy-engine +default-person,1992-09-01T06:03:00,Australia/Sydney,JA,Lahiri,Astronomy Engine Ephemeris,,Nakṣatra Chitrā,Moon longitude 186.49° (sidereal),Timing,Nakṣatra,,,,,+,0,0.8,1.0,ayanamsa:lahiri +default-person,1992-09-01T06:03:00,Australia/Sydney,FS,Period 8,Traditional Flying Star,,North palace prosperity,Star 9 (base 1 / period 8),Direction,Flying Star,N,0,,,+,1,0.7,1.0,period:8; facing:0 +default-person,1992-09-01T06:03:00,Australia/Sydney,BaZi,Standard,SolarLunar Chinese Calendar,,Day pillar Ji-Si (己巳),Ji-Si (己巳) stem/branch,Timing,Day Master,,,,,+,1,0.85,1.0,gender:unspecified +default-person,1992-09-01T06:03:00,Australia/Sydney,QMDJ,Zhi Run · yang,Lo Shu QMDJ,,Center palace,Wu | Open | Chief,Guidance,Palace,,,,,+,0,0.7,1.0,arrangement:yang +default-person,1992-09-01T06:03:00,Australia/Sydney,HD,BodyGraph,Human Design Gate Mapper,,Type,Manifesting Generator,Personality,Type,,,,,+,0,0.7,0.6,authority:Sacral +default-person,1992-09-01T06:03:00,Australia/Sydney,GK,Life's Work,Gene Keys Profile,,Gene Key 29,Line 2,Guidance,Sphere,,,,,+,0,0.6,0.5,sequence:Activation +default-person,1992-09-01T06:03:00,Australia/Sydney,Numerology_Pythagorean,,MetaMap numerology,,Life Path 13/4,Life Path 13/4,Personality,Life Path,,,,,+,1,0.6,1.0,auto-calculated:numerology +default-person,1992-09-01T06:03:00,Australia/Sydney,Tarot,Celtic Cross,MetaMap RNG,,The Sun,UNKNOWN,Guidance,Outcome,,,2024-03-01T10:15:00+11:00,2024-03-01T10:15:00+11:00,+,0,0.5,1.0,spread:celtic; position:Outcome diff --git a/src/app/api/providers/[provider]/route.ts b/src/app/api/providers/[provider]/route.ts index 92c4a4b..1fd306b 100644 --- a/src/app/api/providers/[provider]/route.ts +++ b/src/app/api/providers/[provider]/route.ts @@ -2,7 +2,14 @@ import { NextResponse } from "next/server"; import { DateTime } from "luxon"; import { getProvider, ProviderUnavailableError, type ProviderKey } from "@/providers"; import { ensureProvidersBootstrapped } from "@/providers/bootstrap"; -import type { ChineseCalendarProvider, QMDJProvider, ZWDSProvider } from "@/calculators"; +import type { + ChineseCalendarProvider, + FSProvider, + GKProvider, + HDProvider, + QMDJProvider, + ZWDSProvider, +} from "@/calculators"; type ProviderParams = { params: { provider: string }; @@ -177,6 +184,80 @@ export async function POST(request: Request, { params }: ProviderParams) { return NextResponse.json({ board }); } + if (params.provider === "fs") { + const payload = await request.json().catch(() => null) as { + sittingDegrees?: number; + facingDegrees?: number; + period?: number; + birthYear?: number; + gender?: "female" | "male" | "unspecified"; + } | null; + + if ( + payload == null || + typeof payload.sittingDegrees !== "number" || + typeof payload.facingDegrees !== "number" || + typeof payload.period !== "number" || + typeof payload.birthYear !== "number" || + (payload.gender && !["female", "male", "unspecified"].includes(payload.gender)) + ) { + return NextResponse.json({ error: "Missing or invalid Feng Shui payload." }, { status: 400 }); + } + + const provider = getProvider("fs") as FSProvider; + const [flyingStars, eightMansions] = await Promise.all([ + provider.computeFlyingStars({ + sittingDegrees: payload.sittingDegrees, + facingDegrees: payload.facingDegrees, + period: payload.period, + }), + provider.computeEightMansions({ + birthYear: payload.birthYear, + gender: payload.gender ?? "unspecified", + }), + ]); + + return NextResponse.json({ flyingStars, eightMansions }); + } + + if (params.provider === "hd") { + const payload = await request.json().catch(() => null) as { + birthIso?: string; + timezone?: string; + } | null; + + if (!payload?.birthIso || !payload.timezone) { + return NextResponse.json({ error: "Missing birthIso or timezone for Human Design request." }, { status: 400 }); + } + + const provider = getProvider("hd") as HDProvider; + const bodyGraph = await provider.computeBodyGraph({ + birthDateTime: payload.birthIso, + timezone: payload.timezone, + }); + + return NextResponse.json(bodyGraph); + } + + if (params.provider === "gk") { + const payload = await request.json().catch(() => null) as { + birthIso?: string; + timezone?: string; + } | null; + + if (!payload?.birthIso || !payload.timezone) { + return NextResponse.json({ error: "Missing birthIso or timezone for Gene Keys request." }, { status: 400 }); + } + + const provider = getProvider("gk") as GKProvider; + const profile = await provider.computeProfile({ + birthDateTime: payload.birthIso, + timezone: payload.timezone, + }); + + return NextResponse.json(profile); + } + return NextResponse.json( { status: "not_implemented", diff --git a/src/app/systems/bazi/page.tsx b/src/app/systems/bazi/page.tsx index 1329fc5..0d5cda3 100644 --- a/src/app/systems/bazi/page.tsx +++ b/src/app/systems/bazi/page.tsx @@ -28,12 +28,16 @@ const BaZiPage = () => { providerLoading, providerErrors, clearProviderError, + appendRow, + pruneRows, } = useStore((state) => ({ birthDetails: state.birthDetails, invokeProvider: state.invokeProvider, providerLoading: state.providerLoading, providerErrors: state.providerErrors, clearProviderError: state.clearProviderError, + appendRow: state.appendRow, + pruneRows: state.pruneRows, })); const [pillars, setPillars] = useState(null); @@ -59,6 +63,56 @@ const BaZiPage = () => { if (response.status === 200 && response.data) { setPillars(response.data.pillars); setLuckPillars(response.data.luckPillars); + const birthIso = `${birthDetails.birthDate}T${birthDetails.birthTime}`; + pruneRows((row) => row.system === "BaZi"); + response.data.pillars.forEach((pillar) => { + appendRow({ + person_id: "default-person", + birth_datetime_local: birthIso, + birth_timezone: birthDetails.timezone, + system: "BaZi", + subsystem: requestPayload.variant, + source_tool: "chineseCalendar", + source_url_or_ref: "", + data_point: `${pillarLabel(pillar.pillar)} pillar`, + verbatim_text: `${pillar.heavenlyStem} · ${pillar.earthlyBranch} (Hidden: ${pillar.hiddenStems.join(", ")})`, + category: "Timing", + subcategory: "Pillar", + direction_cardinal: "", + direction_degrees: null, + timing_window_start: null, + timing_window_end: null, + polarity: "+", + strength: 0, + confidence: 0.85, + weight_system: 1, + notes: `pillar=${pillar.pillar}`, + }); + }); + response.data.luckPillars.forEach((luck) => { + appendRow({ + person_id: "default-person", + birth_datetime_local: birthIso, + birth_timezone: birthDetails.timezone, + system: "BaZi", + subsystem: `${requestPayload.variant} luck`, + source_tool: "chineseCalendar", + source_url_or_ref: "", + data_point: `Luck pillar ${luck.index + 1}`, + verbatim_text: `${luck.pillar.heavenlyStem} · ${luck.pillar.earthlyBranch} (start age ${luck.startingAge})`, + category: "Timing", + subcategory: "Luck", + direction_cardinal: "", + direction_degrees: null, + timing_window_start: null, + timing_window_end: null, + polarity: "+", + strength: 0, + confidence: 0.8, + weight_system: 1, + notes: `duration=${luck.durationYears}y`, + }); + }); } }; @@ -68,11 +122,11 @@ const BaZiPage = () => { description="Compute Heavenly Stems and Earthly Branches using the configured Chinese calendar provider." >
diff --git a/src/app/systems/fs/page.tsx b/src/app/systems/fs/page.tsx index 92ca37e..72a0686 100644 --- a/src/app/systems/fs/page.tsx +++ b/src/app/systems/fs/page.tsx @@ -1,150 +1,258 @@ -"use client"; - -import { useMemo, useState } from "react"; -import { WarningBanner } from "@/components/WarningBanner"; -import { SystemPageLayout } from "@/components/SystemPageLayout"; -import { useStore } from "@/store/useStore"; - -const periods = Array.from({ length: 9 }, (_, index) => index + 1); - -const floorGrid = ["N", "NE", "E", "SE", "S", "SW", "W", "NW", "Centre"]; - -const computeLifeGua = (year: number, gender: "female" | "male" | "unspecified") => { - const yearDigits = year - .toString() - .split("") - .reduce((acc, digit) => acc + Number(digit), 0); - const reduce = (value: number): number => { - let result = value; - while (result > 9) { - result = result - .toString() - .split("") - .reduce((acc, digit) => acc + Number(digit), 0); - } - return result; - }; - const base = reduce(yearDigits); - if (gender === "unspecified") return "UNKNOWN"; - if (gender === "male") { - const gua = 10 - base; - return gua === 5 ? "2" : gua.toString(); - } - const gua = base + 5; - const reduced = gua > 9 ? gua - 9 : gua; - return reduced === 5 ? "8" : reduced.toString(); -}; - -const FsPage = () => { - const birthDetails = useStore((state) => state.birthDetails); - const [period, setPeriod] = useState(8); - const [facing, setFacing] = useState(null); - const [gender, setGender] = useState<"female" | "male" | "unspecified">("unspecified"); - - const lifeGua = useMemo(() => { - const year = Number(birthDetails.birthDate.split("-")[0]); - if (Number.isNaN(year)) return "UNKNOWN"; - return computeLifeGua(year, gender); - }, [birthDetails.birthDate, gender]); - - return ( - - -
-
-
-
- - -
-
-
- {floorGrid.map((label) => ( -
-

{label}

-

UNKNOWN

-

Period {period}

-
- ))} -
- {facing != null && ( -

- Facing marker: {facing}°. Update this after measuring the site. -

- )} -
-
-
-

Eight Mansions Life Gua

- -

- Life Gua: {lifeGua} -

-

- When integrating an FSProvider, write the computed gua number into the dataset with{" "} - system="FS" and category="Direction". -

-
-
-

Integration notes

-
    -
  • Use subsystem to track Flying Star period (1-9).
  • -
  • - For paid calculators, append notes:"privacy:paid" so users can filter. -
  • -
  • Log conflict sets when facing/sitting outputs disagree across schools.
  • -
-
-
-
-
- ); -}; - -export default FsPage; +"use client"; + +import { useState } from "react"; +import { WarningBanner } from "@/components/WarningBanner"; +import { SystemPageLayout } from "@/components/SystemPageLayout"; +import { useStore } from "@/store/useStore"; +import type { EightMansionsResult, FlyingStar } from "@/calculators"; + +const periods = Array.from({ length: 9 }, (_, index) => index + 1); +const floorGrid = ["N", "NE", "E", "SE", "S", "SW", "W", "NW", "Centre"]; + +const placeholderStars = floorGrid.map((label) => ({ + palace: label, + star: null as number | null, + baseStar: null as number | null, + periodStar: null as number | null, +})); + +const FsPage = () => { + const { + birthDetails, + invokeProvider, + providerLoading, + providerErrors, + clearProviderError, + appendRow, + pruneRows, + } = useStore((state) => ({ + birthDetails: state.birthDetails, + invokeProvider: state.invokeProvider, + providerLoading: state.providerLoading, + providerErrors: state.providerErrors, + clearProviderError: state.clearProviderError, + appendRow: state.appendRow, + pruneRows: state.pruneRows, + })); + + const [period, setPeriod] = useState(8); + const [facing, setFacing] = useState(null); + const [gender, setGender] = useState<"female" | "male" | "unspecified">("unspecified"); + const [flyingStars, setFlyingStars] = useState(null); + const [eightMansions, setEightMansions] = useState(null); + + const loading = providerLoading.fs ?? false; + const errorMessage = providerErrors.fs; + + const handleCompute = async () => { + const birthYear = Number.parseInt(birthDetails.birthDate.split("-")[0] ?? "0", 10); + if (Number.isNaN(birthYear) || facing == null) { + return; + } + + clearProviderError("fs"); + + const response = await invokeProvider< + { + sittingDegrees: number; + facingDegrees: number; + period: number; + birthYear: number; + gender: "female" | "male" | "unspecified"; + }, + { flyingStars: FlyingStar[]; eightMansions: EightMansionsResult } + >("fs", { + sittingDegrees: (facing + 180) % 360, + facingDegrees: facing, + period, + birthYear, + gender, + }); + + if (response.status !== 200 || !response.data) { + return; + } + + setFlyingStars(response.data.flyingStars); + setEightMansions(response.data.eightMansions); + + const birthIso = `${birthDetails.birthDate}T${birthDetails.birthTime}`; + pruneRows((row) => row.system === "FS"); + + response.data.flyingStars.forEach((cell) => { + appendRow({ + person_id: "default-person", + birth_datetime_local: birthIso, + birth_timezone: birthDetails.timezone, + system: "FS", + subsystem: `Period ${period}`, + source_tool: "fs", + source_url_or_ref: "", + data_point: `${cell.palace} palace`, + verbatim_text: `Star ${cell.star} (base ${cell.baseStar} / period ${cell.periodStar})`, + category: "Direction", + subcategory: "Flying Star", + direction_cardinal: "", + direction_degrees: cell.palace === "Centre" ? null : facing, + timing_window_start: null, + timing_window_end: null, + polarity: "+", + strength: 0, + confidence: 0.7, + weight_system: 1, + notes: `facing=${facing}`, + }); + }); + + appendRow({ + person_id: "default-person", + birth_datetime_local: birthIso, + birth_timezone: birthDetails.timezone, + system: "FS", + subsystem: "Eight Mansions", + source_tool: "fs", + source_url_or_ref: "", + data_point: "Life Gua", + verbatim_text: response.data.eightMansions.mingGua, + category: "Guidance", + subcategory: "Eight Mansions", + direction_cardinal: "", + direction_degrees: null, + timing_window_start: null, + timing_window_end: null, + polarity: "+", + strength: 0, + confidence: 0.6, + weight_system: 1, + notes: `favourable=${response.data.eightMansions.favourableDirections.join("|")}`, + }); + }; + + const displayStars = flyingStars ?? placeholderStars; + + return ( + + +
+
+
+
+ + +
+ +
+ {errorMessage && ( +

+ {errorMessage} +

+ )} +
+ {displayStars.map((cell) => ( +
+

{cell.palace}

+

+ {cell.star == null ? "—" : cell.star} +

+

+ {cell.baseStar == null || cell.periodStar == null + ? `Period ${period}` + : `Base ${cell.baseStar} · Period ${cell.periodStar}`} +

+
+ ))} +
+ {facing != null && ( +

+ Facing marker: {facing}°. Update this after measuring the site. +

+ )} +
+
+
+

Eight Mansions Life Gua

+ +

+ Life Gua: {eightMansions?.mingGua ?? "UNKNOWN"} +

+ {eightMansions && ( +
+

Favourable: {eightMansions.favourableDirections.join(", ") || "—"}

+

Unfavourable: {eightMansions.unfavourableDirections.join(", ") || "—"}

+
+ )} +
+
+

Integration notes

+
    +
  • Use subsystem to track Flying Star period (1-9).
  • +
  • + For paid calculators, append notes:"privacy:paid" so users can filter. +
  • +
  • Log conflict sets when facing/sitting outputs disagree across schools.
  • +
+
+
+
+
+ ); +}; + +export default FsPage; diff --git a/src/app/systems/gk/page.tsx b/src/app/systems/gk/page.tsx index 4bd2af3..c2c31bd 100644 --- a/src/app/systems/gk/page.tsx +++ b/src/app/systems/gk/page.tsx @@ -1,43 +1,136 @@ -"use client"; - -import { WarningBanner } from "@/components/WarningBanner"; -import { SystemPageLayout } from "@/components/SystemPageLayout"; - -const sequences = ["Activation", "Venus", "Pearl"]; - -const GkPage = () => ( - - -
-

Profile overview

-
- {sequences.map((sequence) => ( -
-

{sequence} sequence

-

UNKNOWN

-

Spheres logged after provider integration.

-
- ))} -
-

- Weight default: 0.5. Adjust using the weights panel to re-balance aggregate visuals. -

-
-
-

Integration notes

-
    -
  • Use subsystem to capture profile sequences (e.g. Activation, Venus).
  • -
  • Annotate paid datasets with notes:"privacy:paid".
  • -
  • Leverage conflict_set for divergent interpretations across schools.
  • -
-
-
-); - -export default GkPage; +"use client"; + +import { useState } from "react"; +import { WarningBanner } from "@/components/WarningBanner"; +import { SystemPageLayout } from "@/components/SystemPageLayout"; +import { useStore } from "@/store/useStore"; + +const sphereOrder = ["Life's Work", "Evolution", "Radiance", "Purpose"]; + +const GkPage = () => { + const { + birthDetails, + invokeProvider, + providerLoading, + providerErrors, + clearProviderError, + appendRow, + pruneRows, + } = useStore((state) => ({ + birthDetails: state.birthDetails, + invokeProvider: state.invokeProvider, + providerLoading: state.providerLoading, + providerErrors: state.providerErrors, + clearProviderError: state.clearProviderError, + appendRow: state.appendRow, + pruneRows: state.pruneRows, + })); + + const [profile, setProfile] = useState<{ name: string; geneKey: number; line?: number }[] | null>(null); + + const loading = providerLoading.gk ?? false; + const errorMessage = providerErrors.gk; + + const handleCompute = async () => { + clearProviderError("gk"); + const response = await invokeProvider< + { birthIso: string; timezone: string }, + { spheres: Array<{ name: string; geneKey: number; line?: number }> } + >("gk", { + birthIso: `${birthDetails.birthDate}T${birthDetails.birthTime}`, + timezone: birthDetails.timezone, + }); + + if (response.status !== 200 || !response.data) { + return; + } + + setProfile(response.data.spheres); + + const birthIso = `${birthDetails.birthDate}T${birthDetails.birthTime}`; + pruneRows((row) => row.system === "GK"); + response.data.spheres.forEach((sphere) => { + appendRow({ + person_id: "default-person", + birth_datetime_local: birthIso, + birth_timezone: birthDetails.timezone, + system: "GK", + subsystem: sphere.name, + source_tool: "gk", + source_url_or_ref: "", + data_point: `Gene Key ${sphere.geneKey}`, + verbatim_text: sphere.line ? `Line ${sphere.line}` : "Line UNKNOWN", + category: "Guidance", + subcategory: "Sphere", + direction_cardinal: "", + direction_degrees: null, + timing_window_start: null, + timing_window_end: null, + polarity: "+", + strength: 0, + confidence: 0.6, + weight_system: 0.5, + notes: "", + }); + }); + }; + + return ( + + +
+
+

Profile overview

+ +
+ {errorMessage && ( +

+ {errorMessage} +

+ )} +
+ {sphereOrder.map((name) => { + const sphere = profile?.find((item) => item.name === name); + return ( +
+

{name}

+

+ {sphere ? `Gene Key ${sphere.geneKey}` : "—"} +

+

{sphere?.line ? `Line ${sphere.line}` : "Awaiting provider"}

+
+ ); + })} +
+

Weight default: 0.5. Adjust using the weights panel to re-balance aggregate visuals.

+
+
+

Integration notes

+
    +
  • Use subsystem to capture profile sequences (e.g. Activation, Venus).
  • +
  • Annotate paid datasets with notes:"privacy:paid".
  • +
  • Leverage conflict_set for divergent interpretations across schools.
  • +
+
+
+ ); +}; + +export default GkPage; diff --git a/src/app/systems/hd/page.tsx b/src/app/systems/hd/page.tsx index 673cd8f..47d30d8 100644 --- a/src/app/systems/hd/page.tsx +++ b/src/app/systems/hd/page.tsx @@ -1,43 +1,167 @@ -"use client"; - -import { WarningBanner } from "@/components/WarningBanner"; -import { SystemPageLayout } from "@/components/SystemPageLayout"; - -const centres = ["Head", "Ajna", "Throat", "G", "Heart", "Sacral", "Solar Plexus", "Spleen", "Root"]; - -const HdPage = () => ( - - -
-

BodyGraph scaffold

-

Centres will display defined/undefined states once integrated.

-
- {centres.map((centre) => ( -
-

{centre}

-

UNKNOWN

-
- ))} -
-

- Weight default: 0.6. Adjust in the weights panel to emphasise or reduce HD contributions. -

-
-
-

Integration notes

-
    -
  • Use HDProvider.computeBodyGraph to populate centres and channels.
  • -
  • Tag paid APIs with notes:"privacy:paid" for filter compatibility.
  • -
  • Support VARIANT flags for Jovian vs. Genetic Matrix interpretations via subsystem.
  • -
-
-
-); - -export default HdPage; +"use client"; + +import { useMemo, useState } from "react"; +import { WarningBanner } from "@/components/WarningBanner"; +import { SystemPageLayout } from "@/components/SystemPageLayout"; +import { useStore } from "@/store/useStore"; + +const centreOrder: Array<{ key: string; label: string }> = [ + { key: "head", label: "Head" }, + { key: "ajna", label: "Ajna" }, + { key: "throat", label: "Throat" }, + { key: "g", label: "G" }, + { key: "ego", label: "Heart" }, + { key: "sacral", label: "Sacral" }, + { key: "solarPlexus", label: "Solar Plexus" }, + { key: "spleen", label: "Spleen" }, + { key: "root", label: "Root" }, +]; + +const HdPage = () => { + const { + birthDetails, + invokeProvider, + providerLoading, + providerErrors, + clearProviderError, + appendRow, + pruneRows, + } = useStore((state) => ({ + birthDetails: state.birthDetails, + invokeProvider: state.invokeProvider, + providerLoading: state.providerLoading, + providerErrors: state.providerErrors, + clearProviderError: state.clearProviderError, + appendRow: state.appendRow, + pruneRows: state.pruneRows, + })); + + const [bodyGraph, setBodyGraph] = useState<{ + centres: Record; + type?: string; + authority?: string; + } | null>(null); + + const loading = providerLoading.hd ?? false; + const errorMessage = providerErrors.hd; + + const definedCentres = useMemo(() => { + if (!bodyGraph) { + return 0; + } + return Object.values(bodyGraph.centres).filter((state) => state === "defined").length; + }, [bodyGraph]); + + const handleCompute = async () => { + clearProviderError("hd"); + const response = await invokeProvider< + { birthIso: string; timezone: string }, + { centres: Record; type?: string; authority?: string } + >("hd", { + birthIso: `${birthDetails.birthDate}T${birthDetails.birthTime}`, + timezone: birthDetails.timezone, + }); + + if (response.status !== 200 || !response.data) { + return; + } + + setBodyGraph(response.data); + + const birthIso = `${birthDetails.birthDate}T${birthDetails.birthTime}`; + pruneRows((row) => row.system === "HD"); + + appendRow({ + person_id: "default-person", + birth_datetime_local: birthIso, + birth_timezone: birthDetails.timezone, + system: "HD", + subsystem: "BodyGraph", + source_tool: "hd", + source_url_or_ref: "", + data_point: "Type", + verbatim_text: response.data.type ?? "UNKNOWN", + category: "Personality", + subcategory: "Type", + direction_cardinal: "", + direction_degrees: null, + timing_window_start: null, + timing_window_end: null, + polarity: "+", + strength: 0, + confidence: 0.7, + weight_system: 0.6, + notes: `authority=${response.data.authority ?? "UNKNOWN"}`, + }); + }; + + return ( + + +
+
+

BodyGraph centres

+ +
+ {errorMessage && ( +

+ {errorMessage} +

+ )} +
+ {centreOrder.map((centre) => { + const state = bodyGraph?.centres?.[centre.key] ?? "undefined"; + return ( +
+

{centre.label}

+

{state === "defined" ? "Defined" : "Open"}

+
+ ); + })} +
+

+ Weight default: 0.6. Adjust in the weights panel to emphasise or reduce Human Design contributions. +

+
+ {bodyGraph && ( +
+

Summary

+

Type: {bodyGraph.type ?? "UNKNOWN"}

+

Authority: {bodyGraph.authority ?? "UNKNOWN"}

+
+ )} +
+

Integration notes

+
    +
  • Use HDProvider.computeBodyGraph to populate centres and channels.
  • +
  • Tag paid APIs with notes:"privacy:paid" for filter compatibility.
  • +
  • Support VARIANT flags for Jovian vs. Genetic Matrix interpretations via subsystem.
  • +
+
+
+ ); +}; + +export default HdPage; diff --git a/src/app/systems/ja/page.tsx b/src/app/systems/ja/page.tsx index 5c814e2..d58e486 100644 --- a/src/app/systems/ja/page.tsx +++ b/src/app/systems/ja/page.tsx @@ -1,179 +1,274 @@ -"use client"; - -import { useMemo, useState } from "react"; -import { DateTime } from "luxon"; -import { WarningBanner } from "@/components/WarningBanner"; -import { SystemPageLayout } from "@/components/SystemPageLayout"; -import { useStore } from "@/store/useStore"; -import { computeLifePath } from "@/lib/numerology"; - -const nakshatraList = [ - "Aśvinī", - "Bharanī", - "Kṛttikā", - "Rohiṇī", - "Mṛgaśīrṣa", - "Ārdrā", - "Punarvasu", - "Puṣya", - "Āśleṣā", - "Maghā", - "Pūrva Phalgunī", - "Uttara Phalgunī", - "Hasta", - "Chitrā", - "Svātī", - "Viśākhā", - "Anurādhā", - "Jyeṣṭhā", - "Mūla", - "Pūrva Aṣāḍhā", - "Uttara Aṣāḍhā", - "Śravaṇā", - "Dhaniṣṭhā", - "Śatabhiṣā", - "Pūrva Bhādrapadā", - "Uttara Bhādrapadā", - "Revatī", -]; - -const ayanamsaVariants = ["Lahiri", "Krishnamurti", "Raman", "Fagan/Bradley", "Yukteswar"]; - -const JaPage = () => { - const { birthDetails, setBirthDetails } = useStore((state) => ({ - birthDetails: state.birthDetails, - setBirthDetails: state.setBirthDetails, - })); - - const [selectedNakshatra, setSelectedNakshatra] = useState(null); - - const variantNote = useMemo( - () => - birthDetails.ayanamsa !== "Lahiri" - ? `VARIANT: ${birthDetails.ayanamsa} ayanāṃśa selected` - : null, - [birthDetails.ayanamsa], - ); - - const placeholderDashas = useMemo(() => { - const lifePath = computeLifePath(birthDetails.birthDate); - return [ - { sequence: "Mahādashā", lord: "UNKNOWN", start: "UNKNOWN", finish: "UNKNOWN" }, - { - sequence: "Seeded example", - lord: "Birth number influence", - start: lifePath.compound.toString(), - finish: lifePath.reduced.toString(), - }, - ]; - }, [birthDetails.birthDate]); - - return ( - - - {variantNote && ( -
- {variantNote} -
- )} -
-
-

Nakṣatra finder (stub)

-

- Select a nakṣatra to log a placeholder row. Once a provider is connected, this interface - will resolve the lunar mansion based on the Moon's sidereal longitude. -

-
- - - -

- Logging will add a dataset row with verbatim_text="UNKNOWN" and - notes="TODO: integrate calculator". Replace once you connect a provider. -

-
-
-
-

Dashā preview

- - - - - - - - - - - {placeholderDashas.map((dashā) => ( - - - - - - - ))} - -
SequenceLordStartFinish
{dashā.sequence}{dashā.lord}{dashā.start}{dashā.finish}
-
-
-
-

Notes for integrators

-
    -
  • Use the EphemerisProvider interface to retrieve sidereal lunar longitude.
  • -
  • - Tag paid services with notes:"privacy:paid" and expose toggles in the UI. -
  • -
  • Provide multiple school variants (Parāśara, KP, Jaimini) by extending subsystem.
  • -
-

- Birth date preview:{" "} - {DateTime.fromISO(`${birthDetails.birthDate}T${birthDetails.birthTime}`, { - zone: birthDetails.timezone, - }).toFormat("d LLL yyyy HH:mm ZZZZ")} -

-
-
- ); -}; - -export default JaPage; +"use client"; + +import { useState } from "react"; +import { DateTime } from "luxon"; +import { WarningBanner } from "@/components/WarningBanner"; +import { SystemPageLayout } from "@/components/SystemPageLayout"; +import { useStore } from "@/store/useStore"; + +const nakshatraList = [ + "Aśvinī", + "Bharanī", + "Kṛttikā", + "Rohiṇī", + "Mṛgaśīrṣa", + "Ārdrā", + "Punarvasu", + "Puṣya", + "Āśleṣā", + "Maghā", + "Pūrva Phalgunī", + "Uttara Phalgunī", + "Hasta", + "Chitrā", + "Svātī", + "Viśākhā", + "Anurādhā", + "Jyeṣṭhā", + "Mūla", + "Pūrva Aṣāḍhā", + "Uttara Aṣāḍhā", + "Śravaṇā", + "Dhaniṣṭhā", + "Śatabhiṣā", + "Pūrva Bhādrapadā", + "Uttara Bhādrapadā", + "Revatī", +]; + +const dashaOrder = ["Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury"]; +const dashaDurations = [7, 20, 6, 10, 7, 18, 16, 19, 17]; +const nakshatraToDashaIndex = [0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8]; + +const JaPage = () => { + const { + birthDetails, + invokeProvider, + providerLoading, + providerErrors, + clearProviderError, + setBirthDetails, + appendRow, + pruneRows, + } = useStore((state) => ({ + birthDetails: state.birthDetails, + invokeProvider: state.invokeProvider, + providerLoading: state.providerLoading, + providerErrors: state.providerErrors, + clearProviderError: state.clearProviderError, + setBirthDetails: state.setBirthDetails, + appendRow: state.appendRow, + pruneRows: state.pruneRows, + })); + + const [nakshatra, setNakshatra] = useState(null); + const [dashas, setDashas] = useState< + Array<{ sequence: string; lord: string; start: string; finish: string }> + >([]); + + const loading = providerLoading.ephemeris ?? false; + const errorMessage = providerErrors.ephemeris; + + const handleCompute = async () => { + clearProviderError("ephemeris"); + const response = await invokeProvider< + { + birthIso: string; + timezone: string; + coordinates: { latitude: number; longitude: number }; + options: { zodiac: "tropical" | "sidereal"; ayanamsa?: string; houseSystem?: string }; + }, + { ephemeris: { bodies: Array<{ id: string; longitude: number }> } } + >("ephemeris", { + birthIso: `${birthDetails.birthDate}T${birthDetails.birthTime}`, + timezone: birthDetails.timezone, + coordinates: { latitude: birthDetails.latitude ?? 0, longitude: birthDetails.longitude ?? 0 }, + options: { + zodiac: "sidereal", + ayanamsa: birthDetails.ayanamsa, + houseSystem: birthDetails.houseSystem, + }, + }); + + const ephemeris = response.data?.ephemeris; + if (response.status !== 200 || !ephemeris) { + return; + } + + const moon = ephemeris.bodies.find((body) => body.id === "moon"); + if (!moon) { + return; + } + + const segment = 360 / 27; + const index = Math.max(0, Math.min(nakshatraList.length - 1, Math.floor(moon.longitude / segment))); + const selectedNakshatra = nakshatraList[index]; + setNakshatra(selectedNakshatra); + + const birthDateTime = DateTime.fromISO(`${birthDetails.birthDate}T${birthDetails.birthTime}`, { + zone: birthDetails.timezone, + }); + + if (!birthDateTime.isValid) { + return; + } + + const startIndex = nakshatraToDashaIndex[index]; + const span = segment; + const offsetWithinNakshatra = moon.longitude - index * span; + const proportionElapsedRaw = span === 0 ? 0 : offsetWithinNakshatra / span; + const proportionElapsed = Math.min(Math.max(proportionElapsedRaw, 0), 1); + const remainingYearsCurrent = (1 - proportionElapsed) * dashaDurations[startIndex]; + + let cursor = birthDateTime; + const generatedDashas = Array.from({ length: dashaOrder.length }).map((_, offset) => { + const orderIndex = (startIndex + offset) % dashaOrder.length; + const durationYears = offset === 0 ? remainingYearsCurrent : dashaDurations[orderIndex]; + const start = cursor; + const finish = start.plus({ years: durationYears }); + cursor = finish; + return { + sequence: dashaOrder[orderIndex], + lord: dashaOrder[orderIndex], + start: start.toISODate() ?? "", + finish: finish.toISODate() ?? "", + }; + }); + + setDashas(generatedDashas); + + const birthIso = `${birthDetails.birthDate}T${birthDetails.birthTime}`; + pruneRows((row) => row.system === "JA"); + appendRow({ + person_id: "default-person", + birth_datetime_local: birthIso, + birth_timezone: birthDetails.timezone, + system: "JA", + subsystem: birthDetails.ayanamsa, + source_tool: "ja", + source_url_or_ref: "", + data_point: `Nakṣatra ${selectedNakshatra}`, + verbatim_text: `Moon longitude ${moon.longitude.toFixed(2)}°`, + category: "Timing", + subcategory: "Nakṣatra", + direction_cardinal: "", + direction_degrees: null, + timing_window_start: null, + timing_window_end: null, + polarity: "+", + strength: 0, + confidence: 0.8, + weight_system: 1, + notes: "", + }); + + generatedDashas.forEach((dasha) => { + appendRow({ + person_id: "default-person", + birth_datetime_local: birthIso, + birth_timezone: birthDetails.timezone, + system: "JA", + subsystem: `${birthDetails.ayanamsa} dashā`, + source_tool: "ja", + source_url_or_ref: "", + data_point: `${dasha.sequence} mahādashā`, + verbatim_text: `Starts ${dasha.start}`, + category: "Timing", + subcategory: "Mahādashā", + direction_cardinal: "", + direction_degrees: null, + timing_window_start: dasha.start, + timing_window_end: dasha.finish, + polarity: "+", + strength: 0, + confidence: 0.7, + weight_system: 1, + notes: "", + }); + }); + }; + + return ( + + +
+
+
+

Nakṣatra

+ +
+ {errorMessage && ( +

+ {errorMessage} +

+ )} +

+ {nakshatra ?? "Awaiting provider"} +

+
+
+

Ayanāṃśa variant

+ +
+
+
+

Mahādashā timeline

+ + + + + + + + + + {dashas.length > 0 ? ( + dashas.map((dasha) => ( + + + + + + )) + ) : ( + + + + )} + +
SequenceStartFinish
{dasha.sequence}{dasha.start}{dasha.finish}
+ Mahādashā sequence will appear after computing the nakṣatra. +
+
+
+ ); +}; + +export default JaPage; diff --git a/src/app/systems/qmdj/page.tsx b/src/app/systems/qmdj/page.tsx index ed6fa6a..3475225 100644 --- a/src/app/systems/qmdj/page.tsx +++ b/src/app/systems/qmdj/page.tsx @@ -24,12 +24,16 @@ const QmdjPage = () => { providerLoading, providerErrors, clearProviderError, + appendRow, + pruneRows, } = useStore((state) => ({ birthDetails: state.birthDetails, invokeProvider: state.invokeProvider, providerLoading: state.providerLoading, providerErrors: state.providerErrors, clearProviderError: state.clearProviderError, + appendRow: state.appendRow, + pruneRows: state.pruneRows, })); const [school, setSchool] = useState<(typeof schools)[number]>("Zhi Run"); @@ -52,6 +56,32 @@ const QmdjPage = () => { }); if (response.status === 200 && response.data) { setBoard(response.data.board.chart); + const birthIso = `${birthDetails.birthDate}T${birthDetails.birthTime}`; + pruneRows((row) => row.system === "QMDJ"); + response.data.board.chart.forEach((cell) => { + appendRow({ + person_id: "default-person", + birth_datetime_local: birthIso, + birth_timezone: birthDetails.timezone, + system: "QMDJ", + subsystem: `${school} · ${arrangement}`, + source_tool: "qmdj", + source_url_or_ref: "", + data_point: `${cell.palace} palace`, + verbatim_text: `${cell.star} | ${cell.door} | ${cell.deity}`, + category: "Guidance", + subcategory: "Palace", + direction_cardinal: "", + direction_degrees: null, + timing_window_start: null, + timing_window_end: null, + polarity: "+", + strength: 0, + confidence: 0.7, + weight_system: 1, + notes: `arrangement=${arrangement}`, + }); + }); } }; @@ -61,11 +91,11 @@ const QmdjPage = () => { description="3×3 Lo Shu board with selectable school and arrangement populated by the configured provider." >
diff --git a/src/app/systems/wa-ha/actions.ts b/src/app/systems/wa-ha/actions.ts index 9aa6305..9ca5034 100644 --- a/src/app/systems/wa-ha/actions.ts +++ b/src/app/systems/wa-ha/actions.ts @@ -2,7 +2,7 @@ import { insertDatasetRows } from "@/server/datasets"; import { createId } from "@/lib/id"; -import type { PlanetPosition } from "@/calculators"; +import type { EphemerisBody } from "@/lib/ephemeris"; import type { ProviderKey } from "@/providers"; import type { DataRow, System } from "@/schema"; @@ -11,7 +11,7 @@ const normaliseDegrees = (value: number): number => { return wrapped < 0 ? wrapped + 360 : wrapped; }; -const buildVerbatimText = (position: PlanetPosition): string => { +const buildVerbatimText = (position: EphemerisBody): string => { const longitude = position.longitude.toFixed(2); const houseLabel = position.house != null ? ` (house ${position.house})` : ""; return `${position.name} @ ${longitude}\u00B0${houseLabel}`; @@ -24,7 +24,7 @@ const buildBaseRow = ( system: System, subsystem: string, sourceTool: ProviderKey, - position: PlanetPosition, + position: EphemerisBody, ): DataRow => ({ id: createId(), person_id: personId, @@ -56,7 +56,7 @@ export interface PersistEphemerisPayload { birth: { date: string; time: string; timezone: string }; options: { zodiac: "Tropical" | "Sidereal"; houseSystem: string; ayanamsa: string }; coordinates: { latitude: number | null; longitude: number | null }; - positions: PlanetPosition[]; + positions: EphemerisBody[]; provider: ProviderKey; } diff --git a/src/app/systems/wa-ha/page.tsx b/src/app/systems/wa-ha/page.tsx index 0ad8566..1fdf87b 100644 --- a/src/app/systems/wa-ha/page.tsx +++ b/src/app/systems/wa-ha/page.tsx @@ -4,13 +4,9 @@ import { useCallback, useMemo, useState } from "react"; import { useStore } from "@/store/useStore"; import { WarningBanner } from "@/components/WarningBanner"; import { SystemPageLayout } from "@/components/SystemPageLayout"; -import type { PlanetPosition } from "@/calculators"; +import type { EphemerisBody, EphemerisResponse } from "@/lib/ephemeris"; import { persistEphemerisResults } from "./actions"; -type EphemerisResponse = { - positions?: PlanetPosition[]; -}; - const WaHaPage = () => { const { birthDetails, setBirthDetails, dataset, addRows, invokeProvider } = useStore((state) => ({ birthDetails: state.birthDetails, @@ -22,6 +18,7 @@ const WaHaPage = () => { const [isComputing, setIsComputing] = useState(false); const [error, setError] = useState(null); + const [ephemeris, setEphemeris] = useState(null); const ephemerisRows = useMemo( () => dataset.filter((row) => row.system === "WA" || row.system === "HA"), @@ -66,13 +63,15 @@ const WaHaPage = () => { return; } - const positions = (response.data ?? {}).positions ?? []; + const ephemerisResponse = (response.data as { ephemeris?: EphemerisResponse } | null)?.ephemeris; - if (positions.length === 0) { - setError("Ephemeris provider returned no positions to persist."); + if (!ephemerisResponse) { + setError("Ephemeris provider returned an empty payload."); return; } + setEphemeris(ephemerisResponse); + const persisted = await persistEphemerisResults({ personId: "default-person", birth: { @@ -89,7 +88,7 @@ const WaHaPage = () => { latitude: birthDetails.latitude ?? null, longitude: birthDetails.longitude ?? null, }, - positions, + positions: ephemerisResponse.bodies as EphemerisBody[], provider: "ephemeris", }); @@ -121,44 +120,49 @@ const WaHaPage = () => { return (
-

Wheel preview

-

- Placeholder wheel shows rising sign, midheaven, and houses once a provider is wired. -

- - - {Array.from({ length: 12 }).map((_, index) => { - const angle = (index / 12) * Math.PI * 2; - return ( - - ); - })} - - {birthDetails.zodiac} · {birthDetails.houseSystem} - - +

Angles & houses

+ {ephemeris ? ( +
+
+

Angles

+
    + {ephemeris.angles.map((angle) => ( +
  • + {angle.id} + {angle.longitude.toFixed(2)}° +
  • + ))} +
+
+
+

House cusps

+
    + {ephemeris.houses.map((house) => ( +
  • + House {house.index} + {house.cusp.toFixed(2)}° +
  • + ))} +
+
+
+ ) : ( +

+ Run the computation to populate ascendant, midheaven, and house cusps. +

+ )}
@@ -260,6 +264,35 @@ const WaHaPage = () => { )}
+ {ephemeris && ( +
+

Current planetary positions

+
+ + + + + + + + + + + + {ephemeris.bodies.map((body) => ( + + + + + + + + ))} + +
BodyLongitudeLatitudeHouseMotion
{body.name}{body.longitude.toFixed(2)}°{body.latitude.toFixed(2)}°{body.house ?? "—"}{body.retrograde ? "Retrograde" : "Direct"}
+
+
+ )}
); }; diff --git a/src/app/systems/zwds/page.tsx b/src/app/systems/zwds/page.tsx index 43f171a..0f115e3 100644 --- a/src/app/systems/zwds/page.tsx +++ b/src/app/systems/zwds/page.tsx @@ -13,12 +13,14 @@ const ZwdsPage = () => { providerLoading, providerErrors, clearProviderError, + appendRow, } = useStore((state) => ({ birthDetails: state.birthDetails, invokeProvider: state.invokeProvider, providerLoading: state.providerLoading, providerErrors: state.providerErrors, clearProviderError: state.clearProviderError, + appendRow: state.appendRow, })); const [chart, setChart] = useState(null); @@ -39,6 +41,31 @@ const ZwdsPage = () => { }); if (response.status === 200 && response.data) { setChart(response.data.chart); + const birthIso = `${birthDetails.birthDate}T${birthDetails.birthTime}`; + response.data.chart.forEach((palace) => { + appendRow({ + person_id: "default-person", + birth_datetime_local: birthIso, + birth_timezone: birthDetails.timezone, + system: "ZWDS", + subsystem: variant, + source_tool: "zwds", + source_url_or_ref: "", + data_point: `${palace.palace} palace`, + verbatim_text: palace.stars.join(", "), + category: "Guidance", + subcategory: "Palace", + direction_cardinal: "", + direction_degrees: null, + timing_window_start: null, + timing_window_end: null, + polarity: "+", + strength: 0, + confidence: 0.75, + weight_system: 1, + notes: palace.notes ?? "", + }); + }); } }; @@ -48,11 +75,11 @@ const ZwdsPage = () => { description="Twelve palace grid populated by the configured ZWDS provider." >
diff --git a/src/components/ProviderStatusPanel.tsx b/src/components/ProviderStatusPanel.tsx index 211caaa..feb0f9d 100644 --- a/src/components/ProviderStatusPanel.tsx +++ b/src/components/ProviderStatusPanel.tsx @@ -23,7 +23,8 @@ export const ProviderStatusPanel = () => {

Provider health

- Register calculator providers via the registry API to unlock live data outputs. + Core calculators bootstrap on demand. Refresh to verify Swiss Ephemeris, Chinese calendar, and metaphysics + providers are responding.