From 4e24e7e2d74c02beff4a0c199b69a1487f2f6ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sat, 27 Jun 2026 10:15:09 +0300 Subject: [PATCH] =?UTF-8?q?docs(=D0=BF=D0=BB=D0=B0=D0=BD):=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B3=D0=BB?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D1=81=D0=B0=20=D0=BF=D0=BE=D0=B7=D0=B8=D1=86=D0=B8?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=8E=20-=204=20TDD-=D0=B7=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D1=87=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/observer/STATUS.md | 18 +- .../2026-06-27-menu-reposition-global-fix.md | 379 ++++++++++++++++++ 2 files changed, 388 insertions(+), 9 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-27-menu-reposition-global-fix.md diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 14d808ef..0e03022c 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-06-27T04:18:13.211Z +Last updated: 2026-06-27T07:09:55.632Z | Контролёр | Состояние | Детали | |---|---|---| @@ -39,16 +39,16 @@ Last updated: 2026-06-27T04:18:13.211Z | Время | Действие | Причина | |---|---|---| +| 2026-06-27T07:03:56.852Z | bash:node -e "1" 2>/dev/null; for f in docs/secretary/*/protocol.md; do printf '%6s %s\n' "$(wc -l < "$f")" "$f"; done | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:node -e "1" 2>/dev/nul | +| 2026-06-27T05:45:19.915Z | bash:rm ~/.claude/runtime/secretary-mode-*.json | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:rm ~/.claude/runtime/s | +| 2026-06-27T04:18:53.490Z | powershell:Stop-Process -Id 4072 -Force | floor: опасная PowerShell-команда без аварийного выхода — блок (правило 8, V1-PS); FLOOR-ESCAPE: powershell:Stop-Process | +| 2026-06-27T04:18:44.603Z | powershell:Stop-Process -Id 4072 -Force; if ($?) { "killed 4072" }; Start-Sleep -Milliseconds 300; Get-CimInstance Win32 | floor: опасная PowerShell-команда без аварийного выхода — блок (правило 8, V1-PS); FLOOR-ESCAPE: powershell:Stop-Process | | 2026-06-27T04:08:56.319Z | bash:cd "/c/Users/Administrator/.claude/runtime/" && for f in secretary-mode-*.json; do printf '%s => ' "$f"; cat "$f"; | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:cd "/c/Users/Administr | | 2026-06-27T04:07:22.527Z | bash:cd "c:/моя/проекты/claude-brain" && ls tools/ 2>&1 \| grep -i secret; echo "---grep config---"; grep -ril "secretar | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:cd "c:/моя/проекты/cla | | 2026-06-27T03:41:52.937Z | bash:node -e "const fs=require('fs'),os=require('os'),p=require('path');const dir=p.join(os.homedir(),'.claude','runtime | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:node -e "const fs=requ | | 2026-06-27T03:14:37.381Z | bash:find "c:\моя\проекты\claude-brain" -name "*sessionstart*" -type f 2>/dev/null | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:find "c:\моя\проекты\c | | 2026-06-27T03:10:25.034Z | bash:grep -n "globals\\|include\\|exclude" vitest.config.tools.mjs 2>/dev/null \|\| echo "NO tools config at root" | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:grep -n "globals\\|inc | | 2026-06-27T03:09:50.695Z | bash:git commit -F "C:/Users/ADMINI~1/AppData/Local/Temp/2/claude/c--------------claude-brain/2c98d47c-4f4d-48cc-98a8-2e | [verify-gate] расписка отклонена (stale-fingerprint) — пере-прогоните verify (staged-diff изменился / битая подпись) | -| 2026-06-27T03:07:30.844Z | bash:git commit -F "C:/Users/ADMINI~1/AppData/Local/Temp/2/claude/c--------------claude-brain/2c98d47c-4f4d-48cc-98a8-2e | [verify-gate] расписка отклонена (stale-fingerprint) — пере-прогоните verify (staged-diff изменился / битая подпись) | -| 2026-06-27T03:06:53.280Z | bash:git add tools/secretary-tap.mjs tools/secretary-tap.test.mjs tools/secretary-distill.mjs tools/secretary-distill.te | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:git add tools/secretar | -| 2026-06-27T02:34:28.483Z | bash:ls -la ~/.claude/runtime/standby-* 2>/dev/null; echo "---"; cat ~/.claude/runtime/standby-mode-2c98d47c-4f4d-48cc-9 | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:ls -la ~/.claude/runti | -| 2026-06-26T16:51:40.211Z | bash:git -C "c:/моя/проекты/claude-brain" add docs/superpowers/router-mentor-wall-GUIDE.md && git -C "c:/моя/проекты/cla | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:git -C "c:/моя/проекты | ## Метрики (информационные, не алерты) @@ -132,9 +132,9 @@ Episodes since last run: 542 / threshold: 10 | PID | Имя | CPU-время | Возраст | |---|---|---|---| -| 3440 | MsMpEng | 15.45ч | 220520.3ч | -| 21928 | Code | 5.53ч | NaNч | -| 1212 | svchost | 3.89ч | 0.0ч | +| 3440 | MsMpEng | 15.63ч | 12285175.2ч | +| 21928 | Code | 5.80ч | NaNч | +| 1212 | svchost | 3.95ч | 0.0ч | ⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий. @@ -150,7 +150,7 @@ Episodes since last run: 542 / threshold: 10 ## Целостность журналов действий -🔴 Битые цепочки (3 из 119): +🔴 Битые цепочки (3 из 120): | session | broken at seq | |---|---| diff --git a/docs/superpowers/plans/2026-06-27-menu-reposition-global-fix.md b/docs/superpowers/plans/2026-06-27-menu-reposition-global-fix.md new file mode 100644 index 00000000..1d9dcfe1 --- /dev/null +++ b/docs/superpowers/plans/2026-06-27-menu-reposition-global-fix.md @@ -0,0 +1,379 @@ +# Глобальный фикс позиционирования выпадающих меню — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Включать обход бага позиционирования меню Vuetify автоматически и глобально, чтобы выпадающие списки не уезжали за экран ни в одном окне, и убрать ручные пометки. + +**Architecture:** Один `MutationObserver` (ставится при запуске SPA из `app.ts`) ловит появление любого `.v-overlay.v-menu` в DOM и один раз запускает уже существующую стабилизацию позиции (`scheduleStabilize` — ядро прежнего `repositionMenuAfterOpen`). Ручные `@update:menu`/`@update:model-value` снимаются — остаётся одно место правды. + +**Tech Stack:** Vue 3, Vuetify 3, TypeScript, Vitest + jsdom (`app/tests/Frontend/**`), vue-tsc, Vite. + +**Спека:** [docs/superpowers/specs/2026-06-27-menu-reposition-global-fix-design.md](../specs/2026-06-27-menu-reposition-global-fix-design.md) + +--- + +## Структура файлов + +| Файл | Ответственность | Действие | +|---|---|---| +| `app/resources/js/utils/menuRepositionFix.ts` | стабилизация позиции меню + глобальный установщик | Modify | +| `app/resources/js/app.ts` | точка запуска SPA — один вызов установщика | Modify | +| `app/tests/Frontend/menuRepositionFix.spec.ts` | тест механизма (observer → один resize) | Create | +| `app/resources/js/views/projects/NewProjectDialog.vue` | убрать ручной обход (регионы) + импорт | Modify | +| `app/resources/js/components/projects/RegionsBulkDialog.vue` | убрать ×2 обход + импорт | Modify | +| `app/resources/js/components/projects/ProjectDetailsDrawer.vue` | убрать ×1 обход + импорт | Modify | +| `app/resources/js/components/layout/AppTopbar.vue` | убрать ×2 обход + импорт | Modify | + +Все команды запускаются из каталога `app/` (там `package.json`). В PowerShell: `cd app; npm run ...`. + +--- + +## Task 1: Глобальный установщик `installMenuRepositionFix` (TDD) + +На этом шаге старый экспорт `repositionMenuAfterOpen` НЕ удаляем — он останется до Task 4, чтобы сборка оставалась зелёной (его ещё импортируют 4 окна). Добавляем новую функцию рядом, общее ядро `scheduleStabilize`. + +**Files:** +- Test: `app/tests/Frontend/menuRepositionFix.spec.ts` (создать) +- Modify: `app/resources/js/utils/menuRepositionFix.ts` + +- [ ] **Step 1: Написать падающий тест** + +Создать `app/tests/Frontend/menuRepositionFix.spec.ts`: + +```ts +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { installMenuRepositionFix } from '../../resources/js/utils/menuRepositionFix'; + +// Ручной requestAnimationFrame: кадры прогоняем детерминированно. +let rafQueue: FrameRequestCallback[] = []; +function flushFrames(n: number): void { + for (let i = 0; i < n; i++) { + const batch = rafQueue; + rafQueue = []; + batch.forEach((cb) => cb(0)); + } +} + +// Дать MutationObserver (микротаска jsdom) сработать. +const tick = (): Promise => new Promise((r) => setTimeout(r, 0)); + +function makeMenu(): HTMLElement { + const menu = document.createElement('div'); + menu.className = 'v-overlay v-menu'; + const content = document.createElement('div'); + content.className = 'v-overlay__content'; + menu.appendChild(content); + return menu; +} + +let teardown: (() => void) | undefined; +let rectWidth = 200; +let rectLeft = 100; +let origRect: typeof HTMLElement.prototype.getBoundingClientRect; +let resizeSpy: ReturnType; + +beforeEach(() => { + rafQueue = []; + rectWidth = 200; + rectLeft = 100; + vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { + rafQueue.push(cb); + return rafQueue.length; + }); + origRect = HTMLElement.prototype.getBoundingClientRect; + HTMLElement.prototype.getBoundingClientRect = function (): DOMRect { + return { width: rectWidth, height: 10, left: rectLeft, top: 0, right: rectLeft + rectWidth, bottom: 10, x: rectLeft, y: 0, toJSON: () => ({}) } as DOMRect; + }; + resizeSpy = vi.fn(); + window.addEventListener('resize', resizeSpy); +}); + +afterEach(() => { + teardown?.(); + teardown = undefined; + HTMLElement.prototype.getBoundingClientRect = origRect; + window.removeEventListener('resize', resizeSpy); + vi.unstubAllGlobals(); + document.body.innerHTML = ''; +}); + +describe('installMenuRepositionFix', () => { + it('при появлении меню стабилизирует позицию и шлёт один resize', async () => { + teardown = installMenuRepositionFix(); + document.body.appendChild(makeMenu()); + await tick(); + flushFrames(6); + expect(resizeSpy).toHaveBeenCalledTimes(1); + }); + + it('на посторонний узел не реагирует', async () => { + teardown = installMenuRepositionFix(); + const other = document.createElement('div'); + other.className = 'some-card'; + document.body.appendChild(other); + await tick(); + flushFrames(6); + expect(resizeSpy).not.toHaveBeenCalled(); + }); + + it('идемпотентна: двойная установка не даёт двойной resize', async () => { + teardown = installMenuRepositionFix(); + installMenuRepositionFix(); // второй вызов — noop + document.body.appendChild(makeMenu()); + await tick(); + flushFrames(6); + expect(resizeSpy).toHaveBeenCalledTimes(1); + }); + + it('предохранитель: если геометрия не устаканилась — не виснет и не шлёт resize', async () => { + rectWidth = 0; // контент «нулевой» → условие стабилизации не выполняется + teardown = installMenuRepositionFix(); + document.body.appendChild(makeMenu()); + await tick(); + flushFrames(120); // больше предохранителя (90 кадров) + expect(resizeSpy).not.toHaveBeenCalled(); + expect(rafQueue.length).toBe(0); // цикл остановился, не зациклился + }); +}); +``` + +- [ ] **Step 2: Прогнать тест — убедиться, что падает** + +Run (из `app/`): `npm run test:vue -- menuRepositionFix` +Expected: FAIL — `installMenuRepositionFix` не экспортируется из `menuRepositionFix.ts`. + +- [ ] **Step 3: Реализовать установщик** + +Переписать `app/resources/js/utils/menuRepositionFix.ts` так (шапку-комментарий сохранить, ниже — тело): + +```ts +// Ядро: дождаться, пока геометрия последнего меню устаканится, и один раз +// послать resize — Vuetify пересчитает позицию по стабильной геометрии. +function scheduleStabilize(): void { + if (typeof window === 'undefined') return; + + let prevLeft = Number.NaN; + let stableFrames = 0; + let totalFrames = 0; + + const tick = (): void => { + const menus = document.querySelectorAll('.v-overlay.v-menu .v-overlay__content'); + const el = menus[menus.length - 1]; + + if (el && el.getBoundingClientRect().width > 0) { + const left = Math.round(el.getBoundingClientRect().left); + stableFrames = left === prevLeft ? stableFrames + 1 : 0; + prevLeft = left; + if (stableFrames >= 3) { + window.dispatchEvent(new Event('resize')); + return; + } + } + + if (++totalFrames < 90) requestAnimationFrame(tick); + }; + + requestAnimationFrame(tick); +} + +/** + * @deprecated Ручной per-instance обход. Заменён глобальным + * installMenuRepositionFix(). Оставлен временно до снятия ручных пометок. + */ +export function repositionMenuAfterOpen(open: boolean): void { + if (!open) return; + scheduleStabilize(); +} + +let installed = false; + +/** + * Глобально включает обход бага позиционирования меню Vuetify: один + * MutationObserver ловит появление любого `.v-overlay.v-menu` и запускает + * стабилизацию. Идемпотентна (повторный вызов — noop). SSR-safe. + * Возвращает teardown (для тестов / явной остановки). + */ +export function installMenuRepositionFix(): () => void { + const noop = (): void => {}; + if (installed) return noop; + if ( + typeof window === 'undefined' || + typeof document === 'undefined' || + typeof MutationObserver === 'undefined' || + !document.body + ) { + return noop; + } + + installed = true; + const observer = new MutationObserver((mutations) => { + for (const m of mutations) { + for (const node of m.addedNodes) { + if (!(node instanceof HTMLElement)) continue; + if (node.matches('.v-overlay.v-menu') || node.querySelector('.v-overlay.v-menu')) { + scheduleStabilize(); + return; // одного запуска на пачку мутаций достаточно + } + } + } + }); + observer.observe(document.body, { childList: true, subtree: true }); + + return () => { + observer.disconnect(); + installed = false; + }; +} +``` + +> Примечание: блок JSDoc-шапки в начале файла (описание бага) сохранить как есть — он остаётся актуальным. + +- [ ] **Step 4: Прогнать тест — зелёный** + +Run (из `app/`): `npm run test:vue -- menuRepositionFix` +Expected: PASS — все 4 кейса. + +- [ ] **Step 5: Коммит** + +```bash +git add app/resources/js/utils/menuRepositionFix.ts app/tests/Frontend/menuRepositionFix.spec.ts +git commit -m "feat(fe): глобальный installMenuRepositionFix + тест механизма" +``` + +--- + +## Task 2: Подключить установщик в точке запуска + +**Files:** +- Modify: `app/resources/js/app.ts` + +- [ ] **Step 1: Добавить импорт и вызов после mount** + +В `app/resources/js/app.ts` добавить импорт рядом с остальными: + +```ts +import { installMenuRepositionFix } from './utils/menuRepositionFix'; +``` + +И сразу после `app.mount('#app');` добавить строку: + +```ts +app.mount('#app'); +installMenuRepositionFix(); +``` + +- [ ] **Step 2: Проверка типов и сборки** + +Run (из `app/`): `npm run type-check` +Expected: PASS (0 ошибок). + +- [ ] **Step 3: Коммит** + +```bash +git add app/resources/js/app.ts +git commit -m "feat(fe): подключить installMenuRepositionFix при запуске SPA" +``` + +--- + +## Task 3: Снять ручные пометки в 4 окнах + +Глобальный обход уже активен (Task 2). Ручные `@update:menu` / `@update:model-value="repositionMenuAfterOpen"` и их импорты теперь лишние — снимаем. Точные строки могли сдвинуться; ориентир — по атрибуту и импорту. + +**Files:** +- Modify: `app/resources/js/views/projects/NewProjectDialog.vue` +- Modify: `app/resources/js/components/projects/RegionsBulkDialog.vue` +- Modify: `app/resources/js/components/projects/ProjectDetailsDrawer.vue` +- Modify: `app/resources/js/components/layout/AppTopbar.vue` + +- [ ] **Step 1: Найти все вхождения** + +Run (из корня репо): `grep -rn "repositionMenuAfterOpen" app/resources/js` +Expected: вхождения только в 4 файлах выше (по 1 импорту + использования: NewProjectDialog ×1, RegionsBulkDialog ×2, ProjectDetailsDrawer ×1, AppTopbar ×2). + +- [ ] **Step 2: Удалить строки-атрибуты и импорты** + +В каждом из 4 файлов: +1. Удалить строку(и) атрибута на компоненте: `@update:menu="repositionMenuAfterOpen"` (NewProjectDialog, RegionsBulkDialog ×2, ProjectDetailsDrawer) либо `@update:model-value="repositionMenuAfterOpen"` (AppTopbar ×2). +2. Удалить строку импорта: `import { repositionMenuAfterOpen } from '../../utils/menuRepositionFix';` (путь относительный — как в файле). + +Ничего другого в этих строках не трогать (атрибут на отдельной строке — удаляется целиком). + +- [ ] **Step 3: Убедиться, что ссылок не осталось** + +Run (из корня репо): `grep -rn "repositionMenuAfterOpen" app/resources/js` +Expected: вхождения ТОЛЬКО в `app/resources/js/utils/menuRepositionFix.ts` (определение `@deprecated`-функции). В `.vue`-файлах — ничего. + +- [ ] **Step 4: Тесты затронутых окон + типы** + +Run (из `app/`): `npm run test:vue -- NewDealDialog RegionsBulkDialog ProjectDetailsDrawer DealsFilters TenantsFilters` +Expected: PASS (затронутые окна рендерятся, обработчик отсутствует — не ломает). +Run (из `app/`): `npm run type-check` +Expected: PASS — нет «неиспользуемый импорт». + +- [ ] **Step 5: Коммит** + +```bash +git add app/resources/js/views/projects/NewProjectDialog.vue app/resources/js/components/projects/RegionsBulkDialog.vue app/resources/js/components/projects/ProjectDetailsDrawer.vue app/resources/js/components/layout/AppTopbar.vue +git commit -m "refactor(fe): снять ручные обходы меню - заменены глобальным установщиком" +``` + +--- + +## Task 4: Удалить мёртвый экспорт `repositionMenuAfterOpen` + +После Task 3 функция нигде не используется. Удаляем экспорт — ядро остаётся внутренним, наружу торчит только `installMenuRepositionFix`. + +**Files:** +- Modify: `app/resources/js/utils/menuRepositionFix.ts` + +- [ ] **Step 1: Удалить `@deprecated`-функцию** + +В `app/resources/js/utils/menuRepositionFix.ts` удалить весь блок: + +```ts +/** + * @deprecated Ручной per-instance обход. Заменён глобальным + * installMenuRepositionFix(). Оставлен временно до снятия ручных пометок. + */ +export function repositionMenuAfterOpen(open: boolean): void { + if (!open) return; + scheduleStabilize(); +} +``` + +`scheduleStabilize` и `installMenuRepositionFix` остаются. (`scheduleStabilize` остаётся используемым из `installMenuRepositionFix`.) + +- [ ] **Step 2: Полная верификация** + +Run (из `app/`): `npm run test:vue -- menuRepositionFix` +Expected: PASS (4 кейса). +Run (из `app/`): `npm run type-check` +Expected: PASS — нет неиспользуемого `scheduleStabilize`/мёртвого кода. +Run (из `app/`): `npm run build` +Expected: сборка успешна. + +- [ ] **Step 3: Коммит** + +```bash +git add app/resources/js/utils/menuRepositionFix.ts +git commit -m "refactor(fe): убрать мёртвый repositionMenuAfterOpen - ядро внутреннее" +``` + +--- + +## Финальная проверка (после всех задач) + +- [ ] Run (из `app/`): `npm run test:vue` — весь фронт-набор зелёный. +- [ ] Run (из `app/`): `npm run type-check` — 0 ошибок. +- [ ] Run (из `app/`): `npm run build` — сборка ок. +- [ ] Глазами (после выката на прод по «выкатывай»): открыть «Тип лица» в визарде нового проекта, списки в «Новой сделке» и в диалоге импорта статусов — меню встают на место (особенно под Windows / reduced-motion). + +--- + +## Self-review (выполнено автором плана) + +- **Покрытие спеки:** §4.1 ядро+установщик → Task 1; §4.2 точка запуска → Task 2; §4.3 уборка пометок → Task 3 + Task 4 (удаление экспорта); §6 тесты → Task 1 (4 кейса) + финальная проверка. Гэпов нет. +- **Плейсхолдеры:** нет TBD/TODO; весь код приведён целиком. +- **Согласованность имён:** `scheduleStabilize`, `installMenuRepositionFix`, `repositionMenuAfterOpen` — употреблены одинаково во всех задачах; экспорт `repositionMenuAfterOpen` живёт Task 1→3, удаляется в Task 4 (по тексту согласовано). +- **Зелёные коммиты:** старый экспорт держится до снятия всех ссылок (Task 3), только потом удаляется (Task 4) — сборка не ломается между коммитами.