SPB Git

Repository Public

inventory.ts 5.2 KB 129 lines typescript

1import type { Family, Lifecycle, NodeRecord, Role } from "./schemas";23/** Pure functions over node records — shared by server pages, the client explorer and unit tests. */45export function publicNodes(nodes: NodeRecord[]): NodeRecord[] {6  return nodes.filter((n) => n.publicVisibility).sort((a, b) => a.number - b.number);7}89export type Aggregate = {10  documented: number;11  active: number;12  /** null when at least one counted record lacks the value: never present a partial sum as a total */13  cpuCores: number | null;14  gpuCores: number | null;15  unifiedMemoryGB: number | null;16  storageGB: number | null;17  byFamily: { family: Family; count: number; memoryGB: number | null; cpuCores: number | null }[];18  byLifecycle: Partial<Record<Lifecycle, number>>;19  chips: string[];20  lastVerifiedAt: string | null;21};2223function sumOrNull(values: (number | null)[]): number | null {24  if (values.length === 0) return 0;25  return values.every((v): v is number => v !== null) ? values.reduce((a, b) => a + b, 0) : null;26}2728/** Planned / retired / unverified hardware never enters the totals. */29export const COUNTED: Lifecycle[] = ["active", "offline", "maintenance"];3031export function aggregate(nodes: NodeRecord[]): Aggregate {32  const counted = nodes.filter((n) => n.publicVisibility && COUNTED.includes(n.lifecycle));33  const families = [...new Set(counted.map((n) => n.family))];34  const byLifecycle: Partial<Record<Lifecycle, number>> = {};35  for (const n of nodes.filter((x) => x.publicVisibility)) byLifecycle[n.lifecycle] = (byLifecycle[n.lifecycle] ?? 0) + 1;36  const verified = counted.map((n) => n.verifiedAt).filter((d): d is string => d !== null).sort();37  return {38    documented: counted.length,39    active: counted.filter((n) => n.lifecycle === "active").length,40    cpuCores: sumOrNull(counted.map((n) => n.cpuCores)),41    gpuCores: sumOrNull(counted.map((n) => n.gpuCores)),42    unifiedMemoryGB: sumOrNull(counted.map((n) => n.unifiedMemoryGB)),43    storageGB: sumOrNull(counted.map((n) => n.storageGB)),44    byFamily: families45      .map((family) => {46        const group = counted.filter((n) => n.family === family);47        return {48          family,49          count: group.length,50          memoryGB: sumOrNull(group.map((n) => n.unifiedMemoryGB)),51          cpuCores: sumOrNull(group.map((n) => n.cpuCores)),52        };53      })54      .sort((a, b) => b.count - a.count || a.family.localeCompare(b.family)),55    byLifecycle,56    chips: [...new Set(counted.map((n) => n.chip).filter((c): c is string => c !== null))].sort(),57    lastVerifiedAt: verified.at(-1) ?? null,58  };59}6061export type NodeFilters = {62  family?: string;63  chip?: string;64  memory?: string; // minimum GB65  role?: string;66  lifecycle?: string;67  rack?: string;68  q?: string;69};7071export function filterNodes(nodes: NodeRecord[], f: NodeFilters): NodeRecord[] {72  const minMem = f.memory ? Number(f.memory) : null;73  const q = f.q?.trim().toLowerCase();74  return nodes.filter((n) => {75    if (f.family && n.family !== f.family) return false;76    if (f.chip && n.chip !== f.chip) return false;77    if (minMem !== null && !Number.isNaN(minMem) && (n.unifiedMemoryGB === null || n.unifiedMemoryGB < minMem)) return false;78    if (f.role && !n.role.includes(f.role as Role)) return false;79    if (f.lifecycle && n.lifecycle !== f.lifecycle) return false;80    if (f.rack && (f.rack === "none" ? n.rack !== null : n.rack !== f.rack)) return false;81    if (q && ![n.publicName, n.id, n.chip ?? "", n.family].some((s) => s.toLowerCase().includes(q))) return false;82    return true;83  });84}8586export const SORT_KEYS = ["number", "name", "family", "chip", "cpu", "gpu", "memory", "storage"] as const;87export type SortKey = (typeof SORT_KEYS)[number];8889const pick: Record<SortKey, (n: NodeRecord) => number | string | null> = {90  number: (n) => n.number,91  name: (n) => n.publicName.toLowerCase(),92  family: (n) => n.family,93  chip: (n) => n.chip,94  cpu: (n) => n.cpuCores,95  gpu: (n) => n.gpuCores,96  memory: (n) => n.unifiedMemoryGB,97  storage: (n) => n.storageGB,98};99100/** Stable sort; unknown (null) values always go last, whatever the direction. */101export function sortNodes(nodes: NodeRecord[], key: SortKey, dir: "asc" | "desc" = "asc"): NodeRecord[] {102  const get = pick[key] ?? pick.number;103  const sign = dir === "desc" ? -1 : 1;104  return nodes105    .map((n, i) => ({ n, i }))106    .sort((a, b) => {107      const va = get(a.n);108      const vb = get(b.n);109      if (va === null && vb === null) return a.n.number - b.n.number;110      if (va === null) return 1;111      if (vb === null) return -1;112      const c = typeof va === "number" && typeof vb === "number" ? va - vb : String(va).localeCompare(String(vb));113      return c !== 0 ? c * sign : a.n.number - b.n.number || a.i - b.i;114    })115    .map((x) => x.n);116}117118export function facetValues(nodes: NodeRecord[]) {119  const uniq = <T,>(xs: T[]) => [...new Set(xs)];120  return {121    families: uniq(nodes.map((n) => n.family)),122    chips: uniq(nodes.map((n) => n.chip).filter((c): c is string => c !== null)).sort(),123    memories: uniq(nodes.map((n) => n.unifiedMemoryGB).filter((m): m is number => m !== null)).sort((a, b) => a - b),124    roles: uniq(nodes.flatMap((n) => n.role)),125    lifecycles: uniq(nodes.map((n) => n.lifecycle)),126    racks: uniq(nodes.map((n) => n.rack).filter((r): r is string => r !== null)).sort(),127  };128}129
Files main
Menu

Theme