Fix timestamp timezone — logs now always show in IST regardless of server TZ

Root cause: postgres.js v3 parses `timestamp without timezone` columns as
`new Date("YYYY-MM-DD HH:mm:ss")` (space format, no Z). V8 treats this as
*local time*, so on a non-UTC server (Dokploy host = Europe/Helsinki UTC+3)
the parsed Date object is 3 hours off, making logged times show as server
time instead of the user's IST.

Fixes:
- db/index.ts: add custom `timestamp` type parser that forces UTC by
  converting space-format to ISO with 'Z' before calling new Date().
  Also set `connection: { TimeZone: "UTC" }` so PostgreSQL sessions always
  store/return timestamps in UTC regardless of server OS timezone.
- CalendarView.tsx: use `dateIST()` for day grouping (fixes midnight-boundary
  bug where a 12:30 AM IST entry appeared on the previous UTC day) and
  `fmtTime()` for time display (replaces toLocaleTimeString without timezone).
- MedicineTab.tsx: replace toLocaleString() with fmtDate/fmtTime (IST-aware).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-28 23:13:57 +05:30
parent d8c9500949
commit cfb0f4b2eb
4 changed files with 27 additions and 6 deletions

File diff suppressed because one or more lines are too long

View file

@ -2,6 +2,7 @@
import { useState, useMemo } from "react";
import type { Log, LogType } from "@/types";
import { dateIST, fmtTime } from "@/lib/date-ist";
const TYPE_COLORS: Record<LogType, string> = {
feed: "bg-rose-400",
@ -36,7 +37,7 @@ export function CalendarView({ logs, filter }: Props) {
const logsByDate = useMemo(() => {
const map: Record<string, Log[]> = {};
filteredLogs.forEach(log => {
const key = new Date(log.loggedAt).toISOString().slice(0, 10);
const key = dateIST(log.loggedAt); // IST date, not UTC
if (!map[key]) map[key] = [];
map[key].push(log);
});
@ -173,7 +174,7 @@ export function CalendarView({ logs, filter }: Props) {
</div>
</div>
<div className="text-xs text-gray-400 flex-shrink-0">
{new Date(log.loggedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
{fmtTime(log.loggedAt)}
</div>
</div>
))}

View file

@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
import { Button, Input, Modal } from "@/components/ui";
import { api } from "@/lib/api";
import type { Medicine, Dose } from "@/types";
import { fmtTime, fmtDate } from "@/lib/date-ist";
interface Props { childId: string }
@ -183,7 +184,7 @@ export function MedicineTab({ childId }: Props) {
<div className="flex justify-between items-start">
<div>
<span className="text-gray-600 dark:text-gray-300 font-medium">
{new Date(d.administered_at).toLocaleString()}
{fmtDate(d.administered_at, { month: "short", day: "numeric" })} {fmtTime(d.administered_at)}
</span>
{displayAmount && <span className="ml-2 text-gray-500">· {displayAmount}</span>}
{displayNotes && <span className="ml-1 text-gray-400">· {displayNotes}</span>}
@ -228,7 +229,7 @@ export function MedicineTab({ childId }: Props) {
<p className="text-xs text-gray-400">Original is kept. A correction record is added.</p>
{correctDose && (
<div className="text-xs bg-gray-50 dark:bg-gray-700 rounded-lg p-2 text-gray-500 dark:text-gray-400">
Original: {correctDose.dose.amount_given || "—"} · {new Date(correctDose.dose.administered_at).toLocaleString()}
Original: {correctDose.dose.amount_given || "—"} · {fmtDate(correctDose.dose.administered_at, { month: "short", day: "numeric" })} {fmtTime(correctDose.dose.administered_at)}
</div>
)}
<Input placeholder="Corrected amount" value={correctAmount} onChange={e => setCorrectAmount(e.target.value)} />

View file

@ -8,7 +8,26 @@ const queryClient = postgres(connectionString, {
max: 10,
idle_timeout: 20,
max_lifetime: 60 * 30,
// Prepare: true enables prepared statements
// Force the PostgreSQL session timezone to UTC so that `timestamp without
// time zone` columns are always stored and returned as UTC values,
// regardless of what timezone the host OS / Dokploy server is configured
// with (the Finnish server is UTC+3 in summer).
connection: { TimeZone: "UTC" },
// postgres.js v3 parses `timestamp` (OID 1114) with `new Date(rawString)`
// where rawString looks like "2024-05-28 09:00:00" (space, no Z). V8
// treats that space-format string as *local* time, so on any server that
// isn't UTC the resulting Date object is wrong. The fix: convert the
// space-format string to ISO 8601 with an explicit Z so V8 always
// interprets it as UTC, regardless of the Node.js process timezone.
types: {
timestamp: {
to: 1114,
from: [1114],
serialize: (x: Date | string) =>
(x instanceof Date ? x : new Date(x)).toISOString(),
parse: (x: string) => new Date(x.replace(" ", "T") + "Z"),
},
},
});
// Raw client for queries that need session context