feat: add Scheduler provider + multi-provider UX fixes
Scheduler provider: - Virtual provider (no external service) that emits SCHEDULED_MESSAGE events on user-defined intervals or cron expressions - Custom variables stored in tracker filters, flattened into template context - fire_count persists across triggers via tracker state - APScheduler CronTrigger support for cron-mode schedules - Default templates (EN+RU), seeded on startup Multi-provider UX fixes: - Tracking config hides Immich-specific sections (periodic, scheduled, memory, asset display) for non-Immich providers - Command config driven by provider capabilities — hides commands/settings for providers without bot commands - Template config hides empty "Scheduled Messages" group - Test menu on tracker targets is provider-aware (Immich shows all 4 test types, others show only basic) - Removed redundant Test button from tracker card - System-owned tracking configs (user_id=0) seeded for Gitea + Scheduler - Fixed ownership checks to allow system configs in tracker-target links - Capabilities cache shared across template-configs and command-configs - Command tracker bot selector uses EntitySelect instead of raw select - Sample context includes Gitea + Scheduler variables for template preview
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { t } from '$lib/i18n';
|
||||
import { commandConfigsCache, commandTemplateConfigsCache } from '$lib/stores/caches.svelte';
|
||||
import { commandConfigsCache, commandTemplateConfigsCache, capabilitiesCache } from '$lib/stores/caches.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Card from '$lib/components/Card.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
@@ -37,22 +37,23 @@
|
||||
let submitting = $state(false);
|
||||
let confirmDelete = $state<any>(null);
|
||||
|
||||
const allCommands = [
|
||||
{ key: 'help', icon: 'mdiHelpCircle' },
|
||||
{ key: 'status', icon: 'mdiChartBox' },
|
||||
{ key: 'albums', icon: 'mdiImageMultiple' },
|
||||
{ key: 'events', icon: 'mdiPulse' },
|
||||
{ key: 'summary', icon: 'mdiFileDocumentEdit' },
|
||||
{ key: 'latest', icon: 'mdiImagePlus' },
|
||||
{ key: 'memory', icon: 'mdiHistory' },
|
||||
{ key: 'random', icon: 'mdiDice3' },
|
||||
{ key: 'search', icon: 'mdiMagnify' },
|
||||
{ key: 'find', icon: 'mdiFileSearch' },
|
||||
{ key: 'person', icon: 'mdiAccount' },
|
||||
{ key: 'place', icon: 'mdiMapMarker' },
|
||||
{ key: 'favorites', icon: 'mdiStar' },
|
||||
{ key: 'people', icon: 'mdiAccountGroup' },
|
||||
];
|
||||
// Immich command icons — used as fallback when capabilities don't specify icons
|
||||
const commandIcons: Record<string, string> = {
|
||||
help: 'mdiHelpCircle', status: 'mdiChartBox', albums: 'mdiImageMultiple',
|
||||
events: 'mdiPulse', summary: 'mdiFileDocumentEdit', latest: 'mdiImagePlus',
|
||||
memory: 'mdiHistory', random: 'mdiDice3', search: 'mdiMagnify',
|
||||
find: 'mdiFileSearch', person: 'mdiAccount', place: 'mdiMapMarker',
|
||||
favorites: 'mdiStar', people: 'mdiAccountGroup',
|
||||
};
|
||||
|
||||
let allCapabilities = $derived(capabilitiesCache.items);
|
||||
let providerCommands = $derived<{key: string, icon: string}[]>(
|
||||
(allCapabilities[form.provider_type]?.commands || []).map((c: any) => ({
|
||||
key: c.name,
|
||||
icon: commandIcons[c.name] || 'mdiConsole',
|
||||
}))
|
||||
);
|
||||
let hasCommands = $derived(providerCommands.length > 0);
|
||||
|
||||
const defaultForm = () => ({
|
||||
name: '',
|
||||
@@ -72,6 +73,7 @@
|
||||
await Promise.all([
|
||||
commandConfigsCache.fetch(true),
|
||||
commandTemplateConfigsCache.fetch(),
|
||||
capabilitiesCache.fetch(),
|
||||
]);
|
||||
} catch (err: any) { error = err.message || t('common.loadError'); snackError(error); }
|
||||
finally { loaded = true; highlightFromUrl(); }
|
||||
@@ -170,11 +172,12 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if hasCommands}
|
||||
<!-- Enabled commands -->
|
||||
<div>
|
||||
<p class="text-sm font-medium mb-2">{t('commandConfig.enabledCommands')}</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-1">
|
||||
{#each allCommands as cmd}
|
||||
{#each providerCommands as cmd}
|
||||
<label class="flex items-center gap-1.5 text-xs cursor-pointer hover:bg-[var(--color-muted)] px-2 py-1 rounded">
|
||||
<input type="checkbox" checked={form.enabled_commands.includes(cmd.key)}
|
||||
onchange={() => toggleCmd(cmd.key)} />
|
||||
@@ -212,6 +215,11 @@
|
||||
<input type="number" bind:value={form.rate_limits.default} min="0" max="300"
|
||||
class="w-full px-2 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-background)]" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-[var(--color-muted-foreground)] p-3 border border-[var(--color-border)] rounded-md">
|
||||
{t('commandConfig.noCommandsForProvider')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button type="submit" disabled={submitting}
|
||||
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">
|
||||
|
||||
Reference in New Issue
Block a user