Merge "Launcher: Added logout url" into main
diff --git a/core/installer/go.mod b/core/installer/go.mod
index bf6e083..a17907f 100644
--- a/core/installer/go.mod
+++ b/core/installer/go.mod
@@ -15,7 +15,6 @@
 	github.com/go-git/go-git/v5 v5.12.0
 	github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0
 	github.com/gorilla/mux v1.8.1
-	github.com/labstack/echo/v4 v4.11.4
 	github.com/libdns/gandi v1.0.3
 	github.com/libdns/libdns v0.2.2
 	github.com/miekg/dns v1.1.58
@@ -98,7 +97,6 @@
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
 	github.com/klauspost/compress v1.17.7 // indirect
-	github.com/labstack/gommon v0.4.2 // indirect
 	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
 	github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
 	github.com/lib/pq v1.10.9 // indirect
@@ -141,8 +139,6 @@
 	github.com/skeema/knownhosts v1.2.2 // indirect
 	github.com/spf13/cast v1.6.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	github.com/valyala/fasttemplate v1.2.2 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
diff --git a/core/installer/go.sum b/core/installer/go.sum
index 76bb831..21eb60a 100644
--- a/core/installer/go.sum
+++ b/core/installer/go.sum
@@ -252,10 +252,6 @@
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
-github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
-github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
-github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
 github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
@@ -395,10 +391,6 @@
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
-github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
-github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
diff --git a/core/installer/welcome/appmanager-tmpl/base.html b/core/installer/welcome/appmanager-tmpl/base.html
index e119c0f..1cd8c3c 100644
--- a/core/installer/welcome/appmanager-tmpl/base.html
+++ b/core/installer/welcome/appmanager-tmpl/base.html
@@ -3,7 +3,7 @@
 	<head>
 		<meta charset="utf-8" />
         <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
-        <link rel="stylesheet" type="text/css" href="/static/appmanager.css">
+        <link rel="stylesheet" type="text/css" href="/static/appmanager.css?v=0.0.1">
 		<meta name="viewport" content="width=device-width, initial-scale=1" />
 	</head>
 	<body>
@@ -26,6 +26,6 @@
 			  {{ block "content" . }}{{ end }}
 		  </div>
       </main>
-    <script src="/static/app-manager.js"></script>
+    <script src="/static/app-manager.js?v=0.0.1"></script>
 	</body>
 </html>
diff --git a/core/installer/welcome/appmanager.go b/core/installer/welcome/appmanager.go
index 052042b..5da26ef 100644
--- a/core/installer/welcome/appmanager.go
+++ b/core/installer/welcome/appmanager.go
@@ -12,7 +12,7 @@
 	"time"
 
 	"github.com/Masterminds/sprig/v3"
-	"github.com/labstack/echo/v4"
+	"github.com/gorilla/mux"
 
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/tasks"
@@ -81,22 +81,31 @@
 	}, nil
 }
 
+type cachingHandler struct {
+	h http.Handler
+}
+
+func (h cachingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Cache-Control", "max-age=604800")
+	h.h.ServeHTTP(w, r)
+}
+
 func (s *AppManagerServer) Start() error {
-	e := echo.New()
-	e.StaticFS("/static", echo.MustSubFS(staticAssets, "static"))
-	e.GET("/api/app-repo", s.handleAppRepo)
-	e.POST("/api/app/:slug/install", s.handleAppInstall)
-	e.GET("/api/app/:slug", s.handleApp)
-	e.GET("/api/instance/:slug", s.handleInstance)
-	e.POST("/api/instance/:slug/update", s.handleAppUpdate)
-	e.POST("/api/instance/:slug/remove", s.handleAppRemove)
-	e.GET("/", s.handleIndex)
-	e.GET("/not-installed", s.handleNotInstalledApps)
-	e.GET("/installed", s.handleInstalledApps)
-	e.GET("/app/:slug", s.handleAppUI)
-	e.GET("/instance/:slug", s.handleInstanceUI)
+	r := mux.NewRouter()
+	r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
+	r.HandleFunc("/api/app-repo", s.handleAppRepo)
+	r.HandleFunc("/api/app/{slug}/install", s.handleAppInstall).Methods(http.MethodPost)
+	r.HandleFunc("/api/app/{slug}", s.handleApp).Methods(http.MethodGet)
+	r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
+	r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
+	r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
+	r.HandleFunc("/", s.handleIndex).Methods(http.MethodGet)
+	r.HandleFunc("/not-installed", s.handleNotInstalledApps).Methods(http.MethodGet)
+	r.HandleFunc("/installed", s.handleInstalledApps).Methods(http.MethodGet)
+	r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
+	r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
 	fmt.Printf("Starting HTTP server on port: %d\n", s.port)
-	return e.Start(fmt.Sprintf(":%d", s.port))
+	return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
 }
 
 type app struct {
@@ -107,76 +116,113 @@
 	Instances        []installer.AppInstanceConfig `json:"instances,omitempty"`
 }
 
