feat(work-time): редизайн модалки Time worked — таймлайны с временем суток (#566) #575
Reference in New Issue
Block a user
Delete Branch "feat/566-worktime-modal-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?
Что (#566)
Порт прототипа
NewDesign/TimeWorkedModalна реальную фичу учёта времени. Ноль изменений бэка — данные те же изPOST /pages/history/time. Главное отличие визуала: на суточных дорожках видно ВРЕМЯ СУТОК — часовая ось00–06–12–18–24с делениями, ночные часы затемнены, у каждого блока hover-тултип «начало–конец · длительность», у сегодня — граница «сейчас».Адаптер (crux — пропсы прототипа → реальный
IPageWorkTime)work-time-adapter.ts(новый, чистый, юнит-тестируемый, без React):toBlocks(day)—day.windows→ блоки времени суток: доля часа(w.start − day.day)/(DAY_MS/24), clamp[0,24];class:'work'|'agent_only'→kind; сохраняетstartEpoch/endEpoch→ тултип DST-safe (по реальному эпоху, не по доле).summaryLabels—totalLabel/agentLabelизworkMs/agentOnlyMsчерезformat-work-time(не хардкод прототипа). Сохранён agent-only fail-safe (#395/#551): приworkMs===0агент-оценка в главном слоте.buildRows— портированcollapseEmptyRuns(≥8 пустых дней → in-place gap).formatBlockTooltip—Intl.DateTimeFormatна реальном эпохе вdata.tz.Переиспользованы без изменений:
usePageWorkTime, endpoint/tz-бакетинг (клиент не ре-бакетит),format-work-time.ts, типы, empty/agent-only состояния.work-time-stat.tsx— 1 строка (размер модалки).Известная мелочь (не блокер)
isToday/now-линия используют фикс-24ч (day.day + DAY_MS), тогда как сервер бакетит tz-aware. В 2 дня перехода DST в году граница «сегодня»/позиция now-линии у полуночи может сместиться до ~1ч. Это ровно поведение исходного punch-card; тултип берёт реальный эпох/tz → часы точны. Регрессии нет.Как проверял
tsc 0 (в затронутых файлах); work-time vitest 16 (format-work-time 6 + adapter 10). Мутации: сломать долю часа (
/24) → падаютtoBlocks-тесты; сломать agent-only fail-safe → падает тот тест. Удалён ТОЛЬКОNewDesign/TimeWorkedModal.tsx(прототипы #561/#568 не тронуты). i18n-ключ тултипа в обоих локалях.внутренний цикл: 1 проход — APPROVE (адаптер корректен, DST-safe тултипы по эпоху, agent-only fail-safe сохранён, now-линия только на «сегодня», переиспользуемая логика цела).
closes #566
✅ APPROVED ·
review/approved· Редизайн модалки Time worked — чисто. Адаптер (чистые функции) хорошо покрыт и mutation-locked, семантика оригинала сохранена, гейт зелёный. Замечания только suggestion-уровня (не блокеры).Детали ревью (3 аспект-субагента + client-гейт) · verified + non-blocking notes
✅ Проверено и чисто
78fc7c48): empty-run collapse (≥8 → «gap») — line-for-line порт; agent-only fail-safe (#395/#551) даже улучшен (старая модалка оставляла заголовок пустым приformatHeadline(0)==="", новая кладётagent: ≈Xв главный слот); day/window раскладка эквивалентна. Регрессий нет.usePageWorkTimeзапрашивает бакеты вviewerTimezone()(браузерный IANA tz),dayHeadingформатит тем же браузерным tz → заголовки и midnight-эпохи бакетов совпадают;formatBlockTooltipиспользует реальный epoch в data-tz (DST-safe для показываемого времени). Не выдуманный DST-баг — поведение оригинала.perDay→[]; вырожденные окна (start≥end) клампятся к min-width, не кидают; делители константны. Zero/zero кейсsummaryLabels(дал бы"agent: ") недостижим — модалка дважды загейчена (WorkTimeStat→null и ранний return «No activity» до рендера summary).dangerouslySetInnerHTML/raw HTML; все строки через React/i18next-эскейпинг; тултипы — Intl-форматированные epoch'и, без пользовательского текста.format-work-time.ts(чистые фн + colocated.test.ts+ JSDoc); новый ключ"{{start}} – {{end}} · {{duration}}"есть в ОБЕИХ локалях (en-US/ru-RU), плейсхолдеры совпадают с call-site.≥8→≥999красит collapse-тест; (b) снятие agent-only fallback красит agent-only тест — оба неваккуумны.vite buildзелёный.📝 Замечания (SUGGESTION, НЕ блокеры — на твоё усмотрение)
<div>в Mantine<Tooltip>безtabIndex/aria-label/title, now-линия тоже без метки. Клавиатура/скринридер не получают timing-детали (регресса НЕТ — у старой модалки тултипов не было вовсе; это НОВАЯ фича, которую стоило бы дать и не-мышиным юзерам). Тривиально:title={formatBlockTooltip(...)}+tabIndex={0}на блок.opacity:0.5) окна≤ config.pSingle(§6.2 «не переоценивать одиночный сэмпл»). Редизайн заменил это на min-width (0.6%/3px) — короткие окна остаются видимыми, но на полной непрозрачности. Разрешил разногласие субагентов чтением кода: это ОСОЗНАННАя замена (добавленMIN_BLOCK_WIDTH_PCT), не оверсайт → ок. Мелкий хвост:config.pSingleтеперь не читается клиентом (мёртвая передача с сервера) — можно подчистить при случае.toTimelineDayговорит «now-линия появляется только когда у сегодня есть правки» — на деле рисуется всегда, когда сегодня рендерится day-row (пустой сегодня в коротком run'е тоже получит now-линию). Код ок, коммент неточен.formatBlockTooltipтестится только сtz="UTC"(DST-safety не проверена на реальной DST-зоне); off-by-one сторона collapse (ровно 7 пустых → остаются day-rows) не заассерчена (сторона 8 — да). Покрытие и так сильное.⛔ DROP — кодеру НЕ делать (калибровка, оператору)
[below-threshold]suggestion[a11y] блоки mouse-only — enhancement, не регресс (baseline без тултипов); тривиальный фикс, если решишь дать.[below-threshold]suggestion[regressions] single-sample dimming убран — осознанно (min-width замена), разногласие субагентов разрешено в пользу «ок».[below-threshold]suggestion[documentation] toTimelineDay коммент про «only when today has edits» неточен.[below-threshold]suggestion[test-coverage] non-UTC tooltip + 7-empty off-by-one не покрыты (покрытие 95% branch).