Compare commits
22 Commits
7dff5454a0
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cfc3899cd | |||
| 2b53a449d1 | |||
| ffdc896d33 | |||
| 5048c44de2 | |||
| 86fe7a8bf1 | |||
| dd41bb5a6a | |||
| 8c8536f668 | |||
| db3a86404a | |||
| d900d905de | |||
| 3d07301992 | |||
| f5be8d856d | |||
| 3bda68282e | |||
| 968dc74555 | |||
| eb2745dd5a | |||
| 1ff69f9328 | |||
| ef24af3302 | |||
| c2c188f09f | |||
| 254b7710d7 | |||
| dd063d5ac5 | |||
| 9a593b8b7c | |||
| 10ed0e46d8 | |||
| 55378f74e0 |
@@ -13,6 +13,7 @@ node_modules
|
|||||||
# Documentation
|
# Documentation
|
||||||
*.md
|
*.md
|
||||||
!README.md
|
!README.md
|
||||||
|
docs
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode
|
.vscode
|
||||||
@@ -49,6 +50,7 @@ docker-compose*.yml
|
|||||||
# CI/CD
|
# CI/CD
|
||||||
.github
|
.github
|
||||||
.gitlab-ci.yml
|
.gitlab-ci.yml
|
||||||
|
.gitea
|
||||||
|
|
||||||
# Scripts
|
# Scripts
|
||||||
scripts
|
scripts
|
||||||
|
|||||||
57
.gitea/workflows/docker-publish-dev.yaml
Normal file
57
.gitea/workflows/docker-publish-dev.yaml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: Build and Push Docker Image (Dev)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'client/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- 'docker-compose.dev.yml'
|
||||||
|
- 'package.json'
|
||||||
|
- '.gitea/workflows/docker-publish-dev.yaml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.app.flexinit.nl
|
||||||
|
IMAGE_NAME: oussamadouhou/ai-stack-deployer
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-dev:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: oussamadouhou
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=dev
|
||||||
|
type=sha,prefix=dev-
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Push Docker Image
|
name: Build and Push Docker Image (Production)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,8 +6,11 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- 'src/**'
|
- 'src/**'
|
||||||
|
- 'client/**'
|
||||||
- 'Dockerfile'
|
- 'Dockerfile'
|
||||||
- '.gitea/workflows/**'
|
- 'docker-compose.prod.yml'
|
||||||
|
- 'package.json'
|
||||||
|
- '.gitea/workflows/docker-publish-main.yaml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -15,7 +18,7 @@ env:
|
|||||||
IMAGE_NAME: oussamadouhou/ai-stack-deployer
|
IMAGE_NAME: oussamadouhou/ai-stack-deployer
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push-main:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -41,8 +44,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
type=raw,value=latest
|
||||||
type=sha,prefix=
|
type=sha,prefix=main-
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
57
.gitea/workflows/docker-publish-staging.yaml
Normal file
57
.gitea/workflows/docker-publish-staging.yaml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: Build and Push Docker Image (Staging)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- staging
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'client/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- 'docker-compose.staging.yml'
|
||||||
|
- 'package.json'
|
||||||
|
- '.gitea/workflows/docker-publish-staging.yaml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.app.flexinit.nl
|
||||||
|
IMAGE_NAME: oussamadouhou/ai-stack-deployer
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-staging:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: oussamadouhou
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=staging
|
||||||
|
type=sha,prefix=staging-
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -36,7 +36,7 @@ export const translations = {
|
|||||||
title: 'AI Stack Deployer',
|
title: 'AI Stack Deployer',
|
||||||
subtitle: 'Implementeer je persoonlijke AI in seconden',
|
subtitle: 'Implementeer je persoonlijke AI in seconden',
|
||||||
chooseStackName: 'Kies Je Stack Naam',
|
chooseStackName: 'Kies Je Stack Naam',
|
||||||
availableAt: 'Je zal AI-assistenten beschikbaar zijn op',
|
availableAt: 'Je AI-assistenten zal beschikbaar zijn op',
|
||||||
stackName: 'Stack Naam',
|
stackName: 'Stack Naam',
|
||||||
placeholder: 'bijv., Oussama',
|
placeholder: 'bijv., Oussama',
|
||||||
inputHint: '3-20 tekens, kleine letters, cijfers en koppeltekens',
|
inputHint: '3-20 tekens, kleine letters, cijfers en koppeltekens',
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function DeployPage() {
|
|||||||
const response = await fetch('/api/deploy', {
|
const response = await fetch('/api/deploy', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name, lang }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -115,17 +115,18 @@ export default function DeployPage() {
|
|||||||
animationSpeed={3}
|
animationSpeed={3}
|
||||||
containerClassName="bg-black"
|
containerClassName="bg-black"
|
||||||
colors={[[255, 255, 255], [255, 255, 255]]}
|
colors={[[255, 255, 255], [255, 255, 255]]}
|
||||||
dotSize={2}
|
dotSize={6}
|
||||||
|
showGradient={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_rgba(0,0,0,1)_0%,_transparent_100%)]" />
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_rgba(0,0,0,0.5)_0%,_transparent_60%)]" />
|
||||||
<div className="absolute top-0 left-0 right-0 h-1/3 bg-gradient-to-b from-black to-transparent" />
|
<div className="absolute top-0 left-0 right-0 h-1/3 bg-gradient-to-b from-black/80 to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LanguageSelector currentLang={lang} onLangChange={setLang} />
|
<LanguageSelector currentLang={lang} onLangChange={setLang} />
|
||||||
|
|
||||||
<div className="relative z-10 w-full max-w-[640px] p-4 md:p-8">
|
<div className="relative z-10 w-full max-w-2xl p-4 md:p-8">
|
||||||
<header className="text-center mb-12">
|
<header className="text-center mb-12 mt-24 md:mt-0">
|
||||||
<motion.h1
|
<motion.h1
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
|||||||
36
docker-compose.dev.yml
Normal file
36
docker-compose.dev.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
services:
|
||||||
|
ai-stack-deployer:
|
||||||
|
image: git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev
|
||||||
|
container_name: ai-stack-deployer-dev
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- PORT=3000
|
||||||
|
- HOST=0.0.0.0
|
||||||
|
- DOKPLOY_URL=${DOKPLOY_URL}
|
||||||
|
- DOKPLOY_API_TOKEN=${DOKPLOY_API_TOKEN}
|
||||||
|
- STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl}
|
||||||
|
- STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest}
|
||||||
|
- RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal}
|
||||||
|
- SHARED_PROJECT_ID=${SHARED_PROJECT_ID}
|
||||||
|
- SHARED_ENVIRONMENT_ID=${SHARED_ENVIRONMENT_ID}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"bun",
|
||||||
|
"--eval",
|
||||||
|
"fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
|
networks:
|
||||||
|
- ai-stack-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ai-stack-network:
|
||||||
|
driver: bridge
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
ai-stack-deployer:
|
ai-stack-deployer:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: ai-stack-deployer
|
container_name: ai-stack-deployer-local
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=development
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- DOKPLOY_URL=${DOKPLOY_URL}
|
- DOKPLOY_URL=${DOKPLOY_URL}
|
||||||
@@ -30,6 +32,9 @@ services:
|
|||||||
start_period: 5s
|
start_period: 5s
|
||||||
networks:
|
networks:
|
||||||
- ai-stack-network
|
- ai-stack-network
|
||||||
|
volumes:
|
||||||
|
- ./src:/app/src:ro
|
||||||
|
- ./client:/app/client:ro
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
ai-stack-network:
|
ai-stack-network:
|
||||||
38
docker-compose.prod.yml
Normal file
38
docker-compose.prod.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
ai-stack-deployer:
|
||||||
|
image: git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest
|
||||||
|
container_name: ai-stack-deployer
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
- HOST=0.0.0.0
|
||||||
|
- DOKPLOY_URL=${DOKPLOY_URL}
|
||||||
|
- DOKPLOY_API_TOKEN=${DOKPLOY_API_TOKEN}
|
||||||
|
- STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl}
|
||||||
|
- STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest}
|
||||||
|
- RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal}
|
||||||
|
- SHARED_PROJECT_ID=${SHARED_PROJECT_ID}
|
||||||
|
- SHARED_ENVIRONMENT_ID=${SHARED_ENVIRONMENT_ID}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"bun",
|
||||||
|
"--eval",
|
||||||
|
"fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
|
networks:
|
||||||
|
- ai-stack-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ai-stack-network:
|
||||||
|
driver: bridge
|
||||||
38
docker-compose.staging.yml
Normal file
38
docker-compose.staging.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
ai-stack-deployer:
|
||||||
|
image: git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:staging
|
||||||
|
container_name: ai-stack-deployer-staging
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=staging
|
||||||
|
- PORT=3000
|
||||||
|
- HOST=0.0.0.0
|
||||||
|
- DOKPLOY_URL=${DOKPLOY_URL}
|
||||||
|
- DOKPLOY_API_TOKEN=${DOKPLOY_API_TOKEN}
|
||||||
|
- STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl}
|
||||||
|
- STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest}
|
||||||
|
- RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal}
|
||||||
|
- SHARED_PROJECT_ID=${SHARED_PROJECT_ID}
|
||||||
|
- SHARED_ENVIRONMENT_ID=${SHARED_ENVIRONMENT_ID}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"bun",
|
||||||
|
"--eval",
|
||||||
|
"fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
|
networks:
|
||||||
|
- ai-stack-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ai-stack-network:
|
||||||
|
driver: bridge
|
||||||
@@ -105,7 +105,7 @@ curl http://localhost:3001/
|
|||||||
## Implementation Date
|
## Implementation Date
|
||||||
|
|
||||||
**Date**: January 13, 2026
|
**Date**: January 13, 2026
|
||||||
**Commit**: [To be added after commit]
|
**Branch**: dev (following Git Flow)
|
||||||
**Files Modified**:
|
**Files Modified**:
|
||||||
- `Dockerfile` - Switched build stage from Bun to Node.js
|
- `Dockerfile` - Switched build stage from Bun to Node.js
|
||||||
- `README.md` - Updated Technology Stack and Troubleshooting sections
|
- `README.md` - Updated Technology Stack and Troubleshooting sections
|
||||||
@@ -173,7 +173,7 @@ If you still encounter AVX errors:
|
|||||||
|
|
||||||
1. **Verify you're using the latest Dockerfile**:
|
1. **Verify you're using the latest Dockerfile**:
|
||||||
```bash
|
```bash
|
||||||
git pull origin main
|
git pull origin dev
|
||||||
head -10 Dockerfile
|
head -10 Dockerfile
|
||||||
# Should show: FROM node:20-alpine AS builder
|
# Should show: FROM node:20-alpine AS builder
|
||||||
```
|
```
|
||||||
|
|||||||
560
docs/DOKPLOY_DEPLOYMENT.md
Normal file
560
docs/DOKPLOY_DEPLOYMENT.md
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
# Dokploy Deployment Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This project uses **Gitea Actions** to build Docker images and **Dokploy** to deploy them. Each branch (dev, staging, main) has its own:
|
||||||
|
- Docker image tag
|
||||||
|
- Docker Compose file
|
||||||
|
- Dokploy application
|
||||||
|
- Domain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ Gitea │
|
||||||
|
│ (Source) │
|
||||||
|
└──────┬──────┘
|
||||||
|
│ push event
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ Gitea │
|
||||||
|
│ Actions │ Builds Docker images
|
||||||
|
│ (CI/CD) │ Tags: dev, staging, latest
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ Gitea │
|
||||||
|
│ Registry │ git.app.flexinit.nl/oussamadouhou/ai-stack-deployer
|
||||||
|
└──────┬──────┘
|
||||||
|
│ webhook (push event)
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ Dokploy │ Pulls & deploys image
|
||||||
|
│ (Deploy) │ Uses docker-compose.{env}.yml
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branch Strategy
|
||||||
|
|
||||||
|
| Branch | Image Tag | Compose File | Domain (suggested) |
|
||||||
|
|-----------|-----------|----------------------------|------------------------------|
|
||||||
|
| `dev` | `dev` | `docker-compose.dev.yml` | portal-dev.ai.flexinit.nl |
|
||||||
|
| `staging` | `staging` | `docker-compose.staging.yml` | portal-staging.ai.flexinit.nl |
|
||||||
|
| `main` | `latest` | `docker-compose.prod.yml` | portal.ai.flexinit.nl |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gitea Actions Workflow
|
||||||
|
|
||||||
|
**File**: `.gitea/workflows/docker-publish.yaml`
|
||||||
|
|
||||||
|
**Triggers**: Push to `dev`, `staging`, or `main` branches
|
||||||
|
|
||||||
|
**Builds**:
|
||||||
|
```yaml
|
||||||
|
dev branch → git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev
|
||||||
|
staging branch → git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:staging
|
||||||
|
main branch → git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Also creates SHA tags**: `{branch}-{short-sha}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker Compose Files
|
||||||
|
|
||||||
|
### `docker-compose.dev.yml`
|
||||||
|
- Pulls: `git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev`
|
||||||
|
- Environment: `NODE_ENV=development`
|
||||||
|
- Container name: `ai-stack-deployer-dev`
|
||||||
|
|
||||||
|
### `docker-compose.staging.yml`
|
||||||
|
- Pulls: `git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:staging`
|
||||||
|
- Environment: `NODE_ENV=staging`
|
||||||
|
- Container name: `ai-stack-deployer-staging`
|
||||||
|
|
||||||
|
### `docker-compose.prod.yml`
|
||||||
|
- Pulls: `git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest`
|
||||||
|
- Environment: `NODE_ENV=production`
|
||||||
|
- Container name: `ai-stack-deployer`
|
||||||
|
|
||||||
|
### `docker-compose.local.yml`
|
||||||
|
- **Builds locally** (doesn't pull from registry)
|
||||||
|
- For local development only
|
||||||
|
- Includes volume mounts for hot reload
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shared Project Configuration (IMPORTANT)
|
||||||
|
|
||||||
|
### What is Shared Project Deployment?
|
||||||
|
|
||||||
|
The portal deploys **all user AI stacks as applications within a single shared Dokploy project**, instead of creating a new project for each user. This provides:
|
||||||
|
|
||||||
|
- ✅ Better organization (all stacks in one place)
|
||||||
|
- ✅ Shared environment variables
|
||||||
|
- ✅ Centralized monitoring
|
||||||
|
- ✅ Easier management
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
Dokploy Project: ai-stack-portal
|
||||||
|
├── Environment: deployments
|
||||||
|
│ ├── Application: john-dev
|
||||||
|
│ ├── Application: jane-prod
|
||||||
|
│ └── Application: alice-test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting Up the Shared Project
|
||||||
|
|
||||||
|
**Step 1: Create the Shared Project in Dokploy**
|
||||||
|
|
||||||
|
1. In Dokploy UI, create a new project:
|
||||||
|
- Name: `ai-stack-portal` (or any name you prefer)
|
||||||
|
- Description: "Shared project for all user AI stacks"
|
||||||
|
|
||||||
|
2. Note the **Project ID** (visible in URL or API response)
|
||||||
|
- Example: `2y2Glhz5Wy0dBNf6BOR_-`
|
||||||
|
|
||||||
|
3. Get the **Environment ID**:
|
||||||
|
```bash
|
||||||
|
curl -s "http://10.100.0.20:3000/api/project.one?projectId=2y2Glhz5Wy0dBNf6BOR_-" \
|
||||||
|
-H "Authorization: Bearer $DOKPLOY_API_TOKEN" | jq -r '.environments[0].id'
|
||||||
|
```
|
||||||
|
- Example: `RqE9OFMdLwkzN7pif1xN8`
|
||||||
|
|
||||||
|
**Step 2: Configure Project-Level Variables**
|
||||||
|
|
||||||
|
In the shared project (`ai-stack-portal`), add these **project-level environment variables**:
|
||||||
|
|
||||||
|
| Variable Name | Value | Purpose |
|
||||||
|
|---------------|-------|---------|
|
||||||
|
| `SHARED_PROJECT_ID` | `2y2Glhz5Wy0dBNf6BOR_-` | The project where user stacks deploy |
|
||||||
|
| `SHARED_ENVIRONMENT_ID` | `RqE9OFMdLwkzN7pif1xN8` | The environment within that project |
|
||||||
|
|
||||||
|
**Step 3: Reference Variables in Portal Applications**
|
||||||
|
|
||||||
|
The portal's docker-compose files use Dokploy's variable syntax to reference these:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- SHARED_PROJECT_ID=$${{project.SHARED_PROJECT_ID}}
|
||||||
|
- SHARED_ENVIRONMENT_ID=$${{project.SHARED_ENVIRONMENT_ID}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**This syntax `$${{project.VARIABLE}}` tells Dokploy**: "Get this value from the project-level environment variables"
|
||||||
|
|
||||||
|
**Note**: The double `$$` is required to escape the dollar sign in Docker Compose files.
|
||||||
|
|
||||||
|
### Important Notes
|
||||||
|
|
||||||
|
- ⚠️ **Both variables MUST be set** in the shared project for deployment to work
|
||||||
|
- ⚠️ If not set, portal will fall back to creating separate projects per user (legacy behavior)
|
||||||
|
- ✅ You can have different shared projects for dev/staging/prod environments
|
||||||
|
- ✅ All 3 portal deployments (dev/staging/prod) should point to their respective shared projects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setting Up Dokploy
|
||||||
|
|
||||||
|
### Step 1: Create Dev Application
|
||||||
|
|
||||||
|
1. **In Dokploy UI**, create new application:
|
||||||
|
- **Name**: `ai-stack-deployer-dev`
|
||||||
|
- **Type**: Docker Compose
|
||||||
|
- **Repository**: `ssh://git@git.app.flexinit.nl:22222/oussamadouhou/ai-stack-deployer.git`
|
||||||
|
- **Branch**: `dev`
|
||||||
|
- **Compose File**: `docker-compose.dev.yml`
|
||||||
|
|
||||||
|
2. **Configure Domain**:
|
||||||
|
- Add domain: `portal-dev.ai.flexinit.nl`
|
||||||
|
- Enable SSL (via Traefik wildcard cert)
|
||||||
|
|
||||||
|
3. **Set Environment Variables**:
|
||||||
|
|
||||||
|
**Important**: The portal application should be deployed **inside the shared project** (e.g., `ai-stack-portal-dev`).
|
||||||
|
|
||||||
|
Then set these **project-level variables** in that shared project:
|
||||||
|
```env
|
||||||
|
SHARED_PROJECT_ID=<your-shared-project-id>
|
||||||
|
SHARED_ENVIRONMENT_ID=<your-shared-environment-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
And these **application-level variables** in the portal app:
|
||||||
|
```env
|
||||||
|
DOKPLOY_URL=http://10.100.0.20:3000
|
||||||
|
DOKPLOY_API_TOKEN=<your-token>
|
||||||
|
STACK_DOMAIN_SUFFIX=ai.flexinit.nl
|
||||||
|
STACK_IMAGE=git.app.flexinit.nl/flexinit/agent-stack:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
The docker-compose file will automatically reference the project-level variables using:
|
||||||
|
```yaml
|
||||||
|
SHARED_PROJECT_ID=${{project.SHARED_PROJECT_ID}}
|
||||||
|
SHARED_ENVIRONMENT_ID=${{project.SHARED_ENVIRONMENT_ID}}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **⚠️ CRITICAL: Configure Custom Docker Compose Command**:
|
||||||
|
|
||||||
|
Because we use non-default compose file names (`docker-compose.dev.yml`, `docker-compose.prod.yml`, etc.), you **MUST** configure a custom command in Dokploy.
|
||||||
|
|
||||||
|
**In Dokploy UI:**
|
||||||
|
- Go to the application **Settings** or **Advanced** tab
|
||||||
|
- Find **"Custom Command"** or **"Docker Compose Command"** field
|
||||||
|
- Set it to:
|
||||||
|
```bash
|
||||||
|
compose -p <app-name> -f ./docker-compose.dev.yml up -d --remove-orphans --pull always
|
||||||
|
```
|
||||||
|
|
||||||
|
**Replace `<app-name>`** with your actual application name from Dokploy (e.g., `aistackportal-portal-0rohwx`)
|
||||||
|
|
||||||
|
**Replace `docker-compose.dev.yml`** with the appropriate file for each environment:
|
||||||
|
- Dev: `docker-compose.dev.yml`
|
||||||
|
- Staging: `docker-compose.staging.yml`
|
||||||
|
- Production: `docker-compose.prod.yml`
|
||||||
|
|
||||||
|
**Why this is required:**
|
||||||
|
- Dokploy's default command is `docker compose up -d` without the `-f` flag
|
||||||
|
- Without `-f`, docker looks for `docker-compose.yml` (which doesn't exist)
|
||||||
|
- This causes the error: `no configuration file provided: not found`
|
||||||
|
|
||||||
|
**Full examples:**
|
||||||
|
```bash
|
||||||
|
# Dev
|
||||||
|
compose -p aistackportal-deployer-dev-xyz123 -f ./docker-compose.dev.yml up -d --remove-orphans --pull always
|
||||||
|
|
||||||
|
# Staging
|
||||||
|
compose -p aistackportal-deployer-staging-abc456 -f ./docker-compose.staging.yml up -d --remove-orphans --pull always
|
||||||
|
|
||||||
|
# Production
|
||||||
|
compose -p aistackportal-portal-0rohwx -f ./docker-compose.prod.yml up -d --remove-orphans --pull always
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Configure Webhook**:
|
||||||
|
- Event: **Push**
|
||||||
|
- Branch: `dev`
|
||||||
|
- This will auto-deploy when you push to dev branch
|
||||||
|
|
||||||
|
6. **Deploy**
|
||||||
|
|
||||||
|
### Step 2: Create Staging Application
|
||||||
|
|
||||||
|
Repeat Step 1 with these changes:
|
||||||
|
- **Name**: `ai-stack-deployer-staging`
|
||||||
|
- **Branch**: `staging`
|
||||||
|
- **Compose File**: `docker-compose.staging.yml`
|
||||||
|
- **Domain**: `portal-staging.ai.flexinit.nl`
|
||||||
|
- **Webhook Branch**: `staging`
|
||||||
|
|
||||||
|
### Step 3: Create Production Application
|
||||||
|
|
||||||
|
Repeat Step 1 with these changes:
|
||||||
|
- **Name**: `ai-stack-deployer-prod`
|
||||||
|
- **Branch**: `main`
|
||||||
|
- **Compose File**: `docker-compose.prod.yml`
|
||||||
|
- **Domain**: `portal.ai.flexinit.nl`
|
||||||
|
- **Webhook Branch**: `main`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment Workflow
|
||||||
|
|
||||||
|
### Development Cycle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Make changes on dev branch
|
||||||
|
git checkout dev
|
||||||
|
# ... make changes ...
|
||||||
|
git commit -m "feat: add new feature"
|
||||||
|
git push origin dev
|
||||||
|
|
||||||
|
# 2. Gitea Actions automatically builds dev image
|
||||||
|
# 3. Dokploy webhook triggers and deploys to portal-dev.ai.flexinit.nl
|
||||||
|
|
||||||
|
# 4. Test on dev environment
|
||||||
|
curl https://portal-dev.ai.flexinit.nl/health
|
||||||
|
|
||||||
|
# 5. When ready, merge to staging
|
||||||
|
git checkout staging
|
||||||
|
git merge dev
|
||||||
|
git push origin staging
|
||||||
|
|
||||||
|
# 6. Gitea Actions builds staging image
|
||||||
|
# 7. Dokploy deploys to portal-staging.ai.flexinit.nl
|
||||||
|
|
||||||
|
# 8. Final testing on staging, then merge to main
|
||||||
|
git checkout main
|
||||||
|
git merge staging
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# 9. Gitea Actions builds production image (latest)
|
||||||
|
# 10. Dokploy deploys to portal.ai.flexinit.nl
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Image Tags Explained
|
||||||
|
|
||||||
|
Each push creates multiple tags:
|
||||||
|
|
||||||
|
### Example: Push to `dev` branch (commit `abc1234`)
|
||||||
|
|
||||||
|
Gitea Actions creates:
|
||||||
|
```
|
||||||
|
git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev ← Latest dev
|
||||||
|
git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev-abc1234 ← Specific commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Push to `main` branch (commit `xyz5678`)
|
||||||
|
|
||||||
|
Gitea Actions creates:
|
||||||
|
```
|
||||||
|
git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest ← Latest production
|
||||||
|
git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:main-xyz5678 ← Specific commit
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why?**
|
||||||
|
- Branch tags (`dev`, `staging`, `latest`) always point to latest build
|
||||||
|
- SHA tags allow you to rollback to specific commits if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Strategy
|
||||||
|
|
||||||
|
### Quick Rollback in Dokploy
|
||||||
|
|
||||||
|
If a deployment breaks, you can quickly rollback:
|
||||||
|
|
||||||
|
1. **In Dokploy UI**, go to the application
|
||||||
|
2. **Edit** the docker-compose file
|
||||||
|
3. Change the image tag to a previous SHA:
|
||||||
|
```yaml
|
||||||
|
image: git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:main-abc1234
|
||||||
|
```
|
||||||
|
4. **Redeploy**
|
||||||
|
|
||||||
|
### Manual Rollback via Git
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find the last working commit
|
||||||
|
git log --oneline
|
||||||
|
|
||||||
|
# Revert to that commit
|
||||||
|
git revert HEAD # or git reset --hard <commit-sha>
|
||||||
|
|
||||||
|
# Push to trigger rebuild
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
### Using docker-compose.local.yml
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and run locally
|
||||||
|
docker-compose -f docker-compose.local.yml up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose -f docker-compose.local.yml logs -f
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
docker-compose -f docker-compose.local.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Bun directly (without Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Run dev server (API + Vite)
|
||||||
|
bun run dev
|
||||||
|
|
||||||
|
# Run API only
|
||||||
|
bun run dev:api
|
||||||
|
|
||||||
|
# Run client only
|
||||||
|
bun run dev:client
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Required in Dokploy
|
||||||
|
|
||||||
|
```env
|
||||||
|
DOKPLOY_URL=http://10.100.0.20:3000
|
||||||
|
DOKPLOY_API_TOKEN=<your-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional (with defaults)
|
||||||
|
|
||||||
|
```env
|
||||||
|
PORT=3000
|
||||||
|
HOST=0.0.0.0
|
||||||
|
STACK_DOMAIN_SUFFIX=ai.flexinit.nl
|
||||||
|
STACK_IMAGE=git.app.flexinit.nl/flexinit/agent-stack:latest
|
||||||
|
RESERVED_NAMES=admin,api,www,root,system,test,demo,portal
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-Environment Overrides
|
||||||
|
|
||||||
|
If dev/staging/prod need different configs, set them in Dokploy:
|
||||||
|
|
||||||
|
**Dev**:
|
||||||
|
```env
|
||||||
|
STACK_DOMAIN_SUFFIX=dev-ai.flexinit.nl
|
||||||
|
```
|
||||||
|
|
||||||
|
**Staging**:
|
||||||
|
```env
|
||||||
|
STACK_DOMAIN_SUFFIX=staging-ai.flexinit.nl
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prod**:
|
||||||
|
```env
|
||||||
|
STACK_DOMAIN_SUFFIX=ai.flexinit.nl
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Fails in Gitea Actions
|
||||||
|
|
||||||
|
Check the workflow logs in Gitea:
|
||||||
|
```
|
||||||
|
https://git.app.flexinit.nl/oussamadouhou/ai-stack-deployer/actions
|
||||||
|
```
|
||||||
|
|
||||||
|
Common issues:
|
||||||
|
- **AVX error**: Fixed in Dockerfile (uses Node.js for build)
|
||||||
|
- **Registry auth**: Check `REGISTRY_TOKEN` secret in Gitea
|
||||||
|
|
||||||
|
### Deployment Fails in Dokploy
|
||||||
|
|
||||||
|
1. **Check Dokploy logs**: Application → Logs
|
||||||
|
2. **Verify image exists**:
|
||||||
|
```bash
|
||||||
|
docker pull git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:dev
|
||||||
|
```
|
||||||
|
3. **Check environment variables**: Make sure all required vars are set
|
||||||
|
|
||||||
|
### Error: "no configuration file provided: not found"
|
||||||
|
|
||||||
|
**Symptom:**
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ Command: docker compose up -d --force-recreate --pull always ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
no configuration file provided: not found
|
||||||
|
Error: ❌ Docker command failed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause:** Dokploy is looking for the default `docker-compose.yml` file, which doesn't exist. We use environment-specific files (`docker-compose.dev.yml`, `docker-compose.prod.yml`, etc.).
|
||||||
|
|
||||||
|
**Solution:** Configure a **custom Docker Compose command** in Dokploy:
|
||||||
|
|
||||||
|
1. Go to your application in Dokploy UI
|
||||||
|
2. Navigate to **Settings** → **Advanced** (or similar section)
|
||||||
|
3. Find **"Custom Command"** field
|
||||||
|
4. Set it to:
|
||||||
|
```bash
|
||||||
|
compose -p <app-name> -f ./docker-compose.{env}.yml up -d --remove-orphans --pull always
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
- `<app-name>` with your actual Dokploy app name (e.g., `aistackportal-portal-0rohwx`)
|
||||||
|
- `{env}` with `dev`, `staging`, or `prod`
|
||||||
|
|
||||||
|
**Example for production:**
|
||||||
|
```bash
|
||||||
|
compose -p aistackportal-portal-0rohwx -f ./docker-compose.prod.yml up -d --remove-orphans --pull always
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Save and redeploy
|
||||||
|
|
||||||
|
**Why the `-f` flag is needed:** Docker Compose defaults to looking for `docker-compose.yml`. The `-f` flag explicitly specifies which file to use.
|
||||||
|
|
||||||
|
### Health Check Failing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH into Dokploy host
|
||||||
|
ssh user@10.100.0.20
|
||||||
|
|
||||||
|
# Check container logs
|
||||||
|
docker logs ai-stack-deployer-dev
|
||||||
|
|
||||||
|
# Test health endpoint
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhook Not Triggering
|
||||||
|
|
||||||
|
1. **In Dokploy**, check webhook configuration
|
||||||
|
2. **In Gitea**, go to repo Settings → Webhooks
|
||||||
|
3. Verify webhook URL and secret match
|
||||||
|
4. Check recent deliveries for errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Considerations
|
||||||
|
|
||||||
|
### 1. Image Size Optimization
|
||||||
|
|
||||||
|
The Docker image excludes dev files via `.dockerignore`:
|
||||||
|
- ✅ `docs/` - excluded
|
||||||
|
- ✅ `scripts/` - excluded
|
||||||
|
- ✅ `.gitea/` - excluded
|
||||||
|
- ✅ `*.md` (except README.md) - excluded
|
||||||
|
|
||||||
|
Current image size: ~150MB
|
||||||
|
|
||||||
|
### 2. Security
|
||||||
|
|
||||||
|
- Container runs as non-root user (`nodejs:1001`)
|
||||||
|
- No secrets in source code (uses `.env`)
|
||||||
|
- Dokploy API accessible only on internal network
|
||||||
|
|
||||||
|
### 3. Monitoring
|
||||||
|
|
||||||
|
Set up alerts for:
|
||||||
|
- Container health check failures
|
||||||
|
- Memory/CPU usage spikes
|
||||||
|
- Deployment failures
|
||||||
|
|
||||||
|
### 4. Backup Strategy
|
||||||
|
|
||||||
|
- **Database**: This app has no database (stateless)
|
||||||
|
- **Configuration**: Environment variables stored in Dokploy (backed up)
|
||||||
|
- **Code**: Stored in Gitea (backed up)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Environment | Domain | Image Tag | Auto-Deploy? |
|
||||||
|
|-------------|------------------------------|-----------|--------------|
|
||||||
|
| Dev | portal-dev.ai.flexinit.nl | `dev` | ✅ On push |
|
||||||
|
| Staging | portal-staging.ai.flexinit.nl | `staging` | ✅ On push |
|
||||||
|
| Production | portal.ai.flexinit.nl | `latest` | ✅ On push |
|
||||||
|
|
||||||
|
**Next Steps**:
|
||||||
|
1. ✅ Push changes to `dev` branch
|
||||||
|
2. ⏳ Create 3 Dokploy applications (dev, staging, prod)
|
||||||
|
3. ⏳ Configure webhooks for each branch
|
||||||
|
4. ⏳ Deploy and test each environment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Questions?** Check the main README.md or CLAUDE.md for more details.
|
||||||
416
docs/LOCALE_STATUS_REPORT.md
Normal file
416
docs/LOCALE_STATUS_REPORT.md
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
# Locale/i18n Implementation Status Report
|
||||||
|
|
||||||
|
**Generated**: 2026-01-13 21:01:11 CET
|
||||||
|
**Project**: AI Stack Deployer
|
||||||
|
**Branch**: dev
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
✅ **Locale system is FULLY IMPLEMENTED and OPERATIONAL**
|
||||||
|
|
||||||
|
The project has a complete multilingual system supporting **3 languages** (English, Dutch, Arabic) across both frontend and backend. No dedicated "locale" folder exists—translations are embedded within the codebase using modern inline patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Two-Tier i18n System
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Frontend (React Client) │
|
||||||
|
│ - client/src/lib/i18n.ts │
|
||||||
|
│ - client/src/hooks/useI18n.ts │
|
||||||
|
│ - LanguageSelector component │
|
||||||
|
│ - Translations: EN, NL, AR │
|
||||||
|
└──────────────┬──────────────────────┘
|
||||||
|
│
|
||||||
|
│ (sends lang preference)
|
||||||
|
│
|
||||||
|
┌──────────────▼──────────────────────┐
|
||||||
|
│ Backend (Hono API) │
|
||||||
|
│ - src/lib/i18n-backend.ts │
|
||||||
|
│ - Deployment progress messages │
|
||||||
|
│ - Translations: EN, NL, AR │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. Frontend i18n System
|
||||||
|
|
||||||
|
**Location**: `client/src/lib/i18n.ts`
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- ✅ Three languages: English (en), Dutch (nl), Arabic (ar)
|
||||||
|
- ✅ 33 translation keys per language
|
||||||
|
- ✅ Auto-detection from browser locale
|
||||||
|
- ✅ Persistent user preference (localStorage)
|
||||||
|
- ✅ RTL support for Arabic
|
||||||
|
- ✅ Type-safe translation keys
|
||||||
|
|
||||||
|
**Key Files**:
|
||||||
|
```
|
||||||
|
client/src/
|
||||||
|
├── lib/
|
||||||
|
│ └── i18n.ts # Translation strings + utilities
|
||||||
|
├── hooks/
|
||||||
|
│ └── useI18n.ts # React hook for translations
|
||||||
|
└── components/
|
||||||
|
└── deploy/
|
||||||
|
└── LanguageSelector.tsx # Language switcher UI (NL/AR/EN)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Translation Coverage**:
|
||||||
|
- Form labels and placeholders
|
||||||
|
- Validation messages
|
||||||
|
- Deployment status messages
|
||||||
|
- Success/error screens
|
||||||
|
- UI buttons and actions
|
||||||
|
|
||||||
|
**Example Translation**:
|
||||||
|
```typescript
|
||||||
|
en: {
|
||||||
|
title: 'AI Stack Deployer',
|
||||||
|
subtitle: 'Deploy your personal AI assistant in seconds',
|
||||||
|
deployBtn: 'Deploy My AI Stack',
|
||||||
|
// ... 30 more keys
|
||||||
|
}
|
||||||
|
nl: {
|
||||||
|
title: 'AI Stack Deployer',
|
||||||
|
subtitle: 'Implementeer je persoonlijke AI in seconden',
|
||||||
|
deployBtn: 'Implementeer Mijn AI Stack',
|
||||||
|
// ... 30 more keys
|
||||||
|
}
|
||||||
|
ar: {
|
||||||
|
title: 'AI Stack Deployer',
|
||||||
|
subtitle: 'انشر مساعد البرمجة الذكي الخاص بك في ثوانٍ',
|
||||||
|
deployBtn: 'انشر مشروعي',
|
||||||
|
// ... 30 more keys
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Backend i18n System
|
||||||
|
|
||||||
|
**Location**: `src/lib/i18n-backend.ts`
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- ✅ Deployment progress messages in 3 languages
|
||||||
|
- ✅ Receives language preference from frontend
|
||||||
|
- ✅ Sends localized SSE events during deployment
|
||||||
|
- ✅ Factory pattern with `createTranslator()`
|
||||||
|
|
||||||
|
**Translation Keys** (14 keys per language):
|
||||||
|
- `initializing` - "Initializing deployment"
|
||||||
|
- `creatingProject` - "Creating project"
|
||||||
|
- `creatingApplication` - "Creating application"
|
||||||
|
- `waitingForSSL` - "Waiting for SSL certificate..."
|
||||||
|
- `deploymentSuccess` - "Application deployed successfully"
|
||||||
|
- ... and 9 more
|
||||||
|
|
||||||
|
**Integration Points**:
|
||||||
|
```typescript
|
||||||
|
// src/orchestrator/production-deployer.ts
|
||||||
|
const t = createTranslator(lang); // lang from request
|
||||||
|
progress.update(50, t('creatingApplication'));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Components Using i18n
|
||||||
|
|
||||||
|
**All deployment components are multilingual**:
|
||||||
|
|
||||||
|
| Component | File | Purpose |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| DeployPage | `client/src/pages/DeployPage.tsx` | Main page, language state |
|
||||||
|
| DeployForm | `client/src/components/deploy/DeployForm.tsx` | Form with validation |
|
||||||
|
| DeployProgress | `client/src/components/deploy/DeployProgress.tsx` | Progress tracking |
|
||||||
|
| DeploySuccess | `client/src/components/deploy/DeploySuccess.tsx` | Success screen |
|
||||||
|
| DeployError | `client/src/components/deploy/DeployError.tsx` | Error screen |
|
||||||
|
| LanguageSelector | `client/src/components/deploy/LanguageSelector.tsx` | Language switcher |
|
||||||
|
|
||||||
|
**Usage Pattern**:
|
||||||
|
```tsx
|
||||||
|
const { lang, setLang, t, isRtl } = useI18n();
|
||||||
|
return <h1>{t('title')}</h1>; // Auto-translated
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
|
||||||
|
### Commit History (Reverse Chronological)
|
||||||
|
|
||||||
|
| Date | Commit | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| 2026-01-13 | `86fe7a8` | **feat: Add multilingual deployment progress messages** - Backend i18n system |
|
||||||
|
| 2026-01-10 | `897a828` | **feat(seo): add Dutch metadata, social previews, and JSON-LD** |
|
||||||
|
| 2026-01-10 | `7aa27f7` | fix: improve language button styling for text labels |
|
||||||
|
| 2026-01-10 | `2f306f7` | **feat: production-ready deployment with multi-language UI** - Frontend i18n system |
|
||||||
|
|
||||||
|
**Total Development Time**: 3 days (Jan 10-13, 2026)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Decisions
|
||||||
|
|
||||||
|
### Why No Separate Locale Folder?
|
||||||
|
|
||||||
|
**Modern Inline Pattern**: Translations are co-located with code for:
|
||||||
|
- ✅ Better type safety (TypeScript can validate keys)
|
||||||
|
- ✅ Easier refactoring (IDE can track references)
|
||||||
|
- ✅ Simpler imports (no file lookup)
|
||||||
|
- ✅ Reduced bundle size (no extra JSON parsing)
|
||||||
|
|
||||||
|
**Traditional Approach** (NOT used):
|
||||||
|
```
|
||||||
|
locales/
|
||||||
|
├── en.json
|
||||||
|
├── nl.json
|
||||||
|
└── ar.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current Approach** (Used):
|
||||||
|
```typescript
|
||||||
|
// All in client/src/lib/i18n.ts
|
||||||
|
export const translations = {
|
||||||
|
en: { ... },
|
||||||
|
nl: { ... },
|
||||||
|
ar: { ... }
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Language Support Details
|
||||||
|
|
||||||
|
### 1. English (en)
|
||||||
|
- **Status**: ✅ Complete (default language)
|
||||||
|
- **Coverage**: 100% (33 frontend + 14 backend keys)
|
||||||
|
- **Notes**: Fallback language if translation missing
|
||||||
|
|
||||||
|
### 2. Dutch (nl)
|
||||||
|
- **Status**: ✅ Complete
|
||||||
|
- **Coverage**: 100% (33 frontend + 14 backend keys)
|
||||||
|
- **Notes**: Primary target language for Dutch users
|
||||||
|
- **Quality**: Professional translations
|
||||||
|
|
||||||
|
### 3. Arabic (ar)
|
||||||
|
- **Status**: ✅ Complete with RTL support
|
||||||
|
- **Coverage**: 100% (33 frontend + 14 backend keys)
|
||||||
|
- **RTL**: Automatic direction switching (`dir="rtl"`)
|
||||||
|
- **Notes**: Full right-to-left layout support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features & Capabilities
|
||||||
|
|
||||||
|
### Frontend Features
|
||||||
|
|
||||||
|
✅ **Automatic Language Detection**
|
||||||
|
```typescript
|
||||||
|
const browserLang = navigator.language?.split('-')[0];
|
||||||
|
// Auto-selects 'nl' if browser is nl-NL, nl-BE, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Persistent Preference**
|
||||||
|
```typescript
|
||||||
|
localStorage.setItem('preferredLanguage', 'nl');
|
||||||
|
// Remembered across sessions
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **RTL Support**
|
||||||
|
```typescript
|
||||||
|
document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr';
|
||||||
|
// Entire layout flips for Arabic
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Type Safety**
|
||||||
|
```typescript
|
||||||
|
type TranslationKey = keyof typeof translations.en;
|
||||||
|
// TypeScript prevents typos in translation keys
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Features
|
||||||
|
|
||||||
|
✅ **Language-Aware Deployment**
|
||||||
|
```typescript
|
||||||
|
POST /api/deploy
|
||||||
|
{ "name": "john-dev", "lang": "nl" }
|
||||||
|
// Backend sends Dutch progress messages
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **SSE Localized Events**
|
||||||
|
```javascript
|
||||||
|
event: progress
|
||||||
|
data: {"progress": 50, "currentStep": "Applicatie aanmaken"} // Dutch
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SEO & Metadata
|
||||||
|
|
||||||
|
### HTML Meta Tags (src/frontend/index.html)
|
||||||
|
|
||||||
|
✅ **Dutch-First SEO** (commit `897a828`):
|
||||||
|
```html
|
||||||
|
<html lang="en">
|
||||||
|
<meta property="og:locale" content="nl_NL">
|
||||||
|
<title>AI Stack Deployer - Persoonlijke AI Assistent Deployen | FLEXINIT</title>
|
||||||
|
<meta name="description" content="Deploy je persoonlijke AI assistent in seconden...">
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Social Media Previews**:
|
||||||
|
- Open Graph tags (Facebook, LinkedIn)
|
||||||
|
- Twitter Card tags
|
||||||
|
- 1200x630 social preview image (`og-image.png`)
|
||||||
|
- Image alt text in Dutch
|
||||||
|
|
||||||
|
✅ **Structured Data**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebApplication",
|
||||||
|
"name": "AI Stack Deployer",
|
||||||
|
"applicationCategory": "DeveloperApplication"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Status
|
||||||
|
|
||||||
|
### Frontend i18n Tests
|
||||||
|
- ❌ No automated tests (manual testing only)
|
||||||
|
- ✅ Manual verification: All 3 languages render correctly
|
||||||
|
- ✅ RTL layout verified for Arabic
|
||||||
|
|
||||||
|
### Backend i18n Tests
|
||||||
|
- ❌ No automated tests
|
||||||
|
- ✅ Manual verification: SSE events show correct language
|
||||||
|
|
||||||
|
### Missing Test Coverage
|
||||||
|
```
|
||||||
|
[ ] Unit tests for translation functions
|
||||||
|
[ ] E2E tests for language switching
|
||||||
|
[ ] Visual regression tests for RTL layout
|
||||||
|
[ ] Integration tests for backend translations
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Inventory
|
||||||
|
|
||||||
|
### Frontend Files (8 files)
|
||||||
|
|
||||||
|
| File | Lines | Purpose | Status |
|
||||||
|
|------|-------|---------|--------|
|
||||||
|
| `client/src/lib/i18n.ts` | 125 | Translation strings + utilities | ✅ Complete |
|
||||||
|
| `client/src/hooks/useI18n.ts` | 25 | React hook for i18n | ✅ Complete |
|
||||||
|
| `client/src/components/deploy/LanguageSelector.tsx` | 38 | Language switcher UI | ✅ Complete |
|
||||||
|
| `client/src/pages/DeployPage.tsx` | 234 | Main page with i18n | ✅ Complete |
|
||||||
|
| `client/src/components/deploy/DeployForm.tsx` | ? | Form with translations | ✅ Complete |
|
||||||
|
| `client/src/components/deploy/DeployProgress.tsx` | ? | Progress with translations | ✅ Complete |
|
||||||
|
| `client/src/components/deploy/DeploySuccess.tsx` | ? | Success with translations | ✅ Complete |
|
||||||
|
| `client/src/components/deploy/DeployError.tsx` | ? | Error with translations | ✅ Complete |
|
||||||
|
|
||||||
|
### Backend Files (2 files)
|
||||||
|
|
||||||
|
| File | Lines | Purpose | Status |
|
||||||
|
|------|-------|---------|--------|
|
||||||
|
| `src/lib/i18n-backend.ts` | 66 | Backend translations + factory | ✅ Complete |
|
||||||
|
| `src/orchestrator/production-deployer.ts` | ? | Uses translations during deploy | ✅ Complete |
|
||||||
|
|
||||||
|
### Legacy Files (2 files)
|
||||||
|
|
||||||
|
| File | Purpose | Status |
|
||||||
|
|------|---------|--------|
|
||||||
|
| `src/frontend/index.html` | Old vanilla JS UI with `data-i18n` | ⚠️ Deprecated (React replaced it) |
|
||||||
|
| `src/frontend/app.js` | Old vanilla JS i18n system | ⚠️ Deprecated (React replaced it) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps & Recommendations
|
||||||
|
|
||||||
|
### Immediate Actions (Priority 1)
|
||||||
|
|
||||||
|
1. ⚠️ **Clean up deprecated files**
|
||||||
|
- `src/frontend/` is no longer served (React client replaced it)
|
||||||
|
- Consider archiving or deleting old vanilla JS files
|
||||||
|
- Update documentation to reflect React-only architecture
|
||||||
|
|
||||||
|
2. ⚠️ **Add translation tests**
|
||||||
|
```bash
|
||||||
|
# Missing test coverage
|
||||||
|
client/src/lib/__tests__/i18n.test.ts
|
||||||
|
src/lib/__tests__/i18n-backend.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Future Enhancements (Priority 2)
|
||||||
|
|
||||||
|
3. 📝 **Add more languages**
|
||||||
|
- French (fr) - Belgium market
|
||||||
|
- German (de) - DACH region
|
||||||
|
- Spanish (es) - Global reach
|
||||||
|
|
||||||
|
4. 📝 **Extract translations to JSON** (if team prefers)
|
||||||
|
```
|
||||||
|
client/src/locales/
|
||||||
|
├── en.json
|
||||||
|
├── nl.json
|
||||||
|
└── ar.json
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 📝 **Add translation management**
|
||||||
|
- Consider tools like i18next, react-intl
|
||||||
|
- Or maintain current simple system (works great for 3 languages)
|
||||||
|
|
||||||
|
### Documentation Updates (Priority 3)
|
||||||
|
|
||||||
|
6. 📝 **Update CLAUDE.md**
|
||||||
|
- Document i18n system architecture
|
||||||
|
- Add guidelines for adding new translation keys
|
||||||
|
- Explain why no separate locale folder exists
|
||||||
|
|
||||||
|
7. 📝 **Update README.md**
|
||||||
|
- Add "Multilingual Support" section
|
||||||
|
- Show how to add new languages
|
||||||
|
- Document translation contribution process
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
✅ **Locale system is COMPLETE and PRODUCTION-READY**
|
||||||
|
|
||||||
|
The AI Stack Deployer has a fully functional internationalization system supporting English, Dutch, and Arabic. The implementation follows modern best practices with:
|
||||||
|
|
||||||
|
- Type-safe translation keys
|
||||||
|
- Automatic language detection
|
||||||
|
- Persistent user preferences
|
||||||
|
- Full RTL support for Arabic
|
||||||
|
- Backend deployment messages in user's language
|
||||||
|
- Professional Dutch translations for SEO
|
||||||
|
|
||||||
|
### Current Status: ✅ OPERATIONAL
|
||||||
|
|
||||||
|
No initialization needed—the locale system is **already deployed and working** in production.
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
|
||||||
|
**No action required** unless adding more languages or improving test coverage. The current system handles the core requirement (multilingual support for Dutch/English/Arabic markets) effectively.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Report Generated By**: Claude Code (Sisyphus)
|
||||||
|
**Data Sources**: Git history, code analysis, direct file inspection
|
||||||
|
**Verification**: Manual cross-reference with 11 source files
|
||||||
140
docs/README.md
Normal file
140
docs/README.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Documentation Index
|
||||||
|
|
||||||
|
**AI Stack Deployer** - Technical documentation and implementation guides.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Documentation Rules
|
||||||
|
|
||||||
|
### File Organization
|
||||||
|
- **Root Level** (`/`): User-facing docs only (README.md, CLAUDE.md, ROADMAP.md)
|
||||||
|
- **docs/** folder: All technical documentation, guides, and reports
|
||||||
|
- **docs/archive/**: Historical/deprecated documentation
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- `UPPERCASE_WITH_UNDERSCORES.md` for formal documentation
|
||||||
|
- Use descriptive names: `FEATURE_NAME_GUIDE.md` or `COMPONENT_STATUS.md`
|
||||||
|
- Date-stamped reports: Include generation date in file header
|
||||||
|
|
||||||
|
### Document Structure
|
||||||
|
All technical docs must include:
|
||||||
|
1. **Title** with brief description
|
||||||
|
2. **Last Updated** date
|
||||||
|
3. **Status** (Draft, In Progress, Complete, Deprecated)
|
||||||
|
4. **Table of Contents** (if > 100 lines)
|
||||||
|
5. **Clear sections** with headers
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
- Update dates when editing
|
||||||
|
- Mark outdated docs as **Deprecated** (move to archive/)
|
||||||
|
- Cross-reference related docs
|
||||||
|
- Keep README.md (this file) up to date
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentation Inventory
|
||||||
|
|
||||||
|
### Status Reports (Generated)
|
||||||
|
| File | Description | Last Updated |
|
||||||
|
|------|-------------|--------------|
|
||||||
|
| [LOCALE_STATUS_REPORT.md](./LOCALE_STATUS_REPORT.md) | i18n implementation status & progress | 2026-01-13 |
|
||||||
|
| [ROADMAP_SUMMARY.md](./ROADMAP_SUMMARY.md) | Roadmap with priorities and timeline | 2026-01-13 |
|
||||||
|
| [TESTING.md](./TESTING.md) | Test results and QA status | 2026-01-13 |
|
||||||
|
|
||||||
|
### Implementation Guides
|
||||||
|
| File | Description | Purpose |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| [AGENTS.md](./AGENTS.md) | Agent instructions for AI assistants | Implementation guidelines |
|
||||||
|
| [DOKPLOY_DEPLOYMENT.md](./DOKPLOY_DEPLOYMENT.md) | Dokploy API integration guide | Deployment orchestration |
|
||||||
|
| [DEPLOYMENT_STRATEGY.md](./DEPLOYMENT_STRATEGY.md) | Overall deployment architecture | System design |
|
||||||
|
| [SHARED_PROJECT_DEPLOYMENT.md](./SHARED_PROJECT_DEPLOYMENT.md) | Shared project configuration | Dokploy setup |
|
||||||
|
|
||||||
|
### System Design
|
||||||
|
| File | Description | Purpose |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| [PRODUCTION_API_SPEC.md](./PRODUCTION_API_SPEC.md) | REST API specification | API reference |
|
||||||
|
| [LOGGING-PLAN.md](./LOGGING-PLAN.md) | Monitoring & logging architecture | Observability |
|
||||||
|
| [MCP_SERVER_GUIDE.md](./MCP_SERVER_GUIDE.md) | MCP server implementation | Claude integration |
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
| File | Description | Purpose |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| [DOCKER_BUILD_FIX.md](./DOCKER_BUILD_FIX.md) | Docker build issues & solutions | Build troubleshooting |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ Quick Reference
|
||||||
|
|
||||||
|
### For Users
|
||||||
|
- Start here: [Main README](../README.md)
|
||||||
|
- Roadmap: [ROADMAP.md](../ROADMAP.md)
|
||||||
|
- Setup: [CLAUDE.md](../CLAUDE.md)
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
- Implementation: [AGENTS.md](./AGENTS.md)
|
||||||
|
- API Docs: [PRODUCTION_API_SPEC.md](./PRODUCTION_API_SPEC.md)
|
||||||
|
- Deployment: [DOKPLOY_DEPLOYMENT.md](./DOKPLOY_DEPLOYMENT.md)
|
||||||
|
|
||||||
|
### For Operations
|
||||||
|
- Monitoring: [LOGGING-PLAN.md](./LOGGING-PLAN.md)
|
||||||
|
- Troubleshooting: [DOCKER_BUILD_FIX.md](./DOCKER_BUILD_FIX.md)
|
||||||
|
- Testing: [TESTING.md](./TESTING.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Creating New Documentation
|
||||||
|
|
||||||
|
### Template
|
||||||
|
```markdown
|
||||||
|
# [Document Title]
|
||||||
|
|
||||||
|
**Last Updated**: YYYY-MM-DD
|
||||||
|
**Status**: [Draft|In Progress|Complete|Deprecated]
|
||||||
|
**Author**: [Name]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Brief description of what this document covers.
|
||||||
|
|
||||||
|
## [Main Sections]
|
||||||
|
...
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
- Link to related docs
|
||||||
|
- Cross-references
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Generated/Updated By**: [Name/Tool]
|
||||||
|
**Review Date**: YYYY-MM-DD
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [ ] Title clearly describes content
|
||||||
|
- [ ] Date stamps included
|
||||||
|
- [ ] Status indicator present
|
||||||
|
- [ ] Sections well-organized
|
||||||
|
- [ ] Cross-references added
|
||||||
|
- [ ] Added to this README.md index
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Update Workflow
|
||||||
|
|
||||||
|
1. **Create/Edit** documentation in `docs/`
|
||||||
|
2. **Update** this README.md index
|
||||||
|
3. **Commit** with descriptive message: `docs: add/update [FILENAME]`
|
||||||
|
4. **Review** outdated docs quarterly
|
||||||
|
5. **Archive** deprecated docs to `docs/archive/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Archive
|
||||||
|
|
||||||
|
See [docs/archive/](./archive/) for historical documentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2026-01-13
|
||||||
|
**Maintained By**: Project maintainers
|
||||||
288
docs/ROADMAP_SUMMARY.md
Normal file
288
docs/ROADMAP_SUMMARY.md
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
# AI Stack Deployer - Roadmap Summary
|
||||||
|
|
||||||
|
**Last Updated**: 2026-01-13
|
||||||
|
**Status**: Production-ready with active development
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Recently Completed (Last 4 Days)
|
||||||
|
|
||||||
|
### Jan 10-13, 2026
|
||||||
|
- ✅ Multi-language UI (NL, AR, EN) with RTL support
|
||||||
|
- ✅ React migration with WebGL design
|
||||||
|
- ✅ SSE deployment progress streaming
|
||||||
|
- ✅ Real-time name validation
|
||||||
|
- ✅ Docker build fix (hybrid Node.js/Bun strategy)
|
||||||
|
- ✅ Repository consolidation (3 repos → 1)
|
||||||
|
- ✅ Unified CI/CD pipeline
|
||||||
|
- ✅ Logging infrastructure (log-ingest → Loki → Grafana)
|
||||||
|
- ✅ AI Stack monitoring dashboard at logs.intra.flexinit.nl
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔥 Current Priority (HIGH)
|
||||||
|
|
||||||
|
### 1. Automated Cleanup System ⚠️ CRITICAL
|
||||||
|
**Problem**: Disk space exhaustion on Dokploy server (10.100.0.20) causes CI failures
|
||||||
|
|
||||||
|
**Target**: Keep 15GB+ free space (85% max usage)
|
||||||
|
|
||||||
|
**Components to Implement**:
|
||||||
|
```
|
||||||
|
[ ] CI workflow cleanup step
|
||||||
|
└─ Prune build cache after each build (keep 2GB)
|
||||||
|
└─ Remove images older than 24h
|
||||||
|
|
||||||
|
[ ] Server-side cron job
|
||||||
|
└─ Daily Docker system prune at 4 AM
|
||||||
|
└─ Remove volumes older than 72h
|
||||||
|
└─ Prune flexinit-runner builder cache (keep 5GB)
|
||||||
|
|
||||||
|
[ ] Disk monitoring
|
||||||
|
└─ Grafana alerts at 80% usage
|
||||||
|
└─ Slack/email notifications
|
||||||
|
|
||||||
|
[ ] Post-deployment cleanup
|
||||||
|
└─ Remove unused resources after stack deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation Files**:
|
||||||
|
- `.gitea/workflows/*.yaml` - Add cleanup steps
|
||||||
|
- `/etc/cron.d/docker-cleanup` on 10.100.0.20 - Cron job
|
||||||
|
- Grafana alert rules
|
||||||
|
|
||||||
|
**Current Disk Usage** (97GB total):
|
||||||
|
- Docker images: ~10GB
|
||||||
|
- Containers: ~1.5GB
|
||||||
|
- Volumes: ~30GB
|
||||||
|
- Build cache: Up to 6GB (needs pruning)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Web-based TUI Support 🚧 IN PROGRESS
|
||||||
|
**Goal**: Enable rich terminal UI apps (htop, lazygit, OpenCode TUI mode) in browser
|
||||||
|
|
||||||
|
**Completed** (Terminal Environment):
|
||||||
|
- ✅ TERM=xterm-256color
|
||||||
|
- ✅ COLORTERM=truecolor (24-bit color)
|
||||||
|
- ✅ ncurses-base and ncurses-term packages
|
||||||
|
- ✅ Locale configuration (en_US.UTF-8)
|
||||||
|
- ✅ Environment variables passed to stacks
|
||||||
|
|
||||||
|
**Remaining** (Direct Web Terminal):
|
||||||
|
```
|
||||||
|
[ ] Add ttyd to stack Docker image
|
||||||
|
[ ] Configure dual-port exposure:
|
||||||
|
├─ Port 8080 → OpenCode Web IDE
|
||||||
|
└─ Port 7681 → ttyd Raw Terminal
|
||||||
|
[ ] Update Traefik routing for terminal port
|
||||||
|
[ ] Test TUI applications:
|
||||||
|
├─ htop, btop (system monitoring)
|
||||||
|
├─ lazygit, lazydocker
|
||||||
|
├─ vim/neovim with full features
|
||||||
|
└─ OpenCode TUI mode
|
||||||
|
[ ] Document TUI capabilities for users
|
||||||
|
```
|
||||||
|
|
||||||
|
**Architecture**:
|
||||||
|
```
|
||||||
|
https://name.ai.flexinit.nl/ → OpenCode Web IDE
|
||||||
|
https://name.ai.flexinit.nl:7681/ → ttyd Raw Terminal
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Cases**:
|
||||||
|
- OpenCode TUI mode in browser
|
||||||
|
- Git TUIs (lazygit, tig)
|
||||||
|
- System monitoring (htop, btop)
|
||||||
|
- vim/neovim with full ncurses support
|
||||||
|
- Any ncurses-based application
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Next (Medium Priority)
|
||||||
|
|
||||||
|
### 3. User Authentication
|
||||||
|
```
|
||||||
|
[ ] Protect deployments with auth
|
||||||
|
[ ] User accounts and sessions
|
||||||
|
[ ] Stack ownership tracking
|
||||||
|
[ ] Permission management
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Rate Limiting
|
||||||
|
```
|
||||||
|
[ ] Prevent deployment abuse
|
||||||
|
[ ] Per-user quotas
|
||||||
|
[ ] API rate limiting
|
||||||
|
[ ] DDoS protection
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Stack Management UI
|
||||||
|
```
|
||||||
|
[ ] List user's stacks
|
||||||
|
[ ] Delete stack functionality
|
||||||
|
[ ] View stack status/health
|
||||||
|
[ ] Restart/redeploy actions
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔮 Later (Backlog)
|
||||||
|
|
||||||
|
### Testing & Quality
|
||||||
|
- [ ] Unit tests for validation logic
|
||||||
|
- [ ] Integration tests for deployment flow
|
||||||
|
- [ ] E2E tests for UI workflows
|
||||||
|
- [ ] Visual regression tests
|
||||||
|
- [ ] Load testing
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- [ ] Resource limits configuration (CPU/memory)
|
||||||
|
- [ ] Custom domain support (bring your own domain)
|
||||||
|
- [ ] Image versioning (semantic versions + rollback)
|
||||||
|
- [ ] Auto-cleanup of abandoned stacks (inactive > 30 days)
|
||||||
|
- [ ] Multi-region deployment
|
||||||
|
- [ ] Stack templates (pre-configured environments)
|
||||||
|
|
||||||
|
### Internationalization
|
||||||
|
- [ ] Add French (fr) for Belgium market
|
||||||
|
- [ ] Add German (de) for DACH region
|
||||||
|
- [ ] Add Spanish (es) for global reach
|
||||||
|
- [ ] Extract translations to JSON files (optional)
|
||||||
|
- [ ] Translation management workflow
|
||||||
|
|
||||||
|
### Observability
|
||||||
|
- [ ] Usage analytics dashboard
|
||||||
|
- [ ] Billing integration
|
||||||
|
- [ ] Performance optimization tracking
|
||||||
|
- [ ] Security auditing
|
||||||
|
- [ ] User behavior insights
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Key Metrics to Track
|
||||||
|
|
||||||
|
### Deployment Success Rate
|
||||||
|
- **Target**: > 95%
|
||||||
|
- **Current**: Not tracked
|
||||||
|
|
||||||
|
### Disk Space Usage
|
||||||
|
- **Target**: < 85% (15GB+ free)
|
||||||
|
- **Current**: ~82% (17GB free)
|
||||||
|
- **Alert**: 80%
|
||||||
|
|
||||||
|
### CI/CD Pipeline
|
||||||
|
- **Target**: < 5 min build time
|
||||||
|
- **Current**: ~3-4 min
|
||||||
|
- **Bottleneck**: Docker build cache
|
||||||
|
|
||||||
|
### Stack Uptime
|
||||||
|
- **Target**: > 99%
|
||||||
|
- **Current**: Monitored via Grafana
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Technical Debt
|
||||||
|
|
||||||
|
### High Priority
|
||||||
|
1. ⚠️ **Clean up deprecated files**
|
||||||
|
- `src/frontend/` (legacy vanilla JS UI)
|
||||||
|
- Old `app.js` with inline i18n (replaced by React)
|
||||||
|
- Outdated documentation references
|
||||||
|
|
||||||
|
2. ⚠️ **Add automated tests**
|
||||||
|
- No unit tests for validation logic
|
||||||
|
- No integration tests for deployment flow
|
||||||
|
- No i18n tests
|
||||||
|
|
||||||
|
### Medium Priority
|
||||||
|
3. **Improve error handling**
|
||||||
|
- Better error messages for API failures
|
||||||
|
- Retry logic for transient failures
|
||||||
|
- Rollback on partial deployment failures
|
||||||
|
|
||||||
|
4. **Refactor deployment orchestrator**
|
||||||
|
- Split into smaller, testable functions
|
||||||
|
- Add progress callback abstraction
|
||||||
|
- Improve state management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Recent Achievements Timeline
|
||||||
|
|
||||||
|
| Date | Achievement | Impact |
|
||||||
|
|------|-------------|--------|
|
||||||
|
| 2026-01-13 | Multilingual deployment progress | Backend sends localized SSE events |
|
||||||
|
| 2026-01-13 | WebGL background effect fix | Clearer, more visible animation |
|
||||||
|
| 2026-01-13 | Docker Compose variable fix | Deployment works correctly |
|
||||||
|
| 2026-01-13 | TUI environment variables | Stacks support rich terminal UIs |
|
||||||
|
| 2026-01-10 | Dutch SEO metadata | Better search visibility |
|
||||||
|
| 2026-01-10 | React migration complete | Modern UI with 87KB gzipped bundle |
|
||||||
|
| 2026-01-10 | Multi-language UI | EN/NL/AR with RTL support |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Growth Roadmap
|
||||||
|
|
||||||
|
### Phase 1: Stability (Current)
|
||||||
|
- ✅ Core deployment working
|
||||||
|
- ✅ Multi-language support
|
||||||
|
- 🚧 Automated cleanup
|
||||||
|
- 🚧 TUI support
|
||||||
|
|
||||||
|
### Phase 2: Scale (Q1 2026)
|
||||||
|
- Authentication & authorization
|
||||||
|
- Rate limiting
|
||||||
|
- Stack management UI
|
||||||
|
- Monitoring & alerting
|
||||||
|
|
||||||
|
### Phase 3: Features (Q2 2026)
|
||||||
|
- Custom domains
|
||||||
|
- Resource limits
|
||||||
|
- Stack templates
|
||||||
|
- Auto-cleanup
|
||||||
|
|
||||||
|
### Phase 4: Enterprise (Q3 2026)
|
||||||
|
- Multi-region deployment
|
||||||
|
- Team collaboration
|
||||||
|
- Billing integration
|
||||||
|
- Advanced analytics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Success Criteria
|
||||||
|
|
||||||
|
### For "Automated Cleanup System"
|
||||||
|
✅ Complete when:
|
||||||
|
- CI builds never fail due to disk space
|
||||||
|
- Disk usage stays below 80%
|
||||||
|
- Automated alerts working
|
||||||
|
- Documentation updated
|
||||||
|
|
||||||
|
### For "Web-based TUI Support"
|
||||||
|
✅ Complete when:
|
||||||
|
- htop renders correctly in browser
|
||||||
|
- lazygit works fully functional
|
||||||
|
- OpenCode TUI mode accessible
|
||||||
|
- User documentation published
|
||||||
|
- No performance degradation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Contact & Contribution
|
||||||
|
|
||||||
|
**Owner**: Oussama Douhou
|
||||||
|
**Repository**: flexinit/agent-stack
|
||||||
|
**Monitoring**: https://logs.intra.flexinit.nl/d/ai-stack-overview
|
||||||
|
**CI/CD**: https://git.app.flexinit.nl/flexinit/agent-stack/actions
|
||||||
|
|
||||||
|
**Want to Contribute?**
|
||||||
|
1. Check open items in roadmap
|
||||||
|
2. Consult AGENTS.md for implementation guidelines
|
||||||
|
3. Read CLAUDE.md for architecture
|
||||||
|
4. Submit PR to dev branch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Generated**: 2026-01-13 21:20 CET
|
||||||
|
**Sources**: ROADMAP.md, git history, test results, documentation
|
||||||
313
docs/SHARED_PROJECT_DEPLOYMENT.md
Normal file
313
docs/SHARED_PROJECT_DEPLOYMENT.md
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
# Shared Project Deployment Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The AI Stack Deployer portal deploys **all user AI stacks to a single shared Dokploy project** instead of creating a new project for each user.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Dokploy: ai-stack-portal (Shared Project) │
|
||||||
|
│ ID: 2y2Glhz5Wy0dBNf6BOR_- │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 📦 Portal Application: ai-stack-deployer-prod │
|
||||||
|
│ ├─ Domain: portal.ai.flexinit.nl │
|
||||||
|
│ ├─ Image: git.app.flexinit.nl/.../ai-stack-deployer:latest│
|
||||||
|
│ └─ Env: SHARED_PROJECT_ID=$${{project.SHARED_PROJECT_ID}} │
|
||||||
|
│ │
|
||||||
|
│ ───────────────────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ 📦 User Stack: john-dev │
|
||||||
|
│ ├─ Domain: john-dev.ai.flexinit.nl │
|
||||||
|
│ ├─ Image: git.app.flexinit.nl/.../agent-stack:latest │
|
||||||
|
│ └─ Deployed by: Portal │
|
||||||
|
│ │
|
||||||
|
│ 📦 User Stack: jane-prod │
|
||||||
|
│ ├─ Domain: jane-prod.ai.flexinit.nl │
|
||||||
|
│ ├─ Image: git.app.flexinit.nl/.../agent-stack:latest │
|
||||||
|
│ └─ Deployed by: Portal │
|
||||||
|
│ │
|
||||||
|
│ 📦 User Stack: alice-test │
|
||||||
|
│ ├─ Domain: alice-test.ai.flexinit.nl │
|
||||||
|
│ ├─ Image: git.app.flexinit.nl/.../agent-stack:latest │
|
||||||
|
│ └─ Deployed by: Portal │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Step 1: Portal Reads Configuration
|
||||||
|
|
||||||
|
When a user submits a stack name (e.g., "john-dev"), the portal:
|
||||||
|
|
||||||
|
1. **Reads environment variables**:
|
||||||
|
```javascript
|
||||||
|
const sharedProjectId = process.env.SHARED_PROJECT_ID;
|
||||||
|
const sharedEnvironmentId = process.env.SHARED_ENVIRONMENT_ID;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **These are set via Dokploy's project-level variables**:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- SHARED_PROJECT_ID=$${{project.SHARED_PROJECT_ID}}
|
||||||
|
- SHARED_ENVIRONMENT_ID=$${{project.SHARED_ENVIRONMENT_ID}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: The double `$$` is required to escape the dollar sign in Docker Compose.
|
||||||
|
|
||||||
|
### Step 2: Portal Deploys to Shared Project
|
||||||
|
|
||||||
|
Instead of creating a new project, the portal:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// OLD BEHAVIOR (legacy):
|
||||||
|
// createProject(`ai-stack-${username}`) ❌ Creates new project per user
|
||||||
|
|
||||||
|
// NEW BEHAVIOR (current):
|
||||||
|
// Uses existing shared project ID ✅
|
||||||
|
const projectId = sharedProjectId; // From environment variable
|
||||||
|
const environmentId = sharedEnvironmentId;
|
||||||
|
|
||||||
|
// Creates application IN the shared project
|
||||||
|
createApplication({
|
||||||
|
projectId: projectId,
|
||||||
|
environmentId: environmentId,
|
||||||
|
name: `${username}-stack`,
|
||||||
|
image: 'git.app.flexinit.nl/.../agent-stack:latest',
|
||||||
|
domain: `${username}.ai.flexinit.nl`
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: User Accesses Their Stack
|
||||||
|
|
||||||
|
User visits `https://john-dev.ai.flexinit.nl` → Traefik routes to their application inside the shared project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Steps
|
||||||
|
|
||||||
|
### 1. Create Shared Project in Dokploy
|
||||||
|
|
||||||
|
1. In Dokploy UI, create project:
|
||||||
|
- **Name**: `ai-stack-portal`
|
||||||
|
- **Description**: "Shared project for all user AI stacks"
|
||||||
|
|
||||||
|
2. Get the **Project ID**:
|
||||||
|
```bash
|
||||||
|
# Via API
|
||||||
|
curl -s "http://10.100.0.20:3000/api/project.all" \
|
||||||
|
-H "Authorization: Bearer $DOKPLOY_API_TOKEN" | \
|
||||||
|
jq -r '.[] | select(.name=="ai-stack-portal") | .id'
|
||||||
|
|
||||||
|
# Output: 2y2Glhz5Wy0dBNf6BOR_-
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Get the **Environment ID**:
|
||||||
|
```bash
|
||||||
|
curl -s "http://10.100.0.20:3000/api/project.one?projectId=2y2Glhz5Wy0dBNf6BOR_-" \
|
||||||
|
-H "Authorization: Bearer $DOKPLOY_API_TOKEN" | \
|
||||||
|
jq -r '.environments[0].id'
|
||||||
|
|
||||||
|
# Output: RqE9OFMdLwkzN7pif1xN8
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Set Project-Level Variables
|
||||||
|
|
||||||
|
In the shared project (`ai-stack-portal`), add these **project-level environment variables**:
|
||||||
|
|
||||||
|
| Variable | Value | Example |
|
||||||
|
|----------|-------|---------|
|
||||||
|
| `SHARED_PROJECT_ID` | Your project ID | `2y2Glhz5Wy0dBNf6BOR_-` |
|
||||||
|
| `SHARED_ENVIRONMENT_ID` | Your environment ID | `RqE9OFMdLwkzN7pif1xN8` |
|
||||||
|
|
||||||
|
**How to set in Dokploy UI**:
|
||||||
|
- Go to Project → Settings → Environment Variables
|
||||||
|
- Add variables at **project level** (not application level)
|
||||||
|
|
||||||
|
### 3. Deploy Portal Application
|
||||||
|
|
||||||
|
Deploy the portal **inside the same shared project**:
|
||||||
|
|
||||||
|
1. **Application Details**:
|
||||||
|
- Name: `ai-stack-deployer-prod`
|
||||||
|
- Type: Docker Compose
|
||||||
|
- Compose File: `docker-compose.prod.yml`
|
||||||
|
- Branch: `main`
|
||||||
|
|
||||||
|
2. **The docker-compose file automatically references project variables**:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- SHARED_PROJECT_ID=${{project.SHARED_PROJECT_ID}} # ← Magic happens here
|
||||||
|
- SHARED_ENVIRONMENT_ID=${{project.SHARED_ENVIRONMENT_ID}}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Dokploy resolves `${{project.VAR}}`** to the actual value from project-level variables.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### ✅ Centralized Management
|
||||||
|
All user stacks in one place:
|
||||||
|
- Easy to list all active stacks
|
||||||
|
- Shared monitoring dashboard
|
||||||
|
- Centralized logging
|
||||||
|
|
||||||
|
### ✅ Resource Efficiency
|
||||||
|
- No overhead of separate projects per user
|
||||||
|
- Shared network and resources
|
||||||
|
- Easier to manage quotas
|
||||||
|
|
||||||
|
### ✅ Simplified Configuration
|
||||||
|
- Project-level environment variables shared by all stacks
|
||||||
|
- Single source of truth for common configs
|
||||||
|
- Easy to update STACK_IMAGE for all users
|
||||||
|
|
||||||
|
### ✅ Better Organization
|
||||||
|
```
|
||||||
|
Projects in Dokploy:
|
||||||
|
├── ai-stack-portal (500 user applications) ✅ Clean
|
||||||
|
└── NOT:
|
||||||
|
├── ai-stack-john
|
||||||
|
├── ai-stack-jane
|
||||||
|
├── ai-stack-alice
|
||||||
|
└── ... (500 separate projects) ❌ Messy
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fallback Behavior
|
||||||
|
|
||||||
|
If `SHARED_PROJECT_ID` and `SHARED_ENVIRONMENT_ID` are **not set**, the portal falls back to **legacy behavior**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Code in src/orchestrator/production-deployer.ts (lines 187-196)
|
||||||
|
const sharedProjectId = config.sharedProjectId || process.env.SHARED_PROJECT_ID;
|
||||||
|
const sharedEnvironmentId = config.sharedEnvironmentId || process.env.SHARED_ENVIRONMENT_ID;
|
||||||
|
|
||||||
|
if (sharedProjectId && sharedEnvironmentId) {
|
||||||
|
// Use shared project ✅
|
||||||
|
state.resources.projectId = sharedProjectId;
|
||||||
|
state.resources.environmentId = sharedEnvironmentId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Create separate project per user ⚠️
|
||||||
|
const projectName = `ai-stack-${config.stackName}`;
|
||||||
|
const existingProject = await this.client.findProjectByName(projectName);
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**This ensures backwards compatibility** but is not recommended.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Portal Creates Separate Projects Instead of Using Shared Project
|
||||||
|
|
||||||
|
**Cause**: `SHARED_PROJECT_ID` or `SHARED_ENVIRONMENT_ID` not set.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check project-level variables in Dokploy:
|
||||||
|
```bash
|
||||||
|
curl -s "http://10.100.0.20:3000/api/project.one?projectId=YOUR_PROJECT_ID" \
|
||||||
|
-H "Authorization: Bearer $DOKPLOY_API_TOKEN" | \
|
||||||
|
jq '.environmentVariables'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ensure the portal application's docker-compose references them:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- SHARED_PROJECT_ID=${{project.SHARED_PROJECT_ID}}
|
||||||
|
- SHARED_ENVIRONMENT_ID=${{project.SHARED_ENVIRONMENT_ID}}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Redeploy the portal application.
|
||||||
|
|
||||||
|
### Variable Reference Not Working
|
||||||
|
|
||||||
|
**Symptom**: Portal logs show `undefined` for `SHARED_PROJECT_ID`.
|
||||||
|
|
||||||
|
**Cause**: Using wrong syntax.
|
||||||
|
|
||||||
|
**Correct syntax**:
|
||||||
|
```yaml
|
||||||
|
- SHARED_PROJECT_ID=${{project.SHARED_PROJECT_ID}} ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wrong syntax**:
|
||||||
|
```yaml
|
||||||
|
- SHARED_PROJECT_ID=${SHARED_PROJECT_ID} ❌ (shell substitution, not Dokploy)
|
||||||
|
- SHARED_PROJECT_ID={{project.SHARED_PROJECT_ID}} ❌ (missing $)
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to Verify Configuration
|
||||||
|
|
||||||
|
Check portal container environment:
|
||||||
|
```bash
|
||||||
|
# SSH into Dokploy host
|
||||||
|
ssh user@10.100.0.20
|
||||||
|
|
||||||
|
# Inspect portal container
|
||||||
|
docker exec ai-stack-deployer env | grep SHARED
|
||||||
|
|
||||||
|
# Should show:
|
||||||
|
SHARED_PROJECT_ID=2y2Glhz5Wy0dBNf6BOR_-
|
||||||
|
SHARED_ENVIRONMENT_ID=RqE9OFMdLwkzN7pif1xN8
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment-Specific Shared Projects
|
||||||
|
|
||||||
|
You can have **separate shared projects for dev/staging/prod**:
|
||||||
|
|
||||||
|
| Portal Environment | Shared Project | Purpose |
|
||||||
|
|--------------------|----------------|---------|
|
||||||
|
| Dev | `ai-stack-portal-dev` | Development user stacks |
|
||||||
|
| Staging | `ai-stack-portal-staging` | Staging user stacks |
|
||||||
|
| Prod | `ai-stack-portal` | Production user stacks |
|
||||||
|
|
||||||
|
Each portal deployment references its own shared project:
|
||||||
|
- `portal-dev.ai.flexinit.nl` → `ai-stack-portal-dev`
|
||||||
|
- `portal-staging.ai.flexinit.nl` → `ai-stack-portal-staging`
|
||||||
|
- `portal.ai.flexinit.nl` → `ai-stack-portal`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration from Legacy
|
||||||
|
|
||||||
|
If you're currently using the legacy behavior (separate projects per user):
|
||||||
|
|
||||||
|
### Option 1: Gradual Migration
|
||||||
|
- New deployments use shared project
|
||||||
|
- Old deployments remain in separate projects
|
||||||
|
- Migrate old stacks manually over time
|
||||||
|
|
||||||
|
### Option 2: Full Migration
|
||||||
|
1. Create shared project
|
||||||
|
2. Set project-level variables
|
||||||
|
3. Redeploy all user stacks to shared project
|
||||||
|
4. Delete old separate projects
|
||||||
|
|
||||||
|
**Note**: Migration requires downtime for each stack being moved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- **Environment Variable Syntax**: See Dokploy docs on project-level variables
|
||||||
|
- **Code Location**: `src/orchestrator/production-deployer.ts` (lines 178-200)
|
||||||
|
- **Example IDs**: `.env.example` (lines 25-27)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Questions?** Check the main deployment guide: `DOKPLOY_DEPLOYMENT.md`
|
||||||
110
docs/TESTING.md
110
docs/TESTING.md
@@ -261,3 +261,113 @@ Authorization: token <your-api-token>
|
|||||||
|-----|---------|
|
|-----|---------|
|
||||||
| `GITEA_API_TOKEN` | Gitea API access for workflow status |
|
| `GITEA_API_TOKEN` | Gitea API access for workflow status |
|
||||||
| `DOKPLOY_API_TOKEN` | Dokploy deployment API (BWS ID: `6b3618fc-ba02-49bc-bdc8-b3c9004087bc`) |
|
| `DOKPLOY_API_TOKEN` | Dokploy deployment API (BWS ID: `6b3618fc-ba02-49bc-bdc8-b3c9004087bc`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Session: 2026-01-13
|
||||||
|
|
||||||
|
### Session Summary
|
||||||
|
|
||||||
|
**Goal:** Verify multi-environment deployment setup and shared project configuration.
|
||||||
|
|
||||||
|
### Completed Tasks
|
||||||
|
|
||||||
|
| Task | Status | Evidence |
|
||||||
|
|------|--------|----------|
|
||||||
|
| Workflow separation (dev/staging/main) | ✅ | Committed as `eb2745d` |
|
||||||
|
| Dollar sign escaping (`$${{project.VAR}}`) | ✅ | Verified in all docker-compose.*.yml |
|
||||||
|
| Shared project exists | ✅ | `ai-stack-portal` (ID: `2y2Glhz5Wy0dBNf6BOR_-`) |
|
||||||
|
| Environment IDs retrieved | ✅ | See below |
|
||||||
|
| Local dev server health | ✅ | `/health` returns healthy |
|
||||||
|
|
||||||
|
### Environment IDs
|
||||||
|
|
||||||
|
```
|
||||||
|
Project: ai-stack-portal
|
||||||
|
ID: 2y2Glhz5Wy0dBNf6BOR_-
|
||||||
|
|
||||||
|
Environments:
|
||||||
|
- production: _dKAmxVcadqi-z73wKpEB (default)
|
||||||
|
- deployments: RqE9OFMdLwkzN7pif1xN8 (for user stacks)
|
||||||
|
- test: KVKn5fXGz10g7KVxPWOQj
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blockers Identified
|
||||||
|
|
||||||
|
#### BLOCKER: Dokploy API Token Permissions
|
||||||
|
|
||||||
|
**Symptom:** All Dokploy API calls return `Forbidden`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Previously working
|
||||||
|
curl -s "https://app.flexinit.nl/api/project.all" -H "x-api-key: $DOKPLOY_API_TOKEN"
|
||||||
|
# Now returns: Forbidden
|
||||||
|
|
||||||
|
# Environment endpoint
|
||||||
|
curl -s "https://app.flexinit.nl/api/environment.one?environmentId=RqE9OFMdLwkzN7pif1xN8" -H "x-api-key: $DOKPLOY_API_TOKEN"
|
||||||
|
# Returns: Forbidden
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root Cause:** The API token `app_deployment...` has been revoked or has limited scope.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- Cannot verify Docker image exists in registry
|
||||||
|
- Cannot test name availability (requires `environment.one`)
|
||||||
|
- Cannot create applications or compose stacks
|
||||||
|
- Cannot deploy portal to Dokploy
|
||||||
|
|
||||||
|
**Resolution Required:**
|
||||||
|
1. Log into Dokploy UI at https://app.flexinit.nl
|
||||||
|
2. Navigate to Settings → API Keys
|
||||||
|
3. Generate new API key with full permissions:
|
||||||
|
- Read/Write access to projects
|
||||||
|
- Read/Write access to applications
|
||||||
|
- Read/Write access to compose stacks
|
||||||
|
- Read/Write access to domains
|
||||||
|
4. Update `.env` with new token
|
||||||
|
5. Update BWS secret (ID: `6b3618fc-ba02-49bc-bdc8-b3c9004087bc`)
|
||||||
|
|
||||||
|
### Local Testing Results
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health check - WORKS
|
||||||
|
curl -s "http://localhost:3000/health"
|
||||||
|
# {"status":"healthy","timestamp":"2026-01-13T13:01:46.100Z","version":"0.2.0",...}
|
||||||
|
|
||||||
|
# Name check - FAILS (API token issue)
|
||||||
|
curl -s "http://localhost:3000/api/check/test-stack"
|
||||||
|
# {"available":false,"valid":false,"error":"Failed to check availability"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required .env Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Added for shared project deployment
|
||||||
|
SHARED_PROJECT_ID=2y2Glhz5Wy0dBNf6BOR_-
|
||||||
|
SHARED_ENVIRONMENT_ID=RqE9OFMdLwkzN7pif1xN8
|
||||||
|
```
|
||||||
|
|
||||||
|
### Next Steps After Token Fix
|
||||||
|
|
||||||
|
1. Verify `project.all` API works with new token
|
||||||
|
2. Deploy portal to Dokploy (docker-compose.dev.yml)
|
||||||
|
3. Test end-to-end stack deployment
|
||||||
|
4. Verify stacks deploy to shared project
|
||||||
|
5. Clean up test deployments
|
||||||
|
|
||||||
|
### Commands Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test API token
|
||||||
|
source .env && curl -s "https://app.flexinit.nl/api/project.all" \
|
||||||
|
-H "x-api-key: $DOKPLOY_API_TOKEN" | jq '.[].name'
|
||||||
|
|
||||||
|
# Get environment applications
|
||||||
|
source .env && curl -s "https://app.flexinit.nl/api/environment.one?environmentId=RqE9OFMdLwkzN7pif1xN8" \
|
||||||
|
-H "x-api-key: $DOKPLOY_API_TOKEN" | jq '.applications'
|
||||||
|
|
||||||
|
# Deploy test stack
|
||||||
|
curl -X POST http://localhost:3000/api/deploy \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"test-'$(date +%s | tail -c 4)'"}'
|
||||||
|
```
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import type { DeploymentState as OrchestratorDeploymentState } from './orchestra
|
|||||||
const PORT = parseInt(process.env.PORT || '3000', 10);
|
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||||
const HOST = process.env.HOST || '0.0.0.0';
|
const HOST = process.env.HOST || '0.0.0.0';
|
||||||
|
|
||||||
// Extended deployment state for HTTP server (adds logs)
|
// Extended deployment state for HTTP server (adds logs and language)
|
||||||
interface HttpDeploymentState extends OrchestratorDeploymentState {
|
interface HttpDeploymentState extends OrchestratorDeploymentState {
|
||||||
logs: string[];
|
logs: string[];
|
||||||
|
lang: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deployments = new Map<string, HttpDeploymentState>();
|
const deployments = new Map<string, HttpDeploymentState>();
|
||||||
@@ -90,6 +91,7 @@ async function deployStack(deploymentId: string): Promise<void> {
|
|||||||
registryId: process.env.STACK_REGISTRY_ID,
|
registryId: process.env.STACK_REGISTRY_ID,
|
||||||
sharedProjectId: process.env.SHARED_PROJECT_ID,
|
sharedProjectId: process.env.SHARED_PROJECT_ID,
|
||||||
sharedEnvironmentId: process.env.SHARED_ENVIRONMENT_ID,
|
sharedEnvironmentId: process.env.SHARED_ENVIRONMENT_ID,
|
||||||
|
lang: deployment.lang,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Final update with logs
|
// Final update with logs
|
||||||
@@ -144,7 +146,7 @@ app.get('/health', (c) => {
|
|||||||
app.post('/api/deploy', async (c) => {
|
app.post('/api/deploy', async (c) => {
|
||||||
try {
|
try {
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const { name } = body;
|
const { name, lang = 'en' } = body;
|
||||||
|
|
||||||
// Validate name
|
// Validate name
|
||||||
const validation = validateStackName(name);
|
const validation = validateStackName(name);
|
||||||
@@ -197,6 +199,7 @@ app.post('/api/deploy', async (c) => {
|
|||||||
started: new Date().toISOString(),
|
started: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
logs: [],
|
logs: [],
|
||||||
|
lang,
|
||||||
};
|
};
|
||||||
|
|
||||||
deployments.set(deploymentId, deployment);
|
deployments.set(deploymentId, deployment);
|
||||||
|
|||||||
65
src/lib/i18n-backend.ts
Normal file
65
src/lib/i18n-backend.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
export const backendTranslations = {
|
||||||
|
en: {
|
||||||
|
'initializing': 'Initializing deployment',
|
||||||
|
'creatingProject': 'Creating project',
|
||||||
|
'gettingEnvironment': 'Getting environment ID',
|
||||||
|
'environmentAvailable': 'Environment ID already available',
|
||||||
|
'environmentRetrieved': 'Environment ID retrieved',
|
||||||
|
'creatingApplication': 'Creating application',
|
||||||
|
'configuringApplication': 'Configuring application',
|
||||||
|
'creatingDomain': 'Creating domain',
|
||||||
|
'deployingApplication': 'Deploying application',
|
||||||
|
'waitingForSSL': 'Waiting for SSL certificate provisioning...',
|
||||||
|
'waitingForStart': 'Waiting for application to start',
|
||||||
|
'deploymentSuccess': 'Application deployed successfully',
|
||||||
|
'verifyingHealth': 'Verifying application health',
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
'initializing': 'Implementatie initialiseren',
|
||||||
|
'creatingProject': 'Project aanmaken',
|
||||||
|
'gettingEnvironment': 'Omgeving ID ophalen',
|
||||||
|
'environmentAvailable': 'Omgeving ID al beschikbaar',
|
||||||
|
'environmentRetrieved': 'Omgeving ID opgehaald',
|
||||||
|
'creatingApplication': 'Applicatie aanmaken',
|
||||||
|
'configuringApplication': 'Applicatie configureren',
|
||||||
|
'creatingDomain': 'Domein aanmaken',
|
||||||
|
'deployingApplication': 'Applicatie implementeren',
|
||||||
|
'waitingForSSL': 'Wachten op SSL-certificaat...',
|
||||||
|
'waitingForStart': 'Wachten tot applicatie start',
|
||||||
|
'deploymentSuccess': 'Applicatie succesvol geïmplementeerd',
|
||||||
|
'verifyingHealth': 'Applicatie gezondheid verifiëren',
|
||||||
|
},
|
||||||
|
ar: {
|
||||||
|
'initializing': 'جاري التهيئة',
|
||||||
|
'creatingProject': 'إنشاء المشروع',
|
||||||
|
'gettingEnvironment': 'الحصول على معرف البيئة',
|
||||||
|
'environmentAvailable': 'معرف البيئة متاح بالفعل',
|
||||||
|
'environmentRetrieved': 'تم استرداد معرف البيئة',
|
||||||
|
'creatingApplication': 'إنشاء التطبيق',
|
||||||
|
'configuringApplication': 'تكوين التطبيق',
|
||||||
|
'creatingDomain': 'إنشاء النطاق',
|
||||||
|
'deployingApplication': 'نشر التطبيق',
|
||||||
|
'waitingForSSL': 'انتظار شهادة SSL...',
|
||||||
|
'waitingForStart': 'انتظار بدء التطبيق',
|
||||||
|
'deploymentSuccess': 'تم نشر التطبيق بنجاح',
|
||||||
|
'verifyingHealth': 'التحقق من صحة التطبيق',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type BackendLanguage = keyof typeof backendTranslations;
|
||||||
|
export type BackendTranslationKey = keyof typeof backendTranslations.en;
|
||||||
|
|
||||||
|
export function createTranslator(lang: BackendLanguage = 'en') {
|
||||||
|
return (key: BackendTranslationKey, params?: Record<string, string | number>): string => {
|
||||||
|
const translations = backendTranslations[lang] || backendTranslations.en;
|
||||||
|
let text: string = translations[key];
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
Object.entries(params).forEach(([paramKey, value]) => {
|
||||||
|
text = text.replace(`{${paramKey}}`, String(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DokployProductionClient } from '../api/dokploy-production.js';
|
import { DokployProductionClient } from '../api/dokploy-production.js';
|
||||||
|
import { createTranslator, type BackendLanguage } from '../lib/i18n-backend.js';
|
||||||
|
|
||||||
export interface DeploymentConfig {
|
export interface DeploymentConfig {
|
||||||
stackName: string;
|
stackName: string;
|
||||||
@@ -22,6 +23,7 @@ export interface DeploymentConfig {
|
|||||||
registryId?: string;
|
registryId?: string;
|
||||||
sharedProjectId?: string;
|
sharedProjectId?: string;
|
||||||
sharedEnvironmentId?: string;
|
sharedEnvironmentId?: string;
|
||||||
|
lang?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeploymentState {
|
export interface DeploymentState {
|
||||||
@@ -71,10 +73,12 @@ export type ProgressCallback = (state: DeploymentState) => void;
|
|||||||
export class ProductionDeployer {
|
export class ProductionDeployer {
|
||||||
private client: DokployProductionClient;
|
private client: DokployProductionClient;
|
||||||
private progressCallback?: ProgressCallback;
|
private progressCallback?: ProgressCallback;
|
||||||
|
private t: ReturnType<typeof createTranslator>;
|
||||||
|
|
||||||
constructor(client: DokployProductionClient, progressCallback?: ProgressCallback) {
|
constructor(client: DokployProductionClient, progressCallback?: ProgressCallback) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.progressCallback = progressCallback;
|
this.progressCallback = progressCallback;
|
||||||
|
this.t = createTranslator('en');
|
||||||
}
|
}
|
||||||
|
|
||||||
private notifyProgress(state: DeploymentState): void {
|
private notifyProgress(state: DeploymentState): void {
|
||||||
@@ -87,13 +91,15 @@ export class ProductionDeployer {
|
|||||||
* Deploy a complete AI stack with full production safeguards
|
* Deploy a complete AI stack with full production safeguards
|
||||||
*/
|
*/
|
||||||
async deploy(config: DeploymentConfig): Promise<DeploymentResult> {
|
async deploy(config: DeploymentConfig): Promise<DeploymentResult> {
|
||||||
|
this.t = createTranslator((config.lang || 'en') as BackendLanguage);
|
||||||
|
|
||||||
const state: DeploymentState = {
|
const state: DeploymentState = {
|
||||||
id: `dep_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
id: `dep_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
||||||
stackName: config.stackName,
|
stackName: config.stackName,
|
||||||
phase: 'initializing',
|
phase: 'initializing',
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
message: 'Initializing deployment',
|
message: this.t('initializing'),
|
||||||
resources: {},
|
resources: {},
|
||||||
timestamps: {
|
timestamps: {
|
||||||
started: new Date().toISOString(),
|
started: new Date().toISOString(),
|
||||||
@@ -228,12 +234,12 @@ export class ProductionDeployer {
|
|||||||
private async getEnvironment(state: DeploymentState): Promise<void> {
|
private async getEnvironment(state: DeploymentState): Promise<void> {
|
||||||
state.phase = 'getting_environment';
|
state.phase = 'getting_environment';
|
||||||
state.progress = 25;
|
state.progress = 25;
|
||||||
state.message = 'Getting environment ID';
|
state.message = this.t('gettingEnvironment');
|
||||||
|
|
||||||
// Skip if we already have environment ID from project creation
|
// Skip if we already have environment ID from project creation
|
||||||
if (state.resources.environmentId) {
|
if (state.resources.environmentId) {
|
||||||
console.log('Environment ID already available from project creation');
|
console.log('Environment ID already available from project creation');
|
||||||
state.message = 'Environment ID already available';
|
state.message = this.t('environmentAvailable');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +249,7 @@ export class ProductionDeployer {
|
|||||||
|
|
||||||
const environment = await this.client.getDefaultEnvironment(state.resources.projectId);
|
const environment = await this.client.getDefaultEnvironment(state.resources.projectId);
|
||||||
state.resources.environmentId = environment.environmentId;
|
state.resources.environmentId = environment.environmentId;
|
||||||
state.message = 'Environment ID retrieved';
|
state.message = this.t('environmentRetrieved');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createOrFindApplication(
|
private async createOrFindApplication(
|
||||||
@@ -252,7 +258,7 @@ export class ProductionDeployer {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
state.phase = 'creating_application';
|
state.phase = 'creating_application';
|
||||||
state.progress = 40;
|
state.progress = 40;
|
||||||
state.message = 'Creating application';
|
state.message = this.t('creatingApplication');
|
||||||
|
|
||||||
if (!state.resources.environmentId) {
|
if (!state.resources.environmentId) {
|
||||||
throw new Error('Environment ID not available');
|
throw new Error('Environment ID not available');
|
||||||
@@ -279,7 +285,7 @@ export class ProductionDeployer {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
state.phase = 'configuring_application';
|
state.phase = 'configuring_application';
|
||||||
state.progress = 50;
|
state.progress = 50;
|
||||||
state.message = 'Configuring application with Docker image';
|
state.message = this.t('configuringApplication');
|
||||||
|
|
||||||
if (!state.resources.applicationId) {
|
if (!state.resources.applicationId) {
|
||||||
throw new Error('Application ID not available');
|
throw new Error('Application ID not available');
|
||||||
@@ -332,7 +338,7 @@ export class ProductionDeployer {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
state.phase = 'creating_domain';
|
state.phase = 'creating_domain';
|
||||||
state.progress = 70;
|
state.progress = 70;
|
||||||
state.message = 'Creating domain';
|
state.message = this.t('creatingDomain');
|
||||||
|
|
||||||
if (!state.resources.applicationId) {
|
if (!state.resources.applicationId) {
|
||||||
throw new Error('Application ID not available');
|
throw new Error('Application ID not available');
|
||||||
@@ -359,7 +365,7 @@ export class ProductionDeployer {
|
|||||||
private async deployApplication(state: DeploymentState): Promise<void> {
|
private async deployApplication(state: DeploymentState): Promise<void> {
|
||||||
state.phase = 'deploying';
|
state.phase = 'deploying';
|
||||||
state.progress = 85;
|
state.progress = 85;
|
||||||
state.message = 'Triggering deployment';
|
state.message = this.t('deployingApplication');
|
||||||
|
|
||||||
if (!state.resources.applicationId) {
|
if (!state.resources.applicationId) {
|
||||||
throw new Error('Application ID not available');
|
throw new Error('Application ID not available');
|
||||||
@@ -375,7 +381,7 @@ export class ProductionDeployer {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
state.phase = 'verifying_health';
|
state.phase = 'verifying_health';
|
||||||
state.progress = 95;
|
state.progress = 95;
|
||||||
state.message = 'Verifying application status via Dokploy';
|
state.message = this.t('verifyingHealth');
|
||||||
|
|
||||||
if (!state.resources.applicationId) {
|
if (!state.resources.applicationId) {
|
||||||
throw new Error('Application ID not available');
|
throw new Error('Application ID not available');
|
||||||
@@ -392,13 +398,13 @@ export class ProductionDeployer {
|
|||||||
console.log(`Application status: ${appStatus}`);
|
console.log(`Application status: ${appStatus}`);
|
||||||
|
|
||||||
if (appStatus === 'done') {
|
if (appStatus === 'done') {
|
||||||
state.message = 'Waiting for SSL certificate provisioning...';
|
state.message = this.t('waitingForSSL');
|
||||||
state.progress = 98;
|
state.progress = 98;
|
||||||
this.notifyProgress(state);
|
this.notifyProgress(state);
|
||||||
|
|
||||||
await this.sleep(15000);
|
await this.sleep(15000);
|
||||||
|
|
||||||
state.message = 'Application deployed successfully';
|
state.message = this.t('deploymentSuccess');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +416,7 @@ export class ProductionDeployer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
||||||
state.message = `Waiting for application to start (${elapsed}s)...`;
|
state.message = `${this.t('waitingForStart')} (${elapsed}s)...`;
|
||||||
this.notifyProgress(state);
|
this.notifyProgress(state);
|
||||||
|
|
||||||
await this.sleep(interval);
|
await this.sleep(interval);
|
||||||
|
|||||||
Reference in New Issue
Block a user