Add deployment dashboard docs and artifacts

- Add design doc for Vercel-style deployment dashboard
- Add wave-based implementation plan (4 waves, 11 agents)
- Add implementation summary artifact
- Update CLAUDE.md with CloudBeaver credentials

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-06 18:00:27 +01:00
parent 16838f1ca1
commit 36698dbc79
4 changed files with 2179 additions and 29 deletions

340
CLAUDE.md
View File

@@ -8,14 +8,80 @@ ssh nuc
```
**Connection Details:**
- Hostname: `192.168.1.3`
- Hostname: `192.168.1.3` (local) or `100.113.153.45` (Tailscale)
- User: `alezmad`
- SSH Key: `~/.ssh/id_ed25519_nuc`
## DNS & Tailscale Setup
### Why Tailscale IP for DNS
All `.nuc.lan` domains resolve to the **Tailscale IP** (`100.113.153.45`) instead of the local IP (`192.168.1.3`). This ensures services work from **anywhere** regardless of your current network's subnet.
**Problem solved:** When connecting from a remote network that also uses `192.168.x.x`, traffic to `192.168.1.3` stays local instead of going through Tailscale. Using Tailscale IP (`100.x.x.x`) avoids this conflict.
### Configured Domains (OpenWrt Router DNS)
| Domain | Resolves To | Service |
|--------|-------------|---------|
| `nuc.lan` | `100.113.153.45` | NUC Portal |
| `nuc.local` | `100.113.153.45` | NUC Portal |
| `coolify.nuc.lan` | `100.113.153.45` | Coolify |
| `gitea.nuc.lan` | `100.113.153.45` | Gitea |
| `outline.nuc.lan` | `100.113.153.45` | Outline Wiki |
| `files.nuc.lan` | `100.113.153.45` | FileBrowser |
| `mail.nuc.lan` | `100.113.153.45` | Snappymail |
| `vault.nuc.lan` | `100.113.153.45` | Vaultwarden |
| `homepage.nuc.lan` | `100.113.153.45` | NUC Portal |
| `brand.nuc.lan` | `100.113.153.45` | Whyrating Brand |
| `templates.nuc.lan` | `100.113.153.45` | Whyrating Templates |
| `whyrating.nuc.lan` | `100.113.153.45` | Whyrating Hub |
### Traefik Routing (Dynamic Config)
Traefik routes domain-based requests to the correct backend. Config location: `/data/coolify/proxy/dynamic/nuc-services.yaml`
```yaml
# Routes for port-based services via domain names
http:
routers:
coolify:
rule: Host(`coolify.nuc.lan`)
service: coolify
services:
coolify:
loadBalancer:
servers:
- url: http://host.docker.internal:8000
```
### Adding a New Domain
```bash
# 1. Add DNS entry on router (via NUC jump host)
ssh nuc "ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 '
uci add dhcp domain
uci set dhcp.@domain[-1].name=\"newservice.nuc.lan\"
uci set dhcp.@domain[-1].ip=\"100.113.153.45\"
uci commit dhcp
/etc/init.d/dnsmasq restart
'"
# 2. Add Traefik route (if needed for port-based service)
# Edit /data/coolify/proxy/dynamic/nuc-services.yaml
```
### Always-On Tailscale
**Keep Tailscale running** - it's designed to be always-on:
- When on home network: Uses direct connection (no relay, same performance as local)
- When remote: Routes through Tailscale mesh
- Minimal resource usage (~0% CPU when idle)
## Service Management
### Coolify (Primary Service Manager)
All services are managed through Coolify at `http://192.168.1.3:8000`
All services are managed through Coolify at `http://coolify.nuc.lan` (or `http://100.113.153.45:8000`)
**Prefer using Coolify MCP** (`mcp__coolify__*`) for service management - it's faster and more reliable than SSH commands.
@@ -128,11 +194,114 @@ Task(subagent_type="general-purpose", prompt="Add services to Homepage...", desc
| MCP | Purpose |
|-----|---------|
| `mcp__coolify__*` | Service management, deployments, env vars |
| `mcp__stalwart-mail__*` | Email server management (users, domains, queue) |
| `mcp__email-client__*` | Read/send emails via IMAP/SMTP (see below) |
| `mcp__nocodb__*` | Database operations, table management |
| `mcp__ssh-manager__*` | Direct SSH commands, file transfers |
| `mcp__n8n__*` | Workflow automation (if configured) |
| `mcp__playwriter__*` | Browser automation fallback (see below) |
### Stalwart Mail MCP (Quick Guide)
**Location:** `~/mcp-servers/stalwart-mail/`
Manage the self-hosted Stalwart mail server via natural language.
**Available Tools:**
| Category | Tools |
|----------|-------|
| **Users** | `list_users`, `get_user`, `create_user`, `update_user_password`, `delete_user`, `add_email_alias` |
| **Domains** | `create_domain`, `generate_dkim` |
| **Queue** | `list_queue`, `get_queue_status`, `delete_queued_message`, `retry_queued_message` |
| **Monitoring** | `get_metrics`, `get_dmarc_reports`, `get_server_logs` |
| **DNS** | `check_dns_records`, `troubleshoot_delivery` |
| **Spam** | `train_spam`, `train_ham`, `update_spam_filter` |
**Usage Examples:**
```
"List all mail users"
"Create user sales with email sales@whyrating.com and password Secret123"
"Check the mail queue"
"Verify DNS records for whyrating.com"
"Show server metrics"
"Delete user john"
```
**Direct API Test (if MCP not responding):**
```bash
curl -s -u "admin:QfKYjCJdxu" "http://192.168.1.3:8081/api/principal" | jq .
```
**Reconfigure MCP:**
```bash
claude mcp remove stalwart-mail
claude mcp add stalwart-mail \
-e STALWART_URL=http://192.168.1.3:8081 \
-e STALWART_USER=admin \
-e STALWART_PASS=QfKYjCJdxu \
--scope user \
-- ~/mcp-servers/stalwart-mail/.venv/bin/python ~/mcp-servers/stalwart-mail/server.py
```
**⚠️ SMTP Authentication Requirements:**
1. **Password format:** Must be SHA-512 hashed (not plaintext). When creating users via API:
```python
import crypt
hashed = crypt.crypt('password', crypt.mksalt(crypt.METHOD_SHA512))
# Use hashed value in 'secrets' field
```
2. **SMTP login:** Use username only (e.g., `info`), NOT full email (`info@whyrating.com`)
3. **Port 465 (SMTPS):** Supports PLAIN/LOGIN auth with implicit TLS
4. **Port 587 (Submission):** Requires STARTTLS, only OAuth supported without TLS
**Send email via Python (from NUC):**
```python
import smtplib, ssl
from email.mime.text import MIMEText
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
with smtplib.SMTP_SSL('localhost', 465, context=context) as server:
server.login('info', 'whyrating2026') # Username only!
server.sendmail('info@whyrating.com', 'recipient@example.com', msg.as_string())
```
### Email Client MCP (Read/Send Emails)
**Package:** [mcp-email-server](https://github.com/ai-zerolab/mcp-email-server)
Read and send emails via IMAP/SMTP directly from Claude.
**Configured for:** `info@whyrating.com` on Stalwart
**Usage Examples:**
```
"Check my inbox"
"Read the latest email"
"Send an email to john@example.com with subject Hello"
"Search emails from support@"
"List email folders"
```
**Reconfigure:**
```bash
claude mcp remove email-client
claude mcp add email-client \
-e MCP_EMAIL_SERVER_EMAIL_ADDRESS=info@whyrating.com \
-e MCP_EMAIL_SERVER_PASSWORD=whyrating2026 \
-e MCP_EMAIL_SERVER_IMAP_HOST=192.168.1.3 \
-e MCP_EMAIL_SERVER_IMAP_PORT=143 \
-e MCP_EMAIL_SERVER_SMTP_HOST=192.168.1.3 \
-e MCP_EMAIL_SERVER_SMTP_PORT=587 \
-e MCP_EMAIL_SERVER_SMTP_VERIFY_SSL=false \
-e MCP_EMAIL_SERVER_ENABLE_ATTACHMENT_DOWNLOAD=true \
--scope user \
-- uvx mcp-email-server@latest stdio
```
### Adding Remote MCP Servers (HTTP Transport)
**Use `claude mcp add --transport http` for remote MCP endpoints** - this is the recommended method for services with native MCP support.
@@ -261,24 +430,31 @@ ssh nuc "docker exec <container_name> <command>"
## Services & Ports
| Service | Port | URL | Container |
|---------|------|-----|-----------|
| Homepage | 3000 | http://192.168.1.3:3000 | homepage-* |
| Coolify | 8000 | http://192.168.1.3:8000 | coolify |
| Gitea | 3030 | http://192.168.1.3:3030 | gitea-* |
| Outline | 3080 | http://192.168.1.3:3080 | outline-* |
| NocoDB | 8084 | http://192.168.1.3:8084 | nocodb-* |
| n8n | 5678 | http://192.168.1.3:5678 | n8n-* |
| Vaultwarden | 8222 | http://192.168.1.3:8222 | vaultwarden-* |
| Ntfy | 8333 | http://192.168.1.3:8333 | ntfy-* |
| MinIO Console | 9001 | http://192.168.1.3:9001 | minio-* |
| MinIO API | 9000 | http://192.168.1.3:9000 | minio-* |
| Authentik | 9090 | http://192.168.1.3:9090 | authentik-* |
| FileBrowser | 8085 | http://192.168.1.3:8085 | filebrowser-* |
| Adminer | 8088 | http://192.168.1.3:8088 | adminer |
| Uptime Kuma | 3001 | http://192.168.1.3:3001 | uptime-kuma |
| Kopia | 51515 | http://192.168.1.3:51515 | kopia |
| Dozzle | 9999 | http://192.168.1.3:9999 | dozzle |
**Preferred access via domain names** (works from anywhere via Tailscale):
| Service | Domain | Port-based URL | Container |
|---------|--------|----------------|-----------|
| NUC Portal | `http://nuc.lan` | - | nuc-portal-* |
| Coolify | `http://coolify.nuc.lan` | `http://100.113.153.45:8000` | coolify |
| Gitea | `http://gitea.nuc.lan` | `http://100.113.153.45:3030` | gitea-* |
| Outline | `http://outline.nuc.lan` | `http://100.113.153.45:3080` | outline-* |
| FileBrowser | `http://files.nuc.lan` | `http://100.113.153.45:8085` | filebrowser-* |
| Snappymail | `http://mail.nuc.lan` | `http://100.113.153.45:8082` | snappymail-* |
| Vaultwarden | `http://vault.nuc.lan` | `http://100.113.153.45:8222` | vaultwarden-* |
| Homepage | `http://homepage.nuc.lan` | `http://100.113.153.45:3000` | nuc-portal-* |
| NocoDB | - | `http://100.113.153.45:8084` | nocodb-* |
| n8n | - | `http://100.113.153.45:5678` | n8n-* |
| Ntfy | - | `http://100.113.153.45:8333` | ntfy-* |
| MinIO Console | - | `http://100.113.153.45:9001` | minio-* |
| MinIO API | - | `http://100.113.153.45:9000` | minio-* |
| Authentik | - | `http://100.113.153.45:9090` | authentik-* |
| Adminer | - | `http://100.113.153.45:8088` | adminer |
| Uptime Kuma | - | `http://100.113.153.45:3001` | uptime-kuma |
| Kopia | - | `http://100.113.153.45:51515` | kopia |
| Dozzle | - | `http://100.113.153.45:9999` | dozzle |
| CloudBeaver | - | `http://100.113.153.45:8978` | cloudbeaver-* |
**Note:** Use Tailscale IP (`100.113.153.45`) instead of `192.168.1.3` to avoid subnet conflicts when remote.
## Port Forwarding
@@ -527,6 +703,63 @@ mcp__chrome-devtools__fill(uid="<uid>", value="<text>")
- Token URL: `http://192.168.1.3:3030/login/oauth/access_token`
- Userinfo URL: `http://192.168.1.3:3030/login/oauth/userinfo`
## ⚠️ Critical Credentials & Access
### CloudBeaver (Database Manager)
| Property | Value |
|----------|-------|
| **URL** | `http://192.168.1.3:8978` |
| **Admin User** | `cbadmin` |
| **Admin Password** | `CloudBeaver2026!` |
| **Service UUID** | `joo4g4k0w08k8kcosgsgswc0` |
**Pre-configured connections:** 9 databases across 3 folders. Turbostarter DB is now in service `v4gogwwc8wkk4888ksscc4k4` (container: `db-v4gogwwc8wkk4888ksscc4k4`).
Connected to 7 Docker networks for direct container-to-container access.
### Vaultwarden (Password Manager)
**⚠️ CRITICAL: Vaultwarden REQUIRES HTTPS** - The Web Crypto API needs a secure context for client-side encryption. HTTP access will NOT work (blank page/loading forever).
| Property | Value |
|----------|-------|
| **HTTPS URL** | `https://nuc-tailscale.tail58f5ad.ts.net:8443` |
| **HTTP URL** | `http://192.168.1.3:8222` (won't load - HTTPS required) |
| **Admin Email** | `admin@nuc.lan` |
| **Admin Password** | `NucVault2026!Secure` |
**Access via Tailscale Funnel:**
```bash
# Vaultwarden is exposed on port 8443 via Tailscale Funnel
open "https://nuc-tailscale.tail58f5ad.ts.net:8443"
```
### Stalwart Mail Server
| Property | Value |
|----------|-------|
| **Admin URL** | `http://192.168.1.3:8081` |
| **Username** | `admin` |
| **Password** | `QfKYjCJdxu` |
| **Webmail (Snappymail)** | `http://192.168.1.3:8082` |
| **Service UUID** | `kw00kok0w0s8gcok008gk04k` |
| **MCP Server** | `mcp__stalwart-mail__*` (see Quick Guide above) |
**Mail Users:**
| Email | Username | Password |
|-------|----------|----------|
| `info@whyrating.com` | `info` | `whyrating2026` |
**DNS Records Configured:** SPF, DKIM (Ed25519 + RSA), DMARC, MX for `whyrating.com`
**Container Status:** Running
### Gitea Users (for Outline OIDC)
| Username | Password | Notes |
|----------|----------|-------|
| `nedas` | `NedasNUC2026!` | Regular user account |
## Gitea-Coolify Integration (Git Auto-Deploy)
Deploy Next.js apps from self-hosted Gitea with auto-deploy on push. Full docs: `docs/gitea-coolify-auto-deploy.md`
@@ -594,7 +827,7 @@ SSH config is set up for direct git operations:
```bash
# Clone a repo
git clone gitea:nuc/nuc-portal.git
git clone gitea:alezmad/nuc-portal.git
# Push changes (triggers auto-deploy via webhook)
git push origin main
@@ -634,7 +867,7 @@ mcp__coolify__application(
project_uuid="a8484ggc88c40w4g4k004ow0",
environment_name="production",
server_uuid="qk84w0goo4w48g4ggsoo0oss",
git_repository="git@gitea-ho0cwgcwos88cwc48g84c0g8:nuc/<repo>.git",
git_repository="git@gitea-ho0cwgcwos88cwc48g84c0g8:alezmad/<repo>.git",
git_branch="main",
build_pack="nixpacks",
ports_exposes="3000",
@@ -681,7 +914,7 @@ Add via Gitea: Repository → Settings → Deploy Keys → **Enable Write Access
When creating a new repo that should auto-deploy:
1. **[ ] Add deploy key to Gitea repo**
- Go to: `http://192.168.1.3:3030/nuc/<repo>/settings/keys`
- Go to: `http://192.168.1.3:3030/alezmad/<repo>/settings/keys`
- Add the deploy key above with **Write Access** enabled
2. **[ ] Create Coolify application** (use `mcp__coolify__application` with `action="create_key"`)
@@ -691,7 +924,7 @@ When creating a new repo that should auto-deploy:
4. **[ ] Set webhook secret** via tinker command (use shared secret above)
5. **[ ] Create Gitea webhook**
- Go to: `http://192.168.1.3:3030/nuc/<repo>/settings/hooks`
- Go to: `http://192.168.1.3:3030/alezmad/<repo>/settings/hooks`
- Add Webhook → Gitea
- **URL:** `http://coolify:8080/webhooks/source/gitea/events/manual?uuid=<app-uuid>`
- **Secret:** `9eb07a77964563378c5d66d99006e06ba3da39d232905d4b12554ff91ca39718`
@@ -728,10 +961,59 @@ Coolify's "Gitea Source" uses GitHub App-style OAuth with JWT - **this doesn't w
| App | URL | Repository | UUID |
|-----|-----|------------|------|
| nuc-portal | http://nuc.lan | `nuc/nuc-portal` | `t80w0cw0oooc4g0soswos4so` |
| whyrating-hub | http://whyrating.nuc.lan | `nuc/whyrating-hub` | `vw4ggc40socwkgwg4osc8wg8` |
| whyrating-brand | http://brand.nuc.lan | `nuc/whyrating-brand` | `r80gk0ccgg0okos8cw848kkk` |
| whyrating-templates | http://templates.nuc.lan | `nuc/whyrating-templates` | `qw80g4sog0kk8cc4wkcs8sgc` |
| nuc-portal | http://nuc.lan | `alezmad/nuc-portal` | `t80w0cw0oooc4g0soswos4so` |
| whyrating-hub | http://whyrating.nuc.lan | `alezmad/whyrating-hub` | `vw4ggc40socwkgwg4osc8wg8` |
| whyrating-brand | http://brand.nuc.lan | `alezmad/whyrating-brand` | `r80gk0ccgg0okos8cw848kkk` |
| whyrating-templates | http://templates.nuc.lan | `alezmad/whyrating-templates` | `qw80g4sog0kk8cc4wkcs8sgc` |
| turbostarter | https://alezmad-nuc.tail58f5ad.ts.net | `alezmad/turbostarter` | `v4gogwwc8wkk4888ksscc4k4` (service) |
### Turbostarter (Knosia) - Build & Deploy
Turbostarter is deployed as a **Coolify Service** (not a standalone app) with full docker-compose infrastructure: web + pgvector + minio.
**Architecture:** Tailscale Funnel (HTTPS) → Traefik (HTTP:80) → web container
**FQDN (Traefik):** `http://alezmad-nuc.tail58f5ad.ts.net` (HTTP internally — Tailscale handles TLS termination)
**Build & Deploy workflow:**
```bash
# 1. Build image locally (ARM→AMD cross-compile)
cd /Users/agutierrez/Desktop/turbostarter-export
docker build --platform linux/amd64 \
--build-arg NEXT_PUBLIC_URL=https://alezmad-nuc.tail58f5ad.ts.net \
-t 192.168.1.3:3030/alezmad/turbostarter:latest .
# 2. Push to Gitea registry
docker push 192.168.1.3:3030/alezmad/turbostarter:latest
# 3. Redeploy via Coolify (stop + start for full container recreation)
mcp__coolify__control(resource="service", action="stop", uuid="v4gogwwc8wkk4888ksscc4k4")
mcp__coolify__control(resource="service", action="start", uuid="v4gogwwc8wkk4888ksscc4k4")
```
**Containers:**
| Container | Image | Purpose |
|-----------|-------|---------|
| `web-v4gogwwc8wkk4888ksscc4k4` | `localhost:3030/alezmad/turbostarter:latest` | Next.js app |
| `db-v4gogwwc8wkk4888ksscc4k4` | `pgvector/pgvector:pg17` | PostgreSQL + pgvector |
| `minio-v4gogwwc8wkk4888ksscc4k4` | `minio/minio:latest` | Object storage |
| `minio-init-v4gogwwc8wkk4888ksscc4k4` | `minio/mc:latest` | One-time bucket init |
**Database access (via SSH tunnel):**
```bash
# Get DB container IP first
ssh nuc "docker inspect db-v4gogwwc8wkk4888ksscc4k4 --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'"
# Then tunnel to IP (not container name)
ssh -L 5440:<container_ip>:5432 nuc
# Connect: postgres://turbostarter:turbostarter@localhost:5440/core
```
**Seeded users:** `me+admin@turbostarter.dev` / `Pa$$w0rd` (admin), `me+user@turbostarter.dev` / `Pa$$w0rd`
**Key env vars:**
- `BETTER_AUTH_TRUSTED_ORIGINS` — comma-separated list of allowed origins (CSRF protection)
- `NEXT_PUBLIC_URL` — build-time arg baked into Next.js static output (must rebuild to change)
- `DATABASE_URL` — internal docker network connection to pgvector
### New Site from nuc-portal Template
@@ -751,7 +1033,7 @@ rm -rf .git .next node_modules
npm install && npm run build # verify it builds
git init && git add -A && git commit -m "Initial commit"
# Create repo in Gitea first, then:
git remote add origin gitea:nuc/<repo-name>.git
git remote add origin gitea:alezmad/<repo-name>.git
git push -u origin main
```