diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md
index f434e99b..7dcacdde 100644
--- a/docs/ux-ui-progress.md
+++ b/docs/ux-ui-progress.md
@@ -29,6 +29,13 @@
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
+## Блок 8.4Ж — история в карточках трафика (2026-09-24)
+
+- Две реализации MiniAreaChart заменены общим TrafficMiniChart. Пустые/полностью невалидные серии показывают «Нет истории трафика»; одна точка не создаёт NaN; нулевой TX не скрывается. Координаты RX/TX используют общую длину временной шкалы. Невалидные точки разделяют линии и заливку на сегменты.
+- 31 E2E деталей/flow/мобильных экранов прошёл. Новый сценарий проверяет пустую историю, одну точку, нулевой TX, разрыв RX и отсутствие NaN/Infinity в SVG. Проверены существующие ширины 320/390/1440.
+- Lint нового компонента, flow-панели и теста чистый; diff-check, production build/TypeScript прошли (34 страницы). На странице остаётся ранее известная ошибка lint flowIface. Использовано 42% пятичасового и 84% недельного лимита; локальный коммит без push.
+- Далее: общий Sparkline в итогах (его обработка разрывов не изменена этим блоком), свежесть трафика, проверки смены backend/сервера и позднего SSE, приёмка этапа 8. Реальные IPFIX/RouterOS не проверены.
+
## Блок 8.4Е — отдельные отсутствующие направления RX/TX (2026-09-24)
- Общий график поясняет отсутствие RX или TX, если второе направление содержит измерения. Линия отсутствующего направления не создаётся; подсказка содержит только доступное направление.
diff --git a/tests/e2e/traffic-details.spec.ts b/tests/e2e/traffic-details.spec.ts
index bbbcba2e..06c56c96 100644
--- a/tests/e2e/traffic-details.spec.ts
+++ b/tests/e2e/traffic-details.spec.ts
@@ -15,7 +15,7 @@ async function setup(page: Page) {
return route.fulfill({ json: { server: { ...server, rxSeries: control.series ?? server.rxSeries, txSeries: control.txSeries ?? control.series ?? server.txSeries, name: `Детали ${range} ${iface}`, rxNow: range === '24h' ? 24 : 15 } } })
}
const json = path === '/api/auth/config' ? { required: false }
- : path === '/api/traffic/servers' ? { servers: [server] }
+ : path === '/api/traffic/servers' ? { servers: [{ ...server, rxSeries: control.series ?? server.rxSeries, txSeries: control.txSeries ?? control.series ?? server.txSeries }] }
: path === '/api/traffic/servers/1/interfaces' ? { interfaces: [{ name: 'ether2', running: true, disabled: false, rxNow: 0, txNow: 0 }] }
: path.endsWith('/interfaces') || path === '/api/traffic/bound-interfaces' ? { interfaces: [] }
: path === '/api/traffic/users' ? { users: [] }
@@ -104,3 +104,23 @@ for (const missing of ['RX', 'TX'] as const) {
await expect(chart.getByText((missing === 'RX' ? 'TX' : 'RX') + ':', { exact: true })).toBeVisible()
})
}
+
+
+test('Карточка трафика: пустая история, одна точка, нулевой TX и разрыв', async ({ page }) => {
+ const control = await setup(page)
+ const card = page.getByRole('button').filter({ hasText: 'Первый сервер' })
+ control.series = []
+ control.txSeries = []
+ await page.getByRole('button', { name: '24ч', exact: true }).click()
+ await expect(card).toContainText('Нет истории трафика')
+ control.series = [2]
+ await page.getByRole('button', { name: '15м', exact: true }).click()
+ await expect(card.locator('circle')).toHaveCount(1)
+ await expect(card.locator('circle')).toHaveAttribute('cx', '150')
+ control.series = [2, 3, -1, 4, 5]
+ control.txSeries = [0, 0, 0, 0, 0]
+ await page.getByRole('button', { name: '4ч', exact: true }).click()
+ await expect(card.locator('[data-direction="rx"] polyline')).toHaveCount(2)
+ await expect(card.locator('[data-direction="tx"] polyline')).toHaveCount(1)
+ expect(await card.locator('svg').last().innerHTML()).not.toMatch(/NaN|Infinity/)
+})