package main import ( "cpone-dashboard/config" "cpone-dashboard/db" "cpone-dashboard/menu/abnormal" "cpone-dashboard/menu/arrival" "cpone-dashboard/menu/auth" "cpone-dashboard/menu/dashboard" "cpone-dashboard/menu/progress" "cpone-dashboard/menu/projects" "cpone-dashboard/menu/result" "embed" "html/template" "io/fs" "log" "net/http" "strings" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) //go:embed templates var templateFS embed.FS //go:embed static var staticFS embed.FS func main() { cfg := config.Load() if err := db.Connect(cfg.DBDSN); err != nil { log.Fatalf("db connect: %v", err) } defer db.DB.Close() dashboard.SetTemplateFuncs(template.FuncMap{ "div": func(a, b int) int { if b == 0 { return 0 } return a / b }, "mod": func(a, b int) int { return a % b }, "pct": func(a, b int) float64 { if b == 0 { return 0 } return float64(a) / float64(b) * 100 }, "stationShort": func(s string) string { s = strings.TrimPrefix(s, "Sample Station ") s = strings.TrimPrefix(s, "sample station ") return s }, "fmtDate": func(s string) string { if s == "" { return "" } layouts := []string{ "2006-01-02", "2006-01-02 15:04:05", time.RFC3339, "02/01/2006", "02/01/2006 15:04:05", } for _, layout := range layouts { if t, err := time.ParseInLocation(layout, s, time.Local); err == nil { return t.Format("02/01/2006") } } if len(s) >= 10 { return s[8:10] + "/" + s[5:7] + "/" + s[0:4] } return s }, "fmtDateTime": func(dateStr, timeStr string) string { if dateStr == "" { return "" } dateLayouts := []string{ "2006-01-02", "2006-01-02 15:04:05", "02/01/2006", "02/01/2006 15:04:05", time.RFC3339, } var datePart time.Time for _, layout := range dateLayouts { if t, err := time.ParseInLocation(layout, dateStr, time.Local); err == nil { datePart = t break } } if datePart.IsZero() { if len(dateStr) >= 10 { dateStr = dateStr[8:10] + "/" + dateStr[5:7] + "/" + dateStr[0:4] } } else { dateStr = datePart.Format("02/01/2006") } if timeStr == "" { return dateStr } timeLayouts := []string{"15:04:05", "15:04"} var timePart string for _, layout := range timeLayouts { if t, err := time.Parse(layout, timeStr); err == nil { timePart = t.Format("15:04:05") break } } if timePart == "" { timePart = timeStr } return dateStr + " " + timePart }, "initials": func(name string) string { parts := strings.Fields(name) if len(parts) == 0 { return "?" } if len(parts) == 1 { return strings.ToUpper(string([]rune(parts[0])[:1])) } return strings.ToUpper(string([]rune(parts[0])[:1]) + string([]rune(parts[len(parts)-1])[:1])) }, "slice": func(args ...int) []int { return args }, "seq": func(n int) []int { s := make([]int, n) for i := range s { s[i] = i } return s }, }) auth.Init(&templateFS, cfg.AuthSecret) projects.SetTemplateFS(&templateFS) pageFuncs := template.FuncMap{ "div": func(a, b int) int { if b == 0 { return 0 } return a / b }, "mod": func(a, b int) int { return a % b }, "pct": func(a, b int) float64 { if b == 0 { return 0 } return float64(a) / float64(b) * 100 }, "stationShort": func(s string) string { s = strings.TrimPrefix(s, "Sample Station ") s = strings.TrimPrefix(s, "sample station ") return s }, "fmtDate": func(s string) string { if s == "" { return "" } layouts := []string{ "2006-01-02", "2006-01-02 15:04:05", time.RFC3339, "02/01/2006", "02/01/2006 15:04:05", } for _, layout := range layouts { if t, err := time.ParseInLocation(layout, s, time.Local); err == nil { return t.Format("02/01/2006") } } if len(s) >= 10 { return s[8:10] + "/" + s[5:7] + "/" + s[0:4] } return s }, "fmtDateTime": func(dateStr, timeStr string) string { if dateStr == "" { return "" } dateLayouts := []string{ "2006-01-02", "2006-01-02 15:04:05", "02/01/2006", "02/01/2006 15:04:05", time.RFC3339, } var datePart time.Time for _, layout := range dateLayouts { if t, err := time.ParseInLocation(layout, dateStr, time.Local); err == nil { datePart = t break } } if datePart.IsZero() { if len(dateStr) >= 10 { dateStr = dateStr[8:10] + "/" + dateStr[5:7] + "/" + dateStr[0:4] } } else { dateStr = datePart.Format("02/01/2006") } if timeStr == "" { return dateStr } timeLayouts := []string{"15:04:05", "15:04"} var timePart string for _, layout := range timeLayouts { if t, err := time.Parse(layout, timeStr); err == nil { timePart = t.Format("15:04:05") break } } if timePart == "" { timePart = timeStr } return dateStr + " " + timePart }, "initials": func(name string) string { parts := strings.Fields(name) if len(parts) == 0 { return "?" } if len(parts) == 1 { return strings.ToUpper(string([]rune(parts[0])[:1])) } return strings.ToUpper(string([]rune(parts[0])[:1]) + string([]rune(parts[len(parts)-1])[:1])) }, "slice": func(args ...int) []int { return args }, "seq": func(n int) []int { s := make([]int, n) for i := range s { s[i] = i } return s }, } bp := cfg.BasePath // e.g. "/cpone-dashboard" or "" pageFuncs["b"] = func(path string) string { return bp + path } dashboard.SetTemplateFuncs(pageFuncs) newPageTmpl := func(files ...string) *template.Template { paths := append([]string{"templates/layout/base.html"}, files...) return template.Must(template.New("").Funcs(pageFuncs).ParseFS(templateFS, paths...)) } // Propagate basePath to all packages that redirect auth.SetBasePath(bp) dashboard.SetBasePath(bp) arrival.SetBasePath(bp) progress.SetBasePath(bp) abnormal.SetBasePath(bp) result.SetBasePath(bp) projects.SetBasePath(bp) // Dashboard pakai templateFS langsung (parse per-handler) dashboard.SetTemplateFS(&templateFS) arrival.SetTemplates(newPageTmpl("templates/arrival/index.html")) progress.SetTemplates(newPageTmpl("templates/progress/index.html")) abnormal.SetTemplates(newPageTmpl("templates/abnormal/index.html")) result.SetTemplates(newPageTmpl("templates/result/index.html")) result.SetPDFBaseURL(cfg.PDFBaseURL) r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Recoverer) // Static files — always mounted at bp+/static/ staticSub, _ := fs.Sub(staticFS, "static") staticPrefix := bp + "/static" r.Handle(staticPrefix+"/*", http.StripPrefix(staticPrefix+"/", http.FileServer(http.FS(staticSub)))) registerRoutes := func(r chi.Router) { auth.Routes(r) r.Group(func(r chi.Router) { r.Use(auth.Require(cfg.AuthSecret)) r.Get("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, bp+"/projects", http.StatusFound) }) auth.ProtectedRoutes(r) r.Route("/projects", func(r chi.Router) { projects.Routes(r) }) r.Route("/dashboard", func(r chi.Router) { dashboard.Routes(r) }) r.Route("/arrival", func(r chi.Router) { arrival.Routes(r) }) r.Route("/progress", func(r chi.Router) { progress.Routes(r) }) r.Route("/abnormal", func(r chi.Router) { abnormal.Routes(r) }) r.Route("/result", func(r chi.Router) { result.Routes(r) }) }) } if bp == "" { registerRoutes(r) } else { r.Route(bp, registerRoutes) // redirect bare /cpone-dashboard → /cpone-dashboard/ r.Get(bp, func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, bp+"/", http.StatusMovedPermanently) }) } log.Printf("server running on :%s (base path: %q)", cfg.AppPort, bp) if err := http.ListenAndServe(":"+cfg.AppPort, r); err != nil { log.Fatal(err) } }