e4673bea658d33a1bd8cb54a08bdde5d3cfbdc13
99 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e2de8bf8a2 |
docs(narrative): sync versions + Histoire 21/43 + cross-refs (audit P1-01/03/06/08 + P2-03 + post-A)
Phase D Спринта 1 «Hygiene». Закрытие аудита 2026-05-09 (
|
||
|
|
93b1af40d0 |
docs(plan-sprint1): implementation plan для Спринта 1 «Hygiene»
По spec'у 2026-05-09-sprint1-hygiene-design.md (
|
||
|
|
08bbe4d9c8 |
docs(spec-sprint1): дизайн Спринта 1 «Hygiene» исправления аудита 2026-05-09
Spec через superpowers:brainstorming. Scope: 22 правки + 3 записи в реестр: - 3 P0 (RLS impersonation_tokens, SetTenantContext middleware, format:sql:check Windows) - 10 P1 (P1-10/P1-11 → реестр) - 3 P2 (test password, орфо, F-K расшифровка) - 6 low-risk O-* (2 FK indices, password rules trait, ESLint anti-vuetify, npm CI, font-display docs) 6 фаз / 6 коммитов: A.DB → B.Backend → C.Configs → D.Docs(narrative) → E.Docs(handoff) → F.Registry. Зависимости: B/D зависят от A (CHANGELOG schema метрики). Бюджет: 2.5-3.5 часа агентов в субагентном режиме. CLAUDE.md/Pravila/Tooling правки в Фазе D — обязательно через claude-md-management:claude-md-improver (CLAUDE.md §5 п.10). Не входит: Спринт 2 (modernization) и Спринт 3 (big refactors) — отдельные spec'и. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b6ae8dd641 |
docs(audit): сводный отчёт аудита проекта на 2026-05-09 — P-дефекты + O-возможности
По spec v1.1 ( |
||
|
|
fb7334af05 |
docs(plan-audit): implementation plan для аудита проекта на 2026-05-09
По spec v1.1 (
|
||
|
|
b0343014db |
docs(spec-audit): пересмотр под Plugin_stack_rules_v1 v1.3 — spec v1.0 → v1.1
§12 «Плагины и MCP» переписана с полным inventory и R-обоснованиями: - 12.1 Superpowers-skills (6 применяемых + 8 из карты §12.2 не релевантны) - 12.2 Встроенные плагины: FD plugin НЕ призывается (R10+R13: аудит ≠ дизайн); claude-md-management вне DoD аудита - 12.3 Внешние плагины (R10): simplify/security-review/review/init/ui-ux-pro-max — только по явному /команде, не призываются - 12.4 MCP: laravel-boost (D2/D4), playwright опционально (D3), github не используется - 12.5 CLI-тулы — не плагины, регулируются Tooling Прил. Н В §3 расширен P0 stack-нарушениями (Pravila §13.9 hard-link на R10). В §6 D1 включает Pravila/Plugin_stack_rules_v1/Tooling в зону аудита; D3 проверяет R11 иерархию источников истины UI/UX; D5 проверяет R6 стек-фильтр Vue+Vuetify и R6.1 Forest. В §7 добавлен общий R-CHECKS блок поверх P/O чеклиста. В §10 DoD self-review проверяет «0 R-нарушений в самом отчёте»; code review мета-проверяет соблюдение Plugin_stack_rules_v1 v1.3. В §13 добавлены риски misuse FD plugin / priority chain / правок Pravila. Новая §15 — карта соответствия Plugin_stack_rules_v1 v1.3 → spec (13 R-правил + Pravila §12/§13.9 + CLAUDE.md §1/§5 п.10/п.11 + Tooling Прил. Н). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4f36bd3601 |
docs(plugin-stack-rules): CLAUDE.md v1.77→v1.81 + Pravila v1.4→v1.6 + Прил. Н v1.9→v1.10 + Plugin_stack_rules_v1 v1.3 (новый) — снятие запрета Frontend Design plugin + paired stack со Superpowers + 23 закрытых трения
Frontend Design plugin (anthropics/frontend-design) подключён через ~/.claude/settings.json paired stack'ом со Superpowers v5.1.0. Координация — через новый docs/Plugin_stack_rules_v1.md (10 правил R0-R9 + R10-R13 + 6 патчей F-K, всего 23 закрытых конфликта). v1.78: снят запрет CLAUDE.md §5 п.5 на Frontend Design plugin. Создан Plugin_stack_rules_v1.md (10 правил, 8 первичных конфликтов закрыты). Pravila +§13 «paired stack». Tooling +#30 в фазе 2. v1.79: PSR v1.0→v1.1 — 5 патчей по реальным трениям A-E (R6.1 hard-override Forest, R1 дезамбигуация компонент=UI-фича, R7 deployable+Pa11y, R0.6 hard-стоп список из 8 триггеров, runtime-заметка о skill list = constant per conversation). v1.80: PSR v1.1→v1.2 — принцип-аксиома «stack — головной» + R10 (внешние плагины как tools, реестр 11 плагинов с ролями: ui-ux-pro-max=резерв, claude-md-management=инструмент, review/security-review/init/simplify=только по /имя), R11 (иерархия 6 источников истины UI: Brandbook→ТЗ+schema→FD→Boost→UPM→Vue/Vuetify docs), R12 (три паттерна дизайн-решений), R13 (decision matrix Auto+§12+R0.6 на 14 типов задач). v1.81: PSR v1.2→v1.3 + Pravila v1.5→v1.6 — 6 трений второго порядка F-K. F: R12 архитектурное override §4.5 через явный brainstorming. G: R12 тактическое split на «с альтернативами» (A/B/C под user-стиль «а/б») и «без» (одна BOLD от FD). H: R13 строка про новую UI-фичу разделена — «вне ТЗ И не в Открытые_вопросы» = hard-стоп (Pravila §7). I: R11.4 fallback при технической недоступности уровней источников. J: R10.4 смягчение + Pravila §13.9 hard-link (нарушение R10 = нарушение §13). K: R0.1 точный scope «головенства» через таблицу priority chain. Skill list = constant per conversation — для активации FD требуется новый чат. cspell-words.txt +5 (инвокация, инвокирован, инвокируемые, инвокируются, головенство). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3d936d0da8 |
docs(spec-audit): добавлен дизайн подробного аудита проекта на 2026-05-09
Spec через superpowers:brainstorming. Гибридный подход (тулы + субагенты): 4 этапа (sweep тулами → 6 семантических субагентов → консолидация → self-review), P0/P1/P2 для дефектов и O-perf/O-refactor/O-stack для возможностей улучшения. Реализация — отдельным implementation plan через superpowers:writing-plans. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4cac61d748 |
docs(superpowers-hard-rule): CLAUDE.md v1.76→v1.77 + Pravila v1.3→v1.4 + Прил. Н v1.8→v1.9 — Superpowers как hard rule §12
Заказчик 09.05.2026 ввёл правило явной командой: «Создай правило, что ты всегда в первую очередь пользуешься superpowers. При этом ты не можешь игнорировать и обходить это правило». Pravila §12 — единственное hard-правило документа: §9 «Отступления» к нему не применяется. Pravila v1.4: - Добавлен §12 «Superpowers — приоритет первого выбора (hard rule)», 8 подсекций. - §12.1 Принцип: skill инвокируется ПЕРВЫМ перед любой содержательной задачей; обычный flow только если skill отсутствует. - §12.2 Карта 14 задач → 14 skills (TDD/debug/plan/parallel/review/ verify/brainstorm/worktree/finishing PR/subagent/writing-skills/ using-superpowers). - §12.3 Когда правило НЕ применяется: чтение/grep/glob, тривиальные правки, справочные ответы, документация §4, открытые вопросы (§7). - §12.4 Hard-rule статус: §9 не применяется; единственная отмена — явный запрос «не используй superpowers сейчас» только на текущее действие; рационализация — нарушение уровня §5 ПДн. - §12.5 §12 имеет приоритет над §11. - §12.6 §5/§7/§3.6 не override-ятся даже §12. - §12.7 Нарушения: фиксация в feedback memory. - §12.8 Ревизия: откат только явным запросом. - §0 priority расширен — §12 встаёт перед §1. CLAUDE.md v1.77 (через /claude-md-management:claude-md-improver): - §1 priority chain +уровень 0: «Pravila §12 — Superpowers hard rule». - §0 — Pravila v1.3 → v1.4. - §3.3 строка №19 дополнена пометкой «§12 hard rule». - §5 +п.11 «Не пропускать инвокацию Superpowers skill'а». Прил. Н v1.9: шапка + §4.1 callout про hard rule. cspell-words.txt: +5 слов второй порцией (инвокацию/инвокируется/ инвокирует/инвокировать/банами/банов). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4993790fd5 |
docs(superpowers-unblock): CLAUDE.md v1.75→v1.76 + Pravila v1.2→v1.3 + Прил. Н v1.7→v1.8 — снятие запрета на Superpowers
Заказчик 09.05.2026 принял вариант "A. Полное снятие, включая Pravila". Плагин obra/superpowers v5.1.0 (14 skills) подключён через ~/.claude/settings.json (extraKnownMarketplaces.superpowers-dev + enabledPlugins.superpowers@superpowers-dev). Декларативная установка — Claude Code сам подтянул кэш на рестарте. Pravila v1.3: - Добавлен §11 «Superpowers plugin — снят запрет, override §2.2/§4.5/§8.4». - §11.1: brainstorming/writing-plans/executing-plans/dispatching-parallel-agents могут перевешивать §4.5/§8.4/§2.2 при явном вызове skill'а. - §11.2: §1/§3.6/§5/§7/§9 не override-ятся. - §11.3: using-git-worktrees физически нестабилен на кириллическом пути — это факт среды, не правил. - §11.4: ревизия — заказчик может откатить §11 одной правкой. - Приоритет в §0 расширен до §11. Таблица версий +v1.3. CLAUDE.md v1.76 (через /claude-md-management:claude-md-improver по §5 п.11): - §5 п.4 удалён (запрет на 5 Superpowers skills). - Перенумерация 5→4..11→10. Перекрёстные ссылки переподписаны (п.11→п.10 в шапке п.7; пп.8→пп.7 внутри п.10). - §3.3 строка №19: «3 skills» → «v5.1.0, все 14 skills, override §2.2/§4.5/§8.4 разрешён, см. Pravila §11». - §0 — Pravila v1.2+ → v1.3 от 09.05.2026. Прил. Н v1.8 (Tooling_v8_3.md): - §4.1 «Поведенческий слой — Superpowers» полностью переписан: 3 skills → 14 skills, установка декларативная (settings.json), а не /plugin install. - Снятые ранее запреты сохранены как историческая запись. - §11.2 «Git worktrees»: запрет снят, но Windows + кириллица остаются враждебной средой, skill сам обрабатывает ошибки. cspell-words.txt: +5 слов (Лидерре, obra, override, ятся, ится). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f596e26153 |
docs(admin-tenant-detail-epic): фиксация Post-MVP AdminTenantDetailView закрытия
- Реестр v1.75→v1.76: запись о закрытии эпика, метрики Pest 416/416 + Vitest 416/416. - CLAUDE.md в этот раз не правлю: §5 п.11 — синхронизация при следующем /claude-md-management:claude-md-improver run'е. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5b13c95180 |
docs(claude-md): refactor v1.73→v1.74 — вынос changelog + правило single-entry plugin
(1) Структурный refactor — 250+ строк changelog v1.1→v1.73 вынесены из шапки CLAUDE.md в новый docs/CHANGELOG_claude_md.md (CLAUDE.md 380→234 строк, −39%; conciseness score 5/15→14/15 по quality-criteria плагина claude-md-management). (2) Введено правило §5 п.11 — все правки CLAUDE.md только через плагин claude-md-management из anthropics/claude-plugins-official: /claude-md-improver (audit + targeted updates) и /revise-claude-md (capture session-learnings). Прямые Edit/Write — нарушение. §5 п.8 дополнен оговоркой о применимости внутри flow п.11. В шапке добавлена строка про владельца. (3) cspell-words.txt +23 слова — техжаргон из перенесённого changelog'а (anthropics/CWD/маппится/бампается/etc); часть из них опечатки в исторических записях, не правлю исторический контент. Содержательно правила/состав 28 инструментов/метрики не тронуты — синхронизация Pravila/Tooling не требуется. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ab6a74052c |
docs(reports-epic): фиксация Post-MVP Reports backend закрытия
- Реестр v1.74→v1.75: запись о закрытии эпика, метрики Pest 403/403 + Vitest 393/393. - CLAUDE.md v1.72→v1.73: подробные изменения по 4 этапам. - cspell-words.txt: +«реструктура». Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
07ff7c8bb2 |
docs(mvp-closed): фиксация финала фазы 2 — Claude-зона MVP закрыта
- Реестр v1.73→v1.74: запись о закрытии MVP, остаточные blocked-only. - CLAUDE.md v1.71→v1.72: финал-метрики Pest 359/359 + Vitest 369/369. - cspell-words.txt: +«бэкенда» (творительный падеж). - Дальнейшая работа возобновляется по триггерам (Б-1, юр., фаза 3, deploy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
830a652588 |
phase2(trash-bin): GET /api/deals?only_deleted + «Корзина» в DealsView
Расширяет stages 5/6 (soft-delete + 8-сек undo) до постоянного доступа
к удалённым сделкам через отдельный view-mode.
Backend (DealController::index):
- Новый query-param only_deleted=true.
- withTrashed() + whereNotNull('deleted_at') — обход global scope
SoftDeletes + явный фильтр для NO-OP idempotency.
- Все остальные фильтры применимы и в trash-mode.
Pest +3 (DealIndexTest):
- only_deleted=true → только soft-deleted (alive скрыты).
- Без only_deleted → soft-deleted скрыты (default behavior).
- RLS+app-фильтр изолирует чужие удалённые.
Frontend:
- ListDealsParams.onlyDeleted?: boolean + axios mapping.
- DealsView: trashMode ref + toggleTrashMode (clear selected + reload) +
applyBulkRestoreFromTrash (optimistic remove + bulkRestoreDeals + toast).
- UI changes в trash-mode:
- Заголовок «Сделки» → «Корзина».
- Toggle-btn 'mdi-arrow-left К сделкам' (warning-flat) вместо
'mdi-trash-can-outline Корзина' (outlined).
- Скрыты Экспорт + Новая сделка.
- Скрыт chiprow filter-bar.
- Info-alert «Корзина: показаны удалённые сделки».
- Bulk-bar: только Восстановить (mdi-restore success-tonal) + clear;
status/export/delete скрыты.
Vitest +2 (DealsListIntegration):
- toggleTrashMode → trashMode=true + listDeals с onlyDeleted=true.
- applyBulkRestoreFromTrash → bulkRestoreDeals + remove from state +
toast «Восстановлено 2».
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 321/321 за 19.60 сек (+2 от 319).
- Vite build 1.04 сек.
- Pint + PHPStan passed.
- Pest 269/269 за 29.12 сек (+3 от 266, 1009 assertions).
Реестр v1.72→v1.73 / CLAUDE.md v1.63→v1.64.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
01c20e7b6c |
phase2(polling): usePolling composable 30 сек + Page Visibility pause
Закрывает последний unblocked production-TODO «Polling/SSE для real-time».
Manual reload-btn остаётся как fast-path; polling — фоновый автообновитель.
Composable (composables/usePolling.ts):
- usePolling(loader, {intervalMs=30_000, enabled=true}).
- Page Visibility API: при document.hidden=true interval останавливается;
при visibilitychange с возвратом hidden=false — restart + немедленный
loader() (не ждать следующего interval'а).
- Cleanup на onBeforeUnmount: clearInterval + removeEventListener.
- enabled=false — composable не стартует (feature-flag).
Integration:
- DealsView + KanbanView → loadDeals.
- AdminTenantsView → loadTenants.
- AdminBillingView → loadBilling.
- AdminIncidentsView → loadIncidents.
Vitest +6 (usePolling.spec.ts) с vi.useFakeTimers:
- Вызов каждые intervalMs / default 30 сек / skip при document.hidden /
cleanup на unmount / enabled=false → no-op / visibilitychange
pause+resume с немедленным loader.
Регресс:
- Lint+type-check+format passed.
- Vitest 319/319 за 18.67 сек (+6 от 313).
- Vite build 899 ms.
- Pint + PHPStan passed.
- Pest 266/266 за 28.62 сек (backend не тронут).
Реестр v1.71→v1.72 / CLAUDE.md v1.62→v1.63.
ВСЕ unblocked production-TODO закрыты.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
fa11c7b223 |
phase2(admin-tenants-mrr): mrr_rub в /api/admin/tenants (этап 7)
Закрывает gap из v1.66 — mock-форма имеет mrrRub, но API возвращал null. Теперь AdminTenantsView показывает реальную колонку MRR. Backend (AdminTenantsController::index): - Добавлено tariff_plans.price_monthly as tariff_price_monthly в select. - mrr_rub в response: price_monthly (string) если не-trial; иначе null. - Aggregate-формат как у /admin/billing — string чтобы decimal не терял точность при передаче через JSON. Pest +3 (AdminTenantsIndexTest): - mrr_rub='990.00' для активного тарифа не-trial. - mrr_rub=null для trial (даже если тариф есть). - mrr_rub=null если current_tariff_id отсутствует. Frontend: - ApiAdminTenant.mrr_rub: string | null в типе. - mapApiAdminTenant: parseFloat(api.mrr_rub) или null (вместо hardcoded null из v1.66). - AdminTenantsView: formatRub(item.mrrRub) для консистентности с другими ₽-полями. Vitest +2: - mrr_rub строка → number. - mrr_rub=null → mrrRub null. PHPStan baseline регенерирован. cspell-glossary +консистентности. Регресс: - Lint+type-check+format passed. - Vitest 313/313 за 18.83 сек (+2 от 311). - Vite build 947 ms. - Pint + PHPStan passed. - Pest 266/266 за 28.39 сек (+3 от 263, 1001 assertion). Реестр v1.70→v1.71 / CLAUDE.md v1.61→v1.62. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c34d4009d1 |
phase2(restore-flow): POST /api/deals/restore + undo-snackbar (этап 6 — completion of stage 5)
Soft-delete был half-done: пользователь не мог отменить случайное удаление.
Теперь после bulk-delete показывается snackbar «Удалено N · Восстановить»
на 8 секунд.
Backend (DealController::restore):
- POST /api/deals/restore {tenant_id, ids: [1..1000 ints]}.
- withTrashed() обходит global scope SoftDeletes + явный
whereNotNull('deleted_at') для NO-OP idempotency на живых.
- RLS + defense-in-depth where(tenant_id).
- ActivityLog event=deal.restored, context.source='bulk' для каждой
ВОССТАНОВЛЕННОЙ. Константа EVENT_DEAL_RESTORED добавлена в модель.
Pest +7 (DealRestoreTest):
- 422/404 базовые / soft-delete + restore + audit / NO-OP на живых
не пишет audit / defense-in-depth (свой restored, чужой остался) /
после restore видна в GET /api/deals / 422 пустой массив.
Frontend:
- dealsApi.bulkRestoreDeals — POST-helper.
- DealsView::applyBulkDelete: snapshot удалённых сделок (deep-clone
manager.*) сохраняется в lastDeletedSnapshot ref.
- undoBulkDelete() async: optimistic re-insert + bulkRestoreDeals если
auth.user; success → toast «Восстановлено N»; fail → warning.
- v-snackbar bulk-delete: 3→8 сек timeout + #actions слот с кнопкой
«Восстановить» (показ только при snapshot.length > 0). После undo
snapshot очищается → кнопка пропадает.
Vitest +3 (DealsListIntegration):
- bulk-delete + undo восстанавливает обе + bulkRestoreDeals + cleanup
snapshot.
- Undo без tenant_id — НЕ вызывает API + только локально.
- Undo reject → warning toast + локальное восстановление остаётся.
PHPStan baseline регенерирован. cspell-glossary +unshift +партиальный.
Регресс:
- Lint+type-check+format passed.
- Vitest 311/311 за 18.71 сек (+3 от 308).
- Vite build 877 ms.
- Pint + PHPStan passed.
- Pest 263/263 за 27.68 сек (+7 от 256, 998 assertions).
Реестр v1.69→v1.70 / CLAUDE.md v1.60→v1.61.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f0dce283a8 |
phase2(soft-delete): schema v8.9 + DELETE /api/deals (этап 5/5 — авто-план закрыт)
Bulk soft-delete для UI applyBulkDelete. Hard-delete отбракован из-за
CASCADE-FK от webhook_dedup_keys: hard уничтожил бы dedup-ключи и
нарушил идемпотентность webhook §5.5.
Schema v8.8 → v8.9:
- deals.deleted_at TIMESTAMPTZ (NULL = живая).
- Partial index (tenant_id, status) WHERE deleted_at IS NULL —
самый частый UI-фильтр.
- ALTER TABLE на партиционированной deals distributes во все 6
партиций автоматически (PG 14+).
- CHANGELOG +§U с обоснованием soft vs hard.
Backend (DealController::destroy):
- DELETE /api/deals {tenant_id, ids: [1..1000 ints]}.
- Bulk-update deleted_at=NOW() через RLS + defense-in-depth where(tenant_id).
- ActivityLog event=deal.deleted (source='bulk') для каждой ИЗМЕНЁННОЙ.
- NO-OP (уже удалена) не пишет audit.
- Deal model: SoftDeletes trait + deleted_at в fillable/casts. Global
scope автоматически добавляет whereNull('deleted_at') ко всем существующим
query (index/show/transition/update/export).
Pest +8 (DealDestroyTest):
- 422/404 базовые / soft-delete + audit / defense-in-depth (свой
удалён, чужой жив) / NO-OP idempotency / GET скрывает soft-deleted
(list+show 404) / 422 пустой массив.
- Quirk: migrate:fresh --env=testing без .env.testing использует liderra
вместо liderra_testing → решение DB_DATABASE=liderra_testing migrate:fresh.
Frontend:
- dealsApi.bulkDeleteDeals — DELETE-helper с config.data (axios особенность).
- DealsView::applyBulkDelete async: optimistic local-removal +
bulkDeleteDeals если auth.user; success → toast «Удалено N из M.»;
fail → warning toast + локальный update НЕ откатывается.
Vitest +3 (DealsListIntegration):
- bulkDeleteDeals с tenant_id + optimistic + toast.
- Без tenant_id — НЕ вызывается.
- Reject → warning toast + локальный update остаётся.
PHPStan baseline регенерирован.
АВТО-ПЛАН (5 этапов) ЗАКРЫТ ПОЛНОСТЬЮ.
Регресс:
- Lint+type-check+format passed.
- Vitest 308/308 за 20.12 сек (+3 от 305).
- Vite build 973 ms.
- Pint + PHPStan passed.
- Pest 256/256 за 27.75 сек (+8 от 248, 977 assertions).
Реестр v1.68→v1.69 / CLAUDE.md v1.59→v1.60.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
14dc317e2b |
phase2(admin-incidents): GET /api/admin/incidents + AdminIncidentsView API (этап 4/5)
Чтение incidents_log с фильтрами type/severity/unresolved_only + summary
(open/investigating/rkn_pending/total_unresolved).
Backend (AdminIncidentsController::index):
- ORDER BY started_at DESC. Filters: type, severity, unresolved_only=true.
- Derived: incident_id (INC-YYYY-MMDD-NNNN), status (resolved_at!=null →
resolved; detected_at!=null → investigating; иначе open),
affected_tenants_count из BIGINT[] (parsePgArray для '{1,2,3}'),
rkn_deadline_at = detected_at+24h для data_breach без notification.
- summary: open/investigating/rkn_pending/total_unresolved.
Pest +11 (AdminIncidentsIndexTest):
- пустой / incident_id формат / derive status / filter type+severity /
unresolved_only / ORDER BY started_at DESC / rkn_deadline +24h для
data_breach / non-data_breach без deadline / summary.rkn_pending /
limit+offset.
- Quirk: saas_admin_users.full_name (не first/last) + нет updated_at.
Frontend:
- api/admin.ts::listAdminIncidents — типизированный helper.
- AdminIncidentsView: унифицированный IncidentRow (mock-category ↔
API-type, mock-title ↔ API-summary). Reactive rowsState+stats default
= MOCK; loadIncidents() async на onMounted; fetchError + warning
alert + MOCK fallback; reload-btn. РКН pending chip учитывает оба
pdn_breach/data_breach.
Vitest +5:
- listAdminIncidents на mount / replace state+stats + rkn_deadline /
reject → fetchError+alert+fallback / reload-btn x2 / РКН pending chip
виден для data_breach без notification.
PHPStan baseline регенерирован. cspell-glossary +MMDD.
Регресс:
- Lint+type-check+format passed.
- Vitest 305/305 за 20.59 сек (+5 от 300).
- Vite build 1.05 сек.
- Pint + PHPStan passed.
- Pest 248/248 за 28.02 сек (+11 от 237, 951 assertion).
Реестр v1.67→v1.68 / CLAUDE.md v1.58→v1.59.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4532b95d64 |
phase2(admin-billing): GET /api/admin/billing + AdminBillingView API (этап 3/5)
Aggregates пополнений/списаний за текущий месяц по balance_transactions
+ summary с MRR/revenue/overdue/refunds_30d.
Backend (AdminBillingController::index):
- GET /api/admin/billing?search=. Per-tenant SUM с CASE WHEN type IN
('topup','lead_charge') GROUP BY tenant_id; ABS для charges.
- Row: id/subdomain/organization_name/contact_email/status/balance_rub/
tariff_id/tariff_name/mrr_rub (=tariff.price_monthly если не-trial)/
monthly_topups_rub/monthly_charges_rub/last_payment_at/
chargeback_unrecovered_rub.
- summary: total_mrr_rub (SUM не-trial), monthly_revenue_rub (SUM topup),
overdue_count (balance<0 || chargeback>0), refunds_count_30d.
- Quirk: schema-колонка tariff_plans.price_monthly (НЕ price_rub_monthly)
— обнаружено первым прогоном Pest, исправлено сразу.
Pest +9 (AdminBillingIndexTest):
- пустой / поля+tariff JOIN / aggregates за месяц / прошлый месяц не
попадает / overdue / refunds_30d (старые исключены) / total_mrr_rub
(trial исключаются) / search ILIKE / soft-deleted скрыт.
Frontend:
- api/admin.ts::listAdminBilling — типизированный helper.
- AdminBillingView: reactive rowsState+summary default = MOCK,
loadBilling() async на onMounted парсит API-строки → numbers + derive
status (suspended/balance<0||chargeback>0→overdue/active). На fail —
fetchError + warning alert + MOCK fallback. Reload-btn.
- tariffLabel/statusInfo обобщены с fallback'ами на новые slug'и.
Vitest +4:
- listAdminBilling на mount / replace rowsState+summary + string→number
+ status derive / reject → fetchError+alert+fallback / reload-btn x2.
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 300/300 за 18.41 сек (+4 от 296).
- Vite build 925 ms.
- Pint + PHPStan passed.
- Pest 237/237 за 27.69 сек (+9 от 228, 926 assertions).
Реестр v1.66→v1.67 / CLAUDE.md v1.57→v1.58.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6ef9961f5f |
phase2(admin-tenants): GET /api/admin/tenants + AdminTenantsView API (этап 2/5)
AdminTenantsView переходит с mock-данных на live backend.
Backend (AdminTenantsController::index):
- GET /api/admin/tenants?status=&search=&limit=&offset=.
- LEFT JOIN tariff_plans для tariff_name. ORDER BY last_activity_at DESC.
- ILIKE search по organization_name + subdomain + contact_email.
- stats {total, active, trial, overdue} — overdue считает balance<0
ИЛИ chargeback_unrecovered_rub > 0.
- На MVP без auth (saas-admin SSO ⏸ Б-1).
Pest +8 (AdminTenantsIndexTest):
- 200 + пустой / все поля / status filter / search ILIKE /
ORDER BY last_activity_at DESC / stats / soft-deleted скрыт /
limit+offset.
Frontend:
- api/admin.ts::listAdminTenants — типизированный helper.
- composables/adminTenantsMapper.ts::mapApiAdminTenant — converter
API → UI: status derive (is_trial→trial, chargeback>0||balance<0
→overdue), inn='', code=subdomain, tariff clamp на known TenantTariff,
todayActual/mrrRub отсутствуют в API → 0/null, activitySince через
formatRelative.
- AdminTenantsView: reactive tenantsState+stats default = MOCK,
loadTenants() на onMounted → splice replace; на fail — fetchError +
warning alert + MOCK fallback. Reload-btn.
Vitest +13:
- View-integration (4): listAdminTenants на mount / replace state+stats /
reject → fetchError + alert + fallback / reload-btn x2.
- Mapper (9): name/code/inn/status-derives (trial/overdue/suspended) /
balance_rub→number / activitySince + null fallback.
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 296/296 за 18.91 сек (+13 от 283).
- Vite build 1.02 сек.
- Pint + PHPStan passed.
- Pest 228/228 за 25.22 сек (+8 от 220, 906 assertions).
Реестр v1.65→v1.66 / CLAUDE.md v1.56→v1.57.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7e1bf8b42d |
phase2(deal-patch): PATCH /api/deals/{id} + comment-editor в DealDetailDrawer (этап 1/5)
Drawer из read-only становится editable. ActivityLog event пишется на
каждое изменение поля.
Backend (DealController::update):
- PATCH /api/deals/{id} {tenant_id, comment?, manager_id?, status?}.
- Каждое изменённое поле → ActivityLog:
comment → deal.commented (context.text);
manager_id → deal.assigned (context.from/to + assigned_at=NOW);
status → deal.status_changed (context.from/to/source='manual').
- NO-OP не пишется в audit. Manager FK guard + status slug validation.
- RLS + defense-in-depth where(tenant_id) → 404 для чужой сделки.
Pest +10 (DealUpdateTest):
- 422/404 базовые / 404 чужая сделка / comment+audit / manager+audit+
assigned_at / status+audit / 422 неизвестный slug / 422 чужой manager /
NO-OP не пишет / комбинированно → 2 audit записи.
Frontend:
- api/deals.ts::updateDeal — PATCH helper c ensureCsrfCookie.
- DealDetailDrawer: новая секция «Комментарий» (только при tenantId).
v-textarea auto-grow + counter=5000 + Save-btn → updateDeal →
toast success + reload events (новый deal.commented в timeline).
На fail → warning toast.
Vitest +3 (DealDetailDrawerApi):
- saveComment вызывает updateDeal + toast + reload events (getDeal x2).
- saveComment reject → commentSaveError + warning toast.
- comment-section не рендерится без tenantId.
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 283/283 за 18.13 сек (+3 от 280).
- Vite build 1.12 сек.
- Pint + PHPStan passed.
- Pest 220/220 за 25.64 сек (+10 от 210, 871 assertion).
Реестр v1.64→v1.65 / CLAUDE.md v1.55→v1.56.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e31ea5354a |
phase2(lead-statuses): GET /api/lead-statuses + Pinia-store с snapshot fallback
Заменяет static-снапшот LEAD_STATUSES в коде на live-данные из БД. Custom slug'и (добавленные после deployment'а) теперь видны UI без rebuild'а. Backend: - LeadStatus model (PK=slug string, incrementing=false, timestamps=null). - LeadStatusController::index — GET /api/lead-statuses, ORDER BY sort_order, slug. Таблица глобальная (не tenant-aware), auth не требуется на MVP. Pest +5 (LeadStatusesIndexTest): - 200 + не пустой / все 14 системных slug'ов из seed / все нужные поля / sort_order ASC / кастомный slug после INSERT появляется в endpoint'е. Frontend: - api/leadStatuses.ts::listLeadStatuses — GET helper. - stores/leadStatuses.ts::useLeadStatusesStore — Pinia setup-store: statuses default = LEAD_STATUSES snapshot (UI работает без fetch'а), load(force=false) идемпотентен, bySlug computed Map, findBySlug helper. На fail — snapshot остаётся, fetchError=true. - DealsView/KanbanView/DealDetailDrawer переехали со static-импорта LEAD_STATUSES на store. KanbanView использует safe-access dealsByStatus[slug] || [] (защита от custom slug'а из API без seeded column). load() в onMounted у обоих view'ов. Vitest +7 (leadStatusesStore.spec.ts): - initial snapshot / findBySlug existing & null / load success replace + loaded / load reject — fetchError + snapshot fallback / load идемпотентен / load(force=true) refetch. - 2 spec'а DealDetailDrawer получили setActivePinia(createPinia()) в beforeEach (без этого Pinia store-injection в jsdom падает). PHPStan baseline регенерирован. Регресс: - Lint+type-check+format passed. - Vitest 280/280 за 19.44 сек (+7 от 273). - Vite build 1.17 сек. - Pint + PHPStan passed. - Pest 210/210 за 24.59 сек (+5 от 205, 840 assertions). Реестр v1.63→v1.64 / CLAUDE.md v1.54→v1.55. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cba76c5d18 |
phase2(deal-show): GET /api/deals/{id} + DealDetailDrawer на реальный ActivityLog
Закрывает gap «timeline в drawer'е показывает hard-coded MOCK_EVENTS» —
теперь drawer fetch'ит реальные activity-events на open из tenant-filtered
activity_log. Без tenant_id — fallback на MOCK_EVENTS как раньше.
Backend (DealController::show):
- GET /api/deals/{id}?tenant_id={id} — возвращает {deal, events}.
- Deal extended (project_name, manager_name/initials, comment, assigned_at).
- Events — последние 50 записей activity_log по (tenant_id, deal_id)
ORDER BY created_at DESC, с актором (user через belongsTo).
- RLS-обёртка + defense-in-depth where(tenant_id) — 404 если чужая.
Pest +8 (DealShowTest):
- 422/404 базовые / 404 чужая сделка / deal-relations / events ORDER BY +
actor + actor=null для system-event / RLS+app-фильтр изоляция событий /
лимит 50 событий.
Frontend:
- api/deals.ts::getDeal — типизированный helper c ApiDealEvent/Detail/Response.
- composables/dealsApiMapper.ts::mapApiDealEvent — converter ApiDealEvent →
DealEvent: clamp event-slug на known types с fallback на 'deal.viewed';
detail зависит от type (status_changed: 'from → to'; created: source;
остальные: JSON-сводка context).
- DealDetailDrawer: optional tenantId prop, watch([open, deal.id, tenantId])
с immediate=true → loadEvents() на open. Reject → eventsFetchError +
v-alert warning + MOCK_EVENTS fallback.
- DealsView/KanbanView передают :tenant-id="auth.user?.tenant_id".
Vitest +4 (DealDetailDrawerApi.spec.ts):
- Без tenantId — getDeal не вызывается + MOCK_EVENTS видны.
- С tenantId — getDeal + events замещены + 'new → paid' виден.
- reject → fetchError + alert + MOCK_EVENTS fallback.
- open=false → getDeal не вызывается.
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 273/273 за 20.76 сек (+4 от 269).
- Vite build 1.12 сек.
- Pint + PHPStan passed.
- Pest 205/205 за 24.19 сек (+8 от 197, 812 assertions).
Реестр v1.62→v1.63 / CLAUDE.md v1.53→v1.54.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
ac186593f2 |
phase2(bulk-transition+reload): POST /api/deals/transition + reload-btn
Закрывает gap «UI меняет статус, но изменения не сохраняются на backend»
из v1.51. Reload-btn заменяет polling/SSE до прихода long-poll'а на prod.
Backend (DealController::transition):
- POST /api/deals/transition {tenant_id, ids: [1..1000 ints], status}.
- Валидация status — exists в lead_statuses (глобальная таблица).
- RLS-обёртка SET LOCAL + defense-in-depth where(tenant_id) для
partial-update: чужие id остаются в исходном статусе.
- ActivityLog event=deal.status_changed с context={from, to, source: 'bulk'}
для каждой ИЗМЕНЁННОЙ сделки. NO-OP (старый==новый) не пишется в audit.
- Ответ: {updated, requested, status}.
Pest +7 (DealTransitionTest):
- 422 missing fields / 404 unknown tenant / 422 неизвестный slug + не апдейт /
batch update 3 сделок + 3 ActivityLog с правильным context /
NO-OP не пишет ActivityLog / defense-in-depth (2 tenant'а — обновляется
только свой) / 422 пустой массив ids.
Frontend:
- dealsApi.transitionDeals — типизированный helper с ensureCsrfCookie.
- applyBulkStatus в DealsView переписан async: optimistic local-update +
backend-вызов если auth.user.tenant_id. На success — toast «Обновлено
N из M.», на fail — warning toast + локальный update НЕ откатывается.
Без auth.user — только optimistic (legacy local-mode сохранён).
- reload-btn в DealsView и KanbanView — outlined «Обновить» mdi-refresh,
привязан к loadDeals. В DealsView :loading="loading" во время fetch'а.
Vitest +5:
- reload-btn (Deals + Kanban) — listDeals вызывается дважды.
- applyBulkStatus с tenant_id — transitionDeals + optimistic + toast.
- applyBulkStatus без tenant_id — НЕ вызывается transitionDeals.
- applyBulkStatus reject — toast warning + локальный update остаётся.
PHPStan baseline регенерирован. cspell-glossary +апдейт*.
Регресс:
- Lint+type-check+format passed.
- Vitest 266/266 за 18.16 сек (+5 от 261).
- Vite build 1.06 сек.
- Pint + PHPStan passed.
- Pest 193/193 за 23.27 сек (+7 от 186, 767 assertions).
Реестр v1.60→v1.61 / CLAUDE.md v1.51→v1.52.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
339e8ea53b |
phase2(deals-list-api): GET /api/deals + замена MOCK_DEALS на fetch с fallback
Закрыт TODO (c) из v1.50: backend-эндпоинт для списка сделок и его
интеграция в DealsView/KanbanView вместо статичного MOCK_DEALS.
Backend (DealController::index):
- Query-params: tenant_id (required, 422/404), status_in[] (whereIn),
project_id, manager_id, search (ILIKE по phone+contact_name),
limit clamp [1..500] default 100, offset default 0.
- ORDER BY received_at DESC, id DESC. Eager-load project + manager.
- RLS-обёртка SET LOCAL app.current_tenant_id + defense-in-depth
where(tenant_id) — на тестах через postgres superuser RLS обходится
BYPASSRLS, app-фильтр гарантирует изоляцию.
- Ответ: {deals: [...], total, limit, offset}; manager_name/initials
форматируются через ManagerController::formatName/formatInitials.
Pest +12 (DealIndexTest):
- 422/404, пустой список, relations (project_name+manager_name+initials),
RLS-изоляция, ORDER BY, status_in[], project_id, manager_id, search
ILIKE, limit+offset, manager=null edge case.
Frontend:
- api/deals.ts::listDeals — типизированный helper c ApiDeal/ListDeals*.
- composables/dealsApiMapper.ts::mapApiDeal — converter ApiDeal→MockDeal:
contact_name fallback на phone, manager.name='Не назначен' /
initials='—' при null, project='—' при null, cost=0,
receivedMinutesAgo=max(0, …) от clock-skew.
- DealsView/KanbanView: onMounted(loadDeals) async-вызывает listDeals
если auth.user.tenant_id, на success replace через splice, на fail
fetchError=true + v-alert warning, MOCK_DEALS как fallback.
Vitest +14:
- dealsApiMapper.spec.ts (8): 1:1, fallback'и, edge cases.
- DealsListIntegration.spec.ts (6): без tenant_id — НЕ вызывает API,
с tenant_id — replace state, reject → fetchError + alert + fallback;
для DealsView и KanbanView.
PHPStan baseline регенерирован. cspell-glossary +ILIKE +DTO.
Регресс:
- Lint+type-check+format passed.
- Vitest 261/261 за 19.62 сек (+14 от 247).
- Vite build 989 ms.
- Pint + PHPStan passed.
- Pest 186/186 за 22 сек (+12 от 174, 742 assertions).
Реестр v1.59→v1.60 / CLAUDE.md v1.50→v1.51.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
eb8ca65c5d |
phase2(supplier-resolver): вынос дублированной логики lookup в App\Services\SupplierResolver
Закрыт TODO (a) из v1.49: общая логика выбора активного supplier'а через
project_suppliers m2m была дублирована между ProcessWebhookJob (webhook-flow)
и DealController (manual-create) — 11 одинаковых строк query-builder'а на
2 файла. Теперь — единственный источник истины + DI через app() (тот же
паттерн, что у DuplicateDetector в v1.23).
App\Services\SupplierResolver:
- resolveForProject(Project): ?int — точная копия прежней query
(project_suppliers JOIN suppliers, is_active+is_active, ORDER BY
sort_order, id).
- costRubSnapshot(int $supplierId): string — вынесенный snapshot цены
для записи в supplier_lead_costs.
ProcessWebhookJob и DealController:
- Удалены private resolveSupplierId() (по 14 строк).
- Удалены локальные DB::table('suppliers')->value('cost_rub').
- Используют app(SupplierResolver::class) внутри handle()/store().
Pest +8 в tests/Feature/Services/SupplierResolverTest.php:
- null без связей / единственный активный / пропуск inactive supplier /
пропуск inactive m2m / ORDER BY sort_order / null если все inactive /
изоляция по project_id / costRubSnapshot формат '137.50'.
PHPStan baseline регенерирован.
Регресс:
- Pint + PHPStan passed (baseline регенерирован).
- Pest 174/174 за 21.46 сек (+8 от 166, 708 assertions).
- Vitest 247/247 за 17.53 сек (нетронут — backend-only refactor).
Реестр v1.58→v1.59 / CLAUDE.md v1.49→v1.50.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
515114cff5 |
phase2(lookups+integrity): GET /api/managers+projects + manager FK guard + SupplierLeadCost для manual
3 интеграционных доработки после backend-completion v1.57.
(1) GET /api/managers + /api/projects + manager FK guard:
- ManagerController::index — active users тенанта (is_active+deleted_at IS NULL).
Формат {id, email, first_name, last_name, name, initials} с
formatName/formatInitials helpers (fallback на email).
- ProjectController::index — active projects (is_active=true).
- Оба endpoint'а: tenant_id query-param, 422 без, 404 unknown, RLS-обёртка.
- DealController::store FK guard: manager_id должен принадлежать tenant'у +
is_active. Иначе 422 (закрывает security-gap чужого менеджера).
- Pest +8 в LookupsTest.
(2) Replace MOCK_MANAGERS / MOCK_PROJECTS на API в NewDealDialog:
- projectOptions/managerOptions ref'ы с MOCK fallback.
- loadLookups через Promise.all([listProjects, listManagers]) на open
диалога с tenantId.
- managerIdByName Map name→id для submit'а.
- Silent fallback на mock при network-error.
- Vitest +2.
(3) SupplierLeadCost для manual-leads:
- В DealController::store после Deal::create — resolveSupplierId (копия
логики ProcessWebhookJob: project_suppliers JOIN suppliers + ORDER BY
sort_order). Если supplier найден — SupplierLeadCost с snapshot cost_rub
+ supplier_lead_id=NULL (manual: нет внешнего id).
- Manual по-прежнему НЕ списывает баланс (Ю-2 reseller-модель — charge
только при webhook'е); cost-аналитика всё равно нужна.
- Pest +2.
- TODO: рефактор resolveSupplierId в App\Services\SupplierResolver чтобы
Job + Controller разделяли логику.
Старый тест manager_id=42 переписан под FK guard через User::factory.
PHPStan baseline регенерирован (+28 ignored Pest TestCall warnings).
Регресс: lint+type-check+format ✅; vitest 247/247 за 16.32 сек (+2);
vite build 951 ms; Pint+PHPStan passed; Pest 166/166 за 22.11 сек
(+10 от 156, 699 assertions). Реестр v1.57→v1.58, CLAUDE.md v1.48→v1.49.
Production TODO остаточные:
- resolveSupplierId → SupplierResolver service.
- XLSX-export через PhpSpreadsheet.
- GET /api/deals для replace MOCK_DEALS в DealsView/KanbanView.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
83bb9de2bb |
phase2(backend-completion): POST /api/deals + webhook_hmac_required + POST /api/deals/export
3 backend-completion после tightening v1.56.
(1) POST /api/deals — manual create endpoint:
- DealController::store. Project firstOrCreate (type='manual'). Deal с
source_crm_id=NULL. RLS-обёрнутая транзакция.
- Manual НЕ списывает баланс / НЕ дедуп / НЕ SupplierLeadCost.
ActivityLog с context.source=manual.
- NewDealDialog получил optional tenantId prop. С tenantId — POST → backend-id;
на error fallback на local-id + warning + dialog open.
- DealsView/KanbanView передают auth.user?.tenant_id.
- Pest +8.
(2) webhook_hmac_required flag в system_settings:
- Seed-row в db/schema.sql (default false backward-compat).
- WebhookReceiveController::isHmacRequired private helper.
- При true: запрос без X-Webhook-Signature → 401.
- Pest +3.
(3) POST /api/deals/export — backend CSV:
- DealController::export. Валидация ids[1-10000]. RLS-обёрнутый whereIn.
- Excel-friendly CSV: BOM "\u{FEFF}" PHP-литерал, ; разделитель, \r\n.
- text/csv attachment headers.
- Frontend applyBulkExport: backend → fallback на client-side
(buildLocalCsv вынесен).
- Pest +4.
Vitest +3 (всего 245/245).
PHPStan убрал лишнюю Deal->id===null проверку (Eloquent int).
DealsView/KanbanView spec'ы получили setActivePinia.
Регресс: lint+type-check+format ✅; vitest 245/245 за 17.07 сек (+3);
vite build 1.04 сек; Pint+PHPStan passed; Pest 156/156 за 20.27 сек
(+15 от 141, 675 assertions). Реестр v1.56→v1.57, CLAUDE.md v1.47→v1.48.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4a385b1df7 |
phase2(prod-tightening): HMAC+rate-limit webhook / fetch system_settings / CSV export
3 production-tightening после 7-фичного пакета v1.55. (1) HMAC + per-token rate-limit для webhook receive endpoint: - WebhookReceiveController::receive: tenant lookup → rate-limit → HMAC → payload validation. - HMAC: опциональный X-Webhook-Signature: sha256=<hex> через hash_hmac + hash_equals (constant-time). Backward-compat: header missing → 202. - Per-token rate-limit: RateLimiter с decay 60 сек. Лимит из system_settings.webhook_rate_limit_rps × 60. На превышении 429 + Retry-After. Hit ставится ДО валидации payload — иначе обходимо 422. - Pest +5: HMAC valid/invalid 401/missing 202; rate-limit 60+1=429; ключ изолирован per-token. (2) Реальный fetch system_settings в AdminSystemView: - onMounted → adminApi.listSystemSettings() → splice replace. - На fetch-error → fallback на mock + warning v-alert. - Кнопка «Обновить» — ручной reload. - Vitest +3: mount fetch / reload / error fallback. (3) Реальный CSV-export для bulk-actions DealsView: - applyBulkExport → CSV через Blob+a[download]. - 8 колонок, ; разделитель, \r\n, BOM через String.fromCharCode(0xFEFF) (литеральный U+FEFF блокируется ESLint no-irregular-whitespace). - Filename deals_export_YYYY-MM-DD.csv. - Empty selection → toast без download. - Vitest +2: spy createObjectURL+anchor.click; empty без blob. PHPStan baseline регенерирован. Регресс: lint+type-check+format ✅; vitest 242/242 за 15.82 сек (+4); vite build 903 ms; Pint+PHPStan passed; Pest 141/141 за 17.8 сек (+5, 627 assertions). Реестр v1.55→v1.56, CLAUDE.md v1.46→v1.47. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
768628d914 |
phase2(7-features): bulk-actions / new-deal / tenant-card / system-edit / webhook / smart-filters / impersonation-list
7-фичный auto-mode пакет согласно «карте что осталось» (после v1.54).
(1) Bulk-actions DealsView:
- dealsState reactive-копия MOCK_DEALS (deep-clone) для безопасного bulk-edit.
- Bulk-bar (sticky, теало-нуар, theme=dark) при selected.length > 0:
count + Сменить статус (v-menu × 14 lead_statuses) + Экспорт (snackbar) +
Удалить (v-dialog confirm) + ✕ clear.
- На production: smart status-transition с проверкой allowed-переходов;
soft-delete (архив 30 дней); реальный CSV/XLSX export через xlsx-lib.
(2) NewDealDialog (used in DealsView+KanbanView):
- 6 полей: name/phone/project (MOCK_PROJECTS) / manager (MOCK_MANAGERS) /
cost / status (default 'new' или presetStatus). Phone-валидация ≥10 цифр.
- emit('created', deal) → DealsView push в начало dealsState; KanbanView push
в правильную колонку по statusSlug + totalDeals++.
(3) AdminTenantDetailView (/admin/tenants/:code):
- 4 KPI cards (Баланс/runway / Тариф+MRR/мес / Лиды сегодня+неделя+месяц /
Средняя цена). 4 v-tabs: Финансы (balance-history) / Пользователи /
Проекты / Активность с event-кодами.
- Кнопка «Войти как клиент» (использует ImpersonationDialog из v1.54).
404-fallback. composables/mockTenantDetail.ts с expandTenantDetail.
- AdminTenantsView получил @click:row → router.push.
(4) Edit-flow AdminSystemView (audit-log + 2-step):
- Backend: SystemSetting + SaasAdminAuditLog Eloquent (append-only,
payload_before/after JSONB casts).
- AdminSystemSettingsController с GET (list) + PUT (update в DB::transaction
+ INSERT в saas_admin_audit_log; hash-chain trigger BEFORE INSERT
заполняет log_hash).
- Type-validation: int/decimal/bool/json. Reason ≥30 chars. No-op → 422.
- Frontend SystemSettingEditDialog — 3-step (edit → confirm с diff
before/after → done).
(5) Webhook receive endpoint (POST /api/webhook/{token}):
- WebhookReceiveController::receive. Token = tenants.webhook_token.
- 404 unknown / 422 bad payload / 202 success + dispatch ProcessWebhookJob.
- Stub-INSERT в webhook_log через DB::table обёрнут в DB::transaction +
SET LOCAL app.current_tenant_id для RLS.
- CSRF-исключение для api/webhook/* в bootstrap/app.php.
- На prod: + HMAC X-Webhook-Signature + per-token rate-limit.
(6) Smart-filters:
- DealsView: multi-select v-select Проект+Менеджер с auto availableProjects/
availableManagers computed.
- AdminTenantsView: filterStatuses (4 STATUS_OPTIONS) + filterTariffs
(computed availableTariffs).
- Кнопка «Сбросить» появляется только когда фильтры активны.
(7) AdminImpersonationView (/admin/impersonation):
- Backend +2 GET endpoints: /active (used_at != null AND session_ended_at
== null) + /recent (last 20 завершённых с duration_seconds через
abs(diffInSeconds) — Carbon signed по умолчанию).
- ImpersonationToken получил belongsTo(Tenant).
- Frontend view: 2 секции (Активные с end-кнопкой / Недавно завершённые
read-only) + refresh + onMounted load.
- Маршрут /admin/impersonation + 5-й nav-пункт «Impersonation» в AdminLayout.
Vitest +48 (всего 238/238 за 15.31 сек).
Pest +16 (всего 136/136 за 15.8 сек, 495 assertions).
PHPStan baseline регенерирован (0 errors после фикса nullsafe.neverNull).
Регресс: lint+type-check+format ✅; vite build 937 ms; Pint+PHPStan passed;
Pest 136/136. Реестр v1.54→v1.55, CLAUDE.md v1.45→v1.46.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
61afa72591 |
phase2(impersonation-ui): UI dialog для Ю-1 в AdminTenantsView (frontend)
Закрывает TODO из v1.44 — frontend для Impersonation backend (`1963694`).
api/admin.ts:
- impersonationInit/Verify/End — типизированные axios-helpers для трёх
endpoint из v1.53. Все три — ensureCsrfCookie + apiClient.post.
На prod автоматически перейдут под middleware('auth:saas-admin').
components/admin/ImpersonationDialog.vue — 4-step state-machine:
- step 1 «reason»: v-textarea ≥30 chars + counter + hint «Ещё N символов».
- step 2 «verify»: info-alert email клиента + 6-digit input
(autocomplete=one-time-code) + dev-banner с _dev_plain_code.
- step 3 «active»: success-alert + кнопка «Завершить сессию».
- step 4 «done»: финальный success.
- persistent dialog (нельзя закрыть кликом за пределами — audit trail).
- watch(modelValue) сбрасывает state при каждом открытии.
AdminTenantsView:
- 8-я колонка actions (width=56) с v-tooltip + icon-btn mdi-account-switch.
- :disabled на suspended (по ТЗ §22.7 — только активные tenant'ы).
- @click.stop, data-testid=impersonate-btn-{id}.
- ADMIN_USER_ID=1 заглушка (на prod удалится — backend возьмёт из auth).
Vitest +11 (всего 190/190 за 13.23 сек):
- ImpersonationDialog.spec.ts (7): hide когда modelValue=false; step-1 mount;
reason<30 показывает counter; init→step2 (email+dev-banner); verify→step3
(end-btn); 5-digit code не вызывает API; end→step4; Cancel emit.
- AdminTenantsView.spec.ts (+4): impersonate-btn в каждой строке; suspended
disabled; click открывает диалог с правильным tenant; props.requestedBy=1.
Vitest quirk: v-dialog/v-tooltip требуют layout-injection — stub'ы
VDialog как passthrough <div v-if="modelValue"><slot/></div>, VTooltip как
<div><slot name="activator" :props="{}"/></div>. ImpersonationDialog
stub'ится в AdminTenantsView spec. api/admin + helpers extractValidationErrors/
extractErrorMessage мокаются через vi.mock — axios.isAxiosError(plain Error)
в jsdom возвращает false (паттерн из auth-store.spec.ts).
Production TODO: SaaS-admin auth (Yandex 360 SSO, Б-1) → middleware,
two-person approval (CTO-15/Ю-9), MailService → _dev_plain_code исчезает,
live cookie-swap session, страница «Активные impersonation-сессии».
Регресс: lint+type-check+format+build OK (924 ms; AdminTenantsView lazy-chunk
20.68 KB включает inline ImpersonationDialog); Vitest 190/190 за 13.23 сек;
Pest 120/120 за 15.69 сек (нетронут). Реестр v1.53→v1.54, CLAUDE.md v1.44→v1.45.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
1963694778 |
phase2(impersonation): backend Ю-1 (init-token + verify + end)
- ImpersonationToken Eloquent (UPDATED_AT=null) + isExpired/isUsable helpers
- ImpersonationController 3 endpoints: init / verify / end
- init: reason ≥30 chars, 6-значный random_int код, bcrypt-hash, TTL 15 мин
- verify: Hash::check; failed_attempts+=1; ≥5 → invalidated_at
- end: session_ended_at; 422 если не было verify
- _dev_plain_code в response (prod заменит email клиенту)
- Routes /api/admin/impersonation/{init,verify,end} (без middleware на MVP)
- TODO: saas-admin auth (Yandex 360 SSO) + two-person approval (CTO-15/Ю-9) + email клиенту + UI dialog в AdminTenantsView
- Pest +9 ImpersonationTest (120/120 за 15.62с, 443 assertions)
- Регресс: lint+type+format OK; build 846ms; Pint+Stan passed
- CLAUDE.md v1.43→v1.44, реестр v1.52→v1.53
- Все 9 пунктов списка v1.46 закрыты (кроме #6 SSO ⏸ Б-1 и #7 browser-mode отложен инфра)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f65b2ca8d8 |
phase2(admin-views): AdminBilling/Incidents/System — реальные display-views
- AdminBillingView: 4 stats (MRR, Выручка, Просрочка, Возвраты) + v-data-table 7 колонок (Тенант с ИНН / Тариф / Баланс с error-color / пополнения / списания / MRR / Статус-chip) + поиск
- AdminIncidentsView: 3 stats + 5 фильтров статуса + v-list с incident_id (INC-YYYY-MMDD-NNNN) + severity/status/РКН-pending chips + дедлайн 24ч по 152-ФЗ
- AdminSystemView: read-only warning + поиск + v-list 7 system_settings (webhook_rate_limit, login_max_attempts, retention и т.д.) с type-chip и updated_at
- composables/mockAdmin.ts: AdminBillingTenantRow + AdminIncidentRow + AdminSystemSetting + mock-данные
- Router: /admin/{billing,incidents,system} → реальные views (не placeholder)
- Vitest +13 (179/179 за 11.98с)
- TODO: edit-flow для system_settings + backend /api/admin/* endpoints
- Регресс: lint+type+format OK; build 743ms; story:build 21/28 за 31.5с
- CLAUDE.md v1.42→v1.43, реестр v1.51→v1.52
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
2e91469a07 |
phase2(suspicious-login): email при 3 неудачах login (ТЗ §22.4.4 п.3)
- App\Mail\SuspiciousLoginNotification Mailable + emails.suspicious_login Blade - maybeNotifySuspiciousLogin срабатывает ровно при count==3 (защита от спама на 4-5) - Для unknown email — skip (некому) - На dev MAIL_MAILER=log → письмо в storage/logs ТЗ §22.4.4 анти-брутфорс закрыт полностью: - email-rate-limit 5/15мин (v1.36) - IP-lockout 10/час (v1.41) - email-notify при 3 неудачах (this commit) - Pest +4 SuspiciousLoginNotificationTest (111/111 за 14.32с, 401 assertions) - Регресс: Pint+Stan passed - CLAUDE.md v1.41→v1.42, реестр v1.50→v1.51 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d1e4237e2f |
phase2(ip-lockout): auth_log записи + 10 неудач/час с IP → 429 (ТЗ §22.4.4 п.2)
- AuthController::isIpLockedOut: count login_failed за час с IP, ≥10 → 429 + Retry-After: 3600
- logAuthEvent: 3 ветки failure_reason (invalid_password / unknown_email / account_locked)
- DB::table('auth_log')->insert; hash-chain trigger заполняет log_hash (OPEN-И-15)
- Защита поверх email-rate-limit: один IP не сможет перебирать множество email'ов
- Pest +6 IpLockoutTest (107/107 за 13.86с, 380 assertions)
- Регресс: Pint+Stan passed
- CLAUDE.md v1.40→v1.41, реестр v1.49→v1.50
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
73e64128dc |
phase2(2fa-setup): wizard init+confirm+disable+regenerate в SettingsView/SecurityTab
- TwoFactorSetupController (auth:sanctum): /api/2fa/{init,confirm,disable,regenerate-recovery-codes}
- init секрет в session (не в БД), QR-URL otpauth://; confirm активирует 2FA + 8 recovery codes
- disable/regenerate требуют password-confirmation
- User.casts: totp_secret => encrypted
Schema v8.7→v8.8: users.totp_secret VARCHAR(255) → TEXT (encrypted ~256 chars)
Migration fix: explicit ALTER TABLE webhook_dedup_keys ADD FK после DB::unprepared (PDO глотал FK на partitioned)
PartitionsCreateMonthsTest fix: DETACH PARTITION + DROP вместо DROP CASCADE
Frontend: SecurityTab реальная логика (setup wizard 3 шага, disable, regenerate dialogs)
- Pest +10 (101/101 за 13.37с, 364 assertions)
- Vitest 166/166
- CLAUDE.md v1.39→v1.40, реестр v1.48→v1.49, schema v8.7→v8.8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c39d555e6f |
phase2(recovery-code): POST /api/auth/2fa/recovery-use + UseRecoveryCodeView
- AuthController::useRecoveryCode перебирает unused codes через Hash::check, нормализация (lowercase + remove dash/space)
- UserRecoveryCode Eloquent (UPDATED_AT=null — schema без updated_at)
- Rate-limit auth:recovery:{pending_user_id}|{ip} (5/15мин)
- Returns recovery_codes_remaining для UI-warning'а (sessionStorage на frontend)
- UseRecoveryCodeView.vue → POST /api/auth/2fa/recovery-use, /recovery-use route, autocomplete=one-time-code
- TwoFactorView "резервный код" ссылка /recovery → /recovery-use
- Pest +6 RecoveryCodeTest (91/91 за 12.77с, 319 assertions)
- Vitest +6 (166/166 за 11.47с)
- TODO: #3 2FA setup wizard (после этого /recovery view получит реальный source данных)
- Регресс: lint+type+format OK; build 849ms; story:build 21/28 за 30.36с; Pint+Stan passed
- CLAUDE.md v1.38→v1.39, реестр v1.47→v1.48
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9c488122a1 |
phase2(reset-password): POST /api/auth/reset-password + ResetPasswordView + DB timezone fix
- AuthController::resetPassword через Password::reset() (callback пишет password_hash)
- ResetPasswordRequest: token + email + password (min 10 по ТЗ §22.4.1) + confirmed
- Rate-limit auth:reset:{sha256(token)[0..16]}|{ip} (5/15мин)
- ResetPasswordView для deep-link /reset/:token?email=...; pre-fill email из query; success → redirect /login через 3 сек
- Vue Router /reset/:token (guestOnly); web.php /reset SPA-path
- DB FIX: config/database.php pgsql.timezone=UTC — без него PG TIMESTAMPTZ +03 терялся при Carbon::parse и tokenExpired ошибочно срабатывал
- Pest +6 ResetPasswordTest (85/85 за 11.50с, 291 assertions)
- Vitest +7 (160/160 за 11.02с)
- Регресс: lint+type+format OK; build 784ms; story:build 21/28 за 30.74с; Pint+Stan passed
- CLAUDE.md v1.37→v1.38, реестр v1.46→v1.47
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
170382878b |
phase2(forgot-password): POST /api/auth/forgot + ForgotPasswordView интеграция
- AuthController::forgotPassword использует Password::sendResetLink (anti-enumeration: всегда 200)
- AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets — указывает на нашу таблицу из schema v8.7
- Rate-limit 5/15мин по auth:forgot:{email}|{ip} — hit ставится ДО sendResetLink (защита перебора через unknown email)
- Frontend: authApi.forgotPassword, auth-store.requestPasswordReset, ForgotPasswordView success-state
- Pest +6 в ForgotPasswordTest (79/79 за 10.55с, 273 assertions)
- Vitest +4 (153/153 за 11.11с)
- TODO: POST /api/auth/reset-password + UI-форма new_password (deep-link)
- Регресс: lint+type+format OK; build 862ms; story:build 21/28 за 32с; Pint+Stan passed
- CLAUDE.md v1.36→v1.37, реестр v1.45→v1.46
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
75897b1636 |
phase2(rate-limit): login + 2FA verify (5/15min) + frontend lockout
- AuthController: RateLimiter::hit/clear на login + verifyTwoFactor по ключу email|ip / pending_user_id|ip - 429 + Retry-After header + JSON retry_after (lockoutResponse helper) - ТЗ §22.4.4: 5 попыток / 15 мин; success чистит throttle; inactive user тоже расходует попытки - extractRateLimitRetry в api/client.ts; auth-store.lockoutSeconds; v-alert в LoginView/TwoFactorView - Pest +6 в RateLimitTest.php (73/73 за 8.07с, 246 assertions) - Vitest +4 в auth-store + LoginView (149/149 за 12.31с) - Quirk: wrong-password в тестах ≥8 символов (LoginRequest::min:8) — иначе валидация падает до controller - Quirk: vi.mock api/client в auth-store.spec — иначе axios.isAxiosError в jsdom возвращает false для plain Error - TODO (отдельные коммиты): IP-lockout 10/час через auth_log + email при 3 неудачах - Регресс: lint+type+format OK; build 886ms; story:build 21/28 за 37.19с; Pint+Stan passed - CLAUDE.md v1.35→v1.36, реестр v1.44→v1.45 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1d1353931d |
phase2(user-chip): реальный user в AppLayout/AdminLayout + Logout-menu
- AppLayout: userInitials/userShortName computed из auth-store, fallback цепочка → email → '?' / 'Гость'
- AdminLayout: тот же паттерн с админ-defaults 'АО' / 'Админ Оператор'
- v-menu offset=8 на user-chip: email + Настройки/Выйти из админки + Выйти
- handleLogout async: auth.logout() (swallows API errors) → router.push('/login')
- Vitest +3 в AppLayout.spec.ts (всего 145/145): store-mock + null-user + email-fallback
- AppShell.spec.ts получил createPinia в plugins
- Регресс: lint+type+format OK, vitest 145/145 за 11.01с, build 855ms, story:build 21/28 за 32.11с, Pest 67/67 за 6.16с
- CLAUDE.md v1.34→v1.35, реестр v1.43→v1.44
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
59299d3c2b |
phase2(auth-frontend): axios + Pinia + auth-store + auth-guards + form integration
- axios@^1.16 + pinia@^3.0 (--legacy-peer-deps). - api/client.ts: axios с withCredentials+withXSRFToken (Sanctum SPA auto-XSRF). ensureCsrfCookie() + extractValidationErrors/Message helpers. - api/auth.ts: типизированные login/register/me/logout с AuthUser interface. - stores/auth.ts: Pinia composition-store (user/loading/requires2fa + isAuthenticated computed + login/register/fetchMe/logout actions). logout() catch-swallow - UI всегда выходит локально. - LoginView/RegisterView: useAuthStore интеграция, real POST через store, errors из 422 на v-text-fields, redirect на /dashboard или /2fa, :loading на btn'ах. - Auth-guard в router.beforeEach: meta.requiresAuth на 10 routes (6 app + 4 admin), meta.guestOnly на login/register/forgot. При первом переходе fetchMe() restore-session. Unauth → /login?redirect=<original>. - / redirect → /dashboard (auth-guard перехватит если не залогинен). - Pinia в app.ts через app.use(createPinia()). - cspell-words.txt: мокаем. Vitest +10 (всего 139/139 за 10.11s): - auth-store 7 (initial state + login success/reject + register + fetchMe success/401 + logout swallow). - router 5 переписан (login.guestOnly + 6 protected + admin layout + 3 error без auth + unauth /dashboard → /login?redirect). - LoginView/RegisterView/router тесты получили createPinia в plugins. - vi.mock api/auth в router+auth-store specs. Регресс: lint+type+format OK; vitest 139/139; vite build (main app-chunk 105→153.64 KB +axios+pinia+auth gzipped 54.54 KB) 806ms; story:build 21/28 за 31.73s; Pest 61/61 за 5.86s. CLAUDE.md v1.32->v1.33, реестр Открытых_вопросов v1.41->v1.42. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
da65cf4bf7 |
phase2(admin): AdminLayout + AdminTenantsView - админка SaaS (12/13 концептов)
- AdminLayout: отдельный sidebar теало-нуар с под-брендом ADMIN (red error 10px JBM uppercase) + 4 nav (Тенанты 142 / Биллинг / Инциденты 3 / Система) + topbar с crumb «Админка → currentPage» + admin-user-chip с error-color avatar. - AdminTenantsView (/admin/tenants): page-head + 5-stats + Экспорт + search/Статус/Тариф фильтры + v-data-table 7 колонок (Тенант с двухстрочным name+inn / Статус-chip 4 цвета / Тариф / Баланс ₽ с error-color при <0 / Желаем×факт / MRR с «—» / Активность). - mockTenants.ts соответствует schema v8.7 §3: 4 статуса × 5 тарифов, 7 mock с разнообразием (active/trial/overdue/suspended) + AdminStats (142/128/9/5/ 1 248 600 ₽). - AdminPlaceholderView универсальный для Биллинг/Инциденты/Система с описаниями ссылающимися на schema v8.7 (incidents_log §9 / system_settings §10). - AppShell расширен meta.layout='admin'. Router: /admin redirect на /tenants + 4 admin-route'а с lazy-imports. Web.php fallback покрывает /admin/*. - cspell-words.txt: Екб. Vitest +11 (всего 129/129 за 10.02s): - заголовок + 5 stats (regex nbsp в 1 248 600 ₽) + 7 columns + 7 rows + Окна Москва ИНН + overdue −1 200 + trial 4 дня + suspended + search filter «Натяжные» → 1 row + Экспорт/Статус/Тариф кнопки. Регресс: lint+type+format OK; vitest 129/129; vite build (admin views в lazy-chunks; main 104.99 KB); story:build 21/28 за 30.32s; Pest 48/48 за 4.89s. CLAUDE.md v1.30->v1.31, реестр Открытых_вопросов v1.39->v1.40. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
034657788d |
phase2(errors): ErrorView 404/403/500 + Laravel fallback
- ErrorView универсальный с конфигурацией через route.meta.errorCode
(404/403/500). По v8_errors.html: full-bleed теало-нуар bg, top-brand,
err-code 96px JBM с accent на средней цифре, title/desc, 2 actions,
опциональные status-list (500) и err-id с copy-btn (403/500).
- AppShell: meta.layout='error' → RouterView напрямую (ErrorView сам
предоставляет v-app).
- Router: /403, /500, catch-all /:pathMatch(.*)* → ErrorView с meta.errorCode.
- web.php: явные Route::view + Route::fallback (срабатывает после Pest
runtime-routes, не ломает SetTenantContextTest).
- cspell-words.txt: резолвится, роуты.
Vitest +8 (всего 118/118 за 9.39s):
- 404 default + 403 с REQ-ID + 500 с INC-ID + status-list (API/Telegram/YooKassa) +
404 actions (На дашборд + Назад) + 403 mailto-link + 500 status-link +
brand-блок + 404 НЕ содержит REQ/INC/status-list (regression-guard).
- stubs:{VApp/VMain} как passthrough — обходим Vuetify layout-injection в jsdom.
Регресс: lint+type+format OK; vitest 118/118; vite build (ErrorView lazy-chunk;
main app-chunk 101.01KB упал на 7KB благодаря shared chunk'ам); story:build
19/26 за 30.96s; Pest 48/48 за 4.88s (fallback не сломал runtime-routes).
CLAUDE.md v1.29->v1.30, реестр Открытых_вопросов v1.38->v1.39.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4de39e70b2 |
phase2(reports): ReportsView - асинхронная генерация отчётов с очередью
- ReportsView (/reports): form-card (Запросить) + jobs-list panel. Form: 4 type-cards radio-grid (Сделки/Менеджеры/Источники/Биллинг) + date-range + Проект/Менеджер v-select + 4 fmt-кнопки (CSV/XLSX/JSON/PDF) + quota-banner alert (CTO-7: 2/3 одновременных + CTO-6: 3 попытки/7 дней) + Запустить/Сброс. - Jobs-list: 5 mock-rows × 4 статуса (done/running/queued/failed) с icon + meta JBM (FORMAT · size · rows · timeText) + status-chip + actions (Скачать done / Повторить failed && attempt<3 / Отменить queued / Удалить done|failed). v-progress-linear для running 62%. - composables/mockReports.ts: type unions (4×4×4) + 5 mock-jobs + MOCK_QUOTA (CTO-6/7 значения). - Маршрут /reports (meta.layout=app, lazy-import) в router + web.php. Vitest +12 (всего 110/110 за 9.38s): - заголовок + page-stats + 4 type-cards + дефолт active + 4 формата + quota-banner («2 из 3» / «3 попыток retry» / «7 дней») + 5 job-rows + done-«Готов»+Скачать-aria + running-«62%»+progressbar role + queued-Отменить + failed-«Ошибка»+«S3 timeout»+Повторить-aria + клик-переключение active. Регресс: lint+type+format OK; vitest 110/110; vite build (ReportsView lazy-chunk; main 108.19 KB); story:build 18/25 за 30.77s; Pest 48/48 за 4.58s. CLAUDE.md v1.28->v1.29, реестр Открытых_вопросов v1.37->v1.38. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |