66import base64
77import io
88from PIL import Image
9- from typing import Any , Optional , List , Tuple
9+ from typing import Optional , List , Tuple
1010
11- try :
12- from ui .models import MonitoringData
13- except (ModuleNotFoundError , ImportError ):
14- from models import MonitoringData
15-
16- try :
17- from ui .config import Config
18- except (ModuleNotFoundError , ImportError ):
19- from config import Config
11+ from models import MonitoringData
12+ from config import Config
2013
2114
2215class ThemeColors :
@@ -43,7 +36,7 @@ class UIComponents:
4336 """UI component generator class"""
4437
4538 @staticmethod
46- def _render_markdown (md_text : str ) -> str :
39+ async def _render_markdown (md_text : str ) -> str :
4740 """Render markdown text to HTML. Falls back to simple replacements if markdown package not installed."""
4841
4942 if not md_text :
@@ -59,7 +52,7 @@ def _render_markdown(md_text: str) -> str:
5952
6053
6154 @staticmethod
62- def _get_traffic_density_color (density : int ) -> str :
55+ async def _get_traffic_density_color (density : int ) -> str :
6356 """Get background color based on traffic density
6457
6558 Args:
@@ -74,8 +67,9 @@ def _get_traffic_density_color(density: int) -> str:
7467 return "#ffff99" # Yellow for moderate density
7568 else :
7669 return "#ffffff" # Default white for low density
70+
7771 @staticmethod
78- def create_header (monitoring_data : Optional [MonitoringData ] = None ) -> str :
72+ async def create_header (monitoring_data : Optional [MonitoringData ] = None ) -> str :
7973 """Create the header section with system title and status"""
8074 colors = ThemeColors .get_colors ()
8175
@@ -96,7 +90,7 @@ def create_header(monitoring_data: Optional[MonitoringData] = None) -> str:
9690 """
9791
9892 @staticmethod
99- def create_traffic_summary (monitoring_data : Optional [MonitoringData ]) -> str :
93+ async def create_traffic_summary (monitoring_data : Optional [MonitoringData ]) -> str :
10094 """Create traffic summary cards"""
10195 if not monitoring_data :
10296 return "<p style='text-align: center; color: #ef4444;'>No traffic data available</p>"
@@ -106,10 +100,10 @@ def create_traffic_summary(monitoring_data: Optional[MonitoringData]) -> str:
106100 total_pedestrians = monitoring_data .get_total_pedestrians ()
107101
108102 # Get background colors for each direction based on traffic density
109- north_bg_color = UIComponents ._get_traffic_density_color (data .northbound_density )
110- south_bg_color = UIComponents ._get_traffic_density_color (data .southbound_density )
111- east_bg_color = UIComponents ._get_traffic_density_color (data .eastbound_density )
112- west_bg_color = UIComponents ._get_traffic_density_color (data .westbound_density )
103+ north_bg_color = await UIComponents ._get_traffic_density_color (data .northbound_density )
104+ south_bg_color = await UIComponents ._get_traffic_density_color (data .southbound_density )
105+ east_bg_color = await UIComponents ._get_traffic_density_color (data .eastbound_density )
106+ west_bg_color = await UIComponents ._get_traffic_density_color (data .westbound_density )
113107
114108 return f"""
115109 <div style="background: { colors ['bg_primary' ]} ; border-radius: 12px; padding: 20px; margin: 10px 0; border: 1px solid { colors ['border' ]} ; box-shadow: { colors ['shadow' ]} ;">
@@ -150,7 +144,7 @@ def create_traffic_summary(monitoring_data: Optional[MonitoringData]) -> str:
150144 """
151145
152146 @staticmethod
153- def create_debug_panel (monitoring_data : Optional [MonitoringData ]) -> str :
147+ async def create_debug_panel (monitoring_data : Optional [MonitoringData ]) -> str :
154148 """
155149 Create a hidden debug panel which is shown only when the debug checkbox is enabled.
156150 Contains timestamp info about data and images for each direction.
@@ -193,7 +187,7 @@ def create_debug_panel(monitoring_data: Optional[MonitoringData]) -> str:
193187
194188
195189 @staticmethod
196- def create_environmental_panel (monitoring_data : Optional [MonitoringData ]) -> str :
190+ async def create_environmental_panel (monitoring_data : Optional [MonitoringData ]) -> str :
197191 """Create environmental data panel"""
198192 if not monitoring_data :
199193 return "<p style='text-align: center; color: #ef4444;'>No environmental data available</p>"
@@ -244,17 +238,6 @@ def create_environmental_panel(monitoring_data: Optional[MonitoringData]) -> str
244238 daytime_status = "Night time"
245239 daytime_icon = "🌙"
246240
247- # Format forecast period
248- forecast_period = "Current"
249- if weather .start_time and weather .end_time :
250- try :
251- from datetime import datetime
252- start_dt = datetime .fromisoformat (weather .start_time .replace ('Z' , '+00:00' ))
253- end_dt = datetime .fromisoformat (weather .end_time .replace ('Z' , '+00:00' ))
254- forecast_period = f"{ start_dt .strftime ('%H:%M' )} - { end_dt .strftime ('%H:%M' )} "
255- except :
256- forecast_period = "Current Hour"
257-
258241 # Use relative humidity from API if available, otherwise use the estimated value
259242 display_humidity = weather .relative_humidity if weather .relative_humidity is not None else weather .humidity_percent
260243
@@ -265,7 +248,7 @@ def create_environmental_panel(monitoring_data: Optional[MonitoringData]) -> str
265248 <!-- Primary Weather Metrics -->
266249 <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 18px;">
267250 <div style="text-align: center; background: { colors ['bg_card' ]} ; padding: 15px; border-radius: 8px; box-shadow: { colors ['shadow' ]} ; border: 1px solid { colors ['border' ]} ;">
268- <div style="font-size: 1.5em; color: #fbbf24; font-weight: bold; margin-bottom: 5px;">{ int ( weather .temperature_fahrenheit ) } °{ weather .temperature_unit } </div>
251+ <div style="font-size: 1.5em; color: #fbbf24; font-weight: bold; margin-bottom: 5px;">{ weather .temperature_fahrenheit } °{ weather .temperature_unit } </div>
269252 <div style="color: { colors ['text_secondary' ]} ; font-size: 0.9em; font-weight: 500;">TEMPERATURE</div>
270253 </div>
271254 <div style="text-align: center; background: { colors ['bg_card' ]} ; padding: 15px; border-radius: 8px; box-shadow: { colors ['shadow' ]} ; border: 1px solid { colors ['border' ]} ;">
@@ -302,7 +285,7 @@ def create_environmental_panel(monitoring_data: Optional[MonitoringData]) -> str
302285 """
303286
304287 @staticmethod
305- def create_alerts_panel (monitoring_data : Optional [MonitoringData ]) -> str :
288+ async def create_alerts_panel (monitoring_data : Optional [MonitoringData ]) -> str :
306289 """Create alerts panel with structured alerts and recommendations"""
307290 if not monitoring_data :
308291 return "<p style='text-align: center; color: #ef4444;'>No alerts data available</p>"
@@ -394,7 +377,7 @@ def create_alerts_panel(monitoring_data: Optional[MonitoringData]) -> str:
394377 </div>
395378 """
396379
397- analysis_html = UIComponents ._render_markdown (monitoring_data .vlm_analysis .analysis )
380+ analysis_html = await UIComponents ._render_markdown (monitoring_data .vlm_analysis .analysis )
398381
399382 return f"""
400383 <div style="background: { colors ['bg_primary' ]} ; border-radius: 12px; padding: 20px; margin: 10px 0; box-shadow: { colors ['shadow' ]} ; border: 1px solid { colors ['border' ]} ;">
@@ -417,7 +400,7 @@ def create_alerts_panel(monitoring_data: Optional[MonitoringData]) -> str:
417400 """
418401
419402 @staticmethod
420- def create_camera_images (monitoring_data : Optional [MonitoringData ]) -> List [Tuple [str , str ]]:
403+ async def create_camera_images (monitoring_data : Optional [MonitoringData ]) -> List [Tuple [str , str ]]:
421404 """Create camera images for display in Gradio Gallery"""
422405 if not monitoring_data or not monitoring_data .camera_images :
423406 return []
@@ -459,98 +442,12 @@ def create_camera_images(monitoring_data: Optional[MonitoringData]) -> List[Tupl
459442 continue
460443
461444 return image_list
462-
463- @staticmethod
464- def create_camera_grid_html (monitoring_data : Optional [MonitoringData ]) -> str :
465- """Create an HTML grid display of camera images"""
466- if not monitoring_data or not monitoring_data .camera_images :
467- return "<p style='text-align: center; color: #ef4444;'>No camera images available</p>"
468-
469- colors = ThemeColors .get_colors ()
470- cameras_html = ""
471-
472- # Define camera order for consistent layout - updated for new API format
473- camera_order = ["north_camera" , "east_camera" , "south_camera" , "west_camera" ]
474-
475- # If the expected camera keys don't exist, use whatever keys are available
476- available_cameras = list (monitoring_data .camera_images .keys ())
477- cameras_to_display = [cam for cam in camera_order if cam in available_cameras ] or available_cameras
478-
479- for camera_key in cameras_to_display :
480- if camera_key in monitoring_data .camera_images :
481- camera_data = monitoring_data .camera_images [camera_key ]
482-
483- # Handle both CameraData objects and dict structures from API
484- if hasattr (camera_data , 'image_base64' ):
485- # CameraData object
486- image_base64 = camera_data .image_base64
487- direction = camera_data .direction
488- camera_id = camera_data .camera_id
489- elif isinstance (camera_data , dict ):
490- # Dict from API
491- image_base64 = camera_data .get ('image_base64' )
492- direction = camera_data .get ('direction' , 'unknown' )
493- camera_id = camera_data .get ('camera_id' , 'unknown' )
494- else :
495- continue
496-
497- if image_base64 :
498- # Create data URL for image
499- image_src = f"data:image/jpeg;base64,{ image_base64 } "
500- border_color = colors ['border' ]
501-
502- cameras_html += f"""
503- <div style="background: { colors ['bg_card' ]} ; border-radius: 8px; padding: 15px; text-align: center;
504- box-shadow: { colors ['shadow' ]} ; width: 100%; border: 1px solid { border_color } ;">
505- <h4 style="color: { colors ['text_primary' ]} ; margin: 0 0 12px 0; font-size: 0.95em; font-weight: 600;">
506- { direction .upper ()} VIEW - { camera_id }
507- </h4>
508- <div style="position: relative; display: block; width: 100%;">
509- <img src="{ image_src } "
510- style="width: 100%; height: 200px; object-fit: cover;
511- border-radius: 6px; border: 2px solid { border_color } ;
512- box-shadow: { colors ['shadow' ]} ; display: block;"
513- alt="{ direction } view">
514- <div style="position: absolute; top: 8px; right: 8px;
515- background: rgba(220,38,38,0.9); color: white;
516- padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: bold;
517- box-shadow: 0 2px 4px rgba(0,0,0,0.3);">
518- ● LIVE
519- </div>
520- </div>
521- </div>
522- """
523- else :
524- cameras_html += f"""
525- <div style="background: { colors ['bg_card' ]} ; border-radius: 8px; padding: 15px; text-align: center;
526- box-shadow: { colors ['shadow' ]} ; width: 100%; border: 1px solid { colors ['border' ]} ;">
527- <h4 style="color: { colors ['text_primary' ]} ; margin: 0 0 12px 0; font-size: 0.95em; font-weight: 600;">
528- { direction .upper ()} VIEW - { camera_id }
529- </h4>
530- <div style="background: { colors ['bg_secondary' ]} ; border-radius: 6px; padding: 40px; color: { colors ['text_secondary' ]} ;
531- height: 200px; display: flex; flex-direction: column; justify-content: center;
532- align-items: center; border: 2px solid { colors ['border' ]} ; box-shadow: { colors ['shadow' ]} ;">
533- <div style="font-size: 48px; margin-bottom: 12px; opacity: 0.7;">📷</div>
534- <div style="font-size: 13px; font-weight: 500;">No image available</div>
535- </div>
536- </div>
537- """
538-
539- return f"""
540- <div style="background: { colors ['bg_primary' ]} ; border-radius: 12px; padding: 20px; margin: 10px 0; box-shadow: { colors ['shadow' ]} ; border: 1px solid { colors ['border' ]} ;">
541- <h3 style="color: { colors ['text_primary' ]} ; margin: 0 0 20px 0; text-align: center; font-size: 1.2em;">📹 Camera Feeds</h3>
542- <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px;">
543- { cameras_html }
544- </div>
545- </div>
546- """
547445
548446 @staticmethod
549- def create_system_info (monitoring_data : Optional [MonitoringData ] = None ) -> str :
447+ async def create_system_info (monitoring_data : Optional [MonitoringData ] = None ) -> str :
550448 """Create system information footer with current status"""
551449 # Use UTC and consistent formatting for both current time and last update
552450 from datetime import datetime , timezone
553- from data_loader import get_last_update_time
554451
555452 colors = ThemeColors .get_colors ()
556453
@@ -564,8 +461,10 @@ def create_system_info(monitoring_data: Optional[MonitoringData] = None) -> str:
564461 if monitoring_data :
565462 # Prefer a nicely formatted last update using the data loader helper
566463 try :
567- last_update = get_last_update_time (monitoring_data ) or current_time
568- except Exception :
464+ timestamp = datetime .fromisoformat (monitoring_data .timestamp .replace ('Z' , '+00:00' ))
465+ last_update = timestamp .strftime ("%Y-%m-%d %H:%M:%S UTC" )
466+ # TODO - Show time since last update in minutes/seconds for better understanding
467+ except Exception as e :
569468 # Fallback to raw timestamp or current time
570469 last_update = monitoring_data .timestamp or current_time
571470 system_status = "ONLINE"
0 commit comments