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}
|
||||
|
||||
Reference in New Issue
Block a user