package main import ( "encoding/json" "inou/lib" "net/http" "time" ) type EntryRequest struct { ID string `json:"id"` Dossier string `json:"dossier"` Parent string `json:"parent"` Product string `json:"product"` Category string `json:"category"` Type string `json:"type"` Value string `json:"value"` Ordinal int `json:"ordinal"` Timestamp int64 `json:"timestamp"` TimestampEnd int64 `json:"timestamp_end"` Status int `json:"status"` Tags string `json:"tags"` Data string `json:"data"` Delete bool `json:"delete"` DeleteChildren bool `json:"delete_children"` DeleteCategory bool `json:"delete_category"` } type EntryResponse struct { ID string `json:"id"` Dossier string `json:"dossier"` Parent string `json:"parent,omitempty"` Product string `json:"product,omitempty"` Category string `json:"category"` Type string `json:"type"` Value string `json:"value,omitempty"` Ordinal int `json:"ordinal"` Timestamp int64 `json:"timestamp"` TimestampEnd int64 `json:"timestamp_end,omitempty"` Status int `json:"status"` Tags string `json:"tags,omitempty"` Data string `json:"data,omitempty"` } func handleEntries(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Method == "GET" { handleEntriesGet(w, r) return } if r.Method != "POST" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // For POST: require localhost OR valid auth with write permission ctx := getAccessContextOrSystem(w, r) if ctx == nil { return } lang := r.URL.Query().Get("lang") if lang == "" { lang = "en" } // Try array first, then single object var requests []EntryRequest decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&requests); err != nil { // Reset and try single object r.Body.Close() http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Handle delete_category first (before any inserts) for _, req := range requests { if req.DeleteCategory && req.Dossier != "" && req.Category != "" { dossierID := req.Dossier catInt, ok := lib.CategoryFromString[req.Category] if !ok { http.Error(w, "unknown category: "+req.Category, http.StatusBadRequest) return } if err := lib.EntryDeleteByCategory(ctx, dossierID, catInt); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } // Collect entries to insert/update var toInsert []*lib.Entry var responses []EntryResponse now := time.Now().Unix() for i, req := range requests { // Skip if this was just a delete_category request if req.DeleteCategory && req.Type == "" { continue } // Handle single delete if req.Delete && req.ID != "" { entryID := req.ID if req.DeleteChildren { if err := lib.EntryDeleteTree(ctx, req.Dossier, entryID); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } else { if err := lib.EntryDelete(entryID); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } responses = append(responses, EntryResponse{ID: req.ID}) continue } // Handle update if req.ID != "" { entryID := req.ID existing, err := lib.EntryGet(ctx, entryID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Update only provided fields if req.Category != "" { if catInt, ok := lib.CategoryFromString[req.Category]; ok { existing.Category = catInt } } if req.Type != "" { existing.Type = req.Type } if req.Value != "" { existing.Value = req.Value } if req.Ordinal != 0 { existing.Ordinal = req.Ordinal } if req.Timestamp != 0 { existing.Timestamp = req.Timestamp } if req.TimestampEnd != 0 { existing.TimestampEnd = req.TimestampEnd } if req.Status != 0 { existing.Status = req.Status } if req.Tags != "" { existing.Tags = req.Tags } if req.Data != "" { existing.Data = req.Data } if err := lib.EntryModify(existing); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } responses = append(responses, entryToResponse(existing, lang)) continue } // Insert new entry if req.Dossier == "" { http.Error(w, "dossier required for insert", http.StatusBadRequest) return } catInt := lib.CategoryFromString[req.Category] // defaults to 0 (CategoryImaging) if not found e := &lib.Entry{ DossierID: req.Dossier, ParentID: req.Parent, ProductID: req.Product, Category: catInt, Type: req.Type, Value: req.Value, Ordinal: req.Ordinal, Timestamp: req.Timestamp, TimestampEnd: req.TimestampEnd, Status: req.Status, Tags: req.Tags, Data: req.Data, } if e.Timestamp == 0 { e.Timestamp = now } if e.Ordinal == 0 { e.Ordinal = i } toInsert = append(toInsert, e) } // Batch insert if len(toInsert) > 0 { if err := lib.EntryAddBatch(toInsert); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } for _, e := range toInsert { responses = append(responses, entryToResponse(e, lang)) } } json.NewEncoder(w).Encode(responses) } func entryToResponse(e *lib.Entry, lang string) EntryResponse { return EntryResponse{ ID: e.EntryID, Dossier: e.DossierID, Parent: e.ParentID, Product: e.ProductID, Category: lib.CategoryTranslate(e.Category, lang), Type: e.Type, Value: e.Value, Ordinal: e.Ordinal, Timestamp: e.Timestamp, TimestampEnd: e.TimestampEnd, Status: e.Status, Tags: e.Tags, Data: e.Data, } } func handleEntriesGet(w http.ResponseWriter, r *http.Request) { ctx := getAccessContextOrFail(w, r) if ctx == nil { return } dossierID := r.URL.Query().Get("dossier") if dossierID == "" { http.Error(w, "dossier parameter required", http.StatusBadRequest) return } // RBAC: Check read access to dossier if !requireDossierAccess(w, ctx, dossierID) { return } lang := r.URL.Query().Get("lang") if lang == "" { lang = "en" } // Get date filter (default to today) dateStr := r.URL.Query().Get("date") var fromTs, toTs int64 if dateStr == "today" || dateStr == "" { now := time.Now() start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) fromTs = start.Unix() toTs = start.Add(24 * time.Hour).Unix() } categoryStr := r.URL.Query().Get("category") typ := r.URL.Query().Get("type") catInt := -1 // -1 means no category filter if categoryStr != "" { if c, ok := lib.CategoryFromString[categoryStr]; ok { catInt = c } } entries, err := lib.EntryQuery(dossierID, catInt, typ) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Filter by date if specified var filtered []*lib.Entry for _, e := range entries { if fromTs > 0 && (e.Timestamp < fromTs || e.Timestamp >= toTs) { continue } filtered = append(filtered, e) } var responses []EntryResponse for _, e := range filtered { responses = append(responses, entryToResponse(e, lang)) } if responses == nil { responses = []EntryResponse{} } json.NewEncoder(w).Encode(responses) }