Files
ridebus-board/src/DeviceSetup.tsx
Zakhar Timoshenko d04b364047 feat: add remote device management and pairing functionality
- Implemented device pairing with a four-digit code and TTL for remote settings.
- Introduced automatic checks for device status and configuration validation.
- Developed UI components for device setup and management, including error handling and retry options.
- Created a device bridge for secure communication between the device and the server.
- Added tests for managed state validation and device pairing logic.
- Established a local end-to-end fixture for testing device service and API interactions.
2026-10-10 00:52:36 +03:00

24 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react';
import logo from './assets/ridebus-logo.svg?raw';
import { NumericText } from './NumericText';
import { Fullscreen, Refresh, Shield } from './Icons';
import type { ManagedState } from './useDeviceControl';
export function DeviceSetup({ control, failed, onRetry }: { control: { state: string; code?: string; expiresAt?: string; adminUrl?: string }; failed: boolean; onRetry: () => void }) {
const [now, setNow] = useState(Date.now());
useEffect(() => { document.title = 'Подключение · RideBus Board'; const interval = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(interval); }, []);
const remaining = Math.max(0, Math.min(300, Math.ceil((Date.parse(control.expiresAt || '') - now) / 1000)));
const pending = control.state === 'pending' && remaining > 0;
return <main className="device-setup" aria-label="Подключение табло"><div className="device-setup-brand"><span dangerouslySetInnerHTML={{ __html: logo }} />ridebus</div><div className="device-setup-card"><span className="eyebrow">RIDEBUS BOARD</span><h1>{control.state === 'revoked' ? 'Устройство отвязано' : 'Подключите табло'}</h1>
<p>{pending ? 'Введите этот код в админке: Устройства → Добавить устройство' : control.state === 'revoked' ? 'Для повторной настройки подключите устройство по новому коду' : control.state === 'storage-error' ? 'Разрешите браузеру сохранять cookies и данные сайта' : control.state === 'loading' ? 'Получаем код подключения…' : 'Для подключения нужен доступ к серверу'}</p>
{pending && <><div className="device-pair-code" aria-label={`Код подключения ${control.code}`}><NumericText value={control.code || ''} /></div><div className="device-pair-expiry">Код действует ещё <NumericText value={`${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, '0')}`} /></div></>}
{control.adminUrl && <a href={control.adminUrl} target="_blank" rel="noreferrer">Открыть админку</a>}
{failed && <div className="device-setup-status" role="status">{control.state === 'storage-error' ? 'Не удалось сохранить привязку устройства' : 'Нет связи с сервером. Повторяем подключение'}</div>}
{!pending && control.state !== 'loading' && <button onClick={onRetry}><Refresh />{control.state === 'revoked' ? 'Подключить заново' : 'Повторить'}</button>}
</div><button className="device-setup-fullscreen" onClick={() => { void document.documentElement.requestFullscreen().catch(() => {}); }}><Fullscreen />На весь экран</button></main>;
}
export function ManagedPanel({ state, failed, onClose, onFullscreen }: { state: ManagedState; failed: boolean; onClose: () => void; onFullscreen: () => void }) {
const dialog = useRef<HTMLDialogElement>(null);
useEffect(() => { dialog.current?.showModal(); }, []);
return <dialog ref={dialog} className="managed-panel" aria-labelledby="managed-title" onCancel={event => { event.preventDefault(); onClose(); }}><Shield size={36} /><h2 id="managed-title">Управляется из админки</h2><strong>{state.device.name}</strong><p>Изменяйте остановку, оформление и экраны в разделе «Устройства».</p><dl><dt>Настройки</dt><dd>Версия {state.device.revision}</dd><dt>Соединение</dt><dd>{failed ? 'Нет связи · сохранённые настройки' : 'Подключено'}</dd></dl><div><button onClick={onFullscreen}><Fullscreen />Полный экран</button><button onClick={onClose}>Закрыть</button></div></dialog>;
}