mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
167 lines
4.6 KiB
Go
167 lines
4.6 KiB
Go
package tui
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"github.com/insights/insights/phrocs/internal/config"
|
|
"github.com/insights/insights/phrocs/internal/process"
|
|
)
|
|
|
|
const ungroupedName = "Ungrouped"
|
|
const pinnedKey = "pinned" // reserved group value: pinned at the top, no header
|
|
|
|
// sidebarEntry represents either a group header, a spacer, or a process in the grouped sidebar.
|
|
type sidebarEntry struct {
|
|
// Only one of these is set.
|
|
groupHeader string // non-empty for header rows
|
|
proc *process.Process // non-nil for process rows
|
|
spacer bool // empty row for visual separation
|
|
|
|
// procIndex is the index into m.services for process entries,
|
|
// allowing navigation to map back to the flat services slice.
|
|
procIndex int
|
|
}
|
|
|
|
func (e sidebarEntry) isHeader() bool {
|
|
return e.groupHeader != ""
|
|
}
|
|
|
|
func (e sidebarEntry) isNonSelectable() bool {
|
|
return e.isHeader() || e.spacer
|
|
}
|
|
|
|
// groupDimensions discovers the available grouping dimensions from the config.
|
|
// User-declared dimensions (from `groups:` on each proc) come first, the
|
|
// inferred `capability` dimension is appended.
|
|
func groupDimensions(cfg *config.Config) []string {
|
|
seen := make(map[string]bool)
|
|
for _, pc := range cfg.Procs {
|
|
for dim := range pc.Groups {
|
|
seen[dim] = true
|
|
}
|
|
}
|
|
// Also include dimensions from group_order even if no process uses them yet
|
|
for dim := range cfg.GroupOrder {
|
|
seen[dim] = true
|
|
}
|
|
|
|
hasCapability := seen[config.CapabilityGroupKey]
|
|
delete(seen, config.CapabilityGroupKey)
|
|
|
|
dims := make([]string, 0, len(seen)+1)
|
|
for dim := range seen {
|
|
dims = append(dims, dim)
|
|
}
|
|
sort.Strings(dims)
|
|
if hasCapability {
|
|
dims = append(dims, config.CapabilityGroupKey)
|
|
}
|
|
return dims
|
|
}
|
|
|
|
// initialGroupDimIndex resolves the config's default_group dimension to an
|
|
// index into dims. Returns -1 (no grouping) when default_group is unset or
|
|
// names a dimension that isn't available.
|
|
func initialGroupDimIndex(dims []string, defaultGroup string) int {
|
|
if defaultGroup == "" {
|
|
return -1
|
|
}
|
|
for i, d := range dims {
|
|
if d == defaultGroup {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// groupOrderFor returns the display order for a given dimension.
|
|
// If group_order is configured, uses that. Otherwise discovers groups from
|
|
// processes in the order they first appear.
|
|
func groupOrderFor(dim string, cfg *config.Config, services []*process.Process) []string {
|
|
if order, ok := cfg.GroupOrder[dim]; ok && len(order) > 0 {
|
|
return order
|
|
}
|
|
// Discover from processes, preserving first-seen order
|
|
seen := make(map[string]bool)
|
|
var order []string
|
|
for _, p := range services {
|
|
if g, ok := p.Cfg.Groups[dim]; ok && !seen[g] {
|
|
seen[g] = true
|
|
order = append(order, g)
|
|
}
|
|
}
|
|
return order
|
|
}
|
|
|
|
// buildGroupedEntries creates an ordered list of sidebar entries with group headers
|
|
// interspersed among processes.
|
|
// When dim is empty, returns a flat list with no headers.
|
|
func buildGroupedEntries(services []*process.Process, dim string, cfg *config.Config) []sidebarEntry {
|
|
if dim == "" {
|
|
entries := make([]sidebarEntry, len(services))
|
|
for i, p := range services {
|
|
entries[i] = sidebarEntry{proc: p, procIndex: i}
|
|
}
|
|
return entries
|
|
}
|
|
|
|
groupOrder := groupOrderFor(dim, cfg, services)
|
|
|
|
// Bucket processes by group; "pinned" processes go to the top without a header
|
|
var pinned []sidebarEntry
|
|
groups := make(map[string][]sidebarEntry)
|
|
for i, p := range services {
|
|
g, ok := p.Cfg.Groups[dim]
|
|
if !ok {
|
|
g = ungroupedName
|
|
}
|
|
if g == pinnedKey {
|
|
pinned = append(pinned, sidebarEntry{proc: p, procIndex: i})
|
|
continue
|
|
}
|
|
groups[g] = append(groups[g], sidebarEntry{proc: p, procIndex: i})
|
|
}
|
|
|
|
// Build ordered entries: pinned first, then groups with headers.
|
|
// A spacer row is inserted before each group header for visual separation.
|
|
entries := append([]sidebarEntry{}, pinned...)
|
|
firstGroup := len(pinned) == 0
|
|
|
|
// Emit groups in configured order first
|
|
emitted := make(map[string]bool)
|
|
for _, g := range groupOrder {
|
|
procs := groups[g]
|
|
if len(procs) == 0 {
|
|
continue
|
|
}
|
|
if !firstGroup {
|
|
entries = append(entries, sidebarEntry{spacer: true})
|
|
}
|
|
firstGroup = false
|
|
entries = append(entries, sidebarEntry{groupHeader: g})
|
|
entries = append(entries, procs...)
|
|
emitted[g] = true
|
|
}
|
|
|
|
// Emit any remaining groups not in the configured order (alphabetically),
|
|
// including "Ungrouped"
|
|
var remaining []string
|
|
for g := range groups {
|
|
if !emitted[g] {
|
|
remaining = append(remaining, g)
|
|
}
|
|
}
|
|
sort.Strings(remaining)
|
|
for _, g := range remaining {
|
|
procs := groups[g]
|
|
if !firstGroup {
|
|
entries = append(entries, sidebarEntry{spacer: true})
|
|
}
|
|
firstGroup = false
|
|
entries = append(entries, sidebarEntry{groupHeader: g})
|
|
entries = append(entries, procs...)
|
|
}
|
|
|
|
return entries
|
|
}
|