Files
dashcaddy/dashcaddy-api/routes/wizard.js
Krystie 503de258b8 [grade=pending] QA sprint: commit 103 at-risk files from multi-agent sprint work
Committed by Hermes autonomous QA sprint 2026-08-13.
These files were modified during the Aug 12 sprint but never committed.
2026-08-12 17:34:10 -07:00

172 lines
6.0 KiB
JavaScript

/**
* 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;
};