From 5728b729a8a0f41050a10886efba37d7a6b385f3 Mon Sep 17 00:00:00 2001 From: nns Date: Sun, 4 Oct 2026 15:42:49 +0500 Subject: [PATCH] =?UTF-8?q?feat(s30):=20=D0=BF=D0=BB=D0=B0=D1=88=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=A2=D0=95=D0=A1=D0=A2=D0=9E=D0=92=D0=AB=D0=99=20?= =?UTF-8?q?=D0=A1=D0=A2=D0=95=D0=9D=D0=94=20+=20robots=20noindex=20(=D1=88?= =?UTF-8?q?=D0=B0=D0=B3=206/9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StageBanner — жёлтая полоса сверху всех страниц, когда /api/dev/ quick-login/accounts отвечает 200 (стенд). В проде эндпоинт 404 → компонент возвращает null, никакая полоса не рисуется. Решение «stage vs prod» кэшируется в sessionStorage, чтобы не стрелять запрос на каждой навигации в SPA (один запрос за открытие вкладки). На стенде динамически добавляется — не хотим чтобы Google индексировал test.admin.food-market.kz. Один web-билд ходит в оба окружения; никаких отдельных index.html. Co-Authored-By: Claude Opus 4.7 --- docs/quick-login-progress.md | 2 +- src/food-market.web/src/App.tsx | 2 + .../src/components/StageBanner.tsx | 74 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/food-market.web/src/components/StageBanner.tsx 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 ( +
+ ⚠️ ТЕСТОВЫЙ СТЕНД · не вводите реальные пароли и ОФД-токены +
+ ) +}