# Sprint 5D — Cleanup dev-артефактов (I3 + I4) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Убрать production fake-data fallback (вьюхи рендерят выдуманные сделки/тенантов/инциденты при сбое API) и DEV-gate баннер `_dev_plain_code` в impersonation-диалоге.

**Architecture:** Аудит портала, находки I3 + I4 (под-план Sprint 5D). 8 production-файлов инициализируют reactive-state mock-данными из `composables/mock*.ts` и держат их как fallback при ошибке API — в проде юзер при 500/network видит фейковые данные. Фикс: state инициализируется пустым (`[]` / нули); при ошибке показывается error-alert (уже есть `v-alert v-if="fetchError"` в каждом файле — меняется только текст). Файлы `mock*.ts` **остаются на месте** (типы + UI-константы там реальные; mock-массивы используются только тестами как фикстуры) — решение заказчика «убрать fake-fallback», без переезда файлов. I4: баннер `_dev_plain_code` гейтится за `import.meta.env.DEV`.

**Tech Stack:** Vue 3.5 + Vuetify 3.12 + TypeScript, Vitest 4 (`@vue/test-utils` + jsdom).

**Scope (I1 — отложен):** I1 (DevIndexBadge/DevIndexOverlay removal) решением заказчика отложен до заморозки UI — не в этом плане.

**НЕ в scope (discovered minor):** `DealsView.vue` строка `const newToday = 3; // mock` — инлайн-литерал (не `mock*.ts`-композабл), показывает фейковое «+3 новых лида с утра». Требует реальной backend-метрики — отдельная находка, в 5D не трогается.

---

## File Structure

Production (8 файлов — убрать mock-fallback):

- `app/resources/js/views/DealsView.vue` — `dealsState` init из `MOCK_DEALS`.
- `app/resources/js/views/KanbanView.vue` — `dealsByStatus` + `totalDeals` init из `MOCK_DEALS`.
- `app/resources/js/components/deals/NewDealDialog.vue` — `projectOptions`/`managerOptions` init из `MOCK_PROJECTS`/`MOCK_MANAGERS`.
- `app/resources/js/components/deals/DealDetailDrawer.vue` — `events` init из `MOCK_EVENTS`, fallback на 2 catch-путях.
- `app/resources/js/views/admin/AdminBillingView.vue` — `rowsState`/`summary` init из `ADMIN_BILLING_TENANTS`/`ADMIN_BILLING_SUMMARY`.
- `app/resources/js/views/admin/AdminIncidentsView.vue` — `rowsState` + initial `stats` init из `ADMIN_INCIDENTS`.
- `app/resources/js/views/admin/AdminSystemView.vue` — `settingsState` init из `ADMIN_SYSTEM_SETTINGS` (тип `AdminSystemSetting` — оставить).
- `app/resources/js/views/admin/AdminTenantsView.vue` — `tenantsState`/`stats` init из `MOCK_TENANTS`/`MOCK_STATS`.

Production (1 файл — I4):

- `app/resources/js/components/admin/ImpersonationDialog.vue` — баннер `dev-code-banner` за `import.meta.env.DEV`.

Не трогаются: `composables/mock*.ts` (типы/константы реальны, mock-массивы — тест-фикстуры).

Tests (обновить — спеки писались вокруг mock-initial-state):

- `app/tests/Frontend/DealsView.spec.ts`, `DealsViewRedesign.spec.ts`, `KanbanView.spec.ts`
- `app/tests/Frontend/NewDealDialog*.spec.ts`, `DealDetailDrawer*.spec.ts` (имена уточнить через `ls`)
- `app/tests/Frontend/AdminBillingView.spec.ts`, `AdminBillingViewApi.spec.ts`
- `app/tests/Frontend/AdminIncidentsView.spec.ts`, `AdminIncidentsViewApi.spec.ts`
- `app/tests/Frontend/AdminSystemView.spec.ts`, `AdminTenantsView.spec.ts`, `AdminTenantsViewApi.spec.ts`

---

## Общий тест-принцип (для всех задач T1–T4)

Init state → `[]` ломает существующие тесты двух типов. Стратегия:

