# Командный центр — Этап 1 (Implementation Plan)

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

**Goal:** Новый SaaS-admin экран «Командный центр» (landing) с 4 плитками-светофорами и проваливанием в детали; в Этапе 1 наполняем области **Финансы** и **Здоровье**.

**Architecture:** Backend — один `AdminDashboardController` с агрегирующими эндпоинтами в группе `['saas-admin','admin-db']` (cross-tenant через `pgsql_admin`). Frontend — Vue-роут `/admin/dashboard` (`AdminDashboardView.vue`), плитки + drill-панели как в макете `web/admin-dashboard-mockup.html`. Переиспользуем существующие detail-экраны как Уровень 3. Без новых таблиц/миграций.

**Tech Stack:** Laravel 13, PostgreSQL 16, Pest 4 (backend), Vue 3 + Vuetify 3 + Vitest (frontend), Forest-палитра.

**Spec:** `docs/superpowers/specs/2026-06-27-admin-command-center-design.md`

**Базовая ветка:** `feat/admin-command-center` (содержит фикс admin-db / `pgsql_admin`). На диске есть **несвязанная** незакоммиченная правка (лимиты проектов) — НЕ трогать, коммитить только файлы из задач ниже по явным путям.

---

## File Structure

- Create: `app/app/Http/Controllers/Api/AdminDashboardController.php` — агрегаты L1 + L2 (Финансы, Здоровье). Одна ответственность: read-only сводки для дашборда.
- Modify: `app/routes/web.php` — 3 маршрута в группе `['saas-admin','admin-db']`.
- Create tests: `app/tests/Feature/Admin/AdminDashboardSummaryTest.php`, `AdminDashboardFinanceTest.php`, `AdminDashboardHealthTest.php`.
- Create: `app/resources/js/views/admin/AdminDashboardView.vue` — L1 плитки + drill-области.
- Create: `app/resources/js/api/adminDashboard.ts` — типизированный клиент 3 эндпоинтов.
- Modify: `app/resources/js/router/index.ts` — роут `/admin/dashboard` (+ редирект `/admin` на него).
- Create test: `app/tests/Frontend/AdminDashboardView.spec.ts`.

Все команды — из каталога `app/`.

---

## ЧАСТЬ A — BACKEND

### Task 1: Эндпоинт сводки L1 — `AdminDashboardController::summary`

**Files:**
- Create: `app/app/Http/Controllers/Api/AdminDashboardController.php`
- Test: `app/tests/Feature/Admin/AdminDashboardSummaryTest.php`

- [ ] **Step 1: Write the failing test**

Create `app/tests/Feature/Admin/AdminDashboardSummaryTest.php`:

```php
<?php

declare(strict_types=1);

use Illuminate\Support\Facades\DB;

it('GET /api/admin/dashboard returns finance + health tiles', function () {
    $tenant = DB::table('tenants')->insertGetId([
        'subdomain' => 'acme', 'organization_name' => 'Acme', 'contact_email' => 'a@acme.ru',
        'status' => 'active', 'is_trial' => false, 'balance_rub' => -500, 'balance_leads' => 0,
        'chargeback_unrecovered_rub' => 0, 'created_at' => now(), 'updated_at' => now(),
    ]);
    DB::table('balance_transactions')->insert([
        ['tenant_id' => $tenant, 'type' => 'topup', 'amount_rub' => 10000, 'created_at' => now(), 'updated_at' => now()],
        ['tenant_id' => $tenant, 'type' => 'lead_charge', 'amount_rub' => -3000, 'created_at' => now(), 'updated_at' => now()],
    ]);

    $res = $this->getJson('/api/admin/dashboard?period=30d');

    $res->assertOk();
    $res->assertJsonStructure([
        'finance' => ['topups_rub', 'charges_rub', 'active_clients', 'new_clients', 'negative_balance_count', 'light'],
        'health' => ['light', 'open_incidents', 'last_sync_status'],
        'period',
    ]);
    expect($res->json('finance.negative_balance_count'))->toBe(1);
    expect($res->json('finance.light'))->toBe('red'); // есть клиент в минусе
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `php artisan test tests/Feature/Admin/AdminDashboardSummaryTest.php`
Expected: FAIL — route `/api/admin/dashboard` not defined (404) / controller missing.

- [ ] **Step 3: Create the controller with `summary()`**

Create `app/app/Http/Controllers/Api/AdminDashboardController.php`:

```php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

