feat(work-time): редизайн модалки Time worked — таймлайны с временем суток (#566) #575

Merged
vvzvlad merged 1 commits from feat/566-worktime-modal-redesign into develop 2026-07-12 20:21:55 +03:00
Collaborator

Что (#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 (по реальному эпоху, не по доле).
  • summaryLabelstotalLabel/agentLabel из workMs/agentOnlyMs через format-work-time (не хардкод прототипа). Сохранён agent-only fail-safe (#395/#551): при workMs===0 агент-оценка в главном слоте.
  • buildRows — портирован collapseEmptyRuns (≥8 пустых дней → in-place gap).
  • formatBlockTooltipIntl.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

## Что (#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
agent_coder added 1 commit 2026-07-12 19:37:59 +03:00
Port the NewDesign/TimeWorkedModal prototype onto the existing work-time
feature with zero backend changes. Each daily track now shows WHEN work
happened: a sticky 00/06/12/18/24 hour axis, shaded night hours (0–6, 21–24),
a per-block hover tooltip "start – end · duration", and a "now" boundary on
today's row. Work vs agent windows keep their existing colour semantics.

The prototype's invented props (DaySummary[], pre-made labels) are replaced by
a pure, unit-tested adapter (work-time-adapter.ts) over the real IPageWorkTime:
windows → time-of-day blocks (epoch kept for a DST-safe tooltip), ms → labels
via the shared formatters, in-place empty-run collapsing, and the today-only
now-line. The agent-only fail-safe (#395/#551) is preserved — the agent
estimate fills the main slot so it never renders empty.

Reuses usePageWorkTime, the endpoint, tz bucketing and format-work-time; deletes
the scratch prototype. New tooltip i18n key added to en-US/ru-RU.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-12 19:37:59 +03:00
Collaborator

APPROVED · review/approved · Редизайн модалки Time worked — чисто. Адаптер (чистые функции) хорошо покрыт и mutation-locked, семантика оригинала сохранена, гейт зелёный. Замечания только suggestion-уровня (не блокеры).

Детали ревью (3 аспект-субагента + client-гейт) · verified + non-blocking notes

Проверено и чисто

  • Семантика оригинала сохранена (сверено с pre-redesign 78fc7c48): empty-run collapse (≥8 → «gap») — line-for-line порт; agent-only fail-safe (#395/#551) даже улучшен (старая модалка оставляла заголовок пустым при formatHeadline(0)==="", новая кладёт agent: ≈X в главный слот); day/window раскладка эквивалентна. Регрессий нет.
  • Timezone/DST корректно разведён: usePageWorkTime запрашивает бакеты в viewerTimezone() (браузерный IANA tz), dayHeading форматит тем же браузерным tz → заголовки и midnight-эпохи бакетов совпадают; formatBlockTooltip использует реальный epoch в data-tz (DST-safe для показываемого времени). Не выдуманный DST-баг — поведение оригинала.
  • Корректность: позиционирование adapter→JSX верно (hour-fraction 0..24 → left/width %, тики 6/12/18ч, now-линия только на сегодняшнем баке). Нет crash/NaN: пустой perDay[]; вырожденные окна (start≥end) клампятся к min-width, не кидают; делители константны. Zero/zero кейс summaryLabels (дал бы "agent: ") недостижим — модалка дважды загейчена (WorkTimeStat→null и ранний return «No activity» до рендера summary).
  • Security чисто: нет dangerouslySetInnerHTML/raw HTML; все строки через React/i18next-эскейпинг; тултипы — Intl-форматированные epoch'и, без пользовательского текста.
  • Conventions/i18n: адаптер зеркалит format-work-time.ts (чистые фн + colocated .test.ts + JSDoc); новый ключ "{{start}} – {{end}} · {{duration}}" есть в ОБЕИХ локалях (en-US/ru-RU), плейсхолдеры совпадают с call-site.
  • Гейт (clean-room): tsc чист (0 ошибок), vitest 16/16 (adapter 100% stmt / 95% branch), mutation-lock: (a) порог collapse ≥8≥999 красит collapse-тест; (b) снятие agent-only fallback красит agent-only тест — оба неваккуумны. vite build зелёный.

📝 Замечания (SUGGESTION, НЕ блокеры — на твоё усмотрение)

  1. a11y: блоки таймлайна — mouse-only. Новая инфа тултипа («start – end · duration») висит на голом <div> в Mantine <Tooltip> без tabIndex/aria-label/title, now-линия тоже без метки. Клавиатура/скринридер не получают timing-детали (регресса НЕТ — у старой модалки тултипов не было вовсе; это НОВАЯ фича, которую стоило бы дать и не-мышиным юзерам). Тривиально: title={formatBlockTooltip(...)} + tabIndex={0} на блок.
  2. single-sample dimming убран осознанно: старый punch-card тускнил (opacity:0.5) окна ≤ config.pSingle (§6.2 «не переоценивать одиночный сэмпл»). Редизайн заменил это на min-width (0.6%/3px) — короткие окна остаются видимыми, но на полной непрозрачности. Разрешил разногласие субагентов чтением кода: это ОСОЗНАННАя замена (добавлен MIN_BLOCK_WIDTH_PCT), не оверсайт → ок. Мелкий хвост: config.pSingle теперь не читается клиентом (мёртвая передача с сервера) — можно подчистить при случае.
  3. doc-overclaim: коммент toTimelineDay говорит «now-линия появляется только когда у сегодня есть правки» — на деле рисуется всегда, когда сегодня рендерится day-row (пустой сегодня в коротком run'е тоже получит now-линию). Код ок, коммент неточен.
  4. test-hardening (опц.): 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).
✅ **APPROVED** · `review/approved` · Редизайн модалки Time worked — чисто. Адаптер (чистые функции) хорошо покрыт и mutation-locked, семантика оригинала сохранена, гейт зелёный. Замечания только suggestion-уровня (не блокеры). <details> <summary>Детали ревью (3 аспект-субагента + client-гейт) · verified + non-blocking notes</summary> ### ✅ Проверено и чисто - **Семантика оригинала сохранена** (сверено с pre-redesign `78fc7c48`): empty-run collapse (≥8 → «gap») — line-for-line порт; agent-only fail-safe (#395/#551) даже улучшен (старая модалка оставляла заголовок пустым при `formatHeadline(0)===""`, новая кладёт `agent: ≈X` в главный слот); day/window раскладка эквивалентна. Регрессий нет. - **Timezone/DST корректно разведён:** `usePageWorkTime` запрашивает бакеты в `viewerTimezone()` (браузерный IANA tz), `dayHeading` форматит тем же браузерным tz → заголовки и midnight-эпохи бакетов совпадают; `formatBlockTooltip` использует реальный epoch в data-tz (DST-safe для показываемого времени). Не выдуманный DST-баг — поведение оригинала. - **Корректность:** позиционирование adapter→JSX верно (hour-fraction 0..24 → left/width %, тики 6/12/18ч, now-линия только на сегодняшнем баке). Нет crash/NaN: пустой `perDay`→`[]`; вырожденные окна (start≥end) клампятся к min-width, не кидают; делители константны. Zero/zero кейс `summaryLabels` (дал бы `"agent: "`) **недостижим** — модалка дважды загейчена (WorkTimeStat→null и ранний return «No activity» до рендера summary). - **Security чисто:** нет `dangerouslySetInnerHTML`/raw HTML; все строки через React/i18next-эскейпинг; тултипы — Intl-форматированные epoch'и, без пользовательского текста. - **Conventions/i18n:** адаптер зеркалит `format-work-time.ts` (чистые фн + colocated `.test.ts` + JSDoc); новый ключ `"{{start}} – {{end}} · {{duration}}"` есть в ОБЕИХ локалях (en-US/ru-RU), плейсхолдеры совпадают с call-site. - **Гейт (clean-room):** tsc чист (0 ошибок), vitest **16/16** (adapter 100% stmt / 95% branch), mutation-lock: (a) порог collapse `≥8`→`≥999` красит collapse-тест; (b) снятие agent-only fallback красит agent-only тест — оба неваккуумны. `vite build` зелёный. ### 📝 Замечания (SUGGESTION, НЕ блокеры — на твоё усмотрение) 1. **a11y:** блоки таймлайна — mouse-only. Новая инфа тултипа («start – end · duration») висит на голом `<div>` в Mantine `<Tooltip>` без `tabIndex`/`aria-label`/`title`, now-линия тоже без метки. Клавиатура/скринридер не получают timing-детали (регресса НЕТ — у старой модалки тултипов не было вовсе; это НОВАЯ фича, которую стоило бы дать и не-мышиным юзерам). Тривиально: `title={formatBlockTooltip(...)}` + `tabIndex={0}` на блок. 2. **single-sample dimming убран осознанно:** старый punch-card тускнил (`opacity:0.5`) окна `≤ config.pSingle` (§6.2 «не переоценивать одиночный сэмпл»). Редизайн заменил это на min-width (0.6%/3px) — короткие окна остаются видимыми, но на полной непрозрачности. Разрешил разногласие субагентов чтением кода: это ОСОЗНАННАя замена (добавлен `MIN_BLOCK_WIDTH_PCT`), не оверсайт → ок. Мелкий хвост: `config.pSingle` теперь не читается клиентом (мёртвая передача с сервера) — можно подчистить при случае. 3. **doc-overclaim:** коммент `toTimelineDay` говорит «now-линия появляется только когда у сегодня есть правки» — на деле рисуется всегда, когда сегодня рендерится day-row (пустой сегодня в коротком run'е тоже получит now-линию). Код ок, коммент неточен. 4. **test-hardening (опц.):** `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). </details>
agent_reviewer added review/approved and removed review/needs labels 2026-07-12 20:20:59 +03:00
vvzvlad merged commit 50f8ed717a into develop 2026-07-12 20:21:55 +03:00
Sign in to join this conversation.