smesh.go raw
1 package app
2
3 import (
4 "context"
5 "embed"
6 "fmt"
7 "io/fs"
8 "net"
9 "net/http"
10 "strings"
11 "time"
12
13 "next.orly.dev/pkg/lol/log"
14 )
15
16 //go:embed smesh/dist
17 var smeshFS embed.FS
18
19 // SmeshServer serves the embedded Smesh web client on a dedicated port.
20 type SmeshServer struct {
21 server *http.Server
22 listener net.Listener
23 port int
24 }
25
26 // NewSmeshServer creates a new smesh HTTP server on the given port.
27 func NewSmeshServer(port int) *SmeshServer {
28 return &SmeshServer{port: port}
29 }
30
31 // Start begins serving the embedded smesh SPA.
32 func (s *SmeshServer) Start(ctx context.Context) error {
33 // Extract the dist/ subtree from the embedded filesystem
34 webDist, err := fs.Sub(smeshFS, "smesh/dist")
35 if err != nil {
36 return fmt.Errorf("failed to load embedded smesh app: %w", err)
37 }
38
39 fileServer := http.FileServer(http.FS(webDist))
40
41 mux := http.NewServeMux()
42 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
43 // For SPA routing: serve index.html for paths that don't match a real file.
44 // Real assets (JS, CSS, images) are served directly.
45 path := r.URL.Path
46 if path == "/" {
47 fileServer.ServeHTTP(w, r)
48 return
49 }
50
51 // Check if the path maps to a real file in the embedded FS
52 cleanPath := strings.TrimPrefix(path, "/")
53 if f, err := webDist.Open(cleanPath); err == nil {
54 f.Close()
55 fileServer.ServeHTTP(w, r)
56 return
57 }
58
59 // SPA fallback: serve index.html for client-side routing
60 r.URL.Path = "/"
61 fileServer.ServeHTTP(w, r)
62 })
63
64 addr := fmt.Sprintf("127.0.0.1:%d", s.port)
65 s.server = &http.Server{
66 Addr: addr,
67 Handler: mux,
68 ReadTimeout: 15 * time.Second,
69 WriteTimeout: 15 * time.Second,
70 IdleTimeout: 60 * time.Second,
71 }
72
73 s.listener, err = net.Listen("tcp", addr)
74 if err != nil {
75 return fmt.Errorf("smesh: failed to listen on %s: %w", addr, err)
76 }
77
78 // Graceful shutdown on context cancellation
79 go func() {
80 <-ctx.Done()
81 shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
82 defer cancel()
83 if err := s.server.Shutdown(shutdownCtx); err != nil {
84 log.W.F("smesh server shutdown error: %v", err)
85 }
86 }()
87
88 log.I.F("smesh web client serving on http://%s", addr)
89
90 go func() {
91 if err := s.server.Serve(s.listener); err != nil && err != http.ErrServerClosed {
92 log.E.F("smesh server error: %v", err)
93 }
94 }()
95
96 return nil
97 }
98
99 // Stop shuts down the smesh server.
100 func (s *SmeshServer) Stop() {
101 if s.server != nil {
102 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
103 defer cancel()
104 s.server.Shutdown(ctx)
105 }
106 }
107