- 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
306 lines
7.7 KiB
Markdown
306 lines
7.7 KiB
Markdown
# AI Stack Deployer - Agent Instructions
|
|
|
|
**MANDATORY: Read before implementing**
|
|
|
|
---
|
|
|
|
## PROJECT IDENTITY
|
|
|
|
Self-service portal: Users enter name → Get deployed AI assistant at `{name}.ai.flexinit.nl`
|
|
|
|
**Stack:**
|
|
- Backend: Bun + Hono
|
|
- Frontend: Vanilla HTML/CSS/JS (NO frameworks)
|
|
- Deploy: Docker + Dokploy
|
|
- Network: Hetzner DNS + Traefik
|
|
|
|
---
|
|
|
|
## MANDATORY: DEPLOYMENT WORKFLOW
|
|
|
|
**User submits name → Deploy AI stack (5 steps):**
|
|
|
|
1. **Validate** - Check name format, reserved names, availability
|
|
2. **DNS** - Create `{name}.ai.flexinit.nl` → 144.76.116.169 (or rely on wildcard)
|
|
3. **Dokploy Project** - Create project via API
|
|
4. **Dokploy App** - Create application, add domain, deploy
|
|
5. **Verify** - Wait for SSL, test OpenCode + ttyd endpoints
|
|
|
|
**CRITICAL:** Each step MUST succeed before proceeding. No parallel execution.
|
|
|
|
---
|
|
|
|
## MANDATORY: SECRETS (Get FIRST)
|
|
|
|
```bash
|
|
DOKPLOY_TOKEN=$(bws-wrapper get 6b3618fc-ba02-49bc-bdc8-b3c9004087bc)
|
|
HETZNER_TOKEN=$(bws-wrapper get <HETZNER_BWS_ID>) # Ask user or search BWS
|
|
```
|
|
|
|
---
|
|
|
|
## MANDATORY: API COMMANDS (COPY EXACTLY)
|
|
|
|
### Hetzner DNS API (MODIFY ONLY: name, comment)
|
|
|
|
**CRITICAL:** Use `api.hetzner.cloud/v1`, NOT `dns.hetzner.com` (deprecated)
|
|
|
|
```bash
|
|
# List existing DNS records
|
|
curl -s "https://api.hetzner.cloud/v1/zones/343733/rrsets" \
|
|
-H "Authorization: Bearer $HETZNER_TOKEN"
|
|
|
|
# Create A record (OPTIONAL - wildcard *.ai.flexinit.nl already exists)
|
|
curl -X POST "https://api.hetzner.cloud/v1/zones/343733/rrsets" \
|
|
-H "Authorization: Bearer $HETZNER_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"name": "{name}.ai",
|
|
"type": "A",
|
|
"ttl": 300,
|
|
"records": [{"value": "144.76.116.169", "comment": "AI Stack for {name}"}]
|
|
}'
|
|
```
|
|
|
|
**Constants:**
|
|
- Zone ID: `343733` (flexinit.nl)
|
|
- Traefik IP: `144.76.116.169`
|
|
- Wildcard: `*.ai.flexinit.nl` → 144.76.116.169 (pre-configured)
|
|
|
|
### Dokploy API (MODIFY ONLY: projectId, applicationId, name, domain)
|
|
|
|
**Base:** `http://10.100.0.20:3000/api`
|
|
|
|
```bash
|
|
# Create project
|
|
curl -X POST "http://10.100.0.20:3000/api/project.create" \
|
|
-H "Authorization: Bearer $DOKPLOY_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name": "{username}-ai-stack"}'
|
|
|
|
# Create application
|
|
curl -X POST "http://10.100.0.20:3000/api/application.create" \
|
|
-H "Authorization: Bearer $DOKPLOY_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"projectId": "...", "name": "{username}-opencode"}'
|
|
|
|
# Add domain
|
|
curl -X POST "http://10.100.0.20:3000/api/domain.create" \
|
|
-H "Authorization: Bearer $DOKPLOY_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"applicationId": "...", "host": "{name}.ai.flexinit.nl"}'
|
|
|
|
# Deploy application
|
|
curl -X POST "http://10.100.0.20:3000/api/application.deploy" \
|
|
-H "Authorization: Bearer $DOKPLOY_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"applicationId": "..."}'
|
|
|
|
# Check status
|
|
curl -s "http://10.100.0.20:3000/api/application.one?applicationId=..." \
|
|
-H "Authorization: Bearer $DOKPLOY_TOKEN"
|
|
```
|
|
|
|
---
|
|
|
|
## CRITICAL: MANDATORY RULES
|
|
|
|
**NEVER:**
|
|
- Use frontend frameworks (React, Vue, Svelte, etc.)
|
|
- Use old Hetzner API (`dns.hetzner.com`)
|
|
- Create Dokploy domain BEFORE application exists
|
|
- Skip error handling on ANY API call
|
|
- Store secrets in code
|
|
- Proceed to next step if current step fails
|
|
|
|
**ALWAYS:**
|
|
- Validate name (alphanumeric, 3-20 chars, not reserved)
|
|
- Get tokens from BWS first
|
|
- Wait for SSL certificate (30-60 seconds)
|
|
- Verify each deployment step
|
|
- Handle errors gracefully with user feedback
|
|
- Mobile-responsive design
|
|
|
|
---
|
|
|
|
## CRITICAL: GOTCHAS
|
|
|
|
1. **Hetzner API** - Use `api.hetzner.cloud`, NOT `dns.hetzner.com` (deprecated)
|
|
2. **Dokploy domain** - MUST create domain AFTER application exists (or 404)
|
|
3. **SSL delay** - Let's Encrypt cert takes 30-60 seconds to provision
|
|
4. **ttyd WebSocket** - Requires Traefik WebSocket support configured
|
|
5. **Container startup** - OpenCode server takes ~10 seconds to be ready
|
|
6. **Reserved names** - Block: admin, api, www, root, test, staging, prod, etc.
|
|
|
|
---
|
|
|
|
## MANDATORY: VALIDATION
|
|
|
|
**Before deployment, check:**
|
|
```javascript
|
|
// Name validation regex
|
|
const isValid = /^[a-z0-9]{3,20}$/.test(name);
|
|
|
|
// Reserved names
|
|
const reserved = ['admin', 'api', 'www', 'root', 'test', 'staging', 'prod'];
|
|
const isReserved = reserved.includes(name);
|
|
```
|
|
|
|
---
|
|
|
|
## MANDATORY: VERIFICATION
|
|
|
|
**After deployment, verify:**
|
|
```bash
|
|
# 1. SSL certificate provisioned
|
|
curl -I "https://{name}.ai.flexinit.nl" # Should return 200
|
|
|
|
# 2. OpenCode server responding
|
|
curl "https://{name}.ai.flexinit.nl" # Should show OpenCode UI
|
|
|
|
# 3. ttyd terminal accessible
|
|
curl -I "https://{name}.ai.flexinit.nl:7681" # Should return 200
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Patterns
|
|
|
|
### Backend (Bun + Hono)
|
|
|
|
```typescript
|
|
// Use Hono for routing
|
|
import { Hono } from 'hono';
|
|
import { cors } from 'hono/cors';
|
|
import { serveStatic } from 'hono/bun';
|
|
|
|
const app = new Hono();
|
|
|
|
// Serve frontend
|
|
app.use('/*', serveStatic({ root: './src/frontend' }));
|
|
|
|
// API routes
|
|
app.post('/api/deploy', deployHandler);
|
|
app.get('/api/status/:id', statusHandler);
|
|
app.get('/api/check/:name', checkHandler);
|
|
```
|
|
|
|
### SSE Implementation
|
|
|
|
```typescript
|
|
// Server-Sent Events for progress updates
|
|
app.get('/api/status/:id', (c) => {
|
|
return streamSSE(c, async (stream) => {
|
|
// Send progress updates
|
|
await stream.writeSSE({
|
|
event: 'progress',
|
|
data: JSON.stringify({ step: 'dns', status: 'completed' })
|
|
});
|
|
|
|
// ... more updates
|
|
|
|
await stream.writeSSE({
|
|
event: 'complete',
|
|
data: JSON.stringify({ url: 'https://...' })
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### Frontend (Vanilla JS State Machine)
|
|
|
|
```javascript
|
|
// State: 'idle' | 'deploying' | 'success' | 'error'
|
|
let state = 'idle';
|
|
|
|
function setState(newState, data = {}) {
|
|
state = newState;
|
|
render(state, data);
|
|
}
|
|
|
|
// SSE connection
|
|
const eventSource = new EventSource(`/api/status/${deploymentId}`);
|
|
eventSource.addEventListener('progress', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
updateProgress(data);
|
|
});
|
|
eventSource.addEventListener('complete', (e) => {
|
|
setState('success', JSON.parse(e.data));
|
|
});
|
|
```
|
|
|
|
### Docker Stack Template
|
|
|
|
The user stack needs:
|
|
1. OpenCode server (port 8080)
|
|
2. ttyd web terminal (port 7681)
|
|
|
|
```dockerfile
|
|
FROM git.app.flexinit.nl/oussamadouhou/oh-my-opencode-free:latest
|
|
|
|
# Install ttyd
|
|
RUN apt-get update && apt-get install -y ttyd
|
|
|
|
# Expose ports
|
|
EXPOSE 8080 7681
|
|
|
|
# Start both services
|
|
CMD ["sh", "-c", "opencode serve --host 0.0.0.0 --port 8080 & ttyd -W -p 7681 opencode attach http://localhost:8080"]
|
|
```
|
|
|
|
---
|
|
|
|
## Code Style
|
|
|
|
### TypeScript
|
|
- Use strict mode
|
|
- Prefer `const` over `let`
|
|
- Use async/await over callbacks
|
|
- Handle all errors explicitly
|
|
- Type all function parameters and returns
|
|
|
|
### CSS
|
|
- Use CSS variables for theming
|
|
- Mobile-first responsive design
|
|
- BEM-like naming: `.component__element--modifier`
|
|
- Dark theme as default
|
|
|
|
### JavaScript (Frontend)
|
|
- No frameworks, vanilla only
|
|
- Module pattern for organization
|
|
- Event delegation where possible
|
|
- Graceful degradation
|
|
|
|
---
|
|
|
|
## MANDATORY: TESTING CHECKLIST
|
|
|
|
**Before marking complete, ALL must pass:**
|
|
|
|
- [ ] Name validation works (alphanumeric, 3-20 chars)
|
|
- [ ] Reserved names blocked (admin, api, www, root, etc.)
|
|
- [ ] DNS record created successfully
|
|
- [ ] Dokploy project created
|
|
- [ ] Application deployed and healthy
|
|
- [ ] SSL certificate provisioned
|
|
- [ ] ttyd accessible in browser
|
|
- [ ] Error states handled gracefully
|
|
- [ ] Mobile responsive
|
|
- [ ] Loading states smooth (no flicker)
|
|
|
|
|
|
|
|
## File Reading Order
|
|
|
|
When starting implementation, read in this order:
|
|
1. `README.md` - Full project specification
|
|
2. `AGENTS.md` - This file
|
|
3. Check existing oh-my-opencode-free for reference patterns
|
|
|
|
---
|
|
|
|
## Reference Projects
|
|
|
|
- `~/locale-projects/oh-my-opencode-free` - The stack being deployed
|
|
- `~/projecten/infrastructure` - Infrastructure patterns and docs
|