postgreSQL integration
This commit is contained in:
parent
e61ddaa8b0
commit
057c2ca5d1
7 changed files with 251 additions and 59 deletions
|
|
@ -2,7 +2,11 @@
|
||||||
PORT=3000
|
PORT=3000
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
# Database & Data Persistence Path
|
# PostgreSQL Connection String (Coolify / Managed Postgres / Docker)
|
||||||
|
DATABASE_URL=postgresql://postgres:postgres_secure_pass_2026@postgres:5432/balance_sheet_db
|
||||||
|
POSTGRES_PASSWORD=postgres_secure_pass_2026
|
||||||
|
|
||||||
|
# Fallback File Storage Path (Used if DATABASE_URL is omitted or offline)
|
||||||
DATA_PATH=/app/data/portal-data.json
|
DATA_PATH=/app/data/portal-data.json
|
||||||
|
|
||||||
# Optional SMTP Email Configuration (If using real SMTP server)
|
# Optional SMTP Email Configuration (If using real SMTP server)
|
||||||
|
|
|
||||||
94
COOLIFY.md
94
COOLIFY.md
|
|
@ -1,69 +1,53 @@
|
||||||
# Deployment Guide for Coolify
|
# Coolify & PostgreSQL Deployment Guide
|
||||||
|
|
||||||
This repository is optimized for deployment on **Coolify** using Docker or Docker Compose.
|
This application consists of:
|
||||||
|
1. **Server Side**: Node.js + Express backend (`server.ts`) serving REST APIs, managing authentication, ledger reconciliation, audit logs, FX rates, and financial reports.
|
||||||
|
2. **Database**: PostgreSQL (via `pg` connection pool) with automatic JSON fall-back backup.
|
||||||
|
3. **Frontend**: React SPA served directly via Express in production.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Deployment Options in Coolify
|
## 1. Quick Coolify Deployment Options
|
||||||
|
|
||||||
### Option A: Deployment via Git Repository (Recommended)
|
### Option A: Docker Compose Deployment (Application + PostgreSQL in One Stack)
|
||||||
|
|
||||||
1. **Push Code to Git**: Push this repository to GitHub, GitLab, or your self-hosted Git service.
|
1. **In Coolify Dashboard**:
|
||||||
2. **Add New Resource in Coolify**:
|
|
||||||
- Go to your Coolify dashboard.
|
|
||||||
- Click **+ Add Resource** -> **Public Repository** or **Private Repository**.
|
|
||||||
- Paste your repository URL and select the `main` branch.
|
|
||||||
3. **Select Build Pack**:
|
|
||||||
- Select **Dockerfile**. Coolify will automatically detect the `Dockerfile` in the root directory.
|
|
||||||
4. **Configure Port & Network**:
|
|
||||||
- **Port**: Set `3000`.
|
|
||||||
5. **Configure Persistent Volume**:
|
|
||||||
- Under **Storage / Volumes**, add a persistent volume mapping to prevent data loss on container redeployments:
|
|
||||||
- **Destination Path**: `/app/data`
|
|
||||||
6. **Deploy**:
|
|
||||||
- Click **Deploy**. Coolify will build the multi-stage Docker image and start your application.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Option B: Deployment via Docker Compose
|
|
||||||
|
|
||||||
1. **Add New Resource in Coolify**:
|
|
||||||
- Click **+ Add Resource** -> **Docker Compose**.
|
- Click **+ Add Resource** -> **Docker Compose**.
|
||||||
2. **Source Code**:
|
- Point to your Git Repository or paste the contents of `docker-compose.yml`.
|
||||||
- Select your Git Repository or paste the contents of `docker-compose.yml`.
|
2. **Environment Variables**:
|
||||||
3. **Environment Variables**:
|
Add the following in Coolify:
|
||||||
- Add the following environment variables in Coolify UI:
|
```env
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
POSTGRES_PASSWORD=your_custom_secure_password
|
||||||
|
```
|
||||||
|
3. **Deploy**:
|
||||||
|
- Coolify will build the app container and spin up a dedicated `postgres:16-alpine` database container with health checks and persistent volume storage (`balance_sheet_postgres_data`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option B: Coolify Managed PostgreSQL + Standalone App Container
|
||||||
|
|
||||||
|
If you prefer using Coolify's built-in managed PostgreSQL database resource:
|
||||||
|
|
||||||
|
1. **Create Database in Coolify**:
|
||||||
|
- Click **+ Add Resource** -> **PostgreSQL**.
|
||||||
|
- Coolify will provide an internal database connection URL (e.g., `postgresql://postgres:pass@coolify-postgres-host:5432/balance_sheet_db`).
|
||||||
|
2. **Deploy App Service**:
|
||||||
|
- Add your Git repo as a **Dockerfile** application.
|
||||||
|
- Set environment variable in Coolify UI:
|
||||||
```env
|
```env
|
||||||
NODE_ENV=production
|
DATABASE_URL=postgresql://postgres:pass@coolify-postgres-host:5432/balance_sheet_db
|
||||||
PORT=3000
|
PORT=3000
|
||||||
DATA_PATH=/app/data/portal-data.json
|
|
||||||
```
|
```
|
||||||
4. **Deploy**:
|
3. **Deploy**:
|
||||||
- Click **Deploy**. Coolify will spin up the container and attach the `portal_data` volume automatically.
|
- The Express server will automatically connect to the PostgreSQL database, create the necessary table structures on boot, and store all portal state in PostgreSQL.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Local Testing with Docker
|
## 2. Server-Side Architecture Details
|
||||||
|
|
||||||
To build and test locally before deploying to Coolify:
|
- **Automatic Table Creation**: On server boot, the backend runs `initPgDatabase()` to verify/create `app_portal_state`.
|
||||||
|
- **Automatic State Recovery**: If PostgreSQL contains existing data, the portal state is loaded directly from PostgreSQL. If empty, baseline seed data is written to PostgreSQL.
|
||||||
### Using Docker Compose:
|
- **Resilient Fallback**: If `DATABASE_URL` is omitted, the app gracefully operates using persistent JSON storage (`/app/data/portal-data.json`).
|
||||||
```bash
|
- **Health Check**: Express exposes `GET /api/health` for Docker container monitoring.
|
||||||
docker-compose up --build -d
|
|
||||||
```
|
|
||||||
Access the app at: `http://localhost:3000`
|
|
||||||
|
|
||||||
### Using Docker directly:
|
|
||||||
```bash
|
|
||||||
docker build -t balance-sheet-portal .
|
|
||||||
docker run -p 3000:3000 -v portal_data:/app/data balance-sheet-portal
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Health Check Endpoint
|
|
||||||
|
|
||||||
The container includes a built-in health check targeting:
|
|
||||||
`GET /api/health`
|
|
||||||
|
|
||||||
It returns `{"status": "ok"}` when the Express server and Vite static engine are healthy.
|
|
||||||
|
|
|
||||||
32
bun.lock
32
bun.lock
|
|
@ -7,6 +7,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^2.4.0",
|
"@google/genai": "^2.4.0",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@types/pg": "^8.20.4",
|
||||||
"@types/three": "^0.185.4",
|
"@types/three": "^0.185.4",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
|
@ -14,6 +15,7 @@
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
"nodemailer": "^9.0.4",
|
"nodemailer": "^9.0.4",
|
||||||
|
"pg": "^8.22.0",
|
||||||
"react": "^19.0.1",
|
"react": "^19.0.1",
|
||||||
"react-dom": "^19.0.1",
|
"react-dom": "^19.0.1",
|
||||||
"recharts": "^3.10.1",
|
"recharts": "^3.10.1",
|
||||||
|
|
@ -292,6 +294,8 @@
|
||||||
|
|
||||||
"@types/nodemailer": ["@types/nodemailer@8.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw=="],
|
"@types/nodemailer": ["@types/nodemailer@8.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw=="],
|
||||||
|
|
||||||
|
"@types/pg": ["@types/pg@8.20.4", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-Jz7UDOlIiFJuacC0TlBoLyNtmwlA/wpIyPDd3tvUqlRM+HzkWy2xUgpFpaXtbfTAFF6sIGq5lsCDBdJnhky1Xg=="],
|
||||||
|
|
||||||
"@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
|
"@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
|
||||||
|
|
||||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||||
|
|
@ -572,6 +576,22 @@
|
||||||
|
|
||||||
"path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
|
"path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
|
||||||
|
|
||||||
|
"pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="],
|
||||||
|
|
||||||
|
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
|
||||||
|
|
||||||
|
"pg-connection-string": ["pg-connection-string@2.14.0", "", {}, "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg=="],
|
||||||
|
|
||||||
|
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||||
|
|
||||||
|
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
|
||||||
|
|
||||||
|
"pg-protocol": ["pg-protocol@1.15.0", "", {}, "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ=="],
|
||||||
|
|
||||||
|
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||||
|
|
||||||
|
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||||
|
|
@ -580,6 +600,14 @@
|
||||||
|
|
||||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
|
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
|
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||||
|
|
||||||
|
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||||
|
|
||||||
|
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||||
|
|
||||||
"protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
|
"protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
|
||||||
|
|
||||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
@ -636,6 +664,8 @@
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||||
|
|
@ -678,6 +708,8 @@
|
||||||
|
|
||||||
"ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="],
|
"ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,26 @@
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
# PostgreSQL Database Service
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: balance-sheet-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: balance_sheet_db
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_secure_pass_2026}
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_db_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d balance_sheet_db"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Express + React Application Web Service
|
||||||
balance-sheet-portal:
|
balance-sheet-portal:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
|
@ -8,11 +28,15 @@ services:
|
||||||
image: balance-sheet-portal:latest
|
image: balance-sheet-portal:latest
|
||||||
container_name: balance-sheet-portal
|
container_name: balance-sheet-portal
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
expose:
|
depends_on:
|
||||||
- "3000"
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "${PORT:-3000}:3000"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
|
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-postgres_secure_pass_2026}@postgres:5432/balance_sheet_db
|
||||||
- DATA_PATH=/app/data/portal-data.json
|
- DATA_PATH=/app/data/portal-data.json
|
||||||
volumes:
|
volumes:
|
||||||
- portal_data:/app/data
|
- portal_data:/app/data
|
||||||
|
|
@ -27,3 +51,6 @@ volumes:
|
||||||
portal_data:
|
portal_data:
|
||||||
name: balance_sheet_portal_data
|
name: balance_sheet_portal_data
|
||||||
driver: local
|
driver: local
|
||||||
|
postgres_db_data:
|
||||||
|
name: balance_sheet_postgres_data
|
||||||
|
driver: local
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^2.4.0",
|
"@google/genai": "^2.4.0",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@types/pg": "^8.20.4",
|
||||||
"@types/three": "^0.185.4",
|
"@types/three": "^0.185.4",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
"nodemailer": "^9.0.4",
|
"nodemailer": "^9.0.4",
|
||||||
|
"pg": "^8.22.0",
|
||||||
"react": "^19.0.1",
|
"react": "^19.0.1",
|
||||||
"react-dom": "^19.0.1",
|
"react-dom": "^19.0.1",
|
||||||
"recharts": "^3.10.1",
|
"recharts": "^3.10.1",
|
||||||
|
|
|
||||||
23
server.ts
23
server.ts
|
|
@ -38,6 +38,7 @@ import {
|
||||||
MakerCheckerStatus
|
MakerCheckerStatus
|
||||||
} from './src/types.js';
|
} from './src/types.js';
|
||||||
import { AccountingEngine, DEFAULT_FX_RATES } from './src/services/accountingEngine.js';
|
import { AccountingEngine, DEFAULT_FX_RATES } from './src/services/accountingEngine.js';
|
||||||
|
import { initPgDatabase, loadStateFromPg, saveStateToPg } from './src/db/postgres.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
||||||
|
|
@ -45,7 +46,7 @@ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// In-Memory Database / Persistent File Storage
|
// In-Memory Database / Persistent File Storage
|
||||||
interface DB {
|
export interface DB {
|
||||||
users: User[];
|
users: User[];
|
||||||
submissions: Record<string, Record<BranchId, BranchSubmission>>; // period -> branchId -> submission
|
submissions: Record<string, Record<BranchId, BranchSubmission>>; // period -> branchId -> submission
|
||||||
smtpConfig: SmtpConfig;
|
smtpConfig: SmtpConfig;
|
||||||
|
|
@ -186,10 +187,30 @@ function saveDB(data: DB) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving portal-data.json:', err);
|
console.error('Error saving portal-data.json:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Asynchronously push state to PostgreSQL if configured
|
||||||
|
saveStateToPg(data).catch((err) => {
|
||||||
|
console.error('Error syncing state to PostgreSQL:', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let db = loadDB();
|
let db = loadDB();
|
||||||
|
|
||||||
|
// Initialize PostgreSQL if DATABASE_URL or POSTGRES_HOST is present
|
||||||
|
(async () => {
|
||||||
|
const pgReady = await initPgDatabase();
|
||||||
|
if (pgReady) {
|
||||||
|
const pgState = await loadStateFromPg();
|
||||||
|
if (pgState) {
|
||||||
|
db = pgState;
|
||||||
|
console.log('[PostgreSQL] DB state successfully synced from PostgreSQL database.');
|
||||||
|
} else {
|
||||||
|
console.log('[PostgreSQL] Initializing PostgreSQL database with baseline state...');
|
||||||
|
await saveStateToPg(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// AUTH ENDPOINTS
|
// AUTH ENDPOINTS
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
|
|
|
||||||
122
src/db/postgres.ts
Normal file
122
src/db/postgres.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import pg from 'pg';
|
||||||
|
import type { DB } from '../../server';
|
||||||
|
|
||||||
|
const { Pool } = pg;
|
||||||
|
|
||||||
|
let pool: pg.Pool | null = null;
|
||||||
|
|
||||||
|
export function getPgPool(): pg.Pool | null {
|
||||||
|
if (pool) return pool;
|
||||||
|
|
||||||
|
const connectionString =
|
||||||
|
process.env.DATABASE_URL ||
|
||||||
|
process.env.POSTGRES_URL ||
|
||||||
|
process.env.DATABASE_PRIVATE_URL;
|
||||||
|
|
||||||
|
const host = process.env.POSTGRES_HOST || process.env.SQL_HOST;
|
||||||
|
const user = process.env.POSTGRES_USER || process.env.SQL_USER;
|
||||||
|
const password = process.env.POSTGRES_PASSWORD || process.env.SQL_PASSWORD;
|
||||||
|
const database = process.env.POSTGRES_DB || process.env.SQL_DB_NAME || 'balance_sheet_db';
|
||||||
|
const port = parseInt(process.env.POSTGRES_PORT || '5432', 10);
|
||||||
|
|
||||||
|
if (connectionString) {
|
||||||
|
console.log('[PostgreSQL] Initializing connection pool via DATABASE_URL');
|
||||||
|
pool = new Pool({
|
||||||
|
connectionString,
|
||||||
|
max: 10,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
connectionTimeoutMillis: 5000,
|
||||||
|
});
|
||||||
|
} else if (host && user) {
|
||||||
|
console.log(`[PostgreSQL] Initializing connection pool via host ${host}:${port}, db: ${database}`);
|
||||||
|
pool = new Pool({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database,
|
||||||
|
max: 10,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
connectionTimeoutMillis: 5000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[PostgreSQL] No DATABASE_URL or POSTGRES_HOST provided. Operating in file-backed mode.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.on('error', (err) => {
|
||||||
|
console.error('[PostgreSQL] Unexpected idle client error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes PostgreSQL schema table if needed
|
||||||
|
*/
|
||||||
|
export async function initPgDatabase(): Promise<boolean> {
|
||||||
|
const p = getPgPool();
|
||||||
|
if (!p) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await p.connect();
|
||||||
|
try {
|
||||||
|
await client.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS app_portal_state (
|
||||||
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
|
data JSONB NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
console.log('[PostgreSQL] Database table app_portal_state ensured.');
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PostgreSQL] Initialization failed:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load state from PostgreSQL
|
||||||
|
*/
|
||||||
|
export async function loadStateFromPg(): Promise<DB | null> {
|
||||||
|
const p = getPgPool();
|
||||||
|
if (!p) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await p.query(`SELECT data FROM app_portal_state WHERE id = $1`, ['main_state']);
|
||||||
|
if (res.rows.length > 0 && res.rows[0].data) {
|
||||||
|
console.log('[PostgreSQL] Successfully restored portal state from PostgreSQL database.');
|
||||||
|
return res.rows[0].data as DB;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PostgreSQL] Error loading state:', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save state to PostgreSQL asynchronously
|
||||||
|
*/
|
||||||
|
export async function saveStateToPg(state: DB): Promise<void> {
|
||||||
|
const p = getPgPool();
|
||||||
|
if (!p) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await p.query(
|
||||||
|
`
|
||||||
|
INSERT INTO app_portal_state (id, data, updated_at)
|
||||||
|
VALUES ($1, $2, NOW())
|
||||||
|
ON CONFLICT (id) DO UPDATE
|
||||||
|
SET data = EXCLUDED.data, updated_at = NOW();
|
||||||
|
`,
|
||||||
|
['main_state', JSON.stringify(state)]
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PostgreSQL] Error saving state:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue