     1→scalar Time
     2→scalar Any
     3→
     4→scalar NullableFloat
     5→scalar MetricScope
     6→scalar JobState
     7→scalar SchedulerState
     8→scalar MonitoringState
     9→
    10→type Node {
    11→  id: ID!
    12→  hostname: String!
    13→  cluster: String!
    14→  subCluster: String!
    15→  jobsRunning: Int!
    16→  cpusAllocated: Int
    17→  memoryAllocated: Int
    18→  gpusAllocated: Int
    19→  schedulerState: SchedulerState!
    20→  healthState: MonitoringState!
    21→  metaData: Any
    22→  healthData: Any
    23→}
    24→
    25→type NodeStates {
    26→  state: String!
    27→  count: Int!
    28→}
    29→
    30→type NodeStatesTimed {
    31→  state: String!
    32→  counts: [Int!]!
    33→  times: [Int!]!
    34→}
    35→
    36→type Job {
    37→  id: ID!
    38→  jobId: Int!
    39→  user: String!
    40→  project: String!
    41→  cluster: String!
    42→  subCluster: String!
    43→  startTime: Time!
    44→  duration: Int!
    45→  walltime: Int!
    46→  numNodes: Int!
    47→  numHWThreads: Int!
    48→  numAcc: Int!
    49→  energy: Float!
    50→  SMT: Int!
    51→  shared: String!
    52→  partition: String!
    53→  arrayJobId: Int!
    54→  monitoringStatus: Int!
    55→  state: JobState!
    56→  tags: [Tag!]!
    57→  resources: [Resource!]!
    58→  concurrentJobs: JobLinkResultList
    59→  footprint: [FootprintValue]
    60→  energyFootprint: [EnergyFootprintValue]
    61→  metaData: Any
    62→  userData: User
    63→}
    64→
    65→type JobLink {
    66→  id: ID!
    67→  jobId: Int!
    68→}
    69→
    70→type Cluster {
    71→  name: String!
    72→  partitions: [String!]! # Slurm partitions
    73→  subClusters: [SubCluster!]! # Hardware partitions/subclusters
    74→}
    75→
    76→type SubCluster {
    77→  name: String!
    78→  nodes: String!
    79→  numberOfNodes: Int!
    80→  processorType: String!
    81→  socketsPerNode: Int!
    82→  coresPerSocket: Int!
    83→  threadsPerCore: Int!
    84→  flopRateScalar: MetricValue!
    85→  flopRateSimd: MetricValue!
    86→  memoryBandwidth: MetricValue!
    87→  topology: Topology!
    88→  metricConfig: [MetricConfig!]!
    89→  footprint: [String!]!
    90→}
    91→
    92→type FootprintValue {
    93→  name: String!
    94→  stat: String!
    95→  value: Float!
    96→}
    97→
    98→type EnergyFootprintValue {
    99→  hardware: String!
   100→  metric: String!
   101→  value: Float!
   102→}
   103→
   104→type MetricValue {
   105→  name: String
   106→  unit: Unit!
   107→  value: Float!
   108→}
   109→
   110→type Topology {
   111→  node: [Int!]
   112→  socket: [[Int!]!]
   113→  memoryDomain: [[Int!]!]
   114→  die: [[Int!]!]
   115→  core: [[Int!]!]
   116→  accelerators: [Accelerator!]
   117→}
   118→
   119→type Accelerator {
   120→  id: String!
   121→  type: String!
   122→  model: String!
   123→}
   124→
   125→type SubClusterConfig {
   126→  name: String!
   127→  peak: Float
   128→  normal: Float
   129→  caution: Float
   130→  alert: Float
   131→  remove: Boolean
   132→}
   133→
   134→type MetricConfig {
   135→  name: String!
   136→  unit: Unit!
   137→  scope: MetricScope!
   138→  aggregation: String!
   139→  timestep: Int!
   140→  peak: Float!
   141→  normal: Float
   142→  caution: Float!
   143→  alert: Float!
   144→  lowerIsBetter: Boolean
   145→  subClusters: [SubClusterConfig!]!
   146→}
   147→
   148→type Tag {
   149→  id: ID!
   150→  type: String!
   151→  name: String!
   152→  scope: String!
   153→}
   154→
   155→type Resource {
   156→  hostname: String!
   157→  hwthreads: [Int!]
   158→  accelerators: [String!]
   159→  configuration: String
   160→}
   161→
   162→type JobMetricWithName {
   163→  name: String!
   164→  scope: MetricScope!
   165→  metric: JobMetric!
   166→}
   167→
   168→type ClusterMetricWithName {
   169→  name: String!
   170→  unit: Unit
   171→  timestep: Int!
   172→  data: [NullableFloat!]!
   173→}
   174→
   175→type JobMetric {
   176→  unit: Unit
   177→  timestep: Int!
   178→  series: [Series!]
   179→  statisticsSeries: StatsSeries
   180→}
   181→
   182→type Series {
   183→  hostname: String!
   184→  id: String
   185→  statistics: MetricStatistics
   186→  data: [NullableFloat!]!
   187→}
   188→
   189→type StatsSeries {
   190→  mean: [NullableFloat!]!
   191→  median: [NullableFloat!]!
   192→  min: [NullableFloat!]!
   193→  max: [NullableFloat!]!
   194→}
   195→
   196→type NamedStatsWithScope {
   197→  name: String!
   198→  scope: MetricScope!
   199→  stats: [ScopedStats!]!
   200→}
   201→
   202→type ScopedStats {
   203→  hostname: String!
   204→  id: String
   205→  data: MetricStatistics!
   206→}
   207→
   208→type JobStats {
   209→  id: Int!
   210→  jobId: String!
   211→  startTime: Int!
   212→  duration: Int!
   213→  cluster: String!
   214→  subCluster: String!
   215→  numNodes: Int!
   216→  numHWThreads: Int
   217→  numAccelerators: Int
   218→  stats: [NamedStats!]!
   219→}
   220→
   221→type NamedStats {
   222→  name: String!
   223→  data: MetricStatistics!
   224→}
   225→
   226→type Unit {
   227→  base: String!
   228→  prefix: String
   229→}
   230→
   231→type MetricStatistics {
   232→  avg: Float!
   233→  min: Float!
   234→  max: Float!
   235→}
   236→
   237→type MetricFootprints {
   238→  metric: String!
   239→  data: [NullableFloat!]!
   240→}
   241→
   242→type Footprints {
   243→  timeWeights: TimeWeights!
   244→  metrics: [MetricFootprints!]!
   245→}
   246→
   247→type TimeWeights {
   248→  nodeHours: [NullableFloat!]!
   249→  accHours: [NullableFloat!]!
   250→  coreHours: [NullableFloat!]!
   251→}
   252→
   253→enum Aggregate {
   254→  USER
   255→  PROJECT
   256→  CLUSTER
   257→  SUBCLUSTER
   258→}
   259→enum SortByAggregate {
   260→  TOTALWALLTIME
   261→  TOTALJOBS
   262→  TOTALUSERS
   263→  TOTALNODES
   264→  TOTALNODEHOURS
   265→  TOTALCORES
   266→  TOTALCOREHOURS
   267→  TOTALACCS
   268→  TOTALACCHOURS
   269→}
   270→
   271→type NodeMetrics {
   272→  host: String!
   273→  state: String!
   274→  subCluster: String!
   275→  metrics: [JobMetricWithName!]!
   276→}
   277→
   278→type ClusterMetrics {
   279→  nodeCount: Int!
   280→  metrics: [ClusterMetricWithName!]!
   281→}
   282→
   283→type NodesResultList {
   284→  items: [NodeMetrics!]!
   285→  offset: Int
   286→  limit: Int
   287→  count: Int
   288→  totalNodes: Int
   289→  hasNextPage: Boolean
   290→}
   291→
   292→type ClusterSupport {
   293→  cluster: String!
   294→  subClusters: [String!]!
   295→}
   296→
   297→type GlobalMetricListItem {
   298→  name: String!
   299→  unit: Unit!
   300→  scope: MetricScope!
   301→  footprint: String
   302→  availability: [ClusterSupport!]!
   303→}
   304→
   305→type Count {
   306→  name: String!
   307→  count: Int!
   308→}
   309→
   310→type User {
   311→  username: String!
   312→  name: String!
   313→  email: String!
   314→}
   315→
   316→input MetricStatItem {
   317→  metricName: String!
   318→  range: FloatRange!
   319→}
   320→
   321→type Query {
   322→  clusters: [Cluster!]! # List of all clusters
   323→  tags: [Tag!]! # List of all tags
   324→  globalMetrics: [GlobalMetricListItem!]!
   325→
   326→  user(username: String!): User
   327→  allocatedNodes(cluster: String!): [Count!]!
   328→
   329→  ## Node Queries New
   330→  node(id: ID!): Node
   331→  nodes(filter: [NodeFilter!], order: OrderByInput): NodeStateResultList!
   332→  nodesWithMeta(filter: [NodeFilter!], order: OrderByInput): NodeStateResultList!
   333→  nodeStates(filter: [NodeFilter!]): [NodeStates!]!
   334→  nodeStatesTimed(filter: [NodeFilter!], type: String!): [NodeStatesTimed!]!
   335→
   336→  job(id: ID!): Job
   337→  jobMetrics(
   338→    id: ID!
   339→    metrics: [String!]
   340→    scopes: [MetricScope!]
   341→    resolution: Int
   342→  ): [JobMetricWithName!]!
   343→
   344→  jobStats(id: ID!, metrics: [String!]): [NamedStats!]!
   345→
   346→  scopedJobStats(
   347→    id: ID!
   348→    metrics: [String!]
   349→    scopes: [MetricScope!]
   350→  ): [NamedStatsWithScope!]!
   351→
   352→  jobs(
   353→    filter: [JobFilter!]
   354→    page: PageRequest
   355→    order: OrderByInput
   356→  ): JobResultList!
   357→
   358→  jobsStatistics(
   359→    filter: [JobFilter!]
   360→    metrics: [String!]
   361→    page: PageRequest
   362→    sortBy: SortByAggregate
   363→    groupBy: Aggregate
   364→    numDurationBins: String
   365→    numMetricBins: Int
   366→  ): [JobsStatistics!]!
   367→
   368→  jobsMetricStats(filter: [JobFilter!], metrics: [String!]): [JobStats!]!
   369→  jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints
   370→
   371→  rooflineHeatmap(
   372→    filter: [JobFilter!]!
   373→    rows: Int!
   374→    cols: Int!
   375→    minX: Float!
   376→    minY: Float!
   377→    maxX: Float!
   378→    maxY: Float!
   379→  ): [[Float!]!]!
   380→
   381→  nodeMetrics(
   382→    cluster: String!
   383→    nodes: [String!]
   384→    scopes: [MetricScope!]
   385→    metrics: [String!]
   386→    from: Time!
   387→    to: Time!
   388→  ): [NodeMetrics!]!
   389→
   390→  nodeMetricsList(
   391→    cluster: String!
   392→    subCluster: String!
   393→    stateFilter: String!
   394→    nodeFilter: String!
   395→    scopes: [MetricScope!]
   396→    metrics: [String!]
   397→    from: Time!
   398→    to: Time!
   399→    page: PageRequest
   400→    resolution: Int
   401→  ): NodesResultList!
   402→
   403→  clusterMetrics(
   404→    cluster: String!
   405→    metrics: [String!]
   406→    from: Time!
   407→    to: Time!
   408→  ): ClusterMetrics!
   409→}
   410→
   411→type Mutation {
   412→  createTag(type: String!, name: String!, scope: String!): Tag!
   413→  deleteTag(id: ID!): ID!
   414→  addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]!
   415→  removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]!
   416→  removeTagFromList(tagIds: [ID!]!): [Int!]!
   417→
   418→  updateConfiguration(name: String!, value: String!): String
   419→}
   420→
   421→type IntRangeOutput {
   422→  from: Int!
   423→  to: Int!
   424→}
   425→type TimeRangeOutput {
   426→  range: String
   427→  from: Time!
   428→  to: Time!
   429→}
   430→
   431→input NodeFilter {
   432→  hostname: StringInput
   433→  cluster: StringInput
   434→  subCluster: StringInput
   435→  schedulerState: SchedulerState
   436→  healthState: MonitoringState
   437→  timeStart: Int
   438→}
   439→
   440→input JobFilter {
   441→  tags: [ID!]
   442→  dbId: [ID!]
   443→  jobId: StringInput
   444→  arrayJobId: Int
   445→  user: StringInput
   446→  project: StringInput
   447→  jobName: StringInput
   448→  cluster: StringInput
   449→  subCluster: StringInput
   450→  partition: StringInput
   451→  duration: IntRange
   452→  energy: FloatRange
   453→
   454→  minRunningFor: Int
   455→
   456→  numNodes: IntRange
   457→  numAccelerators: IntRange
   458→  numHWThreads: IntRange
   459→
   460→  startTime: TimeRange
   461→  state: [JobState!]
   462→  metricStats: [MetricStatItem!]
   463→  shared: String
   464→  schedule: String
   465→  node: StringInput
   466→}
   467→
   468→input OrderByInput {
   469→  field: String!
   470→  type: String!
   471→  order: SortDirectionEnum! = ASC
   472→}
   473→
   474→enum SortDirectionEnum {
   475→  DESC
   476→  ASC
   477→}
   478→
   479→input StringInput {
   480→  eq: String
   481→  neq: String
   482→  contains: String
   483→  startsWith: String
   484→  endsWith: String
   485→  in: [String!]
   486→}
   487→
   488→input IntRange {
   489→  from: Int!
   490→  to: Int!
   491→}
   492→input TimeRange {
   493→  range: String
   494→  from: Time
   495→  to: Time
   496→}
   497→
   498→input FloatRange {
   499→  from: Float!
   500→  to: Float!
   501→}
   502→
   503→type NodeStateResultList {
   504→  items: [Node!]!
   505→  count: Int
   506→}
   507→
   508→type JobResultList {
   509→  items: [Job!]!
   510→  offset: Int
   511→  limit: Int
   512→  count: Int
   513→  hasNextPage: Boolean
   514→}
   515→
   516→type JobLinkResultList {
   517→  listQuery: String
   518→  items: [JobLink!]!
   519→  count: Int
   520→}
   521→
   522→type HistoPoint {
   523→  count: Int!
   524→  value: Int!
   525→}
   526→
   527→type MetricHistoPoints {
   528→  metric: String!
   529→  unit: String!
   530→  stat: String
   531→  data: [MetricHistoPoint!]
   532→}
   533→
   534→type MetricHistoPoint {
   535→  bin: Int
   536→  count: Int!
   537→  min: Int
   538→  max: Int
   539→}
   540→
   541→type JobsStatistics {
   542→  id: ID! # If `groupBy` was used, ID of the user/project/cluster/subcluster
   543→  name: String! # if User-Statistics: Given Name of Account (ID) Owner
   544→  totalUsers: Int! # if *not* User-Statistics: Number of active users (based on running jobs)
   545→  totalJobs: Int! # Number of jobs
   546→  runningJobs: Int! # Number of running jobs
   547→  shortJobs: Int! # Number of jobs with a duration of less than config'd ShortRunningJobsDuration
   548→  totalWalltime: Int! # Sum of the duration of all matched jobs in hours
   549→  totalNodes: Int! # Sum of the nodes of all matched jobs
   550→  totalNodeHours: Int! # Sum of the node hours of all matched jobs
   551→  totalCores: Int! # Sum of the cores of all matched jobs
   552→  totalCoreHours: Int! # Sum of the core hours of all matched jobs
   553→  totalAccs: Int! # Sum of the accs of all matched jobs
   554→  totalAccHours: Int! # Sum of the gpu hours of all matched jobs
   555→  histDuration: [HistoPoint!]! # value: hour, count: number of jobs with a rounded duration of value
   556→  histNumNodes: [HistoPoint!]! # value: number of nodes, count: number of jobs with that number of nodes
   557→  histNumCores: [HistoPoint!]! # value: number of cores, count: number of jobs with that number of cores
   558→  histNumAccs: [HistoPoint!]! # value: number of accs, count: number of jobs with that number of accs
   559→  histMetrics: [MetricHistoPoints!]! # metric: metricname, data array of histopoints: value: metric average bin, count: number of jobs with that metric average
   560→}
   561→
   562→input PageRequest {
   563→  itemsPerPage: Int!
   564→  page: Int!
   565→}
   566→


