feat(deploy): version-coherence — WS-анонс версии + guarded reload устаревших вкладок (#481) #499
Open
agent_coder
wants to merge 3 commits from
feat/481-version-coherence into develop
pull from: feat/481-version-coherence
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:ci/476-publish-gates
vvzvlad:feat/488-client-fsm
vvzvlad:feat/489-tool-resilience
vvzvlad:feat/487-terminal-lifecycle
vvzvlad:fix/486-iter1-prod-fires
vvzvlad:develop
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:feat/370-page-versioning
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
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
03c91dfb0b |
docs(deploy): поправить устаревший коммент про auto-reload скрытой вкладки под вариант C (#481, ревью F1)
r1-коммент утверждал «auto-reload on a hidden tab», но вариант C (r2) убрал авто-reload скрытой вкладки ради защиты несохранённого ввода. Переписал под фактическое поведение: баннер + отложенный reload на следующей in-app навигации. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
83f792ca39 |
fix(deploy): variant C — reload по in-app навигации вместо visibilitychange + лог-след + CHANGELOG (#481, ревью)
Правки по ревью #499. Владелец выбрал вариант C по вопросу потери несохранённого ввода. F1 (reload по навигации): убрал ветку visibilitychange→reload И немедленный reload скрытой-при-получении вкладки — visibility больше не влияет на авто-reload вообще. На реальном mismatch: баннер (кнопка «Обновить» — немедленный reload под тем же one-shot гардом) + модульный флаг pendingNavReload. Хук useVersionReloadOnNavigation (useLocation + useEffect по location.key, пропуск первого рендера через useRef; react-router v7 BrowserRouter компонентный, объекта роутера нет — подписка изнутри дерева) на СЛЕДУЮЩЕЙ навигации зовёт consumeNavigationReload → performAutoReload (mark-перед-reload, при spent/непишущемся storage — только баннер). Скрытая вкладка идёт тем же путём (C единообразно). chunk-load-error-boundary reload не тронут — остаётся бэкапом для не-навигирующей вкладки. Так reload в безопасной точке (юзер и так уходит со страницы), а не когда свернул с недописанным комментарием. F2 (лог-след): общий recordReloadBreadcrumb/takeReloadBreadcrumb (sessionStorage, переживает reload). performAutoReload пишет breadcrumb {path:proactive,server, client} + console.warn перед reload; chunk-boundary пишет {path:chunk-boundary}; surfacePreviousReloadBreadcrumb на маунте UserProvider логирует прошлый след. F3 (CHANGELOG + AGENTS.md): user-facing запись про version-coherence + строка про артефакт client/dist/version.json. Тесты: vitest version-coherence+reload-guard+guarded-reload 23 (переписан на MemoryRouter+useNavigate: reload РОВНО раз на первой навигации после mismatch, НЕ на 2-й навигации, НЕ на 2-м mismatch one-shot, НЕ от ухода в фон; скрытая вкладка — только на навигации; Update — сразу; banner-only/write-fail — никогда). Шире: features/user 34. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1e4925007d |
feat(deploy): version-coherence — WS-анонс версии сервера + guarded reload устаревших вкладок (#481)
SPA — долгоживущий клиент: вкладку держат часами, сервер за это время передеплоивают. Старый index.html ссылается на content-hashed чанки, которых в новом образе нет → ленивый import() → catch-all отдаёт index.html как text/html → WebKit «not a valid JavaScript MIME type» (наблюдали в проде). Реактивный предохранитель (chunk-error-boundary) ловит УЖЕ случившуюся ошибку. Делаем сигнал ПРОАКТИВНЫМ: сервер сообщает версию по существующему WS, клиент сравнивает и делает guarded reload ДО битого чанка. closes #481 - Единый источник версии: vite.config.ts вычисляет resolveAppVersion РОВНО раз → и в define.APP_VERSION, и в плагине, пишущем dist/version.json. Бандл и файл тождественны by construction. Сервер читает client/dist/version.json при старте (client-version.ts: resolveClientDistPath вынесен из static.module — единый источник пути; readClientBuildVersion → версия или '' на любой ошибке, fail-safe). НИКАКИХ Docker/ENV-изменений. - WS: отдельное событие app-version {version} (НЕ в message/WebSocketEvent — член без operation сломал бы union). ws.gateway читает версию в onModuleInit + boot-лог ACTIVE/DISABLED; emit в handleConnection (success-ветка, после join). ТОЛЬКО per-connect анонс, БЕЗ broadcast (broadcast в кластере → thundering-herd reload флота; естественный реконнект покрывает и single-container, и кластер). - Клиент: listener навешан СИНХРОННО в том же useEffect, что создаёт сокет (до connect — иначе гонка с немедленным emit сервера). Чистая decideVersionAction (reload|banner|noop; пустая версия → noop fail-safe). Грязная оболочка triggerGuardedReload: модульный latch, hidden→немедленный reload, visible→баннер + reload-on-hidden {once}, фикс-id закрываемый баннер с кнопкой «Обновить». - Общий one-shot флаг: reload-guard.ts (hasAutoReloaded/markAutoReloaded над существующим chunk-reload-attempted); chunk-load-error-boundary переведён на него. Проактивный + реактивный reload делят ОДИН счётчик → максимум одна авто-перезагрузка за сессию суммарно; permanent skew / oscillation / disabled storage → после первого reload только баннер, петли нет (проверено трассировкой). Проверка: server jest client-version 7/7; client vitest version-coherence+ reload-guard+guarded-reload 16/16 (coverage 97.9%). Build-proof единого источника: APP_VERSION=test-A → dist/version.json {"version":"test-A"} + вшито в бандл. Полный staging-приём (docker build-arg, WS-кадры в DevTools, мульти-таб) — вне автостенда, шаги приёмки #481 (крит.1-4) на ручную проверку. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |