Compare commits

...

11 Commits

Author SHA1 Message Date
86fe7a8bf1 feat: Add multilingual deployment progress messages
Some checks failed
Build and Push Docker Image (Production) / build-and-push-main (push) Successful in 2m32s
Build and Push Docker Image (Dev) / build-and-push-dev (push) Has been cancelled
Build and Push Docker Image (Staging) / build-and-push-staging (push) Successful in 4m17s
- Created backend i18n system with EN/NL/AR translations
- Frontend now sends language preference with deployment request
- Backend deployment messages follow user's selected language
- Translated key messages: initializing, creating app, SSL waiting, etc.
- Added top margin (100px) on mobile to prevent language button overlap

Fixes real-time deployment status showing English regardless of language selection.
2026-01-13 16:40:05 +01:00
dd41bb5a6a Margin top mobile
All checks were successful
Build and Push Docker Image (Production) / build-and-push-main (push) Successful in 2m39s
2026-01-13 16:33:04 +01:00
8c8536f668 fix: Use standard Docker Compose variable syntax
All checks were successful
Build and Push Docker Image (Production) / build-and-push-main (push) Successful in 2m57s
Changed from ${{project.VAR}} to ${VAR} syntax.

The ${{project.VAR}} syntax is for Dokploy's Environment editor UI,
not for docker-compose.yml files. Docker Compose requires standard
${VAR} syntax to read from .env file.

The .env file (managed by Dokploy) already contains the actual values.
2026-01-13 16:19:44 +01:00
db3a86404a fix: Use single dollar for Dokploy variable substitution
All checks were successful
Build and Push Docker Image (Production) / build-and-push-main (push) Successful in 2m13s
Official Dokploy docs specify ${{project.VAR}} (single $), not $${{project.VAR}}.
Double $$ is Docker Compose escape syntax preventing Dokploy substitution.

Caused missing SHARED_PROJECT_ID/SHARED_ENVIRONMENT_ID in portal container.

Ref: https://docs.dokploy.com/docs/core/variables
2026-01-13 16:17:13 +01:00
d900d905de docs: add critical Docker Compose custom command configuration
- Document requirement for custom compose command with -f flag
- Add troubleshooting section for 'no configuration file provided' error
- Include examples for dev/staging/prod environments
- Explain why Dokploy needs explicit -f flag for non-default filenames

Resolves issue where Dokploy couldn't find docker-compose.prod.yml
2026-01-13 15:52:17 +01:00
3d07301992 trigger redeploy 2026-01-13 15:20:38 +01:00
f5be8d856d Merge staging into main - resolve conflicts
All checks were successful
Build and Push Docker Image (Production) / build-and-push-main (push) Successful in 3m34s
2026-01-13 15:03:06 +01:00
3bda68282e Merge dev into staging - resolve docker-compose.local.yml conflict
All checks were successful
Build and Push Docker Image (Staging) / build-and-push-staging (push) Successful in 2m16s
2026-01-13 15:01:43 +01:00
ef24af3302 Docker ports removed 2026-01-13 13:34:13 +01:00
7dff5454a0 Docker ports removed 2026-01-13 13:33:35 +01:00
2885990ac6 fixed bun AVX
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 5m22s
2026-01-13 11:33:27 +01:00
9 changed files with 177 additions and 31 deletions

View File

@@ -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',

View File

@@ -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();
@@ -118,14 +118,14 @@ export default function DeployPage() {
dotSize={2} dotSize={2}
/> />
</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,1)_0%,transparent_100%)]" />
<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-linear-to-b from-black 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-w160 p-4 md:p-8">
<header className="text-center mb-12"> <header className="text-center mb-12 mt-25 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 }}

View File

@@ -11,8 +11,8 @@ services:
- STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl} - STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl}
- STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest} - STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest}
- RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal} - RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal}
- SHARED_PROJECT_ID=$${{project.SHARED_PROJECT_ID}} - SHARED_PROJECT_ID=${SHARED_PROJECT_ID}
- SHARED_ENVIRONMENT_ID=$${{project.SHARED_ENVIRONMENT_ID}} - SHARED_ENVIRONMENT_ID=${SHARED_ENVIRONMENT_ID}
env_file: env_file:
- .env - .env
restart: unless-stopped restart: unless-stopped

View File

@@ -13,8 +13,8 @@ services:
- STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl} - STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl}
- STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest} - STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest}
- RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal} - RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal}
- SHARED_PROJECT_ID=$${{project.SHARED_PROJECT_ID}} - SHARED_PROJECT_ID=${SHARED_PROJECT_ID}
- SHARED_ENVIRONMENT_ID=$${{project.SHARED_ENVIRONMENT_ID}} - SHARED_ENVIRONMENT_ID=${SHARED_ENVIRONMENT_ID}
env_file: env_file:
- .env - .env
restart: unless-stopped restart: unless-stopped

View File

@@ -13,8 +13,8 @@ services:
- STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl} - STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl}
- STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest} - STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/flexinit/agent-stack:latest}
- RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal} - RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal}
- SHARED_PROJECT_ID=$${{project.SHARED_PROJECT_ID}} - SHARED_PROJECT_ID=${SHARED_PROJECT_ID}
- SHARED_ENVIRONMENT_ID=$${{project.SHARED_ENVIRONMENT_ID}} - SHARED_ENVIRONMENT_ID=${SHARED_ENVIRONMENT_ID}
env_file: env_file:
- .env - .env
restart: unless-stopped restart: unless-stopped

View File

@@ -201,12 +201,48 @@ The portal's docker-compose files use Dokploy's variable syntax to reference the
SHARED_ENVIRONMENT_ID=${{project.SHARED_ENVIRONMENT_ID}} SHARED_ENVIRONMENT_ID=${{project.SHARED_ENVIRONMENT_ID}}
``` ```
4. **Configure Webhook**: 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** - Event: **Push**
- Branch: `dev` - Branch: `dev`
- This will auto-deploy when you push to dev branch - This will auto-deploy when you push to dev branch
5. **Deploy** 6. **Deploy**
### Step 2: Create Staging Application ### Step 2: Create Staging Application
@@ -414,6 +450,42 @@ Common issues:
``` ```
3. **Check environment variables**: Make sure all required vars are set 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 ### Health Check Failing
```bash ```bash

View File

@@ -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
View 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;
};
}

View File

@@ -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);