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