appmanager: replace svelte implementation with go based one
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index 90f2c4b..6027204 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -1,32 +1,25 @@
 package main
 
 import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"log"
 	"net"
-	"net/http"
-	"net/http/httputil"
-	"net/url"
 	"os"
 
 	"github.com/go-git/go-billy/v5/memfs"
 	"github.com/go-git/go-git/v5"
 	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 	"github.com/go-git/go-git/v5/storage/memory"
-	"github.com/labstack/echo/v4"
 	"github.com/spf13/cobra"
 	"golang.org/x/crypto/ssh"
 
 	"github.com/giolekva/pcloud/core/installer"
+	"github.com/giolekva/pcloud/core/installer/welcome"
 )
 
 var appManagerFlags struct {
-	sshKey   string
-	repoAddr string
-	port     int
+	sshKey     string
+	repoAddr   string
+	port       int
+	webAppAddr string
 }
 
 func appManagerCmd() *cobra.Command {
@@ -52,6 +45,12 @@
 		8080,
 		"",
 	)
+	cmd.Flags().StringVar(
+		&appManagerFlags.webAppAddr,
+		"web-app-addr",
+		"",
+		"",
+	)
 	return cmd
 }
 
@@ -80,261 +79,16 @@
 		return err
 	}
 	r := installer.NewInMemoryAppRepository[installer.StoreApp](installer.CreateStoreApps())
-	s := &server{
-		port: appManagerFlags.port,
-		m:    m,
-		r:    r,
-	}
-	s.start()
+	s := welcome.NewAppManagerServer(
+		appManagerFlags.port,
+		appManagerFlags.webAppAddr,
+		m,
+		r,
+	)
+	s.Start()
 	return nil
 }
 
