ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 50s
ci / ts / ts (packages + web) (push) Successful in 43s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 27s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m22s
- core: AuthSession keeps the access token in memory, refreshes it once for all parallel requests (single-flight) and retries them; refresh races between tabs are retried, reuse signs out; ApiError carries problem+json codes and field errors - web: TanStack Router with guards — guests go to /login, the bootstrap admin to /setup/admin, temporary passwords to /setup/password, admin pages by role - login, setup screens, Classic shell (top bar, role-based sidebar, empty player), settings: name, theme, language, password, devices - users admin: search, role filter, create with a one-time temporary password, reset, block, role change, delete as the policy allows; audit log for admins - ru/en strings for auth, admin and error codes; language and theme are saved in the profile Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
74 lines
2.1 KiB
TypeScript
74 lines
2.1 KiB
TypeScript
import './styles.css';
|
|
|
|
import { AuthSession } from '@pipu/core';
|
|
import { createI18n, pickLocale } from '@pipu/i18n';
|
|
import { StrictMode } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { I18nextProvider } from 'react-i18next';
|
|
|
|
import { App } from './App';
|
|
import { createAppRouter } from './app/router';
|
|
import { applyTheme } from './app/session';
|
|
|
|
const LOCALE_KEY = 'pipu.locale';
|
|
|
|
function storedLocale(): string[] {
|
|
try {
|
|
const v = localStorage.getItem(LOCALE_KEY);
|
|
return v ? [v] : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/** «Chrome / Windows» — подпись сессии в списке устройств. */
|
|
function deviceName(): string {
|
|
const ua = navigator.userAgent;
|
|
const browser = /Edg\//.test(ua)
|
|
? 'Edge'
|
|
: /Firefox\//.test(ua)
|
|
? 'Firefox'
|
|
: /Chrome\//.test(ua)
|
|
? 'Chrome'
|
|
: /Safari\//.test(ua)
|
|
? 'Safari'
|
|
: 'Browser';
|
|
const os = /Windows/.test(ua)
|
|
? 'Windows'
|
|
: /Android/.test(ua)
|
|
? 'Android'
|
|
: /iPhone|iPad/.test(ua)
|
|
? 'iOS'
|
|
: /Mac OS X/.test(ua)
|
|
? 'macOS'
|
|
: /Linux/.test(ua)
|
|
? 'Linux'
|
|
: '';
|
|
return os ? `${browser} / ${os}` : browser;
|
|
}
|
|
|
|
applyTheme(undefined);
|
|
const i18n = await createI18n(pickLocale([...storedLocale(), ...navigator.languages]));
|
|
document.documentElement.lang = i18n.language;
|
|
i18n.on('languageChanged', (l) => {
|
|
document.documentElement.lang = l;
|
|
try {
|
|
localStorage.setItem(LOCALE_KEY, l);
|
|
} catch {
|
|
// недоступное хранилище не мешает работе
|
|
}
|
|
});
|
|
|
|
const session = new AuthSession({ clientType: 'web', deviceName: deviceName() });
|
|
// До первого рендера узнаём, есть ли сессия (refresh-cookie), — guards роутера решают сразу.
|
|
await session.restore();
|
|
const router = createAppRouter(session);
|
|
|
|
createRoot(document.getElementById('root')!).render(
|
|
<StrictMode>
|
|
<I18nextProvider i18n={i18n}>
|
|
<App session={session} router={router} />
|
|
</I18nextProvider>
|
|
</StrictMode>,
|
|
);
|