From e0cfd371c09194955b05e79f1a3a434a84144603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sun, 23 Nov 2025 18:23:51 +0100 Subject: [PATCH] feat: initial commit - Supervisor UI with Next.js 16 and Tailwind CSS 4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modern web interface for Supervisor process management - Built with Next.js 16 (App Router) and Tailwind CSS 4 - Full XML-RPC client implementation for Supervisor API - Real-time process monitoring with auto-refresh - Process control: start, stop, restart operations - Modern dashboard with system status and statistics - Dark/light theme with OKLCH color system - Docker multi-stage build with runtime env var configuration - Gitea CI/CD workflow for automated builds - Comprehensive documentation (README, IMPLEMENTATION, DEPLOYMENT) Features: - Backend proxy pattern for secure API communication - React Query for state management and caching - TypeScript strict mode with Zod validation - Responsive design with mobile support - Health check endpoint for monitoring - Non-root user security in Docker Environment Variables: - SUPERVISOR_HOST, SUPERVISOR_PORT - SUPERVISOR_USERNAME, SUPERVISOR_PASSWORD (optional) - Configurable at build-time and runtime 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude --- .dockerignore | 59 + .env.example | 12 + .eslintrc.json | 3 + .gitattributes | 53 + .gitea/workflows/docker-build-push.yml | 132 + .gitignore | 51 + .prettierrc | 8 + DEPLOYMENT.md | 550 ++ Dockerfile | 76 + IMPLEMENTATION.md | 648 +++ README.md | 257 + app/api/health/route.ts | 5 + .../processes/[name]/logs/stderr/route.ts | 27 + .../processes/[name]/logs/stdout/route.ts | 27 + .../processes/[name]/restart/route.ts | 21 + app/api/supervisor/processes/[name]/route.ts | 23 + .../processes/[name]/start/route.ts | 24 + .../supervisor/processes/[name]/stop/route.ts | 24 + app/api/supervisor/processes/route.ts | 18 + app/api/supervisor/system/route.ts | 18 + app/config/page.tsx | 25 + app/globals.css | 329 ++ app/layout.tsx | 50 + app/logs/page.tsx | 25 + app/page.tsx | 135 + app/processes/page.tsx | 71 + components/layout/Navbar.tsx | 70 + components/process/ProcessCard.tsx | 115 + components/process/SystemStatus.tsx | 88 + components/providers/Providers.tsx | 30 + components/providers/ThemeProvider.tsx | 77 + components/ui/badge.tsx | 27 + components/ui/button.tsx | 44 + components/ui/card.tsx | 69 + docker-compose.yml | 38 + lib/hooks/useSupervisor.ts | 190 + lib/supervisor/client.ts | 281 ++ lib/supervisor/types.ts | 196 + lib/utils/cn.ts | 10 + next.config.ts | 12 + package.json | 48 + pnpm-lock.yaml | 4492 +++++++++++++++++ postcss.config.mjs | 5 + tsconfig.json | 41 + 44 files changed, 8504 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .eslintrc.json create mode 100644 .gitattributes create mode 100644 .gitea/workflows/docker-build-push.yml create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 DEPLOYMENT.md create mode 100644 Dockerfile create mode 100644 IMPLEMENTATION.md create mode 100644 README.md create mode 100644 app/api/health/route.ts create mode 100644 app/api/supervisor/processes/[name]/logs/stderr/route.ts create mode 100644 app/api/supervisor/processes/[name]/logs/stdout/route.ts create mode 100644 app/api/supervisor/processes/[name]/restart/route.ts create mode 100644 app/api/supervisor/processes/[name]/route.ts create mode 100644 app/api/supervisor/processes/[name]/start/route.ts create mode 100644 app/api/supervisor/processes/[name]/stop/route.ts create mode 100644 app/api/supervisor/processes/route.ts create mode 100644 app/api/supervisor/system/route.ts create mode 100644 app/config/page.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/logs/page.tsx create mode 100644 app/page.tsx create mode 100644 app/processes/page.tsx create mode 100644 components/layout/Navbar.tsx create mode 100644 components/process/ProcessCard.tsx create mode 100644 components/process/SystemStatus.tsx create mode 100644 components/providers/Providers.tsx create mode 100644 components/providers/ThemeProvider.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 docker-compose.yml create mode 100644 lib/hooks/useSupervisor.ts create mode 100644 lib/supervisor/client.ts create mode 100644 lib/supervisor/types.ts create mode 100644 lib/utils/cn.ts create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.mjs create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cda28f9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,59 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Build output +.next +out +dist +build + +# Git +.git +.gitignore +.gitattributes + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# Environment files +.env +.env.local +.env.*.local +.env.production + +# Testing +coverage +.nyc_output +*.test.ts +*.test.tsx +*.spec.ts +*.spec.tsx + +# Misc +.DS_Store +*.pem +.cache +.turbo +.vercel + +# Documentation +README.md +CHANGELOG.md +LICENSE +docs + +# CI/CD +.github +.gitlab-ci.yml +.travis.yml + +# Claude +.claude diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..47b5d06 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Supervisor XML-RPC Connection +# For local development +SUPERVISOR_HOST=localhost +SUPERVISOR_PORT=9001 + +# For Docker Compose +# SUPERVISOR_HOST=supervisor +# SUPERVISOR_PORT=9001 + +# Optional: HTTP Basic Auth (if configured in supervisord.conf) +# SUPERVISOR_USERNAME=user +# SUPERVISOR_PASSWORD=pass diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2ae49f2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,53 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Source code +*.js text eol=lf +*.jsx text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.json text eol=lf +*.css text eol=lf +*.scss text eol=lf +*.md text eol=lf + +# Shell scripts +*.sh text eol=lf + +# Docker +Dockerfile text eol=lf +*.dockerignore text eol=lf + +# Config files +*.yml text eol=lf +*.yaml text eol=lf +*.toml text eol=lf +*.ini text eol=lf +*.env text eol=lf +.env* text eol=lf + +# Documentation +*.md text eol=lf +LICENSE text eol=lf +README* text eol=lf + +# Archives +*.zip binary +*.tar binary +*.gz binary + +# Images +*.jpg binary +*.jpeg binary +*.png binary +*.gif binary +*.ico binary +*.svg text eol=lf +*.webp binary + +# Fonts +*.woff binary +*.woff2 binary +*.ttf binary +*.otf binary +*.eot binary diff --git a/.gitea/workflows/docker-build-push.yml b/.gitea/workflows/docker-build-push.yml new file mode 100644 index 0000000..581f515 --- /dev/null +++ b/.gitea/workflows/docker-build-push.yml @@ -0,0 +1,132 @@ +name: Build and Push Docker Image to Gitea + +on: + push: + branches: + - main + - develop + tags: + - 'v*.*.*' + pull_request: + branches: + - main + workflow_dispatch: + inputs: + tag: + description: 'Custom tag for the image' + required: false + default: 'manual' + +env: + REGISTRY: dev.pivoine.art + IMAGE_NAME: valknar/supervisor-ui + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # Tag as 'latest' for main branch + type=raw,value=latest,enable={{is_default_branch}} + # Tag with branch name + type=ref,event=branch + # Tag with PR number + type=ref,event=pr + # Tag with git tag (semver) + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + # Tag with commit SHA + type=sha,prefix={{branch}}- + # Custom tag from workflow_dispatch + type=raw,value=${{ gitea.event.inputs.tag }},enable=${{ gitea.event_name == 'workflow_dispatch' }} + labels: | + org.opencontainers.image.title=Supervisor UI + org.opencontainers.image.description=Modern web interface for Supervisor process management built with Next.js 16 and Tailwind CSS 4 + org.opencontainers.image.vendor=valknar + org.opencontainers.image.source=https://dev.pivoine.art/${{ gitea.repository }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64 + push: ${{ gitea.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max + build-args: | + NODE_ENV=production + CI=true + SUPERVISOR_HOST=localhost + SUPERVISOR_PORT=9001 + + - name: Generate image digest + if: gitea.event_name != 'pull_request' + run: | + echo "### Docker Image Published :rocket:" >> $GITEA_STEP_SUMMARY + echo "" >> $GITEA_STEP_SUMMARY + echo "**Registry:** \`${{ env.REGISTRY }}\`" >> $GITEA_STEP_SUMMARY + echo "**Image:** \`${{ env.IMAGE_NAME }}\`" >> $GITEA_STEP_SUMMARY + echo "" >> $GITEA_STEP_SUMMARY + echo "**Tags:**" >> $GITEA_STEP_SUMMARY + echo "\`\`\`" >> $GITEA_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" >> $GITEA_STEP_SUMMARY + echo "\`\`\`" >> $GITEA_STEP_SUMMARY + echo "" >> $GITEA_STEP_SUMMARY + echo "**Pull command:**" >> $GITEA_STEP_SUMMARY + echo "\`\`\`bash" >> $GITEA_STEP_SUMMARY + echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITEA_STEP_SUMMARY + echo "\`\`\`" >> $GITEA_STEP_SUMMARY + echo "" >> $GITEA_STEP_SUMMARY + echo "**Run with custom Supervisor connection:**" >> $GITEA_STEP_SUMMARY + echo "\`\`\`bash" >> $GITEA_STEP_SUMMARY + echo "docker run -d \\" >> $GITEA_STEP_SUMMARY + echo " -p 3000:3000 \\" >> $GITEA_STEP_SUMMARY + echo " -e SUPERVISOR_HOST=your-supervisor-host \\" >> $GITEA_STEP_SUMMARY + echo " -e SUPERVISOR_PORT=9001 \\" >> $GITEA_STEP_SUMMARY + echo " -e SUPERVISOR_USERNAME=user \\" >> $GITEA_STEP_SUMMARY + echo " -e SUPERVISOR_PASSWORD=pass \\" >> $GITEA_STEP_SUMMARY + echo " --name supervisor-ui \\" >> $GITEA_STEP_SUMMARY + echo " ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITEA_STEP_SUMMARY + echo "\`\`\`" >> $GITEA_STEP_SUMMARY + + - name: PR Comment - Image built but not pushed + if: gitea.event_name == 'pull_request' + run: | + echo "### Docker Image Built Successfully :white_check_mark:" >> $GITEA_STEP_SUMMARY + echo "" >> $GITEA_STEP_SUMMARY + echo "Image was built successfully but **not pushed** (PR builds are not published)." >> $GITEA_STEP_SUMMARY + echo "" >> $GITEA_STEP_SUMMARY + echo "**Would be tagged as:**" >> $GITEA_STEP_SUMMARY + echo "\`\`\`" >> $GITEA_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" >> $GITEA_STEP_SUMMARY + echo "\`\`\`" >> $GITEA_STEP_SUMMARY + echo "" >> $GITEA_STEP_SUMMARY + echo "**Note:** The image supports runtime environment variables:" >> $GITEA_STEP_SUMMARY + echo "- \`SUPERVISOR_HOST\` - Supervisor API host (default: localhost)" >> $GITEA_STEP_SUMMARY + echo "- \`SUPERVISOR_PORT\` - Supervisor API port (default: 9001)" >> $GITEA_STEP_SUMMARY + echo "- \`SUPERVISOR_USERNAME\` - Optional basic auth username" >> $GITEA_STEP_SUMMARY + echo "- \`SUPERVISOR_PASSWORD\` - Optional basic auth password" >> $GITEA_STEP_SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93d2a21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build +/dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env +.env*.local +.env.production + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# ide +.vscode +.idea +*.swp +*.swo +*~ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..29b9d1f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false +} diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..fd52a62 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,550 @@ +# Deployment Guide - Supervisor UI + +## Environment Variables + +The Supervisor UI supports flexible configuration through environment variables that can be set at both build-time and runtime. + +### Available Environment Variables + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `SUPERVISOR_HOST` | Supervisor API host/IP address | `localhost` | Yes | +| `SUPERVISOR_PORT` | Supervisor API port | `9001` | Yes | +| `SUPERVISOR_USERNAME` | Basic auth username (if enabled) | - | No | +| `SUPERVISOR_PASSWORD` | Basic auth password (if enabled) | - | No | +| `NODE_ENV` | Node environment | `production` | No | +| `PORT` | Application port | `3000` | No | + +### Configuration Priority + +Environment variables can be set at different stages, with runtime taking precedence: + +1. **Runtime** (highest priority) - Docker run `-e` flags or docker-compose environment +2. **Build-time** - Docker build `--build-arg` flags +3. **Dockerfile defaults** (lowest priority) + +## Docker Build Arguments + +You can customize the build with build arguments: + +```bash +docker build -t supervisor-ui \ + --build-arg SUPERVISOR_HOST=supervisor.example.com \ + --build-arg SUPERVISOR_PORT=9001 \ + --build-arg SUPERVISOR_USERNAME=admin \ + --build-arg SUPERVISOR_PASSWORD=secret \ + . +``` + +**Note**: Build arguments are embedded in the image layer. For sensitive credentials, prefer runtime environment variables. + +## Docker Runtime Configuration + +### Option 1: Environment Variables via Docker Run + +```bash +docker run -d \ + --name supervisor-ui \ + -p 3000:3000 \ + -e SUPERVISOR_HOST=supervisor.example.com \ + -e SUPERVISOR_PORT=9001 \ + -e SUPERVISOR_USERNAME=admin \ + -e SUPERVISOR_PASSWORD=secret \ + dev.pivoine.art/valknar/supervisor-ui:latest +``` + +### Option 2: Environment File + +Create an `.env` file: + +```env +SUPERVISOR_HOST=supervisor.example.com +SUPERVISOR_PORT=9001 +SUPERVISOR_USERNAME=admin +SUPERVISOR_PASSWORD=secret +``` + +Run with env file: + +```bash +docker run -d \ + --name supervisor-ui \ + -p 3000:3000 \ + --env-file .env \ + dev.pivoine.art/valknar/supervisor-ui:latest +``` + +### Option 3: Docker Compose + +Create a `.env` file in the same directory as `docker-compose.yml`: + +```env +SUPERVISOR_HOST=supervisor.example.com +SUPERVISOR_PORT=9001 +SUPERVISOR_USERNAME=admin +SUPERVISOR_PASSWORD=secret +``` + +The `docker-compose.yml` automatically picks up these variables: + +```bash +docker-compose up -d +``` + +### Option 4: Docker Compose with Inline Environment + +Edit `docker-compose.yml`: + +```yaml +services: + supervisor-ui: + image: dev.pivoine.art/valknar/supervisor-ui:latest + environment: + - SUPERVISOR_HOST=supervisor.example.com + - SUPERVISOR_PORT=9001 + - SUPERVISOR_USERNAME=admin + - SUPERVISOR_PASSWORD=secret +``` + +## CI/CD - Gitea Workflows + +### Automatic Builds + +The project includes a Gitea Actions workflow that automatically builds and pushes Docker images to the Gitea registry. + +**Workflow file**: `.gitea/workflows/docker-build-push.yml` + +### Trigger Events + +Images are built and pushed on: + +1. **Push to main branch** → Tagged as `latest` and `main-` +2. **Push to develop branch** → Tagged as `develop` and `develop-` +3. **Git tags** (v*.*.* pattern) → Tagged as version (e.g., `v1.0.0`, `1.0`, `1`) +4. **Pull requests** → Built but not pushed (validation only) +5. **Manual workflow dispatch** → Custom tag specified by user + +### Workflow Secrets + +The workflow requires one secret to be configured in your Gitea repository: + +- `REGISTRY_TOKEN` - Gitea personal access token with registry push permissions + +#### Creating the Registry Token + +1. Go to Gitea Settings → Applications → Generate New Token +2. Name: `GitHub Actions Registry` +3. Select scope: `write:package` +4. Copy the generated token +5. Go to Repository Settings → Secrets → Add Secret +6. Name: `REGISTRY_TOKEN` +7. Value: Paste the token + +### Registry Authentication + +To pull images from the Gitea registry: + +```bash +# Login to registry +docker login dev.pivoine.art + +# Pull image +docker pull dev.pivoine.art/valknar/supervisor-ui:latest +``` + +### Image Tags + +After pushing to main, the following tags are available: + +```bash +# Latest stable +dev.pivoine.art/valknar/supervisor-ui:latest + +# Specific commit +dev.pivoine.art/valknar/supervisor-ui:main-abc1234 + +# Version tags (from git tags) +dev.pivoine.art/valknar/supervisor-ui:v1.0.0 +dev.pivoine.art/valknar/supervisor-ui:1.0 +dev.pivoine.art/valknar/supervisor-ui:1 + +# Development branch +dev.pivoine.art/valknar/supervisor-ui:develop +``` + +## Production Deployment Scenarios + +### Scenario 1: Supervisor on Same Host + +Deploy UI on the same host as Supervisor: + +```bash +docker run -d \ + --name supervisor-ui \ + --network host \ + -e SUPERVISOR_HOST=localhost \ + -e SUPERVISOR_PORT=9001 \ + dev.pivoine.art/valknar/supervisor-ui:latest +``` + +Using `--network host` allows the container to access localhost services. + +### Scenario 2: Supervisor on Different Host + +Deploy UI on a different host: + +```bash +docker run -d \ + --name supervisor-ui \ + -p 3000:3000 \ + -e SUPERVISOR_HOST=192.168.1.100 \ + -e SUPERVISOR_PORT=9001 \ + dev.pivoine.art/valknar/supervisor-ui:latest +``` + +Ensure the Supervisor inet_http_server is accessible from the UI host (not bound to 127.0.0.1). + +### Scenario 3: Multiple Supervisor Instances (Future) + +While the current version connects to a single instance, you can run multiple UI containers: + +```bash +# Production Supervisor UI +docker run -d \ + --name supervisor-ui-prod \ + -p 3000:3000 \ + -e SUPERVISOR_HOST=prod.supervisor.local \ + dev.pivoine.art/valknar/supervisor-ui:latest + +# Staging Supervisor UI +docker run -d \ + --name supervisor-ui-staging \ + -p 3001:3000 \ + -e SUPERVISOR_HOST=staging.supervisor.local \ + dev.pivoine.art/valknar/supervisor-ui:latest +``` + +### Scenario 4: Behind Reverse Proxy (nginx/Traefik) + +Example nginx configuration: + +```nginx +server { + listen 80; + server_name supervisor.example.com; + + location / { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +Example Traefik docker-compose labels: + +```yaml +services: + supervisor-ui: + image: dev.pivoine.art/valknar/supervisor-ui:latest + environment: + - SUPERVISOR_HOST=supervisor.local + - SUPERVISOR_PORT=9001 + labels: + - "traefik.enable=true" + - "traefik.http.routers.supervisor-ui.rule=Host(`supervisor.example.com`)" + - "traefik.http.routers.supervisor-ui.entrypoints=websecure" + - "traefik.http.routers.supervisor-ui.tls.certresolver=letsencrypt" + - "traefik.http.services.supervisor-ui.loadbalancer.server.port=3000" +``` + +### Scenario 5: Docker Compose Stack + +Complete production-ready stack with reverse proxy: + +```yaml +version: '3.8' + +services: + supervisor-ui: + image: dev.pivoine.art/valknar/supervisor-ui:latest + container_name: supervisor-ui + restart: unless-stopped + environment: + - SUPERVISOR_HOST=${SUPERVISOR_HOST} + - SUPERVISOR_PORT=${SUPERVISOR_PORT} + - SUPERVISOR_USERNAME=${SUPERVISOR_USERNAME} + - SUPERVISOR_PASSWORD=${SUPERVISOR_PASSWORD} + networks: + - supervisor-network + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + labels: + - "traefik.enable=true" + - "traefik.http.routers.supervisor-ui.rule=Host(`supervisor.yourdomain.com`)" + - "traefik.http.services.supervisor-ui.loadbalancer.server.port=3000" + +networks: + supervisor-network: + external: true +``` + +## Security Considerations + +### 1. Credentials Management + +**Bad** (credentials in image): +```bash +docker build --build-arg SUPERVISOR_PASSWORD=secret . +``` + +**Good** (credentials at runtime): +```bash +docker run -e SUPERVISOR_PASSWORD=secret ... +``` + +**Better** (use secrets management): +```bash +# Docker Swarm secrets +echo "secret_password" | docker secret create supervisor_password - +``` + +### 2. Network Isolation + +Use Docker networks to isolate services: + +```yaml +services: + supervisor-ui: + networks: + - internal # Only accessible from other containers + + nginx: + networks: + - internal + - public # Exposed to internet +``` + +### 3. Read-only Filesystem + +For enhanced security, run with read-only root filesystem: + +```bash +docker run -d \ + --read-only \ + --tmpfs /tmp \ + -e SUPERVISOR_HOST=supervisor.local \ + dev.pivoine.art/valknar/supervisor-ui:latest +``` + +### 4. Resource Limits + +Prevent resource exhaustion: + +```yaml +services: + supervisor-ui: + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + cpus: '0.25' + memory: 128M +``` + +## Monitoring & Health Checks + +### Health Check Endpoint + +The application provides a health check at `/api/health`: + +```bash +curl http://localhost:3000/api/health +# {"status":"healthy","timestamp":"2025-11-23T17:00:00.000Z"} +``` + +### Docker Health Check + +Built-in health check monitors the API: + +```bash +docker inspect supervisor-ui --format='{{.State.Health.Status}}' +``` + +### Integration with Monitoring Tools + +#### Prometheus + +Add health check as a target in `prometheus.yml`: + +```yaml +scrape_configs: + - job_name: 'supervisor-ui' + metrics_path: '/api/health' + static_configs: + - targets: ['supervisor-ui:3000'] +``` + +#### Uptime Kuma + +Add HTTP(s) monitor: +- Monitor Type: HTTP(s) +- URL: `http://supervisor-ui:3000/api/health` +- Interval: 60 seconds + +## Troubleshooting + +### Container won't start + +Check logs: +```bash +docker logs supervisor-ui +``` + +Common issues: +- Port 3000 already in use +- Invalid environment variables +- Network connectivity issues + +### Can't connect to Supervisor + +1. Check if Supervisor is accessible: +```bash +# From host +curl http://supervisor-host:9001/RPC2 + +# From container +docker exec supervisor-ui curl http://supervisor-host:9001/RPC2 +``` + +2. Verify Supervisor configuration in `/etc/supervisor/supervisord.conf`: +```ini +[inet_http_server] +port = *:9001 ; Listen on all interfaces, not just 127.0.0.1 +``` + +3. Check environment variables: +```bash +docker exec supervisor-ui env | grep SUPERVISOR +``` + +### Authentication failures + +Verify credentials match your Supervisor configuration: + +```bash +# Test with curl +curl -u username:password http://supervisor-host:9001/RPC2 +``` + +## Rollback Strategy + +### Quick Rollback to Previous Version + +```bash +# Stop current version +docker stop supervisor-ui +docker rm supervisor-ui + +# Run previous version +docker run -d \ + --name supervisor-ui \ + -p 3000:3000 \ + --env-file .env \ + dev.pivoine.art/valknar/supervisor-ui:v1.0.0 # Specific version +``` + +### Using Docker Compose + +```bash +# Edit docker-compose.yml to use previous tag +# Then: +docker-compose up -d --force-recreate +``` + +## Backup & Recovery + +### No Persistent Data + +The Supervisor UI is stateless and doesn't store any data. All configuration is in environment variables. + +To "backup" your deployment: +1. Save your `.env` file or docker-compose.yml +2. Document your Supervisor connection details + +To "restore": +1. Pull the image +2. Apply your saved environment configuration +3. Start the container + +## Performance Tuning + +### Node.js Optimization + +For better performance in production: + +```bash +docker run -d \ + -e NODE_OPTIONS="--max-old-space-size=256" \ + -e SUPERVISOR_HOST=supervisor.local \ + dev.pivoine.art/valknar/supervisor-ui:latest +``` + +### Caching Strategies + +The application uses React Query with intelligent caching: +- System info: 5 second stale time +- Processes: 3 second stale time +- Logs: 2 second stale time + +These are optimized for real-time monitoring while minimizing API calls. + +## Scaling Considerations + +### Horizontal Scaling + +The application is stateless and can be scaled horizontally: + +```yaml +services: + supervisor-ui: + image: dev.pivoine.art/valknar/supervisor-ui:latest + deploy: + replicas: 3 + update_config: + parallelism: 1 + delay: 10s + restart_policy: + condition: on-failure +``` + +### Load Balancing + +Use nginx or Traefik to load balance across replicas: + +```nginx +upstream supervisor_ui { + server supervisor-ui-1:3000; + server supervisor-ui-2:3000; + server supervisor-ui-3:3000; +} + +server { + location / { + proxy_pass http://supervisor_ui; + } +} +``` + +--- + +**Last Updated**: November 23, 2025 +**Version**: 0.1.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0ea7eed --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +# Stage 1: Dependencies +FROM node:20-alpine AS deps +RUN corepack enable && corepack prepare pnpm@latest --activate +WORKDIR /app + +# Copy dependency files +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies with frozen lockfile +RUN pnpm install --frozen-lockfile + +# Stage 2: Builder +FROM node:20-alpine AS builder +RUN corepack enable && corepack prepare pnpm@latest --activate +WORKDIR /app + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules + +# Copy source code +COPY . . + +# Build arguments for environment variables (optional defaults) +ARG SUPERVISOR_HOST=localhost +ARG SUPERVISOR_PORT=9001 +ARG SUPERVISOR_USERNAME= +ARG SUPERVISOR_PASSWORD= + +# Set environment variables for production build +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production +ENV SUPERVISOR_HOST=${SUPERVISOR_HOST} +ENV SUPERVISOR_PORT=${SUPERVISOR_PORT} +ENV SUPERVISOR_USERNAME=${SUPERVISOR_USERNAME} +ENV SUPERVISOR_PASSWORD=${SUPERVISOR_PASSWORD} + +# Build the Next.js application +RUN pnpm build + +# Stage 3: Production runner +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +# Create non-root user +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copy necessary files from builder +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Switch to non-root user +USER nextjs + +# Expose port +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# Runtime environment variables (can be overridden at runtime) +ENV SUPERVISOR_HOST=localhost +ENV SUPERVISOR_PORT=9001 +ENV SUPERVISOR_USERNAME= +ENV SUPERVISOR_PASSWORD= + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Start the application +CMD ["node", "server.js"] diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md new file mode 100644 index 0000000..acf35c1 --- /dev/null +++ b/IMPLEMENTATION.md @@ -0,0 +1,648 @@ +# Supervisor UI - Implementation Summary + +## Overview + +A sophisticated, production-ready web application for managing Supervisor processes, built with Next.js 16 and Tailwind CSS 4. This implementation follows modern best practices and patterns from the pastel-ui project. + +## Project Architecture + +### Technology Stack + +- **Framework**: Next.js 16.0.3 (App Router, Standalone Output) +- **React**: 19.2.0 with strict mode +- **Styling**: Tailwind CSS 4.1.17 (CSS-first approach with OKLCH colors) +- **State Management**: + - TanStack Query 5.90.10 (server state) + - Zustand 5.0.8 (client state - ready for future use) +- **API Communication**: XML-RPC client for Supervisor +- **Type Safety**: TypeScript 5.9.3 + Zod 3.25.76 +- **UI Components**: Custom components with lucide-react icons +- **Notifications**: Sonner 1.7.4 +- **Package Manager**: pnpm 10.20.0 + +### Key Features Implemented + +1. **Real-time Monitoring** + - Auto-refreshing process list (every 3 seconds) + - System status updates (every 5 seconds) + - Process state indicators with color-coded badges + - Uptime tracking and formatting + +2. **Process Control** + - Start/Stop/Restart individual processes + - Intelligent button states based on process status + - Optimistic UI updates with React Query mutations + - Toast notifications for all actions + - Proper error handling with user feedback + +3. **Modern Dashboard** + - System status card with version info + - Process statistics (total, running, stopped, fatal) + - Quick action cards for navigation + - Responsive grid layout + - Smooth animations and transitions + +4. **Backend API Proxy** + - Next.js API routes for all Supervisor methods + - Secure server-side XML-RPC communication + - Environment-based configuration + - Error handling and validation + - RESTful API design + +5. **Theme System** + - Dark/light mode with system preference detection + - Smooth theme transitions + - OKLCH color space for perceptual uniformity + - Custom color palette for process states + - Persistent theme storage + +6. **Docker Support** + - Multi-stage Dockerfile for optimized builds + - Node.js 20 Alpine base (lightweight) + - Non-root user security + - Health check endpoint + - Docker Compose configuration + - Arty integration ready + +## File Structure + +``` +supervisor-ui/ +├── app/ +│ ├── api/ +│ │ ├── health/route.ts # Health check endpoint +│ │ └── supervisor/ +│ │ ├── system/route.ts # System info API +│ │ └── processes/ +│ │ ├── route.ts # All processes API +│ │ └── [name]/ +│ │ ├── route.ts # Single process info +│ │ ├── start/route.ts # Start process +│ │ ├── stop/route.ts # Stop process +│ │ ├── restart/route.ts # Restart process +│ │ └── logs/ +│ │ ├── stdout/route.ts # Stdout logs +│ │ └── stderr/route.ts # Stderr logs +│ ├── config/page.tsx # Configuration page (placeholder) +│ ├── logs/page.tsx # Logs page (placeholder) +│ ├── processes/page.tsx # Process management page +│ ├── globals.css # Tailwind CSS + theme system +│ ├── layout.tsx # Root layout with providers +│ └── page.tsx # Dashboard home +├── components/ +│ ├── layout/ +│ │ └── Navbar.tsx # Navigation bar with theme toggle +│ ├── process/ +│ │ ├── ProcessCard.tsx # Individual process card +│ │ └── SystemStatus.tsx # System status display +│ ├── providers/ +│ │ ├── Providers.tsx # Combined provider wrapper +│ │ └── ThemeProvider.tsx # Theme context provider +│ └── ui/ +│ ├── badge.tsx # Badge component +│ ├── button.tsx # Button component +│ └── card.tsx # Card components +├── lib/ +│ ├── hooks/ +│ │ └── useSupervisor.ts # React Query hooks +│ ├── supervisor/ +│ │ ├── client.ts # XML-RPC client class +│ │ └── types.ts # TypeScript types + Zod schemas +│ └── utils/ +│ └── cn.ts # Class name utility +├── .dockerignore # Docker ignore patterns +├── .env.example # Environment variable template +├── .eslintrc.json # ESLint configuration +├── .gitignore # Git ignore patterns +├── .prettierrc # Prettier configuration +├── docker-compose.yml # Docker Compose setup +├── Dockerfile # Multi-stage production build +├── next.config.ts # Next.js configuration +├── package.json # Dependencies and scripts +├── postcss.config.mjs # PostCSS with Tailwind plugin +├── README.md # User documentation +└── tsconfig.json # TypeScript configuration +``` + +## API Routes Specification + +### System Information +- **GET** `/api/health` - Health check (returns status and timestamp) +- **GET** `/api/supervisor/system` - Get system info (version, state, PID) + +### Process Management +- **GET** `/api/supervisor/processes` - List all processes +- **GET** `/api/supervisor/processes/[name]` - Get single process info +- **POST** `/api/supervisor/processes/[name]/start` - Start process +- **POST** `/api/supervisor/processes/[name]/stop` - Stop process +- **POST** `/api/supervisor/processes/[name]/restart` - Restart process + +### Logs +- **GET** `/api/supervisor/processes/[name]/logs/stdout` - Get stdout logs +- **GET** `/api/supervisor/processes/[name]/logs/stderr` - Get stderr logs + +Query parameters for logs: +- `offset` (default: -4096) - Byte offset for reading +- `length` (default: 4096) - Number of bytes to read + +## Configuration + +### Environment Variables + +```env +# Supervisor Connection +SUPERVISOR_HOST=localhost +SUPERVISOR_PORT=9001 + +# Optional: Basic Authentication +SUPERVISOR_USERNAME=user +SUPERVISOR_PASSWORD=pass +``` + +### Supervisor Setup + +Your `supervisord.conf` must enable the inet HTTP server: + +```ini +[inet_http_server] +port = 127.0.0.1:9001 +username = user ; Optional +password = pass ; Optional +``` + +## Development Workflow + +### Local Development + +```bash +# Install dependencies +pnpm install + +# Create environment file +cp .env.example .env.local +# Edit .env.local with your Supervisor connection details + +# Run development server +pnpm dev + +# Open http://localhost:3000 +``` + +### Code Quality + +```bash +# Type checking +pnpm type-check + +# Linting +pnpm lint +pnpm lint:fix + +# Formatting +pnpm format +``` + +### Production Build + +```bash +# Build for production +pnpm build + +# Start production server +pnpm start +``` + +## Docker Deployment + +### Option 1: Docker Build & Run + +```bash +# Build image +docker build -t supervisor-ui:latest . + +# Run container +docker run -d \ + --name supervisor-ui \ + -p 3000:3000 \ + -e SUPERVISOR_HOST=localhost \ + -e SUPERVISOR_PORT=9001 \ + supervisor-ui:latest +``` + +### Option 2: Docker Compose + +```bash +# Start service +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop service +docker-compose down +``` + +### Option 3: Arty (Recommended for this environment) + +```bash +# Start service +arty up -d supervisor-ui + +# Check status +arty ps + +# View logs +arty logs supervisor-ui + +# Stop service +arty down supervisor-ui +``` + +## Design System + +### Color Palette (OKLCH) + +The application uses OKLCH color space for perceptually uniform colors: + +**Light Mode**: +- Background: `oklch(98% 0 0)` - Nearly white +- Foreground: `oklch(20% 0 0)` - Dark gray +- Primary: `oklch(55% 0.15 220)` - Blue-green +- Success: `oklch(60% 0.15 140)` - Green +- Warning: `oklch(75% 0.15 90)` - Yellow +- Destructive: `oklch(55% 0.20 25)` - Red + +**Dark Mode**: +- Background: `oklch(15% 0.01 250)` - Very dark blue-gray +- Foreground: `oklch(95% 0 0)` - Nearly white +- Colors automatically adjusted for dark mode + +### Process State Styling + +- **RUNNING**: Green background with success colors +- **STOPPED/EXITED**: Gray muted colors +- **STARTING/BACKOFF**: Yellow warning colors +- **STOPPING**: Yellow warning colors +- **FATAL**: Red destructive colors +- **UNKNOWN**: Gray muted colors + +### Typography + +- Font: Inter (Google Fonts) +- Headings: Bold with gradient text effects +- Body: Regular with good readability +- Code/Mono: For PIDs, uptimes, versions + +### Animations + +- `fade-in`: Gentle fade-in (0.3s) +- `slide-up/down`: Slide animations (0.4s) +- `scale-in`: Scale animation (0.2s) +- `pulse-slow`: Slow pulsing (3s) +- `spin-slow`: Slow rotation (3s) + +## React Query Configuration + +### Query Settings + +- **Stale Time**: 3 seconds (for real-time feel) +- **Refetch on Window Focus**: Disabled +- **Retry**: 2 attempts +- **Refetch Intervals**: + - System info: 5 seconds + - Processes list: 3 seconds + - Individual process: 3 seconds + - Logs: 2 seconds (when enabled) + +### Mutation Handling + +All mutations (start/stop/restart) automatically: +1. Show loading states on buttons +2. Display toast notifications on success/error +3. Invalidate related queries to trigger refetch +4. Update UI optimistically where possible + +## Supervisor Client Implementation + +The XML-RPC client (`lib/supervisor/client.ts`) implements all Supervisor API methods: + +### System Methods +- `getAPIVersion()` +- `getSupervisorVersion()` +- `getIdentification()` +- `getState()` +- `getPID()` +- `getSystemInfo()` - Combined method + +### Process Control +- `getAllProcessInfo()` +- `getProcessInfo(name)` +- `startProcess(name, wait)` +- `stopProcess(name, wait)` +- `restartProcess(name)` - Custom implementation +- `signalProcess(name, signal)` +- `signalProcessGroup(name, signal)` +- `signalAllProcesses(signal)` + +### Log Methods +- `readProcessStdoutLog(name, offset, length)` +- `readProcessStderrLog(name, offset, length)` +- `tailProcessStdoutLog(name, offset, length)` +- `tailProcessStderrLog(name, offset, length)` +- `clearProcessLogs(name)` +- `clearAllProcessLogs()` + +### Configuration Methods +- `reloadConfig()` +- `addProcessGroup(name)` +- `removeProcessGroup(name)` + +### Control Methods +- `shutdown()` +- `restart()` +- `sendProcessStdin(name, chars)` + +## Type Safety + +### Zod Schemas + +All API responses are validated with Zod schemas: +- `ProcessInfoSchema` - Process information structure +- `SupervisorStateInfoSchema` - System state structure +- `ConfigInfoSchema` - Configuration structure +- `ReloadConfigResultSchema` - Reload results + +### TypeScript Types + +Comprehensive type definitions for: +- Process states and state codes +- Supervisor states +- API responses and requests +- Component props +- Hook return types + +## Future Enhancements + +### Planned Features + +1. **Log Viewer** + - Real-time log tailing with auto-scroll + - Search and filtering capabilities + - Multi-process log aggregation + - Log level highlighting + - Download log files + +2. **Configuration Management** + - Reload configuration UI + - Add/remove process groups + - View full configuration + - Validation before applying + +3. **Advanced Features** + - Process group batch operations + - Keyboard shortcuts (start/stop/restart) + - Charts for uptime and resource usage + - Process dependency visualization + - Event history timeline + - Notifications for state changes + +4. **Real-time Updates** + - WebSocket support for push updates + - Server-Sent Events (SSE) fallback + - Reduce polling when WebSocket available + +5. **Multi-Instance Support** + - Connect to multiple Supervisor servers + - Instance switcher in navbar + - Dashboard showing all instances + - Stored connections management + +## Performance Optimizations + +### Current Optimizations + +1. **React Query Caching** + - Intelligent cache invalidation + - Stale-while-revalidate pattern + - Background refetching + +2. **Component Optimizations** + - Proper React.memo usage where needed + - Efficient re-render prevention + - Loading skeletons for better UX + +3. **Docker Build** + - Multi-stage build reduces image size + - Layer caching for faster rebuilds + - Non-root user for security + - Health checks for reliability + +4. **Tailwind CSS** + - CSS-first approach with v4 + - Tree-shaking unused styles + - Optimized for production + +### Potential Optimizations + +1. **Code Splitting** + - Dynamic imports for heavy components + - Route-based code splitting + - Lazy loading for charts/graphs + +2. **Image Optimization** + - Next.js Image component + - WebP format support + - Lazy loading images + +3. **Bundle Analysis** + - Use webpack-bundle-analyzer + - Identify large dependencies + - Replace or optimize heavy libraries + +## Security Considerations + +### Implemented + +1. **Backend Proxy Pattern** + - No direct browser-to-Supervisor connection + - Credentials stored server-side only + - CSRF protection via Next.js + +2. **Docker Security** + - Non-root user in container + - Minimal attack surface + - Health checks for monitoring + +3. **Type Safety** + - Runtime validation with Zod + - TypeScript strict mode + - No unsafe type assertions + +### Recommendations + +1. **Authentication** + - Add user authentication layer + - Role-based access control + - Session management + +2. **HTTPS** + - Use HTTPS in production + - Secure cookie flags + - HSTS headers + +3. **Rate Limiting** + - Implement rate limiting on API routes + - Prevent DoS attacks + - Monitor for abuse + +## Testing Strategy + +### Current State + +- Development testing completed +- Manual UI testing performed +- API route functionality verified + +### Recommended Testing + +1. **Unit Tests** + - Supervisor client methods + - Utility functions + - Component logic + +2. **Integration Tests** + - API route handlers + - React Query hooks + - Component interactions + +3. **E2E Tests** + - Critical user flows + - Process control operations + - Error scenarios + +4. **Tools to Add** + - Vitest for unit tests + - React Testing Library + - Playwright for E2E tests + +## Deployment Checklist + +### Pre-Deployment + +- [ ] Set environment variables +- [ ] Configure Supervisor connection +- [ ] Test API connectivity +- [ ] Review security settings +- [ ] Enable HTTPS if possible +- [ ] Set up monitoring/logging + +### Docker Deployment + +- [ ] Build Docker image +- [ ] Push to registry (if using one) +- [ ] Configure docker-compose.yml +- [ ] Set up persistent storage (if needed) +- [ ] Configure networking +- [ ] Set up reverse proxy (nginx/traefik) + +### Post-Deployment + +- [ ] Verify health check endpoint +- [ ] Test all process operations +- [ ] Check real-time updates +- [ ] Verify theme switching +- [ ] Test on different browsers +- [ ] Monitor error logs + +## Maintenance + +### Regular Tasks + +1. **Dependency Updates** + ```bash + pnpm update --latest + pnpm audit + ``` + +2. **Security Patches** + - Monitor security advisories + - Apply patches promptly + - Test after updates + +3. **Performance Monitoring** + - Check response times + - Monitor memory usage + - Review error logs + +4. **Backup** + - No persistent data stored + - Configuration in environment variables + - Code in version control + +## Troubleshooting + +### Common Issues + +1. **"Failed to connect to Supervisor"** + - Check Supervisor is running + - Verify inet_http_server enabled + - Check SUPERVISOR_HOST and PORT + - Test with `curl http://localhost:9001/RPC2` + +2. **Theme Not Persisting** + - Clear browser localStorage + - Check for JavaScript errors + - Verify ThemeProvider is wrapping app + +3. **Processes Not Updating** + - Check React Query devtools + - Verify API routes returning data + - Check browser console for errors + +4. **Docker Container Won't Start** + - Check health check endpoint + - Review container logs + - Verify environment variables + - Check network connectivity + +### Debug Mode + +Enable React Query DevTools by installing: +```bash +pnpm add @tanstack/react-query-devtools +``` + +Add to `components/providers/Providers.tsx`: +```tsx +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; + +// In return statement: + +``` + +## Credits & References + +### Inspired By + +- **pastel-ui**: Configuration patterns, Docker setup, and Tailwind CSS v4 usage +- **Supervisor**: The excellent process control system this UI manages +- **Next.js**: Modern React framework with excellent DX + +### Built With + +- [Next.js](https://nextjs.org/) - React framework +- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS +- [TanStack Query](https://tanstack.com/query/) - Async state management +- [Zod](https://zod.dev/) - TypeScript-first validation +- [Lucide React](https://lucide.dev/) - Beautiful icons +- [Sonner](https://sonner.emilkowal.ski/) - Toast notifications + +## License + +MIT License - Feel free to use and modify for your needs. + +--- + +**Last Updated**: November 23, 2025 +**Version**: 0.1.0 +**Status**: Production Ready (Core Features) diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9d7125 --- /dev/null +++ b/README.md @@ -0,0 +1,257 @@ +# Supervisor UI + +A modern, sophisticated web interface for [Supervisor](http://supervisord.org/) process management built with Next.js 16 and Tailwind CSS 4. + +## Features + +- **Real-time Monitoring**: Auto-refreshing process status and system information +- **Process Control**: Start, stop, and restart processes with a single click +- **Modern Dashboard**: Clean, responsive interface with dark/light mode +- **Backend Proxy**: Secure API proxy for Supervisor XML-RPC calls +- **Docker Ready**: Multi-stage Docker build for production deployment +- **Type-Safe**: Full TypeScript coverage with Zod validation + +## Tech Stack + +- **Framework**: Next.js 16 (App Router) +- **Styling**: Tailwind CSS 4 with OKLCH color system +- **State Management**: TanStack Query (React Query) + Zustand +- **API**: XML-RPC client for Supervisor +- **Type Safety**: TypeScript + Zod schemas +- **UI Components**: Custom components with lucide-react icons +- **Notifications**: Sonner toast notifications + +## Prerequisites + +- Node.js 20+ with pnpm 10+ +- Supervisor instance running with XML-RPC interface enabled + +## Configuration + +### Supervisor Setup + +Ensure your `supervisord.conf` has the inet HTTP server enabled: + +```ini +[inet_http_server] +port = 127.0.0.1:9001 +;username = user +;password = pass +``` + +### Environment Variables + +Create a `.env.local` file: + +```bash +# Supervisor connection +SUPERVISOR_HOST=localhost +SUPERVISOR_PORT=9001 + +# Optional: Basic auth (if configured in supervisord.conf) +# SUPERVISOR_USERNAME=user +# SUPERVISOR_PASSWORD=pass +``` + +## Development + +```bash +# Install dependencies +pnpm install + +# Run development server +pnpm dev + +# Open http://localhost:3000 +``` + +## Production Build + +```bash +# Build for production +pnpm build + +# Start production server +pnpm start +``` + +## Docker Deployment + +### Using Pre-built Image from Gitea Registry + +The easiest way to deploy is using the pre-built image from the Gitea registry: + +```bash +# Pull the latest image +docker pull dev.pivoine.art/valknar/supervisor-ui:latest + +# Run container with custom Supervisor connection +docker run -d \ + -p 3000:3000 \ + -e SUPERVISOR_HOST=your-supervisor-host \ + -e SUPERVISOR_PORT=9001 \ + -e SUPERVISOR_USERNAME=user \ + -e SUPERVISOR_PASSWORD=pass \ + --name supervisor-ui \ + dev.pivoine.art/valknar/supervisor-ui:latest +``` + +### Build and Run Locally + +```bash +# Build Docker image with custom defaults +docker build -t supervisor-ui \ + --build-arg SUPERVISOR_HOST=localhost \ + --build-arg SUPERVISOR_PORT=9001 \ + . + +# Run container (environment variables override build args) +docker run -d \ + -p 3000:3000 \ + -e SUPERVISOR_HOST=localhost \ + -e SUPERVISOR_PORT=9001 \ + --name supervisor-ui \ + supervisor-ui +``` + +### Using Docker Compose + +Create a `.env` file for your configuration: + +```env +SUPERVISOR_HOST=localhost +SUPERVISOR_PORT=9001 +SUPERVISOR_USERNAME=user +SUPERVISOR_PASSWORD=pass +``` + +Then use Docker Compose: + +```bash +# Start with docker-compose +docker-compose up -d + +# View logs +docker-compose logs -f supervisor-ui + +# Stop +docker-compose down +``` + +To use the pre-built image, edit `docker-compose.yml` and uncomment the registry image line. + +### Arty Integration + +This project supports [Arty](https://github.com/yourusername/arty) for container management: + +```bash +# Start the service +arty up -d supervisor-ui + +# View status +arty ps + +# View logs +arty logs supervisor-ui +``` + +## Container Registry + +Docker images are automatically built and pushed to the Gitea registry on every commit to main: + +- **Registry**: `dev.pivoine.art` +- **Image**: `valknar/supervisor-ui` +- **Tags**: + - `latest` - Latest stable build from main branch + - `develop` - Latest development build + - `v*.*.*` - Semantic version tags + - `main-` - Commit-specific builds + +### Available Tags + +```bash +# Pull specific versions +docker pull dev.pivoine.art/valknar/supervisor-ui:latest +docker pull dev.pivoine.art/valknar/supervisor-ui:v1.0.0 +docker pull dev.pivoine.art/valknar/supervisor-ui:develop +``` + +## Project Structure + +``` +supervisor-ui/ +├── app/ # Next.js App Router +│ ├── api/supervisor/ # API routes (Supervisor proxy) +│ ├── processes/ # Process management page +│ ├── logs/ # Logs viewer page +│ ├── config/ # Configuration page +│ └── page.tsx # Dashboard home +├── components/ +│ ├── layout/ # Layout components (Navbar) +│ ├── process/ # Process-specific components +│ ├── providers/ # Context providers +│ └── ui/ # Reusable UI components +├── lib/ +│ ├── hooks/ # React Query hooks +│ ├── supervisor/ # Supervisor client & types +│ └── utils/ # Utility functions +├── Dockerfile # Multi-stage production build +├── docker-compose.yml # Docker Compose configuration +└── package.json # Dependencies and scripts +``` + +## API Routes + +All Supervisor operations are proxied through Next.js API routes: + +- `GET /api/health` - Health check endpoint +- `GET /api/supervisor/system` - System information +- `GET /api/supervisor/processes` - All processes +- `GET /api/supervisor/processes/[name]` - Single process info +- `POST /api/supervisor/processes/[name]/start` - Start process +- `POST /api/supervisor/processes/[name]/stop` - Stop process +- `POST /api/supervisor/processes/[name]/restart` - Restart process +- `GET /api/supervisor/processes/[name]/logs/stdout` - Stdout logs +- `GET /api/supervisor/processes/[name]/logs/stderr` - Stderr logs + +## Features Roadmap + +- [x] Real-time process monitoring +- [x] Process control (start/stop/restart) +- [x] System status dashboard +- [x] Dark/light mode theme +- [x] Docker deployment +- [ ] Log viewer with real-time tailing +- [ ] Configuration management UI +- [ ] Process group operations +- [ ] Batch process actions +- [ ] Charts and metrics visualization +- [ ] Search and filtering +- [ ] Keyboard shortcuts +- [ ] WebSocket support for push updates + +## Development Scripts + +```bash +# Development +pnpm dev # Start dev server with hot reload +pnpm dev:turbo # Start dev server with Turbopack + +# Build +pnpm build # Production build +pnpm start # Start production server + +# Code Quality +pnpm lint # Run ESLint +pnpm lint:fix # Fix ESLint issues +pnpm format # Format with Prettier +pnpm type-check # TypeScript type checking +``` + +## License + +MIT + +## Credits + +Built with inspiration from modern UI patterns and the excellent [pastel-ui](https://github.com/yourusername/pastel-ui) project configuration. diff --git a/app/api/health/route.ts b/app/api/health/route.ts new file mode 100644 index 0000000..c6080ba --- /dev/null +++ b/app/api/health/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ status: 'healthy', timestamp: new Date().toISOString() }); +} diff --git a/app/api/supervisor/processes/[name]/logs/stderr/route.ts b/app/api/supervisor/processes/[name]/logs/stderr/route.ts new file mode 100644 index 0000000..2abc187 --- /dev/null +++ b/app/api/supervisor/processes/[name]/logs/stderr/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +export const dynamic = 'force-dynamic'; + +interface RouteParams { + params: Promise<{ name: string }>; +} + +export async function GET(request: NextRequest, { params }: RouteParams) { + try { + const { name } = await params; + const searchParams = request.nextUrl.searchParams; + const offset = parseInt(searchParams.get('offset') || '-4096', 10); + const length = parseInt(searchParams.get('length') || '4096', 10); + + const client = createSupervisorClient(); + const logs = await client.tailProcessStderrLog(name, offset, length); + return NextResponse.json(logs); + } catch (error: any) { + console.error('Supervisor stderr log error:', error); + return NextResponse.json( + { error: error.message || 'Failed to fetch stderr logs' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/processes/[name]/logs/stdout/route.ts b/app/api/supervisor/processes/[name]/logs/stdout/route.ts new file mode 100644 index 0000000..e8a73f4 --- /dev/null +++ b/app/api/supervisor/processes/[name]/logs/stdout/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +export const dynamic = 'force-dynamic'; + +interface RouteParams { + params: Promise<{ name: string }>; +} + +export async function GET(request: NextRequest, { params }: RouteParams) { + try { + const { name } = await params; + const searchParams = request.nextUrl.searchParams; + const offset = parseInt(searchParams.get('offset') || '-4096', 10); + const length = parseInt(searchParams.get('length') || '4096', 10); + + const client = createSupervisorClient(); + const logs = await client.tailProcessStdoutLog(name, offset, length); + return NextResponse.json(logs); + } catch (error: any) { + console.error('Supervisor stdout log error:', error); + return NextResponse.json( + { error: error.message || 'Failed to fetch stdout logs' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/processes/[name]/restart/route.ts b/app/api/supervisor/processes/[name]/restart/route.ts new file mode 100644 index 0000000..9d2299f --- /dev/null +++ b/app/api/supervisor/processes/[name]/restart/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +interface RouteParams { + params: Promise<{ name: string }>; +} + +export async function POST(request: NextRequest, { params }: RouteParams) { + try { + const { name } = await params; + const client = createSupervisorClient(); + const result = await client.restartProcess(name); + return NextResponse.json({ success: result, message: `Process ${name} restarted` }); + } catch (error: any) { + console.error('Supervisor restart process error:', error); + return NextResponse.json( + { error: error.message || 'Failed to restart process' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/processes/[name]/route.ts b/app/api/supervisor/processes/[name]/route.ts new file mode 100644 index 0000000..510efbc --- /dev/null +++ b/app/api/supervisor/processes/[name]/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +export const dynamic = 'force-dynamic'; + +interface RouteParams { + params: Promise<{ name: string }>; +} + +export async function GET(request: NextRequest, { params }: RouteParams) { + try { + const { name } = await params; + const client = createSupervisorClient(); + const processInfo = await client.getProcessInfo(name); + return NextResponse.json(processInfo); + } catch (error: any) { + console.error('Supervisor process info error:', error); + return NextResponse.json( + { error: error.message || 'Failed to fetch process info' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/processes/[name]/start/route.ts b/app/api/supervisor/processes/[name]/start/route.ts new file mode 100644 index 0000000..7dc367e --- /dev/null +++ b/app/api/supervisor/processes/[name]/start/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +interface RouteParams { + params: Promise<{ name: string }>; +} + +export async function POST(request: NextRequest, { params }: RouteParams) { + try { + const { name } = await params; + const body = await request.json().catch(() => ({})); + const wait = body.wait !== undefined ? body.wait : true; + + const client = createSupervisorClient(); + const result = await client.startProcess(name, wait); + return NextResponse.json({ success: result, message: `Process ${name} started` }); + } catch (error: any) { + console.error('Supervisor start process error:', error); + return NextResponse.json( + { error: error.message || 'Failed to start process' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/processes/[name]/stop/route.ts b/app/api/supervisor/processes/[name]/stop/route.ts new file mode 100644 index 0000000..10c6540 --- /dev/null +++ b/app/api/supervisor/processes/[name]/stop/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +interface RouteParams { + params: Promise<{ name: string }>; +} + +export async function POST(request: NextRequest, { params }: RouteParams) { + try { + const { name } = await params; + const body = await request.json().catch(() => ({})); + const wait = body.wait !== undefined ? body.wait : true; + + const client = createSupervisorClient(); + const result = await client.stopProcess(name, wait); + return NextResponse.json({ success: result, message: `Process ${name} stopped` }); + } catch (error: any) { + console.error('Supervisor stop process error:', error); + return NextResponse.json( + { error: error.message || 'Failed to stop process' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/processes/route.ts b/app/api/supervisor/processes/route.ts new file mode 100644 index 0000000..a60c797 --- /dev/null +++ b/app/api/supervisor/processes/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + try { + const client = createSupervisorClient(); + const processes = await client.getAllProcessInfo(); + return NextResponse.json(processes); + } catch (error: any) { + console.error('Supervisor processes error:', error); + return NextResponse.json( + { error: error.message || 'Failed to fetch processes' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/system/route.ts b/app/api/supervisor/system/route.ts new file mode 100644 index 0000000..976193f --- /dev/null +++ b/app/api/supervisor/system/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + try { + const client = createSupervisorClient(); + const systemInfo = await client.getSystemInfo(); + return NextResponse.json(systemInfo); + } catch (error: any) { + console.error('Supervisor system info error:', error); + return NextResponse.json( + { error: error.message || 'Failed to fetch system info' }, + { status: 500 } + ); + } +} diff --git a/app/config/page.tsx b/app/config/page.tsx new file mode 100644 index 0000000..fe53395 --- /dev/null +++ b/app/config/page.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { Settings } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; + +export default function ConfigPage() { + return ( +
+
+

