feat(datetime): timezone/time_format в настройках + единый хелпер дат
Часовой пояс и формат времени (24h/12h) перенесены из браузера в настройки приложения: валидация IANA-зоны, сид в db/init.sql + db/migration.sql, отдача в GET /api/public-settings, раздел #sec-datetime в настройках с живым превью. Границы суток в SQL переведены на tzDayStart/tzDayEnd + bindTz ($TZ$) — раньше $n::date по TIMESTAMPTZ считал дни в UTC (у контейнера TimeZone=UTC) и молча сдвигал выборку на день; хардкод Europe/Moscow вычищен, now() заменён на tzWall(). Новый public/js/datetime.js: три семейства хелперов (instant / чистая DATE-строка без Date() / чистая TIME-строка), подключён на всех страницах включая публичные share/report/index, initDateTime() встроен в checkAuth(). Прямые toLocale*/getFullYear/toISOString().slice(0,10) в public/ выпилены. Попутно: bindTz добавляет параметр только при наличии $TZ$ в SQL (иначе запрос виснет вечно без global error handler) — регрессия закрыта в api.smoketest.js; починены off-by-one месяца в report.js и группировка по дате в share.js.
This commit is contained in:
@@ -123,6 +123,27 @@ async function main() {
|
||||
const restoredCheck = await api('/api/public-settings');
|
||||
ok('cache invalidation: restore visible', restoredCheck.data.system_name === before.data.system_name, restoredCheck.data.system_name);
|
||||
|
||||
const tzBad = await api('/api/settings', { token, method: 'PUT', body: { settings: { timezone: 'Nope/Nope' } } });
|
||||
ok('timezone: невалидная зона отклонена', tzBad.status === 400, tzBad.status);
|
||||
const fmtBad = await api('/api/settings', { token, method: 'PUT', body: { settings: { time_format: '36h' } } });
|
||||
ok('time_format: невалидный формат отклонён', fmtBad.status === 400, fmtBad.status);
|
||||
const pubTz = await api('/api/public-settings');
|
||||
ok('public-settings отдаёт timezone/time_format', !!pubTz.data.timezone && ['24h', '12h'].includes(pubTz.data.time_format), {
|
||||
timezone: pubTz.data.timezone,
|
||||
time_format: pubTz.data.time_format,
|
||||
});
|
||||
|
||||
const noFilter = await api('/api/files?limit=5', { token });
|
||||
ok('files без фильтров: 200 (без висящих bind-параметров)', noFilter.status === 200, noFilter.status);
|
||||
const withFilter = await api('/api/files?limit=5&date_from=2026-01-01&date_to=2026-12-31', { token });
|
||||
ok('files с фильтром дат: 200', withFilter.status === 200, withFilter.status);
|
||||
const entriesNoFilter = await api('/api/entries?limit=5', { token });
|
||||
ok('entries без фильтров: 200', entriesNoFilter.status === 200, entriesNoFilter.status);
|
||||
const entriesWithFilter = await api('/api/entries?limit=5&date_from=2026-01-01&date_to=2026-12-31', { token });
|
||||
ok('entries с фильтром дат: 200', entriesWithFilter.status === 200, entriesWithFilter.status);
|
||||
const photosFilter = await api('/api/photos?limit=5&date_from=2026-01-01&date_to=2026-12-31', { token });
|
||||
ok('photos с фильтром дат: 200', photosFilter.status === 200, photosFilter.status);
|
||||
|
||||
const groupCountBefore = Array.isArray(groups.data) ? groups.data.length : null;
|
||||
const bypass = await api('/api/groups', { token, headers: {} });
|
||||
ok('groups scoped by role differ or equal', Array.isArray(bypass.data));
|
||||
|
||||
Reference in New Issue
Block a user