package main import ( "bufio" "encoding/json" "fmt" "log" "net" "net/http" "os" "strings" "time" ) const ( fieldSep = "\x01" listSep = "\x02" ) var serverAddr string func init() { host := os.Getenv("INVENTORY_HOST") if host == "" { host = "inventory-server" } port := os.Getenv("INVENTORY_PORT") if port == "" { port = "9876" } serverAddr = host + ":" + port } // sendCommand sends a protocol command and returns the response rows. func sendCommand(tokens []string) ([][]string, error) { conn, err := net.DialTimeout("tcp", serverAddr, 5*time.Second) if err != nil { return nil, fmt.Errorf("connect to %s: %w", serverAddr, err) } defer conn.Close() conn.SetDeadline(time.Now().Add(15 * time.Second)) line := strings.Join(tokens, fieldSep) + "\n" if _, err := conn.Write([]byte(line)); err != nil { return nil, fmt.Errorf("write: %w", err) } scanner := bufio.NewScanner(conn) scanner.Buffer(make([]byte, 256*1024), 256*1024) if !scanner.Scan() { return nil, fmt.Errorf("no response from server") } status := scanner.Text() if strings.HasPrefix(status, "ERR") { msg := "" if len(status) > 4 { msg = status[4:] } return nil, fmt.Errorf("server: %s", msg) } if status != "OK" { return nil, fmt.Errorf("unexpected status: %s", status) } var rows [][]string for scanner.Scan() { row := scanner.Text() if row == "END" { break } rows = append(rows, strings.Split(row, fieldSep)) } return rows, scanner.Err() } // ── Domain types ───────────────────────────────────────────────────────────── type Server struct { ID string `json:"id"` Name string `json:"name"` Hostname string `json:"hostname"` Location string `json:"location"` Description string `json:"description"` } type PartType struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` } type Part struct { ID string `json:"id"` Type string `json:"type"` ServerID string `json:"server_id"` Serial string `json:"serial"` LastUpdated int64 `json:"last_updated"` Fields map[string]string `json:"fields"` } // ── Query helpers ───────────────────────────────────────────────────────────── func listServers() ([]Server, error) { rows, err := sendCommand([]string{"LIST_SERVERS"}) if err != nil { return nil, err } var out []Server for _, f := range rows { if len(f) < 5 { continue } out = append(out, Server{ID: f[0], Name: f[1], Hostname: f[2], Location: f[3], Description: f[4]}) } return out, nil } func listPartTypes() ([]PartType, error) { rows, err := sendCommand([]string{"LIST_PART_TYPES"}) if err != nil { return nil, err } var out []PartType for _, f := range rows { if len(f) < 3 { continue } out = append(out, PartType{ID: f[0], Name: f[1], Description: f[2]}) } return out, nil } func listParts(serverID string) ([]Part, error) { rows, err := sendCommand([]string{"LIST_PARTS", serverID}) if err != nil { return nil, err } var out []Part for _, f := range rows { if len(f) < 5 { continue } fields := make(map[string]string) for i := 5; i+1 < len(f); i += 2 { val := strings.ReplaceAll(f[i+1], listSep, ", ") fields[f[i]] = val } var ts int64 fmt.Sscanf(f[4], "%d", &ts) out = append(out, Part{Type: f[0], ID: f[1], ServerID: f[2], Serial: f[3], LastUpdated: ts, Fields: fields}) } return out, nil } // ── HTTP handlers ───────────────────────────────────────────────────────────── func writeJSON(w http.ResponseWriter, v interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) } func main() { mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "ok") }) mux.HandleFunc("/api/servers", func(w http.ResponseWriter, r *http.Request) { servers, err := listServers() if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } if servers == nil { servers = []Server{} } writeJSON(w, servers) }) mux.HandleFunc("/api/part-types", func(w http.ResponseWriter, r *http.Request) { pts, err := listPartTypes() if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } if pts == nil { pts = []PartType{} } writeJSON(w, pts) }) // /api/servers/{id}/parts mux.HandleFunc("/api/servers/", func(w http.ResponseWriter, r *http.Request) { parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") // parts = ["api", "servers", "{id}", "parts"] if len(parts) != 4 || parts[3] != "parts" { http.NotFound(w, r) return } serverID := parts[2] plist, err := listParts(serverID) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } if plist == nil { plist = []Part{} } writeJSON(w, plist) }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, dashboardHTML) }) port := os.Getenv("PORT") if port == "" { port = "8080" } log.Printf("inventory-web-ui listening on :%s, backend: %s", port, serverAddr) if err := http.ListenAndServe(":"+port, mux); err != nil { log.Fatal(err) } } // ── Embedded dashboard ──────────────────────────────────────────────────────── const dashboardHTML = `