feat(mvp): phase 4 - app registry & healthcheck
Add app CRUD API endpoints, healthcheck service with node-cron scheduler, icon resolver (Lucide, Simple Icons, CDN, uploads), app management UI with Superforms, health badge component, and Docker health endpoint.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import * as appService from './appService.js';
|
||||
import { AppStatusValue } from '$lib/utils/constants.js';
|
||||
|
||||
export interface HealthcheckResult {
|
||||
readonly appId: string;
|
||||
readonly status: string;
|
||||
readonly responseTime: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a health check on a single app by making an HTTP request to its URL.
|
||||
*/
|
||||
export async function checkAppHealth(app: {
|
||||
readonly id: string;
|
||||
readonly url: string;
|
||||
readonly healthcheckMethod: string;
|
||||
readonly healthcheckExpectedStatus: number;
|
||||
readonly healthcheckTimeout: number;
|
||||
}): Promise<HealthcheckResult> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), app.healthcheckTimeout);
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const response = await fetch(app.url, {
|
||||
method: app.healthcheckMethod,
|
||||
signal: controller.signal,
|
||||
redirect: 'follow',
|
||||
headers: {
|
||||
'User-Agent': 'WebAppLauncher-Healthcheck/1.0'
|
||||
}
|
||||
});
|
||||
|
||||
const responseTime = Date.now() - start;
|
||||
|
||||
const status =
|
||||
response.status === app.healthcheckExpectedStatus
|
||||
? AppStatusValue.ONLINE
|
||||
: AppStatusValue.DEGRADED;
|
||||
|
||||
return { appId: app.id, status, responseTime };
|
||||
} catch (err) {
|
||||
const responseTime = Date.now() - start;
|
||||
|
||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||
return { appId: app.id, status: AppStatusValue.OFFLINE, responseTime };
|
||||
}
|
||||
|
||||
return { appId: app.id, status: AppStatusValue.OFFLINE, responseTime: null };
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all apps that have healthcheck enabled, record their statuses.
|
||||
*/
|
||||
export async function checkAllApps(): Promise<readonly HealthcheckResult[]> {
|
||||
const targets = await appService.getHealthcheckTargets();
|
||||
|
||||
if (targets.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(targets.map((target) => checkAppHealth(target)));
|
||||
|
||||
const outcomes: HealthcheckResult[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled') {
|
||||
const { appId, status, responseTime } = result.value;
|
||||
try {
|
||||
await appService.recordStatus(appId, status, responseTime);
|
||||
} catch {
|
||||
// Log but don't fail the whole batch
|
||||
}
|
||||
outcomes.push(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
return outcomes;
|
||||
}
|
||||
Reference in New Issue
Block a user