dashboard of nationalrail train times
at main 4.0 kB view raw
1package main 2 3import ( 4 "html/template" 5 "log" 6 "net/http" 7 "strconv" 8 "time" 9) 10 11type Server struct { 12 config *Config 13 data []StationData 14} 15 16func NewServer(config *Config) *Server { 17 return &Server{ 18 config: config, 19 } 20} 21 22func (s *Server) refreshData() { 23 log.Println("Refreshing station data...") 24 s.data = FetchAllStationsData(s.config.Stations) 25 log.Printf("Updated data for %d stations", len(s.data)) 26} 27 28func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) { 29 // Refresh data on page load 30 s.refreshData() 31 32 tmpl := `<!DOCTYPE html> 33<html> 34<head> 35 <title>Sundial - Live Train Departures</title> 36 <meta http-equiv="refresh" content="{{.RefreshInterval}}"> 37 <style> 38 body { 39 font-family: monospace; 40 margin: 20px; 41 font-size: 14px; 42 line-height: 1.4; 43 } 44 .header { 45 display: flex; 46 justify-content: space-between; 47 align-items: center; 48 } 49 table { 50 border-collapse: collapse; 51 width: 100%; 52 margin-bottom: 20px; 53 table-layout: fixed; 54 } 55 th, td { 56 padding: 8px; 57 text-align: left; 58 } 59 th:nth-child(1), td:nth-child(1) { width: 50px; padding: 4px; } /* Time */ 60 th:nth-child(2), td:nth-child(2) { width: auto; } /* Destination */ 61 th:nth-child(3), td:nth-child(3) { width: 50px; text-align: right; padding: 4px; } /* Platform */ 62 .delayed { 63 color: red; 64 } 65 .station-name { 66 font-weight: bold; 67 margin: 20px 0 10px 0; 68 } 69 </style> 70</head> 71<body> 72 <div class="header"> 73 <div>SUNDIAL - LIVE TRAIN DEPARTURES</div> 74 <div>Last updated: {{.LastUpdateTime}}</div> 75 </div> 76 <hr> 77 78 {{range .Stations}} 79 <div class="station-name">{{.Station.Name}} ({{.Station.Code}})</div> 80 81 {{if .Error}} 82 <div>ERROR: {{.Error}}</div> 83 {{else}} 84 {{if not .Departures}} 85 <div>No departures found - The National Rail website now uses dynamic JavaScript to load departure data.</div> 86 <div>This simple HTTP scraper cannot access the live data. Consider using the National Rail API instead.</div> 87 {{else}} 88 <table> 89 <tr> 90 <th>Time</th> 91 <th>Destination</th> 92 <th>Pl.</th> 93 </tr> 94 {{range .Departures}} 95 <tr> 96 <td{{if and .ExpectedTime (ne .ExpectedTime .ScheduledTime)}} class="delayed"{{end}}> 97 {{if .ExpectedTime}}{{.ExpectedTime}}{{else}}{{.ScheduledTime}}{{end}} 98 </td> 99 <td> 100 {{.Destination}}{{if .Via}} {{.Via}}{{end}} 101 </td> 102 <td class="platform">{{.Platform}}</td> 103 </tr> 104 {{end}} 105 </table> 106 {{end}} 107 {{end}} 108 109 {{end}} 110 111</body> 112</html>` 113 114 t, err := template.New("index").Parse(tmpl) 115 if err != nil { 116 http.Error(w, "Template error", http.StatusInternalServerError) 117 return 118 } 119 120 data := struct { 121 Stations []StationData 122 RefreshInterval int 123 LastUpdateTime string 124 }{ 125 Stations: s.data, 126 RefreshInterval: s.config.Server.RefreshInterval, 127 LastUpdateTime: getLastUpdateTime(s.data).Format("15:04"), 128 } 129 130 w.Header().Set("Content-Type", "text/html") 131 err = t.Execute(w, data) 132 if err != nil { 133 log.Printf("Template execution error: %v", err) 134 } 135} 136 137func (s *Server) Start() error { 138 http.HandleFunc("/", s.indexHandler) 139 140 addr := ":" + strconv.Itoa(s.config.Server.Port) 141 log.Printf("Starting server on http://localhost%s", addr) 142 log.Printf("Auto-refresh every %d seconds", s.config.Server.RefreshInterval) 143 144 return http.ListenAndServe(addr, nil) 145} 146 147// getLastUpdateTime finds the most recent update time from all stations 148func getLastUpdateTime(stations []StationData) time.Time { 149 if len(stations) == 0 { 150 return time.Now() 151 } 152 153 latest := stations[0].LastUpdate 154 for _, station := range stations[1:] { 155 if station.LastUpdate.After(latest) { 156 latest = station.LastUpdate 157 } 158 } 159 return latest 160}