-func (s *AppManagerServer) handleAppRepo(c echo.Context) error {
+func (s *AppManagerServer) handleAppRepo(w http.ResponseWriter, r *http.Request) {
 	all, err := s.r.GetAll()
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	resp := make([]app, len(all))
 	for i, a := range all {
 		resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
 	}
-	return c.JSON(http.StatusOK, resp)
+	w.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(w).Encode(resp); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 }
 
-func (s *AppManagerServer) handleApp(c echo.Context) error {
-	slug := c.Param("slug")
+func (s *AppManagerServer) handleApp(w http.ResponseWriter, r *http.Request) {
+	slug, ok := mux.Vars(r)["slug"]
+	if !ok {
+		http.Error(w, "empty slug", http.StatusBadRequest)
+		return
+	}
 	a, err := s.r.Find(slug)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	instances, err := s.m.FindAllAppInstances(slug)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
-	return c.JSON(http.StatusOK, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
+	resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
+	w.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(w).Encode(resp); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 }
 
-func (s *AppManagerServer) handleInstance(c echo.Context) error {
-	slug := c.Param("slug")
+func (s *AppManagerServer) handleInstance(w http.ResponseWriter, r *http.Request) {
+	slug, ok := mux.Vars(r)["slug"]
+	if !ok {
+		http.Error(w, "empty slug", http.StatusBadRequest)
+		return
+	}
 	instance, err := s.m.FindInstance(slug)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	a, err := s.r.Find(instance.AppId)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
-	return c.JSON(http.StatusOK, app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}})
+	resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
+	w.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(w).Encode(resp); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 }
 
-func (s *AppManagerServer) handleAppInstall(c echo.Context) error {
-	slug := c.Param("slug")
-	contents, err := ioutil.ReadAll(c.Request().Body)
+func (s *AppManagerServer) handleAppInstall(w http.ResponseWriter, r *http.Request) {
+	slug, ok := mux.Vars(r)["slug"]
+	if !ok {
+		http.Error(w, "empty slug", http.StatusBadRequest)
+		return
+	}
+	contents, err := ioutil.ReadAll(r.Body)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	var values map[string]any
 	if err := json.Unmarshal(contents, &values); err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	log.Printf("Values: %+v\n", values)
 	a, err := installer.FindEnvApp(s.r, slug)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	log.Printf("Found application: %s\n", slug)
 	env, err := s.m.Config()
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	log.Printf("Configuration: %+v\n", env)
 	suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
 	suffix, err := suffixGen.Generate()
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	instanceId := a.Slug() + suffix
 	appDir := fmt.Sprintf("/apps/%s", instanceId)
 	namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, a.Namespace(), suffix)
 	rr, err := s.m.Install(a, instanceId, appDir, namespace, values)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
 	go s.reconciler.Reconcile(ctx)
@@ -189,33 +235,46 @@
 	})
 	s.tasks[instanceId] = t
 	go t.Start()
-	return c.String(http.StatusOK, fmt.Sprintf("/instance/%s", instanceId))
+	if _, err := fmt.Fprintf(w, "/instance/%s", instanceId); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 }
 
