appmanager: list and update app instances
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index 8cb6cad..90f2c4b 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -101,6 +101,8 @@
 	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)
@@ -113,12 +115,12 @@
 }
 
 type app struct {
-	Name             string `json:"name"`
-	Icon             string `json:"icon"`
-	ShortDescription string `json:"shortDescription"`
-	Slug             string `json:"slug"`
-	Schema           string `json:"schema"`
-	Config           any    `json:"config"`
+	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 {
@@ -128,8 +130,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.Icon, a.ShortDescription, a.Name, a.Schema, config}
+		resp[i] = app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, nil}
 	}
 	return c.JSON(http.StatusOK, resp)
 }
@@ -140,8 +141,42 @@
 	if err != nil {
 		return err
 	}
-	config, _ := s.m.AppConfig(a.Name) // TODO(gio): handle error
-	return c.JSON(http.StatusOK, app{a.Name, a.Icon, a.ShortDescription, a.Name, a.Schema, config["Values"]})
+	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 {
@@ -265,6 +300,41 @@
 	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,