/** * DC-105: Smart defaults wizard — "What do you want to self-host?" * * Guides users through initial setup by asking what they want to host, * then generates optimal configuration based on their hardware and needs. * * POST /api/v1/wizard/recommend — returns recommended services based on answers * POST /api/v1/wizard/apply — applies the wizard configuration */ const express = require('express'); const { ok, errorResponse } = require('../src/utils/responses'); // Recommendation matrix: user intent → suggested services const RECOMMENDATIONS = { 'media-streaming': { label: 'Media Streaming', icon: '🎬', services: [ { template: 'plex', priority: 1, reason: 'Stream movies, TV shows, and music' }, { template: 'sonarr', priority: 2, reason: 'Automatically download TV shows' }, { template: 'radarr', priority: 2, reason: 'Automatically download movies' }, { template: 'qbittorrent', priority: 3, reason: 'Download client for media' }, { template: 'prowlarr', priority: 3, reason: 'Indexer management' }, ], }, 'file-sync': { label: 'File Storage & Sync', icon: '📁', services: [ { template: 'nextcloud', priority: 1, reason: 'Self-hosted Google Drive alternative' }, { template: 'vaultwarden', priority: 2, reason: 'Password manager (Bitwarden compatible)' }, ], }, 'home-network': { label: 'Home Network', icon: '🌐', services: [ { template: 'adguard', priority: 1, reason: 'Network-wide ad blocking' }, { template: 'wireguard', priority: 2, reason: 'VPN for remote access' }, { template: 'pihole', priority: 3, reason: 'Alternative DNS ad blocker' }, ], }, 'smart-home': { label: 'Smart Home', icon: '🏠', services: [ { template: 'homeassistant', priority: 1, reason: 'Central smart home automation' }, { template: 'mosquitto', priority: 2, reason: 'MQTT broker for IoT devices' }, ], }, 'development': { label: 'Development', icon: '💻', services: [ { template: 'gitea', priority: 1, reason: 'Self-hosted Git with CI/CD' }, { template: 'code', priority: 2, reason: 'VS Code in the browser' }, { template: 'portainer', priority: 2, reason: 'Docker container management' }, ], }, 'monitoring': { label: 'Monitoring & Analytics', icon: '📊', services: [ { template: 'grafana', priority: 1, reason: 'Beautiful dashboards and graphs' }, { template: 'prometheus', priority: 2, reason: 'Time-series metrics collection' }, { template: 'uptimekuma', priority: 2, reason: 'Uptime monitoring with alerts' }, ], }, }; module.exports = function({ APP_TEMPLATES, asyncHandler }) { const wrap = asyncHandler || ((fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next)); const router = express.Router(); // GET /api/v1/wizard/categories — list available categories router.get('/wizard/categories', wrap(async (req, res) => { ok(res, { categories: Object.entries(RECOMMENDATIONS).map(([key, val]) => ({ id: key, label: val.label, icon: val.icon, serviceCount: val.services.length, })), }); })); // POST /api/v1/wizard/recommend — get recommendations based on selected categories router.post('/wizard/recommend', wrap(async (req, res) => { const { categories = [], hardwareProfile = 'medium' } = req.body || {}; if (!Array.isArray(categories) || categories.length === 0) { return errorResponse(res, 400, 'categories array is required (at least one)'); } // Collect all recommended services from selected categories const recommended = new Map(); for (const cat of categories) { const rec = RECOMMENDATIONS[cat]; if (!rec) continue; for (const svc of rec.services) { if (!recommended.has(svc.template)) { recommended.set(svc.template, { ...svc, categories: [cat] }); } else { recommended.get(svc.template).categories.push(cat); } } } // Sort by priority (lower = more important) const sorted = [...recommended.values()].sort((a, b) => a.priority - b.priority); // Adjust based on hardware profile const limits = { minimal: { maxServices: 3, maxMemory: '512m' }, medium: { maxServices: 6, maxMemory: '1g' }, powerful: { maxServices: 12, maxMemory: '2g' }, }; const profile = limits[hardwareProfile] || limits.medium; const filtered = sorted.slice(0, profile.maxServices); // Enrich with template details const enriched = filtered.map(svc => { const template = (APP_TEMPLATES || []).find(t => (t.id || t.name?.toLowerCase().replace(/\s+/g, '-')) === svc.template ); return { ...svc, available: !!template, image: template?.image || null, ports: template?.ports || [], estimatedMemory: template?.memory || '256m', }; }); ok(res, { hardwareProfile, categories: categories.filter(c => RECOMMENDATIONS[c]), totalRecommended: enriched.length, services: enriched, resourceLimits: profile, }); })); // POST /api/v1/wizard/apply — deploy the selected services // (Delegates to the existing deploy endpoint for each service) router.post('/wizard/apply', wrap(async (req, res) => { const { services = [], subdomainPrefix = '' } = req.body || {}; if (!Array.isArray(services) || services.length === 0) { return errorResponse(res, 400, 'services array is required (at least one template ID)'); } // Return deployment plan — actual deployment happens via the existing // POST /api/v1/apps/deploy endpoint for each service const plan = services.map((templateId, index) => ({ step: index + 1, templateId, subdomain: `${subdomainPrefix}${templateId}`.toLowerCase(), deployEndpoint: '/api/v1/apps/deploy', status: 'pending', })); ok(res, { totalSteps: plan.length, plan, message: 'Use POST /api/v1/apps/deploy for each step to execute', }); })); return router; };