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
+85
View File
@@ -0,0 +1,85 @@
<script lang="ts">
import AppHealthBadge from './AppHealthBadge.svelte';
interface AppWithStatus {
id: string;
name: string;
url: string;
icon: string | null;
iconType: string;
description: string | null;
category: string | null;
statuses: Array<{ status: string; responseTime: number | null; checkedAt: string | Date }>;
}
interface Props {
app: AppWithStatus;
}
let { app }: Props = $props();
const currentStatus = $derived(app.statuses?.[0]?.status ?? 'unknown');
const iconDisplay = $derived.by(() => {
if (!app.icon) return null;
switch (app.iconType) {
case 'emoji':
return { kind: 'emoji' as const, value: app.icon };
case 'url':
return { kind: 'image' as const, src: app.icon };
case 'simple':
return {
kind: 'image' as const,
src: `https://cdn.simpleicons.org/${app.icon.toLowerCase()}`
};
default:
return { kind: 'text' as const, value: app.icon };
}
});
</script>
<a
href={app.url}
target="_blank"
rel="noopener noreferrer"
class="group flex flex-col rounded-lg border border-border bg-card p-4 transition-colors hover:border-primary/50 hover:bg-accent/50"
title={app.description ?? app.name}
>
<div class="mb-3 flex items-start justify-between">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-muted text-lg text-muted-foreground"
>
{#if iconDisplay?.kind === 'emoji'}
<span class="text-xl">{iconDisplay.value}</span>
{:else if iconDisplay?.kind === 'image'}
<img
src={iconDisplay.src}
alt="{app.name} icon"
class="h-6 w-6 rounded object-contain"
/>
{:else if iconDisplay?.kind === 'text'}
<span class="text-xs font-medium">{iconDisplay.value}</span>
{:else}
<span class="text-xs font-bold">{app.name.charAt(0).toUpperCase()}</span>
{/if}
</div>
<AppHealthBadge status={currentStatus} />
</div>
<h3 class="truncate text-sm font-semibold text-card-foreground group-hover:text-primary">
{app.name}
</h3>
{#if app.description}
<p class="mt-1 line-clamp-2 text-xs text-muted-foreground">{app.description}</p>
{/if}
{#if app.category}
<span
class="mt-2 inline-block self-start rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
>
{app.category}
</span>
{/if}
</a>