--- name: hd-prism-bulk-import description: Import birth data in bulk into HD Prism without triggering LLM/AI reading generation version: 1.0 --- # HD Prism Bulk Chart Import Import birth data in bulk into HD Prism without triggering LLM/AI reading generation. ## Architecture - **Charts** are saved as full bodygraph JSON in `saved_charts` SQLite table - **Readings** are triggered separately via `POST /api/readings/generate` — only when a user opens a chart - **Key insight**: Calling `createBodygraph()` + `createSavedChart()` directly inserts charts without any LLM calls ## Key Modules (all in `apps/api/src/`) | Module | Function | Purpose | |--------|----------|---------| | `models/bodygraph.ts` | `createBodygraph(name, dateUtc, location)` | Computes HD chart from UTC Date + location string | | `descriptions/descriptionDb.ts` | `createSavedChart({id, userId, name, chartJson, chartFingerprint})` | Persists chart to SQLite | | `descriptions/descriptionDb.ts` | `listSavedChartsForUser(userId)` | Check for existing charts (dedup) | | `interpretation/chartFingerprint.ts` | `computeChartFingerprint(bodygraphObj)` | SHA256 hash for caching | ## Import Script Pattern Script must live inside `apps/api/` for relative imports to resolve. Run with `npx tsx`: ```bash cd apps/api npx tsx scripts/import-.ts [json-path] [userId] [--dry-run] ``` ### Template ```typescript import * as fs from "fs"; import * as crypto from "crypto"; import { DateTime } from "luxon"; import { createBodygraph } from "../src/models/bodygraph"; import { createSavedChart, listSavedChartsForUser } from "../src/descriptions/descriptionDb"; import { computeChartFingerprint } from "../src/interpretation/chartFingerprint"; const DEFAULT_USER_ID = "e05b2131-3833-4056-b7db-3dc6850da5c2"; // firemountain@gmail.com // Suppress noisy Swiss Eph console.log const originalLog = console.log; function suppressLog() { console.log = (...args: any[]) => { const msg = args.join(" "); if (/^[✅⏭️⚠️❌🔮📋📁✨⏳]/.test(msg)) originalLog(...args); }; } function main() { const dryRun = process.argv.includes("--dry-run"); const userId = /* from args or default */; // 1. Load source data // 2. Filter to Persons with birth data // 3. Parse coordinates, dates, timezones from source format // 4. Convert local time + IANA timezone -> UTC via Luxon // 5. Call createBodygraph(name, dateUtc, locationString) // 6. Attach timezoneUsed + birthDateTimeUTC to bodygraph // 7. Call createSavedChart() with fingerprint // 8. Deduplicate by name (case-insensitive) against listSavedChartsForUser() suppressLog(); for (const record of records) { const dtLocal = DateTime.local(y, m, d, h, min, { zone: tz }); const dateUtc = dtLocal.toJSDate(); const bg = createBodygraph(name, dateUtc, locationStr); (bg as any).timezoneUsed = tz; (bg as any).birthDateTimeUTC = dateUtc.toISOString(); const fp = computeChartFingerprint(bg as any); if (!dryRun) { createSavedChart({ id: crypto.randomUUID(), userId, name, chartJson: JSON.stringify(bg), chartFingerprint: fp, }); } } console.log = originalLog; } ``` ## Coordinate Parsing Time Nomad uses `"33.8650 S, 151.2094 E"` format: ```typescript const match = coord.match(/([\d.]+)\xb0\s*([NS])\s*,\s*([\d.]+)\xb0\s*([EW])/i); const lat = parseFloat(match[1]) * (match[2] === "S" ? -1 : 1); const lon = parseFloat(match[3]) * (match[4] === "W" ? -1 : 1); ``` ## Pitfalls - **`getDb()` is NOT exported** - use `listSavedChartsForUser()` instead for dedup checks - **Swiss Eph is extremely noisy** - suppress console.log, only pass through your own prefixed messages - **`createBodygraph` takes UTC Date**, not local - always convert via `DateTime.local(..., { zone: tz }).toJSDate()` - **Location string is display-only** - `createBodygraph(name, dateUtc, location)` only uses `location` for display; the actual astrology is computed from the UTC date - **`tsx` is available** on the VPS - no need for `ts-node` or compilation - **SQLite uses single quotes** for string literals, not double quotes - **Duplicate names**: existing charts from manual creation will be skipped by name dedup - **Timezone fallback**: some Time Nomad records lack `dateTimeZone` - fall back to `currentGeoTimeZone`