From baf2822b81827173f5499e0585a07bd7025a3518 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Tue, 15 Jul 2025 21:17:53 -0600 Subject: [PATCH] fix: browse page timestamps show UTC instead of user local time (#482) * fix: browse page timestamps show UTC instead of user local time Closes #474 * fix: tests --- server/reflector/db/transcripts.py | 14 ++++++++++---- server/reflector/views/transcripts.py | 11 +++++++++-- www/app/(app)/browse/page.tsx | 16 +++------------- www/app/lib/time.ts | 26 ++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/server/reflector/db/transcripts.py b/server/reflector/db/transcripts.py index dc832850..de55cac9 100644 --- a/server/reflector/db/transcripts.py +++ b/server/reflector/db/transcripts.py @@ -3,13 +3,13 @@ import json import os import shutil from contextlib import asynccontextmanager -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import Any, Literal import sqlalchemy from fastapi import HTTPException -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_serializer from reflector.db import database, metadata from reflector.processors.types import Word as ProcessorWord from reflector.settings import settings @@ -82,7 +82,7 @@ transcripts = sqlalchemy.Table( def generate_transcript_name() -> str: - now = datetime.utcnow() + now = datetime.now(timezone.utc) return f"Transcript {now.strftime('%Y-%m-%d %H:%M:%S')}" @@ -150,7 +150,7 @@ class Transcript(BaseModel): status: str = "idle" locked: bool = False duration: float = 0 - created_at: datetime = Field(default_factory=datetime.utcnow) + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) title: str | None = None short_summary: str | None = None long_summary: str | None = None @@ -168,6 +168,12 @@ class Transcript(BaseModel): source_kind: SourceKind audio_deleted: bool | None = None + @field_serializer("created_at", when_used="json") + def serialize_datetime(self, dt: datetime) -> str: + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat() + def add_event(self, event: str, data: BaseModel) -> TranscriptEvent: ev = TranscriptEvent(event=event, data=data.model_dump()) self.events.append(ev) diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index 19c273c3..51d59a1c 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Annotated, Literal, Optional import reflector.auth as auth @@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException from fastapi_pagination import Page from fastapi_pagination.ext.databases import paginate from jose import jwt -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_serializer from reflector.db.meetings import meetings_controller from reflector.db.migrate_user import migrate_user from reflector.db.rooms import rooms_controller @@ -61,6 +61,13 @@ class GetTranscriptMinimal(BaseModel): target_language: str | None reviewed: bool meeting_id: str | None + + @field_serializer("created_at", when_used="json") + def serialize_datetime(self, dt: datetime) -> str: + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat() + source_kind: SourceKind room_id: str | None = None room_name: str | None = None diff --git a/www/app/(app)/browse/page.tsx b/www/app/(app)/browse/page.tsx index 4a7de7b7..a36a4aba 100644 --- a/www/app/(app)/browse/page.tsx +++ b/www/app/(app)/browse/page.tsx @@ -46,7 +46,7 @@ import useSessionUser from "../../lib/useSessionUser"; import NextLink from "next/link"; import { Room, GetTranscriptMinimal } from "../../api"; import Pagination from "./pagination"; -import { formatTimeMs } from "../../lib/time"; +import { formatTimeMs, formatLocalDate } from "../../lib/time"; import useApi from "../../lib/useApi"; import { useError } from "../../(errors)/errorContext"; import { SourceKind } from "../../api"; @@ -381,15 +381,7 @@ export default function TranscriptBrowser() { ? item.room_name : item.source_kind} - - {new Date(item.created_at).toLocaleString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - })} - + {formatLocalDate(item.created_at)} {formatTimeMs(item.duration)} @@ -466,9 +458,7 @@ export default function TranscriptBrowser() { ? item.room_name : item.source_kind} - - Date: {new Date(item.created_at).toLocaleString()} - + Date: {formatLocalDate(item.created_at)} Duration: {formatTimeMs(item.duration)} diff --git a/www/app/lib/time.ts b/www/app/lib/time.ts index a632c979..7283475c 100644 --- a/www/app/lib/time.ts +++ b/www/app/lib/time.ts @@ -28,3 +28,29 @@ export const formatTimeDifference = (seconds: number): string => { return timeString; }; + +export const formatRelativeTime = (dateString: string): string => { + const now = new Date(); + const past = new Date(dateString); + const diffMs = now.getTime() - past.getTime(); + + const diffSeconds = Math.floor(diffMs / 1000); + const diffMinutes = Math.floor(diffSeconds / 60); + const diffHours = Math.floor(diffMinutes / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffSeconds < 60) return `${diffSeconds}s ago`; + if (diffMinutes < 60) return `${diffMinutes}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + return `${diffDays}d ago`; +}; + +export const formatLocalDate = (dateString: string): string => { + return new Date(dateString).toLocaleString(navigator.language || "en-US", { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + }); +};