[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.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
Reference in New Issue
Block a user