-type server struct {
-	port int
-	m    *installer.AppManager
-	r    installer.AppRepository[installer.StoreApp]
-}
-
-func (s *server) start() {
-	e := echo.New()
-	e.GET("/api/app-repo", s.handleAppRepo)
-	e.POST("/api/app/:slug/render", s.handleAppRender)
-	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)
-	webapp, err := url.Parse("http://localhost:5173")
-	if err != nil {
-		panic(err)
-	}
-	// var f ff
-	e.Any("/*", echo.WrapHandler(httputil.NewSingleHostReverseProxy(webapp)))
-	// e.Any("/*", echo.WrapHandler(&f))
-	fmt.Printf("Starting HTTP server on port: %d\n", s.port)
-	log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
-}
-
-type app struct {
-	Name             string                `json:"name"`
-	Icon             string                `json:"icon"`
-	ShortDescription string                `json:"shortDescription"`
-	Slug             string                `json:"slug"`
-	Schema           string                `json:"schema"`
-	Instances        []installer.AppConfig `json:"instances,omitempty"`
-}
-
-func (s *server) handleAppRepo(c echo.Context) error {
-	all, err := s.r.GetAll()
-	if err != nil {
-		return err
-	}
-	resp := make([]app, len(all))
-	for i, a := range all {
-		resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, nil}
-	}
-	return c.JSON(http.StatusOK, resp)
-}
-
-func (s *server) handleApp(c echo.Context) error {
-	slug := c.Param("slug")
-	a, err := s.r.Find(slug)
-	if err != nil {
-		return err
-	}
-	instances, err := s.m.FindAllInstances(slug)
-	if err != nil {
-		return err
-	}
-	return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, instances})
-}
-
-func (s *server) handleInstance(c echo.Context) error {
-	slug := c.Param("slug")
-	instance, err := s.m.FindInstance(slug)
-	if err != nil {
-		return err
-	}
-	values, ok := instance.Config["Values"].(map[string]any)
-	if !ok {
-		return fmt.Errorf("Expected map")
-	}
-	for k, v := range values {
-		if k == "Network" {
-			n, ok := v.(map[string]any)
-			if !ok {
-				return fmt.Errorf("Expected map")
-			}
-			values["Network"], ok = n["Name"]
-			if !ok {
-				return fmt.Errorf("Missing Name")
-			}
-			break
-		}
-
-	}
-	a, err := s.r.Find(instance.Id)
-	if err != nil {
-		return err
-	}
-	return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, []installer.AppConfig{instance}})
-}
-
-type file struct {
-	Name     string `json:"name"`
-	Contents string `json:"contents"`
-}
-
-type rendered struct {
-	Readme string `json:"readme"`
-	Files  []file `json:"files"`
-}
-
-type network struct {
-	Name              string
-	IngressClass      string
-	CertificateIssuer string
-	Domain            string
-}
-
-func createNetworks(global installer.Config) []network {
-	return []network{
-		{
-			Name:              "Public",
-			IngressClass:      fmt.Sprintf("%s-ingress-public", global.Values.PCloudEnvName),
-			CertificateIssuer: fmt.Sprintf("%s-public", global.Values.Id),
-			Domain:            global.Values.Domain,
-		},
-		{
-			Name:         "Private",
-			IngressClass: fmt.Sprintf("%s-ingress-private", global.Values.Id),
-			Domain:       global.Values.PrivateDomain,
-		},
-	}
-}
-
-func (s *server) handleAppRender(c echo.Context) error {
-	slug := c.Param("slug")
-	contents, err := ioutil.ReadAll(c.Request().Body)
-	if err != nil {
-		return err
-	}
-	global, err := s.m.Config()
-	if err != nil {
-		return err
-	}
-	var values map[string]any
-	if err := json.Unmarshal(contents, &values); err != nil {
-		return err
-	}
-	if network, ok := values["Network"]; ok {
-		for _, n := range createNetworks(global) {
-			if n.Name == network { // TODO(giolekva): handle not found
-				values["Network"] = n
-			}
-		}
-	}
-	all := map[string]any{
-		"Global": global.Values,
-		"Values": values,
-	}
-	a, err := s.r.Find(slug)
-	if err != nil {
-		return err
-	}
-	var readme bytes.Buffer
-	if err := a.Readme.Execute(&readme, all); err != nil {
-		return err
-	}
-	var resp rendered
-	resp.Readme = readme.String()
-	for _, tmpl := range a.Templates { // TODO(giolekva): deduplicate with Install
-		var f bytes.Buffer
-		if err := tmpl.Execute(&f, all); err != nil {
-			fmt.Printf("%+v\n", all)
-			fmt.Println(err.Error())
-			return err
-		} else {
-			resp.Files = append(resp.Files, file{tmpl.Name(), f.String()})
-		}
-	}
-	out, err := json.Marshal(resp)
-	if err != nil {
-		return err
-	}
-	if _, err := c.Response().Writer.Write(out); err != nil {
-		return err
-	}
-	return nil
-}
-
-func (s *server) handleAppInstall(c echo.Context) error {
-	slug := c.Param("slug")
-	contents, err := ioutil.ReadAll(c.Request().Body)
-	if err != nil {
-		return err
-	}
-	var values map[string]any
-	if err := json.Unmarshal(contents, &values); err != nil {
-		return err
-	}
-	a, err := s.r.Find(slug)
-	if err != nil {
-		return err
-	}
-	config, err := s.m.Config()
-	if err != nil {
-		return err
-	}
-	if network, ok := values["Network"]; ok {
-		for _, n := range createNetworks(config) {
-			if n.Name == network { // TODO(giolekva): handle not found
-				values["Network"] = n
-			}
-		}
-	}
-	nsGen := installer.NewPrefixGenerator(config.Values.NamespacePrefix)
-	suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
-	if err := s.m.Install(a.App, nsGen, suffixGen, values); err != nil {
-		return err
-	}
-	return c.String(http.StatusOK, "Installed")
-}
-
-func (s *server) handleAppUpdate(c echo.Context) error {
-	slug := c.Param("slug")
-	appConfig, err := s.m.AppConfig(slug)
-	if err != nil {
-		return err
-	}
-	contents, err := ioutil.ReadAll(c.Request().Body)
-	if err != nil {
-		return err
-	}
-	var values map[string]any
-	if err := json.Unmarshal(contents, &values); err != nil {
-		return err
-	}
-	a, err := s.r.Find(appConfig.Id)
-	if err != nil {
-		return err
-	}
-	config, err := s.m.Config()
-	if err != nil {
-		return err
-	}
-	if network, ok := values["Network"]; ok {
-		for _, n := range createNetworks(config) {
-			if n.Name == network { // TODO(giolekva): handle not found
-				values["Network"] = n
-			}
-		}
-	}
-	if err := s.m.Update(a.App, slug, values); err != nil {
-		return err
-	}
-	return c.String(http.StatusOK, "Installed")
-}
-
 func cloneRepo(address string, signer ssh.Signer) (*git.Repository, error) {
 	return git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
 		URL:             address,