Implement active IP server rows display in user traffic table
Publish telemt-api gateway Docker image / test (push) Successful in 25s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m8s

- Introduced a new derived state to calculate and display active IP-server pairs for each user, enhancing the visibility of user connections.
- Replaced the previous badge display with a structured table format, improving data organization and readability.
- Updated the UI to conditionally render active connections, providing clearer feedback when no active connections are present.
This commit is contained in:
Denozordec
2026-03-30 14:27:16 +07:00
parent b89bd7b632
commit bc15d4e2ca
+95 -14
View File
@@ -205,9 +205,60 @@
function chipLabel(ip: components['schemas']['IPAssignments']): string {
const cc = ip.country_code?.trim();
const flag = flagEmoji(cc ?? undefined);
const short = ip.ip && ip.ip.length > 12 ? ip.ip.slice(0, 8) + '…' : (ip.ip ?? '');
return `${flag} ${cc ?? '?'} ${short}`.trim();
const addr = ip.ip ?? '';
return `${flag} ${cc ?? '?'} ${addr}`.trim();
}
type ActiveIpServerRow = {
username: string;
ip: string;
country_code: string | null | undefined;
server: string;
activeLabel: string;
};
let activeIpServerRows = $derived.by((): ActiveIpServerRow[] => {
const out: ActiveIpServerRow[] = [];
const seen = new Set<string>();
const maxUniqueIpRows = 80;
const maxOutRows = 2000;
for (const ur of uniqueIps?.slice(0, maxUniqueIpRows) ?? []) {
const username = ur.username ?? '';
if (!username) continue;
for (const ipa of ur.ips ?? []) {
const ip = ipa.ip ?? '';
if (!ip) continue;
const servers = ipa.active_on_servers ?? [];
if (servers.length === 0) continue;
for (const server of servers) {
if (!server) continue;
const key = `${username}|${ip}|${server}`;
if (seen.has(key)) continue;
seen.add(key);
out.push({
username,
ip,
country_code: ipa.country_code,
server,
activeLabel: chipLabel(ipa)
});
if (out.length >= maxOutRows) return out;
}
}
}
out.sort(
(a, b) =>
a.ip.localeCompare(b.ip) || a.server.localeCompare(b.server) || a.username.localeCompare(b.username)
);
return out;
});
</script>
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
@@ -387,18 +438,48 @@
<Card.Description>С GeoIP при включённой базе на шлюзе</Card.Description>
</Card.Header>
<Card.Content>
<div class="flex flex-wrap gap-2">
{#each (uniqueIps ?? []).slice(0, 80) as row (row.username)}
{#each row.ips ?? [] as ip (`${row.username ?? ''}-${ip.ip ?? ''}`)}
<Badge variant="secondary" class="font-normal" title={ip.ip}>
{chipLabel(ip)}
</Badge>
{/each}
{/each}
{#if (uniqueIps?.length ?? 0) === 0}
<span class="text-sm text-muted-foreground">Нет данных</span>
{/if}
</div>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>IP со страной</Table.Head>
<Table.Head>Клиент</Table.Head>
<Table.Head>Сервер</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if (activeIpServerRows?.length ?? 0) === 0}
<Table.Row>
<Table.Cell colspan="3" class="p-4 text-center text-muted-foreground">
Нет активных подключений
</Table.Cell>
</Table.Row>
{:else}
{#each activeIpServerRows as r (r.username + '|' + r.ip + '|' + r.server)}
<Table.Row>
<Table.Cell class="font-mono text-sm">
{r.activeLabel}
</Table.Cell>
<Table.Cell>
<a
href="/users/{encodeURIComponent(r.username)}"
class="text-primary hover:underline"
>
{r.username}
</a>
</Table.Cell>
<Table.Cell>
<a
href="/servers/{encodeURIComponent(r.server)}"
class="text-primary hover:underline"
>
{r.server}
</a>
</Table.Cell>
</Table.Row>
{/each}
{/if}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>