package handlers import ( "net/http" "strings" "github.com/gin-gonic/gin" ) // slotInfo describes a declared slot and its contributors. type slotInfo struct { Host string `json:"host"` Description string `json:"description"` Context map[string]any `json:"context,omitempty"` Contributors []slotContributor `json:"contributors"` } // slotContributor describes one package's contribution to a slot. type slotContributor struct { Package string `json:"package"` Label string `json:"label,omitempty"` Icon string `json:"icon,omitempty"` Description string `json:"description,omitempty"` } // ListSlots returns an aggregated map of all declared slots across installed // packages together with their contributors. Built on-the-fly from manifests. // GET /api/v1/admin/slots func (h *PackageHandler) ListSlots(c *gin.Context) { pkgs, err := h.stores.Packages.List(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list packages"}) return } slots := map[string]*slotInfo{} // Pass 1: collect declared slots from host surfaces for _, pkg := range pkgs { slotsRaw, ok := pkg.Manifest["slots"].(map[string]any) if !ok { continue } for name, v := range slotsRaw { fullName := pkg.ID + ":" + name entry, ok := v.(map[string]any) if !ok { continue } desc, _ := entry["description"].(string) var ctx map[string]any if c, ok := entry["context"].(map[string]any); ok { ctx = c } slots[fullName] = &slotInfo{ Host: pkg.ID, Description: desc, Context: ctx, Contributors: []slotContributor{}, } } } // Pass 2: collect contributions for _, pkg := range pkgs { if !pkg.Enabled { continue } contribs, ok := pkg.Manifest["contributes"].(map[string]any) if !ok { continue } for slotKey, v := range contribs { // Ensure the slot entry exists (contributor can arrive before host) if _, exists := slots[slotKey]; !exists { host := slotKey if idx := strings.Index(slotKey, ":"); idx >= 0 { host = slotKey[:idx] } slots[slotKey] = &slotInfo{ Host: host, Contributors: []slotContributor{}, } } contrib := slotContributor{Package: pkg.ID} if m, ok := v.(map[string]any); ok { contrib.Label, _ = m["label"].(string) contrib.Icon, _ = m["icon"].(string) contrib.Description, _ = m["description"].(string) } slots[slotKey].Contributors = append(slots[slotKey].Contributors, contrib) } } c.JSON(http.StatusOK, gin.H{"data": slots}) }