feat: entity cache system, nav UX improvements, split CLAUDE.md
- Add $state-based entity cache layer with 30s TTL, request deduplication, and local mutation helpers (entity-cache.svelte.ts + caches.svelte.ts) - Wire all 10 page components to use shared caches for cross-page data - Add slide animation for nav tree expand/collapse with rotating chevron - Remove aggregate count badges from container nav nodes (keep on leaves) - Convert Targets from flat leaf to group with per-type children (Telegram, Webhook, Email, Discord, Slack, ntfy, Matrix) - Add URL-based type filtering on Targets page with per-type descriptions - Add Bots group children for Email and Matrix alongside Telegram - Tab-based routing for bots page (?tab=telegram/email/matrix) - Add per-type target counts and email/matrix bot counts to /status/counts - Split CLAUDE.md into focused context files under .claude/docs/ - Fix .gitignore: scope lib/ to root, allow .claude/docs/ tracking - Clear all caches on logout - Reset form state when switching target type tabs
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
import '../app.css';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { api } from '$lib/api';
|
||||
import { getAuth, loadUser, logout } from '$lib/auth.svelte';
|
||||
import { t, getLocale, setLocale, type Locale } from '$lib/i18n';
|
||||
@@ -50,7 +51,6 @@
|
||||
key: string;
|
||||
icon: string;
|
||||
children: NavItem[];
|
||||
countKeys?: string[];
|
||||
}
|
||||
|
||||
type NavEntry = NavItem | NavGroup;
|
||||
@@ -64,7 +64,6 @@
|
||||
{ href: '/providers', key: 'nav.providers', icon: 'mdiServer', countKey: 'providers' },
|
||||
{
|
||||
key: 'nav.notification', icon: 'mdiBellOutline',
|
||||
countKeys: ['notification_trackers', 'tracking_configs', 'template_configs'],
|
||||
children: [
|
||||
{ href: '/notification-trackers', key: 'nav.trackers', icon: 'mdiRadar', countKey: 'notification_trackers' },
|
||||
{ href: '/tracking-configs', key: 'nav.configs', icon: 'mdiCog', countKey: 'tracking_configs' },
|
||||
@@ -73,7 +72,6 @@
|
||||
},
|
||||
{
|
||||
key: 'nav.commands', icon: 'mdiConsoleLine',
|
||||
countKeys: ['command_trackers', 'command_configs', 'command_template_configs'],
|
||||
children: [
|
||||
{ href: '/command-trackers', key: 'nav.trackers', icon: 'mdiRadar', countKey: 'command_trackers' },
|
||||
{ href: '/command-configs', key: 'nav.configs', icon: 'mdiCog', countKey: 'command_configs' },
|
||||
@@ -82,12 +80,24 @@
|
||||
},
|
||||
{
|
||||
key: 'nav.bots', icon: 'mdiRobot',
|
||||
countKeys: ['telegram_bots'],
|
||||
children: [
|
||||
{ href: '/telegram-bots', key: 'nav.telegram', icon: 'mdiSendCircle', countKey: 'telegram_bots' },
|
||||
{ href: '/telegram-bots?tab=email', key: 'nav.email', icon: 'mdiEmailOutline', countKey: 'email_bots' },
|
||||
{ href: '/telegram-bots?tab=matrix', key: 'nav.matrix', icon: 'mdiMatrix', countKey: 'matrix_bots' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'nav.targets', icon: 'mdiTarget',
|
||||
children: [
|
||||
{ href: '/targets?type=telegram', key: 'nav.targetTelegram', icon: 'mdiSend', countKey: 'targets_telegram' },
|
||||
{ href: '/targets?type=webhook', key: 'nav.targetWebhook', icon: 'mdiWebhook', countKey: 'targets_webhook' },
|
||||
{ href: '/targets?type=email', key: 'nav.targetEmail', icon: 'mdiEmailOutline', countKey: 'targets_email' },
|
||||
{ href: '/targets?type=discord', key: 'nav.targetDiscord', icon: 'mdiChat', countKey: 'targets_discord' },
|
||||
{ href: '/targets?type=slack', key: 'nav.targetSlack', icon: 'mdiSlack', countKey: 'targets_slack' },
|
||||
{ href: '/targets?type=ntfy', key: 'nav.targetNtfy', icon: 'mdiBell', countKey: 'targets_ntfy' },
|
||||
{ href: '/targets?type=matrix', key: 'nav.targetMatrix', icon: 'mdiMatrix', countKey: 'targets_matrix' },
|
||||
],
|
||||
},
|
||||
{ href: '/targets', key: 'nav.targets', icon: 'mdiTarget', countKey: 'targets' },
|
||||
];
|
||||
|
||||
const navEntries = $derived<NavEntry[]>(auth.isAdmin
|
||||
@@ -115,12 +125,10 @@
|
||||
}
|
||||
|
||||
function isGroupActive(group: NavGroup): boolean {
|
||||
return group.children.some(c => page.url.pathname === c.href);
|
||||
}
|
||||
|
||||
function groupCount(group: NavGroup): number {
|
||||
if (!group.countKeys) return 0;
|
||||
return group.countKeys.reduce((s, k) => s + (navCounts[k] || 0), 0);
|
||||
return group.children.some(c => {
|
||||
const path = c.href.split('?')[0];
|
||||
return page.url.pathname === path;
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile: flatten nav for bottom bar
|
||||
@@ -182,6 +190,15 @@
|
||||
}
|
||||
|
||||
function isActive(href: string): boolean {
|
||||
if (href.includes('?')) {
|
||||
const [path, qs] = href.split('?');
|
||||
if (page.url.pathname !== path) return false;
|
||||
const params = new URLSearchParams(qs);
|
||||
for (const [k, v] of params) {
|
||||
if (page.url.searchParams.get(k) !== v) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return page.url.pathname === href;
|
||||
}
|
||||
</script>
|
||||
@@ -241,15 +258,14 @@
|
||||
<MdiIcon name={entry.icon} size={18} />
|
||||
{#if !collapsed}
|
||||
<span class="truncate flex-1">{t(entry.key)}</span>
|
||||
{#if groupCount(entry) > 0}
|
||||
<span class="nav-badge">{groupCount(entry)}</span>
|
||||
{/if}
|
||||
<MdiIcon name={expandedGroups[entry.key] ? 'mdiChevronDown' : 'mdiChevronRight'} size={14} />
|
||||
<span class="nav-chevron" style="display: inline-flex; transition: transform 0.2s ease; transform: rotate({expandedGroups[entry.key] ? '90deg' : '0deg'});">
|
||||
<MdiIcon name="mdiChevronRight" size={14} />
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
<!-- Group children -->
|
||||
{#if expandedGroups[entry.key] && !collapsed}
|
||||
<div class="ml-3 pl-3 space-y-0.5" style="border-left: 1px solid var(--color-border);">
|
||||
<div transition:slide={{ duration: 200, easing: cubicOut }} class="ml-3 pl-3 space-y-0.5" style="border-left: 1px solid var(--color-border);">
|
||||
{#each entry.children as child}
|
||||
<a
|
||||
href={child.href}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { t } from '$lib/i18n';
|
||||
import { providersCache } 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';
|
||||
@@ -9,7 +10,7 @@
|
||||
import EventChart from '$lib/components/EventChart.svelte';
|
||||
|
||||
let status = $state<any>(null);
|
||||
let providers = $state<any[]>([]);
|
||||
let providers = $derived(providersCache.items);
|
||||
let chartDays = $state<any[]>([]);
|
||||
let loaded = $state(false);
|
||||
let error = $state('');
|
||||
@@ -130,13 +131,12 @@
|
||||
|
||||
async function loadInitial() {
|
||||
try {
|
||||
const [statusRes, providersRes, chartRes] = await Promise.all([
|
||||
const [statusRes, , chartRes] = await Promise.all([
|
||||
api<any>(`/status?limit=${eventsLimit}`),
|
||||
api<any[]>('/providers'),
|
||||
providersCache.fetch(),
|
||||
api<any>('/status/chart'),
|
||||
]);
|
||||
status = statusRes;
|
||||
providers = providersRes;
|
||||
chartDays = chartRes.days || [];
|
||||
setTimeout(() => {
|
||||
animateCount(0, status.providers, (v) => displayProviders = v);
|
||||
|
||||
@@ -2,6 +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 PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Card from '$lib/components/Card.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
@@ -12,8 +13,8 @@
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
|
||||
let configs = $state<any[]>([]);
|
||||
let cmdTemplateConfigs = $state<any[]>([]);
|
||||
let configs = $derived(commandConfigsCache.items);
|
||||
let cmdTemplateConfigs = $derived(commandTemplateConfigsCache.items);
|
||||
let loaded = $state(false);
|
||||
let showForm = $state(false);
|
||||
let editing = $state<number | null>(null);
|
||||
@@ -54,9 +55,9 @@
|
||||
onMount(load);
|
||||
async function load() {
|
||||
try {
|
||||
[configs, cmdTemplateConfigs] = await Promise.all([
|
||||
api('/command-configs'),
|
||||
api('/command-template-configs'),
|
||||
await Promise.all([
|
||||
commandConfigsCache.fetch(true),
|
||||
commandTemplateConfigsCache.fetch(),
|
||||
]);
|
||||
} catch (err: any) { error = err.message || t('common.loadError'); snackError(error); }
|
||||
finally { loaded = true; }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import { api } from '$lib/api';
|
||||
import { t } from '$lib/i18n';
|
||||
import { commandTemplateConfigsCache } 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';
|
||||
@@ -66,7 +67,7 @@
|
||||
async function load() {
|
||||
try {
|
||||
const [cfgs, caps, vars] = await Promise.all([
|
||||
api('/command-template-configs'),
|
||||
commandTemplateConfigsCache.fetch(true),
|
||||
api('/providers/capabilities'),
|
||||
api('/command-template-configs/variables'),
|
||||
]);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import { api } from '$lib/api';
|
||||
import { t } from '$lib/i18n';
|
||||
import { providersCache, telegramBotsCache, commandConfigsCache } 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';
|
||||
@@ -15,9 +16,9 @@
|
||||
import type { ServiceProvider, TelegramBot } from '$lib/types';
|
||||
|
||||
let trackers = $state<any[]>([]);
|
||||
let providers = $state<ServiceProvider[]>([]);
|
||||
let commandConfigs = $state<any[]>([]);
|
||||
let telegramBots = $state<TelegramBot[]>([]);
|
||||
let providers = $derived(providersCache.items);
|
||||
let commandConfigs = $derived(commandConfigsCache.items);
|
||||
let telegramBots = $derived(telegramBotsCache.items);
|
||||
let loaded = $state(false);
|
||||
let showForm = $state(false);
|
||||
let editing = $state<number | null>(null);
|
||||
@@ -53,11 +54,10 @@
|
||||
onMount(load);
|
||||
async function load() {
|
||||
try {
|
||||
[trackers, providers, commandConfigs, telegramBots] = await Promise.all([
|
||||
[trackers] = await Promise.all([
|
||||
api('/command-trackers'),
|
||||
api('/providers'),
|
||||
api('/command-configs'),
|
||||
api('/telegram-bots'),
|
||||
providersCache.fetch(), commandConfigsCache.fetch(),
|
||||
telegramBotsCache.fetch(),
|
||||
]);
|
||||
} catch (err: any) { error = err.message || t('common.loadError'); snackError(error); }
|
||||
finally { loaded = true; }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import { api } from '$lib/api';
|
||||
import { t, getLocale } from '$lib/i18n';
|
||||
import { providersCache, targetsCache, trackingConfigsCache, templateConfigsCache } 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';
|
||||
@@ -19,10 +20,10 @@
|
||||
let loaded = $state(false);
|
||||
let loadError = $state('');
|
||||
let notificationTrackers = $state<Tracker[]>([]);
|
||||
let providers = $state<ServiceProvider[]>([]);
|
||||
let targets = $state<NotificationTarget[]>([]);
|
||||
let trackingConfigs = $state<TrackingConfig[]>([]);
|
||||
let templateConfigs = $state<TemplateConfig[]>([]);
|
||||
let providers = $derived(providersCache.items);
|
||||
let targets = $derived(targetsCache.items);
|
||||
let trackingConfigs = $derived(trackingConfigsCache.items);
|
||||
let templateConfigs = $derived(templateConfigsCache.items);
|
||||
let collections = $state<any[]>([]);
|
||||
let showForm = $state(false);
|
||||
let editing = $state<number | null>(null);
|
||||
@@ -59,9 +60,10 @@
|
||||
async function load() {
|
||||
loadError = '';
|
||||
try {
|
||||
[notificationTrackers, providers, targets, trackingConfigs, templateConfigs] = await Promise.all([
|
||||
api('/notification-trackers'), api('/providers'), api('/targets'),
|
||||
api('/tracking-configs'), api('/template-configs'),
|
||||
[notificationTrackers] = await Promise.all([
|
||||
api('/notification-trackers'),
|
||||
providersCache.fetch(), targetsCache.fetch(),
|
||||
trackingConfigsCache.fetch(), templateConfigsCache.fetch(),
|
||||
]);
|
||||
} catch (err: any) {
|
||||
loadError = err.message || 'Failed to load data';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import { api } from '$lib/api';
|
||||
import { t } from '$lib/i18n';
|
||||
import { providersCache } 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';
|
||||
@@ -14,7 +15,7 @@
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
import type { ServiceProvider } from '$lib/types';
|
||||
|
||||
let providers = $state<ServiceProvider[]>([]);
|
||||
let providers = $derived(providersCache.items);
|
||||
let showForm = $state(false);
|
||||
let editing = $state<number | null>(null);
|
||||
let form = $state({ name: 'Immich', type: 'immich', url: '', api_key: '', external_domain: '', icon: '' });
|
||||
@@ -29,7 +30,7 @@
|
||||
onMount(load);
|
||||
async function load() {
|
||||
try {
|
||||
providers = await api('/providers');
|
||||
await providersCache.fetch(true);
|
||||
loadError = '';
|
||||
} catch (err: any) {
|
||||
loadError = err.message || t('providers.loadError');
|
||||
@@ -65,7 +66,7 @@
|
||||
config.api_key = form.api_key; // required on create
|
||||
await api('/providers', { method: 'POST', body: JSON.stringify({ type: form.type, name: form.name, icon: form.icon, config }) });
|
||||
}
|
||||
showForm = false; editing = null; await load();
|
||||
showForm = false; editing = null; providersCache.invalidate(); await load();
|
||||
snackSuccess(t('snack.providerSaved'));
|
||||
} catch (err: any) { error = err.message; snackError(err.message); }
|
||||
submitting = false;
|
||||
@@ -76,7 +77,7 @@
|
||||
if (!confirmDelete) return;
|
||||
const id = confirmDelete.id;
|
||||
confirmDelete = null;
|
||||
try { await api(`/providers/${id}`, { method: 'DELETE' }); await load(); snackSuccess(t('snack.providerDeleted')); }
|
||||
try { await api(`/providers/${id}`, { method: 'DELETE' }); providersCache.invalidate(); await load(); snackSuccess(t('snack.providerDeleted')); }
|
||||
catch (err: any) { error = err.message; snackError(err.message); }
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { page } from '$app/state';
|
||||
import { api } from '$lib/api';
|
||||
import { t, getLocale } from '$lib/i18n';
|
||||
import { targetsCache, telegramBotsCache, emailBotsCache, matrixBotsCache } 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';
|
||||
@@ -21,11 +23,17 @@
|
||||
telegram: 'mdiSend', webhook: 'mdiWebhook', email: 'mdiEmailOutline',
|
||||
discord: 'mdiChat', slack: 'mdiSlack', ntfy: 'mdiBell', matrix: 'mdiMatrix',
|
||||
};
|
||||
const TYPE_DESC_KEYS: Record<string, string> = {
|
||||
telegram: 'targets.descTelegram', webhook: 'targets.descWebhook', email: 'targets.descEmail',
|
||||
discord: 'targets.descDiscord', slack: 'targets.descSlack', ntfy: 'targets.descNtfy', matrix: 'targets.descMatrix',
|
||||
};
|
||||
|
||||
let targets = $state<NotificationTarget[]>([]);
|
||||
let bots = $state<TelegramBot[]>([]);
|
||||
let emailBots = $state<EmailBot[]>([]);
|
||||
let matrixBots = $state<MatrixBot[]>([]);
|
||||
let allTargets = $derived(targetsCache.items);
|
||||
let activeType = $derived(page.url.searchParams.get('type') as TargetType | null);
|
||||
let targets = $derived(activeType ? allTargets.filter(t => t.type === activeType) : allTargets);
|
||||
let bots = $derived(telegramBotsCache.items);
|
||||
let emailBots = $derived(emailBotsCache.items);
|
||||
let matrixBots = $derived(matrixBotsCache.items);
|
||||
let botChats = $state<Record<number, TelegramChat[]>>({});
|
||||
let showForm = $state(false);
|
||||
let editing = $state<number | null>(null);
|
||||
@@ -51,11 +59,20 @@
|
||||
let showTelegramSettings = $state(false);
|
||||
let confirmDelete = $state<NotificationTarget | null>(null);
|
||||
|
||||
// Reset form when switching target type tabs
|
||||
$effect(() => {
|
||||
activeType; // track
|
||||
showForm = false;
|
||||
editing = null;
|
||||
error = '';
|
||||
});
|
||||
|
||||
onMount(load);
|
||||
async function load() {
|
||||
try {
|
||||
[targets, bots, emailBots, matrixBots] = await Promise.all([
|
||||
api('/targets'), api('/telegram-bots'), api('/email-bots'), api('/matrix-bots'),
|
||||
await Promise.all([
|
||||
targetsCache.fetch(true), telegramBotsCache.fetch(),
|
||||
emailBotsCache.fetch(), matrixBotsCache.fetch(),
|
||||
]);
|
||||
loadError = '';
|
||||
} catch (err: any) { loadError = err.message || t('common.loadError'); snackError(loadError); } finally { loaded = true; }
|
||||
@@ -66,7 +83,7 @@
|
||||
try { botChats[form.bot_id] = await api(`/telegram-bots/${form.bot_id}/chats`); } catch {}
|
||||
}
|
||||
|
||||
function openNew() { form = defaultForm(); formType = 'telegram'; editing = null; showTelegramSettings = false; showForm = true; }
|
||||
function openNew() { form = defaultForm(); formType = activeType || 'telegram'; editing = null; showTelegramSettings = false; showForm = true; }
|
||||
async function edit(tgt: any) {
|
||||
formType = tgt.type;
|
||||
const c = tgt.config || {};
|
||||
@@ -152,7 +169,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<PageHeader title={t('targets.title')} description={t('targets.description')}>
|
||||
<PageHeader title={activeType ? `${t('targets.title')} — ${activeType.charAt(0).toUpperCase() + activeType.slice(1)}` : t('targets.title')} description={activeType ? t(TYPE_DESC_KEYS[activeType]) : t('targets.description')}>
|
||||
<button onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}
|
||||
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
|
||||
{showForm ? t('targets.cancel') : t('targets.addTarget')}
|
||||
@@ -170,6 +187,7 @@
|
||||
<Card class="mb-6">
|
||||
{#if error}<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>{/if}
|
||||
<form onsubmit={save} class="space-y-4">
|
||||
{#if !activeType}
|
||||
<div>
|
||||
<label for="tgt-type" class="block text-sm font-medium mb-1">{t('targets.type')}</label>
|
||||
<select id="tgt-type" bind:value={formType}
|
||||
@@ -179,6 +197,7 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<label for="tgt-name" class="block text-sm font-medium mb-1">{t('targets.name')}</label>
|
||||
<div class="flex gap-2">
|
||||
@@ -359,7 +378,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span style="color: var(--color-primary);"><MdiIcon name={target.icon || TYPE_ICONS[target.type] || 'mdiTarget'} size={20} /></span>
|
||||
<p class="font-medium">{target.name}</p>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{target.type}</span>
|
||||
{#if !activeType}<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{target.type}</span>{/if}
|
||||
{#if target.receiver_count}<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{target.receiver_count} receiver(s)</span>{/if}
|
||||
</div>
|
||||
<p class="text-sm text-[var(--color-muted-foreground)]">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { page } from '$app/state';
|
||||
import { api } from '$lib/api';
|
||||
import { t, getLocale } from '$lib/i18n';
|
||||
import { telegramBotsCache, emailBotsCache, matrixBotsCache } 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';
|
||||
@@ -14,9 +16,12 @@
|
||||
import { snackSuccess, snackError, snackInfo } from '$lib/stores/snackbar.svelte';
|
||||
import type { TelegramBot, TelegramChat, EmailBot, MatrixBot } from '$lib/types';
|
||||
|
||||
let bots = $state<TelegramBot[]>([]);
|
||||
let emailBots = $state<EmailBot[]>([]);
|
||||
let matrixBots = $state<MatrixBot[]>([]);
|
||||
type BotTab = 'telegram' | 'email' | 'matrix';
|
||||
let activeTab = $derived((page.url.searchParams.get('tab') as BotTab | null) || 'telegram');
|
||||
|
||||
let bots = $derived(telegramBotsCache.items);
|
||||
let emailBots = $derived(emailBotsCache.items);
|
||||
let matrixBots = $derived(matrixBotsCache.items);
|
||||
let loaded = $state(false);
|
||||
let showForm = $state(false);
|
||||
let editing = $state<number | null>(null);
|
||||
@@ -39,11 +44,11 @@
|
||||
onMount(load);
|
||||
async function load() {
|
||||
try {
|
||||
[bots, settings, emailBots, matrixBots] = await Promise.all([
|
||||
api('/telegram-bots'),
|
||||
[, settings] = await Promise.all([
|
||||
telegramBotsCache.fetch(true),
|
||||
api('/settings'),
|
||||
api('/email-bots'),
|
||||
api('/matrix-bots'),
|
||||
emailBotsCache.fetch(true),
|
||||
matrixBotsCache.fetch(true),
|
||||
]);
|
||||
} catch (err: any) { error = err.message || t('common.loadError'); snackError(error); }
|
||||
finally { loaded = true; }
|
||||
@@ -341,6 +346,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !loaded}<Loading />{:else}
|
||||
|
||||
{#if activeTab === 'telegram'}
|
||||
<PageHeader title={t('telegramBot.title')} description={t('telegramBot.description')}>
|
||||
<button onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}
|
||||
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
|
||||
@@ -348,8 +356,6 @@
|
||||
</button>
|
||||
</PageHeader>
|
||||
|
||||
{#if !loaded}<Loading />{:else}
|
||||
|
||||
{#if showForm}
|
||||
<Card class="mb-6">
|
||||
{#if error}<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>{/if}
|
||||
@@ -559,8 +565,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ConfirmModal open={confirmDelete !== null} message={t('telegramBot.confirmDelete')}
|
||||
onconfirm={() => confirmDelete?.onconfirm()} oncancel={() => confirmDelete = null} />
|
||||
{/if}
|
||||
|
||||
<!-- ======= Email Bots Section ======= -->
|
||||
<div class="mt-8">
|
||||
{#if activeTab === 'email'}
|
||||
<PageHeader title={t('emailBot.title')} description={t('emailBot.description')}>
|
||||
<button onclick={() => { showEmailForm ? (showEmailForm = false, editingEmail = null) : openNewEmail(); }}
|
||||
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
|
||||
@@ -653,18 +663,13 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
<ConfirmModal open={confirmDelete !== null} message={t('telegramBot.confirmDelete')}
|
||||
onconfirm={() => confirmDelete?.onconfirm()} oncancel={() => confirmDelete = null} />
|
||||
|
||||
<ConfirmModal open={confirmDeleteEmail !== null} message={t('emailBot.confirmDelete')}
|
||||
onconfirm={() => confirmDeleteEmail?.onconfirm()} oncancel={() => confirmDeleteEmail = null} />
|
||||
{/if}
|
||||
|
||||
<!-- ======= Matrix Bots Section ======= -->
|
||||
<div class="mt-8">
|
||||
{#if activeTab === 'matrix'}
|
||||
<PageHeader title={t('matrixBot.title')} description={t('matrixBot.description')}>
|
||||
<button onclick={() => { showMatrixForm ? (showMatrixForm = false, editingMatrix = null) : openNewMatrix(); }}
|
||||
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
|
||||
@@ -740,7 +745,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ConfirmModal open={confirmDeleteMatrix !== null} message={t('matrixBot.confirmDelete')}
|
||||
onconfirm={() => confirmDeleteMatrix?.onconfirm()} oncancel={() => confirmDeleteMatrix = null} />
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import { api } from '$lib/api';
|
||||
import { t } from '$lib/i18n';
|
||||
import { templateConfigsCache } 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';
|
||||
@@ -17,7 +18,7 @@
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
import type { TemplateConfig } from '$lib/types';
|
||||
|
||||
let configs = $state<TemplateConfig[]>([]);
|
||||
let configs = $derived(templateConfigsCache.items);
|
||||
let loaded = $state(false);
|
||||
let varsRef = $state<Record<string, any>>({});
|
||||
let showVarsFor = $state<string | null>(null);
|
||||
@@ -133,8 +134,8 @@
|
||||
onMount(load);
|
||||
async function load() {
|
||||
try {
|
||||
[configs, varsRef, allCapabilities] = await Promise.all([
|
||||
api('/template-configs'),
|
||||
[, varsRef, allCapabilities] = await Promise.all([
|
||||
templateConfigsCache.fetch(true),
|
||||
api('/template-configs/variables'),
|
||||
api('/providers/capabilities'),
|
||||
]);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import { api } from '$lib/api';
|
||||
import { t } from '$lib/i18n';
|
||||
import { trackingConfigsCache } 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';
|
||||
@@ -15,7 +16,7 @@
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
import type { TrackingConfig } from '$lib/types';
|
||||
|
||||
let configs = $state<TrackingConfig[]>([]);
|
||||
let configs = $derived(trackingConfigsCache.items);
|
||||
let loaded = $state(false);
|
||||
let showForm = $state(false);
|
||||
let editing = $state<number | null>(null);
|
||||
@@ -40,7 +41,7 @@
|
||||
|
||||
onMount(load);
|
||||
async function load() {
|
||||
try { configs = await api('/tracking-configs'); }
|
||||
try { await trackingConfigsCache.fetch(true); }
|
||||
catch (err: any) { error = err.message || t('common.loadError'); snackError(error); }
|
||||
finally { loaded = true; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user