diff --git a/docs/quick-login-progress.md b/docs/quick-login-progress.md index e860a9e..113da46 100644 --- a/docs/quick-login-progress.md +++ b/docs/quick-login-progress.md @@ -31,7 +31,7 @@ - [x] 5. **Страница входа (web)** — если `GET .../accounts` вернул 200 — над обычной формой блок «Быстрый вход (тестовый стенд)» с карточками + поиск. Клик — моментальный логин через мастер-пароль. На проде блока нет. -- [ ] 6. **Плашка «ТЕСТОВЫЙ СТЕНД»** — при Enabled=true жёлтая полоса +- [x] 6. **Плашка «ТЕСТОВЫЙ СТЕНД»** — при Enabled=true жёлтая полоса сверху + `` в index.html стенда. - [ ] 7. **Чистка автотестового мусора** — SQL-скрипт для стейджа + расширение `HousekeepingJobs.PruneQualityTestOrgsAsync` на новые autotest-патéрны, diff --git a/src/food-market.web/src/App.tsx b/src/food-market.web/src/App.tsx index 28dcec0..81382bf 100644 --- a/src/food-market.web/src/App.tsx +++ b/src/food-market.web/src/App.tsx @@ -30,6 +30,7 @@ import { ProtectedRoute } from '@/components/ProtectedRoute' import { RoleGuard } from '@/components/RoleGuard' import { AdvancedModeGuard } from '@/components/AdvancedModeGuard' import { Toaster } from '@/components/Toaster' +import { StageBanner } from '@/components/StageBanner' import { toast } from '@/lib/toast' import { FormSkeleton } from '@/components/Skeleton' @@ -117,6 +118,7 @@ export default function App() { return ( + } /> diff --git a/src/food-market.web/src/components/StageBanner.tsx b/src/food-market.web/src/components/StageBanner.tsx new file mode 100644 index 0000000..efcb7df --- /dev/null +++ b/src/food-market.web/src/components/StageBanner.tsx @@ -0,0 +1,74 @@ +import { useEffect, useState } from 'react' +import { api } from '@/lib/api' + +/** + * Жёлтая полоса сверху «ТЕСТОВЫЙ СТЕНД — не вводите реальные данные». + * Рендерится ТОЛЬКО когда /api/dev/quick-login/accounts отвечает 200 + * (стенд). В проде эндпоинт 404 → компонент возвращает null, пустое + * место сверху не занимается. + * + * Решение «показывать / скрывать» делается один раз при первом монтировании + * (на уровне App.tsx) и кешируется в sessionStorage, чтобы не делать запрос + * на каждой навигации. Ключ очищается при закрытии вкладки. + */ +const CACHE_KEY = 'fm.stage-flag' // "stage" | "prod" + +export function StageBanner() { + const [state, setState] = useState<'stage' | 'prod' | 'loading'>(() => { + const cached = typeof window !== 'undefined' ? sessionStorage.getItem(CACHE_KEY) : null + if (cached === 'stage') return 'stage' + if (cached === 'prod') return 'prod' + return 'loading' + }) + + useEffect(() => { + if (state !== 'loading') return + let cancelled = false + api + .get('/api/dev/quick-login/accounts') + .then(() => { + if (cancelled) return + setState('stage') + sessionStorage.setItem(CACHE_KEY, 'stage') + }) + .catch(() => { + if (cancelled) return + setState('prod') + sessionStorage.setItem(CACHE_KEY, 'prod') + }) + return () => { cancelled = true } + }, [state]) + + // На стенде добавляем — + // не хотим чтобы Google индексировал test.admin.food-market.kz. Инъектим + // динамически (а не в index.html), чтобы один билд ходил и в прод (там + // стенда нет и meta не добавляется). + useEffect(() => { + if (state !== 'stage') return + const existing = document.querySelector('meta[name="robots"]') + if (existing) return // уже есть — не ломаем + const meta = document.createElement('meta') + meta.name = 'robots' + meta.content = 'noindex,nofollow' + meta.setAttribute('data-origin', 'stage-banner') + document.head.appendChild(meta) + return () => { + // при переходе prod ↔ stage в рамках одной SPA-сессии не нужно + // держать meta — но на самом деле это не произойдёт (мы кешируем + // решение). На всякий случай очищаем, если флаг улетел в prod. + if (meta.parentNode) meta.parentNode.removeChild(meta) + } + }, [state]) + + if (state !== 'stage') return null + + return ( +
+ ⚠️ ТЕСТОВЫЙ СТЕНД · не вводите реальные пароли и ОФД-токены +
+ ) +}