+ {isLoading && (
+
+ Загрузка телеметрии...
+
+ )}
+
+ {isError && (
+
+ Не удалось загрузить телеметрию
+
+ )}
+
+
+
+
События
+
Хронология активности устройства за выбранный период
+
+
+
+ {EVENT_CATEGORIES.map((category) => {
+ const checked = enabledCategories.includes(category.key)
+
+ return (
+
toggleCategory(category.key)}
+ >
+
+ {checked && (
+ Check SVG Icon
+ )}
+
+
+ {category.label}
+
+ )
+ })}
+
+
+
+
Температура
+
Динамика температуры устройства за выбранный период
+
+
+
+
Аккумулятор
+
Динамика уровня заряда за выбранный период
+
+
+
Приложения
+
События жизненного цикла приложений за выбранный период
+
+
+
+
+
+ На экране
+
+
+
+
+ Фон
+
+
+
+ {enabledAppRows.map((row, index) => {
+ const checked = enabledPackagesSet.has(row.packageName)
+ const { Icon, modifier } = getAppIcon(row)
+ const top =
+ APP_GRID_TOP +
+ appRowBand * index +
+ appRowBand / 2 -
+ APP_ROW_HEIGHT / 2
+
+ return (
+
togglePackage(row.packageName)}
+ >
+
+ {row.app?.iconUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {row.name}
+
+ {formatDuration(row.activeDuration)} · {formatUsagePercent(row.activePercent)}
+
+
+
+ )
+ })}
+
+ {!enabledAppRows.length && !isLoading && (
+
+
+ {appRows.length
+ ? 'Приложения не выбраны'
+ : 'Нет событий по приложениям'}
+
+
+
+ {appRows.length
+ ? 'Выберите приложения, которые нужно показать на графике.'
+ : 'За выбранный период не найдено событий жизненного цикла приложений.'}
+
+
+ )}
+
+ {appRows.length > 0 && (
+
+
+ Выбрано {enabledAppRows.length} из {appRows.length}
+
+
+
+
+ {appRows.map((row) => {
+ const checked = enabledPackagesSet.has(row.packageName)
+
+ return (
+ togglePackage(row.packageName)}
+ >
+ {row.name}
+
+ {formatDuration(row.activeDuration)}
+
+
+ )
+ })}
+
+
+
+ )}
+
+ )
+})
diff --git a/frontend/src/pages/DevicePage/components/DeviceHistoryModal/DeviceHistoryModal.scss b/frontend/src/pages/DevicePage/components/DeviceHistoryModal/DeviceHistoryModal.scss
new file mode 100644
index 0000000..9b7fe59
--- /dev/null
+++ b/frontend/src/pages/DevicePage/components/DeviceHistoryModal/DeviceHistoryModal.scss
@@ -0,0 +1,645 @@
+@use '../../../../shared/styles/variables' as *;
+
+.device-history-modal__overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 500;
+ background: $color-backdrop-soft;
+}
+
+.device-history-modal {
+ position: fixed;
+ z-index: 600;
+ inset: 24px;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ max-height: calc(100vh - 48px);
+
+ border-radius: 24px;
+ background: $color-surface;
+ padding: 20px;
+
+ //overflow: hidden;
+
+ box-shadow: 0 24px 80px rgba($color-shadow-rgb, 0.22);
+}
+
+.device-history-modal__scroll {
+ flex: 1 1 auto;
+ height: 100%;
+ min-height: 0;
+
+ .simplebar-content {
+ min-height: 100%;
+ }
+
+ .simplebar-track.simplebar-vertical {
+ top: 14px;
+ right: -18px;
+ bottom: 14px;
+ width: 8px;
+ }
+
+ .simplebar-scrollbar::before {
+ background: $gray50;
+ border-radius: 4px;
+ opacity: 1;
+ }
+}
+
+.device-history-modal__chart-placeholder {
+ min-height: 720px;
+ padding-top: 220px;
+
+ color: $color-text-muted;
+ font-size: $font-size-14;
+ font-weight: 600;
+ text-align: center;
+}
+
+.device-history-modal__header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 24px;
+ margin-bottom: 14px;
+ .device-history-modal__title{
+ padding-left: 18px;
+ padding-top: 18px;
+ }
+ .device-history-modal__meta{
+ padding-left: 18px;
+ }
+}
+
+.device-history-modal {
+ .history-period-control__top {
+ //padding-left: 18px;
+ padding-bottom: 18px;
+ }
+}
+
+.device-history-chart__zoom {
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ right: 20px;
+ z-index: 50;
+
+ padding: 8px;
+ //margin: 0 64px;
+ //border-bottom-right-radius: 20px;
+ //border-bottom-left-radius: 20px;
+ border-radius: 20px;
+
+ background: rgba($white, 0.3);
+ backdrop-filter: blur(12px);
+ box-shadow: 0 0 40px -6px rgba($gray50-rgb, .3);
+ .sticky-line{
+ display: flex;
+ flex: 1;
+ height: 1px;
+ background-color: $gray20;
+ margin-top: 26px;
+ margin-right: 90px;
+
+ }
+}
+
+.device-history-modal__title {
+ margin: 0;
+
+ color: $color-text-strong;
+ font-size: $font-size-20;
+ font-weight: 650;
+ text-transform: uppercase;
+
+ span {
+ margin-left: 6px;
+ color: $gray50;
+ font-size: $font-size-14;
+ font-weight: 500;
+ text-transform: none;
+ }
+}
+
+.device-history-modal__meta {
+ margin-top: 4px;
+
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 14px;
+
+ color: $gray50;
+ font-size: $font-size-14;
+
+ .is-green {
+ position: relative;
+ padding-left: 20px;
+ padding-right: 8px;
+ border-radius: 8px;
+ color: $green;
+ font-weight: 500;
+ font-size: $font-size-16;
+ background-color: rgba($color-success-rgb, .1);
+ &::before {
+ content: '';
+ position: absolute;
+ left: 8px;
+ top: 50%;
+
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+
+ background: $green;
+ transform: translateY(-50%);
+ }
+ }
+ .is-red {
+ position: relative;
+ padding-left: 20px;
+ padding-right: 8px;
+ border-radius: 8px;
+ color: $red;
+ font-weight: 500;
+ font-size: $font-size-16;
+ background-color: rgba($color-danger-rgb, .1);
+ &::before {
+ content: '';
+ position: absolute;
+ left: 8px;
+ top: 50%;
+
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+
+ background: $red;
+ transform: translateY(-50%);
+ }
+ }
+ .is-gray {
+ position: relative;
+ padding-left: 20px;
+ padding-right: 8px;
+ border-radius: 8px;
+ color: $gray50;
+ font-weight: 500;
+ font-size: $font-size-16;
+ background-color: rgba($gray50-rgb, .1);
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 8px;
+ top: 50%;
+
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+
+ background: $gray50;
+ transform: translateY(-50%);
+ }
+ }
+}
+
+.device-history-modal__close {
+ width: 36px;
+ height: 36px;
+ border: none;
+ border-radius: 12px;
+ background: $transparent;
+
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+
+ color: $color-text-strong;
+ cursor: pointer;
+
+ &:hover {
+ background: $gray20;
+ color: $blue;
+ }
+}
+
+.device-history-controls {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 18px;
+}
+
+.device-history-tabs {
+ padding: 4px;
+ border-radius: 999px;
+ background: $color-bg;
+
+ display: flex;
+ align-items: center;
+ gap: 2px;
+}
+
+.device-history-tabs__item {
+ height: 34px;
+ padding: 0 16px;
+
+ border: none;
+ border-radius: 999px;
+ background: $transparent;
+
+ color: $gray50;
+ font-size: $font-size-14;
+ font-weight: 500;
+ cursor: pointer;
+
+ &.is-active {
+ background: $color-surface;
+ color: $blue;
+ box-shadow: 0 2px 8px rgba($color-shadow-rgb, 0.08);
+ }
+}
+
+.device-history-period {
+ height: 42px;
+ padding: 0 16px;
+
+ border: none;
+ border-radius: 999px;
+ background: $color-bg;
+
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+
+ color: $color-text-heading;
+ font-size: $font-size-14;
+ cursor: pointer;
+
+ svg {
+ color: $blue;
+ }
+}
+
+.device-history-range {
+ max-width: 1280px;
+ margin: 0 100px 22px;
+}
+
+.device-history-range__labels {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 8px;
+
+ color: $blue;
+ font-size: $font-size-14;
+}
+
+.device-history-range__track {
+ position: relative;
+ height: 22px;
+}
+
+.device-history-range__active {
+ position: absolute;
+ left: 0;
+ top: 9px;
+ height: 4px;
+ border-radius: 999px;
+ background: $blue;
+}
+
+.device-history-range__track input {
+ position: absolute;
+ inset: 0;
+
+ width: 100%;
+ margin: 0;
+
+ appearance: none;
+ background: $transparent;
+ pointer-events: none;
+}
+
+.device-history-range__track input::-webkit-slider-thumb {
+ appearance: none;
+ pointer-events: auto;
+
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ border: none;
+
+ background: $blue;
+ cursor: pointer;
+}
+
+.device-history-modal__content {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.device-history-timeline {
+ max-width: 1280px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.history-section {
+ margin-bottom: 28px;
+
+ h3 {
+ margin: 0 0 8px 100px;
+ color: $color-text-heading;
+ font-size: $font-size-16;
+ font-weight: 600;
+ }
+}
+
+.history-grid {
+ position: relative;
+}
+
+.history-grid__axis {
+ margin-left: 100px;
+ height: 20px;
+
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+
+ color: $gray50;
+ font-size: $font-size-13;
+
+ span {
+ transform: translateX(-50%);
+
+ &:first-child {
+ transform: translateX(-25%);
+ }
+
+ &:last-child {
+ text-align: right;
+ transform: translateX(0);
+ }
+ }
+}
+
+.history-row {
+ display: grid;
+ grid-template-columns: 100px 1fr;
+ min-height: 28px;
+}
+
+.history-row__label {
+ padding-right: 12px;
+ text-align: right;
+
+ color: $gray50;
+ font-size: $font-size-14;
+}
+
+.history-row__line {
+ position: relative;
+ border-top: 1px solid $color-border-muted;
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ top: -1px;
+ bottom: 0;
+ width: 1px;
+ background: $color-border-muted;
+ }
+
+ &::before {
+ left: 25%;
+ }
+
+ &::after {
+ left: 75%;
+ }
+}
+
+.history-segment {
+ position: absolute;
+ top: 9px;
+ height: 7px;
+ border-radius: 999px;
+}
+
+.history-segment--blue {
+ background: $blue;
+}
+
+.history-segment--purple {
+ background: $accent-purple;
+}
+
+.history-segment--green {
+ background: $accent-teal;
+}
+
+.history-segment--light-blue {
+ background: $accent-sky;
+}
+
+.history-point {
+ position: absolute;
+ top: -5px;
+
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+
+ transform: translateX(-50%);
+}
+
+.history-point--red {
+ background: $red;
+}
+
+.history-point--orange {
+ background: $color-warning-strong;
+}
+
+.battery-chart {
+ position: relative;
+ height: 110px;
+ margin-left: 100px;
+ border-bottom: 1px solid $color-border-muted;
+
+ background:
+ linear-gradient(to bottom, $transparent 19%, $color-border-muted 20%, $transparent 21%),
+ linear-gradient(to bottom, $transparent 39%, $color-border-muted 40%, $transparent 41%),
+ linear-gradient(to bottom, $transparent 59%, $color-border-muted 60%, $transparent 61%),
+ linear-gradient(to bottom, $transparent 79%, $color-border-muted 80%, $transparent 81%);
+}
+
+.battery-chart__area {
+ position: absolute;
+ inset: 0;
+
+ background: linear-gradient(
+ 168deg,
+ rgba($color-success-strong-rgb, 0.38) 0%,
+ rgba($color-success-strong-rgb, 0.28) 45%,
+ rgba($color-success-strong-rgb, 0.05) 46%,
+ rgba($color-success-strong-rgb, 0.05) 100%
+ );
+
+ clip-path: polygon(0 10%, 100% 90%, 100% 100%, 0 100%);
+}
+
+.battery-chart__lines {
+ position: absolute;
+ left: -55px;
+ top: 0;
+ bottom: 0;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ color: $gray50;
+ font-size: $font-size-13;
+}
+
+.history-summary {
+ margin: 18px 0 0 100px;
+
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+
+ span {
+ padding: 10px 16px;
+ border-radius: 999px;
+ background: $color-bg;
+
+ color: $gray50;
+ font-size: $font-size-13;
+
+ b {
+ color: $color-text-strong;
+ font-weight: 600;
+ }
+ }
+}
+
+.device-apps-card {
+ padding: 18px;
+ border-radius: 20px;
+ background: $color-bg;
+
+ h3 {
+ margin: 0 0 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid $color-border-muted;
+
+ color: $color-text-heading;
+ font-size: $font-size-16;
+ font-weight: 600;
+ }
+}
+
+.device-apps-list {
+ border-radius: 14px;
+ background: $color-surface;
+ overflow: hidden;
+}
+
+.device-app-row {
+ width: 100%;
+ min-height: 48px;
+ padding: 0 16px;
+
+ border: none;
+ border-bottom: 1px solid $color-border-muted;
+ background: $color-surface;
+
+ display: grid;
+ grid-template-columns: 28px 220px 1fr auto 20px;
+ align-items: center;
+ gap: 12px;
+
+ text-align: left;
+ cursor: pointer;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ background: $color-surface-muted;
+ }
+}
+
+.device-app-row__icon {
+ width: 24px;
+ height: 24px;
+ border-radius: 7px;
+ background: $blue;
+}
+
+.device-app-row__name {
+ color: $color-text-heading;
+ font-size: $font-size-15;
+ font-weight: 500;
+}
+
+.device-app-row__time {
+ color: $gray50;
+ font-size: $font-size-13;
+}
+
+.device-app-row__percent {
+ color: $gray50;
+ font-size: $font-size-13;
+}
+
+.device-app-row__arrow {
+ color: $gray50;
+ font-size: $font-size-22;
+}
+
+.device-apps-card__more {
+ margin-top: 12px;
+
+ border: none;
+ background: $transparent;
+
+ color: $blue;
+ font-size: $font-size-14;
+ font-weight: 500;
+ cursor: pointer;
+}
+
+[data-theme='dark'] {
+ .device-history-modal {
+ background: $color-surface;
+ box-shadow: 0 26px 80px rgba($color-shadow-rgb, 0.62);
+ }
+
+ .device-history-chart__zoom {
+ background: rgba($color-surface-rgb, 0.76);
+ backdrop-filter: blur(14px);
+ box-shadow:
+ 0 18px 44px rgba($color-shadow-rgb, 0.38),
+ inset 0 0 0 1px rgba($gray50-rgb, 0.08);
+
+ .sticky-line {
+ background-color: rgba($gray50-rgb, 0.16);
+ }
+ }
+
+ .device-history-modal__close:hover {
+ background: $color-surface-hover;
+ color: $color-primary-hover;
+ }
+}
diff --git a/frontend/src/pages/DevicePage/components/DeviceHistoryModal/DeviceHistoryModal.tsx b/frontend/src/pages/DevicePage/components/DeviceHistoryModal/DeviceHistoryModal.tsx
new file mode 100644
index 0000000..b7e9d8a
--- /dev/null
+++ b/frontend/src/pages/DevicePage/components/DeviceHistoryModal/DeviceHistoryModal.tsx
@@ -0,0 +1,275 @@
+import {
+ lazy,
+ Suspense,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
+import * as Dialog from '@radix-ui/react-dialog'
+import { X } from 'lucide-react'
+import { useQuery } from '@apollo/client/react'
+import SimpleBar from 'simplebar-react'
+import 'simplebar-react/dist/simplebar.min.css'
+
+import type { Device } from '../../types'
+import { conditionText, connectionText } from '../../types'
+import {
+ GET_TELEMETRY_QUERY,
+} from '../../../../entities/device/api/device.graphql'
+import type {
+ GetTelemetryData,
+ GetTelemetryVariables,
+ DeviceHistoryChartItem,
+ DevicePackageUseEventsGroup,
+ DevicePhoneStateEventType
+} from '../../../../entities/device/model/types'
+
+import {
+ DeviceHistoryPeriodControl,
+ type DeviceHistoryPeriodValue,
+} from '../DeviceHistoryPeriodControl/DeviceHistoryPeriodControl'
+import { TimelineZoom } from '../TimelineZoom/TimelineZoom'
+import { MotionDialogContent } from '../../../../shared/ui/MotionDialog/MotionDialog'
+
+import './DeviceHistoryModal.scss'
+
+type DeviceHistoryModalProps = {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ device: Device
+}
+
+const PHONE_STATE_EVENT_LABELS: Record