- Add .gitignore: exclude compiled binaries, build artifacts, and Helm values files containing real secrets (authentik, prometheus) - Add all Kubernetes deployment manifests (deployment/) - Add services source code: ha-sync, device-inventory, games-console, paperclip, parts-inventory - Add Ansible orchestration: playbooks, roles, inventory, cloud-init - Add hardware specs, execution plans, scripts, HOMELAB.md - Add skills/homelab/SKILL.md + skills/install.sh to preserve Copilot skill - Remove previously-tracked inventory-cli binary from git index Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
205 lines
5.1 KiB
Go
205 lines
5.1 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
)
|
|
|
|
// Part mirrors the API's IPart document.
|
|
type Part struct {
|
|
ID string `json:"_id"`
|
|
Title string `json:"title"`
|
|
Type string `json:"type"`
|
|
Category string `json:"category"`
|
|
Manufacturer string `json:"manufacturer,omitempty"`
|
|
Dimensions *Dimensions `json:"dimensions,omitempty"`
|
|
Quantity int `json:"quantity"`
|
|
Location string `json:"location,omitempty"`
|
|
Notes string `json:"notes,omitempty"`
|
|
Tags []string `json:"tags"`
|
|
Properties map[string]interface{} `json:"properties"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
// Dimensions holds optional physical size info.
|
|
type Dimensions struct {
|
|
Width *float64 `json:"width,omitempty"`
|
|
Length *float64 `json:"length,omitempty"`
|
|
Height *float64 `json:"height,omitempty"`
|
|
Unit string `json:"unit,omitempty"`
|
|
}
|
|
|
|
// ListResponse is the shape returned by GET /api/parts.
|
|
type ListResponse struct {
|
|
Total int `json:"total"`
|
|
Parts []Part `json:"parts"`
|
|
}
|
|
|
|
// Client wraps HTTP calls to the parts API.
|
|
type Client struct {
|
|
BaseURL string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
// New creates a Client with a 15-second timeout.
|
|
func New(baseURL string) *Client {
|
|
return &Client{
|
|
BaseURL: baseURL,
|
|
HTTPClient: &http.Client{Timeout: 15 * time.Second},
|
|
}
|
|
}
|
|
|
|
// ListParts fetches parts with optional full-text query and field filters.
|
|
func (c *Client) ListParts(query, partType, category string) (*ListResponse, error) {
|
|
u, err := url.Parse(c.BaseURL + "/api/parts")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
params := url.Values{}
|
|
if query != "" {
|
|
params.Set("q", query)
|
|
}
|
|
if partType != "" {
|
|
params.Set("type", partType)
|
|
}
|
|
if category != "" {
|
|
params.Set("category", category)
|
|
}
|
|
u.RawQuery = params.Encode()
|
|
|
|
resp, err := c.HTTPClient.Get(u.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("API returned %d", resp.StatusCode)
|
|
}
|
|
|
|
var result ListResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
// GetPart fetches a single part by ID.
|
|
func (c *Client) GetPart(id string) (*Part, error) {
|
|
resp, err := c.HTTPClient.Get(c.BaseURL + "/api/parts/" + id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, fmt.Errorf("part %s not found", id)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("API returned %d", resp.StatusCode)
|
|
}
|
|
|
|
var part Part
|
|
if err := json.NewDecoder(resp.Body).Decode(&part); err != nil {
|
|
return nil, err
|
|
}
|
|
return &part, nil
|
|
}
|
|
|
|
// CreatePart posts a new part to the API.
|
|
func (c *Client) CreatePart(part map[string]interface{}) (*Part, error) {
|
|
body, err := json.Marshal(part)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Post(
|
|
c.BaseURL+"/api/parts",
|
|
"application/json",
|
|
bytes.NewReader(body),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
var apiErr map[string]string
|
|
_ = json.NewDecoder(resp.Body).Decode(&apiErr)
|
|
if msg, ok := apiErr["error"]; ok {
|
|
return nil, fmt.Errorf("API error: %s", msg)
|
|
}
|
|
return nil, fmt.Errorf("API returned %d", resp.StatusCode)
|
|
}
|
|
|
|
var created Part
|
|
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
|
|
return nil, err
|
|
}
|
|
return &created, nil
|
|
}
|
|
|
|
// UpdatePart sends a PUT request with the provided fields.
|
|
func (c *Client) UpdatePart(id string, updates map[string]interface{}) (*Part, error) {
|
|
body, err := json.Marshal(updates)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPut, c.BaseURL+"/api/parts/"+id, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, fmt.Errorf("part %s not found", id)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
var apiErr map[string]string
|
|
_ = json.NewDecoder(resp.Body).Decode(&apiErr)
|
|
if msg, ok := apiErr["error"]; ok {
|
|
return nil, fmt.Errorf("API error: %s", msg)
|
|
}
|
|
return nil, fmt.Errorf("API returned %d", resp.StatusCode)
|
|
}
|
|
|
|
var updated Part
|
|
if err := json.NewDecoder(resp.Body).Decode(&updated); err != nil {
|
|
return nil, err
|
|
}
|
|
return &updated, nil
|
|
}
|
|
|
|
// DeletePart sends a DELETE request for the given part ID.
|
|
func (c *Client) DeletePart(id string) error {
|
|
req, err := http.NewRequest(http.MethodDelete, c.BaseURL+"/api/parts/"+id, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return fmt.Errorf("part %s not found", id)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("API returned %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|