[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:
Krystie
2026-08-12 17:34:10 -07:00
parent 0bf4406253
commit 503de258b8
105 changed files with 14057 additions and 2632 deletions
+136
View File
@@ -0,0 +1,136 @@
/**
* DC-100: Service Discovery — auto-detect running Docker containers
* and suggest them as services to add to the dashboard.
*
* Scans all running containers, extracts port mappings, image info,
* and labels to suggest service configurations.
*/
const express = require('express');
const { ok, errorResponse } = require('../src/utils/responses');
const { ErrorCodes } = require('../src/utilities/error-codes');
// Known image patterns → suggested service type and default config
const IMAGE_PATTERNS = {
'plexinc/pms': { type: 'plex', name: 'Plex', port: 32400, https: false },
'linuxserver/jellyfin': { type: 'jellyfin', name: 'Jellyfin', port: 8096, https: false },
'linuxserver/emby': { type: 'emby', name: 'Emby', port: 8096, https: false },
'lscr.io/linuxserver/sonarr': { type: 'sonarr', name: 'Sonarr', port: 8989, https: false },
'lscr.io/linuxserver/radarr': { type: 'radarr', name: 'Radarr', port: 7878, https: false },
'lscr.io/linuxserver/prowlarr': { type: 'prowlarr', name: 'Prowlarr', port: 9696, https: false },
'lscr.io/linuxserver/lidarr': { type: 'lidarr', name: 'Lidarr', port: 8686, https: false },
'lscr.io/linuxserver/readarr': { type: 'readarr', name: 'Readarr', port: 8787, https: false },
'lscr.io/linuxserver/qbittorrent': { type: 'qbittorrent', name: 'qBittorrent', port: 8080, https: false },
'lscr.io/linuxserver/transmission': { type: 'transmission', name: 'Transmission', port: 9091, https: false },
'haugene/transmission-openvpn': { type: 'transmission', name: 'Transmission+VPN', port: 9091, https: false },
'gitea/gitea': { type: 'gitea', name: 'Gitea', port: 3000, https: false },
'nextcloud': { type: 'nextcloud', name: 'Nextcloud', port: 80, https: false },
'vaultwarden': { type: 'vaultwarden', name: 'Vaultwarden', port: 80, https: false },
'nginx': { type: 'web', name: 'Nginx', port: 80, https: false },
'caddy': { type: 'web', name: 'Caddy', port: 80, https: false },
'redis': { type: 'redis', name: 'Redis', port: 6379, https: false },
'postgres': { type: 'postgres', name: 'PostgreSQL', port: 5432, https: false },
'mariadb': { type: 'mariadb', name: 'MariaDB', port: 3306, https: false },
'mongo': { type: 'mongodb', name: 'MongoDB', port: 27017, https: false },
};
module.exports = function({ docker, servicesStateManager, asyncHandler }) {
const router = express.Router();
/**
* GET /api/v1/discover — scan running containers for auto-detection
*
* Returns a list of discovered services with suggested configurations.
* Services already in the dashboard are marked as `existing: true`.
*/
router.get('/discover', asyncHandler(async (req, res) => {
if (!docker || !docker.client) {
return errorResponse(res, 503, 'Docker daemon not available', {
code: ErrorCodes.CONTAINER.DOCKER_UNREACHABLE,
});
}
try {
// Get all running containers
const containers = await docker.client.listContainers({ all: false });
// Get existing service IDs to mark duplicates
let existingIds = new Set();
if (servicesStateManager) {
try {
const services = await servicesStateManager.read();
const list = Array.isArray(services) ? services : (services.services || []);
existingIds = new Set(list.map(s => s.id));
} catch { /* ignore — treat as empty */ }
}
const discovered = [];
const seen = new Set();
for (const container of containers) {
const name = (container.Names && container.Names[0] || '').replace(/^\//, '');
if (!name || seen.has(name)) continue;
seen.add(name);
const image = container.Image || '';
const imageBase = image.split(':')[0].toLowerCase();
// Match against known patterns
let matched = null;
for (const [pattern, config] of Object.entries(IMAGE_PATTERNS)) {
if (imageBase.includes(pattern)) {
matched = config;
break;
}
}
// Extract port mappings
const ports = (container.Ports || []).map(p => ({
ip: p.IP || '0.0.0.0',
privatePort: p.PrivatePort,
publicPort: p.PublicPort,
type: p.Type || 'tcp',
})).filter(p => p.publicPort);
// Suggested config
const suggestedPort = matched ? matched.port : (ports[0] && ports[0].publicPort) || null;
const suggestedId = name.replace(/[^a-z0-9-]/gi, '-').toLowerCase();
discovered.push({
containerId: container.Id.substring(0, 12),
name,
image,
status: container.State,
suggested: {
id: suggestedId,
name: matched ? matched.name : name.charAt(0).toUpperCase() + name.slice(1),
type: matched ? matched.type : 'generic',
port: suggestedPort,
protocol: matched ? (matched.https ? 'https' : 'http') : 'http',
},
ports,
labels: container.Labels || {},
existing: existingIds.has(suggestedId),
});
}
// Sort: unmatched first (more interesting to discover), then by name
discovered.sort((a, b) => {
if (a.existing !== b.existing) return a.existing ? 1 : -1;
return a.name.localeCompare(b.name);
});
ok(res, {
total: discovered.length,
matched: discovered.filter(d => d.suggested.type !== 'generic').length,
newServices: discovered.filter(d => !d.existing).length,
discovered,
});
} catch (err) {
return errorResponse(res, 500, `Discovery failed: ${err.message}`, {
code: ErrorCodes.GENERAL.INTERNAL,
});
}
}));
return router;
};