Files
jellybit/openspec/changes/archive/2026-07-04-time-storage-rfc3339/proposal.md
T
avandClaude Opus 4.8 5d5456fa68 Хранение времени: RFC 3339 (UTC) + таймзона отображения в конфиге
Метки времени в SQLite переведены с формата datetime('now')
(«2006-01-02 15:04:05») на RFC 3339 всегда-UTC («2006-01-02T15:04:05Z»):
самоописываемое хранилище (зона в значении), валидный ISO 8601, единый
формат с логами. Фиксированная ширина сохраняет лексикографическую
сортировку TEXT = хронологию (COALESCE(source_added_at, created_at)).

- Единая точка генерации времени в Go: store.Now()/FormatTime; DEFAULT
  (datetime('now')) снят со всех колонок — время всегда пишет приложение
  (зеркально ident.NewID для id), fail-loud при забытой вставке (NOT NULL).
  Все INSERT-сайты в store передают created_at/updated_at явно.
- Миграция 0008 (rebuild 7 таблиц без DEFAULT + backfill strftime, FK/PK/
  индексы сохранены байт-в-байт по образцу 0006); симметричная down.
- Новая секция конфига [general] с полем timezone (дефолт UTC) — зона
  ОТОБРАЖЕНИЯ в веб-UI; хранение остаётся UTC. Жёсткая валидация зоны на
  старте; zoneinfo встроен (time/tzdata), заменён зашитый Europe/Moscow.
- Тесты: round-trip миграции (up/down, NULL source_added_at), валидация
  зоны, сдвиг даты по зоне; обновлены фикстуры и TestUlidMigration.
- Docs: конвенции database/config, ER-схема; спека web-ui (таймзона).

OpenSpec change time-storage-rfc3339 (заархивирован).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:32:07 +03:00

58 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Why
Временные метки в БД хранятся как TEXT в формате `datetime('now')`
`2006-01-02 15:04:05`: пробел вместо `T`, без суффикса зоны. UTC гарантируется
только конвенцией и кодом, но не самим значением — тот, кто откроет базу
напрямую (sqlite3 CLI, дамп, сторонний инструмент), не видит зону, а строка не
является валидным ISO 8601 (footgun для `new Date()` в веб-UI, требует
переформатирования для JSON API / `<time datetime>`). Одновременно зона
отображения зашита в код (`Europe/Moscow`), хотя серверы часто настроены по UTC.
## What Changes
- Формат хранения меток времени переходит на **RFC 3339, всегда UTC с суффиксом
`Z`** (`2006-01-02T15:04:05Z`). Секундная точность сохраняется. Фиксированная
ширина сохраняет лексикографическую сортировку TEXT = хронологию.
- **Единая точка генерации времени в Go** (зеркально принципу «ULID генерит
только `internal/ident`»): `DEFAULT (datetime('now'))` **убирается** со всех
колонок; метки времени всегда пишет приложение через `store.FormatTime`.
- Go-хелперы `FormatTime`/`ParseTime` переводятся на stdlib `time.RFC3339`.
- **BREAKING (хранение):** одноразовая goose-миграция пересобирает таблицы без
`DEFAULT` и backfill-ит существующие строки в новый формат. Backfill + смена
схемы + смена кода — один change, чтобы не возникло окна со смешанными
форматами (иначе ломается лексикографическая сортировка `COALESCE`).
- Добавляется секция конфига **`[general]`** (общие настройки) с явной настройкой
**таймзоны отображения** (`timezone`, дефолт `UTC`), валидируемой на старте через
`time.LoadLocation`. Хранение остаётся всегда UTC — настройка влияет только на
рендеринг времени в веб-UI, заменяя зашитый `Europe/Moscow`.
## Capabilities
### New Capabilities
_Нет._ Формат хранения времени — кросс-каттинг **конвенция** (`how we store`),
по правилам проекта живёт в `docs/conventions/database.md`, а не в OpenSpec.
### Modified Capabilities
- `web-ui`: временные метки в интерфейсе SHALL отображаться в таймзоне из
конфига (`[general].timezone`, дефолт UTC), а не в жёстко зашитой `Europe/Moscow`.
## Impact
- **Код:** `internal/store/download.go` (`sqliteTimeLayout``time.RFC3339`,
`FormatTime`/`ParseTime`, инлайн `updated_at = datetime('now')`); Create-методы
`store` (`download.go`, `recognition.go`) — все INSERT, полагавшиеся на
`DEFAULT`, теперь явно передают `created_at`/`updated_at`; новый `store.Now()`
как единая тестируемая точка.
- **Миграции:** новая goose-миграция — rebuild 7 таблиц без `DEFAULT` + backfill
колонок `created_at`/`updated_at`/`source_added_at`.
- **Конфиг:** новая секция `[general]` с полем `timezone` в `internal/config`,
жёсткая валидация на старте, проброс зоны в `internal/httpapi` (замена `mskLoc`
в `live.go`). Встраивание zoneinfo — `import _ "time/tzdata"` в `cmd/jellybit`
(устраняет latent silent-UTC на контейнере без tzdata; +~450КБ к бинарю).
- **Docs:** `docs/conventions/database.md`, `docs/conventions/config.md`,
`docs/specs/database.md` (комментарий формата в ER-схеме).
- **Совместимость:** ULID-корреляция и хронология сохраняются (значения меток
эквивалентны по моменту); секунды не теряются.