package dashboard import ( "bytes" "fmt" "net/http" "strings" "time" ) type pollState struct { kpiHash string stationsHash string arrivalsHash string } func formatSSE(event, html string) string { data := strings.ReplaceAll(strings.TrimSpace(html), "\n", " ") return fmt.Sprintf("event: %s\ndata: %s\n\n", event, data) } func renderPartial(name string, data interface{}) string { t := parse("templates/dashboard/partials/" + name + ".html") var buf bytes.Buffer t.ExecuteTemplate(&buf, name, data) return buf.String() } func SSEStream(w http.ResponseWriter, r *http.Request) { flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "streaming not supported", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") // penting untuk nginx reverse proxy project, _ := GetProject(activeMcuID(r)) if project.McuID == 0 { return } availableDates, _ := GetCheckinDates(project.McuID) mode, dateFrom, dateTo := activeDateRange(r, project, availableDates) isLive := mode == "daily" && dateFrom == time.Now().Format("2006-01-02") var prev pollState pushKPI := func(force bool) { kpi, _ := GetKPI(project.McuID, dateFrom, dateTo) key := fmt.Sprintf("%d|%d|%d", kpi.TotalStaff, kpi.CheckedIn, kpi.CheckedOut) if force || key != prev.kpiHash { fmt.Fprint(w, formatSSE("kpi", renderPartial("kpi", kpi))) prev.kpiHash = key } } pushStations := func(force bool) { rows, _ := GetStations(project.McuID, dateFrom, dateTo) key := fmt.Sprintf("%v", rows) if force || key != prev.stationsHash { fmt.Fprint(w, formatSSE("stations", renderPartial("stations", StationsPartial{Rows: rows, IsLive: isLive}))) prev.stationsHash = key } } pushArrivals := func(force bool) { rows, _ := GetArrivals(project.McuID, dateFrom, dateTo, 8) key := fmt.Sprintf("%v", rows) if force || key != prev.arrivalsHash { fmt.Fprint(w, formatSSE("arrivals", renderPartial("arrivals", ArrivalsPartial{Rows: rows, IsLive: isLive}))) prev.arrivalsHash = key } } // Kirim data langsung saat connect (force=true) pushKPI(true) pushStations(true) pushArrivals(true) flusher.Flush() ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: pushKPI(false) pushStations(false) pushArrivals(false) flusher.Flush() case <-r.Context().Done(): // Browser disconnect — goroutine ini langsung berhenti return } } }