fix(ui): показывать остановку потока live-трафика

This commit is contained in:
Denozordec committed 2026-09-24 01:13:11 +07:00
1 parent e05b3a38c8
commit 9eb6a3fad1
4 files changed
+53 -2

No files matched your search

+3 -1
View File
@@ -1464,7 +1464,9 @@ export default function TrafficPage() {
setRange={setRange}
liveRx={liveSample?.rxMbps}
liveTx={liveSample?.txMbps}
liveHint={liveSample ? "live" : (liveStreamError ? "история" : undefined)}
liveHint={liveStreamError
? (liveSample ? "Последнее live-измерение · поток недоступен" : "История · live недоступен")
: liveSample ? "live" : isLive ? "История · ожидание live" : undefined}
/>
)}
{effectiveMode === "users" && selUser && <UserDetail sel={selUser} range={range} setRange={setRange} />}
+7
View File
@@ -29,6 +29,13 @@
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
## Блок 8.3А — достоверность live-подписей трафика (2026-09-24)
- useTrafficLive сообщает завершение SSE, проверяет отмену после fetch/read и не применяет некорректные sample (нечисловые/отрицательные RX/TX или неверное время). Последний корректный sample сохраняется при сбое, но подписан «Последнее live-измерение · поток недоступен»; история при отсутствии sample помечена отдельно, включая ожидание live.
- 3 E2E прошли: штатное завершение потока, повреждённое событие после корректного sample, HTTP 503. Неправильное событие не заменяет последнее корректное измерение. Lint теста чистый; сравнение с HEAD подтвердило прежние замечания traffic (3 ошибки/1 предупреждение), hook (1 ошибка). Production build с TypeScript прошёл, 34 страницы.
- Коммит отложился до продолжения после сброса квоты: в предыдущем окне она достигла 100%, сборка уже была запущена и завершилась успешно. На продолжении доступно 99% пятичасового окна. Локальный коммит без push.
- Далее 8.3Б: гонки чтения истории/деталей при смене сервера, интерфейса и периода; API-ошибка не должна оставлять данные другого контекста. Автоматическое переподключение SSE, парсер CRLF, useFlowLive и реальный IPFIX остаются непроверенными. Этап 8 целиком не завершён.
## Блок 8.2Б — URL-контекст статистики (2026-09-23)
- Проверяются дубли/пустые значения, dim/tab, view, planes, измерения pivot, ID сервера, ASN и длина кода страны. Неверный срез блокирует запрос, а не удаляется из API-query с расширением отчёта. Совпадающие dim/tab поддержаны; смена dim удаляет устаревший tab. Сброс удаляет параметры контекста, сохраняя from/to, чужие ключи и hash.
+9 -1
View File
@@ -55,6 +55,7 @@ export function useTrafficLive(opts: {
signal: ac.signal,
credentials: "include",
})
if (ac.signal.aborted) return
if (!res.ok || !res.body) {
setError(`live HTTP ${res.status}`)
return
@@ -63,7 +64,11 @@ export function useTrafficLive(opts: {
const decoder = new TextDecoder()
while (!ac.signal.aborted) {
const { done, value } = await reader.read()
if (done) break
if (ac.signal.aborted) return
if (done) {
setError("Поток live завершён")
break
}
buf += decoder.decode(value, { stream: true })
const parts = buf.split("\n\n")
buf = parts.pop() ?? ""
@@ -72,6 +77,9 @@ export function useTrafficLive(opts: {
const ev = parseSseBlock(raw)
if (ev.event === "sample" && ev.data) {
const parsed = JSON.parse(ev.data) as TrafficLiveSample
if (!parsed || !Number.isFinite(parsed.rxMbps) || !Number.isFinite(parsed.txMbps) || parsed.rxMbps < 0 || parsed.txMbps < 0 || typeof parsed.at !== "string" || !Number.isFinite(Date.parse(parsed.at))) {
throw new Error("Некорректные данные live-потока")
}
setSample(parsed)
setError(null)
} else if (ev.event === "error" && ev.data) {
+34
View File
@@ -0,0 +1,34 @@
import { test, expect } from '@playwright/test'
for (const variant of ['closed', 'invalid', 'http-error'] as const) {
test(`Трафик: SSE ${variant} не выдаёт старое измерение за live`, async ({ page }) => {
const server = { id: '1', name: 'Тестовый сервер', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, rxPeak: 5, txPeak: 6, rxTotal: 100, txTotal: 200, sessions: 1, rxSeries: [1, 2], txSeries: [2, 3] }
await page.route('**/health', route => route.fulfill({ json: { ok: true } }))
await page.route('**/api/**', route => {
const path = new URL(route.request().url()).pathname
if (path.endsWith('/live')) {
if (variant === 'http-error') return route.fulfill({ status: 503, json: { error: 'Поток недоступен' } })
const sample = { rxMbps: 17, txMbps: 19, at: '2026-09-23T12:00:00Z' }
const body = 'event: sample\ndata: ' + JSON.stringify(sample) + '\n\n' + (variant === 'invalid' ? 'event: sample\ndata: {"rxMbps":null,"txMbps":-1}\n\n' : '')
return route.fulfill({ contentType: 'text/event-stream', body })
}
const json = path === '/api/auth/config' ? { required: false }
: path === '/api/traffic/servers' ? { servers: [server] }
: path === '/api/traffic/servers/1' ? { server }
: path.endsWith('/interfaces') || path === '/api/traffic/bound-interfaces' ? { interfaces: [] }
: path === '/api/traffic/users' ? { users: [] }
: path === '/api/servers' ? [] : {}
return route.fulfill({ json })
})
await page.goto('/traffic')
const summary = page.getByLabel('Скорость выбранного сервера')
await expect(summary).toBeVisible()
const hint = variant === 'http-error' ? 'История · live недоступен' : 'Последнее live-измерение · поток недоступен'
await expect(summary.getByText(hint, { exact: true }).filter({ visible: true })).toHaveCount(2)
await expect(summary.getByText('live', { exact: true })).toHaveCount(0)
if (variant !== 'http-error') {
await expect(summary).toContainText('17.0')
await expect(summary).toContainText('19.0')
}
})
}