refactor: provider descriptor registry — eliminate provider-specific hardcoding

Replace all if/else chains keyed on provider type strings with a
descriptor-driven architecture. Each provider type (immich, gitea,
planka, scheduler, nut, google_photos) has a descriptor in
frontend/src/lib/providers/ that declares config fields, event
tracking fields, collection metadata, validation, and hooks.

Components now use getDescriptor(type) and render dynamically.
Dashboard provider card shows provider name + type when global
filter is active. Grid-items derived from registry.
This commit is contained in:
2026-03-24 12:40:33 +03:00
parent c6bb2b5b51
commit 8cb836e16c
19 changed files with 904 additions and 353 deletions
+15 -4
View File
@@ -12,6 +12,7 @@
import EntitySelect from '$lib/components/EntitySelect.svelte';
import { eventTypeFilterItems, sortFilterItems, providerDefaultIcon } from '$lib/grid-items';
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import { getDescriptor } from '$lib/providers';
import type { DashboardStatus } from '$lib/types';
let status = $state<DashboardStatus | null>(null);
@@ -181,8 +182,18 @@
? providers.filter(p => p.type === globalProviderFilter.providerType).length
: displayProviders);
const statCards = $derived(status ? [
{ icon: 'mdiServer', label: 'dashboard.providers', value: filteredProviderCount, color: '#0d9488' },
const providerCard = $derived.by(() => {
const gp = globalProviderFilter.provider;
if (gp) {
const desc = getDescriptor(gp.type);
return { icon: providerDefaultIcon(gp), label: '', literalLabel: desc?.defaultName ?? gp.type, value: 0, literalValue: gp.name, color: '#0d9488' };
}
return { icon: 'mdiServer', label: 'dashboard.providers', value: filteredProviderCount, color: '#0d9488' };
});
interface StatCard { icon: string; label: string; literalLabel?: string; value: number; literalValue?: string; suffix?: string; color: string }
const statCards = $derived<StatCard[]>(status ? [
providerCard,
{ icon: 'mdiRadar', label: 'dashboard.activeTrackers', value: displayActive, suffix: ` / ${displayTotal}`, color: '#6366f1' },
{ icon: 'mdiTarget', label: 'dashboard.targets', value: displayTargets, color: '#f59e0b' },
...(status.command_trackers !== undefined ? [{ icon: 'mdiConsoleLine', label: 'nav.commandTrackers', value: displayCommandTrackers, color: '#8b5cf6' }] : []),
@@ -238,9 +249,9 @@
<MdiIcon name={card.icon} size={22} />
</div>
<div>
<p class="text-sm" style="color: var(--color-muted-foreground);">{t(card.label)}</p>
<p class="text-sm" style="color: var(--color-muted-foreground);">{card.literalLabel || t(card.label)}</p>
<p class="stat-value font-mono" style="animation-delay: {i * 80 + 200}ms;">
{card.value}{#if card.suffix}<span class="stat-suffix">{card.suffix}</span>{/if}
{#if card.literalValue}{card.literalValue}{:else}{card.value}{/if}{#if card.suffix}<span class="stat-suffix">{card.suffix}</span>{/if}
</p>
</div>
</div>