fix(ui): исключить подмену выбранного среза в сводке трафика
This commit is contained in:
1 parent
c6d146d38f
commit
5734340db2
5 files changed
+40
-19
No files matched your search
+12
-13
@@ -13,7 +13,7 @@ import { Sparkline } from "@/components/sparkline"
|
|||||||
import { StatusDot } from "@/components/status-dot"
|
import { StatusDot } from "@/components/status-dot"
|
||||||
import { Flag } from "@/components/flag"
|
import { Flag } from "@/components/flag"
|
||||||
import {
|
import {
|
||||||
RefreshCwIcon, DownloadIcon, TrendingUpIcon, TrendingDownIcon,
|
RefreshCwIcon, TrendingUpIcon, TrendingDownIcon,
|
||||||
ArrowUpIcon, ArrowDownIcon, ActivityIcon, UsersIcon, CableIcon, ServerIcon, SearchIcon, GitBranchIcon, PlusIcon,
|
ArrowUpIcon, ArrowDownIcon, ActivityIcon, UsersIcon, CableIcon, ServerIcon, SearchIcon, GitBranchIcon, PlusIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@@ -590,7 +590,7 @@ function ServerDetail({
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
id: "rx-now",
|
id: "rx-now",
|
||||||
label: "RX сейчас",
|
label: "RX",
|
||||||
value: fmtRate(rxNow),
|
value: fmtRate(rxNow),
|
||||||
hint: liveHint,
|
hint: liveHint,
|
||||||
icon: <ArrowDownIcon className="size-4" />,
|
icon: <ArrowDownIcon className="size-4" />,
|
||||||
@@ -598,7 +598,7 @@ function ServerDetail({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "tx-now",
|
id: "tx-now",
|
||||||
label: "TX сейчас",
|
label: "TX",
|
||||||
value: fmtRate(txNow),
|
value: fmtRate(txNow),
|
||||||
hint: liveHint,
|
hint: liveHint,
|
||||||
icon: <ArrowUpIcon className="size-4" />,
|
icon: <ArrowUpIcon className="size-4" />,
|
||||||
@@ -655,8 +655,8 @@ function UserDetail({ sel, range, setRange }: { sel: UserTraffic; range: Range;
|
|||||||
<>
|
<>
|
||||||
<TrafficRxTxChart rx={sel.rxSeries} tx={sel.txSeries} range={range} />
|
<TrafficRxTxChart rx={sel.rxSeries} tx={sel.txSeries} range={range} />
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-4 pt-4 border-t">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-4 pt-4 border-t">
|
||||||
<StatChip icon={<ArrowDownIcon className="size-3.5 text-emerald-500" />} label="RX сейчас" value={fmtRate(sel.rxNow)} />
|
<StatChip icon={<ArrowDownIcon className="size-3.5 text-emerald-500" />} label="RX" value={fmtRate(sel.rxNow)} />
|
||||||
<StatChip icon={<ArrowUpIcon className="size-3.5 text-blue-500" />} label="TX сейчас" value={fmtRate(sel.txNow)} />
|
<StatChip icon={<ArrowUpIcon className="size-3.5 text-blue-500" />} label="TX" value={fmtRate(sel.txNow)} />
|
||||||
<StatChip icon={<TrendingUpIcon className="size-3.5 text-amber-500" />} label="Пик RX" value={fmtRate(sel.rxPeak)} />
|
<StatChip icon={<TrendingUpIcon className="size-3.5 text-amber-500" />} label="Пик RX" value={fmtRate(sel.rxPeak)} />
|
||||||
<StatChip icon={<CableIcon className="size-3.5 text-purple-500" />} label="Интерфейсы" value={`${sel.interfaces.length}`} />
|
<StatChip icon={<CableIcon className="size-3.5 text-purple-500" />} label="Интерфейсы" value={`${sel.interfaces.length}`} />
|
||||||
</div>
|
</div>
|
||||||
@@ -691,8 +691,8 @@ function IfaceDetail({ sel, range, setRange }: { sel: BoundIfaceTraffic; range:
|
|||||||
</DetailHeader>
|
</DetailHeader>
|
||||||
{sel.status === "offline" ? <OfflinePlaceholder /> : <TrafficRxTxChart rx={sel.rxSeries} tx={sel.txSeries} range={range} />}
|
{sel.status === "offline" ? <OfflinePlaceholder /> : <TrafficRxTxChart rx={sel.rxSeries} tx={sel.txSeries} range={range} />}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-4 pt-4 border-t">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-4 pt-4 border-t">
|
||||||
<StatChip icon={<ArrowDownIcon className="size-3.5 text-emerald-500" />} label="RX сейчас" value={fmtRate(sel.rxNow)} />
|
<StatChip icon={<ArrowDownIcon className="size-3.5 text-emerald-500" />} label="RX" value={fmtRate(sel.rxNow)} />
|
||||||
<StatChip icon={<ArrowUpIcon className="size-3.5 text-blue-500" />} label="TX сейчас" value={fmtRate(sel.txNow)} />
|
<StatChip icon={<ArrowUpIcon className="size-3.5 text-blue-500" />} label="TX" value={fmtRate(sel.txNow)} />
|
||||||
<StatChip icon={<TrendingUpIcon className="size-3.5 text-amber-500" />} label="Пик RX" value={fmtRate(sel.rxPeak)} />
|
<StatChip icon={<TrendingUpIcon className="size-3.5 text-amber-500" />} label="Пик RX" value={fmtRate(sel.rxPeak)} />
|
||||||
<StatChip icon={<TrendingUpIcon className="size-3.5 text-amber-500" />} label="Пик TX" value={fmtRate(sel.txPeak)} />
|
<StatChip icon={<TrendingUpIcon className="size-3.5 text-amber-500" />} label="Пик TX" value={fmtRate(sel.txPeak)} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1084,7 +1084,7 @@ export default function TrafficPage() {
|
|||||||
{
|
{
|
||||||
id: "bps",
|
id: "bps",
|
||||||
label: "Скорость",
|
label: "Скорость",
|
||||||
value: displayedFlow ? fmtRate(displayedFlow.bpsNow / 1_000_000) : (flowStats ? fmtRate((flowStats.bytesPerMin * 8) / 1_000_000) : "—"),
|
value: displayedFlow ? fmtRate(displayedFlow.bpsNow / 1_000_000) : "—",
|
||||||
hint: flowLiveHint,
|
hint: flowLiveHint,
|
||||||
icon: <ActivityIcon className="size-4" />,
|
icon: <ActivityIcon className="size-4" />,
|
||||||
iconClassName: "text-success",
|
iconClassName: "text-success",
|
||||||
@@ -1092,14 +1092,14 @@ export default function TrafficPage() {
|
|||||||
{
|
{
|
||||||
id: "src",
|
id: "src",
|
||||||
label: "Уник. src",
|
label: "Уник. src",
|
||||||
value: String(displayedFlow?.uniqueSrc ?? flowStats?.uniqueSrc ?? 0),
|
value: displayedFlow ? String(displayedFlow.uniqueSrc) : "—",
|
||||||
icon: <ArrowUpIcon className="size-4" />,
|
icon: <ArrowUpIcon className="size-4" />,
|
||||||
iconClassName: "text-muted-foreground",
|
iconClassName: "text-muted-foreground",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "proto",
|
id: "proto",
|
||||||
label: "Топ протокол",
|
label: "Топ протокол",
|
||||||
value: displayedFlow?.topProto ?? flowStats?.topProto ?? "—",
|
value: displayedFlow?.topProto ?? "—",
|
||||||
icon: <GitBranchIcon className="size-4" />,
|
icon: <GitBranchIcon className="size-4" />,
|
||||||
iconClassName: "text-warning",
|
iconClassName: "text-warning",
|
||||||
},
|
},
|
||||||
@@ -1108,14 +1108,14 @@ export default function TrafficPage() {
|
|||||||
const counterKpiItems = [
|
const counterKpiItems = [
|
||||||
{
|
{
|
||||||
id: "rx",
|
id: "rx",
|
||||||
label: "RX сейчас",
|
label: "RX",
|
||||||
value: fmtRate(totalRx),
|
value: fmtRate(totalRx),
|
||||||
icon: <ArrowDownIcon className="size-4" />,
|
icon: <ArrowDownIcon className="size-4" />,
|
||||||
iconClassName: "text-success",
|
iconClassName: "text-success",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "tx",
|
id: "tx",
|
||||||
label: "TX сейчас",
|
label: "TX",
|
||||||
value: fmtRate(totalTx),
|
value: fmtRate(totalTx),
|
||||||
icon: <ArrowUpIcon className="size-4" />,
|
icon: <ArrowUpIcon className="size-4" />,
|
||||||
iconClassName: "text-info",
|
iconClassName: "text-info",
|
||||||
@@ -1153,7 +1153,6 @@ export default function TrafficPage() {
|
|||||||
>
|
>
|
||||||
<RefreshCwIcon className={cn("size-4", isLive && (effectiveMode === "flows" ? liveBusy : trafficLoading) && "animate-spin")} />Обновить
|
<RefreshCwIcon className={cn("size-4", isLive && (effectiveMode === "flows" ? liveBusy : trafficLoading) && "animate-spin")} />Обновить
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm"><DownloadIcon className="size-4" />Экспорт</Button>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+4
-5
@@ -84,14 +84,13 @@ flowchart LR
|
|||||||
|
|
||||||
Подробные планы: [этап 6](ux-ui-stage-6.md), [этап 7](ux-ui-stage-7.md), [этап 8](ux-ui-stage-8.md).
|
Подробные планы: [этап 6](ux-ui-stage-6.md), [этап 7](ux-ui-stage-7.md), [этап 8](ux-ui-stage-8.md).
|
||||||
|
|
||||||
Для этапа 8 уже выполнены блоки периодов/URL статистики, ошибок и гонок трафика, месячного отчёта, адаптивности, неизвестных метрик, пустой истории и разрывов графиков. Проверены смена flow-сервера, отмена SSE до ответа, позднее чтение открытого SSE на управляемом ReadableStream и смена backend через настройки. Точный состав тестов и оговорки — в журнале, блоки 8.1–8.4П. В flow и остальных видах трафика добавлены отметки получения API/SSE; для серверного SSE отдельно показана метка обработки ответа сервером. Далее — сводная приёмка этапа 8.
|
Для этапа 8 уже выполнены блоки периодов/URL статистики, ошибок и гонок трафика, месячного отчёта, адаптивности, неизвестных метрик, пустой истории и разрывов графиков. Проверены смена flow-сервера, отмена SSE до ответа, позднее чтение открытого SSE на управляемом ReadableStream и смена backend через настройки. Точный состав тестов и оговорки — в журнале, блоки 8.1–8.4П. В flow и остальных видах трафика добавлены отметки получения API/SSE; для серверного SSE отдельно показана метка обработки ответа сервером. Выполнена промежуточная приёмка 8.5; этап ещё открыт.
|
||||||
|
|
||||||
Ближайший оставшийся объём:
|
Ближайший оставшийся объём:
|
||||||
|
|
||||||
1. Задержанное чтение открытого SSE проверено в блоке 8.4Н; продолжить со свежести данных ниже.
|
1. Обработка молчащего открытого SSE: последнее значение/время, прекращение отметки live и возобновление после нового sample.
|
||||||
2. Свежесть данных трафика: ясное различие времени получения, истории и текущего live-измерения.
|
2. Итоговая приёмка этапа 8: решение по архивным серверам/месяцам, проверка CRLF/heartbeat и единичной точки графика, ReUI/accessibility checklist. Промежуточная матрица проверок сохранена в [этапе 8](ux-ui-stage-8.md).
|
||||||
3. Сводная приёмка этапа 8 и оставшихся ограничений (включая выбор архивных серверов месячного отчёта), профильная регрессия и ReUI checklist.
|
3. После фиксации результата этапа 8 — этап 9.
|
||||||
4. После фиксации результата этапа 8 — этап 9.
|
|
||||||
|
|
||||||
## Порядок продолжения и журналирования
|
## Порядок продолжения и журналирования
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,20 @@
|
|||||||
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
||||||
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||||
| 8 | Трафик и статистика | В работе: выполнены блоки 8.1–8.4П; далее сводная приёмка этапа 8 |
|
| 8 | Трафик и статистика | В работе: выполнены блоки 8.1–8.5; промежуточная приёмка пройдена, далее молчащий SSE |
|
||||||
| 9 | VPN, Firewall и Filters | Не начат |
|
| 9 | VPN, Firewall и Filters | Не начат |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
## Блок 8.5 — промежуточная приёмка и достоверность сводки (2026-09-28)
|
||||||
|
|
||||||
|
- В flow-сводке устранена подмена выбранного среза общими метриками коллектора при недоступной аналитике. Удалён ошибочный пересчёт bytesPerMin в бит/с без деления на 60. Скорость/источники/протокол показывают «—» до подтверждения аналитики; доступное число экспортёров сохраняется. Ошибка и успешный повтор покрыты E2E.
|
||||||
|
- Убрана неработающая кнопка экспорта трафика; экспорт не объявлен реализованным. Подписи RX/TX не обещают «сейчас» для исторических/последних значений.
|
||||||
|
- Прошли 53 E2E трафика/статистики, 3 отдельных сценария смены backend, 10 модульных тестов периодов/контекста/фильтров статистики. ESLint изменённых файлов, diff-check и production build/TypeScript прошли (34 страницы). Это регрессия этапа 8, не всего проекта. Осмотрен flow-экран 390px; адаптивные сценарии 320/390/1440 прошли.
|
||||||
|
- Матрица приёмки и ограничения сохранены в ux-ui-stage-8.md, ближайший план обновлён. Этап не закрыт: следующий шаг — молчащий открытый SSE (снять live, сохранить данные/время, проверить восстановление и смену контекста). Далее — месячные ограничения и оставшийся ReUI/accessibility аудит. Реальный RouterOS/IPFIX/JWT не проверен. Использовано 71% пятичасового и 11% недельного лимита. Локальный коммит без push.
|
||||||
|
|
||||||
## Блок 8.4П — временные отметки остальных видов трафика (2026-09-28)
|
## Блок 8.4П — временные отметки остальных видов трафика (2026-09-28)
|
||||||
|
|
||||||
- Общий REST-список серверов/клиентов/интерфейсов и история выбранного сервера показывают время успешного получения. При загрузке другого контекста или ошибке скрытые данные не оставляют прежнюю отметку.
|
- Общий REST-список серверов/клиентов/интерфейсов и история выбранного сервера показывают время успешного получения. При загрузке другого контекста или ошибке скрытые данные не оставляют прежнюю отметку.
|
||||||
|
|||||||
@@ -26,3 +26,18 @@
|
|||||||
- readRange сравнивает строки без строгой проверки даты; parseYmd нормализует несуществующие дни через JavaScript Date. Проверить в 8.2.
|
- readRange сравнивает строки без строгой проверки даты; parseYmd нормализует несуществующие дни через JavaScript Date. Проверить в 8.2.
|
||||||
- useTrafficLive требует отдельной проверки завершения SSE и позднего ответа после abort в 8.3; это пока риск, а не доказанный дефект UI.
|
- useTrafficLive требует отдельной проверки завершения SSE и позднего ответа после abort в 8.3; это пока риск, а не доказанный дефект UI.
|
||||||
- Реальный IPFIX/RouterOS недоступен локально. HTTP/SSE-моки не доказывают сбор и точность сетевой телеметрии.
|
- Реальный IPFIX/RouterOS недоступен локально. HTTP/SSE-моки не доказывают сбор и точность сетевой телеметрии.
|
||||||
|
|
||||||
|
## Промежуточная приёмка 2026-09-28 (блок 8.5)
|
||||||
|
|
||||||
|
| Проверка | Результат и границы |
|
||||||
|
| --- | --- |
|
||||||
|
| Периоды, URL и срезы статистики | 12 браузерных и 10 модульных проверок прошли; ошибки и некорректные параметры не становятся пустым/расширенным отчётом |
|
||||||
|
| Трафик: REST, SSE, гонки и время получения | 41 браузерный сценарий прошёл: детали, списки, flow, разрывы графиков, поздние ответы, ошибки, временные подписи |
|
||||||
|
| Смена backend | 3 сценария отдельной конфигурации прошли (трафик и два Ping); переход через настройки размонтирует экран |
|
||||||
|
| ReUI и адаптивность | Сохранены Frame/KpiStatGrid/DataGrid; профильные проверки 320/390/1440 проходят; осмотрен месячный flow на 390px. Полный аудит клавиатуры/скринридера ещё не выполнен |
|
||||||
|
| Недостоверная сводка flow | Убрана подстановка общих метрик коллектора вместо выбранного среза при ошибке/загрузке. Она также неверно переводила bytesPerMin в бит/с без деления на 60. Теперь три метрики среза показывают «—», доступное количество экспортёров сохраняется |
|
||||||
|
| Действия и подписи | Неработающая кнопка «Экспорт» удалена. RX/TX больше не называются «сейчас», поскольку данные могут быть историческими; источник и временные отметки остаются рядом |
|
||||||
|
|
||||||
|
Этап 8 пока не закрыт. Следующий блок — обработка молчащего открытого SSE: после прекращения валидных sample нельзя бесконечно показывать live; сохранить последнее значение/время, обозначить отсутствие обновлений, проверить возобновление и смену контекста. Порог определить по фактической частоте backend и проверить управляемыми часами.
|
||||||
|
|
||||||
|
После этого требуется итоговое решение по месячному отчёту: текущий список экспортёров охватывает только 24 часа, архивные серверы и прошлые месяцы недоступны. Экспорт трафика не реализован. Проверку CRLF/heartbeat и единичной точки основного графика включить в оставшийся аудит, затем закончить ReUI/accessibility checklist. Реальные RouterOS, JWT и IPFIX не проверены локальными HTTP/SSE-моками.
|
||||||
@@ -53,6 +53,7 @@ test('Flow: ошибка аналитики и повтор сохраняют
|
|||||||
await page.getByRole('button', { name: '15м', exact: true }).click()
|
await page.getByRole('button', { name: '15м', exact: true }).click()
|
||||||
await expect(page.getByRole('alert').filter({ hasText: 'Аналитика недоступна' })).toContainText('Срез временно недоступен')
|
await expect(page.getByRole('alert').filter({ hasText: 'Аналитика недоступна' })).toContainText('Срез временно недоступен')
|
||||||
await expect(page.getByLabel('Скорость потоков')).toHaveCount(0)
|
await expect(page.getByLabel('Скорость потоков')).toHaveCount(0)
|
||||||
|
await expect(page.getByLabel('Сводка трафика').getByText('—', { exact: true })).toHaveCount(3)
|
||||||
control.fail = false
|
control.fail = false
|
||||||
await page.getByRole('button', { name: 'Повторить аналитику', exact: true }).click()
|
await page.getByRole('button', { name: 'Повторить аналитику', exact: true }).click()
|
||||||
await expect(page.getByText('Категория 15m', { exact: true }).filter({ visible: true })).toBeVisible()
|
await expect(page.getByText('Категория 15m', { exact: true }).filter({ visible: true })).toBeVisible()
|
||||||
|
|||||||
Reference in new issue
Block a user