installer: refactor App interface
diff --git a/core/installer/app.go b/core/installer/app.go
index 671f81c..5791780 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -7,17 +7,15 @@
 	"embed"
 	"encoding/json"
 	"fmt"
-	htemplate "html/template"
+	template "html/template"
 	"io"
 	"log"
 	"net/http"
 	"strings"
-	"text/template"
 
 	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/cuecontext"
 	cueyaml "cuelang.org/go/encoding/yaml"
-	"github.com/Masterminds/sprig/v3"
 	"github.com/go-git/go-billy/v5"
 	"sigs.k8s.io/yaml"
 )
@@ -25,6 +23,39 @@
 //go:embed values-tmpl
 var valuesTmpls embed.FS
 
+var storeAppConfigs = []string{
+	"values-tmpl/jellyfin.cue",
+	// "values-tmpl/maddy.cue",
+	"values-tmpl/matrix.cue",
+	"values-tmpl/penpot.cue",
+	"values-tmpl/pihole.cue",
+	"values-tmpl/qbittorrent.cue",
+	"values-tmpl/rpuppy.cue",
+	"values-tmpl/soft-serve.cue",
+	"values-tmpl/vaultwarden.cue",
+}
+
+var infraAppConfigs = []string{
+	"values-tmpl/appmanager.cue",
+	"values-tmpl/cert-manager.cue",
+	"values-tmpl/certificate-issuer-private.cue",
+	"values-tmpl/certificate-issuer-public.cue",
+	"values-tmpl/config-repo.cue",
+	"values-tmpl/core-auth.cue",
+	"values-tmpl/csi-driver-smb.cue",
+	"values-tmpl/dns-zone-manager.cue",
+	"values-tmpl/env-manager.cue",
+	"values-tmpl/fluxcd-reconciler.cue",
+	"values-tmpl/headscale-controller.cue",
+	"values-tmpl/headscale-user.cue",
+	"values-tmpl/headscale.cue",
+	"values-tmpl/ingress-public.cue",
+	"values-tmpl/metallb-ipaddresspool.cue",
+	"values-tmpl/private-network.cue",
+	"values-tmpl/resource-renderer-controller.cue",
+	"values-tmpl/welcome.cue",
+}
+
 const cueBaseConfigImports = `
 import (
     "list"
@@ -33,7 +64,11 @@
 
 // TODO(gio): import
 const cueBaseConfig = `
+name: string | *""
+description: string | *""
 readme: string | *""
+icon: string | *""
+namespace: string | *""
 
 #Network: {
 	name: string
