diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index a926074..5759920 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -569,6 +569,34 @@ Examples: - `src/lib/server/db/queries/arrInstances.ts` - `src/lib/server/db/queries/jobs.ts` +**Date Handling** + +SQLite stores timestamps from `CURRENT_TIMESTAMP` as UTC in the format +`"YYYY-MM-DD HH:MM:SS"` without a timezone indicator. JavaScript's `Date` +constructor interprets strings without timezone info as local time, causing +incorrect display. + +Use the shared date utilities in `src/lib/shared/dates.ts`: + +```typescript +import { parseUTC, toUTC } from "$shared/dates"; + +// Parse SQLite timestamp to Date object (correctly interpreted as UTC) +const date = parseUTC("2026-01-17 03:21:52"); // Date in UTC + +// Normalize to ISO 8601 string with Z suffix +const iso = toUTC("2026-01-17 03:21:52"); // "2026-01-17T03:21:52Z" +``` + +**Never** manually append `'Z'` or manipulate timestamp strings directly. Always +use these utilities to ensure consistent timezone handling across the codebase. + +For SQL queries that compare timestamps, normalize both sides: + +```sql +datetime(replace(replace(timestamp_col, 'T', ' '), 'Z', '')) <= datetime('now') +``` + #### PCD (Profilarr Compliant Database) PCDs are git repositories containing versioned configuration data—quality diff --git a/src/lib/shared/dates.ts b/src/lib/shared/dates.ts new file mode 100644 index 0000000..43187fd --- /dev/null +++ b/src/lib/shared/dates.ts @@ -0,0 +1,51 @@ +/** + * Date utilities for handling SQLite timestamps + * + * SQLite stores timestamps from CURRENT_TIMESTAMP as UTC in the format + * "YYYY-MM-DD HH:MM:SS" (no timezone indicator). JavaScript's Date constructor + * interprets strings without timezone info as local time, causing incorrect + * display. + * + * These utilities normalize SQLite timestamps to proper ISO 8601 format + * so JavaScript correctly interprets them as UTC. + */ + +/** + * Normalizes a SQLite timestamp to ISO 8601 format with UTC indicator. + * Handles both SQLite format ("YYYY-MM-DD HH:MM:SS") and ISO format. + * + * @param timestamp - SQLite or ISO timestamp string + * @returns ISO 8601 formatted string with Z suffix, or null if input is null/undefined + * + * @example + * toUTC("2026-01-17 03:21:52") // "2026-01-17T03:21:52Z" + * toUTC("2026-01-17T03:21:52") // "2026-01-17T03:21:52Z" + * toUTC("2026-01-17T03:21:52Z") // "2026-01-17T03:21:52Z" (unchanged) + * toUTC(null) // null + */ +export function toUTC(timestamp: string | null | undefined): string | null { + if (!timestamp) return null; + + // Already has Z suffix - return as-is + if (timestamp.endsWith('Z')) return timestamp; + + // Replace space with T (SQLite format) and add Z + return timestamp.replace(' ', 'T') + 'Z'; +} + +/** + * Parses a SQLite timestamp into a JavaScript Date object. + * Normalizes the timestamp to UTC before parsing. + * + * @param timestamp - SQLite or ISO timestamp string + * @returns Date object, or null if input is null/undefined + * + * @example + * parseUTC("2026-01-17 03:21:52") // Date object in UTC + * parseUTC(null) // null + */ +export function parseUTC(timestamp: string | null | undefined): Date | null { + const normalized = toUTC(timestamp); + if (!normalized) return null; + return new Date(normalized); +} diff --git a/src/routes/arr/[id]/upgrades/components/CooldownTracker.svelte b/src/routes/arr/[id]/upgrades/components/CooldownTracker.svelte index 8336359..1188bcc 100644 --- a/src/routes/arr/[id]/upgrades/components/CooldownTracker.svelte +++ b/src/routes/arr/[id]/upgrades/components/CooldownTracker.svelte @@ -1,6 +1,7 @@