feat: grouped nav tree with badges, dashboard events section with filtered chart

Navigation:
- Restructure flat nav into grouped tree: Notification (Trackers,
  Configs, Templates), Commands (same), Bots (Telegram), Settings
  (Common, Users)
- Collapsible groups with expand/collapse state persisted in localStorage
- Auto-expand group containing the active page
- Counter badges on groups (sum of children) and individual items
- New /api/status/counts endpoint for nav badge data
- Mobile bottom nav uses flattened key pages

Dashboard:
- Rename "Recent Events" to "Events"
- Move chart under Events section (after filters, before event list)
- Filters (event type, provider, search) now affect both the event
  list AND the chart simultaneously
- Add event_type, provider_id, search filter params to /api/status/chart
This commit is contained in:
2026-03-21 23:07:55 +03:00
parent ddcbfdaa0b
commit 2c740ff2d2
5 changed files with 366 additions and 65 deletions
+26 -8
View File
@@ -34,8 +34,8 @@
/** Calculate how many event rows fit in the remaining viewport space. */
function calcPageSize(): number {
if (typeof window === 'undefined') return 8;
const EVENT_ROW_HEIGHT = 50; // px per event row (content + gap)
const FIXED_OVERHEAD = 600; // header + stats + chart + events header + filters + paginator + padding
const EVENT_ROW_HEIGHT = 50;
const FIXED_OVERHEAD = 700; // slightly more for chart in Events section
const available = window.innerHeight - FIXED_OVERHEAD;
return Math.max(3, Math.floor(available / EVENT_ROW_HEIGHT));
}
@@ -53,13 +53,19 @@
requestAnimationFrame(frame);
}
/** Build filter query string (shared by events list + chart). */
function buildFilterParams(): URLSearchParams {
const params = new URLSearchParams();
if (filterEventType) params.set('event_type', filterEventType);
if (filterProviderId) params.set('provider_id', filterProviderId);
if (filterSearch) params.set('search', filterSearch);
return params;
}
async function loadEvents() {
eventsLoading = true;
try {
const params = new URLSearchParams();
if (filterEventType) params.set('event_type', filterEventType);
if (filterProviderId) params.set('provider_id', filterProviderId);
if (filterSearch) params.set('search', filterSearch);
const params = buildFilterParams();
params.set('sort', filterSort);
params.set('limit', String(eventsLimit));
params.set('offset', String(eventsOffset));
@@ -72,9 +78,19 @@
}
}
async function loadChart() {
try {
const params = buildFilterParams();
const qs = params.toString();
const chartRes = await api<any>(`/status/chart${qs ? '?' + qs : ''}`);
chartDays = chartRes.days || [];
} catch {}
}
function applyFilters() {
eventsOffset = 0;
loadEvents();
loadChart();
}
function goToPage(page: number) {
@@ -216,8 +232,7 @@
{/each}
</div>
<EventChart days={chartDays} />
<!-- Events section -->
<h3 class="text-base font-semibold mb-3 flex items-center gap-2">
<MdiIcon name="mdiPulse" size={18} />
{t('dashboard.recentEvents')}
@@ -253,6 +268,9 @@
</select>
</div>
<!-- Chart (now inside Events section, affected by filters) -->
<EventChart days={chartDays} />
{#if eventsLoading}
<Card><p class="text-sm text-center py-4" style="color: var(--color-muted-foreground);">{t('dashboard.loadingEvents')}</p></Card>
{:else if status.recent_events.length === 0}