feat: implement night mode functionality with UI updates and journey tracking

- Added NightScreen component for displaying night mode information and alerts.
- Introduced NextStops component to show upcoming stops during a journey.
- Implemented logic for determining night mode activation based on schedule and user settings.
- Created journey utility functions to manage onward journey data and stop information.
- Updated tests to cover new night mode features and journey logic.
- Added fixture for testing night mode scenarios with specific timestamps.
- Removed QR code from night screen for a cleaner UI experience.
- Enhanced admin settings to manage night mode configurations.
This commit is contained in:
2026-10-11 05:04:00 +03:00
parent d04b364047
commit 5c69f6691d
49 changed files with 590 additions and 121 deletions

View File

@@ -1,9 +1,9 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { Config, Settings } from './types';
import { isManagedState, MANAGED_KEY } from './lib/device.mjs';
import { hydrateManagedState, isManagedState, MANAGED_KEY } from './lib/device.mjs';
export type ManagedState = { state: 'paired'; enabled: true; adminUrl: string; config: Config; device: { id: string; name: string; cityId: number; revision: number; settings: Settings } };
type Control = { state: string; enabled?: boolean; code?: string; expiresAt?: string; adminUrl?: string };
function cached(): ManagedState | null { try { const value = JSON.parse(localStorage.getItem(MANAGED_KEY) || 'null'); return isManagedState(value) ? value : null; } catch { return null; } }
function cached(): ManagedState | null { try { const value = hydrateManagedState(JSON.parse(localStorage.getItem(MANAGED_KEY) || 'null')); return isManagedState(value) ? value : null; } catch { return null; } }
const info = () => ({ width: innerWidth, height: innerHeight, version: 'board-0.1.0' });
export function useDeviceControl() {
const [managed, setManaged] = useState<ManagedState | null>(cached), [control, setControl] = useState<Control>({ state: 'loading' });
@@ -38,6 +38,7 @@ export function useDeviceControl() {
if (['unpaired', 'expired'].includes(value.state)) { if (!stopped) { setManaged(null); applied.current = 0; localStorage.removeItem(MANAGED_KEY); } value = await call('pair', info()); }
if (value.state === 'paired' && !value.config) value = await call('status');
if (value.state === 'paired') {
value = hydrateManagedState(value);
if (!isManagedState(value)) throw new Error('invalid_settings');
if (!stopped) { setManaged(previous => previous && previous.device.id === value.device.id && previous.device.revision === value.device.revision ? previous : value); setControl(value); setFailed(false); }
if (Date.now() - lastHeartbeat >= 30000) { await call('heartbeat', { ...info(), appliedRevision: current.current?.device.id === value.device.id ? applied.current : 0 }); lastHeartbeat = Date.now(); }