feat: telegram commands, app settings, bot polling, webhook handling, UI improvements

Adds telegram bot command system with 13 commands (search, latest, random, etc.),
webhook/polling handlers, rate limiting, app settings page, and various UI/UX
improvements across all entity pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 23:11:42 +03:00
parent 5015e378fe
commit 03ec9b3c86
64 changed files with 2585 additions and 648 deletions
+5 -6
View File
@@ -8,11 +8,13 @@
import Loading from '$lib/components/Loading.svelte';
import IconPicker from '$lib/components/IconPicker.svelte';
import MdiIcon from '$lib/components/MdiIcon.svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
import type { ServiceProvider } from '$lib/types';
let providers = $state<any[]>([]);
let providers = $state<ServiceProvider[]>([]);
let showForm = $state(false);
let editing = $state<number | null>(null);
let form = $state({ name: 'Immich', type: 'immich', url: '', api_key: '', external_domain: '', icon: '' });
@@ -20,7 +22,7 @@
let loadError = $state('');
let submitting = $state(false);
let loaded = $state(false);
let confirmDelete = $state<any>(null);
let confirmDelete = $state<ServiceProvider | null>(null);
let health = $state<Record<number, boolean | null>>({});
@@ -143,10 +145,7 @@
{#if providers.length === 0 && !showForm}
<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>
<EmptyState icon="mdiServer" message={t('providers.noProviders')} />
</Card>
{:else}
<div class="space-y-3 stagger-children">
@@ -17,7 +17,7 @@
let saving = $state(false);
async function testAndSave() {
if (!url || !apiKey) { error = 'URL and API Key are required'; return; }
if (!url || !apiKey) { error = t('providers.urlApiKeyRequired'); return; }
testing = true; error = '';
let createdId: number | null = null;
try {
@@ -44,7 +44,7 @@
}
async function saveWithoutTest() {
if (!url || !apiKey) { error = 'URL and API Key are required'; return; }
if (!url || !apiKey) { error = t('providers.urlApiKeyRequired'); return; }
saving = true; error = '';
try {
await api('/providers', {
@@ -86,7 +86,7 @@
<div>
<label for="prv-ext" class="block text-sm font-medium mb-1">{t('providers.externalDomain')} <span class="text-xs text-[var(--color-muted-foreground)]">({t('providers.optional')})</span></label>
<input id="prv-ext" type="url" bind:value={externalDomain} placeholder="https://photos.example.com" class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
<p class="text-xs text-[var(--color-muted-foreground)] mt-1">Public-facing URL for notification links. Falls back to server URL.</p>
<p class="text-xs text-[var(--color-muted-foreground)] mt-1">{t('providers.externalDomainHint')}</p>
</div>
{/if}
@@ -97,11 +97,11 @@
<div class="flex gap-3 pt-2">
<button onclick={testAndSave} disabled={testing || saving}
class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50">
{testing ? t('providers.connecting') : 'Test & Save'}
{testing ? t('providers.connecting') : t('providers.testAndSave')}
</button>
<button onclick={saveWithoutTest} disabled={testing || saving}
class="px-4 py-2 bg-[var(--color-muted)] text-[var(--color-foreground)] rounded-md text-sm font-medium hover:opacity-80 disabled:opacity-50">
{saving ? t('common.loading') : 'Save without testing'}
{saving ? t('common.loading') : t('providers.saveWithoutTest')}
</button>
<a href="/providers" class="px-4 py-2 bg-[var(--color-muted)] text-[var(--color-muted-foreground)] rounded-md text-sm font-medium hover:opacity-80">
{t('common.cancel')}