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

77 lines
5.8 KiB
Markdown
Raw Permalink 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.
## 1. Ревью дизайна (чекпоинт ДО кода)
- [x] 1.1 Согласовать design.md: стратегию миграции/backfill, снятие `DEFAULT`,
секцию `[general]` с `timezone`. Правки внести до реализации.
## 2. Хелперы формата времени (store)
- [x] 2.1 `internal/store/download.go`: убрать `sqliteTimeLayout`; `FormatTime`
`t.UTC().Format(time.RFC3339)`, `ParseTime``time.Parse(time.RFC3339, s)`.
Обновить комментарии (формат `...Z`, не `datetime('now')`), включая коммент про
`SourceAddedAt` на `download.go:118` (ссылается на `sqliteTimeLayout`).
- [x] 2.2 Добавить `store.Now()` (обёртка над `time.Now().UTC()`) как единую
точку получения «сейчас» в слое store.
- [x] 2.3 Проверить `migrations/0006_ulid_identity.go:204` (`parseCreatedAt`
парсит старый формат) — это историческая миграция, формат менять не нужно;
убедиться, что она не ломается и не пересекается с новой.
## 3. Убрать DEFAULT и писать время из Go
- [x] 3.1 Каждый INSERT-**сайт** со временем явно передаёт `created_at`/`updated_at`
(= `FormatTime(store.Now())`). Полный список сайтов (не методов!):
`download.go``CreateDownloadIfNoActive` (вставка download :219 + вставка
хешей :228), dedup top-up хешей :203 (`INSERT OR IGNORE`), `AddInfohashes` :306
(`INSERT OR IGNORE`); `recognition.go``CreateRecognition` :72, `AddHint` :108,
`SetOverride` :130, `AddFileLink` :185, `AddMetadataCandidate` :332. Для
`INSERT OR IGNORE` в `download_infohash` `created_at` обязателен на новой строке
(после снятия `DEFAULT` + `NOT NULL`); при конфликте PK строка не вставляется — ок.
- [x] 3.2 `download.go:510`: инлайн `updated_at = datetime('now')` → параметр
`FormatTime(store.Now())` (sortability-ловушка — проверить, что не осталось
сырых `datetime('now')` в SQL: `grep -rn "datetime('now')" internal`).
## 4. Миграция БД (goose)
- [x] 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-колонках.
- [x] 4.2 Down-миграция: обратный backfill `strftime('%Y-%m-%d %H:%M:%S', col)` +
восстановление `DEFAULT`.
- [x] 4.3 Тест миграции: строки со старым форматом → новый формат; хронология
сортировки сохранена; `source_added_at IS NULL` не затронут; обратный backfill
(down) на строке с `T`/`Z` даёт старый формат.
- [x] 4.4 Обновить существующий `TestUlidMigration` (`migration_test.go:100-101`):
ассерт ждёт старый формат `2026-01-01 10:00:00`, после новой миграции метки
станут `...T...Z` — поправить ожидаемое (заодно канарейка, что backfill отработал).
## 5. Настройка таймзоны отображения
- [x] 5.1 `internal/config`: секция `[general]` (`General struct`) с полем
`Timezone string` (дефолт `UTC`) + распарсенный `*time.Location`; жёсткая
валидация на старте через `time.LoadLocation`, невалидная зона → ошибка
конфигурации (без рантайм-фолбэка).
- [x] 5.1a Встроить zoneinfo: `import _ "time/tzdata"` в точке входа
(`cmd/jellybit`). Тогда `LoadLocation` падает только на битом имени.
- [x] 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.
- [x] 5.3 Обновить пример конфига (`config.example.toml`/деплой): секция
`[general]` первой, `timezone`; для этого сервера — `Europe/Moscow`.
## 6. Документация
- [x] 6.1 `docs/conventions/database.md:42-43`: формат меток → RFC 3339 (UTC,
`Z`), генерация только из Go (`store.Now`/`FormatTime`), без `DEFAULT`.
- [x] 6.2 `docs/conventions/config.md`: зона отображения конфигурируема (дефолт
UTC); хранение и логи — UTC.
- [x] 6.3 `docs/specs/database.md`: комментарий формата timestamp-колонок в ER.
## 7. Проверка и ревью кода (чекпоинт до archive)
- [x] 7.1 `task test` / `task lint` зелёные; тест сортировки списка на смешанных
и однотипных метках.
- [x] 7.2 Ревью кода реализации; затем `openspec validate --strict` и archive.