1. **Тесты, использующие mock как фикстуру** (рендер строк, bulk-actions, фильтры). Mock-композабл импортируется **в spec-файле** как фикстура (это разрешено — тесты могут использовать mock-данные). Два варианта вернуть данные в state — выбрать минимально-инвазивный для конкретного спека:
   - **Вариант A (предпочтительно для `*Api.spec.ts` и где API уже `vi.mock`'нут):** мок data-API резолвится фикстурой → success-путь `loadX()` наполняет state, `fetchError=false`.
   - **Вариант B (для smoke-спеков без `vi.mock`):** после `mount` + `flushPromises()` засеять exposed-state: `vm.<stateRef>.push(...<MOCK_FIXTURE>.map(clone))`. Для seed может потребоваться расширить `defineExpose` (см. задачи).
2. **Тесты, явно ассертящие fake-fallback как поведение** (напр. `AdminBillingViewApi.spec.ts:96-106` — `expect(vm.rowsState.length).toBeGreaterThan(0)` после reject, заголовок «MOCK fallback остаётся»). **Инвертировать**: `toBe(0)`, заголовок → «state пустой».
3. **Новый regression-тест на каждый production-файл:** API/loadX отклоняется → state пустой (`length === 0`) **и** error-видим (`fetchError`/alert). См. red-green в шагах задач.

Каждая задача завершается полным зелёным прогоном `npm run test:vue` (0 fail) + `npm run lint:vue` + `npm run type-check`.

---

## Task 1: DealsView + KanbanView — убрать MOCK_DEALS fallback

**Files:**

- Modify: `app/resources/js/views/DealsView.vue`
- Modify: `app/resources/js/views/KanbanView.vue`
- Test: `app/tests/Frontend/DealsView.spec.ts`, `DealsViewRedesign.spec.ts`, `KanbanView.spec.ts`

- [ ] **Step 1: Regression-тест на пустой state при ошибке (DealsView) — должен упасть**

В `DealsView.spec.ts` добавить (рядом с C3-тестами в конце файла):

```ts
test('I3: loadDeals reject → dealsState пустой + fetchError', async () => {
    vi.spyOn(dealsApi, 'listDeals').mockRejectedValue(new Error('500'));
    const wrapper = await mountDeals();
    const auth = useAuthStore();
    auth.user = { id: 1, tenant_id: 42, email: 't@t.io' } as AuthUser;
    const vm = wrapper.vm as unknown as { loadDeals: () => Promise<void>; dealsState: unknown[]; fetchError: boolean };
    await vm.loadDeals();
    await flushPromises();
    expect(vm.dealsState.length).toBe(0);
    expect(vm.fetchError).toBe(true);
});
```

- [ ] **Step 2: Прогнать — убедиться, что падает**

Run: `cd app && npm run test:vue -- DealsView.spec.ts`
Expected: новый тест FAIL (`dealsState.length` = длина `MOCK_DEALS`, не 0).

- [ ] **Step 3: DealsView.vue — init пустой**

Импорт (строка 18) — убрать `MOCK_DEALS`:

```ts
import { DEALS_TABS, type MockDeal } from '../composables/mockDeals';
```

Init `dealsState` (строка 113):

```ts
// Локальная reactive-копия. Наполняется через API (см. loadDeals/onMounted).
// До загрузки и при ошибке — пустой массив; ошибка показывается через fetchError.
const dealsState = reactive<MockDeal[]>([]);
```

Catch в `loadDeals` (строка 133) — комментарий:

```ts
    } catch {
        fetchError.value = true; // state остаётся пустым — показываем error-alert
    }
```

Шаблон, alert `fetch-error-alert` (строки ~714-724) — текст:

```
            Не удалось загрузить сделки. Попробуйте обновить.
```

Doc-комментарий вверху файла — строку `MVP: page-head + chiprow со срезами + поиск + v-data-table с mock'ами.` поправить на `... + v-data-table (данные из API).`

- [ ] **Step 4: KanbanView.vue — init пустой**

Импорт (строка 23):

```ts
import { type MockDeal } from '../composables/mockDeals';
```

Init `dealsByStatus` (строки 49-54):

```ts
const dealsByStatus = reactive<Record<string, MockDeal[]>>(
    LEAD_STATUSES.reduce<Record<string, MockDeal[]>>((acc, s) => {
        acc[s.slug] = [];
        return acc;
    }, {}),
);
```

`totalDeals` (строка 111):

```ts
const totalDeals = ref(0);
```

Catch в `loadDeals` (строка 142) — комментарий: `fetchError.value = true; // state остаётся пустым — показываем error-alert`
Alert `fetch-error-alert` (строки ~199-209) — текст: `Не удалось загрузить сделки. Попробуйте обновить.`

- [ ] **Step 5: Аналогичный regression-тест для KanbanView**

В `KanbanView.spec.ts` добавить тест: замокать `dealsApi.listDeals` reject, выставить `auth.user` с `tenant_id`, вызвать `vm.loadDeals()`, проверить что все массивы `dealsByStatus` пусты (`Object.values(vm.dealsByStatus).every(c => c.length === 0)`) и `vm.fetchError === true`.

- [ ] **Step 6: Починить существующие тесты (DealsView.spec.ts, DealsViewRedesign.spec.ts, KanbanView.spec.ts)**

Многие тесты используют `MOCK_DEALS` как фикстуру (рендер строк, `applyBulkStatus`, фильтры «Окна Москва»/«Иван П.», `route.query.openId=MOCK_DEALS[0].id`). Применить **Вариант B**: в mount-хелперах (`mountDeals`, `mountDealsViewAt`, аналог в KanbanView) после `mount` засеять state из mock-фикстуры:

```ts
const wrapper = mount(DealsView, { /* ... */ });
await flushPromises();
const vm = wrapper.vm as unknown as { dealsState: MockDeal[] };
vm.dealsState.push(...MOCK_DEALS.map((d) => ({ ...d, manager: { ...d.manager } })));
await flushPromises();
return wrapper;
```

`MOCK_DEALS` уже импортируется в `DealsView.spec.ts`. Для KanbanView — засеять `dealsByStatus` по slug'ам. Тесты на новый-deal/bulk/фильтры/openId после seed работают как раньше. Прогонять после правки каждого спека.

- [ ] **Step 7: Полный прогон + линт**

Run: `cd app && npm run test:vue -- DealsView KanbanView && npm run lint:vue && npm run type-check`
Expected: все DealsView/KanbanView спеки PASS (0 fail), ESLint 0, vue-tsc 0.

- [ ] **Step 8: Commit**

```bash
git add app/resources/js/views/DealsView.vue app/resources/js/views/KanbanView.vue app/tests/Frontend/DealsView.spec.ts app/tests/Frontend/DealsViewRedesign.spec.ts app/tests/Frontend/KanbanView.spec.ts
git commit -m "fix(deals): I3 — убрать MOCK_DEALS fallback в DealsView/KanbanView"
```

---

## Task 2: NewDealDialog + DealDetailDrawer — убрать mock-fallback

**Files:**

- Modify: `app/resources/js/components/deals/NewDealDialog.vue`
- Modify: `app/resources/js/components/deals/DealDetailDrawer.vue`
- Test: spec-файлы NewDealDialog / DealDetailDrawer (уточнить `ls tests/Frontend | grep -E "NewDeal|DealDetailDrawer"`)

- [ ] **Step 1: NewDealDialog.vue — пустые опции**

Импорт (строка 17):

```ts
import { type MockDeal, type MockManager } from '../../composables/mockDeals';
```

`projectOptions`/`managerOptions` (строки 24-25):

```ts
const projectOptions = ref<string[]>([]);
const managerOptions = ref<MockManager[]>([]);
```

Doc-комментарий блока (строки 19-23) — переписать без «fallback на MOCK»:

```ts
/**
 * Списки проектов и менеджеров грузятся с backend через GET /api/projects,
 * /api/managers при открытии диалога (если передан tenantId). На fail —
 * списки пустые + degradation-alter (lookupsFailed), создание блокируется
 * до повторной успешной загрузки.
 */
```

Комментарий строки 80 → `// Audit C6: loadLookups упал → показываем degradation-alert (списки пусты).`
Alert `lookups-error-alert` (строки ~207-210) — текст:

```
                    Не удалось загрузить списки проектов и менеджеров — попробуйте позже.
```

`defineExpose` (строка 178) — добавить `projectOptions`, `managerOptions` для seed в тестах:

```ts
defineExpose({ lookupsFailed, projectOptions, managerOptions });
```

- [ ] **Step 2: DealDetailDrawer.vue — пустой timeline**

Импорт (строка 23):

```ts
import { type DealEvent } from '../../composables/mockDealEvents';
```

`events` init (строка 60):

```ts
const events = ref<DealEvent[]>([]);
```

`loadEvents` — путь без deal/tenantId (строка 119): `events.value = [];`
`loadEvents` — catch (строка 131): `events.value = [];`
Комментарий строки 59 → `// показываем реальные events. На fail / без tenant_id — events пуст + eventsFetchError.`

- [ ] **Step 3: Regression-тесты (red-green)**

NewDealDialog spec: тест — открыть диалог **без** `tenantId`, проверить `vm.projectOptions.length === 0` и `vm.managerOptions.length === 0` (раньше были mock). DealDetailDrawer spec: тест — `loadEvents` без tenantId / при reject `getDeal` → `vm.events.length === 0`. Сначала прогнать (увидеть fail на старом коде — но код уже правится в Step 1-2, поэтому: написать тест → если spec'и проверяют наличие mock-данных, они упадут до правки; порядок — допустимо написать тест и правку вместе, ключ: после правки тест зелёный и проверяет именно пустоту).

- [ ] **Step 4: Починить существующие тесты**

NewDealDialog: тесты submit-flow выбирают проект/менеджера — засеять `vm.projectOptions`/`vm.managerOptions` после mount (Вариант B), либо замокать `dealsApi.listProjects`/`listManagers` резолвом с фикстурой (Вариант A) при открытии с `tenantId`. DealDetailDrawer: тесты timeline — замокать `dealsApi.getDeal` резолвом с events, либо засеять `vm.events`. Тесты на `events-fetch-error-alert` (Sprint 5B C7) — events уже пуст при ошибке, проверить что alert по-прежнему рендерится.

- [ ] **Step 5: Полный прогон + линт**

Run: `cd app && npm run test:vue -- NewDealDialog DealDetailDrawer && npm run lint:vue && npm run type-check`
Expected: PASS 0 fail, ESLint 0, vue-tsc 0.

- [ ] **Step 6: Commit**

```bash
git add app/resources/js/components/deals/NewDealDialog.vue app/resources/js/components/deals/DealDetailDrawer.vue app/tests/Frontend/
git commit -m "fix(deals): I3 — убрать mock-fallback в NewDealDialog/DealDetailDrawer"
```

---

## Task 3: AdminBillingView + AdminIncidentsView — убрать mockAdmin fallback

**Files:**

- Modify: `app/resources/js/views/admin/AdminBillingView.vue`
- Modify: `app/resources/js/views/admin/AdminIncidentsView.vue`
- Test: `app/tests/Frontend/AdminBillingView.spec.ts`, `AdminBillingViewApi.spec.ts`, `AdminIncidentsView.spec.ts`, `AdminIncidentsViewApi.spec.ts`

- [ ] **Step 1: AdminBillingView.vue — init пустой**

Удалить импорт (строка 11) `import { ADMIN_BILLING_SUMMARY as MOCK_SUMMARY, ADMIN_BILLING_TENANTS } from '../../composables/mockAdmin';` целиком.
`rowsState` (строки 37-49):

```ts
const rowsState = reactive<BillingRow[]>([]);
```

`summary` (строки 51-56):

```ts
const summary = reactive({
    total_mrr_rub: 0,
    monthly_revenue_rub: 0,
    overdue_count: 0,
    refunds_count_30d: 0,
});
```

Doc-комментарий вверху (строки 8-9): `MVP — только display-вьюха с mock-данными.` → `Данные грузятся с backend GET /api/admin/billing.`
Комментарий строки 20-24 (над `BillingRow`) — убрать упоминание «initial = MOCK».
Alert `fetch-error-alert` (строки ~249-259) — текст: `Не удалось загрузить биллинг. Попробуйте обновить.`

- [ ] **Step 2: AdminIncidentsView.vue — init пустой**

Удалить импорт (строка 12) `import { ADMIN_INCIDENTS } from '../../composables/mockAdmin';`.
`rowsState` (строки 77-90):

```ts
const rowsState = reactive<IncidentRow[]>([]);
```

Удалить блок initial-stats из mock (строки 96-100 — `stats.open = rowsState.filter(...)` ×3). `stats` остаётся инициализированным нулями на строке 91.
Комментарий строки 76 (`// Reactive — initial = MOCK; replace на API на mount.`) → `// Reactive — наполняется через loadIncidents (API).`
Комментарий строки 95 (`// Initial stats из mock ...`) — удалить вместе с блоком.
Doc-комментарий (строки 8-9): `MVP — display + фильтр ...` → `Display + фильтр по статусу/severity. Данные с backend GET /api/admin/incidents.`
Alert `fetch-error-alert` (строки ~170-180) — текст: `Не удалось загрузить инциденты. Попробуйте обновить.`

- [ ] **Step 3: Тесты — инвертировать fake-fallback ассерты + regression**

`AdminBillingViewApi.spec.ts:96-106` — тест `'reject → fetchError=true + alert виден + MOCK fallback остаётся'`:

- заголовок → `'reject → fetchError=true + alert виден + rowsState пустой'`
- `expect(vm.rowsState.length).toBeGreaterThan(0);` → `expect(vm.rowsState.length).toBe(0);`
Аналогично проверить `AdminIncidentsViewApi.spec.ts` на наличие «MOCK fallback»-ассертов — инвертировать.
Smoke-спеки `AdminBillingView.spec.ts` / `AdminIncidentsView.spec.ts`: если рендерят строки из mock-init — применить Вариант A (замокать `adminApi.listAdminBilling`/`listAdminIncidents` резолвом фикстуры — фикстуру взять из `composables/mockAdmin` импортом в спеке) или Вариант B (seed `vm.rowsState`). Добавить regression-тест где ещё нет: reject → `rowsState.length === 0` + `fetchError`.

- [ ] **Step 4: Полный прогон + линт**

Run: `cd app && npm run test:vue -- AdminBilling AdminIncidents && npm run lint:vue && npm run type-check`
Expected: PASS 0 fail, ESLint 0, vue-tsc 0.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/views/admin/AdminBillingView.vue app/resources/js/views/admin/AdminIncidentsView.vue app/tests/Frontend/
git commit -m "fix(admin): I3 — убрать mockAdmin fallback в Billing/Incidents"
```

---

## Task 4: AdminSystemView + AdminTenantsView — убрать mock fallback

**Files:**

- Modify: `app/resources/js/views/admin/AdminSystemView.vue`
- Modify: `app/resources/js/views/admin/AdminTenantsView.vue`
- Test: `app/tests/Frontend/AdminSystemView.spec.ts`, `AdminTenantsView.spec.ts`, `AdminTenantsViewApi.spec.ts`

- [ ] **Step 1: AdminSystemView.vue — init пустой**

Удалить импорт mock-данных (строка 11) `import { ADMIN_SYSTEM_SETTINGS } from '../../composables/mockAdmin';`.
**Оставить** импорт типа (строка 12) `import type { AdminSystemSetting } from '../../composables/mockAdmin';` — тип используется.
`settingsState` (строка 30):

```ts
const settingsState = reactive<AdminSystemSetting[]>([]);
```

Комментарий строки 23-29 (над `settingsState`) — убрать «Инициируется mock-данными (fallback...)»:

```ts
/**
 * Settings-state. Наполняется на mount через `adminApi.listSystemSettings()`.
 * До загрузки и при ошибке — пустой; ошибка показывается через fetchError-banner.
 */
```

Catch в `loadSettings` (строка 41) — текст fallback в `extractErrorMessage`:

```ts
        fetchError.value = extractErrorMessage(err, 'Не удалось загрузить настройки с сервера. Попробуйте обновить.');
```

Комментарий строки 39-40 (`// На fail оставляем mock ...`) → `// На fail — settingsState пустой, показываем error-banner.`
Doc-комментарий (строки 8-9): `MVP — display + read-only edit-режим.` → `Display + edit-режим. Данные с backend GET /api/admin/system-settings.`

- [ ] **Step 2: AdminTenantsView.vue — init пустой**

Импорт (строка 18) — убрать `MOCK_STATS`, `MOCK_TENANTS`:

```ts
import { type AdminTenant, type TenantStatus } from '../../composables/mockTenants';
```

`tenantsState` (строка 32):

```ts
const tenantsState = reactive<AdminTenant[]>([]);
```

`stats` (строка 33) — заменить `{ ...MOCK_STATS }` объектом с теми же ключами в нулях. **Сверить точную форму `MOCK_STATS` в `composables/mockTenants.ts`** (`loadTenants` пишет `total/active/trial/overdue`):

```ts
const stats = reactive({ total: 0, active: 0, trial: 0, overdue: 0 });
```

Alert `fetch-error-alert` (строки ~117-127) — текст: `Не удалось загрузить тенантов. Попробуйте обновить.`

- [ ] **Step 3: Тесты — починить + regression**

`AdminTenantsViewApi.spec.ts` — проверить на «MOCK fallback»-ассерты после reject, инвертировать на `length === 0`.
Smoke-спеки `AdminSystemView.spec.ts` / `AdminTenantsView.spec.ts` — рендер строк из mock-init: Вариант A (мок `adminApi.listSystemSettings`/`listAdminTenants` резолвом фикстуры) или B (seed `vm.settingsState`/`vm.tenantsState`). Regression-тест: reject → state пустой + ошибка видна (`AdminSystemView.fetchError` — это `string|null`, при ошибке непустая строка; `AdminTenantsView.fetchError` — boolean).

- [ ] **Step 4: Полный прогон + линт**

Run: `cd app && npm run test:vue -- AdminSystem AdminTenants && npm run lint:vue && npm run type-check`
Expected: PASS 0 fail, ESLint 0, vue-tsc 0.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/views/admin/AdminSystemView.vue app/resources/js/views/admin/AdminTenantsView.vue app/tests/Frontend/
git commit -m "fix(admin): I3 — убрать mock fallback в System/Tenants"
```

---

## Task 5: I4 — ImpersonationDialog devPlainCode за DEV-gate

**Files:**

- Modify: `app/resources/js/components/admin/ImpersonationDialog.vue`
- Test: `app/tests/Frontend/ImpersonationDialog*.spec.ts` (уточнить `ls`)

Контекст: баннер `data-testid="dev-code-banner"` (строки ~218-228) показывает `_dev_plain_code` (плейн-код impersonation) при `v-if="devPlainCode"`. Сейчас гейт — только наличие данных (backend на prod не отдаёт `_dev_plain_code`). Аудит I4: добавить явный frontend DEV-gate, чтобы баннер не отрисовывался в prod-сборке даже если бэк случайно отдаст код.

- [ ] **Step 1: Regression-тест (red) — баннер скрыт в prod**

В spec ImpersonationDialog добавить тест: `vi.stubEnv('DEV', false)` **до** mount → пройти flow до step `verify` с непустым `devPlainCode` (замокать `adminApi.impersonationInit` резолвом с `_dev_plain_code: '123456'`) → `expect(wrapper.find('[data-testid="dev-code-banner"]').exists()).toBe(false)`. `afterEach(() => vi.unstubAllEnvs())`.

- [ ] **Step 2: Прогон — упадёт**

Run: `cd app && npm run test:vue -- ImpersonationDialog`
Expected: новый тест FAIL (баннер рендерится — гейт только по `devPlainCode`).

- [ ] **Step 3: ImpersonationDialog.vue — DEV-gate**

В `<script setup>` после `const devPlainCode = ref<string | null>(null);` (строка 49) добавить:

```ts
// I4: явный frontend DEV-gate. import.meta.env.DEV статически заменяется Vite —
// в prod-сборке = false, баннер с плейн-кодом tree-shake'ится.
const isDevEnv = import.meta.env.DEV;
```

`defineExpose` отсутствует — не добавлять (тест проверяет через DOM).
Шаблон, баннер (строка 219) — гейт:

```html
                    <v-alert
                        v-if="isDevEnv && devPlainCode"
```

Doc-комментарий (строка 8) — уточнить: `На dev показывается _dev_plain_code (за import.meta.env.DEV; на prod — баннер не рендерится).`

- [ ] **Step 4: Прогон — зелёный**

Run: `cd app && npm run test:vue -- ImpersonationDialog`
Expected: новый тест PASS. Существующие тесты (в Vitest `import.meta.env.DEV === true`) — баннер по-прежнему виден при `devPlainCode`, PASS.

- [ ] **Step 5: Линт + type-check**

Run: `cd app && npm run lint:vue && npm run type-check`
Expected: ESLint 0, vue-tsc 0.

- [ ] **Step 6: Commit**

```bash
git add app/resources/js/components/admin/ImpersonationDialog.vue app/tests/Frontend/
git commit -m "fix(admin): I4 — devPlainCode-баннер за import.meta.env.DEV"
```

---

## Self-Review (контроллер, после всех задач)

- **Покрытие спека:** I3 — 8 production-файлов init→пусто + error-текст (✓ T1-T4). I4 — DEV-gate (✓ T5). I1 — отложен (вне scope).
- **Нет fake-data в prod-путях:** grep `MOCK_|ADMIN_` по `resources/js/views` + `resources/js/components/deals` + `ImpersonationDialog` — 0 совпадений в production-импортах (только типы). `mock*.ts` не удалены — типы/константы (`MockDeal`, `DEALS_TABS`, `AdminTenant`...) живы.
- **Тесты:** полный `npm run test:vue` зелёный, 0 fail; новые regression-тесты на каждый файл; инвертированы явные «MOCK fallback» ассерты.
- **Регрессия:** `npm run lint:vue` 0, `npm run type-check` 0, Pest не затронут (только frontend).
