diff --git a/internal/server/gateway.go b/internal/server/gateway.go index 4576558..da00e3b 100644 --- a/internal/server/gateway.go +++ b/internal/server/gateway.go @@ -356,6 +356,13 @@ func (s *statusWriter) WriteHeader(code int) { s.ResponseWriter.WriteHeader(code) } +// Flush keeps streaming compatibility (SSE/EventSource) through middleware wrappers. +func (s *statusWriter) Flush() { + if f, ok := s.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + // Shutdown idle connections on the shared transport. func (g *Gateway) Shutdown(ctx context.Context) error { g.transport.CloseIdleConnections() diff --git a/web/src/routes/incidents/+page.svelte b/web/src/routes/incidents/+page.svelte index 8771d82..e38ce15 100644 --- a/web/src/routes/incidents/+page.svelte +++ b/web/src/routes/incidents/+page.svelte @@ -5,6 +5,7 @@ import { ApiError, fetchAggIncidents, + fetchAggSummary, type IncidentItem, type IncidentsData } from '$lib/api/client.js'; @@ -29,8 +30,8 @@ let partial = $state(false); let generatedAt = $state(null); let data = $state(null); - let aliases = $state(''); - let aliasesInput = $state(''); + let availableAliases = $state([]); + let selectedAliases = $state([]); let refreshSeconds = $state(30); let refreshInput = $state('30'); let lastSuccessAtMs = $state(null); @@ -47,6 +48,27 @@ return Math.max(10, Math.min(300, Math.floor(n))); } + function parseAliases(raw: string | null): string[] { + if (!raw) return []; + const out = raw + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + return Array.from(new Set(out)).sort(); + } + + function aliasesLabel(): string { + if (selectedAliases.length === 0) return 'Все ноды'; + if (selectedAliases.length <= 3) return selectedAliases.join(', '); + return `${selectedAliases.slice(0, 3).join(', ')} +${selectedAliases.length - 3}`; + } + + function toggleAlias(a: string) { + if (!a) return; + const has = selectedAliases.includes(a); + selectedAliases = has ? selectedAliases.filter((x) => x !== a) : [...selectedAliases, a].sort(); + } + function triageFor(id: string): TriageState { return triageById[id] ?? {}; } @@ -111,7 +133,8 @@ loading = true; err = null; try { - const env = await fetchAggIncidents({ aliases: aliases || undefined }); + const aliasesParam = selectedAliases.length ? selectedAliases.join(',') : undefined; + const env = await fetchAggIncidents({ aliases: aliasesParam }); data = env.data ?? null; partial = !!env.partial; generatedAt = env.generated_at; @@ -125,9 +148,8 @@ async function applyControls() { const q = new URLSearchParams(page.url.searchParams); - const normalizedAliases = aliasesInput.trim(); const parsedRefresh = parseRefresh(refreshInput); - if (normalizedAliases) q.set('aliases', normalizedAliases); + if (selectedAliases.length > 0) q.set('aliases', selectedAliases.join(',')); else q.delete('aliases'); if (parsedRefresh === 30) q.delete('refresh'); else q.set('refresh', String(parsedRefresh)); @@ -143,9 +165,8 @@ $effect(() => { queryKey; if (typeof window === 'undefined') return; - aliases = page.url.searchParams.get('aliases')?.trim() ?? ''; + selectedAliases = parseAliases(page.url.searchParams.get('aliases')); refreshSeconds = parseRefresh(page.url.searchParams.get('refresh')); - aliasesInput = aliases; refreshInput = String(refreshSeconds); restartPolling(); void load(); @@ -160,6 +181,19 @@ onMount(() => { loadTriage(); + void (async () => { + try { + const env = await fetchAggSummary({ top_n: 10 }); + const aliases = (env.data.servers ?? []) + .map((s) => s.alias) + .filter((a): a is string => typeof a === 'string' && a.length > 0) + .sort(); + availableAliases = aliases; + selectedAliases = selectedAliases.filter((a) => aliases.includes(a)); + } catch { + availableAliases = []; + } + })(); staleTimer = setInterval(() => { nowMs = Date.now(); }, 1000); @@ -190,15 +224,31 @@
- +
+ + Aliases + {aliasesLabel()} + +
+ {#if availableAliases.length === 0} +
Список нод не загружен
+ {:else} +
+ {#each availableAliases as a (a)} + + {/each} +
+ {/if} +
+
- +
+ + Aliases + {aliasesLabel()} + +
+ {#if availableAliases.length === 0} +
Список нод не загружен
+ {:else} +
+ {#each availableAliases as a (a)} + + {/each} +
+ {/if} +
+