feat(s30): плашка ТЕСТОВЫЙ СТЕНД + robots noindex (шаг 6/9)
StageBanner — жёлтая полоса сверху всех страниц, когда /api/dev/ quick-login/accounts отвечает 200 (стенд). В проде эндпоинт 404 → компонент возвращает null, никакая полоса не рисуется. Решение «stage vs prod» кэшируется в sessionStorage, чтобы не стрелять запрос на каждой навигации в SPA (один запрос за открытие вкладки). На стенде динамически добавляется <meta name=robots content=noindex, nofollow> — не хотим чтобы Google индексировал test.admin.food-market.kz. Один web-билд ходит в оба окружения; никаких отдельных index.html. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6582333def
commit
5728b729a8
|
|
@ -31,7 +31,7 @@
|
||||||
- [x] 5. **Страница входа (web)** — если `GET .../accounts` вернул 200 — над
|
- [x] 5. **Страница входа (web)** — если `GET .../accounts` вернул 200 — над
|
||||||
обычной формой блок «Быстрый вход (тестовый стенд)» с карточками + поиск.
|
обычной формой блок «Быстрый вход (тестовый стенд)» с карточками + поиск.
|
||||||
Клик — моментальный логин через мастер-пароль. На проде блока нет.
|
Клик — моментальный логин через мастер-пароль. На проде блока нет.
|
||||||
- [ ] 6. **Плашка «ТЕСТОВЫЙ СТЕНД»** — при Enabled=true жёлтая полоса
|
- [x] 6. **Плашка «ТЕСТОВЫЙ СТЕНД»** — при Enabled=true жёлтая полоса
|
||||||
сверху + `<meta robots="noindex,nofollow">` в index.html стенда.
|
сверху + `<meta robots="noindex,nofollow">` в index.html стенда.
|
||||||
- [ ] 7. **Чистка автотестового мусора** — SQL-скрипт для стейджа + расширение
|
- [ ] 7. **Чистка автотестового мусора** — SQL-скрипт для стейджа + расширение
|
||||||
`HousekeepingJobs.PruneQualityTestOrgsAsync` на новые autotest-патéрны,
|
`HousekeepingJobs.PruneQualityTestOrgsAsync` на новые autotest-патéрны,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||||||
import { RoleGuard } from '@/components/RoleGuard'
|
import { RoleGuard } from '@/components/RoleGuard'
|
||||||
import { AdvancedModeGuard } from '@/components/AdvancedModeGuard'
|
import { AdvancedModeGuard } from '@/components/AdvancedModeGuard'
|
||||||
import { Toaster } from '@/components/Toaster'
|
import { Toaster } from '@/components/Toaster'
|
||||||
|
import { StageBanner } from '@/components/StageBanner'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { FormSkeleton } from '@/components/Skeleton'
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
|
|
||||||
|
|
@ -117,6 +118,7 @@ export default function App() {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
<StageBanner />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
|
|
||||||
74
src/food-market.web/src/components/StageBanner.tsx
Normal file
74
src/food-market.web/src/components/StageBanner.tsx
Normal file
|
|
@ -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])
|
||||||
|
|
||||||
|
// На стенде добавляем <meta name="robots" content="noindex,nofollow"> —
|
||||||
|
// не хотим чтобы Google индексировал test.admin.food-market.kz. Инъектим
|
||||||
|
// динамически (а не в index.html), чтобы один билд ходил и в прод (там
|
||||||
|
// стенда нет и meta не добавляется).
|
||||||
|
useEffect(() => {
|
||||||
|
if (state !== 'stage') return
|
||||||
|
const existing = document.querySelector<HTMLMetaElement>('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 (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
data-testid="stage-banner"
|
||||||
|
className="w-full bg-amber-400 text-amber-950 font-semibold text-xs sm:text-sm text-center py-1 shadow-sm"
|
||||||
|
>
|
||||||
|
⚠️ ТЕСТОВЫЙ СТЕНД · не вводите реальные пароли и ОФД-токены
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue