web.go raw
1 package main
2
3 import (
4 "embed"
5 "io"
6 "io/fs"
7 "net/http"
8 "path"
9 "strings"
10 )
11
12 //go:embed all:web/dist all:web/public
13 var adminFS embed.FS
14
15 // getAdminSubFS returns the embedded filesystem for the admin UI.
16 func getAdminSubFS() (fs.FS, error) {
17 // Try dist first (built assets)
18 distFS, err := fs.Sub(adminFS, "web/dist")
19 if err == nil {
20 // Check if dist has content
21 entries, _ := fs.ReadDir(distFS, ".")
22 if len(entries) > 0 {
23 return distFS, nil
24 }
25 }
26
27 // Fall back to public (template)
28 return fs.Sub(adminFS, "web/public")
29 }
30
31 // serveAdminUI serves the embedded admin web UI.
32 func (s *AdminServer) serveAdminUI(w http.ResponseWriter, r *http.Request) {
33 fsys, err := getAdminSubFS()
34 if err != nil {
35 http.Error(w, "Admin UI not available", http.StatusInternalServerError)
36 return
37 }
38
39 // Strip /admin prefix from path
40 urlPath := r.URL.Path
41 if strings.HasPrefix(urlPath, "/admin") {
42 urlPath = strings.TrimPrefix(urlPath, "/admin")
43 }
44 urlPath = strings.TrimPrefix(urlPath, "/")
45
46 // Default to index.html
47 if urlPath == "" {
48 urlPath = "index.html"
49 }
50
51 // Try to open the file
52 f, err := fsys.Open(urlPath)
53 if err != nil {
54 // For SPA routing, serve index.html for non-existent paths
55 urlPath = "index.html"
56 f, err = fsys.Open(urlPath)
57 if err != nil {
58 http.Error(w, "Not found", http.StatusNotFound)
59 return
60 }
61 }
62 defer f.Close()
63
64 // Check if it's a directory
65 stat, err := f.Stat()
66 if err != nil {
67 http.Error(w, "Internal error", http.StatusInternalServerError)
68 return
69 }
70 if stat.IsDir() {
71 // Try index.html in the directory
72 f.Close()
73 urlPath = path.Join(urlPath, "index.html")
74 f, err = fsys.Open(urlPath)
75 if err != nil {
76 http.Error(w, "Not found", http.StatusNotFound)
77 return
78 }
79 defer f.Close()
80 }
81
82 // Set content type based on extension
83 switch path.Ext(urlPath) {
84 case ".html":
85 w.Header().Set("Content-Type", "text/html; charset=utf-8")
86 case ".css":
87 w.Header().Set("Content-Type", "text/css; charset=utf-8")
88 case ".js":
89 w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
90 case ".json":
91 w.Header().Set("Content-Type", "application/json; charset=utf-8")
92 case ".svg":
93 w.Header().Set("Content-Type", "image/svg+xml")
94 case ".png":
95 w.Header().Set("Content-Type", "image/png")
96 case ".ico":
97 w.Header().Set("Content-Type", "image/x-icon")
98 }
99
100 // Serve the file content directly
101 io.Copy(w, f)
102 }
103