This commit is contained in:
+195
-8
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"kb-editor/internal/aifallback"
|
||||
"kb-editor/internal/staging"
|
||||
"kb-editor/internal/store"
|
||||
)
|
||||
|
||||
@@ -23,13 +25,15 @@ type appConfig struct {
|
||||
AIFallbackEnabled bool `json:"ai_fallback_enabled"`
|
||||
AIFallbackTimeoutSeconds int `json:"ai_fallback_timeout_seconds,omitempty"`
|
||||
AIFallbackModel string `json:"ai_fallback_model,omitempty"`
|
||||
StagingEnabled bool `json:"staging_enabled"`
|
||||
}
|
||||
|
||||
type app struct {
|
||||
store *store.Store
|
||||
web fs.FS
|
||||
config appConfig
|
||||
ai *aifallback.Service
|
||||
store *store.Store
|
||||
web fs.FS
|
||||
config appConfig
|
||||
ai *aifallback.Service
|
||||
staging *staging.Store
|
||||
}
|
||||
|
||||
func newApp(s *store.Store, web fs.FS, configs ...appConfig) *app {
|
||||
@@ -45,6 +49,12 @@ func (a *app) withAI(service *aifallback.Service) *app {
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *app) withStaging(st *staging.Store) *app {
|
||||
a.staging = st
|
||||
a.config.StagingEnabled = st != nil
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *app) routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /api/health", a.handleHealth)
|
||||
@@ -54,16 +64,25 @@ func (a *app) routes() http.Handler {
|
||||
mux.HandleFunc("GET /api/facets", a.handleFacets)
|
||||
mux.HandleFunc("GET /api/items/{key}", a.handleGet)
|
||||
mux.HandleFunc("POST /api/ai/fallback", a.handleAIFallback)
|
||||
mux.HandleFunc("GET /api/staging", a.handleStagingList)
|
||||
mux.HandleFunc("GET /api/staging/{key}", a.handleStagingGet)
|
||||
|
||||
if a.config.Writable {
|
||||
mux.HandleFunc("PUT /api/items/{key}", a.handlePut)
|
||||
mux.HandleFunc("POST /api/bulk", a.handleBulk)
|
||||
mux.HandleFunc("POST /api/reload", a.handleReload)
|
||||
mux.HandleFunc("PUT /api/staging/{key}", a.handleStagingPut)
|
||||
mux.HandleFunc("DELETE /api/staging/{key}", a.handleStagingDelete)
|
||||
mux.HandleFunc("POST /api/staging/{key}/promote", a.handleStagingPromote)
|
||||
mux.HandleFunc("POST /api/staging/bulk", a.handleStagingBulk)
|
||||
} else {
|
||||
mux.HandleFunc("PUT /api/items/{key}", a.handleReadOnly)
|
||||
mux.HandleFunc("POST /api/bulk", a.handleReadOnly)
|
||||
mux.HandleFunc("POST /api/reload", a.handleReadOnly)
|
||||
mux.HandleFunc("PUT /api/staging/{key}", a.handleReadOnly)
|
||||
mux.HandleFunc("DELETE /api/staging/{key}", a.handleReadOnly)
|
||||
mux.HandleFunc("POST /api/staging/{key}/promote", a.handleReadOnly)
|
||||
mux.HandleFunc("POST /api/staging/bulk", a.handleReadOnly)
|
||||
}
|
||||
|
||||
static := http.FileServer(http.FS(a.web))
|
||||
@@ -90,6 +109,11 @@ func (a *app) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
"mode": a.config.Mode,
|
||||
"writable": a.config.Writable,
|
||||
"ai_fallback_enabled": a.config.AIFallbackEnabled && a.ai != nil,
|
||||
"staging_enabled": a.staging != nil,
|
||||
}
|
||||
if a.staging != nil {
|
||||
payload["staging_count"] = a.staging.Count()
|
||||
payload["staging_dir"] = a.staging.Dir()
|
||||
}
|
||||
if a.config.Writable {
|
||||
payload["backup_dir"] = a.store.BackupDir()
|
||||
@@ -187,12 +211,40 @@ func (a *app) handleAIFallback(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, result)
|
||||
}
|
||||
|
||||
func (a *app) handleStagingGet(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.config.AIFallbackEnabled || a.ai == nil {
|
||||
writeError(w, http.StatusNotFound, "Staging-Viewer ist auf dieser Instanz deaktiviert")
|
||||
func (a *app) handleStagingList(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.config.Writable {
|
||||
writeError(w, http.StatusForbidden, "Die Staging-Liste ist nur im Editor-Modus verfügbar")
|
||||
return
|
||||
}
|
||||
result, err := a.ai.GetStaging(r.PathValue("key"))
|
||||
if a.staging == nil {
|
||||
writeError(w, http.StatusNotFound, "Staging ist auf dieser Instanz nicht konfiguriert")
|
||||
return
|
||||
}
|
||||
v := r.URL.Query()
|
||||
page, _ := strconv.Atoi(v.Get("page"))
|
||||
pageSize, _ := strconv.Atoi(v.Get("page_size"))
|
||||
result, err := a.staging.List(staging.Query{
|
||||
Q: v.Get("q"),
|
||||
AutoReply: v.Get("auto_reply"),
|
||||
Language: v.Get("language"),
|
||||
CommunicationStyle: v.Get("communication_style"),
|
||||
Source: v.Get("source"),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (a *app) handleStagingGet(w http.ResponseWriter, r *http.Request) {
|
||||
if a.staging == nil || (!a.config.Writable && (!a.config.AIFallbackEnabled || a.ai == nil)) {
|
||||
writeError(w, http.StatusNotFound, "Staging ist auf dieser Instanz nicht konfiguriert")
|
||||
return
|
||||
}
|
||||
result, err := a.staging.Get(r.PathValue("key"))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
writeError(w, http.StatusNotFound, "Staging-Eintrag nicht gefunden")
|
||||
return
|
||||
@@ -204,6 +256,141 @@ func (a *app) handleStagingGet(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
type stagingBulkRequest struct {
|
||||
Keys []string `json:"keys"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
func (a *app) handleStagingPut(w http.ResponseWriter, r *http.Request) {
|
||||
if a.staging == nil {
|
||||
writeError(w, http.StatusNotFound, "Staging ist auf dieser Instanz nicht konfiguriert")
|
||||
return
|
||||
}
|
||||
if !mustJSONContentType(w, r) {
|
||||
return
|
||||
}
|
||||
var doc map[string]any
|
||||
if err := decodeJSON(r, &doc); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
result, err := a.staging.Update(r.PathValue("key"), doc)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
writeError(w, http.StatusNotFound, "Staging-Eintrag nicht gefunden")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "document": result.Document, "meta": result.Meta})
|
||||
}
|
||||
|
||||
func (a *app) handleStagingDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if a.staging == nil {
|
||||
writeError(w, http.StatusNotFound, "Staging ist auf dieser Instanz nicht konfiguriert")
|
||||
return
|
||||
}
|
||||
trash, err := a.staging.Delete(r.PathValue("key"))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
writeError(w, http.StatusNotFound, "Staging-Eintrag nicht gefunden")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "trash": trash})
|
||||
}
|
||||
|
||||
func (a *app) handleStagingPromote(w http.ResponseWriter, r *http.Request) {
|
||||
if a.staging == nil {
|
||||
writeError(w, http.StatusNotFound, "Staging ist auf dieser Instanz nicht konfiguriert")
|
||||
return
|
||||
}
|
||||
result, err := a.promoteStaging(r.PathValue("key"))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
writeError(w, http.StatusNotFound, "Staging-Eintrag nicht gefunden")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, result)
|
||||
}
|
||||
|
||||
func (a *app) handleStagingBulk(w http.ResponseWriter, r *http.Request) {
|
||||
if a.staging == nil {
|
||||
writeError(w, http.StatusNotFound, "Staging ist auf dieser Instanz nicht konfiguriert")
|
||||
return
|
||||
}
|
||||
if !mustJSONContentType(w, r) {
|
||||
return
|
||||
}
|
||||
var req stagingBulkRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige Anfrage: "+err.Error())
|
||||
return
|
||||
}
|
||||
if len(req.Keys) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "Keine Staging-Dateien ausgewählt")
|
||||
return
|
||||
}
|
||||
if len(req.Keys) > 500 {
|
||||
writeError(w, http.StatusBadRequest, "Maximal 500 Staging-Dateien pro Vorgang")
|
||||
return
|
||||
}
|
||||
action := strings.ToLower(strings.TrimSpace(req.Action))
|
||||
if action != "promote" && action != "delete" {
|
||||
writeError(w, http.StatusBadRequest, "action muss promote oder delete sein")
|
||||
return
|
||||
}
|
||||
type itemResult struct {
|
||||
Key string `json:"key"`
|
||||
OK bool `json:"ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
items := make([]itemResult, 0, len(req.Keys))
|
||||
succeeded := 0
|
||||
for _, key := range req.Keys {
|
||||
key = strings.TrimSpace(key)
|
||||
var err error
|
||||
if action == "promote" {
|
||||
_, err = a.promoteStaging(key)
|
||||
} else {
|
||||
_, err = a.staging.Delete(key)
|
||||
}
|
||||
item := itemResult{Key: key, OK: err == nil}
|
||||
if err != nil {
|
||||
item.Error = err.Error()
|
||||
} else {
|
||||
succeeded++
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"action": action, "targeted": len(req.Keys), "succeeded": succeeded,
|
||||
"failed": len(req.Keys) - succeeded, "items": items,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *app) promoteStaging(key string) (map[string]any, error) {
|
||||
staged, err := a.staging.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
summary, err := a.store.ImportDocument(staged.Document, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
archive, err := a.staging.ArchiveApproved(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Produktivdatei wurde erstellt (%s), aber Staging konnte nicht als freigegeben archiviert werden: %w", summary.RelPath, err)
|
||||
}
|
||||
return map[string]any{"ok": true, "production": summary, "staging_key": key, "staging_archive": archive}, nil
|
||||
}
|
||||
|
||||
func (a *app) handleReadOnly(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusForbidden, "Diese Instanz läuft im Google-/Viewer-Modus und ist schreibgeschützt")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user