package handlers import ( "context" "encoding/json" "fmt" "net/http" "strconv" "strings" "github.com/go-chi/chi/v5" "git.georgsen.dk/hwlab/internal/netbox" ) // InventoryNetBoxClient is the narrow interface the inventory handler needs. // *netbox.Client satisfies this interface. type InventoryNetBoxClient interface { ListDevices(ctx context.Context, limit int) ([]netbox.Device, error) GetDevice(ctx context.Context, id int) (*netbox.Device, error) } // InventoryHandler handles GET /api/inventory and GET /api/inventory/{id}. type InventoryHandler struct { nb InventoryNetBoxClient } // NewInventoryHandler creates an InventoryHandler. func NewInventoryHandler(nb InventoryNetBoxClient) *InventoryHandler { return &InventoryHandler{nb: nb} } // InventoryItemResponse is the JSON shape the frontend consumes. type InventoryItemResponse struct { ID int `json:"id"` Name string `json:"name"` AssetTag string `json:"asset_tag"` HWID string `json:"hw_id"` CatalogStatus string `json:"catalog_status"` ProductURL string `json:"product_url,omitempty"` Firmware string `json:"firmware_version,omitempty"` TestDate string `json:"test_date,omitempty"` TestData string `json:"test_data,omitempty"` AINotes string `json:"ai_notes,omitempty"` PhotoURLs []string `json:"photo_urls"` } func deviceToResponse(d netbox.Device) InventoryItemResponse { urls := d.CustomFields.PhotoURLs if urls == nil { urls = []string{} } return InventoryItemResponse{ ID: d.ID, Name: d.Name, AssetTag: d.AssetTag, HWID: d.CustomFields.HWID, CatalogStatus: d.CustomFields.CatalogStatus, ProductURL: d.CustomFields.ProductURL, Firmware: d.CustomFields.FirmwareVersion, TestDate: d.CustomFields.TestDate, TestData: d.CustomFields.TestData, AINotes: d.CustomFields.AINotes, PhotoURLs: urls, } } // ListInventory handles GET /api/inventory — returns up to 200 items. func (h *InventoryHandler) ListInventory(w http.ResponseWriter, r *http.Request) { devices, err := h.nb.ListDevices(r.Context(), 200) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadGateway) _ = json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("netbox unavailable: %s", err)}) return } items := make([]InventoryItemResponse, 0, len(devices)) for _, d := range devices { items = append(items, deviceToResponse(d)) } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(items) } // GetInventoryItem handles GET /api/inventory/{id}. func (h *InventoryHandler) GetInventoryItem(w http.ResponseWriter, r *http.Request) { rawID := chi.URLParam(r, "id") id, err := strconv.Atoi(rawID) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]string{"error": "id must be an integer"}) return } device, err := h.nb.GetDevice(r.Context(), id) if err != nil { w.Header().Set("Content-Type", "application/json") if strings.Contains(err.Error(), "404") || strings.Contains(strings.ToLower(err.Error()), "not found") { w.WriteHeader(http.StatusNotFound) _ = json.NewEncoder(w).Encode(map[string]string{"error": "item not found"}) return } w.WriteHeader(http.StatusBadGateway) _ = json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("netbox unavailable: %s", err)}) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(deviceToResponse(*device)) }