/**
 * SaaS-admin «Командный центр» — read-only агрегаты для дашборда.
 * Под группой ['saas-admin','admin-db'] → cross-tenant через pgsql_admin.
 * Spec: docs/superpowers/specs/2026-06-27-admin-command-center-design.md
 */
class AdminDashboardController extends Controller
{
    /** Период из query: today | 7d | 30d (дефолт 7d). */
    private function periodStart(Request $request): Carbon
    {
        return match ((string) $request->query('period', '7d')) {
            'today' => now()->startOfDay(),
            '30d' => now()->subDays(30),
            default => now()->subDays(7),
        };
    }

    /** GET /api/admin/dashboard — сводка L1 (плитки Финансы + Здоровье). */
    public function summary(Request $request): JsonResponse
    {
        $from = $this->periodStart($request);

        return response()->json([
            'period' => (string) $request->query('period', '7d'),
            'finance' => $this->financeTile($from),
            'health' => $this->healthTile(),
        ]);
    }

    /** @return array<string,mixed> */
    private function financeTile(Carbon $from): array
    {
        $topups = (float) DB::table('balance_transactions')
            ->where('type', 'topup')->where('created_at', '>=', $from)->sum('amount_rub');
        $charges = (float) DB::table('balance_transactions')
            ->where('type', 'lead_charge')->where('created_at', '>=', $from)->sum('amount_rub');
        $active = DB::table('tenants')->where('status', 'active')->whereNull('deleted_at')->count();
        $newClients = DB::table('tenants')->where('created_at', '>=', $from)->whereNull('deleted_at')->count();
        $negative = DB::table('tenants')->whereNull('deleted_at')->where('balance_rub', '<', 0)->count();

        return [
            'topups_rub' => (string) $topups,
            'charges_rub' => (string) abs($charges),
            'active_clients' => $active,
            'new_clients' => $newClients,
            'negative_balance_count' => $negative,
            'light' => $negative > 0 ? 'red' : 'green',
        ];
    }