Configuration

+

Manage Supervisor settings

+
+ + + + +

Configuration Coming Soon

+

+ This feature will allow you to reload configuration, add/remove process groups, and manage settings. +

+
+
+
+ ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..0dbc643 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,329 @@ +@import "tailwindcss"; +@plugin "@tailwindcss/typography"; +@plugin "@tailwindcss/forms"; + +/* Content source definitions for Tailwind v4 */ +@source "../components/**/*.{js,ts,jsx,tsx}"; +@source "../lib/**/*.{js,ts,jsx,tsx}"; +@source "./**/*.{js,ts,jsx,tsx}"; + +/* Custom dark mode variant */ +@custom-variant dark (&:is(.dark *)); + +/* Color System - Supervisor Theme */ +:root { + /* Base colors using OKLCH for perceptual uniformity */ + --background: oklch(98% 0 0); + --foreground: oklch(20% 0 0); + + /* Cards and panels */ + --card: oklch(100% 0 0); + --card-foreground: oklch(20% 0 0); + + /* Primary - Process management (blue-green) */ + --primary: oklch(55% 0.15 220); + --primary-foreground: oklch(98% 0 0); + + /* Secondary - System status (slate) */ + --secondary: oklch(75% 0.03 250); + --secondary-foreground: oklch(20% 0 0); + + /* Accent - Highlights (cyan) */ + --accent: oklch(65% 0.12 200); + --accent-foreground: oklch(98% 0 0); + + /* Success - Running state (green) */ + --success: oklch(60% 0.15 140); + --success-foreground: oklch(98% 0 0); + + /* Warning - Stopped/paused (yellow) */ + --warning: oklch(75% 0.15 90); + --warning-foreground: oklch(20% 0 0); + + /* Destructive - Fatal/error (red) */ + --destructive: oklch(55% 0.20 25); + --destructive-foreground: oklch(98% 0 0); + + /* Muted - Disabled states */ + --muted: oklch(92% 0.01 250); + --muted-foreground: oklch(55% 0.01 250); + + /* Borders and inputs */ + --border: oklch(88% 0.01 250); + --input: oklch(88% 0.01 250); + --ring: oklch(55% 0.15 220); + + /* Chart colors */ + --chart-1: oklch(55% 0.15 220); + --chart-2: oklch(60% 0.15 140); + --chart-3: oklch(75% 0.15 90); + --chart-4: oklch(55% 0.20 25); + --chart-5: oklch(65% 0.12 200); + + /* Radius */ + --radius: 0.5rem; +} + +.dark { + --background: oklch(15% 0.01 250); + --foreground: oklch(95% 0 0); + + --card: oklch(18% 0.01 250); + --card-foreground: oklch(95% 0 0); + + --primary: oklch(65% 0.15 220); + --primary-foreground: oklch(15% 0 0); + + --secondary: oklch(25% 0.03 250); + --secondary-foreground: oklch(95% 0 0); + + --accent: oklch(55% 0.12 200); + --accent-foreground: oklch(15% 0 0); + + --success: oklch(55% 0.15 140); + --success-foreground: oklch(15% 0 0); + + --warning: oklch(65% 0.15 90); + --warning-foreground: oklch(15% 0 0); + + --destructive: oklch(60% 0.20 25); + --destructive-foreground: oklch(98% 0 0); + + --muted: oklch(20% 0.01 250); + --muted-foreground: oklch(60% 0.01 250); + + --border: oklch(25% 0.01 250); + --input: oklch(25% 0.01 250); + --ring: oklch(65% 0.15 220); + + --chart-1: oklch(65% 0.15 220); + --chart-2: oklch(55% 0.15 140); + --chart-3: oklch(65% 0.15 90); + --chart-4: oklch(60% 0.20 25); + --chart-5: oklch(55% 0.12 200); +} + +/* Map colors to Tailwind utilities */ +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + + /* Custom animations */ + --animate-fade-in: fadeIn 0.3s ease-in-out; + --animate-slide-up: slideUp 0.4s ease-out; + --animate-slide-down: slideDown 0.4s ease-out; + --animate-slide-in-right: slideInRight 0.3s ease-out; + --animate-slide-in-left: slideInLeft 0.3s ease-out; + --animate-scale-in: scaleIn 0.2s ease-out; + --animate-bounce-gentle: bounceGentle 0.5s ease-in-out; + --animate-shimmer: shimmer 2s infinite; + --animate-pulse-slow: pulseSlow 3s ease-in-out infinite; + --animate-spin-slow: spinSlow 3s linear infinite; +} + +/* Global Styles */ +html { + scroll-behavior: smooth; +} + +* { + @apply border-border; +} + +body { + @apply bg-background text-foreground; + font-feature-settings: "rlig" 1, "calt" 1; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + @apply bg-muted; +} + +::-webkit-scrollbar-thumb { + @apply bg-muted-foreground/30 rounded; +} + +::-webkit-scrollbar-thumb:hover { + @apply bg-muted-foreground/50; +} + +/* Screen reader only */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +/* Disable transitions during theme change */ +.disable-transitions * { + transition: none !important; +} + +/* Animation Keyframes */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + transform: translateY(10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slideDown { + from { + transform: translateY(-10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slideInRight { + from { + transform: translateX(-10px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideInLeft { + from { + transform: translateX(10px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes scaleIn { + from { + transform: scale(0.95); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes bounceGentle { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } +} + +@keyframes pulseSlow { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +@keyframes spinSlow { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* Prose styles for log viewers */ +.prose-log { + @apply font-mono text-sm; + white-space: pre-wrap; + word-break: break-all; +} + +/* Process state indicators */ +.process-running { + @apply bg-success/10 text-success border-success/20; +} + +.process-stopped { + @apply bg-muted text-muted-foreground border-border; +} + +.process-starting { + @apply bg-warning/10 text-warning border-warning/20; +} + +.process-stopping { + @apply bg-warning/10 text-warning border-warning/20; +} + +.process-fatal { + @apply bg-destructive/10 text-destructive border-destructive/20; +} + +.process-unknown { + @apply bg-muted text-muted-foreground border-border; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..c9401d9 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,50 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import './globals.css'; +import { Providers } from '@/components/providers/Providers'; +import { Navbar } from '@/components/layout/Navbar'; + +const inter = Inter({ + subsets: ['latin'], + variable: '--font-inter', +}); + +export const metadata: Metadata = { + title: 'Supervisor UI', + description: 'Modern web interface for Supervisor process management', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +