package main import ( "encoding/json" "net/http" "strings" "inou/lib" ) type CategoryCount struct { Shown int `json:"shown"` Hidden int `json:"hidden"` } func handleCategories(w http.ResponseWriter, r *http.Request) { // Get accessor (who is asking) ctx := getAccessContextOrFail(w, r) if ctx == nil { return } dossierHex := r.URL.Query().Get("dossier") if dossierHex == "" { http.Error(w, "missing dossier", http.StatusBadRequest) return } dossierID := dossierHex obsType := r.URL.Query().Get("type") category := r.URL.Query().Get("category") // Pass accessor + dossier to lib - RBAC handled there var counts map[string]CategoryCount if obsType == "" { counts = getTopLevelCounts(ctx.AccessorID, dossierID) } else if obsType == "genome" { if category != "" { counts = getGenomeSubcategoryCounts(ctx.AccessorID, dossierID, category) } else { counts = getGenomeCounts(ctx.AccessorID, dossierID) } } else { counts = make(map[string]CategoryCount) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(counts) } func getTopLevelCounts(accessorID, dossierID string) map[string]CategoryCount { counts := make(map[string]CategoryCount) categoryNames := map[int]string{ 1: "imaging", 2: "documents", 3: "labs", 4: "genome", } // Check each category - only include if accessor has access for catInt, catName := range categoryNames { // Try to get a count by querying entries with RBAC entries, err := lib.EntryList(accessorID, "", catInt, &lib.EntryFilter{ DossierID: dossierID, Limit: 1, // Just check if we can see any }) if err != nil || len(entries) == 0 { continue // No access or no entries } // Get actual count (using system context for counting only) var catCounts []struct { Count int `db:"cnt"` } lib.Query("SELECT COUNT(*) as cnt FROM entries WHERE dossier_id = ? AND category = ?", []any{dossierID, catInt}, &catCounts) if len(catCounts) > 0 && catCounts[0].Count > 0 { counts[catName] = CategoryCount{Shown: catCounts[0].Count, Hidden: 0} } } // For genome, replace with detailed subcategory counts genomeCats := getGenomeCounts(accessorID, dossierID) if len(genomeCats) > 0 { totalShown, totalHidden := 0, 0 for _, c := range genomeCats { totalShown += c.Shown totalHidden += c.Hidden } counts["genome"] = CategoryCount{Shown: len(genomeCats), Hidden: totalHidden} } return counts } // variantData for parsing the data JSON type variantData struct { Mag float64 `json:"mag"` Rep string `json:"rep"` Sub string `json:"sub"` } // shouldIncludeVariant returns true if the variant should be counted/shown func shouldIncludeVariant(data variantData, includeHidden bool) bool { if includeHidden { return true } // Hide high magnitude variants if data.Mag > 4.0 { return false } // Hide "Bad" repute variants if data.Rep == "Bad" || data.Rep == "bad" { return false } return true } // getGenomeCounts reads cached counts from the extraction entry (fast path) func getGenomeCounts(accessorID, dossierID string) map[string]CategoryCount { counts := make(map[string]CategoryCount) // Create access context for RBAC ctx := &lib.AccessContext{AccessorID: accessorID} // Find extraction entry and read its data extraction, err := lib.GenomeGetExtraction(ctx, dossierID) if err != nil { return counts } if extraction.Data == "" { return counts } // Parse extraction data which contains pre-computed counts var extractionData struct { Counts map[string]CategoryCount `json:"counts"` } if err := json.Unmarshal([]byte(extraction.Data), &extractionData); err != nil { return counts } // Return cached counts if available if extractionData.Counts != nil { return extractionData.Counts } // Fallback: compute counts (for old data without cached counts) return getGenomeCountsSlow(accessorID, dossierID) } // getGenomeCountsSlow computes counts by scanning all variants (fallback for old data) func getGenomeCountsSlow(accessorID, dossierID string) map[string]CategoryCount { counts := make(map[string]CategoryCount) // Create access context for RBAC ctx := &lib.AccessContext{AccessorID: accessorID} // Find extraction entry extraction, err := lib.GenomeGetExtraction(ctx, dossierID) if err != nil { return counts } // Get all tiers tiers, err := lib.GenomeGetTiers(ctx, dossierID, extraction.EntryID) if err != nil { return counts } // For each tier, count shown and hidden variants for _, tier := range tiers { variants, err := lib.GenomeGetVariantsByTier(ctx, dossierID, tier.TierID) if err != nil { continue } shown, hidden := 0, 0 for _, v := range variants { data := variantData{ Mag: v.Magnitude, Rep: v.Repute, Sub: v.Subcategory, } if shouldIncludeVariant(data, false) { shown++ } else { hidden++ } } if shown > 0 || hidden > 0 { counts[tier.Category] = CategoryCount{Shown: shown, Hidden: hidden} } } return counts } func getGenomeSubcategoryCounts(accessorID, dossierID string, category string) map[string]CategoryCount { counts := make(map[string]CategoryCount) shownCounts := make(map[string]int) hiddenCounts := make(map[string]int) // Create access context for RBAC ctx := &lib.AccessContext{AccessorID: accessorID} // Find extraction entry extraction, err := lib.GenomeGetExtraction(ctx, dossierID) if err != nil { return counts } // Find tier for this category tier, err := lib.GenomeGetTierByCategory(ctx, dossierID, extraction.EntryID, category) if err != nil { return counts } // Get variants and count by subcategory variants, err := lib.GenomeGetVariantsByTier(ctx, dossierID, tier.TierID) if err != nil { return counts } for _, v := range variants { if v.Subcategory == "" { continue } data := variantData{ Mag: v.Magnitude, Rep: v.Repute, Sub: v.Subcategory, } if shouldIncludeVariant(data, false) { shownCounts[v.Subcategory]++ } else { hiddenCounts[v.Subcategory]++ } } // Combine into CategoryCount allSubs := make(map[string]bool) for k := range shownCounts { allSubs[k] = true } for k := range hiddenCounts { allSubs[k] = true } for sub := range allSubs { counts[sub] = CategoryCount{Shown: shownCounts[sub], Hidden: hiddenCounts[sub]} } return counts } // Unused but keeping for compatibility - remove if not needed var _ = strings.Contains