package handlers import ( "context" "encoding/json" "fmt" "net/http" "time" "git.nakama.town/fmartingr/hako/internal/archival/archiver" archivalRules "git.nakama.town/fmartingr/hako/internal/archival/rules" archivalStore "git.nakama.town/fmartingr/hako/internal/archival/store" "git.nakama.town/fmartingr/hako/internal/extractors" "git.nakama.town/fmartingr/hako/internal/model" "git.nakama.town/fmartingr/hako/internal/server/webcontext" ) // SystemHandler handles system-related requests type SystemHandler struct { getVersion func(ctx context.Context) (string, error) extractorMgr *extractors.Manager archiverMgr *archiver.Manager archiverConfigStore *archivalStore.ArchiverConfigStore settingsStore *archivalStore.SettingsStore } // NewSystemHandler creates a new SystemHandler func NewSystemHandler(getVersion func(ctx context.Context) (string, error), extractorMgr *extractors.Manager, archiverMgr *archiver.Manager, archiverConfigStore *archivalStore.ArchiverConfigStore, settingsStore *archivalStore.SettingsStore) *SystemHandler { return &SystemHandler{ getVersion: getVersion, extractorMgr: extractorMgr, archiverMgr: archiverMgr, archiverConfigStore: archiverConfigStore, settingsStore: settingsStore, } } // SystemResponse represents a system response type SystemResponse struct { Version string `json:"version,omitempty"` Commit string `json:"commit,omitempty"` Date string `json:"date,omitempty"` } // HandleSystem handles GET /system func (h *SystemHandler) HandleSystem(w http.ResponseWriter, r *http.Request) { // Create webcontext for this request c := webcontext.NewWebContext(w, r) c.ResponseWriter().Header().Set("Content-Type", "application/json") c.ResponseWriter().WriteHeader(http.StatusOK) response := SystemResponse{ Version: model.BuildVersion, Commit: model.BuildCommit, Date: model.BuildDate, } _ = json.NewEncoder(c.ResponseWriter()).Encode(response) } // HandleExtractors handles GET /system/extractors func (h *SystemHandler) HandleExtractors(w http.ResponseWriter, r *http.Request) { c := webcontext.NewWebContext(w, r) // Get status of all extractors statuses := h.extractorMgr.GetStatus() c.ResponseWriter().Header().Set("Content-Type", "application/json") c.ResponseWriter().WriteHeader(http.StatusOK) _ = json.NewEncoder(c.ResponseWriter()).Encode(statuses) } // HandleArchivers handles GET /system/archivers func (h *SystemHandler) HandleArchivers(w http.ResponseWriter, r *http.Request) { c := webcontext.NewWebContext(w, r) // Get status of all archivers statuses := h.archiverMgr.GetStatus(c.Context(), h.archiverConfigStore) c.ResponseWriter().Header().Set("Content-Type", "application/json") c.ResponseWriter().WriteHeader(http.StatusOK) _ = json.NewEncoder(c.ResponseWriter()).Encode(statuses) } // HandleGetArchiverConfig handles GET /system/archivers/{key}/config func (h *SystemHandler) HandleGetArchiverConfig(w http.ResponseWriter, r *http.Request) { c := webcontext.NewWebContext(w, r) // Extract archiver key from path key := r.PathValue("key") if key == "" { http.Error(w, "Archiver key is required", http.StatusBadRequest) return } // Get archiver to access default config arch, ok := h.archiverMgr.GetArchiver(key) if !ok { http.Error(w, fmt.Sprintf("Archiver not found: %s", key), http.StatusNotFound) return } // Try to get stored config storedConfig, err := h.archiverConfigStore.Get(c.Context(), key) if err != nil { http.Error(w, fmt.Sprintf("Failed to get archiver config: %v", err), http.StatusInternalServerError) return } var config any if storedConfig != nil && storedConfig.ConfigJSON != "" { // Parse stored config JSON if err := json.Unmarshal([]byte(storedConfig.ConfigJSON), &config); err != nil { // If parsing fails, fall back to default config = arch.GetDefaultConfig() } } else { // No stored config, use default config = arch.GetDefaultConfig() } c.ResponseWriter().Header().Set("Content-Type", "application/json") c.ResponseWriter().WriteHeader(http.StatusOK) _ = json.NewEncoder(c.ResponseWriter()).Encode(config) } // HandleSaveArchiverConfig handles PUT /system/archivers/{key}/config func (h *SystemHandler) HandleSaveArchiverConfig(w http.ResponseWriter, r *http.Request) { c := webcontext.NewWebContext(w, r) // Extract archiver key from path key := r.PathValue("key") if key == "" { http.Error(w, "Archiver key is required", http.StatusBadRequest) return } // Verify archiver exists arch, ok := h.archiverMgr.GetArchiver(key) if !ok { http.Error(w, fmt.Sprintf("Archiver not found: %s", key), http.StatusNotFound) return } // Parse request body var config map[string]any if err := json.NewDecoder(r.Body).Decode(&config); err != nil { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) return } // Validate config by trying to apply it if err := arch.ApplyConfig(config); err != nil { http.Error(w, fmt.Sprintf("Invalid config: %v", err), http.StatusBadRequest) return } // Marshal config back to JSON configJSON, err := json.Marshal(config) if err != nil { http.Error(w, fmt.Sprintf("Failed to marshal config: %v", err), http.StatusInternalServerError) return } // Save to database archiverConfig := &model.ArchiverConfig{ ArchiverKey: key, ConfigJSON: string(configJSON), UpdatedAt: time.Now(), } if err := h.archiverConfigStore.Upsert(c.Context(), archiverConfig); err != nil { http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError) return } c.ResponseWriter().Header().Set("Content-Type", "application/json") c.ResponseWriter().WriteHeader(http.StatusOK) _ = json.NewEncoder(c.ResponseWriter()).Encode(map[string]string{"message": "Config saved successfully"}) } // HandleDeleteArchiverConfig handles DELETE /system/archivers/{key}/config func (h *SystemHandler) HandleDeleteArchiverConfig(w http.ResponseWriter, r *http.Request) { c := webcontext.NewWebContext(w, r) // Extract archiver key from path key := r.PathValue("key") if key == "" { http.Error(w, "Archiver key is required", http.StatusBadRequest) return } // Verify archiver exists _, ok := h.archiverMgr.GetArchiver(key) if !ok { http.Error(w, fmt.Sprintf("Archiver not found: %s", key), http.StatusNotFound) return } // Delete from database if err := h.archiverConfigStore.Delete(c.Context(), key); err != nil { http.Error(w, fmt.Sprintf("Failed to delete config: %v", err), http.StatusInternalServerError) return } c.ResponseWriter().Header().Set("Content-Type", "application/json") c.ResponseWriter().WriteHeader(http.StatusOK) _ = json.NewEncoder(c.ResponseWriter()).Encode(map[string]string{"message": "Config reset to default successfully"}) } // HandleGetRulesConfig handles GET /system/rules/config func (h *SystemHandler) HandleGetRulesConfig(w http.ResponseWriter, r *http.Request) { c := webcontext.NewWebContext(w, r) // Get rules config from settings store rulesConfig, err := h.settingsStore.GetRulesConfig(c.Context()) if err != nil { http.Error(w, fmt.Sprintf("Failed to get rules config: %v", err), http.StatusInternalServerError) return } // Convert to JSON format for frontend // We need to manually construct JSON since Rule is an interface rulesJSON := make([]json.RawMessage, len(rulesConfig.Rules)) for i, rule := range rulesConfig.Rules { ruleBytes, err := json.Marshal(rule) if err != nil { http.Error(w, fmt.Sprintf("Failed to marshal rule %d: %v", i, err), http.StatusInternalServerError) return } rulesJSON[i] = ruleBytes } response := map[string]interface{}{ "rules": rulesJSON, "default_extractors": rulesConfig.DefaultArchivers, // JSON key kept for backward compatibility } c.ResponseWriter().Header().Set("Content-Type", "application/json") c.ResponseWriter().WriteHeader(http.StatusOK) _ = json.NewEncoder(c.ResponseWriter()).Encode(response) } // HandleSaveRulesConfig handles PUT /system/rules/config func (h *SystemHandler) HandleSaveRulesConfig(w http.ResponseWriter, r *http.Request) { c := webcontext.NewWebContext(w, r) // Read request body var requestBody map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) return } // Convert to JSON bytes for unmarshaling requestBytes, err := json.Marshal(requestBody) if err != nil { http.Error(w, fmt.Sprintf("Failed to marshal request: %v", err), http.StatusBadRequest) return } // Unmarshal and validate structure rulesConfig, err := archivalRules.UnmarshalRuleConfig(requestBytes) if err != nil { http.Error(w, fmt.Sprintf("Invalid rules config structure: %v", err), http.StatusBadRequest) return } // Validate each rule for i, rule := range rulesConfig.Rules { if err := rule.IsValid(); err != nil { http.Error(w, fmt.Sprintf("Rule %d is invalid: %v", i, err), http.StatusBadRequest) return } } // Save to database if err := h.settingsStore.SetRulesConfig(c.Context(), rulesConfig); err != nil { http.Error(w, fmt.Sprintf("Failed to save rules config: %v", err), http.StatusInternalServerError) return } c.ResponseWriter().Header().Set("Content-Type", "application/json") c.ResponseWriter().WriteHeader(http.StatusOK) _ = json.NewEncoder(c.ResponseWriter()).Encode(map[string]string{"message": "Rules config saved successfully"}) }