diff --git a/docs/gns3-copilot/troubleshooting/datetime-timezone-issue.md b/docs/gns3-copilot/troubleshooting/datetime-timezone-issue.md new file mode 100644 index 000000000..8ebb5061d --- /dev/null +++ b/docs/gns3-copilot/troubleshooting/datetime-timezone-issue.md @@ -0,0 +1,151 @@ +# Datetime Timezone Issue + +## Problem Description + +The API returns datetime strings without timezone suffixes, causing frontend JavaScript to parse them as local time instead of UTC time. + +### Example + +| Aspect | Value | +|--------|-------| +| Actual time (Beijing) | 2026-03-07 01:30 | +| Backend returns | `2026-03-06T16:31:36.547762` (no timezone) | +| Frontend displays | March 6, 16:31 | +| Should display | March 7, 00:31 (UTC 16:31 + 8 hours) | + +### Root Cause + +- Backend stores UTC time in database as naive datetime (no timezone info) +- FastAPI's `jsonable_encoder` serializes datetime as ISO 8601 **without timezone suffix** +- JavaScript `new Date()` treats strings without 'Z' or timezone offset as **local time** + +``` +Backend intention: 2026-03-06T16:31:36Z (UTC) +Actually returned: 2026-03-06T16:31:36.547762 (no timezone) +JavaScript parses as: local time 16:31 +Should be: UTC 16:31 → Beijing time 00:31 (next day) +``` + +--- + +## Frontend Solutions + +### Solution 1: Use dayjs UTC Parsing (Recommended) + +```typescript +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +// Parse naive datetime as UTC +const createdAt = '2026-03-06T16:31:36.547762'; +const date = dayjs.utc(createdAt).tz('Asia/Shanghai'); +console.log(date.format('YYYY-MM-DD HH:mm')); // 2026-03-07 00:31 +``` + +### Solution 2: Add 'Z' Suffix Manually + +```typescript +// Utility function to normalize API dates +function parseAPIDate(dateStr: string): Date { + // Add 'Z' suffix if no timezone info present + const normalized = dateStr.includes('Z') || dateStr.includes('+') + ? dateStr + : `${dateStr}Z`; + return new Date(normalized); +} + +// Usage +const date = parseAPIDate('2026-03-06T16:31:36.547762'); +console.log(date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })); +``` + +### Solution 3: Global Axios Interceptor (Most Thorough) + +```typescript +import axios from 'axios'; + +api.interceptors.response.use((response) => { + // Recursively normalize all date fields + function normalizeDates(obj: any): any { + // Match ISO 8601 datetime pattern without timezone + if (typeof obj === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(obj)) { + return obj.includes('Z') || obj.includes('+') ? obj : `${obj}Z`; + } + if (Array.isArray(obj)) { + return obj.map(normalizeDates); + } + if (obj && typeof obj === 'object') { + return Object.fromEntries( + Object.entries(obj).map(([k, v]) => [k, normalizeDates(v)]) + ); + } + return obj; + } + + response.data = normalizeDates(response.data); + return response; +}); +``` + +--- + +## Backend Context + +### Current Implementation + +**File**: `gns3server/api/server.py:58-64` + +```python +application = FastAPI( + title="GNS3 controller API", + description="This page describes the public controller API for GNS3", + version="v3", + docs_url=None, + redoc_url=None +) +``` + +**File**: `gns3server/db/models/base.py` + +```python +from fastapi.encoders import jsonable_encoder + +class Base: + def asjson(self): + return jsonable_encoder(self.asdict()) +``` + +### Datetime Flow + +1. Database stores naive datetime (no timezone) +2. FastAPI uses `jsonable_encoder` to serialize +3. Output format: `YYYY-MM-DDTHH:MM:SS.ffffff` (no 'Z' suffix) +4. JavaScript interprets as local time + +### Affected Fields + +- `created_at` +- `updated_at` +- `last_login` +- Any other datetime fields in API responses + +--- + +## Testing Checklist + +- [ ] Verify datetime displays correctly in Beijing timezone (UTC+8) +- [ ] Test with other timezones (e.g., UTC-5, UTC+0) +- [ ] Check daylight saving time transitions (if applicable) +- [ ] Verify datetime input/insertion still works correctly + +--- + +## Related Files + +- Backend: `gns3server/api/server.py` +- Backend: `gns3server/db/models/base.py` +- Schemas: `gns3server/schemas/controller/base.py`