11import  os 
22from  datetime  import  datetime , timedelta 
3+ import  pytz 
4+ import  time 
35from  flask  import  Flask , render_template , request , redirect , url_for , flash , jsonify , send_from_directory 
46from  flask_login  import  LoginManager , UserMixin , login_user , logout_user , login_required , current_user 
57from  flask_sqlalchemy  import  SQLAlchemy 
5254# Initialize extensions 
5355db .init_app (app )
5456
57+ # Configure local timezone detection 
58+ def  get_local_timezone ():
59+     """Detect the local system timezone""" 
60+     # Try environment variable first (Docker/container support) 
61+     tz_env  =  os .environ .get ('TZ' )
62+     if  tz_env :
63+         try :
64+             return  pytz .timezone (tz_env )
65+         except  pytz .UnknownTimeZoneError :
66+             logger .warning (f"Unknown timezone in TZ environment variable: { tz_env }  )
67+     
68+     # Try system timezone 
69+     try :
70+         # Get system timezone 
71+         local_tz_name  =  time .tzname [time .daylight ] if  time .daylight  else  time .tzname [0 ]
72+         if  local_tz_name :
73+             # Try to map common abbreviations to full timezone names 
74+             tz_mapping  =  {
75+                 'CET' : 'Europe/Amsterdam' ,
76+                 'CEST' : 'Europe/Amsterdam' , 
77+                 'EST' : 'America/New_York' ,
78+                 'EDT' : 'America/New_York' ,
79+                 'PST' : 'America/Los_Angeles' ,
80+                 'PDT' : 'America/Los_Angeles' ,
81+                 'UTC' : 'UTC' ,
82+                 'GMT' : 'UTC' 
83+             }
84+             
85+             full_tz_name  =  tz_mapping .get (local_tz_name , local_tz_name )
86+             return  pytz .timezone (full_tz_name )
87+     except :
88+         pass 
89+     
90+     # Fallback to UTC 
91+     logger .warning ("Could not detect local timezone, using UTC" )
92+     return  pytz .UTC 
93+ 
94+ LOCAL_TZ  =  get_local_timezone ()
95+ logger .info (f"Using timezone: { LOCAL_TZ }  )
96+ 
97+ def  to_local_time (utc_dt ):
98+     """Convert UTC datetime to local time""" 
99+     if  utc_dt  is  None :
100+         return  None 
101+     if  utc_dt .tzinfo  is  None :
102+         # Assume UTC if no timezone info 
103+         utc_dt  =  pytz .utc .localize (utc_dt )
104+     return  utc_dt .astimezone (LOCAL_TZ )
105+ 
106+ # Add Jinja2 filters 
107+ @app .template_filter ('local_time' ) 
108+ def  local_time_filter (utc_dt ):
109+     """Jinja2 filter to convert UTC time to local time""" 
110+     return  to_local_time (utc_dt )
111+ 
112+ @app .template_filter ('format_local_time' ) 
113+ def  format_local_time_filter (utc_dt , format_str = '%Y-%m-%d %H:%M' ):
114+     """Jinja2 filter to format UTC time as local time""" 
115+     local_dt  =  to_local_time (utc_dt )
116+     if  local_dt  is  None :
117+         return  "Never" 
118+     
119+     # Get timezone abbreviation 
120+     tz_name  =  local_dt .strftime ('%Z' )
121+     if  not  tz_name :  # Fallback if %Z doesn't work 
122+         tz_name  =  str (LOCAL_TZ ).split ('/' )[- 1 ] if  '/'  in  str (LOCAL_TZ ) else  str (LOCAL_TZ )
123+     
124+     return  f"{ local_dt .strftime (format_str )} { tz_name }  
125+ 
55126# Immediate connectivity test (runs once at startup) 
56127from  sqlalchemy  import  text 
57128with  app .app_context ():
@@ -417,7 +488,15 @@ def backup_jobs():
417488
418489@app .route ('/health' ) 
419490def  health_check ():
420-     return  jsonify ({'status' : 'healthy' , 'timestamp' : datetime .utcnow ().isoformat ()})
491+     local_time  =  datetime .now (LOCAL_TZ )
492+     utc_time  =  datetime .utcnow ()
493+     return  jsonify ({
494+         'status' : 'healthy' , 
495+         'utc_time' : utc_time .isoformat (),
496+         'local_time' : local_time .isoformat (),
497+         'timezone' : str (LOCAL_TZ ),
498+         'timezone_name' : local_time .strftime ('%Z' )
499+     })
421500
422501@app .route ('/api/scheduler/status' ) 
423502@login_required  
@@ -508,13 +587,13 @@ def backup_with_context():
508587
509588    # Create new schedule based on schedule_type 
510589    if  repository .schedule_type  ==  'hourly' :
511-         trigger  =  CronTrigger (minute = 0 )
590+         trigger  =  CronTrigger (minute = 0 ,  timezone = LOCAL_TZ )
512591    elif  repository .schedule_type  ==  'daily' :
513-         trigger  =  CronTrigger (hour = 2 , minute = 0 )  # 2 AM daily  
592+         trigger  =  CronTrigger (hour = 2 , minute = 0 ,  timezone = LOCAL_TZ )  # 2 AM local time  
514593    elif  repository .schedule_type  ==  'weekly' :
515-         trigger  =  CronTrigger (day_of_week = 0 , hour = 2 , minute = 0 )  # Sunday 2 AM 
594+         trigger  =  CronTrigger (day_of_week = 0 , hour = 2 , minute = 0 ,  timezone = LOCAL_TZ )  # Sunday 2 AM local time  
516595    elif  repository .schedule_type  ==  'monthly' :
517-         trigger  =  CronTrigger (day = 1 , hour = 2 , minute = 0 )  # 1st of month 2 AM 
596+         trigger  =  CronTrigger (day = 1 , hour = 2 , minute = 0 ,  timezone = LOCAL_TZ )  # 1st of month 2 AM local time  
518597    elif  repository .schedule_type  ==  'custom' :
519598        # Handle custom schedule 
520599        hour  =  repository .custom_hour  or  2 
@@ -525,40 +604,40 @@ def backup_with_context():
525604        if  unit  ==  'days' :
526605            # For daily intervals, use interval_trigger if more than 1 day 
527606            if  interval  ==  1 :
528-                 trigger  =  CronTrigger (hour = hour , minute = minute )  # Daily 
607+                 trigger  =  CronTrigger (hour = hour , minute = minute ,  timezone = LOCAL_TZ )  # Daily 
529608            else :
530609                # Use interval trigger for multi-day schedules 
531610                from  apscheduler .triggers .interval  import  IntervalTrigger 
532611                from  datetime  import  datetime , time 
533-                 # Calculate next run time at the specified hour/minute 
534-                 now  =  datetime .now ()
612+                 # Calculate next run time at the specified hour/minute in local timezone  
613+                 now  =  datetime .now (LOCAL_TZ )
535614                start_date  =  now .replace (hour = hour , minute = minute , second = 0 , microsecond = 0 )
536615                if  start_date  <=  now :
537-                     start_date  =  start_date . replace ( day = start_date . day  +  1 )
538-                 trigger  =  IntervalTrigger (days = interval , start_date = start_date )
616+                     start_date  =  start_date  +  timedelta ( days = 1 )
617+                 trigger  =  IntervalTrigger (days = interval , start_date = start_date ,  timezone = LOCAL_TZ )
539618        elif  unit  ==  'weeks' :
540619            # For weekly intervals 
541620            if  interval  ==  1 :
542-                 trigger  =  CronTrigger (day_of_week = 0 , hour = hour , minute = minute )  # Every Sunday 
621+                 trigger  =  CronTrigger (day_of_week = 0 , hour = hour , minute = minute ,  timezone = LOCAL_TZ )  # Every Sunday 
543622            else :
544623                from  apscheduler .triggers .interval  import  IntervalTrigger 
545624                from  datetime  import  datetime 
546-                 now  =  datetime .now ()
625+                 now  =  datetime .now (LOCAL_TZ )
547626                start_date  =  now .replace (hour = hour , minute = minute , second = 0 , microsecond = 0 )
548627                # Find next Sunday 
549628                days_until_sunday  =  (6  -  now .weekday ()) %  7 
550629                if  days_until_sunday  ==  0  and  start_date  <=  now :
551630                    days_until_sunday  =  7 
552-                 start_date  =  start_date . replace ( day = start_date . day  +  days_until_sunday )
553-                 trigger  =  IntervalTrigger (weeks = interval , start_date = start_date )
631+                 start_date  =  start_date  +  timedelta ( days = days_until_sunday )
632+                 trigger  =  IntervalTrigger (weeks = interval , start_date = start_date ,  timezone = LOCAL_TZ )
554633        elif  unit  ==  'months' :
555634            # For monthly intervals 
556635            if  interval  ==  1 :
557-                 trigger  =  CronTrigger (day = 1 , hour = hour , minute = minute )  # 1st of every month 
636+                 trigger  =  CronTrigger (day = 1 , hour = hour , minute = minute ,  timezone = LOCAL_TZ )  # 1st of every month 
558637            else :
559638                from  apscheduler .triggers .interval  import  IntervalTrigger 
560639                from  datetime  import  datetime 
561-                 now  =  datetime .now ()
640+                 now  =  datetime .now (LOCAL_TZ )
562641                start_date  =  now .replace (day = 1 , hour = hour , minute = minute , second = 0 , microsecond = 0 )
563642                if  start_date  <=  now :
564643                    # Move to next month 
@@ -567,7 +646,7 @@ def backup_with_context():
567646                    else :
568647                        start_date  =  start_date .replace (month = start_date .month  +  1 )
569648                # Note: Using weeks approximation for months since APScheduler doesn't have months interval 
570-                 trigger  =  IntervalTrigger (weeks = interval * 4 , start_date = start_date )
649+                 trigger  =  IntervalTrigger (weeks = interval * 4 , start_date = start_date ,  timezone = LOCAL_TZ )
571650        else :
572651            return   # Invalid unit 
573652    else :
0 commit comments