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:
@@ -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)]">
|
||||
|
||||
Reference in New Issue
Block a user