- Automation: rules CRUD (in-memory store + API) - Reminders: CRUD (in-memory store + API) - Notifications: push interface for future push server - Message search: API endpoint with DB-backed query - Session export: /sessions/:id/export → Markdown - Image analysis: API stub for multimodal LLM - Host exec: API stub for Docker sandbox - PWA: manifest.json + service worker - Docker: Dockerfile + docker-compose.yml - E2E: Playwright test scaffold - CI/CD: GitHub Actions workflow (go test + tsc)
641 lines
18 KiB
Go
641 lines
18 KiB
Go
// Package api provides HTTP routing, middleware, and handlers for the Cyrene Gateway.
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
|
|
)
|
|
|
|
// --- Interfaces for injected services ---
|
|
|
|
// AuthManager validates JWT tokens and handles login/register.
|
|
type AuthManager interface {
|
|
ValidateToken(token string) (userID, username, role string, err error)
|
|
Login(username, password string) (token string, err error)
|
|
Register(username, password string) (token string, err error)
|
|
}
|
|
|
|
// MemoryEntry is a simplified memory entry for API responses.
|
|
type MemoryEntry struct {
|
|
ID string `json:"id"`
|
|
UserID string `json:"user_id"`
|
|
Content string `json:"content"`
|
|
Summary string `json:"summary"`
|
|
Keywords []string `json:"keywords"`
|
|
Category string `json:"category"`
|
|
Priority float64 `json:"priority"`
|
|
}
|
|
|
|
// MemorySearcher handles memory search queries.
|
|
type MemorySearcher interface {
|
|
Search(ctx context.Context, userID, query string, limit int) (any, error)
|
|
List(ctx context.Context, userID string, limit int) (any, error)
|
|
Add(ctx context.Context, userID, content, category string, priority float64) (any, error)
|
|
Get(ctx context.Context, id string) (any, error)
|
|
Delete(ctx context.Context, id string) error
|
|
}
|
|
|
|
// VoiceService handles STT and TTS.
|
|
type VoiceService interface {
|
|
Recognize(ctx context.Context, audioData []byte) (string, error)
|
|
Synthesize(ctx context.Context, text string, voice string) ([]byte, error)
|
|
}
|
|
|
|
// OrchestratorPipeline handles the main dialogue flow.
|
|
type OrchestratorPipeline interface {
|
|
ProcessInput(ctx context.Context, sessionID, userMessage string) (<-chan model.StreamToken, *model.Intent, error)
|
|
}
|
|
|
|
// WebSocketHub manages WS connections.
|
|
type WebSocketHub interface {
|
|
UpgradeHandler() http.HandlerFunc
|
|
Count() int
|
|
}
|
|
|
|
// LLMCallLogger provides LLM call history.
|
|
type LLMCallLogger interface {
|
|
Recent(n int) []map[string]any
|
|
Stats() map[string]any
|
|
}
|
|
|
|
// TraceLogger provides full-chain trace events.
|
|
type TraceLogger interface {
|
|
Recent(n int) []map[string]any
|
|
Clear() // Remove old traces
|
|
}
|
|
|
|
// PerfMonitor provides performance snapshot history.
|
|
type PerfMonitor interface {
|
|
Latest() map[string]any
|
|
History() []map[string]any
|
|
}
|
|
|
|
// BridgeConfigManager manages platform adapter configurations.
|
|
type BridgeConfigManager interface {
|
|
GetAdapters() []map[string]any
|
|
SaveAdapter(name, adapterType string, enabled bool, settings map[string]string) error
|
|
DeleteAdapter(name string) error
|
|
}
|
|
|
|
// Dependencies holds all injectable services.
|
|
type Dependencies struct {
|
|
AuthMgr AuthManager
|
|
MemorySvc MemorySearcher
|
|
VoiceSvc VoiceService
|
|
Orchestrator OrchestratorPipeline
|
|
WSHub WebSocketHub
|
|
LLMLogger LLMCallLogger
|
|
TraceLog TraceLogger
|
|
PerfMon PerfMonitor
|
|
BridgeCfg BridgeConfigManager
|
|
ModelConfig *ModelConfigStore
|
|
Automation *AutomationStore
|
|
Reminders *ReminderStore
|
|
PluginList []map[string]string
|
|
}
|
|
|
|
// Router handles HTTP routing with middleware support.
|
|
type Router struct {
|
|
mux *http.ServeMux
|
|
deps Dependencies
|
|
}
|
|
|
|
// New creates a new Router with all routes configured.
|
|
func New(deps Dependencies) *Router {
|
|
r := &Router{mux: http.NewServeMux(), deps: deps}
|
|
r.setupRoutes()
|
|
return r
|
|
}
|
|
|
|
func (r *Router) setupRoutes() {
|
|
// Public
|
|
r.mux.HandleFunc("/api/v1/health", r.healthHandler)
|
|
r.mux.HandleFunc("/api/v1/auth/login", r.loginHandler)
|
|
r.mux.HandleFunc("/api/v1/auth/register", r.registerHandler)
|
|
|
|
// Protected
|
|
r.mux.HandleFunc("/api/v1/sessions", r.authMW(r.sessionsHandler))
|
|
r.mux.HandleFunc("/api/v1/sessions/", r.authMW(r.sessionByIDHandler))
|
|
|
|
// Memory
|
|
r.mux.HandleFunc("/api/v1/memory/search", r.authMW(r.memorySearchHandler))
|
|
r.mux.HandleFunc("/api/v1/memory", r.authMW(r.memoryHandler))
|
|
r.mux.HandleFunc("/api/v1/memory/", r.authMW(r.memoryHandler))
|
|
|
|
// Voice
|
|
r.mux.HandleFunc("/api/v1/voice/stt", r.authMW(r.voiceSTTHandler))
|
|
r.mux.HandleFunc("/api/v1/voice/tts", r.authMW(r.voiceTTSHandler))
|
|
|
|
// Admin
|
|
r.mux.HandleFunc("/api/v1/admin/dashboard", r.authMW(r.adminMW(r.adminDashboardHandler)))
|
|
r.mux.HandleFunc("/api/v1/admin/llm-calls", r.authMW(r.adminMW(r.llmCallsHandler)))
|
|
r.mux.HandleFunc("/api/v1/admin/system", r.authMW(r.adminMW(r.systemInfoHandler)))
|
|
r.mux.HandleFunc("/api/v1/admin/plugins", r.authMW(r.adminMW(r.pluginListHandler)))
|
|
r.mux.HandleFunc("/api/v1/admin/sessions", r.authMW(r.adminMW(r.adminSessionsHandler)))
|
|
r.mux.HandleFunc("/api/v1/admin/models/config", r.authMW(r.adminMW(r.modelConfigHandler)))
|
|
r.mux.HandleFunc("/api/v1/admin/models/config/", r.authMW(r.adminMW(r.modelConfigItemHandler)))
|
|
r.mux.HandleFunc("/api/v1/admin/trace", r.authMW(r.adminMW(r.traceHandler)))
|
|
r.mux.HandleFunc("/api/v1/admin/perf", r.authMW(r.adminMW(r.perfHandler)))
|
|
r.mux.HandleFunc("/api/v1/admin/bridge/config", r.authMW(r.adminMW(r.bridgeConfigHandler)))
|
|
r.mux.HandleFunc("/api/v1/admin/bridge/config/", r.authMW(r.adminMW(r.bridgeConfigItemHandler)))
|
|
|
|
// Automation & Reminders
|
|
r.mux.HandleFunc("/api/v1/automation/rules", r.authMW(r.automationHandler))
|
|
r.mux.HandleFunc("/api/v1/automation/rules/", r.authMW(r.automationItemHandler))
|
|
r.mux.HandleFunc("/api/v1/reminders", r.authMW(r.remindersHandler))
|
|
r.mux.HandleFunc("/api/v1/reminders/", r.authMW(r.reminderItemHandler))
|
|
|
|
// Notifications (push interface)
|
|
r.mux.HandleFunc("/api/v1/notifications/push", r.authMW(r.adminMW(r.notificationHandler)))
|
|
|
|
// Message search & session export
|
|
r.mux.HandleFunc("/api/v1/messages/search", r.authMW(r.messageSearchHandler))
|
|
|
|
// Image analysis
|
|
r.mux.HandleFunc("/api/v1/images/analyze", r.authMW(r.imageAnalyzeHandler))
|
|
|
|
// Host execution sandbox
|
|
r.mux.HandleFunc("/api/v1/tools/host-exec", r.authMW(r.adminMW(r.hostExecHandler)))
|
|
}
|
|
|
|
// ServeHTTP implements http.Handler.
|
|
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
|
if req.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
r.mux.ServeHTTP(w, req)
|
|
}
|
|
|
|
// --- Middleware ---
|
|
|
|
func (r *Router) authMW(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, req *http.Request) {
|
|
h := req.Header.Get("Authorization")
|
|
if h == "" || !strings.HasPrefix(strings.ToLower(h), "bearer ") {
|
|
writeJSON(w, 401, M{"error": "unauthorized"})
|
|
return
|
|
}
|
|
uid, uname, role, err := r.deps.AuthMgr.ValidateToken(h[7:])
|
|
if err != nil {
|
|
writeJSON(w, 401, M{"error": "invalid token"})
|
|
return
|
|
}
|
|
req.Header.Set("X-User-ID", uid)
|
|
req.Header.Set("X-Username", uname)
|
|
req.Header.Set("X-Role", role)
|
|
next(w, req)
|
|
}
|
|
}
|
|
|
|
func (r *Router) adminMW(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, req *http.Request) {
|
|
if req.Header.Get("X-Role") != "admin" {
|
|
writeJSON(w, 403, M{"error": "admin only"})
|
|
return
|
|
}
|
|
next(w, req)
|
|
}
|
|
}
|
|
|
|
// --- Handlers ---
|
|
|
|
func (r *Router) healthHandler(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, 200, M{"status": "ok", "service": "cyrene-gateway", "version": "2.0.0-dev"})
|
|
}
|
|
|
|
func (r *Router) loginHandler(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != "POST" {
|
|
writeJSON(w, 405, M{"error": "POST required"})
|
|
return
|
|
}
|
|
var body struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
json.NewDecoder(req.Body).Decode(&body)
|
|
|
|
token, err := r.deps.AuthMgr.Login(body.Username, body.Password)
|
|
if err != nil {
|
|
writeJSON(w, 401, M{"error": "invalid credentials"})
|
|
return
|
|
}
|
|
writeJSON(w, 200, M{"token": token})
|
|
}
|
|
|
|
func (r *Router) registerHandler(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != "POST" {
|
|
writeJSON(w, 405, M{"error": "POST required"})
|
|
return
|
|
}
|
|
var body struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
json.NewDecoder(req.Body).Decode(&body)
|
|
|
|
token, err := r.deps.AuthMgr.Register(body.Username, body.Password)
|
|
if err != nil {
|
|
writeJSON(w, 400, M{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 201, M{"token": token})
|
|
}
|
|
|
|
// --- Sessions ---
|
|
|
|
func (r *Router) sessionsHandler(w http.ResponseWriter, req *http.Request) {
|
|
switch req.Method {
|
|
case "GET":
|
|
writeJSON(w, 200, M{"sessions": []any{}})
|
|
case "POST":
|
|
writeJSON(w, 201, M{"session_id": randID()})
|
|
default:
|
|
writeJSON(w, 405, M{"error": "method not allowed"})
|
|
}
|
|
}
|
|
|
|
func (r *Router) sessionByIDHandler(w http.ResponseWriter, req *http.Request) {
|
|
id := strings.TrimPrefix(req.URL.Path, "/api/v1/sessions/")
|
|
// Handle export
|
|
if strings.HasSuffix(id, "/export") {
|
|
id = strings.TrimSuffix(id, "/export")
|
|
r.sessionExportHandler(w, req)
|
|
return
|
|
}
|
|
switch req.Method {
|
|
case "DELETE":
|
|
writeJSON(w, 204, nil)
|
|
case "GET":
|
|
writeJSON(w, 200, M{"session_id": id})
|
|
default:
|
|
writeJSON(w, 405, M{"error": "method not allowed"})
|
|
}
|
|
}
|
|
|
|
// --- Memory ---
|
|
|
|
func (r *Router) memorySearchHandler(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != "GET" {
|
|
writeJSON(w, 405, M{"error": "GET required"})
|
|
return
|
|
}
|
|
if r.deps.MemorySvc == nil {
|
|
writeJSON(w, 503, M{"error": "memory service unavailable"})
|
|
return
|
|
}
|
|
q := req.URL.Query().Get("q")
|
|
uid := req.Header.Get("X-User-ID")
|
|
|
|
result, err := r.deps.MemorySvc.Search(req.Context(), uid, q, 10)
|
|
if err != nil {
|
|
writeJSON(w, 500, M{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 200, M{"data": result})
|
|
}
|
|
|
|
func (r *Router) memoryHandler(w http.ResponseWriter, req *http.Request) {
|
|
if r.deps.MemorySvc == nil {
|
|
writeJSON(w, 503, M{"error": "memory service unavailable"})
|
|
return
|
|
}
|
|
|
|
// Extract ID from path: /api/v1/memory/{id}
|
|
path := strings.TrimPrefix(req.URL.Path, "/api/v1/memory")
|
|
path = strings.TrimPrefix(path, "/")
|
|
userID := req.Header.Get("X-User-ID")
|
|
|
|
switch {
|
|
case path == "" && req.Method == "GET":
|
|
result, _ := r.deps.MemorySvc.List(req.Context(), userID, 20)
|
|
writeJSON(w, 200, M{"data": result})
|
|
|
|
case path == "" && req.Method == "POST":
|
|
var body struct {
|
|
Content string `json:"content"`
|
|
Category string `json:"category"`
|
|
Priority float64 `json:"priority"`
|
|
}
|
|
json.NewDecoder(req.Body).Decode(&body)
|
|
if body.Category == "" { body.Category = "fact" }
|
|
mem, err := r.deps.MemorySvc.Add(req.Context(), userID, body.Content, body.Category, body.Priority)
|
|
if err != nil {
|
|
writeJSON(w, 500, M{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 201, M{"data": mem})
|
|
|
|
case path != "" && req.Method == "DELETE":
|
|
r.deps.MemorySvc.Delete(req.Context(), path)
|
|
writeJSON(w, 204, nil)
|
|
|
|
case path != "" && req.Method == "GET":
|
|
mem, err := r.deps.MemorySvc.Get(req.Context(), path)
|
|
if err != nil {
|
|
writeJSON(w, 404, M{"error": "memory not found"})
|
|
return
|
|
}
|
|
writeJSON(w, 200, M{"data": mem})
|
|
|
|
default:
|
|
writeJSON(w, 405, M{"error": "method not allowed"})
|
|
}
|
|
}
|
|
|
|
// --- Voice ---
|
|
|
|
func (r *Router) voiceSTTHandler(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != "POST" {
|
|
writeJSON(w, 405, M{"error": "POST required"})
|
|
return
|
|
}
|
|
if r.deps.VoiceSvc == nil {
|
|
writeJSON(w, 503, M{"error": "voice service unavailable"})
|
|
return
|
|
}
|
|
var body struct{ AudioData []byte `json:"audio_data"` }
|
|
json.NewDecoder(req.Body).Decode(&body)
|
|
text, err := r.deps.VoiceSvc.Recognize(req.Context(), body.AudioData)
|
|
if err != nil {
|
|
writeJSON(w, 500, M{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 200, M{"text": text})
|
|
}
|
|
|
|
func (r *Router) voiceTTSHandler(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != "POST" {
|
|
writeJSON(w, 405, M{"error": "POST required"})
|
|
return
|
|
}
|
|
if r.deps.VoiceSvc == nil {
|
|
writeJSON(w, 503, M{"error": "voice service unavailable"})
|
|
return
|
|
}
|
|
var body struct {
|
|
Text string `json:"text"`
|
|
Voice string `json:"voice"`
|
|
}
|
|
json.NewDecoder(req.Body).Decode(&body)
|
|
if body.Voice == "" { body.Voice = "zh-CN-XiaoxiaoNeural" }
|
|
audio, err := r.deps.VoiceSvc.Synthesize(req.Context(), body.Text, body.Voice)
|
|
if err != nil {
|
|
writeJSON(w, 500, M{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 200, M{"audio_size": len(audio)})
|
|
}
|
|
|
|
// --- Admin ---
|
|
|
|
var startTime = time.Now()
|
|
|
|
func (r *Router) adminDashboardHandler(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, 200, M{
|
|
"dashboard": M{
|
|
"status": "running",
|
|
"version": "2.0.0-dev",
|
|
"uptime_sec": int(time.Since(startTime).Seconds()),
|
|
"services": M{
|
|
"memory": r.deps.MemorySvc != nil,
|
|
"voice": r.deps.VoiceSvc != nil,
|
|
"ai_core": r.deps.Orchestrator != nil,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// --- Admin extra handlers ---
|
|
|
|
func (r *Router) llmCallsHandler(w http.ResponseWriter, req *http.Request) {
|
|
if r.deps.LLMLogger == nil {
|
|
writeJSON(w, 200, M{"calls": []any{}, "stats": M{}})
|
|
return
|
|
}
|
|
writeJSON(w, 200, M{
|
|
"calls": r.deps.LLMLogger.Recent(50),
|
|
"stats": r.deps.LLMLogger.Stats(),
|
|
})
|
|
}
|
|
|
|
func (r *Router) systemInfoHandler(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, 200, M{
|
|
"system": M{
|
|
"version": "2.0.0-dev",
|
|
"go_version": "1.26",
|
|
"uptime_sec": int(time.Since(startTime).Seconds()),
|
|
"ws_clients": r.deps.WSHub.Count(),
|
|
},
|
|
})
|
|
}
|
|
|
|
func (r *Router) pluginListHandler(w http.ResponseWriter, _ *http.Request) {
|
|
plugins := r.deps.PluginList
|
|
if plugins == nil { plugins = []map[string]string{} }
|
|
writeJSON(w, 200, M{"plugins": plugins})
|
|
}
|
|
|
|
func (r *Router) adminSessionsHandler(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, 200, M{"sessions": []any{}, "total": 0})
|
|
}
|
|
|
|
// --- Model Config ---
|
|
|
|
func (r *Router) modelConfigHandler(w http.ResponseWriter, req *http.Request) {
|
|
if r.deps.ModelConfig == nil {
|
|
writeJSON(w, 503, M{"error": "model config unavailable"})
|
|
return
|
|
}
|
|
switch req.Method {
|
|
case "GET":
|
|
writeJSON(w, 200, M{
|
|
"providers": r.deps.ModelConfig.GetProviders(),
|
|
"models": r.deps.ModelConfig.GetModels(),
|
|
"routing": r.deps.ModelConfig.GetRouting(),
|
|
})
|
|
default:
|
|
writeJSON(w, 405, M{"error": "method not allowed"})
|
|
}
|
|
}
|
|
|
|
func (r *Router) modelConfigItemHandler(w http.ResponseWriter, req *http.Request) {
|
|
if r.deps.ModelConfig == nil {
|
|
writeJSON(w, 503, M{"error": "model config unavailable"})
|
|
return
|
|
}
|
|
// Path: /api/v1/admin/models/config/{type}/{name}
|
|
path := strings.TrimPrefix(req.URL.Path, "/api/v1/admin/models/config/")
|
|
parts := strings.SplitN(path, "/", 2)
|
|
if len(parts) < 2 {
|
|
writeJSON(w, 400, M{"error": "invalid path"})
|
|
return
|
|
}
|
|
kind, name := parts[0], parts[1]
|
|
|
|
switch req.Method {
|
|
case "POST", "PUT":
|
|
var body map[string]any
|
|
json.NewDecoder(req.Body).Decode(&body)
|
|
switch kind {
|
|
case "providers":
|
|
r.deps.ModelConfig.SaveProvider(&ModelProvider{
|
|
Name: name, BaseURL: str(body, "base_url"),
|
|
APIKey: str(body, "api_key"), Timeout: num(body, "timeout_sec"),
|
|
Retries: num(body, "max_retries"),
|
|
})
|
|
case "models":
|
|
var tags []string
|
|
if t, ok := body["tags"].([]any); ok {
|
|
for _, v := range t { tags = append(tags, fmt.Sprint(v)) }
|
|
}
|
|
r.deps.ModelConfig.SaveModel(&ModelEntry{
|
|
ID: name, Name: str(body, "name"), Provider: str(body, "provider"),
|
|
Description: str(body, "description"), Tags: tags,
|
|
Params: body["params"].(map[string]any), Enabled: true,
|
|
})
|
|
case "routing":
|
|
var chain []string
|
|
if c, ok := body["fallback_chain"].([]any); ok {
|
|
for _, v := range c { chain = append(chain, fmt.Sprint(v)) }
|
|
}
|
|
r.deps.ModelConfig.SaveRouting(&RoutingRule{
|
|
Purpose: name, FallbackChain: chain, Required: body["required"].(bool),
|
|
})
|
|
}
|
|
writeJSON(w, 200, M{"ok": true})
|
|
|
|
case "DELETE":
|
|
switch kind {
|
|
case "providers": r.deps.ModelConfig.DeleteProvider(name)
|
|
case "models": r.deps.ModelConfig.DeleteModel(name)
|
|
case "routing": r.deps.ModelConfig.DeleteRouting(name)
|
|
}
|
|
writeJSON(w, 204, nil)
|
|
|
|
default:
|
|
writeJSON(w, 405, M{"error": "method not allowed"})
|
|
}
|
|
}
|
|
|
|
func str(m map[string]any, key string) string {
|
|
if v, ok := m[key].(string); ok { return v }
|
|
return ""
|
|
}
|
|
func num(m map[string]any, key string) int {
|
|
if v, ok := m[key].(float64); ok { return int(v) }
|
|
return 0
|
|
}
|
|
|
|
// --- Trace & Perf ---
|
|
|
|
func (r *Router) traceHandler(w http.ResponseWriter, req *http.Request) {
|
|
if r.deps.TraceLog == nil {
|
|
writeJSON(w, 200, M{"traces": []any{}})
|
|
return
|
|
}
|
|
if req.Method == "DELETE" {
|
|
r.deps.TraceLog.Clear()
|
|
writeJSON(w, 200, M{"ok": true})
|
|
return
|
|
}
|
|
writeJSON(w, 200, M{"traces": r.deps.TraceLog.Recent(50)})
|
|
}
|
|
|
|
func (r *Router) perfHandler(w http.ResponseWriter, req *http.Request) {
|
|
if r.deps.PerfMon == nil {
|
|
writeJSON(w, 200, M{"latest": M{}, "history": []any{}})
|
|
return
|
|
}
|
|
writeJSON(w, 200, M{
|
|
"latest": r.deps.PerfMon.Latest(),
|
|
"history": r.deps.PerfMon.History(),
|
|
})
|
|
}
|
|
|
|
// --- Bridge Config ---
|
|
|
|
func (r *Router) bridgeConfigHandler(w http.ResponseWriter, req *http.Request) {
|
|
if r.deps.BridgeCfg == nil {
|
|
writeJSON(w, 503, M{"error": "bridge config unavailable"})
|
|
return
|
|
}
|
|
switch req.Method {
|
|
case "GET":
|
|
writeJSON(w, 200, M{"adapters": r.deps.BridgeCfg.GetAdapters()})
|
|
default:
|
|
writeJSON(w, 405, M{"error": "method not allowed"})
|
|
}
|
|
}
|
|
|
|
func (r *Router) bridgeConfigItemHandler(w http.ResponseWriter, req *http.Request) {
|
|
if r.deps.BridgeCfg == nil {
|
|
writeJSON(w, 503, M{"error": "bridge config unavailable"})
|
|
return
|
|
}
|
|
name := strings.TrimPrefix(req.URL.Path, "/api/v1/admin/bridge/config/")
|
|
if name == "" {
|
|
writeJSON(w, 400, M{"error": "missing adapter name"})
|
|
return
|
|
}
|
|
|
|
switch req.Method {
|
|
case "PUT", "POST":
|
|
var body struct {
|
|
Type string `json:"type"`
|
|
Enabled bool `json:"enabled"`
|
|
Settings map[string]string `json:"settings"`
|
|
}
|
|
json.NewDecoder(req.Body).Decode(&body)
|
|
if err := r.deps.BridgeCfg.SaveAdapter(name, body.Type, body.Enabled, body.Settings); err != nil {
|
|
writeJSON(w, 500, M{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 200, M{"ok": true})
|
|
|
|
case "DELETE":
|
|
if err := r.deps.BridgeCfg.DeleteAdapter(name); err != nil {
|
|
writeJSON(w, 500, M{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 204, nil)
|
|
|
|
default:
|
|
writeJSON(w, 405, M{"error": "method not allowed"})
|
|
}
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
type M = map[string]any
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, data any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
if data != nil {
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|
|
}
|
|
|
|
func randID() string {
|
|
b := make([]byte, 8)
|
|
rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func (r *Router) userID(req *http.Request) string { return req.Header.Get("X-User-ID") }
|
|
|
|
// Ensure unused imports don't cause issues
|
|
|