Commit Graph

10 Commits

Author SHA1 Message Date
Дмитрий 86dc930915 refactor(backend): Sprint 3 Phase A — DealController split + N+1 fix + export streaming
Sprint 3 Phase A. Закрытие audit O-refactor-01 + O-perf-01 + O-perf-05.

O-refactor-01: DealController (802 строки) → split на 3 контроллера
по ответственности:
  * DealController (466 строк) — single-resource CRUD (index/show/store/update).
  * DealBulkActionController (264 строки, новый) — bulk transition/destroy/restore.
  * DealExportController (120 строк, новый) — export() с streaming.
API endpoints без изменений; в routes/web.php обновлены только controller@method.

O-perf-01: N+1 в bulk-actions устранён в новом DealBulkActionController.
  * transition: SELECT (id, status) → filter NO-OP → bulk-UPDATE
    whereIn(changed)->update(status) + ActivityLog::insert(массив).
    100 сделок: ~200 SQL → 2 SQL (после SELECT 1 UPDATE + 1 INSERT).
  * destroy/restore: SELECT id'шников живых/удалённых → bulk-UPDATE
    deleted_at + ActivityLog::insert. Аналогично 2 SQL вместо N.
  Defense-in-depth where(tenant_id) сохранён — DealTransitionTest
  «не апдейтит чужие сделки» проходит.

O-perf-05: export() переписан на OpenSpout streaming (composer require
openspout/openspout ^5.3). PhpSpreadsheet строил весь .xlsx в памяти
(10K сделок ≈ 100+ MB RAM). Теперь:
  * Writer::openToFile('php://output') + Row::fromValues + chunkById(500).
  * StreamedResponse → пик памяти O(1) от размера экспорта.
  * CSV: OpenSpout Options(FIELD_DELIMITER=';', SHOULD_ADD_BOM=true) —
    Excel-friendly RU-локаль сохранена.
  * XLSX: getCurrentSheet()->setName('Сделки'), header через
    Row::fromValuesWithStyle с (new Style)->withFontBold(true).

DealCreateTest.php (4 теста про export): getContent() → streamedContent()
для StreamedResponse + getCell()->getFormattedValue() для inline-string
ячеек, которые OpenSpout пишет как RichText (PhpSpreadsheet writer писал
как plain shared-string). Логика тестов и assertion'ы не меняются.

Verification:
  Pest: 416/416 passed (+ 2 skipped), 1388 assertions, 47.5s.
  Larastan: 0 errors above baseline.
  Pint: passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:08:20 +03:00
Дмитрий 20d4682e21 feat(backend): Sprint 2 Phase A — Pest 4 browser scaffold + mutation + lazy-loading + Larastan cache
Sprint 2 Phase A (modernization). Закрытие audit O-stack-01/02/03 + O-perf-07:

- O-stack-01: Pest 4 browser-tests scaffold. testsuite Browser в phpunit.xml,
  Pest.php extend(...)->in('Browser'), tests/Browser/SmokeTest.php
  (login-flow + deal-create-flow). На Windows native PHP плагин
  pest-plugin-browser требует ext-sockets (отсутствует в стандартной
  сборке) и при автозагрузке вызывает socket_create_listen() ДО старта
  тестов, что ломает весь Pest. Поэтому плагин НЕ в require-dev,
  тесты помечены ->skip(...) с инструкцией активации на Linux CI.
  Скелет тестов в комментариях — на CI достаточно
  composer require pestphp/pest-plugin-browser --dev + npx playwright
  install + раскомментировать тела.
- O-stack-02: infection/infection ^0.32.7 в require-dev + app/infection.json
  (minMsi=50, source: Http/Models/Services). composer mutation script.
  Запуск отдельной задачей в CI (медленный). allow-plugins для
  infection/extension-installer.
