mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 23:06:49 +00:00
ba2e7bded9
* feat: add compose discovery for setup preflight and sidebar empty state Expose read-only compose discovery via GET /api/stacks/discovery and setup diagnostics. Replace the blank sidebar with path-aware discovery and move adopt into a dedicated dialog with a three-tab Create Stack flow. * test: assert post-setup handoff via sessionStorage read-back The Setup preflight test spied on Storage.prototype.setItem to check the post-setup adopt handoff. When the jsdom storage probe fails and the test harness swaps in its in-memory storage stub (which does not extend Storage), that stub's setItem never touches Storage.prototype, so the spy records zero calls and the assertion fails even though the component wrote the value. Read the value back with sessionStorage.getItem instead, matching how every other storage test in the suite asserts. This is robust to both the native jsdom storage and the in-memory fallback. * fix(setup): surface compose discovery as a preflight check row Drop the Setup discovery banner and non-working Review button. Show counts as a pass row in EnvironmentChecks (Setup only) and keep Enter Sencho as the handoff that opens adopt when candidates exist. * test(setup): cover zero-count discovery row omission * fix(stacks): widen adopt scan to any yaml and rename into place Homelab layouts often use nginx.yml or plex.yml. Surface those for adopt (except overrides), rename to compose.yaml on move so stacks register, and reset the confirm UI when a move fails.
68 lines
3.3 KiB
TypeScript
68 lines
3.3 KiB
TypeScript
import { Router, type Request, type Response } from 'express';
|
|
import { requireAdmin, requireUserSession } from '../middleware/tierGates';
|
|
import { collectDiagnostics } from '../services/DiagnosticsService';
|
|
import { collectEnvironmentReport, buildRealProbes } from '../services/EnvironmentCheckService';
|
|
import { probeComposeDiscovery } from '../services/ComposeDiscoveryService';
|
|
import DockerController from '../services/DockerController';
|
|
import { withTimeout } from '../utils/withTimeout';
|
|
|
|
export const diagnosticsRouter = Router();
|
|
|
|
const DOCKER_PING_TIMEOUT_MS = 2000;
|
|
|
|
// Recovery diagnostics for the local control plane. Restricted to a genuine
|
|
// signed-in admin session: requireUserSession rejects API tokens and
|
|
// node_proxy / pilot_tunnel machine credentials so a long-lived machine token
|
|
// cannot read the control plane's configuration inventory. Read-only and
|
|
// secret-free (see DiagnosticsService for the redaction allowlist). The Docker
|
|
// probe is bounded and wrapped so a down or hung daemon yields
|
|
// `docker.reachable: false` instead of failing the whole request, which is the
|
|
// exact condition an operator opens this surface to diagnose.
|
|
diagnosticsRouter.get('/', async (req: Request, res: Response): Promise<void> => {
|
|
if (!requireUserSession(req, res)) return;
|
|
if (!requireAdmin(req, res)) return;
|
|
try {
|
|
const report = await collectDiagnostics({
|
|
checkDocker: async () => {
|
|
await withTimeout(
|
|
DockerController.getInstance().getDocker().ping(),
|
|
DOCKER_PING_TIMEOUT_MS,
|
|
'docker-ping',
|
|
);
|
|
return true;
|
|
},
|
|
});
|
|
res.json(report);
|
|
} catch (err) {
|
|
console.error('[diagnostics] failed to collect report:', (err as Error).message);
|
|
res.status(500).json({ error: 'Failed to collect diagnostics.' });
|
|
}
|
|
});
|
|
|
|
// First-run / preflight environment checks (Docker engine + Compose, the
|
|
// compose directory and its host path mapping, TLS, disk headroom). Same admin
|
|
// session gate as the recovery report. proto / host come from the request so
|
|
// the TLS verdict reflects how this browser reached the dashboard; behind a
|
|
// reverse proxy that terminates TLS, x-forwarded-proto carries the real scheme.
|
|
diagnosticsRouter.get('/environment', async (req: Request, res: Response): Promise<void> => {
|
|
if (!requireUserSession(req, res)) return;
|
|
if (!requireAdmin(req, res)) return;
|
|
try {
|
|
const proto = (req.get('x-forwarded-proto')?.split(',')[0].trim()) || req.protocol;
|
|
const host = req.get('host') || '';
|
|
const report = await collectEnvironmentReport(buildRealProbes({ proto, host }));
|
|
try {
|
|
const probe = await probeComposeDiscovery(req.nodeId);
|
|
if (probe.readable) {
|
|
(report as typeof report & { discovery?: typeof probe.discovery }).discovery = probe.discovery;
|
|
}
|
|
} catch (discoveryErr) {
|
|
console.error('[diagnostics] compose discovery probe failed:', (discoveryErr as Error).message);
|
|
}
|
|
res.json(report);
|
|
} catch (err) {
|
|
console.error('[diagnostics] failed to collect environment report:', (err as Error).message);
|
|
res.status(500).json({ error: 'Failed to collect environment checks.' });
|
|
}
|
|
});
|