Files
glchat/web/src/pages/StatusPage.tsx
T

131 lines
5.0 KiB
TypeScript
Raw Normal View History

import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router';
import { fetchReadiness, readyQueryKey } from '@/api/health';
import { fetchMeta, metaQueryKey } from '@/api/meta';
import { Badge, Button, Card } from '@/components/ui/primitives';
import { StatBytes, StatRow } from '@/components/ui/StatRow';
import { supportedLanguages, type SupportedLanguage } from '@/i18n';
import { useApiErrorMessage } from '@/lib/useApiErrorMessage';
import { useUiStore } from '@/stores/ui';
/** Публичная страница состояния инстанса: мета, healthz/readyz, тема и язык. */
export default function StatusPage() {
const { t, i18n } = useTranslation();
const describeError = useApiErrorMessage();
const theme = useUiStore((state) => state.theme);
const toggleTheme = useUiStore((state) => state.toggleTheme);
const locale = i18n.resolvedLanguage ?? 'ru';
const meta = useQuery({
queryKey: metaQueryKey,
queryFn: ({ signal }) => fetchMeta(signal),
retry: 1,
});
const readiness = useQuery({
queryKey: readyQueryKey,
queryFn: ({ signal }) => fetchReadiness(signal),
retry: 1,
refetchInterval: 30_000,
});
const online = readiness.isSuccess || meta.isSuccess;
const schemaReady = readiness.data?.checks['database'] === 'ok';
const changeLanguage = (language: SupportedLanguage): void => {
window.localStorage.setItem('glchat.language', language);
void i18n.changeLanguage(language);
};
return (
<div className="gl-page mx-auto flex min-h-full w-full max-w-3xl flex-col gap-6 px-4 py-10">
<header className="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold tracking-tight">{t('app.name')}</h1>
<p className="mt-1 text-fg-muted">{t('app.tagline')}</p>
</div>
<div className="flex items-center gap-2">
<Badge aria-live="polite" data-testid="connection-state">
<span
aria-hidden="true"
className={`h-2 w-2 rounded-full ${online ? 'bg-success' : 'bg-danger'}`}
/>
{t(online ? 'health.online' : 'health.offline')}
</Badge>
<Button variant="ghost" onClick={toggleTheme}>
{t(theme === 'dark' ? 'theme.switchToLight' : 'theme.switchToDark')}
</Button>
<label className="flex items-center gap-1 text-sm text-fg-muted">
<span className="sr-only">{t('language.label')}</span>
<select
className="rounded-[var(--radius-sm)] border border-border/60 bg-surface-2 px-2 py-1 text-fg"
value={locale.startsWith('en') ? 'en' : 'ru'}
onChange={(event) => changeLanguage(event.target.value === 'en' ? 'en' : 'ru')}
>
{supportedLanguages.map((language) => (
<option key={language} value={language}>
{t(`language.${language}`)}
</option>
))}
</select>
</label>
</div>
</header>
<Card>
<h2 className="text-lg font-semibold">{t('status.title')}</h2>
<p className="mt-1 text-fg-muted">{t('status.description')}</p>
{meta.isPending ? (
<p className="mt-4 text-fg-muted" role="status">
{t('status.connecting')}
</p>
) : null}
{meta.isError ? (
<div className="mt-4 flex items-center justify-between gap-4" role="alert">
<p className="text-danger">{describeError(meta.error)}</p>
<Button variant="ghost" onClick={() => void meta.refetch()}>
{t('status.retry')}
</Button>
</div>
) : null}
{meta.data ? (
<dl className="mt-4 text-sm">
<StatRow label={t('status.instance')} value={meta.data.name} />
<StatRow label={t('status.version')} value={meta.data.version} />
<StatRow label={t('status.apiVersion')} value={meta.data.api_version} />
<StatRow
label={t('status.schemaReady')}
value={schemaReady ? t('status.schemaOk') : (readiness.data?.status ?? '…')}
/>
<StatRow
label={t('status.maxUpload')}
value={<StatBytes bytes={meta.data.max_upload_size} locale={locale} />}
/>
</dl>
) : null}
</Card>
<div className="flex flex-wrap gap-3">
<Link className="text-sm text-accent underline" to="/login">
{t('auth.login.title')}
</Link>
<Link className="text-sm text-accent underline" to="/register">
{t('auth.register.title')}
</Link>
</div>
<footer className="mt-auto flex items-center justify-between text-xs text-fg-muted">
<span>
{t('app.name')} · {t('footer.license')}
</span>
<span className="tabular-nums">{meta.data?.commit ?? ''}</span>
</footer>
</div>
);
}