641-	}
642-
643-	return &model.JobResultList{Items: jobs, Count: &count, HasNextPage: &hasNextPage}, nil
644-}
645-
646:// JobsStatistics is the resolver for the jobsStatistics field.
647:func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) ([]*model.JobsStatistics, error) {
648-	startOverall := time.Now()
649-	var err error
650:	var stats []*model.JobsStatistics
651-
652-	// Top Level Defaults
653-	defaultDurationBins := "1h"
654-	defaultMetricBins := 10
655-
--
674-			startGrouped := time.Now()
675-			stats, err = r.Repo.JobsStatsGrouped(ctx, filter, page, sortBy, groupBy, reqFields)
676-			cclog.Infof("Timer JobsStatsGrouped call: %s", time.Since(startGrouped))
677-		}
678-	} else {
679:		stats = make([]*model.JobsStatistics, 0, 1)
680:		stats = append(stats, &model.JobsStatistics{})
681-	}
682-
683-	// runningJobs and shortJobs are already inlined in JobsStats/JobsStatsGrouped.
684-	// Only run separate count queries if main stats were not fetched.
685-	if !fetchedMainStats {
--
734-		} else {
735-			return nil, errors.New("metric histograms only implemented without groupBy argument")
736-		}
737-	}
738-
739:	cclog.Infof("Timer JobsStatistics overall: %s", time.Since(startOverall))
740-	return stats, nil
741-}
742-
743-// JobsMetricStats is the resolver for the jobsMetricStats field.
744-func (r *queryResolver) JobsMetricStats(ctx context.Context, filter []*model.JobFilter, metrics []string) ([]*model.JobStats, error) {

[Showing results with pagination = limit: 50, offset: 0]


   646→// JobsStatistics is the resolver for the jobsStatistics field.
   647→func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) ([]*model.JobsStatistics, error) {
   648→	startOverall := time.Now()
   649→	var err error
   650→	var stats []*model.JobsStatistics
   651→
   652→	// Top Level Defaults
   653→	defaultDurationBins := "1h"
   654→	defaultMetricBins := 10
   655→
   656→	// Build requested fields map for selective column computation
   657→	statsFields := []string{"totalJobs", "totalUsers", "totalWalltime", "totalNodes", "totalCores",
   658→		"totalAccs", "totalNodeHours", "totalCoreHours", "totalAccHours", "runningJobs", "shortJobs"}
   659→	reqFields := make(map[string]bool, len(statsFields))
   660→	fetchedMainStats := false
   661→	for _, f := range statsFields {
   662→		if requireField(ctx, f) {
   663→			reqFields[f] = true
   664→			if f != "runningJobs" && f != "shortJobs" {
   665→				fetchedMainStats = true
   666→			}
   667→		}
   668→	}
   669→
   670→	if fetchedMainStats {
   671→		if groupBy == nil {
   672→			stats, err = r.Repo.JobsStats(ctx, filter, reqFields)
   673→		} else {
   674→			startGrouped := time.Now()
   675→			stats, err = r.Repo.JobsStatsGrouped(ctx, filter, page, sortBy, groupBy, reqFields)
   676→			cclog.Infof("Timer JobsStatsGrouped call: %s", time.Since(startGrouped))
   677→		}
   678→	} else {
   679→		stats = make([]*model.JobsStatistics, 0, 1)
   680→		stats = append(stats, &model.JobsStatistics{})
   681→	}
   682→
   683→	// runningJobs and shortJobs are already inlined in JobsStats/JobsStatsGrouped.
   684→	// Only run separate count queries if main stats were not fetched.
   685→	if !fetchedMainStats {
   686→		if groupBy != nil {
   687→			if requireField(ctx, "shortJobs") {
   688→				stats, err = r.Repo.AddJobCountGrouped(ctx, filter, groupBy, stats, "short")
   689→			}
   690→			if requireField(ctx, "runningJobs") {
   691→				stats, err = r.Repo.AddJobCountGrouped(ctx, filter, groupBy, stats, "running")
   692→			}
   693→		} else {
   694→			if requireField(ctx, "shortJobs") {
   695→				stats, err = r.Repo.AddJobCount(ctx, filter, stats, "short")
   696→			}
   697→			if requireField(ctx, "runningJobs") {
   698→				stats, err = r.Repo.AddJobCount(ctx, filter, stats, "running")
   699→			}
   700→		}
   701→	}
   702→
   703→	if err != nil {
   704→		return nil, err
   705→	}
   706→
   707→	if requireField(ctx, "histDuration") || requireField(ctx, "histNumNodes") || requireField(ctx, "histNumCores") || requireField(ctx, "histNumAccs") {
   708→
   709→		if numDurationBins == nil {
   710→			numDurationBins = &defaultDurationBins
   711→		}
   712→
   713→		if groupBy == nil {
   714→			stats[0], err = r.Repo.AddHistograms(ctx, filter, stats[0], numDurationBins)
   715→			if err != nil {
   716→				return nil, err
   717→			}
   718→		} else {
   719→			return nil, errors.New("histograms only implemented without groupBy argument")
   720→		}
   721→	}
   722→
   723→	if requireField(ctx, "histMetrics") {
   724→
   725→		if numMetricBins == nil {
   726→			numMetricBins = &defaultMetricBins
   727→		}
   728→
   729→		if groupBy == nil {
   730→			stats[0], err = r.Repo.AddMetricHistograms(ctx, filter, metrics, stats[0], numMetricBins)
   731→			if err != nil {
   732→				return nil, err
   733→			}
   734→		} else {
   735→			return nil, errors.New("metric histograms only implemented without groupBy argument")
   736→		}
   737→	}
   738→
   739→	cclog.Infof("Timer JobsStatistics overall: %s", time.Since(startOverall))
   740→	return stats, nil
   741→}
   742→
   743→// JobsMetricStats is the resolver for the jobsMetricStats field.
   744→func (r *queryResolver) JobsMetricStats(ctx context.Context, filter []*model.JobFilter, metrics []string) ([]*model.JobStats, error) {
   745→	// No Paging, Fixed Order by StartTime ASC
   746→	order := &model.OrderByInput{
   747→		Field: "startTime",
   748→		Type:  "col",
   749→		Order: "ASC",
   750→	}
   751→
   752→	jobs, err := r.Repo.QueryJobs(ctx, filter, nil, order)
   753→	if err != nil {
   754→		cclog.Warn("Error while querying jobs for comparison")
   755→		return nil, err
   756→	}
   757→
   758→	res := []*model.JobStats{}
   759→	for _, job := range jobs {
   760→		data, err := metricdispatch.LoadJobStats(job, metrics, ctx)
   761→		if err != nil {
   762→			cclog.Warnf("Error while loading comparison jobStats data for job id %d", job.JobID)
   763→			continue
   764→			// return nil, err
   765→		}
   766→
   767→		sres := []*model.NamedStats{}
   768→		for name, md := range data {
   769→			sres = append(sres, &model.NamedStats{
   770→				Name: name,
   771→				Data: &md,
   772→			})
   773→		}
   774→
   775→		numThreadsInt := int(job.NumHWThreads)
   776→		numAccsInt := int(job.NumAcc)
   777→		res = append(res, &model.JobStats{
   778→			ID:              int(*job.ID),
   779→			JobID:           strconv.Itoa(int(job.JobID)),
   780→			StartTime:       int(job.StartTime),
   781→			Duration:        int(job.Duration),
   782→			Cluster:         job.Cluster,
   783→			SubCluster:      job.SubCluster,
   784→			NumNodes:        int(job.NumNodes),
   785→			NumHWThreads:    &numThreadsInt,
   786→			NumAccelerators: &numAccsInt,
   787→			Stats:           sres,
   788→		})
   789→	}
   790→	return res, err
   791→}
   792→
   793→// JobsFootprints is the resolver for the jobsFootprints field.
   794→func (r *queryResolver) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) {
   795→	// NOTE: Legacy Naming! This resolver is for normalized histograms in analysis view only - *Not* related to DB "footprint" column!


3-// Use of this source code is governed by a MIT-style
4-// license that can be found in the LICENSE file.
5-
6-// This file contains job statistics and histogram generation functionality for the JobRepository.
7-//
8-// # Job Statistics
9-//
10-// The statistics methods provide aggregated metrics about jobs including total jobs, users,
11-/ walltime, and resource usage (nodes, cores, accelerators). Statistics can be computed:
12-/   - Overall (JobsStats): Single aggregate across all matching jobs
13://   - Grouped (JobsStatsGrouped): Aggregated by user, project, cluster, or subcluster
14-/   - Counts (JobCountGrouped, AddJobCount): Simple job counts with optional filtering
15-//
16-// All statistics methods support filtering via JobFilter and respect security contexts.
17-//
18-// # Histograms
19-//
20-/ Histogram methods generate distribution data for visualization:
21-//   - Duration, nodes, cores, accelerators (AddHistograms)
22-//   - Job metrics like CPU load, memory usage (AddMetricHistograms)
23-//
--
51-	"github.com/ClusterCockpit/cc-backend/internal/config"
52-	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
53-	"github.com/ClusterCockpit/cc-backend/internal/metricdispatch"
54-	"github.com/ClusterCockpit/cc-backend/pkg/archive"
55-	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
56-	"github.com/ClusterCockpit/cc-lib/v2/schema"
57-	sq "github.com/Masterminds/squirrel"
58-)
59-
60-// groupBy2column maps GraphQL Aggregate enum values to their corresponding database column names.
61:// Used by JobsStatsGrouped and JobCountGrouped to translate user-facing grouping dimensions
62-// into SQL GROUP BY clauses. GraphQL validation ensures only valid enum values are accepted.
63-var groupBy2column = map[model.Aggregate]string{
64-	model.AggregateUser:       "job.hpc_user",
65-	model.AggregateProject:    "job.project",
66-	model.AggregateCluster:    "job.cluster",
67-	model.AggregateSubcluster: "job.subcluster",
68-}
69-
70-// sortBy2column maps GraphQL SortByAggregate enum values to their corresponding computed column names.
71:// Used by JobsStatsGrouped to translate sort preferences into SQL ORDER BY clauses.
72:// Column names match the AS aliases used in buildStatsQuery.
73-var sortBy2column = map[model.SortByAggregate]string{
74-	model.SortByAggregateTotaljobs:      "totalJobs",
75-	model.SortByAggregateTotalusers:     "totalUsers",
76-	model.SortByAggregateTotalwalltime:  "totalWalltime",
77-	model.SortByAggregateTotalnodes:     "totalNodes",
78-	model.SortByAggregateTotalnodehours: "totalNodeHours",
79-	model.SortByAggregateTotalcores:     "totalCores",
80-	model.SortByAggregateTotalcorehours: "totalCoreHours",
81-	model.SortByAggregateTotalaccs:      "totalAccs",
82-	model.SortByAggregateTotalacchours:  "totalAccHours",
--
117-		query = query.Where("job.duration < ?", config.Keys.ShortRunningJobsDuration)
118-	}
119-
120-	for _, f := range filter {
121-		query = BuildWhereClause(f, query)
122-	}
123-
124-	return query
125-}
126-
127:// buildStatsQuery constructs a SQL query to compute job statistics with optional grouping.
128-// Only requested columns are computed; unrequested columns select 0 as placeholder.
129-//
130-/ Parameters:
131-/   - filter: Job filters to apply (cluster, user, time range, etc.)
132-/   - col: Column name to GROUP BY; empty string for overall statistics without grouping
133-/   - shortThreshold: Duration threshold in seconds for counting short-running jobs
134-/   - reqFields: Set of requested field names; nil means compute all fields
135:func (r *JobRepository) buildStatsQuery(
136-	filter []*model.JobFilter,
137-	col string,
138-	shortThreshold int,
139-	reqFields map[string]bool,
140-) sq.SelectBuilder {
141-	now := time.Now().Unix()
142-
143-	/ Helper: return real expression if field is requested (or reqFields is nil), else "0 as alias"
144-	need := func(field string) bool {
145-		return reqFields == nil || reqFields[field]
--
220-		query = query.GroupBy(col)
221-	}
222-
223-	for _, f := range filter {
224-		query = BuildWhereClause(f, query)
225-	}
226-
227-	return query
228-}
229-
230:// JobsStatsGrouped computes job statistics grouped by a dimension (user, project, cluster, or subcluster).
231-// Only columns listed in reqFields are computed; others return 0. User display names are looked up
232-// in a separate lightweight query to avoid JOIN overhead on the main aggregation.
233:func (r *JobRepository) JobsStatsGrouped(
234-	ctx context.Context,

[Showing results with pagination = limit: 100, offset: 0]


   230→// JobsStatsGrouped computes job statistics grouped by a dimension (user, project, cluster, or subcluster).
   231→// Only columns listed in reqFields are computed; others return 0. User display names are looked up
   232→// in a separate lightweight query to avoid JOIN overhead on the main aggregation.
   233→func (r *JobRepository) JobsStatsGrouped(
   234→	ctx context.Context,
   235→	filter []*model.JobFilter,
   236→	page *model.PageRequest,
   237→	sortBy *model.SortByAggregate,
   238→	groupBy *model.Aggregate,
   239→	reqFields map[string]bool,
   240→) ([]*model.JobsStatistics, error) {
   241→	start := time.Now()
   242→	col := groupBy2column[*groupBy]
   243→	query := r.buildStatsQuery(filter, col, config.Keys.ShortRunningJobsDuration, reqFields)
   244→
   245→	query, err := SecurityCheck(ctx, query)
   246→	if err != nil {
   247→		return nil, err
   248→	}
   249→
   250→	if sortBy != nil {
   251→		sortBy := sortBy2column[*sortBy]
   252→		query = query.OrderBy(fmt.Sprintf("%s DESC", sortBy))
   253→	}
   254→	if page != nil && page.ItemsPerPage != -1 {
   255→		limit := uint64(page.ItemsPerPage)
   256→		query = query.Offset((uint64(page.Page) - 1) * limit).Limit(limit)
   257→	}
   258→
   259→	rows, err := query.RunWith(r.DB).QueryContext(ctx)
   260→	if err != nil {
   261→		cclog.Warn("Error while querying DB for job statistics")
   262→		return nil, err
   263→	}
   264→	defer rows.Close()
   265→
   266→	stats := make([]*model.JobsStatistics, 0, 100)
   267→
   268→	for rows.Next() {
   269→		var id sql.NullString
   270→		var jobs, users, walltime, nodes, nodeHours, cores, coreHours, accs, accHours, runningJobs, shortJobs sql.NullInt64
   271→		if err := rows.Scan(&id, &jobs, &users, &walltime, &nodes, &nodeHours, &cores, &coreHours, &accs, &accHours, &runningJobs, &shortJobs); err != nil {
   272→			cclog.Warnf("Error while scanning rows: %s", err.Error())
   273→			return nil, err
   274→		}
   275→
   276→		if id.Valid {
   277→			stats = append(stats,
   278→				&model.JobsStatistics{
   279→					ID:             id.String,
   280→					TotalJobs:      int(jobs.Int64),
   281→					TotalUsers:     int(users.Int64),
   282→					TotalWalltime:  int(walltime.Int64),
   283→					TotalNodes:     int(nodes.Int64),
   284→					TotalNodeHours: int(nodeHours.Int64),
   285→					TotalCores:     int(cores.Int64),
   286→					TotalCoreHours: int(coreHours.Int64),
   287→					TotalAccs:      int(accs.Int64),
   288→					TotalAccHours:  int(accHours.Int64),
   289→					RunningJobs:    int(runningJobs.Int64),
   290→					ShortJobs:      int(shortJobs.Int64),
   291→				})
   292→		}
   293→	}
   294→
   295→	if err := rows.Err(); err != nil {
   296→		return nil, err
   297→	}
   298→
   299→	// Post-query name lookup for user grouping (avoids LEFT JOIN on aggregation query)
   300→	if col == "job.hpc_user" && len(stats) > 0 {
   301→		usernames := make([]any, len(stats))
   302→		for i, s := range stats {
   303→			usernames[i] = s.ID
   304→		}
   305→
   306→		nameQuery := sq.Select("username", "name").From("hpc_user").Where(sq.Eq{"username": usernames})
   307→		nameRows, err := nameQuery.RunWith(r.DB).QueryContext(ctx)
   308→		if err != nil {
   309→			cclog.Warnf("Error looking up user names: %s", err.Error())
   310→			// Non-fatal: stats are still valid without display names
   311→		} else {
   312→			defer nameRows.Close()
   313→			nameMap := make(map[string]string, len(stats))
   314→			for nameRows.Next() {
   315→				var username, name string
   316→				if err := nameRows.Scan(&username, &name); err == nil {
   317→					nameMap[username] = name
   318→				}
   319→			}
   320→			for _, s := range stats {
   321→				if name, ok := nameMap[s.ID]; ok {
   322→					s.Name = name
   323→				}
   324→			}
   325→		}
   326→	}
   327→
   328→	cclog.Debugf("Timer JobsStatsGrouped %s", time.Since(start))
   329→	return stats, nil
   330→}
   331→
   332→// JobsStats computes overall job statistics across all matching jobs without grouping.
   333→//
   334→// This method provides a single aggregate view of job metrics, useful for dashboard
   335→// summaries and overall system utilization reports.
   336→//
   337→// Parameters:
   338→//   - ctx: Context for security checks and cancellation
   339→//   - filter: Filters to apply (time range, cluster, job state, etc.)
   340→//
   341→// Returns a single-element slice containing aggregate statistics:
   342→//   - totalJobs, totalUsers, totalWalltime
   343→//   - totalNodeHours, totalCoreHours, totalAccHours
   344→//
   345→// Unlike JobsStatsGrouped, this returns overall totals without breaking down by dimension.
   346→// Security checks are applied via SecurityCheck to respect user access levels.
   347→func (r *JobRepository) JobsStats(
   348→	ctx context.Context,
   349→	filter []*model.JobFilter,
   350→	reqFields map[string]bool,
   351→) ([]*model.JobsStatistics, error) {
   352→	start := time.Now()
   353→	query := r.buildStatsQuery(filter, "", config.Keys.ShortRunningJobsDuration, reqFields)
   354→	query, err := SecurityCheck(ctx, query)
   355→	if err != nil {
   356→		return nil, err
   357→	}
   358→
   359→	row := query.RunWith(r.DB).QueryRowContext(ctx)
   360→	stats := make([]*model.JobsStatistics, 0, 1)
   361→
   362→	var jobs, users, walltime, nodes, nodeHours, cores, coreHours, accs, accHours, runningJobs, shortJobs sql.NullInt64
   363→	if err := row.Scan(&jobs, &users, &walltime, &nodes, &nodeHours, &cores, &coreHours, &accs, &accHours, &runningJobs, &shortJobs); err != nil {
   364→		cclog.Warn("Error while scanning rows")
   365→		return nil, err
   366→	}
   367→
   368→	if jobs.Valid {
   369→		var totalNodeHours, totalCoreHours, totalAccHours int
   370→
   371→		if nodeHours.Valid {
   372→			totalNodeHours = int(nodeHours.Int64)
   373→		}
   374→		if coreHours.Valid {
   375→			totalCoreHours = int(coreHours.Int64)
   376→		}
   377→		if accHours.Valid {
   378→			totalAccHours = int(accHours.Int64)
   379→		}
   380→		stats = append(stats,
   381→			&model.JobsStatistics{
   382→				TotalJobs:      int(jobs.Int64),
   383→				TotalUsers:     int(users.Int64),
   384→				TotalWalltime:  int(walltime.Int64),
   385→				TotalNodeHours: totalNodeHours,
   386→				TotalCoreHours: totalCoreHours,
   387→				TotalAccHours:  totalAccHours,
   388→				RunningJobs:    int(runningJobs.Int64),
   389→				ShortJobs:      int(shortJobs.Int64),
   390→			})
   391→	}
   392→
   393→	cclog.Debugf("Timer JobStats %s", time.Since(start))
   394→	return stats, nil
   395→}
   396→
   397→// LoadJobStat retrieves a specific statistic for a metric from a job's statistics.
   398→// Returns 0.0 if the metric is not found or statType is invalid.
   399→//
   400→// Parameters:
   401→//   - job: Job struct with populated Statistics field
   402→//   - metric: Name of the metric to query (e.g., "cpu_load", "mem_used")
   403→//   - statType: Type of statistic: "avg", "min", or "max"
   404→//
   405→// Returns the requested statistic value or 0.0 if not found.
   406→func LoadJobStat(job *schema.Job, metric string, statType string) float64 {
   407→	if stats, ok := job.Statistics[metric]; ok {
   408→		switch statType {
   409→		case "avg":
   410→			return stats.Avg
   411→		case "max":
   412→			return stats.Max
   413→		case "min":
   414→			return stats.Min
   415→		default:
   416→			cclog.Errorf("Unknown stat type %s", statType)
   417→		}
   418→	}
   419→
   420→	return 0.0
   421→}
   422→
   423→// JobCountGrouped counts jobs grouped by a dimension without computing detailed statistics.
   424→//
   425→// This is a lightweight alternative to JobsStatsGrouped when only job counts are needed,
   426→// avoiding the overhead of calculating walltime and resource usage metrics.
   427→//
   428→// Parameters:
   429→//   - ctx: Context for security checks
   430→//   - filter: Filters to apply
   431→//   - groupBy: Grouping dimension (User, Project, Cluster, or SubCluster)
   432→//
   433→// Returns JobsStatistics with only ID and TotalJobs populated for each group.
   434→func (r *JobRepository) JobCountGrouped(
   435→	ctx context.Context,
   436→	filter []*model.JobFilter,
   437→	groupBy *model.Aggregate,
   438→) ([]*model.JobsStatistics, error) {
   439→	start := time.Now()
   440→	col := groupBy2column[*groupBy]
   441→	query := r.buildCountQuery(filter, "", col)
   442→	query, err := SecurityCheck(ctx, query)
   443→	if err != nil {
   444→		return nil, err
   445→	}
   446→	rows, err := query.RunWith(r.DB).QueryContext(ctx)
   447→	if err != nil {
   448→		cclog.Warn("Error while querying DB for job statistics")
   449→		return nil, err
   450→	}
   451→	defer rows.Close()
   452→
   453→	stats := make([]*model.JobsStatistics, 0, 100)
   454→
   455→	for rows.Next() {
   456→		var id sql.NullString
   457→		var cnt sql.NullInt64
   458→		if err := rows.Scan(&id, &cnt); err != nil {
   459→			cclog.Warn("Error while scanning rows")
   460→			return nil, err
   461→		}
   462→		if id.Valid {
   463→			stats = append(stats,
   464→				&model.JobsStatistics{
   465→					ID:        id.String,
   466→					TotalJobs: int(cnt.Int64),
   467→				})
   468→		}
   469→	}
   470→
   471→	if err := rows.Err(); err != nil {
   472→		return nil, err
   473→	}
   474→
   475→	cclog.Debugf("Timer JobCountGrouped %s", time.Since(start))
   476→	return stats, nil
   477→}
   478→
   479→// AddJobCountGrouped augments existing statistics with additional job counts by category.


   127→// buildStatsQuery constructs a SQL query to compute job statistics with optional grouping.
   128→// Only requested columns are computed; unrequested columns select 0 as placeholder.
   129→//
   130→// Parameters:
   131→//   - filter: Job filters to apply (cluster, user, time range, etc.)
   132→//   - col: Column name to GROUP BY; empty string for overall statistics without grouping
   133→//   - shortThreshold: Duration threshold in seconds for counting short-running jobs
   134→//   - reqFields: Set of requested field names; nil means compute all fields
   135→func (r *JobRepository) buildStatsQuery(
   136→	filter []*model.JobFilter,
   137→	col string,
   138→	shortThreshold int,
   139→	reqFields map[string]bool,
   140→) sq.SelectBuilder {
   141→	now := time.Now().Unix()
   142→
   143→	// Helper: return real expression if field is requested (or reqFields is nil), else "0 as alias"
   144→	need := func(field string) bool {
   145→		return reqFields == nil || reqFields[field]
   146→	}
   147→	durationExpr := fmt.Sprintf(`(CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END)`, now)
   148→
   149→	// Build column list
   150→	columns := make([]string, 0, 14)
   151→
   152→	if col != "" {
   153→		columns = append(columns, col)
   154→	}
   155→
   156→	columns = append(columns, "COUNT(job.id) as totalJobs")
   157→
   158→	if need("totalUsers") && col != "job.hpc_user" {
   159→		columns = append(columns, "COUNT(DISTINCT job.hpc_user) AS totalUsers")
   160→	} else {
   161→		columns = append(columns, "0 AS totalUsers")
   162→	}
   163→
   164→	if need("totalWalltime") {
   165→		columns = append(columns, fmt.Sprintf(`CAST(ROUND(SUM(%s) / 3600) as int) as totalWalltime`, durationExpr))
   166→	} else {
   167→		columns = append(columns, "0 as totalWalltime")
   168→	}
   169→
   170→	if need("totalNodes") {
   171→		columns = append(columns, `CAST(SUM(job.num_nodes) as int) as totalNodes`)
   172→	} else {
   173→		columns = append(columns, "0 as totalNodes")
   174→	}
   175→
   176→	if need("totalNodeHours") {
   177→		columns = append(columns, fmt.Sprintf(`CAST(ROUND(SUM(%s * job.num_nodes) / 3600) as int) as totalNodeHours`, durationExpr))
   178→	} else {
   179→		columns = append(columns, "0 as totalNodeHours")
   180→	}
   181→
   182→	if need("totalCores") {
   183→		columns = append(columns, `CAST(SUM(job.num_hwthreads) as int) as totalCores`)
   184→	} else {
   185→		columns = append(columns, "0 as totalCores")
   186→	}
   187→
   188→	if need("totalCoreHours") {
   189→		columns = append(columns, fmt.Sprintf(`CAST(ROUND(SUM(%s * job.num_hwthreads) / 3600) as int) as totalCoreHours`, durationExpr))
   190→	} else {
   191→		columns = append(columns, "0 as totalCoreHours")
   192→	}
   193→
   194→	if need("totalAccs") {
   195→		columns = append(columns, `CAST(SUM(job.num_acc) as int) as totalAccs`)
   196→	} else {
   197→		columns = append(columns, "0 as totalAccs")
   198→	}
   199→
   200→	if need("totalAccHours") {
   201→		columns = append(columns, fmt.Sprintf(`CAST(ROUND(SUM(%s * job.num_acc) / 3600) as int) as totalAccHours`, durationExpr))
   202→	} else {
   203→		columns = append(columns, "0 as totalAccHours")
   204→	}
   205→
   206→	if need("runningJobs") {
   207→		columns = append(columns, `COUNT(CASE WHEN job.job_state = 'running' THEN 1 END) as runningJobs`)
   208→	} else {
   209→		columns = append(columns, "0 as runningJobs")
   210→	}
   211→
   212→	if need("shortJobs") {
   213→		columns = append(columns, fmt.Sprintf(`COUNT(CASE WHEN job.duration < %d THEN 1 END) as shortJobs`, shortThreshold))
   214→	} else {
   215→		columns = append(columns, "0 as shortJobs")
   216→	}
   217→
   218→	query := sq.Select(columns...).From("job")
   219→	if col != "" {
   220→		query = query.GroupBy(col)
   221→	}
   222→
   223→	for _, f := range filter {
   224→		query = BuildWhereClause(f, query)
   225→	}
   226→
   227→	return query
   228→}


Found 1 file
internal/repository/jobQuery.go


132-
133-	return count, nil
134-}
135-
136-// SecurityCheckWithUser applies role-based access control filters to a job query
137-// based on the provided user's roles and permissions.
138-//
139-/ Access rules by role:
140-/   - API role (exclusive): Full access to all jobs
141-/   - Admin/Support roles: Full access to all jobs
142-/   - Manager role: Access to jobs in managed projects plus own jobs
143-/   - User role: Access only to own jobs
144-//
145-// Returns an error if the user is nil or has no recognized roles.
146-func SecurityCheckWithUser(user *schema.User, query sq.SelectBuilder) (sq.SelectBuilder, error) {
147-	if user == nil {
148-		var qnil sq.SelectBuilder
149-		return qnil, fmt.Errorf("user context is nil")
150-	}
151-
152-	switch {
153-	case len(user.Roles) == 1 && user.HasRole(schema.RoleAPI):
154-		return query, nil
155-	case user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}):
156-		return query, nil
157-	case user.HasRole(schema.RoleManager):
158-		if len(user.Projects) != 0 {
159-			return query.Where(sq.Or{sq.Eq{"job.project": user.Projects}, sq.Eq{"job.hpc_user": user.Username}}), nil
160-		}
161-		cclog.Debugf("Manager '%s' has no assigned projects, restricting to personal jobs", user.Username)
162-		return query.Where("job.hpc_user = ?", user.Username), nil
163-	case user.HasRole(schema.RoleUser):
164-		return query.Where("job.hpc_user = ?", user.Username), nil
165-	default:
166-		var qnil sq.SelectBuilder
167-		return qnil, fmt.Errorf("user has no or unknown roles")
168-	}
169-}
170-
171-// SecurityCheck extracts the user from the context and applies role-based access
172-// control filters to the query. This is a convenience wrapper around SecurityCheckWithUser.
173-func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilder, error) {
174-	user := GetUserFromContext(ctx)
175-	return SecurityCheckWithUser(user, query)
176-}
177-
178-// BuildWhereClause constructs SQL WHERE conditions from a JobFilter and applies
179-// them to the query. Supports filtering by job properties (cluster, state, user),
180-// time ranges, resource usage, tags, and JSON field searches in meta_data,
181-// footprint, and resources columns.
182:func BuildWhereClause(filter *model.JobFilter, query sq.SelectBuilder) sq.SelectBuilder {
183-	// Primary Key
184-	if filter.DbID != nil {
185-		dbIDs := make([]string, len(filter.DbID))
186-		copy(dbIDs, filter.DbID)
187-		query = query.Where(sq.Eq{"job.id": dbIDs})
188-	}
189-	// Explicit indices
190-	if filter.Cluster != nil {
191-		query = buildStringCondition("job.cluster", filter.Cluster, query)
192-	}
193-	if filter.SubCluster != nil {
194-		query = buildStringCondition("job.subcluster", filter.SubCluster, query)
195-	}
196-	if filter.Partition != nil {
197-		query = buildStringCondition("job.cluster_partition", filter.Partition, query)
198-	}
199-	if filter.State != nil {
200-		states := make([]string, len(filter.State))
201-		for i, val := range filter.State {
202-			states[i] = string(val)
203-		}
204-		query = query.Where(sq.Eq{"job.job_state": states})
205-	}
206-	if filter.Shared != nil {
207-		query = query.Where("job.shared = ?", *filter.Shared)
208-	}
209-	if filter.Project != nil {
210-		query = buildStringCondition("job.project", filter.Project, query)
211-	}
212-	if filter.User != nil {
213-		query = buildStringCondition("job.hpc_user", filter.User, query)
214-	}
215-	if filter.NumNodes != nil {
216-		query = buildIntCondition("job.num_nodes", filter.NumNodes, query)
217-	}
218-	if filter.NumAccelerators != nil {
219-		query = buildIntCondition("job.num_acc", filter.NumAccelerators, query)
220-	}
221-	if filter.NumHWThreads != nil {
222-		query = buildIntCondition("job.num_hwthreads", filter.NumHWThreads, query)
223-	}
224-	if filter.ArrayJobID != nil {
225-		query = query.Where("job.array_job_id = ?", *filter.ArrayJobID)
226-	}
227-	if filter.StartTime != nil {
228-		query = buildTimeCondition("job.start_time", filter.StartTime, query)
229-	}
230-	if filter.Duration != nil {
231-		query = buildIntCondition("job.duration", filter.Duration, query)
232-	}


   178→// BuildWhereClause constructs SQL WHERE conditions from a JobFilter and applies
   179→// them to the query. Supports filtering by job properties (cluster, state, user),
   180→// time ranges, resource usage, tags, and JSON field searches in meta_data,
   181→// footprint, and resources columns.
   182→func BuildWhereClause(filter *model.JobFilter, query sq.SelectBuilder) sq.SelectBuilder {
   183→	// Primary Key
   184→	if filter.DbID != nil {
   185→		dbIDs := make([]string, len(filter.DbID))
   186→		copy(dbIDs, filter.DbID)
   187→		query = query.Where(sq.Eq{"job.id": dbIDs})
   188→	}
   189→	// Explicit indices
   190→	if filter.Cluster != nil {
   191→		query = buildStringCondition("job.cluster", filter.Cluster, query)
   192→	}
   193→	if filter.SubCluster != nil {
   194→		query = buildStringCondition("job.subcluster", filter.SubCluster, query)
   195→	}
   196→	if filter.Partition != nil {
   197→		query = buildStringCondition("job.cluster_partition", filter.Partition, query)
   198→	}
   199→	if filter.State != nil {
   200→		states := make([]string, len(filter.State))
   201→		for i, val := range filter.State {
   202→			states[i] = string(val)
   203→		}
   204→		query = query.Where(sq.Eq{"job.job_state": states})
   205→	}
   206→	if filter.Shared != nil {
   207→		query = query.Where("job.shared = ?", *filter.Shared)
   208→	}
   209→	if filter.Project != nil {
   210→		query = buildStringCondition("job.project", filter.Project, query)
   211→	}
   212→	if filter.User != nil {
   213→		query = buildStringCondition("job.hpc_user", filter.User, query)
   214→	}
   215→	if filter.NumNodes != nil {
   216→		query = buildIntCondition("job.num_nodes", filter.NumNodes, query)
   217→	}
   218→	if filter.NumAccelerators != nil {
   219→		query = buildIntCondition("job.num_acc", filter.NumAccelerators, query)
   220→	}
   221→	if filter.NumHWThreads != nil {
   222→		query = buildIntCondition("job.num_hwthreads", filter.NumHWThreads, query)
   223→	}
   224→	if filter.ArrayJobID != nil {
   225→		query = query.Where("job.array_job_id = ?", *filter.ArrayJobID)
   226→	}
   227→	if filter.StartTime != nil {
   228→		query = buildTimeCondition("job.start_time", filter.StartTime, query)
   229→	}
   230→	if filter.Duration != nil {
   231→		query = buildIntCondition("job.duration", filter.Duration, query)
   232→	}
   233→	if filter.Energy != nil {
   234→		query = buildFloatCondition("job.energy", filter.Energy, query)
   235→	}
   236→	// Indices on Tag Table
   237→	if filter.Tags != nil {
   238→		// This is an OR-Logic query: Returns all distinct jobs with at least one of the requested tags; TODO: AND-Logic query?
   239→		query = query.Join("jobtag ON jobtag.job_id = job.id").Where(sq.Eq{"jobtag.tag_id": filter.Tags}).Distinct()
   240→	}
   241→	// No explicit Indices
   242→	if filter.JobID != nil {
   243→		query = buildStringCondition("job.job_id", filter.JobID, query)
   244→	}
   245→	// Queries Within JSONs
   246→	if filter.MetricStats != nil {
   247→		for _, ms := range filter.MetricStats {
   248→			query = buildFloatJSONCondition(ms.MetricName, ms.Range, query)
   249→		}
   250→	}
   251→	if filter.Node != nil {
   252→		query = buildResourceJSONCondition("hostname", filter.Node, query)
   253→	}
   254→	if filter.JobName != nil {
   255→		query = buildMetaJSONCondition("jobName", filter.JobName, query)
   256→	}
   257→	if filter.Schedule != nil {
   258→		interactiveJobname := "interactive"
   259→		switch *filter.Schedule {
   260→		case "interactive":
   261→			iFilter := model.StringInput{Eq: &interactiveJobname}
   262→			query = buildMetaJSONCondition("jobName", &iFilter, query)
   263→		case "batch":
   264→			sFilter := model.StringInput{Neq: &interactiveJobname}
   265→			query = buildMetaJSONCondition("jobName", &sFilter, query)
   266→		}
   267→	}
   268→
   269→	// Configurable Filter to exclude recently started jobs, see config.go: ShortRunningJobsDuration
   270→	if filter.MinRunningFor != nil {
   271→		now := time.Now().Unix()
   272→		// Only jobs whose start timestamp is more than MinRunningFor seconds in the past
   273→		// If a job completed within the configured timeframe, it will still show up after the start_time matches the condition!
   274→		query = query.Where(sq.Lt{"job.start_time": (now - int64(*filter.MinRunningFor))})
   275→	}
   276→	return query
   277→}
   278→
   279→// buildIntCondition creates clauses for integer range filters, using BETWEEN only if required.
   280→func buildIntCondition(field string, cond *config.IntRange, query sq.SelectBuilder) sq.SelectBuilder {
   281→	if cond.From != 1 && cond.To != 0 {
   282→		return query.Where(field+" BETWEEN ? AND ?", cond.From, cond.To)
   283→	} else if cond.From != 1 && cond.To == 0 {
   284→		return query.Where(field+" >= ?", cond.From)
   285→	} else if cond.From == 1 && cond.To != 0 {
   286→		return query.Where(field+" <= ?", cond.To)
   287→	} else {
   288→		return query
   289→	}
   290→}
   291→
   292→// buildFloatCondition creates a clauses for float range filters, using BETWEEN only if required.
   293→func buildFloatCondition(field string, cond *model.FloatRange, query sq.SelectBuilder) sq.SelectBuilder {
   294→	if cond.From != 1.0 && cond.To != 0.0 {
   295→		return query.Where(field+" BETWEEN ? AND ?", cond.From, cond.To)
   296→	} else if cond.From != 1.0 && cond.To == 0.0 {
   297→		return query.Where(field+" >= ?", cond.From)
   298→	} else if cond.From == 1.0 && cond.To != 0.0 {
   299→		return query.Where(field+" <= ?", cond.To)
   300→	} else {
   301→		return query
   302→	}
   303→}
   304→
   305→// buildTimeCondition creates time range filters supporting absolute timestamps,
   306→// relative time ranges (last6h, last24h, last7d, last30d), or open-ended ranges.
   307→// Reminder: BETWEEN Queries are slower and dont use indices as frequently: Only use if both conditions required
   308→func buildTimeCondition(field string, cond *config.TimeRange, query sq.SelectBuilder) sq.SelectBuilder {
   309→	if cond.From != nil && cond.To != nil {
   310→		return query.Where(field+" BETWEEN ? AND ?", cond.From.Unix(), cond.To.Unix())
   311→	} else if cond.From != nil {
   312→		return query.Where(field+" >= ?", cond.From.Unix())
   313→	} else if cond.To != nil {
   314→		return query.Where(field+" <= ?", cond.To.Unix())
   315→	} else if cond.Range != "" {
   316→		now := time.Now().Unix()
   317→		var then int64
   318→		switch cond.Range {
   319→		case "last6h":
   320→			then = now - (60 * 60 * 6)
   321→		case "last24h":
   322→			then = now - (60 * 60 * 24)
   323→		case "last7d":
   324→			then = now - (60 * 60 * 24 * 7)
   325→		case "last30d":
   326→			then = now - (60 * 60 * 24 * 30)
   327→		default:


     1→CREATE TABLE IF NOT EXISTS job (
     2→id                INTEGER PRIMARY KEY,
     3→job_id            BIGINT NOT NULL,
     4→cluster           VARCHAR(255) NOT NULL,
     5→subcluster        VARCHAR(255) NOT NULL,
     6→start_time        BIGINT NOT NULL, -- Unix timestamp
     7→user              VARCHAR(255) NOT NULL,
     8→project           VARCHAR(255) NOT NULL,
     9→partition         VARCHAR(255),
    10→array_job_id      BIGINT,
    11→duration          INT NOT NULL,
    12→walltime          INT NOT NULL,
    13→job_state         VARCHAR(255) NOT NULL
    14→CHECK(job_state IN ('running', 'completed', 'failed', 'canceled', 'stopped', 'timeout')),
    15→meta_data         TEXT,          -- JSON
    16→resources         TEXT NOT NULL, -- JSON
    17→num_nodes         INT NOT NULL,
    18→num_hwthreads     INT,
    19→num_acc           INT,
    20→smt               TINYINT NOT NULL DEFAULT 1 CHECK(smt               IN (0, 1   )),
    21→exclusive         TINYINT NOT NULL DEFAULT 1 CHECK(exclusive         IN (0, 1, 2)),
    22→monitoring_status TINYINT NOT NULL DEFAULT 1 CHECK(monitoring_status IN (0, 1, 2, 3)),
    23→mem_used_max        REAL NOT NULL DEFAULT 0.0,
    24→flops_any_avg       REAL NOT NULL DEFAULT 0.0,
    25→mem_bw_avg          REAL NOT NULL DEFAULT 0.0,
    26→load_avg            REAL NOT NULL DEFAULT 0.0,
    27→net_bw_avg          REAL NOT NULL DEFAULT 0.0,
    28→net_data_vol_total  REAL NOT NULL DEFAULT 0.0,
    29→file_bw_avg         REAL NOT NULL DEFAULT 0.0,
    30→file_data_vol_total REAL NOT NULL DEFAULT 0.0);
    31→
    32→CREATE TABLE IF NOT EXISTS tag (
    33→id       INTEGER PRIMARY KEY,
    34→tag_type VARCHAR(255) NOT NULL,
    35→tag_name VARCHAR(255) NOT NULL,
    36→UNIQUE (tag_type, tag_name));
    37→
    38→CREATE TABLE IF NOT EXISTS jobtag (
    39→job_id INTEGER,
    40→tag_id INTEGER,
    41→PRIMARY KEY (job_id, tag_id),
    42→FOREIGN KEY (job_id) REFERENCES job (id) ON DELETE CASCADE,
    43→FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE);
    44→
    45→CREATE TABLE IF NOT EXISTS user (
    46→username varchar(255) PRIMARY KEY NOT NULL,
    47→password varchar(255) DEFAULT NULL,
    48→ldap     tinyint      NOT NULL DEFAULT 0, /* col called "ldap" for historic reasons, fills the "AuthSource" */
    49→name     varchar(255) DEFAULT NULL,
    50→roles    varchar(255) NOT NULL DEFAULT "[]",
    51→email    varchar(255) DEFAULT NULL);
    52→
    53→CREATE TABLE IF NOT EXISTS configuration (
    54→username varchar(255),
    55→confkey  varchar(255),
    56→value    varchar(255),
    57→PRIMARY KEY (username, confkey),
    58→FOREIGN KEY (username) REFERENCES user (username) ON DELETE CASCADE ON UPDATE NO ACTION);
    59→
    60→
    61→


     1→CREATE INDEX IF NOT EXISTS job_stats        ON job (cluster,subcluster,user);
     2→CREATE INDEX IF NOT EXISTS job_by_user      ON job (user);
     3→CREATE INDEX IF NOT EXISTS job_by_starttime ON job (start_time);
     4→CREATE INDEX IF NOT EXISTS job_by_job_id    ON job (job_id, cluster, start_time);
     5→CREATE INDEX IF NOT EXISTS job_list         ON job (cluster, job_state);
     6→CREATE INDEX IF NOT EXISTS job_list_user    ON job (user, cluster, job_state);
     7→CREATE INDEX IF NOT EXISTS job_list_users   ON job (user, job_state);
     8→CREATE INDEX IF NOT EXISTS job_list_users_start ON job (start_time, user, job_state);
     9→


     1→-- Migration 11: Remove overly specific table indexes formerly used in sorting
     2→-- When one or two indexed columns are used, sorting usually is fast
     3→-- Reduces from ~78 indexes to 48 for better write performance,
     4→-- reduced disk usage, and more reliable query planner decisions.
     5→-- Requires ANALYZE to be run after migration (done automatically on startup).
     6→
     7→-- ============================================================
     8→-- Drop SELECTED existing job indexes (from migrations 08/09)
     9→-- sqlite_autoindex_job_1 (UNIQUE constraint) is kept automatically
    10→-- ============================================================
    11→
    12→-- Cluster+Partition Filter Sorting
    13→DROP INDEX IF EXISTS jobs_cluster_partition_numnodes;
    14→DROP INDEX IF EXISTS jobs_cluster_partition_numhwthreads;
    15→DROP INDEX IF EXISTS jobs_cluster_partition_numacc;
    16→DROP INDEX IF EXISTS jobs_cluster_partition_energy;
    17→
    18→-- Cluster+JobState Filter Sorting
    19→DROP INDEX IF EXISTS jobs_cluster_jobstate_numnodes;
    20→DROP INDEX IF EXISTS jobs_cluster_jobstate_numhwthreads;
    21→DROP INDEX IF EXISTS jobs_cluster_jobstate_numacc;
    22→DROP INDEX IF EXISTS jobs_cluster_jobstate_energy;
    23→
    24→-- Cluster+Shared Filter Sorting
    25→DROP INDEX IF EXISTS jobs_cluster_shared_numnodes;
    26→DROP INDEX IF EXISTS jobs_cluster_shared_numhwthreads;
    27→DROP INDEX IF EXISTS jobs_cluster_shared_numacc;
    28→DROP INDEX IF EXISTS jobs_cluster_shared_energy;
    29→
    30→-- User Filter Sorting
    31→DROP INDEX IF EXISTS jobs_user_numnodes;
    32→DROP INDEX IF EXISTS jobs_user_numhwthreads;
    33→DROP INDEX IF EXISTS jobs_user_numacc;
    34→DROP INDEX IF EXISTS jobs_user_energy;
    35→
    36→-- Project Filter Sorting
    37→DROP INDEX IF EXISTS jobs_project_numnodes;
    38→DROP INDEX IF EXISTS jobs_project_numhwthreads;
    39→DROP INDEX IF EXISTS jobs_project_numacc;
    40→DROP INDEX IF EXISTS jobs_project_energy;
    41→
    42→-- JobState Filter Sorting
    43→DROP INDEX IF EXISTS jobs_jobstate_numnodes;
    44→DROP INDEX IF EXISTS jobs_jobstate_numhwthreads;
    45→DROP INDEX IF EXISTS jobs_jobstate_numacc;
    46→DROP INDEX IF EXISTS jobs_jobstate_energy;
    47→
    48→-- Shared Filter Sorting
    49→DROP INDEX IF EXISTS jobs_shared_numnodes;
    50→DROP INDEX IF EXISTS jobs_shared_numhwthreads;
    51→DROP INDEX IF EXISTS jobs_shared_numacc;
    52→DROP INDEX IF EXISTS jobs_shared_energy;
    53→
    54→-- ArrayJob Filter
    55→DROP INDEX IF EXISTS jobs_cluster_arrayjobid_starttime;
    56→
    57→-- Backup Indices For High Variety Columns
    58→DROP INDEX IF EXISTS jobs_duration;
    59→
    60→-- Optimize DB index usage
    61→PRAGMA optimize;
    62→


     1→-- Migration 12: Add covering index for grouped stats queries
     2→-- Covers the common pattern: WHERE cluster=X AND start_time >= Y GROUP BY hpc_user
     3→-- Includes aggregated columns to avoid main table lookups entirely.
     4→
     5→CREATE INDEX IF NOT EXISTS jobs_cluster_starttime_user_stats
     6→  ON job (cluster, start_time, hpc_user, duration, job_state, num_nodes, num_hwthreads, num_acc);
     7→
     8→PRAGMA optimize;
     9→


     1→CREATE TABLE "node" (
     2→    id INTEGER PRIMARY KEY,
     3→    hostname VARCHAR(255) NOT NULL,
     4→    cluster VARCHAR(255) NOT NULL,
     5→    subcluster VARCHAR(255) NOT NULL,
     6→    meta_data TEXT,          -- JSON
     7→    UNIQUE (hostname, cluster)
     8→);
     9→
    10→CREATE TABLE "node_state" (
    11→    id INTEGER PRIMARY KEY,
    12→    time_stamp INTEGER NOT NULL,
    13→    jobs_running INTEGER DEFAULT 0 NOT NULL,
    14→    cpus_allocated INTEGER DEFAULT 0 NOT NULL,
    15→    memory_allocated INTEGER DEFAULT 0 NOT NULL,
    16→    gpus_allocated INTEGER DEFAULT 0 NOT NULL,
    17→    node_state VARCHAR(255) NOT NULL
    18→    CHECK (node_state IN (
    19→        'allocated', 'reserved', 'idle', 'mixed',
    20→        'down', 'unknown'
    21→    )),
    22→    health_state VARCHAR(255) NOT NULL
    23→    CHECK (health_state IN (
    24→        'full', 'partial', 'failed'
    25→    )),
    26→    health_metrics TEXT,          -- JSON array of strings
    27→    node_id INTEGER,
    28→    FOREIGN KEY (node_id) REFERENCES node (id)
    29→);
    30→
    31→-- Add NEW Indices For New Node Table VARCHAR Fields
    32→CREATE INDEX IF NOT EXISTS nodes_cluster ON node (cluster);
    33→CREATE INDEX IF NOT EXISTS nodes_cluster_subcluster ON node (cluster, subcluster);
    34→
    35→-- Add NEW Indices For New Node_State Table Fields
    36→CREATE INDEX IF NOT EXISTS nodestates_timestamp ON node_state (time_stamp);
    37→CREATE INDEX IF NOT EXISTS nodestates_state_timestamp ON node_state (node_state, time_stamp);
    38→CREATE INDEX IF NOT EXISTS nodestates_health_timestamp ON node_state (health_state, time_stamp);
    39→CREATE INDEX IF NOT EXISTS nodestates_nodeid_state ON node_state (node_id, node_state);
    40→CREATE INDEX IF NOT EXISTS nodestates_nodeid_health ON node_state (node_id, health_state);
    41→CREATE INDEX IF NOT EXISTS nodestates_nodeid_timestamp ON node_state (node_id, time_stamp DESC);
    42→
    43→-- Add NEW Indices For Increased Amounts of Tags
    44→CREATE INDEX IF NOT EXISTS tags_jobid ON jobtag (job_id);
    45→CREATE INDEX IF NOT EXISTS tags_tagid ON jobtag (tag_id);
    46→
    47→-- Optimize DB index usage
    48→PRAGMA optimize;


Bash

grep -l "hpc_user" /Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/migrations/sqlite3/*.sql


     1→DROP INDEX IF EXISTS job_stats;
     2→DROP INDEX IF EXISTS job_by_user;
     3→DROP INDEX IF EXISTS job_by_starttime;
     4→DROP INDEX IF EXISTS job_by_job_id;
     5→DROP INDEX IF EXISTS job_list;
     6→DROP INDEX IF EXISTS job_list_user;
     7→DROP INDEX IF EXISTS job_list_users;
     8→DROP INDEX IF EXISTS job_list_users_start;
     9→
    10→ALTER TABLE job ADD COLUMN energy REAL NOT NULL DEFAULT 0.0;
    11→ALTER TABLE job ADD COLUMN energy_footprint TEXT DEFAULT NULL;
    12→
    13→ALTER TABLE job ADD COLUMN footprint TEXT DEFAULT NULL;
    14→ALTER TABLE tag ADD COLUMN tag_scope TEXT NOT NULL DEFAULT 'global';
    15→
    16→-- Do not use reserved keywords anymore
    17→ALTER TABLE "user" RENAME TO hpc_user;
    18→ALTER TABLE job RENAME COLUMN "user" TO hpc_user;
    19→ALTER TABLE job RENAME COLUMN "partition" TO cluster_partition;
    20→
    21→UPDATE job SET footprint = '{"flops_any_avg": 0.0}';
    22→UPDATE job SET footprint = json_replace(footprint, '$.flops_any_avg', job.flops_any_avg);
    23→UPDATE job SET footprint = json_insert(footprint, '$.mem_bw_avg', job.mem_bw_avg);
    24→UPDATE job SET footprint = json_insert(footprint, '$.mem_used_max', job.mem_used_max);
    25→UPDATE job SET footprint = json_insert(footprint, '$.cpu_load_avg', job.load_avg);
    26→UPDATE job SET footprint = json_insert(footprint, '$.net_bw_avg', job.net_bw_avg) WHERE job.net_bw_avg != 0;
    27→UPDATE job SET footprint = json_insert(footprint, '$.net_data_vol_total', job.net_data_vol_total) WHERE job.net_data_vol_total != 0;
    28→UPDATE job SET footprint = json_insert(footprint, '$.file_bw_avg', job.file_bw_avg) WHERE job.file_bw_avg != 0;
    29→UPDATE job SET footprint = json_insert(footprint, '$.file_data_vol_total', job.file_data_vol_total) WHERE job.file_data_vol_total != 0;
    30→
    31→ALTER TABLE job DROP flops_any_avg;
    32→ALTER TABLE job DROP mem_bw_avg;
    33→ALTER TABLE job DROP mem_used_max;
    34→ALTER TABLE job DROP load_avg;
    35→ALTER TABLE job DROP net_bw_avg;
    36→ALTER TABLE job DROP net_data_vol_total;
    37→ALTER TABLE job DROP file_bw_avg;
    38→ALTER TABLE job DROP file_data_vol_total;
    39→
    40→-- Indices for: Single filters, combined filters, sorting, sorting with filters
    41→-- Cluster Filter
    42→CREATE INDEX IF NOT EXISTS jobs_cluster ON job (cluster);
    43→CREATE INDEX IF NOT EXISTS jobs_cluster_user ON job (cluster, hpc_user);
    44→CREATE INDEX IF NOT EXISTS jobs_cluster_project ON job (cluster, project);
    45→CREATE INDEX IF NOT EXISTS jobs_cluster_subcluster ON job (cluster, subcluster);
    46→-- Cluster Filter Sorting
    47→CREATE INDEX IF NOT EXISTS jobs_cluster_starttime ON job (cluster, start_time);
    48→CREATE INDEX IF NOT EXISTS jobs_cluster_duration ON job (cluster, duration);
    49→CREATE INDEX IF NOT EXISTS jobs_cluster_numnodes ON job (cluster, num_nodes);
    50→CREATE INDEX IF NOT EXISTS jobs_cluster_numhwthreads ON job (cluster, num_hwthreads);
    51→CREATE INDEX IF NOT EXISTS jobs_cluster_numacc ON job (cluster, num_acc);
    52→CREATE INDEX IF NOT EXISTS jobs_cluster_energy ON job (cluster, energy);
    53→
    54→-- Cluster+Partition Filter
    55→CREATE INDEX IF NOT EXISTS jobs_cluster_partition ON job (cluster, cluster_partition);
    56→-- Cluster+Partition Filter Sorting
    57→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_starttime ON job (cluster, cluster_partition, start_time);
    58→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_duration ON job (cluster, cluster_partition, duration);
    59→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_numnodes ON job (cluster, cluster_partition, num_nodes);
    60→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_numhwthreads ON job (cluster, cluster_partition, num_hwthreads);
    61→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_numacc ON job (cluster, cluster_partition, num_acc);
    62→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_energy ON job (cluster, cluster_partition, energy);
    63→
    64→-- Cluster+Partition+Jobstate Filter
    65→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate ON job (cluster, cluster_partition, job_state);
    66→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_user ON job (cluster, cluster_partition, job_state, hpc_user);
    67→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_project ON job (cluster, cluster_partition, job_state, project);
    68→-- Cluster+Partition+Jobstate Filter Sorting
    69→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_starttime ON job (cluster, cluster_partition, job_state, start_time);
    70→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_duration ON job (cluster, cluster_partition, job_state, duration);
    71→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_numnodes ON job (cluster, cluster_partition, job_state, num_nodes);
    72→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_numhwthreads ON job (cluster, cluster_partition, job_state, num_hwthreads);
    73→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_numacc ON job (cluster, cluster_partition, job_state, num_acc);
    74→CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_energy ON job (cluster, cluster_partition, job_state, energy);
    75→
    76→-- Cluster+JobState Filter
    77→CREATE INDEX IF NOT EXISTS jobs_cluster_jobstate ON job (cluster, job_state);
    78→CREATE INDEX IF NOT EXISTS jobs_cluster_jobstate_user ON job (cluster, job_state, hpc_user);
    79→CREATE INDEX IF NOT EXISTS jobs_cluster_jobstate_project ON job (cluster, job_state, project);
    80→-- Cluster+JobState Filter Sorting
    81→CREATE INDEX IF NOT EXISTS jobs_cluster_jobstate_starttime ON job (cluster, job_state, start_time);
    82→CREATE INDEX IF NOT EXISTS jobs_cluster_jobstate_duration ON job (cluster, job_state, duration);
    83→CREATE INDEX IF NOT EXISTS jobs_cluster_jobstate_numnodes ON job (cluster, job_state, num_nodes);
    84→CREATE INDEX IF NOT EXISTS jobs_cluster_jobstate_numhwthreads ON job (cluster, job_state, num_hwthreads);
    85→CREATE INDEX IF NOT EXISTS jobs_cluster_jobstate_numacc ON job (cluster, job_state, num_acc);
    86→CREATE INDEX IF NOT EXISTS jobs_cluster_jobstate_energy ON job (cluster, job_state, energy);
    87→
    88→-- User Filter
    89→CREATE INDEX IF NOT EXISTS jobs_user ON job (hpc_user);
    90→-- User Filter Sorting
    91→CREATE INDEX IF NOT EXISTS jobs_user_starttime ON job (hpc_user, start_time);
    92→CREATE INDEX IF NOT EXISTS jobs_user_duration ON job (hpc_user, duration);
    93→CREATE INDEX IF NOT EXISTS jobs_user_numnodes ON job (hpc_user, num_nodes);
    94→CREATE INDEX IF NOT EXISTS jobs_user_numhwthreads ON job (hpc_user, num_hwthreads);
    95→CREATE INDEX IF NOT EXISTS jobs_user_numacc ON job (hpc_user, num_acc);
    96→CREATE INDEX IF NOT EXISTS jobs_user_energy ON job (hpc_user, energy);
    97→
    98→-- Project Filter
    99→CREATE INDEX IF NOT EXISTS jobs_project ON job (project);
   100→CREATE INDEX IF NOT EXISTS jobs_project_user ON job (project, hpc_user);
   101→-- Project Filter Sorting
   102→CREATE INDEX IF NOT EXISTS jobs_project_starttime ON job (project, start_time);
   103→CREATE INDEX IF NOT EXISTS jobs_project_duration ON job (project, duration);
   104→CREATE INDEX IF NOT EXISTS jobs_project_numnodes ON job (project, num_nodes);
   105→CREATE INDEX IF NOT EXISTS jobs_project_numhwthreads ON job (project, num_hwthreads);
   106→CREATE INDEX IF NOT EXISTS jobs_project_numacc ON job (project, num_acc);
   107→CREATE INDEX IF NOT EXISTS jobs_project_energy ON job (project, energy);
   108→
   109→-- JobState Filter
   110→CREATE INDEX IF NOT EXISTS jobs_jobstate ON job (job_state);
   111→CREATE INDEX IF NOT EXISTS jobs_jobstate_user ON job (job_state, hpc_user);
   112→CREATE INDEX IF NOT EXISTS jobs_jobstate_project ON job (job_state, project);
   113→CREATE INDEX IF NOT EXISTS jobs_jobstate_cluster ON job (job_state, cluster);
   114→-- JobState Filter Sorting
   115→CREATE INDEX IF NOT EXISTS jobs_jobstate_starttime ON job (job_state, start_time);
   116→CREATE INDEX IF NOT EXISTS jobs_jobstate_duration ON job (job_state, duration);
   117→CREATE INDEX IF NOT EXISTS jobs_jobstate_numnodes ON job (job_state, num_nodes);
   118→CREATE INDEX IF NOT EXISTS jobs_jobstate_numhwthreads ON job (job_state, num_hwthreads);
   119→CREATE INDEX IF NOT EXISTS jobs_jobstate_numacc ON job (job_state, num_acc);
   120→CREATE INDEX IF NOT EXISTS jobs_jobstate_energy ON job (job_state, energy);
   121→
   122→-- ArrayJob Filter
   123→CREATE INDEX IF NOT EXISTS jobs_arrayjobid_starttime ON job (array_job_id, start_time);
   124→CREATE INDEX IF NOT EXISTS jobs_cluster_arrayjobid_starttime ON job (cluster, array_job_id, start_time);
   125→
   126→-- Sorting without active filters
   127→CREATE INDEX IF NOT EXISTS jobs_starttime ON job (start_time);
   128→CREATE INDEX IF NOT EXISTS jobs_duration ON job (duration);
   129→CREATE INDEX IF NOT EXISTS jobs_numnodes ON job (num_nodes);
   130→CREATE INDEX IF NOT EXISTS jobs_numhwthreads ON job (num_hwthreads);
   131→CREATE INDEX IF NOT EXISTS jobs_numacc ON job (num_acc);
   132→CREATE INDEX IF NOT EXISTS jobs_energy ON job (energy);
   133→
   134→-- Single filters with default starttime sorting
   135→CREATE INDEX IF NOT EXISTS jobs_duration_starttime ON job (duration, start_time);
   136→CREATE INDEX IF NOT EXISTS jobs_numnodes_starttime ON job (num_nodes, start_time);
   137→CREATE INDEX IF NOT EXISTS jobs_numhwthreads_starttime ON job (num_hwthreads, start_time);
   138→CREATE INDEX IF NOT EXISTS jobs_numacc_starttime ON job (num_acc, start_time);
   139→CREATE INDEX IF NOT EXISTS jobs_energy_starttime ON job (energy, start_time);
   140→
   141→-- Optimize DB index usage
   142→PRAGMA optimize;
   143→


grep -n "cluster_partition\|hpc_user" /Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/migrations/sqlite3/08_add-footprint.up.sql | head -20


17:ALTER TABLE "user" RENAME TO hpc_user;
18:ALTER TABLE job RENAME COLUMN "user" TO hpc_user;
19:ALTER TABLE job RENAME COLUMN "partition" TO cluster_partition;
43:CREATE INDEX IF NOT EXISTS jobs_cluster_user ON job (cluster, hpc_user);
55:CREATE INDEX IF NOT EXISTS jobs_cluster_partition ON job (cluster, cluster_partition);
57:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_starttime ON job (cluster, cluster_partition, start_time);
58:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_duration ON job (cluster, cluster_partition, duration);
59:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_numnodes ON job (cluster, cluster_partition, num_nodes);
60:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_numhwthreads ON job (cluster, cluster_partition, num_hwthreads);
61:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_numacc ON job (cluster, cluster_partition, num_acc);
62:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_energy ON job (cluster, cluster_partition, energy);
65:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate ON job (cluster, cluster_partition, job_state);
66:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_user ON job (cluster, cluster_partition, job_state, hpc_user);
67:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_project ON job (cluster, cluster_partition, job_state, project);
69:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_starttime ON job (cluster, cluster_partition, job_state, start_time);
70:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_duration ON job (cluster, cluster_partition, job_state, duration);
71:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_numnodes ON job (cluster, cluster_partition, job_state, num_nodes);
72:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_numhwthreads ON job (cluster, cluster_partition, job_state, num_hwthreads);
73:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_numacc ON job (cluster, cluster_partition, job_state, num_acc);
74:CREATE INDEX IF NOT EXISTS jobs_cluster_partition_jobstate_energy ON job (cluster, cluster_partition, job_state, energy);


36-var routes []Route = []Route{
37-	{"/", "home.tmpl", "ClusterCockpit", false, setupHomeRoute},
38-	{"/config", "config.tmpl", "Settings", false, setupConfigRoute},
39-	{"/monitoring/jobs/", "monitoring/jobs.tmpl", "Jobs - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { return i }},
40-	{"/monitoring/job/{id:[0-9]+}", "monitoring/job.tmpl", "Job <ID> - ClusterCockpit", false, setupJobRoute},
41:	{"/monitoring/users/", "monitoring/list.tmpl", "Users - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "USER"; return i }},
42-	{"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }},
43-	{"/monitoring/tags/", "monitoring/taglist.tmpl", "Tags - ClusterCockpit", false, setupTaglistRoute},
44:	{"/monitoring/user/{id}", "monitoring/user.tmpl", "User <ID> - ClusterCockpit", true, setupUserRoute},
45-	{"/monitoring/systems/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> Node Overview - ClusterCockpit", false, setupClusterOverviewRoute},
46-	{"/monitoring/systems/list/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> Node List - ClusterCockpit", false, setupClusterListRoute},
47-	{"/monitoring/systems/list/{cluster}/{subcluster}", "monitoring/systems.tmpl", "Cluster <ID> <SID> Node List - ClusterCockpit", false, setupClusterListRoute},
48-	{"/monitoring/node/{cluster}/{hostname}", "monitoring/node.tmpl", "Node <ID> - ClusterCockpit", false, setupNodeRoute},
49-	{"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analysis - ClusterCockpit", true, setupAnalysisRoute},
--
538-				fromTime := strconv.FormatInt((time.Now().Unix() - int64(30*24*3600)), 10)
539-
540-				http.Redirect(rw, r, "/monitoring/jobs/?startTime="+fromTime+"-"+untilTime+"&arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) / All Users: Redirect to Tablequery
541-			case "username":
542-				if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
543:					http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " "))+"&startTime=last30d", http.StatusFound)
544-				} else {
545-					web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo})
546-				}
547-			case "name":
548-				usernames, _ := repo.FindColumnValues(user, strings.Trim(splitSearch[1], " "), "user", "username", "name")
549-				if len(usernames) != 0 {
550-					joinedNames := strings.Join(usernames, "&user=")
551:					http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames+"&startTime=last30d", http.StatusFound)
552-				} else {
553-					if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
554:						http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound&startTime=last30d", http.StatusPermanentRedirect)
555-					} else {
556-						web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Error", MsgType: "alert-danger", Message: "Missing Access Rights", User: *user, Roles: availableRoles, Build: buildInfo})
557-					}
558-				}
559-			default:
--
564-			jobid, username, project, jobname := repo.FindUserOrProjectOrJobname(user, strings.Trim(search, " "))
565-
566-			if jobid != "" {
567-				http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(jobid), http.StatusFound) // JobId (Match)
568-			} else if username != "" {
569:				http.Redirect(rw, r, "/monitoring/user/"+username, http.StatusFound) // User: Redirect to user page of first match
570-			} else if project != "" {
571-				http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(project), http.StatusFound) // projectId (equal)
572-			} else if jobname != "" {
573-				// Add Last 30 Days to migitate timeouts
574-				untilTime := strconv.FormatInt(time.Now().Unix(), 10)


find /Users/jan/prg/ClusterCockpit/cc-backend/web -name "list.tmpl" -o -name "monitoring.root.svelte" 2>/dev/null | head -5


1→{{define "content"}}
     2→

     3→{{end}}
     4→
     5→{{define "stylesheets"}}
     6→    <link rel='stylesheet' href='/build/list.css'>
     7→{{end}}
     8→{{define "javascript"}}
     9→
    14→
    15→{{end}}
    16→


grep -n "FilterPresets\|setupListRoute\|monitoring/list" /Users/jan/prg/ClusterCockpit/cc-backend/internal/routerConfig/routes.go | head -20


41:	{"/monitoring/users/", "monitoring/list.tmpl", "Users - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "USER"; return i }},
42:	{"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }},
235:func buildFilterPresets(query url.Values) map[string]any {
507:				page.FilterPresets = buildFilterPresets(r.URL.Query())


205-	if userAuthlevel >= 4 { / Support+ : Show tags for all scopes, regardless of count
206-		for _, tag := range tags {
207-			tagItem := map[string]any{
208-				"id":    tag.ID,
209-				"name":  tag.Name,
210-				"scope": tag.Scope,
211-				"count": counts[fmt.Sprint(tag.Type, tag.Name, tag.ID)],
212-			}
213-			tagMap[tag.Type] = append(tagMap[tag.Type], tagItem)
214-		}
215-	} else if userAuthlevel < 4 && userAuthlevel >= 2 { / User+ : Show global and admin scope only if at least 1 tag used, private scope regardless of count
216-		for _, tag := range tags {
217-			tagCount := counts[fmt.Sprint(tag.Type, tag.Name, tag.ID)]
218-			if ((tag.Scope == "global" || tag.Scope == "admin") && tagCount >= 1) || (tag.Scope != "global" && tag.Scope != "admin") {
219-				tagItem := map[string]any{
220-					"id":    tag.ID,
221-					"name":  tag.Name,
222-					"scope": tag.Scope,
223-					"count": tagCount,
224-				}
225-				tagMap[tag.Type] = append(tagMap[tag.Type], tagItem)
226-			}
227-		}
228-	} // auth < 2 return nothing for this route
229-
230-	i["tagmap"] = tagMap
231-	return i
232-}
233-
234-/ FIXME: Lots of redundant code. Needs refactoring
235:func buildFilterPresets(query url.Values) map[string]any {
236-	filterPresets := map[string]any{}
237-
238-	if query.Get("cluster") != "" {
239-		filterPresets["cluster"] = query.Get("cluster")
240-	}
241-	if query.Get("partition") != "" {
242-		filterPresets["partition"] = query.Get("partition")
243-	}
244-	if query.Get("project") != "" {
245-		filterPresets["project"] = query.Get("project")
246-		filterPresets["projectMatch"] = "eq"
247-	}
248-	if query.Get("jobName") != "" {
249-		filterPresets["jobName"] = query.Get("jobName")
250-	}
251-	if len(query["user"]) != 0 {
252-		if len(query["user"]) == 1 {
253-			filterPresets["user"] = query.Get("user")
254-			filterPresets["userMatch"] = "contains"
255-		} else {
256-			filterPresets["user"] = query["user"]
257-			filterPresets["userMatch"] = "in"
258-		}
259-	}
260-	if len(query["state"]) != 0 {
261-		filterPresets["state"] = query["state"]
262-	}
263-	if query.Get("shared") != "" {
264-		filterPresets["shared"] = query.Get("shared")
265-	}


   235→func buildFilterPresets(query url.Values) map[string]any {
   236→	filterPresets := map[string]any{}
   237→
   238→	if query.Get("cluster") != "" {
   239→		filterPresets["cluster"] = query.Get("cluster")
   240→	}
   241→	if query.Get("partition") != "" {
   242→		filterPresets["partition"] = query.Get("partition")
   243→	}
   244→	if query.Get("project") != "" {
   245→		filterPresets["project"] = query.Get("project")
   246→		filterPresets["projectMatch"] = "eq"
   247→	}
   248→	if query.Get("jobName") != "" {
   249→		filterPresets["jobName"] = query.Get("jobName")
   250→	}
   251→	if len(query["user"]) != 0 {
   252→		if len(query["user"]) == 1 {
   253→			filterPresets["user"] = query.Get("user")
   254→			filterPresets["userMatch"] = "contains"
   255→		} else {
   256→			filterPresets["user"] = query["user"]
   257→			filterPresets["userMatch"] = "in"
   258→		}
   259→	}
   260→	if len(query["state"]) != 0 {
   261→		filterPresets["state"] = query["state"]
   262→	}
   263→	if query.Get("shared") != "" {
   264→		filterPresets["shared"] = query.Get("shared")
   265→	}
   266→	if query.Get("schedule") != "" {
   267→		filterPresets["schedule"] = query.Get("schedule")
   268→	}
   269→	if rawtags, ok := query["tag"]; ok {
   270→		tags := make([]int, len(rawtags))
   271→		for i, tid := range rawtags {
   272→			var err error
   273→			tags[i], err = strconv.Atoi(tid)
   274→			if err != nil {
   275→				tags[i] = -1
   276→			}
   277→		}
   278→		filterPresets["tags"] = tags
   279→	}
   280→	if query.Get("duration") != "" {
   281→		parts := strings.Split(query.Get("duration"), "-")
   282→		if len(parts) == 2 {
   283→			if parts[0] == "lessthan" {
   284→				lt, lte := strconv.Atoi(parts[1])
   285→				if lte == nil {
   286→					filterPresets["duration"] = map[string]int{"lessThan": lt, "from": 0, "to": 0}
   287→				}
   288→			} else if parts[0] == "morethan" {
   289→				mt, mte := strconv.Atoi(parts[1])
   290→				if mte == nil {
   291→					filterPresets["duration"] = map[string]int{"moreThan": mt, "from": 0, "to": 0}
   292→				}
   293→			} else {
   294→				a, e1 := strconv.Atoi(parts[0])
   295→				b, e2 := strconv.Atoi(parts[1])
   296→				if e1 == nil && e2 == nil {
   297→					filterPresets["duration"] = map[string]int{"from": a, "to": b}
   298→				}
   299→			}
   300→		}
   301→	}
   302→	if query.Get("node") != "" {
   303→		filterPresets["node"] = query.Get("node")
   304→	}
   305→	if query.Get("numNodes") != "" {
   306→		parts := strings.Split(query.Get("numNodes"), "-")
   307→		if len(parts) == 2 {
   308→			if parts[0] == "lessthan" {
   309→				lt, lte := strconv.Atoi(parts[1])
   310→				if lte == nil {
   311→					filterPresets["numNodes"] = map[string]int{"from": 1, "to": lt}
   312→				}
   313→			} else if parts[0] == "morethan" {
   314→				mt, mte := strconv.Atoi(parts[1])


   300→		}
   301→	}
   302→	if query.Get("node") != "" {
   303→		filterPresets["node"] = query.Get("node")
   304→	}
   305→	if query.Get("numNodes") != "" {
   306→		parts := strings.Split(query.Get("numNodes"), "-")
   307→		if len(parts) == 2 {
   308→			if parts[0] == "lessthan" {
   309→				lt, lte := strconv.Atoi(parts[1])
   310→				if lte == nil {
   311→					filterPresets["numNodes"] = map[string]int{"from": 1, "to": lt}
   312→				}
   313→			} else if parts[0] == "morethan" {
   314→				mt, mte := strconv.Atoi(parts[1])
   315→				if mte == nil {
   316→					filterPresets["numNodes"] = map[string]int{"from": mt, "to": 0}
   317→				}
   318→			} else {
   319→				a, e1 := strconv.Atoi(parts[0])
   320→				b, e2 := strconv.Atoi(parts[1])
   321→				if e1 == nil && e2 == nil {
   322→					filterPresets["numNodes"] = map[string]int{"from": a, "to": b}
   323→				}
   324→			}
   325→		}
   326→	}
   327→	if query.Get("numHWThreads") != "" {
   328→		parts := strings.Split(query.Get("numHWThreads"), "-")
   329→		if len(parts) == 2 {
   330→			if parts[0] == "lessthan" {
   331→				lt, lte := strconv.Atoi(parts[1])
   332→				if lte == nil {
   333→					filterPresets["numHWThreads"] = map[string]int{"from": 1, "to": lt}
   334→				}
   335→			} else if parts[0] == "morethan" {
   336→				mt, mte := strconv.Atoi(parts[1])
   337→				if mte == nil {
   338→					filterPresets["numHWThreads"] = map[string]int{"from": mt, "to": 0}
   339→				}
   340→			} else {
   341→				a, e1 := strconv.Atoi(parts[0])
   342→				b, e2 := strconv.Atoi(parts[1])
   343→				if e1 == nil && e2 == nil {
   344→					filterPresets["numHWThreads"] = map[string]int{"from": a, "to": b}
   345→				}
   346→			}
   347→		}
   348→	}
   349→	if query.Get("numAccelerators") != "" {
   350→		parts := strings.Split(query.Get("numAccelerators"), "-")
   351→		if len(parts) == 2 {
   352→			if parts[0] == "lessthan" {
   353→				lt, lte := strconv.Atoi(parts[1])
   354→				if lte == nil {
   355→					filterPresets["numAccelerators"] = map[string]int{"from": 1, "to": lt}
   356→				}
   357→			} else if parts[0] == "morethan" {
   358→				mt, mte := strconv.Atoi(parts[1])
   359→				if mte == nil {
   360→					filterPresets["numAccelerators"] = map[string]int{"from": mt, "to": 0}
   361→				}
   362→			} else {
   363→				a, e1 := strconv.Atoi(parts[0])
   364→				b, e2 := strconv.Atoi(parts[1])
   365→				if e1 == nil && e2 == nil {
   366→					filterPresets["numAccelerators"] = map[string]int{"from": a, "to": b}
   367→				}
   368→			}
   369→		}
   370→	}
   371→	if len(query["dbId"]) != 0 {
   372→		filterPresets["dbId"] = query["dbId"]
   373→	}
   374→	if query.Get("jobId") != "" {
   375→		if len(query["jobId"]) == 1 {
   376→			filterPresets["jobId"] = query.Get("jobId")
   377→			filterPresets["jobIdMatch"] = "eq"
   378→		} else {
   379→			filterPresets["jobId"] = query["jobId"]


236-	filterPresets := map[string]any{}
237-
238:	if query.Get("cluster") != "" {
239:		filterPresets["cluster"] = query.Get("cluster")
240-	}
241:	if query.Get("partition") != "" {
242:		filterPresets["partition"] = query.Get("partition")
243-	}
244:	if query.Get("project") != "" {
245:		filterPresets["project"] = query.Get("project")
246-		filterPresets["projectMatch"] = "eq"
247-	}
248:	if query.Get("jobName") != "" {
249:		filterPresets["jobName"] = query.Get("jobName")
250-	}
251-	if len(query["user"]) != 0 {
252-		if len(query["user"]) == 1 {
253:			filterPresets["user"] = query.Get("user")
254-			filterPresets["userMatch"] = "contains"
255-		} else {
--
261-		filterPresets["state"] = query["state"]
262-	}
263:	if query.Get("shared") != "" {
264:		filterPresets["shared"] = query.Get("shared")
265-	}
266:	if query.Get("schedule") != "" {
267:		filterPresets["schedule"] = query.Get("schedule")
268-	}
269-	if rawtags, ok := query["tag"]; ok {

[Showing results with pagination = limit: 30, offset: 0]


grep -n "startTime\|fromTime\|toTime" /Users/jan/prg/ClusterCockpit/cc-backend/internal/routerConfig/routes.go | head -20


388:	if query.Get("startTime") != "" {
389:		parts := strings.Split(query.Get("startTime"), "-")
394:				filterPresets["startTime"] = map[string]string{
400:			filterPresets["startTime"] = map[string]string{
401:				"range": query.Get("startTime"),
530:				fromTime := strconv.FormatInt((time.Now().Unix() - int64(30*24*3600)), 10)
532:				http.Redirect(rw, r, "/monitoring/jobs/?startTime="+fromTime+"-"+untilTime+"&jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
538:				fromTime := strconv.FormatInt((time.Now().Unix() - int64(30*24*3600)), 10)
540:				http.Redirect(rw, r, "/monitoring/jobs/?startTime="+fromTime+"-"+untilTime+"&arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
543:					http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " "))+"&startTime=last30d", http.StatusFound)
551:					http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames+"&startTime=last30d", http.StatusFound)
554:						http.Redirect(rw, r, "/monitoring/users/?user=NoUserNameFound&startTime=last30d", http.StatusPermanentRedirect)
575:				fromTime := strconv.FormatInt((time.Now().Unix() - int64(30*24*3600)), 10)
577:				http.Redirect(rw, r, "/monitoring/jobs/?startTime="+fromTime+"-"+untilTime+"&jobName="+url.QueryEscape(jobname), http.StatusFound) // 30D Fitler + JobName (contains)


   388→	if query.Get("startTime") != "" {
   389→		parts := strings.Split(query.Get("startTime"), "-")
   390→		if len(parts) == 2 { // Time in seconds, from - to
   391→			a, e1 := strconv.ParseInt(parts[0], 10, 64)
   392→			b, e2 := strconv.ParseInt(parts[1], 10, 64)
   393→			if e1 == nil && e2 == nil {
   394→				filterPresets["startTime"] = map[string]string{
   395→					"from": time.Unix(a, 0).Format(time.RFC3339),
   396→					"to":   time.Unix(b, 0).Format(time.RFC3339),
   397→				}
   398→			}
   399→		} else { // named range
   400→			filterPresets["startTime"] = map[string]string{
   401→				"range": query.Get("startTime"),
   402→			}
   403→		}
   404→	}
   405→	if query.Get("energy") != "" {
   406→		parts := strings.Split(query.Get("energy"), "-")
   407→		if len(parts) == 2 {
   408→			if parts[0] == "lessthan" {
   409→				lt, lte := strconv.Atoi(parts[1])
   410→				if lte == nil {
   411→					filterPresets["energy"] = map[string]int{"from": 1, "to": lt}
   412→				}
   413→			} else if parts[0] == "morethan" {
   414→				mt, mte := strconv.Atoi(parts[1])
   415→				if mte == nil {
   416→					filterPresets["energy"] = map[string]int{"from": mt, "to": 0}
   417→				}


8-  - `startTimeQuickSelect Bool?`: Render startTime quick selections [Default: false]
9-  - `shortJobQuickSelect Bool?`: Render short job quick selections [Default: false]
10-  - `shortJobCutoff Int?`: Time in seconds for jobs to be considered short [Default: null]
11-  - `matchedJobs Number?`: Number of jobs matching the filter [Default: -2]
12-  - `showFilter Func`: If the filter component should be rendered in addition to total count info [Default: true]
13:  - `applyFilters Func`: The callback function to apply current filter selection
14-
15-  Functions:
16:  - `void updateFilters (additionalFilters: Object, force: Bool)`:
17-    Handles new filters from nested components, triggers upstream update event.
18-    'additionalFilters' usually added to existing selection, but can be forced to overwrite instead.
19--->
20-
21-<script>
--
49-    startTimeQuickSelect = false,
50-    shortJobQuickSelect = false,
51-    shortJobCutoff = 0,
52-    matchedJobs = -2,
53-    showFilter = true,
54:    applyFilters
55-  } = $props();
56-
57-  /* Const Init */
58-  const nodeMatchLabels = {
59-    eq: "",
--
142-  let isStatsOpen = $state(false)
143-
144-  /* Functions */
145-  // Can be called from the outside to trigger a 'update' event from this component.
146-  // 'force' option empties existing filters and then applies only 'additionalFilters'
147:  export function updateFilters(additionalFilters = null, force = false) {
148-    // Empty Current Filter For Force
149-    if (additionalFilters != null && force) {
150-      filters = {...filterReset}
151-    }
152-    // Add Additional Filters
--
215-    if (filters.stats.length != 0)
216-      items.push({ metricStats: filters.stats.map((st) => { return { metricName: st.field, range: { from: st.from, to: st.to }} }) });
217-    if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
218-    if (filters.jobName) items.push({ jobName: { contains: filters.jobName } });
219-    if (filters.schedule) items.push({ schedule: filters.schedule });
220:    applyFilters({ filters: items });
221-    changeURL();
222-    return items;
223-  }
224-
225-  function changeURL() {

[Showing results with pagination = limit: 50, offset: 0]


Perfect! Now I have comprehensive information. Let me create a summary report of my findings:

## Summary of Code Path Analysis for `/monitoring/users/?cluster=woody&startTime=last30d`

Based on my thorough investigation of the ClusterCockpit codebase, here's the complete flow:

### 1. GraphQL Query Made When Visiting `/monitoring/users/?cluster=woody&startTime=last30d`

The **`List.root.svelte`** component (lines 77-99) executes this GraphQL query:

```graphql
query($jobFilters: [JobFilter!]!, $fetchRunning: Boolean!) {
  rows: jobsStatistics(filter: $jobFilters, groupBy: USER) {
    id
    name
    totalJobs
    shortJobs
    totalCores @include(if: $fetchRunning)
    totalAccs @include(if: $fetchRunning)
    totalWalltime
    totalCoreHours
    totalAccHours
  }
}
```

**Variables passed:**
- `jobFilters`: Array of JobFilter objects (built from query parameters)
- `fetchRunning`: Boolean (true if running jobs are being filtered)

### 2. Filters Passed to `buildStatsQuery`

For the URL `/monitoring/users/?cluster=woody&startTime=last30d`, the following filters are extracted:

**In `buildFilterPresets()` (routes.go, lines 235-404):**
- `cluster: "woody"`
- `startTime: { range: "last30d" }`

**These become `JobFilter` objects:**
```
[
  {
    cluster: { eq: "woody" },
    startTime: { range: "last30d" }
  }
]
```

These filters are passed to **`JobRepository.JobsStatsGrouped()`** (stats.go, line 233) which calls **`buildStatsQuery()`** (stats.go, line 135).

### 3. What `BuildWhereClause` Does

Located in `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/jobQuery.go` (lines 178-277), `BuildWhereClause()` translates GraphQL filter expressions into SQL WHERE conditions:

**For cluster filter:**
- Line 190-191: Translates `cluster` → `job.cluster` column via `buildStringCondition()`

**For startTime filter:**
- Line 227-228: Calls `buildTimeCondition()` which:
  - Recognizes the "last30d" range (line 325)
  - Converts to: `now - (60 * 60 * 24 * 30)` seconds
  - Generates WHERE clause: `job.start_time >= <timestamp>`

**Other supported filters include:**
- User, project, job state, partition
- Duration, energy, number of nodes/accelerators
- Tags (via JOIN on jobtag table)
- JSON searches in meta_data, footprint, resources

### 4. Indexes on the Job Table

**Key migration files:**
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/migrations/sqlite3/02_add-index.up.sql` - Initial basic indexes
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/migrations/sqlite3/08_add-footprint.up.sql` - Comprehensive index coverage
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/migrations/sqlite3/11_optimize-indexes.up.sql` - Index optimization (removes 30 redundant indexes)
- `/Users/jan/prg/ClusterCockpit/cc-backend/internal/repository/migrations/sqlite3/12_stats-covering-index.up.sql` - **NEW covering index for stats queries**

**Active indexes after all migrations:**

Basic indexes:
```sql
job_stats        ON (cluster, subcluster, user)  -- Legacy name, actually hpc_user
job_by_user      ON (user)                        -- Legacy column name
job_by_starttime ON (start_time)
job_list         ON (cluster, job_state)
```

Cluster-specific indexes:
```sql
jobs_cluster                ON (cluster)
jobs_cluster_user           ON (cluster, hpc_user)
jobs_cluster_project        ON (cluster, project)
jobs_cluster_starttime      ON (cluster, start_time)
```

**CRITICAL NEW COVERING INDEX (Migration 12):**
```sql
jobs_cluster_starttime_user_stats
  ON job (cluster, start_time, hpc_user, duration, job_state, num_nodes, num_hwthreads, num_acc)
```

This covering index is specifically designed for the grouped stats query pattern and includes all columns needed for the GROUP BY and aggregation calculations.

### 5. Job Table Schema

**Full schema from Migration 01 + subsequent modifications (Migration 08):**

```
CREATE TABLE job (
  id                 INTEGER PRIMARY KEY,
  job_id             BIGINT NOT NULL,
  cluster            VARCHAR(255) NOT NULL,
  subcluster         VARCHAR(255) NOT NULL,
  start_time         BIGINT NOT NULL,          -- Unix timestamp (INDEXED)
  hpc_user           VARCHAR(255) NOT NULL,    -- Renamed from "user" in Migration 08 (INDEXED)
  project            VARCHAR(255) NOT NULL,
  cluster_partition  VARCHAR(255),              -- Renamed from "partition" in Migration 08
  array_job_id       BIGINT,
  duration           INT NOT NULL,
  walltime           INT NOT NULL,
  job_state          VARCHAR(255) NOT NULL,    -- CHECK: running|completed|failed|canceled|stopped|timeout
  meta_data          TEXT,                      -- JSON
  resources          TEXT NOT NULL,             -- JSON
  num_nodes          INT NOT NULL,
  num_hwthreads      INT,
  num_acc            INT,
  smt                TINYINT DEFAULT 1,
  exclusive          TINYINT DEFAULT 1,
  monitoring_status  TINYINT DEFAULT 1,
  energy             REAL DEFAULT 0.0,          -- Added in Migration 08
  footprint          TEXT DEFAULT NULL,         -- JSON, added in Migration 08
  energy_footprint   TEXT DEFAULT NULL          -- JSON, added in Migration 08
)
```

**Supporting tables:**
- `hpc_user` (users)
- `tag` (tags with scope)
- `jobtag` (job-tag relationships with indexes on both job_id and tag_id)
- `node` and `node_state` (node tracking, added in Migration 10)

### Data Flow Summary

```
/monitoring/users/?cluster=woody&startTime=last30d
         ↓
routes.go: buildFilterPresets() extracts query params
         ↓
List.root.svelte: Builds GraphQL query with jobFilters
         ↓
GraphQL Resolver: JobsStatistics() [schema.resolvers.go:647]
         ↓
JobRepository.JobsStatsGrouped() [stats.go:233]
  - Calls buildStatsQuery() with groupBy="job.hpc_user"
  - Applies BuildWhereClause() for each JobFilter
  - Uses index: jobs_cluster_starttime_user_stats (covering index)
         ↓
SQL Query executes GROUP BY hpc_user with aggregations:
  - COUNT(job.id) as totalJobs
  - CAST(ROUND(SUM(...) / 3600) as int) as totalWalltime
  - SUM(num_nodes), SUM(num_hwthreads), SUM(num_acc) aggregations
         ↓
Post-query user name lookup (avoids JOIN overhead)
         ↓
Returns [JobsStatistics] array grouped by user
```

The system is heavily optimized for this exact query pattern, evidenced by the new covering index in Migration 12 that was specifically created for "grouped stats queries" with the exact columns needed for statistics aggregation.

agentId: a76ed373ae73bdc18 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 71496
tool_uses: 38
duration_ms: 83172</usage>

Prompt:
In /Users/jan/prg/ClusterCockpit/cc-backend, find all code that calls JobsStatsGrouped or leads to the `/monitoring/users/` endpoint. I need to understand:
1. What GraphQL query is made when visiting `/monitoring/users/?cluster=woody&startTime=last30d`
2. What filters are passed to buildStatsQuery
3. What the BuildWhereClause function does (in internal/repository/)
4. What indexes exist on the job table (check migration files in internal/repository/migrations/sqlite3/)
5. The schema of the job table

Be thorough - check the frontend Svelte files (web/frontend/src/List.root.svelte), GraphQL schema (api/schema.graphqls), resolvers (internal/graph/schema.resolvers.go), and the filter/where clause builder.


Let me check the resolver to see exactly what queries are executed for this endpoint, and look at the `buildTimeCondition` to understand the filter.