installer: separate infra and repo apps. make network configurable
diff --git a/core/installer/app.go b/core/installer/app.go
index 8d79c58..4ed1549 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -12,6 +12,10 @@
 //go:embed values-tmpl
 var valuesTmpls embed.FS
 
+type Named interface {
+	Nam() string
+}
+
 type App struct {
 	Name       string
 	Namespaces []string
@@ -20,31 +24,45 @@
 	Readme     *template.Template
 }
 
-type AppRepository interface {
-	GetAll() ([]App, error)
-	Find(name string) (*App, error)
+type StoreApp struct {
+	App
+	Icon             string
+	ShortDescription string
 }
 
-type InMemoryAppRepository struct {
-	apps []App
+func (a App) Nam() string {
+	return a.Name
 }
 
-func NewInMemoryAppRepository(apps []App) AppRepository {
-	return &InMemoryAppRepository{
+func (a StoreApp) Nam() string {
+	return a.Name
+}
+
+type AppRepository[A Named] interface {
+	GetAll() ([]A, error)
+	Find(name string) (*A, error)
+}
+
+type InMemoryAppRepository[A Named] struct {
+	apps []A
+}
+
+func NewInMemoryAppRepository[A Named](apps []A) AppRepository[A] {
+	return &InMemoryAppRepository[A]{
 		apps,
 	}
 }
 
-func (r InMemoryAppRepository) Find(name string) (*App, error) {
+func (r InMemoryAppRepository[A]) Find(name string) (*A, error) {
 	for _, a := range r.apps {
-		if a.Name == name {
+		if a.Nam() == name {
 			return &a, nil
 		}
 	}
 	return nil, fmt.Errorf("Application not found: %s", name)
 }
 
-func (r InMemoryAppRepository) GetAll() ([]App, error) {
+func (r InMemoryAppRepository[A]) GetAll() ([]A, error) {
 	return r.apps, nil
 }
 
@@ -53,18 +71,11 @@
 	if err != nil {
 		log.Fatal(err)
 	}
-	return []App{
+	ret := []App{
 		CreateAppIngressPrivate(valuesTmpls, tmpls),
 		CreateCertificateIssuerPublic(valuesTmpls, tmpls),
 		CreateCertificateIssuerPrivate(valuesTmpls, tmpls),
 		CreateAppCoreAuth(valuesTmpls, tmpls),
-		CreateAppVaultwarden(valuesTmpls, tmpls),
-		CreateAppMatrix(valuesTmpls, tmpls),
-		CreateAppPihole(valuesTmpls, tmpls),
-		CreateAppMaddy(valuesTmpls, tmpls),
-		CreateAppQBittorrent(valuesTmpls, tmpls),
-		CreateAppJellyfin(valuesTmpls, tmpls),
-		CreateAppRpuppy(valuesTmpls, tmpls),
 		CreateAppHeadscale(valuesTmpls, tmpls),
 		CreateAppTailscaleProxy(valuesTmpls, tmpls),
 		CreateMetallbConfigEnv(valuesTmpls, tmpls),
@@ -78,6 +89,26 @@
 		CreateResourceRendererController(valuesTmpls, tmpls),
 		CreateHeadscaleController(valuesTmpls, tmpls),
 	}
+	for _, a := range CreateStoreApps() {
+		ret = append(ret, a.App)
+	}
+	return ret
+}
+
+func CreateStoreApps() []StoreApp {
+	tmpls, err := template.New("root").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(valuesTmpls, "values-tmpl/*")
+	if err != nil {
+		log.Fatal(err)
+	}
+	return []StoreApp{
+		CreateAppVaultwarden(valuesTmpls, tmpls),
+		CreateAppMatrix(valuesTmpls, tmpls),
+		CreateAppPihole(valuesTmpls, tmpls),
+		CreateAppMaddy(valuesTmpls, tmpls),
+		CreateAppQBittorrent(valuesTmpls, tmpls),
+		CreateAppJellyfin(valuesTmpls, tmpls),
+		CreateAppRpuppy(valuesTmpls, tmpls),
+	}
 }
 
 // TODO(gio): service account needs permission to create/update secret
@@ -146,116 +177,144 @@
 	}
 }
 
-func CreateAppVaultwarden(fs embed.FS, tmpls *template.Template) App {
+func CreateAppVaultwarden(fs embed.FS, tmpls *template.Template) StoreApp {
 	schema, err := fs.ReadFile("values-tmpl/vaultwarden.jsonschema")
 	if err != nil {
 		panic(err)
 	}
-	return App{
-		"vaultwarden",
-		[]string{"app-vaultwarden"},
-		[]*template.Template{
-			tmpls.Lookup("vaultwarden.yaml"),
+	return StoreApp{
+		App: App{
+			"vaultwarden",
+			[]string{"app-vaultwarden"},
+			[]*template.Template{
+				tmpls.Lookup("vaultwarden.yaml"),
+			},
+			string(schema),
+			tmpls.Lookup("vaultwarden.md"),
 		},
-		string(schema),
-		tmpls.Lookup("vaultwarden.md"),
+		Icon:             "arcticons:bitwarden",
+		ShortDescription: "Open source implementation of Bitwarden password manager. Can be used with official client applications.",
 	}
 }
 
-func CreateAppMatrix(fs embed.FS, tmpls *template.Template) App {
+func CreateAppMatrix(fs embed.FS, tmpls *template.Template) StoreApp {
 	schema, err := fs.ReadFile("values-tmpl/matrix.jsonschema")
 	if err != nil {
 		panic(err)
 	}
-	return App{
-		"matrix",
-		[]string{"app-matrix"},
-		[]*template.Template{
-			tmpls.Lookup("matrix-storage.yaml"),
-			tmpls.Lookup("matrix.yaml"),
+	return StoreApp{
+		App{
+			"matrix",
+			[]string{"app-matrix"},
+			[]*template.Template{
+				tmpls.Lookup("matrix-storage.yaml"),
+				tmpls.Lookup("matrix.yaml"),
+			},
+			string(schema),
+			nil,
 		},
-		string(schema),
-		nil,
+		"simple-icons:matrix",
+		"An open network for secure, decentralised communication",
 	}
 }
 
-func CreateAppPihole(fs embed.FS, tmpls *template.Template) App {
+func CreateAppPihole(fs embed.FS, tmpls *template.Template) StoreApp {
 	schema, err := fs.ReadFile("values-tmpl/pihole.jsonschema")
 	if err != nil {
 		panic(err)
 	}
-	return App{
-		"pihole",
-		[]string{"app-pihole"},
-		[]*template.Template{
-			tmpls.Lookup("pihole.yaml"),
+	return StoreApp{
+		App{
+			"pihole",
+			[]string{"app-pihole"},
+			[]*template.Template{
+				tmpls.Lookup("pihole.yaml"),
+			},
+			string(schema),
+			tmpls.Lookup("pihole.md"),
 		},
-		string(schema),
-		tmpls.Lookup("pihole.md"),
+		"simple-icons:pihole",
+		"Pi-hole is a Linux network-level advertisement and Internet tracker blocking application which acts as a DNS sinkhole and optionally a DHCP server, intended for use on a private network.",
 	}
 }
 
-func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
+func CreateAppMaddy(fs embed.FS, tmpls *template.Template) StoreApp {
 	schema, err := fs.ReadFile("values-tmpl/maddy.jsonschema")
 	if err != nil {
 		panic(err)
 	}
-	return App{
-		"maddy",
-		[]string{"app-maddy"},
-		[]*template.Template{
-			tmpls.Lookup("maddy.yaml"),
+	return StoreApp{
+		App{
+			"maddy",
+			[]string{"app-maddy"},
+			[]*template.Template{
+				tmpls.Lookup("maddy.yaml"),
+			},
+			string(schema),
+			nil,
 		},
-		string(schema),
-		nil,
+		"arcticons:huawei-email",
+		"SMPT/IMAP server to communicate via email.",
 	}
 }
 
-func CreateAppQBittorrent(fs embed.FS, tmpls *template.Template) App {
+func CreateAppQBittorrent(fs embed.FS, tmpls *template.Template) StoreApp {
 	schema, err := fs.ReadFile("values-tmpl/qbittorrent.jsonschema")
 	if err != nil {
 		panic(err)
 	}
-	return App{
-		"qbittorrent",
-		[]string{"app-qbittorrent"},
-		[]*template.Template{
-			tmpls.Lookup("qbittorrent.yaml"),
+	return StoreApp{
+		App{
+			"qbittorrent",
+			[]string{"app-qbittorrent"},
+			[]*template.Template{
+				tmpls.Lookup("qbittorrent.yaml"),
+			},
+			string(schema),
+			tmpls.Lookup("qbittorrent.md"),
 		},
-		string(schema),
-		tmpls.Lookup("qbittorrent.md"),
+		"arcticons:qbittorrent-remote",
+		"qBittorrent is a cross-platform free and open-source BitTorrent client written in native C++. It relies on Boost, Qt 6 toolkit and the libtorrent-rasterbar library, with an optional search engine written in Python.",
 	}
 }
 
-func CreateAppJellyfin(fs embed.FS, tmpls *template.Template) App {
+func CreateAppJellyfin(fs embed.FS, tmpls *template.Template) StoreApp {
 	schema, err := fs.ReadFile("values-tmpl/jellyfin.jsonschema")
 	if err != nil {
 		panic(err)
 	}
-	return App{
-		"jellyfin",
-		[]string{"app-jellyfin"},
-		[]*template.Template{
-			tmpls.Lookup("jellyfin.yaml"),
+	return StoreApp{
+		App{
+			"jellyfin",
+			[]string{"app-jellyfin"},
+			[]*template.Template{
+				tmpls.Lookup("jellyfin.yaml"),
+			},
+			string(schema),
+			nil,
 		},
-		string(schema),
-		nil,
+		"arcticons:jellyfin",
+		"Jellyfin is a free and open-source media server and suite of multimedia applications designed to organize, manage, and share digital media files to networked devices.",
 	}
 }
 
-func CreateAppRpuppy(fs embed.FS, tmpls *template.Template) App {
+func CreateAppRpuppy(fs embed.FS, tmpls *template.Template) StoreApp {
 	schema, err := fs.ReadFile("values-tmpl/rpuppy.jsonschema")
 	if err != nil {
 		panic(err)
 	}
-	return App{
-		"rpuppy",
-		[]string{"app-rpuppy"},
-		[]*template.Template{
-			tmpls.Lookup("rpuppy.yaml"),
+	return StoreApp{
+		App{
+			"rpuppy",
+			[]string{"app-rpuppy"},
+			[]*template.Template{
+				tmpls.Lookup("rpuppy.yaml"),
+			},
+			string(schema),
+			tmpls.Lookup("rpuppy.md"),
 		},
-		string(schema),
-		tmpls.Lookup("rpuppy.md"),
+		"ph:dog-thin",
+		"Delights users with randomly generate puppy pictures. Can be configured to be reachable only from private network or publicly.",
 	}
 }
 
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index d2feccc..b5815e4 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -72,5 +72,6 @@
 			"Namespace": namespaces[0],
 		}
 	}
+	// TODO(giolekva): use ns suffix for app directory
 	return m.repoIO.InstallApp(app, "apps", all)
 }
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index cff270e..9a15c6e 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -68,7 +68,7 @@
 	if err != nil {
 		return err
 	}
-	kube, err := installer.NewOutOfClusterNamespaceCreator(rootFlags.kubeConfig)
+	kube, err := newNSCreator()
 	if err != nil {
 		return err
 	}
@@ -79,7 +79,7 @@
 	if err != nil {
 		return err
 	}
-	r := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	r := installer.NewInMemoryAppRepository[installer.StoreApp](installer.CreateStoreApps())
 	s := &server{
 		port: appManagerFlags.port,
 		m:    m,
@@ -92,7 +92,7 @@
 type server struct {
 	port int
 	m    *installer.AppManager
-	r    installer.AppRepository
+	r    installer.AppRepository[installer.StoreApp]
 }
 
 func (s *server) start() {
@@ -113,10 +113,12 @@
 }
 
 type app struct {
-	Name   string         `json:"name"`
-	Slug   string         `json:"slug"`
-	Schema string         `json:"schema"`
-	Config map[string]any `json:"config"`
+	Name             string `json:"name"`
+	Icon             string `json:"icon"`
+	ShortDescription string `json:"shortDescription"`
+	Slug             string `json:"slug"`
+	Schema           string `json:"schema"`
+	Config           any    `json:"config"`
 }
 
 func (s *server) handleAppRepo(c echo.Context) error {
@@ -127,7 +129,7 @@
 	resp := make([]app, len(all))
 	for i, a := range all {
 		config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error
-		resp[i] = app{a.Name, a.Name, a.Schema, config}
+		resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config}
 	}
 	return c.JSON(http.StatusOK, resp)
 }
@@ -139,7 +141,7 @@
 		return err
 	}
 	config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error
-	return c.JSON(http.StatusOK, app{a.Name, a.Name, a.Schema, config})
+	return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config["Values"]})
 }
 
 type file struct {
@@ -152,6 +154,29 @@
 	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)
@@ -166,6 +191,13 @@
 	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,
@@ -218,11 +250,18 @@
 	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.NewCombine(
 		installer.NewPrefixGenerator(config.Values.Id+"-"),
 		installer.NewRandomSuffixGenerator(3),
 	)
-	if err := s.m.Install(*a, nsGen, values); err != nil {
+	if err := s.m.Install(a.App, nsGen, values); err != nil {
 		return err
 	}
 	return c.String(http.StatusOK, "Installed")