Default new devices to Direct routing
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
byteString,
|
||||
formatByteString,
|
||||
formatLastSeen,
|
||||
isNewDevice,
|
||||
positiveByteDelta,
|
||||
stabilizeDevicesByTraffic,
|
||||
} from '../../utils/format.js';
|
||||
@@ -444,6 +445,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
const groupStart = group !== previousGroup;
|
||||
const hasName = Boolean(device.alias || device.hostname);
|
||||
const title = device.alias || device.hostname || device.ip || 'Неизвестное устройство';
|
||||
const newDevice = isNewDevice(device.firstSeenAt);
|
||||
const editing = editingId === device.id;
|
||||
const saving = savingId === device.id;
|
||||
const seen = formatLastSeen(device.lastSeenAt);
|
||||
@@ -493,7 +495,9 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
: device.confidence === 'ambiguous' && device.desiredPolicy !== 'direct'
|
||||
? 'Маршрут недоступен, пока Gateway видит несколько сетевых адресов одного устройства'
|
||||
: displayPolicy === 'direct'
|
||||
? 'Полностью обходит sing-box. Нажмите, чтобы вернуть обработку Gateway'
|
||||
? newDevice
|
||||
? 'Новое устройство идёт напрямую. Нажмите, чтобы разрешить VPN через правила Gateway'
|
||||
: 'Полностью обходит sing-box. Нажмите, чтобы вернуть обработку Gateway'
|
||||
: 'Проходит через sing-box и правила Gateway. Нажмите, чтобы пустить полностью напрямую';
|
||||
return <article
|
||||
key={device.id}
|
||||
@@ -553,6 +557,10 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
onClick={() => startEditing(device)}
|
||||
>{title}</button>}
|
||||
{!hasName && !editing && <span className="client-device-fallback-name">{title}</span>}
|
||||
{!editing && newDevice && <span className="client-device-new-badge">
|
||||
<span aria-hidden="true">NEW</span>
|
||||
<span className="client-device-new-a11y">Новое устройство</span>
|
||||
</span>}
|
||||
{!editing && <span className="client-device-identity-details" role="group" aria-label={`Технические данные устройства ${title}`}>
|
||||
{device.ip && <button
|
||||
className={`client-device-identity-copy${feedback?.field === 'IP' ? feedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface Device extends Record<string, unknown> {
|
||||
hostname: string | null;
|
||||
mac: string;
|
||||
ip: string | null;
|
||||
firstSeenAt: string;
|
||||
lastSeenAt: string | null;
|
||||
status: DeviceStatus;
|
||||
pinned: boolean;
|
||||
@@ -151,6 +152,7 @@ function validDevice(value: unknown): value is Device {
|
||||
&& typeof value.mac === 'string'
|
||||
&& /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/.test(value.mac)
|
||||
&& nullableString(value.ip)
|
||||
&& timestamp(value.firstSeenAt)
|
||||
&& nullableTimestamp(value.lastSeenAt)
|
||||
&& (value.status === 'online' || value.status === 'recent' || value.status === 'offline')
|
||||
&& typeof value.pinned === 'boolean'
|
||||
|
||||
@@ -421,6 +421,31 @@
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.client-device-new-badge {
|
||||
height: 14px;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
border: 1px solid color-mix(in oklch, var(--client-accent) 48%, transparent);
|
||||
border-radius: 999px;
|
||||
background: var(--client-accent-soft);
|
||||
color: var(--client-accent);
|
||||
font: var(--type-micro);
|
||||
letter-spacing: var(--type-micro-tracking);
|
||||
text-transform: var(--type-micro-transform);
|
||||
}
|
||||
|
||||
.client-device-new-a11y {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.client-device-identity-details {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
|
||||
@@ -13,6 +13,7 @@ export function formatBytes(value: number) {
|
||||
}
|
||||
|
||||
const BYTE_STRING_PATTERN = /^\d+$/;
|
||||
const NEW_DEVICE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export function byteString(value: unknown) {
|
||||
const normalized = String(value ?? '0');
|
||||
@@ -126,6 +127,13 @@ export function formatTime(iso: string | null | undefined) {
|
||||
return new Date(iso).toLocaleTimeString("ru-RU", { hour12: false });
|
||||
}
|
||||
|
||||
export function isNewDevice(iso: string | null | undefined, now: Date | string | number = Date.now()) {
|
||||
const firstSeen = Date.parse(iso || '');
|
||||
const current = new Date(now).getTime();
|
||||
const age = current - firstSeen;
|
||||
return Number.isFinite(firstSeen) && Number.isFinite(current) && age >= 0 && age < NEW_DEVICE_MS;
|
||||
}
|
||||
|
||||
export function formatLastSeen(iso: string | null | undefined, now: Date | string | number = new Date()) {
|
||||
const date = new Date(iso || '');
|
||||
if (Number.isNaN(date.getTime())) return { label: "Нет данных", relative: "Нет данных", tooltip: "Нет данных" };
|
||||
|
||||
Reference in New Issue
Block a user