@@ -158,29 +193,12 @@
 }
 `
 
-type Named interface {
-	Nam() string
-}
-
 type appConfig struct {
-	Name        string         `json:"name"`
-	Version     string         `json:"version"`
-	Description string         `json:"description"`
-	Namespaces  []string       `json:"namespaces"`
-	Icon        htemplate.HTML `json:"icon"`
-}
-
-type App struct {
-	Name       string
-	Namespaces []string
-	templates  []*template.Template
-	schema     Schema
-	Readme     *template.Template
-	cfg        *cue.Value
-}
-
-func (a App) Schema() Schema {
-	return a.schema
+	Name        string        `json:"name"`
+	Version     string        `json:"version"`
+	Description string        `json:"description"`
+	Namespaces  []string      `json:"namespaces"`
+	Icon        template.HTML `json:"icon"`
 }
 
 type Rendered struct {
@@ -188,693 +206,187 @@
 	Resources map[string][]byte
 }
 
-func cleanName(s string) string {
-	return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
+type App interface {
+	Name() string
+	Description() string
+	Icon() template.HTML
+	Schema() Schema
+	Namespaces() []string
+	Render(derived Derived) (Rendered, error)
 }
 
-func (a App) Render(derived Derived) (Rendered, error) {
+type cueApp struct {
+	name        string
+	description string
+	icon        template.HTML
+	namespace   string
+	schema      Schema
+	cfg         *cue.Value
+}
+
+type cueAppConfig struct {
+	Name        string `json:"name"`
+	Namespace   string `json:"namespace"`
+	Description string `json:"description"`
+	Icon        string `json:"icon"`
+}
+
+func newCueApp(config *cue.Value) (cueApp, error) {
+	if config == nil {
+		return cueApp{}, fmt.Errorf("config not provided")
+	}
+	var cfg cueAppConfig
+	if err := config.Decode(&cfg); err != nil {
+		return cueApp{}, err
+	}
+	schema, err := NewCueSchema(config.LookupPath(cue.ParsePath("input")))
+	if err != nil {
+		return cueApp{}, err
+	}
+	return cueApp{
+		name:        cfg.Name,
+		description: cfg.Description,
+		icon:        template.HTML(cfg.Icon),
+		namespace:   cfg.Namespace,
+		schema:      schema,
+		cfg:         config,
+	}, nil
+}
+
+func (a cueApp) Name() string {
+	return a.name
+}
+
+func (a cueApp) Description() string {
+	return a.description
+}
+
+func (a cueApp) Icon() template.HTML {
+	return a.icon
+}
+
+func (a cueApp) Schema() Schema {
+	return a.schema
+}
+
+func (a cueApp) Namespaces() []string {
+	return []string{a.namespace}
+}
+
+func (a cueApp) Render(derived Derived) (Rendered, error) {
 	ret := Rendered{
 		Resources: make(map[string][]byte),
 	}
-	if a.cfg != nil {
-		var buf bytes.Buffer
-		if err := json.NewEncoder(&buf).Encode(derived); err != nil {
-			return Rendered{}, err
-		}
-		ctx := a.cfg.Context()
-		d := ctx.CompileBytes(buf.Bytes())
-		res := a.cfg.Unify(d).Eval()
-		if err := res.Err(); err != nil {
-			return Rendered{}, err
-		}
-		if err := res.Validate(); err != nil {
-			return Rendered{}, err
-		}
-		readme, err := res.LookupPath(cue.ParsePath("readme")).String()
-		if err != nil {
-			return Rendered{}, err
-		}
-		ret.Readme = readme
-		output := res.LookupPath(cue.ParsePath("output"))
-		i, err := output.Fields()
-		if err != nil {
-			return Rendered{}, err
-		}
-		for i.Next() {
-			name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
-			contents, err := cueyaml.Encode(i.Value())
-			if err != nil {
-				return Rendered{}, err
-			}
-			ret.Resources[name] = contents
-		}
-		return ret, nil
-	}
-	var readme bytes.Buffer
-	if err := a.Readme.Execute(&readme, derived); err != nil {
+	var buf bytes.Buffer
+	if err := json.NewEncoder(&buf).Encode(derived); err != nil {
 		return Rendered{}, err
 	}
-	ret.Readme = readme.String()
-	for _, t := range a.templates {
-		var buf bytes.Buffer
-		if err := t.Execute(&buf, derived); err != nil {
+	ctx := a.cfg.Context()
+	d := ctx.CompileBytes(buf.Bytes())
+	res := a.cfg.Unify(d).Eval()
+	if err := res.Err(); err != nil {
+		return Rendered{}, err
+	}
+	if err := res.Validate(); err != nil {
+		return Rendered{}, err
+	}
+	readme, err := res.LookupPath(cue.ParsePath("readme")).String()
+	if err != nil {
+		return Rendered{}, err
+	}
+	ret.Readme = readme
+	output := res.LookupPath(cue.ParsePath("output"))
+	i, err := output.Fields()
+	if err != nil {
+		return Rendered{}, err
+	}
+	for i.Next() {
+		name := fmt.Sprintf("%s.yaml", cleanName(i.Selector().String()))
+		contents, err := cueyaml.Encode(i.Value())
+		if err != nil {
 			return Rendered{}, err
 		}
-		ret.Resources[t.Name()] = buf.Bytes()
+		ret.Resources[name] = contents
 	}
 	return ret, nil
 }
 
-type StoreApp struct {
-	App
-	Icon             htemplate.HTML
-	ShortDescription string
+type AppRepository interface {
+	GetAll() ([]App, error)
+	Find(name string) (App, error)
 }
 
-func (a App) Nam() string {
-	return a.Name
+type InMemoryAppRepository struct {
+	apps []App
 }
 
-func (a StoreApp) Nam() string {
-	return a.Name
+func NewInMemoryAppRepository(apps []App) InMemoryAppRepository {
+	return InMemoryAppRepository{apps}
 }
 
-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) InMemoryAppRepository[A] {
-	return InMemoryAppRepository[A]{
-		apps,
-	}
-}
-
-func (r InMemoryAppRepository[A]) Find(name string) (*A, error) {
+func (r InMemoryAppRepository) Find(name string) (App, error) {
 	for _, a := range r.apps {
-		if a.Nam() == name {
-			return &a, nil
+		if a.Name() == name {
+			return a, nil
 		}
 	}
 	return nil, fmt.Errorf("Application not found: %s", name)
 }
 
-func (r InMemoryAppRepository[A]) GetAll() ([]A, error) {
+func (r InMemoryAppRepository) GetAll() ([]App, error) {
 	return r.apps, nil
 }
 
 func CreateAllApps() []App {
-	tmpls, err := template.New("root").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(valuesTmpls, "values-tmpl/*")
-	if err != nil {
-		log.Fatal(err)
-	}
-	ret := []App{
-		CreateAppIngressPrivate(valuesTmpls, tmpls),
-		CreateCertificateIssuerPublic(valuesTmpls, tmpls),
-		CreateCertificateIssuerPrivate(valuesTmpls, tmpls),
-		CreateAppCoreAuth(valuesTmpls, tmpls),
-		CreateAppHeadscale(valuesTmpls, tmpls),
-		CreateAppHeadscaleUser(valuesTmpls, tmpls),
-		CreateMetallbIPAddressPool(valuesTmpls, tmpls),
-		CreateEnvManager(valuesTmpls, tmpls),
-		CreateWelcome(valuesTmpls, tmpls),
-		CreateAppManager(valuesTmpls, tmpls),
-		CreateIngressPublic(valuesTmpls, tmpls),
-		CreateCertManager(valuesTmpls, tmpls),
-		CreateCSIDriverSMB(valuesTmpls, tmpls),
-		CreateResourceRendererController(valuesTmpls, tmpls),
-		CreateHeadscaleController(valuesTmpls, tmpls),
-		CreateDNSZoneManager(valuesTmpls, tmpls),
-		CreateFluxcdReconciler(valuesTmpls, tmpls),
-		CreateAppConfigRepo(valuesTmpls, tmpls),
-	}
-	for _, a := range CreateStoreApps() {
-		ret = append(ret, a.App)
+	return append(
+		createApps(infraAppConfigs),
+		CreateStoreApps()...,
+	)
+}
+
+func CreateStoreApps() []App {
+	return createApps(storeAppConfigs)
+}
+
+func createApps(configs []string) []App {
+	ret := make([]App, len(configs))
+	for _, cfgFile := range configs {
+		cfg, err := readCueConfigFromFile(valuesTmpls, cfgFile)
+		if err != nil {
+			panic(err)
+		}
+		if app, err := newCueApp(cfg); err != nil {
+			panic(err)
+		} else {
+			ret = append(ret, 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),
-		CreateAppPenpot(valuesTmpls, tmpls),
-		CreateAppMaddy(valuesTmpls, tmpls),
-		CreateAppQBittorrent(valuesTmpls, tmpls),
-		CreateAppJellyfin(valuesTmpls, tmpls),
-		CreateAppSoftServe(valuesTmpls, tmpls),
-		CreateAppRpuppy(valuesTmpls, tmpls),
-	}
-}
-
-func readJSONSchemaFromFile(fs embed.FS, f string) (Schema, error) {
-	schema, err := fs.ReadFile(f)
-	if err != nil {
-		return nil, err
-	}
-	ret, err := NewJSONSchema(string(schema))
-	if err != nil {
-		return nil, err
-	}
-	return ret, nil
-}
-
-// TODO(gio): service account needs permission to create/update secret
-func CreateAppIngressPrivate(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/private-network.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"private-network",
-		[]string{"ingress-private"}, // TODO(gio): rename to private network
-		[]*template.Template{
-			tmpls.Lookup("ingress-private.yaml"),
-			tmpls.Lookup("tailscale-proxy.yaml"),
-		},
-		schema,
-		tmpls.Lookup("private-network.md"),
-		cfg,
-	}
-}
-
-func CreateCertificateIssuerPrivate(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/certificate-issuer-private.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"certificate-issuer-private",
-		[]string{"ingress-private"},
-		[]*template.Template{
-			tmpls.Lookup("certificate-issuer-private.yaml"),
-		},
-		schema,
-		tmpls.Lookup("certificate-issuer-private.md"),
-		cfg,
-	}
-}
-
-func CreateCertificateIssuerPublic(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/certificate-issuer-public.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"certificate-issuer-public",
-		[]string{"ingress-private"},
-		[]*template.Template{
-			tmpls.Lookup("certificate-issuer-public.yaml"),
-		},
-		schema,
-		tmpls.Lookup("certificate-issuer-public.md"),
-		cfg,
-	}
-}
-
-func CreateAppCoreAuth(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/core-auth.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"core-auth",
-		[]string{"core-auth"},
-		[]*template.Template{
-			tmpls.Lookup("core-auth-storage.yaml"),
-			tmpls.Lookup("core-auth.yaml"),
-		},
-		schema,
-		tmpls.Lookup("core-auth.md"),
-		cfg,
-	}
-}
-
-func CreateAppVaultwarden(fs embed.FS, tmpls *template.Template) StoreApp {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/vaultwarden.cue")
-	if err != nil {
-		panic(err)
-	}
-	return StoreApp{
-		App: App{
-			"vaultwarden",
-			[]string{"app-vaultwarden"},
-			[]*template.Template{
-				tmpls.Lookup("vaultwarden.yaml"),
-			},
-			schema,
-			tmpls.Lookup("vaultwarden.md"),
-			cfg,
-		},
-		Icon:             `<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M35.38 25.63V9.37H24v28.87a34.93 34.93 0 0 0 5.41-3.48q6-4.66 6-9.14Zm4.87-19.5v19.5A11.58 11.58 0 0 1 39.4 30a16.22 16.22 0 0 1-2.11 3.81a23.52 23.52 0 0 1-3 3.24a34.87 34.87 0 0 1-3.22 2.62c-1 .69-2 1.35-3.07 2s-1.82 1-2.27 1.26l-1.08.51a1.53 1.53 0 0 1-1.32 0l-1.08-.51c-.45-.22-1.21-.64-2.27-1.26s-2.09-1.27-3.07-2A34.87 34.87 0 0 1 13.7 37a23.52 23.52 0 0 1-3-3.24A16.22 16.22 0 0 1 8.6 30a11.58 11.58 0 0 1-.85-4.32V6.13A1.64 1.64 0 0 1 9.38 4.5h29.24a1.64 1.64 0 0 1 1.63 1.63Z"/></svg>`,
-		ShortDescription: "Alternative implementation of the Bitwarden server API written in Rust and compatible with upstream Bitwarden clients, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.",
-	}
-}
-
-func CreateAppMatrix(fs embed.FS, tmpls *template.Template) StoreApp {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/matrix.cue")
-	if err != nil {
-		panic(err)
-	}
-	return StoreApp{
-		App{
-			"matrix",
-			[]string{"app-matrix"},
-			[]*template.Template{
-				tmpls.Lookup("matrix-storage.yaml"),
-				tmpls.Lookup("matrix.yaml"),
-			},
-			schema,
-			nil,
-			cfg,
-		},
-		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 24 24"><path fill="currentColor" d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033a3.312 3.312 0 0 1 1.117-1.024c.433-.245.936-.365 1.5-.365c.54 0 1.033.107 1.481.314c.448.208.785.582 1.02 1.108c.254-.374.6-.706 1.034-.992c.434-.287.95-.43 1.546-.43c.453 0 .872.056 1.26.167c.388.11.716.286.993.53c.276.245.489.559.646.951c.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66a1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166c-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499a1.946 1.946 0 0 0-.231.696a5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688a1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19c-.111 0-.259.024-.439.074c-.18.051-.36.143-.53.282a1.637 1.637 0 0 0-.439.595c-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"/></svg>`,
-		"An open network for secure, decentralised communication",
-	}
-}
-
-func CreateAppPihole(fs embed.FS, tmpls *template.Template) StoreApp {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/pihole.cue")
-	if err != nil {
-		panic(err)
-	}
-	return StoreApp{
-		App{
-			"pihnole",
-			[]string{"app-pihole"},
-			[]*template.Template{
-				tmpls.Lookup("pihole.yaml"),
-			},
-			schema,
-			tmpls.Lookup("pihole.md"),
-			cfg,
-		},
-		// "simple-icons:pihole",
-		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 24 24"><path fill="currentColor" d="M4.344 0c.238 4.792 3.256 7.056 6.252 7.376c.165-1.692-4.319-5.6-4.319-5.6c-.008-.011.009-.025.019-.014c0 0 4.648 4.01 5.423 5.645c2.762-.15 5.196-1.947 5-4.912c0 0-4.12-.613-5 4.618C11.48 2.753 8.993 0 4.344 0zM12 7.682v.002a3.68 3.68 0 0 0-2.591 1.077L4.94 13.227a3.683 3.683 0 0 0-.86 1.356a3.31 3.31 0 0 0-.237 1.255A3.681 3.681 0 0 0 4.92 18.45l4.464 4.466a3.69 3.69 0 0 0 2.251 1.06l.002.001c.093.01.187.015.28.017l-.1-.008c.06.003.117.009.177.009l-.077-.001L12 24l-.004-.005a3.68 3.68 0 0 0 2.61-1.077l4.469-4.465a3.683 3.683 0 0 0 1.006-1.888l.012-.063a3.682 3.682 0 0 0 .057-.541l.003-.061c0-.017.003-.05.004-.06h-.002a3.683 3.683 0 0 0-1.077-2.607l-4.466-4.468a3.694 3.694 0 0 0-1.564-.927l-.07-.02a3.43 3.43 0 0 0-.946-.133L12 7.682zm3.165 3.357c.023 1.748-1.33 3.078-1.33 4.806c.164 2.227 1.733 3.207 3.266 3.146c-.035.003-.068.007-.104.009c-1.847.135-3.209-1.326-5.002-1.326c-2.23.164-3.21 1.736-3.147 3.27l-.008-.104c-.133-1.847 1.328-3.21 1.328-5.002c-.173-2.32-1.867-3.284-3.46-3.132c.1-.011.203-.021.31-.027c1.847-.133 3.209 1.328 5.002 1.328c2.082-.155 3.074-1.536 3.145-2.968zM4.344 0c.238 4.792 3.256 7.056 6.252 7.376c.165-1.692-4.319-5.6-4.319-5.6c-.008-.011.009-.025.019-.014c0 0 4.648 4.01 5.423 5.645c2.762-.15 5.196-1.947 5-4.912c0 0-4.12-.613-5 4.618C11.48 2.753 8.993 0 4.344 0zM12 7.682v.002a3.68 3.68 0 0 0-2.591 1.077L4.94 13.227a3.683 3.683 0 0 0-.86 1.356a3.31 3.31 0 0 0-.237 1.255A3.681 3.681 0 0 0 4.92 18.45l4.464 4.466a3.69 3.69 0 0 0 2.251 1.06l.002.001c.093.01.187.015.28.017l-.1-.008c.06.003.117.009.177.009l-.077-.001L12 24l-.004-.005a3.68 3.68 0 0 0 2.61-1.077l4.469-4.465a3.683 3.683 0 0 0 1.006-1.888l.012-.063a3.682 3.682 0 0 0 .057-.541l.003-.061c0-.017.003-.05.004-.06h-.002a3.683 3.683 0 0 0-1.077-2.607l-4.466-4.468a3.694 3.694 0 0 0-1.564-.927l-.07-.02a3.43 3.43 0 0 0-.946-.133L12 7.682zm3.165 3.357c.023 1.748-1.33 3.078-1.33 4.806c.164 2.227 1.733 3.207 3.266 3.146c-.035.003-.068.007-.104.009c-1.847.135-3.209-1.326-5.002-1.326c-2.23.164-3.21 1.736-3.147 3.27l-.008-.104c-.133-1.847 1.328-3.21 1.328-5.002c-.173-2.32-1.867-3.284-3.46-3.132c.1-.011.203-.021.31-.027c1.847-.133 3.209 1.328 5.002 1.328c2.082-.155 3.074-1.536 3.145-2.968z"/></svg>`,
-		"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 CreateAppPenpot(fs embed.FS, tmpls *template.Template) StoreApp {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/penpot.cue")
-	if err != nil {
-		panic(err)
-	}
-	return StoreApp{
-		App{
-			"penpot",
-			[]string{"app-penpot"},
-			[]*template.Template{
-				tmpls.Lookup("penpot.yaml"),
-			},
-			schema,
-			tmpls.Lookup("penpot.md"),
-			cfg,
-		},
-		// "simple-icons:pihole",
-		`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M7.654 0L5.13 3.554v2.01L2.934 6.608l-.02-.009v13.109l8.563 4.045L12 24l.523-.247l8.563-4.045V6.6l-.017.008l-2.196-1.045V3.555l-.077-.108L16.349.001l-2.524 3.554v.004L11.989.973l-1.823 2.566l-.065-.091zm.447 2.065l.976 1.374H6.232l.964-1.358zm8.694 0l.976 1.374h-2.845l.965-1.358zm-4.36.971l.976 1.375h-2.845l.965-1.359zM5.962 4.132h1.35v4.544l-1.35-.638Zm2.042 0h1.343v5.506l-1.343-.635zm6.652 0h1.35V9l-1.35.637zm2.042 0h1.343v3.905l-1.343.634zm-6.402.972h1.35v5.62l-1.35-.638zm2.042 0h1.343v4.993l-1.343.634zm6.534 1.493l1.188.486l-1.188.561zM5.13 6.6v1.047l-1.187-.561ZM3.96 8.251l7.517 3.55v10.795l-7.516-3.55zm16.08 0v10.794l-7.517 3.55V11.802z"/></svg>`,
-		"Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open standards (SVG). Penpot invites designers all over the world to fall in love with open source while getting developers excited about the design process in return.",
-	}
-}
-
-func CreateAppMaddy(fs embed.FS, tmpls *template.Template) StoreApp {
-	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
-	if err != nil {
-		panic(err)
-	}
-	return StoreApp{
-		App{
-			"maddy",
-			[]string{"app-maddy"},
-			[]*template.Template{
-				tmpls.Lookup("maddy.yaml"),
-			},
-			schema,
-			nil,
-			nil,
-		},
-		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M9.5 13c13.687 13.574 14.825 13.09 29 0"/><rect width="37" height="31" x="5.5" y="8.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" rx="2"/></svg>`,
-		"SMPT/IMAP server to communicate via email.",
-	}
-}
-
-func CreateAppQBittorrent(fs embed.FS, tmpls *template.Template) StoreApp {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/qbittorrent.cue")
-	if err != nil {
-		panic(err)
-	}
-	return StoreApp{
-		App{
-			"qbittorrent",
-			[]string{"app-qbittorrent"},
-			[]*template.Template{
-				tmpls.Lookup("qbittorrent.yaml"),
-			},
-			schema,
-			tmpls.Lookup("qbittorrent.md"),
-			cfg,
-		},
-		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><circle cx="24" cy="24" r="21.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M26.651 22.364a5.034 5.034 0 0 1 5.035-5.035h0a5.034 5.034 0 0 1 5.034 5.035v3.272a5.034 5.034 0 0 1-5.034 5.035h0a5.034 5.034 0 0 1-5.035-5.035m0 5.035V10.533m-5.302 15.103a5.034 5.034 0 0 1-5.035 5.035h0a5.034 5.034 0 0 1-5.034-5.035v-3.272a5.034 5.034 0 0 1 5.034-5.035h0a5.034 5.034 0 0 1 5.035 5.035m0-5.035v20.138"/></svg>`,
-		"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) StoreApp {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/jellyfin.cue")
-	if err != nil {
-		panic(err)
-	}
-	return StoreApp{
-		App{
-			"jellyfin",
-			[]string{"app-jellyfin"},
-			[]*template.Template{
-				tmpls.Lookup("jellyfin.yaml"),
-			},
-			schema,
-			nil,
-			cfg,
-		},
-		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M24 20c-1.62 0-6.85 9.48-6.06 11.08s11.33 1.59 12.12 0S25.63 20 24 20Z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M24 5.5c-4.89 0-20.66 28.58-18.25 33.4s34.13 4.77 36.51 0S28.9 5.5 24 5.5Zm12 29.21c-1.56 3.13-22.35 3.17-23.93 0S20.8 12.83 24 12.83s13.52 18.76 12 21.88Z"/></svg>`,
-		"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 processCueConfig(contents string) (*cue.Value, Schema, error) {
-	ctx := cuecontext.New()
-	cfg := ctx.CompileString(cueBaseConfigImports + contents + cueBaseConfig)
-	if err := cfg.Err(); err != nil {
-		return nil, nil, err
-	}
-	if err := cfg.Validate(); err != nil {
-		return nil, nil, err
-	}
-	schema, err := NewCueSchema(cfg.LookupPath(cue.ParsePath("input")))
-	if err != nil {
-		return nil, nil, err
-	}
-	return &cfg, schema, nil
-}
-
-func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, Schema, error) {
-	contents, err := fs.ReadFile(f)
-	if err != nil {
-		return nil, nil, err
-	}
-	return processCueConfig(string(contents))
-}
-
-func CreateAppRpuppy(fs embed.FS, tmpls *template.Template) StoreApp {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/rpuppy.cue")
-	if err != nil {
-		panic(err)
-	}
-	return StoreApp{
-		App{
-			"rpuppy",
-			[]string{"app-rpuppy"},
-			[]*template.Template{
-				tmpls.Lookup("rpuppy.yaml"),
-			},
-			schema,
-			tmpls.Lookup("rpuppy.md"),
-			cfg,
-		},
-		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 256 256"><path fill="currentColor" d="M100 140a8 8 0 1 1-8-8a8 8 0 0 1 8 8Zm64 8a8 8 0 1 0-8-8a8 8 0 0 0 8 8Zm64.94-9.11a12.12 12.12 0 0 1-5 1.11a11.83 11.83 0 0 1-9.35-4.62l-2.59-3.29V184a36 36 0 0 1-36 36H80a36 36 0 0 1-36-36v-51.91l-2.53 3.27A11.88 11.88 0 0 1 32.1 140a12.08 12.08 0 0 1-5-1.11a11.82 11.82 0 0 1-6.84-13.14l16.42-88a12 12 0 0 1 14.7-9.43h.16L104.58 44h46.84l53.08-15.6h.16a12 12 0 0 1 14.7 9.43l16.42 88a11.81 11.81 0 0 1-6.84 13.06ZM97.25 50.18L49.34 36.1a4.18 4.18 0 0 0-.92-.1a4 4 0 0 0-3.92 3.26l-16.42 88a4 4 0 0 0 7.08 3.22ZM204 121.75L150 52h-44l-54 69.75V184a28 28 0 0 0 28 28h44v-18.34l-14.83-14.83a4 4 0 0 1 5.66-5.66L128 186.34l13.17-13.17a4 4 0 0 1 5.66 5.66L132 193.66V212h44a28 28 0 0 0 28-28Zm23.92 5.48l-16.42-88a4 4 0 0 0-4.84-3.16l-47.91 14.11l62.11 80.28a4 4 0 0 0 7.06-3.23Z"/></svg>`,
-		"Delights users with randomly generate puppy pictures. Can be configured to be reachable only from private network or publicly.",
-	}
-}
-
-func CreateAppConfigRepo(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/config-repo.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"config-repo",
-		[]string{"config-repo"},
-		[]*template.Template{},
-		schema,
-		nil,
-		cfg,
-	}
-}
-
-func CreateAppSoftServe(fs embed.FS, tmpls *template.Template) StoreApp {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/soft-serve.cue")
-	if err != nil {
-		panic(err)
-	}
-	return StoreApp{
-		App{
-			"soft-serve",
-			[]string{"app-soft-serve"},
-			[]*template.Template{
-				tmpls.Lookup("soft-serve.yaml"),
-			},
-			schema,
-			tmpls.Lookup("soft-serve.md"),
-			cfg,
-		},
-		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="4"><path stroke-linejoin="round" d="M15.34 22.5L21 37l3 6l3-6l5.66-14.5"/><path d="M19 32h10"/><path stroke-linejoin="round" d="M24 3c-6 0-8 6-8 6s-6 2-6 7s5 7 5 7s3.5-2 9-2s9 2 9 2s5-2 5-7s-6-7-6-7s-2-6-8-6Z"/></g></svg>`,
-		"A tasty, self-hostable Git server for the command line. 🍦",
-	}
-}
-
-func CreateAppHeadscale(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/headscale.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"headscale",
-		[]string{"app-headscale"},
-		[]*template.Template{
-			tmpls.Lookup("headscale.yaml"),
-		},
-		schema,
-		tmpls.Lookup("headscale.md"),
-		cfg,
-	}
-}
-
-func CreateAppHeadscaleUser(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/headscale-user.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"headscale-user",
-		[]string{"app-headscale"},
-		[]*template.Template{
-			tmpls.Lookup("headscale-user.yaml"),
-		},
-		schema,
-		tmpls.Lookup("headscale-user.md"),
-		cfg,
-	}
-}
-
-func CreateMetallbIPAddressPool(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/metallb-ipaddresspool.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"metallb-ipaddresspool",
-		[]string{"metallb-ipaddresspool"},
-		[]*template.Template{
-			tmpls.Lookup("metallb-ipaddresspool.yaml"),
-		},
-		schema,
-		tmpls.Lookup("metallb-ipaddresspool.md"),
-		cfg,
-	}
-}
-
-func CreateEnvManager(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/env-manager.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"env-manager",
-		[]string{"env-manager"},
-		[]*template.Template{
-			tmpls.Lookup("env-manager.yaml"),
-		},
-		schema,
-		tmpls.Lookup("env-manager.md"),
-		cfg,
-	}
-}
-
-func CreateWelcome(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/welcome.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"welcome",
-		[]string{"app-welcome"},
-		[]*template.Template{
-			tmpls.Lookup("welcome.yaml"),
-		},
-		schema,
-		tmpls.Lookup("welcome.md"),
-		cfg,
-	}
-}
-
-func CreateAppManager(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/appmanager.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"app-manager",
-		[]string{"core-appmanager"},
-		[]*template.Template{
-			tmpls.Lookup("appmanager.yaml"),
-		},
-		schema,
-		tmpls.Lookup("appmanager.md"),
-		cfg,
-	}
-}
-
-func CreateIngressPublic(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/ingress-public.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"ingress-public",
-		[]string{"ingress-public"},
-		[]*template.Template{
-			tmpls.Lookup("ingress-public.yaml"),
-		},
-		schema,
-		tmpls.Lookup("ingress-public.md"),
-		cfg,
-	}
-}
-
-func CreateCertManager(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/cert-manager.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"cert-manager",
-		[]string{"cert-manager"},
-		[]*template.Template{
-			tmpls.Lookup("cert-manager.yaml"),
-		},
-		schema,
-		tmpls.Lookup("cert-manager.md"),
-		cfg,
-	}
-}
-
-func CreateCSIDriverSMB(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/csi-driver-smb.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"csi-driver-smb",
-		[]string{"csi-driver-smb"},
-		[]*template.Template{
-			tmpls.Lookup("csi-driver-smb.yaml"),
-		},
-		schema,
-		tmpls.Lookup("csi-driver-smb.md"),
-		cfg,
-	}
-}
-
-func CreateResourceRendererController(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/resource-renderer-controller.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"resource-renderer-controller",
-		[]string{"rr-controller"},
-		[]*template.Template{
-			tmpls.Lookup("resource-renderer-controller.yaml"),
-		},
-		schema,
-		tmpls.Lookup("resource-renderer-controller.md"),
-		cfg,
-	}
-}
-
-func CreateHeadscaleController(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/headscale-controller.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"headscale-controller",
-		[]string{"headscale-controller"},
-		[]*template.Template{
-			tmpls.Lookup("headscale-controller.yaml"),
-		},
-		schema,
-		tmpls.Lookup("headscale-controller.md"),
-		cfg,
-	}
-}
-
-func CreateDNSZoneManager(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/dns-zone-manager.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"dns-zone-manager",
-		[]string{"dns-zone-manager"},
-		[]*template.Template{
-			tmpls.Lookup("dns-zone-storage.yaml"),
-			tmpls.Lookup("coredns.yaml"),
-			tmpls.Lookup("dns-zone-controller.yaml"),
-		},
-		schema,
-		tmpls.Lookup("dns-zone-controller.md"),
-		cfg,
-	}
-}
-
-func CreateFluxcdReconciler(fs embed.FS, tmpls *template.Template) App {
-	cfg, schema, err := readCueConfigFromFile(fs, "values-tmpl/fluxcd-reconciler.cue")
-	if err != nil {
-		panic(err)
-	}
-	return App{
-		"fluxcd-reconciler",
-		[]string{"fluxcd-reconciler"},
-		[]*template.Template{
-			tmpls.Lookup("fluxcd-reconciler.yaml"),
-		},
-		schema,
-		tmpls.Lookup("fluxcd-reconciler.md"),
-		cfg,
-	}
-}
+// func CreateAppMaddy(fs embed.FS, tmpls *template.Template) App {
+// 	schema, err := readJSONSchemaFromFile(fs, "values-tmpl/maddy.jsonschema")
+// 	if err != nil {
+// 		panic(err)
+// 	}
+// 	return StoreApp{
+// 		App{
+// 			"maddy",
+// 			[]string{"app-maddy"},
+// 			[]*template.Template{
+// 				tmpls.Lookup("maddy.yaml"),
+// 			},
+// 			schema,
+// 			nil,
+// 			nil,
+// 		},
+// 		`<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M9.5 13c13.687 13.574 14.825 13.09 29 0"/><rect width="37" height="31" x="5.5" y="8.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" rx="2"/></svg>`,
+// 		"SMPT/IMAP server to communicate via email.",
+// 	}
+// }
 
 type httpAppRepository struct {
-	apps []StoreApp
+	apps []App
 }
 
 type appVersion struct {
@@ -958,16 +470,16 @@
 }
 
 type fsAppRepository struct {
-	InMemoryAppRepository[StoreApp]
+	InMemoryAppRepository
 	fs billy.Filesystem
 }
 
-func NewFSAppRepository(fs billy.Filesystem) (AppRepository[StoreApp], error) {
+func NewFSAppRepository(fs billy.Filesystem) (AppRepository, error) {
 	all, err := fs.ReadDir(".")
 	if err != nil {
 		return nil, err
 	}
-	apps := make([]StoreApp, 0)
+	apps := make([]App, 0)
 	for _, e := range all {
 		if !e.IsDir() {
 			continue
@@ -984,15 +496,15 @@
 		apps = append(apps, app)
 	}
 	return &fsAppRepository{
-		NewInMemoryAppRepository[StoreApp](apps),
+		NewInMemoryAppRepository(apps),
 		fs,
 	}, nil
 }
 
-func loadApp(fs billy.Filesystem) (StoreApp, error) {
+func loadApp(fs billy.Filesystem) (App, error) {
 	items, err := fs.ReadDir(".")
 	if err != nil {
-		return StoreApp{}, err
+		return nil, err
 	}
 	var contents bytes.Buffer
 	for _, i := range items {
@@ -1001,43 +513,52 @@
 		}
 		f, err := fs.Open(i.Name())
 		if err != nil {
-			return StoreApp{}, err
+			return nil, err
 		}
 		defer f.Close()
 		if _, err := io.Copy(&contents, f); err != nil {
-			return StoreApp{}, err
+			return nil, err
 		}
 	}
-	cfg, schema, err := processCueConfig(contents.String())
+	cfg, err := processCueConfig(contents.String())
 	if err != nil {
-		return StoreApp{}, err
+		return nil, err
 	}
-	return newCueApp(cfg, schema)
+	return newCueApp(cfg)
 }
 
-type cueAppConfig struct {
-	Name        string `json:"name"`
-	Namespace   string `json:"namespace"`
-	Description string `json:"description"`
-	Icon        string `json:"icon"`
+func cleanName(s string) string {
+	return strings.ReplaceAll(strings.ReplaceAll(s, "\"", ""), "'", "")
 }
 
-func newCueApp(cfg *cue.Value, schema Schema) (StoreApp, error) {
-	var config cueAppConfig
-	if err := cfg.Decode(&config); err != nil {
-		return StoreApp{}, err
+func processCueConfig(contents string) (*cue.Value, error) {
+	ctx := cuecontext.New()
+	cfg := ctx.CompileString(cueBaseConfigImports + contents + cueBaseConfig)
+	if err := cfg.Err(); err != nil {
+		return nil, err
 	}
-	fmt.Printf("%#v\n", config)
-	return StoreApp{
-		App: App{
-			Name:       config.Name,
-			Readme:     nil,
-			schema:     schema,
-			Namespaces: []string{config.Namespace},
-			templates:  []*template.Template{},
-			cfg:        cfg,
-		},
-		ShortDescription: config.Description,
-		Icon:             htemplate.HTML(config.Icon),
-	}, nil
+	if err := cfg.Validate(); err != nil {
+		return nil, err
+	}
+	return &cfg, nil
+}
+
+func readCueConfigFromFile(fs embed.FS, f string) (*cue.Value, error) {
+	contents, err := fs.ReadFile(f)
+	if err != nil {
+		return nil, err
+	}
+	return processCueConfig(string(contents))
+}
+
+func createApp(fs embed.FS, configFile string) App {
+	cfg, err := readCueConfigFromFile(fs, configFile)
+	if err != nil {
+		panic(err)
+	}
+	if app, err := newCueApp(cfg); err != nil {
+		panic(err)
+	} else {
+		return app
+	}
 }