initial commit
This commit is contained in:
		
							parent
							
								
									f3cf05f3ef
								
							
						
					
					
						commit
						65c47b720b
					
				
							
								
								
									
										387
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										387
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,387 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bufio"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						defaultPort     = "79"
 | 
				
			||||||
 | 
						weatherAPIURL   = "http://api.worldweatheronline.com/premium/v1/weather.ashx"
 | 
				
			||||||
 | 
						readTimeout     = 30 * time.Second
 | 
				
			||||||
 | 
						writeTimeout    = 30 * time.Second
 | 
				
			||||||
 | 
						httpTimeout     = 15 * time.Second
 | 
				
			||||||
 | 
						forecastDays    = 3
 | 
				
			||||||
 | 
						hourlyInterval  = 6
 | 
				
			||||||
 | 
						separatorLength = 60
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Config holds server configuration
 | 
				
			||||||
 | 
					type Config struct {
 | 
				
			||||||
 | 
						Port   string
 | 
				
			||||||
 | 
						APIKey string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Server represents the weather finger server
 | 
				
			||||||
 | 
					type Server struct {
 | 
				
			||||||
 | 
						config   *Config
 | 
				
			||||||
 | 
						client   *http.Client
 | 
				
			||||||
 | 
						logger   *log.Logger
 | 
				
			||||||
 | 
						shutdown chan struct{}
 | 
				
			||||||
 | 
						wg       sync.WaitGroup
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Weather API response structures
 | 
				
			||||||
 | 
					type WeatherResponse struct {
 | 
				
			||||||
 | 
						Data  WeatherData    `json:"data"`
 | 
				
			||||||
 | 
						Error []WeatherError `json:"error,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type WeatherData struct {
 | 
				
			||||||
 | 
						Request          []RequestInfo      `json:"request"`
 | 
				
			||||||
 | 
						NearestArea      []NearestArea      `json:"nearest_area"`
 | 
				
			||||||
 | 
						CurrentCondition []CurrentCondition `json:"current_condition"`
 | 
				
			||||||
 | 
						Weather          []DailyWeather     `json:"weather"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RequestInfo struct {
 | 
				
			||||||
 | 
						Query string `json:"query"`
 | 
				
			||||||
 | 
						Type  string `json:"type"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type NearestArea struct {
 | 
				
			||||||
 | 
						AreaName []NameValue `json:"areaName"`
 | 
				
			||||||
 | 
						Country  []NameValue `json:"country"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type NameValue struct {
 | 
				
			||||||
 | 
						Value string `json:"value"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type CurrentCondition struct {
 | 
				
			||||||
 | 
						TempC           string      `json:"temp_C"`
 | 
				
			||||||
 | 
						TempF           string      `json:"temp_F"`
 | 
				
			||||||
 | 
						WeatherDesc     []NameValue `json:"weatherDesc"`
 | 
				
			||||||
 | 
						Humidity        string      `json:"humidity"`
 | 
				
			||||||
 | 
						Visibility      string      `json:"visibility"`
 | 
				
			||||||
 | 
						Pressure        string      `json:"pressure"`
 | 
				
			||||||
 | 
						CloudCover      string      `json:"cloudcover"`
 | 
				
			||||||
 | 
						FeelsLikeC      string      `json:"FeelsLikeC"`
 | 
				
			||||||
 | 
						FeelsLikeF      string      `json:"FeelsLikeF"`
 | 
				
			||||||
 | 
						WindSpeedKmph   string      `json:"windspeedKmph"`
 | 
				
			||||||
 | 
						WindSpeedMiles  string      `json:"windspeedMiles"`
 | 
				
			||||||
 | 
						WindDir16Point  string      `json:"winddir16Point"`
 | 
				
			||||||
 | 
						ObservationTime string      `json:"observation_time"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type DailyWeather struct {
 | 
				
			||||||
 | 
						Date     string          `json:"date"`
 | 
				
			||||||
 | 
						MaxTempC string          `json:"maxtempC"`
 | 
				
			||||||
 | 
						MaxTempF string          `json:"maxtempF"`
 | 
				
			||||||
 | 
						MinTempC string          `json:"mintempC"`
 | 
				
			||||||
 | 
						MinTempF string          `json:"mintempF"`
 | 
				
			||||||
 | 
						Hourly   []HourlyWeather `json:"hourly"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type HourlyWeather struct {
 | 
				
			||||||
 | 
						Time         string      `json:"time"`
 | 
				
			||||||
 | 
						TempC        string      `json:"tempC"`
 | 
				
			||||||
 | 
						TempF        string      `json:"tempF"`
 | 
				
			||||||
 | 
						WeatherDesc  []NameValue `json:"weatherDesc"`
 | 
				
			||||||
 | 
						ChanceOfRain string      `json:"chanceofrain"`
 | 
				
			||||||
 | 
						Humidity     string      `json:"humidity"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type WeatherError struct {
 | 
				
			||||||
 | 
						Msg string `json:"msg"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func main() {
 | 
				
			||||||
 | 
						config := &Config{
 | 
				
			||||||
 | 
							Port:   getEnvOrDefault("WEATHER_PORT", defaultPort),
 | 
				
			||||||
 | 
							APIKey: os.Getenv("WEATHER_API_KEY"),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if config.APIKey == "" {
 | 
				
			||||||
 | 
							log.Fatal("WEATHER_API_KEY environment variable is not set")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						server := NewServer(config)
 | 
				
			||||||
 | 
						if err := server.Start(); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("Server failed to start: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewServer creates a new weather finger server instance
 | 
				
			||||||
 | 
					func NewServer(config *Config) *Server {
 | 
				
			||||||
 | 
						return &Server{
 | 
				
			||||||
 | 
							config: config,
 | 
				
			||||||
 | 
							client: &http.Client{
 | 
				
			||||||
 | 
								Timeout: httpTimeout,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							logger:   log.New(os.Stdout, "[WeatherServer] ", log.LstdFlags),
 | 
				
			||||||
 | 
							shutdown: make(chan struct{}),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Start begins listening for connections
 | 
				
			||||||
 | 
					func (s *Server) Start() error {
 | 
				
			||||||
 | 
						listener, err := net.Listen("tcp", ":"+s.config.Port)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to start listener: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer listener.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s.logger.Printf("Weather Finger Server started on port %s", s.config.Port)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case <-s.shutdown:
 | 
				
			||||||
 | 
								s.logger.Println("Shutting down server...")
 | 
				
			||||||
 | 
								s.wg.Wait()
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								conn, err := listener.Accept()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									s.logger.Printf("Failed to accept connection: %v", err)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								s.wg.Add(1)
 | 
				
			||||||
 | 
								go s.handleConnection(conn)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Stop gracefully shuts down the server
 | 
				
			||||||
 | 
					func (s *Server) Stop() {
 | 
				
			||||||
 | 
						close(s.shutdown)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleConnection processes a single client connection
 | 
				
			||||||
 | 
					func (s *Server) handleConnection(conn net.Conn) {
 | 
				
			||||||
 | 
						defer s.wg.Done()
 | 
				
			||||||
 | 
						defer conn.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := s.processRequest(conn); err != nil {
 | 
				
			||||||
 | 
							s.logger.Printf("Error processing request: %v", err)
 | 
				
			||||||
 | 
							fmt.Fprintf(conn, "Error: %v\n", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// processRequest reads the query and writes the weather response
 | 
				
			||||||
 | 
					func (s *Server) processRequest(conn net.Conn) error {
 | 
				
			||||||
 | 
						// Set timeouts
 | 
				
			||||||
 | 
						conn.SetReadDeadline(time.Now().Add(readTimeout))
 | 
				
			||||||
 | 
						conn.SetWriteDeadline(time.Now().Add(writeTimeout))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Read request
 | 
				
			||||||
 | 
						reader := bufio.NewReader(conn)
 | 
				
			||||||
 | 
						request, err := reader.ReadString('\n')
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if err == io.EOF {
 | 
				
			||||||
 | 
								return fmt.Errorf("client disconnected")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to read request: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						query := strings.TrimSpace(request)
 | 
				
			||||||
 | 
						if query == "" {
 | 
				
			||||||
 | 
							return fmt.Errorf("empty query")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						clientAddr := conn.RemoteAddr().String()
 | 
				
			||||||
 | 
						s.logger.Printf("Request from %s: '%s'", clientAddr, query)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get weather data
 | 
				
			||||||
 | 
						ctx, cancel := context.WithTimeout(context.Background(), httpTimeout)
 | 
				
			||||||
 | 
						defer cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						forecast, err := s.getWeatherForecast(ctx, query)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to get weather: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Format and send response
 | 
				
			||||||
 | 
						response := formatWeatherResponse(forecast)
 | 
				
			||||||
 | 
						_, err = conn.Write([]byte(response))
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getWeatherForecast fetches weather data from the API
 | 
				
			||||||
 | 
					func (s *Server) getWeatherForecast(ctx context.Context, query string) (*WeatherResponse, error) {
 | 
				
			||||||
 | 
						params := url.Values{
 | 
				
			||||||
 | 
							"key":             {s.config.APIKey},
 | 
				
			||||||
 | 
							"q":               {query},
 | 
				
			||||||
 | 
							"format":          {"json"},
 | 
				
			||||||
 | 
							"num_of_days":     {fmt.Sprintf("%d", forecastDays)},
 | 
				
			||||||
 | 
							"tp":              {fmt.Sprintf("%d", hourlyInterval)},
 | 
				
			||||||
 | 
							"includelocation": {"yes"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiURL := fmt.Sprintf("%s?%s", weatherAPIURL, params.Encode())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to create request: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := s.client.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("API request failed: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if resp.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						body, err := io.ReadAll(resp.Body)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to read response: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var weatherData WeatherResponse
 | 
				
			||||||
 | 
						if err := json.Unmarshal(body, &weatherData); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to parse response: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(weatherData.Error) > 0 {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("API error: %s", weatherData.Error[0].Msg)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &weatherData, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// formatWeatherResponse creates a human-readable weather report
 | 
				
			||||||
 | 
					func formatWeatherResponse(response *WeatherResponse) string {
 | 
				
			||||||
 | 
						var sb strings.Builder
 | 
				
			||||||
 | 
						data := &response.Data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Write header
 | 
				
			||||||
 | 
						writeHeader(&sb, data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Write current conditions
 | 
				
			||||||
 | 
						writeCurrentConditions(&sb, data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Write forecast
 | 
				
			||||||
 | 
						writeForecast(&sb, data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return sb.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// writeHeader writes the location header
 | 
				
			||||||
 | 
					func writeHeader(sb *strings.Builder, data *WeatherData) {
 | 
				
			||||||
 | 
						if len(data.NearestArea) > 0 {
 | 
				
			||||||
 | 
							area := data.NearestArea[0]
 | 
				
			||||||
 | 
							areaName := getFirstValue(area.AreaName)
 | 
				
			||||||
 | 
							countryName := getFirstValue(area.Country)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							sb.WriteString(fmt.Sprintf("Weather forecast for: %s, %s\n", areaName, countryName))
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							sb.WriteString("Weather forecast\n")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sb.WriteString(strings.Repeat("=", separatorLength))
 | 
				
			||||||
 | 
						sb.WriteString("\n")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// writeCurrentConditions writes the current weather conditions
 | 
				
			||||||
 | 
					func writeCurrentConditions(sb *strings.Builder, data *WeatherData) {
 | 
				
			||||||
 | 
						if len(data.CurrentCondition) == 0 {
 | 
				
			||||||
 | 
							sb.WriteString("CURRENT CONDITIONS: Not available\n\n")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						current := data.CurrentCondition[0]
 | 
				
			||||||
 | 
						desc := getFirstValue(current.WeatherDesc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sb.WriteString("CURRENT CONDITIONS:\n")
 | 
				
			||||||
 | 
						fmt.Fprintf(sb, "  Temperature: %s°C (%s°F) - feels like %s°C (%s°F)\n",
 | 
				
			||||||
 | 
							current.TempC, current.TempF, current.FeelsLikeC, current.FeelsLikeF)
 | 
				
			||||||
 | 
						fmt.Fprintf(sb, "  Conditions: %s\n", desc)
 | 
				
			||||||
 | 
						fmt.Fprintf(sb, "  Humidity: %s%%\n", current.Humidity)
 | 
				
			||||||
 | 
						fmt.Fprintf(sb, "  Wind: %s km/h %s (%s mph)\n",
 | 
				
			||||||
 | 
							current.WindSpeedKmph, current.WindDir16Point, current.WindSpeedMiles)
 | 
				
			||||||
 | 
						fmt.Fprintf(sb, "  Pressure: %s mb\n", current.Pressure)
 | 
				
			||||||
 | 
						fmt.Fprintf(sb, "  Visibility: %s km\n", current.Visibility)
 | 
				
			||||||
 | 
						fmt.Fprintf(sb, "  Cloud Cover: %s%%\n", current.CloudCover)
 | 
				
			||||||
 | 
						fmt.Fprintf(sb, "  Observed: %s\n", current.ObservationTime)
 | 
				
			||||||
 | 
						sb.WriteString("\n")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// writeForecast writes the daily forecast
 | 
				
			||||||
 | 
					func writeForecast(sb *strings.Builder, data *WeatherData) {
 | 
				
			||||||
 | 
						if len(data.Weather) == 0 {
 | 
				
			||||||
 | 
							sb.WriteString("FORECAST: Not available\n")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmt.Fprintf(sb, "%d-DAY FORECAST:\n", len(data.Weather))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, day := range data.Weather {
 | 
				
			||||||
 | 
							dateStr := formatDate(day.Date)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							fmt.Fprintf(sb, "  %s:\n", dateStr)
 | 
				
			||||||
 | 
							fmt.Fprintf(sb, "    High: %s°C (%s°F)  Low: %s°C (%s°F)\n",
 | 
				
			||||||
 | 
								day.MaxTempC, day.MaxTempF, day.MinTempC, day.MinTempF)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(day.Hourly) > 0 {
 | 
				
			||||||
 | 
								sb.WriteString("    Hourly:\n")
 | 
				
			||||||
 | 
								for _, hour := range day.Hourly {
 | 
				
			||||||
 | 
									timeStr := formatTime(hour.Time)
 | 
				
			||||||
 | 
									desc := getFirstValue(hour.WeatherDesc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									fmt.Fprintf(sb, "      %s: %s°C (%s°F) %s (rain: %s%%, humidity: %s%%)\n",
 | 
				
			||||||
 | 
										timeStr, hour.TempC, hour.TempF, desc, hour.ChanceOfRain, hour.Humidity)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Helper functions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getFirstValue safely extracts the first value from a NameValue slice
 | 
				
			||||||
 | 
					func getFirstValue(values []NameValue) string {
 | 
				
			||||||
 | 
						if len(values) > 0 {
 | 
				
			||||||
 | 
							return values[0].Value
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "N/A"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// formatDate formats a date string for display
 | 
				
			||||||
 | 
					func formatDate(dateStr string) string {
 | 
				
			||||||
 | 
						date, err := time.Parse("2006-01-02", dateStr)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return dateStr
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return date.Format("Mon, Jan 2")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// formatTime formats API time values to HH:MM format
 | 
				
			||||||
 | 
					func formatTime(timeStr string) string {
 | 
				
			||||||
 | 
						// Normalize to 4-digit format
 | 
				
			||||||
 | 
						padded := fmt.Sprintf("%04s", timeStr)
 | 
				
			||||||
 | 
						if len(padded) >= 4 {
 | 
				
			||||||
 | 
							return padded[:2] + ":" + padded[2:4]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return timeStr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getEnvOrDefault returns environment variable value or default
 | 
				
			||||||
 | 
					func getEnvOrDefault(key, defaultValue string) string {
 | 
				
			||||||
 | 
						if value := os.Getenv(key); value != "" {
 | 
				
			||||||
 | 
							return value
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return defaultValue
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user