/** * n8n-install Welcome Page * Dynamic rendering of services and credentials from data.json */ (function() { 'use strict'; // ============================================ // CINEMATIC ANIMATIONS MODULE // ============================================ const CinematicAnimations = { CONFETTI_STORAGE_KEY: 'n8n_install_welcomed', isFirstVisit() { return !localStorage.getItem(this.CONFETTI_STORAGE_KEY); }, markVisited() { localStorage.setItem(this.CONFETTI_STORAGE_KEY, Date.now().toString()); }, triggerConfetti() { if (!window.confetti) return; const colors = ['#3ECF8E', '#24B374', '#47DF97', '#ffffff', '#75E7B1']; const defaults = { spread: 60, ticks: 100, gravity: 1, decay: 0.94, startVelocity: 30, colors: colors }; function fire(particleRatio, opts) { confetti({ ...defaults, ...opts, particleCount: Math.floor(200 * particleRatio) }); } // Staggered confetti bursts fire(0.25, { spread: 26, startVelocity: 55 }); fire(0.2, { spread: 60 }); fire(0.35, { spread: 100, decay: 0.91, scalar: 0.8 }); fire(0.1, { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 }); fire(0.1, { spread: 120, startVelocity: 45 }); // Side cannons setTimeout(() => { confetti({ particleCount: 50, angle: 60, spread: 55, origin: { x: 0 }, colors: colors }); confetti({ particleCount: 50, angle: 120, spread: 55, origin: { x: 1 }, colors: colors }); }, 250); }, init() { // Check for first visit and trigger confetti if (this.isFirstVisit()) { setTimeout(() => { this.triggerConfetti(); this.markVisited(); }, 800); } } }; // ============================================ // ICONS - SVG icons as template functions // ============================================ const Icons = { copy: (className = '') => ` `, check: (className = '') => ` `, eyeOpen: (className = '') => ` `, eyeClosed: (className = '') => ` `, externalLink: (className = '') => ` `, server: (className = '') => ` `, bolt: (className = '') => ` `, refresh: (className = '') => ` `, terminal: (className = '') => ` `, book: (className = '') => ` `, warning: (className = '') => ` ` }; // ============================================ // DATA - Service metadata and commands // ============================================ const SERVICE_METADATA = { 'n8n': { name: 'n8n', description: 'Workflow Automation', icon: 'n8n', color: 'bg-orange-500', category: 'automation' }, 'flowise': { name: 'Flowise', description: 'AI Agent Builder', icon: 'FL', color: 'bg-blue-500', category: 'ai' }, 'open-webui': { name: 'Open WebUI', description: 'ChatGPT-like Interface', icon: 'AI', color: 'bg-emerald-500', category: 'ai' }, 'grafana': { name: 'Grafana', description: 'Monitoring Dashboard', icon: 'GF', color: 'bg-orange-600', category: 'monitoring' }, 'prometheus': { name: 'Prometheus', description: 'Metrics Collection', icon: 'PM', color: 'bg-red-500', category: 'monitoring' }, 'portainer': { name: 'Portainer', description: 'Docker Management UI', icon: 'PT', color: 'bg-cyan-500', category: 'infra' }, 'postgresus': { name: 'Postgresus', description: 'PostgreSQL Backups & Monitoring', icon: 'PG', color: 'bg-blue-600', category: 'database' }, 'langfuse': { name: 'Langfuse', description: 'AI Observability', icon: 'LF', color: 'bg-violet-500', category: 'ai' }, 'supabase': { name: 'Supabase', description: 'Backend as a Service', icon: 'SB', color: 'bg-emerald-500', category: 'database' }, 'dify': { name: 'Dify', description: 'AI Application Platform', icon: 'DF', color: 'bg-indigo-500', category: 'ai' }, 'qdrant': { name: 'Qdrant', description: 'Vector Database', icon: 'QD', color: 'bg-purple-500', category: 'database' }, 'weaviate': { name: 'Weaviate', description: 'Vector Database', icon: 'WV', color: 'bg-green-600', category: 'database' }, 'neo4j': { name: 'Neo4j', description: 'Graph Database', icon: 'N4', color: 'bg-blue-700', category: 'database' }, 'searxng': { name: 'SearXNG', description: 'Private Metasearch Engine', icon: 'SX', color: 'bg-teal-500', category: 'tools' }, 'ragapp': { name: 'RAGApp', description: 'RAG UI & API', icon: 'RA', color: 'bg-amber-500', category: 'ai' }, 'ragflow': { name: 'RAGFlow', description: 'Document Understanding RAG', icon: 'RF', color: 'bg-rose-500', category: 'ai' }, 'lightrag': { name: 'LightRAG', description: 'Graph-based RAG', icon: 'LR', color: 'bg-lime-600', category: 'ai' }, 'letta': { name: 'Letta', description: 'Agent Server & SDK', icon: 'LT', color: 'bg-fuchsia-500', category: 'ai' }, 'comfyui': { name: 'ComfyUI', description: 'Stable Diffusion UI', icon: 'CU', color: 'bg-pink-500', category: 'ai' }, 'libretranslate': { name: 'LibreTranslate', description: 'Translation API', icon: 'TR', color: 'bg-sky-500', category: 'tools' }, 'docling': { name: 'Docling', description: 'Document Converter', icon: 'DL', color: 'bg-stone-500', category: 'tools' }, 'paddleocr': { name: 'PaddleOCR', description: 'OCR API Server', icon: 'OC', color: 'bg-yellow-600', category: 'tools' }, 'postiz': { name: 'Postiz', description: 'Social Publishing Platform', icon: 'PZ', color: 'bg-violet-600', category: 'tools' }, 'waha': { name: 'WAHA', description: 'WhatsApp HTTP API', icon: 'WA', color: 'bg-green-700', category: 'tools' }, 'crawl4ai': { name: 'Crawl4AI', description: 'Web Crawler for AI', icon: 'C4', color: 'bg-gray-600', category: 'tools' }, 'gotenberg': { name: 'Gotenberg', description: 'PDF Generator API', icon: 'GT', color: 'bg-red-600', category: 'tools' }, 'ollama': { name: 'Ollama', description: 'Local LLM Runner', icon: 'OL', color: 'bg-gray-700', category: 'ai' }, 'redis': { name: 'Redis (Valkey)', description: 'In-Memory Data Store', icon: 'RD', color: 'bg-red-700', category: 'infra' }, 'postgres': { name: 'PostgreSQL', description: 'Relational Database', icon: 'PG', color: 'bg-blue-800', category: 'infra' }, 'python-runner': { name: 'Python Runner', description: 'Custom Python Scripts', icon: 'PY', color: 'bg-yellow-500', category: 'tools' }, 'cloudflare-tunnel': { name: 'Cloudflare Tunnel', description: 'Zero-Trust Network Access', icon: 'CF', color: 'bg-orange-500', category: 'infra' } }; const COMMANDS = [ { cmd: 'make status', desc: 'Show container status' }, { cmd: 'make logs', desc: 'View logs (all services)' }, { cmd: 'make logs s=', desc: 'View logs for specific service' }, { cmd: 'make monitor', desc: 'Live CPU/memory monitoring' }, { cmd: 'make restarts', desc: 'Show restart count per container' }, { cmd: 'make doctor', desc: 'Run system diagnostics' }, { cmd: 'make update', desc: 'Update system and services' }, { cmd: 'make update-preview', desc: 'Preview available updates' }, { cmd: 'make clean', desc: 'Remove unused Docker resources' } ]; // ============================================ // UTILS - Helper functions // ============================================ /** * Escape HTML to prevent XSS */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Copy text to clipboard and show visual feedback */ async function copyToClipboard(text, button) { try { await navigator.clipboard.writeText(text); showCopySuccess(button); return true; } catch (err) { console.error('Failed to copy:', err); return false; } } /** * Show copy success visual feedback */ function showCopySuccess(button) { const copyIcon = button.querySelector('.icon-copy'); const checkIcon = button.querySelector('.icon-check'); if (copyIcon && checkIcon) { copyIcon.classList.add('hidden'); checkIcon.classList.remove('hidden'); setTimeout(() => { copyIcon.classList.remove('hidden'); checkIcon.classList.add('hidden'); }, 2000); } } /** * Toggle password visibility */ function togglePasswordVisibility(button, passwordSpan, password) { const isHidden = passwordSpan.dataset.hidden === 'true'; const eyeOpen = button.querySelector('.icon-eye-open'); const eyeClosed = button.querySelector('.icon-eye-closed'); if (isHidden) { passwordSpan.textContent = password; passwordSpan.dataset.hidden = 'false'; button.setAttribute('aria-pressed', 'true'); eyeOpen?.classList.add('hidden'); eyeClosed?.classList.remove('hidden'); } else { passwordSpan.textContent = '*'.repeat(Math.min(password.length, 12)); passwordSpan.dataset.hidden = 'true'; button.setAttribute('aria-pressed', 'false'); eyeOpen?.classList.remove('hidden'); eyeClosed?.classList.add('hidden'); } } // ============================================ // COMPONENTS - UI component functions // ============================================ /** * Create a copy button for any text */ function createCopyButton(textToCopy) { const button = document.createElement('button'); button.type = 'button'; button.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50'; button.setAttribute('aria-label', 'Copy to clipboard'); button.innerHTML = ` ${Icons.copy('w-4 h-4 text-gray-500 hover:text-brand transition-colors icon-copy')} ${Icons.check('w-4 h-4 text-brand icon-check hidden')} `; button.addEventListener('click', () => copyToClipboard(textToCopy, button)); return button; } /** * Create password field with toggle and copy buttons */ function createPasswordField(password) { const container = document.createElement('div'); container.className = 'flex items-center gap-1'; // Password display const passwordSpan = document.createElement('span'); passwordSpan.className = 'font-mono text-sm select-all text-gray-300'; passwordSpan.textContent = '*'.repeat(Math.min(password.length, 12)); passwordSpan.dataset.password = password; passwordSpan.dataset.hidden = 'true'; // Toggle visibility button const toggleBtn = document.createElement('button'); toggleBtn.type = 'button'; toggleBtn.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50'; toggleBtn.setAttribute('aria-label', 'Toggle password visibility'); toggleBtn.setAttribute('aria-pressed', 'false'); toggleBtn.innerHTML = ` ${Icons.eyeOpen('w-4 h-4 text-gray-500 hover:text-brand transition-colors icon-eye-open')} ${Icons.eyeClosed('w-4 h-4 text-gray-500 hover:text-brand transition-colors icon-eye-closed hidden')} `; toggleBtn.addEventListener('click', () => { togglePasswordVisibility(toggleBtn, passwordSpan, password); }); // Copy button (reusing the component) const copyBtn = createCopyButton(password); container.appendChild(passwordSpan); container.appendChild(toggleBtn); container.appendChild(copyBtn); return container; } /** * Create a credential row with label and value */ function createCredentialRow(label, value, isSecret) { const row = document.createElement('div'); row.className = 'flex justify-between items-center'; const labelSpan = document.createElement('span'); labelSpan.className = 'text-gray-500 text-sm'; labelSpan.textContent = `${label}:`; row.appendChild(labelSpan); if (isSecret) { row.appendChild(createPasswordField(value)); } else { const valueContainer = document.createElement('div'); valueContainer.className = 'flex items-center gap-1'; const valueSpan = document.createElement('span'); valueSpan.className = 'font-mono text-sm select-all text-gray-300'; valueSpan.textContent = value; valueContainer.appendChild(valueSpan); valueContainer.appendChild(createCopyButton(value)); row.appendChild(valueContainer); } return row; } /** * Create credentials section for a service card */ function createCredentialsSection(creds) { const section = document.createElement('div'); section.className = 'mt-4 pt-4 border-t border-surface-400 space-y-2'; if (creds.note) { const noteP = document.createElement('p'); noteP.className = 'text-sm text-gray-500 italic'; noteP.textContent = creds.note; section.appendChild(noteP); return section; } if (creds.username) { section.appendChild(createCredentialRow('Username', creds.username, false)); } if (creds.password) { section.appendChild(createCredentialRow('Password', creds.password, true)); } if (creds.api_key) { section.appendChild(createCredentialRow('API Key', creds.api_key, true)); } return section; } /** * Create extra info section (internal URLs, etc.) */ function createExtraSection(extra) { const items = []; if (extra.internal_api) { items.push(`Internal: ${escapeHtml(extra.internal_api)}`); } if (extra.internal_url) { items.push(`Internal: ${escapeHtml(extra.internal_url)}`); } if (extra.workers) { items.push(`Workers: ${escapeHtml(extra.workers)}`); } if (extra.recommendation) { items.push(`${escapeHtml(extra.recommendation)}`); } if (items.length === 0) return null; const container = document.createElement('div'); container.className = 'mt-2 flex flex-wrap gap-2'; container.innerHTML = items.join(''); return container; } /** * Create service card header */ function createCardHeader(metadata, serviceData) { const header = document.createElement('div'); header.className = 'flex items-start gap-4'; // Icon const iconDiv = document.createElement('div'); iconDiv.className = `${metadata.color} w-11 h-11 rounded-lg flex items-center justify-center text-white font-bold text-sm flex-shrink-0 shadow-lg`; iconDiv.textContent = metadata.icon; // Content const content = document.createElement('div'); content.className = 'flex-1 min-w-0'; const title = document.createElement('h3'); title.className = 'font-semibold text-white'; title.textContent = metadata.name; const desc = document.createElement('p'); desc.className = 'text-sm text-gray-500 mb-2'; desc.textContent = metadata.description; content.appendChild(title); content.appendChild(desc); // Link or internal service indicator if (serviceData.hostname) { const link = document.createElement('a'); link.href = `https://${serviceData.hostname}`; link.target = '_blank'; link.rel = 'noopener'; link.className = 'text-brand hover:text-brand-400 text-sm font-medium inline-flex items-center gap-1 group transition-colors'; link.innerHTML = ` ${escapeHtml(serviceData.hostname)} ${Icons.externalLink('w-3 h-3 group-hover:translate-x-0.5 transition-transform')} `; content.appendChild(link); } else { const internalSpan = document.createElement('span'); internalSpan.className = 'text-sm text-gray-600 italic'; internalSpan.textContent = 'Internal service'; content.appendChild(internalSpan); } // Extra info if (serviceData.extra) { const extraSection = createExtraSection(serviceData.extra); if (extraSection) content.appendChild(extraSection); } header.appendChild(iconDiv); header.appendChild(content); return header; } /** * Render a single service card */ function renderServiceCard(key, serviceData) { const metadata = SERVICE_METADATA[key] || { name: key, description: '', icon: key.substring(0, 2).toUpperCase(), color: 'bg-gray-600' }; const card = document.createElement('article'); card.className = 'bg-surface-100 rounded-xl border border-surface-400 p-5 hover:border-brand/30 hover:bg-surface-200 transition-all'; // Build card using DOM API (no innerHTML + setTimeout hack) const header = createCardHeader(metadata, serviceData); card.appendChild(header); // Credentials section if (serviceData.credentials && Object.keys(serviceData.credentials).length > 0) { const credsSection = createCredentialsSection(serviceData.credentials); card.appendChild(credsSection); } return card; } // ============================================ // APP - Initialization and rendering // ============================================ // DOM Elements const servicesContainer = document.getElementById('services-container'); const quickstartContainer = document.getElementById('quickstart-container'); const commandsContainer = document.getElementById('commands-container'); const domainInfo = document.getElementById('domain-info'); /** * Inject section icons from JS (replaces inline SVG in HTML) */ function injectSectionIcons() { document.querySelectorAll('[data-section-icon]').forEach(container => { const iconName = container.dataset.sectionIcon; if (Icons[iconName]) { container.innerHTML = Icons[iconName]('w-5 h-5 text-brand'); } }); } /** * Render all services */ function renderServices(services) { if (!servicesContainer) return; servicesContainer.innerHTML = ''; if (!services || Object.keys(services).length === 0) { servicesContainer.innerHTML = `

No services configured. Run the installer to set up services.

`; return; } // Sort all services alphabetically by display name const sortedKeys = Object.keys(services).sort((a, b) => { const aName = (SERVICE_METADATA[a]?.name || a).toLowerCase(); const bName = (SERVICE_METADATA[b]?.name || b).toLowerCase(); return aName.localeCompare(bName); }); // Use DocumentFragment for better performance const fragment = document.createDocumentFragment(); sortedKeys.forEach(key => { fragment.appendChild(renderServiceCard(key, services[key])); }); servicesContainer.appendChild(fragment); } /** * Render quick start steps */ function renderQuickStart(steps) { if (!quickstartContainer) return; quickstartContainer.innerHTML = ''; if (!steps || steps.length === 0) { // Default steps if none provided steps = [ { step: 1, title: 'Log into n8n', description: 'Use the email you provided during installation' }, { step: 2, title: 'Create your first workflow', description: 'Start with a Manual Trigger + HTTP Request nodes' }, { step: 3, title: 'Explore community workflows', description: 'Check imported workflows for 300+ examples' }, { step: 4, title: 'Monitor your system', description: 'Use Grafana to track performance' } ]; } const fragment = document.createDocumentFragment(); steps.forEach(item => { const stepEl = document.createElement('div'); stepEl.className = 'flex items-start gap-4 p-4 bg-surface-100 rounded-xl border border-surface-400 hover:border-brand/30 hover:bg-surface-200 transition-all'; stepEl.innerHTML = `
${item.step}

${escapeHtml(item.title)}

${escapeHtml(item.description)}

`; fragment.appendChild(stepEl); }); quickstartContainer.appendChild(fragment); } /** * Render make commands */ function renderCommands() { if (!commandsContainer) return; commandsContainer.innerHTML = ''; const grid = document.createElement('div'); grid.className = 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'; COMMANDS.forEach(item => { const cmdEl = document.createElement('div'); cmdEl.className = 'flex items-start gap-3 p-3 rounded-lg bg-surface-200/50 border border-surface-400 hover:border-brand/30 hover:bg-surface-300 transition-all'; // Command content const content = document.createElement('div'); content.className = 'flex flex-col gap-1 flex-1 min-w-0'; content.innerHTML = ` ${escapeHtml(item.cmd)} ${escapeHtml(item.desc)} `; // Copy button const copyBtn = createCopyButton(item.cmd); copyBtn.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50 flex-shrink-0'; cmdEl.appendChild(content); cmdEl.appendChild(copyBtn); grid.appendChild(cmdEl); }); commandsContainer.appendChild(grid); } /** * Render error state in services container */ function renderServicesError() { if (!servicesContainer) return; servicesContainer.innerHTML = ` `; } /** * Load data and render page */ async function init() { // Inject section icons injectSectionIcons(); // Always render commands (static content) renderCommands(); try { const response = await fetch('data.json'); if (!response.ok) { throw new Error(`Failed to load data (${response.status})`); } const data = await response.json(); // Update domain info if (domainInfo) { if (data.domain) { domainInfo.textContent = `Domain: ${data.domain}`; } if (data.generated_at) { const date = new Date(data.generated_at); domainInfo.textContent += ` | Generated: ${date.toLocaleString()}`; } } // Render services renderServices(data.services); // Render quick start renderQuickStart(data.quick_start); } catch (error) { console.error('Error loading data:', error); // Show error in UI renderServicesError(); // Still render default quick start renderQuickStart(null); } // Initialize confetti animation on first visit CinematicAnimations.init(); } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();