    /** @return array<string,mixed> */
    private function healthTile(): array
    {
        $openIncidents = DB::table('incidents_log')->whereNull('resolved_at')->count();
        $lastSync = DB::table('supplier_sync_runs')->orderByDesc('id')->first();
        $failedJobs = DB::table('failed_jobs')->where('failed_at', '>=', now()->subDay())->count();

        $light = 'green';
        if ($openIncidents > 0 || $failedJobs > 0
            || ($lastSync !== null && in_array($lastSync->status, ['failed', 'aborted'], true))) {
            $light = 'red';
        }

        return [
            'light' => $light,
            'open_incidents' => $openIncidents,
            'failed_jobs_24h' => $failedJobs,
            'last_sync_status' => $lastSync->status ?? 'none',
            'last_sync_at' => $lastSync->finished_at ?? null,
        ];
    }
}
```

- [ ] **Step 4: Register the route (so the test can hit it)**

In `app/routes/web.php`, inside the existing `Route::middleware(['saas-admin', 'admin-db'])->group(function () {` block, add:

```php
    // Командный центр (дашборд) — read-only агрегаты.
    Route::get('/api/admin/dashboard', 'App\Http\Controllers\Api\AdminDashboardController@summary');
```

- [ ] **Step 5: Run test to verify it passes**

Run: `php artisan test tests/Feature/Admin/AdminDashboardSummaryTest.php`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add app/app/Http/Controllers/Api/AdminDashboardController.php app/routes/web.php app/tests/Feature/Admin/AdminDashboardSummaryTest.php
git commit -m "feat(дашборд): эндпоинт сводки L1 (Финансы+Здоровье)"
```

---

### Task 2: Финансы L2 — `AdminDashboardController::finance`

**Files:**
- Modify: `app/app/Http/Controllers/Api/AdminDashboardController.php`
- Modify: `app/routes/web.php`
- Test: `app/tests/Feature/Admin/AdminDashboardFinanceTest.php`

- [ ] **Step 1: Write the failing test**

Create `app/tests/Feature/Admin/AdminDashboardFinanceTest.php`:

```php
<?php

declare(strict_types=1);

use Illuminate\Support\Facades\DB;

it('GET /api/admin/dashboard/finance returns KPIs + attention + top tables', function () {
    $neg = DB::table('tenants')->insertGetId([
        'subdomain' => 'neg', 'organization_name' => 'Negative', 'contact_email' => 'n@x.ru',
        'status' => 'active', 'is_trial' => false, 'balance_rub' => -100, 'balance_leads' => 0,
        'chargeback_unrecovered_rub' => 0, 'created_at' => now(), 'updated_at' => now(),
    ]);
    DB::table('balance_transactions')->insert([
        'tenant_id' => $neg, 'type' => 'topup', 'amount_rub' => 5000, 'created_at' => now(), 'updated_at' => now(),
    ]);

    $res = $this->getJson('/api/admin/dashboard/finance?period=30d');

    $res->assertOk();
    $res->assertJsonStructure([
        'kpi' => ['topups_rub', 'charges_rub', 'net_inflow_rub', 'negative_balance_count'],
        'attention', 'top_by_turnover', 'period',
    ]);
    expect($res->json('kpi.negative_balance_count'))->toBe(1);
    expect(collect($res->json('attention'))->pluck('organization_name'))->toContain('Negative');
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `php artisan test tests/Feature/Admin/AdminDashboardFinanceTest.php`
Expected: FAIL — route/method missing (404).

- [ ] **Step 3: Add `finance()` method**

In `AdminDashboardController.php`, add:

```php
    /** GET /api/admin/dashboard/finance — детали Финансов (L2). */
    public function finance(Request $request): JsonResponse
    {
        $from = $this->periodStart($request);

        $topups = (float) DB::table('balance_transactions')
            ->where('type', 'topup')->where('created_at', '>=', $from)->sum('amount_rub');
        $charges = abs((float) DB::table('balance_transactions')
            ->where('type', 'lead_charge')->where('created_at', '>=', $from)->sum('amount_rub'));

        // «Требуют внимания»: баланс < 0 (по возрастанию — самые глубокие минусы сверху).
        $attention = DB::table('tenants')->whereNull('deleted_at')
            ->where('balance_rub', '<', 0)
            ->orderBy('balance_rub')
            ->limit(20)
            ->get(['id', 'subdomain', 'organization_name', 'balance_rub', 'balance_leads'])
            ->map(fn ($t) => [
                'id' => (int) $t->id,
                'subdomain' => $t->subdomain,
                'organization_name' => $t->organization_name,
                'balance_rub' => (string) $t->balance_rub,
                'state' => 'negative',
            ]);

        // Топ по обороту: сумма пополнений за период.
        $top = DB::table('balance_transactions')
            ->join('tenants', 'tenants.id', '=', 'balance_transactions.tenant_id')
            ->where('balance_transactions.type', 'topup')
            ->where('balance_transactions.created_at', '>=', $from)
            ->whereNull('tenants.deleted_at')
            ->groupBy('tenants.id', 'tenants.organization_name')
            ->orderByRaw('SUM(balance_transactions.amount_rub) DESC')
            ->limit(10)
            ->get([
                'tenants.id',
                'tenants.organization_name',
                DB::raw('SUM(balance_transactions.amount_rub) AS topped_rub'),
            ])
            ->map(fn ($r) => [
                'id' => (int) $r->id,
                'organization_name' => $r->organization_name,
                'topped_rub' => (string) $r->topped_rub,
            ]);

        return response()->json([
            'period' => (string) $request->query('period', '7d'),
            'kpi' => [
                'topups_rub' => (string) $topups,
                'charges_rub' => (string) $charges,
                'net_inflow_rub' => (string) ($topups - $charges),
                'negative_balance_count' => $attention->count(),
            ],
            'attention' => $attention,
            'top_by_turnover' => $top,
        ]);
    }
```

- [ ] **Step 4: Register the route**

In `app/routes/web.php`, in the same admin group, add after the dashboard summary route:

```php
    Route::get('/api/admin/dashboard/finance', 'App\Http\Controllers\Api\AdminDashboardController@finance');
```

- [ ] **Step 5: Run test to verify it passes**

Run: `php artisan test tests/Feature/Admin/AdminDashboardFinanceTest.php`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add app/app/Http/Controllers/Api/AdminDashboardController.php app/routes/web.php app/tests/Feature/Admin/AdminDashboardFinanceTest.php
git commit -m "feat(дашборд): эндпоинт Финансы L2 (KPI + внимание + топ)"
```

---

### Task 3: Здоровье L2 — `AdminDashboardController::health`

**Files:**
- Modify: `app/app/Http/Controllers/Api/AdminDashboardController.php`
- Modify: `app/routes/web.php`
- Test: `app/tests/Feature/Admin/AdminDashboardHealthTest.php`

- [ ] **Step 1: Write the failing test**

Create `app/tests/Feature/Admin/AdminDashboardHealthTest.php`:

```php
<?php

declare(strict_types=1);

use Illuminate\Support\Facades\DB;

it('GET /api/admin/dashboard/health returns 6 subsystems with light', function () {
    $res = $this->getJson('/api/admin/dashboard/health');

    $res->assertOk();
    $res->assertJsonStructure([
        'subsystems' => [['key', 'light', 'detail']],
        'overall_light',
    ]);
    $keys = collect($res->json('subsystems'))->pluck('key')->all();
    expect($keys)->toContain('queues', 'scheduler', 'supplier_sync', 'csv_drift', 'webhooks', 'incidents');
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `php artisan test tests/Feature/Admin/AdminDashboardHealthTest.php`
Expected: FAIL — route/method missing.

- [ ] **Step 3: Add `health()` method**

In `AdminDashboardController.php`, add:

```php
    /** GET /api/admin/dashboard/health — 6 подсистем эксплуатации (L2). */
    public function health(): JsonResponse
    {
        $failedJobs = DB::table('failed_jobs')->where('failed_at', '>=', now()->subDay())->count();
        $lastSync = DB::table('supplier_sync_runs')->orderByDesc('id')->first();
        $lastReconcile = DB::table('supplier_csv_reconcile_log')->orderByDesc('id')->first();
        $unresolvedWebhooks = DB::table('failed_webhook_jobs')->whereNull('resolved_at')->count();
        $openIncidents = DB::table('incidents_log')->whereNull('resolved_at')->count();
        $staleHeartbeat = DB::table('scheduler_heartbeats')->where('consecutive_failures', '>', 0)->count();

        $subsystems = [
            ['key' => 'queues',        'light' => $failedJobs > 0 ? 'amber' : 'green',
             'detail' => $failedJobs.' упавших за сутки'],
            ['key' => 'scheduler',     'light' => $staleHeartbeat > 0 ? 'red' : 'green',
             'detail' => $staleHeartbeat > 0 ? $staleHeartbeat.' задач с пропусками' : 'по расписанию'],
            ['key' => 'supplier_sync', 'light' => ($lastSync && in_array($lastSync->status, ['failed','aborted'], true)) ? 'red' : 'green',
             'detail' => 'последний: '.($lastSync->status ?? 'нет')],
            ['key' => 'csv_drift',     'light' => ($lastReconcile && $lastReconcile->status === 'drift_alert') ? 'red' : 'green',
             'detail' => 'статус: '.($lastReconcile->status ?? 'нет')],
            ['key' => 'webhooks',      'light' => $unresolvedWebhooks > 0 ? 'amber' : 'green',
             'detail' => $unresolvedWebhooks.' неразобранных'],
            ['key' => 'incidents',     'light' => $openIncidents > 0 ? 'red' : 'green',
             'detail' => $openIncidents.' открытых'],
        ];

        $order = ['green' => 0, 'amber' => 1, 'red' => 2];
        $overall = collect($subsystems)->sortByDesc(fn ($s) => $order[$s['light']])->first()['light'];

        return response()->json(['subsystems' => $subsystems, 'overall_light' => $overall]);
    }
```

- [ ] **Step 4: Register the route**

In `app/routes/web.php`, same admin group:

```php
    Route::get('/api/admin/dashboard/health', 'App\Http\Controllers\Api\AdminDashboardController@health');
```

- [ ] **Step 5: Run test to verify it passes**

Run: `php artisan test tests/Feature/Admin/AdminDashboardHealthTest.php`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add app/app/Http/Controllers/Api/AdminDashboardController.php app/routes/web.php app/tests/Feature/Admin/AdminDashboardHealthTest.php
git commit -m "feat(дашборд): эндпоинт Здоровье L2 (6 подсистем + светофор)"
```

---

### Task 4: Backend self-review

- [ ] **Step 1:** Run admin slice — `php artisan test tests/Feature/Admin` → PASS (новые 3 + существующие admin-тесты не сломаны).
- [ ] **Step 2:** `composer stan` → 0 ошибок в `AdminDashboardController.php` (типизировать `@return array<string,mixed>`; для `config()`/`first()`->mixed добавить аккуратные касты, как в Task 1 коде).
- [ ] **Step 3:** `composer pint -- app/Http/Controllers/Api/AdminDashboardController.php routes/web.php` → форматирование.
- [ ] **Step 4 (commit если pint менял):** `git add -A && git commit -m "chore(дашборд): pint/stan backend"`.

---

## ЧАСТЬ B — FRONTEND

### Task 5: API-клиент дашборда

**Files:**
- Create: `app/resources/js/api/adminDashboard.ts`
- Test: covered via view test (Task 7).

- [ ] **Step 1: Create the typed client**

Create `app/resources/js/api/adminDashboard.ts` (следовать стилю существующего `app/resources/js/api/admin.ts` — axios instance, типы):

```ts
import http from './http' // тот же axios-инстанс, что в api/admin.ts (проверить имя импорта в admin.ts)

export type Light = 'green' | 'amber' | 'red'

export interface DashboardSummary {
  period: string
  finance: {
    topups_rub: string; charges_rub: string; active_clients: number
    new_clients: number; negative_balance_count: number; light: Light
  }
  health: { light: Light; open_incidents: number; failed_jobs_24h: number; last_sync_status: string; last_sync_at: string | null }
}

export interface FinanceDetail {
  period: string
  kpi: { topups_rub: string; charges_rub: string; net_inflow_rub: string; negative_balance_count: number }
  attention: Array<{ id: number; subdomain: string; organization_name: string; balance_rub: string; state: string }>
  top_by_turnover: Array<{ id: number; organization_name: string; topped_rub: string }>
}

export interface HealthDetail {
  overall_light: Light
  subsystems: Array<{ key: string; light: Light; detail: string }>
}

export const adminDashboard = {
  summary: (period: string) => http.get<DashboardSummary>(`/api/admin/dashboard?period=${period}`).then(r => r.data),
  finance: (period: string) => http.get<FinanceDetail>(`/api/admin/dashboard/finance?period=${period}`).then(r => r.data),
  health: () => http.get<HealthDetail>('/api/admin/dashboard/health').then(r => r.data),
}
```

> NB при реализации: открыть `app/resources/js/api/admin.ts` и использовать ТОТ ЖЕ axios-импорт/паттерн (имя по факту — `http`/`api`/`client`), чтобы не плодить инстансы.

- [ ] **Step 2: Commit**

```bash
git add app/resources/js/api/adminDashboard.ts
git commit -m "feat(дашборд-fe): типизированный API-клиент"
```

### Task 6: Экран `AdminDashboardView.vue` + роут

**Files:**
- Create: `app/resources/js/views/admin/AdminDashboardView.vue`
- Modify: `app/resources/js/router/index.ts`

- [ ] **Step 1: Create the view (L1 плитки + drill Финансы/Здоровье)**

Create `app/resources/js/views/admin/AdminDashboardView.vue` — структура по макету `web/admin-dashboard-mockup.html`: период-тумблер (today/7d/30d), сетка 2×2 из 4 плиток (Финансы, Здоровье — живые из API; Лиды, Заказ — заглушки «Этап 2»), и панель деталей, переключаемая кликом по плитке. Использовать Vuetify 3 (`v-card`, `v-chip` для светофора, `v-table`), Forest-палитру. Светофор-цвет: green→`success`, amber→`warning`, red→`error`. Загрузка через `adminDashboard.summary/finance/health` в `onMounted` и при смене периода. Строки таблицы «внимание»/«топ» — `@click` → `router.push('/admin/tenants/'+subdomain)` (L3, существующий экран). Layout — `admin` (как у прочих admin-view, см. `meta.layout:'admin'`).

- [ ] **Step 2: Register the route**

In `app/resources/js/router/index.ts`, в admin-секции (рядом с `/admin/tenants`):

```ts
    {
        path: '/admin/dashboard',
        name: 'admin-dashboard',
        component: () => import('../views/admin/AdminDashboardView.vue'),
        meta: { layout: 'admin', title: 'Командный центр', requiresAuth: true, devIndex: 20, devLabel: 'Admin Dashboard' },
    },
```

И изменить редирект `/admin` с `/admin/tenants` на `/admin/dashboard`:
```ts
    { path: '/admin', redirect: '/admin/dashboard' },
```

- [ ] **Step 3: Commit**

```bash
git add app/resources/js/views/admin/AdminDashboardView.vue app/resources/js/router/index.ts
git commit -m "feat(дашборд-fe): экран Командного центра + роут /admin/dashboard"
```

### Task 7: Frontend-тест рендера

**Files:**
- Create: `app/tests/Frontend/AdminDashboardView.spec.ts`

- [ ] **Step 1: Write the test** (mock `adminDashboard`, проверить рендер 4 плиток + переключение области по клику; паттерн взять из `app/tests/Frontend/AdminTenantsView.spec.ts`).

```ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
vi.mock('@/api/adminDashboard', () => ({
  adminDashboard: {
    summary: () => Promise.resolve({ period: '7d',
      finance: { topups_rub: '1000', charges_rub: '500', active_clients: 5, new_clients: 2, negative_balance_count: 1, light: 'red' },
      health: { light: 'green', open_incidents: 0, failed_jobs_24h: 0, last_sync_status: 'ok', last_sync_at: null } }),
    finance: () => Promise.resolve({ period: '7d', kpi: { topups_rub:'1000', charges_rub:'500', net_inflow_rub:'500', negative_balance_count:1 }, attention: [], top_by_turnover: [] }),
    health: () => Promise.resolve({ overall_light: 'green', subsystems: [] }),
  },
}))
import AdminDashboardView from '@/views/admin/AdminDashboardView.vue'

describe('AdminDashboardView', () => {
  it('renders four tiles', async () => {
    const wrapper = mount(AdminDashboardView, { global: { stubs: { 'router-link': true } } })
    await new Promise(r => setTimeout(r))
    expect(wrapper.text()).toContain('Финансы')
    expect(wrapper.text()).toContain('Здоровье')
    expect(wrapper.text()).toContain('Лиды')
    expect(wrapper.text()).toContain('Заказ у поставщика')
  })
})
```

- [ ] **Step 2: Run** `npm run test:vue -- AdminDashboardView` → подгонять view, пока PASS.
- [ ] **Step 3: Commit** `git add app/tests/Frontend/AdminDashboardView.spec.ts && git commit -m "test(дашборд-fe): рендер плиток"`.

### Task 8: Frontend polish (опционально, через frontend-design)

- [ ] При желании отполировать вид через skill `frontend-design` (типографика/отступы/состояния), сверяясь с макетом и Brandbook Forest. Не менять контракты API.

---

## ЧАСТЬ C — ПРОВЕРКА И ВЫКАТ

### Task 9: Регрессия

- [ ] `php artisan test tests/Feature/Admin tests/Feature/SaasAdminMiddlewareTest.php` → PASS.
- [ ] `npm run test:vue` (frontend) → новые тесты зелёные.
- [ ] `npm run type-check` → 0 ошибок в новых .ts/.vue.
- [ ] Vite-сборка `npm run build` → без ошибок.

### Task 10: Выкат на боевой (по отдельному разрешению владельца)

> ⚠️ Необратимо. Только после явного «выкатывай» + `prod-deploy-validator` GO.
- [ ] Влить ветку в gitea main, выкатить overlay (как в `bin/deploy-source-edit.sh`: clone gitea → `npm ci && npm run build` (фронт изменился!) → rsync → optimize → restart). Этот выкат **включает фронт-сборку** (в отличие от admin-db фикса).
- [ ] Live-приёмка: открыть `liderra.ru/admin` под входом владельца → редирект на Командный центр → плитки Финансы/Здоровье показывают реальные числа; клик проваливает в детали и в карточку клиента.
- [ ] Обновить `ПИЛОТ.md` + память.

---

## Notes / Self-review

- **Spec coverage:** Финансы (Task 1,2) ✓; Здоровье (Task 1,3) ✓; L1 каркас + плитки (Task 6) ✓; drill в существующие экраны L3 (Task 6 router.push) ✓; Лиды/Заказ — Этап 2 (заглушки в Task 6) ✓; без маржи ✓; без новых таблиц ✓.
- **Пороги светофоров (дефолты по spec):** Финансы 🔴 при `balance_rub<0`; (🟡 runway<3д — добавить в Этапе 1.1, требует расчёта 7-дн среднего списания на тенанта; в этом плане L1/L2 показывают «в минусе», runway-🟡 как follow-up чтобы не раздувать). Здоровье: queues/webhooks 🟡 при >0, scheduler/sync/csv/incidents 🔴 при проблеме.
- **admin-db зависимость:** все эндпоинты в группе `['saas-admin','admin-db']` → читают cross-tenant под `pgsql_admin`. В тестах работает через глобальный `SharesAdminPdo`.
- **Не трогать** незакоммиченную правку лимитов проектов в рабочей папке.
- **Уточнить при реализации:** точное имя axios-импорта в `api/admin.ts`; точный layout-механизм admin-view; наличие колонок `failed_jobs.failed_at`, `scheduler_heartbeats.consecutive_failures`, `supplier_csv_reconcile_log.status` (data-map подтвердил — перепроверить именами при первом прогоне теста).
