fix: render admin timestamps in the viewer's timezone (#1041) - #1043
Conversation
The audit log on hugo showed every event three hours in the future. Not corrupt data — the DB stores UTC, which is right — but the templates interpolated `DateTime<Utc>` straight into the page, and chrono's `format` prints the wall clock OF THE VALUE. A UTC−3 operator therefore read UTC. Five screens carried the same defect: audit, user activity, apps (updated at), credentials and users. The date-only columns hid it except between 21h and midnight local, which is why only the audit log — seconds precision, recent events — got reported. Fixed client-side rather than with a server timezone setting: box and hugo would each need configuring, and operators can sit in different zones anyway. Each cell now ships as `<time datetime="…Z" data-rk-time="pattern">` with the UTC text kept as the no-JS fallback, and a small renderer in the admin layout rewrites it into the viewer's zone. The patterns mirror the chrono formats they replace, so no screen changes shape — only the hour is now right. Hovering shows the full date plus the zone name, in the page's locale. Bonus fix in the same pass: the renderer stamps `data-sort-value` (epoch ms) on the cell, so the table enhancer sorts the date column by instant. It used to strip the separators and read `29/07/2026` as one number, i.e. sort by day-of-month. Verified live against a local instance on an America/Recife machine (the same offset as hugo) driving the installed Chrome: a seeded 13:00Z row renders 10:00:00, and a 2026-07-30T01:00:00Z credential renders 29/07/2026 — the date-boundary case the old code got a full day wrong. All 5 screens convert; 46 cells checked, none skipped. Two lints keep it from regressing: a bare timestamp format in a template without the `<time>` wrapper fails, and so does dropping the renderer from the layout. Plus a render test asserting the activity page emits a real RFC-3339 instant in the attribute. The scheduler evaluating cron in UTC is the same gap with an operational consequence, tracked separately in #1042. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reviewing my own PR turned up a sixth screen with the same defect, which #1041's survey missed: it grepped for chrono `format` calls in templates, and the Logs tab builds its timestamp in JavaScript. `/admin/logs` takes the tracing stream's ISO timestamp, strips the date and the trailing Z, and prints the remaining `HH:MM:SS.mmm` — i.e. UTC, three hours ahead for a UTC−3 operator, exactly like the audit table. Now parsed into a Date and rendered in the viewer's zone, keeping the millisecond precision the design asks for. The raw UTC token moves to the cell's `title`, so correlating a line with the downloaded log file (a server endpoint, still UTC — as a log file should be) stays possible. An unparseable first field falls back to the old string stripping rather than blanking the column. Verified live: a line logged at 17:38:06.973Z renders 14:38:06.973 with the full UTC instant on hover. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Auto-review (autor = revisor, então com rigor extra)Passei o diff procurando o que ele quebra. Três verificações e um achado real. Achado real, corrigido em Verificações que passaram:
Gate local: clippy |
Closes #1041.
O bug
Reportado no hugo: a aba Auditoria mostrava tudo 3 horas adiantado.
O banco guarda UTC (correto). O que faltava era conversão na renderização —
chrono::DateTime::formatimprime o relógio do próprio valor, então umDateTime<Utc>interpolado direto no template sai em UTC. Para quem está em UTC−3, o futuro.Cinco telas tinham o mesmo defeito: Auditoria, Atividades dos usuários, Aplicações (atualizado em), Credenciais e Usuários. Nas colunas só-data o erro só aparecia entre 21h e meia-noite locais — por isso só a auditoria (com segundos, em eventos recentes) foi notada.
A correção
No cliente, não por configuração de fuso no servidor: box e hugo precisariam ser configurados um a um, e operadores podem estar em fusos diferentes de qualquer forma.
<time datetime="…Z" data-rk-time="<padrão>">, com o texto UTC preservado como fallback sem JS.admin/_layout.htmlreescreve para o fuso do navegador. Os padrões espelham os formatos chrono que substituem (dd/mm/aaaa hh:mm:ss,dd/mm/aaaa hh:mm,dd/mm/aaaa,aaaa-mm-dd) — nenhuma tela muda de forma, só a hora fica certa.De brinde: o renderizador grava
data-sort-value(epoch) na célula, consertando a ordenação da coluna de data — o enhancer de tabelas tirava os separadores e lia29/07/2026como um número só, ou seja, ordenava por dia do mês.Verificação ao vivo
Instância local numa máquina em
America/Recife(mesmo offset do hugo), Playwright dirigindo o Chrome instalado:datetime-s2026-07-29T13:00:00Z29/07/2026 10:00:00datetime2026-07-29T13:00:00Z29/07/2026 10:00date-iso2026-07-29T13:00:00Z2026-07-29date2026-07-30T01:00:00Z29/07/2026A última linha é o caso de virada de dia: o código antigo mostrava
30/07/2026, um dia inteiro errado. 46 células conferidas nas 5 telas, todas convertidas, nenhuma pulada.Regressão
template_lint: um timestamp formatado sem o wrapper<time data-rk-time>quebra o teste.template_lint: some o renderizador do layout, quebra também (o par attributo+script é o que faz a coisa funcionar).activity_ui: a página renderizada tem que emitir um instante RFC-3339 de verdade no atributo, não o texto pt-BR.Gate:
cargo testverde,cargo clippy --all-targets -- -D warningslimpo,i18n-checkOK.Fora do escopo
jobs.rs:118); um0 8 * * *dispara às 05h locais. Mesmo buraco, consequência operacional, e não dá para resolver no navegador. Precisa de decisão de config (server.timezone).UTC(schedules_ui.rs:114,routes/admin/users.rs:374,routes/admin/mfa.rs:198) — honestos hoje, mas inconsistentes com estas telas; alinhar junto do Agendador de cronjobs avalia expressões cron em UTC — jobs disparam fora do horário local esperado #1042.Gotcha reconfirmada
Editar só um
.htmlnão rebuildaruscker-admin— o smoke rodou contra o binário velho e o tooltip saiu no idioma errado até eu dartouchnolib.rs. O de sempre, registrado no CLAUDE.md.🤖 Generated with Claude Code