Implement alias selection and summary fetching in incident and live pages
Publish telemt-api gateway Docker image / test (push) Successful in 29s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m2s

- Added functionality to manage and display available aliases for incidents and live updates, enhancing user interaction.
- Refactored alias handling to use checkboxes for selection, improving usability and clarity in the UI.
- Integrated fetching of aggregated summary data to populate available aliases dynamically, ensuring up-to-date information.
- Updated the logic for applying selected aliases in API requests, streamlining data retrieval based on user preferences.
This commit is contained in:
Denozordec
2026-03-30 19:50:42 +07:00
parent 8c8ccce6ee
commit d0d7b8c1e6
3 changed files with 139 additions and 32 deletions
+7
View File
@@ -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()
+66 -16
View File
@@ -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<string | null>(null);
let data = $state<IncidentsData | null>(null);
let aliases = $state('');
let aliasesInput = $state('');
let availableAliases = $state<string[]>([]);
let selectedAliases = $state<string[]>([]);
let refreshSeconds = $state(30);
let refreshInput = $state('30');
let lastSuccessAtMs = $state<number | null>(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 @@
</div>
<div class="mb-4 flex flex-wrap items-end gap-2">
<label class="text-xs text-muted-foreground">
Aliases
<input
type="text"
class="mt-1 h-9 w-56 rounded-md border bg-background px-2 text-sm"
bind:value={aliasesInput}
placeholder="node-a,node-b"
/>
</label>
<details class="rounded-md border bg-background p-2">
<summary class="cursor-pointer select-none text-sm">
<span class="text-xs text-muted-foreground">Aliases</span>
<span class="ml-2 font-medium">{aliasesLabel()}</span>
</summary>
<div class="mt-2 max-h-48 overflow-auto pr-1">
{#if availableAliases.length === 0}
<div class="px-2 py-1 text-xs text-muted-foreground">Список нод не загружен</div>
{:else}
<div class="flex flex-col gap-1">
{#each availableAliases as a (a)}
<label class="flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-accent/50">
<input
type="checkbox"
class="rounded"
checked={selectedAliases.includes(a)}
onchange={() => toggleAlias(a)}
/>
<span class="text-sm">{a}</span>
</label>
{/each}
</div>
{/if}
</div>
</details>
<label class="text-xs text-muted-foreground">
Refresh (sec)
<input
+66 -16
View File
@@ -4,6 +4,7 @@
import { goto } from '$app/navigation';
import {
liveEventsUrl,
fetchAggSummary,
type IncidentItem,
type IncidentSeverity,
type IncidentStatus
@@ -32,8 +33,8 @@
aliases_used?: string[];
};
let aliases = $state('');
let aliasesInput = $state('');
let availableAliases = $state<string[]>([]);
let selectedAliases = $state<string[]>([]);
let err = $state<string | null>(null);
let connected = $state(false);
let reconnectInSec = $state<number | null>(null);
@@ -57,6 +58,27 @@
return 'default';
}
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 normIncident(v: unknown): IncidentItem | null {
if (!v || typeof v !== 'object') return null;
const x = v as Record<string, unknown>;
@@ -148,7 +170,8 @@
closeStream();
clearReconnectTimer();
err = null;
const url = liveEventsUrl({ aliases: aliases || undefined });
const aliasesParam = selectedAliases.length ? selectedAliases.join(',') : undefined;
const url = liveEventsUrl({ aliases: aliasesParam });
const next = new EventSource(url);
next.onopen = () => {
connected = true;
@@ -192,8 +215,7 @@
async function applyControls() {
const q = new URLSearchParams(page.url.searchParams);
const normalizedAliases = aliasesInput.trim();
if (normalizedAliases) q.set('aliases', normalizedAliases);
if (selectedAliases.length > 0) q.set('aliases', selectedAliases.join(','));
else q.delete('aliases');
const qs = q.toString();
await goto(`${page.url.pathname}${qs ? `?${qs}` : ''}`, {
@@ -207,8 +229,7 @@
$effect(() => {
queryKey;
if (typeof window === 'undefined') return;
aliases = page.url.searchParams.get('aliases')?.trim() ?? '';
aliasesInput = aliases;
selectedAliases = parseAliases(page.url.searchParams.get('aliases'));
connectStream();
});
@@ -218,6 +239,19 @@
let isStale = $derived.by(() => staleSeconds == null || staleSeconds > 12);
onMount(() => {
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);
@@ -249,15 +283,31 @@
</div>
<div class="mb-4 flex flex-wrap items-end gap-2">
<label class="text-xs text-muted-foreground">
Aliases
<input
type="text"
class="mt-1 h-9 w-56 rounded-md border bg-background px-2 text-sm"
bind:value={aliasesInput}
placeholder="node-a,node-b"
/>
</label>
<details class="rounded-md border bg-background p-2">
<summary class="cursor-pointer select-none text-sm">
<span class="text-xs text-muted-foreground">Aliases</span>
<span class="ml-2 font-medium">{aliasesLabel()}</span>
</summary>
<div class="mt-2 max-h-48 overflow-auto pr-1">
{#if availableAliases.length === 0}
<div class="px-2 py-1 text-xs text-muted-foreground">Список нод не загружен</div>
{:else}
<div class="flex flex-col gap-1">
{#each availableAliases as a (a)}
<label class="flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-accent/50">
<input
type="checkbox"
class="rounded"
checked={selectedAliases.includes(a)}
onchange={() => toggleAlias(a)}
/>
<span class="text-sm">{a}</span>
</label>
{/each}
</div>
{/if}
</div>
</details>
<Button variant="outline" size="sm" onclick={() => void applyControls()}>
Apply
</Button>