feat(page-history): редизайн окна истории — плотный список + мини-календарь heatmap (#568) #578
Open
agent_coder
wants to merge 2 commits from
feat/568-page-history-redesign into develop
pull from: feat/568-page-history-redesign
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:develop
vvzvlad:feat/557-apikey-phase3a
vvzvlad:feat/561-comments-redesign
vvzvlad:feat/530-search-semantic
vvzvlad:feat/566-worktime-modal-redesign
vvzvlad:feat/370-agent-save-version
vvzvlad:recover/506-apikey-ui
vvzvlad:feat/370-share-approved
vvzvlad:fix/520-recovery-escalation
vvzvlad:fix/554-literal-br
vvzvlad:feat/529-search-lexical
vvzvlad:feat/501-mcp-apikey-auth
vvzvlad:feat/506-apikeys-ui
vvzvlad:feat/395-work-time
vvzvlad:feat/502-mcp-md-modes
vvzvlad:feat/492-incremental-render
vvzvlad:fix/549-hardbreak-md-canon
vvzvlad:feat/542-resolve-undo
vvzvlad:fix/505-drawio-strip-comments
vvzvlad:fix/503-hardbreak-canon
vvzvlad:docs/471-breadcrumb-permission
vvzvlad:fix/534-mcp-spaceid
vvzvlad:feat/481-version-coherence
vvzvlad:fix/535-list-seam-coalesce
vvzvlad:fix/525-insert-unloaded
vvzvlad:fix/522-internal-links
vvzvlad:fix/507-drawio-cyrillic
vvzvlad:fix/541-reconnect-timeout
vvzvlad:feat/493-converter
vvzvlad:feat/370-page-versioning
vvzvlad:fix/515-code-emphasis
vvzvlad:fix/523-tree-dnd
vvzvlad:fix/517-toasts
vvzvlad:feat/495-tails
vvzvlad:feat/494-mcp-registry-guards
vvzvlad:feat/496-suggestions-audit
vvzvlad:integ/497-wave1
vvzvlad:feat/488-client-fsm
vvzvlad:ci/476-publish-gates
vvzvlad:feat/489-tool-resilience
vvzvlad:feat/487-terminal-lifecycle
vvzvlad:fix/486-iter1-prod-fires
vvzvlad:refactor/347-client-md-paste
vvzvlad:perf/479-getpage-cache
vvzvlad:refactor/449-write-invariants
vvzvlad:refactor/450-split-client
vvzvlad:feat/443-get-page-context
vvzvlad:feat/443-get-tree
vvzvlad:feat/443-search
vvzvlad:docs/415-lossy-descriptions
vvzvlad:feat/413-markdown-default
vvzvlad:refactor/412-camelcase-tool-names
vvzvlad:feat/425-drawio-graph
vvzvlad:fix/464-diffdocs-cpu-guard
vvzvlad:refactor/411-update-page-markdown
vvzvlad:feat/424-drawio-rules
vvzvlad:fix/409-invalid-node-validation
vvzvlad:refactor/448-generate-inventory
vvzvlad:refactor/445-execute-mapping
vvzvlad:refactor/446-derive-client-types
vvzvlad:fix/447-registry-stamp-ci
vvzvlad:fix/444-agent-loop-guards
vvzvlad:fix/396-sendnow-detached-run
vvzvlad:fix/452-immutable-cache-control
vvzvlad:docs/footnote-authoring-comment-cleanup
vvzvlad:feat/437-error-diagnostics
vvzvlad:fix/442-cursor-pagination
vvzvlad:feat/419-footnote-normalize
vvzvlad:perf/399-comment-resolve-async
vvzvlad:perf/435-collab-token-cache
vvzvlad:perf/344-background-rerenders
vvzvlad:perf/348-backend-lowhanging
vvzvlad:feat/423-drawio-crud
vvzvlad:perf/401-collab-hotpath
vvzvlad:feat/430-live-reconnect
vvzvlad:perf/400-collab-session
vvzvlad:refactor/414-dedup-node-ops
vvzvlad:feat/417-new-comments-signal
vvzvlad:feat/408-createcomment-hints
vvzvlad:feat/407-persist-tool-errors
vvzvlad:feat/420-footnote-render
vvzvlad:dummy-review/mcp-hang-fix
vvzvlad:dummy-review/mcp-hang-base
vvzvlad:feat/381-resumable-sse-pr1
vvzvlad:feat/git-sync-2
vvzvlad:docs/agents-workspace-build-order
vvzvlad:feature/offline-sync
vvzvlad:perf/342-code-splitting
vvzvlad:perf/346-compression-cache
vvzvlad:feat/196-multi-cursor
vvzvlad:perf/343-typing-latency
vvzvlad:docs/how-to-test
vvzvlad:fix/ai-sdk-partial-output-oom
vvzvlad:test/351-generative-converter
vvzvlad:feat/371-roles-catalog
vvzvlad:refactor/345-server-converter
vvzvlad:refactor/294-spec-registry-cont
vvzvlad:fix/363-migration-order
vvzvlad:fix/e2e-callout-and-gate-build
vvzvlad:fix/docker-re2-toolchain
vvzvlad:feat/git-sync
vvzvlad:fix/media-roundtrip-stability
vvzvlad:fix/340-comment-panel-perf
vvzvlad:fix/332-deferred-tools
vvzvlad:fix/329-ephemeral-suggestions
vvzvlad:fix/330-search-in-page
vvzvlad:fix/328-resolved-anchor-spam
vvzvlad:fix/331-intraline-diff
vvzvlad:fix/324-coverage-gate
vvzvlad:fix/325-mobile-390
vvzvlad:feat/293-A-git-sync-package
vvzvlad:feat/300-avatar-oklch
vvzvlad:fix/321-banner-mobile
vvzvlad:feat/300-avatar-colors
vvzvlad:feat/315-comment-suggestions
vvzvlad:feat/scroll-restore-stable-wait
vvzvlad:feat/300-agent-avatar-stack
vvzvlad:feat/300-avatar-polish
vvzvlad:refactor/294-tool-spec-registry
vvzvlad:feat/scroll-restore-ux
vvzvlad:fix/responsive-tablet-sidebar
vvzvlad:feature/ai-chat-page-change-observability
vvzvlad:image-inline-center
vvzvlad:fix/283-short-remap-title
vvzvlad:fix/283-slash-layout
vvzvlad:image-inline-row
vvzvlad:feat/276-ai-chat-dock
vvzvlad:fix/269-table-menu-refocus
vvzvlad:docs/dev-stand-guide
vvzvlad:feat/266-scroll-position
vvzvlad:fix/260-collab-docname-slugid
vvzvlad:test/244-phase2-tail
vvzvlad:fix/262-reindex-progress-realtime
vvzvlad:fix/258-changelog-compare-links
vvzvlad:fix/244-dataloss-bugs
vvzvlad:feat/246-spoiler
vvzvlad:feat/221-image-captions
vvzvlad:test/244-part-b
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
возможно, баг
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Похоже на баг, но требует подтверждения/воспроизведения; чиним, если повторится
No Label
review/changes-requested
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#578
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "feat/568-page-history-redesign"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Что (#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 + CASLManage,Page); задизейблен для новейшего снапшота и при!canRestore. Серверного restore-API для истории не выдумано.resolvePrevSnapshotId— на ПОЛНОМ плоском списке (группировка презентационна), иначе дифф ломается.useDiffNavigation— ref правой панели.manual; agent-идентичность (глиф + launcher) ортогональна kind; автосейвы диммятся.historyKindMetaвынесен в чистый модуль, переиспользован badge'ом и фильтром «Only versions».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
🔧 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
✅ Проверено и ЧИСТО
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-тест (гейт реально бьёт).viewerTimezone()(=зона группировки списка), serverisoDayи clientisoDayInTzдают идентичный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.tstz-aware Today/Yesterday, edge-инпуты обработаны. Регресс-аудит: список/select/restore(+confirm)/diff/highlight/N-of-M nav/infinite-scroll/«Only versions» — сохранены.vite buildзелёный.🔧 DO — применить, затем re-review
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.". НовыйHistoryNavPanelempty-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(ключи уже есть).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→Предыдущий/Следующий месяц, и т.д.).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-состояние.history-nav-panel.tsx(+196) без единого теста — самый рисковый клиентский путь: (a)scrollToDayjoin ячейка↔строка через[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фильтрует не-версии.mini-calendar.test.tsxассертит только «42 ячейки» + today. Не покрыты: Monday-offset, adjacent-month fill, year-wrapstep(), что outside-month ячейки НЕ кликабельны, prev/next-month. Матан верен (проверено), но тихо регрессирует. Fix: экспортироватьbuildGridдля юнита (week-start/DST-месяц/год-boundary) ИЛИ расширить: клик prev/next + Dec→Jan label + первый заголовок=Monday + outside-month клик не зовёт onPickDay.HistoryListпрефетчил выбранный + предыдущий снапшот на hover (150мс debounce).RevisionRow/HistoryNavPanelвсё ещё принимаютonHover/onHoverEnd, ноHistoryModalDesktopих не пробрасывает → префетч пропал, версия грузится только по клику (видимый round-trip). Fix: пробросить debounce-prefetchid+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'ом).Ответ на ревью (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.
Натягиваю прототип 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>6035cf182cto3d44fd1943🔧 CHANGES — фикс-раунд закрыл 5 из 6 замечаний (все mutation-locked), но сам внёс одну мелкую регрессию (load-flash). Один DO, потом approve.
✅ Закрыто и проверено (mutation-lock'ами)
🔧 DO — применить, затем approve
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.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.