Code-quality review of Task 5: adds tests for the loadApiKey/loadWebhook
catch branches (apiKeyError/webhookError -> error v-alert) and changes
the Copy-button disabled check to the idiomatic falsy form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit D2/D3/D4/D5: all four ApiTab buttons were handler-less and the
fields were hardcoded. Adds api/apiKeys.ts + api/webhooks.ts modules and
rewires ApiTab: loads the api-key prefix + webhook settings on mount;
Copy -> clipboard + snackbar; Regenerate -> confirm dialog -> POST
regenerate (full key shown once); Save Webhook -> PUT webhook-settings;
Test Webhook -> POST test with the result in a snackbar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-quality review of Task 2: documents why ProfileTab needs no
watch-resync of auth.user (router beforeEach awaits fetchMe before
requiresAuth navigation); tightens the save-error test to assert the
exact fallback message instead of mere truthiness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit D1: ProfileTab fields were hardcoded refs and the Save button had
no handler. Rewired to the auth store + a new api/auth updateProfile()
calling PATCH /api/auth/me. Single «Полное имя» field split into Имя +
Фамилия (matches users.first_name/last_name); decorative «Роль» field
removed (no such column). AuthUser type gains phone + timezone.
SettingsView.spec.ts updated: «Полное имя» assertion changed to check
for «Имя» and «Фамилия» (collateral fix for the intentional field split).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-quality review of the legal stub pages: the always-present
informational v-alert defaulted to role=alert (assertive live-region) —
changed to role=note for a static advisory (WCAG 2.1 AA). Trimmed
cosmetic whitespace inside the back-link element.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit A7: the «Оферта» / «Политика» links in the AuthLayout footer were
raw <a href> pointing at unrouted paths -> 404 via the SPA catch-all.
Adds a single DRY LegalDocView served by /legal/:doc(offer|privacy),
rendering an honest «document being finalized» stub (real legal text
needs юр. редактура — реестр K3 / blocker Б-1). Footer links upgraded
to <RouterLink> for SPA navigation. Also refreshes two stale auth-layout
doc-comments left by the /recovery removal (review M1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit A2/A3: RecoveryCodesView (route /recovery) had a TODO no-op
continue handler and 8 hardcoded mock codes. Recon found the page is
orphaned — nothing in the UI navigates to /recovery. The real 2FA
recovery-codes flow lives entirely in Settings -> Безопасность
(TwoFactorCard setup wizard + RecoveryCodesCard regeneration), both
already wired to the real API. Per user decision (2026-05-15) the
orphan is deleted rather than polished.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
axios.patch теперь в try/catch с extractErrorMessage() helper. Per-row
ошибки — reactive errorMessages: Record<number, string> отображаются как
v-icon mdi-alert-circle с v-tooltip рядом с кнопкой «Сохранить».
Success — v-snackbar (3s timeout, color=success, bottom-right) с именем
поставщика.
Retry на той же строке очищает предыдущий error перед новым axios.patch.
load() тоже обёрнут — fetchError ref + v-alert warning сверху таблицы.
+3 Vitest specs (save error / save success / retry clears error).
Регрессий 0.
Closes audit ID G2 from docs/superpowers/specs/2026-05-15-portal-audit-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
axios.post/delete теперь обёрнуты в try/catch с extractErrorMessage()
хелпером из api/client.ts (same pattern as AdminSystemView.vue:32-45).
errorMessage отображается в v-alert (closable, type=error, tonal),
successMessage — в v-snackbar (color=success, 4s timeout).
На failed submit диалог остаётся открытым чтобы пользователь мог
исправить и повторить (UX-pattern). saving=false гарантированно
сбрасывается в finally.
+4 Vitest specs (submit error / submit success / delete error / delete success).
Регрессий 0.
Closes audit ID G1 from docs/superpowers/specs/2026-05-15-portal-audit-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
window.alert блокирует UI thread, не accessible (a11y), breaks браузерный
automation (Playwright/Selenium). Заменено на v-snackbar (timeout 6s,
color warning, location bottom-right, кнопка «Закрыть»). Текст идентичен:
«Применено: N. Пропущено: M (конфликт с уже доставленными лидами).»
+2 Vitest specs (snackbar opens / snackbar НЕ opens at skipped=0).
window.confirm для pause/resume/archive намеренно оставлен — это
deliberate blocking прерывание для деструктивных операций (UX-pattern).
Closes audit ID C5 from docs/superpowers/specs/2026-05-15-portal-audit-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review fixes для commit fbe5e9e (C4 KanbanView DnD persist):
I-2 (test coverage gap): Revert test «onColumnChange reverts...» теперь
seed'ит deal в dealsByStatus['hot'] до вызова onColumnChange (имитируя
vuedraggable mutation pre-event). После failed transition — assert
карточка удалена из hot + восстановлена в new. Раньше array-revert
branch в KanbanView.vue:80-87 (splice + push) имел 0 test coverage —
findIndex возвращал -1, splice silent. Теперь coverage 100%.
I-3 (stale JSDoc): File-header comment в KanbanView.vue lines 7-16
обновлён — описывает actual behavior после Task 2 (optimistic + API call
+ revert). Раньше явно врал «не входит в этот коммит: PATCH /api/deals/
{id}» когда POST /api/deals/transition уже реализован.
Регрессий 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drag-drop между колонками теперь сохраняется в БД через существующий
DealBulkActionController@transition endpoint (single-element массив).
Optimistic UI update (statusSlug меняется сразу) + revert-on-fail с
toast «Не удалось переместить — восстановлен исходный статус».
Без auth.user.tenant_id (dev/demo без login) — local-only mode, API не
зовётся (graceful degradation).
+3 Vitest specs в KanbanView.spec.ts (success / revert / no-auth skip).
Pest covered by existing DealTransitionTest. Регрессий 0.
Closes audit ID C4 from docs/superpowers/specs/2026-05-15-portal-audit-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review fixes для commit 9b93635 (C2 FilterChip popovers):
I-4 (latent interaction bug): Удалена двойная open-path в FilterChip
activator. v-menu сам управляет projectMenuOpen/managerMenuOpen через
activatorProps. Draft-state копируется при menu open → true через
watch(menuOpen, ...). Раньше:
- Activator click: menuOpen=true
- @click on FilterChip: onRedesignFilterClick → menuOpen=true (duplicate)
- Re-click для close: activator toggles false → onRedesignFilterClick
forces true back → menu не закрывается.
I-2 (inline toggle extract): Multi-line ternary @click заменён на
named methods toggleProjectDraft(proj) / toggleManagerDraft(name).
Консистентно с existing clearProjectDraft / clearManagerDraft. Также
unit-testable независимо от template.
onRedesignFilterClick остаётся для Status chip read-only behavior (P2
backlog Sprint 5). defineExpose обновлён: убран onRedesignFilterClick,
добавлены toggleProjectDraft/toggleManagerDraft/clearProjectDraft/
clearManagerDraft (для будущих spec'ов).
Vitest 3/3 C2-specs обновлены на прямой trigger projectMenuOpen=true
+ $nextTick (watcher seeds draft). Регрессий 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A11y rescan Pattern H — Vuetify <v-text-field> без `label` prop рендерит
empty `<label id="input-v-NN-label">` (referenced via aria-labelledby).
Pa11y/axe видит unlabelled input на /admin/billing (search «Поиск по
названию или ИНН») и /admin/system (search «Поиск по ключу или описанию»).
Initial naive fix добавил `aria-label="..."` — но ARIA priority говорит
aria-labelledby overrides aria-label, поэтому осталось violation.
Final fix: add `label="Поиск"` prop on VTextField. Vuetify рендерит
floating label с правильным accessible text → axe-core resolves через
aria-labelledby chain successfully. Placeholder сохранён (split: «Поиск»
теперь в label, «по названию или ИНН» / «по ключу или описанию» —
placeholder).
Files:
- AdminBillingView.vue:209-217
- AdminSystemView.vue:130-138
Closes Pa11y «label» violations на 2 admin URLs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A11y rescan Pattern A — Vuetify <v-app-bar-nav-icon class="d-md-none">
без accessible name. Pa11y/axe видит button в DOM даже на desktop где
он hidden via CSS — флагает «button-name» violation на 9 AppLayout views
(/dashboard, /deals, /kanban, /projects, /billing, /settings, /reports,
/reminders, /admin/tenants).
Fix: AppTopbar.vue:90-94 — `aria-label="Открыть меню навигации"`.
Closes 9 of 14 authenticated routes' a11y violations (down 14→5 affected
URLs after this commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit #2 Phase 10.2 P2: axe-core 4.10 reported aria-tooltip-name
violation — <div role="tooltip"> had no accessible name. Adding
aria-label to <v-tooltip> passes it through to the rendered overlay.
Verified: axe-core on /admin/tenants — 0 tooltip violations post-fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vue-tsc was emitting 9 errors from two issues:
1. ProjectCard.vue had a local `interface Project` missing region_mask /
region_mode / delivery_days_mask, while stores/projectsStore.ts
exported the canonical one with those fields. ProjectsView.vue passed
the canonical Project to ProjectCard handler signatures which expected
the local incomplete one → 5× TS2322.
2. EditProjectDialog passed `project: Project | Record<string, unknown>`
to NewProjectDialog which expected `Record<string, unknown> | null`.
Project lacks an index signature → TS2322.
3. AppSidebar.vue template referenced `item.countKey` not declared in
NavItem interface → 2× TS2339.
Changes:
- ProjectCard.vue: drop local Project, import from projectsStore.
- NewProjectDialog.vue: project prop type → Project | null (was Record).
Drop `as { id: number }` cast on PATCH URL.
- EditProjectDialog.vue: project prop type → Project | null.
- AppSidebar.vue: add `countKey?: string` to NavItem.
- projectsStore.ts: make region_mask/region_mode/delivery_days_mask
optional (backward-compat for mock fixtures; production rows always
populate them by schema).
- Test/story fixtures expanded with delivered_today/is_active/archived_at/
sync_status to match strict Project shape.
Verification:
- npx vue-tsc --noEmit → 0 errors (was 9).
- npx vitest run on 5 affected specs → 16/16 passed + 2 skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BulkActionsBar.story.vue calls useProjectsStore() in top-level setup,
which executes before story collection. Without Pinia plugin, Histoire
build aborts with `getActivePinia() was called but there was no active
Pinia` — uncaught exception kills the whole build (24 → 0 stories).
Add createPinia() to histoire.setup.ts alongside Vuetify + vue-router.
Also add `/recovery-use` and `/projects` routes to the stub router
(parity with router/index.ts after Plan 5 frontend), so future story
files needing those paths don't emit Vue Router warns.
Histoire build now: exit 0, 35 stories / 63 variants in 80.6s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Корень: dev-БД `liderra` создавалась с LC_CTYPE=C — lower()/upper() не
делает case-folding для кириллицы, `ILIKE '%сп%'` на «Окна СПб» = 0 строк.
Test-БД с Russian_Russia.1251 маскировала проблему.
Системный fix: dev-БД пересоздана через `LOCALE_PROVIDER icu ICU_LOCALE 'und'`
(PG 16+ ICU collation, кросс-платформенно). Точечный COLLATE-workaround не
понадобился — все 5 ILIKE-endpoint'ов теперь работают с кириллицей без
правки кода. CTO-20 закрыт в реестре v1.81; команда CREATE DATABASE с ICU
зафиксирована для prod-deploy.
Сопутствующее:
- ProjectsView clearable: workaround `::after content '✕'` + видимость
через `.v-field--dirty` (mdi-* font не подключён в проекте — CTO-19
заведён в реестре).
- LookupsTest: удалён stale case `GET /api/projects?tenant_id=N`,
заменённый auth:sanctum-роутом в Plan 5.
- Pest +1 регрессионный тест (`search is case-insensitive for Cyrillic`)
в ProjectsListShowTest, 10/10 / 37 assertions.
- phpstan-baseline регенерирован (3 actingAs + удалённый case).
- cspell-words: +Регистронезависимый, +und.
- app/.backups/ в gitignore.
Verify:
- Pest --parallel: 742 passed / 1 flaky error (CsvReconcileJobTest cache
race, в изоляции 2/2 PASS) / 3 skipped.
- Browser: «сп» и «окн» возвращают «Окна СПб».
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ProjectCard.vue: replace 2px noir solid border on .card-check__box with
1px var(--liderra-line) idle / var(--liderra-line-strong) hover / var(--liderra-teal)
checked. Checked state uses tonal 10% teal bg instead of full fill. Size 20→16px.
Added :focus-visible outline for keyboard nav.
NewProjectDialog.vue: add a local .ld-input-quiet class to all 5 v-text-field
in the dialog (domain / phone / sms keyword / name / daily limit). The class
overrides v-field outline border-color through :deep() to use the tokens.css
1px line / line-strong / teal palette, and sets border-radius to var(--radius-8).
All variant/density/color values come from Vuetify global defaults in
plugins/vuetify.ts:50-54. Includes opacity:1 on every override to neutralize
Vuetify's --v-field-border-opacity 0.38 cascade, plus an explicit error-state
rule with border-color:currentColor to preserve Vuetify's red error border.
Twin elements left out of scope: .toolbar-check__box in ProjectsView.vue,
v-combobox/v-autocomplete/v-btn-toggle inside the same dialog, and the
filter-bar v-select inputs.
Spec: docs/superpowers/specs/2026-05-12-quiet-luxury-elements-1440-896-design.md
Plan: docs/superpowers/plans/2026-05-12-quiet-luxury-elements-1440-896.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delta mode combines Add/Remove numeric inputs into a single signed delta;
Replace mode switches to an absolute value input via v-checkbox toggle.
5/5 Vitest pass; full suite 611 passed + 3 skipped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>