- O-stack-03: Laravel 13 string-based lazy-loading в routes/web.php —
  убран блок use App\Http\Controllers\Api\* (16 импортов), все
  ссылки заменены на строки 'App\Http\...\X@method'. Контроллеры
  не загружаются автозагрузчиком при boot route'ов; класс резолвится
  только при матче маршрута. Использован строковый синтаксис, а не
  FQN-class — Pint default preset (fully_qualified_strict_types fixer)
  сворачивает FQN-class обратно в use, на строки не действует.
- O-perf-07: Larastan result cache через tmpDir: .phpstan-cache в
  phpstan.neon (cache-dir не дублируется флагом — phpstan не принимает
  оба источника). lefthook job 6 (larastan) использует этот же
  tmpDir автоматически. Ускорение инкрементальных pre-commit прогонов:
  на правке одного файла — переанализ только зависящих, не всего
  графа классов. .phpstan-cache в .gitignore.

Pest: 416/416 PASS + 2 skipped (browser smoke pending Linux CI).
Larastan: 0 errors above baseline.
Infection: vendor/bin/infection --version → 0.32.7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 19:18:38 +03:00
Дмитрий a1ea003642 phase2(xlsx-export): PhpSpreadsheet 5.7 + format=csv|xlsx на /api/deals/export
Закрыт TODO «реальный XLSX-export» из v1.51. Russian users prefer .xlsx
(1С/Excel) — заменяет CSV как default. CSV остаётся через format=csv.

Backend (DealController::export):
- Body теперь: {tenant_id, ids, format?: 'csv' | 'xlsx'}; default 'csv'.
- buildXlsx: Spreadsheet + setTitle 'Сделки' + setCellValue A1..G1
  headers + bold(A1:G1) + setAutoSize всех колонок A..G. Writer пишет
  через ob_start/php://output для возврата бинарной строки.
- Content-Type application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
  + Content-Disposition с .xlsx.

Quirk: PhpSpreadsheet 5.x удалил deprecated setCellValueByColumnAndRow —
мигрировал на A1-нотацию (setCellValue('A2', $val)).

Pest +4 (DealCreateTest):
- xlsx binary с Content-Type + magic bytes "PK\x03\x04" + size >2KB.
- IOFactory::createReader('Xlsx') распаковывает: sheet «Сделки» +
  A1='ID' bold + A2/B2/C2 — реальные данные сделки.
- 422 на неизвестный format.
- Default (без format) — backward-compat CSV.

Frontend:
- api/deals.ts разделён: exportDeals (CSV string) + exportDealsXlsx
  (Blob, responseType='blob').
- applyBulkExport(format='xlsx' | 'csv') в DealsView — default 'xlsx'.
  XLSX → triggerBlobDownload (новый helper). CSV → старый CSV-helper.
  На fail — fallback на local CSV.

Vitest +3 (DealsListIntegration):
- xlsx default → exportDealsXlsx + Blob download + toast «XLSX».
- 'csv' → exportDeals + toast «CSV».
- xlsx reject → fallback на local CSV + toast «Backend недоступен».

PHPStan baseline регенерирован (удалена unmatched ignore-запись для
setCellValueByColumnAndRow). cspell-glossary +дефолтит +vnd +spreadsheetml.

Регресс:
- Lint+type-check+format passed.
- Vitest 269/269 за 18.49 сек (+3 от 266).
- Vite build 982 ms.
- Pint + PHPStan passed.
- Pest 197/197 за 26.05 сек (+4 от 193, 784 assertions).

Реестр v1.61→v1.62 / CLAUDE.md v1.52→v1.53.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 08:08:53 +03:00
Дмитрий 374724a7a3 phase2(auth-2fa): TOTP-verify endpoint + TwoFactorView интеграция
- pragmarx/google2fa@^9.0 для TOTP RFC 6238.
- AuthController::login изменён: при totp_enabled=true НЕ делает Auth::login,
  сохраняет auth.pending_user_id+pending_remember в session, возвращает
  requires_2fa=true. /me=401 пока 2FA не пройдена.
- AuthController::verifyTwoFactor: читает pending_user_id, верифицирует TOTP
  через Google2FA::verifyKey($secret, $code, window: 1) (окно ±1 = 30s).
  Success → Auth::login + regenerate + clear pending + last_login_at.
