server/src/routes/health.ts
Rama main · réplica de lectura del repositorio original.
import { Router, Request, Response } from 'express';
import { getDatabase } from '../db';
import config, { isServiceConfigured } from '../config';
import logger from '../utils/logger';
import { getAppVersion } from '../utils/version';
import { getScheduler } from '../scheduler';
import * as scanHistoryRepo from '../db/repositories/scanHistoryRepo';
import { getPlexService, getRadarrService, getSonarrService, getTautulliService, getTracearrService, getOverseerrService } from '../services/init';
import { getConfiguredServerType, getMediaServerLabel, type MediaServerType } from '../services/mediaServer';
import {
getLastSyncCompletedAt,
getLastSyncFinishedAt,
getLastSyncSuccess,
} from '../services/syncCoordinator';
// APP_VERSION at Docker build time, package.json otherwise. Shared with the
// telemetry heartbeat so both report the same string.
const appVersion = getAppVersion();
const router = Router();
interface HealthStatus {
status: 'healthy' | 'unhealthy' | 'degraded';
timestamp: string;
version: string;
uptime: number;
database: {
status: 'connected' | 'disconnected';
path: string;
};
services: {
plex: { configured: boolean };
tautulli: { configured: boolean };
tracearr: { configured: boolean };
sonarr: { configured: boolean };
radarr: { configured: boolean };
overseerr: { configured: boolean };
discord: { configured: boolean };
};
}
router.get('/', (_req: Request, res: Response) => {
let dbStatus: 'connected' | 'disconnected' = 'disconnected';
try {
const db = getDatabase();
// Test database connection
db.prepare('SELECT 1').get();
dbStatus = 'connected';
} catch (error) {
logger.error('Database health check failed:', error);
dbStatus = 'disconnected';
}
const health: HealthStatus = {
status: dbStatus === 'connected' ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
version: appVersion,
uptime: process.uptime(),
database: {
status: dbStatus,
path: config.dbPath,
},
services: {
plex: { configured: isServiceConfigured('plex') },
tautulli: { configured: isServiceConfigured('tautulli') },
tracearr: { configured: isServiceConfigured('tracearr') },
sonarr: { configured: isServiceConfigured('sonarr') },
radarr: { configured: isServiceConfigured('radarr') },
overseerr: { configured: isServiceConfigured('overseerr') },
discord: { configured: isServiceConfigured('discord') },
},
};
// Determine overall status
const anyServiceConfigured = Object.values(health.services).some((s) => s.configured);
if (dbStatus === 'connected' && anyServiceConfigured) {
health.status = 'healthy';
} else if (dbStatus === 'connected') {
health.status = 'degraded';
} else {
health.status = 'unhealthy';
}
const statusCode = health.status === 'unhealthy' ? 503 : 200;
res.status(statusCode).json(health);
});
// Simple ping endpoint for basic connectivity checks
router.get('/ping', (_req: Request, res: Response) => {
res.json({ pong: true, timestamp: Date.now() });
});
// Version endpoint
router.get('/version', (_req: Request, res: Response) => {
res.json({ version: appVersion });
});
// Detailed readiness check
router.get('/ready', (_req: Request, res: Response) => {
try {
const db = getDatabase();
db.prepare('SELECT 1').get();
res.json({ ready: true });
} catch (error) {
logger.error('Readiness check failed:', error);
res.status(503).json({ ready: false, error: 'Database not ready' });
}
});
// Liveness check
router.get('/live', (_req: Request, res: Response) => {
res.json({ live: true, timestamp: Date.now() });
});
// ============================================================================
// Aggregated Health Status for Dashboard
// ============================================================================
interface ServiceHealthStatus {
service: string;
configured: boolean;
connected: boolean;
responseTimeMs?: number;
error?: string;
lastChecked: string;
}
interface SchedulerStatus {
isRunning: boolean;
lastScan: string | null;
nextRun: string | null;
scanSchedule: string;
lastSync: string | null; // last successful Plex sync
lastSyncAt: string | null; // last sync attempt finish (success OR failure)
lastSyncSuccess: boolean | null; // whether that finish was a success
nextSync: string | null;
syncSchedule: string;
}
interface SystemHealthResponse {
services: ServiceHealthStatus[];
scheduler: SchedulerStatus;
overall: 'healthy' | 'degraded' | 'unhealthy';
/**
* Which backend the 'plex' service entry actually refers to. The entry keeps
* its historical key so existing clients keep resolving it; this field lets
* the UI label it correctly for Jellyfin/Emby installs.
*/
mediaServerType: MediaServerType;
mediaServerLabel: string;
}
async function checkService(
name: string,
service: { testConnection: () => Promise<boolean> } | null
): Promise<ServiceHealthStatus> {
const lastChecked = new Date().toISOString();
if (!service) {
return { service: name, configured: false, connected: false, lastChecked };
}
const startTime = Date.now();
try {
// Race the connection test against a 5-second timeout
const connected = await Promise.race([
service.testConnection(),
new Promise<boolean>((_, reject) =>
setTimeout(() => reject(new Error('Connection timeout')), 5000)
),
]);
return {
service: name,
configured: true,
connected,
responseTimeMs: Date.now() - startTime,
lastChecked,
};
} catch (error) {
return {
service: name,
configured: true,
connected: false,
error: error instanceof Error ? error.message : 'Unknown error',
responseTimeMs: Date.now() - startTime,
lastChecked,
};
}
}
function determineOverallHealth(services: ServiceHealthStatus[]): 'healthy' | 'degraded' | 'unhealthy' {
const configuredServices = services.filter((s) => s.configured);
if (configuredServices.length === 0) return 'unhealthy';
const connectedCount = configuredServices.filter((s) => s.connected).length;
if (connectedCount === configuredServices.length) return 'healthy';
if (connectedCount > 0) return 'degraded';
return 'unhealthy';
}
// Aggregated health status for dashboard
router.get('/status', async (_req: Request, res: Response) => {
try {
const serverType = getConfiguredServerType();
// Run all service checks in parallel
const serviceChecks = await Promise.allSettled([
checkService('plex', getPlexService()),
checkService('radarr', getRadarrService()),
checkService('sonarr', getSonarrService()),
checkService('tautulli', getTautulliService()),
checkService('tracearr', getTracearrService()),
checkService('overseerr', getOverseerrService()),
]);
const serviceNames = ['plex', 'radarr', 'sonarr', 'tautulli', 'tracearr', 'overseerr'];
const services: ServiceHealthStatus[] = serviceChecks.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
}
return {
service: serviceNames[index] || 'unknown',
configured: false,
connected: false,
error: result.reason instanceof Error ? result.reason.message : 'Check failed',
lastChecked: new Date().toISOString(),
};
});
// Get scheduler info
const scheduler = getScheduler();
const scanJobStatus = scheduler.getJobStatus('scanLibraries');
const syncJobStatus = scheduler.getJobStatus('syncPlexLibrary');
const schedulerConfig = scheduler.getConfig();
// Get last scan from history
const latestScan = scanHistoryRepo.getLatest();
const lastSyncCompletedAt = getLastSyncCompletedAt();
const lastSyncFinishedAt = getLastSyncFinishedAt();
const lastSyncSuccess = getLastSyncSuccess();
const response: SystemHealthResponse = {
services,
scheduler: {
isRunning: scheduler.isSchedulerRunning(),
lastScan: latestScan?.completed_at || latestScan?.started_at || null,
nextRun: scanJobStatus?.nextRun?.toISOString() || null,
scanSchedule: schedulerConfig.schedules.scanLibraries,
lastSync: lastSyncCompletedAt?.toISOString() || null,
lastSyncAt: lastSyncFinishedAt?.toISOString() || null,
lastSyncSuccess,
nextSync: syncJobStatus?.nextRun?.toISOString() || null,
syncSchedule: schedulerConfig.schedules.syncPlexLibrary,
},
overall: determineOverallHealth(services),
mediaServerType: serverType,
mediaServerLabel: getMediaServerLabel(serverType),
};
res.json({ success: true, data: response });
} catch (error) {
logger.error('Health status check failed:', error);
res.status(500).json({
success: false,
error: 'Failed to check system health',
});
}
});
export default router;