From 87d698bc5c0746fa921d754362b6dddf2e5f0ac4 Mon Sep 17 00:00:00 2001 From: Giuseppe Raffa <77052701+sesee3@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:23:18 +0200 Subject: [PATCH] feat: add Docker and Gitea services, monitoring, queue, and Telegram notification functionalities - Implemented Docker operations including image building, container management, and resource stats. - Added Gitea API client for repository management and webhook handling. - Introduced monitoring service to collect and store container metrics in InfluxDB. - Created a queue system using BullMQ for managing deployment jobs with real-time log streaming. - Developed Telegram notification service for deployment status updates. - Added Traefik label generation for dynamic reverse proxy configuration. - Implemented WebSocket endpoints for log streaming and terminal access to containers. - Created an updater sidecar for self-updating the AutoDeployer container. --- .claude/settings.local.json | 10 + .env.example | 43 ++ .gitignore | 7 + Dockerfile | 38 ++ README.md | 63 +++ dashboard/css/style.css | 392 +++++++++++++++ dashboard/index.html | 21 + dashboard/js/api.js | 114 +++++ dashboard/js/app.js | 85 ++++ dashboard/js/icons.js | 47 ++ dashboard/js/pages/dashboard.js | 144 ++++++ dashboard/js/pages/login.js | 67 +++ dashboard/js/pages/logs.js | 137 +++++ dashboard/js/pages/monitoring.js | 184 +++++++ dashboard/js/pages/service-detail.js | 720 +++++++++++++++++++++++++++ dashboard/js/pages/settings.js | 168 +++++++ dashboard/js/router.js | 74 +++ dashboard/lib/chart.min.js | 20 + dashboard/lib/xterm-addon-fit.min.js | 8 + dashboard/lib/xterm.min.css | 8 + dashboard/lib/xterm.min.js | 8 + docker-compose.yml | 66 +++ server/package.json | 27 + server/src/config.js | 39 ++ server/src/db/index.js | 235 +++++++++ server/src/index.js | 147 ++++++ server/src/middleware/auth.js | 86 ++++ server/src/routes/auth.js | 185 +++++++ server/src/routes/deploys.js | 51 ++ server/src/routes/logs.js | 34 ++ server/src/routes/monitoring.js | 49 ++ server/src/routes/networks.js | 45 ++ server/src/routes/services.js | 404 +++++++++++++++ server/src/routes/settings.js | 77 +++ server/src/routes/system.js | 50 ++ server/src/routes/webhooks.js | 100 ++++ server/src/services/builder.js | 237 +++++++++ server/src/services/cleanup.js | 107 ++++ server/src/services/docker.js | 350 +++++++++++++ server/src/services/gitea.js | 134 +++++ server/src/services/monitoring.js | 146 ++++++ server/src/services/queue.js | 125 +++++ server/src/services/telegram.js | 92 ++++ server/src/services/traefik.js | 195 ++++++++ server/src/ws/logs.js | 82 +++ server/src/ws/terminal.js | 82 +++ updater/Dockerfile | 10 + updater/update.sh | 45 ++ 48 files changed, 5558 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 dashboard/css/style.css create mode 100644 dashboard/index.html create mode 100644 dashboard/js/api.js create mode 100644 dashboard/js/app.js create mode 100644 dashboard/js/icons.js create mode 100644 dashboard/js/pages/dashboard.js create mode 100644 dashboard/js/pages/login.js create mode 100644 dashboard/js/pages/logs.js create mode 100644 dashboard/js/pages/monitoring.js create mode 100644 dashboard/js/pages/service-detail.js create mode 100644 dashboard/js/pages/settings.js create mode 100644 dashboard/js/router.js create mode 100644 dashboard/lib/chart.min.js create mode 100644 dashboard/lib/xterm-addon-fit.min.js create mode 100644 dashboard/lib/xterm.min.css create mode 100644 dashboard/lib/xterm.min.js create mode 100644 docker-compose.yml create mode 100644 server/package.json create mode 100644 server/src/config.js create mode 100644 server/src/db/index.js create mode 100644 server/src/index.js create mode 100644 server/src/middleware/auth.js create mode 100644 server/src/routes/auth.js create mode 100644 server/src/routes/deploys.js create mode 100644 server/src/routes/logs.js create mode 100644 server/src/routes/monitoring.js create mode 100644 server/src/routes/networks.js create mode 100644 server/src/routes/services.js create mode 100644 server/src/routes/settings.js create mode 100644 server/src/routes/system.js create mode 100644 server/src/routes/webhooks.js create mode 100644 server/src/services/builder.js create mode 100644 server/src/services/cleanup.js create mode 100644 server/src/services/docker.js create mode 100644 server/src/services/gitea.js create mode 100644 server/src/services/monitoring.js create mode 100644 server/src/services/queue.js create mode 100644 server/src/services/telegram.js create mode 100644 server/src/services/traefik.js create mode 100644 server/src/ws/logs.js create mode 100644 server/src/ws/terminal.js create mode 100644 updater/Dockerfile create mode 100644 updater/update.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..911887a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(curl -sL \"https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js\" -o chart.min.js)", + "Bash(curl -sL \"https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css\" -o xterm.min.css)", + "Bash(curl -sL \"https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js\" -o xterm.min.js)", + "Bash(curl -sL \"https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js\" -o xterm-addon-fit.min.js)" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..225c1c7 --- /dev/null +++ b/.env.example @@ -0,0 +1,43 @@ +# =========================================== +# AutoDeployer Configuration +# =========================================== + +# JWT secret for authentication (generate with: openssl rand -hex 64) +JWT_SECRET=change-me-to-a-random-64-char-hex-string + +# Admin password (will be set on first login via setup wizard) +# Leave empty on first run — the app will prompt you to create it +ADMIN_PASSWORD_HASH= + +# Webhook secret for Gitea webhook validation +WEBHOOK_SECRET=change-me-to-a-random-string + +# =========================================== +# Gitea Configuration +# =========================================== +GITEA_URL=http://gitea:3000 +GITEA_TOKEN=your-gitea-api-token + +# =========================================== +# Redis (existing instance on meb-private) +# =========================================== +REDIS_URL=redis://redis:6379/2 + +# =========================================== +# Telegram Notifications +# =========================================== +TELEGRAM_BOT_TOKEN=your-telegram-bot-token +TELEGRAM_CHAT_ID=your-chat-id + +# =========================================== +# InfluxDB (existing instance for monitoring) +# =========================================== +INFLUXDB_URL=http://influxdb:8086 +INFLUXDB_TOKEN=your-influxdb-token +INFLUXDB_ORG=autodeployer +INFLUXDB_BUCKET=metrics + +# =========================================== +# Domain +# =========================================== +DEPLOY_DOMAIN=deploy.example.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc52bbc --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.env +server/data/ +*.db +*.db-wal +*.db-shm diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..884bcbc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# ============================================ +# AutoDeployer — No build step, static dashboard +# ============================================ +FROM node:20-alpine + +# Install build dependencies for native modules (better-sqlite3, node-pty) +RUN apk add --no-cache \ + python3 \ + make \ + g++ \ + git \ + docker-cli + +WORKDIR /app + +# Install server dependencies +COPY server/package*.json ./ +RUN npm ci --production && npm cache clean --force + +# Copy server source +COPY server/src ./src + +# Copy static dashboard (no build step needed) +COPY dashboard/index.html ./public/index.html +COPY dashboard/css ./public/css +COPY dashboard/js ./public/js +COPY dashboard/lib ./public/lib + +# Create data directory +RUN mkdir -p /app/data /tmp/builds + +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://localhost:3000/api/health || exit 1 + +CMD ["node", "src/index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3fe25b --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# AutoDeployer + +Piattaforma di deployment self-hosted per gestire i servizi Docker con integrazione Gitea, Traefik, e monitoring in tempo reale. + +## Feature + +- 🔗 **Integrazione Gitea** — ogni repo = un servizio, deploy automatico via webhook +- 🚀 **Auto-Deploy** — webhook push → build → deploy automatico +- 🐳 **Dockerfile Build** — supporto per path e context personalizzati +- 🏷️ **Traefik Labels** — gestione visuale con preview dei label +- 📋 **Log Viewer** — streaming real-time via WebSocket +- 🔐 **Variabili d'Ambiente** — build-time e runtime, con supporto secret +- ⚡ **Zero-Downtime Deploy** — health check + switch traffico senza interruzioni +- 🔔 **Notifiche Telegram** — notifiche deploy su Telegram +- 📊 **Monitoring** — CPU, RAM, Network in tempo reale con storico su InfluxDB +- 💻 **Web Terminal** — shell interattiva nei container dal browser +- 🛡️ **Docker Networks** — gestione reti Docker dall'interfaccia + +## Setup Rapido + +```bash +# 1. Copia .env +cp .env.example .env + +# 2. Configura le variabili in .env +# - JWT_SECRET (genera con: openssl rand -hex 64) +# - WEBHOOK_SECRET +# - GITEA_TOKEN +# - TELEGRAM_BOT_TOKEN e TELEGRAM_CHAT_ID +# - INFLUXDB_TOKEN +# - DEPLOY_DOMAIN + +# 3. Build e avvia +docker compose up -d --build + +# 4. Apri https://deploy.tuodominio.com +# Al primo accesso, crea l'account admin +``` + +## Architettura + +- **Backend**: Node.js + Express + SQLite + BullMQ +- **Frontend**: React + Vite (servito dallo stesso container) +- **Build Queue**: BullMQ su Redis esistente (DB 2) +- **Monitoring**: InfluxDB per metriche storiche +- **Container**: Singolo container con accesso al Docker socket + +## Reti Docker + +- `meb-public` — Traefik, Gitea, AutoDeployer +- `meb-private` — Redis, PostgreSQL, InfluxDB, servizi interni + +## Webhook + +Ogni servizio genera un URL webhook univoco. Configura il webhook nel repository Gitea: + +1. Gitea → Settings → Webhooks → Add Webhook +2. Target URL: `http://autodeployer:3000/api/webhooks/{webhook-id}` +3. Secret: il `WEBHOOK_SECRET` del `.env` +4. Trigger: Push Events + +> **Nota**: Usa l'URL interno Docker (`http://autodeployer:3000`) per evitare +> il passaggio da Cloudflare. diff --git a/dashboard/css/style.css b/dashboard/css/style.css new file mode 100644 index 0000000..ce9f234 --- /dev/null +++ b/dashboard/css/style.css @@ -0,0 +1,392 @@ +/* ═══════════════════════════════════════════════════════════ + AutoDeployer — Design System + Premium dark theme with glassmorphism + ═══════════════════════════════════════════════════════════ */ + +/* ─── CSS Variables ──────────────────────────────────────── */ +:root { + --bg-primary: #0a0e1a; + --bg-secondary: #111827; + --bg-card: rgba(17, 24, 39, 0.7); + --bg-card-hover: rgba(30, 41, 59, 0.8); + --bg-glass: rgba(255, 255, 255, 0.03); + --bg-glass-hover: rgba(255, 255, 255, 0.06); + --bg-input: rgba(255, 255, 255, 0.05); + --bg-input-focus: rgba(255, 255, 255, 0.08); + --border-primary: rgba(255, 255, 255, 0.06); + --border-hover: rgba(255, 255, 255, 0.12); + --border-focus: rgba(99, 102, 241, 0.5); + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --text-tertiary: #64748b; + --text-muted: #475569; + --accent: #6366f1; + --accent-hover: #818cf8; + --accent-muted: rgba(99, 102, 241, 0.15); + --accent-glow: rgba(99, 102, 241, 0.3); + --status-running: #10b981; + --status-running-bg: rgba(16, 185, 129, 0.12); + --status-building: #f59e0b; + --status-building-bg: rgba(245, 158, 11, 0.12); + --status-error: #ef4444; + --status-error-bg: rgba(239, 68, 68, 0.12); + --status-stopped: #64748b; + --status-stopped-bg: rgba(100, 116, 139, 0.12); + --status-queued: #8b5cf6; + --status-queued-bg: rgba(139, 92, 246, 0.12); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5); + --shadow-glow: 0 0 20px var(--accent-glow); + --sidebar-width: 260px; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 20px; + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1); +} + +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } +html { font-size: 14px; -webkit-font-smoothing: antialiased; } +body { + font-family: var(--font-sans); + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; + overflow-x: hidden; +} +body::before { + content: ''; + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background-image: + radial-gradient(circle at 25% 25%, rgba(99, 102, 241, 0.03) 0%, transparent 50%), + radial-gradient(circle at 75% 75%, rgba(139, 92, 246, 0.03) 0%, transparent 50%); + pointer-events: none; z-index: 0; +} +#app { position: relative; z-index: 1; } +a { color: var(--accent); text-decoration: none; transition: color var(--transition-fast); } +a:hover { color: var(--accent-hover); } + +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); } + +/* ─── Layout ─────────────────────────────────────────────── */ +.app-layout { display: flex; min-height: 100vh; } +.main-content { + flex: 1; margin-left: var(--sidebar-width); + padding: 32px; max-width: calc(100vw - var(--sidebar-width)); min-height: 100vh; +} + +/* ─── Sidebar ────────────────────────────────────────────── */ +.sidebar { + position: fixed; top: 0; left: 0; width: var(--sidebar-width); height: 100vh; + background: rgba(17, 24, 39, 0.85); backdrop-filter: blur(20px); + border-right: 1px solid var(--border-primary); + display: flex; flex-direction: column; z-index: 100; overflow-y: auto; +} +.sidebar-brand { + padding: 24px 20px; border-bottom: 1px solid var(--border-primary); + display: flex; align-items: center; gap: 12px; +} +.sidebar-brand-icon { + width: 36px; height: 36px; + background: linear-gradient(135deg, var(--accent), #8b5cf6); + border-radius: var(--radius-md); + display: flex; align-items: center; justify-content: center; + font-size: 18px; box-shadow: var(--shadow-glow); color: white; +} +.sidebar-brand h1 { + font-size: 1.15rem; font-weight: 700; letter-spacing: -0.02em; + background: linear-gradient(135deg, var(--text-primary), var(--accent-hover)); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; +} +.sidebar-nav { flex: 1; padding: 16px 12px; display: flex; flex-direction: column; gap: 2px; } +.sidebar-section { + margin-top: 20px; margin-bottom: 8px; padding: 0 8px; + font-size: 0.7rem; font-weight: 600; text-transform: uppercase; + letter-spacing: 0.08em; color: var(--text-tertiary); +} +.sidebar-link { + display: flex; align-items: center; gap: 10px; + padding: 10px 12px; border-radius: var(--radius-sm); + color: var(--text-secondary); font-size: 0.9rem; font-weight: 500; + transition: all var(--transition-fast); cursor: pointer; + border: 1px solid transparent; +} +.sidebar-link:hover { background: var(--bg-glass-hover); color: var(--text-primary); } +.sidebar-link.active { background: var(--accent-muted); color: var(--accent-hover); border-color: rgba(99, 102, 241, 0.2); } +.sidebar-link svg { width: 18px; height: 18px; flex-shrink: 0; } +.sidebar-footer { padding: 16px; border-top: 1px solid var(--border-primary); } + +/* ─── Page Header ────────────────────────────────────────── */ +.page-header { + margin-bottom: 28px; display: flex; align-items: center; + justify-content: space-between; flex-wrap: wrap; gap: 16px; +} +.page-header h2 { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; } +.page-header p { color: var(--text-secondary); font-size: 0.9rem; margin-top: 4px; } + +/* ─── Cards ──────────────────────────────────────────────── */ +.card { + background: var(--bg-card); backdrop-filter: blur(12px); + border: 1px solid var(--border-primary); border-radius: var(--radius-lg); + padding: 24px; transition: all var(--transition-normal); +} +.card:hover { border-color: var(--border-hover); box-shadow: var(--shadow-md); } +.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } +.card-title { font-size: 1rem; font-weight: 600; display: flex; align-items: center; gap: 8px; } +.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; } + +/* ─── Status Badges ──────────────────────────────────────── */ +.status-badge { + display: inline-flex; align-items: center; gap: 6px; + padding: 4px 10px; border-radius: 100px; + font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; +} +.status-badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; } +.status-badge.running { background: var(--status-running-bg); color: var(--status-running); } +.status-badge.running::before { background: var(--status-running); box-shadow: 0 0 6px var(--status-running); } +.status-badge.building { background: var(--status-building-bg); color: var(--status-building); animation: pulse-badge 2s ease-in-out infinite; } +.status-badge.building::before { background: var(--status-building); } +.status-badge.error, .status-badge.failed { background: var(--status-error-bg); color: var(--status-error); } +.status-badge.error::before, .status-badge.failed::before { background: var(--status-error); } +.status-badge.stopped { background: var(--status-stopped-bg); color: var(--status-stopped); } +.status-badge.stopped::before { background: var(--status-stopped); } +.status-badge.queued { background: var(--status-queued-bg); color: var(--status-queued); } +.status-badge.queued::before { background: var(--status-queued); animation: pulse-dot 1.5s infinite; } +.status-badge.success { background: var(--status-running-bg); color: var(--status-running); } +.status-badge.success::before { background: var(--status-running); } +@keyframes pulse-badge { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } +@keyframes pulse-dot { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.5); opacity: 0.5; } } + +/* ─── Buttons ────────────────────────────────────────────── */ +.btn { + display: inline-flex; align-items: center; gap: 8px; + padding: 9px 18px; border-radius: var(--radius-sm); + font-size: 0.85rem; font-weight: 600; font-family: var(--font-sans); + cursor: pointer; border: 1px solid transparent; + transition: all var(--transition-fast); white-space: nowrap; +} +.btn svg { width: 16px; height: 16px; } +.btn-primary { + background: linear-gradient(135deg, var(--accent), #7c3aed); color: white; + border-color: rgba(99, 102, 241, 0.3); box-shadow: 0 2px 8px rgba(99, 102, 241, 0.25); +} +.btn-primary:hover { background: linear-gradient(135deg, var(--accent-hover), #8b5cf6); box-shadow: 0 4px 16px rgba(99, 102, 241, 0.35); transform: translateY(-1px); } +.btn-secondary { background: var(--bg-glass); color: var(--text-secondary); border-color: var(--border-primary); } +.btn-secondary:hover { background: var(--bg-glass-hover); color: var(--text-primary); border-color: var(--border-hover); } +.btn-danger { background: rgba(239, 68, 68, 0.1); color: var(--status-error); border-color: rgba(239, 68, 68, 0.2); } +.btn-danger:hover { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.4); } +.btn-sm { padding: 5px 12px; font-size: 0.78rem; } +.btn-icon { padding: 8px; border-radius: var(--radius-sm); } +.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; } + +/* ─── Forms ──────────────────────────────────────────────── */ +.form-group { margin-bottom: 18px; } +.form-label { + display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); + margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.04em; +} +.form-input, .form-select, textarea.form-input { + width: 100%; padding: 10px 14px; background: var(--bg-input); + border: 1px solid var(--border-primary); border-radius: var(--radius-sm); + color: var(--text-primary); font-family: var(--font-sans); font-size: 0.9rem; + transition: all var(--transition-fast); outline: none; +} +.form-input:focus, .form-select:focus, textarea.form-input:focus { + background: var(--bg-input-focus); border-color: var(--border-focus); + box-shadow: 0 0 0 3px var(--accent-muted); +} +.form-input::placeholder { color: var(--text-muted); } +.form-input.mono { font-family: var(--font-mono); font-size: 0.85rem; } +textarea.form-input { min-height: 100px; resize: vertical; font-family: var(--font-mono); font-size: 0.85rem; } +.form-select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%2394a3b8' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; background-position: right 12px center; padding-right: 36px; +} +.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } +.form-hint { font-size: 0.75rem; color: var(--text-tertiary); margin-top: 4px; } +.form-checkbox { display: flex; align-items: center; gap: 10px; cursor: pointer; } +.form-checkbox input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; } + +/* ─── Toggle ──────────────────────────────────────────── */ +.toggle { position: relative; display: inline-flex; align-items: center; gap: 10px; cursor: pointer; } +.toggle input { display: none; } +.toggle-track { + width: 40px; height: 22px; background: var(--bg-input); + border: 1px solid var(--border-primary); border-radius: 11px; + position: relative; transition: all var(--transition-fast); +} +.toggle-track::after { + content: ''; position: absolute; width: 16px; height: 16px; + background: var(--text-tertiary); border-radius: 50%; top: 2px; left: 2px; + transition: all var(--transition-fast); +} +.toggle input:checked + .toggle-track { background: var(--accent-muted); border-color: var(--accent); } +.toggle input:checked + .toggle-track::after { background: var(--accent); left: 20px; } + +/* ─── Tables ─────────────────────────────────────────────── */ +.table-container { overflow-x: auto; border: 1px solid var(--border-primary); border-radius: var(--radius-md); } +table { width: 100%; border-collapse: collapse; } +th, td { padding: 12px 16px; text-align: left; } +th { background: var(--bg-glass); font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-tertiary); border-bottom: 1px solid var(--border-primary); } +td { border-bottom: 1px solid var(--border-primary); font-size: 0.85rem; } +tr:last-child td { border-bottom: none; } +tr:hover td { background: var(--bg-glass); } + +/* ─── Tabs ───────────────────────────────────────────────── */ +.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border-primary); margin-bottom: 24px; overflow-x: auto; } +.tab { + padding: 12px 20px; font-size: 0.85rem; font-weight: 500; + color: var(--text-tertiary); cursor: pointer; + border-bottom: 2px solid transparent; transition: all var(--transition-fast); + white-space: nowrap; background: none; border-top: none; border-left: none; + border-right: none; font-family: var(--font-sans); +} +.tab:hover { color: var(--text-primary); } +.tab.active { color: var(--accent-hover); border-bottom-color: var(--accent); } + +/* ─── Log Viewer ─────────────────────────────────────────── */ +.log-viewer { + background: #0d1117; border: 1px solid var(--border-primary); + border-radius: var(--radius-md); font-family: var(--font-mono); + font-size: 0.8rem; line-height: 1.7; overflow-y: auto; + max-height: 500px; padding: 16px; +} +.log-line { white-space: pre-wrap; word-break: break-all; color: var(--text-secondary); } +.log-line.error { color: var(--status-error); } +.log-line.info { color: var(--accent); } + +/* ─── Modal ──────────────────────────────────────────────── */ +.modal-overlay { + position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); z-index: 1000; + display: flex; align-items: center; justify-content: center; + animation: fadeIn var(--transition-fast) ease-out; +} +.modal { + background: var(--bg-secondary); border: 1px solid var(--border-primary); + border-radius: var(--radius-xl); padding: 32px; + max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto; + box-shadow: var(--shadow-lg); animation: slideUp var(--transition-normal) ease-out; +} +.modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; } +.modal-title { font-size: 1.2rem; font-weight: 700; } +.hidden { display: none !important; } +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } +@keyframes slideUp { from { opacity: 0; transform: translateY(20px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } + +/* ─── Empty State ────────────────────────────────────────── */ +.empty-state { text-align: center; padding: 60px 20px; color: var(--text-tertiary); } +.empty-state h3 { font-size: 1.1rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; } +.empty-state p { font-size: 0.9rem; max-width: 400px; margin: 0 auto 20px; } + +/* ─── Login ─────────────────────────────────────────────── */ +.login-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg-primary); } +.login-card { + background: var(--bg-card); backdrop-filter: blur(20px); + border: 1px solid var(--border-primary); border-radius: var(--radius-xl); + padding: 40px; width: 90%; max-width: 420px; box-shadow: var(--shadow-lg); +} +.login-brand { text-align: center; margin-bottom: 32px; } +.login-brand-icon { + width: 56px; height: 56px; + background: linear-gradient(135deg, var(--accent), #8b5cf6); + border-radius: var(--radius-lg); display: flex; align-items: center; + justify-content: center; font-size: 28px; margin: 0 auto 16px; + box-shadow: var(--shadow-glow); color: white; +} +.login-brand h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: 4px; } +.login-brand p { color: var(--text-tertiary); font-size: 0.85rem; } +.login-error { + background: var(--status-error-bg); color: var(--status-error); + padding: 10px 14px; border-radius: var(--radius-sm); + font-size: 0.85rem; margin-bottom: 16px; border: 1px solid rgba(239, 68, 68, 0.2); +} + +/* ─── Service Card ───────────────────────────────────────── */ +.service-card { position: relative; overflow: hidden; cursor: pointer; } +.service-card::before { + content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; + background: linear-gradient(90deg, transparent, var(--accent), transparent); + opacity: 0; transition: opacity var(--transition-normal); +} +.service-card:hover::before { opacity: 1; } +.service-card-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 14px; } +.service-card-name { font-size: 1.05rem; font-weight: 700; display: flex; align-items: center; gap: 10px; } +.service-card-repo { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-tertiary); margin-top: 4px; display: flex; align-items: center; gap: 6px; } +.service-card-meta { display: flex; gap: 16px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-primary); } +.service-card-meta-item { font-size: 0.78rem; color: var(--text-tertiary); display: flex; align-items: center; gap: 4px; } +.service-card-actions { display: flex; gap: 6px; margin-top: 14px; } + +/* ─── Monitoring ─────────────────────────────────────────── */ +.monitoring-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; } +.metric-value { + font-size: 2rem; font-weight: 800; letter-spacing: -0.03em; + background: linear-gradient(135deg, var(--text-primary), var(--accent-hover)); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; +} +.metric-label { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-tertiary); margin-top: 2px; } + +/* ─── Terminal ───────────────────────────────────────────── */ +.terminal-container { background: #0d1117; border: 1px solid var(--border-primary); border-radius: var(--radius-md); overflow: hidden; } +.terminal-header { background: rgba(255, 255, 255, 0.03); border-bottom: 1px solid var(--border-primary); padding: 10px 16px; display: flex; align-items: center; justify-content: space-between; } +.terminal-dots { display: flex; gap: 6px; } +.terminal-dot { width: 10px; height: 10px; border-radius: 50%; } +.terminal-dot.red { background: #ff5f56; } +.terminal-dot.yellow { background: #ffbd2e; } +.terminal-dot.green { background: #27c93f; } +.terminal-body { padding: 4px; min-height: 300px; } + +/* ─── Chart container ─────────────────────────────────────── */ +.chart-container { width: 100%; height: 300px; position: relative; } +.chart-container canvas { width: 100% !important; height: 100% !important; } + +/* ─── Settings grid ───────────────────────────────────────── */ +.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } + +/* ─── Utility ────────────────────────────────────────────── */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.flex-wrap { flex-wrap: wrap; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-2 { gap: 8px; } +.gap-3 { gap: 12px; } +.gap-4 { gap: 16px; } +.mt-2 { margin-top: 8px; } +.mt-4 { margin-top: 16px; } +.mb-2 { margin-bottom: 8px; } +.mb-4 { margin-bottom: 16px; } +.text-sm { font-size: 0.85rem; } +.text-xs { font-size: 0.75rem; } +.text-muted { color: var(--text-tertiary); } +.text-mono { font-family: var(--font-mono); } +.font-bold { font-weight: 700; } +.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.w-full { width: 100%; } + +@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } +.animate-spin { animation: spin 1s linear infinite; } +@keyframes slideInRight { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } +.animate-slide-in { animation: slideInRight var(--transition-normal) ease-out; } + +/* ─── Logs page layout ─────────────────────────────────────── */ +.logs-layout { display: grid; grid-template-columns: 350px 1fr; gap: 16px; min-height: 500px; } + +@media (max-width: 768px) { + .sidebar { width: 100%; height: auto; position: relative; } + .main-content { margin-left: 0; padding: 16px; max-width: 100%; } + .card-grid { grid-template-columns: 1fr; } + .form-row { grid-template-columns: 1fr; } + .page-header { flex-direction: column; align-items: flex-start; } + .settings-grid { grid-template-columns: 1fr; } + .logs-layout { grid-template-columns: 1fr; } +} diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..b4a29e8 --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,21 @@ + + +
+ + + +${s.description}
` : ''} +Gestisci i tuoi servizi
Crea il tuo primo servizio per iniziare
Errore: ${err.message}
Storico di tutti i deploy con i relativi log
Caricamento...
+Clicca su un deploy nella lista per visualizzarne i log
Errore: ${err.message}
`; + } + } + + function renderList() { + const el = document.getElementById('deploy-list'); + el.innerHTML = `Nessun deploy trovato
' : + `${esc(d.commit_message)}
` : ''} +Clicca su un deploy nella lista per visualizzarne i log
Risorse in tempo reale di tutti i servizi attivi
I dati di monitoring saranno visibili quando almeno un servizio sarà in esecuzione
Chart.js non caricato. Aggiungi chart.min.js nella cartella lib/
Nessun dato disponibile
Middleware esistenti (file provider):
Aggiungi middleware inline:
Compressione gzip attivata. Nessuna configurazione aggiuntiva.
`; + default: return `${JSON.stringify(mw, null, 2)}`;
+ }
+ }
+
+ // ── Env Tab ──
+ function renderEnvTab(el) {
+ let vars = [];
+ let envSaving = false;
+ const revealedKeys = new Set();
+
+ el.innerHTML = `Caricamento variabili...
Errore: ${err.message}
Nessuna variabile configurata
Nessun deploy ancora. Esegui il primo deploy!
${esc(d.commit_message)}
` : ''} + ${expandedId === d.id ? `` : ''} +Il container deve essere in esecuzione per usare il terminal
Caricamento reti...
Errore: ${err.message}
Seleziona le reti Docker a cui connettere il servizio al deploy.
+| Assegnata | Nome | Driver | Containers | Interna | |
|---|---|---|---|---|---|
| + | ${n.name} | +${n.driver} | +${n.containers} | +${n.internal ? icons.check(14) : icons.x(14)} | +${!['meb-public','meb-private','bridge','host','none'].includes(n.name) ? `` : ''} | +
Configura questo webhook nel tuo repository Gitea per abilitare il deploy automatico ad ogni push.
+WEBHOOK_SECRET configurato nel .env${service.gitea_branch}) corrisponda al branch monitorato💡 Nota: Se Gitea e AutoDeployer sono sulla stessa rete Docker (meb-public), il webhook bypassa Cloudflare automaticamente. L'URL interno sarà: http://autodeployer:3000/api/webhooks/${service.webhook_id}
Configurazione globale e test connessioni
Testa la connessione al server Gitea configurato
+ + +Invia un messaggio di test al bot Telegram configurato
+ + +Caricamento...
Aggiorna AutoDeployer all'ultima versione. Il servizio si riavvierà automaticamente.
+ + +Rimuovi container temporanei rimasti da deploy falliti.
+ + +Rimuovi immagini Docker obsolete, mantenendo le 2 più recenti per servizio.
+ + +