--- name: hd-prism-app description: HD Prism app architecture, deployment patterns, and known gotchas for development on the VPS version: 1 --- # HD Prism App Development ## Architecture - **Next.js 16 client** (hd-prism-client): Port 3010, PM2 process `hd-prism-client` - Path: `/home/avalon/apps/hd-prism/apps/hdprism/` - Build: `cd /home/avalon/apps/hd-prism/apps/hdprism && npx next build` - Restart: `pm2 restart hd-prism-client` - **Express API** (hd-prism-api): Port 3005, PM2 process `hd-prism-api` - Path: `/home/avalon/apps/hd-prism/apps/api/` - TypeScript source in `src/`, compiled to `dist/` - **SQLite DB**: active API DB is usually `/home/avalon/apps/hd-prism/apps/api/descriptions.sqlite` because `descriptionDb.ts` resolves `descriptions.sqlite` from `process.cwd()` and the API runs from `apps/api` - **Domain**: `hdprism.apps.poofc.com` - **Reference**: `references/environment-and-chart-storage.md` — Environment calculation mapping, saved-chart/readings tables, and DB-path/storage notes ## Nginx Config - Server block in `/etc/nginx/sites-available/apps.poofc.com` - Main app is now served **without nginx basic auth** so the standalone PWA can launch correctly; rely on app-level auth instead - `/api/` proxies to port 3005 - `/_next/static/` must point at the live repo build output: `/home/avalon/apps/hd-prism/apps/hdprism/.next/static/` - `/share/` proxies to port 3010 and remains publicly accessible - `location /` proxies to port 3010 ### Adding public paths When adding any path that must be publicly accessible without auth (share links, webhooks, etc.), add a dedicated `location` block with `auth_basic off` BEFORE the catch-all `location /`. Example: ```nginx location /share/ { auth_basic off; proxy_pass http://127.0.0.1:3010; # ... standard proxy headers } ``` ## Known Gotchas 1. **Port mismatch**: The Next.js client runs on port 3010, NOT 3000. Always verify with `pm2 logs hd-prism-client` or `ss -tlnp`. 2. **Static files path must match the live build**: Nginx should serve `_next/static` from `/home/avalon/apps/hd-prism/apps/hdprism/.next/static/`. If it points at an old deploy directory, new builds will not actually reach users and PWA updates will appear stuck. 3. **PWA cannot sit behind nginx basic auth**: Standalone PWAs do not reliably carry HTTP Basic Auth. If `auth_basic` is enabled on `location /`, the installed app may open to a blank or broken screen. Keep nginx basic auth off for the main app and use app-level auth instead. 4. **Share links need public routing**: If nginx auth is ever reintroduced, `/share/` still needs a dedicated public location block or share pages will fail. 4. **URL sync**: The app uses `?chart=` query param to track the current chart in the URL. The `HdprismShell` component syncs `currentChartId` to the URL and reads it on initial load (priority: URL param > localStorage > most recent chart). 5. **Library/I Ching mandala uses app-consistent gate order**: If you add or edit the custom I Ching SVG view, keep `apps/hdprism/lib/ichingWheelData.js` `GATE_RING_ORDER` identical to `components/RaveMandala.tsx` gate order/direction. A visually plausible reverse order is WRONG for this app. 6. **I Ching build rings are not repeated 64 times**: The inner study rings must render only 4 / 8 / 16 / 32 visible figures, centered in their parent sectors. Do not repeat the 2/3/4/5-line stacks at every outer wedge. 7. **I Ching build logic is bottom-preserving, top-adding**: Each next ring keeps the previous ring as the LOWER lines and adds exactly one NEW TOP line. If the progression looks plausible but cannot be followed radially from inner to outer rings, the build order is wrong. 8. **Gate info panels must use King Wen line patterns, not radial wedge mapping**: The I Ching mandala ring helpers (`GATE_RING_ORDER`, `getRingFigure`) are correct for the radial/library view but can be WRONG for a gate detail card. If a gate panel shows the right gate title/glyph but the wrong six-line stack, use a direct King Wen mapping keyed by gate/hexagram number for the figure and keep the mandala helpers only for the wheel visualization. 10. **Environment uses Design South Node, not Design Sun**: When adding Human Design Environment to bodygraph JSON, UI, filters, or reports, compute it from `activations.Design.SouthNode` — `Color` selects the environment family and `Tone < 4` determines left/right subtype. A common bug is using `Design.Sun.Tone`; that produces wrong environment variants. See `references/environment-and-chart-storage.md`. ## Human Design Environment wiring - Existing source strings already live in the backend: - `apps/api/src/strings/environments.json` - `apps/api/src/strings/environmental-tones.json` - Preferred implementation pattern: 1. Derive a structured `environment` object from `bodygraph.activations.Design.SouthNode` 2. Attach it to the `/api/bodygraph` JSON response in the backend 3. Reuse that field in frontend result cards, saved-chart metadata, and PDF/report surfaces 4. For old saved charts, backfill on read from `chart_json.activations.Design.SouthNode` instead of forcing regeneration - The bottom-left variable arrow corresponds to `Design.SouthNode.Tone`; if the displayed variable and Environment orientation disagree, inspect that field first. ## Known Gotchas - Sidebar bottom row includes a `Library` button near Settings / Theme. - `Library` opens a library home view; `I Ching` opens a custom SVG study view. - Main files: - `apps/hdprism/components/LibraryView.tsx` - `apps/hdprism/components/IChingMandalaDiagram.tsx` - `apps/hdprism/lib/ichingWheelData.js` - `apps/hdprism/lib/ichingWheelData.test.mjs` - `ChartContext` view modes include `library` and `iching`. - Regression checks when editing this feature: - `node --test apps/hdprism/lib/ichingWheelData.test.mjs` - `cd apps/hdprism && npm run build` - `pm2 restart hd-prism-client` - Common pitfall: `Sidebar.tsx` already had a local single/compare list toggle state; avoid naming collisions with app-wide `viewMode` from `ChartContext` (use a distinct local name like `listViewMode`). 5. **Adding new app-level views**: If you add a new top-level view (e.g. `library`, `iching`), update `ChartContext.tsx` in three places: the `ViewMode` union, `scrollPositions`, and `visitedViews`/`resetScrollTracking`. `HdprismShell.tsx` also needs explicit `showXView` flags so existing chart/reading/transit surfaces are hidden when the new view is active. 6. **Sidebar naming collision**: `Sidebar.tsx` already has its own local single-vs-compare list toggle state. Do NOT reuse the name `viewMode` for that local state once you consume app-level `viewMode` from `ChartContext`; rename the sidebar-local state to something like `listViewMode` to avoid Next/TypeScript compile failures from duplicate identifiers. ## Reusable implementation pattern: add a library/reference section When adding a library-style reference area to HD Prism: 1. Create a dedicated component such as `components/LibraryView.tsx` for the landing screen and any subviews. 2. Add new app-level view modes in `ChartContext.tsx` (for example `library` and `iching`). 3. In `Sidebar.tsx`, add the new navigation button in the bottom row next to Settings / Theme and route it through `switchViewMode(...)`. 4. In `HdprismShell.tsx`, render the new view components near the top-level return and guard existing chart/compare/reading/transit sections with `!showLibraryView` / `!showIChingView` so the new screens fully replace the main surfaces. 5. Verify with: - `node --test apps/hdprism/lib/.test.mjs` for any pure helper logic - `npm run build` in `apps/hdprism` (lint currently has many pre-existing unrelated failures, so build is the reliable gate) - `pm2 restart hd-prism-client` 6. After restart, if browser automation still only shows `Loading...`, treat that as a possible stale client/runtime state and also hard-refresh or reopen the PWA before assuming the new UI is missing. 5. **Sidebar naming collision**: `components/Sidebar.tsx` already has a local view toggle for saved lists (`"single" | "compare"`). If you also pull `viewMode` from `ChartContext` to add app-level views (chart/reading/transits/library/etc.), DO NOT reuse the name `viewMode` for the local sidebar state or the Next build will fail with `the name "viewMode" is defined multiple times`. Rename the local state to something like `listViewMode` / `setListViewMode`. 6. **Verification workflow**: `npm run lint` currently reports many pre-existing repo-wide errors/warnings unrelated to most feature work. For targeted UI changes, use build as the primary gate (`cd apps/hdprism && npm run build`) after running any targeted tests you add, and only treat new errors in touched files as regressions you introduced. ## Key Components - `HdprismShell.tsx` — Main app shell, chart loading/saving, URL sync, single-vs-compare mode, library/I Ching routing, and reading/transit coordination. For most UI/flow bugs, inspect this file first. - `ChartContext.tsx` — Global chart state (currentChartId, viewMode, reading state) - `Sidebar.tsx` — Chart list with search, pill-style sort toggle (New/A-Z), filter drawer, info button per row (opens ChartInfoModal), type-based emoji icons (⚡👁🔥🪞), no date sublines - `DescriptionDrawer.tsx` — Gate/line/color/tone/base info panel. If a gate panel needs extra I Ching context, this is where to add it. - `ShareChartShell.tsx` — Shell for public share page (no auth required) - `app/share/c/[id]/page.tsx` — Public share route (single chart) - `app/share/x/[id]/page.tsx` — Public share route (comparison) - `apps/api/app.js` — Live Express bootstrap that mounts the compiled `dist/routes/*` handlers plus health/log/socket routes. When debugging production routing, check this file, not just the TypeScript route sources. - `app/api/**/route.ts` — Next.js proxy layer. Frontend components usually call same-origin `/api/*`; these route handlers forward to the backend via `BACKEND_URL` and pass cookies through. ### Gate panel hexagram figure pattern - To show the six-line I Ching figure for a gate inside `DescriptionDrawer.tsx`, reuse the existing wheel data instead of hardcoding another mapping. - Import from `apps/hdprism/lib/ichingWheelData.js`: - `GATE_RING_ORDER` - `getRingFigure(6, wedgeIndex)` - `HEXAGRAM_SYMBOLS` - Resolve the gate figure like this: 1. parse the active gate number from `activeEntity` 2. find its `wedgeIndex` with `GATE_RING_ORDER.indexOf(gate)` 3. call `getRingFigure(6, wedgeIndex)` - This keeps the drawer’s gate figure consistent with the Library / I Ching mandala mapping and avoids maintaining a second gate→hexagram table. - When rendering the six-line figure in a small card, reverse the bottom-up line array for top-down visual display. ## Frontend ↔ Backend Request Pattern - Browser components generally fetch same-origin Next routes such as `/api/charts`, `/api/comparisons`, `/api/shared/...`, `/api/auth/...`, or `/api/bodygraph`. - Those handlers live under `apps/hdprism/app/api/**/route.ts` and proxy to the Express backend using `BACKEND_URL` (default local dev fallback is `http://localhost:3001`). - This means a bug can live in three places: the React component, the Next proxy route, or the Express route. Check all three before assuming the backend is wrong. - The repo READMEs still mention older local-dev ports in places; for deployed VPS work, trust PM2 / nginx / live process config over README port references. ## Sharing System (added Apr 2026) - `is_public` column on `saved_charts` and `saved_comparisons` tables (INTEGER DEFAULT 0) - Public API routes (no auth, requires is_public=1): `GET /api/shared/charts/:id`, `GET /api/shared/comparisons/:id` - `GET /api/shared/charts/:id` returns the full saved chart JSON plus all persisted chart readings/sections for the chart fingerprint, so logged-out guests can read owner-generated readings without calling generation endpoints. - PUT /charts/:id and /comparisons/:id accept `isPublic` in body to toggle - Share button in AppHeader: toggles is_public, then uses Web Share API (iOS) or Clipboard API - ShareChartShell: renders Chart/Reading/Transits tabs for guests, creation actions gated; guest `InlineReadingView` is read-only and hydrated from the share payload. - Public single-chart shares should mirror the logged-in chart surface: render the bodygraph SVG AND `BodygraphResult` below it so guests see Profile, Authority, Definition, Environment, Variable, Incarnation Cross, centers, channels, gates, circuitry, and planet activations. Wire `DescriptionDrawer` from shared pages too, using a local selected-entity/open state and `useChartReadingForContext={false}` for read-only public context. - Share pages: `/share/c/[id]` (chart), `/share/x/[id]` (comparison) - Public share shells often render a loading/error state before the payload arrives. Keep all React hooks in `ShareChartShell` before conditional `loading`/`error` returns; putting a `useCallback`/hook after those returns causes production React error #310 (`Rendered more hooks than during the previous render`) once the share payload loads. - **BUG**: `/api/shared/` routes may 404 through nginx — verify nginx proxies ALL `/api/` subpaths to port 3005, not just specific routes. The Next.js app on 3010 may intercept first. ## Reading generation performance - Chart reading generation lives in `apps/api/src/routes/readings.ts` (`POST /api/readings/generate`). - This is the full LLM prose reading route. It persists to `readings` and `reading_sections` and emits SSE events; use it when Alex asks to actually “get the HD reading” for a chart. - If the chart already exists in `saved_charts`, pass its stored `chart_json` as `{ bodygraph, regenerate: false }` to `http://127.0.0.1:3005/api/readings/generate` rather than recalculating the chart. - The live app now generates all section-level LLM requests in parallel before synthesis/overview generation, then generates the three overview/synthesis calls in parallel from the completed section drafts. - In a real saved-chart run, the parallelized full reading completed 11/11 sections in about 88 seconds; still expect roughly 1–2 minutes depending on OpenRouter/model latency and credits. - Important streaming detail: because parallel `section_delta` events can arrive before final section completion, the backend emits empty section placeholders first. Keep that placeholder behavior if refactoring, or the current frontend handlers may drop deltas for sections not yet present in state. - Verification for reading-route changes: run `cd apps/api && npx tsc`, `cd apps/hdprism && npm run build`, restart `pm2 restart hd-prism-api`, then check `/api/health` and PM2 logs. A full live reading generation test may spend OpenRouter credits and can fail if credits are insufficient. - Do not confuse `POST /api/tenant/report` with the full reading generator: tenant/report is deterministic/instant-ish and returns report/fact-pack structure only, with no LLM prose and no OpenRouter spend. ## API Routes - Express API: `/api/charts`, `/api/charts/:id`, `/api/comparisons`, `/api/shared/charts/:id`, `/api/shared/comparisons/:id` - Tenant-safe shared API for Astral Hermes: mounted in `apps/api/app.js` as `/api/tenant`, source `apps/api/src/routes/tenantBodygraph.ts` - Public live base URL: `https://hdprism.apps.poofc.com/api/tenant` - Endpoints: - `GET /api/tenant/health` - `POST /api/tenant/bodygraph` → `{ ok, service, input, resolved, summary, bodygraph }` - `POST /api/tenant/fact-pack` → reusable LLM-safe chart facts: centers, channels, gates, activation stack, variables, split/bridge candidates, hanging/electromagnetic gates - `GET /api/tenant/reference/:entityType/:entityId` → HD Prism description outputs for gates, centers, authorities, profiles, variables, channels, circuitry, etc. - `POST /api/tenant/reference/batch` → same as above for up to 128 references - `POST /api/tenant/chart-query` → selected structured facts from a chart/bodygraph via `include: [...]` - `POST /api/tenant/report` → deterministic assembled Reading V1 plan/snippet context from the fact pack; no LLM call or PDF generation - `bodygraph`, `fact-pack`, `chart-query`, and `report` accept either birth data (`birthDate`, `birthTime`, `timeZone`/`location`) or an existing `bodygraph` object. - Uses `timeZone`/`utc` directly when supplied; otherwise resolves local birth date/time/location through `transit-list-demo` `/api/time/resolve`. - Does **not** generate PDFs; use tenant endpoints for tenant/tool calls instead of `/api/bodygraph`, which is still backed by the PDF-report route. - Regression test: `cd apps/api && npx tsc && node --test test/tenantBodygraph.test.mjs` - Session detail: `references/tenant-api-astral-integration.md` - Next.js API routes also exist under `/api/` — these proxy or supplement the Express API ## Bulk chart import workflow absorbed from `hd-prism-bulk-import` Use the existing app internals to insert charts directly without triggering reading generation. Key modules under `apps/api/src/`: - `models/bodygraph.ts` → `createBodygraph(name, dateUtc, location)` - `descriptions/descriptionDb.ts` → `createSavedChart(...)`, `listSavedChartsForUser(...)` - `interpretation/chartFingerprint.ts` → `computeChartFingerprint(...)` Pattern: 1. parse source birth records 2. convert local civil time + timezone to UTC before calling `createBodygraph` 3. attach provenance like timezone/UTC to the bodygraph JSON if useful 4. compute chart fingerprint 5. dedupe against existing saved charts 6. persist with `createSavedChart` Important lesson: readings are generated separately, so direct bodygraph/chart insertion is the safe path for bulk imports. ## Library / I Ching View (added Apr 2026) - Sidebar bottom row now includes a `Library` button next to Settings / Theme toggle. - Library opens a reference area with an `I Ching` section. - I Ching view renders `apps/hdprism/components/IChingMandalaDiagram.tsx`. - Data helpers live in `apps/hdprism/lib/ichingWheelData.js` with tests in `apps/hdprism/lib/ichingWheelData.test.mjs`. - `ChartContext` view modes were extended with `library` and `iching`; remember to update scroll bookkeeping when adding more custom views. ### Important implementation pitfall The inner build rings must NOT repeat symbols at all 64 outer wedge positions. Correct structure: - 2-line ring: exactly 4 visible figures - 3-line ring: exactly 8 visible figures - 4-line ring: exactly 16 visible figures - 5-line ring: exactly 32 visible figures - number ring: 64 positions - outer ring: 64 hexagrams The right approach is: - keep the outer 64-gate/hexagram wheel intact - center each inner-ring figure in its parent sector using helper placements (see `getDisplayRingPlacements()`) - do NOT render 64 repeated parent-pattern stacks for the 2/3/4/5-line rings ### Sidebar pitfall `Sidebar.tsx` already had local list state named `viewMode` for single vs compare chart lists. When adding app-level `viewMode` from `ChartContext`, this caused a build-breaking duplicate identifier. Rename the sidebar-local state (for example `listViewMode`) instead of reusing `viewMode`.