feat(work-time): интеграция редизайна модалки «Time worked» — таймлайны с временем суток (клиент, grid) #566
Closed
opened 2026-07-12 19:04:58 +03:00 by agent_vscode
·
0 comments
No Branch/Tag Specified
main
develop
feat/557-apikey-phase3a
feat/568-page-history-redesign
feat/561-comments-redesign
feat/530-search-semantic
feat/566-worktime-modal-redesign
feat/370-agent-save-version
recover/506-apikey-ui
feat/370-share-approved
fix/520-recovery-escalation
fix/554-literal-br
feat/529-search-lexical
feat/501-mcp-apikey-auth
feat/506-apikeys-ui
feat/395-work-time
feat/502-mcp-md-modes
feat/492-incremental-render
fix/549-hardbreak-md-canon
feat/542-resolve-undo
fix/505-drawio-strip-comments
fix/503-hardbreak-canon
docs/471-breadcrumb-permission
fix/534-mcp-spaceid
feat/481-version-coherence
fix/535-list-seam-coalesce
fix/525-insert-unloaded
fix/522-internal-links
fix/507-drawio-cyrillic
fix/541-reconnect-timeout
feat/493-converter
feat/370-page-versioning
fix/515-code-emphasis
fix/523-tree-dnd
fix/517-toasts
feat/495-tails
feat/494-mcp-registry-guards
feat/496-suggestions-audit
integ/497-wave1
feat/488-client-fsm
ci/476-publish-gates
feat/489-tool-resilience
feat/487-terminal-lifecycle
fix/486-iter1-prod-fires
refactor/347-client-md-paste
perf/479-getpage-cache
refactor/449-write-invariants
refactor/450-split-client
feat/443-get-page-context
feat/443-get-tree
feat/443-search
docs/415-lossy-descriptions
feat/413-markdown-default
refactor/412-camelcase-tool-names
feat/425-drawio-graph
fix/464-diffdocs-cpu-guard
refactor/411-update-page-markdown
feat/424-drawio-rules
fix/409-invalid-node-validation
refactor/448-generate-inventory
refactor/445-execute-mapping
refactor/446-derive-client-types
fix/447-registry-stamp-ci
fix/444-agent-loop-guards
fix/396-sendnow-detached-run
fix/452-immutable-cache-control
docs/footnote-authoring-comment-cleanup
feat/437-error-diagnostics
fix/442-cursor-pagination
feat/419-footnote-normalize
perf/399-comment-resolve-async
perf/435-collab-token-cache
perf/344-background-rerenders
perf/348-backend-lowhanging
feat/423-drawio-crud
perf/401-collab-hotpath
feat/430-live-reconnect
perf/400-collab-session
refactor/414-dedup-node-ops
feat/417-new-comments-signal
feat/408-createcomment-hints
feat/407-persist-tool-errors
feat/420-footnote-render
dummy-review/mcp-hang-fix
dummy-review/mcp-hang-base
feat/381-resumable-sse-pr1
feat/git-sync-2
docs/agents-workspace-build-order
feature/offline-sync
perf/342-code-splitting
perf/346-compression-cache
feat/196-multi-cursor
perf/343-typing-latency
docs/how-to-test
fix/ai-sdk-partial-output-oom
test/351-generative-converter
feat/371-roles-catalog
refactor/345-server-converter
refactor/294-spec-registry-cont
fix/363-migration-order
fix/e2e-callout-and-gate-build
fix/docker-re2-toolchain
feat/git-sync
fix/media-roundtrip-stability
fix/340-comment-panel-perf
fix/332-deferred-tools
fix/329-ephemeral-suggestions
fix/330-search-in-page
fix/328-resolved-anchor-spam
fix/331-intraline-diff
fix/324-coverage-gate
fix/325-mobile-390
feat/293-A-git-sync-package
feat/300-avatar-oklch
fix/321-banner-mobile
feat/300-avatar-colors
feat/315-comment-suggestions
feat/scroll-restore-stable-wait
feat/300-agent-avatar-stack
feat/300-avatar-polish
refactor/294-tool-spec-registry
feat/scroll-restore-ux
fix/responsive-tablet-sidebar
feature/ai-chat-page-change-observability
image-inline-center
fix/283-short-remap-title
fix/283-slash-layout
image-inline-row
feat/276-ai-chat-dock
fix/269-table-menu-refocus
docs/dev-stand-guide
feat/266-scroll-position
fix/260-collab-docname-slugid
test/244-phase2-tail
fix/262-reindex-progress-realtime
fix/258-changelog-compare-links
fix/244-dataloss-bugs
feat/246-spoiler
feat/221-image-captions
test/244-part-b
feat/251-intentional-clear
fix/embeddings-reindex-progress
refactor/193-tool-spec-registry
fix/255-ws-redis-adapter-leak
fix/252-e2e-open-handles
feat/229-catalog-yaml
feat/243-blob-sandbox
feat/228-inline-footnotes
fix/qa-ui-bugs-216-218
feature/agent-roles-catalog
fix/share-alias-rename
fix/ai-chat-empty-render
feat/191-chat-doc-binding
feat/201-temporary-notes
feat/198-interrupt-agent
feat/ai-chat-full-history
feat/199-ai-generate-title
feat/205-share-aliases
batch/issues-189-187-170
feat/170-mcp-test-button
feat/189-context-badge
feat/198-interrupt-agent-send-now
fix/issues-190-159
fix/ai-chat-new-chat-during-stream
fix/ai-chat-stream-perf
batch/issues-2026-06-25
feat/ai-chat-persistent-history
fix/ai-chat-copy-chat-wysiwyg
fix/ai-stream-reset-resilience
fix/ai-stream-undici-timeout
fix/footnote-review-1227-followup
fix/ai-chat-token-counter-realtime
docs/manual-qa-test-plan
v0.94.1
v0.94.0
v0.93.0
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
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#566
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 "%!s()"
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?
Проблема / Цель
Есть готовый прототип редизайна окна «Time worked on this article» —
NewDesign/TimeWorkedModal.tsx(Mantine v7, светлая/тёмная тема). Главное отличие от текущей версии: на суточных дорожках видно время суток — часовая ось00–06–12–18–24сверху с делениями, ночные часы затемнены, у каждого блока hover-тултип «начало–конец · длительность», а у сегодняшнего дня — граница «сейчас».Нужно натянуть этот визуал на существующую фичу учёта времени с нулём изменений бэкенда: все данные уже отдаёт текущий эндпоинт
POST /pages/history/time. Прототип писался без знания нашей модели данных, поэтому его входные пропсы (days: DaySummary[], заранее готовыеtotalLabel/agentLabel) надо заменить адаптером над реальнымIPageWorkTime.Для кого: авторы/редакторы, которые смотрят, сколько времени и в какие часы шла работа над статьёй (человек и, отдельно, агент).
Контекст и исследование
Фича #395 «time worked», подсистема
work-time:<Modal title="Time worked on this article">с теломWorkTimePunchCard. Виджет скрыт, покаworkMs<=0 && agentOnlyMs<=0; на agent-only странице заголовок показывает агентскую оценку.left=(w.start-day.day)/DAY_MS, деления 6/12/18, цвета work/agent, суточные итоги, и схлопывает прогоны ≥8 пустых дней в строку-разделитель «× N days without edits» (collapseEmptyRuns).IPageWorkTime = {workMs, agentOnlyMs, perDay: IPerDay[], config, tz};IPerDay = {day (epoch-ms локальной полуночи в tz), dayISO, activeMs, agentMs, windows: IDayWindow[]};IDayWindow = {start, end (epoch-ms), class: 'work'|'agent_only'}. Окна клипаются к суткам на сервере (bucket-by-day.ts).tz = viewerTimezone()(браузерная IANA-зона); та же tz уходит на сервер для бакетирования по дням, поэтомуday(epoch-ms локальной полуночи) и браузерное форматирование даты согласованы.formatHeadline(«≈ 1h 24m»,""приworkMs<=0),formatDayTotal(«1h 24m» / «—»),formatGapMinutes. Все берутt().Вывод: данные для прототипа полностью доступны на клиенте — это чисто визуальный оверхол
WorkTimePunchCardи обёртки-модалки. Совпадает с заявлением прототипа «изменений API не требуется».Расхождения прототипа ↔ реальность (закрываются адаптером):
DaySummary[]с готовыми строками; реально приходитIPageWorkTime— маппим и форматируем существующими хелперами.Block.start/end— часовые доли 0..24; реальноwindowsв epoch-ms →(w.start-day.day)/3.6e6.kind: 'work'|'agent'↔class: 'work'|'agent_only'.tz='Europe/Moscow',inactivityGapMin=15захардкожены — берёмdata.tzиformatGapMinutes(data.config.tGap).t().Дизайн
Принцип: контейнер, запрос, эндпоинт, алгоритм — прежние; меняем только визуал тела модалки (и её хром). Ось времени —
grid(решение человека). Бэкенд не трогаем.1. Ось
grid(только она). Порт визуала из TimeWorkedModal.tsx: часовая шкала00/06/12/18/24(липкая), вертикальные деления 25/50/75%, затемнение ночных часов (0–6 и 21–24, как вtrackBgпрототипа), легенда Work/Agent, per-block тултип, граница «сейчас». Веткуaxis='phases'не портируем (вне scope; возможный будущий тумблер).2. Адаптер
IPageWorkTime → входы компонента(заменяет пропсы прототипа):day.windows.map(w => ({ start:(w.start-day.day)/3.6e6, end:(w.end-day.day)/3.6e6, kind: w.class==='work' ? 'work' : 'agent' })); позиционирование/ширина — как в текущем punch-card (left,width, clamp[0,24], мин. видимая ширина 0.6%).formatDayTotal(day.activeMs, t)(«—» при 0).dayHeading(day.day)(browser-tz, согласован с бакетингом).total = workMs>0 ? formatHeadline(workMs,t) : t("agent: {{value}}", {value: formatHeadline(agentOnlyMs,t)}). Вторичную строкуagent: …показываем только когдаworkMs>0 && agentOnlyMs>0(иначе агентская оценка уже в главном слоте). Так большой 22px-слот НИКОГДА не пустой (formatHeadline(0)==="").data.tz,formatGapMinutes(data.config.tGap)в подписи-футере.3. Сохранение схлопывания пустых прогонов (находка критика №2). Контракт входа делаем
rows: (DaySummary | { gap: number })[](или добавляемDaySummary.gap?: number-вариант). Портируем существующийcollapseEmptyRuns(EMPTY_RUN_COLLAPSE = 8): прогон ≥8 пустых дней → строка-разделительt("× {{count}} days without edits", {count})в исходном порядке (не в конец); короткий (<8) прогон пустых дней рендерится полупрозрачными дорожками с итогом «—». Прототипный плоскийdays.mapзаменяем на рендерrowsс веткой gap-строки.4. Граница «сейчас». Помечаем
isTodayтотperDay, у которого[day, day+DAY_MS)содержитDate.now(), сnowFraction=(Date.now()-day.day)/DAY_MS. Если сегодняшнего дня вperDayнет (сегодня правок не было —bucketByDayне добавляет пустой хвост), маркер не показываем. Ложных срабатываний нет (последнийperDay— всегда активный день).5. Тултип блока (находка критика №3 — DST). Позиционирование остаётся по часовой доле
(w.start-day.day)/DAY_MS(как сейчас). Но время в тултипе («начало–конец») форматируем из реального epoch (w.start/w.end) вdata.tzчерезIntl.DateTimeFormat(locale,{hour,minute,timeZone:data.tz}), а НЕ из 24-часовой доли — иначе в дни перевода часов (сутки 23/25ч) тултип покажет время, съехавшее до часа. Длительность блока — из epoch (w.end-w.start), т.е. клипнутая к суткам порция (для посуточной дорожки это корректно).6. i18n. Все видимые строки — через
react-i18nextt()(шапка, легенда, пустое состояние, футер, «× N days», тултип-разделители).Policy-константы:
EMPTY_RUN_COLLAPSE=8(существующая), мин. ширина блока0.6%, затемнение ночных часов0–6и21–24, ширина модалки ~46rem — из прототипа.Затрагиваемые файлы:
work-time-timeline.tsxрядом и переключить импорт вwork-time-stat). СохранитьcollapseEmptyRuns, форматтеры, позиционную математику.Modal(ширеsize, паддинги/шапка по прототипу); логика триггера/useDisclosure/скрытия виджета — без изменений.usePageWorkTime,formatHeadline/formatDayTotal/formatGapMinutes,dayHeading,IPageWorkTime/IPerDay/IDayWindow.NewDesign/TimeWorkedModal.tsx(исходник-прототип; прочие файлы вNewDesign/— для отдельных задач, не трогаем).Границы scope
Не входит:
axis='phases'(цветные полосы фаз) — не портируем; возможный будущий тумблер grid/phases.Критерии приёмки
Проверяемо на стенде (страница с историей правок в разные часы):
00/06/12/18/24, ночные часы затемнены.(w.start-day.day)/DAY_MS).× N days without editsна своём месте в списке; короткий прогон пустых дней — полупрозрачные дорожки с итогом «—».workMs=0, agentOnlyMs>0) открывается, и главный слот заголовка показывает агентскую оценку (НЕ пустой), без дублирующей строкиagent:.data.tz(в DST-переходный день не съезжает).Отказы и наблюдаемость
usePageWorkTime(react-query); состояния загрузки/ошибки/пусто — как сейчас (No time tracked yetприworkMs<=0 && agentOnlyMs<=0). Новых сетевых путей нет.[0,24], мин. ширина) — как в текущем punch-card.Совместимость и миграции
Решённые развилки
phases— возможный будущий тумблер, вне scope.formatHeadline(0)===""иначе даёт пустой 22px-слот — находка критика).collapseEmptyRunsчерезrows: (DaySummary|{gap})[]. Кто решил: анти-регрессия (у прототипа концепции нет).Связи
axis='phases'из прототипа).