- VerifyTwoFactorRequest: regex /^\d{6}$/.
- /api/auth/2fa/verify публичный (нет session-auth до verify).

Frontend:
- auth-store::login: при requires_2fa=true user остаётся null (иначе
  isAuthenticated=true и guard пустит на /dashboard минуя 2FA).
- auth-store::verifyTwoFactor action.
- api/auth.ts::verifyTwoFactor(code).
- TwoFactorView: onMounted redirect на /login если нет pending state;
  submit → verify → /dashboard; на error - clear code + focus first cell.
  userEmail из auth.user?.email.

Pest +6 (всего 67/67 за 6.97s, 194 assertions): login для 2FA НЕ создаёт
session + verify success/неверный код/без login/валидация формата +
после verify /me=200.

Vitest +3 (всего 142/142 за 10.75s): login pending vs success state +
verifyTwoFactor success/reject. TwoFactorView spec получил setActivePinia
+ requires2fa=true для bypass onMounted-redirect.

PHPStan baseline +26 Pest TestCall warnings (накопительно).

Регресс: pint+stan passed; vitest 142/142; vite build 908ms;
story:build 21/28 за 31.28s; Pest 67/67 за 6.97s.

CLAUDE.md v1.33->v1.34, реестр Открытых_вопросов v1.42->v1.43.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:14:33 +03:00
Дмитрий 04b90afda4 phase2(auth-backend): Sanctum SPA mode + AuthController + 13 Pest tests
- laravel/sanctum@^4.3 install. SPA mode (cookie-based session, не tokens).
  personal_access_tokens migration удалена (для SPA не нужна).
- AuthController (Api/): login + register + me + logout с детальной валидацией
  + кастомные русские error-messages.
