Files
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

5.8 KiB
Raw Permalink Blame History

1. Ревью дизайна (чекпоинт ДО кода)

  • 1.1 Согласовать design.md: стратегию миграции/backfill, снятие DEFAULT, секцию [general] с timezone. Правки внести до реализации.

2. Хелперы формата времени (store)

  • 2.1 internal/store/download.go: убрать sqliteTimeLayout; FormatTimet.UTC().Format(time.RFC3339), ParseTimetime.Parse(time.RFC3339, s). Обновить комментарии (формат ...Z, не datetime('now')), включая коммент про SourceAddedAt на download.go:118 (ссылается на sqliteTimeLayout).
  • 2.2 Добавить store.Now() (обёртка над time.Now().UTC()) как единую точку получения «сейчас» в слое store.
  • 2.3 Проверить migrations/0006_ulid_identity.go:204 (parseCreatedAt парсит старый формат) — это историческая миграция, формат менять не нужно; убедиться, что она не ломается и не пересекается с новой.

3. Убрать DEFAULT и писать время из Go

  • 3.1 Каждый INSERT-сайт со временем явно передаёт created_at/updated_at (= FormatTime(store.Now())). Полный список сайтов (не методов!): download.goCreateDownloadIfNoActive (вставка download :219 + вставка хешей :228), dedup top-up хешей :203 (INSERT OR IGNORE), AddInfohashes :306 (INSERT OR IGNORE); recognition.goCreateRecognition :72, AddHint :108, SetOverride :130, AddFileLink :185, AddMetadataCandidate :332. Для INSERT OR IGNORE в download_infohash created_at обязателен на новой строке (после снятия DEFAULT + NOT NULL); при конфликте PK строка не вставляется — ок.
  • 3.2 download.go:510: инлайн updated_at = datetime('now') → параметр FormatTime(store.Now()) (sortability-ловушка — проверить, что не осталось сырых datetime('now') в SQL: grep -rn "datetime('now')" internal).

4. Миграция БД (goose)

  • 4.1 Новая миграция (стиль 0006): backfill каждой timestamp-колонки strftime('%Y-%m-%dT%H:%M:%SZ', col) (7 таблиц: download.created_at/updated_at/ source_added_at, download_infohash/recognition/hint/override/metadata_candidate/ file_link .created_at) + rebuild таблиц без DEFAULT на timestamp-колонках.
  • 4.2 Down-миграция: обратный backfill strftime('%Y-%m-%d %H:%M:%S', col) + восстановление DEFAULT.
  • 4.3 Тест миграции: строки со старым форматом → новый формат; хронология сортировки сохранена; source_added_at IS NULL не затронут; обратный backfill (down) на строке с T/Z даёт старый формат.
  • 4.4 Обновить существующий TestUlidMigration (migration_test.go:100-101): ассерт ждёт старый формат 2026-01-01 10:00:00, после новой миграции метки станут ...T...Z — поправить ожидаемое (заодно канарейка, что backfill отработал).

5. Настройка таймзоны отображения

  • 5.1 internal/config: секция [general] (General struct) с полем Timezone string (дефолт UTC) + распарсенный *time.Location; жёсткая валидация на старте через time.LoadLocation, невалидная зона → ошибка конфигурации (без рантайм-фолбэка).
  • 5.1a Встроить zoneinfo: import _ "time/tzdata" в точке входа (cmd/jellybit). Тогда LoadLocation падает только на битом имени.
  • 5.2 internal/httpapi: пробросить *time.Location до fmtDate/humanizeAge (свободные функции — тянуть параметром через toView, вызовы httpapi.go:288,545, либо сделать методами server); заменить зашитый mskLoc (live.go:189-201) целиком (после встраивания tzdata рантайм-фолбэк на UTC внутри mskLoc не нужен). Guard: t.In(nil) паникует — при незаданной зоне подставлять time.UTC; тест-хелпер testRouterLive тоже задаёт loc.
  • 5.3 Обновить пример конфига (config.example.toml/деплой): секция [general] первой, timezone; для этого сервера — Europe/Moscow.

6. Документация

  • 6.1 docs/conventions/database.md:42-43: формат меток → RFC 3339 (UTC, Z), генерация только из Go (store.Now/FormatTime), без DEFAULT.
  • 6.2 docs/conventions/config.md: зона отображения конфигурируема (дефолт UTC); хранение и логи — UTC.
  • 6.3 docs/specs/database.md: комментарий формата timestamp-колонок в ER.

7. Проверка и ревью кода (чекпоинт до archive)

  • 7.1 task test / task lint зелёные; тест сортировки списка на смешанных и однотипных метках.
  • 7.2 Ревью кода реализации; затем openspec validate --strict и archive.