From 67069f3bdab3906340db198d21fb8b22bfe031ce Mon Sep 17 00:00:00 2001 From: Oussama Douhou Date: Sat, 10 Jan 2026 11:39:14 +0100 Subject: [PATCH] fix: resolve 4 UI/UX issues 1. Fix typewriter double-letter bug (race condition) 2. Replace flag emojis with text labels (NL/AR/EN) 3. Fix health check TLS options for Bun compatibility 4. Translate 'yourname' placeholder per language --- docs/TESTING.md | 68 +++++++++++++++++++++++++ src/frontend/app.js | 36 +++++++++---- src/frontend/index.html | 8 +-- src/orchestrator/production-deployer.ts | 31 +++++++---- 4 files changed, 120 insertions(+), 23 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 6de43f4..0d61124 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -108,3 +108,71 @@ source .env && curl -s -H "x-api-key: $DOKPLOY_API_TOKEN" \ | Health check timeout | Container startup can take 1-2 min, timeout is 3 min | | SSL cert errors | Health check treats SSL errors as "alive" during provisioning | | SSE disconnects | idleTimeout set to 255s (max), long deployments should complete | + +--- + +## Production Verification (2026-01-10) + +### Verified Working + +| Component | Status | Notes | +|-----------|--------|-------| +| Portal Health | ✅ | `https://portal.ai.flexinit.nl/health` returns healthy | +| Name Validation | ✅ | `/api/check/:name` validates correctly | +| Frontend UI | ✅ | 3 languages (NL, AR, EN), RTL support, typewriter animation | +| Stack Deployment | ✅ | Full flow: project → app → domain → deploy → health check | +| Stack Cleanup | ✅ | `DELETE /api/stack/:name` removes all resources | +| SSL/HTTPS | ✅ | Wildcard cert working for all `*.ai.flexinit.nl` | + +### Critical Configuration + +```bash +# These settings are REQUIRED for deployment to work +DOKPLOY_URL=https://app.flexinit.nl # Public URL, NOT internal 10.100.0.20 +STACK_IMAGE=git.app.flexinit.nl/oussamadouhou/oh-my-opencode-free:latest +STACK_REGISTRY_ID=bKDYM5X7NN34x_lRDjWbz # Registry ID for Docker auth +``` + +### Known Gotchas (Fixed) + +1. **Registry URL format** - Use `git.app.flexinit.nl`, NOT `https://git.app.flexinit.nl` +2. **Username in image path** - Must be `oussamadouhou`, not `odouhou` +3. **Dokploy URL** - Must use public URL for container-to-container communication +4. **Health check** - SSL errors treated as "alive" during cert provisioning + +### Test Commands + +```bash +# Quick health check +curl -s https://portal.ai.flexinit.nl/health | jq . + +# Full deployment test +NAME="test-$(date +%s | tail -c 5)" +RESULT=$(curl -s -X POST https://portal.ai.flexinit.nl/api/deploy \ + -H "Content-Type: application/json" \ + -d "{\"name\": \"$NAME\"}") +echo $RESULT | jq . + +# Monitor deployment (use deploymentId from above) +curl -N "https://portal.ai.flexinit.nl/api/status/$(echo $RESULT | jq -r .deploymentId)" + +# Verify stack accessible (after deployment completes) +curl -s -k https://$NAME.ai.flexinit.nl | head -5 + +# Cleanup +curl -s -X DELETE https://portal.ai.flexinit.nl/api/stack/$NAME | jq . +``` + +### Dokploy Direct Commands + +```bash +# List all ai-stack projects +source .env && curl -s -H "x-api-key: $DOKPLOY_API_TOKEN" \ + https://app.flexinit.nl/api/project.all | \ + jq '[.[] | select(.name | startswith("ai-stack-")) | {name: .name, id: .projectId}]' + +# Get application status +source .env && curl -s -H "x-api-key: $DOKPLOY_API_TOKEN" \ + "https://app.flexinit.nl/api/project.one?projectId=" | \ + jq '.environments[0].applications[0] | {name: .name, status: .applicationStatus}' +``` diff --git a/src/frontend/app.js b/src/frontend/app.js index 39068f0..5898942 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -29,7 +29,8 @@ const translations = { nameNotAvailable: 'Name is not available', checkFailed: 'Failed to check availability', connectionLost: 'Connection lost. Please refresh and try again.', - deployingText: 'Deploying...' + deployingText: 'Deploying...', + yournamePlaceholder: 'yourname' }, nl: { title: 'AI Stack Deployer', @@ -61,7 +62,8 @@ const translations = { nameNotAvailable: 'Naam is niet beschikbaar', checkFailed: 'Controle mislukt', connectionLost: 'Verbinding verbroken. Ververs de pagina en probeer opnieuw.', - deployingText: 'Implementeren...' + deployingText: 'Implementeren...', + yournamePlaceholder: 'jouwnaam' }, ar: { title: 'AI Stack Deployer', @@ -93,7 +95,8 @@ const translations = { nameNotAvailable: 'الاسم غير متاح', checkFailed: 'فشل التحقق', connectionLost: 'انقطع الاتصال. يرجى تحديث الصفحة والمحاولة مرة أخرى.', - deployingText: 'جاري النشر...' + deployingText: 'جاري النشر...', + yournamePlaceholder: 'اسمك' } }; @@ -139,6 +142,11 @@ function setLanguage(lang) { } }); + const previewNameEl = document.getElementById('preview-name'); + if (previewNameEl && !stackNameInput?.value) { + previewNameEl.textContent = t('yournamePlaceholder'); + } + const typewriterTarget = document.getElementById('typewriter-target'); if (typewriterTarget && currentState === STATE.FORM) { typewriter(typewriterTarget, t('chooseStackName')); @@ -157,7 +165,17 @@ function initLanguage() { }); } +// Track active typewriter instances to prevent race conditions +let activeTypewriters = new Map(); + function typewriter(element, text, speed = 50) { + // Cancel any existing typewriter on this element + const elementId = element.id || 'default'; + if (activeTypewriters.has(elementId)) { + clearTimeout(activeTypewriters.get(elementId)); + activeTypewriters.delete(elementId); + } + let i = 0; element.innerHTML = ''; @@ -175,9 +193,10 @@ function typewriter(element, text, speed = 50) { if (i < text.length) { element.textContent += text.charAt(i); i++; - setTimeout(type, speed); + const timeoutId = setTimeout(type, speed); + activeTypewriters.set(elementId, timeoutId); } else { - + activeTypewriters.delete(elementId); } } type(); @@ -289,10 +308,9 @@ function validateName(name) { let checkTimeout; stackNameInput.addEventListener('input', (e) => { const value = e.target.value.toLowerCase(); - e.target.value = value; // Force lowercase + e.target.value = value; - // Update preview - previewName.textContent = value || 'yourname'; + previewName.textContent = value || t('yournamePlaceholder'); // Clear previous timeout clearTimeout(checkTimeout); @@ -469,7 +487,7 @@ function resetToForm() { deploymentId = null; deploymentUrl = null; stackNameInput.value = ''; - previewName.textContent = 'yourname'; + previewName.textContent = t('yournamePlaceholder'); validationMessage.textContent = ''; validationMessage.className = 'validation-message'; stackNameInput.classList.remove('error', 'success'); diff --git a/src/frontend/index.html b/src/frontend/index.html index 6765106..ace3ec1 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -8,9 +8,9 @@
- - - + + +
@@ -25,7 +25,7 @@

