Commit Graph

103 Commits

Author SHA1 Message Date
Дмитрий ae20033652 docs(claude.md): v1.92 → v1.93 — sync schema header drift 62→63 (Audit #3 P2)
Tail closure of Audit #3 P2 «schema.sql header drift» finding. Schema
source-of-truth was already updated in commit e746b3c (db/schema.sql:4
header «62 базовые таблицы» → «63 (61 regular + 2 partitioned parents:
deals + supplier_lead_costs)»). This commit syncs three CLAUDE.md
references to match.

Touch points:
- Header version 1.92 → 1.93 + description of session
- §0 «Источник истины» row «Схема БД» — 62 → 63 baseline
- §2 «Стек проекта» БД row — 62 → 63 baseline
- §8 self-review triggers row `db/schema.sql` — 62 → 63 baseline
- §9 history — new v1.93 entry summarising 5-commit sprint
  (8ba9c55..c524227), closure tally (1 P1 + 7 P2 + 4 P3), and regression
  check (Pest 742/739/3sk/0, Vitest 91f/736/3sk/0, gitleaks 0/442,
  lychee 325/0).

Via `/claude-md-management:claude-md-improver` per CLAUDE.md §5 п.10
(only sanctioned channel for direct CLAUDE.md edits).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:47:42 +03:00
Дмитрий db167c1beb docs(meta): CLAUDE.md v1.91 → v1.92 — §3 +#34/#35 sentry+redis (off-phase debug-runtime)
Применены 9 edits через /claude-md-management:claude-md-improver per §5 п.10:
- Шапка: v1.91 → v1.92 от 13.05.2026 day +1
- §0 row Pravila: v1.12 → v1.13 (§13.2 +Off-phase MCP debug-runtime)
- §0 row PSR_v1: v2.0 → v2.1 (R10.1 Блок 3 +sentry+redis)
- §0 row Tooling: v1.16 → v1.17; «33 формализованных» → «35»
- §1 priority chain row 2b: «33 инструментов» → «35»
- §3 title: «Карта 33» → «Карта 35»
- §3.3 table: +#34 Sentry MCP + #35 Redis MCP rows после #33
- §3.3 footer: «Total: 33 = 29+3+1» → «35 = 29+5+1»
- §9 история: +v1.92 entry

Категория debug-runtime — отдельная от UI-пула (UPM/21st) и от infrastructure
(claude-md-management). Не trigger'ит R6.0/R6.1 и не входит в R14 pipeline.
READ-ONLY usage обязателен.

Связано: Tooling 763aeae (v1.17), PSR_v1 c1f9719 (v2.1), Pravila 318aed4 (v1.13).
PR #3 (cc5f63b) merge precedent. Branch: feat/claude-automation-norm-sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 09:52:39 +03:00
Дмитрий 19d12c9f95 docs(meta): session-end hygiene — CLAUDE.md v1.91 + Pravila v1.12 + audit gap
Capture session-end documentation learnings 13.05.2026 day +1 после
CTO-19  closure (commit 0832997 + fixup f6e1e64).

CLAUDE.md v1.90 → v1.91 (через `/claude-md-management:revise-claude-md`
per §5 п.10):
— Шапка version line: новый v1.91 narrative bump с session-end summary
— §0 row Pravila: bump v1.11 → v1.12 (cross-ref sync)
— §9 история версий: +v1.91 entry (session learning capture)

Pravila v1.11 → v1.12 (manual Edit per explicit user approval choice
«iii. Pravila v1.11 → v1.12 methodology additions»):
— §4.6 self-review: +subsection «Для UI-refactor (icon migration /
  palette swap / layout overhaul)» — visual smoke verification
  обязательна; unit tests jsdom недостаточны; user-grep
  resources/js insufficient (Vuetify-internal default mdi-* gap learning
  от CTO-19); axe-core scan для palette changes
— §4.7 объединение/переименование файлов: +п.4 «Plans/specs относительные
  пути» — для ссылок на app/db/docs из docs/superpowers/{plans,specs}/
  использовать `../../../<target>` (lychee strict filesystem semantics;
  прецедент CTO-19 fixup `f6e1e64`)

audit findings.md Q.INFO.001 entry: +«Audit methodology gap»
subsection — Phase 4 SAST coverage check must begin с `ls
.github/workflows/` ДО conclusions про tool availability. Audit
12.05.2026 пропустил `.github/workflows/sast.yml` (commit 53fb1ec от
PR #25, 10.05.2026 — 2 дня до audit). Generalize: any «X not
configured» finding должен включать explicit check репо-уровневых
configurations (.github/, .gitlab-ci.yml, lefthook.yml, etc.).

cspell-words.txt +2: «рендерить» / «рендерятся» (dev jargon).
+опечатки fix: «гap» → «gap», «zafiksирован» → «зафиксирован»,
«инсуффициентны» → «недостаточны».

Регрессия зелёная (verified в commit 0832997):
— Pest --parallel 742/739/0/3 / Vitest 88/683+3 / Vite build 3.52s
— axe-core /admin/billing 0 iconography violations
— lychee 252 OK / gitleaks 0 (372+ commits)

0 code changes / 0 schema / 0 migrations / 0 npm install / 0 test impact.

Memory updates (отдельный шаг, git-untracked):
— feedback_environment.md +3 quirks 74-76 (Lucide+Histoire peerDep,
  Vuetify-internal mdi defaults gap, plans-relative-paths)
— MEMORY.md index quirks count bump 73→76

Workflow: `superpowers:brainstorming` (F-option scope) →
`:writing-plans` → `/claude-md-management:revise-claude-md` (CLAUDE.md
bump per §5 п.10) + manual Edit (Pravila + findings.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 05:39:22 +03:00
Дмитрий 0832997b6e feat(icons): CTO-19 closed via Lucide migration (Vuetify custom IconSet)
Closes CTO-19 ⏸ from реестр v1.79 — иконочная система портала не была
подключена (`@mdi/font` отсутствовал в `package.json`, все `mdi-*`
рендерились пустыми glyph'ами).

PATH α (aliases-only, brand-compliant) approved заказчиком 13.05.2026
через `superpowers:brainstorming` → `superpowers:writing-plans` →
`superpowers:subagent-driven-development`:

— `npm i lucide-vue-next ^1.0.0` (~25-30 KB gzip tree-shakable)
— `app/resources/js/plugins/vuetify.ts`: custom `IconSet`
  (`liderraLucideSet`) с 103-entry `lucideMap`:
  · 78 user-grep'нутых mdi-* names из resources/js/**/*.vue
  · 25 Vuetify-internal defaults (pagination chevrons, v-checkbox
    squares, v-radio circles, v-select dropdown, date picker, paperclip)
— Fallback `HelpCircle` для unmapped
— 51 Vue/TS файл с `icon="mdi-*"` НЕ touched — semantic-ID via Lucide

CLAUDE.md §2 «Иконки: Lucide» бренд-spec compliance achieved.

VERIFICATION (comprehensive, 13.05.2026 day +1):
— vue-tsc 0 errors
— Pest --parallel --recreate-databases: **742/739/0/3**
— Vitest: 88 files / 683 passed / 3 skipped (baseline match)
— Vite build: exit 0, 3.52s
— Visual smoke 8 views via Playwright MCP — все glyph'ы рендерятся
— axe-core a11y scan /admin/billing: **0 iconography violations**
— Pagination + v-checkbox + v-radio fixes (Task 2.b extension)

РЕЕСТР v1.82 → v1.83:
— CTO-19 §3: ⏸ →  (Pravila §2.2 / §7.1 — явное «закрываем» получено)
— Сводка §0 CTO: 17/1⏸/1 P2 [?] → 18 /0⏸/0
— Сводка §0 Итого: 70/12⏸ → 71 /11 ⏸
— Header v1.82 → v1.83 + новый changelog block
— Footer v1.83 (match header)

CLAUDE.md §0 row sync v1.82 → v1.83 — прямой Edit per «registry version
sync» rationale, не content authoring (CLAUDE.md §5 п.10).

cspell-words.txt +1: «grep'нутых» (Russian-tech jargon).

Path (i) `npm i @mdi/font` REJECTED (250 KB CSS, против бренда).
Path β rename all strings REJECTED (большой diff 51 файл).

Spec: docs/superpowers/specs/2026-05-13-cto-19-lucide-icon-migration-design.md
Plan: docs/superpowers/plans/2026-05-13-cto-19-lucide-icon-migration.md

Quirk 64: app/dev-indices.json attached per Vite watcher auto-regen.
Memory updates — git-untracked, отдельный шаг.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 05:16:31 +03:00
Дмитрий 9bc041992d chore(registry): v1.77 → v1.82 catch-up + section ## 13 fix + Сводка counters + CLAUDE.md sync
Реестр Открытые_вопросы имел hidden inconsistency: header v1.77, но footer
trail v1.78/v1.79/v1.80/v1.81 + post-Plan-4 work 12.05–13.05 не отражена.

ИЗМЕНЕНИЯ В docs/Открытые_вопросы_v8_3.md:
— Section ## 13 collision fix: Plan 4 (Billing+CSV+Admin) → ## 14
  (Аудит C сохраняется ## 13).
— Header v1.77 → v1.82 + новый changelog block «Что изменилось v1.82 vs
  v1.77» с детализацией trail v1.78–v1.81 + post-Plan-4 context.
— Сводка §0 counters update под факт:
  · CTO: 16/16 → 18/17/1⏸ (CTO-19) +CTO-20 закрыт
  · Бизнес/продакт: 17/17 → 24/17/7⏸ (Биз-25..31 Plan 4 deferred)
  · +новая строка «Plan 4 (v1.78)» 7/0/7⏸
  · Итого продуктовых: 78/69/5🟦/4⏸/1P0/3P1/0P2
    → 87/70/5🟦/12⏸/1P0/5P1/3P2 +2P3 (Биз-29/30) +1 P2 [?] (CTO-19)
— Сводка строка «Истинные P0-блокеры на 07.05.2026» → «на 13.05.2026
  (после v1.82)» с уточнением Plan 4 deferred placeholders нужны до prod.
— Сводка строка «Все P2 закрыты» → актуализирована: 3 open Биз (26/27/31).
— Сводка строка «Открыто 3 P1» → 5 P1 (+Биз-25, +Биз-28).
— Сводка +строка «P3 после v1.82 (2)»: Биз-29, Биз-30 — эмпирические
  данные после 1-2 мес эксплуатации.
— Footer v1.81 → v1.82 summary (match header).

ИЗМЕНЕНИЯ В CLAUDE.md:
— §0 row «Открытые вопросы» — v1.77 → v1.82 + удалена post-v1.77 deviation
  note (теперь baked в v1.82). Прямой Edit per approved plan «через плагин
  ИЛИ ручной Edit с обоснованием 'registry version sync'» (CLAUDE.md §5
  п.10 — это registry version-string sync, не content authoring).

PRAVILA §2.2 СТРОГО СОБЛЮДЕНА:
— Ни один новый Q-item не закрыт без явного «закрываем» заказчика.
— Биз-25..31 (Plan 4 deferred) — все остаются ⏸.
— CTO-19 (иконочная система) — остаётся ⏸.
— CTO-20 уже  в v1.81 (ICU collation fix 12.05.2026).
— Plan 5 / Quiet Luxury / Q.DEFER closures / R15 merge — feature delivery
  / audit-internal / regulatory; не Q-items registry.

POST-PLAN-4 CONTEXT (documented в v1.82 changelog):
— Plan 5 frontend Tasks 7-11 delivered (schema v8.20).
— Quiet Luxury portal redesign 20 commits Direction A.
— Portal full audit 2026-05-12 ночь — 10/10 Q-items audit-internal closed
  в blocked.md (Q.DEFER.002 sub-B + Q.DEFER.003 sub-A+B+C +
  Q.DEFER.004 sub-A+B).
— Audit-cleanup tail 5 commits 54c69a6..d1b2f5d.
— R15 motion-runtime cleanup merge 323957a (PSR_v1 v2.0, Pravila v1.11,
  Tooling v1.16, CLAUDE.md v1.90). framer-motion: regulatory hard-ban →
  technical block (React-only peerDep).

Regression-baseline (pre-commit): Pest --parallel 742/739/0/3  (после
--recreate-databases), Vitest 88 files / 683 passed + 3 skipped, Vite
build 2.67s, lychee 248 OK / 0 errors, gitleaks 0 (367 commits).

0 code changes. 0 schema. 0 migrations. 0 npm install.

Memory updates (отдельный шаг, git-untracked):
reference_archive.md description + MEMORY.md index line 7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 04:16:32 +03:00
Дмитрий 323957ad34 chore(merge): R15 motion cleanup origin/main → plan5-frontend-projects
Merge `origin/main` (commits 0fd93fd planning + 615db99 normative) into
plan5-frontend-projects. Merge-base 48f27b4. plan5 был 113 ahead / 2 behind.

CONFLICTS RESOLVED (2 files, manual):
— CLAUDE.md: шапка → v1.90; §0 cross-refs → take origin/main (Pravila
  v1.11 / PSR_v1 v2.0 / Tooling v1.16); §2 Animation default stack → take
  origin/main (motion-runtime guidance); §5 п.12 → take origin/main
  (marker «Резерв (снят 12.05.2026)»); §6 фаза + §8 self-review → keep
  plan5 (Plan 4 MERGED + Plan 5 frontend + Quiet Luxury context); §9 →
  keep both v1.88 entries explicitly labelled (plan5 schema-sync +
  origin/main R15 removal — version-number collision result of parallel-
  branch bump'ов) + v1.89 plan5 factual fix + new v1.90 merge entry.
— docs/CHANGELOG_claude_md.md: keep all three entries (v1.90/v1.89/v1.88).

FAST-FORWARDED (3 files, no conflict — plan5 не редактировал):
— docs/Plugin_stack_rules_v1.md v1.7 → v2.0 (R15 удалён, 162 lines diff)
— docs/Pravila_raboty_Claude_v1_1.md v1.10 → v1.11 (§11.5/§13.2 счётчик
  16→15 правил + cross-refs)
— docs/Tooling_v8_3.md v1.15 → v1.16 (§9.2 reformulated в technical
  guidance: motion-v , framer-motion technical block)

ADDED FROM origin/main (2 files):
— docs/superpowers/plans/2026-05-12-remove-r15-motion-restrictions.md
— docs/superpowers/specs/2026-05-12-remove-r15-motion-restrictions-design.md

cspell-words.txt +1: «форкнулась» (валидный дев-жаргон, в merge-entries).

0 code changes (resources/js/, app/, db/ нетронуты).
0 npm install (motion-v / gsap / anime.js теперь разрешены, не делается).
0 schema changes.

POST-MERGE TODO (отдельные шаги):
— /claude-md-management:revise-claude-md polish (per §5 п.10)
— memory updates: feedback_plugin_paired_stack + project_state +
  reference_archive (бывшая «branch-divergent state» note → resolved)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 03:51:27 +03:00
Дмитрий a5e99ba0e9 docs(claude-md): v1.89 — factual fix §6 + шапка v1.88 (615db99 ≠ Plan 4)
В рамках post-audit continuation session 12.05.2026 ночь обнаружен factual
error в v1.88: коммит 615db99 в двух местах представлен как Plan 4 merge,
коммит f4ec5dc как PSR_v1 R15 removal. Оба идентификации неверны.

Verified через git log origin/main + git show <commit>:
- 615db99 = «chore(rules): remove R15 motion-runtime restrictions (PSR_v1 v2.0)» (12.05.2026 07:30) — R15 removal, НЕ Plan 4 merge
- 8681040 = «docs: Plan 4 closure — CLAUDE.md v1.87 + Открытые_вопросы v1.78» — правильный Plan 4 closure marker на origin/main
- a907fea..174dbae = backend Plan 4 task-коммиты (Tasks 9-11), merged ранее
- f4ec5dc = «fix(redesign): sidebar position:fixed + main padding-left» — Quiet Luxury sidebar hotfix на ветке plan5-frontend-projects, НЕ на origin/main, НЕ R15 removal

Правки v1.89:
1. §6 строка обновлена с правильными коммитами + явное разделение «Plan 4 closure 8681040» и «R15 removal 0fd93fd + 615db99» как разные истории
2. Шапка v1.88 changelog inline: 615db998681040 + NB-маркер про factual error
3. §9 v1.88 entry inline: то же исправление + NB
4. Bump CLAUDE.md v1.88 → v1.89 (новая шапка)
5. Новая v1.89 entry в §9 CLAUDE.md + параллельная запись в CHANGELOG
6. CHANGELOG intro обновлён: документировано что v1.84..v1.88 живут inline в §9 (CHANGELOG-обслуживание не велось 10.05.2026–12.05.2026)

Связанные документы (Pravila v1.10 / PSR_v1 v1.7 / Tooling v1.15 / реестр
v1.77 на ветке plan5-frontend-projects) НЕ требуют изменений — фикс
локален в CLAUDE.md.

Источник: post-audit continuation session, bonus-finding во время Q.DEFER.001
(memory description downgrade). Заказчик: «доделывать аудит, поправить
ошибку в CLAUDE.md». Через /claude-md-management:claude-md-improver per
CLAUDE.md §5 п.10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:02:36 +03:00
Дмитрий 57f0b8e64c docs(claude.md): v1.87 → v1.88 audit-driven sync §0/§2/§6/§8
Phase 6 audit found doc-drift:
- §0/§2/§8 schema метрики застряли на baseline v8.19 (62/12/117/39/5/13/5).
  Dev `liderra` factual after migrate:fresh + partitions:create-months:
  75 root + 102 partition children + 289 indexes + 39 RLS + 5 user funcs
  + 19 triggers + 0 dev roles (5 on prod via db/00_create_roles.sql).
- §0 row «Открытые_вопросы v1.75» → факт v1.77 (Sprint 4 Audit tail
  close, 10.05.2026); note про post-v1.77 deviation (Plan 4/5 + Quiet
  Luxury merged без registry bump).
- §6 «Plan 4 ready for FF-merge» → факт «Plan 4 MERGED в origin/main
  615db99» + новый параграф про Plan 5 frontend + Quiet Luxury + dev-indices
  в `plan5-frontend-projects` (85+ commits ahead на 12.05.2026 night).
- §8 self-review row: baseline ИЛИ dev-actual disambiguation.
- 5 user-функций перечислены поимённо (audit_block_mutation, audit_chain_hash,
  calc_lead_score, report_jobs_log_export, set_pd_subject_request_deadline).

§9 entry для v1.88 описывает полный аудит-сессии: 8 commits Phase 8,
0 регрессий, final baseline Pest 742 / Vitest 614 / Histoire 35 / Vite 1.80s.

Через `/claude-md-management:revise-claude-md` (см. blanket approval
заказчика «исправь всё что сможешь в моё отсутствие»).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:30:14 +03:00
Дмитрий 615db99547 chore(rules): remove R15 motion-runtime restrictions (PSR_v1 v2.0)
Conscious rollback of v1.83 audited construction per user decision
12.05.2026. R15 PSR_v1 section deleted entirely; framer-motion remains
technical block (React-only peerDep), no longer regulatory rule.

Affected:
- PSR_v1 v1.7 -> v2.0 (R15, R0.6 p.11, R8 motion, R11.6, R13 motion rows
  removed; finale + properties reformulated)
- CLAUDE.md v1.87 -> v1.88 (#5 p.12 -> marker; #2 motion stack -> guidance)
- Tooling v1.15 -> v1.16 (#9.2 reformulated; framer-motion + react-spring
  marked as technical block, not regulatory)
- Pravila v1.10 -> v1.11 (#11.5/#13.2 counts updated; #13.9/#13.10 cross-ref
  bumps; #13.10 NOT deleted - it governs R14 UPM/21st pipeline, not R15)
- CHANGELOG_claude_md.md - v1.88 entry

Brainstormed via superpowers:brainstorming. Planned via
superpowers:writing-plans. Executed via superpowers:executing-plans +
/claude-md-management:claude-md-improver + manual Edit.

Spec: docs/superpowers/specs/2026-05-12-remove-r15-motion-restrictions-design.md
Plan: docs/superpowers/plans/2026-05-12-remove-r15-motion-restrictions.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 07:30:57 +03:00
Дмитрий 8681040479 docs: Plan 4 closure — CLAUDE.md v1.87 + Открытые_вопросы v1.78
CLAUDE.md v1.86 → v1.87:
- §0 Источник истины row «Схема БД»: schema v8.11 → v8.19, 56 → 62 базовых
  таблиц, 97 → 117 индексов, 38 → 39 RLS-политик.
- §2 Стек строка БД: 4 → 5 ролей БД (+crm_supplier_worker BYPASSRLS из Plan 3
  для sharing-flow + Plan 4 ResetMonthlyCountersCommand + CsvReconcileJob).
- §6 Текущая фаза: +Plan 4 closure block (15 коммитов на plan4-billing,
  Pest 687/684+3/0, Vitest 49/428, Histoire 24/31, lychee 0, gitleaks 0).
- §8 Self-review триггеры: метрики обновлены до v8.19 = 62/12/117/39/5/13.
- §9 История версий: +v1.87 entry с накопленным drift'ом от Plans 1+2+3+4.

Через /claude-md-management:revise-claude-md (project rule §5 п.10).

Открытые_вопросы v1.77 → v1.78:
- +Раздел 13 «Plan 4 — 7 новых открытых вопросов» (Биз-25..31).
- Биз-25 P1: дефолтные tier-цены (placeholder в PricingTierSeeder).
- Биз-26 P2: rate-limit 1/час/tenant ZeroBalancePausedMail.
- Биз-27 P2: tenant видит ВСЕ ступени (transparent).
- Биз-28 P1: CSV-схема discovery (BLOCKED Plan 3 Tasks 1-2).
- Биз-29 P3: CSV window 25h.
- Биз-30 P3: Drift threshold 5%.
- Биз-31 P2: pricing-tier повышение цены — единая логика effective 1-е след. мес.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:34:44 +03:00
Дмитрий 3c13cc5d64 docs(rules): третий аудит нормативки — закрытие 13 находок (CLAUDE.md v1.86 + PSR_v1 v1.7 + Tooling v1.15)
P0 (4) — арифметические конфликты в CLAUDE.md, прошли мимо второго аудита:
- §3 header «Карта 28 инструментов» → «33» (header застрял с pre-FD эпохи)
- §3.4 header «(+5, итого 28)» → «итого 29» (после #30 в фазу 2)
- §3.3 footer «из 30 номеров минус #1 = 29» → «33 номеров: 29 phase-active + 3 off-phase + 1 historic»
- §6 «Активно: 19 инструментов из 29» (vs раскладка 9+8+7=24) → «24» в обоих местах

P1 (5) — sync stale `+`-refs после второго аудита:
- PSR_v1 шапка: «CLAUDE.md v1.84+/Pravila v1.9+» → «v1.86+/v1.10+»
- Tooling шапка: «Pravila v1.9+/PSR_v1 v1.5+/CLAUDE.md v1.84+» → «v1.10+/v1.7+/v1.86+»
- CLAUDE.md §5 п.5 «PSR_v1 v1.5+» → «v1.7+»

P2 (2) — внутренние несогласованности:
- PSR_v1 line 4/856 «slot уровня 2.5» → «уровня 2b» (описка внутри changelog'а v1.6)
- CLAUDE.md §3.3 #33 «вне Pravila §13» → «вне UI-пула §13» (Pravila §13.2 v1.10 включает claude-md-management как infrastructure subsection)

Sync-правки в connected files:
- README.md + README_АРХИВ_v8_5.md «карта 28 инструментов» → «33»
- Tooling §11.5/§12 «не входят в 28 инструментов» → «33 формализованные позиции»

Через `/claude-md-management:claude-md-improver` с cross-impact-анализом перед каждой группой правок. Pravila v1.10 без изменений (cross-refs к нему уже актуальны).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 06:32:17 +03:00
Дмитрий 07ed3f1be6 docs(rules): второй аудит нормативки — закрытие 15 находок (CLAUDE.md v1.85 + Pravila v1.10 + PSR_v1 v1.6 + Tooling v1.14)
Закрыто 15 находок второго аудита правил использования плагинов и скилов
(4 P0 реальных противоречий + 7 P1 stale-refs + 4 P2 структурных).
Через `/claude-md-management:claude-md-improver`.

P0 — реальные противоречия:
- Tooling §10.3 шаг 2: «3 skills» → «14, все запреты сняты Pravila §11»
  (закрыта внутри-документная контрадикция с §4.1 того же файла)
- CLAUDE.md §6: арифметика «33» исправлена (+1 заменённый PG MCP
  исторически; раньше 19/29 + 3 = 32, не 33)
- Pravila §13.2: «v1.4 (15 правил)» → «v1.6 (16 правил R0–R15)»
- Tooling §13: добавлены v1.13 + v1.14 entries (раньше история
  обрывалась на v1.12, хотя шапка описывала v1.13)

P1 — массовый stale-refs дрейф v1.4→v1.6 + v1.12→v1.14:
- CLAUDE.md §0 cross-refs (Pravila/PSR_v1/Tooling rows), §3.3 #31, #32, §5 п.12
- Pravila §11.5 («10 правил»→«16»), §13.9, §13.10
- Tooling §4.4 («10 правил»→«16»), §4.5, §4.6, §4.7

P2 — структурные:
- Tooling Прил. Н добавлен explicit-слотом уровня 2b в priority chain
  (CLAUDE.md §1, PSR_v1 R0.1 таблица, Tooling §7) — раньше формальная
  дыра «PSR_v1 R0.1 говорит stack ниже Tooling, но Tooling нет ни
  в одной priority chain»
- PSR_v1 R0.4.A свёрнут до cross-ref на Pravila §12.3 SoT — устранён
  риск дрейфа формулировок (раньше параллелил список разной формулировкой)
- Pravila §0 +note про §11 локальное override-исключение над §2.2/§4.5/§8.4
  (раньше §11 формально стоял ниже §9 в цепочке вопреки фактическому
  override §2.2/§4.5/§8.4 при skill-инвокации)
- PSR_v1 R0.6 hard-стопы пронумерованы 1–11 для надёжности cross-refs
  «пункт 9/10/11» (раньше буллет-list, ссылки молча ломались)

Все 4 файла нормативной документации внутренне непротиворечивы.
История версий синхронизирована.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 05:56:43 +03:00
Дмитрий 27dec3d459 docs(rules): аудит нормативки — закрытие 14 находок (CLAUDE.md v1.84 + Pravila v1.9 + PSR_v1 v1.5 + Tooling v1.13)
Audit конфликтов и запутанностей между CLAUDE.md / Pravila / PSR_v1 / Tooling
выявил 14 находок (3 🔴 high, 6 🟡 medium, 5 🟢 low). Все правки — через
paired stack: writing-plans → executing-plans → claude-md-improver (для
CLAUDE.md по §5 п.10) → verification-before-completion с grep-evidence.

Ключевые правки:

1. claude-md-management формализован #33 в Tooling §4.7 — пятый
   включённый плагин (категория «инфраструктурная», вне UI-пула).
   Tooling §0 счётчик 31 → 33 (3 off-phase tools).

2. Tooling §7 + PSR_v1 уровнем 3 — иерархия source of truth расширена
   с 5 до 7 уровней, sync с CLAUDE.md §1.

3. Tooling §6 +5 конфликтов v1.4 — UPM↔FD, 21st↔Vuetify, 21st↔App*,
   framer↔motion-v, UPM↔21st (с 5 до 10 строк).

4. Pravila §12.3 объявлен Single Source of Truth для exclusions §12;
   PSR_v1 R0.4.A + CLAUDE.md §5 п.11 — cross-ref сюда.

5. Pravila §13.6 +tier-таблица hard-rule (explicit / transitive /
   standard) — снимает скрытую иерархию между §12 и §13.9/§13.10.

6. PSR_v1 R10.1 разбит на 3 блока: enabledPlugins / built-in skills
   Claude Code / MCP-серверы — раньше всё было одним списком.

7. PSR_v1 R8 +тай-брейкер FD↔21st (последовательно, FD ведущий).

8. PSR_v1 R10.4 + R14.7 — tier-метки transitive hard-rule с явным
   указанием, что Pravila §9 «Отступления» к ним не применяется.

9. Scope-метки приоритетных цепочек — Pravila §0 (внутрипараграфный),
   CLAUDE.md §1 (межфайловый), PSR_v1 R0.1 (scope головенства stack'а).
   Снимает путаницу 4-х представлений.

10. CLAUDE.md §5 п.5 свёрнут до 2 строк со ссылкой на PSR_v1 R14
    (был копией PSR_v1 на 12 строк).

11. Tooling §4.6 — settings.json → ~/.claude.json (где реально лежит
    API-ключ 21st).

cspell-words.txt: +внутрипараграфный, внутрипараграфные, скилов
(новые термины из scope-меток и plan-файла).

Намеренно оставлено: R0.6 пункт 11 ⊂ пункт 6 (motion-специальный flow);
Pravila §13.10 формально избыточен (явная запись лучше транзитивного).

Plan: docs/superpowers/plans/2026-05-10-rules-audit-fixes-plan.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 05:06:16 +03:00
Дмитрий 833e3e6083 docs(rules): PSR_v1 v1.4 — формализация UPM + 21st Magic MCP + R15 motion-системы
Триггер: пользователь спросил «хочу добавить плагины 21st, framer motion,
UI UX max — проанализируй конфликты». Проверка показала: UPM (skill) и 21st
Magic MCP (`magic` сервер) уже фактически включены в ~/.claude/settings.json
и ~/.claude.json, но в правилах не описаны. Framer Motion — React-only
runtime npm-библиотека, не Claude-плагин, физически не работает в Vue.

Через цикл brainstorming → 3 варианта → итерации согласовано: формализовать
UPM + 21st; для motion — двухуровневая R15 (framer-motion hard-запрет
навсегда + motion-v узкое окно по 4 условиям).

PSR_v1 v1.3 → v1.4 (главный артефакт):
- R6 → R6.0 универсальная таблица фильтра для FD/UPM/21st
- R6.1 hard-override Forest расширен на все три плагина
- R10.1 +21st row, ослабление UPM (FD молчит ИЛИ R12 третий вариант)
- R11.5 (новое) активация UPM в R12 архитектурном
- R11.6 (новое) иерархия 7 motion-источников
- R0.6 +3 hard-стопа (App* через 21st, Vuetify-эквивалент, motion-v)
- R13 +9 строк matrix'а
- R14 (новое, 7 подразделов) pipeline UPM + 21st
- R15 (новое, 7 подразделов) motion-системы
- R8 +7 тай-брейкеров

Pravila v1.7 → v1.8: §13 расширен на расширенный пул, §13.10 hard-link на R14.

Tooling Прил. Н v1.11 → v1.12: #31 UPM + #32 21st (off-phase tools), §9.2
motion-runtime denylist (framer-motion + react-spring R15.1; motion-v + gsap
+ anime + lottie + popmotion R15.2/R15.7). 31 формализованных позиций
(19/29 активных по фазам + 2 off-phase).

CLAUDE.md v1.82 → v1.83: §0 cross-refs, §2 +Animation default stack, §3.3
+#31 UPM +#32 21st, §5 п.5 расширен, §5 п.12 motion-runtime новый,
§6 обновлён, §9 +entry.

cspell-words.txt: +UPM +gsap +LLM (валидные термины проекта).

Через /claude-md-management:claude-md-improver. 6 файлов, 0 изменений
в коде проекта (resources/js/, app/, db/ нетронуты), 0 npm install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:33:42 +03:00
Дмитрий 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 (b6ae8dd):
- P1-01: README.md обновлён (CLAUDE.md ссылка с 28→29 инструментов и 5→6 уровней;
  Tooling v1.0→v1.10; Pravila v1.2→v1.6; PSR_v1 v1.3 добавлен; schema v8.5→v8.11
  (56 базовых + 12 партиций / 97 индексов / 38 RLS / 5 функций / 13 триггеров)).
- P1-03: CLAUDE.md Histoire 21/28 → 21/43 (заменено все вхождения, включая §0 и §3.3).
- P1-06: Pravila §13.9 cross-ref на Plugin_stack_rules_v1 теперь с (v1.3).
- P1-08: Tooling §2.1 п.7 Stylelint раздел дополнен версией stylelint-config-standard ^40.0.0.
- P2-03: CLAUDE.md F-K cross-link на Plugin_stack_rules_v1.md#история-версий.
- post-A: метрики schema в CLAUDE.md синхронизированы до v8.11 (97 / 38).

Также фикс битой ссылки в spec'е Sprint 1 (lychee 1 error → 0): относительный путь
до Plugin_stack_rules_v1.md из docs/superpowers/specs/ исправлен на ../../

Bump версий:
- CLAUDE.md v1.81 → v1.82
- Pravila v1.6 → v1.7
- Tooling Прил. Н v1.10 → v1.11

Все правки CLAUDE.md/Pravila/Tooling — через claude-md-management:claude-md-improver
(CLAUDE.md §5 п.10). README.md и spec — обычные правки.

lychee: 0 errors. markdownlint: 0 errors. cspell на web/v8/* — out-of-scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:36:36 +03:00
Дмитрий 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>
2026-05-09 16:46:31 +03:00
Дмитрий 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>
2026-05-09 15:20:58 +03:00
Дмитрий 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>
2026-05-09 15:13:17 +03:00
Дмитрий b8f1c16ce2 docs(claude-md): v1.74→v1.75 — currency refresh через claude-md-improver
Application of skill /claude-md-management:claude-md-improver Phase 5 после повторного audit'а (88/100, A−). 4 stale места — все currency-only, не меняют правила/состав инструментов:

(1) §2 «Стек проекта» — БД метрики 55/12/92/36 → 56/12/95/37 (v8.10 от 09.05.2026, прежде стояло v8.6).

(2) §6 hero-параграф — заголовок «Фаза 2 ⏸ (08.05.2026)» → «Post-MVP (09.05.2026)»; добавлен полный backend-coverage (auth/deals/reminders/notifications/admin/impersonation/webhook); метрики Pest 403/403 + Vitest 393/393 + Histoire 21/28.

(3) §3.1 строка #1 (PostgreSQL MCP) — strikethrough + пометка «заменён #10 Boost (см. §3.2)». Заголовок секции «Фаза 0 — сейчас» → «Фаза 0 — документация».

(4) §8 self-review триггеры — пример schema-метрик 54/12/91/34 (v8.5) → v8.10 56/12/95/37 + текст «сверять с текущей версией».

Шапка v1.74 → v1.75. §9 «История версий» — добавлен entry v1.75. Pravila/Tooling sync не требуется (§5 п.8 — currency, не правила).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:18:10 +03:00
Дмитрий 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>
2026-05-09 14:12:36 +03:00
Дмитрий 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>
2026-05-09 13:52:25 +03:00
Дмитрий 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>
2026-05-09 13:14:14 +03:00
Дмитрий 4c33323f0e phase2(notifications-stage6): low_balance + zero_balance + topup + invoice email/inapp
P0 этап 6 — 4 оставшихся email-события. Авто-план P0 (6 этапов) закрыт
полностью: все 8 schema-default событий имеют рабочую интеграцию
(new_lead/reminder/low_balance/zero_balance/topup_success/invoice_paid +
заглушки для new_device_login/marketing).

Backend:
- 4 новых Mailable: LowBalanceNotification (threshold), ZeroBalanceNotification,
  TopupSuccessNotification (amountRub, amountLeads?), InvoicePaidNotification
  (amountRub, invoiceNumber?, tariffName?).
- 4 blade-шаблона в emails/ (Forest-палитра, таблицы balance/amount/invoice).
- NotificationService +4 методов: notifyLowBalance / notifyZeroBalance /
  notifyTopupSuccess / notifyInvoicePaid. Все шлют email + inapp по prefs.

Интеграция в ProcessWebhookJob:
- chargeNewLead после lead_charge: notifyLowBalance при пересечении порога
  сверху-вниз (balance_after <= threshold AND (balance_after+1) > threshold).
  Иначе спам при каждом lead_charge при balance < threshold.
- logRejection(zero_balance): notifyZeroBalance ТОЛЬКО если в последний час
  не было другого RejectedDealsLog с тем же reason (anti-spam 1 email/час).
  Защита от self-just-inserted через id!= (timestamp-сравнение ненадёжно
  из-за PG microsecond precision).
- topup_success / invoice_paid — service-методы готовы, integration после
  появления endpoints для пополнения (ЮKassa-webhook) и оплаты тарифа.
- lowBalanceThreshold() читает system_settings.low_balance_threshold_leads
  (default 10, schema seed).

Pest +12 в BalanceNotificationsTest (359/359 за 41.37 сек, 1233 assertions):
- low_balance: пересечение порога / уже < threshold / > threshold /
  prefs.email=false (только inapp).
- zero_balance: первое отклонение / 2-е в час не дублирует / >1ч снова шлёт.
- topup_success / invoice_paid: notify создаёт email+inapp / prefs=email:false.
- balance events изолированы между tenants.

NewLeadNotificationTest: «balance=0 не шлёт» обновлён —
Mail::assertNotSent(NewLeadNotification) вместо Mail::assertNothingSent
(ZeroBalanceNotification теперь шлётся при balance=0 — новое поведение).

PHPStan baseline регенерирован.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:51:32 +03:00
Дмитрий dc1457a008 phase2(reminders-frontend): RemindersView + DealDetailDrawer + nav-badge
P0 этап 5 — frontend для reminders (после backend-этапа 4).
Пользователь может создавать/просматривать/завершать/удалять напоминания
из UI с inline-create в DealDetailDrawer.

Frontend:
- api/reminders.ts: типизированные helpers для 5 endpoints + ensureCsrfCookie
  для mutating. Types ReminderFilter/ApiReminder/ReminderCounts.
- stores/reminders.ts: Pinia с items/counts/currentFilter +
  load/refreshCounts/create/update/complete/remove. Optimistic для
  complete/remove с revert на reject.
- components/reminders/ReminderDialog.vue: dual-mode (create/edit) modal
  с native datetime-local input. Props dealId?/reminder? (edit),
  ISO-конверсия при submit.
- views/RemindersView.vue: page-head с stats (active/overdue) + reload-btn;
  4 tabs (today/upcoming/overdue/completed) с counts на бейджах
  (overdue=error color); v-list с complete-btn / dropdown
  Изменить/Удалить с confirm-dialog; empty-state.
- router: /reminders маршрут (lazy).
- AppLayout: nav-badge «Напоминания» биндит count из store
  (replace static «12»); скрыт при count=0; polling 60 сек для
  refreshCounts.
- DealDetailDrawer: секция «Напоминания» (только при tenantId+deal):
  inline + create-btn / список / complete / встроенный ReminderDialog.

Vitest +20 (369/369 за 21.20 сек):
- reminders-store 11: initial / load+reject / refreshCounts /
  create+reject / complete optimistic+revert / remove+reject / reset.
- RemindersView 7: mount / 4 tabs / counts / empty-state /
  список / reload-btn / filter=today default.
- AppLayout +2: бейдж скрыт при count=0 / показывает count при >0.

Pest 347/347 (без изменений — backend нетронут).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:41:41 +03:00
Дмитрий 39b6127bce phase2(reminders-backend): CRUD + cron-диспетчер + email/inapp (P0 этап 4)
Закрыт пункт «Reminders ⏸ no-view» из AppLayout nav-tree. Schema-таблица
reminders уже была в v8.10 §17.5 — теперь работает целиком backend-side.

Backend:
- App\Models\Reminder — Eloquent с casts/relations + isCompleted/isOverdue.
- ReminderFactory с states overdue/completed/sent.
- App\Http\Controllers\Api\ReminderController под auth:sanctum:
  GET ?filter=&deal_id=&limit= (active/today/upcoming/overdue/completed,
    окно ±1 день, counts для UI badges);
  POST {deal_id, text?, remind_at, assignee_id?} (FK guard на assignee);
  PATCH {id} (при смене remind_at сбрасывает is_sent+sent_at для retrigger);
  POST {id}/complete (idempotent);
  DELETE {id}.
  RLS-обёртка + defense-in-depth where('tenant_id').
- App\Mail\ReminderDueNotification + emails/reminder.blade.php (Forest,
  TZ из recipient.timezone).
- NotificationService::notifyReminder(Reminder) — recipient = assignee_id
  ?? created_by (если active+!deleted). Каналы email+inapp по prefs.
  payload {reminder_id, deal_id} для UI deep-link.
- App\Console\Commands\RemindersDispatchDue — cron reminders:dispatch-due
  {--dry-run} {--limit=500}. По одному reminder в DB::transaction (SET
  LOCAL app.current_tenant_id нельзя переключать). После notifyReminder
  ставит is_sent=true даже если recipient deactivated (защита от retry-spam).

Pest +32 (347/347 за 41.21 сек, 1203 assertions):
- ReminderControllerTest 21: 401 / RLS / 5 filter'ов / counts / deal_id /
  store + FK guard / update text+remind_at сбрасывает is_sent / complete
  idempotent / delete + 404 чужой.
- RemindersDispatchDueTest 11: due → email+inapp / future skip / completed
  skip / уже sent / assignee вместо created_by / deactivated user (is_sent
  всё равно) / только inapp при email=false / --dry-run / --limit / RLS.

PHPStan baseline регенерирован. IDE-helper для всех моделей.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:30:38 +03:00
Дмитрий f55b91cfa4 phase2(notifications-stage3): NotificationsTab schema-aligned + prefs API
Закрывает архитектурное расхождение v1.28 — Tab сохранял prefs только
локально без API. Backend events не совпадали с handoff'ом.

Backend:
- PATCH /api/auth/me/notification-preferences под auth:sanctum.
- Replace-семантика: незадекларированные events/channels отбрасываются.
- userResource расширен: notification_preferences + sound_enabled.
- UserFactory с schema-default JSON (Eloquent не перечитывает после INSERT,
  DB-DEFAULT JSONB виден как null без явного override).
- Pest +10: 401 / replace / неизвестные events/channels отбрасываются /
  422 без prefs / sound_enabled опционален / bool-cast 1/'1' / replace-
  семантика (отсутствующие events исчезают).

Frontend:
- api/auth.ts: типы NotificationChannel/EventKey/Preferences +
  updateNotificationPreferences helper. AuthUser получил optional поля.
- NotificationsTab.vue переписан под schema:
  8 событий (new_lead/reminder/low_balance/zero_balance/topup_success/
  invoice_paid/new_device_login/marketing) × 3 канала (inapp/push/email,
  НЕ sms). Sync-init prefs (без onMounted — иначе v-if блокирует рендер
  и тесты mount-then-find падают). dirty через computed-сравнение с
  originalPrefs snapshot. save async + success/error alerts.
- SettingsView.spec.ts: legacy event-имена → schema-aligned.
- Vitest +10: 8 schema events / 3 channels (НЕ sms) / legacy отсутствуют /
  читает prefs из user / save calls API + alerts / Отменить возвращает.

cspell-words: +prefs.
PHPStan baseline регенерирован.

Pest 315/315 (+10) за 36.73 сек, 1130 assertions.
Vitest 349/349 (+10) за 20.42 сек.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:41:35 +03:00
Дмитрий 508de4eaf3 phase2(notifications-stage2b): API + Pinia + bell в AppLayout (P0 этап 2b)
Закрывает этап 2 P0 целиком (UI bell с unread badge + polling).

Backend:
- App\Http\Controllers\Api\InAppNotificationController под auth:sanctum:
  GET /api/notifications?unread_only=&limit= (1..100 default 50);
  PATCH /api/notifications/{id}/read (idempotent);
  POST /api/notifications/mark-all-read (bulk + count);
  DELETE /api/notifications/{id}.
- Route::middleware('auth:sanctum')->prefix('/api/notifications') в web.php.
- DB::transaction + SET LOCAL app.current_tenant_id для RLS.
- Защита от кражи чужого id через where('user_id', $auth->id).
- Pest +14 (305/305 за 34.71 сек, 1099 assertions).

Frontend:
- api/notifications.ts — типизированные axios-helpers + ensureCsrfCookie.
- stores/notifications.ts — Pinia: items/unreadCount/total/loading +
  optimistic markRead/markAllRead/remove с revert на reject.
- AppLayout: bell-icon → v-menu offset=8 location=bottom-end:
  pip badge показывает unreadDisplay (1..99 / 99+ / hidden);
  v-list последних 10 из sortedItems с event-icon + formatRelative;
  Mark-all-read btn только при unreadCount > 0;
  click на item → markRead + router.push('/deals') если deal_id.
- usePolling(loadNotifications, {intervalMs: 30_000}) с Page Visibility.
- loadNotifications no-op без auth.user.
- Vitest +18 (339/339 за 20.03 сек): store 12 + AppLayout +6
  (bell-btn / pip скрыт при 0 / pip count / 99+ / listNotifications
  на mount с user / no-op без user).

PHPStan baseline регенерирован (50 Pest false-positives подавлены).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:27:57 +03:00
Дмитрий 7f5ff874a8 phase2(notifications-stage2a): in_app_notifications + notifyInApp (schema v8.10)
P0 этап 2a — backend-фундамент bell-icon канала. UI bell + API endpoints
— этап 2b отдельным коммитом.

Schema v8.9 → v8.10:
- Новая таблица in_app_notifications (после reminders в schema):
  id/tenant_id/user_id/event/title/body/deal_id/payload/read_at/created_at.
- 2 индекса: unread (user_id, created_at DESC) WHERE read_at IS NULL
  + recent (user_id, created_at DESC).
- RLS tenant_isolation.
- Метрики: 55→56 таблиц, 93→95 индексов, 36→37 RLS.
- CHANGELOG_schema.md +§T.

Backend:
- App\Models\InAppNotification — Eloquent с UPDATED_AT=null.
- NotificationService::notifyInApp — INSERT через DB::transaction + SET
  LOCAL app.current_tenant_id для RLS. Throwable + Log::warning.
- notifyNewLead шлёт два канала параллельно: email + inapp.

Pest +11 (291/291 за 32.94 сек, 1060 assertions):
- inapp=true/false; schema-default (inapp=true в схеме);
- 2 user'а получают / inactive не получает / RLS изоляция;
- дубль Биз-19 / повторный vid / оба канала / payload deal_id;
- notifyInApp напрямую с reminder.

PHPStan baseline регенерирован. IDE-helper для InAppNotification.
cspell-words: +inapp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:17:39 +03:00
Дмитрий a4601fe84b phase2(notifications-stage1): NotificationService + new_lead email (P0 этап 1)
Старт closing «Notification delivery» из карты P0. Этап 1/6 плана:
NotificationService + Mailable + интеграция в ProcessWebhookJob::chargeNewLead.

- App\Services\NotificationService — диспетчер 8 событий × 3 каналов
  (inapp/push/email) согласно schema.sql:699 users.notification_preferences.
  Этап 1 реализует только email-канал для new_lead.
- App\Mail\NewLeadNotification + emails/new_lead.blade.php — HTML-письмо
  в Forest-палитре с таблицей phone/contact_name/received_at/deal_id.
- ProcessWebhookJob::chargeNewLead — после ActivityLog вызывает
  notifyNewLead. Throwable от Mail::send проглатывается + Log::warning
  (отказ канала не должен валить транзакцию).
- Pest 11/11 в tests/Feature/Notifications/NewLeadNotificationTest.php:
  email=true получает / email=false не получает / schema-default не шлёт /
  inactive не получает / soft-deleted не получает / другой тенант не
  получает / Биз-19 дубль не дублирует / повторный vid не дублирует /
  balance=0 не шлёт / subject содержит project_name.
- IDE-helper регенерирован (4 модели получили @mixin docblocks).
- PHPStan baseline регенерирован (138 ignore.unmatched схлопнулись).

Pest 280/280 за 31.27 сек (+11 от 269, 1029 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:03:43 +03:00
Дмитрий 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>
2026-05-09 10:27:11 +03:00
Дмитрий 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>
2026-05-09 10:17:51 +03:00
Дмитрий 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>
2026-05-09 10:08:12 +03:00
Дмитрий 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>
2026-05-09 10:01:35 +03:00
Дмитрий 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>
2026-05-09 09:51:47 +03:00
Дмитрий 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>
2026-05-09 09:38:34 +03:00
Дмитрий 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>
2026-05-09 09:28:49 +03:00
Дмитрий 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>
2026-05-09 09:19:53 +03:00
Дмитрий 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>
2026-05-09 09:10:58 +03:00
Дмитрий 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>
2026-05-09 08:59:17 +03:00
Дмитрий 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>
2026-05-09 08:21:50 +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
Дмитрий 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>
2026-05-09 07:46:19 +03:00
Дмитрий 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>
2026-05-09 07:34:39 +03:00
Дмитрий 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>
2026-05-09 07:11:44 +03:00
Дмитрий 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>
2026-05-09 06:58:49 +03:00
Дмитрий 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>
2026-05-09 06:43:21 +03:00
Дмитрий 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>
2026-05-09 05:49:34 +03:00
Дмитрий 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>
2026-05-09 05:33:21 +03:00
Дмитрий 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>
2026-05-09 04:52:52 +03:00
Дмитрий 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>
2026-05-09 04:24:02 +03:00