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:
parent
d8c9500949
commit
cfb0f4b2eb
4 changed files with 27 additions and 6 deletions
File diff suppressed because one or more lines are too long
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import type { Log, LogType } from "@/types";
|
import type { Log, LogType } from "@/types";
|
||||||
|
import { dateIST, fmtTime } from "@/lib/date-ist";
|
||||||
|
|
||||||
const TYPE_COLORS: Record<LogType, string> = {
|
const TYPE_COLORS: Record<LogType, string> = {
|
||||||
feed: "bg-rose-400",
|
feed: "bg-rose-400",
|
||||||
|
|
@ -36,7 +37,7 @@ export function CalendarView({ logs, filter }: Props) {
|
||||||
const logsByDate = useMemo(() => {
|
const logsByDate = useMemo(() => {
|
||||||
const map: Record<string, Log[]> = {};
|
const map: Record<string, Log[]> = {};
|
||||||
filteredLogs.forEach(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] = [];
|
if (!map[key]) map[key] = [];
|
||||||
map[key].push(log);
|
map[key].push(log);
|
||||||
});
|
});
|
||||||
|
|
@ -173,7 +174,7 @@ export function CalendarView({ logs, filter }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 flex-shrink-0">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
|
||||||
import { Button, Input, Modal } from "@/components/ui";
|
import { Button, Input, Modal } from "@/components/ui";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import type { Medicine, Dose } from "@/types";
|
import type { Medicine, Dose } from "@/types";
|
||||||
|
import { fmtTime, fmtDate } from "@/lib/date-ist";
|
||||||
|
|
||||||
interface Props { childId: string }
|
interface Props { childId: string }
|
||||||
|
|
||||||
|
|
@ -183,7 +184,7 @@ export function MedicineTab({ childId }: Props) {
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-600 dark:text-gray-300 font-medium">
|
<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>
|
</span>
|
||||||
{displayAmount && <span className="ml-2 text-gray-500">· {displayAmount}</span>}
|
{displayAmount && <span className="ml-2 text-gray-500">· {displayAmount}</span>}
|
||||||
{displayNotes && <span className="ml-1 text-gray-400">· {displayNotes}</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>
|
<p className="text-xs text-gray-400">Original is kept. A correction record is added.</p>
|
||||||
{correctDose && (
|
{correctDose && (
|
||||||
<div className="text-xs bg-gray-50 dark:bg-gray-700 rounded-lg p-2 text-gray-500 dark:text-gray-400">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Input placeholder="Corrected amount" value={correctAmount} onChange={e => setCorrectAmount(e.target.value)} />
|
<Input placeholder="Corrected amount" value={correctAmount} onChange={e => setCorrectAmount(e.target.value)} />
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,26 @@ const queryClient = postgres(connectionString, {
|
||||||
max: 10,
|
max: 10,
|
||||||
idle_timeout: 20,
|
idle_timeout: 20,
|
||||||
max_lifetime: 60 * 30,
|
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
|
// Raw client for queries that need session context
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue