Fix drag-and-drop timezone bug between dev and production environments
All checks were successful
Build and Push Docker Image / docker (push) Successful in 1m12s
All checks were successful
Build and Push Docker Image / docker (push) Successful in 1m12s
The root cause was that drag operations sent naive local time to the backend, which the backend interpreted using the SERVER's local timezone rather than the USER's timezone. This caused different behavior between development and production servers in different timezones. **Frontend Changes:** - Convert naive datetime from drag operations to UTC before sending to backend - Use client-side Local timezone to properly convert user's intended times - Handle DST transition edge cases with fallback logic **Backend Changes:** - Update parse_event_datetime to treat incoming times as UTC (no server timezone conversion) - Update series handlers to expect UTC times from frontend - Remove server-side Local timezone dependency for event parsing **Result:** - Consistent behavior across all server environments regardless of server timezone - Drag operations now correctly preserve user's intended local times - Fixes "4 hours too early" issue in production drag-and-drop operations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -845,7 +845,7 @@ fn parse_event_datetime(
|
||||
time_str: &str,
|
||||
all_day: bool,
|
||||
) -> Result<chrono::DateTime<chrono::Utc>, String> {
|
||||
use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
|
||||
|
||||
// Parse the date
|
||||
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
|
||||
@@ -866,11 +866,7 @@ fn parse_event_datetime(
|
||||
// Combine date and time
|
||||
let datetime = NaiveDateTime::new(date, time);
|
||||
|
||||
// Treat the datetime as local time and convert to UTC
|
||||
let local_datetime = Local.from_local_datetime(&datetime)
|
||||
.single()
|
||||
.ok_or_else(|| "Ambiguous local datetime".to_string())?;
|
||||
|
||||
Ok(local_datetime.with_timezone(&Utc))
|
||||
// Frontend now sends UTC times, so treat as UTC directly
|
||||
Ok(Utc.from_utc_datetime(&datetime))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,13 +130,9 @@ pub async fn create_event_series(
|
||||
.and_hms_opt(23, 59, 59)
|
||||
.ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?;
|
||||
|
||||
// Convert from local time to UTC
|
||||
let start_local = chrono::Local.from_local_datetime(&start_dt)
|
||||
.single()
|
||||
.ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?;
|
||||
let end_local = chrono::Local.from_local_datetime(&end_dt)
|
||||
.single()
|
||||
.ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?;
|
||||
// Frontend now sends UTC times, so treat as UTC directly
|
||||
let start_local = chrono::Utc.from_utc_datetime(&start_dt);
|
||||
let end_local = chrono::Utc.from_utc_datetime(&end_dt);
|
||||
|
||||
(
|
||||
start_local.with_timezone(&chrono::Utc),
|
||||
@@ -171,13 +167,9 @@ pub async fn create_event_series(
|
||||
start_date.and_time(end_time)
|
||||
};
|
||||
|
||||
// Convert from local time to UTC
|
||||
let start_local = chrono::Local.from_local_datetime(&start_dt)
|
||||
.single()
|
||||
.ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?;
|
||||
let end_local = chrono::Local.from_local_datetime(&end_dt)
|
||||
.single()
|
||||
.ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?;
|
||||
// Frontend now sends UTC times, so treat as UTC directly
|
||||
let start_local = chrono::Utc.from_utc_datetime(&start_dt);
|
||||
let end_local = chrono::Utc.from_utc_datetime(&end_dt);
|
||||
|
||||
(
|
||||
start_local.with_timezone(&chrono::Utc),
|
||||
@@ -456,13 +448,9 @@ pub async fn update_event_series(
|
||||
(chrono::Utc.from_utc_datetime(&start_dt) + original_duration).naive_utc()
|
||||
};
|
||||
|
||||
// Convert from local time to UTC
|
||||
let start_local = chrono::Local.from_local_datetime(&start_dt)
|
||||
.single()
|
||||
.ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?;
|
||||
let end_local = chrono::Local.from_local_datetime(&end_dt)
|
||||
.single()
|
||||
.ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?;
|
||||
// Frontend now sends UTC times, so treat as UTC directly
|
||||
let start_local = chrono::Utc.from_utc_datetime(&start_dt);
|
||||
let end_local = chrono::Utc.from_utc_datetime(&end_dt);
|
||||
|
||||
(
|
||||
start_local.with_timezone(&chrono::Utc),
|
||||
|
||||
@@ -761,11 +761,30 @@ pub fn App() -> Html {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// Send local time directly to backend (backend will handle UTC conversion)
|
||||
let start_date = new_start.format("%Y-%m-%d").to_string();
|
||||
let start_time = new_start.format("%H:%M").to_string();
|
||||
let end_date = new_end.format("%Y-%m-%d").to_string();
|
||||
let end_time = new_end.format("%H:%M").to_string();
|
||||
// Convert local naive datetime to UTC before sending to backend
|
||||
use chrono::TimeZone;
|
||||
let local_tz = chrono::Local;
|
||||
|
||||
let start_utc = local_tz.from_local_datetime(&new_start)
|
||||
.single()
|
||||
.unwrap_or_else(|| {
|
||||
// Fallback for ambiguous times (DST transitions)
|
||||
local_tz.from_local_datetime(&new_start).earliest().unwrap()
|
||||
})
|
||||
.with_timezone(&chrono::Utc);
|
||||
|
||||
let end_utc = local_tz.from_local_datetime(&new_end)
|
||||
.single()
|
||||
.unwrap_or_else(|| {
|
||||
// Fallback for ambiguous times (DST transitions)
|
||||
local_tz.from_local_datetime(&new_end).earliest().unwrap()
|
||||
})
|
||||
.with_timezone(&chrono::Utc);
|
||||
|
||||
let start_date = start_utc.format("%Y-%m-%d").to_string();
|
||||
let start_time = start_utc.format("%H:%M").to_string();
|
||||
let end_date = end_utc.format("%Y-%m-%d").to_string();
|
||||
let end_time = end_utc.format("%H:%M").to_string();
|
||||
|
||||
// Convert existing event data to string formats for the API
|
||||
let status_str = match original_event.status {
|
||||
|
||||
Reference in New Issue
Block a user