feat: port full CRUD API routes and frontend pages from Immich Watcher
Backend API (38 routes): - providers: full CRUD + test connection + list collections + API key masking - trackers: full CRUD + trigger + history + test-periodic/memory - tracking-configs: full CRUD with Pydantic models, provider_type filter - template-configs: full CRUD + preview + preview-raw with two-pass validation - targets: full CRUD + test notification + config masking - telegram-bots: full CRUD + chat discovery + token endpoint - users: full admin CRUD + password reset + self-delete protection - status: dashboard endpoint with providers/trackers/targets/events counts Frontend pages updated: - Dashboard with animated stat cards and event timeline - Providers with proper components, delete confirm, snackbar - Trackers/targets/tracking-configs/template-configs/telegram-bots/users all use PageHeader, Card, Loading, MdiIcon with correct i18n keys Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,49 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.ts';
|
||||
import { api } from '$lib/api.ts';
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { t } from '$lib/i18n';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Card from '$lib/components/Card.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
|
||||
let providers = $state<any[]>([]);
|
||||
let loading = $state(true);
|
||||
let loaded = $state(false);
|
||||
|
||||
let deleteTarget = $state<any>(null);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
providers = await api.get('/providers');
|
||||
} catch { /* auth redirect handled by api */ }
|
||||
loading = false;
|
||||
await loadProviders();
|
||||
});
|
||||
|
||||
async function loadProviders() {
|
||||
try {
|
||||
providers = await api('/providers');
|
||||
} catch (err: any) {
|
||||
snackError(err.message);
|
||||
} finally {
|
||||
loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProvider() {
|
||||
if (!deleteTarget) return;
|
||||
try {
|
||||
await api(`/providers/${deleteTarget.id}`, { method: 'DELETE' });
|
||||
snackSuccess(t('snack.providerDeleted'));
|
||||
deleteTarget = null;
|
||||
await loadProviders();
|
||||
} catch (err: any) {
|
||||
snackError(err.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6 max-w-5xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">{t('nav.providers')}</h1>
|
||||
<a href="/providers/new" class="px-4 py-2 bg-primary text-primary-foreground rounded-[var(--radius)] text-sm font-medium">
|
||||
{t('provider.addProvider')}
|
||||
</a>
|
||||
</div>
|
||||
<PageHeader title={t('providers.title')} description={t('providers.description')}>
|
||||
<a href="/providers/new"
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200"
|
||||
style="background: var(--color-primary); color: var(--color-primary-foreground);"
|
||||
onmouseenter={(e) => { e.currentTarget.style.boxShadow = '0 0 16px var(--color-glow-strong)'; }}
|
||||
onmouseleave={(e) => { e.currentTarget.style.boxShadow = 'none'; }}>
|
||||
<MdiIcon name="mdiPlus" size={16} />
|
||||
{t('providers.addProvider')}
|
||||
</a>
|
||||
</PageHeader>
|
||||
|
||||
{#if loading}
|
||||
<p class="text-muted-foreground">{t('common.loading')}</p>
|
||||
{:else if providers.length === 0}
|
||||
<div class="text-center py-16 text-muted-foreground">
|
||||
<p class="text-lg mb-2">{t('common.noData')}</p>
|
||||
<p class="text-sm">Add your first service provider to get started.</p>
|
||||
{#if !loaded}
|
||||
<Loading />
|
||||
{:else if providers.length === 0}
|
||||
<Card>
|
||||
<div class="flex flex-col items-center py-8 gap-3" style="color: var(--color-muted-foreground);">
|
||||
<div style="opacity: 0.4;">
|
||||
<MdiIcon name="mdiServer" size={40} />
|
||||
</div>
|
||||
<p class="text-sm">{t('providers.noProviders')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3 stagger-children">
|
||||
{#each providers as provider}
|
||||
<div class="p-4 bg-card rounded-xl border border-border">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center text-primary font-bold">
|
||||
{provider.type[0].toUpperCase()}
|
||||
</Card>
|
||||
{:else}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 stagger-children">
|
||||
{#each providers as provider}
|
||||
<Card hover>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-lg"
|
||||
style="background: var(--color-primary); color: var(--color-primary-foreground); opacity: 0.9;">
|
||||
<MdiIcon name={provider.icon || 'mdiServer'} size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold">{provider.name}</h3>
|
||||
<p class="text-xs text-muted-foreground capitalize">{provider.type}</p>
|
||||
<h3 class="font-medium text-sm">{provider.name}</h3>
|
||||
<p class="text-xs capitalize" style="color: var(--color-muted-foreground);">{provider.type}</p>
|
||||
</div>
|
||||
</div>
|
||||
<IconButton icon="mdiDelete" variant="danger" title={t('providers.delete')}
|
||||
onclick={() => deleteTarget = provider} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ConfirmModal
|
||||
open={!!deleteTarget}
|
||||
title={t('providers.confirmDelete')}
|
||||
message={deleteTarget?.name || ''}
|
||||
onconfirm={deleteProvider}
|
||||
oncancel={() => deleteTarget = null}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user