mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-19 07:50:43 +01:00
- Add MetadataProvider interface for AI to update resource URLs - Add set_resource_url tool to AI service - Wire up metadata stores to AI service via router - Add URL discovery guidance to AI system prompt - AI can now inspect guests/containers/hosts for web services and automatically save discovered URLs to Pulse metadata Usage: Ask the AI 'Find the web URL for this container' and it will: 1. Check for listening ports and web servers 2. Get the IP address 3. Verify the URL works 4. Save it to Pulse for quick dashboard access
165 lines
5.2 KiB
Go
165 lines
5.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// DockerMetadataHandler handles Docker resource metadata operations
|
|
type DockerMetadataHandler struct {
|
|
store *config.DockerMetadataStore
|
|
}
|
|
|
|
// NewDockerMetadataHandler creates a new Docker metadata handler
|
|
func NewDockerMetadataHandler(dataPath string) *DockerMetadataHandler {
|
|
return &DockerMetadataHandler{
|
|
store: config.NewDockerMetadataStore(dataPath),
|
|
}
|
|
}
|
|
|
|
// Store returns the underlying metadata store
|
|
func (h *DockerMetadataHandler) Store() *config.DockerMetadataStore {
|
|
return h.store
|
|
}
|
|
|
|
// HandleGetMetadata retrieves metadata for a specific Docker resource or all resources
|
|
func (h *DockerMetadataHandler) HandleGetMetadata(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Check if requesting specific resource
|
|
path := r.URL.Path
|
|
// Handle both /api/docker/metadata and /api/docker/metadata/
|
|
if path == "/api/docker/metadata" || path == "/api/docker/metadata/" {
|
|
// Get all metadata
|
|
w.Header().Set("Content-Type", "application/json")
|
|
allMeta := h.store.GetAll()
|
|
if allMeta == nil {
|
|
// Return empty object instead of null
|
|
json.NewEncoder(w).Encode(make(map[string]*config.DockerMetadata))
|
|
} else {
|
|
json.NewEncoder(w).Encode(allMeta)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get specific resource ID from path
|
|
resourceID := strings.TrimPrefix(path, "/api/docker/metadata/")
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if resourceID != "" {
|
|
// Get specific Docker resource metadata
|
|
meta := h.store.Get(resourceID)
|
|
if meta == nil {
|
|
// Return empty metadata instead of 404
|
|
json.NewEncoder(w).Encode(&config.DockerMetadata{ID: resourceID})
|
|
} else {
|
|
json.NewEncoder(w).Encode(meta)
|
|
}
|
|
} else {
|
|
// This shouldn't happen with current routing, but handle it anyway
|
|
http.Error(w, "Invalid request path", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
// HandleUpdateMetadata updates metadata for a Docker resource
|
|
func (h *DockerMetadataHandler) HandleUpdateMetadata(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPut && r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
resourceID := strings.TrimPrefix(r.URL.Path, "/api/docker/metadata/")
|
|
if resourceID == "" || resourceID == "metadata" {
|
|
http.Error(w, "Resource ID required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Limit request body to 16KB to prevent memory exhaustion
|
|
r.Body = http.MaxBytesReader(w, r.Body, 16*1024)
|
|
|
|
var meta config.DockerMetadata
|
|
if err := json.NewDecoder(r.Body).Decode(&meta); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate URL if provided
|
|
if meta.CustomURL != "" {
|
|
// Parse and validate the URL
|
|
parsedURL, err := url.Parse(meta.CustomURL)
|
|
if err != nil {
|
|
http.Error(w, "Invalid URL format: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check scheme
|
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
|
http.Error(w, "URL must use http:// or https:// scheme", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check host is present and valid
|
|
if parsedURL.Host == "" {
|
|
http.Error(w, "Invalid URL: missing host/domain (e.g., use https://192.168.1.100:8006 or https://emby.local)", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check for incomplete URLs like "https://emby."
|
|
if strings.HasSuffix(parsedURL.Host, ".") && !strings.Contains(parsedURL.Host, "..") {
|
|
http.Error(w, "Incomplete URL: '"+meta.CustomURL+"' - please enter a complete domain or IP address", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := h.store.Set(resourceID, &meta); err != nil {
|
|
log.Error().Err(err).Str("resourceID", resourceID).Msg("Failed to save Docker metadata")
|
|
// Provide more specific error message
|
|
errMsg := "Failed to save metadata"
|
|
if strings.Contains(err.Error(), "permission") {
|
|
errMsg = "Permission denied - check file permissions"
|
|
} else if strings.Contains(err.Error(), "no space") {
|
|
errMsg = "Disk full - cannot save metadata"
|
|
}
|
|
http.Error(w, errMsg, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
log.Info().Str("resourceID", resourceID).Str("url", meta.CustomURL).Msg("Updated Docker metadata")
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(&meta)
|
|
}
|
|
|
|
// HandleDeleteMetadata removes metadata for a Docker resource
|
|
func (h *DockerMetadataHandler) HandleDeleteMetadata(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodDelete {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
resourceID := strings.TrimPrefix(r.URL.Path, "/api/docker/metadata/")
|
|
if resourceID == "" || resourceID == "metadata" {
|
|
http.Error(w, "Resource ID required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := h.store.Delete(resourceID); err != nil {
|
|
log.Error().Err(err).Str("resourceID", resourceID).Msg("Failed to delete Docker metadata")
|
|
http.Error(w, "Failed to delete metadata", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
log.Info().Str("resourceID", resourceID).Msg("Deleted Docker metadata")
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|