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}