- LoginRequest + RegisterRequest Form Requests. Register требует
  accept_offer:accepted + accept_pdn:accepted (по ТЗ §1.5/§4.1, БЕЗ
  маркетингового click-wrap'а - расхождение #2 handoff vs ТЗ).
- User::fillable += last_login_at, last_active_at.
- Auth-routes в web.php (НЕ api.php): Sanctum SPA нуждается в session-cookie
  middleware из web-группы (laravel.com/docs/sanctum#spa-authentication).
- cspell-words.txt: pdn, залогинен.

Pest +13 (всего 61/61 за 6.22s):
- login success + 2FA-flag + invalid pass + missing email + blocked + format
  validation + last_login_at update + register success/duplicate/без accept +
  me 401/200 + logout 200.
- Logout-test упрощён до 200+message - Pest cookie-jar держит session между
  запросами теста, full flow через browser-mode (отдельный коммит).
- phpstan-baseline: +25 ignored Pest TestCall warnings (Larastan+Pest quirk).

Регресс: pint+stan passed; vitest 129/129 за 9.59s; vite build 802ms;
story:build 21/28 за 30.39s; Pest 61/61 за 6.22s.

CLAUDE.md v1.31->v1.32, реестр Открытых_вопросов v1.40->v1.41.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:41:35 +03:00
Дмитрий 0eb2f72da2 phase1(security): roave/security-advisories — CVE-блокировка на install (Прил. Н #13)
Метапакет roave/security-advisories:dev-latest добавлен в require-dev.
Это conflict-only пакет — он не содержит кода, а просто помечает
известные CVE-версии других пакетов как несовместимые. При попытке
установки/обновления таких версий composer падает с conflict-resolution
до того, как уязвимый код попадёт в vendor/.

Текущий снапшот зависимостей чист: «No security vulnerability advisories
found» при установке (95 пакетов).

Закрывает Прил. Н #13. Не требует конфигурации, бинарников, env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:10:05 +03:00
Дмитрий e04f53b103 phase1(boost): laravel/boost v2.4.6 + Vuetify guideline (Прил. Н #10)
Установка Laravel Boost вручную (без `php artisan boost:install`):
wizard падает в обоих режимах из-за двух багов окружения:
  - интерактив: PHP fatal на кириллице-пути при рендере laravel/prompts
    multiselect (нет UTF-8 codepage в Windows PowerShell)
  - --no-interaction: ConfiguresPrompts::multiselectFallback возвращает
    null → array_map() crash в L13 framework

Manual setup вместо wizard'а:
  - app/composer.json: laravel/boost ^2.4 + транзитивы (laravel/mcp v0.7,
    laravel/roster v0.5.1, symfony/yaml v7.4)
  - app/boost.json: agents=[claude_code], guidelines=true, mcp=true
  - .mcp.json (корень): добавлен блок laravel-boost — command=php,
    args=[app/artisan, boost:mcp]. Заменяет PostgreSQL MCP из фазы 0
    (был убран в _comment_postgres ещё ранее).
  - app/.ai/guidelines/vuetify.md: кастомный Vuetify 3 guideline по
    Tooling §10.2 + Forest-палитра + 14 slug'ов воронки берутся из
    schema.sql:2076 (НЕ из BRANDBOOK_v2 §3.6).

Smoke-test (boost:mcp via JSON-RPC):
  - protocolVersion 2024-11-05, serverInfo "Laravel Boost"
  - 9 tools: application-info, browser-logs, database-(connections|query|
    schema), get-absolute-url, last-error, read-log-entries, search-docs

Уточнение к CLAUDE.md §7: «отключать guidelines Inertia/Livewire/Tailwind/
Filament/Sail/PHPUnit» — избыточно. Boost через laravel/roster детектит
установленные пакеты из composer.lock; то, что не установлено, не серверит.
Pest 4 будет detected, PHPUnit guideline не активируется (PHPUnit убран
из direct deps в коммите 30f0335). Тех-уточнение к §10.1 шаг 4 — отдельно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:05:21 +03:00
Дмитрий 03456833eb phase1(tooling): Pint + Larastan + IDE Helper (Прил. Н #11/#12/#14)
Установлены 3 dev-инструмента из Прил. Н фазы 1:

- laravel/pint v1.29 (форматтер PHP, Прил. Н #11) — Laravel preset
  по умолчанию. Команды: composer pint, composer pint:test
- larastan/larastan v3.9.6 + phpstan/phpstan v2.1.54 (статанализ
  PHP, Прил. Н #12) — level 5, paths app/bootstrap/config/database/
  routes/tests. Команда: composer stan
- barryvdh/laravel-ide-helper v3.7.0 (IDE-stubs, Прил. Н #14) —
  команда: composer ide-helper. Артефакт _ide_helper.php в gitignore

Конфиги:
- app/phpstan.neon — base config с includes на larastan extension
  и baseline. Включены checkOctaneCompatibility и checkModelProperties
- app/phpstan-baseline.neon — зафиксированы 3 ошибки в default
  Laravel scaffold (UserFactory return type covariance + 2× ExampleTest
  always-true). Двигаемся вверх с этого baseline'а.

Composer scripts (composer.json):
- pint, pint:test (форматирование / dry-run проверка)
- stan (PHPStan analyse с memory-limit 512M)
- ide-helper (generate + meta)

Smoke-test'ы все 3 прошли: Pint passed, PHPStan passed (с baseline),
ide-helper:generate написал _ide_helper.php.

Фикс окружения: на этой машине composer require падал с "Permission
denied на vendor/composer/tmp-XXX.zip" из-за race condition
параллельных curl'ов. Решение: COMPOSER_MAX_PARALLEL_HTTP=1
(сохранено в memory feedback_environment).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:18:06 +03:00
Дмитрий 30f0335f5f phase1(test): Pest 4 swap + reopen(CTO-12) — Pest 3 → Pest 4
Произошло так: при `composer require pestphp/pest pestphp/pest-plugin-laravel
--dev --with-all-dependencies` я не зафиксировал `^3` — composer подтянул
свежайшую Pest v4.7.0 + pest-plugin-laravel v4.1.0. Smoke-test
(`./vendor/bin/pest`) на default-тестах Laravel 11 прошёл 2/2 за 281 ms —
backward-compat с Pest 3 syntax подтверждён. Заказчик 08.05 (поздний вечер)
принял Pest 4 после live-проверки на стеке.

Бонус Pest 4: browser testing (без Dusk), stress testing, mutation
testing v2. Откат дёшев — `composer require pestphp/pest:^3`.

Что сделано:
- composer remove phpunit/phpunit (был direct dev-dep) — phpunit остался
  как транзитивная зависимость Pest
- composer require pestphp/pest:^4.7 pestphp/pest-plugin-laravel:^4.1 --dev
- Pest.php создан через `vendor/bin/pest --init` (`tests/Pest.php`)
- `vendor/bin/pest` smoke-test: 2 passed (Unit/Feature ExampleTest), 281 ms
- `vendor/bin/pest --init` упал на интерактивном промпте «Wanna show Pest
  some love?» в non-interactive PowerShell — Pest.php к этому моменту уже
  создан, нефатально

Reopen(CTO-12): по правилу «явная фиксация переоткрытий» обновлены
3 источника:
- docs/Открытые_вопросы_v8_3.md v1.15→v1.16: блок «Что изменилось в v1.16»,
  таблица §0 (CTO-12 переоткрыт+закрыт), запись §3 (Pest 4 + обоснование),
  финальный список §X (Pest 4)
- docs/Tooling_v8_3.md v1.2→v1.3: блок «Что нового в v1.3», §3.1 п.4
  (Pest 4 в boost:install), §3.4 строка 18 (Pest 4 + бонусы), §6 п.2
  (конфликт Pest↔PHPUnit), §10.1 п.9 (процедура установки + warning про
  --init на Windows non-interactive)
- CLAUDE.md v1.4→v1.5: §0 источники (Tooling v1.3, Реестр v1.16),
  §3.2 строка 18 (Pest 4), §7 п.5 (Pest 4 в boost:install), футер с
  историей версий

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:56:20 +03:00
Дмитрий 4d38f75826 phase1(scaffold): Laravel 11 + predis + .env под PG 16 + Memurai
Триггер фазы 1 запущен 08.05.2026 (вечер):
composer create-project laravel/laravel app

Стек подтверждён native (без Docker/WSL2/Sail):
- PostgreSQL 16.13 (Chocolatey, Windows-сервис, port 5432)
- Memurai Developer 4.1.8 (Redis 7-совм., port 6379) — TCP +PONG OK
- PHP 8.3.31 + 11/11 Laravel-required ext (pdo_pgsql, mbstring,
  openssl, tokenizer, xml, ctype, json, bcmath, fileinfo, curl, pgsql)
- Composer 2.9.7

Что в коммите (59 файлов, 11059 строк скаффолда Laravel 11 +
правки):
- composer require predis/predis (v3.4.2) — PHP-only Redis-клиент,
  т.к. php_redis ext не установлен (см. project_phase1_strategy.md)
- app/.env (gitignored) — APP_NAME=Liderra, APP_LOCALE=ru,
  APP_TIMEZONE=Europe/Moscow, DB_CONNECTION=pgsql → liderra@localhost,
  REDIS_CLIENT=predis
- app/.env.example — те же правки без секретов (для команды)

Smoke-test PG ↔ Laravel ↔ pdo_pgsql прошёл:
3/3 default-миграций → 9 таблиц в liderra (cache, cache_locks,
failed_jobs, job_batches, jobs, migrations, password_reset_tokens,
sessions, users).

Артефакт стартера app/database/database.sqlite (0 B) удалён —
sqlite не используется.

Что НЕ в этом коммите (следующие шаги фазы 1):
- Pest 3 swap (CTO-12) — composer remove phpunit + require pest
- Laravel Boost MCP + 9 guidelines disable по CLAUDE.md §5/§7
- Pint, Larastan, Roave/SecurityAdvisories, IDE Helper, squawk,
  pgFormatter (Прил. Н #11–18)
- resources/boost/guidelines/vuetify.blade.php

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:37:16 +03:00