-

Your AI assistant will be available at yourname.ai.flexinit.nl

+

Your AI assistant will be available at yourname.ai.flexinit.nl

diff --git a/src/orchestrator/production-deployer.ts b/src/orchestrator/production-deployer.ts index 3d80ce8..dad6c26 100644 --- a/src/orchestrator/production-deployer.ts +++ b/src/orchestrator/production-deployer.ts @@ -278,6 +278,10 @@ export class ProductionDeployer { dockerImage: config.dockerImage, sourceType: 'docker', registryId: config.registryId, + memoryLimit: 2048, + memoryReservation: 1024, + cpuLimit: 2, + cpuReservation: 0.5, }); state.progress = 55; @@ -353,33 +357,40 @@ export class ProductionDeployer { throw new Error('Application URL not available'); } - const timeout = config.healthCheckTimeout || 120000; // 2 minutes - const interval = config.healthCheckInterval || 5000; // 5 seconds + const timeout = config.healthCheckTimeout || 120000; + const interval = config.healthCheckInterval || 5000; const startTime = Date.now(); - // Try multiple endpoints - the container may not have /health const endpoints = ['/', '/health', '/api']; while (Date.now() - startTime < timeout) { for (const endpoint of endpoints) { try { const checkUrl = `${state.url}${endpoint}`; + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + const response = await fetch(checkUrl, { method: 'GET', - signal: AbortSignal.timeout(5000), - tls: { rejectUnauthorized: false }, + signal: controller.signal, }); - - // Accept ANY HTTP response (even 404) as "server is alive" - // Only connection errors mean the container isn't ready + + clearTimeout(timeoutId); console.log(`Health check ${checkUrl} returned ${response.status}`); state.message = 'Application is responding'; return; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); - // SSL cert errors mean server IS responding, just cert issue during provisioning - if (errorMsg.includes('certificate') || errorMsg.includes('SSL') || errorMsg.includes('TLS')) { + if ( + errorMsg.includes('certificate') || + errorMsg.includes('SSL') || + errorMsg.includes('TLS') || + errorMsg.includes('CERT') || + errorMsg.includes('unable to verify') || + errorMsg.includes('self signed') || + errorMsg.includes('self-signed') + ) { console.log(`Health check SSL error (treating as alive): ${errorMsg}`); state.message = 'Application is responding (SSL provisioning)'; return;