diff --git a/tradein-mvp/frontend/src/app/ui-preview/estimate/fixture.ts b/tradein-mvp/frontend/src/app/ui-preview/estimate/fixture.ts
new file mode 100644
index 00000000..5ca782f3
--- /dev/null
+++ b/tradein-mvp/frontend/src/app/ui-preview/estimate/fixture.ts
@@ -0,0 +1,189 @@
+// #801: статичная фикстура результата оценки для preview-маршрута (a11y/lighthouse).
+// Полностью оффлайн — никакой сети. Значения правдоподобны (ЕКБ, 2-к вторичка),
+// заполнены ВСЕ surface'ы «денежного экрана»: asking + expected-sold + Avito IMV +
+// ДКП-коридор + cian valuation + price_trend + аналоги/сделки.
+// photo_url=null у всех лотов — намеренно, чтобы не тянуть внешние картинки
+// (чистый оффлайн-рендер для lighthouse/axe).
+
+import type { AggregatedEstimate, AnalogLot, TradeInEstimateInput } from "@/types/trade-in";
+
+const analogs: AnalogLot[] = [
+ {
+ address: "ул. Репина, 73",
+ area_m2: 54,
+ rooms: 2,
+ floor: 7,
+ total_floors: 16,
+ price_rub: 9_700_000,
+ price_per_m2: 179_629,
+ listing_date: "2026-05-12",
+ days_on_market: 18,
+ photo_url: null,
+ source: "avito",
+ source_url: "https://www.avito.ru/ekaterinburg/kvartiry/preview-1",
+ distance_m: 120,
+ tier: null,
+ lat: 56.8401,
+ lon: 60.5702,
+ },
+ {
+ address: "ул. Викулова, 33",
+ area_m2: 57,
+ rooms: 2,
+ floor: 4,
+ total_floors: 10,
+ price_rub: 9_950_000,
+ price_per_m2: 174_561,
+ listing_date: "2026-05-20",
+ days_on_market: 10,
+ photo_url: null,
+ source: "cian",
+ source_url: "https://www.cian.ru/sale/flat/preview-2",
+ distance_m: 340,
+ tier: null,
+ lat: 56.8389,
+ lon: 60.5681,
+ },
+ {
+ address: "ул. Кирова, 28",
+ area_m2: 53,
+ rooms: 2,
+ floor: 9,
+ total_floors: 12,
+ price_rub: 9_400_000,
+ price_per_m2: 177_358,
+ listing_date: "2026-05-04",
+ days_on_market: 31,
+ photo_url: null,
+ source: "avito",
+ source_url: "https://www.avito.ru/ekaterinburg/kvartiry/preview-3",
+ distance_m: 510,
+ tier: null,
+ lat: 56.8372,
+ lon: 60.5749,
+ },
+];
+
+const actualDeals: AnalogLot[] = [
+ {
+ address: "ул. Репина",
+ area_m2: 55,
+ rooms: 2,
+ floor: null,
+ total_floors: null,
+ price_rub: 8_800_000,
+ price_per_m2: 160_000,
+ listing_date: "2026-01-01",
+ days_on_market: null,
+ photo_url: null,
+ source: "rosreestr",
+ source_url: null,
+ distance_m: null,
+ tier: "T1_per_street",
+ lat: null,
+ lon: null,
+ },
+ {
+ address: "ул. Викулова",
+ area_m2: 58,
+ rooms: 2,
+ floor: null,
+ total_floors: null,
+ price_rub: 9_280_000,
+ price_per_m2: 160_000,
+ listing_date: "2025-10-01",
+ days_on_market: null,
+ photo_url: null,
+ source: "rosreestr",
+ source_url: null,
+ distance_m: null,
+ tier: "T1_per_street",
+ lat: null,
+ lon: null,
+ },
+];
+
+export const FIXTURE_ESTIMATE: AggregatedEstimate = {
+ estimate_id: "preview-0000-0000-0000-000000000000",
+ median_price_rub: 9_850_000,
+ range_low_rub: 9_100_000,
+ range_high_rub: 10_600_000,
+ median_price_per_m2: 178_000,
+ expected_sold_price_rub: 8_900_000,
+ expected_sold_range_low_rub: 8_300_000,
+ expected_sold_range_high_rub: 9_500_000,
+ expected_sold_per_m2: 160_800,
+ asking_to_sold_ratio: 0.9,
+ ratio_basis: "per_rooms",
+ confidence: "high",
+ confidence_explanation: "12 аналогов в радиусе 800 м, разброс цен ±9%.",
+ n_analogs: 12,
+ insufficient_data: false,
+ period_months: 24,
+ analogs,
+ actual_deals: actualDeals,
+ expires_at: "2026-06-30T00:00:00Z",
+ target_address: "Екатеринбург, ул. Репина, 75/2",
+ target_lat: 56.84,
+ target_lon: 60.57,
+ sources_used: ["avito", "cian", "rosreestr"],
+ data_freshness_minutes: 35,
+ last_scraped_at: "2026-05-30T12:00:00Z",
+ est_days_on_market: 48,
+ address_precision: "house",
+ area_m2: 55.3,
+ rooms: 2,
+ floor: 5,
+ total_floors: 11,
+ year_built: 2018,
+ house_type: "monolith",
+ repair_state: "good",
+ has_balcony: true,
+ cian_valuation: {
+ sale_price_rub: 9_700_000,
+ rent_price_rub: 42_000,
+ chart: [
+ { date: "2025-11", price: 9_300_000 },
+ { date: "2025-12", price: 9_420_000 },
+ { date: "2026-01", price: 9_510_000 },
+ { date: "2026-02", price: 9_560_000 },
+ { date: "2026-03", price: 9_640_000 },
+ { date: "2026-04", price: 9_700_000 },
+ ],
+ chart_change_pct: 3.2,
+ chart_change_direction: "increase",
+ },
+ avito_imv: {
+ recommended_price: 10_200_000,
+ lower_price: 9_400_000,
+ higher_price: 11_000_000,
+ market_count: 7,
+ },
+ dkp_corridor: {
+ count: 9,
+ low_ppm2: 150_000,
+ median_ppm2: 162_000,
+ high_ppm2: 178_000,
+ period_months: 24,
+ },
+ price_trend: [
+ { month: "2025-11", ppm2: 168_000 },
+ { month: "2025-12", ppm2: 170_500 },
+ { month: "2026-01", ppm2: 172_000 },
+ { month: "2026-02", ppm2: 174_000 },
+ { month: "2026-03", ppm2: 176_500 },
+ { month: "2026-04", ppm2: 178_000 },
+ ],
+};
+
+export const FIXTURE_INPUT: TradeInEstimateInput = {
+ address: "Екатеринбург, ул. Репина, 75/2",
+ area_m2: 55.3,
+ rooms: 2,
+ floor: 5,
+ total_floors: 11,
+ year_built: 2018,
+ house_type: "monolith",
+ repair_state: "good",
+ has_balcony: true,
+};
diff --git a/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx b/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx
new file mode 100644
index 00000000..609f3fa7
--- /dev/null
+++ b/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+/**
+ * #801: dev/CI-only preview-маршрут «денежного экрана» на статичной фикстуре.
+ *
+ * Зачем: a11y (axe) и lighthouse требуют отрендеренную контентную страницу.
+ * Live trade-in за auth-gate, а локальный фронт без бэкенда = белый экран
+ * (RouteGuard висит на pending /me). Эта страница рендерит реальные компоненты
+ * результата (HeroSummary + карточки/чарты) полностью оффлайн — без сети.
+ *
+ * Включается ТОЛЬКО при NEXT_PUBLIC_ENABLE_PREVIEW=1 (иначе notFound) — в проде
+ * по умолчанию недоступна. RouteGuard пропускает /ui-preview/* под тем же флагом.
+ * (Папка НЕ может начинаться с "_" — Next трактует такие как private/non-routable.)
+ *
+ * Локально: echo NEXT_PUBLIC_ENABLE_PREVIEW=1 > .env.local && npm run dev
+ * → http://localhost:3000/ui-preview/estimate
+ * CI: собрать с флагом, поднять next start, навести axe/lighthouse.
+ */
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { notFound } from "next/navigation";
+import { useState } from "react";
+
+import { CianValuationCard } from "@/components/trade-in/CianValuationCard";
+import { DealsCard } from "@/components/trade-in/DealsCard";
+import { DistributionCard } from "@/components/trade-in/DistributionCard";
+import { ExposureCard } from "@/components/trade-in/ExposureCard";
+import { HeroSummary } from "@/components/trade-in/HeroSummary";
+import { PriceTrendCard } from "@/components/trade-in/PriceTrendCard";
+import { StreetDealsCard } from "@/components/trade-in/StreetDealsCard";
+import { ME_QUERY_KEY, type UserScope } from "@/lib/useMe";
+
+import { FIXTURE_ESTIMATE, FIXTURE_INPUT } from "./fixture";
+
+const ENABLED = process.env.NEXT_PUBLIC_ENABLE_PREVIEW === "1";
+
+// Фейковый authorized-юзер: засеивается в QueryClient → useMe/useBrand резолвятся
+// оффлайн (никакого /me, /quota, /brand), generic-бренд (brand=null).
+const PREVIEW_USER: UserScope = {
+ username: "preview",
+ role: "admin",
+ allowed_paths: ["/**"],
+ deny_paths: [],
+ brand: null,
+};
+
+function PreviewContent() {
+ const [client] = useState(() => {
+ const qc = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, staleTime: Infinity, refetchOnWindowFocus: false },
+ },
+ });
+ qc.setQueryData(ME_QUERY_KEY, PREVIEW_USER);
+ return qc;
+ });
+
+ return (
+
+
+ {}}
+ isResubmitting={false}
+ />
+
+
+
+
+
+
+
+
+ );
+}
+
+export default function PreviewEstimatePage() {
+ if (!ENABLED) notFound();
+ return ;
+}
diff --git a/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx b/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx
index 5756c3da..c7f54f3b 100644
--- a/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx
+++ b/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx
@@ -20,6 +20,8 @@ import { isPathAllowed } from "@/lib/isPathAllowed";
import { useMe } from "@/lib/useMe";
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
+// #801: dev/CI-only preview-маршрут (a11y/lighthouse) рендерится оффлайн без RBAC.
+const ENABLE_PREVIEW = process.env.NEXT_PUBLIC_ENABLE_PREVIEW === "1";
interface RouteGuardProps {
children: React.ReactNode;
@@ -34,6 +36,12 @@ export function RouteGuard({ children }: RouteGuardProps) {
: rawPath;
const { data, isLoading, error } = useMe();
+ // #801: preview-страница самодостаточна (свой QueryClient с фейковым me),
+ // RBAC к ней не применяем. Только под флагом — в проде по умолчанию выключено.
+ if (ENABLE_PREVIEW && rawPath.startsWith("/ui-preview")) {
+ return <>{children}>;
+ }
+
if (isLoading) return null;
if (error instanceof HTTPError && error.status === 401) {