Files
tajniak81andClaude Opus 4.8 15bff7f517 Add adjustable probe area for OpenSky test connection
Let the OpenSky health probe run over a chosen bounding box without
touching the saved default, so its credit cost (which scales with area)
can be checked cheaply.

- Backend: the plugin health endpoint (panel Check) and the OpenSky
  health endpoint (Web App Test connection) accept an optional ?bbox=,
  validated with validBBox and applied to a transient probe instance;
  both BFF and API forward it. Saved config is untouched.
- API panel: a "Probe area" picker beside each bbox-capable plugin's
  Check button — saved default, all countries (grouped by continent),
  or Custom. Adds a compact pv-input-sm style.
- Web App: a "Test area" picker beside Test connection, using the full
  country list (new countryGroups() helper) plus Custom. This is where
  the probe is authenticated and the credit cost is shown.

Builds pass across both Go modules and both frontends. Panel picker
verified live (186 options, defaults to saved default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 10:25:59 +02:00

141 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"encoding/json"
"net/http"
"strings"
"pilotvault/apiserver/internal/plugins"
)
// GET /api/admin/plugins — every known plugin (registry persisted), secrets masked.
func (s *Server) handleListPlugins(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"plugins": s.plugins.List()})
}
// GET /api/admin/plugins/{name} — one plugin's view.
func (s *Server) handleGetPlugin(w http.ResponseWriter, r *http.Request) {
v, ok := s.plugins.Get(r.PathValue("name"))
if !ok {
writeError(w, http.StatusNotFound, "unknown plugin")
return
}
writeJSON(w, http.StatusOK, map[string]any{"plugin": v})
}
// PUT /api/admin/plugins/{name} — enable/disable + merge config. Body:
// {enabled?, config?}. A secret left at the mask keeps its stored value.
func (s *Server) handleUpdatePlugin(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
current, ok := s.plugins.Get(name)
if !ok {
writeError(w, http.StatusNotFound, "unknown plugin")
return
}
var body struct {
Enabled *bool `json:"enabled"`
Config map[string]string `json:"config"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
enabled := current.Enabled
if body.Enabled != nil {
enabled = *body.Enabled
}
v, err := s.plugins.Upsert(r.Context(), name, enabled, body.Config)
if err != nil {
if plugins.IsUnknown(err) {
writeError(w, http.StatusNotFound, "unknown plugin")
return
}
// A failed init (e.g. bad credentials) is reported but the state was saved.
writeJSON(w, http.StatusOK, map[string]any{"plugin": v, "warning": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{"plugin": v})
}
// POST /api/admin/plugins — register an external (remote HTTP) plugin. Body:
// {name, baseURL, provider?}. This is the "add a plugin without a rebuild" path.
func (s *Server) handleRegisterPlugin(w http.ResponseWriter, r *http.Request) {
var body struct {
Name string `json:"name"`
BaseURL string `json:"baseURL"`
Provider string `json:"provider"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
body.Name = strings.TrimSpace(body.Name)
if body.Name == "" || body.BaseURL == "" {
writeError(w, http.StatusBadRequest, "name and baseURL are required")
return
}
if err := s.plugins.RegisterExternal(body.Name, body.BaseURL, body.Provider); err != nil {
writeError(w, http.StatusConflict, err.Error())
return
}
v, _ := s.plugins.Get(body.Name)
writeJSON(w, http.StatusCreated, map[string]any{"plugin": v})
}
// DELETE /api/admin/plugins/{name} — remove an external plugin (builtins can only
// be disabled).
func (s *Server) handleDeletePlugin(w http.ResponseWriter, r *http.Request) {
if err := s.plugins.Remove(r.Context(), r.PathValue("name")); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}
// POST /api/admin/plugins/{name}/health — run a health check now. An optional
// ?bbox= overrides the plugin's stored bounding box for this probe only (used by
// the panel to test-probe OpenSky over a smaller, cheaper area without changing
// the saved default). Ignored by plugins that don't use a bbox.
func (s *Server) handlePluginHealth(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if bbox := strings.TrimSpace(r.URL.Query().Get("bbox")); bbox != "" {
if !validBBox(bbox) {
writeError(w, http.StatusBadRequest, "invalid bbox")
return
}
cfg, _, ok := s.plugins.RawConfig(name)
if !ok {
writeError(w, http.StatusNotFound, "unknown plugin")
return
}
if cfg == nil {
cfg = map[string]string{}
}
cfg["bbox"] = bbox
h, err := s.plugins.HealthCheckWith(r.Context(), name, cfg)
if err != nil {
if plugins.IsUnknown(err) {
writeError(w, http.StatusNotFound, "unknown plugin")
return
}
writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{"health": h})
return
}
h, err := s.plugins.HealthCheck(r.Context(), name)
if err != nil {
if plugins.IsUnknown(err) {
writeError(w, http.StatusNotFound, "unknown plugin")
return
}
writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{"health": h})
}