Replaces the hardcoded version string in SettingsPage.tsx with a single
source of truth: a /VERSION file at the repo root. To cut a new release,
edit only that one file.
Frontend:
- vite.config.ts reads /VERSION at dev-server startup and injects it as
the __APP_VERSION__ global via Vite's define mechanism (compile-time,
zero runtime cost). Falls back to VITE_APP_VERSION env var for prod
Docker builds that pass it as a build arg.
- vite-env.d.ts adds the TypeScript declaration for __APP_VERSION__.
- SettingsPage.tsx Badge now renders {__APP_VERSION__} instead of the
literal string.
Backend:
- app.controller.ts reads /VERSION once at module load and includes
"version" in both GET /api and GET /api/health responses.
- NewRelicTransactionInterceptor tags every NR transaction with
newrelic.addCustomAttribute('appVersion', version) so releases can be
compared in NRQL: SELECT average(duration) FROM Transaction WHERE
appVersion = '2026.5.22'
Docker:
- docker-compose.yml mounts ./VERSION:/app/VERSION:ro in both backend
and frontend dev containers.
- Production Dockerfiles include COPY VERSION ./ with a comment
instructing CI to copy the root VERSION into each build context before
docker build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Issue 2 fix made the opening investment balance point-in-time
(only CDs purchased before startYear-01-01), with a comment promising
that later purchases would be re-added "when their purchase month is
processed in the forecast loop" — but that loop code never existed.
The loop only ever subtracted maturing CDs, never added purchased ones.
Result: every CD bought during the charted window vanished from the
chart. For Pine Creek (all 5 CDs purchased in 2026) the operating
investment line showed $0 instead of $65,000 and reserve showed
$10,000 instead of $60,032.
Fix: build a purchaseIndex (mirroring maturityIndex) of investments
purchased on/after startYear-01-01, keyed by purchase year-month, and
credit each CD's value to the running investment balance in its
purchase month — applied before the historical/forecast branch so it
works for both actual and projected months.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three root causes addressed:
1. nginx routing gap — bare GET /api (no trailing slash) fell through
`location /api/` to the Vite dev proxy, which forwarded it to the
backend as an unmatched path. Added `location = /api` exact-match
block before the prefix block to catch it and proxy directly to
the backend health handler.
2. AppController root handler — added @Get() (maps to GET /api with
global prefix) so bare /api requests return a clean 200 instead of
a 404 that registers as a phantom NR transaction.
3. New Relic transaction naming — NestJS's setGlobalPrefix('api')
causes NR's Express instrumentation to bucket ALL requests into the
generic "Expressjs/GET/api$" segment, making per-endpoint APM data
completely useless. The new NewRelicTransactionInterceptor calls
newrelic.setTransactionName() with "METHOD /route/pattern" for
every request (after routing, so req.route is populated with the
matched template). Gracefully no-ops in dev where NR is not loaded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Monthly Actuals — Option A:
- Replace operating cash account offset with per-fund equity account clearing
- Equity accounts 3000/3100 now absorb the net P&L from actuals entries
- Cash account is never touched by monthly actuals, eliminating the balance
discrepancy that required manual cash adjustments
- Per-fund routing: operating income/expense clears to 3000, reserve to 3100
- Falls back gracefully if only one equity account exists
Scenario Activation (Issue 4):
- updateScenario now accepts userId and triggers materialisation when
status transitions to 'active'
- Each pending scenario investment is created as a real investment_accounts
record dated to its purchase_date (future dates are supported)
- Journal entries are posted at the purchase_date using the fund's primary
cash account and equity offset (matching manual account creation)
- Rollover detection: if an existing active investment matures within 7 days
of the new investment's purchase_date and shares the same fund_type, the
system creates a maturity JE (proceeds → cash) and a reinvestment JE
(cash → new CD) rather than a fresh cash deduction, then retires the
source investment
- Per-investment failures are logged but do not abort the rest of the batch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Monthly actuals grid now filters actual_amount to entry_type='monthly_actual'
only, so other posted JEs in the same month don't bleed into the actuals UI
- Remove manual accounts.balance reversal from void() — the reversal JE's post()
call already handles balance updates, preventing double-decrement per void
- Date void reversal entries to the original entry's date (not today) so
historical monthly cash-flow periods stay accurate when actuals are re-edited
- Cash flow forecast now derives opening investment balances from investments
purchased before the forecast start date rather than using current-snapshot
totals, fixing historical months showing wrong investment balances
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Step-by-step guide covering act_runner installation, registration,
host execution mode configuration, systemd service setup, and
troubleshooting for the HOALedgerIQ production deployment workflow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The health check used curl which is not installed on the prod server.
Replace with a dual approach:
1. Primary: check Docker's own container health status (already running
via docker-compose.prod.yml healthcheck with wget inside container)
2. Secondary: wget from host as fallback signal
Also add diagnostic logging (container status + recent backend logs)
before triggering rollback on health check failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The APPLIED_MIGRATIONS associative array triggered "unbound variable"
under set -u when empty (first run / seed-existing). Fix by initializing
with =() and using a safe helper function with ${:-} default syntax.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace bc-based floating point division with pure bash integer
arithmetic so the script works on minimal Ubuntu servers without
bc installed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add automated production deployment pipeline:
- scripts/deploy-prod.sh: Full deployment script with pre/post DB backups,
migration tracking via shared.schema_migrations table, health checks,
and automatic rollback on failure (restores DB, reverts code, rebuilds)
- .gitea/workflows/deploy.yml: Manual-trigger Gitea Actions workflow for
intentional production deployments with optional --seed-existing flag
- scripts/db-backup.sh: Add --yes/-y flag to skip interactive confirmation
prompts, enabling automated restore during rollback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces a capability layer on top of existing roles that controls
feature visibility and access. Capabilities follow an area.feature.action
taxonomy (~35 capabilities) with sensible defaults per role. Tenant admins
can customize via grant/revoke overrides stored in org settings JSONB.
Key changes:
- Add vice_president role to DB schema
- Backend: capability constants, resolution logic, CapabilityGuard (global),
@RequireCapability decorator on all 16 tenant controllers
- Frontend: permission hooks (useCanEdit, useHasCapability), CapabilityGate
component, sidebar filtering by capability, all 17 pages migrated from
useIsReadOnly to capability-based checks
- New admin UI: /settings/permissions matrix page for per-tenant role
customization with grant/revoke delta model
- GET /organizations/my-capabilities endpoint for capability refresh
- Validation of permissionOverrides in settings updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users entering the full endpoint URL (e.g. https://openrouter.ai/api/v1/chat/completions)
caused a 404 because the code appended /chat/completions again. Now strips any trailing
/chat/completions before re-appending, and adds a hint in the UI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new admin-only feature that allows the platform owner to benchmark
the production AI model against up to 2 alternate models (any OpenAI-compatible
API) using real tenant data, without impacting users.
Backend:
- Shared AI caller utility (ai-caller.ts) for OpenAI-compatible endpoints
- Shadow AI module with service, controller, and 3 entities
- 6 admin API endpoints for model config CRUD, run trigger, and history
- Auto-creates shadow_ai_models, shadow_runs, shadow_run_results tables
- Exposes health-scores and investment-planning prompt builders for reuse
Frontend:
- New admin page at /admin/shadow-ai with 3 tabs:
- Model Configuration (production + 2 alternate slots)
- Run Comparison (tenant select, feature select, side-by-side results)
- History (filterable run log with detail drill-down)
- Full side-by-side output display with diff highlighting
- Sidebar navigation link for AI Benchmarking
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a dedicated super admin page for managing idea submissions across
all tenants. Includes status summary cards, filterable/searchable table,
detail modal with status updates, and private admin notes for internal
tracking (sprint refs, thoughts, follow-ups). Notes are not visible to
tenant users.
- Database: admin_note column on shared.ideas (019 migration)
- Backend: PUT /admin/ideas/:id/note endpoint
- Frontend: AdminIdeasPage with table, filters, detail modal
- Sidebar: "Idea Submissions" nav link in admin sections
- Routing: /admin/ideas route under SuperAdminRoute guard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds idea submission capability gated by a per-tenant feature flag.
Super admins can enable/disable ideation for specific tenants via the
admin tenant detail drawer. Users see a lightbulb icon in the header
when enabled, opening a modal to submit ideas (title + description).
Ideas are stored in shared schema for cross-tenant backlog querying.
- Database: shared.ideas table (018-ideas.sql migration)
- Backend: Ideas NestJS module (entity, service, controller)
- Admin API: GET /admin/ideas, PUT /admin/ideas/:id/status,
PUT /admin/organizations/:id/settings
- Frontend: IdeaModal component, lightbulb ActionIcon in header
- Admin UI: Feature Toggles card with ideation Switch in drawer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remaining life years is documentation-only reference info. The board's
planned project date is the authoritative timeline for urgency assessment.
Updates data gathering, prompt construction, and system instructions to
base all urgency on target_year/target_month instead of remaining_life_years.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move useMemo hook above early returns to satisfy React Rules of Hooks,
fixing blank screen when navigating to scenario detail. Also re-fetch
scenario after projection updates so auto-renew renewal records appear
automatically without requiring manual navigation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Lock InvestmentTimeline and ProjectionChart to shared X axis range
- Auto-create renewal scenario_investments records when auto_renew is true
- Add fund transfer mechanism between asset accounts with journal entries
- Add Capital Planning Report (5-year forecast grouped by category)
- Add Upcoming Investment Activities dashboard card (maturities + planned purchases)
- Bump version to 2026.3.24
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Onboarding wizard: add Reserve Account step between Operating and Assessments,
redirect to Budget Planning on completion
- Dashboard: health score pending state shows clickable links to set up missing items
- Projects/Vendors: rich empty-state wizard screens with real-world examples and CTAs
- Investment Planning: auto-refresh AI recommendations when empty or stale (>30 days)
- Hide Invoices and Payments menus (see PARKING-LOT.md for re-enablement)
- Send welcome email via Resend when new members are added to a tenant
- Enforce 5-member limit for Starter/Standard/Professional plans (Enterprise unlimited)
- Cash flow forecast: only mark months as "Actual" when journal entries exist,
fixing the issue where months without data showed as actuals
- Bump version to 2026.3.19
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix "Manage Billing" button error for trial orgs without Stripe customer;
add fallback to retrieve customer from subscription, show helpful message
for trial users, and surface real error messages in the UI
- Add "Balance As-Of Date" field to onboarding wizard so opening balance
journal entries use the correct statement date instead of today
- Add "Total Unit Count" field to onboarding wizard assessment group step
so cash flow projections work immediately
- Remove broken budget upload step from onboarding wizard (was using legacy
budgets endpoint); replace with guidance to use Budget Planning page
- Replace bare "No budget plan lines" text with rich onboarding-style card
featuring download template and upload CSV action buttons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Interactive CLI for managing test organizations, users, and tenant schemas.
Supports list, delete-org, delete-user, purge-all, and reseed commands
with dry-run mode and safety guards for platform owner protection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add monthly/annual billing toggle with 25% annual discount on pricing page
- Implement 14-day no-card free trial (server-side Stripe subscription creation)
- Enable upgrade/downgrade via Stripe Customer Portal
- Add admin-initiated ACH/invoice billing for enterprise customers
- Add billing card to Settings page with plan info and Manage Billing button
- Handle past_due status with read-only grace period access
- Add trial ending and trial expired email templates
- Add DB migration for billing_interval and collection_method columns
- Update ONBOARDING-AND-AUTH.md documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the 4 summary cards from the Cash Flow page as they don't
properly represent the story over time. Increase gradient opacity
on stacked area charts (cash flow and investment scenarios) from
0.3-0.4/0-0.05 to 0.6/0.15 for better visual shading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the stubbed email service with Resend API integration.
Emails are sent with branded HTML templates including activation,
welcome, payment failed, member invite, and password reset flows.
- Install resend@6.9.4 in backend
- Rewrite EmailService with Resend SDK + graceful fallback to
stub mode when API key is not configured
- Add branded HTML email template with CTA buttons, preheader
text, and fallback URL for all email types
- Add reply-to support (sales@hoaledgeriq.com in production)
- Track send status (sent/failed) in shared.email_log metadata
- Add RESEND_API_KEY, RESEND_FROM_ADDRESS, RESEND_REPLY_TO env
vars to both docker-compose.yml and docker-compose.prod.yml
- Add sendPasswordResetEmail() method for future use
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
APP_URL was never passed to the backend container, causing Stripe
checkout success_url to redirect to http://localhost instead of the
production domain. The prod overlay also completely replaced the base
environment block, dropping all Stripe, SSO, WebAuthn, and invite
token variables.
- Add APP_URL to base docker-compose.yml (default: http://localhost)
- Add all missing vars to docker-compose.prod.yml with production
defaults (app.hoaledgeriq.com)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enterprise plan no longer displays a fixed price. Instead it shows
"Request Quote" and the CTA opens the interest form on hoaledgeriq.com
in a new tab to capture leads for custom quotes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- L2: Add server_tokens off to nginx configs to hide version
- M1: Add X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
Permissions-Policy headers to all nginx routes
- L3: Add global NoCacheInterceptor (Cache-Control: no-store) on all
API responses to prevent caching of sensitive financial data
- C1: Disable open registration by default (ALLOW_OPEN_REGISTRATION env)
- H3: Add logout endpoint with correct HTTP 200 status code
- M2: Implement full password reset flow (forgot-password, reset-password,
change-password) with hashed tokens, 15-min expiry, single-use
- Reduce JWT access token expiry from 24h to 1h
- Add EmailService stub (logs to shared.email_log)
- Add DB migration 016 for password_reset_tokens table
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>