Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
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.
2026-08-01 15:44:40 -07:00

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
}