Initial commit
This commit is contained in:
99
cpone-dashboard/menu/dashboard/sse.go
Normal file
99
cpone-dashboard/menu/dashboard/sse.go
Normal file
@@ -0,0 +1,99 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user