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,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>
|
||||
Reference in New Issue
Block a user