package dashboard import ( "bytes" "fmt" "net/http" "strings" "time" ) type pollState struct { kpiHash string stationsHash string arrivalsHash string tatHash string trendHash 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 formatSSEData(event, data string) string { return fmt.Sprintf("event: %s\ndata: %s\n\n", event, strings.TrimSpace(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 } } pushTrend := func(force bool) { trend, _ := GetPeriodTrend(project.McuID, dateFrom, dateTo, dateFrom != dateTo) labels, checkedIn, checkedOut := []string{}, []int{}, []int{} for _, p := range trend { labels = append(labels, p.Label) checkedIn = append(checkedIn, p.CheckedIn) checkedOut = append(checkedOut, p.CheckedOut) } // Keep stable hash based on rendered arrays. key := fmt.Sprintf("%v|%v|%v", labels, checkedIn, checkedOut) if force || key != prev.trendHash { // Build valid JSON without extra deps. payload := fmt.Sprintf(`{"labels":[%s],"checkedIn":[%s],"checkedOut":[%s]}`, joinQuoted(labels), joinInts(checkedIn), joinInts(checkedOut)) fmt.Fprint(w, formatSSEData("trend", payload)) prev.trendHash = key } } pushTAT := func(force bool) { points, _ := GetPeriodTAT(project.McuID, dateFrom, dateTo, dateFrom != dateTo) labels := []string{} values := []float64{} for _, p := range points { labels = append(labels, p.Label) values = append(values, p.Value) } key := fmt.Sprintf("%v|%v", labels, values) if force || key != prev.tatHash { payload := fmt.Sprintf(`{"labels":[%s],"values":[%s]}`, joinQuoted(labels), joinFloats(values)) fmt.Fprint(w, formatSSEData("tat", payload)) prev.tatHash = key } } // Kirim data langsung saat connect (force=true) pushKPI(true) pushStations(true) pushArrivals(true) pushTAT(true) pushTrend(true) flusher.Flush() ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: pushKPI(false) pushStations(false) pushArrivals(false) pushTAT(false) pushTrend(false) flusher.Flush() case <-r.Context().Done(): // Browser disconnect — goroutine ini langsung berhenti return } } } func joinQuoted(items []string) string { if len(items) == 0 { return "" } escaped := make([]string, 0, len(items)) for _, s := range items { s = strings.ReplaceAll(s, `\`, `\\`) s = strings.ReplaceAll(s, `"`, `\"`) escaped = append(escaped, `"`+s+`"`) } return strings.Join(escaped, ",") } func joinInts(items []int) string { if len(items) == 0 { return "" } out := make([]string, 0, len(items)) for _, v := range items { out = append(out, fmt.Sprintf("%d", v)) } return strings.Join(out, ",") } func joinFloats(items []float64) string { if len(items) == 0 { return "" } out := make([]string, 0, len(items)) for _, v := range items { out = append(out, fmt.Sprintf("%.2f", v)) } return strings.Join(out, ",") }