fix: timezone handling across fullstack — UTC parsing + display in configured TZ

Root cause: backend returned naive ISO datetimes (no Z/offset) → JS parsed them as
browser local time → 3h drift for MSK users → '3ч' instead of 'только что'.

Backend:
- schemas/agent.py: add field_serializer('Z' suffix) for ConversationItem.updated_at
  and MessageItem.created_at — naive datetimes serialized as UTC
- routes/agent_conversations.py: datetime.utcnow() → datetime.now(timezone.utc) (3x)

Frontend:
- New: stores/timezone.svelte.ts — global reactive appTimezone store
- dateFormat.ts: add parseDateUTC() (appends 'Z' to naive ISO), all format*()
  functions now use parseDateUTC + { timeZone: appTimezone.current }
- ~25 files: replace new Date(apiString) → parseDateUTC(apiString),
  add timeZone: appTimezone.current to toLocaleString()/toLocaleDateString()
- SystemSettings.svelte + HealthCenterModel sync appTimezone to global store
- ConversationList.svelte: fix relativeTime() and date grouping (the '3ч' bug)

Verified: backend schema test, frontend 2501 tests pass, build succeeds,
browser validation on /agent and /settings.
This commit is contained in:
2026-06-30 18:11:42 +03:00
parent 60674c8639
commit e40724a0fe
30 changed files with 215 additions and 61 deletions

View File

@@ -2,7 +2,7 @@
# #region AgentChat.Api.Conversations [C:3] [TYPE Module] [SEMANTICS agent-chat,api,rest]
# @defgroup AgentChat REST routes for conversation lifecycle.
from datetime import datetime
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
@@ -170,10 +170,10 @@ async def save_conversation(
id=body.conversation_id,
user_id=user_id,
title=body.title or "",
created_at=datetime.utcnow(),
created_at=datetime.now(timezone.utc),
)
db.add(conv)
conv.updated_at = datetime.utcnow()
conv.updated_at = datetime.now(timezone.utc)
if body.title:
conv.title = body.title
@@ -196,7 +196,7 @@ async def save_conversation(
state=msg_data.get("state"),
tool_calls=msg_data.get("tool_calls"),
attachments=msg_data.get("attachments"),
created_at=datetime.utcnow(),
created_at=datetime.now(timezone.utc),
)
db.add(msg)
db.flush()

View File

@@ -4,7 +4,14 @@
from datetime import datetime
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_serializer
def _serialize_datetime(v: datetime) -> str:
"""Serialize datetime as ISO 8601 with Z suffix. Naive datetimes are treated as UTC."""
if v.tzinfo is None:
return v.strftime("%Y-%m-%dT%H:%M:%S") + "Z"
return v.isoformat()
# #region Schemas.Agent.ConversationItem [C:1] [TYPE Class] [SEMANTICS agent,schema,conversation]
@@ -18,6 +25,8 @@ class ConversationItem(BaseModel):
has_tool_calls: bool = False # conversation includes tool usage
has_error: bool = False # conversation has error state
risk_level: str | None = None # "safe" | "guarded" | "dangerous"
_serialize_updated_at = field_serializer("updated_at")(_serialize_datetime)
# #endregion Schemas.Agent.ConversationItem
@@ -63,6 +72,8 @@ class MessageItem(BaseModel):
tool_calls: list[ToolCall] | None = None
attachments: list[AttachmentMeta] | None = None
created_at: datetime
_serialize_created_at = field_serializer("created_at")(_serialize_datetime)
# #endregion Schemas.Agent.MessageItem