Compare commits

..

3 Commits

Author SHA1 Message Date
Дмитрий fbf982e12c docs: обновление состояния — фича на проде, флаг ВКЛ, тумблер; ПИЛОТ снимок 26.06; CLAUDE §6
Accessibility (Pa11y live) / a11y (push) Waiting to run
ПИЛОТ.md — снимки выката source-edit + включения флага и тумблера. findings tails-doc — статус ВЫКАЧЕНО НА БОЕВОЙ. CLAUDE.md §6 последняя продуктовая фича обновлена, снята устаревшая ремарка про синк квинтета (закрыто в PSR/Tooling), плюс досессионная правка Б-1 ИП/ЮKassa. Нормативный квинтет Pravila/PSR/Tooling без изменений (агент normative-sync подтвердил).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 04:38:22 +03:00
Дмитрий f9f86ca05f feat/admin: тумблер разблокировки смены источника на экране интеграции с поставщиком
Accessibility (Pa11y live) / a11y (push) Waiting to run
SAST — Semgrep / Semgrep SAST scan (push) Waiting to run
Дружелюбный переключатель ВКЛ/ВЫКЛ флага routing_match_by_snapshot для владельца — без правки БД и без 30-символьного основания общего edit-flow. GET/POST source-edit-flag в AdminSupplierIntegrationController пишут в system_settings type=bool + audit-журнал. На экране карточка с VSwitch и диалогом подтверждения, бамп ключа возвращает тумблер к факту при отмене. TDD: 5 эндпоинт-тестов + фронт-спек. Larastan чист, baseline дополнен Pest-шумом. Проверено глазами через Playwright.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 04:27:32 +03:00
Дмитрий f82596c527 docs/pilot: снимок выката source-edit-snapshot-routing на боевой + пометка ИП/ЮKassa
Accessibility (Pa11y live) / a11y (push) Waiting to run
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 04:02:38 +03:00
9 changed files with 294 additions and 6 deletions
+3 -3
View File
@@ -13,7 +13,7 @@
# CLAUDE.md — техконтекст Лидерры
**Версия:** 2.47 от 15.06.2026 — структурная компактизация: история версий и журнал фаз вынесены в [docs/CHANGELOG_claude_md.md](docs/CHANGELOG_claude_md.md); разделы про «мозг» (router / наставник / observer / enforcement / разработка реестра инструментов) убраны — управляющий слой выделен в отдельный репозиторий **claude-brain** (ADR-020). Правила, нормативка и состав продукта **не изменены** — только структура файла. Полная история — в CHANGELOG. **NB:** cross-ref версии CLAUDE.md в Pravila/PSR/Tooling указывают 2.46 — синхронизация квинтета на 2.47 — отдельный follow-up.
**Версия:** 2.47 от 15.06.2026 — структурная компактизация: история версий и журнал фаз вынесены в [docs/CHANGELOG_claude_md.md](docs/CHANGELOG_claude_md.md); разделы про «мозг» (router / наставник / observer / enforcement / разработка реестра инструментов) убраны — управляющий слой выделен в отдельный репозиторий **claude-brain** (ADR-020). Правила, нормативка и состав продукта **не изменены** — только структура файла. Полная история — в CHANGELOG. (Прежняя ремарка про рассинхрон cross-ref квинтета на 2.47 снята — закрыто в PSR v3.24 / Tooling v2.25 от 14.06.2026.)
**Назначение:** оперативная карта для Claude Code. Не первоисточник — первоисточники указаны в §0.
**Владелец и режим правок:** все изменения этого файла — **только** через плагин `claude-md-management` (skills `/claude-md-management:claude-md-improver` для audit/targeted-updates и `/claude-md-management:revise-claude-md` для capture session-learnings). Прямые правки запрещены — см. §5 п.11.
@@ -241,11 +241,11 @@ trivy image liderra:latest
- `ЭТАЛОН.md` (корень репо) — локальная dev-версия (git/окружение/временное/демо).
- `ПИЛОТ.md` (корень репо) — боевая интернет-версия liderra.ru (доступ/HTTPS/сервер/БД/безопасность/YC Lockbox).
**Последняя продуктовая фича:** определение региона лида по телефону + каскадная маршрутизация (DaData → реестр Россвязи → tag-fallback) — на проде, включена на 100%.
**Последняя продуктовая фича:** разблокировка смены источника проекта без потери лидов — матч поставщиковых лидов по слепку `project_routing_snapshots` (флаг `routing_match_by_snapshot`), Эпик 4 онлайн-заморозка 18:00→00:00 + `FlushDeferredOnlineSyncJob` (00:05 МСК), экран «Вечерняя заливка» (`supplier_sync_runs`) и дружелюбный тумблер управления флагом в админке «Интеграция с поставщиком». На проде liderra.ru (26.06.2026), флаг **ВКЛЮЧЁН**, идёт суточное наблюдение. Откат — тумблер в ВЫКЛ.
**Полный журнал фаз и работ** (что и когда делалось, включая историю «мозга») — в [docs/CHANGELOG_claude_md.md](docs/CHANGELOG_claude_md.md).
**P0-блокер:** **Б-1** (реквизиты юр. лица, ждут регистрации ООО). От него зависят Диз-3, DO-2, DO-4.
**Б-1 (юр. лицо) — закрыт:** ИП **зарегистрирован** (НЕ ООО), договор с **ЮKassa** готов — осталось только подписать; после подписи включается онлайн-оплата (флаг `billing_yookassa_enabled`). Зависевшие Диз-3, DO-2, DO-4 — разблокированы. Источник истины — память `project-legal-entity-ip-yookassa-2026-06-25` (25.06.2026).
---
@@ -15,6 +15,7 @@ use App\Services\Supplier\Channel\SupplierProjectChannel;
use App\Services\Supplier\SupplierExportMode;
use App\Services\Supplier\SupplierPortalClient;
use App\Support\RussianRegions;
use App\Support\SystemSettings;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -225,6 +226,49 @@ final class AdminSupplierIntegrationController extends Controller
return response()->json(['mode' => $data['mode']]);
}
/**
* Тумблер «Разблокировка смены источника» (флаг routing_match_by_snapshot).
* GET текущее состояние ВКЛ/ВЫКЛ для переключателя в админке.
*/
public function getSourceEditFlag(): JsonResponse
{
return response()->json(['enabled' => SystemSettings::bool('routing_match_by_snapshot', false)]);
}
/**
* POST включить/выключить разблокировку смены источника (матч по слепку).
* Пишет в system_settings (type=bool) + audit-журнал; основание не требуется
* (дружелюбный тумблер для владельца, в отличие от общего edit-flow §settings).
*/
public function setSourceEditFlag(Request $request): JsonResponse
{
$data = $request->validate([
'enabled' => ['required', 'boolean'],
]);
$enabled = (bool) $data['enabled'];
$prev = DB::table('system_settings')->where('key', 'routing_match_by_snapshot')->value('value');
DB::table('system_settings')->updateOrInsert(
['key' => 'routing_match_by_snapshot'],
['value' => $enabled ? 'true' : 'false', 'type' => 'bool', 'updated_at' => now()],
);
SaasAdminAuditLog::create([
'admin_user_id' => $this->resolveAdminUserId($request, 'supplier-integration@system.stub', 'Supplier Integration Stub'),
'action' => 'supplier_integration.source_edit_flag_set',
'target_type' => 'system_setting',
'target_id' => null,
'payload_before' => $prev !== null ? ['enabled' => $prev] : null,
'payload_after' => ['enabled' => $enabled ? 'true' : 'false'],
'ip_address' => $request->ip() ?? '127.0.0.1',
'user_agent' => $request->userAgent(),
'requires_approval' => false,
]);
return response()->json(['enabled' => $enabled]);
}
/**
* Plan 4 Task 2: список supplier_projects + кто заказывал (через pivot
* projects tenants) + дата последней поставки лида.
+18
View File
@@ -420,6 +420,24 @@ parameters:
count: 2
path: tests/Feature/Admin/SupplierExportModeEndpointTest.php
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:actingAs\(\)\.$#'
identifier: method.notFound
count: 5
path: tests/Feature/Admin/SupplierSourceEditFlagEndpointTest.php
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:getJson\(\)\.$#'
identifier: method.notFound
count: 2
path: tests/Feature/Admin/SupplierSourceEditFlagEndpointTest.php
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:postJson\(\)\.$#'
identifier: method.notFound
count: 3
path: tests/Feature/Admin/SupplierSourceEditFlagEndpointTest.php
-
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:actingAs\(\)\.$#'
identifier: method.notFound
@@ -59,6 +59,55 @@ async function setExportMode(mode: ExportMode): Promise<void> {
}
}
// --- Тумблер «Разблокировка смены источника» (флаг routing_match_by_snapshot) ---
const sourceEditEnabled = ref(false);
const sourceEditError = ref<string | null>(null);
const sourceEditSaving = ref(false);
const sourceEditConfirmOpen = ref(false);
const pendingSourceEditValue = ref(false);
// VSwitch флипает внутреннее состояние по клику; бамп ключа ре-маунтит тумблер,
// чтобы он вернулся к фактическому sourceEditEnabled после отмены/ошибки.
const sourceEditSwitchKey = ref(0);
async function loadSourceEditFlag(): Promise<void> {
try {
const { data } = await axios.get('/api/admin/supplier-integration/source-edit-flag');
sourceEditEnabled.value = data?.enabled === true;
} catch {
sourceEditError.value = 'Не удалось загрузить переключатель.';
}
}
// Тумблер привязан к sourceEditEnabled один-в-один; запрос смены открывает
// подтверждение, фактическое значение меняется только после «Подтвердить».
function onSourceEditToggleRequest(val: boolean | null): void {
pendingSourceEditValue.value = val === true;
sourceEditConfirmOpen.value = true;
}
function cancelSourceEditToggle(): void {
sourceEditConfirmOpen.value = false;
sourceEditSwitchKey.value++; // вернуть тумблер к фактическому состоянию
}
async function confirmSourceEditToggle(): Promise<void> {
sourceEditConfirmOpen.value = false;
sourceEditSaving.value = true;
sourceEditError.value = null;
try {
const { data } = await axios.post('/api/admin/supplier-integration/source-edit-flag', {
enabled: pendingSourceEditValue.value,
});
sourceEditEnabled.value = data?.enabled === true;
} catch {
sourceEditError.value = 'Не удалось сохранить переключатель.';
} finally {
sourceEditSaving.value = false;
sourceEditSwitchKey.value++; // синхронизировать тумблер с фактом (вкл. при ошибке)
}
}
async function load(): Promise<void> {
loading.value = true;
error.value = null;
@@ -196,6 +245,7 @@ onMounted(() => {
void load();
void loadManualQueue();
void loadExportMode();
void loadSourceEditFlag();
void loadSyncRuns();
});
</script>
@@ -233,6 +283,63 @@ onMounted(() => {
</v-card-text>
</v-card>
<v-card class="mb-4" data-testid="source-edit-flag-card">
<v-card-title>Разблокировка смены источника</v-card-title>
<v-card-text>
<v-alert v-if="sourceEditError" type="error" density="compact" class="mb-3">
{{ sourceEditError }}
</v-alert>
<v-switch
:key="sourceEditSwitchKey"
:model-value="sourceEditEnabled"
:loading="sourceEditSaving"
:disabled="sourceEditSaving"
color="primary"
hide-details
inset
data-testid="source-edit-flag-switch"
:label="sourceEditEnabled ? 'Включена' : 'Выключена'"
@update:model-value="onSourceEditToggleRequest"
/>
<p class="text-caption text-medium-emphasis mt-1 mb-0">
ВКЛ клиенты могут менять источник проекта без потери лидов (маршрутизация по слепку).
ВЫКЛ смена источника заблокирована. Откат безопасен в любой момент.
</p>
</v-card-text>
</v-card>
<v-dialog v-model="sourceEditConfirmOpen" max-width="480" data-testid="source-edit-confirm">
<v-card>
<v-card-title>
{{ pendingSourceEditValue ? 'Включить' : 'Выключить' }} разблокировку смены источника?
</v-card-title>
<v-card-text>
<template v-if="pendingSourceEditValue">
Клиенты смогут менять источник проекта без потери лидов (матч по слепку).
Рекомендуется сутки понаблюдать по «Вечерней заливке», что лиды доезжают.
</template>
<template v-else>
Вернётся прежнее поведение: смена источника заблокирована. Откат безопасен.
</template>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" data-testid="source-edit-confirm-cancel" @click="cancelSourceEditToggle">
Отмена
</v-btn>
<v-btn
color="primary"
variant="flat"
:loading="sourceEditSaving"
data-testid="source-edit-confirm-apply"
@click="confirmSourceEditToggle"
>
Подтвердить
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-card class="mb-4" data-testid="sync-runs-card">
<v-card-title>Вечерняя заливка проектов поставщику</v-card-title>
<v-card-text>
+4
View File
@@ -188,6 +188,10 @@ Route::middleware('saas-admin')->group(function () {
Route::get('/api/admin/supplier-integration/export-mode', 'App\Http\Controllers\Api\AdminSupplierIntegrationController@getExportMode');
Route::post('/api/admin/supplier-integration/export-mode', 'App\Http\Controllers\Api\AdminSupplierIntegrationController@setExportMode');
// Тумблер «Разблокировка смены источника» (флаг routing_match_by_snapshot).
Route::get('/api/admin/supplier-integration/source-edit-flag', 'App\Http\Controllers\Api\AdminSupplierIntegrationController@getSourceEditFlag');
Route::post('/api/admin/supplier-integration/source-edit-flag', 'App\Http\Controllers\Api\AdminSupplierIntegrationController@setSourceEditFlag');
// Plan 4 Task 2: экран «Проекты у поставщика» — список + bulk-delete.
Route::get('/api/admin/supplier-integration/projects', 'App\Http\Controllers\Api\AdminSupplierIntegrationController@projectsIndex');
Route::post('/api/admin/supplier-integration/projects/delete', 'App\Http\Controllers\Api\AdminSupplierIntegrationController@projectsDestroy');
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
// Тумблер «Разблокировка смены источника» (флаг routing_match_by_snapshot) на экране
// «Интеграция с поставщиком» — чтобы владелец включал/выключал мышкой без правки БД.
// EnsureSaasAdmin — стаб в testing; actingAs нужен для прохода auth+admin middleware.
it('GET source-edit-flag returns false when flag absent', function (): void {
$this->actingAs(User::factory()->create());
DB::table('system_settings')->where('key', 'routing_match_by_snapshot')->delete();
$this->getJson('/api/admin/supplier-integration/source-edit-flag')
->assertOk()
->assertJson(['enabled' => false]);
});
it('GET source-edit-flag returns true when flag set', function (): void {
$this->actingAs(User::factory()->create());
DB::table('system_settings')->updateOrInsert(
['key' => 'routing_match_by_snapshot'],
['value' => 'true', 'type' => 'bool', 'updated_at' => now()],
);
$this->getJson('/api/admin/supplier-integration/source-edit-flag')
->assertOk()
->assertJson(['enabled' => true]);
});
it('POST source-edit-flag enables the flag', function (): void {
$this->actingAs(User::factory()->create());
DB::table('system_settings')->where('key', 'routing_match_by_snapshot')->delete();
$this->postJson('/api/admin/supplier-integration/source-edit-flag', ['enabled' => true])
->assertOk()
->assertJson(['enabled' => true]);
expect(DB::table('system_settings')->where('key', 'routing_match_by_snapshot')->value('value'))
->toBe('true');
});
it('POST source-edit-flag disables the flag', function (): void {
$this->actingAs(User::factory()->create());
DB::table('system_settings')->updateOrInsert(
['key' => 'routing_match_by_snapshot'],
['value' => 'true', 'type' => 'bool', 'updated_at' => now()],
);
$this->postJson('/api/admin/supplier-integration/source-edit-flag', ['enabled' => false])
->assertOk()
->assertJson(['enabled' => false]);
expect(DB::table('system_settings')->where('key', 'routing_match_by_snapshot')->value('value'))
->toBe('false');
});
it('POST source-edit-flag rejects non-boolean', function (): void {
$this->actingAs(User::factory()->create());
$this->postJson('/api/admin/supplier-integration/source-edit-flag', ['enabled' => 'maybe'])
->assertStatus(422);
});
@@ -0,0 +1,40 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import axios from 'axios';
import AdminSupplierIntegrationView from '../../resources/js/views/admin/AdminSupplierIntegrationView.vue';
vi.mock('axios');
const vuetify = createVuetify();
describe('AdminSupplierIntegrationView — тумблер разблокировки смены источника', () => {
beforeEach(() => {
vi.clearAllMocks();
(axios.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) => {
if (url.endsWith('/source-edit-flag')) {
return Promise.resolve({ data: { enabled: true } });
}
if (url.endsWith('/export-mode')) {
return Promise.resolve({ data: { mode: 'batch' } });
}
if (url.endsWith('/manual-queue')) {
return Promise.resolve({ data: { queue: [] } });
}
return Promise.resolve({ data: { health: null, history: [] } });
});
(axios.post as ReturnType<typeof vi.fn>).mockResolvedValue({ data: { enabled: false } });
});
it('GETs the flag on mount and renders the toggle card with current label', async () => {
const wrapper = mount(AdminSupplierIntegrationView, { global: { plugins: [vuetify] } });
await new Promise((r) => setTimeout(r, 50));
expect(axios.get).toHaveBeenCalledWith('/api/admin/supplier-integration/source-edit-flag');
const card = wrapper.find('[data-testid="source-edit-flag-card"]');
expect(card.exists()).toBe(true);
expect(wrapper.text()).toContain('Разблокировка смены источника');
// флаг enabled=true с бэка → подпись «Включена»
expect(wrapper.text()).toContain('Включена');
});
});
@@ -1,7 +1,8 @@
# Хвосты сессии — разблокировка смены источника (Эпики 1–6)
**Дата:** 2526.06.2026. **Ветка:** `feat/source-edit-snapshot-routing` (20 коммитов, **НЕ запушено**).
**Флаг:** `system_settings.routing_match_by_snapshot`**ВЫКЛ** (прод не меняется до включения).
> ✅ **СТАТУС 26.06.2026: ВЫКАЧЕНО НА БОЕВОЙ liderra.ru, ФЛАГ ВКЛЮЧЁН, ТУМБЛЕР В АДМИНКЕ.** Ветка влита в gitea `main` (`3cedf28f..f9f86ca0`, 24 коммита), выкачена скриптом `bin/deploy-source-edit.sh`. Флаг `routing_match_by_snapshot=true` (фича боевая активна). Дружелюбный тумблер ВКЛ/ВЫКЛ на экране «Интеграция с поставщиком» (коммит `f9f86ca0`). Идёт сутки наблюдения (drift=0, сверять «Вечерней заливкой»). Деньги клиента целы (1 838 400 ₽ / 1013). Детали выката — `ПИЛОТ.md` (снимки 26.06 ~01:00 и ~01:25). Откат — тумблер в ВЫКЛ или флаг `false`. Хвосты ниже — историческая хроника до выката.
**Дата:** 2526.06.2026. **Ветка:** `feat/source-edit-snapshot-routing` (выкачена в main 26.06).
**План:** [docs/superpowers/plans/2026-06-25-source-edit-unblock-snapshot-routing.md](../plans/2026-06-25-source-edit-unblock-snapshot-routing.md).
## Что СДЕЛАНО и закоммичено (все эпики 1–6 закрыты)
+7 -1
View File
@@ -8,7 +8,13 @@
- Волатильную часть (доступ, версии, что развёрнуто) перед рискованными действиями **перепроверять реальной командой по SSH**, не доверять снимку вслепую.
- Обновляется по команде заказчика **«обнови пилот»**.
**Снимок снят:** 25.06.2026 (~07:00 UTC) — **🚀 ВЫКАЧЕН ВЕСЬ main на боевой liderra.ru (gitea/main, поверх состояния 23.06).** По команде владельца «выкатывай» накатан весь накопившийся с 23.06 стек (15 коммитов), не только U1+B1. **Что теперь живёт:** U1 (понятные подписи источника проекта вместо «домен конкурента/донор»), B1 (счётчик «хватит на N дней» от дневного заказа `requiredLeadsForTomorrow`), онбординг-фиксы косяк 03–07 (реквизиты первым шагом визарда, «Вся РФ» одной галочкой, неподтверждённая почта зовёт подтвердить, окно перегрузки на «вы» + кнопка пополнения, мелочи 07), нормализация телефона-источника, почта поддержки `support@liderra.ru`, биллинг-фикс «префлайт/блок берут действующую версию тарифа по дате» (`116b0aaa`), **публичные юр-страницы оферта/политика/возврат + страница цен** (готовились под ЮKassa-модерацию). **Метод (по `deploy/README.md`):** `git archive HEAD app/ db/` + локальная Vite-сборка (sitekey SmartCaptcha `ysc1_XhLB…` совпал с прод-бандлом) → `cat | ssh` через бастион → `sudo tar` overlay в `/var/www/liderra/` (+`app/public/build`) → `chown www-data``redeploy.sh`. **Миграций НЕТ** (`migrate --force` = «Nothing to migrate»); **БД не менялась**, schema-канон v8.53 (тело функции, не DDL). **💰 Деньги клиента ЦЕЛЫ:** tenant 2 = **1 836 400.00 ₽ / 1013 сделок** (БД не тронута). **Verify после:** HTTP **200** (главная+`/login`), queue перезапущен (PID активен), 0 ошибок в свежем логе, `config.php` = **`www-data:www-data 664` READABLE** (квирк 107 чисто), `migrate:status` 0 pending. **Бэкап отката на проде:** `/home/ubuntu/pre-deploy-u1b1-20260625-064928-code.tar.gz` (2.95М, код без vendor) + `-env.bak`. **`redeploy.sh` дополнительно усилен** (`rm` старых кэшей перед `optimize` → config.php владелец www-data; синкнут на прод, sha совпал). **⚠️ Онлайн-оплата ЮKassa ПО-ПРЕЖНЕМУ OFF** (флаг `billing_yookassa_enabled` отсутствует/false) — юр-страницы и цены теперь публичны (для модерации), но приём платежей выключен до Б-1/ООО. Кодовая фраза стены — «роутер-наставник».
> 🟢 **АКТУАЛЬНО (25.06.2026, со слов владельца) — перебивает любые упоминания «ждут ООО / Б-1» в снимках ниже:** юр. лицо — **ИП, зарегистрировано** (НЕ ООО). Договор с **ЮKassa готов, осталось только подписать**; после подписи включается флаг `billing_yookassa_enabled` и приём онлайн-оплаты. «Регистрация ООО» как блокер — **снято, не актуально.**
**Снимок снят:** 26.06.2026 (~01:25 UTC) — **🟢 ФЛАГ `routing_match_by_snapshot` ВКЛЮЧЁН + ТУМБЛЕР В АДМИНКЕ (по команде владельца «включай и выведи в админку»).** Фича «разблокировка смены источника» теперь **боевая активна**: маршрутизация поставщиковых лидов по слепку, замок смены источника снят. Флаг в `system_settings` = `true` (type=bool, понятное рус. описание для админки). **Добавлен дружелюбный тумблер ВКЛ/ВЫКЛ** на экране админки «Интеграция с поставщиком» (карточка «Разблокировка смены источника»): `GET/POST /api/admin/supplier-integration/source-edit-flag` (`AdminSupplierIntegrationController` → пишет `system_settings` + `saas_admin_audit_log`, без 30-симв. основания), VSwitch + диалог подтверждения. Коммит `f9f86ca0` (TDD: 5 эндпоинт-тестов + фронт-спек; larastan чист; проверено глазами через Playwright), выкачен тем же скриптом `bin/deploy-source-edit.sh` (миграции no-op — таблицы уже есть). **Verify:** HTTP 200, роут тумблера зарегистрирован на проде, флаг читается `true` → тумблер показывает «Включена». **💰 Деньги клиента ЦЕЛЫ** (правки только код/настройка): tenant 2 = 1 838 400 ₽ / 1013 сделок. **Откат:** тумблер в ВЫКЛ (или флаг `false`) — мгновенно. **ИДЁТ сутки наблюдения** (drift=0, лиды доезжают, сверять экраном «Вечерняя заливка»). После чистых суток — можно спокойно показывать клиентам снятие замка. Кодовая фраза стены — «роутер-наставник».
**Снимок снят:** 26.06.2026 (~01:00 UTC) — **🚀 ВЫКАЧЕНА фича «разблокировка смены источника проекта без потери лидов» (Эпики 1–6) на боевой liderra.ru (gitea/main `3cedf28f..e8434231`, 23 коммита).** Метод — рунбук `docs/superpowers/runbooks/2026-06-18-gitea-prod-deploy-pipeline.md` через скрипт `bin/deploy-source-edit.sh`: бэкап кода + дамп БД `/tmp/liderra-pre-deploy-2026-06-26-0052.dump` → клон gitea main → npm build → maintenance → rsync overlay → composer/optimize (www-data) → **миграции через `sudo -u postgres psql`** (2 таблицы `supplier_deferred_sync` v8.54 + `supplier_sync_runs` v8.55, GRANT для `crm_supplier_worker`, отмечены в `migrations` batch 22) → up. **Что теперь живёт:** матч источника по слепку `project_routing_snapshots` (Путь A, **за флагом `routing_match_by_snapshot` = ВЫКЛ** — снятие замка клиентам пока НЕ видно), UX редактирования источника + баннер applies-from (Эпик 3, тоже за флагом по факту замка), **Эпик 4 онлайн-заморозка 18:00→00:00 АКТИВЕН** (прод в `supplier_export_mode=online`): вечерние правки откладываются в `supplier_deferred_sync`, `FlushDeferredOnlineSyncJob` досылает в 00:05 МСК (в расписании, `/etc/cron.d/liderra-scheduler` schedule:run ежеминутно www-data, `scheduler_heartbeats` свежие 0 fail), экран «Вечерняя заливка» в админке (Эпик 5). **Также в этом выкате — фикс реального прод-бага:** смена только лимита/региона/дней на защищённом поставщиком проекте больше не блокируется ложным 422 «нельзя сменить источник» (`ProjectService::sourceValueChanged` сравнивает значение, а не присутствие ключа). **Verify после:** HTTP **200**, обе таблицы + grants на месте, `migrate:status` оба `[22] Ran`, флаг ВЫКЛ подтверждён, планировщик жив. **💰 Деньги клиента ЦЕЛЫ** (выкат добавил лишь 2 пустые таблицы, балансы/сделки не трогал): tenant 2 = **1 838 400.00 ₽ / 1013 сделок** (+2000₽ к снимку 25.06 — обычное пополнение за день, сделок столько же). **Откат:** бэкап кода `/tmp/app-backup-2026-06-26-0052.tgz` + дамп БД (см. выше) на проде; БД-откат = `DROP TABLE supplier_deferred_sync, supplier_sync_runs CASCADE` + удалить 2 строки из `migrations`. **СЛЕДУЮЩЕЕ (за владельцем):** включить флаг `routing_match_by_snapshot` → сутки наблюдать (drift=0, лиды доезжают, сверять экраном «Вечерняя заливка») → потом показывать клиентам снятие замка (Эпик 3). GitHub `origin` мёртв (suspended, 403) — бэкап только gitea. Кодовая фраза стены — «роутер-наставник».
**Снимок снят:** 25.06.2026 (~07:00 UTC) — **🚀 ВЫКАЧЕН ВЕСЬ main на боевой liderra.ru (gitea/main, поверх состояния 23.06).** По команде владельца «выкатывай» накатан весь накопившийся с 23.06 стек (15 коммитов), не только U1+B1. **Что теперь живёт:** U1 (понятные подписи источника проекта вместо «домен конкурента/донор»), B1 (счётчик «хватит на N дней» от дневного заказа `requiredLeadsForTomorrow`), онбординг-фиксы косяк 03–07 (реквизиты первым шагом визарда, «Вся РФ» одной галочкой, неподтверждённая почта зовёт подтвердить, окно перегрузки на «вы» + кнопка пополнения, мелочи 07), нормализация телефона-источника, почта поддержки `support@liderra.ru`, биллинг-фикс «префлайт/блок берут действующую версию тарифа по дате» (`116b0aaa`), **публичные юр-страницы оферта/политика/возврат + страница цен** (готовились под ЮKassa-модерацию). **Метод (по `deploy/README.md`):** `git archive HEAD app/ db/` + локальная Vite-сборка (sitekey SmartCaptcha `ysc1_XhLB…` совпал с прод-бандлом) → `cat | ssh` через бастион → `sudo tar` overlay в `/var/www/liderra/` (+`app/public/build`) → `chown www-data``redeploy.sh`. **Миграций НЕТ** (`migrate --force` = «Nothing to migrate»); **БД не менялась**, schema-канон v8.53 (тело функции, не DDL). **💰 Деньги клиента ЦЕЛЫ:** tenant 2 = **1 836 400.00 ₽ / 1013 сделок** (БД не тронута). **Verify после:** HTTP **200** (главная+`/login`), queue перезапущен (PID активен), 0 ошибок в свежем логе, `config.php` = **`www-data:www-data 664` READABLE** (квирк 107 чисто), `migrate:status` 0 pending. **Бэкап отката на проде:** `/home/ubuntu/pre-deploy-u1b1-20260625-064928-code.tar.gz` (2.95М, код без vendor) + `-env.bak`. **`redeploy.sh` дополнительно усилен** (`rm` старых кэшей перед `optimize` → config.php владелец www-data; синкнут на прод, sha совпал). **⚠️ Онлайн-оплата ЮKassa ПО-ПРЕЖНЕМУ OFF** (флаг `billing_yookassa_enabled` отсутствует/false) — юр-страницы и цены теперь публичны (для модерации), но приём платежей выключен **до подписания договора с ЮKassa** (ИП зарегистрирован; ООО не требуется — см. пометку «АКТУАЛЬНО» вверху файла). Кодовая фраза стены — «роутер-наставник».
**Снимок снят:** 25.06.2026 (~05:30 UTC) — **прод-КОД НЕ менялся (деплоя продуктовых фич в эту сессию НЕ было), но закрыт повтор квирка 107 + прод config.php приведён к www-data.** Сессия: оздоровление тест-стенда + canon-sync схемы + разбор квирка 107. **✅ Квирк 107 разобран до конца — реального дефекта НЕ было:** на проде `bootstrap/cache/config.php` был `ubuntu:www-data` mode `775` — www-data **читал** его через группу, портал HTTP **200**. «Лечили 3 раза» зря — гонялись за строгой проверкой валидатора «владелец == www-data», тогда как важна **читаемость**. Первопричины повтора закрыты в коде (gitea `c370ff82`): (1) `deploy/redeploy.sh``optimize` перенесён в КОНЕЦ (раньше шёл ДО `chown -R ubuntu:www-data bootstrap/cache`, и тот переписывал владельца свежего config.php обратно на ubuntu); (2) агент `prod-deploy-validator` П1 — критерий сменён на читаемость www-data (`test -r`), `ubuntu:www-data 775` больше НЕ даёт ложный NO-GO. **Прод пере-кэширован вручную (разрешение владельца):** `sudo -u www-data php artisan optimize` → теперь `config.php` = **`www-data:www-data 664`**, READABLE, HTTP 200. Прод-код и деньги НЕ затронуты (re-cache не меняет содержимое конфига). **schema-канон v8.52 → v8.53 (gitea `41dd8c0b`, НЕ DB-change):** тело функции `audit_chain_hash()` в `db/schema.sql` синхронизировано с миграцией `2026_05_30_000001` (per-partition `pg_advisory_xact_lock`) — канон отставал, но прод и так защищён через миграцию (`migrate:fresh` даёт функцию С блокировкой); счётчики без изменений; запись в `db/CHANGELOG_schema.md`. **Тест-стенд оздоровлён 55→0** (backend-набор зелёный; всё тест-долг, НЕ баги прода) — на прод НЕ идёт (тесты на боевой не выкатываются); 4 коммита gitea `2ec70b33`/`88ace4e3`/`a49916b7`/`41dd8c0b`. **🔴 U1+B1 — на gitea (`7fbd75d3`/`31f0e869`), но ещё НЕ на проде:** U1 (понятные подписи источника проекта вместо «домен конкурента/донор»), B1 (счётчик «хватит на N дней» от дневного заказа). Блокер выката (квирк 107) снят — валидатор теперь GREEN на текущем проде; ждёт «выкатывай» владельца (redeploy соберёт фронт сам). **💰 Деньги клиента ЦЕЛЫ** (прод-код не менялся): tenant 2 = **1 836 400.00 ₽ / 1013 сделок**. Кодовая фраза стены — «роутер-наставник».