Datetime Handling
QDash stores native datetime objects in MongoDB, returns ISO8601 UTC strings via the API, and converts them to the configured UI timezone for display in the frontend. The default UI timezone is Asia/Tokyo.
Data Flow

The diagram above shows the complete datetime and elapsed time handling flow across Backend, API, and Frontend layers.
Backend (Python)
Calendar Timezone Configuration
The server calendar timezone is configured in src/qdash/config.py and is used only for server-generated calendar labels such as recorded_date and execution ID date prefixes:
timezone: str = "Asia/Tokyo"Utility Module
All datetime operations should use src/qdash/common/datetime_utils.py:
| Function | Description |
|---|---|
now() | Get current UTC datetime for DB/API storage |
local_now() | Get current datetime in configured calendar timezone for server labels such as YYYYMMDD |
now_iso() | Get current UTC datetime as ISO8601 string |
ensure_timezone(value) | Ensure datetime is timezone-aware (handles naive MongoDB datetimes) |
to_datetime(value) | Convert string/datetime/pendulum to standard datetime |
format_iso(dt) | Format datetime as ISO8601 string |
parse_elapsed_time(elapsed_str) | Parse elapsed time string to timedelta |
format_elapsed_time(elapsed) | Format timedelta as "H:MM:SS" string |
Pydantic Serialization
# In API schema models
@field_validator("elapsed_time", mode="before")
def _parse_elapsed_time(cls, v: Any) -> timedelta | None:
return parse_elapsed_time(v)
@field_serializer("elapsed_time")
def _serialize_elapsed_time(cls, v: timedelta | None) -> str | None:
return format_elapsed_time(v) if v else NoneMongoDB Considerations
MongoDB returns naive datetimes (without timezone info), assumed to be UTC. Use ensure_timezone() to convert them:
# In dbmodel validators
@field_validator("start_at", "end_at", mode="before")
@classmethod
def _ensure_timezone(cls, v: Any) -> datetime | None:
if v is None:
return None
if isinstance(v, datetime):
return ensure_timezone(v)
return vFrontend (TypeScript)
Utility Module
All datetime formatting should use ui/src/lib/utils/datetime.ts:
| Function | Format | Example Output |
|---|---|---|
formatDateTime(utcString) | yyyy-MM-dd HH:mm:ss | 2025-12-21 10:30:45 |
formatDate(utcString) | yyyy-MM-dd | 2025-12-21 |
formatTime(utcString) | HH:mm:ss | 10:30:45 |
formatDateTimeCompact(utcString) | MM/dd HH:mm | 12/21 10:30 |
By default, frontend utilities use NEXT_PUBLIC_TIMEZONE, falling back to Asia/Tokyo. Pass an explicit timezone only for tests or a UI that intentionally needs a different fixed timezone.
Usage Example
import { formatDateTime, formatDateTimeCompact } from "@/lib/utils/datetime";
// Full datetime
<span>{formatDateTime(execution.start_at)}</span>
// Output: "2025-12-21 10:30:45"
// Compact format for lists
<span>{formatDateTimeCompact(task.end_at)}</span>
// Output: "12/21 10:30"
// Null handling (returns "-" for null/undefined)
<span>{formatDateTime(task.start_at)}</span>
// Output: "-" if null/undefinedDependencies
date-fns: Date manipulation utilitiesdate-fns-tz: Timezone conversion withformatInTimeZone()
Conventions
- Always use utility functions — never use
new Date().toLocaleString()directly - Use
dateToDateInput(),dateToDateTimeLocal(), andtoDateTimeLocal()before putting dates into native date/datetime inputs - Use
toIsoSeconds()when submittingdatetime-localvalues so the configured UI timezone offset is explicit - Handle null/undefined — API responses may have null datetime fields
- Use
formatDateTimeCompact()for space-constrained UIs - Store
elapsed_timeas seconds (float) before MongoDB storage
Implementation Files
src/qdash/common/datetime_utils.py— backend utilitiesui/src/lib/utils/datetime.ts— frontend utilitiessrc/qdash/api/schemas/*.py— API schemassrc/qdash/dbmodel/*.py— DB models