feat: production-ready deployment with multi-language UI

- Add multi-language support (NL, AR, EN) with RTL
- Improve health checks (SSL-tolerant, multi-endpoint)
- Add DELETE /api/stack/:name for cleanup
- Add persistent storage (portal-ai-workspace-{name})
- Improve rollback (delete domain, app, project)
- Increase SSE timeout to 255s
- Add deployment strategy documentation
This commit is contained in:
Oussama Douhou
2026-01-10 09:56:33 +01:00
parent eb6d5142ca
commit 2f306f7d68
10 changed files with 1196 additions and 462 deletions

View File

@@ -86,8 +86,8 @@ async function deployStack(deploymentId: string): Promise<void> {
dockerImage: process.env.STACK_IMAGE || 'git.app.flexinit.nl/oussamadouhou/oh-my-opencode-free:latest',
domainSuffix: process.env.STACK_DOMAIN_SUFFIX || 'ai.flexinit.nl',
port: 8080,
healthCheckTimeout: 60000, // 60 seconds
healthCheckInterval: 5000, // 5 seconds
healthCheckTimeout: 180000,
healthCheckInterval: 5000,
});
// Final update with logs
@@ -371,9 +371,66 @@ app.get('/api/check/:name', async (c) => {
}
});
app.delete('/api/stack/:name', async (c) => {
try {
const name = c.req.param('name');
const normalizedName = name.trim().toLowerCase();
const projectName = `ai-stack-${normalizedName}`;
const client = createProductionDokployClient();
const existingProject = await client.findProjectByName(projectName);
if (!existingProject) {
return c.json({
success: false,
error: 'Stack not found',
code: 'NOT_FOUND'
}, 404);
}
console.log(`Deleting stack: ${projectName} (projectId: ${existingProject.project.projectId})`);
await client.deleteProject(existingProject.project.projectId);
return c.json({
success: true,
message: `Stack ${normalizedName} deleted successfully`,
deletedProjectId: existingProject.project.projectId
});
} catch (error) {
console.error('Delete endpoint error:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Failed to delete stack',
code: 'DELETE_FAILED'
}, 500);
}
});
// Serve static files (frontend)
app.use('/static/*', serveStatic({ root: './src/frontend' }));
app.use('/*', serveStatic({ root: './src/frontend', path: '/index.html' }));
// Serve CSS and JS files directly
app.get('/style.css', async (c) => {
const file = Bun.file('./src/frontend/style.css');
return new Response(file, {
headers: { 'Content-Type': 'text/css' }
});
});
app.get('/app.js', async (c) => {
const file = Bun.file('./src/frontend/app.js');
return new Response(file, {
headers: { 'Content-Type': 'application/javascript' }
});
});
// Serve index.html for all other routes (SPA fallback)
app.get('/', async (c) => {
const file = Bun.file('./src/frontend/index.html');
return new Response(file, {
headers: { 'Content-Type': 'text/html' }
});
});
console.log(`🚀 AI Stack Deployer (Production) starting on http://${HOST}:${PORT}`);
console.log(`✅ Production features enabled:`);
@@ -387,4 +444,5 @@ export default {
port: PORT,
hostname: HOST,
fetch: app.fetch,
idleTimeout: 255,
};