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:
2026-03-24 20:53:50 +03:00
parent 2c001df322
commit 4d941f566f
17 changed files with 962 additions and 21 deletions
@@ -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;
}