Docker Commit
This commit is contained in:
parent
541f2ac0c0
commit
76e0eb782d
7 changed files with 178 additions and 70 deletions
12
.dockerignore
Normal file
12
.dockerignore
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
portal-data.json
|
||||
.env
|
||||
.env.local
|
||||
20
.env.example
20
.env.example
|
|
@ -1,9 +1,13 @@
|
|||
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||
# AI Studio automatically injects this at runtime from user secrets.
|
||||
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||
# Server Configuration
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
|
||||
# APP_URL: The URL where this applet is hosted.
|
||||
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||
APP_URL="MY_APP_URL"
|
||||
# Database & Data Persistence Path
|
||||
DATA_PATH=/app/data/portal-data.json
|
||||
|
||||
# Optional SMTP Email Configuration (If using real SMTP server)
|
||||
# SMTP_HOST=smtp.gmail.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USER=your-email@domain.com
|
||||
# SMTP_PASS=your-app-password
|
||||
# SMTP_FROM="Balance Sheet Portal <noreply@domain.com>"
|
||||
|
|
|
|||
69
COOLIFY.md
Normal file
69
COOLIFY.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Deployment Guide for Coolify
|
||||
|
||||
This repository is optimized for deployment on **Coolify** using Docker or Docker Compose.
|
||||
|
||||
---
|
||||
|
||||
## Quick Deployment Options in Coolify
|
||||
|
||||
### Option A: Deployment via Git Repository (Recommended)
|
||||
|
||||
1. **Push Code to Git**: Push this repository to GitHub, GitLab, or your self-hosted Git service.
|
||||
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**.
|
||||
2. **Source Code**:
|
||||
- Select your Git Repository or paste the contents of `docker-compose.yml`.
|
||||
3. **Environment Variables**:
|
||||
- Add the following environment variables in Coolify UI:
|
||||
```env
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
DATA_PATH=/app/data/portal-data.json
|
||||
```
|
||||
4. **Deploy**:
|
||||
- Click **Deploy**. Coolify will spin up the container and attach the `portal_data` volume automatically.
|
||||
|
||||
---
|
||||
|
||||
## Local Testing with Docker
|
||||
|
||||
To build and test locally before deploying to Coolify:
|
||||
|
||||
### Using Docker Compose:
|
||||
```bash
|
||||
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.
|
||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
balance-sheet-portal:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: balance-sheet-portal:latest
|
||||
container_name: balance-sheet-portal
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-3000}:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DATA_PATH=/app/data/portal-data.json
|
||||
volumes:
|
||||
- portal_data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
portal_data:
|
||||
name: balance_sheet_portal_data
|
||||
driver: local
|
||||
|
|
@ -40,7 +40,7 @@ import {
|
|||
import { AccountingEngine, DEFAULT_FX_RATES } from './src/services/accountingEngine.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
|
|
@ -62,7 +62,8 @@ interface DB {
|
|||
pkrRates?: Record<string, number>;
|
||||
}
|
||||
|
||||
const dbPath = path.join(process.cwd(), 'portal-data.json');
|
||||
const dataDir = process.env.DATA_DIR || process.cwd();
|
||||
const dbPath = process.env.DATA_PATH || path.join(dataDir, 'portal-data.json');
|
||||
|
||||
const DEFAULT_PKR_RATES: Record<string, number> = {
|
||||
USDPKR: 278.16,
|
||||
|
|
@ -177,6 +178,10 @@ function loadDB(): DB {
|
|||
|
||||
function saveDB(data: DB) {
|
||||
try {
|
||||
const parentDir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(parentDir)) {
|
||||
fs.mkdirSync(parentDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(dbPath, JSON.stringify(data, null, 2));
|
||||
} catch (err) {
|
||||
console.error('Error saving portal-data.json:', err);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,11 @@ export default function App() {
|
|||
}
|
||||
};
|
||||
|
||||
// Handle user logout
|
||||
const handleLogout = () => {
|
||||
setIsAuthModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-screen w-screen bg-slate-950 text-slate-100 flex flex-col md:flex-row overflow-hidden font-sans antialiased selection:bg-cyan-500 selection:text-black">
|
||||
{/* 3D WebGL Holographic Particle Canvas Background */}
|
||||
|
|
@ -90,6 +95,7 @@ export default function App() {
|
|||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
allUsers={allUsers}
|
||||
onLogout={handleLogout}
|
||||
onOpenAuthModal={() => setIsAuthModalOpen(true)}
|
||||
/>
|
||||
|
||||
|
|
@ -105,7 +111,7 @@ export default function App() {
|
|||
setActivePeriod={setActivePeriod}
|
||||
periodsList={periodsList}
|
||||
allUsers={allUsers}
|
||||
onResetSeed={handleResetSeed}
|
||||
onLogout={handleLogout}
|
||||
onOpenAuthModal={() => setIsAuthModalOpen(true)}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ import {
|
|||
PanelLeftOpen,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Key
|
||||
Key,
|
||||
LogOut
|
||||
} from 'lucide-react';
|
||||
import { User, BranchId, BRANCHES_LIST } from '../types';
|
||||
|
||||
|
|
@ -38,7 +39,8 @@ interface NavigationProps {
|
|||
setActivePeriod: (period: string) => void;
|
||||
periodsList: string[];
|
||||
allUsers: User[];
|
||||
onResetSeed: () => void;
|
||||
onLogout?: () => void;
|
||||
onResetSeed?: () => void;
|
||||
onOpenAuthModal?: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +51,7 @@ export const Sidebar: React.FC<Omit<NavigationProps, 'activePeriod' | 'setActive
|
|||
activeTab,
|
||||
setActiveTab,
|
||||
allUsers,
|
||||
onLogout,
|
||||
onOpenAuthModal
|
||||
}) => {
|
||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||
|
|
@ -315,16 +318,11 @@ export const Sidebar: React.FC<Omit<NavigationProps, 'activePeriod' | 'setActive
|
|||
</nav>
|
||||
|
||||
{/* Sidebar Footer User Profile */}
|
||||
<div className="p-2 border-t border-cyan-500/20 bg-slate-950/90 relative shrink-0">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUserMenu(false);
|
||||
onOpenAuthModal?.();
|
||||
}}
|
||||
<div className="p-2 border-t border-cyan-500/20 bg-slate-950/90 relative shrink-0 space-y-1.5">
|
||||
<div
|
||||
className={`w-full flex items-center ${
|
||||
isCollapsed ? 'justify-center p-2' : 'justify-between p-2'
|
||||
} rounded-xl bg-slate-900/60 border border-cyan-500/30 hover:border-cyan-400/60 transition-all text-left cursor-pointer shadow-sm`}
|
||||
title={`Active Session: ${currentUser.name} - Click to Authenticate Credentials`}
|
||||
} rounded-xl bg-slate-900/60 border border-cyan-500/30 text-left`}
|
||||
>
|
||||
<div className="flex items-center gap-2.5 overflow-hidden">
|
||||
<div className="w-8 h-8 rounded-full bg-cyan-500/20 border border-cyan-400/60 flex items-center justify-center text-cyan-300 font-bold text-xs shrink-0 shadow-[0_0_10px_rgba(6,182,212,0.4)]">
|
||||
|
|
@ -336,12 +334,24 @@ export const Sidebar: React.FC<Omit<NavigationProps, 'activePeriod' | 'setActive
|
|||
{currentUser.name}
|
||||
{isAdmin && <ShieldCheck className="w-3 h-3 text-cyan-400 shrink-0" />}
|
||||
</p>
|
||||
<p className="text-[10px] text-cyan-400 font-mono truncate font-semibold">Authenticate / Switch</p>
|
||||
<p className="text-[10px] text-cyan-400 font-mono truncate font-semibold">
|
||||
{isAdmin ? 'Head Office Admin' : currentBranch?.name || 'Branch User'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isCollapsed && <Key className="w-3.5 h-3.5 text-amber-400 shrink-0" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && onLogout && (
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="w-full py-2 px-3 rounded-xl bg-slate-900 hover:bg-rose-950/50 border border-rose-500/40 hover:border-rose-500/70 text-rose-300 hover:text-rose-100 text-xs font-bold flex items-center justify-center gap-2 transition-all cursor-pointer shadow-sm shadow-rose-950/20"
|
||||
title="Sign out of current banking session"
|
||||
>
|
||||
<LogOut className="w-3.5 h-3.5 text-rose-400" />
|
||||
<span>Sign Out</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
|
@ -357,6 +367,7 @@ export const HeaderBar: React.FC<NavigationProps> = ({
|
|||
setActivePeriod,
|
||||
periodsList,
|
||||
allUsers,
|
||||
onLogout,
|
||||
onResetSeed,
|
||||
onOpenAuthModal
|
||||
}) => {
|
||||
|
|
@ -439,25 +450,16 @@ export const HeaderBar: React.FC<NavigationProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{onOpenAuthModal && (
|
||||
{onLogout && (
|
||||
<button
|
||||
onClick={onOpenAuthModal}
|
||||
className="bg-gradient-to-r from-cyan-500/20 via-blue-500/20 to-indigo-500/20 hover:from-cyan-500/30 hover:to-indigo-500/30 text-cyan-300 text-xs px-2.5 py-1.5 rounded-lg font-bold border border-cyan-400/50 transition-all flex items-center gap-1.5 cursor-pointer shadow-[0_0_10px_rgba(6,182,212,0.2)]"
|
||||
title="Authenticate Branch Credentials or Switch User Persona"
|
||||
onClick={onLogout}
|
||||
className="bg-slate-900 hover:bg-rose-950/50 text-rose-300 hover:text-rose-200 text-xs px-3 py-1.5 rounded-lg font-bold border border-rose-500/40 hover:border-rose-500/70 transition-all flex items-center gap-1.5 cursor-pointer shadow-sm shadow-rose-950/20"
|
||||
title="Sign out of current active session"
|
||||
>
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-cyan-400" />
|
||||
<span className="hidden sm:inline">Auth Matrix</span>
|
||||
<LogOut className="w-3.5 h-3.5 text-rose-400" />
|
||||
<span className="hidden sm:inline">Sign Out</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onResetSeed}
|
||||
className="bg-slate-900 hover:bg-slate-800 text-cyan-300 hover:text-white text-xs px-2.5 py-1.5 rounded-lg font-semibold border border-cyan-500/30 transition-all flex items-center gap-1.5 cursor-pointer shadow-sm hover:shadow-[0_0_15px_rgba(6,182,212,0.3)]"
|
||||
title="Reset sample data back to baseline"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5 text-cyan-400" />
|
||||
<span className="hidden sm:inline">Reset Demo</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -582,38 +584,19 @@ export const HeaderBar: React.FC<NavigationProps> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick User Identity Switcher for Mobile */}
|
||||
{/* Sign Out Button for Mobile */}
|
||||
<div className="pt-2 border-t border-slate-800">
|
||||
<button
|
||||
onClick={() => setShowUserMenu(!showUserMenu)}
|
||||
className="w-full flex items-center justify-between p-2 bg-slate-900 rounded-lg border border-cyan-500/30 text-xs text-cyan-300"
|
||||
>
|
||||
<span>Simulate User: {currentUser.name}</span>
|
||||
<ChevronDown className="w-4 h-4 text-cyan-400" />
|
||||
</button>
|
||||
|
||||
{showUserMenu && (
|
||||
<div className="mt-2 p-2 bg-slate-900 rounded-lg border border-cyan-500/40 space-y-1 max-h-48 overflow-y-auto">
|
||||
{allUsers.filter((u) => u.approved).map((user) => (
|
||||
<button
|
||||
key={user.id}
|
||||
onClick={() => {
|
||||
setCurrentUser(user);
|
||||
setShowUserMenu(false);
|
||||
setMobileMenuOpen(false);
|
||||
if (user.role === 'branch_user') {
|
||||
setActiveTab('branch_input');
|
||||
}
|
||||
}}
|
||||
className={`w-full text-left p-1.5 rounded text-xs flex justify-between items-center ${
|
||||
user.id === currentUser.id ? 'bg-cyan-500/30 text-white font-bold' : 'text-slate-300 hover:bg-slate-800'
|
||||
}`}
|
||||
>
|
||||
<span>{user.name}</span>
|
||||
<span className="text-[9px] font-mono text-cyan-400">{user.role}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{onLogout && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setMobileMenuOpen(false);
|
||||
onLogout();
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-2 p-2.5 bg-slate-900 hover:bg-rose-950/60 border border-rose-500/40 text-rose-300 text-xs font-bold rounded-xl transition-all cursor-pointer"
|
||||
>
|
||||
<LogOut className="w-4 h-4 text-rose-400" />
|
||||
<span>Sign Out / Logout</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue