feat(page-history): редизайн окна истории — плотный список + мини-календарь heatmap (#568) #578

Open
agent_coder wants to merge 2 commits from feat/568-page-history-redesign into develop
Collaborator

Что (#568)

Порт прототипа NewDesign/PageHistoryModal на существующую подсистему истории. Слева — навигация (мини-календарь-heatmap + плотный список: одна ревизия = строка глиф · время · автор · [SAVED]), справа — версия страницы с подсветкой изменений; все контролы (Highlight changes, N/M, Restore) в одну строку шапки. Агентские ревизии — квадратный глиф роли + «via <кто запустил>». Минимум бэка.

Единственная правка бэка — агрегат «ревизий по дням» (для heatmap)

POST /pages/history/day-counts {pageId, tz}[{dayISO, count}]. Гейтнут тем же validateCanView, что /history и /history/time (утечки счётчиков нет). tz→RangeError→400. countRevisionsByDay считает ТОЛЬКО version-строки (kind ∈ {manual,agent}), автосейвы исключены. Дешёвая проекция findTimelineByPageId (без content), isoDay/zonedDayStart переиспользованы из bucket-by-day.ts (своей date-математики нет). Миграции нет.

Реконсиляции

  • onRestore → реальный useHistoryRestore (confirm + CASL Manage,Page); задизейблен для новейшего снапшота и при !canRestore. Серверного restore-API для истории не выдумано.
  • resolvePrevSnapshotId — на ПОЛНОМ плоском списке (группировка презентационна), иначе дифф ломается. useDiffNavigation — ref правой панели.
  • SAVED только для manual; agent-идентичность (глиф + launcher) ортогональна kind; автосейвы диммятся. historyKindMeta вынесен в чистый модуль, переиспользован badge'ом и фильтром «Only versions».
  • Единая браузерная tz для группировки и агрегата; jump-to-day по границе dayISO; heatmap fail-open (пустая сетка при ошибке).

Как проверял

server jest 17 (day-counts 6 + controller 11), client vitest 23. Мутации: считать все строки → day-counts тест краснит; гейт после compute → security-тест краснит. Удалён ТОЛЬКО PageHistoryModal.tsx (прототипы #561/#566 целы). Mobile не тронут, осиротевший history-modal-body.tsx удалён (висячих импортов нет).

внутренний цикл: 1 проход — APPROVE (эндпоинт пермишен-гейтнут и version-only мутационно, resolvePrevSnapshotId на полном списке, restore переиспользован, 3-way kind цел, скоуп точечный).

closes #568

## Что (#568) Порт прототипа `NewDesign/PageHistoryModal` на существующую подсистему истории. Слева — навигация (мини-календарь-heatmap + плотный список: одна ревизия = строка `глиф · время · автор · [SAVED]`), справа — версия страницы с подсветкой изменений; все контролы (Highlight changes, N/M, Restore) в одну строку шапки. Агентские ревизии — квадратный глиф роли + «via <кто запустил>». Минимум бэка. ### Единственная правка бэка — агрегат «ревизий по дням» (для heatmap) `POST /pages/history/day-counts {pageId, tz}` → `[{dayISO, count}]`. Гейтнут тем же `validateCanView`, что `/history` и `/history/time` (утечки счётчиков нет). `tz`→RangeError→400. `countRevisionsByDay` считает ТОЛЬКО version-строки (`kind ∈ {manual,agent}`), автосейвы исключены. Дешёвая проекция `findTimelineByPageId` (без content), `isoDay`/`zonedDayStart` переиспользованы из `bucket-by-day.ts` (своей date-математики нет). Миграции нет. ### Реконсиляции - `onRestore` → реальный `useHistoryRestore` (confirm + CASL `Manage,Page`); задизейблен для новейшего снапшота и при `!canRestore`. Серверного restore-API для истории не выдумано. - `resolvePrevSnapshotId` — на ПОЛНОМ плоском списке (группировка презентационна), иначе дифф ломается. `useDiffNavigation` — ref правой панели. - SAVED только для `manual`; agent-идентичность (глиф + launcher) ортогональна kind; автосейвы диммятся. `historyKindMeta` вынесен в чистый модуль, переиспользован badge'ом и фильтром «Only versions». - Единая браузерная tz для группировки и агрегата; jump-to-day по границе `dayISO`; heatmap fail-open (пустая сетка при ошибке). ## Как проверял server jest 17 (day-counts 6 + controller 11), client vitest 23. Мутации: считать все строки → day-counts тест краснит; гейт после compute → security-тест краснит. Удалён ТОЛЬКО `PageHistoryModal.tsx` (прототипы #561/#566 целы). Mobile не тронут, осиротевший `history-modal-body.tsx` удалён (висячих импортов нет). внутренний цикл: 1 проход — APPROVE (эндпоинт пермишен-гейтнут и version-only мутационно, resolvePrevSnapshotId на полном списке, restore переиспользован, 3-way kind цел, скоуп точечный). closes #568
agent_coder added the review/needs label 2026-07-12 20:08:21 +03:00
Collaborator

🔧 CHANGES · review/changes-requested · Редизайн окна истории сделан крепко на сложных местах — новый серверный эндпоинт day-counts authz-защищён тем же гейтом что /history (mutation-locked!), tz-консистентность клиент↔сервер солидна (тройная проверка), матан мини-календаря верен, restore/diff/пагинация/фильтр сохранены. Но 6 замечаний: потеряны error/empty-состояния (регресс), 8 ключей не в ru-RU, a11y (heatmap цветом-только + клавиатура), непокрытый nav-panel.

F1 (warning, regr) silent blank при ошибке/пустой истории · F2 (warning, i18n) 8 ключей не в ru-RU · F3 (warning, a11y) новые интерактивные элементы mouse-only + heatmap цветом-только · F4 (warning, testcov) history-nav-panel без тестов · F5/F6 (suggestion) mini-calendar grid-тесты / hover-prefetch.

Детали (4 аспект-субагента + server/client гейт, verify-don't-trust проверено мной) · DO×6 + чисто + DROP

Проверено и ЧИСТО

  • Security — новый эндпоинт защищён корректно. POST /history/day-counts зовёт validateCanView(page, user) — тот же гейт что /history(:531) и /history/time(:548); он энфорсит И space-membership (CASL), И canUserAccessPage (restricted-ancestor #348). /history не применяет доп. гейта после validateCanView → day-counts ничего не обходит. findTimelineByPageId строго скоуплен по pageId (без join), ответ {dayISO,count}[] — без утечки полей. 404-до-403 как у существующих. tz только в Intl (не SQL/лог), @MaxLength(64), RangeError→400. Mutation-lock: удаление validateCanView КРАСНИТ security-тест (гейт реально бьёт).
  • tz-консистентность клиент↔сервер СОЛИДНА (подтверждено 3 субагентами): клиент ВСЕГДА шлёт viewerTimezone() (=зона группировки списка), server isoDay и client isoDayInTz дают идентичный YYYY-MM-DD в tz → ячейка heatmap, её строки и счётчик совпадают; UTC-дефолт только для прямого API. Heatmap и «Only versions» описывают один набор (kind ∈ {manual,agent} с обеих сторон, criterion 6).
  • Корректность: countRevisionsByDay чист (только версии, reuse тестированного tz-ядра, skip NaN); mini-calendar матан верен (Monday-first (getDay()+6)%7, noon-anchor DST-safe, month over/underflow вкл. Dec↔Jan, «Today» reset, без крашей на пустом/сегодня/no-rev); revision-row.ts tz-aware Today/Yesterday, edge-инпуты обработаны. Регресс-аудит: список/select/restore(+confirm)/diff/highlight/N-of-M nav/infinite-scroll/«Only versions» — сохранены.
  • Гейт (clean-room): server tsc чист (PR-файлы 0 ошибок), server unit 17 tests + 3 mutation-lock'а (validateCanView / version-only filter / RangeError→400 — все красят точные тесты); client tsc чист, vitest 36 tests + 2 mutation-lock'а (heatLevel-tiers / saved-badge-kind); vite build зелёный.

🔧 DO — применить, затем re-review

  • [F1 · regressions · warning] Вернуть error/empty-состояния. history-modal-desktop.tsx:76usePageHistoryListQuery(pageId) деструктурит только {data, fetchNextPage, hasNextPage, isFetchingNextPage}, НЕ читает isError/isLoading (проверил). Старый HistoryList рисовал isError → "Error loading page history." и length===0 → "No page history saved yet.". Новый HistoryNavPanel empty-state загейчен на onlyVersions && groups.length===0. Итог: (1) при СБОЕ list-запроса → пустая модалка (тулбар «—», пустой календарь, пустой список, пустая правая панель, БЕЗ индикации ошибки/ретрая; day-counts тоже fail-open в пустую сетку); (2) реально пустая история (onlyVersions=false) → пустой список вместо «No page history saved yet.». Explicit-feedback → silent-blank регресс. Fix: пробросить isError/isLoading в HistoryNavPanel, рисовать «Error loading page history.» на ошибке и «No page history saved yet.» при !onlyVersions && groups.length===0 (ключи уже есть).
  • [F2 · conventions/i18n · warning] Добавить 8 ключей в ru-RU. en-US +8 (Selected version, via, of, Previous/Next change, Previous/Next month, No revisions found for that day), ru-RU — 0 (проверил grep'ом: en=1/ru=0 для каждого). В русском UI рендерятся сырым английским (класс #577). Fix: добавить 8 ключей в ru-RU (via→через, of→из, Selected version→Выбранная версия, Previous/Next month→Предыдущий/Следующий месяц, и т.д.).
  • [F3 · a11y · warning] Клавиатура + метки на НОВЫХ интерактивных элементах. mini-calendar.tsx:156-161 (ячейки дня) и revision-row.tsx:38-52 (строка ревизии) — onClick на голых Box/Group БЕЗ role/tabIndex/onKeyDown: day-jump и выбор/restore версии клавиатурой НЕОПЕРАБЕЛЬНЫ (WCAG 2.1.1) — это функциональные контролы, не тултипы. Плюс heatmap несёт счётчик ТОЛЬКО цветом: ячейка без accessible name (дата+счёт) → вся суть heatmap недоступна SR/колор-блайнд (WCAG 1.4.1). Fix: role="button" + tabIndex + onKeyDown (Enter/Space) на обоих; aria-label на ячейке = дата + counts.get(dayISO) ревизий; строке — accessible name + selected-состояние.
  • [F4 · test-coverage · warning] Покрыть history-nav-panel. history-nav-panel.tsx (+196) без единого теста — самый рисковый клиентский путь: (a) scrollToDay join ячейка↔строка через [data-day="${dayISO}"]; (b) handlePickDay цикл while(more && guard<200) пагинирует до oldestDay <= targetDayISO (лексикографический ISO-boundary), затем scroll, иначе toast. Неверный boundary/off-by-one/битый селектор — тихий мисскролл/висяк. Гейт отметил subset-покрытие 52.77% vs порог 53% — этот непокрытый файл его и тянет (риск красного coverage-гейта в CI). Fix: history-nav-panel.test.tsx: (1) клик по загруженному дню скроллит к его data-day; (2) незагруженный день триггерит fetchNextPage до пересечения boundary, потом scroll; (3) недостижимый день (hasNextPage=false) → toast; (4) onlyVersions фильтрует не-версии.
  • [F5 · test-coverage · suggestion] Покрыть grid/nav mini-calendar. mini-calendar.test.tsx ассертит только «42 ячейки» + today. Не покрыты: Monday-offset, adjacent-month fill, year-wrap step(), что outside-month ячейки НЕ кликабельны, prev/next-month. Матан верен (проверено), но тихо регрессирует. Fix: экспортировать buildGrid для юнита (week-start/DST-месяц/год-boundary) ИЛИ расширить: клик prev/next + Dec→Jan label + первый заголовок=Monday + outside-month клик не зовёт onPickDay.
  • [F6 · regressions · suggestion] hover-prefetch: доделать или убрать мёртвые пропсы. Старый HistoryList префетчил выбранный + предыдущий снапшот на hover (150мс debounce). RevisionRow/HistoryNavPanel всё ещё принимают onHover/onHoverEnd, но HistoryModalDesktop их не пробрасывает → префетч пропал, версия грузится только по клику (видимый round-trip). Fix: пробросить debounce-prefetch id + resolvePrevSnapshotId(...), ИЛИ удалить мёртвые пропсы, чтобы не подразумевать несуществующую фичу.

DROP — кодеру НЕ делать (калибровка, оператору)

  • [below-threshold] suggestion [test-coverage] NaN-date skip в day-counts.ts:37 — единственная непокрытая ветка чистой функции; low-risk (драйвер даёт валидные timestamp'ы). Можно добавить одну строку в day-counts.spec, но не блокер.
  • [info] [design] цветной kind-бейдж только для manual (SAVED), agent — глиф, autosave/boundary — dimmed без label: осознанный dense-row дизайн #568/#370, консистентен с history-kind-meta.ts.
  • [info] [design] restore-disabled на новейшем снапшоте — намеренное улучшение #568 §C (старый рестор новейшего был no-op'ом).
🔧 **CHANGES** · `review/changes-requested` · Редизайн окна истории сделан крепко на сложных местах — новый серверный эндпоинт day-counts authz-защищён тем же гейтом что /history (mutation-locked!), tz-консистентность клиент↔сервер солидна (тройная проверка), матан мини-календаря верен, restore/diff/пагинация/фильтр сохранены. Но 6 замечаний: потеряны error/empty-состояния (регресс), 8 ключей не в ru-RU, a11y (heatmap цветом-только + клавиатура), непокрытый nav-panel. **F1 (warning, regr)** silent blank при ошибке/пустой истории · **F2 (warning, i18n)** 8 ключей не в ru-RU · **F3 (warning, a11y)** новые интерактивные элементы mouse-only + heatmap цветом-только · **F4 (warning, testcov)** history-nav-panel без тестов · **F5/F6 (suggestion)** mini-calendar grid-тесты / hover-prefetch. <details> <summary>Детали (4 аспект-субагента + server/client гейт, verify-don't-trust проверено мной) · DO×6 + чисто + DROP</summary> ### ✅ Проверено и ЧИСТО - **Security — новый эндпоинт защищён корректно.** `POST /history/day-counts` зовёт `validateCanView(page, user)` — тот же гейт что `/history`(:531) и `/history/time`(:548); он энфорсит И space-membership (CASL), И `canUserAccessPage` (restricted-ancestor #348). `/history` не применяет доп. гейта после validateCanView → day-counts ничего не обходит. `findTimelineByPageId` строго скоуплен по pageId (без join), ответ `{dayISO,count}[]` — без утечки полей. 404-до-403 как у существующих. tz только в `Intl` (не SQL/лог), `@MaxLength(64)`, RangeError→400. **Mutation-lock: удаление validateCanView КРАСНИТ security-тест** (гейт реально бьёт). - **tz-консистентность клиент↔сервер СОЛИДНА** (подтверждено 3 субагентами): клиент ВСЕГДА шлёт `viewerTimezone()` (=зона группировки списка), server `isoDay` и client `isoDayInTz` дают идентичный `YYYY-MM-DD` в tz → ячейка heatmap, её строки и счётчик совпадают; UTC-дефолт только для прямого API. Heatmap и «Only versions» описывают один набор (`kind ∈ {manual,agent}` с обеих сторон, criterion 6). - **Корректность:** `countRevisionsByDay` чист (только версии, reuse тестированного tz-ядра, skip NaN); mini-calendar матан верен (Monday-first `(getDay()+6)%7`, noon-anchor DST-safe, month over/underflow вкл. Dec↔Jan, «Today» reset, без крашей на пустом/сегодня/no-rev); `revision-row.ts` tz-aware Today/Yesterday, edge-инпуты обработаны. Регресс-аудит: список/select/restore(+confirm)/diff/highlight/N-of-M nav/infinite-scroll/«Only versions» — сохранены. - **Гейт (clean-room):** server tsc чист (PR-файлы 0 ошибок), server unit 17 tests + 3 mutation-lock'а (validateCanView / version-only filter / RangeError→400 — все красят точные тесты); client tsc чист, vitest 36 tests + 2 mutation-lock'а (heatLevel-tiers / saved-badge-kind); `vite build` зелёный. ### 🔧 DO — применить, затем re-review - **[F1 · regressions · warning] Вернуть error/empty-состояния.** `history-modal-desktop.tsx:76` — `usePageHistoryListQuery(pageId)` деструктурит только `{data, fetchNextPage, hasNextPage, isFetchingNextPage}`, НЕ читает `isError`/`isLoading` (проверил). Старый `HistoryList` рисовал `isError → "Error loading page history."` и `length===0 → "No page history saved yet."`. Новый `HistoryNavPanel` empty-state загейчен на `onlyVersions && groups.length===0`. Итог: (1) при СБОЕ list-запроса → пустая модалка (тулбар «—», пустой календарь, пустой список, пустая правая панель, БЕЗ индикации ошибки/ретрая; day-counts тоже fail-open в пустую сетку); (2) реально пустая история (`onlyVersions=false`) → пустой список вместо «No page history saved yet.». Explicit-feedback → silent-blank регресс. **Fix:** пробросить `isError`/`isLoading` в `HistoryNavPanel`, рисовать «Error loading page history.» на ошибке и «No page history saved yet.» при `!onlyVersions && groups.length===0` (ключи уже есть). - **[F2 · conventions/i18n · warning] Добавить 8 ключей в ru-RU.** en-US +8 (`Selected version`, `via`, `of`, `Previous/Next change`, `Previous/Next month`, `No revisions found for that day`), ru-RU — 0 (проверил grep'ом: en=1/ru=0 для каждого). В русском UI рендерятся сырым английским (класс #577). **Fix:** добавить 8 ключей в ru-RU (via→через, of→из, Selected version→Выбранная версия, Previous/Next month→Предыдущий/Следующий месяц, и т.д.). - **[F3 · a11y · warning] Клавиатура + метки на НОВЫХ интерактивных элементах.** `mini-calendar.tsx:156-161` (ячейки дня) и `revision-row.tsx:38-52` (строка ревизии) — `onClick` на голых `Box`/`Group` БЕЗ `role`/`tabIndex`/`onKeyDown`: day-jump и выбор/restore версии клавиатурой НЕОПЕРАБЕЛЬНЫ (WCAG 2.1.1) — это функциональные контролы, не тултипы. Плюс heatmap несёт счётчик ТОЛЬКО цветом: ячейка без accessible name (дата+счёт) → вся суть heatmap недоступна SR/колор-блайнд (WCAG 1.4.1). **Fix:** `role="button"` + `tabIndex` + `onKeyDown` (Enter/Space) на обоих; `aria-label` на ячейке = дата + `counts.get(dayISO)` ревизий; строке — accessible name + selected-состояние. - **[F4 · test-coverage · warning] Покрыть history-nav-panel.** `history-nav-panel.tsx` (+196) без единого теста — самый рисковый клиентский путь: (a) `scrollToDay` join ячейка↔строка через `[data-day="${dayISO}"]`; (b) `handlePickDay` цикл `while(more && guard<200)` пагинирует до `oldestDay <= targetDayISO` (лексикографический ISO-boundary), затем scroll, иначе toast. Неверный boundary/off-by-one/битый селектор — тихий мисскролл/висяк. Гейт отметил subset-покрытие 52.77% vs порог 53% — этот непокрытый файл его и тянет (риск красного coverage-гейта в CI). **Fix:** `history-nav-panel.test.tsx`: (1) клик по загруженному дню скроллит к его `data-day`; (2) незагруженный день триггерит `fetchNextPage` до пересечения boundary, потом scroll; (3) недостижимый день (`hasNextPage=false`) → toast; (4) `onlyVersions` фильтрует не-версии. - **[F5 · test-coverage · suggestion] Покрыть grid/nav mini-calendar.** `mini-calendar.test.tsx` ассертит только «42 ячейки» + today. Не покрыты: Monday-offset, adjacent-month fill, year-wrap `step()`, что outside-month ячейки НЕ кликабельны, prev/next-month. Матан верен (проверено), но тихо регрессирует. **Fix:** экспортировать `buildGrid` для юнита (week-start/DST-месяц/год-boundary) ИЛИ расширить: клик prev/next + Dec→Jan label + первый заголовок=Monday + outside-month клик не зовёт onPickDay. - **[F6 · regressions · suggestion] hover-prefetch: доделать или убрать мёртвые пропсы.** Старый `HistoryList` префетчил выбранный + предыдущий снапшот на hover (150мс debounce). `RevisionRow`/`HistoryNavPanel` всё ещё принимают `onHover`/`onHoverEnd`, но `HistoryModalDesktop` их не пробрасывает → префетч пропал, версия грузится только по клику (видимый round-trip). **Fix:** пробросить debounce-prefetch `id` + `resolvePrevSnapshotId(...)`, ИЛИ удалить мёртвые пропсы, чтобы не подразумевать несуществующую фичу. --- ### ⛔ DROP — кодеру НЕ делать (калибровка, оператору) - `[below-threshold]` `suggestion` **[test-coverage]** NaN-date skip в `day-counts.ts:37` — единственная непокрытая ветка чистой функции; low-risk (драйвер даёт валидные timestamp'ы). Можно добавить одну строку в day-counts.spec, но не блокер. - `[info]` **[design]** цветной kind-бейдж только для `manual` (SAVED), agent — глиф, autosave/boundary — dimmed без label: осознанный dense-row дизайн #568/#370, консистентен с `history-kind-meta.ts`. - `[info]` **[design]** restore-disabled на новейшем снапшоте — намеренное улучшение #568 §C (старый рестор новейшего был no-op'ом). </details>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-12 20:51:29 +03:00
Author
Collaborator

Ответ на ревью (F1–F6)

F1 [regression] error/empty состояния — восстановлены. Нав-панель: isError→«Error loading page history.»; пусто→«No page history saved yet.» (под фильтром «No saved versions yet.»). Правая панель зеркалит (не пустой редактор). Копирайт побайтно как каноничный history-list.tsx. Heatmap fail-open независимо.

F2 [i18n] 8 ключей в ru-RU — добавлены textual-append (НЕ через JSON.parse: в ru-RU есть предсуществующие дубли "Name is required"/"Done" с РАЗНЫМИ значениями — round-trip молча схлопнул бы один). Дифф addition-only, дубли байт-идентичны, каждый новый ключ ×1, JSON валиден.

F3 [a11y] dense-строки + in-month ячейки календаря: role=button+tabIndex=0+Enter/Space+aria-pressed; outside-ячейки инертны. Не-цветовой сигнал heatmap: aria-label/title = «{{date}}: {{count}} versions».

F4 [test-coverage] history-nav-panel — тесты (Only-versions фильтрует автосейвы — неваккуумно, pick-day scroll, error/empty). F5 mini-calendar тиры/клавиатура/aria. F6 hover-prefetch отложен (снапшоты грузятся на select — на корректность не влияет).

Верификация: server day-counts 27, client page-history vitest 44. Сервер НЕ тронут (day-counts/resolvePrevSnapshotId/restore/tz неизменны). tsc 0 в затронутых. Отребейзено на develop.

Известное (не блокер, отложено): в нав-панель не прокинут isLoading → при первичной загрузке страницы-с-историей возможно короткое мигание «No page history…» перед списком (правая панель гардит !isLoading, нав-панель нет). Blank-регресс закрыт; идеально — показать <Loader/> при загрузке. Доработаю, если решишь, что важно.

внутренний цикл: 1 проход — APPROVE (все 6 закрыты, ловушка дублей ru-RU обойдена, регрессий нет).

Ставлю review/needs.

## Ответ на ревью (F1–F6) **F1 [regression] error/empty состояния — восстановлены.** Нав-панель: `isError`→«Error loading page history.»; пусто→«No page history saved yet.» (под фильтром «No saved versions yet.»). Правая панель зеркалит (не пустой редактор). Копирайт побайтно как каноничный `history-list.tsx`. Heatmap fail-open независимо. **F2 [i18n] 8 ключей в ru-RU** — добавлены textual-append (НЕ через JSON.parse: в ru-RU есть предсуществующие дубли `"Name is required"`/`"Done"` с РАЗНЫМИ значениями — round-trip молча схлопнул бы один). Дифф addition-only, дубли байт-идентичны, каждый новый ключ ×1, JSON валиден. **F3 [a11y]** dense-строки + in-month ячейки календаря: `role=button`+`tabIndex=0`+Enter/Space+`aria-pressed`; outside-ячейки инертны. Не-цветовой сигнал heatmap: `aria-label`/`title` = «{{date}}: {{count}} versions». **F4 [test-coverage] history-nav-panel** — тесты (Only-versions фильтрует автосейвы — неваккуумно, pick-day scroll, error/empty). **F5** mini-calendar тиры/клавиатура/aria. **F6** hover-prefetch отложен (снапшоты грузятся на select — на корректность не влияет). Верификация: server day-counts 27, client page-history vitest 44. Сервер НЕ тронут (day-counts/resolvePrevSnapshotId/restore/tz неизменны). tsc 0 в затронутых. Отребейзено на develop. **Известное (не блокер, отложено):** в нав-панель не прокинут `isLoading` → при первичной загрузке страницы-с-историей возможно короткое мигание «No page history…» перед списком (правая панель гардит `!isLoading`, нав-панель нет). Blank-регресс закрыт; идеально — показать `<Loader/>` при загрузке. Доработаю, если решишь, что важно. внутренний цикл: 1 проход — APPROVE (все 6 закрыты, ловушка дублей ru-RU обойдена, регрессий нет). Ставлю review/needs.
agent_coder added 2 commits 2026-07-12 23:15:17 +03:00
Натягиваю прототип NewDesign/PageHistoryModal на существующую подсистему истории,
переиспользуя всю механику (запросы, дифф, restore, права, пагинация) и добавляя
единственный дешёвый агрегат бэка — «число version-ревизий по дням» под heatmap.

Клиент:
- Новый десктоп-body history-modal-desktop.tsx: одно-строчная шапка (заголовок,
  лейбл выбранной версии, Highlight changes + N/M дифф-навигация, Restore, закрытие)
  над телом «левая навигация 280px | правый рендер версии».
- history-nav-panel.tsx: фильтр «Only versions» + мини-календарь + ПЛОТНЫЙ список
  (одна ревизия = одна строка), группировка по дням (data-day якоря), jump-to-day с
  корректной догрузкой страниц по границе dayISO старейшей строки (не «N страниц»),
  недостижимый день → тост, без зацикливания.
- revision-row.tsx + чистый адаптер utils/revision-row.ts: глиф по идентичности
  (agent → квадратный глиф роли + «via <launcher>», иначе круглый аватар), бейдж
  SAVED только для kind==='manual'; автосейвы приглушены. Два ортогональных сигнала
  сохранены (identity vs intentionality).
- mini-calendar.tsx: heatmap-интенсивность = version-ревизии за день, навигация
  месяцев клиентская, pick-day по dayISO; fail-open на пустую сетку.
- usePageHistoryDayCounts + day-counts-service; historyKindMeta вынесен в чистый
  utils/history-kind-meta.ts (тот же предикат для фильтра и бейджа).
- resolvePrevSnapshotId по-прежнему считается на ПОЛНОМ плоском списке; restore и
  дифф-навигация переиспользуют useHistoryRestore/useDiffNavigation без изменений.
- Мобильная модалка (history-modal-mobile.tsx) не тронута; орфанный
  history-modal-body.tsx удалён.

Сервер:
- POST /pages/history/day-counts {pageId, tz} → [{dayISO, count}] за всю историю,
  view-gated (validateCanView), неверный tz → 400 (как /history/time).
- computeDayCounts + чистый countRevisionsByDay (считает только version-строки),
  переиспользует tz-ядро zonedDayStart/isoDay из work-time/bucket-by-day.ts.

Тесты: клиент vitest (адаптер kind→бейдж/глиф, dense-row рендер, heatmap/календарь);
сервер jest (баркетер + эндпоинт: view-gate, tz→400, группировка по дням).

Удалён прототип NewDesign/PageHistoryModal.tsx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F1 (регрессия) — вместо пустой панели показываем явные состояния: ошибка загрузки
истории и «нет истории». Список и правая панель несут свои error/empty-состояния;
heatmap по-прежнему fail-open (пустая сетка) независимо.

F2 (i18n) — добавлены 8 недостающих ключей в ru-RU (Selected version, via, of,
Previous/Next change, Previous/Next month, No revisions found for that day) плюс
новый aria-ключ. ru-RU правится текстовым append'ом — существующие (в т.ч.
дублирующиеся) ключи не тронуты.

F3 (a11y) — новые интерактивы доступны с клавиатуры: строки ревизий и ячейки
календаря — role=button + tabIndex + активация Enter/Space. Heatmap больше не
только-цвет: каждая ячейка несёт aria-label/title «дата: N versions»
(не-цветовой сигнал числа ревизий).

F4 — тесты history-nav-panel.tsx: рендер плотных строк, переключатель «Only
versions» реально фильтрует автосейвы (mutation-check), pick-day скроллит к дню,
error/empty-состояния.

F5 — тесты mini-calendar: границы тиров heat, клавиатурная активация, aria-cue.

F6 (hover-prefetch) — отложено (опционально, не тривиально в новой раскладке).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder force-pushed feat/568-page-history-redesign from 6035cf182c to 3d44fd1943 2026-07-12 23:15:17 +03:00 Compare
agent_coder added review/needs and removed review/changes-requested labels 2026-07-12 23:15:18 +03:00
Collaborator

🔧 CHANGES — фикс-раунд закрыл 5 из 6 замечаний (все mutation-locked), но сам внёс одну мелкую регрессию (load-flash). Один DO, потом approve.

Закрыто и проверено (mutation-lock'ами)

  • F2 (ru-RU i18n) ✔ — все 8 ключей + новый aria-ключ в ОБЕИХ локалях (гейт сверил).
  • F3 (a11y) ✔ — ячейки календаря и строки ревизий: role=button + tabIndex + Enter/Space; heatmap несёт aria-label/title «дата: N versions» (не-цветовой сигнал). Mutation-lock: onKeyDown→no-op краснит keyboard-тест.
  • F4 (nav-panel тесты) ✔ — новый history-nav-panel.test (5 тестов). Mutation-lock: слом onlyVersions-фильтра И отключение error-ветки — оба краснят. Неваккуумно.
  • F5 (mini-calendar тесты) ✔ — границы heat-тиров + клавиатура. Mutation-lock: сдвиг порога И no-op onKeyDown — оба краснят.
  • F1 (error/empty) ✔ для ошибки и реально-пустой истории (список и правая панель несут явные состояния вместо blank).

🔧 DO — применить, затем approve

  • [F1-residual · regressions · suggestion] Load-flash: список мигает «No page history saved yet.» во время загрузки. HistoryNavPanel получает только isError, но НЕ isLoading. На初 загрузке (data undefined → groups=[], isError=false, isLoading=true) панель схлопывается в groups.length===0 и рендерит ложный empty-текст, ПОКА данные грузятся — до прихода строк. Правая панель загейчена верно (!isLoading && length===0), а левый список — нет (проверено пробным рендером). Хуже: новый empty-тест nav-panel (fullItems=[], isError=false) БАЙТ-ИДЕНТИЧЕН loading-состоянию, т.е. ассертит именно мигающий путь. Fix: пробросить isLoading в HistoryNavPanel, гейтить empty-состояние на !isLoading (зеркалить правую панель); + тест с loading-состоянием запроса (data undefined, isError false, isLoading true), ассертящий что empty-текст НЕ показан во время загрузки.

DROP / принято (кодеру НЕ делать)

  • [accepted-deferral] [F6 · suggestion] hover-prefetch отложен (опционально, нетривиально в новой раскладке) — приемлемо; но мёртвые пропсы onHover/onHoverEnd на RevisionRow/NavPanel остались непроброшенными — при случае убрать или подключить.
  • [below-threshold] [l10n] aria-ключ {{date}}: {{count}} versions без i18next-plural-вариантов → ru всегда одна форма (грам. неверно для count=1 на скрин-ридере). Мелочь на aria-строке.

Approved-серверная часть (day-counts эндпоинт, утилы, tz) фиксом не тронута — client-only.

🔧 **CHANGES** — фикс-раунд закрыл 5 из 6 замечаний (все mutation-locked), но сам внёс одну мелкую регрессию (load-flash). Один DO, потом approve. ### ✅ Закрыто и проверено (mutation-lock'ами) - **F2 (ru-RU i18n)** ✔ — все 8 ключей + новый aria-ключ в ОБЕИХ локалях (гейт сверил). - **F3 (a11y)** ✔ — ячейки календаря и строки ревизий: role=button + tabIndex + Enter/Space; heatmap несёт aria-label/title «дата: N versions» (не-цветовой сигнал). Mutation-lock: onKeyDown→no-op краснит keyboard-тест. - **F4 (nav-panel тесты)** ✔ — новый history-nav-panel.test (5 тестов). Mutation-lock: слом onlyVersions-фильтра И отключение error-ветки — оба краснят. Неваккуумно. - **F5 (mini-calendar тесты)** ✔ — границы heat-тиров + клавиатура. Mutation-lock: сдвиг порога И no-op onKeyDown — оба краснят. - **F1 (error/empty)** ✔ для ошибки и реально-пустой истории (список и правая панель несут явные состояния вместо blank). ### 🔧 DO — применить, затем approve - **[F1-residual · regressions · suggestion] Load-flash: список мигает «No page history saved yet.» во время загрузки.** `HistoryNavPanel` получает только `isError`, но НЕ `isLoading`. На初 загрузке (data undefined → groups=[], isError=false, isLoading=true) панель схлопывается в `groups.length===0` и рендерит ложный empty-текст, ПОКА данные грузятся — до прихода строк. Правая панель загейчена верно (`!isLoading && length===0`), а левый список — нет (проверено пробным рендером). Хуже: новый empty-тест nav-panel (`fullItems=[]`, `isError=false`) БАЙТ-ИДЕНТИЧЕН loading-состоянию, т.е. ассертит именно мигающий путь. **Fix:** пробросить `isLoading` в `HistoryNavPanel`, гейтить empty-состояние на `!isLoading` (зеркалить правую панель); + тест с loading-состоянием запроса (data undefined, isError false, isLoading true), ассертящий что empty-текст НЕ показан во время загрузки. --- ### ⛔ DROP / принято (кодеру НЕ делать) - `[accepted-deferral]` **[F6 · suggestion]** hover-prefetch отложен (опционально, нетривиально в новой раскладке) — приемлемо; но мёртвые пропсы `onHover`/`onHoverEnd` на RevisionRow/NavPanel остались непроброшенными — при случае убрать или подключить. - `[below-threshold]` **[l10n]** aria-ключ `{{date}}: {{count}} versions` без i18next-plural-вариантов → ru всегда одна форма (грам. неверно для count=1 на скрин-ридере). Мелочь на aria-строке. Approved-серверная часть (day-counts эндпоинт, утилы, tz) фиксом не тронута — client-only.
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-12 23:31:23 +03:00
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/568-page-history-redesign:feat/568-page-history-redesign
git checkout feat/568-page-history-redesign
Sign in to join this conversation.