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.",
 	}
 }