-func (s *AppManagerServer) handleAppUpdate(c echo.Context) error {
-	slug := c.Param("slug")
+func (s *AppManagerServer) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
+	slug, ok := mux.Vars(r)["slug"]
+	if !ok {
+		http.Error(w, "empty slug", http.StatusBadRequest)
+		return
+	}
 	appConfig, err := s.m.AppConfig(slug)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
-	contents, err := ioutil.ReadAll(c.Request().Body)
+	contents, err := ioutil.ReadAll(r.Body)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	var values map[string]any
 	if err := json.Unmarshal(contents, &values); err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	a, err := installer.FindEnvApp(s.r, appConfig.AppId)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	if _, ok := s.tasks[slug]; ok {
-		return fmt.Errorf("Update already in progress")
+		http.Error(w, "Update already in progress", http.StatusBadRequest)
+		return
 	}
 	rr, err := s.m.Update(a, slug, values)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
 	go s.reconciler.Reconcile(ctx)
@@ -225,17 +284,28 @@
 	})
 	s.tasks[slug] = t
 	go t.Start()
-	return c.String(http.StatusOK, fmt.Sprintf("/instance/%s", slug))
+	if _, err := fmt.Fprintf(w, "/instance/%s", slug); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 }
 
-func (s *AppManagerServer) handleAppRemove(c echo.Context) error {
-	slug := c.Param("slug")
+func (s *AppManagerServer) handleAppRemove(w http.ResponseWriter, r *http.Request) {
+	slug, ok := mux.Vars(r)["slug"]
+	if !ok {
+		http.Error(w, "empty slug", http.StatusBadRequest)
+		return
+	}
 	if err := s.m.Remove(slug); err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
 	go s.reconciler.Reconcile(ctx)
-	return c.String(http.StatusOK, "/")
+	if _, err := fmt.Fprint(w, "/"); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 }
 
 type PageData struct {
@@ -243,17 +313,19 @@
 	CurrentPage string
 }
 
-func (s *AppManagerServer) handleIndex(c echo.Context) error {
+func (s *AppManagerServer) handleIndex(w http.ResponseWriter, r *http.Request) {
 	all, err := s.r.GetAll()
 	if err != nil {
 		log.Printf("all apps: %v", err)
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	resp := make([]app, 0)
 	for _, a := range all {
 		instances, err := s.m.FindAllAppInstances(a.Slug())
 		if err != nil {
-			return err
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
 		}
 		resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
 	}
@@ -261,23 +333,25 @@
 		Apps:        resp,
 		CurrentPage: "ALL",
 	}
-	if err := s.tmpl.index.Execute(c.Response(), data); err != nil {
+	if err := s.tmpl.index.Execute(w, data); err != nil {
 		log.Printf("executing template: %v", err)
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
-	return nil
 }
 
-func (s *AppManagerServer) handleNotInstalledApps(c echo.Context) error {
+func (s *AppManagerServer) handleNotInstalledApps(w http.ResponseWriter, r *http.Request) {
 	all, err := s.r.GetAll()
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	resp := make([]app, 0)
 	for _, a := range all {
 		instances, err := s.m.FindAllAppInstances(a.Slug())
 		if err != nil {
-			return err
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
 		}
 		if len(instances) == 0 {
 			resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
@@ -287,22 +361,24 @@
 		Apps:        resp,
 		CurrentPage: "NOT_INSTALLED",
 	}
-	if err := s.tmpl.index.Execute(c.Response(), data); err != nil {
-		return err
+	if err := s.tmpl.index.Execute(w, data); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
-	return nil
 }
 
-func (s *AppManagerServer) handleInstalledApps(c echo.Context) error {
+func (s *AppManagerServer) handleInstalledApps(w http.ResponseWriter, r *http.Request) {
 	all, err := s.r.GetAll()
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	resp := make([]app, 0)
 	for _, a := range all {
 		instances, err := s.m.FindAllAppInstances(a.Slug())
 		if err != nil {
-			return err
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
 		}
 		if len(instances) != 0 {
 			resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
@@ -312,10 +388,10 @@
 		Apps:        resp,
 		CurrentPage: "INSTALLED",
 	}
-	if err := s.tmpl.index.Execute(c.Response(), data); err != nil {
-		return err
+	if err := s.tmpl.index.Execute(w, data); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
-	return nil
 }
 
 type appPageData struct {
@@ -327,19 +403,26 @@
 	CurrentPage       string
 }
 
-func (s *AppManagerServer) handleAppUI(c echo.Context) error {
+func (s *AppManagerServer) handleAppUI(w http.ResponseWriter, r *http.Request) {
 	global, err := s.m.Config()
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
-	slug := c.Param("slug")
+	slug, ok := mux.Vars(r)["slug"]
+	if !ok {
+		http.Error(w, "empty slug", http.StatusBadRequest)
+		return
+	}
 	a, err := installer.FindEnvApp(s.r, slug)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	instances, err := s.m.FindAllAppInstances(slug)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	data := appPageData{
 		App:               a,
@@ -347,26 +430,37 @@
 		AvailableNetworks: installer.CreateNetworks(global),
 		CurrentPage:       a.Name(),
 	}
-	return s.tmpl.app.Execute(c.Response(), data)
+	if err := s.tmpl.app.Execute(w, data); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 }
 
-func (s *AppManagerServer) handleInstanceUI(c echo.Context) error {
+func (s *AppManagerServer) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
 	global, err := s.m.Config()
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
-	slug := c.Param("slug")
+	slug, ok := mux.Vars(r)["slug"]
+	if !ok {
+		http.Error(w, "empty slug", http.StatusBadRequest)
+		return
+	}
 	instance, err := s.m.FindInstance(slug)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	a, err := installer.FindEnvApp(s.r, instance.AppId)
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	instances, err := s.m.FindAllAppInstances(a.Slug())
 	if err != nil {
-		return err
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 	t := s.tasks[slug]
 	data := appPageData{
@@ -377,5 +471,8 @@
 		Task:              t,
 		CurrentPage:       instance.Id,
 	}
-	return s.tmpl.app.Execute(c.Response(), data)
+	if err := s.tmpl.app.Execute(w, data); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 }
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 15be0eb..5d2206f 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -125,7 +125,7 @@
 
 func (s *EnvServer) Start() {
 	r := mux.NewRouter()
-	r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
+	r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
 	r.Path("/env/{key}").Methods("GET").HandlerFunc(s.monitorTask)
 	r.Path("/env/{key}").Methods("POST").HandlerFunc(s.publishDNSRecords)
 	r.Path("/").Methods("GET").HandlerFunc(s.createEnvForm)
diff --git a/core/installer/welcome/launcher.go b/core/installer/welcome/launcher.go
index 4e3f01f..8619459 100644
--- a/core/installer/welcome/launcher.go
+++ b/core/installer/welcome/launcher.go
@@ -134,7 +134,7 @@
 }
 
 func (s *LauncherServer) Start() {
-	http.Handle("/static/", http.FileServer(http.FS(files)))
+	http.Handle("/static/", cachingHandler{http.FileServer(http.FS(files))})
 	http.HandleFunc("/", s.homeHandler)
 	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
 }
diff --git a/core/installer/welcome/welcome.go b/core/installer/welcome/welcome.go
index 1592a5c..64f4cf1 100644
--- a/core/installer/welcome/welcome.go
+++ b/core/installer/welcome/welcome.go
@@ -55,7 +55,7 @@
 
 func (s *Server) Start() {
 	r := mux.NewRouter()
-	r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
+	r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
 	r.Path("/").Methods("POST").HandlerFunc(s.createAccount)
 	r.Path("/").Methods("GET").HandlerFunc(s.createAccountForm)
 	http.Handle("/", r)